diff --git a/CHANGELOG.md b/CHANGELOG.md index db86d527..b6c418e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), This project uses [*towncrier*](https://towncrier.readthedocs.io/) and the changes for the upcoming release can be found in . +## [1.18.1](https://github.com/opsmill/infrahub-sdk-python/tree/v1.18.1) - 2026-01-08 + +### Fixed + +- Reverted #723 (Fix code for HFID casting of strings that aren't UUIDs) + +## [1.18.0](https://github.com/opsmill/infrahub-sdk-python/tree/v1.18.0) - 2026-01-08 + +### Added + +- Add ability to query for metadata on nodes to include information such as creation and update timestamps, creator and last user to update an object. +- Added ability to order nodes by metadata created_at or updated_at fields + +### Removed + +- The previously deprecated 'background_execution' parameter under client.branch.create() was removed. + +### Fixed + +- Rewrite and re-enable integration tests ([#187](https://github.com/opsmill/infrahub-sdk-python/issues/187)) +- Fixed SDK including explicit `null` values for uninitialized optional relationships when creating nodes with object templates, which prevented the backend from applying template defaults. ([#630](https://github.com/opsmill/infrahub-sdk-python/issues/630)) + +### Housekeeping + +- Fixed Python 3.14 compatibility warnings. Testing now requires pytest>=9. ## [1.17.0](https://github.com/opsmill/infrahub-sdk-python/tree/v1.17.0) - 2025-12-11 diff --git a/changelog/+1b40f022.housekeeping.md b/changelog/+1b40f022.housekeeping.md deleted file mode 100644 index 40a566c7..00000000 --- a/changelog/+1b40f022.housekeeping.md +++ /dev/null @@ -1 +0,0 @@ -Fixed Python 3.14 compatibility warnings. Testing now requires pytest>=9. diff --git a/changelog/+86c0992a.added.md b/changelog/+86c0992a.added.md deleted file mode 100644 index 1e53de4b..00000000 --- a/changelog/+86c0992a.added.md +++ /dev/null @@ -1 +0,0 @@ -Added ability to order nodes by metadata created_at or updated_at fields diff --git a/changelog/+d3b5369f.added.md b/changelog/+d3b5369f.added.md deleted file mode 100644 index 3942c0cc..00000000 --- a/changelog/+d3b5369f.added.md +++ /dev/null @@ -1 +0,0 @@ -Add ability to query for metadata on nodes to include information such as creation and update timestamps, creator and last user to update an object. diff --git a/changelog/+e2f96e7b.removed.md b/changelog/+e2f96e7b.removed.md deleted file mode 100644 index 52e96350..00000000 --- a/changelog/+e2f96e7b.removed.md +++ /dev/null @@ -1 +0,0 @@ -The previously deprecated 'background_execution' parameter under client.branch.create() was removed. diff --git a/changelog/187.fixed.md b/changelog/187.fixed.md deleted file mode 100644 index 1911c8dc..00000000 --- a/changelog/187.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Rewrite and re-enable integration tests \ No newline at end of file diff --git a/changelog/630.fixed.md b/changelog/630.fixed.md deleted file mode 100644 index 34a54db3..00000000 --- a/changelog/630.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fixed SDK including explicit `null` values for uninitialized optional relationships when creating nodes with object templates, which prevented the backend from applying template defaults. diff --git a/infrahub_sdk/spec/object.py b/infrahub_sdk/spec/object.py index 548b5fc3..0df3f95c 100644 --- a/infrahub_sdk/spec/object.py +++ b/infrahub_sdk/spec/object.py @@ -7,7 +7,6 @@ from ..exceptions import ObjectValidationError, ValidationError from ..schema import GenericSchemaAPI, RelationshipKind, RelationshipSchema -from ..utils import is_valid_uuid from ..yaml import InfrahubFile, InfrahubFileKind from .models import InfrahubObjectParameters from .processors.factory import DataProcessorFactory @@ -34,32 +33,6 @@ def validate_list_of_objects(value: list[Any]) -> bool: return all(isinstance(item, dict) for item in value) -def normalize_hfid_reference(value: str | list[str]) -> str | list[str]: - """Normalize a reference value to HFID format. - - Args: - value: Either a string (ID or single-component HFID) or a list of strings (multi-component HFID). - - Returns: - - If value is already a list: returns it unchanged as list[str] - - If value is a valid UUID string: returns it unchanged as str (will be treated as an ID) - - If value is a non-UUID string: wraps it in a list as list[str] (single-component HFID) - """ - if isinstance(value, list): - return value - if is_valid_uuid(value): - return value - return [value] - - -def normalize_hfid_references(values: list[str | list[str]]) -> list[str | list[str]]: - """Normalize a list of reference values to HFID format. - - Each string that is not a valid UUID will be wrapped in a list to treat it as a single-component HFID. - """ - return [normalize_hfid_reference(v) for v in values] - - class RelationshipDataFormat(str, Enum): UNKNOWN = "unknown" @@ -471,13 +444,10 @@ async def create_node( # - if the relationship is bidirectional and is mandatory on the other side, then we need to create this object First # - if the relationship is bidirectional and is not mandatory on the other side, then we need should create the related object First # - if the relationship is not bidirectional, then we need to create the related object First - if rel_info.format == RelationshipDataFormat.MANY_REF and isinstance(value, list): - # Cardinality-many: normalize each string HFID to list format: "name" -> ["name"] - # UUIDs are left as-is since they are treated as IDs - clean_data[key] = normalize_hfid_references(value) - elif rel_info.format == RelationshipDataFormat.ONE_REF: - # Cardinality-one: normalize string to HFID list format: "name" -> ["name"] or keep as string (UUID) - clean_data[key] = normalize_hfid_reference(value) + if rel_info.is_reference and isinstance(value, list): + clean_data[key] = value + elif rel_info.format == RelationshipDataFormat.ONE_REF and isinstance(value, str): + clean_data[key] = [value] elif not rel_info.is_reference and rel_info.is_bidirectional and rel_info.is_mandatory: remaining_rels.append(key) elif not rel_info.is_reference and not rel_info.is_mandatory: diff --git a/pyproject.toml b/pyproject.toml index 491e5ae0..cab7c7c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "infrahub-sdk" -version = "1.17.0" +version = "1.18.1" description = "Python Client to interact with Infrahub" authors = [ {name = "OpsMill", email = "info@opsmill.com"} diff --git a/tests/unit/sdk/spec/test_object.py b/tests/unit/sdk/spec/test_object.py index 581b2572..1af02ac3 100644 --- a/tests/unit/sdk/spec/test_object.py +++ b/tests/unit/sdk/spec/test_object.py @@ -1,8 +1,6 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any -from unittest.mock import AsyncMock, patch +from typing import TYPE_CHECKING import pytest @@ -11,7 +9,6 @@ if TYPE_CHECKING: from infrahub_sdk.client import InfrahubClient - from infrahub_sdk.node import InfrahubNode @pytest.fixture @@ -266,105 +263,3 @@ async def test_parameters_non_dict(client_with_schema_01: InfrahubClient, locati obj = ObjectFile(location="some/path", content=location_with_non_dict_parameters) with pytest.raises(ValidationError): await obj.validate_format(client=client_with_schema_01) - - -@dataclass -class HfidLoadTestCase: - """Test case for HFID normalization in object loading.""" - - name: str - data: list[dict[str, Any]] - expected_primary_tag: str | list[str] | None - expected_tags: list[str] | list[list[str]] | None - - -HFID_NORMALIZATION_TEST_CASES = [ - HfidLoadTestCase( - name="cardinality_one_string_hfid_normalized", - data=[{"name": "Mexico", "type": "Country", "primary_tag": "Important"}], - expected_primary_tag=["Important"], - expected_tags=None, - ), - HfidLoadTestCase( - name="cardinality_one_list_hfid_unchanged", - data=[{"name": "Mexico", "type": "Country", "primary_tag": ["Important"]}], - expected_primary_tag=["Important"], - expected_tags=None, - ), - HfidLoadTestCase( - name="cardinality_one_uuid_unchanged", - data=[{"name": "Mexico", "type": "Country", "primary_tag": "550e8400-e29b-41d4-a716-446655440000"}], - expected_primary_tag="550e8400-e29b-41d4-a716-446655440000", - expected_tags=None, - ), - HfidLoadTestCase( - name="cardinality_many_string_hfids_normalized", - data=[{"name": "Mexico", "type": "Country", "tags": ["Important", "Active"]}], - expected_primary_tag=None, - expected_tags=[["Important"], ["Active"]], - ), - HfidLoadTestCase( - name="cardinality_many_list_hfids_unchanged", - data=[{"name": "Mexico", "type": "Country", "tags": [["Important"], ["Active"]]}], - expected_primary_tag=None, - expected_tags=[["Important"], ["Active"]], - ), - HfidLoadTestCase( - name="cardinality_many_mixed_hfids_normalized", - data=[{"name": "Mexico", "type": "Country", "tags": ["Important", ["namespace", "name"]]}], - expected_primary_tag=None, - expected_tags=[["Important"], ["namespace", "name"]], - ), - HfidLoadTestCase( - name="cardinality_many_uuids_unchanged", - data=[ - { - "name": "Mexico", - "type": "Country", - "tags": ["550e8400-e29b-41d4-a716-446655440000", "6ba7b810-9dad-11d1-80b4-00c04fd430c8"], - } - ], - expected_primary_tag=None, - expected_tags=["550e8400-e29b-41d4-a716-446655440000", "6ba7b810-9dad-11d1-80b4-00c04fd430c8"], - ), -] - - -@pytest.mark.parametrize("test_case", HFID_NORMALIZATION_TEST_CASES, ids=lambda tc: tc.name) -async def test_hfid_normalization_in_object_loading( - client_with_schema_01: InfrahubClient, test_case: HfidLoadTestCase -) -> None: - """Test that HFIDs are normalized correctly based on cardinality and format.""" - - root_location = {"apiVersion": "infrahub.app/v1", "kind": "Object", "spec": {"kind": "BuiltinLocation", "data": []}} - location = { - "apiVersion": root_location["apiVersion"], - "kind": root_location["kind"], - "spec": {"kind": root_location["spec"]["kind"], "data": test_case.data}, - } - - obj = ObjectFile(location="some/path", content=location) - await obj.validate_format(client=client_with_schema_01) - - create_calls: list[dict[str, Any]] = [] - - async def mock_create( - kind: str, - branch: str | None = None, - data: dict | None = None, - **kwargs: Any, # noqa: ANN401 - ) -> InfrahubNode: - create_calls.append({"kind": kind, "data": data}) - original_create = client_with_schema_01.__class__.create - return await original_create(client_with_schema_01, kind=kind, branch=branch, data=data, **kwargs) - - client_with_schema_01.create = mock_create - - with patch("infrahub_sdk.node.InfrahubNode.save", new_callable=AsyncMock): - await obj.process(client=client_with_schema_01) - - assert len(create_calls) == 1 - if test_case.expected_primary_tag is not None: - assert create_calls[0]["data"]["primary_tag"] == test_case.expected_primary_tag - if test_case.expected_tags is not None: - assert create_calls[0]["data"]["tags"] == test_case.expected_tags diff --git a/uv.lock b/uv.lock index dc511d55..a4855b8b 100644 --- a/uv.lock +++ b/uv.lock @@ -731,7 +731,7 @@ wheels = [ [[package]] name = "infrahub-sdk" -version = "1.17.0" +version = "1.18.1" source = { editable = "." } dependencies = [ { name = "dulwich" }, @@ -877,8 +877,8 @@ dev = [ lint = [ { name = "astroid", specifier = ">=3.1,<4.0" }, { name = "mypy", specifier = "==1.11.2" }, - { name = "ty", specifier = "==0.0.8" }, { name = "ruff", specifier = "==0.14.10" }, + { name = "ty", specifier = "==0.0.8" }, { name = "yamllint" }, ] tests = [