From 4545e8add7cd0ca6f6638a86689ba4422f25db8d Mon Sep 17 00:00:00 2001 From: rllin Date: Thu, 13 Aug 2020 16:37:28 -0700 Subject: [PATCH 1/6] ontology entity --- labelbox/__init__.py | 1 + labelbox/orm/model.py | 5 +++ labelbox/schema/__init__.py | 1 + labelbox/schema/ontology.py | 28 +++++++++++++++ labelbox/schema/project.py | 1 + tests/integration/test_ontology.py | 56 ++++++++++++++++++++++++++++++ 6 files changed, 92 insertions(+) create mode 100644 labelbox/schema/ontology.py create mode 100644 tests/integration/test_ontology.py diff --git a/labelbox/__init__.py b/labelbox/__init__.py index 115023bd4..680e44068 100644 --- a/labelbox/__init__.py +++ b/labelbox/__init__.py @@ -14,3 +14,4 @@ from labelbox.schema.asset_metadata import AssetMetadata from labelbox.schema.webhook import Webhook from labelbox.schema.prediction import Prediction, PredictionModel +from labelbox.schema.ontology import Ontology diff --git a/labelbox/orm/model.py b/labelbox/orm/model.py index ee93eea22..15f8d5cee 100644 --- a/labelbox/orm/model.py +++ b/labelbox/orm/model.py @@ -42,6 +42,7 @@ class Type(Enum): Boolean = auto() ID = auto() DateTime = auto() + Json = auto() class EnumType: @@ -85,6 +86,10 @@ def DateTime(*args): def Enum(enum_cls: type, *args): return Field(Field.EnumType(enum_cls), *args) + @staticmethod + def Json(*args): + return Field(Field.Type.Json, *args) + def __init__(self, field_type: Union[Type, EnumType], name, diff --git a/labelbox/schema/__init__.py b/labelbox/schema/__init__.py index eadb49ab8..223920a1b 100644 --- a/labelbox/schema/__init__.py +++ b/labelbox/schema/__init__.py @@ -12,3 +12,4 @@ import labelbox.schema.user import labelbox.schema.webhook import labelbox.schema.prediction +import labelbox.schema.ontology diff --git a/labelbox/schema/ontology.py b/labelbox/schema/ontology.py new file mode 100644 index 000000000..6148634f1 --- /dev/null +++ b/labelbox/schema/ontology.py @@ -0,0 +1,28 @@ +"""Client side object for interacting with the ontology.""" +from labelbox.orm import query +from labelbox.orm.db_object import DbObject, Updateable, BulkDeletable +from labelbox.orm.model import Entity, Field, Relationship + + +class Ontology(DbObject): + """ A ontology specifies which tools and classifications are available + to a project. + + NOTE: This is read only for now. + + >>> project = client.get_project(name="") + >>> ontology = project.ontology() + >>> ontology.normalized + + """ + + name = Field.String("name") + description = Field.String("description") + updated_at = Field.DateTime("updated_at") + created_at = Field.DateTime("created_at") + normalized = Field.Json("normalized") + object_schema_count = Field.Int("object_schema_count") + classification_schema_count = Field.Int("classification_schema_count") + + projects = Relationship.ToMany("Project", True) + created_by = Relationship.ToOne("User", False, "created_by") diff --git a/labelbox/schema/project.py b/labelbox/schema/project.py index bd35d7306..b1c75bc83 100644 --- a/labelbox/schema/project.py +++ b/labelbox/schema/project.py @@ -46,6 +46,7 @@ class Project(DbObject, Updateable, Deletable): active_prediction_model = Relationship.ToOne("PredictionModel", False, "active_prediction_model") predictions = Relationship.ToMany("Prediction", False) + ontology = Relationship.ToOne("Ontology", True) def create_label(self, **kwargs): """ Creates a label on this Project. diff --git a/tests/integration/test_ontology.py b/tests/integration/test_ontology.py new file mode 100644 index 000000000..cbec30376 --- /dev/null +++ b/tests/integration/test_ontology.py @@ -0,0 +1,56 @@ +import unittest +from typing import Any, Dict, List, Union + + +def sample_ontology() -> Dict[str, Any]: + return { + "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", + "options": [{ + "label": "Yes", + "value": "yes" + }, { + "label": "No", + "value": "no" + }] + }] + } + + +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() From ac98d9c9ebd60d1ffffac63514200906a6f7aeba Mon Sep 17 00:00:00 2001 From: rllin Date: Thu, 13 Aug 2020 19:11:01 -0700 Subject: [PATCH 2/6] ontology nested objects --- labelbox/schema/ontology.py | 91 ++++++++++++++++++++++++++++++ tests/integration/test_ontology.py | 13 +++++ 2 files changed, 104 insertions(+) diff --git a/labelbox/schema/ontology.py b/labelbox/schema/ontology.py index 6148634f1..047d15434 100644 --- a/labelbox/schema/ontology.py +++ b/labelbox/schema/ontology.py @@ -1,7 +1,70 @@ """Client side object for interacting with the ontology.""" +import abc +from dataclasses import dataclass +from functools import cached_property + +from typing import Any, Callable, Dict, List, Optional, Union + 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 + + +@dataclass +class Option: + label: str + value: str + 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) + return cls(**_dict) + + +@dataclass +class Classification(OntologyEntity): + type: str + instructions: str + options: List[Option] + 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['options'] = [ + Option.from_json(option) + for option in _dict['options'] + ] + return cls(**_dict) + + +@dataclass +class Tool(OntologyEntity): + tool: str + color: str + classifications: List[Classification] + 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) class Ontology(DbObject): @@ -26,3 +89,31 @@ class Ontology(DbObject): projects = Relationship.ToMany("Project", True) created_by = Relationship.ToOne("User", False, "created_by") + + @cached_property + def tools(self) -> List[Tool]: + return [ + Tool.from_json(tool) + for tool in self.normalized['tools'] + ] + + @cached_property + def classifications(self) -> List[Classification]: + return [ + Classification.from_json(classification) + for classification in self.normalized['classifications'] + ] + + +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 diff --git a/tests/integration/test_ontology.py b/tests/integration/test_ontology.py index cbec30376..0ccebf0c4 100644 --- a/tests/integration/test_ontology.py +++ b/tests/integration/test_ontology.py @@ -54,3 +54,16 @@ def _remove_schema_ids( removed = _remove_schema_ids(normalized_ontology) assert removed == sample_ontology() + + ontology = project.ontology() + + for tool in ontology.tools: + assert tool.feature_schema_id + assert tool.schema_node_id + + for classification in ontology.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 5bb0f47b85b5472455a6002d69cfc69975f7e582 Mon Sep 17 00:00:00 2001 From: rllin Date: Tue, 1 Sep 2020 08:59:51 -0700 Subject: [PATCH 3/6] yapf --- labelbox/schema/ontology.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/labelbox/schema/ontology.py b/labelbox/schema/ontology.py index 047d15434..7f6c58cab 100644 --- a/labelbox/schema/ontology.py +++ b/labelbox/schema/ontology.py @@ -11,7 +11,6 @@ from labelbox.utils import snake_case, camel_case - @dataclass class OntologyEntity: required: bool @@ -43,8 +42,7 @@ class Classification(OntologyEntity): def from_json(cls, json_dict): _dict = convert_keys(json_dict, snake_case) _dict['options'] = [ - Option.from_json(option) - for option in _dict['options'] + Option.from_json(option) for option in _dict['options'] ] return cls(**_dict) @@ -92,10 +90,7 @@ class Ontology(DbObject): @cached_property def tools(self) -> List[Tool]: - return [ - Tool.from_json(tool) - for tool in self.normalized['tools'] - ] + return [Tool.from_json(tool) for tool in self.normalized['tools']] @cached_property def classifications(self) -> List[Classification]: @@ -105,15 +100,13 @@ def classifications(self) -> List[Classification]: ] -def convert_keys(json_dict: Dict[str, Any], converter: Callable) -> Dict[str, Any]: +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 [convert_keys(ele, converter) for ele in json_dict] return json_dict From 81b67d6577e503f00faf7491491fce36df60a7b9 Mon Sep 17 00:00:00 2001 From: rllin Date: Tue, 1 Sep 2020 09:11:55 -0700 Subject: [PATCH 4/6] remove cached_property --- labelbox/schema/ontology.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/labelbox/schema/ontology.py b/labelbox/schema/ontology.py index 7f6c58cab..d1d8d2295 100644 --- a/labelbox/schema/ontology.py +++ b/labelbox/schema/ontology.py @@ -1,7 +1,6 @@ """Client side object for interacting with the ontology.""" import abc from dataclasses import dataclass -from functools import cached_property from typing import Any, Callable, Dict, List, Optional, Union @@ -88,16 +87,25 @@ class Ontology(DbObject): projects = Relationship.ToMany("Project", True) created_by = Relationship.ToOne("User", False, "created_by") - @cached_property + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._tools = None + self._classifications = None + def tools(self) -> List[Tool]: - return [Tool.from_json(tool) for tool in self.normalized['tools']] + if self._tools is None: + self._tools = [ + Tool.from_json(tool) for tool in self.normalized['tools'] + ] + return self._tools - @cached_property def classifications(self) -> List[Classification]: - return [ - Classification.from_json(classification) - for classification in self.normalized['classifications'] - ] + if self._classifications is None: + self._classfications = [ + Classification.from_json(classification) + for classification in self.normalized['classifications'] + ] + return self._classifications def convert_keys(json_dict: Dict[str, Any], From fc2b2b46dff6df50cb754b7cb6106225625e5c7f Mon Sep 17 00:00:00 2001 From: rllin Date: Tue, 1 Sep 2020 09:54:39 -0700 Subject: [PATCH 5/6] fix types --- labelbox/schema/ontology.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/labelbox/schema/ontology.py b/labelbox/schema/ontology.py index d1d8d2295..3c344be59 100644 --- a/labelbox/schema/ontology.py +++ b/labelbox/schema/ontology.py @@ -89,15 +89,15 @@ class Ontology(DbObject): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - self._tools = None - self._classifications = None + self._tools: Optional[List[Tool]] = None + self._classifications: Optional[List[Classification]] = None def tools(self) -> List[Tool]: if self._tools is None: self._tools = [ Tool.from_json(tool) for tool in self.normalized['tools'] ] - return self._tools + return self._tools # type: ignore def classifications(self) -> List[Classification]: if self._classifications is None: @@ -105,7 +105,7 @@ def classifications(self) -> List[Classification]: Classification.from_json(classification) for classification in self.normalized['classifications'] ] - return self._classifications + return self._classifications # type: ignore def convert_keys(json_dict: Dict[str, Any], From 187752c340d401f207360f31fb41f51aa66e5202 Mon Sep 17 00:00:00 2001 From: rllin Date: Wed, 9 Sep 2020 16:55:54 -0700 Subject: [PATCH 6/6] fix --- labelbox/schema/ontology.py | 2 +- tests/integration/test_ontology.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/labelbox/schema/ontology.py b/labelbox/schema/ontology.py index 3c344be59..301507884 100644 --- a/labelbox/schema/ontology.py +++ b/labelbox/schema/ontology.py @@ -101,7 +101,7 @@ def tools(self) -> List[Tool]: def classifications(self) -> List[Classification]: if self._classifications is None: - self._classfications = [ + self._classifications = [ Classification.from_json(classification) for classification in self.normalized['classifications'] ] diff --git a/tests/integration/test_ontology.py b/tests/integration/test_ontology.py index 0ccebf0c4..9f6f7e257 100644 --- a/tests/integration/test_ontology.py +++ b/tests/integration/test_ontology.py @@ -57,11 +57,15 @@ def _remove_schema_ids( ontology = project.ontology() - for tool in ontology.tools: + tools = ontology.tools() + assert tools + for tool in tools: assert tool.feature_schema_id assert tool.schema_node_id - for classification in ontology.classifications: + classifications = ontology.classifications() + assert classifications + for classification in classifications: assert classification.feature_schema_id assert classification.schema_node_id for option in classification.options: