Skip to content

ref(hc): Introduce RpcUserProfile and UserService.get_many_profiles #68847

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/sentry/services/hybrid_cloud/app/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ class _AppServiceFilterQuery(
SentryAppInstallation, SentryAppInstallationFilterArgs, RpcSentryAppInstallation, None
]
):
def base_query(self, ids_only: bool = False) -> QuerySet[SentryAppInstallation]:
if ids_only:
def base_query(self, select_related: bool = True) -> QuerySet[SentryAppInstallation]:
if not select_related:
return SentryAppInstallation.objects
return SentryAppInstallation.objects.select_related("sentry_app")

Expand Down
12 changes: 6 additions & 6 deletions src/sentry/services/hybrid_cloud/filter_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class FilterQueryDatabaseImpl(
# Required Overrides

@abc.abstractmethod
def base_query(self, ids_only: bool = False) -> QuerySet[BASE_MODEL]:
def base_query(self, select_related: bool = True) -> QuerySet[BASE_MODEL]:
# This should return a QuerySet for the model in question along with any other required data
# that is not a filter
pass
Expand Down Expand Up @@ -91,14 +91,14 @@ def validator(d: FILTER_ARGS) -> str | None:

# Helpers

def _query_many(self, filter: FILTER_ARGS, ids_only: bool = False) -> QuerySet:
def query_many(self, filter: FILTER_ARGS, select_related: bool = True) -> QuerySet:
validation_error = self.filter_arg_validator()(filter)
if validation_error is not None:
raise TypeError(
f"Failed to validate filter arguments passed to {self.__class__.__name__}: {validation_error}"
)

query = self.base_query(ids_only=ids_only)
query = self.base_query(select_related=select_related)
return self.apply_filters(query, filter)

# Public Interface
Expand All @@ -121,15 +121,15 @@ def serialize_many(
if as_user is None and auth_context:
as_user = auth_context.user

result = self._query_many(filter=filter)
result = self.query_many(filter=filter)
return serialize(
list(result),
user=as_user,
serializer=self.serialize_api(serializer),
)

def get_many(self, filter: FILTER_ARGS) -> list[RPC_RESPONSE]:
return [self.serialize_rpc(o) for o in self._query_many(filter=filter)]
return [self.serialize_rpc(o) for o in self.query_many(filter=filter)]

def get_many_ids(self, filter: FILTER_ARGS) -> list[int]:
return [o.id for o in self._query_many(filter=filter, ids_only=True)]
return [o.id for o in self.query_many(filter=filter, select_related=False)]
2 changes: 1 addition & 1 deletion src/sentry/services/hybrid_cloud/identity/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def apply_filters(
query = query.filter(idp__type=filters["provider_type"])
return query

def base_query(self, ids_only: bool = False) -> QuerySet[Identity]:
def base_query(self, select_related: bool = True) -> QuerySet[Identity]:
return Identity.objects

def filter_arg_validator(self) -> Callable[[IdentityFilterArgs], str | None]:
Expand Down
20 changes: 16 additions & 4 deletions src/sentry/services/hybrid_cloud/user/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,16 @@
UserSerializeType,
UserUpdateArgs,
)
from sentry.services.hybrid_cloud.user.model import RpcVerifyUserEmail, UserIdEmailArgs
from sentry.services.hybrid_cloud.user.serial import serialize_rpc_user, serialize_user_avatar
from sentry.services.hybrid_cloud.user.model import (
RpcUserProfile,
RpcVerifyUserEmail,
UserIdEmailArgs,
)
from sentry.services.hybrid_cloud.user.serial import (
serialize_rpc_user,
serialize_rpc_user_profile,
serialize_user_avatar,
)
from sentry.services.hybrid_cloud.user.service import UserService
from sentry.signals import user_signup

Expand All @@ -62,6 +70,10 @@ def get_many(self, *, filter: UserFilterArgs) -> list[RpcUser]:
def get_many_ids(self, *, filter: UserFilterArgs) -> list[int]:
return self._FQ.get_many_ids(filter)

def get_many_profiles(self, *, filter: UserFilterArgs) -> list[RpcUserProfile]:
users = self._FQ.query_many(filter, select_related=False)
return [serialize_rpc_user_profile(user) for user in users]

def get_many_by_email(
self,
emails: list[str],
Expand Down Expand Up @@ -313,8 +325,8 @@ def apply_filters(self, query: QuerySet[User], filters: UserFilterArgs) -> Query

return query

def base_query(self, ids_only: bool = False) -> QuerySet[User]:
if ids_only:
def base_query(self, select_related: bool = True) -> QuerySet[User]:
if not select_related:
return User.objects

return User.objects.extra(
Expand Down
8 changes: 6 additions & 2 deletions src/sentry/services/hybrid_cloud/user/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@ class RpcAuthenticator(RpcModel):
config: Any = None


class RpcUser(RpcModel):
class RpcUserProfile(RpcModel):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know introducing this new noun ("profile") could be a little confusing. I hope it fits.

I considered naming it something like RpcMinimalUser, but that would mess up the semantics of the "is-a" relationship. That is, RpcUser extending RpcMinimalUser implies "an RpcUser is an RpcMinimalUser", which is wrong because RpcUser is not minimal.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Profile seems like a decent name.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think profile is better than minimal. We have also used Summary for similar result types like OrganizationSummary.

"""Minimal set of user attributes that can be fetched efficiently."""

id: int = -1
pk: int = -1
name: str = ""
email: str = ""
emails: frozenset[str] = frozenset()
username: str = ""
actor_id: int | None = None
display_name: str = ""
Expand All @@ -55,9 +56,12 @@ class RpcUser(RpcModel):
is_password_expired: bool = False
session_nonce: str | None = None


class RpcUser(RpcUserProfile):
roles: frozenset[str] = frozenset()
permissions: frozenset[str] = frozenset()
avatar: RpcAvatar | None = None
emails: frozenset[str] = frozenset()
useremails: list[RpcUserEmail] = Field(default_factory=list)
authenticators: list[RpcAuthenticator] = Field(default_factory=list)

Expand Down
34 changes: 25 additions & 9 deletions src/sentry/services/hybrid_cloud/user/serial.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@
from sentry.db.models import BaseQuerySet
from sentry.models.avatars.user_avatar import UserAvatar
from sentry.models.user import User
from sentry.services.hybrid_cloud.user import RpcAuthenticator, RpcAvatar, RpcUser, RpcUserEmail
from sentry.services.hybrid_cloud.user import (
RpcAuthenticator,
RpcAvatar,
RpcUser,
RpcUserEmail,
RpcUserProfile,
)


def serialize_generic_user(user: Any) -> RpcUser | None:
Expand All @@ -27,10 +33,10 @@ def serialize_generic_user(user: Any) -> RpcUser | None:
raise TypeError(f"Can't serialize {type(user)} to RpcUser")


def serialize_rpc_user(user: User) -> RpcUser:
def _serialize_from_user_fields(user: User) -> dict[str, Any]:
args = {
field_name: getattr(user, field_name)
for field_name in RpcUser.__fields__
for field_name in RpcUserProfile.__fields__
if hasattr(user, field_name)
}
args["pk"] = user.pk
Expand All @@ -39,22 +45,32 @@ def serialize_rpc_user(user: User) -> RpcUser:
args["is_superuser"] = user.is_superuser
args["is_sentry_app"] = bool(user.is_sentry_app)
args["password_usable"] = user.has_usable_password()
args["session_nonce"] = user.session_nonce

if args["name"] is None:
# This field is non-nullable according to the Django schema, but may be null
# on some servers due to migration history
args["name"] = ""

return args


def serialize_rpc_user_profile(user: User) -> RpcUserProfile:
return RpcUserProfile(**_serialize_from_user_fields(user))


def serialize_rpc_user(user: User) -> RpcUser:
args = _serialize_from_user_fields(user)

# Prefer eagerloaded attributes from _base_query
if hasattr(user, "useremails") and user.useremails is not None:
args["emails"] = frozenset([e["email"] for e in user.useremails if e["is_verified"]])
else:
args["emails"] = frozenset([email.email for email in user.get_verified_emails()])
args["session_nonce"] = user.session_nonce

# And process the _base_query special data additions
args["permissions"] = frozenset(getattr(user, "permissions", None) or ())

if args["name"] is None:
# This field is non-nullable according to the Django schema, but may be null
# on some servers due to migration history
args["name"] = ""

roles: frozenset[str] = frozenset()
if hasattr(user, "roles") and user.roles is not None:
roles = frozenset(_flatten(user.roles))
Expand Down
12 changes: 11 additions & 1 deletion src/sentry/services/hybrid_cloud/user/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
UserSerializeType,
UserUpdateArgs,
)
from sentry.services.hybrid_cloud.user.model import RpcAvatar, RpcVerifyUserEmail, UserIdEmailArgs
from sentry.services.hybrid_cloud.user.model import (
RpcAvatar,
RpcUserProfile,
RpcVerifyUserEmail,
UserIdEmailArgs,
)
from sentry.silo import SiloMode


Expand Down Expand Up @@ -53,6 +58,11 @@ def get_many(self, *, filter: UserFilterArgs) -> list[RpcUser]:
def get_many_ids(self, *, filter: UserFilterArgs) -> list[int]:
pass

@rpc_method
@abstractmethod
def get_many_profiles(self, *, filter: UserFilterArgs) -> list[RpcUserProfile]:
pass

@rpc_method
@abstractmethod
def get_many_by_email(
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/services/hybrid_cloud/user_option/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def set_option(
class _UserOptionFilterQuery(
FilterQueryDatabaseImpl[UserOption, UserOptionFilterArgs, RpcUserOption, None]
):
def base_query(self, ids_only: bool = False) -> QuerySet[UserOption]:
def base_query(self, select_related: bool = True) -> QuerySet[UserOption]:
return UserOption.objects

def filter_arg_validator(self) -> Callable[[UserOptionFilterArgs], str | None]:
Expand Down
6 changes: 3 additions & 3 deletions src/sentry/services/hybrid_cloud/usersocialauth/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def revoke_token(
"""
Calls UserSocialAuth.revoke_token() on all matching results, returning the modified RpcUserSocialAuths.
"""
db_auths = self._FQ._query_many(filter=filter)
db_auths = self._FQ.query_many(filter=filter)
for db_auth in db_auths:
db_auth.revoke_token(drop_token=drop_token)
return self.get_many(filter=filter)
Expand All @@ -46,7 +46,7 @@ def refresh_token(self, *, filter: UserSocialAuthFilterArgs) -> list[RpcUserSoci
"""
Calls UserSocialAuth.refresh_token() on all matching results, returning the modified RpcUserSocialAuths.
"""
db_auths = self._FQ._query_many(filter=filter)
db_auths = self._FQ.query_many(filter=filter)
for db_auth in db_auths:
db_auth.refresh_token()
return self.get_many(filter=filter)
Expand Down Expand Up @@ -78,7 +78,7 @@ def apply_filters(
query = query.filter(uid=filters["uid"])
return query

def base_query(self, ids_only: bool = False) -> QuerySet[UserSocialAuth]:
def base_query(self, select_related: bool = True) -> QuerySet[UserSocialAuth]:
return UserSocialAuth.objects.filter()

def filter_arg_validator(self) -> Callable[[UserSocialAuthFilterArgs], str | None]:
Expand Down
7 changes: 7 additions & 0 deletions tests/sentry/hybridcloud/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,10 @@ def test_user_serialize_multiple_emails(self):
assert len(rpc_user.useremails) == 3
expected = {self.user.email, email.email, unverified_email.email}
assert expected == {e.email for e in rpc_user.useremails}

def test_get_many_profiles(self):
users = [self.create_user() for _ in range(2)]
target_ids = [users[0].id]
profiles = user_service.get_many_profiles(filter=dict(user_ids=target_ids))
assert len(profiles) == 1
assert profiles[0].id == users[0].id
Loading