From ba3b31628e873608b6808c838140a38e0388af2b Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Thu, 14 Jan 2021 13:51:04 -0800 Subject: [PATCH 01/36] first commit of this branch --- labelbox/schema/ontology_generator.py | 175 ++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 labelbox/schema/ontology_generator.py diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py new file mode 100644 index 000000000..76743b211 --- /dev/null +++ b/labelbox/schema/ontology_generator.py @@ -0,0 +1,175 @@ +from dataclasses import dataclass, field +from enum import Enum, auto +import os +from typing import List, Optional + +from labelbox import Client, Project, Dataset, LabelingFrontend + +class InconsistentOntologyException(Exception): + pass + +@dataclass +class Option(): + value: str + label: str = None + schema_id: Optional[str] = None + feature_schema_id: Optional[str] = None + + def __post_init__(self): + self.label = self.value + + +@dataclass +class Classification(): + + class Type(Enum): + TEXT = "text" + CHECKLIST = "checklist" + RADIO = "radio" + DROPDOWN = "dropdown" + + type: Type + instructions: str + name: str = None + required: bool = False + options: List[Option] = field(default_factory=list) + schema_id: Optional[str] = None + feature_schema_id: Optional[str] = None + + def __post_init__(self): + self.name = self.instructions + + +@dataclass +class Tool(): + + class Type(Enum): + POLYGON = "polygon" + SEGMENTATION = "superpixel" + POINT = "point" + BBOX = "rectangle" + LINE = "line" + + tool: Type + name: str + required: bool = False + color: str = "#000000" + classifications: List[Classification] = field(default_factory=list) + schema_id: Optional[str] = None + feature_schema_id: Optional[str] = None + + # @classmethod + # def _from_existing_ontology(cls, dict): + # for key,value in dict.items(): + # print(key,value) + + +@dataclass +class Ontology(): + + tools: List[Tool] = field(default_factory=list) + classifications: List[Classification] = field(default_factory=list) + + @classmethod + def from_project(cls, project: Project): + ontology = project.ontology().normalized + return_ontology = Ontology() + + for tool in ontology['tools']: + tool['schema_id'] = tool.pop('schemaNodeId') + tool['feature_schema_id'] = tool.pop('featureSchemaId') + tool['tool'] = Tool.Type(tool['tool']) + return_ontology.add_tool(**tool) + + for classification in ontology['classifications']: + classification['schema_id'] = classification.pop('schemaNodeId') + classification['feature_schema_id'] = classification.pop('featureSchemaId') + classification['type'] = Classification.Type(classification['type']) + return_ontology.add_classification(**classification) + + return return_ontology + + def add_tool(self, *args, **kwargs): + new_tool = Tool(*args, **kwargs) + if new_tool.name in (tool.name for tool in self.tools): + raise InconsistentOntologyException(f"Duplicate tool name '{new_tool.name}'. ") + self.tools.append(new_tool) + return new_tool + + def add_classification(self, *args, **kwargs): + new_classification = Classification(*args, **kwargs) + if new_classification.instructions in (classification.instructions for classification in self.classifications): + raise InconsistentOntologyException(f"Duplicate classifications instructions '{new_classification.instructions}'. ") + self.classifications.append(new_classification) + return new_classification + + def build(self): + self.all_tools = [] + self.all_classifications = [] + + for tool in self.tools: + curr_tool = dict((key,value) for (key,value) in tool.__dict__.items()) + curr_tool['tool'] = curr_tool['tool'].value + curr_tool['schemaNodeId'] = curr_tool.pop('schema_id') + curr_tool['featureSchemaId'] = curr_tool.pop('feature_schema_id') + self.all_tools.append(curr_tool) + + for classification in self.classifications: + curr_classification = dict((key,value) for (key,value) in classification.__dict__.items()) + curr_classification['type'] = curr_classification['type'].value + curr_classification['schemaNodeId'] = curr_classification.pop('schema_id') + curr_classification['featureSchemaId'] = curr_classification.pop('feature_schema_id') + self.all_classifications.append(curr_classification) + + return {"tools": self.all_tools, "classifications": self.all_classifications} + + +#made this just to test in my own project. not keeping this +def run(): + frontend = list(client.get_labeling_frontends(where=LabelingFrontend.name == "Editor"))[0] + project.setup(frontend, o.build()) + return project + +if __name__ == "__main__": + import json + os.system('clear') + apikey = os.environ['apikey'] + client = Client(apikey) + project = client.get_project("ckhchkye62xn30796uui5lu34") + + o = Ontology().from_project(project) + o.add_tool(tool=Tool.Type.SEGMENTATION, name="hello world") + + run() + + + + + +''' +TODO +work on enforcing certain classifications need options (and work on other things other than text) + +create an example of a classification with options + +work on nesting classes inside tools + +work on nesting classes inside other classes + +in the future: work on adding NER capability for tool types (?) + + +TODO: Questions for Florjian: +1. when taking in an ontology from an existing project, I converted 'schemaNodeId' to schema_id and 'featureSchemaId' to feature_schema_id. + then, when planning to move setup the project with the new ontology, I converted them back to the original id's. Is it better to do it + this way because it follows PEP8? or should I just change the naming conventino of the variables? + +2. I am a little confused on what is the best way to enforce certain classifications require a series of options. My last version basically had a list + of Classification.Type that if it fell into that list, it would require options before it could be created. What is the best way to do this? + +3. My ontology class is getting a little long because I am manually changing certain dict keys to another key (part of #1's problem). Is there a + better way to do dict key renaming? + +4. I was thinking instead of lines 79-81 and 85-87 could be converted to @classmethods instead of what I am doing now. Would that be better? +''' + From c053bf79d7eab10a3e4051e35ca4e2df644cbb7f Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Mon, 18 Jan 2021 18:32:49 -0800 Subject: [PATCH 02/36] updating the file so that it now properly pulls in Options when looking at classifications. Also now successfully adds options with the add_option() method --- labelbox/schema/ontology_generator.py | 172 +++++++++++++++----------- 1 file changed, 98 insertions(+), 74 deletions(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index 76743b211..a03b87a10 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -1,3 +1,24 @@ +''' +TODO +currently allows creation of a TEXT classification and options inside this file +↑ should not be possible, and in fact causes some unintended behavior when creating the ontology (creates options as a totally new classification) +↑ need to ensure that with TEXT we cannot embed options + +validate prior to submission, so likely in the o.build() - like have a way to make sure that classifications that need options have options + +work on enforcing certain classifications need options (and work on other things other than text) + +create an example of a classification with options + +work on nesting classes inside tools (and properly extrapolating the options when taking it from inside a project) + +work on nesting classes inside other classes + +maybe there should be a way to check if a project has an existing ontology, and that it would overwrite it? + +in the future: work on adding NER capability for tool types (?) +''' + from dataclasses import dataclass, field from enum import Enum, auto import os @@ -5,22 +26,32 @@ from labelbox import Client, Project, Dataset, LabelingFrontend + class InconsistentOntologyException(Exception): pass @dataclass -class Option(): +class Option: value: str - label: str = None schema_id: Optional[str] = None feature_schema_id: Optional[str] = None - def __post_init__(self): - self.label = self.value + @property + def label(self): + return self.value + + @classmethod + def option_class_to_dict(cls, option): + return { + "schemaNodeId": option.schema_id, + "featureSchemaId": option.feature_schema_id, + "label": option.value, + "value": option.value + } @dataclass -class Classification(): +class Classification: class Type(Enum): TEXT = "text" @@ -28,20 +59,25 @@ class Type(Enum): RADIO = "radio" DROPDOWN = "dropdown" - type: Type + class_type: Type instructions: str - name: str = None required: bool = False options: List[Option] = field(default_factory=list) schema_id: Optional[str] = None feature_schema_id: Optional[str] = None - def __post_init__(self): - self.name = self.instructions - + @property + def name(self): + return self.instructions + + def add_option(self, *args, **kwargs): + new_option = Option(*args, **kwargs) + if new_option.value in (option.value for option in self.options): + raise InconsistentOntologyException(f"Duplicate option '{new_option.value}' for classification '{self.name}'.") + self.options.append(new_option) @dataclass -class Tool(): +class Tool: class Type(Enum): POLYGON = "polygon" @@ -58,14 +94,8 @@ class Type(Enum): schema_id: Optional[str] = None feature_schema_id: Optional[str] = None - # @classmethod - # def _from_existing_ontology(cls, dict): - # for key,value in dict.items(): - # print(key,value) - - @dataclass -class Ontology(): +class Ontology: tools: List[Tool] = field(default_factory=list) classifications: List[Classification] = field(default_factory=list) @@ -75,17 +105,26 @@ def from_project(cls, project: Project): ontology = project.ontology().normalized return_ontology = Ontology() - for tool in ontology['tools']: - tool['schema_id'] = tool.pop('schemaNodeId') - tool['feature_schema_id'] = tool.pop('featureSchemaId') - tool['tool'] = Tool.Type(tool['tool']) - return_ontology.add_tool(**tool) - - for classification in ontology['classifications']: - classification['schema_id'] = classification.pop('schemaNodeId') - classification['feature_schema_id'] = classification.pop('featureSchemaId') - classification['type'] = Classification.Type(classification['type']) - return_ontology.add_classification(**classification) + for tool in ontology["tools"]: + return_ontology.add_tool( + name=tool['name'], + schema_id=tool["schemaNodeId"], + feature_schema_id=tool["featureSchemaId"], + required=tool["required"], + tool=Tool.Type(tool["tool"]), + classifications=tool["classifications"], + color=tool["color"], + ) + + for classification in ontology["classifications"]: + return_ontology.add_classification( + schema_id=classification["schemaNodeId"], + feature_schema_id=classification["schemaNodeId"], + required=classification["required"], + instructions=classification["instructions"], + class_type=Classification.Type(classification["type"]), + options = [Option(value=option["value"], schema_id=option["schemaNodeId"], feature_schema_id=option["featureSchemaId"]) for option in classification["options"] if len(classification["options"]) > 0] + ) return return_ontology @@ -104,24 +143,35 @@ def add_classification(self, *args, **kwargs): return new_classification def build(self): - self.all_tools = [] - self.all_classifications = [] + all_tools = [] + all_classifications = [] for tool in self.tools: - curr_tool = dict((key,value) for (key,value) in tool.__dict__.items()) - curr_tool['tool'] = curr_tool['tool'].value - curr_tool['schemaNodeId'] = curr_tool.pop('schema_id') - curr_tool['featureSchemaId'] = curr_tool.pop('feature_schema_id') - self.all_tools.append(curr_tool) + + all_tools.append({ + "tool": tool.tool.value, + "name": tool.name, + "required": tool.required, + "color": tool.color, + "classifications": tool.classifications, + "schemaNodeId": tool.schema_id, + "featureSchemaId": tool.feature_schema_id + + }) for classification in self.classifications: - curr_classification = dict((key,value) for (key,value) in classification.__dict__.items()) - curr_classification['type'] = curr_classification['type'].value - curr_classification['schemaNodeId'] = curr_classification.pop('schema_id') - curr_classification['featureSchemaId'] = curr_classification.pop('feature_schema_id') - self.all_classifications.append(curr_classification) + all_classifications.append({ + "type": classification.class_type.value, + "instructions": classification.instructions, + "name": classification.name, + "required": classification.required, + # "options": classification.options, + "options": [Option.option_class_to_dict(option) for option in classification.options], + "schemaNodeId": classification.schema_id, + "featureSchemaId": classification.feature_schema_id + }) - return {"tools": self.all_tools, "classifications": self.all_classifications} + return {"tools": all_tools, "classifications": all_classifications} #made this just to test in my own project. not keeping this @@ -138,38 +188,12 @@ def run(): project = client.get_project("ckhchkye62xn30796uui5lu34") o = Ontology().from_project(project) - o.add_tool(tool=Tool.Type.SEGMENTATION, name="hello world") - - run() - + # o.add_tool(tool=Tool.Type.POLYGON, name="i am a polygon tool") + checklist = o.add_classification(class_type=Classification.Type.CHECKLIST, instructions="I AM A CHECKLIST2") + checklist.add_option(value="checklist answer 1") + checklist.add_option(value="checklist answer 1") - -''' -TODO -work on enforcing certain classifications need options (and work on other things other than text) - -create an example of a classification with options - -work on nesting classes inside tools - -work on nesting classes inside other classes - -in the future: work on adding NER capability for tool types (?) - - -TODO: Questions for Florjian: -1. when taking in an ontology from an existing project, I converted 'schemaNodeId' to schema_id and 'featureSchemaId' to feature_schema_id. - then, when planning to move setup the project with the new ontology, I converted them back to the original id's. Is it better to do it - this way because it follows PEP8? or should I just change the naming conventino of the variables? - -2. I am a little confused on what is the best way to enforce certain classifications require a series of options. My last version basically had a list - of Classification.Type that if it fell into that list, it would require options before it could be created. What is the best way to do this? - -3. My ontology class is getting a little long because I am manually changing certain dict keys to another key (part of #1's problem). Is there a - better way to do dict key renaming? - -4. I was thinking instead of lines 79-81 and 85-87 could be converted to @classmethods instead of what I am doing now. Would that be better? -''' - + print(o.build()) + run() \ No newline at end of file From 94f0f7815d99dbaf2c88eef8625423429faa2b1a Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Mon, 18 Jan 2021 18:36:28 -0800 Subject: [PATCH 03/36] small edit --- labelbox/schema/ontology_generator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index a03b87a10..b7e1140be 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -75,6 +75,7 @@ def add_option(self, *args, **kwargs): if new_option.value in (option.value for option in self.options): raise InconsistentOntologyException(f"Duplicate option '{new_option.value}' for classification '{self.name}'.") self.options.append(new_option) + return new_option @dataclass class Tool: @@ -189,10 +190,10 @@ def run(): o = Ontology().from_project(project) - # o.add_tool(tool=Tool.Type.POLYGON, name="i am a polygon tool") - checklist = o.add_classification(class_type=Classification.Type.CHECKLIST, instructions="I AM A CHECKLIST2") - checklist.add_option(value="checklist answer 1") + o.add_tool(tool=Tool.Type.POLYGON, name="i am a polygon tool") + checklist = o.add_classification(class_type=Classification.Type.CHECKLIST, instructions="I AM A CHECKLIST") checklist.add_option(value="checklist answer 1") + checklist.add_option(value="checklist answer 2") print(o.build()) From a58f99cebdd3a795e8b70c35e29484a414f2bc3d Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Wed, 3 Feb 2021 16:33:32 -0800 Subject: [PATCH 04/36] updates to the file to have helper functions that smoothen the writeup --- labelbox/schema/ontology_generator.py | 149 ++++++++++++++++++-------- 1 file changed, 105 insertions(+), 44 deletions(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index b7e1140be..2fd2a2bf1 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -1,5 +1,10 @@ ''' TODO +1. make classification pulls recursive (go beyond just the 1st layer) -> but how to make classification->option->classification->infinitely? +2. validate that we can pull in all sorts of project ontology classes +3. validate we can submit basic ones back in +4. do the rest of the stuff below + currently allows creation of a TEXT classification and options inside this file ↑ should not be possible, and in fact causes some unintended behavior when creating the ontology (creates options as a totally new classification) ↑ need to ensure that with TEXT we cannot embed options @@ -8,15 +13,12 @@ work on enforcing certain classifications need options (and work on other things other than text) -create an example of a classification with options - -work on nesting classes inside tools (and properly extrapolating the options when taking it from inside a project) - -work on nesting classes inside other classes - maybe there should be a way to check if a project has an existing ontology, and that it would overwrite it? in the future: work on adding NER capability for tool types (?) + +EXTRA + #TODO: look into static methods and differences vs. class methods ''' from dataclasses import dataclass, field @@ -39,15 +41,22 @@ class Option: @property def label(self): return self.value - - @classmethod - def option_class_to_dict(cls, option): + + def to_dict(self) -> dict: return { - "schemaNodeId": option.schema_id, - "featureSchemaId": option.feature_schema_id, - "label": option.value, - "value": option.value + "schemaNodeId": self.schema_id, + "featureSchemaId": self.feature_schema_id, + "label": self.label, + "value": self.value } + + @classmethod + def from_dict(cls, dictionary: dict): + return Option( + value = dictionary["value"], + schema_id = dictionary['schemaNodeId'], + feature_schema_id = dictionary['featureSchemaId'] + ) @dataclass @@ -70,12 +79,33 @@ class Type(Enum): def name(self): return self.instructions + def to_dict(self) -> dict: + return { + "type": self.class_type.value, + "instructions": self.instructions, + "name": self.name, + "required": self.required, + "options": [option.to_dict() for option in classification.options], + "schemaNodeId": self.schema_id, + "featureSchemaId": self.feature_schema_id + } + + @classmethod + def from_dict(cls, dictionary: dict): + return Classification( + class_type = Classification.Type(dictionary["type"]), + instructions = dictionary["instructions"], + required = dictionary["required"], + options = [Option.from_dict(option) for option in dictionary["options"]], + schema_id = dictionary["schemaNodeId"], + feature_schema_id = dictionary["schemaNodeId"] + ) + def add_option(self, *args, **kwargs): new_option = Option(*args, **kwargs) if new_option.value in (option.value for option in self.options): raise InconsistentOntologyException(f"Duplicate option '{new_option.value}' for classification '{self.name}'.") self.options.append(new_option) - return new_option @dataclass class Tool: @@ -95,6 +125,31 @@ class Type(Enum): schema_id: Optional[str] = None feature_schema_id: Optional[str] = None + def to_dict(self) -> dict: + return { + "tool": self.tool.value, + "name": self.name, + "required": self.required, + "color": self.color, + "classifications": self.classifications, + "schemaNodeId": self.schema_id, + "featureSchemaId": self.feature_schema_id + } + + @classmethod + def from_dict(cls, dictionary: dict): + return Tool( + name = dictionary['name'], + schema_id = dictionary["schemaNodeId"], + feature_schema_id = dictionary["featureSchemaId"], + required = dictionary["required"], + tool = Tool.Type(dictionary["tool"]), + classifications = [Classification.from_dict(classification) for classification in dictionary["classifications"]], + color = dictionary["color"] + ) + + + @dataclass class Ontology: @@ -106,38 +161,23 @@ def from_project(cls, project: Project): ontology = project.ontology().normalized return_ontology = Ontology() - for tool in ontology["tools"]: - return_ontology.add_tool( - name=tool['name'], - schema_id=tool["schemaNodeId"], - feature_schema_id=tool["featureSchemaId"], - required=tool["required"], - tool=Tool.Type(tool["tool"]), - classifications=tool["classifications"], - color=tool["color"], - ) + for tool in ontology["tools"]: + return_ontology.add_tool(tool) for classification in ontology["classifications"]: - return_ontology.add_classification( - schema_id=classification["schemaNodeId"], - feature_schema_id=classification["schemaNodeId"], - required=classification["required"], - instructions=classification["instructions"], - class_type=Classification.Type(classification["type"]), - options = [Option(value=option["value"], schema_id=option["schemaNodeId"], feature_schema_id=option["featureSchemaId"]) for option in classification["options"] if len(classification["options"]) > 0] - ) + return_ontology.add_classification(classification) return return_ontology - def add_tool(self, *args, **kwargs): - new_tool = Tool(*args, **kwargs) + def add_tool(self, tool: dict) -> Tool: + new_tool = Tool.from_dict(tool) if new_tool.name in (tool.name for tool in self.tools): raise InconsistentOntologyException(f"Duplicate tool name '{new_tool.name}'. ") self.tools.append(new_tool) return new_tool - def add_classification(self, *args, **kwargs): - new_classification = Classification(*args, **kwargs) + def add_classification(self, classification: dict) -> Classification: + new_classification = Classification.from_dict(classification) if new_classification.instructions in (classification.instructions for classification in self.classifications): raise InconsistentOntologyException(f"Duplicate classifications instructions '{new_classification.instructions}'. ") self.classifications.append(new_classification) @@ -166,8 +206,7 @@ def build(self): "instructions": classification.instructions, "name": classification.name, "required": classification.required, - # "options": classification.options, - "options": [Option.option_class_to_dict(option) for option in classification.options], + "options": [option.to_dict() for option in classification.options], "schemaNodeId": classification.schema_id, "featureSchemaId": classification.feature_schema_id }) @@ -180,6 +219,17 @@ def run(): frontend = list(client.get_labeling_frontends(where=LabelingFrontend.name == "Editor"))[0] project.setup(frontend, o.build()) return project +#also filler right here for now for testing +def print_stuff(): + tools = o.tools + classifications = o.classifications + + print("tools\n") + for tool in tools: + print(tool) + print("\nclassifications\n") + for classification in classifications: + print(classification) if __name__ == "__main__": import json @@ -189,12 +239,23 @@ def run(): project = client.get_project("ckhchkye62xn30796uui5lu34") o = Ontology().from_project(project) + print_stuff() + - o.add_tool(tool=Tool.Type.POLYGON, name="i am a polygon tool") - checklist = o.add_classification(class_type=Classification.Type.CHECKLIST, instructions="I AM A CHECKLIST") - checklist.add_option(value="checklist answer 1") - checklist.add_option(value="checklist answer 2") + # o.add_tool(tool=Tool.Type.POLYGON, name="i am a polygon tool2") + # checklist = o.add_classification(class_type=Classification.Type.CHECKLIST, instructions="I AM A CHECKLIST2") + # checklist.add_option(value="checklist answer 1") + # checklist.add_option(value="checklist answer 2") + + + # print(o.build()) + # print(type(o.build())) + # print(o.build()) + # run() + + + + + - print(o.build()) - run() \ No newline at end of file From 3ce0fc59b33455afa009435ac65f84c8301801f7 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Wed, 3 Feb 2021 16:34:37 -0800 Subject: [PATCH 05/36] updates to the file to have helper functions that smoothen the writeup --- labelbox/schema/ontology_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index 2fd2a2bf1..fae6c56ee 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -2,7 +2,7 @@ TODO 1. make classification pulls recursive (go beyond just the 1st layer) -> but how to make classification->option->classification->infinitely? 2. validate that we can pull in all sorts of project ontology classes -3. validate we can submit basic ones back in +3. validate we can submit basic ones back in (fix build() method) 4. do the rest of the stuff below currently allows creation of a TEXT classification and options inside this file From e0b3d8e52ab394d9277d2e7518186a45af5e0aee Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Tue, 9 Feb 2021 16:43:41 -0800 Subject: [PATCH 06/36] able to now get and set basic ontologies --- labelbox/schema/ontology_generator.py | 65 ++++++++++++++++----------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index fae6c56ee..1cf4b2514 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -1,6 +1,5 @@ ''' TODO -1. make classification pulls recursive (go beyond just the 1st layer) -> but how to make classification->option->classification->infinitely? 2. validate that we can pull in all sorts of project ontology classes 3. validate we can submit basic ones back in (fix build() method) 4. do the rest of the stuff below @@ -32,11 +31,16 @@ class InconsistentOntologyException(Exception): pass +class Classification: + pass + @dataclass class Option: value: str schema_id: Optional[str] = None feature_schema_id: Optional[str] = None + #TODO: need to look further into how to make this so that the user cannot input anything here + options: Optional[Classification] = None @property def label(self): @@ -47,15 +51,22 @@ def to_dict(self) -> dict: "schemaNodeId": self.schema_id, "featureSchemaId": self.feature_schema_id, "label": self.label, - "value": self.value + "value": self.value, + "options": [classification.to_dict() for classification in self.options] } @classmethod def from_dict(cls, dictionary: dict): + def has_nested_classifications(dictionary: dict): + if "options" in dictionary.keys(): + return [Classification.from_dict(nested_class) for nested_class in dictionary["options"]] + return list() + return Option( value = dictionary["value"], - schema_id = dictionary['schemaNodeId'], - feature_schema_id = dictionary['featureSchemaId'] + schema_id = dictionary["schemaNodeId"], + feature_schema_id = dictionary["featureSchemaId"], + options = has_nested_classifications(dictionary) ) @@ -81,13 +92,13 @@ def name(self): def to_dict(self) -> dict: return { - "type": self.class_type.value, - "instructions": self.instructions, - "name": self.name, - "required": self.required, - "options": [option.to_dict() for option in classification.options], - "schemaNodeId": self.schema_id, - "featureSchemaId": self.feature_schema_id + "type": self.class_type.value, + "instructions": self.instructions, + "name": self.name, + "required": self.required, + "options": [option.to_dict() for option in self.options], + "schemaNodeId": self.schema_id, + "featureSchemaId": self.feature_schema_id } @classmethod @@ -162,22 +173,22 @@ def from_project(cls, project: Project): return_ontology = Ontology() for tool in ontology["tools"]: - return_ontology.add_tool(tool) + return_ontology.tools.append(Tool.from_dict(tool)) for classification in ontology["classifications"]: - return_ontology.add_classification(classification) + return_ontology.classifications.append(Classification.from_dict(classification)) return return_ontology - def add_tool(self, tool: dict) -> Tool: - new_tool = Tool.from_dict(tool) + def add_tool(self, *args, **kwargs) -> Tool: + new_tool = Tool(*args, **kwargs) if new_tool.name in (tool.name for tool in self.tools): raise InconsistentOntologyException(f"Duplicate tool name '{new_tool.name}'. ") self.tools.append(new_tool) return new_tool - def add_classification(self, classification: dict) -> Classification: - new_classification = Classification.from_dict(classification) + def add_classification(self, *args, **kwargs) -> Classification: + new_classification = Classification(*args, **kwargs) if new_classification.instructions in (classification.instructions for classification in self.classifications): raise InconsistentOntologyException(f"Duplicate classifications instructions '{new_classification.instructions}'. ") self.classifications.append(new_classification) @@ -194,7 +205,7 @@ def build(self): "name": tool.name, "required": tool.required, "color": tool.color, - "classifications": tool.classifications, + "classifications": [classification.to_dict() for classification in tool.classifications], "schemaNodeId": tool.schema_id, "featureSchemaId": tool.feature_schema_id @@ -213,23 +224,24 @@ def build(self): return {"tools": all_tools, "classifications": all_classifications} - -#made this just to test in my own project. not keeping this +''' +EVERYTHING BELOW THIS LINE IS JUST FOR SELF TESTING +''' def run(): frontend = list(client.get_labeling_frontends(where=LabelingFrontend.name == "Editor"))[0] project.setup(frontend, o.build()) return project -#also filler right here for now for testing + def print_stuff(): tools = o.tools classifications = o.classifications print("tools\n") for tool in tools: - print(tool) + print("\n",tool) print("\nclassifications\n") for classification in classifications: - print(classification) + print("\n",classification) if __name__ == "__main__": import json @@ -239,19 +251,20 @@ def print_stuff(): project = client.get_project("ckhchkye62xn30796uui5lu34") o = Ontology().from_project(project) - print_stuff() + # print_stuff() - # o.add_tool(tool=Tool.Type.POLYGON, name="i am a polygon tool2") + o.add_tool(tool=Tool.Type.POLYGON, name="I AM HERE FOR TESTING") # checklist = o.add_classification(class_type=Classification.Type.CHECKLIST, instructions="I AM A CHECKLIST2") # checklist.add_option(value="checklist answer 1") # checklist.add_option(value="checklist answer 2") + # print_stuff() # print(o.build()) # print(type(o.build())) # print(o.build()) - # run() + run() From deec7f2bc78e7fb6bd1dfb2d42b0d7762aabd8fa Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Tue, 9 Feb 2021 16:44:30 -0800 Subject: [PATCH 07/36] able to now get and set basic ontologies --- labelbox/schema/ontology_generator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index 1cf4b2514..ac224a7e6 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -1,8 +1,7 @@ ''' TODO 2. validate that we can pull in all sorts of project ontology classes -3. validate we can submit basic ones back in (fix build() method) -4. do the rest of the stuff below +3. do the rest of the stuff below currently allows creation of a TEXT classification and options inside this file ↑ should not be possible, and in fact causes some unintended behavior when creating the ontology (creates options as a totally new classification) From e2740aff3493b4ec0449f73603bb545dd621722d Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Tue, 9 Feb 2021 16:59:35 -0800 Subject: [PATCH 08/36] able to now get and set basic ontologies --- labelbox/schema/ontology_generator.py | 29 ++++++--------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index ac224a7e6..459cf2570 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -141,7 +141,7 @@ def to_dict(self) -> dict: "name": self.name, "required": self.required, "color": self.color, - "classifications": self.classifications, + "classifications": [classification.to_dict() for classification in self.classifications], "schemaNodeId": self.schema_id, "featureSchemaId": self.feature_schema_id } @@ -198,28 +198,10 @@ def build(self): all_classifications = [] for tool in self.tools: - - all_tools.append({ - "tool": tool.tool.value, - "name": tool.name, - "required": tool.required, - "color": tool.color, - "classifications": [classification.to_dict() for classification in tool.classifications], - "schemaNodeId": tool.schema_id, - "featureSchemaId": tool.feature_schema_id - - }) + all_tools.append(tool.to_dict()) for classification in self.classifications: - all_classifications.append({ - "type": classification.class_type.value, - "instructions": classification.instructions, - "name": classification.name, - "required": classification.required, - "options": [option.to_dict() for option in classification.options], - "schemaNodeId": classification.schema_id, - "featureSchemaId": classification.feature_schema_id - }) + all_classifications.append(classification.to_dict()) return {"tools": all_tools, "classifications": all_classifications} @@ -253,13 +235,14 @@ def print_stuff(): # print_stuff() - o.add_tool(tool=Tool.Type.POLYGON, name="I AM HERE FOR TESTING") + # o.add_tool(tool=Tool.Type.POLYGON, name="I AM HERE FOR TESTING YET AGAIN!!") # checklist = o.add_classification(class_type=Classification.Type.CHECKLIST, instructions="I AM A CHECKLIST2") # checklist.add_option(value="checklist answer 1") # checklist.add_option(value="checklist answer 2") - + o.add_classification(class_type=Classification.Type.TEXT, instructions="I AM TEXT INFO MAN 2") # print_stuff() + # print("\n\n\n\n\n") # print(o.build()) # print(type(o.build())) # print(o.build()) From 5376a1e28fd65b08e76037ddbc5c1a8b4ace36c7 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Tue, 9 Feb 2021 17:04:00 -0800 Subject: [PATCH 09/36] able to now get and set basic ontologies --- labelbox/schema/ontology_generator.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index 459cf2570..a9e4cd76c 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -3,10 +3,6 @@ 2. validate that we can pull in all sorts of project ontology classes 3. do the rest of the stuff below -currently allows creation of a TEXT classification and options inside this file -↑ should not be possible, and in fact causes some unintended behavior when creating the ontology (creates options as a totally new classification) -↑ need to ensure that with TEXT we cannot embed options - validate prior to submission, so likely in the o.build() - like have a way to make sure that classifications that need options have options work on enforcing certain classifications need options (and work on other things other than text) From 9cc29ef8b7cb3ff0050f2e4e7e528ff1767c2e7a Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Wed, 10 Feb 2021 16:07:50 -0800 Subject: [PATCH 10/36] added the ability to strip schema ids and feature schema ids for when you want to inherit an ontology from a different project. also updated options to return an object class and insert nested_classes to Tool and Option --- labelbox/schema/ontology_generator.py | 104 +++++++++++++------------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index a9e4cd76c..6302f8b96 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -1,18 +1,8 @@ ''' TODO -2. validate that we can pull in all sorts of project ontology classes -3. do the rest of the stuff below - -validate prior to submission, so likely in the o.build() - like have a way to make sure that classifications that need options have options - -work on enforcing certain classifications need options (and work on other things other than text) - maybe there should be a way to check if a project has an existing ontology, and that it would overwrite it? in the future: work on adding NER capability for tool types (?) - -EXTRA - #TODO: look into static methods and differences vs. class methods ''' from dataclasses import dataclass, field @@ -34,20 +24,19 @@ class Option: value: str schema_id: Optional[str] = None feature_schema_id: Optional[str] = None - #TODO: need to look further into how to make this so that the user cannot input anything here - options: Optional[Classification] = None + nested_classes: List[Classification] = field(default_factory=list) @property def label(self): return self.value - def to_dict(self) -> dict: + def to_dict(self,for_different_project=False) -> dict: return { - "schemaNodeId": self.schema_id, - "featureSchemaId": self.feature_schema_id, + "schemaNodeId": None if for_different_project else self.schema_id, + "featureSchemaId": None if for_different_project else self.feature_schema_id, "label": self.label, "value": self.value, - "options": [classification.to_dict() for classification in self.options] + "options": [classification.to_dict() for classification in self.nested_classes] } @classmethod @@ -61,12 +50,19 @@ def has_nested_classifications(dictionary: dict): value = dictionary["value"], schema_id = dictionary["schemaNodeId"], feature_schema_id = dictionary["featureSchemaId"], - options = has_nested_classifications(dictionary) + nested_classes = has_nested_classifications(dictionary) ) + + def add_nested_class(self, *args, **kwargs): + new_classification = Classification(*args, **kwargs) + if new_classification.instructions in (classification.instructions for classification in self.nested_classes): + raise InconsistentOntologyException(f"Duplicate nested classification '{new_classification.instructions}' for option '{self.label}'") + self.nested_classes.append(new_classification) + return new_classification @dataclass -class Classification: +class Classification: class Type(Enum): TEXT = "text" @@ -81,19 +77,25 @@ class Type(Enum): schema_id: Optional[str] = None feature_schema_id: Optional[str] = None + @property + def requires_options(self): + return set((Classification.Type.CHECKLIST, Classification.Type.RADIO, Classification.Type.DROPDOWN)) + @property def name(self): return self.instructions - def to_dict(self) -> dict: + def to_dict(self, for_different_project=False) -> dict: + if self.class_type in self.requires_options and len(self.options) < 1: + raise InconsistentOntologyException(f"Classification '{self.instructions}' requires options.") return { "type": self.class_type.value, "instructions": self.instructions, "name": self.name, "required": self.required, - "options": [option.to_dict() for option in self.options], - "schemaNodeId": self.schema_id, - "featureSchemaId": self.feature_schema_id + "options": [option.to_dict(for_different_project) for option in self.options], + "schemaNodeId": None if for_different_project else self.schema_id, + "featureSchemaId": None if for_different_project else self.feature_schema_id } @classmethod @@ -112,6 +114,7 @@ def add_option(self, *args, **kwargs): if new_option.value in (option.value for option in self.options): raise InconsistentOntologyException(f"Duplicate option '{new_option.value}' for classification '{self.name}'.") self.options.append(new_option) + return new_option @dataclass class Tool: @@ -131,15 +134,15 @@ class Type(Enum): schema_id: Optional[str] = None feature_schema_id: Optional[str] = None - def to_dict(self) -> dict: + def to_dict(self,for_different_project=False) -> dict: return { "tool": self.tool.value, "name": self.name, "required": self.required, "color": self.color, - "classifications": [classification.to_dict() for classification in self.classifications], - "schemaNodeId": self.schema_id, - "featureSchemaId": self.feature_schema_id + "classifications": [classification.to_dict(for_different_project) for classification in self.classifications], + "schemaNodeId": None if for_different_project else self.schema_id, + "featureSchemaId": None if for_different_project else self.feature_schema_id } @classmethod @@ -154,6 +157,13 @@ def from_dict(cls, dictionary: dict): color = dictionary["color"] ) + def add_nested_class(self, *args, **kwargs): + new_classification = Classification(*args, **kwargs) + if new_classification.instructions in (classification.instructions for classification in self.classifications): + raise InconsistentOntologyException(f"Duplicate nested classification '{new_classification.instructions}' for option '{self.label}'") + self.classifications.append(new_classification) + return new_classification + @dataclass @@ -164,6 +174,8 @@ class Ontology: @classmethod def from_project(cls, project: Project): + #TODO: consider if this should take in a Project object, or the project.uid. + #if we take in project.uid, we need to then get the project from a client object. ontology = project.ontology().normalized return_ontology = Ontology() @@ -189,15 +201,15 @@ def add_classification(self, *args, **kwargs) -> Classification: self.classifications.append(new_classification) return new_classification - def build(self): + def build(self, for_different_project=False): all_tools = [] all_classifications = [] for tool in self.tools: - all_tools.append(tool.to_dict()) + all_tools.append(tool.to_dict(for_different_project)) - for classification in self.classifications: - all_classifications.append(classification.to_dict()) + for classification in self.classifications: + all_classifications.append(classification.to_dict(for_different_project)) return {"tools": all_tools, "classifications": all_classifications} @@ -206,8 +218,7 @@ def build(self): ''' def run(): frontend = list(client.get_labeling_frontends(where=LabelingFrontend.name == "Editor"))[0] - project.setup(frontend, o.build()) - return project + project.setup(frontend, o.build(for_different_project=False)) def print_stuff(): tools = o.tools @@ -221,30 +232,21 @@ def print_stuff(): print("\n",classification) if __name__ == "__main__": - import json os.system('clear') - apikey = os.environ['apikey'] - client = Client(apikey) - project = client.get_project("ckhchkye62xn30796uui5lu34") + # apikey = os.environ['apikey'] + # client = Client(apikey) - o = Ontology().from_project(project) - # print_stuff() - + # print("START\n") + # project = client.get_project("ckhchkye62xn30796uui5lu34") + # o = Ontology().from_project(project) - # o.add_tool(tool=Tool.Type.POLYGON, name="I AM HERE FOR TESTING YET AGAIN!!") - # checklist = o.add_classification(class_type=Classification.Type.CHECKLIST, instructions="I AM A CHECKLIST2") - # checklist.add_option(value="checklist answer 1") - # checklist.add_option(value="checklist answer 2") - o.add_classification(class_type=Classification.Type.TEXT, instructions="I AM TEXT INFO MAN 2") + # tool = o.add_tool(tool = Tool.Type.POINT, name = "first tool") + # nested_class = tool.add_nested_class(class_type = Classification.Type.DROPDOWN, instructions = "nested class") + # dropdown_option = nested_class.add_option(value="answer") # print_stuff() - # print("\n\n\n\n\n") - # print(o.build()) - # print(type(o.build())) - # print(o.build()) - run() - - + # o.build() + # run() From 021d11b7a2c8a7bec26f8ad008207f8f05046579 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Fri, 12 Feb 2021 13:00:36 -0800 Subject: [PATCH 11/36] added NER tool --- labelbox/schema/ontology_generator.py | 40 +++++++++++++++------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index 6302f8b96..2e801c834 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -36,7 +36,7 @@ def to_dict(self,for_different_project=False) -> dict: "featureSchemaId": None if for_different_project else self.feature_schema_id, "label": self.label, "value": self.value, - "options": [classification.to_dict() for classification in self.nested_classes] + "options": [classification.to_dict(for_different_project) for classification in self.nested_classes] } @classmethod @@ -60,7 +60,6 @@ def add_nested_class(self, *args, **kwargs): self.nested_classes.append(new_classification) return new_classification - @dataclass class Classification: @@ -125,6 +124,7 @@ class Type(Enum): POINT = "point" BBOX = "rectangle" LINE = "line" + NER = "named-entity" tool: Type name: str @@ -164,8 +164,6 @@ def add_nested_class(self, *args, **kwargs): self.classifications.append(new_classification) return new_classification - - @dataclass class Ontology: @@ -216,9 +214,6 @@ def build(self, for_different_project=False): ''' EVERYTHING BELOW THIS LINE IS JUST FOR SELF TESTING ''' -def run(): - frontend = list(client.get_labeling_frontends(where=LabelingFrontend.name == "Editor"))[0] - project.setup(frontend, o.build(for_different_project=False)) def print_stuff(): tools = o.tools @@ -233,20 +228,29 @@ def print_stuff(): if __name__ == "__main__": os.system('clear') - # apikey = os.environ['apikey'] - # client = Client(apikey) + apikey = os.environ['apikey'] + client = Client(apikey) + project = client.get_project("ckhchkye62xn30796uui5lu34") + + # apikey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJja2szYTRhemRoN2x4MDcyNmU2MjFkamFlIiwib3JnYW5pemF0aW9uSWQiOiJja2szYTRheXlwdXlnMDczNjdzamZmanU5IiwiYXBpS2V5SWQiOiJja2wwNDJzMXFraGw0MDcwNnZmNTNodTNsIiwiaWF0IjoxNjEzMDAyNTU5LCJleHAiOjIyNDQxNTQ1NTl9.ViguRPw-Zv5KIs0Ho5VOjARUJtc5dTcQFFl4zGbBdbM' + # client = Client(apikey) + # project = client.get_project("ckkyi8ih56sc207570tb35of1") + + o = Ontology().from_project(project) - # print("START\n") - # project = client.get_project("ckhchkye62xn30796uui5lu34") - # o = Ontology().from_project(project) - # tool = o.add_tool(tool = Tool.Type.POINT, name = "first tool") - # nested_class = tool.add_nested_class(class_type = Classification.Type.DROPDOWN, instructions = "nested class") - # dropdown_option = nested_class.add_option(value="answer") + #create a Point tool and add a nested dropdown in it + tool = o.add_tool(tool = Tool.Type.POINT, name = "Example Point Tool") + nested_class = tool.add_nested_class(class_type = Classification.Type.DROPDOWN, instructions = "nested class") + dropdown_option = nested_class.add_option(value="answer") + + #to old existing project + frontend = list(client.get_labeling_frontends(where=LabelingFrontend.name == "Editor"))[0] + project.setup(frontend, o.build(for_different_project=False)) - # print_stuff() - # o.build() - # run() + #to a different project + other_project = client.get_project("ckkzzw5qk1yje0712uqjn0oqs") + other_project.setup(frontend, o.build(for_different_project=True)) From c910fade04f1e1c5d25527d672299c9d1eca71ce Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Fri, 12 Feb 2021 13:33:32 -0800 Subject: [PATCH 12/36] added NER tool --- labelbox/schema/ontology_generator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index 2e801c834..f9127b2cc 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -1,8 +1,6 @@ ''' TODO maybe there should be a way to check if a project has an existing ontology, and that it would overwrite it? - -in the future: work on adding NER capability for tool types (?) ''' from dataclasses import dataclass, field @@ -244,13 +242,15 @@ def print_stuff(): nested_class = tool.add_nested_class(class_type = Classification.Type.DROPDOWN, instructions = "nested class") dropdown_option = nested_class.add_option(value="answer") + tool = o.add_tool(tool = Tool.Type.NER, name="NER value") + #to old existing project frontend = list(client.get_labeling_frontends(where=LabelingFrontend.name == "Editor"))[0] project.setup(frontend, o.build(for_different_project=False)) #to a different project - other_project = client.get_project("ckkzzw5qk1yje0712uqjn0oqs") - other_project.setup(frontend, o.build(for_different_project=True)) + # other_project = client.get_project("ckkzzw5qk1yje0712uqjn0oqs") + # other_project.setup(frontend, o.build(for_different_project=True)) From aed5a989a94d9cf19f3e5c4de4f8e60cb42e854a Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Fri, 12 Feb 2021 16:05:54 -0800 Subject: [PATCH 13/36] made minor adjustments --- labelbox/schema/ontology_generator.py | 34 +++++++++++++-------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index f9127b2cc..91fa73ebf 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -6,7 +6,7 @@ from dataclasses import dataclass, field from enum import Enum, auto import os -from typing import List, Optional +from typing import List, Optional, Dict from labelbox import Client, Project, Dataset, LabelingFrontend @@ -28,7 +28,7 @@ class Option: def label(self): return self.value - def to_dict(self,for_different_project=False) -> dict: + def to_dict(self,for_different_project=False) -> Dict[str, str]: return { "schemaNodeId": None if for_different_project else self.schema_id, "featureSchemaId": None if for_different_project else self.feature_schema_id, @@ -38,11 +38,9 @@ def to_dict(self,for_different_project=False) -> dict: } @classmethod - def from_dict(cls, dictionary: dict): - def has_nested_classifications(dictionary: dict): - if "options" in dictionary.keys(): - return [Classification.from_dict(nested_class) for nested_class in dictionary["options"]] - return list() + def from_dict(cls, dictionary: Dict[str,str]): + def has_nested_classifications(dictionary: Dict[str,str]): + return [Classification.from_dict(nested_class) for nested_class in dictionary.get("options", [])] return Option( value = dictionary["value"], @@ -74,16 +72,16 @@ class Type(Enum): schema_id: Optional[str] = None feature_schema_id: Optional[str] = None - @property - def requires_options(self): + @staticmethod + def requires_options(): return set((Classification.Type.CHECKLIST, Classification.Type.RADIO, Classification.Type.DROPDOWN)) @property def name(self): return self.instructions - def to_dict(self, for_different_project=False) -> dict: - if self.class_type in self.requires_options and len(self.options) < 1: + def to_dict(self, for_different_project=False) -> Dict[str,str]: + if self.class_type in Classification.requires_options() and len(self.options) < 1: raise InconsistentOntologyException(f"Classification '{self.instructions}' requires options.") return { "type": self.class_type.value, @@ -96,7 +94,7 @@ def to_dict(self, for_different_project=False) -> dict: } @classmethod - def from_dict(cls, dictionary: dict): + def from_dict(cls, dictionary: Dict[str,str]): return Classification( class_type = Classification.Type(dictionary["type"]), instructions = dictionary["instructions"], @@ -132,7 +130,7 @@ class Type(Enum): schema_id: Optional[str] = None feature_schema_id: Optional[str] = None - def to_dict(self,for_different_project=False) -> dict: + def to_dict(self,for_different_project=False) -> Dict[str,str]: return { "tool": self.tool.value, "name": self.name, @@ -144,7 +142,7 @@ def to_dict(self,for_different_project=False) -> dict: } @classmethod - def from_dict(cls, dictionary: dict): + def from_dict(cls, dictionary: Dict[str,str]): return Tool( name = dictionary['name'], schema_id = dictionary["schemaNodeId"], @@ -238,11 +236,11 @@ def print_stuff(): #create a Point tool and add a nested dropdown in it - tool = o.add_tool(tool = Tool.Type.POINT, name = "Example Point Tool") - nested_class = tool.add_nested_class(class_type = Classification.Type.DROPDOWN, instructions = "nested class") - dropdown_option = nested_class.add_option(value="answer") + # tool = o.add_tool(tool = Tool.Type.POINT, name = "Example Point Tool") + # nested_class = tool.add_nested_class(class_type = Classification.Type.DROPDOWN, instructions = "nested class") + # dropdown_option = nested_class.add_option(value="answer") - tool = o.add_tool(tool = Tool.Type.NER, name="NER value") + # tool = o.add_tool(tool = Tool.Type.NER, name="NER value") #to old existing project frontend = list(client.get_labeling_frontends(where=LabelingFrontend.name == "Editor"))[0] From e44d6728e0d30351f983f76851941c33fe45ea1e Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Fri, 12 Feb 2021 16:35:54 -0800 Subject: [PATCH 14/36] made minor adjustments --- labelbox/schema/ontology_generator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index 91fa73ebf..f0862cafb 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -227,8 +227,7 @@ def print_stuff(): apikey = os.environ['apikey'] client = Client(apikey) project = client.get_project("ckhchkye62xn30796uui5lu34") - - # apikey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJja2szYTRhemRoN2x4MDcyNmU2MjFkamFlIiwib3JnYW5pemF0aW9uSWQiOiJja2szYTRheXlwdXlnMDczNjdzamZmanU5IiwiYXBpS2V5SWQiOiJja2wwNDJzMXFraGw0MDcwNnZmNTNodTNsIiwiaWF0IjoxNjEzMDAyNTU5LCJleHAiOjIyNDQxNTQ1NTl9.ViguRPw-Zv5KIs0Ho5VOjARUJtc5dTcQFFl4zGbBdbM' + # client = Client(apikey) # project = client.get_project("ckkyi8ih56sc207570tb35of1") From 0ecb479d0c07a57dcfefe5867b477b5907d56200 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Fri, 12 Feb 2021 16:38:25 -0800 Subject: [PATCH 15/36] cleanup --- labelbox/schema/ontology_generator.py | 51 ++++----------------------- 1 file changed, 6 insertions(+), 45 deletions(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index f0862cafb..167b784d6 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -22,7 +22,7 @@ class Option: value: str schema_id: Optional[str] = None feature_schema_id: Optional[str] = None - nested_classes: List[Classification] = field(default_factory=list) + options: List[Classification] = field(default_factory=list) @property def label(self): @@ -34,7 +34,7 @@ def to_dict(self,for_different_project=False) -> Dict[str, str]: "featureSchemaId": None if for_different_project else self.feature_schema_id, "label": self.label, "value": self.value, - "options": [classification.to_dict(for_different_project) for classification in self.nested_classes] + "options": [classification.to_dict(for_different_project) for classification in self.options] } @classmethod @@ -46,14 +46,14 @@ def has_nested_classifications(dictionary: Dict[str,str]): value = dictionary["value"], schema_id = dictionary["schemaNodeId"], feature_schema_id = dictionary["featureSchemaId"], - nested_classes = has_nested_classifications(dictionary) + options = has_nested_classifications(dictionary) ) def add_nested_class(self, *args, **kwargs): new_classification = Classification(*args, **kwargs) - if new_classification.instructions in (classification.instructions for classification in self.nested_classes): + if new_classification.instructions in (classification.instructions for classification in self.options): raise InconsistentOntologyException(f"Duplicate nested classification '{new_classification.instructions}' for option '{self.label}'") - self.nested_classes.append(new_classification) + self.options.append(new_classification) return new_classification @dataclass @@ -207,47 +207,8 @@ def build(self, for_different_project=False): return {"tools": all_tools, "classifications": all_classifications} -''' -EVERYTHING BELOW THIS LINE IS JUST FOR SELF TESTING -''' - -def print_stuff(): - tools = o.tools - classifications = o.classifications - - print("tools\n") - for tool in tools: - print("\n",tool) - print("\nclassifications\n") - for classification in classifications: - print("\n",classification) - if __name__ == "__main__": - os.system('clear') - apikey = os.environ['apikey'] - client = Client(apikey) - project = client.get_project("ckhchkye62xn30796uui5lu34") - - # client = Client(apikey) - # project = client.get_project("ckkyi8ih56sc207570tb35of1") - - o = Ontology().from_project(project) - - - #create a Point tool and add a nested dropdown in it - # tool = o.add_tool(tool = Tool.Type.POINT, name = "Example Point Tool") - # nested_class = tool.add_nested_class(class_type = Classification.Type.DROPDOWN, instructions = "nested class") - # dropdown_option = nested_class.add_option(value="answer") - - # tool = o.add_tool(tool = Tool.Type.NER, name="NER value") - - #to old existing project - frontend = list(client.get_labeling_frontends(where=LabelingFrontend.name == "Editor"))[0] - project.setup(frontend, o.build(for_different_project=False)) - - #to a different project - # other_project = client.get_project("ckkzzw5qk1yje0712uqjn0oqs") - # other_project.setup(frontend, o.build(for_different_project=True)) + pass From 7cda334fdaf236bfbd1a109fb31306086ddc3f78 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Mon, 15 Feb 2021 16:27:52 -0800 Subject: [PATCH 16/36] updates to make the code more consistent and cleaner --- labelbox/schema/ontology_generator.py | 92 +++++++++++++-------------- 1 file changed, 43 insertions(+), 49 deletions(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index 167b784d6..1eda198aa 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -1,6 +1,15 @@ ''' TODO -maybe there should be a way to check if a project has an existing ontology, and that it would overwrite it? +Option.add_option() currently creates a new Classification object. however, this does not work for certain Classification options. + Example: + Classification.Type.DROPDOWN -> the options for this class_type should only generate more nested dropdowns + -> Dropdowns are supposed to be removed moving forward, but this is a current problem + -> This is the most major issue because going to the doubly nested class will break the UI + Classification.Type.CHECKLIST & Classification.Type.TEXT-> the option cannot have a nested Classification. + -> this reflects accurately in the UI without issues, but when you query via graphql, it shows what was input + -> this is a lesser issue because the UI will not reflect the unavailable fields + Is there an effective way to enforce limitations on Option.add_option()? + -> Maybe a way to check if the Option itself has options when adding it to a Classification? ''' from dataclasses import dataclass, field @@ -14,47 +23,41 @@ class InconsistentOntologyException(Exception): pass -class Classification: - pass - @dataclass class Option: value: str schema_id: Optional[str] = None feature_schema_id: Optional[str] = None - options: List[Classification] = field(default_factory=list) + options: List["Classification"] = field(default_factory=list) @property def label(self): return self.value - def to_dict(self,for_different_project=False) -> Dict[str, str]: + def asdict(self) -> Dict[str, str]: return { - "schemaNodeId": None if for_different_project else self.schema_id, - "featureSchemaId": None if for_different_project else self.feature_schema_id, + "schemaNodeId": self.schema_id, + "featureSchemaId": self.feature_schema_id, "label": self.label, "value": self.value, - "options": [classification.to_dict(for_different_project) for classification in self.options] + "options": [c.asdict() for c in self.options] } @classmethod def from_dict(cls, dictionary: Dict[str,str]): - def has_nested_classifications(dictionary: Dict[str,str]): - return [Classification.from_dict(nested_class) for nested_class in dictionary.get("options", [])] - return Option( value = dictionary["value"], schema_id = dictionary["schemaNodeId"], feature_schema_id = dictionary["featureSchemaId"], - options = has_nested_classifications(dictionary) + options = [Classification.from_dict(nested_class) for nested_class in dictionary.get("options", [])] ) - def add_nested_class(self, *args, **kwargs): - new_classification = Classification(*args, **kwargs) - if new_classification.instructions in (classification.instructions for classification in self.options): - raise InconsistentOntologyException(f"Duplicate nested classification '{new_classification.instructions}' for option '{self.label}'") - self.options.append(new_classification) - return new_classification + def add_option(self, *args, **kwargs): + new_option = Classification(*args, **kwargs) + if new_option.instructions in (c.instructions for c in self.options): + raise InconsistentOntologyException(f"Duplicate nested classification '{new_option.instructions}' for option '{self.label}'") + self.options.append(new_option) + return new_option @dataclass class Classification: @@ -65,6 +68,8 @@ class Type(Enum): RADIO = "radio" DROPDOWN = "dropdown" + _REQUIRES_OPTIONS = set((Type.CHECKLIST, Type.RADIO, Type.DROPDOWN)) + class_type: Type instructions: str required: bool = False @@ -72,25 +77,21 @@ class Type(Enum): schema_id: Optional[str] = None feature_schema_id: Optional[str] = None - @staticmethod - def requires_options(): - return set((Classification.Type.CHECKLIST, Classification.Type.RADIO, Classification.Type.DROPDOWN)) - @property def name(self): return self.instructions - def to_dict(self, for_different_project=False) -> Dict[str,str]: - if self.class_type in Classification.requires_options() and len(self.options) < 1: + def asdict(self) -> Dict[str,str]: + if self.class_type in Classification._REQUIRES_OPTIONS and len(self.options) < 1: raise InconsistentOntologyException(f"Classification '{self.instructions}' requires options.") return { "type": self.class_type.value, "instructions": self.instructions, "name": self.name, "required": self.required, - "options": [option.to_dict(for_different_project) for option in self.options], - "schemaNodeId": None if for_different_project else self.schema_id, - "featureSchemaId": None if for_different_project else self.feature_schema_id + "options": [o.asdict() for o in self.options], + "schemaNodeId": self.schema_id, + "featureSchemaId": self.feature_schema_id } @classmethod @@ -99,14 +100,14 @@ def from_dict(cls, dictionary: Dict[str,str]): class_type = Classification.Type(dictionary["type"]), instructions = dictionary["instructions"], required = dictionary["required"], - options = [Option.from_dict(option) for option in dictionary["options"]], + options = [Option.from_dict(o) for o in dictionary["options"]], schema_id = dictionary["schemaNodeId"], feature_schema_id = dictionary["schemaNodeId"] ) def add_option(self, *args, **kwargs): new_option = Option(*args, **kwargs) - if new_option.value in (option.value for option in self.options): + if new_option.value in (o.value for o in self.options): raise InconsistentOntologyException(f"Duplicate option '{new_option.value}' for classification '{self.name}'.") self.options.append(new_option) return new_option @@ -130,15 +131,15 @@ class Type(Enum): schema_id: Optional[str] = None feature_schema_id: Optional[str] = None - def to_dict(self,for_different_project=False) -> Dict[str,str]: + def asdict(self) -> Dict[str,str]: return { "tool": self.tool.value, "name": self.name, "required": self.required, "color": self.color, - "classifications": [classification.to_dict(for_different_project) for classification in self.classifications], - "schemaNodeId": None if for_different_project else self.schema_id, - "featureSchemaId": None if for_different_project else self.feature_schema_id + "classifications": [c.asdict() for c in self.classifications], + "schemaNodeId": self.schema_id, + "featureSchemaId": self.feature_schema_id } @classmethod @@ -149,13 +150,13 @@ def from_dict(cls, dictionary: Dict[str,str]): feature_schema_id = dictionary["featureSchemaId"], required = dictionary["required"], tool = Tool.Type(dictionary["tool"]), - classifications = [Classification.from_dict(classification) for classification in dictionary["classifications"]], + classifications = [Classification.from_dict(c) for c in dictionary["classifications"]], color = dictionary["color"] ) def add_nested_class(self, *args, **kwargs): new_classification = Classification(*args, **kwargs) - if new_classification.instructions in (classification.instructions for classification in self.classifications): + if new_classification.instructions in (c.instructions for c in self.classifications): raise InconsistentOntologyException(f"Duplicate nested classification '{new_classification.instructions}' for option '{self.label}'") self.classifications.append(new_classification) return new_classification @@ -168,8 +169,6 @@ class Ontology: @classmethod def from_project(cls, project: Project): - #TODO: consider if this should take in a Project object, or the project.uid. - #if we take in project.uid, we need to then get the project from a client object. ontology = project.ontology().normalized return_ontology = Ontology() @@ -183,34 +182,29 @@ def from_project(cls, project: Project): def add_tool(self, *args, **kwargs) -> Tool: new_tool = Tool(*args, **kwargs) - if new_tool.name in (tool.name for tool in self.tools): + if new_tool.name in (t.name for t in self.tools): raise InconsistentOntologyException(f"Duplicate tool name '{new_tool.name}'. ") self.tools.append(new_tool) return new_tool def add_classification(self, *args, **kwargs) -> Classification: new_classification = Classification(*args, **kwargs) - if new_classification.instructions in (classification.instructions for classification in self.classifications): + if new_classification.instructions in (c.instructions for c in self.classifications): raise InconsistentOntologyException(f"Duplicate classifications instructions '{new_classification.instructions}'. ") self.classifications.append(new_classification) return new_classification - def build(self, for_different_project=False): + def asdict(self): all_tools = [] all_classifications = [] for tool in self.tools: - all_tools.append(tool.to_dict(for_different_project)) + all_tools.append(tool.asdict()) for classification in self.classifications: - all_classifications.append(classification.to_dict(for_different_project)) + all_classifications.append(classification.asdict()) return {"tools": all_tools, "classifications": all_classifications} if __name__ == "__main__": - pass - - - - - + pass \ No newline at end of file From f74d0279076b50c447f68b983c84797157385d04 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Mon, 15 Feb 2021 16:48:11 -0800 Subject: [PATCH 17/36] added some commentary on potential pitfalls --- labelbox/schema/ontology_generator.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index 1eda198aa..2db04e133 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -10,6 +10,18 @@ -> this is a lesser issue because the UI will not reflect the unavailable fields Is there an effective way to enforce limitations on Option.add_option()? -> Maybe a way to check if the Option itself has options when adding it to a Classification? + +Classification.add_.. +Tool.add_.. +Tool.add_.. + Currently the above will let you input values to generate a new object, but they do not play well with already made objects + Example: + Classification.add_option(value=...) will work fine + + option = Option(value=...) + Classification.add_option(option) will not work + + What is the best way to allow both the creation of both an object but also accept an already existing object? ''' from dataclasses import dataclass, field From 76dfc14716740af7cc9d580418e2906345876798 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Tue, 16 Feb 2021 16:19:21 -0800 Subject: [PATCH 18/36] some cleanup on length --- labelbox/schema/ontology_generator.py | 100 +++++++++----------------- 1 file changed, 33 insertions(+), 67 deletions(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index 2db04e133..b56955882 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -1,29 +1,3 @@ -''' -TODO -Option.add_option() currently creates a new Classification object. however, this does not work for certain Classification options. - Example: - Classification.Type.DROPDOWN -> the options for this class_type should only generate more nested dropdowns - -> Dropdowns are supposed to be removed moving forward, but this is a current problem - -> This is the most major issue because going to the doubly nested class will break the UI - Classification.Type.CHECKLIST & Classification.Type.TEXT-> the option cannot have a nested Classification. - -> this reflects accurately in the UI without issues, but when you query via graphql, it shows what was input - -> this is a lesser issue because the UI will not reflect the unavailable fields - Is there an effective way to enforce limitations on Option.add_option()? - -> Maybe a way to check if the Option itself has options when adding it to a Classification? - -Classification.add_.. -Tool.add_.. -Tool.add_.. - Currently the above will let you input values to generate a new object, but they do not play well with already made objects - Example: - Classification.add_option(value=...) will work fine - - option = Option(value=...) - Classification.add_option(option) will not work - - What is the best way to allow both the creation of both an object but also accept an already existing object? -''' - from dataclasses import dataclass, field from enum import Enum, auto import os @@ -61,15 +35,15 @@ def from_dict(cls, dictionary: Dict[str,str]): value = dictionary["value"], schema_id = dictionary["schemaNodeId"], feature_schema_id = dictionary["featureSchemaId"], - options = [Classification.from_dict(nested_class) for nested_class in dictionary.get("options", [])] + options = [Classification.from_dict(o) for o in dictionary.get("options", [])] ) - def add_option(self, *args, **kwargs): - new_option = Classification(*args, **kwargs) - if new_option.instructions in (c.instructions for c in self.options): - raise InconsistentOntologyException(f"Duplicate nested classification '{new_option.instructions}' for option '{self.label}'") - self.options.append(new_option) - return new_option + def add_option(self, new_o: 'Classification') -> 'Classification': + if new_o.instructions in (c.instructions for c in self.options): + #what is the best way to shorten exceptions? + raise InconsistentOntologyException(f"Duplicate nested classification '{new_o.instructions}' for option '{self.label}'") + self.options.append(new_o) + return new_o @dataclass class Classification: @@ -80,7 +54,7 @@ class Type(Enum): RADIO = "radio" DROPDOWN = "dropdown" - _REQUIRES_OPTIONS = set((Type.CHECKLIST, Type.RADIO, Type.DROPDOWN)) + _REQUIRES_OPTIONS = {Type.CHECKLIST, Type.RADIO, Type.DROPDOWN} class_type: Type instructions: str @@ -94,7 +68,8 @@ def name(self): return self.instructions def asdict(self) -> Dict[str,str]: - if self.class_type in Classification._REQUIRES_OPTIONS and len(self.options) < 1: + #unsure how to shorten this specification + if self.class_type in Classification._REQUIRES_OPTIONS and len(self.options) < 1: raise InconsistentOntologyException(f"Classification '{self.instructions}' requires options.") return { "type": self.class_type.value, @@ -117,12 +92,11 @@ def from_dict(cls, dictionary: Dict[str,str]): feature_schema_id = dictionary["schemaNodeId"] ) - def add_option(self, *args, **kwargs): - new_option = Option(*args, **kwargs) - if new_option.value in (o.value for o in self.options): - raise InconsistentOntologyException(f"Duplicate option '{new_option.value}' for classification '{self.name}'.") - self.options.append(new_option) - return new_option + def add_option(self, new_o: Option): + if new_o.value in (o.value for o in self.options): + raise InconsistentOntologyException(f"Duplicate option '{new_o.value}' for classification '{self.name}'.") + self.options.append(new_o) + return new_o @dataclass class Tool: @@ -161,17 +135,17 @@ def from_dict(cls, dictionary: Dict[str,str]): schema_id = dictionary["schemaNodeId"], feature_schema_id = dictionary["featureSchemaId"], required = dictionary["required"], - tool = Tool.Type(dictionary["tool"]), + tool = Tool.Type(dictionary["tool"]), + #is there a way to shorten this classifications line at 140? classifications = [Classification.from_dict(c) for c in dictionary["classifications"]], color = dictionary["color"] ) - def add_nested_class(self, *args, **kwargs): - new_classification = Classification(*args, **kwargs) - if new_classification.instructions in (c.instructions for c in self.classifications): - raise InconsistentOntologyException(f"Duplicate nested classification '{new_classification.instructions}' for option '{self.label}'") - self.classifications.append(new_classification) - return new_classification + def add_nested_class(self, new_c: Classification) -> Classification: + if new_c.instructions in (c.instructions for c in self.classifications): + raise InconsistentOntologyException(f"Duplicate nested classification '{new_c.instructions}' for option '{self.label}'") + self.classifications.append(new_c) + return new_c @dataclass class Ontology: @@ -192,31 +166,23 @@ def from_project(cls, project: Project): return return_ontology - def add_tool(self, *args, **kwargs) -> Tool: - new_tool = Tool(*args, **kwargs) + def add_tool(self, new_tool: Tool) -> Tool: if new_tool.name in (t.name for t in self.tools): raise InconsistentOntologyException(f"Duplicate tool name '{new_tool.name}'. ") self.tools.append(new_tool) return new_tool - - def add_classification(self, *args, **kwargs) -> Classification: - new_classification = Classification(*args, **kwargs) - if new_classification.instructions in (c.instructions for c in self.classifications): - raise InconsistentOntologyException(f"Duplicate classifications instructions '{new_classification.instructions}'. ") - self.classifications.append(new_classification) - return new_classification + + def add_classification(self, new_c: Classification) -> Classification: + if new_c.instructions in (c.instructions for c in self.classifications): + raise InconsistentOntologyException(f"Duplicate classifications instructions '{new_c.instructions}'. ") + self.classifications.append(new_c) + return new_c def asdict(self): - all_tools = [] - all_classifications = [] - - for tool in self.tools: - all_tools.append(tool.asdict()) - - for classification in self.classifications: - all_classifications.append(classification.asdict()) - - return {"tools": all_tools, "classifications": all_classifications} + return { + "tools": [t.asdict() for t in self.tools], + "classifications": [c.asdict() for c in self.classifications] + } if __name__ == "__main__": pass \ No newline at end of file From d8d16d4ef7f2462d8691bf3d3130bc991725a5ba Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Wed, 17 Feb 2021 15:49:12 -0800 Subject: [PATCH 19/36] shortening of lines and cleaning up code --- labelbox/schema/ontology_generator.py | 89 ++++++++++++++------------- 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index b56955882..d295e345b 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field from enum import Enum, auto import os -from typing import List, Optional, Dict +from typing import List, Optional, Dict, Any from labelbox import Client, Project, Dataset, LabelingFrontend @@ -20,7 +20,7 @@ class Option: def label(self): return self.value - def asdict(self) -> Dict[str, str]: + def asdict(self) -> Dict[str, Any]: return { "schemaNodeId": self.schema_id, "featureSchemaId": self.feature_schema_id, @@ -30,20 +30,21 @@ def asdict(self) -> Dict[str, str]: } @classmethod - def from_dict(cls, dictionary: Dict[str,str]): + def from_dict(cls, dictionary: Dict[str,Any]): return Option( value = dictionary["value"], schema_id = dictionary["schemaNodeId"], feature_schema_id = dictionary["featureSchemaId"], - options = [Classification.from_dict(o) for o in dictionary.get("options", [])] + options = [Classification.from_dict(o) + for o in dictionary.get("options", [])] ) - def add_option(self, new_o: 'Classification') -> 'Classification': - if new_o.instructions in (c.instructions for c in self.options): - #what is the best way to shorten exceptions? - raise InconsistentOntologyException(f"Duplicate nested classification '{new_o.instructions}' for option '{self.label}'") - self.options.append(new_o) - return new_o + def add_option(self, option: 'Classification') -> 'Classification': + if option.instructions in (c.instructions for c in self.options): + raise InconsistentOntologyException( + f"Duplicate nested classification '{option.instructions}' " + f"for option '{self.label}'") + self.options.append(option) @dataclass class Classification: @@ -67,10 +68,11 @@ class Type(Enum): def name(self): return self.instructions - def asdict(self) -> Dict[str,str]: - #unsure how to shorten this specification - if self.class_type in Classification._REQUIRES_OPTIONS and len(self.options) < 1: - raise InconsistentOntologyException(f"Classification '{self.instructions}' requires options.") + def asdict(self) -> Dict[str,Any]: + if self.class_type in Classification._REQUIRES_OPTIONS \ + and len(self.options) < 1: + raise InconsistentOntologyException( + f"Classification '{self.instructions}' requires options.") return { "type": self.class_type.value, "instructions": self.instructions, @@ -82,7 +84,7 @@ def asdict(self) -> Dict[str,str]: } @classmethod - def from_dict(cls, dictionary: Dict[str,str]): + def from_dict(cls, dictionary: Dict[str,Any]): return Classification( class_type = Classification.Type(dictionary["type"]), instructions = dictionary["instructions"], @@ -92,11 +94,12 @@ def from_dict(cls, dictionary: Dict[str,str]): feature_schema_id = dictionary["schemaNodeId"] ) - def add_option(self, new_o: Option): - if new_o.value in (o.value for o in self.options): - raise InconsistentOntologyException(f"Duplicate option '{new_o.value}' for classification '{self.name}'.") - self.options.append(new_o) - return new_o + def add_option(self, option: Option): + if option.value in (o.value for o in self.options): + raise InconsistentOntologyException( + f"Duplicate option '{option.value}' " + f"for classification '{self.name}'.") + self.options.append(option) @dataclass class Tool: @@ -117,7 +120,7 @@ class Type(Enum): schema_id: Optional[str] = None feature_schema_id: Optional[str] = None - def asdict(self) -> Dict[str,str]: + def asdict(self) -> Dict[str,Any]: return { "tool": self.tool.value, "name": self.name, @@ -129,23 +132,25 @@ def asdict(self) -> Dict[str,str]: } @classmethod - def from_dict(cls, dictionary: Dict[str,str]): + def from_dict(cls, dictionary: Dict[str,Any]): return Tool( name = dictionary['name'], schema_id = dictionary["schemaNodeId"], feature_schema_id = dictionary["featureSchemaId"], required = dictionary["required"], tool = Tool.Type(dictionary["tool"]), - #is there a way to shorten this classifications line at 140? - classifications = [Classification.from_dict(c) for c in dictionary["classifications"]], + classifications = [Classification.from_dict(c) + for c in dictionary["classifications"]], color = dictionary["color"] ) - def add_nested_class(self, new_c: Classification) -> Classification: - if new_c.instructions in (c.instructions for c in self.classifications): - raise InconsistentOntologyException(f"Duplicate nested classification '{new_c.instructions}' for option '{self.label}'") - self.classifications.append(new_c) - return new_c + def add_classification(self, classification: Classification): + if classification.instructions in (c.instructions + for c in self.classifications): + raise InconsistentOntologyException( + f"Duplicate nested classification '{classification.instructions}' " + f"for option '{self.label}'") + self.classifications.append(classification) @dataclass class Ontology: @@ -166,23 +171,21 @@ def from_project(cls, project: Project): return return_ontology - def add_tool(self, new_tool: Tool) -> Tool: - if new_tool.name in (t.name for t in self.tools): - raise InconsistentOntologyException(f"Duplicate tool name '{new_tool.name}'. ") - self.tools.append(new_tool) - return new_tool + def add_tool(self, tool: Tool) -> Tool: + if tool.name in (t.name for t in self.tools): + raise InconsistentOntologyException( + f"Duplicate tool name '{tool.name}'. ") + self.tools.append(tool) - def add_classification(self, new_c: Classification) -> Classification: - if new_c.instructions in (c.instructions for c in self.classifications): - raise InconsistentOntologyException(f"Duplicate classifications instructions '{new_c.instructions}'. ") - self.classifications.append(new_c) - return new_c + def add_classification(self, classification: Classification) -> Classification: + if classification.instructions in (c.instructions + for c in self.classifications): + raise InconsistentOntologyException( + f"Duplicate classifications instructions '{classification.instructions}'. ") + self.classifications.append(classification) def asdict(self): return { "tools": [t.asdict() for t in self.tools], "classifications": [c.asdict() for c in self.classifications] - } - -if __name__ == "__main__": - pass \ No newline at end of file + } \ No newline at end of file From c40ebb70e6de2f4ca34643b0c424e0aa2cfbe5ea Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Wed, 17 Feb 2021 15:57:47 -0800 Subject: [PATCH 20/36] shortening of lines and cleaning up code --- labelbox/schema/ontology_generator.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index d295e345b..8b857b7f5 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -26,8 +26,7 @@ def asdict(self) -> Dict[str, Any]: "featureSchemaId": self.feature_schema_id, "label": self.label, "value": self.value, - "options": [c.asdict() for c in self.options] - } + "options": [c.asdict() for c in self.options]} @classmethod def from_dict(cls, dictionary: Dict[str,Any]): @@ -36,8 +35,7 @@ def from_dict(cls, dictionary: Dict[str,Any]): schema_id = dictionary["schemaNodeId"], feature_schema_id = dictionary["featureSchemaId"], options = [Classification.from_dict(o) - for o in dictionary.get("options", [])] - ) + for o in dictionary.get("options", [])]) def add_option(self, option: 'Classification') -> 'Classification': if option.instructions in (c.instructions for c in self.options): @@ -80,8 +78,7 @@ def asdict(self) -> Dict[str,Any]: "required": self.required, "options": [o.asdict() for o in self.options], "schemaNodeId": self.schema_id, - "featureSchemaId": self.feature_schema_id - } + "featureSchemaId": self.feature_schema_id} @classmethod def from_dict(cls, dictionary: Dict[str,Any]): @@ -91,8 +88,7 @@ def from_dict(cls, dictionary: Dict[str,Any]): required = dictionary["required"], options = [Option.from_dict(o) for o in dictionary["options"]], schema_id = dictionary["schemaNodeId"], - feature_schema_id = dictionary["schemaNodeId"] - ) + feature_schema_id = dictionary["schemaNodeId"]) def add_option(self, option: Option): if option.value in (o.value for o in self.options): @@ -128,8 +124,7 @@ def asdict(self) -> Dict[str,Any]: "color": self.color, "classifications": [c.asdict() for c in self.classifications], "schemaNodeId": self.schema_id, - "featureSchemaId": self.feature_schema_id - } + "featureSchemaId": self.feature_schema_id} @classmethod def from_dict(cls, dictionary: Dict[str,Any]): @@ -141,8 +136,7 @@ def from_dict(cls, dictionary: Dict[str,Any]): tool = Tool.Type(dictionary["tool"]), classifications = [Classification.from_dict(c) for c in dictionary["classifications"]], - color = dictionary["color"] - ) + color = dictionary["color"]) def add_classification(self, classification: Classification): if classification.instructions in (c.instructions @@ -187,5 +181,4 @@ def add_classification(self, classification: Classification) -> Classification: def asdict(self): return { "tools": [t.asdict() for t in self.tools], - "classifications": [c.asdict() for c in self.classifications] - } \ No newline at end of file + "classifications": [c.asdict() for c in self.classifications]} \ No newline at end of file From 1152dd3ddbb2b5aaecb0468893b01f9b90c17d50 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Thu, 18 Feb 2021 13:50:49 -0800 Subject: [PATCH 21/36] small nit changes --- labelbox/schema/ontology_generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index 8b857b7f5..a19ddaa57 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -26,7 +26,7 @@ def asdict(self) -> Dict[str, Any]: "featureSchemaId": self.feature_schema_id, "label": self.label, "value": self.value, - "options": [c.asdict() for c in self.options]} + "options": [o.asdict() for o in self.options]} @classmethod def from_dict(cls, dictionary: Dict[str,Any]): @@ -38,7 +38,7 @@ def from_dict(cls, dictionary: Dict[str,Any]): for o in dictionary.get("options", [])]) def add_option(self, option: 'Classification') -> 'Classification': - if option.instructions in (c.instructions for c in self.options): + if option.instructions in (o.instructions for o in self.options): raise InconsistentOntologyException( f"Duplicate nested classification '{option.instructions}' " f"for option '{self.label}'") From ef191c8aa265e82b21d1017bb561379ec62f9565 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Tue, 23 Feb 2021 12:15:07 -0800 Subject: [PATCH 22/36] nit changes --- Makefile | 2 +- labelbox/schema/ontology_generator.py | 2 +- tests/integration/test_ontology.py | 174 ++++++++++++++++++++------ 3 files changed, 135 insertions(+), 43 deletions(-) diff --git a/Makefile b/Makefile index 60ceb6697..feb418821 100644 --- a/Makefile +++ b/Makefile @@ -11,5 +11,5 @@ test-staging: build test-prod: build docker run -it -v ${PWD}:/usr/src -w /usr/src \ -e LABELBOX_TEST_ENVIRON="prod" \ - -e LABELBOX_TEST_API_KEY_PROD="" \ + -e LABELBOX_TEST_API_KEY_PROD=${apikey} \ local/labelbox-python:test pytest $(PATH_TO_TEST) -svvx diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index a19ddaa57..b92f63492 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -3,7 +3,7 @@ import os from typing import List, Optional, Dict, Any -from labelbox import Client, Project, Dataset, LabelingFrontend +from labelbox import Client, Project, Dataset, LabelingFrontend, InconsistentOntologyException class InconsistentOntologyException(Exception): diff --git a/tests/integration/test_ontology.py b/tests/integration/test_ontology.py index 9f6f7e257..64cb55c7d 100644 --- a/tests/integration/test_ontology.py +++ b/tests/integration/test_ontology.py @@ -1,6 +1,10 @@ -import unittest +# import unittest +import pytest from typing import Any, Dict, List, Union +#want to import ontology_generator.py properly, not the bad way we are currently doing +from ontology_generator import Ontology, Tool, Classification, Option + def sample_ontology() -> Dict[str, Any]: return { @@ -30,44 +34,132 @@ def sample_ontology() -> Dict[str, Any]: }] } +#want to create base case tests, each indiv tool, classification, option +#want to then do nested objects inside each +#do we want to test colors, bool, etc? +#test inside methods? like asdict/fromdict? +#test ontology.from_project? +#test ontology.build? +""" +Tool tests +""" +# def test_create_tool(client, project) -> None: +def test_create_bbox_tool() -> None: + t = Tool(tool=Tool.Type.BBOX, name="box tool") + assert(t.tool==Tool.Type.BBOX) + assert(t.name=="box tool") + +def test_create_point_tool() -> None: + t = Tool(tool=Tool.Type.POINT, name="point tool") + assert(t.tool==Tool.Type.POINT) + assert(t.name=="point tool") + +def test_create_polygon_tool() -> None: + t = Tool(tool=Tool.Type.POLYGON, name="polygon tool") + assert(t.tool==Tool.Type.POLYGON) + assert(t.name=="polygon tool") + +def test_create_ner_tool() -> None: + t = Tool(tool=Tool.Type.NER, name="ner tool") + assert(t.tool==Tool.Type.NER) + assert(t.name=="ner tool") + +def test_create_segment_tool() -> None: + t = Tool(tool=Tool.Type.SEGMENTATION, name="segment tool") + assert(t.tool==Tool.Type.SEGMENTATION) + assert(t.name=="segment tool") + +def test_create_line_tool() -> None: + t = Tool(tool=Tool.Type.LINE, name="line tool") + assert(t.tool==Tool.Type.LINE) + assert(t.name=="line tool") + +""" +Classification tests +""" +def test_create_text_classification() -> None: + c = Classification(class_type=Classification.Type.TEXT, instructions="text") + assert(c.class_type==Classification.Type.TEXT) + assert(c.instructions=="text") + assert(c.class_type not in c._REQUIRES_OPTIONS) + +def test_create_radio_classification() -> None: + c = Classification(class_type=Classification.Type.RADIO, instructions="radio") + assert(c.class_type==Classification.Type.RADIO) + assert(c.instructions=="radio") + assert(c.class_type in c._REQUIRES_OPTIONS) + +def test_create_checklist_classification() -> None: + c = Classification(class_type=Classification.Type.CHECKLIST, instructions="checklist") + assert(c.class_type==Classification.Type.CHECKLIST) + assert(c.instructions=="checklist") + assert(c.class_type in c._REQUIRES_OPTIONS) + +def test_create_dropdown_classification() -> None: + c = Classification(class_type=Classification.Type.DROPDOWN, instructions="dropdown") + assert(c.class_type==Classification.Type.DROPDOWN) + assert(c.instructions=="dropdown") + assert(c.class_type in c._REQUIRES_OPTIONS) + +""" +Option tests +""" +def test_create_int_option() -> None: + o = Option(value=3) + assert(o.value==3) + assert(type(o.value) == int) + +def test_create_string_option() -> None: + o = Option(value="3") + assert(o.value=="3") + assert(type(o.value)== str) + +""" +Ontology tests +""" +def test_create_ontology() -> None: + o = Ontology() + assert(o.tools == []) + assert(o.classifications == []) + + +# def test_create_ontology(client, project) -> None: +# """ Tests that the ontology that a project was set up with can be grabbed.""" +# frontend = list(client.get_labeling_frontends())[0] +# project.setup(frontend, sample_ontology()) +# normalized_ontology = project.ontology().normalized + +# def _remove_schema_ids( +# ontology_part: Union[List, Dict[str, Any]]) -> Dict[str, Any]: +# """ Recursively scrub the normalized ontology of any schema information.""" +# removals = {'featureSchemaId', 'schemaNodeId'} + +# if isinstance(ontology_part, list): +# return [_remove_schema_ids(part) for part in ontology_part] +# if isinstance(ontology_part, dict): +# return { +# key: _remove_schema_ids(value) +# for key, value in ontology_part.items() +# if key not in removals +# } +# return ontology_part + +# removed = _remove_schema_ids(normalized_ontology) +# assert removed == sample_ontology() + +# ontology = project.ontology() + +# tools = ontology.tools() +# assert tools +# for tool in tools: +# assert tool.feature_schema_id +# assert tool.schema_node_id -def test_create_ontology(client, project) -> None: - """ Tests that the ontology that a project was set up with can be grabbed.""" - frontend = list(client.get_labeling_frontends())[0] - project.setup(frontend, sample_ontology()) - normalized_ontology = project.ontology().normalized - - def _remove_schema_ids( - ontology_part: Union[List, Dict[str, Any]]) -> Dict[str, Any]: - """ Recursively scrub the normalized ontology of any schema information.""" - removals = {'featureSchemaId', 'schemaNodeId'} - - if isinstance(ontology_part, list): - return [_remove_schema_ids(part) for part in ontology_part] - if isinstance(ontology_part, dict): - return { - key: _remove_schema_ids(value) - for key, value in ontology_part.items() - if key not in removals - } - return ontology_part - - removed = _remove_schema_ids(normalized_ontology) - assert removed == sample_ontology() - - ontology = project.ontology() - - tools = ontology.tools() - assert tools - for tool in tools: - assert tool.feature_schema_id - assert tool.schema_node_id - - classifications = ontology.classifications() - assert classifications - for classification in classifications: - assert classification.feature_schema_id - assert classification.schema_node_id - for option in classification.options: - assert option.feature_schema_id - assert option.schema_node_id +# classifications = ontology.classifications() +# assert classifications +# for classification in classifications: +# assert classification.feature_schema_id +# assert classification.schema_node_id +# for option in classification.options: +# assert option.feature_schema_id +# assert option.schema_node_id From f575abbd1396127816c4a7dfa6549dc9c627822b Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Thu, 25 Feb 2021 16:07:47 -0800 Subject: [PATCH 23/36] updated tests --- tests/integration/test_ontology.py | 166 +++++++++++++++++++++-------- 1 file changed, 121 insertions(+), 45 deletions(-) diff --git a/tests/integration/test_ontology.py b/tests/integration/test_ontology.py index eca28db4c..fd7354c22 100644 --- a/tests/integration/test_ontology.py +++ b/tests/integration/test_ontology.py @@ -6,7 +6,6 @@ #want to import ontology_generator.py properly, not the bad way we are currently doing from labelbox.schema.ontology_generator import Ontology, Tool, Classification, Option, InconsistentOntologyException - def sample_ontology() -> Dict[str, Any]: return { "tools": [{ @@ -35,12 +34,9 @@ def sample_ontology() -> Dict[str, Any]: }] } -#want to create base case tests, each indiv tool, classification, option -#want to then do nested objects inside each -#do we want to test colors, bool, etc? -#test inside methods? like asdict/fromdict? -#test ontology.from_project? -#test ontology.build? +#test asdict +#test ontology.asdict +#test nested classifications """ Tool tests """ @@ -119,50 +115,130 @@ def test_create_string_option() -> None: Ontology tests """ def test_create_ontology() -> None: + """ + Tests the initial structure of an Ontology object + """ o = Ontology() + assert type(o) == Ontology assert(o.tools == []) assert(o.classifications == []) -def test_create_ontology(client, project) -> None: - """ Tests that the ontology that a project was set up with can be grabbed.""" +def test_add_ontology_tool() -> None: + """ + Tests the possible ways to add a tool to an ontology + """ + o = Ontology() + o.add_tool(Tool(tool = Tool.Type.BBOX, name = "bounding box")) + + second_tool = Tool(tool = Tool.Type.SEGMENTATION, name = "segmentation") + o.add_tool(second_tool) + + for tool in o.tools: + assert type(tool) == Tool + +def test_add_ontology_classification() -> None: + """ + Tests the possible ways to add a classification to an ontology + """ + o = Ontology() + o.add_classification(Classification( + class_type = Classification.Type.TEXT, instructions = "text")) + + second_classification = Classification( + class_type = Classification.Type.CHECKLIST, instructions = "checklist") + o.add_classification(second_classification) + + for classification in o.classifications: + assert type(classification) == Classification + +def test_ontology_asdict(project) -> None: + """ + Tests the asdict() method to ensure that it matches the format + of a project ontology + """ + from_project_ontology = project.ontology().normalized + + o = Ontology.from_project(project) + assert o.asdict() == from_project_ontology + +def test_from_project_ontology(client, project) -> None: + """ + Tests the ability to correctly get an existing project's ontology + and if it can correctly convert it to the right object types + """ frontend = list( client.get_labeling_frontends( where=LabelingFrontend.name == "Editor"))[0] project.setup(frontend, sample_ontology()) - normalized_ontology = project.ontology().normalized - - def _remove_schema_ids( - ontology_part: Union[List, Dict[str, Any]]) -> Dict[str, Any]: - """ Recursively scrub the normalized ontology of any schema information.""" - removals = {'featureSchemaId', 'schemaNodeId'} - - if isinstance(ontology_part, list): - return [_remove_schema_ids(part) for part in ontology_part] - if isinstance(ontology_part, dict): - return { - key: _remove_schema_ids(value) - for key, value in ontology_part.items() - if key not in removals - } - return ontology_part - - removed = _remove_schema_ids(normalized_ontology) - assert removed == sample_ontology() - - ontology = project.ontology() - - tools = ontology.tools() - assert tools - for tool in tools: - assert tool.feature_schema_id - assert tool.schema_node_id - - classifications = ontology.classifications() - assert classifications - for classification in classifications: - assert classification.feature_schema_id - assert classification.schema_node_id - for option in classification.options: - assert option.feature_schema_id - assert option.schema_node_id + + ontology = Ontology.from_project(project) + assert len(ontology.tools) == 1 + assert ontology.tools[0].tool == Tool.Type.BBOX + for tool in ontology.tools: + assert type(tool) == Tool + + assert len(ontology.classifications) == 1 + assert ontology.classifications[0].class_type == Classification.Type.RADIO + for classification in ontology.classifications: + assert type(classification) == Classification + + assert len(ontology.classifications[0].options) == 2 + assert ontology.classifications[0].options[0].value.lower() == "yes" + assert ontology.classifications[0].options[0].label.lower() == "yes" + for option in ontology.classifications[0].options: + assert type(option) == Option + +# def test_ontology_asdict() -> None: +# o = Ontology() + + + + + + +""" +Old ontology file test +""" +# def test_create_ontology(client, project) -> None: +# """ Tests that the ontology that a project was set up with can be grabbed.""" +# frontend = list( +# client.get_labeling_frontends( +# where=LabelingFrontend.name == "Editor"))[0] +# project.setup(frontend, sample_ontology()) +# normalized_ontology = project.ontology().normalized + +# def _remove_schema_ids( +# ontology_part: Union[List, Dict[str, Any]]) -> Dict[str, Any]: +# """ Recursively scrub the normalized ontology of any schema information.""" +# removals = {'featureSchemaId', 'schemaNodeId'} + +# if isinstance(ontology_part, list): +# return [_remove_schema_ids(part) for part in ontology_part] +# if isinstance(ontology_part, dict): +# return { +# key: _remove_schema_ids(value) +# for key, value in ontology_part.items() +# if key not in removals +# } +# return ontology_part + +# removed = _remove_schema_ids(normalized_ontology) +# assert removed == sample_ontology() + +# ontology = project.ontology() + +# tools = ontology.tools() +# assert tools +# for tool in tools: +# assert tool.feature_schema_id +# assert tool.schema_node_id + +# classifications = ontology.classifications() +# assert classifications +# for classification in classifications: +# assert classification.feature_schema_id +# assert classification.schema_node_id +# for option in classification.options: +# assert option.feature_schema_id +# assert option.schema_node_id From 7cd448ae18055644681e24e3b7bb587f98cbaa27 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Thu, 25 Feb 2021 16:08:30 -0800 Subject: [PATCH 24/36] updated tests --- tests/integration/test_ontology.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/integration/test_ontology.py b/tests/integration/test_ontology.py index fd7354c22..8f5f1d1f6 100644 --- a/tests/integration/test_ontology.py +++ b/tests/integration/test_ontology.py @@ -1,9 +1,7 @@ -# import unittest import pytest from typing import Any, Dict, List, Union from labelbox import LabelingFrontend -#want to import ontology_generator.py properly, not the bad way we are currently doing from labelbox.schema.ontology_generator import Ontology, Tool, Classification, Option, InconsistentOntologyException def sample_ontology() -> Dict[str, Any]: @@ -35,7 +33,6 @@ def sample_ontology() -> Dict[str, Any]: } #test asdict -#test ontology.asdict #test nested classifications """ Tool tests @@ -187,9 +184,6 @@ def test_from_project_ontology(client, project) -> None: assert ontology.classifications[0].options[0].label.lower() == "yes" for option in ontology.classifications[0].options: assert type(option) == Option - -# def test_ontology_asdict() -> None: -# o = Ontology() From 312ef7f062a6f9ff86310489797bd77cb245741f Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Sun, 28 Feb 2021 23:26:07 -0800 Subject: [PATCH 25/36] update to testing --- tests/integration/test_ontology.py | 241 +++++++---------------------- 1 file changed, 54 insertions(+), 187 deletions(-) diff --git a/tests/integration/test_ontology.py b/tests/integration/test_ontology.py index 8f5f1d1f6..e60acec49 100644 --- a/tests/integration/test_ontology.py +++ b/tests/integration/test_ontology.py @@ -1,142 +1,81 @@ -import pytest +""" +TODO: +test option.add_option +test all classes' asdicts (what is the best way...) +test classification.add_option +test tool.add_classification +consider testing and ensuring failed scenarios +""" from typing import Any, Dict, List, Union + +import pytest + from labelbox import LabelingFrontend +from labelbox.schema.ontology_generator import Ontology, \ + Tool, Classification, Option, InconsistentOntologyException -from labelbox.schema.ontology_generator import Ontology, Tool, Classification, Option, InconsistentOntologyException -def sample_ontology() -> Dict[str, Any]: - return { +_SAMPLE_ONTOLOGY = { "tools": [{ "required": False, "name": "Dog", "color": "#FF0000", "tool": "rectangle", "classifications": [] - }], +}], "classifications": [{ - "required": - True, - "instructions": - "This is a question.", - "name": - "this_is_a_question.", - "type": - "radio", + "required": True, + "instructions": "This is a question.", + "name": "This is a question.", + "type": "radio", "options": [{ - "label": "Yes", + "label": "yes", "value": "yes" }, { - "label": "No", + "label": "no", "value": "no" }] }] } -#test asdict -#test nested classifications -""" -Tool tests -""" -# def test_create_tool(client, project) -> None: -def test_create_bbox_tool() -> None: - t = Tool(tool=Tool.Type.BBOX, name="box tool") - assert(t.tool==Tool.Type.BBOX) - assert(t.name=="box tool") - -def test_create_point_tool() -> None: - t = Tool(tool=Tool.Type.POINT, name="point tool") - assert(t.tool==Tool.Type.POINT) - assert(t.name=="point tool") - -def test_create_polygon_tool() -> None: - t = Tool(tool=Tool.Type.POLYGON, name="polygon tool") - assert(t.tool==Tool.Type.POLYGON) - assert(t.name=="polygon tool") - -def test_create_ner_tool() -> None: - t = Tool(tool=Tool.Type.NER, name="ner tool") - assert(t.tool==Tool.Type.NER) - assert(t.name=="ner tool") - -def test_create_segment_tool() -> None: - t = Tool(tool=Tool.Type.SEGMENTATION, name="segment tool") - assert(t.tool==Tool.Type.SEGMENTATION) - assert(t.name=="segment tool") - -def test_create_line_tool() -> None: - t = Tool(tool=Tool.Type.LINE, name="line tool") - assert(t.tool==Tool.Type.LINE) - assert(t.name=="line tool") - -""" -Classification tests -""" -def test_create_text_classification() -> None: - c = Classification(class_type=Classification.Type.TEXT, instructions="text") - assert(c.class_type==Classification.Type.TEXT) - assert(c.instructions=="text") - assert(c.class_type not in c._REQUIRES_OPTIONS) - -def test_create_radio_classification() -> None: - c = Classification(class_type=Classification.Type.RADIO, instructions="radio") - assert(c.class_type==Classification.Type.RADIO) - assert(c.instructions=="radio") - assert(c.class_type in c._REQUIRES_OPTIONS) - -def test_create_checklist_classification() -> None: - c = Classification(class_type=Classification.Type.CHECKLIST, instructions="checklist") - assert(c.class_type==Classification.Type.CHECKLIST) - assert(c.instructions=="checklist") - assert(c.class_type in c._REQUIRES_OPTIONS) - -def test_create_dropdown_classification() -> None: - c = Classification(class_type=Classification.Type.DROPDOWN, instructions="dropdown") - assert(c.class_type==Classification.Type.DROPDOWN) - assert(c.instructions=="dropdown") - assert(c.class_type in c._REQUIRES_OPTIONS) - -""" -Option tests -""" -def test_create_int_option() -> None: - o = Option(value=3) - assert(o.value==3) - assert(type(o.value) == int) - -def test_create_string_option() -> None: - o = Option(value="3") - assert(o.value=="3") - assert(type(o.value)== str) - -""" -Ontology tests -""" -def test_create_ontology() -> None: - """ - Tests the initial structure of an Ontology object - """ +@pytest.mark.parametrize("tool_type", list(Tool.Type)) +@pytest.mark.parametrize("tool_name", ["tool"]) +def test_create_tool(tool_type, tool_name) -> None: + t = Tool(tool = tool_type, name = tool_name) + assert(t.tool == tool_type) + assert(t.name == tool_name) + +@pytest.mark.parametrize("class_type", list(Classification.Type)) +@pytest.mark.parametrize("class_instr", ["classification"]) +def test_create_classification(class_type, class_instr) -> None: + c = Classification(class_type = class_type, instructions = class_instr) + assert(c.class_type == class_type) + assert(c.instructions == class_instr) + assert(c.name == c.instructions) + +@pytest.mark.parametrize( + "value, expected_value, typing",[(3,3, int),("string","string", str)]) +def test_create_option(value, expected_value, typing) -> None: + o = Option(value = value) + assert(o.value == expected_value) + assert(o.value == o.label) + assert(type(o.value) == typing) + +def test_create_empty_ontology() -> None: o = Ontology() - assert type(o) == Ontology assert(o.tools == []) assert(o.classifications == []) def test_add_ontology_tool() -> None: - """ - Tests the possible ways to add a tool to an ontology - """ o = Ontology() o.add_tool(Tool(tool = Tool.Type.BBOX, name = "bounding box")) second_tool = Tool(tool = Tool.Type.SEGMENTATION, name = "segmentation") o.add_tool(second_tool) - for tool in o.tools: - assert type(tool) == Tool + assert len(o.tools) == 2 def test_add_ontology_classification() -> None: - """ - Tests the possible ways to add a classification to an ontology - """ o = Ontology() o.add_classification(Classification( class_type = Classification.Type.TEXT, instructions = "text")) @@ -145,94 +84,22 @@ def test_add_ontology_classification() -> None: class_type = Classification.Type.CHECKLIST, instructions = "checklist") o.add_classification(second_classification) - for classification in o.classifications: - assert type(classification) == Classification + assert len(o.classifications) == 2 def test_ontology_asdict(project) -> None: - """ - Tests the asdict() method to ensure that it matches the format - of a project ontology - """ - from_project_ontology = project.ontology().normalized - o = Ontology.from_project(project) - assert o.asdict() == from_project_ontology + assert o.asdict() == project.ontology().normalized def test_from_project_ontology(client, project) -> None: - """ - Tests the ability to correctly get an existing project's ontology - and if it can correctly convert it to the right object types - """ frontend = list( client.get_labeling_frontends( where=LabelingFrontend.name == "Editor"))[0] - project.setup(frontend, sample_ontology()) - - ontology = Ontology.from_project(project) - assert len(ontology.tools) == 1 - assert ontology.tools[0].tool == Tool.Type.BBOX - for tool in ontology.tools: - assert type(tool) == Tool - - assert len(ontology.classifications) == 1 - assert ontology.classifications[0].class_type == Classification.Type.RADIO - for classification in ontology.classifications: - assert type(classification) == Classification - - assert len(ontology.classifications[0].options) == 2 - assert ontology.classifications[0].options[0].value.lower() == "yes" - assert ontology.classifications[0].options[0].label.lower() == "yes" - for option in ontology.classifications[0].options: - assert type(option) == Option - - + project.setup(frontend, _SAMPLE_ONTOLOGY) + o = Ontology.from_project(project) + assert o.tools[0].tool == Tool.Type.BBOX + assert o.classifications[0].class_type == Classification.Type.RADIO + assert o.classifications[0].options[0].value.lower() == "yes" - -""" -Old ontology file test -""" -# def test_create_ontology(client, project) -> None: -# """ Tests that the ontology that a project was set up with can be grabbed.""" -# frontend = list( -# client.get_labeling_frontends( -# where=LabelingFrontend.name == "Editor"))[0] -# project.setup(frontend, sample_ontology()) -# normalized_ontology = project.ontology().normalized - -# def _remove_schema_ids( -# ontology_part: Union[List, Dict[str, Any]]) -> Dict[str, Any]: -# """ Recursively scrub the normalized ontology of any schema information.""" -# removals = {'featureSchemaId', 'schemaNodeId'} - -# if isinstance(ontology_part, list): -# return [_remove_schema_ids(part) for part in ontology_part] -# if isinstance(ontology_part, dict): -# return { -# key: _remove_schema_ids(value) -# for key, value in ontology_part.items() -# if key not in removals -# } -# return ontology_part - -# removed = _remove_schema_ids(normalized_ontology) -# assert removed == sample_ontology() - -# ontology = project.ontology() - -# tools = ontology.tools() -# assert tools -# for tool in tools: -# assert tool.feature_schema_id -# assert tool.schema_node_id - -# classifications = ontology.classifications() -# assert classifications -# for classification in classifications: -# assert classification.feature_schema_id -# assert classification.schema_node_id -# for option in classification.options: -# assert option.feature_schema_id -# assert option.schema_node_id - + From 5c27405bb1bd143162189ea99ef786d31510c471 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Sun, 28 Feb 2021 23:27:56 -0800 Subject: [PATCH 26/36] update to testing --- tests/integration/test_ontology.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/test_ontology.py b/tests/integration/test_ontology.py index e60acec49..cf6613378 100644 --- a/tests/integration/test_ontology.py +++ b/tests/integration/test_ontology.py @@ -1,7 +1,6 @@ """ TODO: test option.add_option -test all classes' asdicts (what is the best way...) test classification.add_option test tool.add_classification consider testing and ensuring failed scenarios From 07388cb180179f3e6bfc7f273393a81777998f25 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Tue, 2 Mar 2021 15:45:45 -0800 Subject: [PATCH 27/36] update to tests --- Dockerfile | 2 +- labelbox/schema/ontology_generator.py | 4 +- tests/integration/test_ontology.py | 63 +++++++++++++++++++-------- 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/Dockerfile b/Dockerfile index 91c97e336..f9517a1c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.6 +FROM python:3.7 COPY . /usr/src/labelbox WORKDIR /usr/src/labelbox diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index a19ddaa57..ae077751b 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -143,7 +143,7 @@ def add_classification(self, classification: Classification): for c in self.classifications): raise InconsistentOntologyException( f"Duplicate nested classification '{classification.instructions}' " - f"for option '{self.label}'") + f"for tool '{self.name}'") self.classifications.append(classification) @dataclass @@ -175,7 +175,7 @@ def add_classification(self, classification: Classification) -> Classification: if classification.instructions in (c.instructions for c in self.classifications): raise InconsistentOntologyException( - f"Duplicate classifications instructions '{classification.instructions}'. ") + f"Duplicate classification instructions '{classification.instructions}'. ") self.classifications.append(classification) def asdict(self): diff --git a/tests/integration/test_ontology.py b/tests/integration/test_ontology.py index cf6613378..7c8589bed 100644 --- a/tests/integration/test_ontology.py +++ b/tests/integration/test_ontology.py @@ -1,10 +1,3 @@ -""" -TODO: -test option.add_option -test classification.add_option -test tool.add_classification -consider testing and ensuring failed scenarios -""" from typing import Any, Dict, List, Union import pytest @@ -21,7 +14,7 @@ "color": "#FF0000", "tool": "rectangle", "classifications": [] -}], + }], "classifications": [{ "required": True, "instructions": "This is a question.", @@ -71,9 +64,12 @@ def test_add_ontology_tool() -> None: second_tool = Tool(tool = Tool.Type.SEGMENTATION, name = "segmentation") o.add_tool(second_tool) - assert len(o.tools) == 2 + with pytest.raises(InconsistentOntologyException) as exc: + o.add_tool(Tool(tool=Tool.Type.BBOX, name = "bounding box")) + assert "Duplicate tool name" in str(exc.value) + def test_add_ontology_classification() -> None: o = Ontology() o.add_classification(Classification( @@ -82,23 +78,56 @@ def test_add_ontology_classification() -> None: second_classification = Classification( class_type = Classification.Type.CHECKLIST, instructions = "checklist") o.add_classification(second_classification) - assert len(o.classifications) == 2 + with pytest.raises(InconsistentOntologyException) as exc: + o.add_classification(Classification( + class_type = Classification.Type.TEXT, instructions = "text")) + assert "Duplicate classification instructions" in str(exc.value) + +def test_tool_add_classification() -> None: + t = Tool(tool = Tool.Type.SEGMENTATION, name = "segmentation") + c = Classification( + class_type = Classification.Type.TEXT, instructions = "text") + t.add_classification(c) + assert t.classifications[0] == c + + with pytest.raises(Exception) as exc: + t.add_classification(c) + assert "Duplicate nested classification" in str(exc) + +def test_classification_add_option() -> None: + c = Classification( + class_type = Classification.Type.RADIO, instructions = "radio") + o = Option(value = "option") + c.add_option(o) + assert c.options[0] == o + + with pytest.raises(InconsistentOntologyException) as exc: + c.add_option(Option(value = "option")) + assert "Duplicate option" in str(exc.value) + +def test_option_add_option() -> None: + o = Option(value = "option") + c = Classification( + class_type = Classification.Type.TEXT, instructions = "text") + o.add_option(c) + assert o.options[0] == c + + with pytest.raises(InconsistentOntologyException) as exc: + o.add_option(c) + assert "Duplicate nested classification" in str(exc.value) + def test_ontology_asdict(project) -> None: o = Ontology.from_project(project) assert o.asdict() == project.ontology().normalized def test_from_project_ontology(client, project) -> None: - frontend = list( - client.get_labeling_frontends( - where=LabelingFrontend.name == "Editor"))[0] + frontend = list(client.get_labeling_frontends( + where=LabelingFrontend.name == "Editor"))[0] project.setup(frontend, _SAMPLE_ONTOLOGY) o = Ontology.from_project(project) assert o.tools[0].tool == Tool.Type.BBOX assert o.classifications[0].class_type == Classification.Type.RADIO - assert o.classifications[0].options[0].value.lower() == "yes" - - - + assert o.classifications[0].options[0].value.lower() == "yes" \ No newline at end of file From 5e7180e6457b5c71d7e7001858e05471c5f076e6 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Wed, 3 Mar 2021 16:49:14 -0800 Subject: [PATCH 28/36] add ontology.from_dict and updated tests --- labelbox/schema/ontology_generator.py | 18 ++-- tests/integration/test_ontology.py | 129 +++++++++++++++++++++----- 2 files changed, 112 insertions(+), 35 deletions(-) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index ae077751b..f56c77379 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -155,15 +155,7 @@ class Ontology: @classmethod def from_project(cls, project: Project): ontology = project.ontology().normalized - return_ontology = Ontology() - - for tool in ontology["tools"]: - return_ontology.tools.append(Tool.from_dict(tool)) - - for classification in ontology["classifications"]: - return_ontology.classifications.append(Classification.from_dict(classification)) - - return return_ontology + return Ontology.from_dict(ontology) def add_tool(self, tool: Tool) -> Tool: if tool.name in (t.name for t in self.tools): @@ -181,4 +173,10 @@ def add_classification(self, classification: Classification) -> Classification: def asdict(self): return { "tools": [t.asdict() for t in self.tools], - "classifications": [c.asdict() for c in self.classifications]} \ No newline at end of file + "classifications": [c.asdict() for c in self.classifications]} + + @classmethod + def from_dict(cls, dictionary: Dict[str,Any]): + return Ontology( + tools = [Tool.from_dict(t) for t in dictionary["tools"]], + classifications = [Classification.from_dict(c) for c in dictionary["classifications"]]) \ No newline at end of file diff --git a/tests/integration/test_ontology.py b/tests/integration/test_ontology.py index 7c8589bed..47686c294 100644 --- a/tests/integration/test_ontology.py +++ b/tests/integration/test_ontology.py @@ -9,41 +9,122 @@ _SAMPLE_ONTOLOGY = { "tools": [{ + "schemaNodeId": None, + "featureSchemaId": None, "required": False, - "name": "Dog", + "name": "poly", + "color": "#FF0000", + "tool": "polygon", + "classifications": [] + }, { + "schemaNodeId": None, + "featureSchemaId": None, + "required": False, + "name": "segment", + "color": "#FF0000", + "tool": "superpixel", + "classifications": [] + }, { + "schemaNodeId": None, + "featureSchemaId": None, + "required": False, + "name": "bbox", "color": "#FF0000", "tool": "rectangle", + "classifications": [{ + "schemaNodeId": None, + "featureSchemaId": None, + "required": True, + "instructions": "nested classification", + "name": "nested classification", + "type": "radio", + "options": [{ + "schemaNodeId": None, + "featureSchemaId": None, + "label": "first", + "value": "first", + "options": [{ + "schemaNodeId": None, + "featureSchemaId": None, + "required": False, + "instructions": "nested nested text", + "name": "nested nested text", + "type": "text", + "options": [] + }] + }, { + "schemaNodeId": None, + "featureSchemaId": None, + "label": "second", + "value": "second", + "options": [] + }] + }, { + "schemaNodeId": None, + "featureSchemaId": None, + "required": True, + "instructions": "nested text", + "name": "nested text", + "type": "text", + "options": [] + }] + }, { + "schemaNodeId": None, + "featureSchemaId": None, + "required": False, + "name": "dot", + "color": "#FF0000", + "tool": "point", + "classifications": [] + }, { + "schemaNodeId": None, + "featureSchemaId": None, + "required": False, + "name": "polyline", + "color": "#FF0000", + "tool": "line", + "classifications": [] + }, { + "schemaNodeId": None, + "featureSchemaId": None, + "required": False, + "name": "ner", + "color": "#FF0000", + "tool": "named-entity", "classifications": [] }], "classifications": [{ + "schemaNodeId": None, + "featureSchemaId": None, "required": True, "instructions": "This is a question.", "name": "This is a question.", "type": "radio", "options": [{ + "schemaNodeId": None, + "featureSchemaId": None, "label": "yes", - "value": "yes" + "value": "yes", + "options": [] }, { + "schemaNodeId": None, + "featureSchemaId": None, "label": "no", - "value": "no" + "value": "no", + "options": [] }] }] } @pytest.mark.parametrize("tool_type", list(Tool.Type)) -@pytest.mark.parametrize("tool_name", ["tool"]) -def test_create_tool(tool_type, tool_name) -> None: - t = Tool(tool = tool_type, name = tool_name) +def test_create_tool(tool_type) -> None: + t = Tool(tool = tool_type, name = "tool") assert(t.tool == tool_type) - assert(t.name == tool_name) @pytest.mark.parametrize("class_type", list(Classification.Type)) -@pytest.mark.parametrize("class_instr", ["classification"]) -def test_create_classification(class_type, class_instr) -> None: - c = Classification(class_type = class_type, instructions = class_instr) +def test_create_classification(class_type) -> None: + c = Classification(class_type = class_type, instructions = "classification") assert(c.class_type == class_type) - assert(c.instructions == class_instr) - assert(c.name == c.instructions) @pytest.mark.parametrize( "value, expected_value, typing",[(3,3, int),("string","string", str)]) @@ -51,7 +132,6 @@ def test_create_option(value, expected_value, typing) -> None: o = Option(value = value) assert(o.value == expected_value) assert(o.value == o.label) - assert(type(o.value) == typing) def test_create_empty_ontology() -> None: o = Ontology() @@ -66,6 +146,9 @@ def test_add_ontology_tool() -> None: o.add_tool(second_tool) assert len(o.tools) == 2 + for tool in o.tools: + assert(type(tool) == Tool) + with pytest.raises(InconsistentOntologyException) as exc: o.add_tool(Tool(tool=Tool.Type.BBOX, name = "bounding box")) assert "Duplicate tool name" in str(exc.value) @@ -80,6 +163,9 @@ def test_add_ontology_classification() -> None: o.add_classification(second_classification) assert len(o.classifications) == 2 + for classification in o.classifications: + assert(type(classification) == Classification) + with pytest.raises(InconsistentOntologyException) as exc: o.add_classification(Classification( class_type = Classification.Type.TEXT, instructions = "text")) @@ -90,7 +176,7 @@ def test_tool_add_classification() -> None: c = Classification( class_type = Classification.Type.TEXT, instructions = "text") t.add_classification(c) - assert t.classifications[0] == c + assert t.classifications == [c] with pytest.raises(Exception) as exc: t.add_classification(c) @@ -101,7 +187,7 @@ def test_classification_add_option() -> None: class_type = Classification.Type.RADIO, instructions = "radio") o = Option(value = "option") c.add_option(o) - assert c.options[0] == o + assert c.options == [o] with pytest.raises(InconsistentOntologyException) as exc: c.add_option(Option(value = "option")) @@ -112,22 +198,15 @@ def test_option_add_option() -> None: c = Classification( class_type = Classification.Type.TEXT, instructions = "text") o.add_option(c) - assert o.options[0] == c + assert o.options == [c] with pytest.raises(InconsistentOntologyException) as exc: o.add_option(c) assert "Duplicate nested classification" in str(exc.value) def test_ontology_asdict(project) -> None: - o = Ontology.from_project(project) - assert o.asdict() == project.ontology().normalized + assert Ontology.from_dict(_SAMPLE_ONTOLOGY).asdict() == _SAMPLE_ONTOLOGY def test_from_project_ontology(client, project) -> None: - frontend = list(client.get_labeling_frontends( - where=LabelingFrontend.name == "Editor"))[0] - project.setup(frontend, _SAMPLE_ONTOLOGY) o = Ontology.from_project(project) - - assert o.tools[0].tool == Tool.Type.BBOX - assert o.classifications[0].class_type == Classification.Type.RADIO - assert o.classifications[0].options[0].value.lower() == "yes" \ No newline at end of file + assert o.asdict() == project.ontology().normalized \ No newline at end of file From c570276d94754f7153f659ea6cb5dedb83fb67c5 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Tue, 9 Mar 2021 20:12:56 -0800 Subject: [PATCH 29/36] making pep8 format --- labelbox/exceptions.py | 8 +- labelbox/schema/ontology_generator.py | 142 ++++++------- tests/integration/test_ontology.py | 276 +++++++++++++++----------- 3 files changed, 234 insertions(+), 192 deletions(-) diff --git a/labelbox/exceptions.py b/labelbox/exceptions.py index 14153586d..efd134023 100644 --- a/labelbox/exceptions.py +++ b/labelbox/exceptions.py @@ -1,6 +1,5 @@ class LabelboxError(Exception): """Base class for exceptions.""" - def __init__(self, message, cause=None): """ Args: @@ -28,7 +27,6 @@ class AuthorizationError(LabelboxError): class ResourceNotFoundError(LabelboxError): """Exception raised when a given resource is not found. """ - def __init__(self, db_object_type, params): """ Constructor. @@ -68,7 +66,6 @@ class InvalidQueryError(LabelboxError): class NetworkError(LabelboxError): """Raised when an HTTPError occurs.""" - def __init__(self, cause): super().__init__(str(cause), cause) self.cause = cause @@ -82,7 +79,6 @@ class TimeoutError(LabelboxError): class InvalidAttributeError(LabelboxError): """ Raised when a field (name or Field instance) is not valid or found for a specific DB object type. """ - def __init__(self, db_object_type, field): super().__init__("Field(s) '%r' not valid on DB type '%s'" % (field, db_object_type.type_name())) @@ -104,3 +100,7 @@ class MalformedQueryException(Exception): class UuidError(LabelboxError): """ Raised when there are repeat Uuid's in bulk import request.""" pass + + +class InconsistentOntologyException(Exception): + pass \ No newline at end of file diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py index f56c77379..e95487fb1 100644 --- a/labelbox/schema/ontology_generator.py +++ b/labelbox/schema/ontology_generator.py @@ -6,12 +6,9 @@ from labelbox import Client, Project, Dataset, LabelingFrontend -class InconsistentOntologyException(Exception): - pass - @dataclass class Option: - value: str + value: str schema_id: Optional[str] = None feature_schema_id: Optional[str] = None options: List["Classification"] = field(default_factory=list) @@ -19,23 +16,25 @@ class Option: @property def label(self): return self.value - + + @classmethod + def from_dict(cls, dictionary: Dict[str, Any]): + return Option(value=dictionary["value"], + schema_id=dictionary["schemaNodeId"], + feature_schema_id=dictionary["featureSchemaId"], + options=[ + Classification.from_dict(o) + for o in dictionary.get("options", []) + ]) + def asdict(self) -> Dict[str, Any]: return { "schemaNodeId": self.schema_id, "featureSchemaId": self.feature_schema_id, "label": self.label, "value": self.value, - "options": [o.asdict() for o in self.options]} - - @classmethod - def from_dict(cls, dictionary: Dict[str,Any]): - return Option( - value = dictionary["value"], - schema_id = dictionary["schemaNodeId"], - feature_schema_id = dictionary["featureSchemaId"], - options = [Classification.from_dict(o) - for o in dictionary.get("options", [])]) + "options": [o.asdict() for o in self.options] + } def add_option(self, option: 'Classification') -> 'Classification': if option.instructions in (o.instructions for o in self.options): @@ -43,10 +42,10 @@ def add_option(self, option: 'Classification') -> 'Classification': f"Duplicate nested classification '{option.instructions}' " f"for option '{self.label}'") self.options.append(option) - -@dataclass -class Classification: + +@dataclass +class Classification: class Type(Enum): TEXT = "text" CHECKLIST = "checklist" @@ -66,9 +65,19 @@ class Type(Enum): def name(self): return self.instructions - def asdict(self) -> Dict[str,Any]: + @classmethod + def from_dict(cls, dictionary: Dict[str, Any]): + return Classification( + class_type=Classification.Type(dictionary["type"]), + instructions=dictionary["instructions"], + required=dictionary["required"], + options=[Option.from_dict(o) for o in dictionary["options"]], + schema_id=dictionary["schemaNodeId"], + feature_schema_id=dictionary["schemaNodeId"]) + + def asdict(self) -> Dict[str, Any]: if self.class_type in Classification._REQUIRES_OPTIONS \ - and len(self.options) < 1: + and len(self.options) < 1: raise InconsistentOntologyException( f"Classification '{self.instructions}' requires options.") return { @@ -78,17 +87,8 @@ def asdict(self) -> Dict[str,Any]: "required": self.required, "options": [o.asdict() for o in self.options], "schemaNodeId": self.schema_id, - "featureSchemaId": self.feature_schema_id} - - @classmethod - def from_dict(cls, dictionary: Dict[str,Any]): - return Classification( - class_type = Classification.Type(dictionary["type"]), - instructions = dictionary["instructions"], - required = dictionary["required"], - options = [Option.from_dict(o) for o in dictionary["options"]], - schema_id = dictionary["schemaNodeId"], - feature_schema_id = dictionary["schemaNodeId"]) + "featureSchemaId": self.feature_schema_id + } def add_option(self, option: Option): if option.value in (o.value for o in self.options): @@ -97,9 +97,9 @@ def add_option(self, option: Option): f"for classification '{self.name}'.") self.options.append(option) + @dataclass class Tool: - class Type(Enum): POLYGON = "polygon" SEGMENTATION = "superpixel" @@ -108,15 +108,28 @@ class Type(Enum): LINE = "line" NER = "named-entity" - tool: Type - name: str + tool: Type + name: str required: bool = False color: str = "#000000" classifications: List[Classification] = field(default_factory=list) schema_id: Optional[str] = None feature_schema_id: Optional[str] = None - def asdict(self) -> Dict[str,Any]: + @classmethod + def from_dict(cls, dictionary: Dict[str, Any]): + return Tool(name=dictionary['name'], + schema_id=dictionary["schemaNodeId"], + feature_schema_id=dictionary["featureSchemaId"], + required=dictionary["required"], + tool=Tool.Type(dictionary["tool"]), + classifications=[ + Classification.from_dict(c) + for c in dictionary["classifications"] + ], + color=dictionary["color"]) + + def asdict(self) -> Dict[str, Any]: return { "tool": self.tool.value, "name": self.name, @@ -124,59 +137,54 @@ def asdict(self) -> Dict[str,Any]: "color": self.color, "classifications": [c.asdict() for c in self.classifications], "schemaNodeId": self.schema_id, - "featureSchemaId": self.feature_schema_id} - - @classmethod - def from_dict(cls, dictionary: Dict[str,Any]): - return Tool( - name = dictionary['name'], - schema_id = dictionary["schemaNodeId"], - feature_schema_id = dictionary["featureSchemaId"], - required = dictionary["required"], - tool = Tool.Type(dictionary["tool"]), - classifications = [Classification.from_dict(c) - for c in dictionary["classifications"]], - color = dictionary["color"]) + "featureSchemaId": self.feature_schema_id + } def add_classification(self, classification: Classification): - if classification.instructions in (c.instructions + if classification.instructions in (c.instructions for c in self.classifications): raise InconsistentOntologyException( f"Duplicate nested classification '{classification.instructions}' " f"for tool '{self.name}'") - self.classifications.append(classification) + self.classifications.append(classification) + @dataclass class Ontology: - + tools: List[Tool] = field(default_factory=list) classifications: List[Classification] = field(default_factory=list) + @classmethod + def from_dict(cls, dictionary: Dict[str, Any]): + return Ontology(tools=[Tool.from_dict(t) for t in dictionary["tools"]], + classifications=[ + Classification.from_dict(c) + for c in dictionary["classifications"] + ]) + + def asdict(self): + return { + "tools": [t.asdict() for t in self.tools], + "classifications": [c.asdict() for c in self.classifications] + } + @classmethod def from_project(cls, project: Project): ontology = project.ontology().normalized return Ontology.from_dict(ontology) - def add_tool(self, tool: Tool) -> Tool: + def add_tool(self, tool: Tool) -> Tool: if tool.name in (t.name for t in self.tools): raise InconsistentOntologyException( f"Duplicate tool name '{tool.name}'. ") self.tools.append(tool) - - def add_classification(self, classification: Classification) -> Classification: - if classification.instructions in (c.instructions + + def add_classification(self, + classification: Classification) -> Classification: + if classification.instructions in (c.instructions for c in self.classifications): raise InconsistentOntologyException( - f"Duplicate classification instructions '{classification.instructions}'. ") + f"Duplicate classification instructions '{classification.instructions}'. " + ) self.classifications.append(classification) - - def asdict(self): - return { - "tools": [t.asdict() for t in self.tools], - "classifications": [c.asdict() for c in self.classifications]} - - @classmethod - def from_dict(cls, dictionary: Dict[str,Any]): - return Ontology( - tools = [Tool.from_dict(t) for t in dictionary["tools"]], - classifications = [Classification.from_dict(c) for c in dictionary["classifications"]]) \ No newline at end of file diff --git a/tests/integration/test_ontology.py b/tests/integration/test_ontology.py index 47686c294..5de15f488 100644 --- a/tests/integration/test_ontology.py +++ b/tests/integration/test_ontology.py @@ -6,175 +6,205 @@ from labelbox.schema.ontology_generator import Ontology, \ Tool, Classification, Option, InconsistentOntologyException - _SAMPLE_ONTOLOGY = { - "tools": [{ - "schemaNodeId": None, - "featureSchemaId": None, - "required": False, - "name": "poly", - "color": "#FF0000", - "tool": "polygon", - "classifications": [] - }, { - "schemaNodeId": None, - "featureSchemaId": None, - "required": False, - "name": "segment", - "color": "#FF0000", - "tool": "superpixel", - "classifications": [] - }, { - "schemaNodeId": None, - "featureSchemaId": None, - "required": False, - "name": "bbox", - "color": "#FF0000", - "tool": "rectangle", - "classifications": [{ - "schemaNodeId": None, - "featureSchemaId": None, - "required": True, - "instructions": "nested classification", - "name": "nested classification", - "type": "radio", + "tools": [{ + "schemaNodeId": None, + "featureSchemaId": None, + "required": False, + "name": "poly", + "color": "#FF0000", + "tool": "polygon", + "classifications": [] + }, { + "schemaNodeId": None, + "featureSchemaId": None, + "required": False, + "name": "segment", + "color": "#FF0000", + "tool": "superpixel", + "classifications": [] + }, { + "schemaNodeId": + None, + "featureSchemaId": + None, + "required": + False, + "name": + "bbox", + "color": + "#FF0000", + "tool": + "rectangle", + "classifications": [{ + "schemaNodeId": + None, + "featureSchemaId": + None, + "required": + True, + "instructions": + "nested classification", + "name": + "nested classification", + "type": + "radio", + "options": [{ + "schemaNodeId": + None, + "featureSchemaId": + None, + "label": + "first", + "value": + "first", "options": [{ "schemaNodeId": None, "featureSchemaId": None, - "label": "first", - "value": "first", - "options": [{ - "schemaNodeId": None, - "featureSchemaId": None, - "required": False, - "instructions": "nested nested text", - "name": "nested nested text", - "type": "text", - "options": [] - }] - }, { - "schemaNodeId": None, - "featureSchemaId": None, - "label": "second", - "value": "second", + "required": False, + "instructions": "nested nested text", + "name": "nested nested text", + "type": "text", "options": [] }] }, { "schemaNodeId": None, "featureSchemaId": None, - "required": True, - "instructions": "nested text", - "name": "nested text", - "type": "text", + "label": "second", + "value": "second", "options": [] }] }, { "schemaNodeId": None, "featureSchemaId": None, - "required": False, - "name": "dot", - "color": "#FF0000", - "tool": "point", - "classifications": [] - }, { + "required": True, + "instructions": "nested text", + "name": "nested text", + "type": "text", + "options": [] + }] + }, { + "schemaNodeId": None, + "featureSchemaId": None, + "required": False, + "name": "dot", + "color": "#FF0000", + "tool": "point", + "classifications": [] + }, { + "schemaNodeId": None, + "featureSchemaId": None, + "required": False, + "name": "polyline", + "color": "#FF0000", + "tool": "line", + "classifications": [] + }, { + "schemaNodeId": None, + "featureSchemaId": None, + "required": False, + "name": "ner", + "color": "#FF0000", + "tool": "named-entity", + "classifications": [] + }], + "classifications": [{ + "schemaNodeId": + None, + "featureSchemaId": + None, + "required": + True, + "instructions": + "This is a question.", + "name": + "This is a question.", + "type": + "radio", + "options": [{ "schemaNodeId": None, "featureSchemaId": None, - "required": False, - "name": "polyline", - "color": "#FF0000", - "tool": "line", - "classifications": [] + "label": "yes", + "value": "yes", + "options": [] }, { "schemaNodeId": None, "featureSchemaId": None, - "required": False, - "name": "ner", - "color": "#FF0000", - "tool": "named-entity", - "classifications": [] - }], - "classifications": [{ - "schemaNodeId": None, - "featureSchemaId": None, - "required": True, - "instructions": "This is a question.", - "name": "This is a question.", - "type": "radio", - "options": [{ - "schemaNodeId": None, - "featureSchemaId": None, - "label": "yes", - "value": "yes", - "options": [] - }, { - "schemaNodeId": None, - "featureSchemaId": None, - "label": "no", - "value": "no", - "options": [] - }] + "label": "no", + "value": "no", + "options": [] }] - } + }] +} + @pytest.mark.parametrize("tool_type", list(Tool.Type)) def test_create_tool(tool_type) -> None: - t = Tool(tool = tool_type, name = "tool") - assert(t.tool == tool_type) + t = Tool(tool=tool_type, name="tool") + assert (t.tool == tool_type) + @pytest.mark.parametrize("class_type", list(Classification.Type)) def test_create_classification(class_type) -> None: - c = Classification(class_type = class_type, instructions = "classification") - assert(c.class_type == class_type) + c = Classification(class_type=class_type, instructions="classification") + assert (c.class_type == class_type) + -@pytest.mark.parametrize( - "value, expected_value, typing",[(3,3, int),("string","string", str)]) +@pytest.mark.parametrize("value, expected_value, typing", + [(3, 3, int), ("string", "string", str)]) def test_create_option(value, expected_value, typing) -> None: - o = Option(value = value) - assert(o.value == expected_value) - assert(o.value == o.label) + o = Option(value=value) + assert (o.value == expected_value) + assert (o.value == o.label) + def test_create_empty_ontology() -> None: o = Ontology() - assert(o.tools == []) - assert(o.classifications == []) + assert (o.tools == []) + assert (o.classifications == []) + def test_add_ontology_tool() -> None: o = Ontology() - o.add_tool(Tool(tool = Tool.Type.BBOX, name = "bounding box")) + o.add_tool(Tool(tool=Tool.Type.BBOX, name="bounding box")) - second_tool = Tool(tool = Tool.Type.SEGMENTATION, name = "segmentation") + second_tool = Tool(tool=Tool.Type.SEGMENTATION, name="segmentation") o.add_tool(second_tool) assert len(o.tools) == 2 for tool in o.tools: - assert(type(tool) == Tool) + assert (type(tool) == Tool) with pytest.raises(InconsistentOntologyException) as exc: - o.add_tool(Tool(tool=Tool.Type.BBOX, name = "bounding box")) + o.add_tool(Tool(tool=Tool.Type.BBOX, name="bounding box")) assert "Duplicate tool name" in str(exc.value) + def test_add_ontology_classification() -> None: o = Ontology() - o.add_classification(Classification( - class_type = Classification.Type.TEXT, instructions = "text")) + o.add_classification( + Classification(class_type=Classification.Type.TEXT, + instructions="text")) second_classification = Classification( - class_type = Classification.Type.CHECKLIST, instructions = "checklist") + class_type=Classification.Type.CHECKLIST, instructions="checklist") o.add_classification(second_classification) assert len(o.classifications) == 2 for classification in o.classifications: - assert(type(classification) == Classification) + assert (type(classification) == Classification) with pytest.raises(InconsistentOntologyException) as exc: - o.add_classification(Classification( - class_type = Classification.Type.TEXT, instructions = "text")) + o.add_classification( + Classification(class_type=Classification.Type.TEXT, + instructions="text")) assert "Duplicate classification instructions" in str(exc.value) + def test_tool_add_classification() -> None: - t = Tool(tool = Tool.Type.SEGMENTATION, name = "segmentation") - c = Classification( - class_type = Classification.Type.TEXT, instructions = "text") + t = Tool(tool=Tool.Type.SEGMENTATION, name="segmentation") + c = Classification(class_type=Classification.Type.TEXT, + instructions="text") t.add_classification(c) assert t.classifications == [c] @@ -182,31 +212,35 @@ def test_tool_add_classification() -> None: t.add_classification(c) assert "Duplicate nested classification" in str(exc) + def test_classification_add_option() -> None: - c = Classification( - class_type = Classification.Type.RADIO, instructions = "radio") - o = Option(value = "option") + c = Classification(class_type=Classification.Type.RADIO, + instructions="radio") + o = Option(value="option") c.add_option(o) assert c.options == [o] with pytest.raises(InconsistentOntologyException) as exc: - c.add_option(Option(value = "option")) + c.add_option(Option(value="option")) assert "Duplicate option" in str(exc.value) + def test_option_add_option() -> None: - o = Option(value = "option") - c = Classification( - class_type = Classification.Type.TEXT, instructions = "text") + o = Option(value="option") + c = Classification(class_type=Classification.Type.TEXT, + instructions="text") o.add_option(c) assert o.options == [c] with pytest.raises(InconsistentOntologyException) as exc: o.add_option(c) - assert "Duplicate nested classification" in str(exc.value) + assert "Duplicate nested classification" in str(exc.value) + def test_ontology_asdict(project) -> None: assert Ontology.from_dict(_SAMPLE_ONTOLOGY).asdict() == _SAMPLE_ONTOLOGY + def test_from_project_ontology(client, project) -> None: o = Ontology.from_project(project) assert o.asdict() == project.ontology().normalized \ No newline at end of file From 0a7c60318d61d53f80911a4da742edc0fc55ef82 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Tue, 9 Mar 2021 20:52:06 -0800 Subject: [PATCH 30/36] updating ontology classes to ontology file --- labelbox/schema/ontology.py | 206 +++++++++++++++++++++----- labelbox/schema/ontology_generator.py | 190 ------------------------ tests/integration/test_ontology.py | 16 +- 3 files changed, 179 insertions(+), 233 deletions(-) delete mode 100644 labelbox/schema/ontology_generator.py diff --git a/labelbox/schema/ontology.py b/labelbox/schema/ontology.py index 9b28f6d01..694481a1a 100644 --- a/labelbox/schema/ontology.py +++ b/labelbox/schema/ontology.py @@ -1,66 +1,158 @@ import abc -from dataclasses import dataclass +from dataclasses import dataclass, field +from enum import Enum, auto from typing import Any, Callable, Dict, List, Optional, Union +from labelbox.schema.project import Project from labelbox.orm import query from labelbox.orm.db_object import DbObject, Updateable, BulkDeletable from labelbox.orm.model import Entity, Field, Relationship from labelbox.utils import snake_case, camel_case - - -@dataclass -class OntologyEntity: - required: bool - name: str +from labelbox.exceptions import InconsistentOntologyException @dataclass class Option: - label: str value: str + schema_id: Optional[str] = None feature_schema_id: Optional[str] = None - schema_node_id: Optional[str] = None + options: List["Classification"] = field(default_factory=list) + + @property + def label(self): + return self.value @classmethod - def from_json(cls, json_dict): - _dict = convert_keys(json_dict, snake_case) - return cls(**_dict) + def from_dict(cls, dictionary: Dict[str, Any]): + return Option(value=dictionary["value"], + schema_id=dictionary["schemaNodeId"], + feature_schema_id=dictionary["featureSchemaId"], + options=[ + Classification.from_dict(o) + for o in dictionary.get("options", []) + ]) + + def asdict(self) -> Dict[str, Any]: + return { + "schemaNodeId": self.schema_id, + "featureSchemaId": self.feature_schema_id, + "label": self.label, + "value": self.value, + "options": [o.asdict() for o in self.options] + } + + def add_option(self, option: 'Classification') -> 'Classification': + if option.instructions in (o.instructions for o in self.options): + raise InconsistentOntologyException( + f"Duplicate nested classification '{option.instructions}' " + f"for option '{self.label}'") + self.options.append(option) @dataclass -class Classification(OntologyEntity): - type: str +class Classification: + class Type(Enum): + TEXT = "text" + CHECKLIST = "checklist" + RADIO = "radio" + DROPDOWN = "dropdown" + + _REQUIRES_OPTIONS = {Type.CHECKLIST, Type.RADIO, Type.DROPDOWN} + + class_type: Type instructions: str - options: List[Option] + required: bool = False + options: List[Option] = field(default_factory=list) + schema_id: Optional[str] = None feature_schema_id: Optional[str] = None - schema_node_id: Optional[str] = None + + @property + def name(self): + return self.instructions @classmethod - def from_json(cls, json_dict): - _dict = convert_keys(json_dict, snake_case) - _dict['options'] = [ - Option.from_json(option) for option in _dict['options'] - ] - return cls(**_dict) + def from_dict(cls, dictionary: Dict[str, Any]): + return Classification( + class_type=Classification.Type(dictionary["type"]), + instructions=dictionary["instructions"], + required=dictionary["required"], + options=[Option.from_dict(o) for o in dictionary["options"]], + schema_id=dictionary["schemaNodeId"], + feature_schema_id=dictionary["schemaNodeId"]) + + def asdict(self) -> Dict[str, Any]: + if self.class_type in Classification._REQUIRES_OPTIONS \ + and len(self.options) < 1: + raise InconsistentOntologyException( + f"Classification '{self.instructions}' requires options.") + return { + "type": self.class_type.value, + "instructions": self.instructions, + "name": self.name, + "required": self.required, + "options": [o.asdict() for o in self.options], + "schemaNodeId": self.schema_id, + "featureSchemaId": self.feature_schema_id + } + + def add_option(self, option: Option): + if option.value in (o.value for o in self.options): + raise InconsistentOntologyException( + f"Duplicate option '{option.value}' " + f"for classification '{self.name}'.") + self.options.append(option) @dataclass -class Tool(OntologyEntity): - tool: str - color: str - classifications: List[Classification] +class Tool: + class Type(Enum): + POLYGON = "polygon" + SEGMENTATION = "superpixel" + POINT = "point" + BBOX = "rectangle" + LINE = "line" + NER = "named-entity" + + tool: Type + name: str + required: bool = False + color: str = "#000000" + classifications: List[Classification] = field(default_factory=list) + schema_id: Optional[str] = None feature_schema_id: Optional[str] = None - schema_node_id: Optional[str] = None @classmethod - def from_json(cls, json_dict): - _dict = convert_keys(json_dict, snake_case) - _dict['classifications'] = [ - Classification.from_json(classification) - for classification in _dict['classifications'] - ] - return cls(**_dict) + def from_dict(cls, dictionary: Dict[str, Any]): + return Tool(name=dictionary['name'], + schema_id=dictionary["schemaNodeId"], + feature_schema_id=dictionary["featureSchemaId"], + required=dictionary["required"], + tool=Tool.Type(dictionary["tool"]), + classifications=[ + Classification.from_dict(c) + for c in dictionary["classifications"] + ], + color=dictionary["color"]) + + def asdict(self) -> Dict[str, Any]: + return { + "tool": self.tool.value, + "name": self.name, + "required": self.required, + "color": self.color, + "classifications": [c.asdict() for c in self.classifications], + "schemaNodeId": self.schema_id, + "featureSchemaId": self.feature_schema_id + } + + def add_classification(self, classification: Classification): + if classification.instructions in (c.instructions + for c in self.classifications): + raise InconsistentOntologyException( + f"Duplicate nested classification '{classification.instructions}' " + f"for tool '{self.name}'") + self.classifications.append(classification) class Ontology(DbObject): @@ -100,7 +192,7 @@ def tools(self) -> List[Tool]: """Get list of tools (AKA objects) in an Ontology.""" if self._tools is None: self._tools = [ - Tool.from_json(tool) for tool in self.normalized['tools'] + Tool.from_dict(tool) for tool in self.normalized['tools'] ] return self._tools # type: ignore @@ -108,7 +200,7 @@ def classifications(self) -> List[Classification]: """Get list of classifications in an Ontology.""" if self._classifications is None: self._classifications = [ - Classification.from_json(classification) + Classification.from_dict(classification) for classification in self.normalized['classifications'] ] return self._classifications # type: ignore @@ -124,3 +216,45 @@ def convert_keys(json_dict: Dict[str, Any], if isinstance(json_dict, list): return [convert_keys(ele, converter) for ele in json_dict] return json_dict + + +@dataclass +class OntologyBuilder: + + tools: List[Tool] = field(default_factory=list) + classifications: List[Classification] = field(default_factory=list) + + @classmethod + def from_dict(cls, dictionary: Dict[str, Any]): + return OntologyBuilder( + tools=[Tool.from_dict(t) for t in dictionary["tools"]], + classifications=[ + Classification.from_dict(c) + for c in dictionary["classifications"] + ]) + + def asdict(self): + return { + "tools": [t.asdict() for t in self.tools], + "classifications": [c.asdict() for c in self.classifications] + } + + @classmethod + def from_project(cls, project: Project): + ontology = project.ontology().normalized + return OntologyBuilder.from_dict(ontology) + + def add_tool(self, tool: Tool) -> Tool: + if tool.name in (t.name for t in self.tools): + raise InconsistentOntologyException( + f"Duplicate tool name '{tool.name}'. ") + self.tools.append(tool) + + def add_classification(self, + classification: Classification) -> Classification: + if classification.instructions in (c.instructions + for c in self.classifications): + raise InconsistentOntologyException( + f"Duplicate classification instructions '{classification.instructions}'. " + ) + self.classifications.append(classification) diff --git a/labelbox/schema/ontology_generator.py b/labelbox/schema/ontology_generator.py deleted file mode 100644 index e95487fb1..000000000 --- a/labelbox/schema/ontology_generator.py +++ /dev/null @@ -1,190 +0,0 @@ -from dataclasses import dataclass, field -from enum import Enum, auto -import os -from typing import List, Optional, Dict, Any - -from labelbox import Client, Project, Dataset, LabelingFrontend - - -@dataclass -class Option: - value: str - schema_id: Optional[str] = None - feature_schema_id: Optional[str] = None - options: List["Classification"] = field(default_factory=list) - - @property - def label(self): - return self.value - - @classmethod - def from_dict(cls, dictionary: Dict[str, Any]): - return Option(value=dictionary["value"], - schema_id=dictionary["schemaNodeId"], - feature_schema_id=dictionary["featureSchemaId"], - options=[ - Classification.from_dict(o) - for o in dictionary.get("options", []) - ]) - - def asdict(self) -> Dict[str, Any]: - return { - "schemaNodeId": self.schema_id, - "featureSchemaId": self.feature_schema_id, - "label": self.label, - "value": self.value, - "options": [o.asdict() for o in self.options] - } - - def add_option(self, option: 'Classification') -> 'Classification': - if option.instructions in (o.instructions for o in self.options): - raise InconsistentOntologyException( - f"Duplicate nested classification '{option.instructions}' " - f"for option '{self.label}'") - self.options.append(option) - - -@dataclass -class Classification: - class Type(Enum): - TEXT = "text" - CHECKLIST = "checklist" - RADIO = "radio" - DROPDOWN = "dropdown" - - _REQUIRES_OPTIONS = {Type.CHECKLIST, Type.RADIO, Type.DROPDOWN} - - class_type: Type - instructions: str - required: bool = False - options: List[Option] = field(default_factory=list) - schema_id: Optional[str] = None - feature_schema_id: Optional[str] = None - - @property - def name(self): - return self.instructions - - @classmethod - def from_dict(cls, dictionary: Dict[str, Any]): - return Classification( - class_type=Classification.Type(dictionary["type"]), - instructions=dictionary["instructions"], - required=dictionary["required"], - options=[Option.from_dict(o) for o in dictionary["options"]], - schema_id=dictionary["schemaNodeId"], - feature_schema_id=dictionary["schemaNodeId"]) - - def asdict(self) -> Dict[str, Any]: - if self.class_type in Classification._REQUIRES_OPTIONS \ - and len(self.options) < 1: - raise InconsistentOntologyException( - f"Classification '{self.instructions}' requires options.") - return { - "type": self.class_type.value, - "instructions": self.instructions, - "name": self.name, - "required": self.required, - "options": [o.asdict() for o in self.options], - "schemaNodeId": self.schema_id, - "featureSchemaId": self.feature_schema_id - } - - def add_option(self, option: Option): - if option.value in (o.value for o in self.options): - raise InconsistentOntologyException( - f"Duplicate option '{option.value}' " - f"for classification '{self.name}'.") - self.options.append(option) - - -@dataclass -class Tool: - class Type(Enum): - POLYGON = "polygon" - SEGMENTATION = "superpixel" - POINT = "point" - BBOX = "rectangle" - LINE = "line" - NER = "named-entity" - - tool: Type - name: str - required: bool = False - color: str = "#000000" - classifications: List[Classification] = field(default_factory=list) - schema_id: Optional[str] = None - feature_schema_id: Optional[str] = None - - @classmethod - def from_dict(cls, dictionary: Dict[str, Any]): - return Tool(name=dictionary['name'], - schema_id=dictionary["schemaNodeId"], - feature_schema_id=dictionary["featureSchemaId"], - required=dictionary["required"], - tool=Tool.Type(dictionary["tool"]), - classifications=[ - Classification.from_dict(c) - for c in dictionary["classifications"] - ], - color=dictionary["color"]) - - def asdict(self) -> Dict[str, Any]: - return { - "tool": self.tool.value, - "name": self.name, - "required": self.required, - "color": self.color, - "classifications": [c.asdict() for c in self.classifications], - "schemaNodeId": self.schema_id, - "featureSchemaId": self.feature_schema_id - } - - def add_classification(self, classification: Classification): - if classification.instructions in (c.instructions - for c in self.classifications): - raise InconsistentOntologyException( - f"Duplicate nested classification '{classification.instructions}' " - f"for tool '{self.name}'") - self.classifications.append(classification) - - -@dataclass -class Ontology: - - tools: List[Tool] = field(default_factory=list) - classifications: List[Classification] = field(default_factory=list) - - @classmethod - def from_dict(cls, dictionary: Dict[str, Any]): - return Ontology(tools=[Tool.from_dict(t) for t in dictionary["tools"]], - classifications=[ - Classification.from_dict(c) - for c in dictionary["classifications"] - ]) - - def asdict(self): - return { - "tools": [t.asdict() for t in self.tools], - "classifications": [c.asdict() for c in self.classifications] - } - - @classmethod - def from_project(cls, project: Project): - ontology = project.ontology().normalized - return Ontology.from_dict(ontology) - - def add_tool(self, tool: Tool) -> Tool: - if tool.name in (t.name for t in self.tools): - raise InconsistentOntologyException( - f"Duplicate tool name '{tool.name}'. ") - self.tools.append(tool) - - def add_classification(self, - classification: Classification) -> Classification: - if classification.instructions in (c.instructions - for c in self.classifications): - raise InconsistentOntologyException( - f"Duplicate classification instructions '{classification.instructions}'. " - ) - self.classifications.append(classification) diff --git a/tests/integration/test_ontology.py b/tests/integration/test_ontology.py index 5de15f488..50e267ddd 100644 --- a/tests/integration/test_ontology.py +++ b/tests/integration/test_ontology.py @@ -3,8 +3,9 @@ import pytest from labelbox import LabelingFrontend -from labelbox.schema.ontology_generator import Ontology, \ - Tool, Classification, Option, InconsistentOntologyException +from labelbox.exceptions import InconsistentOntologyException +from labelbox.schema.ontology import Tool, Classification, Option, \ + Ontology, OntologyBuilder _SAMPLE_ONTOLOGY = { "tools": [{ @@ -159,13 +160,13 @@ def test_create_option(value, expected_value, typing) -> None: def test_create_empty_ontology() -> None: - o = Ontology() + o = OntologyBuilder() assert (o.tools == []) assert (o.classifications == []) def test_add_ontology_tool() -> None: - o = Ontology() + o = OntologyBuilder() o.add_tool(Tool(tool=Tool.Type.BBOX, name="bounding box")) second_tool = Tool(tool=Tool.Type.SEGMENTATION, name="segmentation") @@ -181,7 +182,7 @@ def test_add_ontology_tool() -> None: def test_add_ontology_classification() -> None: - o = Ontology() + o = OntologyBuilder() o.add_classification( Classification(class_type=Classification.Type.TEXT, instructions="text")) @@ -238,9 +239,10 @@ def test_option_add_option() -> None: def test_ontology_asdict(project) -> None: - assert Ontology.from_dict(_SAMPLE_ONTOLOGY).asdict() == _SAMPLE_ONTOLOGY + assert OntologyBuilder.from_dict( + _SAMPLE_ONTOLOGY).asdict() == _SAMPLE_ONTOLOGY def test_from_project_ontology(client, project) -> None: - o = Ontology.from_project(project) + o = OntologyBuilder.from_project(project) assert o.asdict() == project.ontology().normalized \ No newline at end of file From 6509054362bf8edb029276a19b66bd106267ee47 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Wed, 10 Mar 2021 17:02:00 -0800 Subject: [PATCH 31/36] added docstrings and method to create colors if empty --- labelbox/schema/ontology.py | 139 ++++++++++++++++++++++++++++++------ 1 file changed, 117 insertions(+), 22 deletions(-) diff --git a/labelbox/schema/ontology.py b/labelbox/schema/ontology.py index 694481a1a..129d40d4a 100644 --- a/labelbox/schema/ontology.py +++ b/labelbox/schema/ontology.py @@ -1,6 +1,7 @@ import abc from dataclasses import dataclass, field from enum import Enum, auto +import colorsys from typing import Any, Callable, Dict, List, Optional, Union @@ -14,7 +15,22 @@ @dataclass class Option: - value: str + """ + An option is a possible answer within a Classification object in + a Project's ontology. + + To instantiate, only the "value" parameter needs to be passed in. + + Example(s): + option = Option(value = "Option Example") + + Attributes: + value: (str) + schema_id: (str) + feature_schema_id: (str) + options: (list) + """ + value: Union[str, int] schema_id: Optional[str] = None feature_schema_id: Optional[str] = None options: List["Classification"] = field(default_factory=list) @@ -26,8 +42,8 @@ def label(self): @classmethod def from_dict(cls, dictionary: Dict[str, Any]): return Option(value=dictionary["value"], - schema_id=dictionary["schemaNodeId"], - feature_schema_id=dictionary["featureSchemaId"], + schema_id=dictionary.get("schemaNodeId", []), + feature_schema_id=dictionary.get("featureSchemaId", []), options=[ Classification.from_dict(o) for o in dictionary.get("options", []) @@ -52,6 +68,36 @@ def add_option(self, option: 'Classification') -> 'Classification': @dataclass class Classification: + """ + A classfication to be added to a Project's ontology. The + classification is dependent on the Classification Type. + + To instantiate, the "class_type" and "instructions" parameters must + be passed in. + + The "options" parameter holds a list of Option objects. This is not + necessary for some Classification types, such as TEXT. To see which + types require options, look at the "_REQUIRES_OPTIONS" class variable. + + Example(s): + classification = Classification( + class_type = Classification.Type.TEXT, + instructions = "Classification Example") + + classification_two = Classification( + class_type = Classification.Type.RADIO, + instructions = "Second Example") + classification_two.add_option(Option( + value = "Option Example")) + + Attributes: + class_type: (Classification.Type) + instructions: (str) + required: (bool) + options: (list) + schema_id: (str) + feature_schema_id: (str) + """ class Type(Enum): TEXT = "text" CHECKLIST = "checklist" @@ -78,8 +124,8 @@ def from_dict(cls, dictionary: Dict[str, Any]): instructions=dictionary["instructions"], required=dictionary["required"], options=[Option.from_dict(o) for o in dictionary["options"]], - schema_id=dictionary["schemaNodeId"], - feature_schema_id=dictionary["schemaNodeId"]) + schema_id=dictionary.get("schemaNodeId", []), + feature_schema_id=dictionary.get("featureSchemaId", [])) def asdict(self) -> Dict[str, Any]: if self.class_type in Classification._REQUIRES_OPTIONS \ @@ -106,6 +152,34 @@ def add_option(self, option: Option): @dataclass class Tool: + """ + A tool to be added to a Project's ontology. The tool is + dependent on the Tool Type. + + To instantiate, the "tool" and "name" parameters must + be passed in. + + The "classifications" parameter holds a list of Classification objects. + This can be used to add nested classifications to a tool. + + Example(s): + tool = Tool( + tool = Tool.Type.LINE, + name = "Tool example") + classification = Classification( + class_type = Classification.Type.TEXT, + instructions = "Classification Example") + tool.add_classification(classification) + + Attributes: + tool: (Tool.Type) + name: (str) + required: (bool) + color: (str) + classifications: (list) + schema_id: (str) + feature_schema_id: (str) + """ class Type(Enum): POLYGON = "polygon" SEGMENTATION = "superpixel" @@ -125,8 +199,8 @@ class Type(Enum): @classmethod def from_dict(cls, dictionary: Dict[str, Any]): return Tool(name=dictionary['name'], - schema_id=dictionary["schemaNodeId"], - feature_schema_id=dictionary["featureSchemaId"], + schema_id=dictionary.get("schemaNodeId", []), + feature_schema_id=dictionary.get("featureSchemaId", []), required=dictionary["required"], tool=Tool.Type(dictionary["tool"]), classifications=[ @@ -194,7 +268,7 @@ def tools(self) -> List[Tool]: self._tools = [ Tool.from_dict(tool) for tool in self.normalized['tools'] ] - return self._tools # type: ignore + return self._tools def classifications(self) -> List[Classification]: """Get list of classifications in an Ontology.""" @@ -203,24 +277,34 @@ def classifications(self) -> List[Classification]: Classification.from_dict(classification) for classification in self.normalized['classifications'] ] - return self._classifications # type: ignore - - -def convert_keys(json_dict: Dict[str, Any], - converter: Callable) -> Dict[str, Any]: - if isinstance(json_dict, dict): - return { - converter(key): convert_keys(value, converter) - for key, value in json_dict.items() - } - if isinstance(json_dict, list): - return [convert_keys(ele, converter) for ele in json_dict] - return json_dict + return self._classifications @dataclass class OntologyBuilder: + """ + A class to help create an ontology for a Project. This should be used + for making Project ontologies from scratch. OntologyBuilder can also + pull from an already existing Project's ontology. + There are no required instantiation arguments. + + To create an ontology, use the asdict() method after fully building your + ontology within this class, and inserting it into project.setup() as the + "labeling_frontend_options" parameter. + + Example: + builder = OntologyBuilder() + ... + frontend = list(client.get_labeling_frontends())[0] + project.setup(frontend, builder.asdict()) + + attributes: + tools: (list) + classifications: (list) + + + """ tools: List[Tool] = field(default_factory=list) classifications: List[Classification] = field(default_factory=list) @@ -234,11 +318,22 @@ def from_dict(cls, dictionary: Dict[str, Any]): ]) def asdict(self): + self._update_colors() return { "tools": [t.asdict() for t in self.tools], "classifications": [c.asdict() for c in self.classifications] } + def _update_colors(self): + num_tools = len(self.tools) + + for index in range(num_tools): + hsv_color = (index * 1 / num_tools, 1, 1) + rgb_color = tuple( + int(255 * x) for x in colorsys.hsv_to_rgb(*hsv_color)) + if self.tools[index].color is None: + self.tools[index].color = '#%02x%02x%02x' % rgb_color + @classmethod def from_project(cls, project: Project): ontology = project.ontology().normalized @@ -257,4 +352,4 @@ def add_classification(self, raise InconsistentOntologyException( f"Duplicate classification instructions '{classification.instructions}'. " ) - self.classifications.append(classification) + self.classifications.append(classification) \ No newline at end of file From 6f523c23b164bee80da445c4149d97d8af4fc7e9 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Wed, 10 Mar 2021 17:06:30 -0800 Subject: [PATCH 32/36] added docstrings and method to create colors if empty --- labelbox/schema/ontology.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labelbox/schema/ontology.py b/labelbox/schema/ontology.py index 129d40d4a..f67dee0b6 100644 --- a/labelbox/schema/ontology.py +++ b/labelbox/schema/ontology.py @@ -331,7 +331,7 @@ def _update_colors(self): hsv_color = (index * 1 / num_tools, 1, 1) rgb_color = tuple( int(255 * x) for x in colorsys.hsv_to_rgb(*hsv_color)) - if self.tools[index].color is None: + if self.tools[index].color == "#000000": self.tools[index].color = '#%02x%02x%02x' % rgb_color @classmethod From 32aeebf89aac409fb364fb9621d1384cee9aee0e Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Wed, 10 Mar 2021 17:08:01 -0800 Subject: [PATCH 33/36] added docstrings and method to create colors if empty --- labelbox/schema/ontology.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/labelbox/schema/ontology.py b/labelbox/schema/ontology.py index f67dee0b6..b2b97c6b1 100644 --- a/labelbox/schema/ontology.py +++ b/labelbox/schema/ontology.py @@ -191,7 +191,7 @@ class Type(Enum): tool: Type name: str required: bool = False - color: str = "#000000" + color: str = None classifications: List[Classification] = field(default_factory=list) schema_id: Optional[str] = None feature_schema_id: Optional[str] = None @@ -331,7 +331,7 @@ def _update_colors(self): hsv_color = (index * 1 / num_tools, 1, 1) rgb_color = tuple( int(255 * x) for x in colorsys.hsv_to_rgb(*hsv_color)) - if self.tools[index].color == "#000000": + if self.tools[index].color is None: self.tools[index].color = '#%02x%02x%02x' % rgb_color @classmethod From e0c73201d9c34336576e9b8c813a6a64c7948d89 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Thu, 11 Mar 2021 14:53:09 -0800 Subject: [PATCH 34/36] update --- labelbox/schema/ontology.py | 12 ++++--- tests/integration/test_ontology.py | 52 ++++++++++++++---------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/labelbox/schema/ontology.py b/labelbox/schema/ontology.py index c5c9da9d9..4890c6819 100644 --- a/labelbox/schema/ontology.py +++ b/labelbox/schema/ontology.py @@ -98,6 +98,7 @@ class Classification: schema_id: (str) feature_schema_id: (str) """ + class Type(Enum): TEXT = "text" CHECKLIST = "checklist" @@ -180,6 +181,7 @@ class Tool: schema_id: (str) feature_schema_id: (str) """ + class Type(Enum): POLYGON = "polygon" SEGMENTATION = "superpixel" @@ -221,8 +223,8 @@ def asdict(self) -> Dict[str, Any]: } def add_classification(self, classification: Classification): - if classification.instructions in (c.instructions - for c in self.classifications): + if classification.instructions in ( + c.instructions for c in self.classifications): raise InconsistentOntologyException( f"Duplicate nested classification '{classification.instructions}' " f"for tool '{self.name}'") @@ -345,9 +347,9 @@ def add_tool(self, tool: Tool) -> Tool: def add_classification(self, classification: Classification) -> Classification: - if classification.instructions in (c.instructions - for c in self.classifications): + if classification.instructions in ( + c.instructions for c in self.classifications): raise InconsistentOntologyException( f"Duplicate classification instructions '{classification.instructions}'. " ) - self.classifications.append(classification) \ No newline at end of file + self.classifications.append(classification) diff --git a/tests/integration/test_ontology.py b/tests/integration/test_ontology.py index 50e267ddd..8666ce055 100644 --- a/tests/integration/test_ontology.py +++ b/tests/integration/test_ontology.py @@ -26,39 +26,39 @@ "classifications": [] }, { "schemaNodeId": - None, + None, "featureSchemaId": - None, + None, "required": - False, + False, "name": - "bbox", + "bbox", "color": - "#FF0000", + "#FF0000", "tool": - "rectangle", + "rectangle", "classifications": [{ "schemaNodeId": - None, + None, "featureSchemaId": - None, + None, "required": - True, + True, "instructions": - "nested classification", + "nested classification", "name": - "nested classification", + "nested classification", "type": - "radio", + "radio", "options": [{ "schemaNodeId": - None, + None, "featureSchemaId": - None, + None, "label": - "first", + "first", "value": - "first", + "first", "options": [{ "schemaNodeId": None, "featureSchemaId": None, @@ -111,17 +111,17 @@ }], "classifications": [{ "schemaNodeId": - None, + None, "featureSchemaId": - None, + None, "required": - True, + True, "instructions": - "This is a question.", + "This is a question.", "name": - "This is a question.", + "This is a question.", "type": - "radio", + "radio", "options": [{ "schemaNodeId": None, "featureSchemaId": None, @@ -204,8 +204,7 @@ def test_add_ontology_classification() -> None: def test_tool_add_classification() -> None: t = Tool(tool=Tool.Type.SEGMENTATION, name="segmentation") - c = Classification(class_type=Classification.Type.TEXT, - instructions="text") + c = Classification(class_type=Classification.Type.TEXT, instructions="text") t.add_classification(c) assert t.classifications == [c] @@ -228,8 +227,7 @@ def test_classification_add_option() -> None: def test_option_add_option() -> None: o = Option(value="option") - c = Classification(class_type=Classification.Type.TEXT, - instructions="text") + c = Classification(class_type=Classification.Type.TEXT, instructions="text") o.add_option(c) assert o.options == [c] @@ -245,4 +243,4 @@ def test_ontology_asdict(project) -> None: def test_from_project_ontology(client, project) -> None: o = OntologyBuilder.from_project(project) - assert o.asdict() == project.ontology().normalized \ No newline at end of file + assert o.asdict() == project.ontology().normalized From db64b0f093e8d40075ceefbcb91930ba1d4c0cb3 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Thu, 11 Mar 2021 14:54:25 -0800 Subject: [PATCH 35/36] update --- labelbox/exceptions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/labelbox/exceptions.py b/labelbox/exceptions.py index cef2b2f7b..45f5fadcc 100644 --- a/labelbox/exceptions.py +++ b/labelbox/exceptions.py @@ -1,5 +1,6 @@ class LabelboxError(Exception): """Base class for exceptions.""" + def __init__(self, message, cause=None): """ Args: @@ -27,6 +28,7 @@ class AuthorizationError(LabelboxError): class ResourceNotFoundError(LabelboxError): """Exception raised when a given resource is not found. """ + def __init__(self, db_object_type, params): """ Constructor. @@ -66,6 +68,7 @@ class InvalidQueryError(LabelboxError): class NetworkError(LabelboxError): """Raised when an HTTPError occurs.""" + def __init__(self, cause): super().__init__(str(cause), cause) self.cause = cause @@ -79,6 +82,7 @@ class TimeoutError(LabelboxError): class InvalidAttributeError(LabelboxError): """ Raised when a field (name or Field instance) is not valid or found for a specific DB object type. """ + def __init__(self, db_object_type, field): super().__init__("Field(s) '%r' not valid on DB type '%s'" % (field, db_object_type.type_name())) From 14f0072ba6b092ad1cd0dd792a5ee1c9345a5350 Mon Sep 17 00:00:00 2001 From: jtsodapop <67922677+jtsodapop@users.noreply.github.com> Date: Thu, 11 Mar 2021 15:20:44 -0800 Subject: [PATCH 36/36] update --- labelbox/schema/ontology.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/labelbox/schema/ontology.py b/labelbox/schema/ontology.py index 4890c6819..de321f722 100644 --- a/labelbox/schema/ontology.py +++ b/labelbox/schema/ontology.py @@ -58,7 +58,7 @@ def asdict(self) -> Dict[str, Any]: "options": [o.asdict() for o in self.options] } - def add_option(self, option: 'Classification') -> 'Classification': + def add_option(self, option: 'Classification'): if option.instructions in (o.instructions for o in self.options): raise InconsistentOntologyException( f"Duplicate nested classification '{option.instructions}' " @@ -193,7 +193,7 @@ class Type(Enum): tool: Type name: str required: bool = False - color: str = None + color: Optional[str] = None classifications: List[Classification] = field(default_factory=list) schema_id: Optional[str] = None feature_schema_id: Optional[str] = None @@ -339,14 +339,13 @@ def from_project(cls, project: Project): ontology = project.ontology().normalized return OntologyBuilder.from_dict(ontology) - def add_tool(self, tool: Tool) -> Tool: + def add_tool(self, tool: Tool): if tool.name in (t.name for t in self.tools): raise InconsistentOntologyException( f"Duplicate tool name '{tool.name}'. ") self.tools.append(tool) - def add_classification(self, - classification: Classification) -> Classification: + def add_classification(self, classification: Classification): if classification.instructions in ( c.instructions for c in self.classifications): raise InconsistentOntologyException(