Skip to content

Commit d325ca9

Browse files
authored
chore(actor) Rename RpcActor -> Actor (#70545)
With the Actor Django model removed we can use the term `Actor` for what is now the only actor in the application. Once this lands I'll update all the usage in both sentry & getsentry.
1 parent e9071c2 commit d325ca9

File tree

2 files changed

+301
-294
lines changed

2 files changed

+301
-294
lines changed
Lines changed: 12 additions & 294 deletions
Original file line numberDiff line numberDiff line change
@@ -1,294 +1,12 @@
1-
# Please do not use
2-
# from __future__ import annotations
3-
# in modules such as this one where hybrid cloud data models or service classes are
4-
# defined, because we want to reflect on type annotations and avoid forward references.
5-
6-
from collections import defaultdict
7-
from collections.abc import Iterable, MutableMapping, Sequence
8-
from enum import Enum
9-
from typing import TYPE_CHECKING, Any, Union, overload
10-
11-
from django.core.exceptions import ObjectDoesNotExist
12-
from rest_framework import serializers
13-
14-
from sentry.services.hybrid_cloud import RpcModel
15-
from sentry.services.hybrid_cloud.user import RpcUser
16-
17-
if TYPE_CHECKING:
18-
from sentry.models.team import Team
19-
from sentry.models.user import User
20-
from sentry.services.hybrid_cloud.organization import RpcTeam
21-
22-
23-
class ActorType(str, Enum):
24-
USER = "User"
25-
TEAM = "Team"
26-
27-
28-
ActorTarget = Union["RpcActor", "User", "RpcUser", "Team", "RpcTeam"]
29-
30-
31-
class RpcActor(RpcModel):
32-
"""Can represent any model object with a foreign key to Actor."""
33-
34-
id: int
35-
"""The id of the user/team this actor represents"""
36-
37-
actor_type: ActorType
38-
"""Whether this actor is a User or Team"""
39-
40-
slug: str | None = None
41-
42-
class InvalidActor(ObjectDoesNotExist):
43-
"""Raised when an Actor fails to resolve or be found"""
44-
45-
pass
46-
47-
@classmethod
48-
def resolve_many(cls, actors: Sequence["RpcActor"]) -> list["Team | RpcUser"]:
49-
"""
50-
Resolve a list of actors in a batch to the Team/User the Actor references.
51-
52-
Will generate more efficient queries to load actors than calling
53-
RpcActor.resolve() individually will.
54-
"""
55-
from sentry.models.team import Team
56-
from sentry.services.hybrid_cloud.user.service import user_service
57-
58-
if not actors:
59-
return []
60-
actors_by_type: dict[ActorType, list[RpcActor]] = defaultdict(list)
61-
for actor in actors:
62-
actors_by_type[actor.actor_type].append(actor)
63-
results: dict[tuple[ActorType, int], Team | RpcUser] = {}
64-
for actor_type, actor_list in actors_by_type.items():
65-
if actor_type == ActorType.USER:
66-
for user in user_service.get_many(filter={"user_ids": [u.id for u in actor_list]}):
67-
results[(actor_type, user.id)] = user
68-
if actor_type == ActorType.TEAM:
69-
for team in Team.objects.filter(id__in=[t.id for t in actor_list]):
70-
results[(actor_type, team.id)] = team
71-
72-
return list(filter(None, [results.get((actor.actor_type, actor.id)) for actor in actors]))
73-
74-
@classmethod
75-
def many_from_object(cls, objects: Iterable[ActorTarget]) -> list["RpcActor"]:
76-
"""
77-
Create a list of RpcActor instances based on a collection of 'objects'
78-
79-
Objects will be grouped by the kind of actor they would be related to.
80-
Queries for actors are batched to increase efficiency. Users that are
81-
missing actors will have actors generated.
82-
"""
83-
from sentry.models.team import Team
84-
from sentry.models.user import User
85-
from sentry.services.hybrid_cloud.organization import RpcTeam
86-
87-
result: list["RpcActor"] = []
88-
grouped_by_type: MutableMapping[str, list[int]] = defaultdict(list)
89-
team_slugs: MutableMapping[int, str] = {}
90-
for obj in objects:
91-
if isinstance(obj, cls):
92-
result.append(obj)
93-
if isinstance(obj, (User, RpcUser)):
94-
grouped_by_type[ActorType.USER].append(obj.id)
95-
if isinstance(obj, (Team, RpcTeam)):
96-
team_slugs[obj.id] = obj.slug
97-
grouped_by_type[ActorType.TEAM].append(obj.id)
98-
99-
if grouped_by_type[ActorType.TEAM]:
100-
team_ids = grouped_by_type[ActorType.TEAM]
101-
for team_id in team_ids:
102-
result.append(
103-
RpcActor(
104-
id=team_id,
105-
actor_type=ActorType.TEAM,
106-
slug=team_slugs.get(team_id),
107-
)
108-
)
109-
110-
if grouped_by_type[ActorType.USER]:
111-
user_ids = grouped_by_type[ActorType.USER]
112-
for user_id in user_ids:
113-
result.append(RpcActor(id=user_id, actor_type=ActorType.USER))
114-
return result
115-
116-
@classmethod
117-
def from_object(cls, obj: ActorTarget) -> "RpcActor":
118-
"""
119-
fetch_actor: whether to make an extra query or call to fetch the actor id
120-
Without the actor_id the RpcActor acts as a tuple of id and type.
121-
"""
122-
from sentry.models.team import Team
123-
from sentry.models.user import User
124-
from sentry.services.hybrid_cloud.organization import RpcTeam
125-
126-
if isinstance(obj, cls):
127-
return obj
128-
if isinstance(obj, User):
129-
return cls.from_orm_user(obj)
130-
if isinstance(obj, Team):
131-
return cls.from_orm_team(obj)
132-
if isinstance(obj, RpcUser):
133-
return cls.from_rpc_user(obj)
134-
if isinstance(obj, RpcTeam):
135-
return cls.from_rpc_team(obj)
136-
raise TypeError(f"Cannot build RpcActor from {type(obj)}")
137-
138-
@classmethod
139-
def from_orm_user(cls, user: "User") -> "RpcActor":
140-
return cls(
141-
id=user.id,
142-
actor_type=ActorType.USER,
143-
)
144-
145-
@classmethod
146-
def from_rpc_user(cls, user: RpcUser) -> "RpcActor":
147-
return cls(
148-
id=user.id,
149-
actor_type=ActorType.USER,
150-
)
151-
152-
@classmethod
153-
def from_orm_team(cls, team: "Team") -> "RpcActor":
154-
return cls(id=team.id, actor_type=ActorType.TEAM, slug=team.slug)
155-
156-
@classmethod
157-
def from_rpc_team(cls, team: "RpcTeam") -> "RpcActor":
158-
return cls(id=team.id, actor_type=ActorType.TEAM, slug=team.slug)
159-
160-
@overload
161-
@classmethod
162-
def from_identifier(cls, id: None) -> None:
163-
...
164-
165-
@overload
166-
@classmethod
167-
def from_identifier(cls, id: int | str) -> "RpcActor":
168-
...
169-
170-
@classmethod
171-
def from_identifier(cls, id: str | int | None) -> "RpcActor | None":
172-
"""
173-
Parse an actor identifier into an RpcActor
174-
175-
Forms `id` can take:
176-
1231 -> look up User by id
177-
"1231" -> look up User by id
178-
"user:1231" -> look up User by id
179-
"team:1231" -> look up Team by id
180-
"maiseythedog" -> look up User by username
181-
"maisey@dogsrule.com" -> look up User by primary email
182-
"""
183-
from sentry.services.hybrid_cloud.user.service import user_service
184-
185-
if not id:
186-
return None
187-
# If we have an integer, fall back to assuming it's a User
188-
if isinstance(id, int):
189-
return cls(id=id, actor_type=ActorType.USER)
190-
191-
# If the actor_identifier is a simple integer as a string,
192-
# we're also a User
193-
if id.isdigit():
194-
return cls(id=int(id), actor_type=ActorType.USER)
195-
196-
if id.startswith("user:"):
197-
return cls(id=int(id[5:]), actor_type=ActorType.USER)
198-
199-
if id.startswith("team:"):
200-
return cls(id=int(id[5:]), actor_type=ActorType.TEAM)
201-
202-
try:
203-
user = user_service.get_by_username(username=id)[0]
204-
return cls(id=user.id, actor_type=ActorType.USER)
205-
except IndexError as e:
206-
raise cls.InvalidActor(f"Unable to resolve actor identifier: {e}")
207-
208-
@classmethod
209-
def from_id(cls, user_id: int | None = None, team_id: int | None = None) -> "RpcActor":
210-
if user_id and team_id:
211-
raise cls.InvalidActor("You can only provide one of user_id and team_id")
212-
if user_id:
213-
return cls(id=user_id, actor_type=ActorType.USER)
214-
if team_id:
215-
return cls(id=team_id, actor_type=ActorType.TEAM)
216-
raise cls.InvalidActor("You must provide one of user_id and team_id")
217-
218-
def __post_init__(self) -> None:
219-
if not self.is_team and self.slug is not None:
220-
raise ValueError("Slugs are expected for teams only")
221-
222-
def __hash__(self) -> int:
223-
return hash((self.id, self.actor_type))
224-
225-
def __eq__(self, other: Any) -> bool:
226-
return (
227-
isinstance(other, self.__class__)
228-
and self.id == other.id
229-
and self.actor_type == other.actor_type
230-
)
231-
232-
def resolve(self) -> "Team | RpcUser":
233-
"""
234-
Resolve an Actor into the Team or RpcUser it represents.
235-
236-
Will raise Team.DoesNotExist or User.DoesNotExist when the actor is invalid
237-
"""
238-
from sentry.models.team import Team
239-
from sentry.services.hybrid_cloud.user.service import user_service
240-
241-
if self.is_team:
242-
team = Team.objects.filter(id=self.id).first()
243-
if team:
244-
return team
245-
raise RpcActor.InvalidActor(f"Cannot find a team with id={self.id}")
246-
if self.is_user:
247-
user = user_service.get_user(user_id=self.id)
248-
if user:
249-
return user
250-
raise RpcActor.InvalidActor(f"Cannot find a User with id={self.id}")
251-
# This should be un-reachable
252-
raise RpcActor.InvalidActor("Cannot resolve an actor with an unknown type")
253-
254-
@property
255-
def identifier(self) -> str:
256-
return f"{self.actor_type.lower()}:{self.id}"
257-
258-
@property
259-
def is_team(self) -> bool:
260-
return self.actor_type == ActorType.TEAM
261-
262-
@property
263-
def is_user(self) -> bool:
264-
return self.actor_type == ActorType.USER
265-
266-
267-
def parse_and_validate_actor(actor_identifier: str | None, organization_id: int) -> RpcActor | None:
268-
from sentry.models.organizationmember import OrganizationMember
269-
from sentry.models.team import Team
270-
271-
if not actor_identifier:
272-
return None
273-
274-
try:
275-
actor = RpcActor.from_identifier(actor_identifier)
276-
except Exception:
277-
raise serializers.ValidationError(
278-
"Could not parse actor. Format should be `type:id` where type is `team` or `user`."
279-
)
280-
try:
281-
obj = actor.resolve()
282-
except RpcActor.InvalidActor:
283-
raise serializers.ValidationError(f"{actor.actor_type} does not exist")
284-
285-
if isinstance(obj, Team):
286-
if obj.organization_id != organization_id:
287-
raise serializers.ValidationError("Team is not a member of this organization")
288-
elif isinstance(obj, RpcUser):
289-
if not OrganizationMember.objects.filter(
290-
organization_id=organization_id, user_id=obj.id
291-
).exists():
292-
raise serializers.ValidationError("User is not a member of this organization")
293-
294-
return actor
1+
# Deprecated module for actor imports
2+
# Use sentry.types.actor instead.
3+
from sentry.types.actor import Actor, ActorTarget, ActorType, parse_and_validate_actor
4+
5+
RpcActor = Actor
6+
7+
__all__ = (
8+
"RpcActor",
9+
"ActorType",
10+
"ActorTarget",
11+
"parse_and_validate_actor",
12+
)

0 commit comments

Comments
 (0)