33
44from dataclasses import dataclass
55from enum import Enum
6+ from typing import Literal
67from uuid import UUID
78
9+ import jsonschema
10+
811
912class IssueType (Enum ):
1013 """General classification of the issue."""
@@ -26,12 +29,18 @@ class IssueType(Enum):
2629 UNEXPECTED_CLASS = "UnexpectedClassIssue"
2730 URI_FORMAT = "UriFormatIssue"
2831
32+ @classmethod
33+ def names (cls ) -> list [str ]:
34+ """Return the string names of all IssueTypes as a list."""
35+ return [type_ .value for type_ in cls ]
36+
2937
3038@dataclass
3139class IssueIdentifiers :
3240 """Information for locating an issue."""
3341
3442 annotation : UUID | None = None
43+ annotation_type : Literal ["Bbox" , "Cuboid" , "Num" , "Poly2d" , "Poly3d" , "Seg3d" ] | None = None
3544 attribute : str | None = None
3645 frame : int | None = None
3746 object : UUID | None = None
@@ -46,28 +55,25 @@ def serialize(self) -> dict[str, str | int]:
4655 dict[str, str | int]
4756 The serialized IssueIdentifiers as a JSON-compatible dictionary
4857 """
49- serialized_issue_identifiers : dict [str , str | int ] = {}
50- if self .annotation is not None :
51- serialized_issue_identifiers ["annotation" ] = str (self .annotation )
52- if self .attribute is not None :
53- serialized_issue_identifiers ["attribute" ] = self .attribute
54- if self .frame is not None :
55- serialized_issue_identifiers ["frame" ] = self .frame
56- if self .object is not None :
57- serialized_issue_identifiers ["object" ] = str (self .object )
58- if self .object_type is not None :
59- serialized_issue_identifiers ["object_type" ] = self .object_type
60- if self .sensor is not None :
61- serialized_issue_identifiers ["sensor" ] = self .sensor
62- return serialized_issue_identifiers
58+ return _clean_dict (
59+ {
60+ "annotation" : str (self .annotation ),
61+ "annotation_type" : self .annotation_type ,
62+ "attribute" : self .attribute ,
63+ "frame" : self .frame ,
64+ "object" : str (self .object ),
65+ "object_type" : self .object_type ,
66+ "sensor" : self .sensor ,
67+ }
68+ )
6369
6470 @classmethod
65- def deserialize (cls , serialized_issue_identifiers : dict [str , str | int ]) -> "IssueIdentifiers" : # noqa: C901
71+ def deserialize (cls , serialized_identifiers : dict [str , str | int ]) -> "IssueIdentifiers" :
6672 """Deserialize a JSON-compatible dictionary back into an IssueIdentifiers class instance.
6773
6874 Parameters
6975 ----------
70- serialized_issue_identifiers : dict[str, str | int]
76+ serialized_identifiers : dict[str, str | int]
7177 The serialized IssueIdentifiers as a JSON-compatible dictionary
7278
7379 Returns
@@ -80,45 +86,20 @@ def deserialize(cls, serialized_issue_identifiers: dict[str, str | int]) -> "Iss
8086 TypeError
8187 If any of the fields have an unexpected type
8288 """
83- identifiers = IssueIdentifiers ()
84-
85- annotation = serialized_issue_identifiers .get ("annotation" )
86- if isinstance (annotation , int ):
87- raise TypeError
88- if annotation is not None :
89- identifiers .annotation = UUID (annotation )
90-
91- attribute = serialized_issue_identifiers .get ("attribute" )
92- if isinstance (attribute , int ):
93- raise TypeError
94- if attribute is not None :
95- identifiers .attribute = attribute
96-
97- frame = serialized_issue_identifiers .get ("frame" )
98- if isinstance (frame , str ):
99- raise TypeError
100- if frame is not None :
101- identifiers .frame = frame
102-
103- object = serialized_issue_identifiers .get ("object" ) # noqa: A001
104- if isinstance (object , int ):
105- raise TypeError
106- if object is not None :
107- identifiers .object = UUID (object )
108-
109- object_type = serialized_issue_identifiers .get ("object_type" )
110- if isinstance (object_type , int ):
111- raise TypeError
112- if object_type is not None :
113- identifiers .object_type = object_type
114-
115- sensor = serialized_issue_identifiers .get ("sensor" )
116- if isinstance (sensor , int ):
117- raise TypeError
118- if sensor is not None :
119- identifiers .sensor = sensor
120-
121- return identifiers
89+ _verify_identifiers_schema (serialized_identifiers )
90+ return IssueIdentifiers (
91+ annotation = UUID (serialized_identifiers .get ("annotation" ))
92+ if serialized_identifiers .get ("annotation" ) is not None
93+ else None ,
94+ annotation_type = serialized_identifiers .get ("annotation_type" ),
95+ attribute = serialized_identifiers .get ("attribute" ),
96+ frame = serialized_identifiers .get ("frame" ),
97+ object = UUID (serialized_identifiers .get ("object" ))
98+ if serialized_identifiers .get ("object" ) is not None
99+ else None ,
100+ object_type = serialized_identifiers .get ("object_type" ),
101+ sensor = serialized_identifiers .get ("sensor" ),
102+ )
122103
123104
124105@dataclass
@@ -137,17 +118,17 @@ def serialize(self) -> dict[str, str | dict[str, str | int] | list[str | int]]:
137118 dict[str, str | dict[str, str | int] | list[str | int]]
138119 The serialized Issue as a JSON-compatible dictionary
139120 """
140- serialized_issue = {
141- "type" : str ( self . type . value ),
142- "identifiers " : (
143- self . identifiers . serialize ()
144- if isinstance ( self .identifiers , IssueIdentifiers )
145- else self .identifiers
146- ),
147- }
148- if self . reason is not None :
149- serialized_issue [ "reason" ] = self . reason
150- return serialized_issue
121+ return _clean_dict (
122+ {
123+ "type " : str ( self . type . value ),
124+ " identifiers" : (
125+ self .identifiers . serialize ( )
126+ if isinstance ( self .identifiers , IssueIdentifiers )
127+ else self . identifiers
128+ ),
129+ "reason" : self . reason ,
130+ }
131+ )
151132
152133 @classmethod
153134 def deserialize (
@@ -167,21 +148,68 @@ def deserialize(
167148
168149 Raises
169150 ------
170- TypeError
171- If the reason is not None or a string or if the identifiers are a string
151+ jsonschema.exceptions.ValidationError
152+ If the serialized data does not match the Issue JSONSchema.
172153 """
173- serialized_type = serialized_issue ["type" ]
174- serialized_identifiers = serialized_issue ["identifiers" ]
175- serialized_reason = serialized_issue .get ("reason" )
176- if serialized_reason is not None and not isinstance (serialized_reason , str ):
177- raise TypeError
178- if isinstance (serialized_identifiers , str ):
179- raise TypeError
180-
154+ _verify_issue_schema (serialized_issue )
181155 return Issue (
182- IssueType (serialized_type ),
183- IssueIdentifiers .deserialize (serialized_identifiers )
184- if not isinstance (serialized_identifiers , list )
185- else serialized_identifiers ,
186- serialized_reason ,
156+ type = IssueType (serialized_issue [ "type" ] ),
157+ identifiers = IssueIdentifiers .deserialize (serialized_issue [ "identifiers" ] )
158+ if not isinstance (serialized_issue [ "identifiers" ] , list )
159+ else serialized_issue [ "identifiers" ] ,
160+ reason = serialized_issue . get ( "reason" ) ,
187161 )
162+
163+
164+ def _clean_dict (d : dict ) -> dict :
165+ """Remove all fields in a dict that are None or 'None'."""
166+ return {k : v for k , v in d .items () if str (v ) != "None" }
167+
168+
169+ ISSUES_SCHEMA = {
170+ "type" : "array" ,
171+ "definitions" : {
172+ "issue" : {
173+ "type" : "object" ,
174+ "properties" : {
175+ "type" : {"enum" : IssueType .names ()},
176+ "identifiers" : {
177+ "anyOf" : [
178+ {
179+ "type" : "object" ,
180+ "properties" : {
181+ "annotation" : {
182+ "type" : "string" ,
183+ "pattern" : "^(-?[0-9]+|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$" , # noqa: E501
184+ },
185+ "annotation_type" : {
186+ "enum" : ["Bbox" , "Cuboid" , "Num" , "Poly2d" , "Poly3d" , "Seg3d" ]
187+ },
188+ "attribute" : {"type" : "string" },
189+ "frame" : {"type" : "integer" },
190+ "object" : {
191+ "type" : "string" ,
192+ "pattern" : "^(-?[0-9]+|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$" , # noqa: E501
193+ },
194+ "object_type" : {"type" : "string" },
195+ "sensor" : {"type" : "string" },
196+ },
197+ },
198+ {"type" : "array" , "items" : {"type" : ["string" , "integer" ]}},
199+ ]
200+ },
201+ "reason" : {"type" : "string" },
202+ },
203+ "required" : ["type" , "identifiers" ],
204+ },
205+ },
206+ "items" : {"$ref" : "#/definitions/issue" },
207+ }
208+
209+
210+ def _verify_issue_schema (d : dict ) -> None :
211+ jsonschema .validate (d , ISSUES_SCHEMA ["definitions" ]["issue" ])
212+
213+
214+ def _verify_identifiers_schema (d : dict ) -> None :
215+ jsonschema .validate (d , ISSUES_SCHEMA ["definitions" ]["issue" ]["properties" ]["identifiers" ])
0 commit comments