Skip to content

Commit c793763

Browse files
ref(api): Improve DifAssembleEndpoint typing (#110870)
Improve the typing in `DifAssembleEndpoint`
1 parent 00a60c7 commit c793763

File tree

1 file changed

+55
-34
lines changed

1 file changed

+55
-34
lines changed

src/sentry/api/endpoints/debug_files.py

Lines changed: 55 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
import re
44
import uuid
55
from collections.abc import Iterable, Mapping, Sequence, Set
6-
from typing import TYPE_CHECKING, TypedDict, TypeGuard
6+
from typing import TYPE_CHECKING, NotRequired, TypedDict, TypeGuard, cast
77

88
import jsonschema
99
import orjson
1010
from django.db import IntegrityError, router
11-
from django.db.models import Case, Exists, IntegerField, Q, QuerySet, Value, When
11+
from django.db.models import Case, Exists, F, IntegerField, Q, QuerySet, Value, When
1212
from django.http import Http404, HttpResponse, StreamingHttpResponse
1313
from rest_framework import status
1414
from rest_framework.request import Request
@@ -422,18 +422,56 @@ def post(self, request: Request, project: Project) -> Response:
422422
return Response({"associatedDsymFiles": []})
423423

424424

425-
def get_file_info(file) -> tuple[str | None, str | None, list[str]]:
425+
class AssembleRequestFile(TypedDict):
426+
"""One file entry from the DIF assemble request body."""
427+
428+
name: str
429+
chunks: list[str]
430+
debug_id: NotRequired[str]
431+
432+
433+
AssembleRequestPayload = dict[str, AssembleRequestFile]
434+
"""Mapping from file checksums to the corresponding assemble request payload."""
435+
436+
437+
def parse_assemble_request_payload(body: bytes) -> AssembleRequestPayload:
438+
"""Parse and validate the DIF assemble request body."""
439+
schema: dict[str, object] = {
440+
"type": "object",
441+
"patternProperties": {
442+
"^[0-9a-f]{40}$": {
443+
"type": "object",
444+
"required": ["name", "chunks"],
445+
"properties": {
446+
"name": {"type": "string"},
447+
"debug_id": {"type": "string"},
448+
"chunks": {
449+
"type": "array",
450+
"items": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
451+
},
452+
},
453+
"additionalProperties": True,
454+
}
455+
},
456+
"additionalProperties": False,
457+
}
458+
payload_obj: object = orjson.loads(body)
459+
jsonschema.validate(payload_obj, schema)
460+
return cast(AssembleRequestPayload, payload_obj)
461+
462+
463+
def get_file_info(file: AssembleRequestFile) -> tuple[str, str | None, list[str]]:
426464
"""
427465
Extracts file information from one assemble payload.
428466
"""
429-
name = file.get("name")
467+
name = file["name"]
430468
debug_id = file.get("debug_id")
431-
chunks = file.get("chunks", [])
469+
chunks = file["chunks"]
432470

433471
return name, debug_id, chunks
434472

435473

436-
def batch_assemble(project, files):
474+
def batch_assemble(project: Project, files: AssembleRequestPayload):
437475
"""
438476
Performs assembling in a batch fashion, issuing queries that span multiple files.
439477
"""
@@ -477,7 +515,7 @@ def batch_assemble(project, files):
477515
)
478516

479517
for debug_file in existing_debug_files:
480-
checksum = debug_file.checksum
518+
checksum = debug_file.nonnull_checksum
481519
file = files_to_check.pop(checksum)
482520
requested_debug_id = requested_debug_ids_by_checksum[checksum]
483521

@@ -558,22 +596,22 @@ def batch_assemble(project, files):
558596
return file_response
559597

560598

561-
def _get_requested_debug_id(file) -> str | None:
599+
def _get_requested_debug_id(file: AssembleRequestFile) -> str | None:
562600
"""Returns the effective requested debug ID for one assemble payload.
563601
564602
This normalizes an explicit ``debug_id`` when present, or derives one from a
565603
ProGuard-style request name such as ``/proguard/mapping-<uuid>.txt``.
566604
"""
567-
return get_debug_id_from_dif_request(name=file.get("name"), debug_id=file.get("debug_id"))
605+
return get_debug_id_from_dif_request(name=file["name"], debug_id=file.get("debug_id"))
568606

569607

570-
def _is_requested_proguard(file) -> bool:
608+
def _is_requested_proguard(file: AssembleRequestFile) -> bool:
571609
"""Returns whether one assemble payload should be treated as a ProGuard request.
572610
573611
This is true only when the request's effective debug ID comes from a
574612
ProGuard-style filename, rather than merely from an explicit ``debug_id``.
575613
"""
576-
name = file.get("name")
614+
name = file["name"]
577615
requested_debug_id = _get_requested_debug_id(file)
578616
return (
579617
requested_debug_id is not None
@@ -583,7 +621,7 @@ def _is_requested_proguard(file) -> bool:
583621

584622
def _is_proguard_reupload_clone_request(
585623
requested_debug_id: str | None,
586-
file,
624+
file: AssembleRequestFile,
587625
selected_debug_id: str | None,
588626
) -> TypeGuard[str]:
589627
"""Return whether the assemble request should clone a ProGuard debug file."""
@@ -595,6 +633,7 @@ def _is_proguard_reupload_clone_request(
595633

596634

597635
class _DebugFileAnnotations(TypedDict):
636+
nonnull_checksum: str
598637
requested_debug_id_match: int
599638
proguard_clone_source_match: int
600639

@@ -639,8 +678,11 @@ def _find_existing_debug_files(
639678
ProjectDebugFile.objects.filter(
640679
project_id=project.id,
641680
checksum__in=checksums,
681+
checksum__isnull=False,
642682
)
643683
.annotate(
684+
# Mirror the filtered checksum into an annotated non-null field for type safety.
685+
nonnull_checksum=F("checksum"),
644686
requested_debug_id_match=_build_requested_debug_id_match_annotation(
645687
requested_debug_ids_by_checksum.items()
646688
),
@@ -740,29 +782,8 @@ def post(self, request: Request, project: Project) -> Response:
740782
741783
:auth: required
742784
"""
743-
schema = {
744-
"type": "object",
745-
"patternProperties": {
746-
"^[0-9a-f]{40}$": {
747-
"type": "object",
748-
"required": ["name", "chunks"],
749-
"properties": {
750-
"name": {"type": "string"},
751-
"debug_id": {"type": "string"},
752-
"chunks": {
753-
"type": "array",
754-
"items": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
755-
},
756-
},
757-
"additionalProperties": True,
758-
}
759-
},
760-
"additionalProperties": False,
761-
}
762-
763785
try:
764-
files = orjson.loads(request.body)
765-
jsonschema.validate(files, schema)
786+
files = parse_assemble_request_payload(request.body)
766787
except jsonschema.ValidationError as e:
767788
return Response({"error": str(e).splitlines()[0]}, status=400)
768789
except Exception:

0 commit comments

Comments
 (0)