Browse Source

feat(apidocs): type organization serializer (#31787)

Josh Ferge 3 years ago
parent
commit
1bd1c98d52

+ 2 - 1
mypy.ini

@@ -81,7 +81,8 @@ files = src/sentry/analytics/,
         tests/sentry/tasks/test_low_priority_symbolication.py,
         tests/sentry/utils/appleconnect/,
         src/sentry/apidocs/,
-        src/sentry/api/serializers/models/user.py
+        src/sentry/api/serializers/models/user.py,
+        src/sentry/api/serializers/models/organization.py
 
 ; Enable all options used with --strict
 warn_unused_configs=True

+ 127 - 22
src/sentry/api/serializers/models/organization.py

@@ -1,11 +1,18 @@
+from __future__ import annotations
+
+from collections.abc import Mapping, MutableMapping, Sequence
+from typing import TYPE_CHECKING, Any, Optional, Union, cast
+
 from rest_framework import serializers
 from sentry_relay.auth import PublicKey
 from sentry_relay.exceptions import RelayError
+from typing_extensions import TypedDict
 
 from sentry import features, roles
 from sentry.api.serializers import Serializer, register, serialize
 from sentry.api.serializers.models import UserSerializer
 from sentry.app import quotas
+from sentry.auth.access import Access
 from sentry.constants import (
     ACCOUNT_RATE_LIMIT_DEFAULT,
     ALERTS_MEMBER_WRITE_DEFAULT,
@@ -37,11 +44,15 @@ from sentry.models import (
     Team,
     TeamStatus,
 )
+from sentry.models.user import User
 
 _ORGANIZATION_SCOPE_PREFIX = "organizations:"
 
+if TYPE_CHECKING:
+    from sentry.api.serializers import UserSerializerResponse, UserSerializerResponseSelf
+
 
-class TrustedRelaySerializer(serializers.Serializer):
+class TrustedRelaySerializer(serializers.Serializer):  # type: ignore
     internal_external = (
         ("name", "name"),
         ("description", "description"),
@@ -50,7 +61,7 @@ class TrustedRelaySerializer(serializers.Serializer):
         ("last_modified", "lastModified"),
     )
 
-    def to_representation(self, instance):
+    def to_representation(self, instance: Any) -> dict[str, Any]:
         ret_val = {}
         for internal_key, external_key in TrustedRelaySerializer.internal_external:
             val = instance.get(internal_key)
@@ -58,7 +69,7 @@ class TrustedRelaySerializer(serializers.Serializer):
                 ret_val[external_key] = val
         return ret_val
 
-    def to_internal_value(self, data):
+    def to_internal_value(self, data: Any) -> dict[str, str]:
         try:
             key_name = data.get("name")
             public_key = data.get("publicKey") or ""
@@ -89,19 +100,41 @@ class TrustedRelaySerializer(serializers.Serializer):
         return {"public_key": public_key, "name": key_name, "description": description}
 
 
+class _Status(TypedDict):
+    id: str
+    name: str
+
+
+class OrganizationSerializerResponse(TypedDict):
+    id: str
+    slug: str
+    status: _Status
+    name: str
+    dateCreated: str
+    isEarlyAdopter: bool
+    require2FA: bool
+    requireEmailVerification: bool
+    avatar: Any  # TODO replace with Avatar
+    features: Any  # TODO
+
+
 @register(Organization)
-class OrganizationSerializer(Serializer):
-    def get_attrs(self, item_list, user):
+class OrganizationSerializer(Serializer):  # type: ignore
+    def get_attrs(
+        self, item_list: Sequence[Organization], user: User
+    ) -> MutableMapping[Organization, MutableMapping[str, Any]]:
         avatars = {
             a.organization_id: a
             for a in OrganizationAvatar.objects.filter(organization__in=item_list)
         }
-        data = {}
+        data: MutableMapping[Organization, MutableMapping[str, Any]] = {}
         for item in item_list:
             data[item] = {"avatar": avatars.get(item.id)}
         return data
 
-    def serialize(self, obj, attrs, user):
+    def serialize(
+        self, obj: Organization, attrs: Mapping[str, Any], user: User
+    ) -> OrganizationSerializerResponse:
         from sentry import features
         from sentry.features.base import OrganizationFeature
 
@@ -180,19 +213,37 @@ class OrganizationSerializer(Serializer):
         }
 
 
-class OnboardingTasksSerializer(Serializer):
-    def get_attrs(self, item_list, user, **kwargs):
+class _OnboardingTasksAttrs(TypedDict):
+    user: Optional[Union[UserSerializerResponse, UserSerializerResponseSelf]]
+
+
+class OnboardingTasksSerializerResponse(TypedDict):
+
+    task: str  # TODO: literal/enum
+    status: str  # TODO: literal/enum
+    user: Optional[Union[UserSerializerResponse, UserSerializerResponseSelf]]
+    completionSeen: str
+    dateCompleted: str
+    data: Any  # JSON object
+
+
+class OnboardingTasksSerializer(Serializer):  # type: ignore
+    def get_attrs(
+        self, item_list: OrganizationOnboardingTask, user: User, **kwargs: Any
+    ) -> MutableMapping[OrganizationOnboardingTask, _OnboardingTasksAttrs]:
         # Unique user list
         users = {item.user for item in item_list if item.user}
         serialized_users = serialize(users, user, UserSerializer())
         user_map = {user["id"]: user for user in serialized_users}
 
-        data = {}
+        data: MutableMapping[OrganizationOnboardingTask, _OnboardingTasksAttrs] = {}
         for item in item_list:
             data[item] = {"user": user_map.get(str(item.user_id))}
         return data
 
-    def serialize(self, obj, attrs, user):
+    def serialize(
+        self, obj: OrganizationOnboardingTask, attrs: _OnboardingTasksAttrs, user: User
+    ) -> OnboardingTasksSerializerResponse:
         return {
             "task": OrganizationOnboardingTask.TASK_KEY_MAP.get(obj.task),
             "status": OrganizationOnboardingTask.STATUS_KEY_MAP.get(obj.status),
@@ -203,11 +254,50 @@ class OnboardingTasksSerializer(Serializer):
         }
 
 
+class _DetailedOrganizationSerializerResponseOptional(OrganizationSerializerResponse, total=False):
+    role: Any  # TODO replace with enum/literal
+
+
+class DetailedOrganizationSerializerResponse(_DetailedOrganizationSerializerResponseOptional):
+    experiments: Any
+    quota: Any
+    isDefault: bool
+    defaultRole: bool
+    availableRoles: list[Any]  # TODO replace with enum/literal
+    openMembership: bool
+    allowSharedIssues: bool
+    enahncedPrivacy: bool
+    dataScrubber: bool
+    dataScrubberDefaults: bool
+    sensitiveFields: list[Any]  # TODO
+    safeFields: list[Any]
+    storeCrashReports: Any  # TODO
+    attachmentsRole: Any  # TODO
+    debugFilesRole: str
+    eventsMemberAdmin: bool
+    alertsMemberWrite: bool
+    scrubIPAddresses: bool
+    scrapeJavaScript: bool
+    allowJoinRequests: bool
+    relayPiiConfig: str
+    apdexThreshold: int
+    trustedRelays: Any  # TODO
+    access: frozenset[str]
+    pendingAccessRequests: int
+    onboardingTasks: OnboardingTasksSerializerResponse
+
+
 class DetailedOrganizationSerializer(OrganizationSerializer):
-    def get_attrs(self, item_list, user, **kwargs):
+    def get_attrs(
+        self, item_list: Sequence[Organization], user: User, **kwargs: Any
+    ) -> MutableMapping[Organization, MutableMapping[str, Any]]:
         return super().get_attrs(item_list, user)
 
-    def serialize(self, obj, attrs, user, access):
+    def serialize(  # type: ignore
+        self, obj: Organization, attrs: Mapping[str, Any], user: User, access: Access
+    ) -> DetailedOrganizationSerializerResponse:
+        # TODO: rectify access argument overriding parent if we want to remove above type ignore
+
         from sentry import experiments
 
         onboarding_tasks = list(
@@ -216,7 +306,7 @@ class DetailedOrganizationSerializer(OrganizationSerializer):
 
         experiment_assignments = experiments.all(org=obj, actor=user)
 
-        context = super().serialize(obj, attrs, user)
+        context = cast(DetailedOrganizationSerializerResponse, super().serialize(obj, attrs, user))
         max_rate = quotas.get_maximum_quota(obj)
         context["experiments"] = experiment_assignments
         context["quota"] = {
@@ -292,7 +382,8 @@ class DetailedOrganizationSerializer(OrganizationSerializer):
                 "apdexThreshold": int(
                     obj.get_option("sentry:apdex_threshold", APDEX_THRESHOLD_DEFAULT)
                 ),
-            }
+            }  # type: ignore
+            # see https://github.com/python/mypy/issues/6462
         )
 
         trusted_relays_raw = obj.get_option("sentry:trusted-relays") or []
@@ -309,11 +400,20 @@ class DetailedOrganizationSerializer(OrganizationSerializer):
         return context
 
 
+class DetailedOrganizationSerializerWithProjectsAndTeamsResponse(
+    DetailedOrganizationSerializerResponse
+):
+    teams: Any  # TODO replace with team type
+    projects: Any  # TODO replace with project type
+
+
 class DetailedOrganizationSerializerWithProjectsAndTeams(DetailedOrganizationSerializer):
-    def get_attrs(self, item_list, user, **kwargs):
+    def get_attrs(
+        self, item_list: Sequence[Organization], user: User, **kwargs: Any
+    ) -> MutableMapping[Organization, MutableMapping[str, Any]]:
         return super().get_attrs(item_list, user)
 
-    def _project_list(self, organization, access):
+    def _project_list(self, organization: Organization, access: Access) -> list[Project]:
         member_projects = list(access.projects)
         member_project_ids = [p.id for p in member_projects]
         other_projects = list(
@@ -321,14 +421,14 @@ class DetailedOrganizationSerializerWithProjectsAndTeams(DetailedOrganizationSer
                 id__in=member_project_ids
             )
         )
-        project_list = sorted(other_projects + member_projects, key=lambda x: x.slug)
+        project_list = sorted(other_projects + member_projects, key=lambda x: x.slug)  # type: ignore
 
         for project in project_list:
             project.set_cached_field_value("organization", organization)
 
         return project_list
 
-    def _team_list(self, organization, access):
+    def _team_list(self, organization: Organization, access: Access) -> list[Team]:
         member_teams = list(access.teams)
         member_team_ids = [p.id for p in member_teams]
         other_teams = list(
@@ -336,18 +436,23 @@ class DetailedOrganizationSerializerWithProjectsAndTeams(DetailedOrganizationSer
                 id__in=member_team_ids
             )
         )
-        team_list = sorted(other_teams + member_teams, key=lambda x: x.slug)
+        team_list = sorted(other_teams + member_teams, key=lambda x: x.slug)  # type: ignore
 
         for team in team_list:
             team.set_cached_field_value("organization", organization)
 
         return team_list
 
-    def serialize(self, obj, attrs, user, access):
+    def serialize(  # type: ignore
+        self, obj: Organization, attrs: Mapping[str, Any], user: User, access: Access
+    ) -> DetailedOrganizationSerializerWithProjectsAndTeamsResponse:
         from sentry.api.serializers.models.project import ProjectSummarySerializer
         from sentry.api.serializers.models.team import TeamSerializer
 
-        context = super().serialize(obj, attrs, user, access)
+        context = cast(
+            DetailedOrganizationSerializerWithProjectsAndTeamsResponse,
+            super().serialize(obj, attrs, user, access),
+        )
 
         team_list = self._team_list(obj, access)
         project_list = self._project_list(obj, access)

+ 1 - 0
src/sentry/apidocs/spectacular_ports.py

@@ -35,6 +35,7 @@ from typing_extensions import _TypedDictMeta
 #   figure out solution for field descriptions
 #   support deprecated fields via extension
 #   map TypedDicts in schema registry
+#   add a case for datetime types
 
 
 def _get_type_hint_origin(hint):

+ 64 - 1
tests/sentry/api/serializers/test_organization.py

@@ -1,11 +1,19 @@
 from unittest import mock
 
 from django.conf import settings
+from django.utils import timezone
 
 from sentry import features
-from sentry.api.serializers import DetailedOrganizationSerializer, serialize
+from sentry.api.serializers import (
+    DetailedOrganizationSerializer,
+    DetailedOrganizationSerializerWithProjectsAndTeams,
+    OnboardingTasksSerializer,
+    serialize,
+)
 from sentry.auth import access
 from sentry.features.base import OrganizationFeature
+from sentry.models import OrganizationOnboardingTask
+from sentry.models.organizationonboardingtask import OnboardingTask, OnboardingTaskStatus
 from sentry.testutils import TestCase
 
 
@@ -85,3 +93,58 @@ class DetailedOrganizationSerializerTest(TestCase):
         assert result["role"] == "owner"
         assert result["access"] == settings.SENTRY_SCOPES
         assert result["relayPiiConfig"] is None
+
+
+class DetailedOrganizationSerializerWithProjectsAndTeamsTest(TestCase):
+    def test_detailed_org_projs_teams(self):
+        # access the test fixtures so they're initialized
+        self.team
+        self.project
+        acc = access.from_user(self.user, self.organization)
+        serializer = DetailedOrganizationSerializerWithProjectsAndTeams()
+        result = serialize(self.organization, self.user, serializer, access=acc)
+
+        assert result["id"] == str(self.organization.id)
+        assert result["role"] == "owner"
+        assert result["access"] == settings.SENTRY_SCOPES
+        assert result["relayPiiConfig"] is None
+        assert len(result["teams"]) == 1
+        assert len(result["projects"]) == 1
+
+
+class OnboardingTasksSerializerTest(TestCase):
+    def test_onboarding_tasks_serializer(self):
+        completion_seen = timezone.now()
+        serializer = OnboardingTasksSerializer()
+        task = OrganizationOnboardingTask.objects.create(
+            organization=self.organization,
+            task=OnboardingTask.FIRST_PROJECT,
+            status=OnboardingTaskStatus.PENDING,
+            user=self.user,
+            completion_seen=completion_seen,
+        )
+
+        result = serialize(task, self.user, serializer)
+        assert result["task"] == "create_project"
+        assert result["status"] == "pending"
+        assert result["completionSeen"] == completion_seen
+        assert result["data"] == {}
+
+
+class TrustedRelaySerializer(TestCase):
+    def test_trusted_relay_serializer(self):
+        completion_seen = timezone.now()
+        serializer = OnboardingTasksSerializer()
+        task = OrganizationOnboardingTask.objects.create(
+            organization=self.organization,
+            task=OnboardingTask.FIRST_PROJECT,
+            status=OnboardingTaskStatus.PENDING,
+            user=self.user,
+            completion_seen=completion_seen,
+        )
+
+        result = serialize(task, self.user, serializer)
+        assert result["task"] == "create_project"
+        assert result["status"] == "pending"
+        assert result["completionSeen"] == completion_seen
+        assert result["data"] == {}