diff --git a/CHANGELOG.md b/CHANGELOG.md index 631f65f5..dc5cd08b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,18 @@ This project uses [*towncrier*](https://towncrier.readthedocs.io/) and the chang +## [1.13.2](https://github.com/opsmill/infrahub-sdk-python/tree/v1.13.2) - 2025-06-27 + +### Fixed + +- Re-enable specifying a cardinality-one relationship using a RelatedNode when creating an InfrahubNode ([#452](https://github.com/opsmill/infrahub-sdk-python/issues/452)) + +## [1.13.1](https://github.com/opsmill/infrahub-sdk-python/tree/v1.13.1) - 2025-06-19 + +### Fixed + +- Fix the import path of the Attribute class [#448](https://github.com/opsmill/infrahub-sdk-python/pull/448) + ## [1.13.0](https://github.com/opsmill/infrahub-sdk-python/tree/v1.13.0) - 2025-06-11 This release adds support for the new NumberPool attribute and loading object and menu files from external repositories in Infrahub 1.3. diff --git a/infrahub_sdk/node/__init__.py b/infrahub_sdk/node/__init__.py index a2d71a87..2a1c39e5 100644 --- a/infrahub_sdk/node/__init__.py +++ b/infrahub_sdk/node/__init__.py @@ -1,5 +1,6 @@ from __future__ import annotations +from .attribute import Attribute from .constants import ( ARTIFACT_DEFINITION_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE, ARTIFACT_FETCH_FEATURE_NOT_SUPPORTED_MESSAGE, @@ -25,6 +26,7 @@ "PROPERTIES_FLAG", "PROPERTIES_OBJECT", "SAFE_VALUE", + "Attribute", "InfrahubNode", "InfrahubNodeBase", "InfrahubNodeSync", diff --git a/infrahub_sdk/node/node.py b/infrahub_sdk/node/node.py index c10b14a0..7a095586 100644 --- a/infrahub_sdk/node/node.py +++ b/infrahub_sdk/node/node.py @@ -501,11 +501,21 @@ async def from_graphql( return cls(client=client, schema=schema, branch=branch, data=cls._strip_alias(data)) - def _init_relationships(self, data: dict | None = None) -> None: + def _init_relationships(self, data: dict | RelatedNode | None = None) -> None: for rel_schema in self._schema.relationships: rel_data = data.get(rel_schema.name, None) if isinstance(data, dict) else None if rel_schema.cardinality == "one": + if isinstance(rel_data, RelatedNode): + peer_id_data: dict[str, Any] = {} + if rel_data.id: + peer_id_data["id"] = rel_data.id + if rel_data.hfid: + peer_id_data["hfid"] = rel_data.hfid + if peer_id_data: + rel_data = peer_id_data + else: + rel_data = None self._relationship_cardinality_one_data[rel_schema.name] = RelatedNode( name=rel_schema.name, branch=self._branch, client=self._client, schema=rel_schema, data=rel_data ) @@ -1079,10 +1089,19 @@ def _init_relationships(self, data: dict | None = None) -> None: rel_data = data.get(rel_schema.name, None) if isinstance(data, dict) else None if rel_schema.cardinality == "one": + if isinstance(rel_data, RelatedNodeSync): + peer_id_data: dict[str, Any] = {} + if rel_data.id: + peer_id_data["id"] = rel_data.id + if rel_data.hfid: + peer_id_data["hfid"] = rel_data.hfid + if peer_id_data: + rel_data = peer_id_data + else: + rel_data = None self._relationship_cardinality_one_data[rel_schema.name] = RelatedNodeSync( name=rel_schema.name, branch=self._branch, client=self._client, schema=rel_schema, data=rel_data ) - else: self._relationship_cardinality_many_data[rel_schema.name] = RelationshipManagerSync( name=rel_schema.name, diff --git a/pyproject.toml b/pyproject.toml index ea4530f4..f6b4f7e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "infrahub-sdk" -version = "1.13.0" +version = "1.13.2" description = "Python Client to interact with Infrahub" authors = ["OpsMill "] readme = "README.md" diff --git a/tests/integration/test_node.py b/tests/integration/test_node.py index 6c2244a3..c398d00a 100644 --- a/tests/integration/test_node.py +++ b/tests/integration/test_node.py @@ -63,6 +63,27 @@ async def test_node_create_with_relationships( assert node_after.name.value == node.name.value assert node_after.manufacturer.peer.id == manufacturer_mercedes.id + async def test_node_create_with_relationships_using_related_node( + self, + default_branch: str, + client: InfrahubClient, + initial_schema: None, + manufacturer_mercedes, + car_golf, + person_joe, + ): + related_node = car_golf.owner + node = await client.create( + kind=TESTING_CAR, name="Tiguan", color="Black", manufacturer=manufacturer_mercedes, owner=related_node + ) + await node.save(allow_upsert=True) + assert node.id is not None + + node_after = await client.get(kind=TESTING_CAR, id=node.id, prefetch_relationships=True) + assert node_after.name.value == node.name.value + assert node_after.manufacturer.peer.id == manufacturer_mercedes.id + assert node_after.owner.peer.id == person_joe.id + async def test_node_update_with_original_data( self, default_branch: str, diff --git a/tests/unit/sdk/test_node.py b/tests/unit/sdk/test_node.py index 31616d26..2938d4a5 100644 --- a/tests/unit/sdk/test_node.py +++ b/tests/unit/sdk/test_node.py @@ -15,6 +15,7 @@ parse_human_friendly_id, ) from infrahub_sdk.node.constants import SAFE_VALUE +from infrahub_sdk.node.related_node import RelatedNode, RelatedNodeSync from infrahub_sdk.schema import GenericSchema, NodeSchemaAPI if TYPE_CHECKING: @@ -194,6 +195,50 @@ async def test_init_node_data_user_with_relationships(client, location_schema: N assert "get_kind" in keys +@pytest.mark.parametrize("client_type", client_types) +@pytest.mark.parametrize("rel_data", [{"id": "pppppppp"}, {"hfid": ["pppp", "pppp"]}]) +async def test_init_node_data_user_with_relationships_using_related_node( + client, location_schema: NodeSchemaAPI, client_type, rel_data +): + rel_schema = location_schema.get_relationship(name="primary_tag") + if client_type == "standard": + primary_tag = RelatedNode(name="primary_tag", branch="main", client=client, schema=rel_schema, data=rel_data) + else: + primary_tag = RelatedNodeSync( + name="primary_tag", branch="main", client=client, schema=rel_schema, data=rel_data + ) + + data = { + "name": {"value": "JFK1"}, + "description": {"value": "JFK Airport"}, + "type": {"value": "SITE"}, + "primary_tag": primary_tag, + "tags": [{"id": "aaaaaa"}, {"id": "bbbb"}], + } + if client_type == "standard": + node = InfrahubNode(client=client, schema=location_schema, data=data) + else: + node = InfrahubNodeSync(client=client, schema=location_schema, data=data) + + assert node.name.value == "JFK1" + assert node.name.is_protected is None + assert node.description.value == "JFK Airport" + assert node.type.value == "SITE" + + assert isinstance(node.tags, RelationshipManagerBase) + assert len(node.tags.peers) == 2 + assert isinstance(node.tags.peers[0], RelatedNodeBase) + assert isinstance(node.primary_tag, RelatedNodeBase) + assert node.primary_tag.id == rel_data.get("id") + assert node.primary_tag.hfid == rel_data.get("hfid") + + keys = dir(node) + assert "name" in keys + assert "type" in keys + assert "tags" in keys + assert "get_kind" in keys + + @pytest.mark.parametrize("property_test", property_tests) @pytest.mark.parametrize("client_type", client_types) async def test_init_node_data_graphql(