Browse Source

ref(apidocs): moved organization projects to drf spectacular (#33992)

* ref(apidocs): Moved organization projects to drf spectacular

Was running through the new documentation tool and used Organization Projects as the guinea pig endpoint.
Aniket Das "Tekky 2 years ago
parent
commit
fa23108344

+ 0 - 3
api-docs/openapi.json

@@ -108,9 +108,6 @@
     "/api/0/organizations/{organization_slug}/": {
       "$ref": "paths/organizations/details.json"
     },
-    "/api/0/organizations/{organization_slug}/projects/": {
-      "$ref": "paths/organizations/projects.json"
-    },
     "/api/0/organizations/{organization_slug}/repos/": {
       "$ref": "paths/organizations/repos.json"
     },

+ 0 - 77
api-docs/paths/organizations/projects.json

@@ -1,77 +0,0 @@
-{
-  "get": {
-    "tags": ["Organizations"],
-    "description": "Return a list of projects bound to a organization.",
-    "operationId": "List an Organization's Projects",
-    "parameters": [
-      {
-        "name": "organization_slug",
-        "in": "path",
-        "description": "The slug of the organization for which the projects should be listed.",
-        "required": true,
-        "schema": {
-          "type": "string"
-        }
-      },
-      {
-        "$ref": "../../components/parameters/pagination-cursor.json#/PaginationCursor"
-      }
-    ],
-    "responses": {
-      "200": {
-        "description": "Success",
-        "content": {
-          "application/json": {
-            "schema": {
-              "type": "array",
-              "items": {
-                "$ref": "../../components/schemas/project.json#/OrganizationProjects"
-              }
-            },
-            "example": [
-              {
-                "dateCreated": "2018-11-06T21:19:58.536Z",
-                "firstEvent": null,
-                "hasAccess": true,
-                "id": "3",
-                "isBookmarked": false,
-                "isMember": true,
-                "latestDeploys": null,
-                "name": "Prime Mover",
-                "platform": null,
-                "platforms": [],
-                "slug": "prime-mover",
-                "team": {
-                  "id": "2",
-                  "name": "Powerful Abolitionist",
-                  "slug": "powerful-abolitionist"
-                },
-                "teams": [
-                  {
-                    "id": "2",
-                    "name": "Powerful Abolitionist",
-                    "slug": "powerful-abolitionist"
-                  }
-                ]
-              }
-            ]
-          }
-        }
-      },
-      "403": {
-        "description": "Forbidden"
-      },
-      "401": {
-        "description": "Unauthorized"
-      },
-      "404": {
-        "description": "Not Found"
-      }
-    },
-    "security": [
-      {
-        "auth_token": ["org: read"]
-      }
-    ]
-  }
-}

+ 64 - 8
src/sentry/api/endpoints/organization_projects.py

@@ -1,4 +1,7 @@
+from typing import List
+
 from django.db.models import Q
+from drf_spectacular.utils import OpenApiExample, extend_schema
 from rest_framework.request import Request
 from rest_framework.response import Response
 
@@ -6,24 +9,77 @@ from sentry.api.base import EnvironmentMixin
 from sentry.api.bases.organization import OrganizationEndpoint
 from sentry.api.paginator import OffsetPaginator
 from sentry.api.serializers import serialize
-from sentry.api.serializers.models.project import ProjectSummarySerializer
+from sentry.api.serializers.models.project import (
+    OrganizationProjectResponseDict,
+    ProjectSummarySerializer,
+)
+from sentry.apidocs.constants import RESPONSE_FORBIDDEN, RESPONSE_NOTFOUND, RESPONSE_UNAUTHORIZED
+from sentry.apidocs.parameters import CURSOR_QUERY_PARAM, GLOBAL_PARAMS
+from sentry.apidocs.utils import inline_sentry_response_serializer
 from sentry.models import Project, ProjectStatus, Team
 from sentry.search.utils import tokenize_query
 
 ERR_INVALID_STATS_PERIOD = "Invalid stats_period. Valid choices are '', '24h', '14d', and '30d'"
 
 
+@extend_schema(tags=["Organizations"])
 class OrganizationProjectsEndpoint(OrganizationEndpoint, EnvironmentMixin):
+    public = {"GET"}
+
+    @extend_schema(
+        operation_id="List an Organization's Projects",
+        parameters=[GLOBAL_PARAMS.ORG_SLUG, CURSOR_QUERY_PARAM],
+        request=None,
+        responses={
+            200: inline_sentry_response_serializer(
+                "OrganizationProjectResponseDict", List[OrganizationProjectResponseDict]
+            ),
+            401: RESPONSE_UNAUTHORIZED,
+            403: RESPONSE_FORBIDDEN,
+            404: RESPONSE_NOTFOUND,
+        },
+        examples=[
+            OpenApiExample(
+                "Success",
+                value=[
+                    {
+                        "dateCreated": "2018-11-06T21:19:58.536Z",
+                        "firstEvent": None,
+                        "hasAccess": True,
+                        "id": "3",
+                        "isBookmarked": False,
+                        "isMember": True,
+                        "name": "Prime Mover",
+                        "platform": "",
+                        "platforms": [],
+                        "slug": "prime-mover",
+                        "team": {
+                            "id": "2",
+                            "name": "Powerful Abolitionist",
+                            "slug": "powerful-abolitionist",
+                        },
+                        "teams": [
+                            {
+                                "id": "2",
+                                "name": "Powerful Abolitionist",
+                                "slug": "powerful-abolitionist",
+                            }
+                        ],
+                        "environments": ["local"],
+                        "eventProcessing": {"symbolicationDegraded": False},
+                        "features": ["releases"],
+                        "firstTransactionEvent": True,
+                        "hasSessions": True,
+                        "latestRelease": None,
+                        "hasUserReports": False,
+                    }
+                ],
+            )
+        ],
+    )
     def get(self, request: Request, organization) -> Response:
         """
-        List an Organization's Projects
-        ```````````````````````````````
-
         Return a list of projects bound to a organization.
-
-        :pparam string organization_slug: the slug of the organization for
-                                          which the projects should be listed.
-        :auth: required
         """
         stats_period = request.GET.get("statsPeriod")
         collapse = request.GET.getlist("collapse", [])

+ 53 - 5
src/sentry/api/serializers/models/project.py

@@ -2,7 +2,7 @@ from __future__ import annotations
 
 from collections import defaultdict
 from datetime import datetime, timedelta
-from typing import Any, Iterable, List, MutableMapping, Sequence
+from typing import Any, Dict, Iterable, List, MutableMapping, Optional, Sequence, cast
 
 import sentry_sdk
 from django.db import connection
@@ -437,6 +437,17 @@ class ProjectWithOrganizationSerializer(ProjectSerializer):
         return data
 
 
+class TeamResponseDict(TypedDict):
+    id: str
+    name: str
+    slug: str
+
+
+class ProjectWithTeamResponseDict(ProjectSerializerResponse):
+    team: TeamResponseDict
+    teams: List[TeamResponseDict]
+
+
 class ProjectWithTeamSerializer(ProjectSerializer):
     def get_attrs(
         self, item_list: Sequence[Project], user: User, **kwargs: Any
@@ -464,8 +475,8 @@ class ProjectWithTeamSerializer(ProjectSerializer):
             attrs[item]["teams"] = teams_by_project_id[item.id]
         return attrs
 
-    def serialize(self, obj, attrs, user):
-        data = super().serialize(obj, attrs, user)
+    def serialize(self, obj, attrs, user) -> ProjectWithTeamResponseDict:
+        data = cast(ProjectWithTeamResponseDict, super().serialize(obj, attrs, user))
         # TODO(jess): remove this when this is deprecated
         try:
             data["team"] = attrs["teams"][0]
@@ -475,6 +486,43 @@ class ProjectWithTeamSerializer(ProjectSerializer):
         return data
 
 
+class EventProcessingDict(TypedDict):
+    symbolicationDegraded: bool
+
+
+class LatestReleaseDict(TypedDict):
+    version: str
+
+
+class _OrganizationProjectResponseDictOptional(TypedDict, total=False):
+    latestDeploys: Optional[Dict[str, Dict[str, str]]]
+    stats: Any
+    transactionStats: Any
+    sessionStats: Any
+
+
+class OrganizationProjectResponseDict(_OrganizationProjectResponseDictOptional):
+    team: Optional[TeamResponseDict]
+    teams: List[TeamResponseDict]
+    id: str
+    name: str
+    slug: str
+    isBookmarked: bool
+    isMember: bool
+    hasAccess: bool
+    dateCreated: str
+    eventProcessing: EventProcessingDict
+    features: List[str]
+    firstTransactionEvent: bool
+    hasSessions: bool
+    platform: Optional[str]
+    platforms: List[str]
+    hasUserReports: bool
+    firstEvent: Optional[str]
+    environments: List[str]
+    latestRelease: Optional[LatestReleaseDict]
+
+
 class ProjectSummarySerializer(ProjectWithTeamSerializer):
     def get_deploys_by_project(self, item_list):
         cursor = connection.cursor()
@@ -572,8 +620,8 @@ class ProjectSummarySerializer(ProjectWithTeamSerializer):
 
         return attrs
 
-    def serialize(self, obj, attrs, user):
-        context = {
+    def serialize(self, obj, attrs, user) -> OrganizationProjectResponseDict:  # type: ignore
+        context: OrganizationProjectResponseDict = {
             "team": attrs["teams"][0] if attrs["teams"] else None,
             "teams": attrs["teams"],
             "id": str(obj.id),

+ 8 - 0
src/sentry/apidocs/parameters.py

@@ -1,4 +1,5 @@
 from drf_spectacular.utils import OpenApiParameter
+from rest_framework import serializers
 
 
 class GLOBAL_PARAMS:
@@ -43,3 +44,10 @@ class ISSUE_ALERT_PARAMS:
         type=int,
         description="The id of the rule you'd like to query",
     )
+
+
+class CURSOR_QUERY_PARAM(serializers.Serializer):  # type: ignore
+    cursor = serializers.CharField(
+        help_text="A pointer to the last object fetched and its' sort order; used to retrieve the next or previous results.",
+        required=False,
+    )

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

@@ -123,4 +123,4 @@ def resolve_type_hint(hint) -> Any:
     elif isinstance(hint, typing._TypedDictMeta):
         raise UnableToProceedError("Wrong TypedDict class, please use typing_extensions.TypedDict")
     else:
-        raise UnableToProceedError()
+        raise UnableToProceedError(hint)