From cbf50aa64e492d4e3ba627bc6e02f635f8501292 Mon Sep 17 00:00:00 2001 From: Karol Jamrozy Date: Tue, 15 Apr 2025 23:04:47 +0200 Subject: [PATCH 1/8] [PTDT-4605] Add ability to specify relationship constraints --- libs/labelbox/src/labelbox/schema/ontology.py | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/libs/labelbox/src/labelbox/schema/ontology.py b/libs/labelbox/src/labelbox/schema/ontology.py index 1cc827c5f..252c88c0f 100644 --- a/libs/labelbox/src/labelbox/schema/ontology.py +++ b/libs/labelbox/src/labelbox/schema/ontology.py @@ -4,7 +4,7 @@ import json from dataclasses import dataclass, field from enum import Enum -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Union, Tuple from lbox.exceptions import InconsistentOntologyException @@ -71,6 +71,15 @@ class Tool: instructions = "Classification Example") tool.add_classification(classification) + relationship_tool = Tool( + tool = Tool.Type.RELATIONSHIP, + name = "Relationship Tool Example", + constraints = [ + ("source_tool_feature_schema_id_1", "target_tool_feature_schema_id_1"), + ("source_tool_feature_schema_id_2", "target_tool_feature_schema_id_2") + ] + ) + Attributes: tool: (Tool.Type) name: (str) @@ -80,6 +89,7 @@ class Tool: schema_id: (str) feature_schema_id: (str) attributes: (list) + constraints: (list of [str, str]) (only available for RELATIONSHIP tool type) """ class Type(Enum): @@ -103,8 +113,14 @@ class Type(Enum): schema_id: Optional[str] = None feature_schema_id: Optional[str] = None attributes: Optional[FeatureSchemaAttributes] = None + constraints: Optional[Tuple[str, str]] = None def __post_init__(self): + if self.constraints is not None and self.tool != Tool.Type.RELATIONSHIP: + warnings.warn( + "The constraints attribute is only available for Relationship tool. The provided constraints will be ignored." + ) + self.constraints = None if self.attributes is not None: warnings.warn( "The attributes for Tools are in beta. The attribute name and signature may change in the future." @@ -112,12 +128,13 @@ def __post_init__(self): @classmethod def from_dict(cls, dictionary: Dict[str, Any]) -> Dict[str, Any]: + tool = Tool.Type(dictionary["tool"]) return cls( name=dictionary["name"], schema_id=dictionary.get("schemaNodeId", None), feature_schema_id=dictionary.get("featureSchemaId", None), required=dictionary.get("required", False), - tool=Tool.Type(dictionary["tool"]), + tool=tool, classifications=[ Classification.from_dict(c) for c in dictionary["classifications"] @@ -129,6 +146,9 @@ def from_dict(cls, dictionary: Dict[str, Any]) -> Dict[str, Any]: ] if dictionary.get("attributes") else None, + constraints=dictionary.get("constraints", None) + if tool == Tool.Type.RELATIONSHIP + else None, ) def asdict(self) -> Dict[str, Any]: @@ -145,6 +165,9 @@ def asdict(self) -> Dict[str, Any]: "attributes": [a.asdict() for a in self.attributes] if self.attributes is not None else None, + "constraints": self.constraints + if self.constraints is not None + else None, } def add_classification(self, classification: Classification) -> None: From 7add87db404db8bb0122b9e395e86cd2f9322470 Mon Sep 17 00:00:00 2001 From: Karol Jamrozy Date: Tue, 27 May 2025 18:47:53 +0200 Subject: [PATCH 2/8] [PTDT-4605] Add ability to specify relationship constraints --- libs/labelbox/src/labelbox/schema/ontology.py | 77 +++++++++++++------ 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/libs/labelbox/src/labelbox/schema/ontology.py b/libs/labelbox/src/labelbox/schema/ontology.py index 252c88c0f..d5cceef8e 100644 --- a/libs/labelbox/src/labelbox/schema/ontology.py +++ b/libs/labelbox/src/labelbox/schema/ontology.py @@ -71,15 +71,6 @@ class Tool: instructions = "Classification Example") tool.add_classification(classification) - relationship_tool = Tool( - tool = Tool.Type.RELATIONSHIP, - name = "Relationship Tool Example", - constraints = [ - ("source_tool_feature_schema_id_1", "target_tool_feature_schema_id_1"), - ("source_tool_feature_schema_id_2", "target_tool_feature_schema_id_2") - ] - ) - Attributes: tool: (Tool.Type) name: (str) @@ -89,7 +80,6 @@ class Tool: schema_id: (str) feature_schema_id: (str) attributes: (list) - constraints: (list of [str, str]) (only available for RELATIONSHIP tool type) """ class Type(Enum): @@ -113,14 +103,8 @@ class Type(Enum): schema_id: Optional[str] = None feature_schema_id: Optional[str] = None attributes: Optional[FeatureSchemaAttributes] = None - constraints: Optional[Tuple[str, str]] = None def __post_init__(self): - if self.constraints is not None and self.tool != Tool.Type.RELATIONSHIP: - warnings.warn( - "The constraints attribute is only available for Relationship tool. The provided constraints will be ignored." - ) - self.constraints = None if self.attributes is not None: warnings.warn( "The attributes for Tools are in beta. The attribute name and signature may change in the future." @@ -128,13 +112,12 @@ def __post_init__(self): @classmethod def from_dict(cls, dictionary: Dict[str, Any]) -> Dict[str, Any]: - tool = Tool.Type(dictionary["tool"]) return cls( name=dictionary["name"], schema_id=dictionary.get("schemaNodeId", None), feature_schema_id=dictionary.get("featureSchemaId", None), required=dictionary.get("required", False), - tool=tool, + tool=Tool.Type(dictionary["tool"]), classifications=[ Classification.from_dict(c) for c in dictionary["classifications"] @@ -146,9 +129,6 @@ def from_dict(cls, dictionary: Dict[str, Any]) -> Dict[str, Any]: ] if dictionary.get("attributes") else None, - constraints=dictionary.get("constraints", None) - if tool == Tool.Type.RELATIONSHIP - else None, ) def asdict(self) -> Dict[str, Any]: @@ -165,9 +145,6 @@ def asdict(self) -> Dict[str, Any]: "attributes": [a.asdict() for a in self.attributes] if self.attributes is not None else None, - "constraints": self.constraints - if self.constraints is not None - else None, } def add_classification(self, classification: Classification) -> None: @@ -178,6 +155,56 @@ def add_classification(self, classification: Classification) -> None: ) self.classifications.append(classification) +@dataclass +class RelationshipTool(Tool): + """ + A relationship tool to be added to a Project's ontology. + + 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 = RelationshipTool( + name = "Relationship Tool example") + constraints = [ + ("source_tool_feature_schema_id_1", "target_tool_feature_schema_id_1"), + ("source_tool_feature_schema_id_2", "target_tool_feature_schema_id_2") + ] + ) + classification = Classification( + class_type = Classification.Type.TEXT, + instructions = "Classification Example") + tool.add_classification(classification) + + Attributes: + tool: Tool.Type.RELATIONSHIP + name: (str) + required: (bool) + color: (str) + classifications: (list) + schema_id: (str) + feature_schema_id: (str) + attributes: (list) + constraints: (list of [str, str]) + """ + + tool: Type = Tool.Type.RELATIONSHIP + constraints: Optional[List[Tuple[str, str]]] = None + + def __post_init__(self): + super().__post_init__() + if self.tool != Tool.Type.RELATIONSHIP: + raise ValueError("RelationshipTool can only be used with Tool.Type.RELATIONSHIP") + + def asdict(self) -> Dict[str, Any]: + result = super().asdict() + if self.constraints is not None: + result["constraints"] = self.constraints + return result + """ The following 2 functions help to bridge the gap between the step reasoning all other tool ontologies. @@ -188,6 +215,8 @@ def tool_cls_from_type(tool_type: str): tool_cls = map_tool_type_to_tool_cls(tool_type) if tool_cls is not None: return tool_cls + if tool_type == Tool.Type.RELATIONSHIP: + return RelationshipTool return Tool From b37e1868e453f601e9a31f06fce1d00595491547 Mon Sep 17 00:00:00 2001 From: Karol Jamrozy Date: Thu, 29 May 2025 18:58:55 +0200 Subject: [PATCH 3/8] Fixes --- libs/labelbox/src/labelbox/schema/ontology.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/libs/labelbox/src/labelbox/schema/ontology.py b/libs/labelbox/src/labelbox/schema/ontology.py index d5cceef8e..b1f8e6dd8 100644 --- a/libs/labelbox/src/labelbox/schema/ontology.py +++ b/libs/labelbox/src/labelbox/schema/ontology.py @@ -160,15 +160,15 @@ class RelationshipTool(Tool): """ A relationship tool to be added to a Project's ontology. - To instantiate, the "tool" and "name" parameters must - be passed in. + The "tool" parameter is automatically set to Tool.Type.RELATIONSHIP + and doesn't need to be passed during instantiation. The "classifications" parameter holds a list of Classification objects. This can be used to add nested classifications to a tool. Example(s): tool = RelationshipTool( - name = "Relationship Tool example") + name = "Relationship Tool example", constraints = [ ("source_tool_feature_schema_id_1", "target_tool_feature_schema_id_1"), ("source_tool_feature_schema_id_2", "target_tool_feature_schema_id_2") @@ -180,7 +180,7 @@ class RelationshipTool(Tool): tool.add_classification(classification) Attributes: - tool: Tool.Type.RELATIONSHIP + tool: Tool.Type.RELATIONSHIP (automatically set) name: (str) required: (bool) color: (str) @@ -191,13 +191,12 @@ class RelationshipTool(Tool): constraints: (list of [str, str]) """ - tool: Type = Tool.Type.RELATIONSHIP constraints: Optional[List[Tuple[str, str]]] = None def __post_init__(self): + # Ensure tool type is set to RELATIONSHIP + self.tool = Tool.Type.RELATIONSHIP super().__post_init__() - if self.tool != Tool.Type.RELATIONSHIP: - raise ValueError("RelationshipTool can only be used with Tool.Type.RELATIONSHIP") def asdict(self) -> Dict[str, Any]: result = super().asdict() From a379b46bd14bda9217caba0db3f16d64bbae1b4f Mon Sep 17 00:00:00 2001 From: Karol Jamrozy Date: Fri, 30 May 2025 16:20:03 +0200 Subject: [PATCH 4/8] Refactor, add unit tests --- libs/labelbox/src/labelbox/schema/ontology.py | 50 ----- .../schema/tool_building/relationship_tool.py | 85 ++++++++ .../tests/unit/test_unit_relationship_tool.py | 195 ++++++++++++++++++ 3 files changed, 280 insertions(+), 50 deletions(-) create mode 100644 libs/labelbox/src/labelbox/schema/tool_building/relationship_tool.py create mode 100644 libs/labelbox/tests/unit/test_unit_relationship_tool.py diff --git a/libs/labelbox/src/labelbox/schema/ontology.py b/libs/labelbox/src/labelbox/schema/ontology.py index b1f8e6dd8..4d4ce6ef3 100644 --- a/libs/labelbox/src/labelbox/schema/ontology.py +++ b/libs/labelbox/src/labelbox/schema/ontology.py @@ -155,56 +155,6 @@ def add_classification(self, classification: Classification) -> None: ) self.classifications.append(classification) -@dataclass -class RelationshipTool(Tool): - """ - A relationship tool to be added to a Project's ontology. - - The "tool" parameter is automatically set to Tool.Type.RELATIONSHIP - and doesn't need to be passed during instantiation. - - The "classifications" parameter holds a list of Classification objects. - This can be used to add nested classifications to a tool. - - Example(s): - tool = RelationshipTool( - name = "Relationship Tool example", - constraints = [ - ("source_tool_feature_schema_id_1", "target_tool_feature_schema_id_1"), - ("source_tool_feature_schema_id_2", "target_tool_feature_schema_id_2") - ] - ) - classification = Classification( - class_type = Classification.Type.TEXT, - instructions = "Classification Example") - tool.add_classification(classification) - - Attributes: - tool: Tool.Type.RELATIONSHIP (automatically set) - name: (str) - required: (bool) - color: (str) - classifications: (list) - schema_id: (str) - feature_schema_id: (str) - attributes: (list) - constraints: (list of [str, str]) - """ - - constraints: Optional[List[Tuple[str, str]]] = None - - def __post_init__(self): - # Ensure tool type is set to RELATIONSHIP - self.tool = Tool.Type.RELATIONSHIP - super().__post_init__() - - def asdict(self) -> Dict[str, Any]: - result = super().asdict() - if self.constraints is not None: - result["constraints"] = self.constraints - return result - - """ The following 2 functions help to bridge the gap between the step reasoning all other tool ontologies. """ diff --git a/libs/labelbox/src/labelbox/schema/tool_building/relationship_tool.py b/libs/labelbox/src/labelbox/schema/tool_building/relationship_tool.py new file mode 100644 index 000000000..93733eea4 --- /dev/null +++ b/libs/labelbox/src/labelbox/schema/tool_building/relationship_tool.py @@ -0,0 +1,85 @@ +# type: ignore + +import uuid +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Tuple + +from labelbox.schema.ontology import Tool + +@dataclass +class RelationshipTool(Tool): + """ + A relationship tool to be added to a Project's ontology. + + The "tool" parameter is automatically set to Tool.Type.RELATIONSHIP + and doesn't need to be passed during instantiation. + + The "classifications" parameter holds a list of Classification objects. + This can be used to add nested classifications to a tool. + + Example(s): + tool = RelationshipTool( + name = "Relationship Tool example", + constraints = [ + ("source_tool_feature_schema_id_1", "target_tool_feature_schema_id_1"), + ("source_tool_feature_schema_id_2", "target_tool_feature_schema_id_2") + ] + ) + classification = Classification( + class_type = Classification.Type.TEXT, + instructions = "Classification Example") + tool.add_classification(classification) + + Attributes: + tool: Tool.Type.RELATIONSHIP (automatically set) + name: (str) + required: (bool) + color: (str) + classifications: (list) + schema_id: (str) + feature_schema_id: (str) + attributes: (list) + constraints: (list of [str, str]) + """ + + constraints: Optional[List[Tuple[str, str]]] = None + + def __init__(self, name: str, constraints: Optional[List[Tuple[str, str]]] = None, **kwargs): + super().__init__(Tool.Type.RELATIONSHIP, name, **kwargs) + if constraints is not None: + self.constraints = constraints + + def __post_init__(self): + # Ensure tool type is set to RELATIONSHIP + self.tool = Tool.Type.RELATIONSHIP + super().__post_init__() + + def asdict(self) -> Dict[str, Any]: + result = super().asdict() + if self.constraints is not None: + result["definition"] = { "constraints": self.constraints } + return result + + def add_constraint(self, start: Tool, end: Tool) -> None: + if self.constraints is None: + self.constraints = [] + + # Ensure feature schema ids are set for the tools, + # the newly set ids will be changed during ontology creation + # but we need to refer to the same ids in the constraints array + # to ensure that the valid constraints are created. + if start.feature_schema_id is None: + start.feature_schema_id = str(uuid.uuid4()) + if start.schema_id is None: + start.schema_id = str(uuid.uuid4()) + if end.feature_schema_id is None: + end.feature_schema_id = str(uuid.uuid4()) + if end.schema_id is None: + end.schema_id = str(uuid.uuid4()) + + self.constraints.append((start.feature_schema_id, end.feature_schema_id)) + + def set_constraints(self, constraints: List[Tuple[Tool, Tool]]) -> None: + self.constraints = [] + for constraint in constraints: + self.add_constraint(constraint[0], constraint[1]) diff --git a/libs/labelbox/tests/unit/test_unit_relationship_tool.py b/libs/labelbox/tests/unit/test_unit_relationship_tool.py new file mode 100644 index 000000000..d18ee80be --- /dev/null +++ b/libs/labelbox/tests/unit/test_unit_relationship_tool.py @@ -0,0 +1,195 @@ +import pytest +import uuid +from unittest.mock import patch + +from labelbox.schema.ontology import Tool +from labelbox.schema.tool_building.relationship_tool import RelationshipTool +from labelbox.schema.tool_building.classification import Classification + + +def test_basic_instantiation(): + tool = RelationshipTool(name="Test Relationship Tool") + + assert tool.name == "Test Relationship Tool" + assert tool.tool == Tool.Type.RELATIONSHIP + assert tool.constraints is None + assert tool.required is False + assert tool.color is None + assert tool.schema_id is None + assert tool.feature_schema_id is None + + +def test_instantiation_with_constraints(): + constraints = [ + ("source_id_1", "target_id_1"), + ("source_id_2", "target_id_2") + ] + tool = RelationshipTool(name="Test Tool", constraints=constraints) + + assert tool.name == "Test Tool" + assert tool.constraints == constraints + assert len(tool.constraints) == 2 + +def test_post_init_sets_tool_type(): + tool = RelationshipTool(name="Test Tool") + assert tool.tool == Tool.Type.RELATIONSHIP + + +def test_asdict_without_constraints(): + tool = RelationshipTool( + name="Test Tool", + required=True, + color="#FF0000" + ) + + result = tool.asdict() + expected = { + "tool": "edge", + "name": "Test Tool", + "required": True, + "color": "#FF0000", + "classifications": [], + "schemaNodeId": None, + "featureSchemaId": None, + "attributes": None + } + + assert result == expected + +def test_asdict_with_constraints(): + constraints = [("source_id", "target_id")] + tool = RelationshipTool(name="Test Tool", constraints=constraints) + + result = tool.asdict() + + assert "definition" in result + assert result["definition"] == {"constraints": constraints} + assert result["tool"] == "edge" + assert result["name"] == "Test Tool" + + +def test_add_constraint_to_empty_constraints(): + tool = RelationshipTool(name="Test Tool") + start_tool = Tool(Tool.Type.BBOX, "Start Tool") + end_tool = Tool(Tool.Type.POLYGON, "End Tool") + + with patch('uuid.uuid4') as mock_uuid: + mock_uuid.return_value.hex = "test-uuid" + tool.add_constraint(start_tool, end_tool) + + assert tool.constraints is not None + assert len(tool.constraints) == 1 + assert start_tool.feature_schema_id is not None + assert start_tool.schema_id is not None + assert end_tool.feature_schema_id is not None + assert end_tool.schema_id is not None + + +def test_add_constraint_to_existing_constraints(): + existing_constraints = [("existing_source", "existing_target")] + tool = RelationshipTool(name="Test Tool", constraints=existing_constraints) + + start_tool = Tool(Tool.Type.BBOX, "Start Tool") + end_tool = Tool(Tool.Type.POLYGON, "End Tool") + + tool.add_constraint(start_tool, end_tool) + + assert len(tool.constraints) == 2 + assert tool.constraints[0] == ("existing_source", "existing_target") + assert tool.constraints[1] == (start_tool.feature_schema_id, end_tool.feature_schema_id) + + +def test_add_constraint_preserves_existing_ids(): + tool = RelationshipTool(name="Test Tool") + start_tool_feature_schema_id = "start_tool_feature_schema_id" + start_tool_schema_id = "start_tool_schema_id" + start_tool = Tool(Tool.Type.BBOX, "Start Tool", feature_schema_id=start_tool_feature_schema_id, schema_id=start_tool_schema_id) + end_tool_feature_schema_id = "end_tool_feature_schema_id" + end_tool_schema_id = "end_tool_schema_id" + end_tool = Tool(Tool.Type.POLYGON, "End Tool", feature_schema_id=end_tool_feature_schema_id, schema_id=end_tool_schema_id) + + tool.add_constraint(start_tool, end_tool) + + assert start_tool.feature_schema_id == start_tool_feature_schema_id + assert start_tool.schema_id == start_tool_schema_id + assert end_tool.feature_schema_id == end_tool_feature_schema_id + assert end_tool.schema_id == end_tool_schema_id + assert tool.constraints == [(start_tool_feature_schema_id, end_tool_feature_schema_id)] + + +def test_set_constraints(): + tool = RelationshipTool(name="Test Tool") + + start_tool1 = Tool(Tool.Type.BBOX, "Start Tool 1") + end_tool1 = Tool(Tool.Type.POLYGON, "End Tool 1") + start_tool2 = Tool(Tool.Type.POINT, "Start Tool 2") + end_tool2 = Tool(Tool.Type.LINE, "End Tool 2") + + tool.set_constraints([ + (start_tool1, end_tool1), + (start_tool2, end_tool2) + ]) + + assert len(tool.constraints) == 2 + assert tool.constraints[0] == (start_tool1.feature_schema_id, end_tool1.feature_schema_id) + assert tool.constraints[1] == (start_tool2.feature_schema_id, end_tool2.feature_schema_id) + + +def test_set_constraints_replaces_existing(): + existing_constraints = [("old_source", "old_target")] + tool = RelationshipTool(name="Test Tool", constraints=existing_constraints) + + start_tool = Tool(Tool.Type.BBOX, "Start Tool") + end_tool = Tool(Tool.Type.POLYGON, "End Tool") + + tool.set_constraints([(start_tool, end_tool)]) + + assert len(tool.constraints) == 1 + assert tool.constraints[0] != ("old_source", "old_target") + assert tool.constraints[0] == (start_tool.feature_schema_id, end_tool.feature_schema_id) + + +def test_uuid_generation_in_add_constraint(): + tool = RelationshipTool(name="Test Tool") + + start_tool = Tool(Tool.Type.BBOX, "Start Tool") + end_tool = Tool(Tool.Type.POLYGON, "End Tool") + + # Ensure tools don't have IDs initially + assert start_tool.feature_schema_id is None + assert start_tool.schema_id is None + assert end_tool.feature_schema_id is None + assert end_tool.schema_id is None + + tool.add_constraint(start_tool, end_tool) + + # Check that UUIDs were generated + assert start_tool.feature_schema_id is not None + assert start_tool.schema_id is not None + assert end_tool.feature_schema_id is not None + assert end_tool.schema_id is not None + + # Check that they are valid UUID strings + uuid.UUID(start_tool.feature_schema_id) # Will raise ValueError if invalid + uuid.UUID(start_tool.schema_id) + uuid.UUID(end_tool.feature_schema_id) + uuid.UUID(end_tool.schema_id) + + +def test_constraints_in_asdict(): + tool = RelationshipTool(name="Test Tool") + + start_tool = Tool(Tool.Type.BBOX, "Start Tool") + end_tool = Tool(Tool.Type.POLYGON, "End Tool") + + tool.add_constraint(start_tool, end_tool) + + result = tool.asdict() + + assert "definition" in result + assert "constraints" in result["definition"] + assert len(result["definition"]["constraints"]) == 1 + assert result["definition"]["constraints"][0] == ( + start_tool.feature_schema_id, + end_tool.feature_schema_id + ) From 3c5bf21754d63fe8fcc024841b12067a674f3da8 Mon Sep 17 00:00:00 2001 From: Karol Jamrozy Date: Wed, 2 Jul 2025 18:06:16 +0200 Subject: [PATCH 5/8] Fix lint --- libs/labelbox/src/labelbox/schema/ontology.py | 2 +- libs/labelbox/tests/unit/test_unit_relationship_tool.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/libs/labelbox/src/labelbox/schema/ontology.py b/libs/labelbox/src/labelbox/schema/ontology.py index 4d4ce6ef3..fcc2a3d31 100644 --- a/libs/labelbox/src/labelbox/schema/ontology.py +++ b/libs/labelbox/src/labelbox/schema/ontology.py @@ -4,7 +4,7 @@ import json from dataclasses import dataclass, field from enum import Enum -from typing import Any, Dict, List, Optional, Union, Tuple +from typing import Any, Dict, List, Optional, Union from lbox.exceptions import InconsistentOntologyException diff --git a/libs/labelbox/tests/unit/test_unit_relationship_tool.py b/libs/labelbox/tests/unit/test_unit_relationship_tool.py index d18ee80be..b23122a9f 100644 --- a/libs/labelbox/tests/unit/test_unit_relationship_tool.py +++ b/libs/labelbox/tests/unit/test_unit_relationship_tool.py @@ -1,10 +1,8 @@ -import pytest import uuid from unittest.mock import patch from labelbox.schema.ontology import Tool from labelbox.schema.tool_building.relationship_tool import RelationshipTool -from labelbox.schema.tool_building.classification import Classification def test_basic_instantiation(): From c375b684cd3d37846ca0b899469298d149b07f58 Mon Sep 17 00:00:00 2001 From: Karol Jamrozy Date: Wed, 2 Jul 2025 18:32:24 +0200 Subject: [PATCH 6/8] Fix lint --- libs/labelbox/src/labelbox/schema/ontology.py | 1 + .../schema/tool_building/relationship_tool.py | 16 ++- .../tests/unit/test_unit_relationship_tool.py | 113 ++++++++++-------- 3 files changed, 79 insertions(+), 51 deletions(-) diff --git a/libs/labelbox/src/labelbox/schema/ontology.py b/libs/labelbox/src/labelbox/schema/ontology.py index fcc2a3d31..897759af1 100644 --- a/libs/labelbox/src/labelbox/schema/ontology.py +++ b/libs/labelbox/src/labelbox/schema/ontology.py @@ -155,6 +155,7 @@ def add_classification(self, classification: Classification) -> None: ) self.classifications.append(classification) + """ The following 2 functions help to bridge the gap between the step reasoning all other tool ontologies. """ diff --git a/libs/labelbox/src/labelbox/schema/tool_building/relationship_tool.py b/libs/labelbox/src/labelbox/schema/tool_building/relationship_tool.py index 93733eea4..f11d204a9 100644 --- a/libs/labelbox/src/labelbox/schema/tool_building/relationship_tool.py +++ b/libs/labelbox/src/labelbox/schema/tool_building/relationship_tool.py @@ -6,6 +6,7 @@ from labelbox.schema.ontology import Tool + @dataclass class RelationshipTool(Tool): """ @@ -44,7 +45,12 @@ class RelationshipTool(Tool): constraints: Optional[List[Tuple[str, str]]] = None - def __init__(self, name: str, constraints: Optional[List[Tuple[str, str]]] = None, **kwargs): + def __init__( + self, + name: str, + constraints: Optional[List[Tuple[str, str]]] = None, + **kwargs, + ): super().__init__(Tool.Type.RELATIONSHIP, name, **kwargs) if constraints is not None: self.constraints = constraints @@ -57,7 +63,7 @@ def __post_init__(self): def asdict(self) -> Dict[str, Any]: result = super().asdict() if self.constraints is not None: - result["definition"] = { "constraints": self.constraints } + result["definition"] = {"constraints": self.constraints} return result def add_constraint(self, start: Tool, end: Tool) -> None: @@ -77,8 +83,10 @@ def add_constraint(self, start: Tool, end: Tool) -> None: if end.schema_id is None: end.schema_id = str(uuid.uuid4()) - self.constraints.append((start.feature_schema_id, end.feature_schema_id)) - + self.constraints.append( + (start.feature_schema_id, end.feature_schema_id) + ) + def set_constraints(self, constraints: List[Tuple[Tool, Tool]]) -> None: self.constraints = [] for constraint in constraints: diff --git a/libs/labelbox/tests/unit/test_unit_relationship_tool.py b/libs/labelbox/tests/unit/test_unit_relationship_tool.py index b23122a9f..0f50187da 100644 --- a/libs/labelbox/tests/unit/test_unit_relationship_tool.py +++ b/libs/labelbox/tests/unit/test_unit_relationship_tool.py @@ -7,7 +7,7 @@ def test_basic_instantiation(): tool = RelationshipTool(name="Test Relationship Tool") - + assert tool.name == "Test Relationship Tool" assert tool.tool == Tool.Type.RELATIONSHIP assert tool.constraints is None @@ -20,26 +20,23 @@ def test_basic_instantiation(): def test_instantiation_with_constraints(): constraints = [ ("source_id_1", "target_id_1"), - ("source_id_2", "target_id_2") + ("source_id_2", "target_id_2"), ] tool = RelationshipTool(name="Test Tool", constraints=constraints) - + assert tool.name == "Test Tool" assert tool.constraints == constraints assert len(tool.constraints) == 2 + def test_post_init_sets_tool_type(): tool = RelationshipTool(name="Test Tool") assert tool.tool == Tool.Type.RELATIONSHIP def test_asdict_without_constraints(): - tool = RelationshipTool( - name="Test Tool", - required=True, - color="#FF0000" - ) - + tool = RelationshipTool(name="Test Tool", required=True, color="#FF0000") + result = tool.asdict() expected = { "tool": "edge", @@ -49,17 +46,18 @@ def test_asdict_without_constraints(): "classifications": [], "schemaNodeId": None, "featureSchemaId": None, - "attributes": None + "attributes": None, } - + assert result == expected + def test_asdict_with_constraints(): constraints = [("source_id", "target_id")] tool = RelationshipTool(name="Test Tool", constraints=constraints) - + result = tool.asdict() - + assert "definition" in result assert result["definition"] == {"constraints": constraints} assert result["tool"] == "edge" @@ -70,11 +68,11 @@ def test_add_constraint_to_empty_constraints(): tool = RelationshipTool(name="Test Tool") start_tool = Tool(Tool.Type.BBOX, "Start Tool") end_tool = Tool(Tool.Type.POLYGON, "End Tool") - - with patch('uuid.uuid4') as mock_uuid: + + with patch("uuid.uuid4") as mock_uuid: mock_uuid.return_value.hex = "test-uuid" tool.add_constraint(start_tool, end_tool) - + assert tool.constraints is not None assert len(tool.constraints) == 1 assert start_tool.feature_schema_id is not None @@ -86,87 +84,108 @@ def test_add_constraint_to_empty_constraints(): def test_add_constraint_to_existing_constraints(): existing_constraints = [("existing_source", "existing_target")] tool = RelationshipTool(name="Test Tool", constraints=existing_constraints) - + start_tool = Tool(Tool.Type.BBOX, "Start Tool") end_tool = Tool(Tool.Type.POLYGON, "End Tool") - + tool.add_constraint(start_tool, end_tool) - + assert len(tool.constraints) == 2 assert tool.constraints[0] == ("existing_source", "existing_target") - assert tool.constraints[1] == (start_tool.feature_schema_id, end_tool.feature_schema_id) + assert tool.constraints[1] == ( + start_tool.feature_schema_id, + end_tool.feature_schema_id, + ) def test_add_constraint_preserves_existing_ids(): tool = RelationshipTool(name="Test Tool") start_tool_feature_schema_id = "start_tool_feature_schema_id" start_tool_schema_id = "start_tool_schema_id" - start_tool = Tool(Tool.Type.BBOX, "Start Tool", feature_schema_id=start_tool_feature_schema_id, schema_id=start_tool_schema_id) + start_tool = Tool( + Tool.Type.BBOX, + "Start Tool", + feature_schema_id=start_tool_feature_schema_id, + schema_id=start_tool_schema_id, + ) end_tool_feature_schema_id = "end_tool_feature_schema_id" end_tool_schema_id = "end_tool_schema_id" - end_tool = Tool(Tool.Type.POLYGON, "End Tool", feature_schema_id=end_tool_feature_schema_id, schema_id=end_tool_schema_id) + end_tool = Tool( + Tool.Type.POLYGON, + "End Tool", + feature_schema_id=end_tool_feature_schema_id, + schema_id=end_tool_schema_id, + ) tool.add_constraint(start_tool, end_tool) - + assert start_tool.feature_schema_id == start_tool_feature_schema_id assert start_tool.schema_id == start_tool_schema_id assert end_tool.feature_schema_id == end_tool_feature_schema_id assert end_tool.schema_id == end_tool_schema_id - assert tool.constraints == [(start_tool_feature_schema_id, end_tool_feature_schema_id)] + assert tool.constraints == [ + (start_tool_feature_schema_id, end_tool_feature_schema_id) + ] def test_set_constraints(): tool = RelationshipTool(name="Test Tool") - + start_tool1 = Tool(Tool.Type.BBOX, "Start Tool 1") end_tool1 = Tool(Tool.Type.POLYGON, "End Tool 1") start_tool2 = Tool(Tool.Type.POINT, "Start Tool 2") end_tool2 = Tool(Tool.Type.LINE, "End Tool 2") - - tool.set_constraints([ - (start_tool1, end_tool1), - (start_tool2, end_tool2) - ]) - + + tool.set_constraints([(start_tool1, end_tool1), (start_tool2, end_tool2)]) + assert len(tool.constraints) == 2 - assert tool.constraints[0] == (start_tool1.feature_schema_id, end_tool1.feature_schema_id) - assert tool.constraints[1] == (start_tool2.feature_schema_id, end_tool2.feature_schema_id) + assert tool.constraints[0] == ( + start_tool1.feature_schema_id, + end_tool1.feature_schema_id, + ) + assert tool.constraints[1] == ( + start_tool2.feature_schema_id, + end_tool2.feature_schema_id, + ) def test_set_constraints_replaces_existing(): existing_constraints = [("old_source", "old_target")] tool = RelationshipTool(name="Test Tool", constraints=existing_constraints) - + start_tool = Tool(Tool.Type.BBOX, "Start Tool") end_tool = Tool(Tool.Type.POLYGON, "End Tool") - + tool.set_constraints([(start_tool, end_tool)]) - + assert len(tool.constraints) == 1 assert tool.constraints[0] != ("old_source", "old_target") - assert tool.constraints[0] == (start_tool.feature_schema_id, end_tool.feature_schema_id) + assert tool.constraints[0] == ( + start_tool.feature_schema_id, + end_tool.feature_schema_id, + ) def test_uuid_generation_in_add_constraint(): tool = RelationshipTool(name="Test Tool") - + start_tool = Tool(Tool.Type.BBOX, "Start Tool") end_tool = Tool(Tool.Type.POLYGON, "End Tool") - + # Ensure tools don't have IDs initially assert start_tool.feature_schema_id is None assert start_tool.schema_id is None assert end_tool.feature_schema_id is None assert end_tool.schema_id is None - + tool.add_constraint(start_tool, end_tool) - + # Check that UUIDs were generated assert start_tool.feature_schema_id is not None assert start_tool.schema_id is not None assert end_tool.feature_schema_id is not None assert end_tool.schema_id is not None - + # Check that they are valid UUID strings uuid.UUID(start_tool.feature_schema_id) # Will raise ValueError if invalid uuid.UUID(start_tool.schema_id) @@ -176,18 +195,18 @@ def test_uuid_generation_in_add_constraint(): def test_constraints_in_asdict(): tool = RelationshipTool(name="Test Tool") - + start_tool = Tool(Tool.Type.BBOX, "Start Tool") end_tool = Tool(Tool.Type.POLYGON, "End Tool") - + tool.add_constraint(start_tool, end_tool) - + result = tool.asdict() - + assert "definition" in result assert "constraints" in result["definition"] assert len(result["definition"]["constraints"]) == 1 assert result["definition"]["constraints"][0] == ( start_tool.feature_schema_id, - end_tool.feature_schema_id + end_tool.feature_schema_id, ) From 3fcfce9094dd95202c33e92edd295e2752012b66 Mon Sep 17 00:00:00 2001 From: Karol Jamrozy Date: Wed, 2 Jul 2025 20:30:39 +0200 Subject: [PATCH 7/8] Address CR comment --- .../src/labelbox/schema/tool_building/tool_type_mapping.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libs/labelbox/src/labelbox/schema/tool_building/tool_type_mapping.py b/libs/labelbox/src/labelbox/schema/tool_building/tool_type_mapping.py index 6de255cc3..b9b202bc2 100644 --- a/libs/labelbox/src/labelbox/schema/tool_building/tool_type_mapping.py +++ b/libs/labelbox/src/labelbox/schema/tool_building/tool_type_mapping.py @@ -6,9 +6,15 @@ StepReasoningTool, ) from labelbox.schema.tool_building.tool_type import ToolType +from labelbox.schema.ontology import Tool +from labelbox.schema.tool_building.relationship_tool import RelationshipTool def map_tool_type_to_tool_cls(tool_type_str: str): + # Relationship tool uses a legacy tool type, so we need to handle it separately. + if tool_type_str.lower() == Tool.Type.RELATIONSHIP: + return RelationshipTool() + if not ToolType.valid(tool_type_str): return None From 0e10bb271266603d2ab0b0be7ad6f43d1ee2ed6b Mon Sep 17 00:00:00 2001 From: Karol Jamrozy Date: Wed, 2 Jul 2025 20:47:27 +0200 Subject: [PATCH 8/8] Revert "Address CR comment" This reverts commit 3fcfce9094dd95202c33e92edd295e2752012b66. --- .../src/labelbox/schema/tool_building/tool_type_mapping.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/libs/labelbox/src/labelbox/schema/tool_building/tool_type_mapping.py b/libs/labelbox/src/labelbox/schema/tool_building/tool_type_mapping.py index b9b202bc2..6de255cc3 100644 --- a/libs/labelbox/src/labelbox/schema/tool_building/tool_type_mapping.py +++ b/libs/labelbox/src/labelbox/schema/tool_building/tool_type_mapping.py @@ -6,15 +6,9 @@ StepReasoningTool, ) from labelbox.schema.tool_building.tool_type import ToolType -from labelbox.schema.ontology import Tool -from labelbox.schema.tool_building.relationship_tool import RelationshipTool def map_tool_type_to_tool_cls(tool_type_str: str): - # Relationship tool uses a legacy tool type, so we need to handle it separately. - if tool_type_str.lower() == Tool.Type.RELATIONSHIP: - return RelationshipTool() - if not ToolType.valid(tool_type_str): return None