|
| 1 | +from collections import defaultdict |
| 2 | +from collections.abc import Sequence |
| 3 | +from datetime import datetime |
| 4 | +from functools import partial |
| 5 | +from typing import Any, TypedDict |
| 6 | + |
| 7 | +from drf_spectacular.utils import extend_schema |
| 8 | +from rest_framework.exceptions import NotFound |
| 9 | +from rest_framework.request import Request |
| 10 | +from rest_framework.response import Response |
| 11 | + |
| 12 | +from sentry import features |
| 13 | +from sentry.api.api_owners import ApiOwner |
| 14 | +from sentry.api.api_publish_status import ApiPublishStatus |
| 15 | +from sentry.api.base import cell_silo_endpoint |
| 16 | +from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission |
| 17 | +from sentry.api.paginator import OffsetPaginator |
| 18 | +from sentry.api.serializers.rest_framework.base import convert_dict_key_case, snake_to_camel_case |
| 19 | +from sentry.apidocs.parameters import CursorQueryParam, GlobalParams |
| 20 | +from sentry.constants import ALL_ACCESS_PROJECT_ID |
| 21 | +from sentry.models.organization import Organization |
| 22 | +from sentry.seer.models.workflow import ( |
| 23 | + SEER_CONFIGURABLE_WORKFLOW_STRATEGIES, |
| 24 | + SeerWorkflowConfig, |
| 25 | + SeerWorkflowConfigProject, |
| 26 | +) |
| 27 | +from sentry.seer.workflows.workflow import SeerWorkflow |
| 28 | + |
| 29 | + |
| 30 | +class SeerWorkflowConfigResponse(TypedDict): |
| 31 | + id: str |
| 32 | + strategy: str |
| 33 | + enabled: bool |
| 34 | + schedule: str |
| 35 | + version: int |
| 36 | + projects: list[int] |
| 37 | + environments: list[str] |
| 38 | + filters: dict[str, Any] |
| 39 | + outputPolicy: dict[str, Any] |
| 40 | + seededDefault: str | None |
| 41 | + dateAdded: datetime |
| 42 | + dateUpdated: datetime |
| 43 | + |
| 44 | + |
| 45 | +class SeerWorkflowConfigsResponse(TypedDict): |
| 46 | + configs: list[SeerWorkflowConfigResponse] |
| 47 | + |
| 48 | + |
| 49 | +@cell_silo_endpoint |
| 50 | +@extend_schema(tags=["Seer"]) |
| 51 | +class OrganizationSeerWorkflowConfigsEndpoint(OrganizationEndpoint): |
| 52 | + owner = ApiOwner.ML_AI |
| 53 | + publish_status = {"GET": ApiPublishStatus.PRIVATE} |
| 54 | + permission_classes = (OrganizationPermission,) |
| 55 | + |
| 56 | + @extend_schema( |
| 57 | + operation_id="List Seer automation workflows", |
| 58 | + parameters=[GlobalParams.ORG_ID_OR_SLUG, CursorQueryParam], |
| 59 | + responses={200: SeerWorkflowConfigsResponse}, |
| 60 | + ) |
| 61 | + def get(self, request: Request, organization: Organization) -> Response: |
| 62 | + """List saved Seer automation workflows. |
| 63 | +
|
| 64 | + The ``configs`` list contains the current page; pagination links are in headers. |
| 65 | + ``projects`` is ``[-1]`` for all projects, ``[]`` for no projects, or a list |
| 66 | + of selected active project IDs accessible to the caller. An empty |
| 67 | + ``environments`` list means all environments; otherwise it contains names. |
| 68 | + ``filters`` contains strategy-specific filters. ``seededDefault`` records |
| 69 | + the seeded strategy even after edits. |
| 70 | + ``outputPolicy`` is the saved, opaque policy document. |
| 71 | + """ |
| 72 | + if not features.has("organizations:seer-automations", organization, actor=request.user): |
| 73 | + raise NotFound |
| 74 | + |
| 75 | + projects = self.get_projects(request, organization, include_all_accessible=True) |
| 76 | + configs = SeerWorkflowConfig.objects.filter( |
| 77 | + organization=organization, strategy__in=SEER_CONFIGURABLE_WORKFLOW_STRATEGIES |
| 78 | + ) |
| 79 | + return self.paginate( |
| 80 | + request=request, |
| 81 | + queryset=configs, |
| 82 | + order_by="id", |
| 83 | + paginator_cls=OffsetPaginator, |
| 84 | + on_results=partial( |
| 85 | + serialize_workflow_configs, accessible_project_ids=[p.id for p in projects] |
| 86 | + ), |
| 87 | + ) |
| 88 | + |
| 89 | + |
| 90 | +def serialize_workflow_configs( |
| 91 | + configs: Sequence[SeerWorkflowConfig], *, accessible_project_ids: Sequence[int] |
| 92 | +) -> SeerWorkflowConfigsResponse: |
| 93 | + selected_projects: dict[int, list[int]] = defaultdict(list) |
| 94 | + links = SeerWorkflowConfigProject.objects.filter( |
| 95 | + config__in=configs, project_id__in=accessible_project_ids |
| 96 | + ).order_by("project_id") |
| 97 | + for config_id, project_id in links.values_list("config_id", "project_id"): |
| 98 | + selected_projects[config_id].append(project_id) |
| 99 | + |
| 100 | + results: list[SeerWorkflowConfigResponse] = [] |
| 101 | + for config in configs: |
| 102 | + workflow = SeerWorkflow(config) |
| 103 | + filters = workflow.filters |
| 104 | + results.append( |
| 105 | + { |
| 106 | + "id": str(config.id), |
| 107 | + "strategy": config.strategy, |
| 108 | + "enabled": config.enabled, |
| 109 | + "schedule": config.schedule, |
| 110 | + "version": config.version, |
| 111 | + "projects": ( |
| 112 | + [ALL_ACCESS_PROJECT_ID] |
| 113 | + if filters.all_projects |
| 114 | + else selected_projects[config.id] |
| 115 | + ), |
| 116 | + "environments": filters.environments, |
| 117 | + "filters": convert_dict_key_case( |
| 118 | + { |
| 119 | + key: value |
| 120 | + for key, value in config.filters.items() |
| 121 | + if key not in {"all_projects", "environments"} |
| 122 | + }, |
| 123 | + snake_to_camel_case, |
| 124 | + ), |
| 125 | + "outputPolicy": config.output_policy, |
| 126 | + "seededDefault": workflow.extras.seeded_default, |
| 127 | + "dateAdded": config.date_added, |
| 128 | + "dateUpdated": config.date_updated, |
| 129 | + } |
| 130 | + ) |
| 131 | + return {"configs": results} |
0 commit comments