@@ -32,19 +32,60 @@ class LineageRepro:
3232 created_at : datetime
3333 count : int
3434 all_verified : bool
35+ # Optional extra metadata available on newer APIs
36+ drivers : Optional [List [str ]] = None
37+ config_indices : Optional [List [int ]] = None
3538
3639 @classmethod
3740 def from_dict (cls , data : Dict [str , Any ]) -> "LineageRepro" :
38- created_at_str = data ["CreatedAt" ]
39- # Handle both RFC3339 formats
40- if created_at_str .endswith ("Z" ):
41- created_at_str = created_at_str [:- 1 ] + "+00:00"
41+ # Hash is required in all known schemas
42+ hash_value = data .get ("Hash" ) or data .get ("hash" )
43+ if not hash_value :
44+ raise KeyError ("Hash" )
45+
46+ # CreatedAt is present in both schemas, normalize if available
47+ created_at_str = data .get ("CreatedAt" ) or data .get ("created_at" )
48+ if created_at_str :
49+ # Handle both RFC3339 formats
50+ if created_at_str .endswith ("Z" ):
51+ created_at_str = created_at_str [:- 1 ] + "+00:00"
52+ created_at = datetime .fromisoformat (created_at_str )
53+ else :
54+ # Fallback to a stable value so sorting still works
55+ created_at = datetime .min
56+
57+ # Count:
58+ # - prefer explicit Count from legacy schema
59+ # - otherwise derive from ConfigIndices (one count per config index)
60+ # - final fallback is a single repro
61+ if "Count" in data :
62+ count = int (data ["Count" ])
63+ else :
64+ cfg_indices = data .get ("ConfigIndices" ) or data .get ("config_indices" ) or []
65+ if isinstance (cfg_indices , list ):
66+ count = max (len (cfg_indices ), 1 ) if cfg_indices else 1
67+ else :
68+ # Unexpected shape: treat as a single repro
69+ count = 1
70+
71+ # AllVerified:
72+ # - legacy AllVerified flag if present
73+ # - otherwise fall back to the newer Verified flag
74+ if "AllVerified" in data :
75+ all_verified = bool (data ["AllVerified" ])
76+ else :
77+ all_verified = bool (data .get ("Verified" ) or data .get ("verified" ) or False )
78+
79+ drivers = data .get ("Drivers" ) or data .get ("drivers" )
80+ cfg_indices_val = data .get ("ConfigIndices" ) or data .get ("config_indices" )
4281
4382 return cls (
44- hash = data ["Hash" ],
45- created_at = datetime .fromisoformat (created_at_str ),
46- count = data ["Count" ],
47- all_verified = data ["AllVerified" ],
83+ hash = hash_value ,
84+ created_at = created_at ,
85+ count = count ,
86+ all_verified = all_verified ,
87+ drivers = drivers ,
88+ config_indices = cfg_indices_val ,
4889 )
4990
5091
@@ -56,14 +97,23 @@ class ReproIndexResponse:
5697 @classmethod
5798 def from_dict (cls , data : Dict [str , Any ]) -> "ReproIndexResponse" :
5899 lineages = {}
59- lineages_data = data .get ("Lineages" ) or {}
100+ # Support both legacy "Lineages" and potential future "lineages"
101+ lineages_data = data .get ("Lineages" ) or data .get ("lineages" ) or {}
60102 for lineage_name , repros in lineages_data .items ():
61103 lineages [lineage_name ] = [LineageRepro .from_dict (r ) for r in (repros or [])]
62104
63- return cls (
64- bundle_id = data .get ("BundleID" , "00000000-0000-0000-0000-000000000000" ),
65- lineages = lineages ,
66- )
105+ # Bundle ID:
106+ # - legacy compat: top-level "BundleID"
107+ # - current NG API: "Bundle" object with "id" field
108+ bundle_id = data .get ("BundleID" )
109+ if not bundle_id :
110+ bundle = data .get ("Bundle" ) or data .get ("bundle" )
111+ if isinstance (bundle , dict ):
112+ bundle_id = bundle .get ("id" ) or bundle .get ("ID" )
113+ if not bundle_id :
114+ bundle_id = data .get ("bundle_id" , "00000000-0000-0000-0000-000000000000" )
115+
116+ return cls (bundle_id = bundle_id , lineages = lineages )
67117
68118
69119@dataclass
@@ -78,14 +128,40 @@ class ReproMetadata:
78128
79129 @classmethod
80130 def from_dict (cls , data : Dict [str , Any ]) -> "ReproMetadata" :
131+ if "repros" in data and isinstance (data ["repros" ], dict ):
132+ inner = data ["repros" ]
133+ else :
134+ inner = data
135+
136+ # Core identifiers
137+ hash_val = inner .get ("hash" ) or inner .get ("Hash" )
138+ if not hash_val :
139+ raise KeyError ("hash" )
140+
141+ bundle_val = inner .get ("bundle" ) or inner .get ("Bundle" )
142+ lineage_val = inner .get ("lineage" ) or inner .get ("Lineage" )
143+ asset_val = inner .get ("asset" ) or inner .get ("Asset" )
144+
145+ # Artifact hashes:
146+ artifact_hashes_val : List [str ]
147+ raw_hashes = inner .get ("artifact_hashes" )
148+ if raw_hashes :
149+ artifact_hashes_val = list (raw_hashes )
150+ else :
151+ single_hash = inner .get ("artifact_hash" ) or inner .get ("ArtifactHash" )
152+ if single_hash :
153+ artifact_hashes_val = [single_hash ]
154+ else :
155+ artifact_hashes_val = []
156+
81157 return cls (
82- hash = data [ "hash" ] ,
83- bundle = data [ "bundle" ] ,
84- lineage = data [ "lineage" ] ,
85- asset = data . get ( "asset" ) ,
86- artifact_hashes = data . get ( "artifact_hashes" ) or [] ,
87- summary = data .get ("summary" ) or "" ,
88- flaky = data .get ("flaky" , False ),
158+ hash = hash_val ,
159+ bundle = str ( bundle_val ) if bundle_val is not None else "" ,
160+ lineage = lineage_val or "" ,
161+ asset = asset_val ,
162+ artifact_hashes = artifact_hashes_val ,
163+ summary = inner .get ("summary" ) or "" ,
164+ flaky = bool ( inner .get ("flaky" , False ) ),
89165 )
90166
91167
0 commit comments