Skip to content

Commit d9fc8c8

Browse files
authored
Merge branch 'develop' into pno/fix_usergroup_members
2 parents 69069c9 + 2343e04 commit d9fc8c8

File tree

3 files changed

+307
-0
lines changed

3 files changed

+307
-0
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ def tool_cls_from_type(tool_type: str):
165165
tool_cls = map_tool_type_to_tool_cls(tool_type)
166166
if tool_cls is not None:
167167
return tool_cls
168+
if tool_type == Tool.Type.RELATIONSHIP:
169+
return RelationshipTool
168170
return Tool
169171

170172

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# type: ignore
2+
3+
import uuid
4+
from dataclasses import dataclass
5+
from typing import Any, Dict, List, Optional, Tuple
6+
7+
from labelbox.schema.ontology import Tool
8+
9+
10+
@dataclass
11+
class RelationshipTool(Tool):
12+
"""
13+
A relationship tool to be added to a Project's ontology.
14+
15+
The "tool" parameter is automatically set to Tool.Type.RELATIONSHIP
16+
and doesn't need to be passed during instantiation.
17+
18+
The "classifications" parameter holds a list of Classification objects.
19+
This can be used to add nested classifications to a tool.
20+
21+
Example(s):
22+
tool = RelationshipTool(
23+
name = "Relationship Tool example",
24+
constraints = [
25+
("source_tool_feature_schema_id_1", "target_tool_feature_schema_id_1"),
26+
("source_tool_feature_schema_id_2", "target_tool_feature_schema_id_2")
27+
]
28+
)
29+
classification = Classification(
30+
class_type = Classification.Type.TEXT,
31+
instructions = "Classification Example")
32+
tool.add_classification(classification)
33+
34+
Attributes:
35+
tool: Tool.Type.RELATIONSHIP (automatically set)
36+
name: (str)
37+
required: (bool)
38+
color: (str)
39+
classifications: (list)
40+
schema_id: (str)
41+
feature_schema_id: (str)
42+
attributes: (list)
43+
constraints: (list of [str, str])
44+
"""
45+
46+
constraints: Optional[List[Tuple[str, str]]] = None
47+
48+
def __init__(
49+
self,
50+
name: str,
51+
constraints: Optional[List[Tuple[str, str]]] = None,
52+
**kwargs,
53+
):
54+
super().__init__(Tool.Type.RELATIONSHIP, name, **kwargs)
55+
if constraints is not None:
56+
self.constraints = constraints
57+
58+
def __post_init__(self):
59+
# Ensure tool type is set to RELATIONSHIP
60+
self.tool = Tool.Type.RELATIONSHIP
61+
super().__post_init__()
62+
63+
def asdict(self) -> Dict[str, Any]:
64+
result = super().asdict()
65+
if self.constraints is not None:
66+
result["definition"] = {"constraints": self.constraints}
67+
return result
68+
69+
def add_constraint(self, start: Tool, end: Tool) -> None:
70+
if self.constraints is None:
71+
self.constraints = []
72+
73+
# Ensure feature schema ids are set for the tools,
74+
# the newly set ids will be changed during ontology creation
75+
# but we need to refer to the same ids in the constraints array
76+
# to ensure that the valid constraints are created.
77+
if start.feature_schema_id is None:
78+
start.feature_schema_id = str(uuid.uuid4())
79+
if start.schema_id is None:
80+
start.schema_id = str(uuid.uuid4())
81+
if end.feature_schema_id is None:
82+
end.feature_schema_id = str(uuid.uuid4())
83+
if end.schema_id is None:
84+
end.schema_id = str(uuid.uuid4())
85+
86+
self.constraints.append(
87+
(start.feature_schema_id, end.feature_schema_id)
88+
)
89+
90+
def set_constraints(self, constraints: List[Tuple[Tool, Tool]]) -> None:
91+
self.constraints = []
92+
for constraint in constraints:
93+
self.add_constraint(constraint[0], constraint[1])
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import uuid
2+
from unittest.mock import patch
3+
4+
from labelbox.schema.ontology import Tool
5+
from labelbox.schema.tool_building.relationship_tool import RelationshipTool
6+
7+
8+
def test_basic_instantiation():
9+
tool = RelationshipTool(name="Test Relationship Tool")
10+
11+
assert tool.name == "Test Relationship Tool"
12+
assert tool.tool == Tool.Type.RELATIONSHIP
13+
assert tool.constraints is None
14+
assert tool.required is False
15+
assert tool.color is None
16+
assert tool.schema_id is None
17+
assert tool.feature_schema_id is None
18+
19+
20+
def test_instantiation_with_constraints():
21+
constraints = [
22+
("source_id_1", "target_id_1"),
23+
("source_id_2", "target_id_2"),
24+
]
25+
tool = RelationshipTool(name="Test Tool", constraints=constraints)
26+
27+
assert tool.name == "Test Tool"
28+
assert tool.constraints == constraints
29+
assert len(tool.constraints) == 2
30+
31+
32+
def test_post_init_sets_tool_type():
33+
tool = RelationshipTool(name="Test Tool")
34+
assert tool.tool == Tool.Type.RELATIONSHIP
35+
36+
37+
def test_asdict_without_constraints():
38+
tool = RelationshipTool(name="Test Tool", required=True, color="#FF0000")
39+
40+
result = tool.asdict()
41+
expected = {
42+
"tool": "edge",
43+
"name": "Test Tool",
44+
"required": True,
45+
"color": "#FF0000",
46+
"classifications": [],
47+
"schemaNodeId": None,
48+
"featureSchemaId": None,
49+
"attributes": None,
50+
}
51+
52+
assert result == expected
53+
54+
55+
def test_asdict_with_constraints():
56+
constraints = [("source_id", "target_id")]
57+
tool = RelationshipTool(name="Test Tool", constraints=constraints)
58+
59+
result = tool.asdict()
60+
61+
assert "definition" in result
62+
assert result["definition"] == {"constraints": constraints}
63+
assert result["tool"] == "edge"
64+
assert result["name"] == "Test Tool"
65+
66+
67+
def test_add_constraint_to_empty_constraints():
68+
tool = RelationshipTool(name="Test Tool")
69+
start_tool = Tool(Tool.Type.BBOX, "Start Tool")
70+
end_tool = Tool(Tool.Type.POLYGON, "End Tool")
71+
72+
with patch("uuid.uuid4") as mock_uuid:
73+
mock_uuid.return_value.hex = "test-uuid"
74+
tool.add_constraint(start_tool, end_tool)
75+
76+
assert tool.constraints is not None
77+
assert len(tool.constraints) == 1
78+
assert start_tool.feature_schema_id is not None
79+
assert start_tool.schema_id is not None
80+
assert end_tool.feature_schema_id is not None
81+
assert end_tool.schema_id is not None
82+
83+
84+
def test_add_constraint_to_existing_constraints():
85+
existing_constraints = [("existing_source", "existing_target")]
86+
tool = RelationshipTool(name="Test Tool", constraints=existing_constraints)
87+
88+
start_tool = Tool(Tool.Type.BBOX, "Start Tool")
89+
end_tool = Tool(Tool.Type.POLYGON, "End Tool")
90+
91+
tool.add_constraint(start_tool, end_tool)
92+
93+
assert len(tool.constraints) == 2
94+
assert tool.constraints[0] == ("existing_source", "existing_target")
95+
assert tool.constraints[1] == (
96+
start_tool.feature_schema_id,
97+
end_tool.feature_schema_id,
98+
)
99+
100+
101+
def test_add_constraint_preserves_existing_ids():
102+
tool = RelationshipTool(name="Test Tool")
103+
start_tool_feature_schema_id = "start_tool_feature_schema_id"
104+
start_tool_schema_id = "start_tool_schema_id"
105+
start_tool = Tool(
106+
Tool.Type.BBOX,
107+
"Start Tool",
108+
feature_schema_id=start_tool_feature_schema_id,
109+
schema_id=start_tool_schema_id,
110+
)
111+
end_tool_feature_schema_id = "end_tool_feature_schema_id"
112+
end_tool_schema_id = "end_tool_schema_id"
113+
end_tool = Tool(
114+
Tool.Type.POLYGON,
115+
"End Tool",
116+
feature_schema_id=end_tool_feature_schema_id,
117+
schema_id=end_tool_schema_id,
118+
)
119+
120+
tool.add_constraint(start_tool, end_tool)
121+
122+
assert start_tool.feature_schema_id == start_tool_feature_schema_id
123+
assert start_tool.schema_id == start_tool_schema_id
124+
assert end_tool.feature_schema_id == end_tool_feature_schema_id
125+
assert end_tool.schema_id == end_tool_schema_id
126+
assert tool.constraints == [
127+
(start_tool_feature_schema_id, end_tool_feature_schema_id)
128+
]
129+
130+
131+
def test_set_constraints():
132+
tool = RelationshipTool(name="Test Tool")
133+
134+
start_tool1 = Tool(Tool.Type.BBOX, "Start Tool 1")
135+
end_tool1 = Tool(Tool.Type.POLYGON, "End Tool 1")
136+
start_tool2 = Tool(Tool.Type.POINT, "Start Tool 2")
137+
end_tool2 = Tool(Tool.Type.LINE, "End Tool 2")
138+
139+
tool.set_constraints([(start_tool1, end_tool1), (start_tool2, end_tool2)])
140+
141+
assert len(tool.constraints) == 2
142+
assert tool.constraints[0] == (
143+
start_tool1.feature_schema_id,
144+
end_tool1.feature_schema_id,
145+
)
146+
assert tool.constraints[1] == (
147+
start_tool2.feature_schema_id,
148+
end_tool2.feature_schema_id,
149+
)
150+
151+
152+
def test_set_constraints_replaces_existing():
153+
existing_constraints = [("old_source", "old_target")]
154+
tool = RelationshipTool(name="Test Tool", constraints=existing_constraints)
155+
156+
start_tool = Tool(Tool.Type.BBOX, "Start Tool")
157+
end_tool = Tool(Tool.Type.POLYGON, "End Tool")
158+
159+
tool.set_constraints([(start_tool, end_tool)])
160+
161+
assert len(tool.constraints) == 1
162+
assert tool.constraints[0] != ("old_source", "old_target")
163+
assert tool.constraints[0] == (
164+
start_tool.feature_schema_id,
165+
end_tool.feature_schema_id,
166+
)
167+
168+
169+
def test_uuid_generation_in_add_constraint():
170+
tool = RelationshipTool(name="Test Tool")
171+
172+
start_tool = Tool(Tool.Type.BBOX, "Start Tool")
173+
end_tool = Tool(Tool.Type.POLYGON, "End Tool")
174+
175+
# Ensure tools don't have IDs initially
176+
assert start_tool.feature_schema_id is None
177+
assert start_tool.schema_id is None
178+
assert end_tool.feature_schema_id is None
179+
assert end_tool.schema_id is None
180+
181+
tool.add_constraint(start_tool, end_tool)
182+
183+
# Check that UUIDs were generated
184+
assert start_tool.feature_schema_id is not None
185+
assert start_tool.schema_id is not None
186+
assert end_tool.feature_schema_id is not None
187+
assert end_tool.schema_id is not None
188+
189+
# Check that they are valid UUID strings
190+
uuid.UUID(start_tool.feature_schema_id) # Will raise ValueError if invalid
191+
uuid.UUID(start_tool.schema_id)
192+
uuid.UUID(end_tool.feature_schema_id)
193+
uuid.UUID(end_tool.schema_id)
194+
195+
196+
def test_constraints_in_asdict():
197+
tool = RelationshipTool(name="Test Tool")
198+
199+
start_tool = Tool(Tool.Type.BBOX, "Start Tool")
200+
end_tool = Tool(Tool.Type.POLYGON, "End Tool")
201+
202+
tool.add_constraint(start_tool, end_tool)
203+
204+
result = tool.asdict()
205+
206+
assert "definition" in result
207+
assert "constraints" in result["definition"]
208+
assert len(result["definition"]["constraints"]) == 1
209+
assert result["definition"]["constraints"][0] == (
210+
start_tool.feature_schema_id,
211+
end_tool.feature_schema_id,
212+
)

0 commit comments

Comments
 (0)