Skip to content

Commit 00f704d

Browse files
authored
[PTDT-2863]: Feature schema attributes (#1930)
2 parents bee0195 + f0dfa74 commit 00f704d

File tree

7 files changed

+189
-3
lines changed

7 files changed

+189
-3
lines changed

libs/labelbox/src/labelbox/schema/ontology.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
from labelbox.schema.tool_building.tool_type_mapping import (
2626
map_tool_type_to_tool_cls,
2727
)
28+
from labelbox.schema.tool_building.types import (
29+
FeatureSchemaAttribute,
30+
FeatureSchemaAttributes,
31+
)
32+
import warnings
2833

2934

3035
class DeleteFeatureFromOntologyResult:
@@ -73,6 +78,7 @@ class Tool:
7378
classifications: (list)
7479
schema_id: (str)
7580
feature_schema_id: (str)
81+
attributes: (list)
7682
"""
7783

7884
class Type(Enum):
@@ -95,6 +101,13 @@ class Type(Enum):
95101
classifications: List[Classification] = field(default_factory=list)
96102
schema_id: Optional[str] = None
97103
feature_schema_id: Optional[str] = None
104+
attributes: Optional[FeatureSchemaAttributes] = None
105+
106+
def __post_init__(self):
107+
if self.attributes is not None:
108+
warnings.warn(
109+
"The attributes for Tools are in beta. The attribute name and signature may change in the future."
110+
)
98111

99112
@classmethod
100113
def from_dict(cls, dictionary: Dict[str, Any]) -> Dict[str, Any]:
@@ -109,6 +122,12 @@ def from_dict(cls, dictionary: Dict[str, Any]) -> Dict[str, Any]:
109122
for c in dictionary["classifications"]
110123
],
111124
color=dictionary["color"],
125+
attributes=[
126+
FeatureSchemaAttribute.from_dict(attr)
127+
for attr in dictionary.get("attributes", []) or []
128+
]
129+
if dictionary.get("attributes")
130+
else None,
112131
)
113132

114133
def asdict(self) -> Dict[str, Any]:
@@ -122,6 +141,9 @@ def asdict(self) -> Dict[str, Any]:
122141
],
123142
"schemaNodeId": self.schema_id,
124143
"featureSchemaId": self.feature_schema_id,
144+
"attributes": [a.asdict() for a in self.attributes]
145+
if self.attributes is not None
146+
else None,
125147
}
126148

127149
def add_classification(self, classification: Classification) -> None:

libs/labelbox/src/labelbox/schema/tool_building/classification.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55

66
from lbox.exceptions import InconsistentOntologyException
77

8-
from labelbox.schema.tool_building.types import FeatureSchemaId
8+
from labelbox.schema.tool_building.types import (
9+
FeatureSchemaId,
10+
FeatureSchemaAttributes,
11+
FeatureSchemaAttribute,
12+
)
913

1014

1115
@dataclass
@@ -42,6 +46,7 @@ class Classification:
4246
schema_id: (str)
4347
feature_schema_id: (str)
4448
scope: (str)
49+
attributes: (list)
4550
"""
4651

4752
class Type(Enum):
@@ -70,6 +75,7 @@ class UIMode(Enum):
7075
ui_mode: Optional[UIMode] = (
7176
None # How this classification should be answered (e.g. hotkeys / autocomplete, etc)
7277
)
78+
attributes: Optional[FeatureSchemaAttributes] = None
7379

7480
def __post_init__(self):
7581
if self.name is None:
@@ -88,6 +94,10 @@ def __post_init__(self):
8894
else:
8995
if self.instructions is None:
9096
self.instructions = self.name
97+
if self.attributes is not None:
98+
warnings.warn(
99+
"The attributes for Classifications are in beta. The attribute name and signature may change in the future."
100+
)
91101

92102
@classmethod
93103
def from_dict(cls, dictionary: Dict[str, Any]) -> "Classification":
@@ -103,6 +113,12 @@ def from_dict(cls, dictionary: Dict[str, Any]) -> "Classification":
103113
schema_id=dictionary.get("schemaNodeId", None),
104114
feature_schema_id=dictionary.get("featureSchemaId", None),
105115
scope=cls.Scope(dictionary.get("scope", cls.Scope.GLOBAL)),
116+
attributes=[
117+
FeatureSchemaAttribute.from_dict(attr)
118+
for attr in dictionary.get("attributes", []) or []
119+
]
120+
if dictionary.get("attributes")
121+
else None,
106122
)
107123

108124
def asdict(self, is_subclass: bool = False) -> Dict[str, Any]:
@@ -118,6 +134,9 @@ def asdict(self, is_subclass: bool = False) -> Dict[str, Any]:
118134
"options": [o.asdict() for o in self.options],
119135
"schemaNodeId": self.schema_id,
120136
"featureSchemaId": self.feature_schema_id,
137+
"attributes": [a.asdict() for a in self.attributes]
138+
if self.attributes is not None
139+
else None,
121140
}
122141
if (
123142
self.class_type == self.Type.RADIO
Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,33 @@
1-
from typing import Annotated
2-
1+
from typing import Annotated, List
32
from pydantic import Field
43

4+
5+
from dataclasses import dataclass
6+
7+
from typing import Any, Dict, List
8+
9+
10+
@dataclass
11+
class FeatureSchemaAttribute:
12+
attributeName: str
13+
attributeValue: str
14+
15+
def asdict(self):
16+
return {
17+
"attributeName": self.attributeName,
18+
"attributeValue": self.attributeValue,
19+
}
20+
21+
@classmethod
22+
def from_dict(cls, dictionary: Dict[str, Any]) -> "FeatureSchemaAttribute":
23+
return cls(
24+
attributeName=dictionary["attributeName"],
25+
attributeValue=dictionary["attributeValue"],
26+
)
27+
28+
529
FeatureSchemaId = Annotated[str, Field(min_length=25, max_length=25)]
630
SchemaId = Annotated[str, Field(min_length=25, max_length=25)]
31+
FeatureSchemaAttributes = Annotated[
32+
List[FeatureSchemaAttribute], Field(default_factory=list)
33+
]

libs/labelbox/tests/integration/conftest.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from labelbox.schema.data_row import DataRowMetadataField
2626
from labelbox.schema.ontology_kind import OntologyKind
2727
from labelbox.schema.user import User
28+
from labelbox.schema.tool_building.types import FeatureSchemaAttribute
2829

2930

3031
@pytest.fixture
@@ -552,6 +553,76 @@ def point():
552553
)
553554

554555

556+
@pytest.fixture
557+
def auto_ocr_text_value_class():
558+
return Classification(
559+
class_type=Classification.Type.TEXT,
560+
name="Auto OCR Text Value",
561+
instructions="Text value for ocr bboxes",
562+
scope=Classification.Scope.GLOBAL,
563+
required=False,
564+
attributes=[
565+
FeatureSchemaAttribute(
566+
attributeName="auto-ocr-text-value", attributeValue="true"
567+
)
568+
],
569+
)
570+
571+
572+
@pytest.fixture
573+
def auto_ocr_bbox(auto_ocr_text_value_class):
574+
return Tool(
575+
tool=Tool.Type.BBOX,
576+
name="Auto ocr bbox",
577+
color="ff0000",
578+
attributes=[
579+
FeatureSchemaAttribute(
580+
attributeName="auto-ocr", attributeValue="true"
581+
)
582+
],
583+
classifications=[auto_ocr_text_value_class],
584+
)
585+
586+
587+
@pytest.fixture
588+
def requires_connection_classification():
589+
return Classification(
590+
name="Requires connection radio",
591+
instructions="Classification that requires a connection",
592+
class_type=Classification.Type.RADIO,
593+
attributes=[
594+
FeatureSchemaAttribute(
595+
attributeName="requires-connection", attributeValue="true"
596+
)
597+
],
598+
options=[Option(value="A"), Option(value="B")],
599+
)
600+
601+
602+
@pytest.fixture
603+
def requires_connection_classification_feature_schema(
604+
client, requires_connection_classification
605+
):
606+
created_feature_schema = client.upsert_feature_schema(
607+
requires_connection_classification.asdict()
608+
)
609+
yield created_feature_schema
610+
client.delete_unused_feature_schema(
611+
created_feature_schema.normalized["featureSchemaId"]
612+
)
613+
614+
615+
@pytest.fixture
616+
def auto_ocr_bbox_feature_schema(client, auto_ocr_bbox):
617+
created_feature_schema = client.upsert_feature_schema(
618+
auto_ocr_bbox.asdict()
619+
)
620+
yield created_feature_schema
621+
client.delete_unused_feature_schema(
622+
created_feature_schema.normalized["featureSchemaId"]
623+
)
624+
625+
555626
@pytest.fixture
556627
def feature_schema(client, point):
557628
created_feature_schema = client.upsert_feature_schema(point.asdict())

libs/labelbox/tests/integration/test_feature_schema.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,29 @@ def test_does_not_include_used_feature_schema(client, feature_schema):
115115
assert feature_schema_id not in unused_feature_schemas
116116

117117
client.delete_unused_ontology(ontology.uid)
118+
119+
120+
def test_upsert_tool_with_attributes(auto_ocr_bbox_feature_schema):
121+
auto_ocr_attributes = auto_ocr_bbox_feature_schema.normalized["attributes"]
122+
auto_ocr_text_value_attributes = auto_ocr_bbox_feature_schema.normalized[
123+
"classifications"
124+
][0]["attributes"]
125+
assert auto_ocr_attributes == [
126+
{"attributeName": "auto-ocr", "attributeValue": "true"}
127+
]
128+
assert auto_ocr_text_value_attributes == [
129+
{"attributeName": "auto-ocr-text-value", "attributeValue": "true"}
130+
]
131+
132+
133+
def test_upsert_classification_with_attributes(
134+
requires_connection_classification_feature_schema,
135+
):
136+
requires_connection_attributes = (
137+
requires_connection_classification_feature_schema.normalized[
138+
"attributes"
139+
]
140+
)
141+
assert requires_connection_attributes == [
142+
{"attributeName": "requires-connection", "attributeValue": "true"}
143+
]

libs/labelbox/tests/unit/test_unit_ontology.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"color": "#FF0000",
1616
"tool": "polygon",
1717
"classifications": [],
18+
"attributes": None,
1819
},
1920
{
2021
"schemaNodeId": None,
@@ -24,6 +25,7 @@
2425
"color": "#FF0000",
2526
"tool": "superpixel",
2627
"classifications": [],
28+
"attributes": None,
2729
},
2830
{
2931
"schemaNodeId": None,
@@ -32,6 +34,12 @@
3234
"name": "bbox",
3335
"color": "#FF0000",
3436
"tool": "rectangle",
37+
"attributes": [
38+
{
39+
"attributeName": "auto-ocr",
40+
"attributeValue": "true",
41+
}
42+
],
3543
"classifications": [
3644
{
3745
"schemaNodeId": None,
@@ -56,6 +64,7 @@
5664
"name": "nested nested text",
5765
"type": "text",
5866
"options": [],
67+
"attributes": None,
5968
}
6069
],
6170
},
@@ -67,6 +76,12 @@
6776
"options": [],
6877
},
6978
],
79+
"attributes": [
80+
{
81+
"attributeName": "requires-connection",
82+
"attributeValue": "true",
83+
}
84+
],
7085
},
7186
{
7287
"schemaNodeId": None,
@@ -76,6 +91,7 @@
7691
"name": "nested text",
7792
"type": "text",
7893
"options": [],
94+
"attributes": None,
7995
},
8096
],
8197
},
@@ -87,6 +103,7 @@
87103
"color": "#FF0000",
88104
"tool": "point",
89105
"classifications": [],
106+
"attributes": None,
90107
},
91108
{
92109
"schemaNodeId": None,
@@ -96,6 +113,7 @@
96113
"color": "#FF0000",
97114
"tool": "line",
98115
"classifications": [],
116+
"attributes": None,
99117
},
100118
{
101119
"schemaNodeId": None,
@@ -105,6 +123,7 @@
105123
"color": "#FF0000",
106124
"tool": "named-entity",
107125
"classifications": [],
126+
"attributes": None,
108127
},
109128
],
110129
"classifications": [
@@ -117,6 +136,7 @@
117136
"type": "radio",
118137
"scope": "global",
119138
"uiMode": "searchable",
139+
"attributes": None,
120140
"options": [
121141
{
122142
"schemaNodeId": None,

libs/labelbox/tests/unit/test_unit_prompt_issue_tool.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def test_as_dict():
4747
"schemaNodeId": None,
4848
"featureSchemaId": None,
4949
"scope": "global",
50+
"attributes": None,
5051
}
5152
],
5253
"color": None,

0 commit comments

Comments
 (0)