Skip to content

Commit bd482a0

Browse files
author
rllin
authored
Rllin/read ontology (#51)
* ontology entity * ontology nested objects * yapf * remove cached_property * fix types * fix
1 parent 75b5d97 commit bd482a0

File tree

6 files changed

+201
-0
lines changed

6 files changed

+201
-0
lines changed

labelbox/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@
1414
from labelbox.schema.asset_metadata import AssetMetadata
1515
from labelbox.schema.webhook import Webhook
1616
from labelbox.schema.prediction import Prediction, PredictionModel
17+
from labelbox.schema.ontology import Ontology

labelbox/orm/model.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class Type(Enum):
4242
Boolean = auto()
4343
ID = auto()
4444
DateTime = auto()
45+
Json = auto()
4546

4647
class EnumType:
4748

@@ -85,6 +86,10 @@ def DateTime(*args):
8586
def Enum(enum_cls: type, *args):
8687
return Field(Field.EnumType(enum_cls), *args)
8788

89+
@staticmethod
90+
def Json(*args):
91+
return Field(Field.Type.Json, *args)
92+
8893
def __init__(self,
8994
field_type: Union[Type, EnumType],
9095
name,

labelbox/schema/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@
1212
import labelbox.schema.user
1313
import labelbox.schema.webhook
1414
import labelbox.schema.prediction
15+
import labelbox.schema.ontology

labelbox/schema/ontology.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""Client side object for interacting with the ontology."""
2+
import abc
3+
from dataclasses import dataclass
4+
5+
from typing import Any, Callable, Dict, List, Optional, Union
6+
7+
from labelbox.orm import query
8+
from labelbox.orm.db_object import DbObject, Updateable, BulkDeletable
9+
from labelbox.orm.model import Entity, Field, Relationship
10+
from labelbox.utils import snake_case, camel_case
11+
12+
13+
@dataclass
14+
class OntologyEntity:
15+
required: bool
16+
name: str
17+
18+
19+
@dataclass
20+
class Option:
21+
label: str
22+
value: str
23+
feature_schema_id: Optional[str] = None
24+
schema_node_id: Optional[str] = None
25+
26+
@classmethod
27+
def from_json(cls, json_dict):
28+
_dict = convert_keys(json_dict, snake_case)
29+
return cls(**_dict)
30+
31+
32+
@dataclass
33+
class Classification(OntologyEntity):
34+
type: str
35+
instructions: str
36+
options: List[Option]
37+
feature_schema_id: Optional[str] = None
38+
schema_node_id: Optional[str] = None
39+
40+
@classmethod
41+
def from_json(cls, json_dict):
42+
_dict = convert_keys(json_dict, snake_case)
43+
_dict['options'] = [
44+
Option.from_json(option) for option in _dict['options']
45+
]
46+
return cls(**_dict)
47+
48+
49+
@dataclass
50+
class Tool(OntologyEntity):
51+
tool: str
52+
color: str
53+
classifications: List[Classification]
54+
feature_schema_id: Optional[str] = None
55+
schema_node_id: Optional[str] = None
56+
57+
@classmethod
58+
def from_json(cls, json_dict):
59+
_dict = convert_keys(json_dict, snake_case)
60+
_dict['classifications'] = [
61+
Classification.from_json(classification)
62+
for classification in _dict['classifications']
63+
]
64+
return cls(**_dict)
65+
66+
67+
class Ontology(DbObject):
68+
""" A ontology specifies which tools and classifications are available
69+
to a project.
70+
71+
NOTE: This is read only for now.
72+
73+
>>> project = client.get_project(name="<project_name>")
74+
>>> ontology = project.ontology()
75+
>>> ontology.normalized
76+
77+
"""
78+
79+
name = Field.String("name")
80+
description = Field.String("description")
81+
updated_at = Field.DateTime("updated_at")
82+
created_at = Field.DateTime("created_at")
83+
normalized = Field.Json("normalized")
84+
object_schema_count = Field.Int("object_schema_count")
85+
classification_schema_count = Field.Int("classification_schema_count")
86+
87+
projects = Relationship.ToMany("Project", True)
88+
created_by = Relationship.ToOne("User", False, "created_by")
89+
90+
def __init__(self, *args, **kwargs) -> None:
91+
super().__init__(*args, **kwargs)
92+
self._tools: Optional[List[Tool]] = None
93+
self._classifications: Optional[List[Classification]] = None
94+
95+
def tools(self) -> List[Tool]:
96+
if self._tools is None:
97+
self._tools = [
98+
Tool.from_json(tool) for tool in self.normalized['tools']
99+
]
100+
return self._tools # type: ignore
101+
102+
def classifications(self) -> List[Classification]:
103+
if self._classifications is None:
104+
self._classifications = [
105+
Classification.from_json(classification)
106+
for classification in self.normalized['classifications']
107+
]
108+
return self._classifications # type: ignore
109+
110+
111+
def convert_keys(json_dict: Dict[str, Any],
112+
converter: Callable) -> Dict[str, Any]:
113+
if isinstance(json_dict, dict):
114+
return {
115+
converter(key): convert_keys(value, converter)
116+
for key, value in json_dict.items()
117+
}
118+
if isinstance(json_dict, list):
119+
return [convert_keys(ele, converter) for ele in json_dict]
120+
return json_dict

labelbox/schema/project.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class Project(DbObject, Updateable, Deletable):
4646
active_prediction_model = Relationship.ToOne("PredictionModel", False,
4747
"active_prediction_model")
4848
predictions = Relationship.ToMany("Prediction", False)
49+
ontology = Relationship.ToOne("Ontology", True)
4950

5051
def create_label(self, **kwargs):
5152
""" Creates a label on this Project.

tests/integration/test_ontology.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import unittest
2+
from typing import Any, Dict, List, Union
3+
4+
5+
def sample_ontology() -> Dict[str, Any]:
6+
return {
7+
"tools": [{
8+
"required": False,
9+
"name": "Dog",
10+
"color": "#FF0000",
11+
"tool": "rectangle",
12+
"classifications": []
13+
}],
14+
"classifications": [{
15+
"required":
16+
True,
17+
"instructions":
18+
"This is a question.",
19+
"name":
20+
"this_is_a_question.",
21+
"type":
22+
"radio",
23+
"options": [{
24+
"label": "Yes",
25+
"value": "yes"
26+
}, {
27+
"label": "No",
28+
"value": "no"
29+
}]
30+
}]
31+
}
32+
33+
34+
def test_create_ontology(client, project) -> None:
35+
""" Tests that the ontology that a project was set up with can be grabbed."""
36+
frontend = list(client.get_labeling_frontends())[0]
37+
project.setup(frontend, sample_ontology())
38+
normalized_ontology = project.ontology().normalized
39+
40+
def _remove_schema_ids(
41+
ontology_part: Union[List, Dict[str, Any]]) -> Dict[str, Any]:
42+
""" Recursively scrub the normalized ontology of any schema information."""
43+
removals = {'featureSchemaId', 'schemaNodeId'}
44+
45+
if isinstance(ontology_part, list):
46+
return [_remove_schema_ids(part) for part in ontology_part]
47+
if isinstance(ontology_part, dict):
48+
return {
49+
key: _remove_schema_ids(value)
50+
for key, value in ontology_part.items()
51+
if key not in removals
52+
}
53+
return ontology_part
54+
55+
removed = _remove_schema_ids(normalized_ontology)
56+
assert removed == sample_ontology()
57+
58+
ontology = project.ontology()
59+
60+
tools = ontology.tools()
61+
assert tools
62+
for tool in tools:
63+
assert tool.feature_schema_id
64+
assert tool.schema_node_id
65+
66+
classifications = ontology.classifications()
67+
assert classifications
68+
for classification in classifications:
69+
assert classification.feature_schema_id
70+
assert classification.schema_node_id
71+
for option in classification.options:
72+
assert option.feature_schema_id
73+
assert option.schema_node_id

0 commit comments

Comments
 (0)