|
| 1 | +import logging |
| 2 | + |
| 3 | +from rest_framework.request import Request |
| 4 | +from rest_framework.response import Response |
| 5 | + |
| 6 | +from sentry import analytics, features |
| 7 | +from sentry.api.api_owners import ApiOwner |
| 8 | +from sentry.api.api_publish_status import ApiPublishStatus |
| 9 | +from sentry.api.base import region_silo_endpoint |
| 10 | +from sentry.api.bases.project import ProjectEndpoint |
| 11 | +from sentry.api.paginator import OffsetPaginator |
| 12 | +from sentry.preprod.analytics import PreprodArtifactApiListBuildsEvent |
| 13 | +from sentry.preprod.api.models.project_preprod_build_details_models import ( |
| 14 | + transform_preprod_artifact_to_build_details, |
| 15 | +) |
| 16 | +from sentry.preprod.api.models.project_preprod_list_builds_models import ( |
| 17 | + ListBuildsApiResponse, |
| 18 | + PaginationInfo, |
| 19 | +) |
| 20 | +from sentry.preprod.models import PreprodArtifact |
| 21 | +from sentry.utils.cursors import Cursor |
| 22 | + |
| 23 | +logger = logging.getLogger(__name__) |
| 24 | + |
| 25 | + |
| 26 | +@region_silo_endpoint |
| 27 | +class ProjectPreprodListBuildsEndpoint(ProjectEndpoint): |
| 28 | + owner = ApiOwner.EMERGE_TOOLS |
| 29 | + publish_status = { |
| 30 | + "GET": ApiPublishStatus.EXPERIMENTAL, |
| 31 | + } |
| 32 | + |
| 33 | + def get(self, request: Request, project) -> Response: |
| 34 | + """ |
| 35 | + List preprod builds for a project |
| 36 | + ```````````````````````````````````````````````````` |
| 37 | +
|
| 38 | + List preprod builds for a project with optional filtering and pagination. |
| 39 | +
|
| 40 | + :pparam string organization_id_or_slug: the id or slug of the organization the |
| 41 | + artifacts belong to. |
| 42 | + :pparam string project_id_or_slug: the id or slug of the project to retrieve the |
| 43 | + artifacts from. |
| 44 | + :qparam string app_id: filter by app identifier (e.g., "com.myapp.MyApp") |
| 45 | + :qparam string state: filter by artifact state (0=uploading, 1=uploaded, 3=processed, 4=failed) |
| 46 | + :qparam string build_version: filter by build version |
| 47 | + :qparam string build_configuration: filter by build configuration name |
| 48 | + :qparam string platform: filter by platform (ios, android, macos) |
| 49 | + :qparam int limit: number of results per page (default 25, max 100) |
| 50 | + :qparam int page: page number (default 1) |
| 51 | + :auth: required |
| 52 | + """ |
| 53 | + |
| 54 | + analytics.record( |
| 55 | + PreprodArtifactApiListBuildsEvent( |
| 56 | + organization_id=project.organization_id, |
| 57 | + project_id=project.id, |
| 58 | + user_id=request.user.id, |
| 59 | + ) |
| 60 | + ) |
| 61 | + |
| 62 | + if not features.has( |
| 63 | + "organizations:preprod-frontend-routes", project.organization, actor=request.user |
| 64 | + ): |
| 65 | + return Response({"error": "Feature not enabled"}, status=403) |
| 66 | + |
| 67 | + queryset = PreprodArtifact.objects.filter(project=project) |
| 68 | + |
| 69 | + app_id = request.GET.get("app_id") |
| 70 | + if app_id: |
| 71 | + queryset = queryset.filter(app_id__icontains=app_id) |
| 72 | + |
| 73 | + state = request.GET.get("state") |
| 74 | + if state: |
| 75 | + try: |
| 76 | + state_int = int(state) |
| 77 | + if state_int in [0, 1, 3, 4]: # Valid states |
| 78 | + queryset = queryset.filter(state=state_int) |
| 79 | + except ValueError: |
| 80 | + pass |
| 81 | + |
| 82 | + build_version = request.GET.get("build_version") |
| 83 | + if build_version: |
| 84 | + queryset = queryset.filter(build_version__icontains=build_version) |
| 85 | + |
| 86 | + build_configuration = request.GET.get("build_configuration") |
| 87 | + if build_configuration: |
| 88 | + queryset = queryset.filter(build_configuration__name__icontains=build_configuration) |
| 89 | + |
| 90 | + platform = request.GET.get("platform") |
| 91 | + if platform: |
| 92 | + # For now, macos artifacts are also XCARCHIVE type |
| 93 | + if platform.lower() == "ios" or platform.lower() == "macos": |
| 94 | + queryset = queryset.filter(artifact_type=PreprodArtifact.ArtifactType.XCARCHIVE) |
| 95 | + elif platform.lower() == "android": |
| 96 | + queryset = queryset.filter( |
| 97 | + artifact_type__in=[ |
| 98 | + PreprodArtifact.ArtifactType.AAB, |
| 99 | + PreprodArtifact.ArtifactType.APK, |
| 100 | + ] |
| 101 | + ) |
| 102 | + else: |
| 103 | + return Response( |
| 104 | + {"error": "Invalid platform: " + platform}, |
| 105 | + status=400, |
| 106 | + ) |
| 107 | + |
| 108 | + queryset = queryset.order_by("-date_added") |
| 109 | + |
| 110 | + try: |
| 111 | + per_page = min(int(request.GET.get("per_page", 25)), 100) |
| 112 | + page = max(int(request.GET.get("page", 1)), 1) |
| 113 | + except ValueError: |
| 114 | + return Response( |
| 115 | + {"error": "Invalid pagination parameters: 'per_page' and 'page' must be integers."}, |
| 116 | + status=400, |
| 117 | + ) |
| 118 | + |
| 119 | + # Create paginator |
| 120 | + paginator = OffsetPaginator( |
| 121 | + queryset, |
| 122 | + order_by="-date_added", |
| 123 | + max_limit=100, |
| 124 | + ) |
| 125 | + |
| 126 | + # Create cursor for pagination |
| 127 | + # For OffsetPaginator: cursor.offset = page number, cursor.value = limit |
| 128 | + # Since we want page 1 to start at offset 0, we need to adjust |
| 129 | + indexed_page = page - 1 |
| 130 | + cursor = Cursor(per_page, indexed_page, False) |
| 131 | + |
| 132 | + # Get paginated results |
| 133 | + result = paginator.get_result( |
| 134 | + limit=per_page, |
| 135 | + cursor=cursor, |
| 136 | + count_hits=True, |
| 137 | + ) |
| 138 | + |
| 139 | + # Transform the results using shared utility |
| 140 | + build_details_list = [] |
| 141 | + for artifact in result.results: |
| 142 | + try: |
| 143 | + build_details_list.append(transform_preprod_artifact_to_build_details(artifact)) |
| 144 | + except Exception as e: |
| 145 | + logger.warning("Failed to transform artifact %s: %s", artifact.id, str(e)) |
| 146 | + continue |
| 147 | + |
| 148 | + # Build response with pagination info |
| 149 | + response_data = ListBuildsApiResponse( |
| 150 | + builds=build_details_list, |
| 151 | + pagination=PaginationInfo( |
| 152 | + next=result.next.offset if result.next.has_results else None, |
| 153 | + prev=result.prev.offset if result.prev.has_results else None, |
| 154 | + has_next=result.next.has_results, |
| 155 | + has_prev=result.prev.has_results, |
| 156 | + page=indexed_page, |
| 157 | + per_page=per_page, |
| 158 | + total_count=result.hits if result.hits is not None else "unknown", |
| 159 | + ), |
| 160 | + ) |
| 161 | + |
| 162 | + return Response(response_data.dict()) |
0 commit comments