Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ This project uses [*towncrier*](https://towncrier.readthedocs.io/) and the chang

<!-- towncrier release notes start -->

## [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.
Expand Down
2 changes: 2 additions & 0 deletions infrahub_sdk/node/__init__.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -25,6 +26,7 @@
"PROPERTIES_FLAG",
"PROPERTIES_OBJECT",
"SAFE_VALUE",
"Attribute",
"InfrahubNode",
"InfrahubNodeBase",
"InfrahubNodeSync",
Expand Down
23 changes: 21 additions & 2 deletions infrahub_sdk/node/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <info@opsmill.com>"]
readme = "README.md"
Expand Down
21 changes: 21 additions & 0 deletions tests/integration/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
45 changes: 45 additions & 0 deletions tests/unit/sdk/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down