4343 BuildDetailsVcsInfo ,
4444)
4545from sentry .preprod .api .models .public .snapshots import (
46+ SnapshotApproverResponseDict ,
4647 SnapshotCreateResponseDict ,
4748 SnapshotDetailsResponseDict ,
49+ SnapshotImageResponseDict ,
50+ VcsInfoResponseDict ,
4851)
4952from sentry .preprod .api .models .snapshots .project_preprod_snapshot_models import (
5053 SnapshotApprover ,
51- SnapshotDetailsApiResponse ,
52- SnapshotImageResponse ,
5354)
5455from sentry .preprod .api .models .snapshots .snapshot_status import (
5556 SnapshotStatusInput ,
6667 MISSING_BASE_GRACE_PERIOD_SECONDS ,
6768 SNAPSHOT_ARCHIVE_MANIFEST_FILENAME ,
6869)
69- from sentry .preprod .snapshots .manifest import (
70- ComparisonManifest ,
71- ImageMetadata ,
72- SnapshotManifest ,
73- image_metadata_extras ,
74- )
70+ from sentry .preprod .snapshots .image_serialization import build_head_image_dict
71+ from sentry .preprod .snapshots .manifest import SnapshotManifest
7572from sentry .preprod .snapshots .models import (
7673 PreprodSnapshotComparison ,
7774 PreprodSnapshotMetrics ,
@@ -130,28 +127,6 @@ def _strip_to_compact(img: dict[str, Any]) -> dict[str, Any]:
130127 return {k : img [k ] for k in _COMPACT_FIELDS if k in img }
131128
132129
133- def build_snapshot_image_response (
134- image_file_name : str ,
135- metadata : ImageMetadata ,
136- global_diff_threshold : float | None ,
137- ) -> SnapshotImageResponse :
138- return SnapshotImageResponse (
139- ** image_metadata_extras (metadata , exclude = {"key" , "image_file_name" }),
140- key = metadata .content_hash ,
141- display_name = metadata .display_name ,
142- image_file_name = image_file_name ,
143- group = metadata .group ,
144- width = metadata .width ,
145- height = metadata .height ,
146- diff_threshold = metadata .diff_threshold
147- if metadata .diff_threshold is not None
148- else global_diff_threshold ,
149- description = metadata .description ,
150- tags = metadata .tags ,
151- canvas_theme = metadata .canvas_theme ,
152- )
153-
154-
155130MAX_SNAPSHOT_REQUEST_BODY_SIZE = 256 * 1024 * 1024
156131
157132
@@ -387,8 +362,10 @@ def get(
387362 with start_span (
388363 op = "preprod.snapshot.parse_manifest" , name = "parse_head_manifest"
389364 ) as span :
390- manifest = SnapshotManifest (** orjson .loads (raw_manifest ))
391- set_span_data (span , "image_count" , len (manifest .images ))
365+ head_manifest = orjson .loads (raw_manifest )
366+ head_images : dict [str , Any ] = head_manifest .get ("images" , {})
367+ head_diff_threshold = head_manifest .get ("diff_threshold" )
368+ set_span_data (span , "image_count" , len (head_images ))
392369 except Exception :
393370 logger .exception (
394371 "Failed to retrieve snapshot manifest" ,
@@ -415,8 +392,8 @@ def get(
415392 else :
416393 vcs_info = BuildDetailsVcsInfo ()
417394
418- comparison_manifest : ComparisonManifest | None = None
419- base_manifest : SnapshotManifest | None = None
395+ comparison_manifest : dict [ str , Any ] | None = None
396+ base_manifest : dict [ str , Any ] | None = None
420397 all_comparisons = list (
421398 PreprodSnapshotComparison .objects .select_related ("base_snapshot_metrics" )
422399 .filter (head_snapshot_metrics = snapshot_metrics )
@@ -441,10 +418,12 @@ def get(
441418 with start_span (
442419 op = "preprod.snapshot.parse_manifest" , name = "parse_comparison_manifest"
443420 ) as span :
444- comparison_manifest = ComparisonManifest (
445- ** orjson .loads (raw_comparison_manifest )
421+ comparison_manifest = orjson .loads (raw_comparison_manifest )
422+ if "base_artifact_id" not in comparison_manifest :
423+ raise ValueError ("comparison manifest missing base_artifact_id" )
424+ set_span_data (
425+ span , "image_count" , len (comparison_manifest .get ("images" , {}))
446426 )
447- set_span_data (span , "image_count" , len (comparison_manifest .images ))
448427 except Exception :
449428 comparison_manifest = None
450429 logger .exception (
@@ -466,8 +445,8 @@ def get(
466445 with start_span (
467446 op = "preprod.snapshot.parse_manifest" , name = "parse_base_manifest"
468447 ) as span :
469- base_manifest = SnapshotManifest ( ** orjson .loads (raw_base_manifest ) )
470- set_span_data (span , "image_count" , len (base_manifest .images ))
448+ base_manifest = orjson .loads (raw_base_manifest )
449+ set_span_data (span , "image_count" , len (base_manifest .get ( " images" , {}) ))
471450 except Exception :
472451 logger .exception (
473452 "Failed to fetch base manifest" ,
@@ -509,26 +488,29 @@ def get(
509488 with start_span (
510489 op = "preprod.snapshot.serialize_images" , name = "serialize_head_images"
511490 ) as span :
512- set_span_data (span , "image_count" , len (manifest . images ))
513- image_list = [
514- build_snapshot_image_response (key , metadata , manifest . diff_threshold )
515- for key , metadata in sorted (manifest . images .items ())
491+ set_span_data (span , "image_count" , len (head_images ))
492+ image_list : list [ SnapshotImageResponseDict ] = [
493+ build_head_image_dict (key , metadata , head_diff_threshold )
494+ for key , metadata in sorted (head_images .items ())
516495 ]
517496
518- images_by_file_name : dict [str , SnapshotImageResponse ] = {
519- img . image_file_name : img for img in image_list
497+ images_by_file_name : dict [str , SnapshotImageResponseDict ] = {
498+ img [ " image_file_name" ] : img for img in image_list
520499 }
521500
522501 base_artifact_id : str | None = None
523502
524503 if comparison_manifest is not None :
525- base_artifact_id = str (comparison_manifest .base_artifact_id )
504+ base_artifact_id = str (comparison_manifest ["base_artifact_id" ])
505+ comparison_images = comparison_manifest .get ("images" , {})
526506 with start_span (
527507 op = "preprod.snapshot.categorize_comparison" , name = "categorize_comparison_images"
528508 ) as span :
529- set_span_data (span , "image_count" , len (comparison_manifest . images ))
509+ set_span_data (span , "image_count" , len (comparison_images ))
530510 categorized = categorize_comparison_images (
531- comparison_manifest , images_by_file_name , base_manifest
511+ comparison_images ,
512+ images_by_file_name ,
513+ base_manifest .get ("images" , {}) if base_manifest else None ,
532514 )
533515 else :
534516 if comparison is not None :
@@ -628,51 +610,55 @@ def get(
628610 op = "preprod.snapshot.serialize_response" , name = "serialize_response_body"
629611 ) as span :
630612 set_span_data (span , "image_count" , len (image_list ))
631- response_data = SnapshotDetailsApiResponse (
632- head_artifact_id = str (artifact .id ),
633- base_artifact_id = base_artifact_id ,
634- project_id = str (artifact .project_id ),
635- comparison_type = comparison_type ,
636- state = artifact .state ,
637- vcs_info = vcs_info ,
638- app_id = artifact .app_id ,
639- is_selective = snapshot_metrics .is_selective ,
640- images = image_list if comparison_type != "diff" else [],
641- image_count = snapshot_metrics .image_count ,
642- changed = categorized .changed ,
643- changed_count = len (categorized .changed ),
644- added = categorized .added ,
645- added_count = len (categorized .added ),
646- removed = categorized .removed ,
647- removed_count = len (categorized .removed ),
648- renamed = categorized .renamed ,
649- renamed_count = len (categorized .renamed ),
650- unchanged = categorized .unchanged ,
651- unchanged_count = len (categorized .unchanged ),
652- errored = categorized .errored ,
653- errored_count = len (categorized .errored ),
654- skipped = categorized .skipped ,
655- skipped_count = len (categorized .skipped ),
656- diff_threshold = manifest .diff_threshold ,
657- comparison_state = derived_status .comparison_state ,
658- approval_status = derived_status .approval_status ,
659- comparison_error_message = derived_status .comparison_error_message ,
660- approvers = approver_list if approved else [],
661- ).dict ()
613+ response_data : SnapshotDetailsResponseDict = {
614+ "head_artifact_id" : str (artifact .id ),
615+ "base_artifact_id" : base_artifact_id ,
616+ "project_id" : str (artifact .project_id ),
617+ "comparison_type" : comparison_type ,
618+ "state" : cast (str , artifact .state ),
619+ "vcs_info" : cast (VcsInfoResponseDict , vcs_info .dict ()),
620+ "app_id" : artifact .app_id ,
621+ "is_selective" : snapshot_metrics .is_selective ,
622+ "images" : image_list if comparison_type != "diff" else [],
623+ "image_count" : snapshot_metrics .image_count ,
624+ "added" : categorized .added ,
625+ "added_count" : len (categorized .added ),
626+ "removed" : categorized .removed ,
627+ "removed_count" : len (categorized .removed ),
628+ "renamed" : categorized .renamed ,
629+ "renamed_count" : len (categorized .renamed ),
630+ "changed" : categorized .changed ,
631+ "changed_count" : len (categorized .changed ),
632+ "unchanged" : categorized .unchanged ,
633+ "unchanged_count" : len (categorized .unchanged ),
634+ "errored" : categorized .errored ,
635+ "errored_count" : len (categorized .errored ),
636+ "skipped" : categorized .skipped ,
637+ "skipped_count" : len (categorized .skipped ),
638+ "diff_threshold" : head_diff_threshold ,
639+ "comparison_state" : derived_status .comparison_state ,
640+ "approval_status" : derived_status .approval_status ,
641+ "comparison_error_message" : derived_status .comparison_error_message ,
642+ "approvers" : (
643+ [cast (SnapshotApproverResponseDict , a .dict ()) for a in approver_list ]
644+ if approved
645+ else []
646+ ),
647+ }
662648
663649 if compact :
650+ # Compact mode strips images to a subset of keys, producing a shape that
651+ # is intentionally looser than the response TypedDict; mutate via a plain
652+ # dict view.
653+ compact_data = cast (dict [str , Any ], response_data )
664654 for key in _COMPACT_IMAGE_LIST_KEYS :
665- response_data [key ] = [_strip_to_compact (img ) for img in response_data [key ]]
655+ compact_data [key ] = [_strip_to_compact (img ) for img in compact_data [key ]]
666656 for key in _COMPACT_PAIR_LIST_KEYS :
667- for pair in response_data [key ]:
657+ for pair in compact_data [key ]:
668658 pair ["base_image" ] = _strip_to_compact (pair ["base_image" ])
669659 pair ["head_image" ] = _strip_to_compact (pair ["head_image" ])
670660
671- # cast() sanctioned here: pydantic .dict() returns dict[str, Any] with no
672- # static link back to SnapshotDetailsResponseDict. The TypedDict and the
673- # Pydantic model are kept in sync by hand at the source of truth.
674- body = cast (SnapshotDetailsResponseDict , response_data )
675- return Response (body )
661+ return Response (response_data )
676662
677663
678664@extend_schema (tags = ["Snapshots" ])
0 commit comments