11import abc
2- from dataclasses import dataclass
2+ from dataclasses import dataclass , field
3+ from enum import Enum , auto
34
45from typing import Any , Callable , Dict , List , Optional , Union
56
7+ from labelbox .schema .project import Project
68from labelbox .orm import query
79from labelbox .orm .db_object import DbObject , Updateable , BulkDeletable
810from labelbox .orm .model import Entity , Field , Relationship
911from labelbox .utils import snake_case , camel_case
10-
11-
12- @dataclass
13- class OntologyEntity :
14- required : bool
15- name : str
12+ from labelbox .exceptions import InconsistentOntologyException
1613
1714
1815@dataclass
1916class Option :
20- label : str
2117 value : str
18+ schema_id : Optional [str ] = None
2219 feature_schema_id : Optional [str ] = None
23- schema_node_id : Optional [str ] = None
20+ options : List ["Classification" ] = field (default_factory = list )
21+
22+ @property
23+ def label (self ):
24+ return self .value
2425
2526 @classmethod
26- def from_json (cls , json_dict ):
27- _dict = convert_keys (json_dict , snake_case )
28- return cls (** _dict )
27+ def from_dict (cls , dictionary : Dict [str , Any ]):
28+ return Option (value = dictionary ["value" ],
29+ schema_id = dictionary ["schemaNodeId" ],
30+ feature_schema_id = dictionary ["featureSchemaId" ],
31+ options = [
32+ Classification .from_dict (o )
33+ for o in dictionary .get ("options" , [])
34+ ])
35+
36+ def asdict (self ) -> Dict [str , Any ]:
37+ return {
38+ "schemaNodeId" : self .schema_id ,
39+ "featureSchemaId" : self .feature_schema_id ,
40+ "label" : self .label ,
41+ "value" : self .value ,
42+ "options" : [o .asdict () for o in self .options ]
43+ }
44+
45+ def add_option (self , option : 'Classification' ) -> 'Classification' :
46+ if option .instructions in (o .instructions for o in self .options ):
47+ raise InconsistentOntologyException (
48+ f"Duplicate nested classification '{ option .instructions } ' "
49+ f"for option '{ self .label } '" )
50+ self .options .append (option )
2951
3052
3153@dataclass
32- class Classification (OntologyEntity ):
33- type : str
54+ class Classification :
55+ class Type (Enum ):
56+ TEXT = "text"
57+ CHECKLIST = "checklist"
58+ RADIO = "radio"
59+ DROPDOWN = "dropdown"
60+
61+ _REQUIRES_OPTIONS = {Type .CHECKLIST , Type .RADIO , Type .DROPDOWN }
62+
63+ class_type : Type
3464 instructions : str
35- options : List [Option ]
65+ required : bool = False
66+ options : List [Option ] = field (default_factory = list )
67+ schema_id : Optional [str ] = None
3668 feature_schema_id : Optional [str ] = None
37- schema_node_id : Optional [str ] = None
69+
70+ @property
71+ def name (self ):
72+ return self .instructions
3873
3974 @classmethod
40- def from_json (cls , json_dict ):
41- _dict = convert_keys (json_dict , snake_case )
42- _dict ['options' ] = [
43- Option .from_json (option ) for option in _dict ['options' ]
44- ]
45- return cls (** _dict )
75+ def from_dict (cls , dictionary : Dict [str , Any ]):
76+ return Classification (
77+ class_type = Classification .Type (dictionary ["type" ]),
78+ instructions = dictionary ["instructions" ],
79+ required = dictionary ["required" ],
80+ options = [Option .from_dict (o ) for o in dictionary ["options" ]],
81+ schema_id = dictionary ["schemaNodeId" ],
82+ feature_schema_id = dictionary ["schemaNodeId" ])
83+
84+ def asdict (self ) -> Dict [str , Any ]:
85+ if self .class_type in Classification ._REQUIRES_OPTIONS \
86+ and len (self .options ) < 1 :
87+ raise InconsistentOntologyException (
88+ f"Classification '{ self .instructions } ' requires options." )
89+ return {
90+ "type" : self .class_type .value ,
91+ "instructions" : self .instructions ,
92+ "name" : self .name ,
93+ "required" : self .required ,
94+ "options" : [o .asdict () for o in self .options ],
95+ "schemaNodeId" : self .schema_id ,
96+ "featureSchemaId" : self .feature_schema_id
97+ }
98+
99+ def add_option (self , option : Option ):
100+ if option .value in (o .value for o in self .options ):
101+ raise InconsistentOntologyException (
102+ f"Duplicate option '{ option .value } ' "
103+ f"for classification '{ self .name } '." )
104+ self .options .append (option )
46105
47106
48107@dataclass
49- class Tool (OntologyEntity ):
50- tool : str
51- color : str
52- classifications : List [Classification ]
108+ class Tool :
109+ class Type (Enum ):
110+ POLYGON = "polygon"
111+ SEGMENTATION = "superpixel"
112+ POINT = "point"
113+ BBOX = "rectangle"
114+ LINE = "line"
115+ NER = "named-entity"
116+
117+ tool : Type
118+ name : str
119+ required : bool = False
120+ color : str = "#000000"
121+ classifications : List [Classification ] = field (default_factory = list )
122+ schema_id : Optional [str ] = None
53123 feature_schema_id : Optional [str ] = None
54- schema_node_id : Optional [str ] = None
55124
56125 @classmethod
57- def from_json (cls , json_dict ):
58- _dict = convert_keys (json_dict , snake_case )
59- _dict ['classifications' ] = [
60- Classification .from_json (classification )
61- for classification in _dict ['classifications' ]
62- ]
63- return cls (** _dict )
126+ def from_dict (cls , dictionary : Dict [str , Any ]):
127+ return Tool (name = dictionary ['name' ],
128+ schema_id = dictionary ["schemaNodeId" ],
129+ feature_schema_id = dictionary ["featureSchemaId" ],
130+ required = dictionary ["required" ],
131+ tool = Tool .Type (dictionary ["tool" ]),
132+ classifications = [
133+ Classification .from_dict (c )
134+ for c in dictionary ["classifications" ]
135+ ],
136+ color = dictionary ["color" ])
137+
138+ def asdict (self ) -> Dict [str , Any ]:
139+ return {
140+ "tool" : self .tool .value ,
141+ "name" : self .name ,
142+ "required" : self .required ,
143+ "color" : self .color ,
144+ "classifications" : [c .asdict () for c in self .classifications ],
145+ "schemaNodeId" : self .schema_id ,
146+ "featureSchemaId" : self .feature_schema_id
147+ }
148+
149+ def add_classification (self , classification : Classification ):
150+ if classification .instructions in (c .instructions
151+ for c in self .classifications ):
152+ raise InconsistentOntologyException (
153+ f"Duplicate nested classification '{ classification .instructions } ' "
154+ f"for tool '{ self .name } '" )
155+ self .classifications .append (classification )
64156
65157
66158class Ontology (DbObject ):
@@ -100,15 +192,15 @@ def tools(self) -> List[Tool]:
100192 """Get list of tools (AKA objects) in an Ontology."""
101193 if self ._tools is None :
102194 self ._tools = [
103- Tool .from_json (tool ) for tool in self .normalized ['tools' ]
195+ Tool .from_dict (tool ) for tool in self .normalized ['tools' ]
104196 ]
105197 return self ._tools # type: ignore
106198
107199 def classifications (self ) -> List [Classification ]:
108200 """Get list of classifications in an Ontology."""
109201 if self ._classifications is None :
110202 self ._classifications = [
111- Classification .from_json (classification )
203+ Classification .from_dict (classification )
112204 for classification in self .normalized ['classifications' ]
113205 ]
114206 return self ._classifications # type: ignore
@@ -124,3 +216,45 @@ def convert_keys(json_dict: Dict[str, Any],
124216 if isinstance (json_dict , list ):
125217 return [convert_keys (ele , converter ) for ele in json_dict ]
126218 return json_dict
219+
220+
221+ @dataclass
222+ class OntologyBuilder :
223+
224+ tools : List [Tool ] = field (default_factory = list )
225+ classifications : List [Classification ] = field (default_factory = list )
226+
227+ @classmethod
228+ def from_dict (cls , dictionary : Dict [str , Any ]):
229+ return OntologyBuilder (
230+ tools = [Tool .from_dict (t ) for t in dictionary ["tools" ]],
231+ classifications = [
232+ Classification .from_dict (c )
233+ for c in dictionary ["classifications" ]
234+ ])
235+
236+ def asdict (self ):
237+ return {
238+ "tools" : [t .asdict () for t in self .tools ],
239+ "classifications" : [c .asdict () for c in self .classifications ]
240+ }
241+
242+ @classmethod
243+ def from_project (cls , project : Project ):
244+ ontology = project .ontology ().normalized
245+ return OntologyBuilder .from_dict (ontology )
246+
247+ def add_tool (self , tool : Tool ) -> Tool :
248+ if tool .name in (t .name for t in self .tools ):
249+ raise InconsistentOntologyException (
250+ f"Duplicate tool name '{ tool .name } '. " )
251+ self .tools .append (tool )
252+
253+ def add_classification (self ,
254+ classification : Classification ) -> Classification :
255+ if classification .instructions in (c .instructions
256+ for c in self .classifications ):
257+ raise InconsistentOntologyException (
258+ f"Duplicate classification instructions '{ classification .instructions } '. "
259+ )
260+ self .classifications .append (classification )
0 commit comments