From 05d636b43215eebe1dbf49bedd4d5a36e7b8140a Mon Sep 17 00:00:00 2001 From: Pete Crocker Date: Sat, 16 May 2026 15:45:55 +0100 Subject: [PATCH 1/5] fix(repository): detect existing repo when .git is a worktree gitlink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitRepoManager.initialize_repo() checked for `.git` with `.is_dir()`, which misses git worktrees (`.git` is a file containing `gitdir: ...`). The existing-repo branch was skipped and `Repo.init` then crashed with `FileExistsError` trying to mkdir the gitlink file. This made `infrahubctl transform` (and any other CLI relying on branch auto-detection) unusable from a worktree. Switch the check to `.exists()` — dulwich's `Repo(path)` already resolves the gitlink to the real controldir, and `porcelain.active_branch` returns the worktree's branch correctly. --- infrahub_sdk/repository.py | 3 ++- tests/unit/sdk/test_repository.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/infrahub_sdk/repository.py b/infrahub_sdk/repository.py index 2ad2080a..5bcd1387 100644 --- a/infrahub_sdk/repository.py +++ b/infrahub_sdk/repository.py @@ -17,7 +17,8 @@ def initialize_repo(self) -> Repo: root_path = Path(self.root_directory) - if root_path.exists() and (root_path / ".git").is_dir(): + if root_path.exists() and (root_path / ".git").exists(): + # `.git` is a directory in a regular clone and a gitlink file in a worktree. repo = Repo(self.root_directory) # Open existing repo else: repo = Repo.init(self.root_directory, default_branch=self.branch.encode("utf-8")) diff --git a/tests/unit/sdk/test_repository.py b/tests/unit/sdk/test_repository.py index 756b2e36..eca1c9a6 100644 --- a/tests/unit/sdk/test_repository.py +++ b/tests/unit/sdk/test_repository.py @@ -41,6 +41,26 @@ def test_initialize_repo_uses_existing_repo(temp_dir: str) -> None: assert (Path(temp_dir) / ".git").is_dir() +def test_initialize_repo_uses_existing_repo_in_worktree(temp_dir: str) -> None: + """A git worktree has a `.git` file (gitlink), not a directory. + + GitRepoManager must open the existing repo via the gitlink instead of trying + to re-initialize one, which would crash on `mkdir` because the file already exists. + """ + main_path = Path(temp_dir) / "main" + worktree_path = Path(temp_dir) / "worktree" + main_path.mkdir() + worktree_path.mkdir() + Repo.init(str(main_path), default_branch=b"main") + (worktree_path / ".git").write_text(f"gitdir: {main_path}/.git\n") + + manager = GitRepoManager(str(worktree_path)) + + assert manager.git is not None + assert isinstance(manager.git, Repo) + assert (worktree_path / ".git").is_file() + + def test_active_branch_returns_correct_branch(temp_dir: str) -> None: """Test that the active branch is correctly returned.""" manager = GitRepoManager(temp_dir, branch="develop") From 05ac99774b1e17c39d04ec07613a331115c5b528 Mon Sep 17 00:00:00 2001 From: Babatunde Olusola Date: Thu, 11 Jun 2026 18:00:39 +0100 Subject: [PATCH 2/5] IHS-119: Fix upsert not working for numberpool and attribute in hfid (#1014) --- changelog/339.fixed.md | 1 + .../python-sdk/guides/resource-manager.mdx | 55 ++++++++ .../sdk_ref/infrahub_sdk/node/attribute.mdx | 16 +++ infrahub_sdk/node/attribute.py | 14 ++ infrahub_sdk/node/node.py | 42 +++++- tests/unit/sdk/pool/conftest.py | 20 +++ .../unit/sdk/pool/test_attribute_from_pool.py | 129 ++++++++++++++++++ 7 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 changelog/339.fixed.md diff --git a/changelog/339.fixed.md b/changelog/339.fixed.md new file mode 100644 index 00000000..4543386c --- /dev/null +++ b/changelog/339.fixed.md @@ -0,0 +1 @@ +Calling `.save(allow_upsert=True)` on a node whose human-friendly identifier contains a CoreNumberPool-sourced attribute now raises a clear `ValidationError` instead of crashing with an opaque backend error. diff --git a/docs/docs/python-sdk/guides/resource-manager.mdx b/docs/docs/python-sdk/guides/resource-manager.mdx index 97cb4b90..8a42b6e9 100644 --- a/docs/docs/python-sdk/guides/resource-manager.mdx +++ b/docs/docs/python-sdk/guides/resource-manager.mdx @@ -309,3 +309,58 @@ query { ``` Notice that we have one IP address allocated by the Resource manager in the test branch. The query in the main branch shows us this allocation, indicating that it has been allocated and the resource cannot be allocated again. However, the IP address does not exist itself within the main branch. + +## CoreNumberPool and attribute allocation + +`CoreNumberPool` allocates integer values (such as VLAN IDs or AS numbers) directly to node attributes. The pool assigns the integer value at the moment the node is created on the server. + +```python +vlan = await client.create( + kind="InfraVLAN", + name="VLAN-100", + vlan_id={"from_pool": {"id": pool_id}}, +) +await vlan.save() +``` + +### Limitation: `allow_upsert=True` with a pool-sourced HFID attribute + +`CoreNumberPool` assigns the integer value at server creation time. The SDK does not know the assigned value before the node exists. When a node's human-friendly identifier (HFID) includes a pool-sourced attribute, the SDK cannot construct the HFID needed to look up an existing node for upsert. + +:::warning + +Calling `save(allow_upsert=True)` on a node whose HFID contains a `CoreNumberPool`-sourced attribute raises `ValidationError` before any network call is made. + +```python +# Schema has human_friendly_id: ["vlan_id__value"] +vlan = await client.create( + kind="InfraVLAN", + name="VLAN-100", + vlan_id={"from_pool": {"id": pool_id}}, +) + +# This raises ValidationError — the pool-assigned vlan_id is unknown client-side +await vlan.save(allow_upsert=True) +``` + +**Alternatives:** + +- **Two-step pattern** — create the node first, then update it in a separate call: + + ```python + await vlan.save() # creates node, pool assigns vlan_id + # later, if you need to update: + vlan.name.value = "VLAN-100-updated" + await vlan.save() # now _existing=True, calls update + ``` + +- **Explicit id** — if you already know the node's UUID, set `node.id` before saving. The upsert will use the UUID directly and skip HFID lookup: + + ```python + vlan.id = "known-uuid" + await vlan.save(allow_upsert=True) # guard bypassed + ``` + +- **Deterministic identifier** — if possible, design your schema so the HFID uses a non-pool attribute (for example, a human-assigned `name`) and keep `vlan_id` out of the HFID. + +::: diff --git a/docs/docs/python-sdk/sdk_ref/infrahub_sdk/node/attribute.mdx b/docs/docs/python-sdk/sdk_ref/infrahub_sdk/node/attribute.mdx index d08c7fc5..92a0ab98 100644 --- a/docs/docs/python-sdk/sdk_ref/infrahub_sdk/node/attribute.mdx +++ b/docs/docs/python-sdk/sdk_ref/infrahub_sdk/node/attribute.mdx @@ -36,3 +36,19 @@ Check whether this attribute's value is sourced from a resource pool. **Returns:** - True if the attribute value is a resource pool node or was explicitly allocated from a pool. + +#### `is_unresolved_pool_attribute` + +```python +is_unresolved_pool_attribute(self) -> bool +``` + +Return True when pool-backed but no concrete scalar value is available yet. + +A pool-backed attribute is unresolved when: + +- its value is a pool node object (the pool reference itself, not an allocated scalar), or +- its value is None and the from_pool allocation dict is set. + +An attribute whose _from_pool dict is set but whose value has already been populated +with the allocated scalar (e.g. after a prior save) is considered resolved. diff --git a/infrahub_sdk/node/attribute.py b/infrahub_sdk/node/attribute.py index c1a424a8..c72802d7 100644 --- a/infrahub_sdk/node/attribute.py +++ b/infrahub_sdk/node/attribute.py @@ -191,3 +191,17 @@ def is_from_pool_attribute(self) -> bool: """ return (isinstance(self.value, CoreNodeBase) and self.value.is_resource_pool()) or self._from_pool is not None + + def is_unresolved_pool_attribute(self) -> bool: + """Return True when pool-backed but no concrete scalar value is available yet. + + A pool-backed attribute is unresolved when: + - its value is a pool node object (the pool reference itself, not an allocated scalar), or + - its value is None and the from_pool allocation dict is set. + + An attribute whose _from_pool dict is set but whose value has already been populated + with the allocated scalar (e.g. after a prior save) is considered resolved. + """ + if isinstance(self.value, CoreNodeBase) and self.value.is_resource_pool(): + return True + return self._from_pool is not None and self.value is None diff --git a/infrahub_sdk/node/node.py b/infrahub_sdk/node/node.py index c39ac293..8213c686 100644 --- a/infrahub_sdk/node/node.py +++ b/infrahub_sdk/node/node.py @@ -7,7 +7,13 @@ from typing import TYPE_CHECKING, Any, BinaryIO, overload from ..constants import InfrahubClientMode -from ..exceptions import FeatureNotSupportedError, NodeNotFoundError, ResourceNotDefinedError, SchemaNotFoundError +from ..exceptions import ( + FeatureNotSupportedError, + NodeNotFoundError, + ResourceNotDefinedError, + SchemaNotFoundError, + ValidationError, +) from ..file_handler import FileHandler, FileHandlerBase, FileHandlerSync, PreparedFile, sha1_of_source from ..graphql import Mutation, Query from ..schema import ( @@ -591,6 +597,36 @@ def _get_attribute(self, name: str) -> Attribute: raise ResourceNotDefinedError(message=f"The node doesn't have an attribute for {name}") + def _validate_upsert(self, allow_upsert: bool) -> None: + """Ensure an upsert can resolve the HFID before attempting to save. + + An attribute sourced from a CoreNumberPool has no concrete value until the node is + created, so it cannot be used to look up an existing node by its human-friendly identifier. + + Raises: + ValidationError: If an HFID attribute is sourced from an unresolved CoreNumberPool. + + """ + if not (allow_upsert and not self.id): + return + + for hfid_path in self._schema.human_friendly_id or []: + attr_name = hfid_path.split("__")[0] + try: + attr = self._get_attribute(attr_name) + except ResourceNotDefinedError: + continue + if attr.is_unresolved_pool_attribute(): + raise ValidationError( + identifier=attr_name, + message=( + f"Attribute '{attr_name}' is sourced from a CoreNumberPool and is part of " + "this node's human-friendly identifier. Upsert cannot resolve the HFID " + "without a concrete value. Use an explicit id, or create the node first " + "and update it in a separate call." + ), + ) + @staticmethod def _build_rel_query_data( rel_schema: RelationshipSchemaAPI, @@ -1265,6 +1301,8 @@ async def _process_mutation_result( async def create( self, allow_upsert: bool = False, timeout: int | None = None, request_context: RequestContext | None = None ) -> None: + self._validate_upsert(allow_upsert=allow_upsert) + if self._file_object_support and self._file_content is None: raise ValueError( f"Cannot create {self._schema.kind} without file content. Use upload_from_path() or upload_from_bytes() to provide " @@ -2237,6 +2275,8 @@ def _process_mutation_result( def create( self, allow_upsert: bool = False, timeout: int | None = None, request_context: RequestContext | None = None ) -> None: + self._validate_upsert(allow_upsert=allow_upsert) + if self._file_object_support and self._file_content is None: raise ValueError( f"Cannot create {self._schema.kind} without file content. Use upload_from_path() or upload_from_bytes() to provide " diff --git a/tests/unit/sdk/pool/conftest.py b/tests/unit/sdk/pool/conftest.py index 9d0fd245..43ab113c 100644 --- a/tests/unit/sdk/pool/conftest.py +++ b/tests/unit/sdk/pool/conftest.py @@ -114,3 +114,23 @@ async def ipprefix_pool_schema() -> NodeSchemaAPI: ], } return NodeSchema(**data).convert_api() + + +@pytest.fixture +async def vlan_schema_with_pool_hfid() -> NodeSchemaAPI: + """VLAN schema where vlan_id (NumberPool-sourced) is part of the human_friendly_id.""" + data: dict[str, Any] = { + "name": "VLAN", + "namespace": "Infra", + "label": "VLAN", + "default_filter": "name__value", + "order_by": ["name__value"], + "display_labels": ["name__value"], + "human_friendly_id": ["vlan_id__value"], + "attributes": [ + {"name": "name", "kind": "Text", "unique": True}, + {"name": "vlan_id", "kind": "Number"}, + ], + "relationships": [], + } + return NodeSchema(**data).convert_api() diff --git a/tests/unit/sdk/pool/test_attribute_from_pool.py b/tests/unit/sdk/pool/test_attribute_from_pool.py index a11f2c63..28b00f81 100644 --- a/tests/unit/sdk/pool/test_attribute_from_pool.py +++ b/tests/unit/sdk/pool/test_attribute_from_pool.py @@ -11,11 +11,17 @@ from __future__ import annotations +import re from typing import TYPE_CHECKING, Any +import pytest + +from infrahub_sdk.exceptions import ValidationError from infrahub_sdk.node import InfrahubNode, InfrahubNodeSync if TYPE_CHECKING: + from pytest_httpx import HTTPXMock + from infrahub_sdk import InfrahubClient, InfrahubClientSync from infrahub_sdk.schema import NodeSchemaAPI @@ -204,3 +210,126 @@ async def test_attribute_with_pool_node_generates_mutation_query( mutation_query = vlan._generate_mutation_query() assert mutation_query["object"]["vlan_id"] == {"value": None} + + +UPSERT_MOCK_RESPONSE = { + "data": { + "InfraVLANUpsert": { + "ok": True, + "object": {"id": "mock-vlan-uuid", "vlan_id": {"value": 100}}, + } + } +} + + +async def test_save_upsert_raises_when_numberpool_attr_in_hfid( + client: InfrahubClient, + vlan_schema_with_pool_hfid: NodeSchemaAPI, +) -> None: + """save(allow_upsert=True) raises ValidationError naming the pool-sourced HFID attribute.""" + node = InfrahubNode( + client=client, + schema=vlan_schema_with_pool_hfid, + data={"name": "Test VLAN", "vlan_id": {"from_pool": {"id": POOL_ID}}}, + ) + + with pytest.raises(ValidationError, match=re.escape("Attribute 'vlan_id' is sourced from a CoreNumberPool")): + await node.save(allow_upsert=True) + + +async def test_save_upsert_proceeds_when_explicit_id_set( + client: InfrahubClient, + vlan_schema_with_pool_hfid: NodeSchemaAPI, + httpx_mock: HTTPXMock, +) -> None: + """Upsert proceeds when an explicit node id is already set.""" + httpx_mock.add_response(method="POST", json=UPSERT_MOCK_RESPONSE) + node = InfrahubNode( + client=client, + schema=vlan_schema_with_pool_hfid, + data={"name": "Test VLAN", "vlan_id": {"from_pool": {"id": POOL_ID}}}, + ) + node.id = "existing-node-uuid" + + await node.save(allow_upsert=True) + + assert node.id == "mock-vlan-uuid" + + +async def test_save_upsert_proceeds_when_numberpool_attr_not_in_hfid( + client: InfrahubClient, + vlan_schema: NodeSchemaAPI, + httpx_mock: HTTPXMock, +) -> None: + """Upsert proceeds when the pool-sourced attribute is not part of the HFID.""" + httpx_mock.add_response(method="POST", json=UPSERT_MOCK_RESPONSE) + node = InfrahubNode( + client=client, + schema=vlan_schema, + data={"name": "Test VLAN", "vlan_id": {"from_pool": {"id": POOL_ID}}}, + ) + + await node.save(allow_upsert=True) + + assert node.id == "mock-vlan-uuid" + + +async def test_save_upsert_raises_when_pool_node_object_in_hfid( + client: InfrahubClient, + vlan_schema_with_pool_hfid: NodeSchemaAPI, + ipaddress_pool_schema: NodeSchemaAPI, + ipam_ipprefix_schema: NodeSchemaAPI, + ipam_ipprefix_data: dict[str, Any], +) -> None: + """save(allow_upsert=True) raises ValidationError when vlan_id is set to a pool node object (not a from_pool dict).""" + ip_prefix = InfrahubNode(client=client, schema=ipam_ipprefix_schema, data=ipam_ipprefix_data) + ip_pool = InfrahubNode( + client=client, + schema=ipaddress_pool_schema, + data={ + "id": NODE_POOL_ID, + "name": "Core loopbacks", + "default_address_type": "IpamIPAddress", + "default_prefix_length": 32, + "ip_namespace": "ip_namespace", + "resources": [ip_prefix], + }, + ) + node = InfrahubNode( + client=client, + schema=vlan_schema_with_pool_hfid, + data={"name": "Test VLAN", "vlan_id": ip_pool}, + ) + + with pytest.raises(ValidationError, match=re.escape("Attribute 'vlan_id' is sourced from a CoreNumberPool")): + await node.save(allow_upsert=True) + + +async def test_create_upsert_raises_when_numberpool_attr_in_hfid( + client: InfrahubClient, + vlan_schema_with_pool_hfid: NodeSchemaAPI, +) -> None: + """create(allow_upsert=True) raises ValidationError directly, not only through save().""" + node = InfrahubNode( + client=client, + schema=vlan_schema_with_pool_hfid, + data={"name": "Test VLAN", "vlan_id": {"from_pool": {"id": POOL_ID}}}, + ) + + with pytest.raises(ValidationError, match=re.escape("Attribute 'vlan_id' is sourced from a CoreNumberPool")): + await node.create(allow_upsert=True) + + +def test_create_upsert_raises_when_numberpool_attr_in_hfid_sync( + client_sync: InfrahubClientSync, + vlan_schema_with_pool_hfid: NodeSchemaAPI, +) -> None: + """Sync create(allow_upsert=True) raises ValidationError directly, not only through save().""" + node = InfrahubNodeSync( + client=client_sync, + schema=vlan_schema_with_pool_hfid, + data={"name": "Test VLAN", "vlan_id": {"from_pool": {"id": POOL_ID}}}, + ) + + with pytest.raises(ValidationError, match=re.escape("Attribute 'vlan_id' is sourced from a CoreNumberPool")): + node.create(allow_upsert=True) From 73a5d35d22f817a51cea047bc00ccf2ec4292ba7 Mon Sep 17 00:00:00 2001 From: Iddo Date: Mon, 15 Jun 2026 07:10:26 +0200 Subject: [PATCH 3/5] fix(node): preserve unfetched optional one-cardinality relationships on upsert A node hydrated by `from_graphql` from a partial response would silently null-clear any optional cardinality-one relationship the response did not fetch, because the serialization gate could not distinguish "never loaded" from "explicitly cleared". Add `_peer_has_been_mutated` on `RelatedNodeBase`, flipped by `Node.__setattr__` on explicit assignment, and require it in the gate. Mirrors the existing `value_has_been_mutated` pattern on attributes. --- ...-graphql-preserves-unfetched-rels.fixed.md | 1 + infrahub_sdk/node/node.py | 16 ++++++---- infrahub_sdk/node/related_node.py | 4 +++ tests/unit/sdk/test_node.py | 30 +++++++++++++++---- 4 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 changelog/+from-graphql-preserves-unfetched-rels.fixed.md diff --git a/changelog/+from-graphql-preserves-unfetched-rels.fixed.md b/changelog/+from-graphql-preserves-unfetched-rels.fixed.md new file mode 100644 index 00000000..70db340e --- /dev/null +++ b/changelog/+from-graphql-preserves-unfetched-rels.fixed.md @@ -0,0 +1 @@ +Calling `.save(allow_upsert=True)` on a node hydrated by `from_graphql` no longer silently clears optional one-cardinality relationships that the GraphQL response didn't fetch. Explicitly assigning `node.rel = None` still clears the relationship. diff --git a/infrahub_sdk/node/node.py b/infrahub_sdk/node/node.py index 8213c686..882ee39a 100644 --- a/infrahub_sdk/node/node.py +++ b/infrahub_sdk/node/node.py @@ -350,9 +350,11 @@ def _generate_input_data( # noqa: C901 rel: RelatedNodeBase | RelationshipManagerBase = getattr(self, item_name) if rel_schema.cardinality == RelationshipCardinality.ONE and rel_schema.optional and not rel.initialized: - # Only include None for existing nodes to allow clearing relationships - # For new nodes, omit the field to allow object template defaults to be applied - if self._existing: + # Emit `None` only when the caller has explicitly cleared the relationship + # (tracked by `_peer_has_been_mutated`). Without this guard, a node hydrated + # from a partial GraphQL payload — one that didn't fetch this relationship — + # would silently clear it on save. + if self._existing and isinstance(rel, RelatedNodeBase) and rel._peer_has_been_mutated: data[item_name] = None continue @@ -788,9 +790,11 @@ def __setattr__(self, name: str, value: Any) -> None: message=f"Unable to find relationship schema for '{name}' on {self._schema.kind}", ) rel_schema = rel_schemas[0] - self._relationship_cardinality_one_data[name] = RelatedNode( + new_rel = RelatedNode( name=rel_schema.name, branch=self._branch, client=self._client, schema=rel_schema, data=value ) + new_rel._peer_has_been_mutated = True + self._relationship_cardinality_one_data[name] = new_rel return super().__setattr__(name, value) @@ -1766,9 +1770,11 @@ def __setattr__(self, name: str, value: Any) -> None: message=f"Unable to find relationship schema for '{name}' on {self._schema.kind}", ) rel_schema = rel_schemas[0] - self._relationship_cardinality_one_data[name] = RelatedNodeSync( + new_rel = RelatedNodeSync( name=rel_schema.name, branch=self._branch, client=self._client, schema=rel_schema, data=value ) + new_rel._peer_has_been_mutated = True + self._relationship_cardinality_one_data[name] = new_rel return super().__setattr__(name, value) diff --git a/infrahub_sdk/node/related_node.py b/infrahub_sdk/node/related_node.py index 0e496f89..ec6cae51 100644 --- a/infrahub_sdk/node/related_node.py +++ b/infrahub_sdk/node/related_node.py @@ -44,6 +44,10 @@ def __init__(self, branch: str, schema: RelationshipSchemaAPI, data: Any | dict, self._kind: str | None = None self._source_typename: str | None = None self._relationship_metadata: RelationshipMetadata | None = None + # True once the user has assigned to this relationship via Node.__setattr__. + # Distinguishes "never loaded" (partial GraphQL payload) from "explicitly cleared" + # so we don't silently null-clear unfetched relationships on save. + self._peer_has_been_mutated: bool = False if isinstance(data, (CoreNodeBase)): self._peer = data diff --git a/tests/unit/sdk/test_node.py b/tests/unit/sdk/test_node.py index 18c4753a..ab0d72ec 100644 --- a/tests/unit/sdk/test_node.py +++ b/tests/unit/sdk/test_node.py @@ -1450,11 +1450,7 @@ async def test_create_input_data_with_dropdown( async def test_update_input_data_existing_node_with_optional_relationship( clients: BothClients, location_schema: NodeSchemaAPI, client_type: str ) -> None: - """Validate that existing nodes include None for uninitialized optional relationships. - - This ensures that we can explicitly clear optional relationships when updating existing nodes. - """ - # Simulate an existing node by including an id + """An explicit `node.rel = None` on an existing node still clears the relationship.""" data = { "id": "existing-node-id", "name": {"value": "JFK1"}, @@ -1467,7 +1463,8 @@ async def test_update_input_data_existing_node_with_optional_relationship( else: node = InfrahubNodeSync(client=clients.sync, schema=location_schema, data=data) - # For existing nodes, optional uninitialized relationships should include None + node.primary_tag = None + assert node._generate_input_data()["data"] == { "data": { "id": "existing-node-id", @@ -1479,6 +1476,27 @@ async def test_update_input_data_existing_node_with_optional_relationship( } +@pytest.mark.parametrize("client_type", client_types) +async def test_update_input_data_existing_node_preserves_untouched_optional_relationship( + clients: BothClients, location_schema: NodeSchemaAPI, client_type: str +) -> None: + """A partial payload that omits an optional one-cardinality rel must NOT silently clear it on save.""" + data = { + "id": "existing-node-id", + "name": {"value": "JFK1"}, + "description": {"value": "JFK Airport"}, + "type": {"value": "SITE"}, + } + + if client_type == "standard": + node = InfrahubNode(client=clients.standard, schema=location_schema, data=data) + else: + node = InfrahubNodeSync(client=clients.sync, schema=location_schema, data=data) + + payload = node._generate_input_data()["data"]["data"] + assert "primary_tag" not in payload + + @pytest.mark.parametrize("client_type", client_types) async def test_create_input_data__with_relationships_02( client: InfrahubClient, location_schema: NodeSchemaAPI, client_type: str From 2cd7f508acb071f8dd4dc897f74f853438427663 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 06:45:02 -0700 Subject: [PATCH 4/5] chore(deps-dev): bump infrahub-testcontainers from 1.9.7 to 1.9.8 (#1081) Bumps [infrahub-testcontainers](https://github.com/opsmill/infrahub) from 1.9.7 to 1.9.8. - [Release notes](https://github.com/opsmill/infrahub/releases) - [Changelog](https://github.com/opsmill/infrahub/blob/stable/CHANGELOG.md) - [Commits](https://github.com/opsmill/infrahub/compare/infrahub-v1.9.7...infrahub-v1.9.8) --- updated-dependencies: - dependency-name: infrahub-testcontainers dependency-version: 1.9.8 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 297018ef..dcf4139b 100644 --- a/uv.lock +++ b/uv.lock @@ -895,7 +895,7 @@ types = [ [[package]] name = "infrahub-testcontainers" -version = "1.9.7" +version = "1.9.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -905,9 +905,9 @@ dependencies = [ { name = "pytest" }, { name = "testcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/02/467481cbc12e0c841b155426e52567b443ab02928566dc0975c27f69f145/infrahub_testcontainers-1.9.7.tar.gz", hash = "sha256:87fbbaf64682ff07937543b2ef2335f8e250a8ecbd5420b525a629462a583b37", size = 17365, upload-time = "2026-06-03T16:07:03.507Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/24/e3be5eb74e157d6944c4f97eb009a4a44d83c16b91127a21d50859015548/infrahub_testcontainers-1.9.8.tar.gz", hash = "sha256:ebfdd5a163e97cca9b9ec24c89f4e6d4bab7b111643f3b210bde73c4b48d7463", size = 17380, upload-time = "2026-06-09T17:08:51.194Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/8b/3d60e3ad79ca9ec492c865f9464d0416ec7a008550df28fe0ced1108ef0c/infrahub_testcontainers-1.9.7-py3-none-any.whl", hash = "sha256:af29200fc4aedcbd432f53692573e3d8bac381642d89c9a56986bc6d6586e45b", size = 23199, upload-time = "2026-06-03T16:07:04.268Z" }, + { url = "https://files.pythonhosted.org/packages/c0/72/0cd541ff20074b3c179a8232162910946a3685f3035b06e6edfa8e11b6a2/infrahub_testcontainers-1.9.8-py3-none-any.whl", hash = "sha256:31da6008c0a5f4ffc8cf386210f012f83095ff3d189bd234f16853e7a6056da7", size = 23220, upload-time = "2026-06-09T17:08:50.14Z" }, ] [[package]] From d15468ddfae7447ef89e7cc0ef8ccf81e2defd3e Mon Sep 17 00:00:00 2001 From: Iddo Date: Tue, 16 Jun 2026 06:55:37 +0200 Subject: [PATCH 5/5] chore(changelog): rename fragment to issue number for towncrier autolinking --- ...om-graphql-preserves-unfetched-rels.fixed.md => 1080.fixed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog/{+from-graphql-preserves-unfetched-rels.fixed.md => 1080.fixed.md} (100%) diff --git a/changelog/+from-graphql-preserves-unfetched-rels.fixed.md b/changelog/1080.fixed.md similarity index 100% rename from changelog/+from-graphql-preserves-unfetched-rels.fixed.md rename to changelog/1080.fixed.md