11import abc
22from dataclasses import dataclass , field
33from enum import Enum , auto
4+ import colorsys
45
56from typing import Any , Callable , Dict , List , Optional , Union
67
1415
1516@dataclass
1617class Option :
17- value : str
18+ """
19+ An option is a possible answer within a Classification object in
20+ a Project's ontology.
21+
22+ To instantiate, only the "value" parameter needs to be passed in.
23+
24+ Example(s):
25+ option = Option(value = "Option Example")
26+
27+ Attributes:
28+ value: (str)
29+ schema_id: (str)
30+ feature_schema_id: (str)
31+ options: (list)
32+ """
33+ value : Union [str , int ]
1834 schema_id : Optional [str ] = None
1935 feature_schema_id : Optional [str ] = None
2036 options : List ["Classification" ] = field (default_factory = list )
@@ -26,8 +42,8 @@ def label(self):
2642 @classmethod
2743 def from_dict (cls , dictionary : Dict [str , Any ]):
2844 return Option (value = dictionary ["value" ],
29- schema_id = dictionary [ "schemaNodeId" ] ,
30- feature_schema_id = dictionary [ "featureSchemaId" ] ,
45+ schema_id = dictionary . get ( "schemaNodeId" , []) ,
46+ feature_schema_id = dictionary . get ( "featureSchemaId" , []) ,
3147 options = [
3248 Classification .from_dict (o )
3349 for o in dictionary .get ("options" , [])
@@ -52,6 +68,36 @@ def add_option(self, option: 'Classification') -> 'Classification':
5268
5369@dataclass
5470class Classification :
71+ """
72+ A classfication to be added to a Project's ontology. The
73+ classification is dependent on the Classification Type.
74+
75+ To instantiate, the "class_type" and "instructions" parameters must
76+ be passed in.
77+
78+ The "options" parameter holds a list of Option objects. This is not
79+ necessary for some Classification types, such as TEXT. To see which
80+ types require options, look at the "_REQUIRES_OPTIONS" class variable.
81+
82+ Example(s):
83+ classification = Classification(
84+ class_type = Classification.Type.TEXT,
85+ instructions = "Classification Example")
86+
87+ classification_two = Classification(
88+ class_type = Classification.Type.RADIO,
89+ instructions = "Second Example")
90+ classification_two.add_option(Option(
91+ value = "Option Example"))
92+
93+ Attributes:
94+ class_type: (Classification.Type)
95+ instructions: (str)
96+ required: (bool)
97+ options: (list)
98+ schema_id: (str)
99+ feature_schema_id: (str)
100+ """
55101 class Type (Enum ):
56102 TEXT = "text"
57103 CHECKLIST = "checklist"
@@ -78,8 +124,8 @@ def from_dict(cls, dictionary: Dict[str, Any]):
78124 instructions = dictionary ["instructions" ],
79125 required = dictionary ["required" ],
80126 options = [Option .from_dict (o ) for o in dictionary ["options" ]],
81- schema_id = dictionary [ "schemaNodeId" ] ,
82- feature_schema_id = dictionary [ "schemaNodeId" ] )
127+ schema_id = dictionary . get ( "schemaNodeId" , []) ,
128+ feature_schema_id = dictionary . get ( "featureSchemaId" , []) )
83129
84130 def asdict (self ) -> Dict [str , Any ]:
85131 if self .class_type in Classification ._REQUIRES_OPTIONS \
@@ -106,6 +152,34 @@ def add_option(self, option: Option):
106152
107153@dataclass
108154class Tool :
155+ """
156+ A tool to be added to a Project's ontology. The tool is
157+ dependent on the Tool Type.
158+
159+ To instantiate, the "tool" and "name" parameters must
160+ be passed in.
161+
162+ The "classifications" parameter holds a list of Classification objects.
163+ This can be used to add nested classifications to a tool.
164+
165+ Example(s):
166+ tool = Tool(
167+ tool = Tool.Type.LINE,
168+ name = "Tool example")
169+ classification = Classification(
170+ class_type = Classification.Type.TEXT,
171+ instructions = "Classification Example")
172+ tool.add_classification(classification)
173+
174+ Attributes:
175+ tool: (Tool.Type)
176+ name: (str)
177+ required: (bool)
178+ color: (str)
179+ classifications: (list)
180+ schema_id: (str)
181+ feature_schema_id: (str)
182+ """
109183 class Type (Enum ):
110184 POLYGON = "polygon"
111185 SEGMENTATION = "superpixel"
@@ -125,8 +199,8 @@ class Type(Enum):
125199 @classmethod
126200 def from_dict (cls , dictionary : Dict [str , Any ]):
127201 return Tool (name = dictionary ['name' ],
128- schema_id = dictionary [ "schemaNodeId" ] ,
129- feature_schema_id = dictionary [ "featureSchemaId" ] ,
202+ schema_id = dictionary . get ( "schemaNodeId" , []) ,
203+ feature_schema_id = dictionary . get ( "featureSchemaId" , []) ,
130204 required = dictionary ["required" ],
131205 tool = Tool .Type (dictionary ["tool" ]),
132206 classifications = [
@@ -194,7 +268,7 @@ def tools(self) -> List[Tool]:
194268 self ._tools = [
195269 Tool .from_dict (tool ) for tool in self .normalized ['tools' ]
196270 ]
197- return self ._tools # type: ignore
271+ return self ._tools
198272
199273 def classifications (self ) -> List [Classification ]:
200274 """Get list of classifications in an Ontology."""
@@ -203,24 +277,34 @@ def classifications(self) -> List[Classification]:
203277 Classification .from_dict (classification )
204278 for classification in self .normalized ['classifications' ]
205279 ]
206- return self ._classifications # type: ignore
207-
208-
209- def convert_keys (json_dict : Dict [str , Any ],
210- converter : Callable ) -> Dict [str , Any ]:
211- if isinstance (json_dict , dict ):
212- return {
213- converter (key ): convert_keys (value , converter )
214- for key , value in json_dict .items ()
215- }
216- if isinstance (json_dict , list ):
217- return [convert_keys (ele , converter ) for ele in json_dict ]
218- return json_dict
280+ return self ._classifications
219281
220282
221283@dataclass
222284class OntologyBuilder :
285+ """
286+ A class to help create an ontology for a Project. This should be used
287+ for making Project ontologies from scratch. OntologyBuilder can also
288+ pull from an already existing Project's ontology.
223289
290+ There are no required instantiation arguments.
291+
292+ To create an ontology, use the asdict() method after fully building your
293+ ontology within this class, and inserting it into project.setup() as the
294+ "labeling_frontend_options" parameter.
295+
296+ Example:
297+ builder = OntologyBuilder()
298+ ...
299+ frontend = list(client.get_labeling_frontends())[0]
300+ project.setup(frontend, builder.asdict())
301+
302+ attributes:
303+ tools: (list)
304+ classifications: (list)
305+
306+
307+ """
224308 tools : List [Tool ] = field (default_factory = list )
225309 classifications : List [Classification ] = field (default_factory = list )
226310
@@ -234,11 +318,22 @@ def from_dict(cls, dictionary: Dict[str, Any]):
234318 ])
235319
236320 def asdict (self ):
321+ self ._update_colors ()
237322 return {
238323 "tools" : [t .asdict () for t in self .tools ],
239324 "classifications" : [c .asdict () for c in self .classifications ]
240325 }
241326
327+ def _update_colors (self ):
328+ num_tools = len (self .tools )
329+
330+ for index in range (num_tools ):
331+ hsv_color = (index * 1 / num_tools , 1 , 1 )
332+ rgb_color = tuple (
333+ int (255 * x ) for x in colorsys .hsv_to_rgb (* hsv_color ))
334+ if self .tools [index ].color is None :
335+ self .tools [index ].color = '#%02x%02x%02x' % rgb_color
336+
242337 @classmethod
243338 def from_project (cls , project : Project ):
244339 ontology = project .ontology ().normalized
@@ -257,4 +352,4 @@ def add_classification(self,
257352 raise InconsistentOntologyException (
258353 f"Duplicate classification instructions '{ classification .instructions } '. "
259354 )
260- self .classifications .append (classification )
355+ self .classifications .append (classification )
0 commit comments