diff --git a/.vale.ini b/.vale.ini index 700cbe87..47c22cbe 100644 --- a/.vale.ini +++ b/.vale.ini @@ -11,5 +11,10 @@ BasedOnStyles = Infrahub ;(```.*?```\n) to ignore code block in .mdx BlockIgnores = (?s) *((import.*?\n)|(```.*?```\n)) +[*.toml] +# Ignore all rules for toml files, to prevent false positives +# in files like pyproject.toml +BasedOnStyles = + [*] BasedOnStyles = Infrahub diff --git a/CHANGELOG.md b/CHANGELOG.md index ea7995c8..218e0228 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,41 @@ This project uses [*towncrier*](https://towncrier.readthedocs.io/) and the chang +## [1.15.0](https://github.com/opsmill/infrahub-sdk-python/tree/v1.15.0) - 2025-11-10 + +### Added + +- Add `create_diff` method to create a diff summary between two timestamps + Update `get_diff_summary` to accept optional time range parameters ([#529](https://github.com/opsmill/infrahub-sdk-python/issues/529)) +- Add the ability to perform range expansions in object files. This feature allows users to define patterns in string fields that will be expanded into multiple objects, facilitating bulk object creation and management. The implementation includes validation to ensure that all expanded lists have the same length, preventing inconsistencies. Documentation has been updated to explain how to use this feature, including examples of valid and invalid configurations. ([#560](https://github.com/opsmill/infrahub-sdk-python/issues/560)) +- Add `convert_object_type` method to allow converting an object to another type. +- Add `graph_version` and `status` properties to `Branch` +- Add `infrahubctl graphql` commands to export schema and generate Pydantic types from GraphQL queries +- Added deprecation warnings when loading or checking schemas + +### Changed + +- Deprecate the use of `raise_for_error=False` across several methods, using a try/except pattern is preferred. ([#493](https://github.com/opsmill/infrahub-sdk-python/issues/493)) + +### Fixed + +- Respect default branch for client.query_gql_query() and client.set_context_properties() ([#236](https://github.com/opsmill/infrahub-sdk-python/issues/236)) +- Fix branch creation with the sync client while setting `wait_until_completion=False` ([#374](https://github.com/opsmill/infrahub-sdk-python/issues/374)) +- Replaced the `Sync` word in the protocol schema name so that the correct kind can be gotten from the cache ([#380](https://github.com/opsmill/infrahub-sdk-python/issues/380)) +- Fix `infrahubctl info` command when run as an anonymous user ([#398](https://github.com/opsmill/infrahub-sdk-python/issues/398)) +- JsonDecodeError now includes server response content in error message when JSON decoding fails, providing better debugging information for non-JSON server responses. ([#473](https://github.com/opsmill/infrahub-sdk-python/issues/473)) +- Allow unsetting optional relationship of cardinality one by setting its value to `None` ([#479](https://github.com/opsmill/infrahub-sdk-python/issues/479)) +- Bump docs dependencies ([#519](https://github.com/opsmill/infrahub-sdk-python/issues/519)) +- Fix branch handling in `_run_transform` and `execute_graphql_query` functions in Infrahubctl to use environment variables for branch management. ([#535](https://github.com/opsmill/infrahub-sdk-python/issues/535)) +- Allow the ability to clear optional attributes by setting them to None if they have been mutated by the user. ([#549](https://github.com/opsmill/infrahub-sdk-python/issues/549)) +- Disable rich console print markup causing regex reformatting ([#565](https://github.com/opsmill/infrahub-sdk-python/issues/565)) +- - Fixed issue with improperly escaped special characters in `hfid` fields and other string values in GraphQL mutations by implementing proper JSON-style string escaping + +### Housekeeping + +- Handle error gracefully when loading schema instead of failing with an exception ([#464](https://github.com/opsmill/infrahub-sdk-python/issues/464)) +- Replace toml package with tomllib and tomli optionally for when Python version is less than 3.11 ([#528](https://github.com/opsmill/infrahub-sdk-python/issues/528)) + ## [1.14.0](https://github.com/opsmill/infrahub-sdk-python/tree/v1.14.0) - 2025-08-26 ### Added diff --git a/changelog/+convert-object-type.added.md b/changelog/+convert-object-type.added.md deleted file mode 100644 index 2a27d473..00000000 --- a/changelog/+convert-object-type.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `convert_object_type` method to allow converting an object to another type. \ No newline at end of file diff --git a/changelog/+escape-hfid.fixed.md b/changelog/+escape-hfid.fixed.md deleted file mode 100644 index b7621ef5..00000000 --- a/changelog/+escape-hfid.fixed.md +++ /dev/null @@ -1 +0,0 @@ -- Fixed issue with improperly escaped special characters in `hfid` fields and other string values in GraphQL mutations by implementing proper JSON-style string escaping \ No newline at end of file diff --git a/changelog/+gql-command.added.md b/changelog/+gql-command.added.md deleted file mode 100644 index 1818e6ed..00000000 --- a/changelog/+gql-command.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `infrahubctl graphql` commands to export schema and generate Pydantic types from GraphQL queries \ No newline at end of file diff --git a/changelog/236.fixed.md b/changelog/236.fixed.md deleted file mode 100644 index 510c0a58..00000000 --- a/changelog/236.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Respect default branch for client.query_gql_query() and client.set_context_properties() diff --git a/changelog/374.fixed.md b/changelog/374.fixed.md deleted file mode 100644 index 0751c038..00000000 --- a/changelog/374.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix branch creation with the sync client while setting `wait_until_completion=False` \ No newline at end of file diff --git a/changelog/380.fixed.md b/changelog/380.fixed.md deleted file mode 100644 index 9e764dfa..00000000 --- a/changelog/380.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Replaced the `Sync` word in the protocol schema name so that the correct kind can be gotten from the cache diff --git a/changelog/398.fixed.md b/changelog/398.fixed.md deleted file mode 100644 index 18de648e..00000000 --- a/changelog/398.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix `infrahubctl info` command when run as an anonymous user \ No newline at end of file diff --git a/changelog/464.housekeeping.md b/changelog/464.housekeeping.md deleted file mode 100644 index 9478ed71..00000000 --- a/changelog/464.housekeeping.md +++ /dev/null @@ -1 +0,0 @@ -Handle error gracefully when loading schema instead of failing with an exception diff --git a/changelog/473.fixed.md b/changelog/473.fixed.md deleted file mode 100644 index 22c5a5af..00000000 --- a/changelog/473.fixed.md +++ /dev/null @@ -1 +0,0 @@ -JsonDecodeError now includes server response content in error message when JSON decoding fails, providing better debugging information for non-JSON server responses. \ No newline at end of file diff --git a/changelog/479.fixed.md b/changelog/479.fixed.md deleted file mode 100644 index 17875aa7..00000000 --- a/changelog/479.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Allow unsetting optional relationship of cardinality one by setting its value to `None` \ No newline at end of file diff --git a/changelog/493.changed.md b/changelog/493.changed.md deleted file mode 100644 index 6628742d..00000000 --- a/changelog/493.changed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecate the use of `raise_for_error=False` across several methods, using a try/except pattern is preferred. \ No newline at end of file diff --git a/changelog/519.fixed.md b/changelog/519.fixed.md deleted file mode 100644 index 191dcb2f..00000000 --- a/changelog/519.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Bump docs dependencies \ No newline at end of file diff --git a/changelog/528.housekeeping.md b/changelog/528.housekeeping.md deleted file mode 100644 index fdf10b4e..00000000 --- a/changelog/528.housekeeping.md +++ /dev/null @@ -1 +0,0 @@ -Replace toml package with tomllib and tomli optionally for when Python version is less than 3.11 diff --git a/changelog/529.added.md b/changelog/529.added.md deleted file mode 100644 index cb02de4f..00000000 --- a/changelog/529.added.md +++ /dev/null @@ -1,2 +0,0 @@ -Add `create_diff` method to create a diff summary between two timestamps -Update `get_diff_summary` to accept optional time range parameters \ No newline at end of file diff --git a/changelog/535.fixed.md b/changelog/535.fixed.md deleted file mode 100644 index 56c8fd43..00000000 --- a/changelog/535.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix branch handling in `_run_transform` and `execute_graphql_query` functions in Infrahubctl to use environment variables for branch management. \ No newline at end of file diff --git a/changelog/549.fixed.md b/changelog/549.fixed.md deleted file mode 100644 index 1a4f975c..00000000 --- a/changelog/549.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Allow the ability to clear optional attributes by setting them to None if they have been mutated by the user. \ No newline at end of file diff --git a/changelog/560.added.md b/changelog/560.added.md deleted file mode 100644 index 95fe3b37..00000000 --- a/changelog/560.added.md +++ /dev/null @@ -1 +0,0 @@ -Add the ability to perform range expansions in object files. This feature allows users to define patterns in string fields that will be expanded into multiple objects, facilitating bulk object creation and management. The implementation includes validation to ensure that all expanded lists have the same length, preventing inconsistencies. Documentation has been updated to explain how to use this feature, including examples of valid and invalid configurations. \ No newline at end of file diff --git a/changelog/565.fixed.md b/changelog/565.fixed.md deleted file mode 100644 index 3492a5be..00000000 --- a/changelog/565.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Disable rich console print markup causing regex reformatting diff --git a/docs/docs/python-sdk/topics/object_file.mdx b/docs/docs/python-sdk/topics/object_file.mdx index 8de01740..f3e426ae 100644 --- a/docs/docs/python-sdk/topics/object_file.mdx +++ b/docs/docs/python-sdk/topics/object_file.mdx @@ -60,23 +60,23 @@ apiVersion: infrahub.app/v1 kind: Object spec: kind: - strategy: # Optional, defaults to normal + parameters: + expand_range: # Optional, defaults to false data: - [...] ``` > Multiple documents in a single YAML file are also supported, each document will be loaded separately. Documents are separated by `---` -### Data Processing Strategies +### Data Processing Parameters -The `strategy` field controls how the data in the object file is processed before loading into Infrahub: +The `parameters` field controls how the data in the object file is processed before loading into Infrahub: -| Strategy | Description | Default | -|----------|-------------|---------| -| `normal` | No data manipulation is performed. Objects are loaded as-is. | Yes | -| `range_expand` | Range patterns (e.g., `[1-5]`) in string fields are expanded into multiple objects. | No | +| Parameter | Description | Default | +|-----------|-------------|---------| +| `expand_range` | When set to `true`, range patterns (e.g., `[1-5]`) in string fields are expanded into multiple objects. | `false` | -When `strategy` is not specified, it defaults to `normal`. +When `expand_range` is not specified, it defaults to `false`. ### Relationship of cardinality one @@ -210,7 +210,7 @@ Metadata support is planned for future releases. Currently, the Object file does ## Range Expansion in Object Files -The Infrahub Python SDK supports **range expansion** for string fields in object files when the `strategy` is set to `range_expand`. This feature allows you to specify a range pattern (e.g., `[1-5]`) in any string value, and the SDK will automatically expand it into multiple objects during validation and processing. +The Infrahub Python SDK supports **range expansion** for string fields in object files when the `parameters > expand_range` is set to `true`. This feature allows you to specify a range pattern (e.g., `[1-5]`) in any string value, and the SDK will automatically expand it into multiple objects during validation and processing. ```yaml --- @@ -218,7 +218,8 @@ apiVersion: infrahub.app/v1 kind: Object spec: kind: BuiltinLocation - strategy: range_expand # Enable range expansion + parameters: + expand_range: true # Enable range expansion data: - name: AMS[1-3] type: Country @@ -237,7 +238,8 @@ spec: ```yaml spec: kind: BuiltinLocation - strategy: range_expand + parameters: + expand_range: true data: - name: AMS[1-3] type: Country @@ -259,7 +261,8 @@ This will expand to: ```yaml spec: kind: BuiltinLocation - strategy: range_expand + parameters: + expand_range: true data: - name: AMS[1-3] description: Datacenter [A-C] @@ -287,7 +290,8 @@ If you use ranges of different lengths in multiple fields: ```yaml spec: kind: BuiltinLocation - strategy: range_expand + parameters: + expand_range: true data: - name: AMS[1-3] description: "Datacenter [10-15]" diff --git a/infrahub_sdk/branch.py b/infrahub_sdk/branch.py index 2b1905ce..4f5e050f 100644 --- a/infrahub_sdk/branch.py +++ b/infrahub_sdk/branch.py @@ -1,6 +1,7 @@ from __future__ import annotations import warnings +from enum import Enum from typing import TYPE_CHECKING, Any, Literal, overload from urllib.parse import urlencode @@ -14,6 +15,13 @@ from .client import InfrahubClient, InfrahubClientSync +class BranchStatus(str, Enum): + OPEN = "OPEN" + NEED_REBASE = "NEED_REBASE" + NEED_UPGRADE_REBASE = "NEED_UPGRADE_REBASE" + DELETING = "DELETING" + + class BranchData(BaseModel): id: str name: str @@ -21,6 +29,8 @@ class BranchData(BaseModel): sync_with_git: bool is_default: bool has_schema_changes: bool + graph_version: int | None = None + status: BranchStatus = BranchStatus.OPEN origin_branch: str | None = None branched_from: str @@ -34,6 +44,8 @@ class BranchData(BaseModel): "is_default": None, "sync_with_git": None, "has_schema_changes": None, + "graph_version": None, + "status": None, } BRANCH_DATA_FILTER = {"@filters": {"name": "$branch_name"}} diff --git a/infrahub_sdk/client.py b/infrahub_sdk/client.py index 1fc65f7c..b837e3a8 100644 --- a/infrahub_sdk/client.py +++ b/infrahub_sdk/client.py @@ -279,16 +279,8 @@ def _build_ip_address_allocation_query( return Mutation( name="AllocateIPAddress", - mutation="IPAddressPoolGetResource", - query={ - "ok": None, - "node": { - "id": None, - "kind": None, - "identifier": None, - "display_label": None, - }, - }, + mutation="InfrahubIPAddressPoolGetResource", + query={"ok": None, "node": {"id": None, "kind": None, "identifier": None, "display_label": None}}, input_data={"data": input_data}, ) @@ -318,16 +310,8 @@ def _build_ip_prefix_allocation_query( return Mutation( name="AllocateIPPrefix", - mutation="IPPrefixPoolGetResource", - query={ - "ok": None, - "node": { - "id": None, - "kind": None, - "identifier": None, - "display_label": None, - }, - }, + mutation="InfrahubIPPrefixPoolGetResource", + query={"ok": None, "node": {"id": None, "kind": None, "identifier": None, "display_label": None}}, input_data={"data": input_data}, ) @@ -1421,7 +1405,7 @@ async def allocate_next_ip_address( raise ValueError("resource_pool is not an IP address pool") branch = branch or self.default_branch - mutation_name = "IPAddressPoolGetResource" + mutation_name = "InfrahubIPAddressPoolGetResource" query = self._build_ip_address_allocation_query( resource_pool_id=resource_pool.id, @@ -1573,7 +1557,7 @@ async def allocate_next_ip_prefix( raise ValueError("resource_pool is not an IP prefix pool") branch = branch or self.default_branch - mutation_name = "IPPrefixPoolGetResource" + mutation_name = "InfrahubIPPrefixPoolGetResource" query = self._build_ip_prefix_allocation_query( resource_pool_id=resource_pool.id, @@ -2659,7 +2643,7 @@ def allocate_next_ip_address( raise ValueError("resource_pool is not an IP address pool") branch = branch or self.default_branch - mutation_name = "IPAddressPoolGetResource" + mutation_name = "InfrahubIPAddressPoolGetResource" query = self._build_ip_address_allocation_query( resource_pool_id=resource_pool.id, @@ -2811,7 +2795,7 @@ def allocate_next_ip_prefix( raise ValueError("resource_pool is not an IP prefix pool") branch = branch or self.default_branch - mutation_name = "IPPrefixPoolGetResource" + mutation_name = "InfrahubIPPrefixPoolGetResource" query = self._build_ip_prefix_allocation_query( resource_pool_id=resource_pool.id, diff --git a/infrahub_sdk/ctl/branch.py b/infrahub_sdk/ctl/branch.py index b44be462..42d88384 100644 --- a/infrahub_sdk/ctl/branch.py +++ b/infrahub_sdk/ctl/branch.py @@ -46,6 +46,7 @@ async def list_branch(_: str = CONFIG_PARAM) -> None: table.add_column("Sync with Git") table.add_column("Has Schema Changes") table.add_column("Is Default") + table.add_column("Status") # identify the default branch and always print it first default_branch = [branch for branch in branches.values() if branch.is_default][0] @@ -57,6 +58,7 @@ async def list_branch(_: str = CONFIG_PARAM) -> None: "[green]True" if default_branch.sync_with_git else "[#FF7F50]False", "[green]True" if default_branch.has_schema_changes else "[#FF7F50]False", "[green]True" if default_branch.is_default else "[#FF7F50]False", + default_branch.status, ) for branch in branches.values(): @@ -71,6 +73,7 @@ async def list_branch(_: str = CONFIG_PARAM) -> None: "[green]True" if branch.sync_with_git else "[#FF7F50]False", "[green]True" if default_branch.has_schema_changes else "[#FF7F50]False", "[green]True" if branch.is_default else "[#FF7F50]False", + branch.status, ) console.print(table) diff --git a/infrahub_sdk/ctl/check.py b/infrahub_sdk/ctl/check.py index 0626d884..92d852e6 100644 --- a/infrahub_sdk/ctl/check.py +++ b/infrahub_sdk/ctl/check.py @@ -11,10 +11,9 @@ from rich.console import Console from rich.logging import RichHandler -from ..ctl import config from ..ctl.client import initialize_client from ..ctl.exceptions import QueryNotFoundError -from ..ctl.repository import get_repository_config +from ..ctl.repository import find_repository_config_file, get_repository_config from ..ctl.utils import catch_exception, execute_graphql_query from ..exceptions import ModuleImportError @@ -59,7 +58,7 @@ def run( FORMAT = "%(message)s" logging.basicConfig(level=log_level, format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]) - repository_config = get_repository_config(Path(config.INFRAHUB_REPO_CONFIG_FILE)) + repository_config = get_repository_config(find_repository_config_file()) if list_available: list_checks(repository_config=repository_config) diff --git a/infrahub_sdk/ctl/cli_commands.py b/infrahub_sdk/ctl/cli_commands.py index 6f8c6bf4..42a8764f 100644 --- a/infrahub_sdk/ctl/cli_commands.py +++ b/infrahub_sdk/ctl/cli_commands.py @@ -20,7 +20,6 @@ from .. import __version__ as sdk_version from ..async_typer import AsyncTyper -from ..ctl import config from ..ctl.branch import app as branch_app from ..ctl.check import run as run_check from ..ctl.client import initialize_client, initialize_client_sync @@ -31,7 +30,7 @@ from ..ctl.object import app as object_app from ..ctl.render import list_jinja2_transforms, print_template_errors from ..ctl.repository import app as repository_app -from ..ctl.repository import get_repository_config +from ..ctl.repository import find_repository_config_file, get_repository_config from ..ctl.schema import app as schema_app from ..ctl.task import app as task_app from ..ctl.transform import list_transforms @@ -263,7 +262,7 @@ async def render( """Render a local Jinja2 Transform for debugging purpose.""" variables_dict = parse_cli_vars(variables) - repository_config = get_repository_config(Path(config.INFRAHUB_REPO_CONFIG_FILE)) + repository_config = get_repository_config(find_repository_config_file()) if list_available or not transform_name: list_jinja2_transforms(config=repository_config) @@ -273,7 +272,7 @@ async def render( try: transform_config = repository_config.get_jinja2_transform(name=transform_name) except KeyError as exc: - console.print(f'[red]Unable to find "{transform_name}" in {config.INFRAHUB_REPO_CONFIG_FILE}') + console.print(f'[red]Unable to find "{transform_name}" in repository config file') list_jinja2_transforms(config=repository_config) raise typer.Exit(1) from exc @@ -313,7 +312,7 @@ def transform( """Render a local transform (TransformPython) for debugging purpose.""" variables_dict = parse_cli_vars(variables) - repository_config = get_repository_config(Path(config.INFRAHUB_REPO_CONFIG_FILE)) + repository_config = get_repository_config(find_repository_config_file()) if list_available or not transform_name: list_transforms(config=repository_config) diff --git a/infrahub_sdk/ctl/config.py b/infrahub_sdk/ctl/config.py index 2f65f4c3..38b527d1 100644 --- a/infrahub_sdk/ctl/config.py +++ b/infrahub_sdk/ctl/config.py @@ -17,6 +17,7 @@ DEFAULT_CONFIG_FILE = "infrahubctl.toml" ENVVAR_CONFIG_FILE = "INFRAHUBCTL_CONFIG" INFRAHUB_REPO_CONFIG_FILE = ".infrahub.yml" +INFRAHUB_REPO_CONFIG_FILE_ALT = ".infrahub.yaml" class Settings(BaseSettings): diff --git a/infrahub_sdk/ctl/generator.py b/infrahub_sdk/ctl/generator.py index c75b5acb..75354ee1 100644 --- a/infrahub_sdk/ctl/generator.py +++ b/infrahub_sdk/ctl/generator.py @@ -6,9 +6,8 @@ import typer from rich.console import Console -from ..ctl import config from ..ctl.client import initialize_client -from ..ctl.repository import get_repository_config +from ..ctl.repository import find_repository_config_file, get_repository_config from ..ctl.utils import execute_graphql_query, init_logging, parse_cli_vars from ..exceptions import ModuleImportError from ..node import InfrahubNode @@ -26,7 +25,7 @@ async def run( variables: Optional[list[str]] = None, ) -> None: init_logging(debug=debug) - repository_config = get_repository_config(Path(config.INFRAHUB_REPO_CONFIG_FILE)) + repository_config = get_repository_config(find_repository_config_file()) if list_available or not generator_name: list_generators(repository_config=repository_config) @@ -65,6 +64,8 @@ async def run( branch=branch or "", params=variables_dict, convert_query_response=generator_config.convert_query_response, + execute_in_proposed_change=generator_config.execute_in_proposed_change, + execute_after_merge=generator_config.execute_after_merge, infrahub_node=InfrahubNode, ) await generator._init_client.schema.all(branch=generator.branch_name) @@ -94,6 +95,8 @@ async def run( branch=branch or "", params=params, convert_query_response=generator_config.convert_query_response, + execute_in_proposed_change=generator_config.execute_in_proposed_change, + execute_after_merge=generator_config.execute_after_merge, infrahub_node=InfrahubNode, ) data = execute_graphql_query( diff --git a/infrahub_sdk/ctl/repository.py b/infrahub_sdk/ctl/repository.py index d23f8484..5c9423d1 100644 --- a/infrahub_sdk/ctl/repository.py +++ b/infrahub_sdk/ctl/repository.py @@ -24,11 +24,49 @@ console = Console() +def find_repository_config_file(base_path: Path | None = None) -> Path: + """Find the repository config file, checking for both .yml and .yaml extensions. + + Args: + base_path: Base directory to search in. If None, uses current directory. + + Returns: + Path to the config file. + + Raises: + FileNotFoundError: If neither .infrahub.yml nor .infrahub.yaml exists. + """ + if base_path is None: + base_path = Path() + + yml_path = base_path / ".infrahub.yml" + yaml_path = base_path / ".infrahub.yaml" + + # Prefer .yml if both exist + if yml_path.exists(): + return yml_path + if yaml_path.exists(): + return yaml_path + # For backward compatibility, return .yml path for error messages + return yml_path + + def get_repository_config(repo_config_file: Path) -> InfrahubRepositoryConfig: + # If the file doesn't exist, try to find it with alternate extension + if not repo_config_file.exists(): + if repo_config_file.name == ".infrahub.yml": + alt_path = repo_config_file.parent / ".infrahub.yaml" + if alt_path.exists(): + repo_config_file = alt_path + elif repo_config_file.name == ".infrahub.yaml": + alt_path = repo_config_file.parent / ".infrahub.yml" + if alt_path.exists(): + repo_config_file = alt_path + try: config_file_data = load_repository_config_file(repo_config_file) except FileNotFoundError as exc: - console.print(f"[red]File not found {exc}") + console.print(f"[red]File not found {exc} (also checked for .infrahub.yml and .infrahub.yaml)") raise typer.Exit(1) from exc except FileNotValidError as exc: console.print(f"[red]{exc.message}") diff --git a/infrahub_sdk/ctl/schema.py b/infrahub_sdk/ctl/schema.py index cb876569..5a977b59 100644 --- a/infrahub_sdk/ctl/schema.py +++ b/infrahub_sdk/ctl/schema.py @@ -14,6 +14,7 @@ from ..ctl.client import initialize_client from ..ctl.utils import catch_exception, init_logging from ..queries import SCHEMA_HASH_SYNC_STATUS +from ..schema import SchemaWarning from ..yaml import SchemaFile from .parameters import CONFIG_PARAM from .utils import load_yamlfile_from_disk_and_exit @@ -152,6 +153,8 @@ async def load( console.print(f"[green] {len(schemas_data)} {schema_definition} processed in {loading_time:.3f} seconds.") + _display_schema_warnings(console=console, warnings=response.warnings) + if response.schema_updated and wait: waited = 0 continue_waiting = True @@ -187,12 +190,24 @@ async def check( success, response = await client.schema.check(schemas=[item.payload for item in schemas_data], branch=branch) - if not success: + if not success or not response: display_schema_load_errors(response=response or {}, schemas_data=schemas_data) + return + + for schema_file in schemas_data: + console.print(f"[green] schema '{schema_file.location}' is Valid!") + + warnings = response.pop("warnings", []) + schema_warnings = [SchemaWarning.model_validate(warning) for warning in warnings] + _display_schema_warnings(console=console, warnings=schema_warnings) + if response == {"diff": {"added": {}, "changed": {}, "removed": {}}}: + print("No diff") else: - for schema_file in schemas_data: - console.print(f"[green] schema '{schema_file.location}' is Valid!") - if response == {"diff": {"added": {}, "changed": {}, "removed": {}}}: - print("No diff") - else: - print(yaml.safe_dump(data=response, indent=4)) + print(yaml.safe_dump(data=response, indent=4)) + + +def _display_schema_warnings(console: Console, warnings: list[SchemaWarning]) -> None: + for warning in warnings: + console.print( + f"[yellow] {warning.type.value}: {warning.message} [{', '.join([kind.display for kind in warning.kinds])}]" + ) diff --git a/infrahub_sdk/generator.py b/infrahub_sdk/generator.py index 3c9d26d7..20b7dc88 100644 --- a/infrahub_sdk/generator.py +++ b/infrahub_sdk/generator.py @@ -26,6 +26,8 @@ def __init__( generator_instance: str = "", params: dict | None = None, convert_query_response: bool = False, + execute_in_proposed_change: bool = True, + execute_after_merge: bool = True, logger: logging.Logger | None = None, request_context: RequestContext | None = None, ) -> None: @@ -44,6 +46,8 @@ def __init__( self._client: InfrahubClient | None = None self.logger = logger if logger else logging.getLogger("infrahub.tasks") self.request_context = request_context + self.execute_in_proposed_change = execute_in_proposed_change + self.execute_after_merge = execute_after_merge @property def subscribers(self) -> list[str] | None: @@ -81,8 +85,10 @@ async def run(self, identifier: str, data: dict | None = None) -> None: unpacked = data.get("data") or data await self.process_nodes(data=unpacked) + group_type = "CoreGeneratorGroup" if self.execute_after_merge else "CoreGeneratorAwareGroup" + async with self._init_client.start_tracking( - identifier=identifier, params=self.params, delete_unused_nodes=True, group_type="CoreGeneratorGroup" + identifier=identifier, params=self.params, delete_unused_nodes=True, group_type=group_type ) as self.client: await self.generate(data=unpacked) diff --git a/infrahub_sdk/protocols.py b/infrahub_sdk/protocols.py index d002e433..b3752bed 100644 --- a/infrahub_sdk/protocols.py +++ b/infrahub_sdk/protocols.py @@ -131,6 +131,7 @@ class CoreGenericRepository(CoreNode): queries: RelationshipManager checks: RelationshipManager generators: RelationshipManager + groups_objects: RelationshipManager class CoreGroup(CoreNode): @@ -233,6 +234,10 @@ class CoreWebhook(CoreNode): validate_certificates: BooleanOptional +class CoreWeightedPoolResource(CoreNode): + allocation_weight: IntegerOptional + + class LineageOwner(CoreNode): pass @@ -321,6 +326,7 @@ class CoreCheckDefinition(CoreTaskTarget): class CoreCustomWebhook(CoreWebhook, CoreTaskTarget): + shared_key: StringOptional transformation: RelatedNode @@ -350,6 +356,10 @@ class CoreGeneratorAction(CoreAction): generator: RelatedNode +class CoreGeneratorAwareGroup(CoreGroup): + pass + + class CoreGeneratorCheck(CoreCheck): instance: String @@ -361,6 +371,8 @@ class CoreGeneratorDefinition(CoreTaskTarget): file_path: String class_name: String convert_query_response: BooleanOptional + execute_in_proposed_change: BooleanOptional + execute_after_merge: BooleanOptional query: RelatedNode repository: RelatedNode targets: RelatedNode @@ -405,12 +417,12 @@ class CoreGraphQLQueryGroup(CoreGroup): class CoreGroupAction(CoreAction): - add_members: Boolean + member_action: Dropdown group: RelatedNode class CoreGroupTriggerRule(CoreTriggerRule): - members_added: Boolean + member_update: Dropdown group: RelatedNode @@ -442,7 +454,7 @@ class CoreNodeTriggerAttributeMatch(CoreNodeTriggerMatch): class CoreNodeTriggerRelationshipMatch(CoreNodeTriggerMatch): relationship_name: String - added: Boolean + modification_type: Dropdown peer: StringOptional @@ -457,6 +469,7 @@ class CoreNumberPool(CoreResourcePool, LineageSource): node_attribute: String start_range: Integer end_range: Integer + pool_type: Enum class CoreObjectPermission(CoreBasePermission): @@ -481,7 +494,10 @@ class CoreProposedChange(CoreTaskTarget): source_branch: String destination_branch: String state: Enum + is_draft: Boolean + total_comments: IntegerOptional approved_by: RelationshipManager + rejected_by: RelationshipManager reviewers: RelationshipManager created_by: RelatedNode comments: RelationshipManager @@ -555,6 +571,14 @@ class InternalAccountToken(CoreNode): account: RelatedNode +class InternalIPPrefixAvailable(BuiltinIPPrefix): + pass + + +class InternalIPRangeAvailable(BuiltinIPAddress): + last_address: IPHost + + class InternalRefreshToken(CoreNode): expiration: DateTime account: RelatedNode @@ -664,6 +688,7 @@ class CoreGenericRepositorySync(CoreNodeSync): queries: RelationshipManagerSync checks: RelationshipManagerSync generators: RelationshipManagerSync + groups_objects: RelationshipManagerSync class CoreGroupSync(CoreNodeSync): @@ -766,6 +791,10 @@ class CoreWebhookSync(CoreNodeSync): validate_certificates: BooleanOptional +class CoreWeightedPoolResourceSync(CoreNodeSync): + allocation_weight: IntegerOptional + + class LineageOwnerSync(CoreNodeSync): pass @@ -854,6 +883,7 @@ class CoreCheckDefinitionSync(CoreTaskTargetSync): class CoreCustomWebhookSync(CoreWebhookSync, CoreTaskTargetSync): + shared_key: StringOptional transformation: RelatedNodeSync @@ -883,6 +913,10 @@ class CoreGeneratorActionSync(CoreActionSync): generator: RelatedNodeSync +class CoreGeneratorAwareGroupSync(CoreGroupSync): + pass + + class CoreGeneratorCheckSync(CoreCheckSync): instance: String @@ -894,6 +928,8 @@ class CoreGeneratorDefinitionSync(CoreTaskTargetSync): file_path: String class_name: String convert_query_response: BooleanOptional + execute_in_proposed_change: BooleanOptional + execute_after_merge: BooleanOptional query: RelatedNodeSync repository: RelatedNodeSync targets: RelatedNodeSync @@ -938,12 +974,12 @@ class CoreGraphQLQueryGroupSync(CoreGroupSync): class CoreGroupActionSync(CoreActionSync): - add_members: Boolean + member_action: Dropdown group: RelatedNodeSync class CoreGroupTriggerRuleSync(CoreTriggerRuleSync): - members_added: Boolean + member_update: Dropdown group: RelatedNodeSync @@ -975,7 +1011,7 @@ class CoreNodeTriggerAttributeMatchSync(CoreNodeTriggerMatchSync): class CoreNodeTriggerRelationshipMatchSync(CoreNodeTriggerMatchSync): relationship_name: String - added: Boolean + modification_type: Dropdown peer: StringOptional @@ -990,6 +1026,7 @@ class CoreNumberPoolSync(CoreResourcePoolSync, LineageSourceSync): node_attribute: String start_range: Integer end_range: Integer + pool_type: Enum class CoreObjectPermissionSync(CoreBasePermissionSync): @@ -1014,7 +1051,10 @@ class CoreProposedChangeSync(CoreTaskTargetSync): source_branch: String destination_branch: String state: Enum + is_draft: Boolean + total_comments: IntegerOptional approved_by: RelationshipManagerSync + rejected_by: RelationshipManagerSync reviewers: RelationshipManagerSync created_by: RelatedNodeSync comments: RelationshipManagerSync @@ -1088,6 +1128,14 @@ class InternalAccountTokenSync(CoreNodeSync): account: RelatedNodeSync +class InternalIPPrefixAvailableSync(BuiltinIPPrefixSync): + pass + + +class InternalIPRangeAvailableSync(BuiltinIPAddressSync): + last_address: IPHost + + class InternalRefreshTokenSync(CoreNodeSync): expiration: DateTime account: RelatedNodeSync diff --git a/infrahub_sdk/pytest_plugin/plugin.py b/infrahub_sdk/pytest_plugin/plugin.py index 871ba45b..64c2080b 100644 --- a/infrahub_sdk/pytest_plugin/plugin.py +++ b/infrahub_sdk/pytest_plugin/plugin.py @@ -9,7 +9,7 @@ from .. import InfrahubClientSync from ..utils import is_valid_url from .loader import InfrahubYamlFile -from .utils import load_repository_config +from .utils import find_repository_config_file, load_repository_config def pytest_addoption(parser: Parser) -> None: @@ -18,9 +18,9 @@ def pytest_addoption(parser: Parser) -> None: "--infrahub-repo-config", action="store", dest="infrahub_repo_config", - default=".infrahub.yml", + default=None, metavar="INFRAHUB_REPO_CONFIG_FILE", - help="Infrahub configuration file for the repository (default: %(default)s)", + help="Infrahub configuration file for the repository (.infrahub.yml or .infrahub.yaml)", ) group.addoption( "--infrahub-address", @@ -63,7 +63,10 @@ def pytest_addoption(parser: Parser) -> None: def pytest_sessionstart(session: Session) -> None: - session.infrahub_config_path = Path(session.config.option.infrahub_repo_config) # type: ignore[attr-defined] + if session.config.option.infrahub_repo_config: + session.infrahub_config_path = Path(session.config.option.infrahub_repo_config) # type: ignore[attr-defined] + else: + session.infrahub_config_path = find_repository_config_file() # type: ignore[attr-defined] if session.infrahub_config_path.is_file(): # type: ignore[attr-defined] session.infrahub_repo_config = load_repository_config(repo_config_file=session.infrahub_config_path) # type: ignore[attr-defined] diff --git a/infrahub_sdk/pytest_plugin/utils.py b/infrahub_sdk/pytest_plugin/utils.py index 2875c23d..b82a2e34 100644 --- a/infrahub_sdk/pytest_plugin/utils.py +++ b/infrahub_sdk/pytest_plugin/utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pathlib import Path import yaml @@ -6,7 +8,45 @@ from .exceptions import FileNotValidError +def find_repository_config_file(base_path: Path | None = None) -> Path: + """Find the repository config file, checking for both .yml and .yaml extensions. + + Args: + base_path: Base directory to search in. If None, uses current directory. + + Returns: + Path to the config file. + + Raises: + FileNotFoundError: If neither .infrahub.yml nor .infrahub.yaml exists. + """ + if base_path is None: + base_path = Path() + + yml_path = base_path / ".infrahub.yml" + yaml_path = base_path / ".infrahub.yaml" + + # Prefer .yml if both exist + if yml_path.exists(): + return yml_path + if yaml_path.exists(): + return yaml_path + # For backward compatibility, return .yml path for error messages + return yml_path + + def load_repository_config(repo_config_file: Path) -> InfrahubRepositoryConfig: + # If the file doesn't exist, try to find it with alternate extension + if not repo_config_file.exists(): + if repo_config_file.name == ".infrahub.yml": + alt_path = repo_config_file.parent / ".infrahub.yaml" + if alt_path.exists(): + repo_config_file = alt_path + elif repo_config_file.name == ".infrahub.yaml": + alt_path = repo_config_file.parent / ".infrahub.yml" + if alt_path.exists(): + repo_config_file = alt_path + if not repo_config_file.is_file(): raise FileNotFoundError(repo_config_file) diff --git a/infrahub_sdk/schema/__init__.py b/infrahub_sdk/schema/__init__.py index 4c89ff52..320c43a9 100644 --- a/infrahub_sdk/schema/__init__.py +++ b/infrahub_sdk/schema/__init__.py @@ -91,6 +91,26 @@ class EnumMutation(str, Enum): ] +class SchemaWarningType(Enum): + DEPRECATION = "deprecation" + + +class SchemaWarningKind(BaseModel): + kind: str = Field(..., description="The kind impacted by the warning") + field: str | None = Field(default=None, description="The attribute or relationship impacted by the warning") + + @property + def display(self) -> str: + suffix = f".{self.field}" if self.field else "" + return f"{self.kind}{suffix}" + + +class SchemaWarning(BaseModel): + type: SchemaWarningType = Field(..., description="The type of warning") + kinds: list[SchemaWarningKind] = Field(default_factory=list, description="The kinds impacted by the warning") + message: str = Field(..., description="The message that describes the warning") + + class InfrahubSchemaBase: client: InfrahubClient | InfrahubClientSync cache: dict[str, BranchSchema] @@ -170,7 +190,9 @@ def generate_payload_create( def _validate_load_schema_response(response: httpx.Response) -> SchemaLoadResponse: if response.status_code == httpx.codes.OK: status = response.json() - return SchemaLoadResponse(hash=status["hash"], previous_hash=status["previous_hash"]) + return SchemaLoadResponse( + hash=status["hash"], previous_hash=status["previous_hash"], warnings=status.get("warnings") or [] + ) if response.status_code in [ httpx.codes.BAD_REQUEST, @@ -807,6 +829,7 @@ class SchemaLoadResponse(BaseModel): hash: str = Field(default="", description="The new hash for the entire schema") previous_hash: str = Field(default="", description="The previous hash for the entire schema") errors: dict = Field(default_factory=dict, description="Errors reported by the server") + warnings: list[SchemaWarning] = Field(default_factory=list, description="Warnings reported by the server") @property def schema_updated(self) -> bool: diff --git a/infrahub_sdk/schema/main.py b/infrahub_sdk/schema/main.py index ba18cf49..f704fdaa 100644 --- a/infrahub_sdk/schema/main.py +++ b/infrahub_sdk/schema/main.py @@ -267,6 +267,7 @@ class BaseSchema(BaseModel): description: str | None = None include_in_menu: bool | None = None menu_placement: str | None = None + display_label: str | None = None display_labels: list[str] | None = None human_friendly_id: list[str] | None = None icon: str | None = None diff --git a/infrahub_sdk/schema/repository.py b/infrahub_sdk/schema/repository.py index 832651c9..7a793d01 100644 --- a/infrahub_sdk/schema/repository.py +++ b/infrahub_sdk/schema/repository.py @@ -96,6 +96,14 @@ class InfrahubGeneratorDefinitionConfig(InfrahubRepositoryConfigElement): default=False, description="Decide if the generator should convert the result of the GraphQL query to SDK InfrahubNode objects.", ) + execute_in_proposed_change: bool = Field( + default=True, + description="Decide if the generator should execute in a proposed change.", + ) + execute_after_merge: bool = Field( + default=True, + description="Decide if the generator should execute after a merge.", + ) def load_class(self, import_root: str | None = None, relative_path: str | None = None) -> type[InfrahubGenerator]: module = import_module(module_path=self.file_path, import_root=import_root, relative_path=relative_path) diff --git a/infrahub_sdk/spec/models.py b/infrahub_sdk/spec/models.py new file mode 100644 index 00000000..9020720b --- /dev/null +++ b/infrahub_sdk/spec/models.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from pydantic import BaseModel + + +class InfrahubObjectParameters(BaseModel): + expand_range: bool = False diff --git a/infrahub_sdk/spec/object.py b/infrahub_sdk/spec/object.py index 16992b1a..3bc738c3 100644 --- a/infrahub_sdk/spec/object.py +++ b/infrahub_sdk/spec/object.py @@ -1,17 +1,15 @@ from __future__ import annotations -import copy -import re -from abc import ABC, abstractmethod from enum import Enum -from typing import TYPE_CHECKING, Any, ClassVar +from typing import TYPE_CHECKING, Any from pydantic import BaseModel, Field from ..exceptions import ObjectValidationError, ValidationError from ..schema import GenericSchemaAPI, RelationshipKind, RelationshipSchema from ..yaml import InfrahubFile, InfrahubFileKind -from .range_expansion import MATCH_PATTERN, range_expansion +from .models import InfrahubObjectParameters +from .processors.factory import DataProcessorFactory if TYPE_CHECKING: from ..client import InfrahubClient @@ -46,11 +44,6 @@ class RelationshipDataFormat(str, Enum): MANY_REF = "many_ref_list" -class ObjectStrategy(str, Enum): - NORMAL = "normal" - RANGE_EXPAND = "range_expand" - - class RelationshipInfo(BaseModel): name: str rel_schema: RelationshipSchema @@ -173,97 +166,21 @@ async def get_relationship_info( return info -def expand_data_with_ranges(data: list[dict[str, Any]]) -> list[dict[str, Any]]: - """Expand any item in data with range pattern in any value. Supports multiple fields, requires equal expansion length.""" - range_pattern = re.compile(MATCH_PATTERN) - expanded = [] - for item in data: - # Find all fields to expand - expand_fields = {} - for key, value in item.items(): - if isinstance(value, str) and range_pattern.search(value): - try: - expand_fields[key] = range_expansion(value) - except Exception: - # If expansion fails, treat as no expansion - expand_fields[key] = [value] - if not expand_fields: - expanded.append(item) - continue - # Check all expanded lists have the same length - lengths = [len(v) for v in expand_fields.values()] - if len(set(lengths)) > 1: - raise ValidationError(f"Range expansion mismatch: fields expanded to different lengths: {lengths}") - n = lengths[0] - # Zip expanded values and produce new items - for i in range(n): - new_item = copy.deepcopy(item) - for key, values in expand_fields.items(): - new_item[key] = values[i] - expanded.append(new_item) - return expanded - - -class DataProcessor(ABC): - """Abstract base class for data processing strategies""" - - @abstractmethod - def process_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]: - """Process the data according to the strategy""" - - -class SingleDataProcessor(DataProcessor): - """Process data without any expansion""" - - def process_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]: - return data - - -class RangeExpandDataProcessor(DataProcessor): - """Process data with range expansion""" - - def process_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]: - return expand_data_with_ranges(data) - - -class DataProcessorFactory: - """Factory to create appropriate data processor based on strategy""" - - _processors: ClassVar[dict[ObjectStrategy, type[DataProcessor]]] = { - ObjectStrategy.NORMAL: SingleDataProcessor, - ObjectStrategy.RANGE_EXPAND: RangeExpandDataProcessor, - } - - @classmethod - def get_processor(cls, strategy: ObjectStrategy) -> DataProcessor: - processor_class = cls._processors.get(strategy) - if not processor_class: - raise ValueError( - f"Unknown strategy: {strategy} - no processor found. Valid strategies are: {list(cls._processors.keys())}" - ) - return processor_class() - - @classmethod - def register_processor(cls, strategy: ObjectStrategy, processor_class: type[DataProcessor]) -> None: - """Register a new processor for a strategy - useful for future extensions""" - cls._processors[strategy] = processor_class - - class InfrahubObjectFileData(BaseModel): kind: str - strategy: ObjectStrategy = ObjectStrategy.NORMAL + parameters: InfrahubObjectParameters = Field(default_factory=InfrahubObjectParameters) data: list[dict[str, Any]] = Field(default_factory=list) - def _get_processed_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]: + async def _get_processed_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]: """Get data processed according to the strategy""" - processor = DataProcessorFactory.get_processor(self.strategy) - return processor.process_data(data) + + return await DataProcessorFactory.process_data(kind=self.kind, parameters=self.parameters, data=data) async def validate_format(self, client: InfrahubClient, branch: str | None = None) -> list[ObjectValidationError]: errors: list[ObjectValidationError] = [] schema = await client.schema.get(kind=self.kind, branch=branch) - processed_data = self._get_processed_data(data=self.data) + processed_data = await self._get_processed_data(data=self.data) self.data = processed_data for idx, item in enumerate(processed_data): @@ -275,14 +192,14 @@ async def validate_format(self, client: InfrahubClient, branch: str | None = Non data=item, branch=branch, default_schema_kind=self.kind, - strategy=self.strategy, # Pass strategy down + parameters=self.parameters, ) ) return errors async def process(self, client: InfrahubClient, branch: str | None = None) -> None: schema = await client.schema.get(kind=self.kind, branch=branch) - processed_data = self._get_processed_data(data=self.data) + processed_data = await self._get_processed_data(data=self.data) for idx, item in enumerate(processed_data): await self.create_node( @@ -304,8 +221,9 @@ async def validate_object( context: dict | None = None, branch: str | None = None, default_schema_kind: str | None = None, - strategy: ObjectStrategy = ObjectStrategy.NORMAL, + parameters: InfrahubObjectParameters | None = None, ) -> list[ObjectValidationError]: + parameters = parameters or InfrahubObjectParameters() errors: list[ObjectValidationError] = [] context = context.copy() if context else {} @@ -354,7 +272,7 @@ async def validate_object( context=context, branch=branch, default_schema_kind=default_schema_kind, - strategy=strategy, + parameters=parameters, ) ) @@ -370,8 +288,9 @@ async def validate_related_nodes( context: dict | None = None, branch: str | None = None, default_schema_kind: str | None = None, - strategy: ObjectStrategy = ObjectStrategy.NORMAL, + parameters: InfrahubObjectParameters | None = None, ) -> list[ObjectValidationError]: + parameters = parameters or InfrahubObjectParameters() context = context.copy() if context else {} errors: list[ObjectValidationError] = [] @@ -399,6 +318,7 @@ async def validate_related_nodes( context=context, branch=branch, default_schema_kind=default_schema_kind, + parameters=parameters, ) ) return errors @@ -412,11 +332,11 @@ async def validate_related_nodes( rel_info.find_matching_relationship(peer_schema=peer_schema) context.update(rel_info.get_context(value="placeholder")) - # Use strategy-aware data processing - processor = DataProcessorFactory.get_processor(strategy) - expanded_data = processor.process_data(data["data"]) + processed_data = await DataProcessorFactory.process_data( + kind=peer_kind, data=data["data"], parameters=parameters + ) - for idx, peer_data in enumerate(expanded_data): + for idx, peer_data in enumerate(processed_data): context["list_index"] = idx errors.extend( await cls.validate_object( @@ -427,7 +347,7 @@ async def validate_related_nodes( context=context, branch=branch, default_schema_kind=default_schema_kind, - strategy=strategy, + parameters=parameters, ) ) return errors @@ -452,6 +372,7 @@ async def validate_related_nodes( context=context, branch=branch, default_schema_kind=default_schema_kind, + parameters=parameters, ) ) return errors @@ -478,7 +399,9 @@ async def create_node( context: dict | None = None, branch: str | None = None, default_schema_kind: str | None = None, + parameters: InfrahubObjectParameters | None = None, ) -> InfrahubNode: + parameters = parameters or InfrahubObjectParameters() context = context.copy() if context else {} errors = await cls.validate_object( @@ -489,6 +412,7 @@ async def create_node( context=context, branch=branch, default_schema_kind=default_schema_kind, + parameters=parameters, ) if errors: messages = [str(error) for error in errors] @@ -534,6 +458,7 @@ async def create_node( data=value, branch=branch, default_schema_kind=default_schema_kind, + parameters=parameters, ) clean_data[key] = nodes[0] @@ -545,6 +470,7 @@ async def create_node( data=value, branch=branch, default_schema_kind=default_schema_kind, + parameters=parameters, ) clean_data[key] = nodes @@ -583,6 +509,7 @@ async def create_node( context=context, branch=branch, default_schema_kind=default_schema_kind, + parameters=parameters, ) return node @@ -598,7 +525,9 @@ async def create_related_nodes( context: dict | None = None, branch: str | None = None, default_schema_kind: str | None = None, + parameters: InfrahubObjectParameters | None = None, ) -> list[InfrahubNode]: + parameters = parameters or InfrahubObjectParameters() nodes: list[InfrahubNode] = [] context = context.copy() if context else {} @@ -618,6 +547,7 @@ async def create_related_nodes( context=context, branch=branch, default_schema_kind=default_schema_kind, + parameters=parameters, ) return [new_node] @@ -631,7 +561,10 @@ async def create_related_nodes( rel_info.find_matching_relationship(peer_schema=peer_schema) context.update(rel_info.get_context(value=parent_node.id)) - expanded_data = expand_data_with_ranges(data=data["data"]) + expanded_data = await DataProcessorFactory.process_data( + kind=peer_kind, data=data["data"], parameters=parameters + ) + for idx, peer_data in enumerate(expanded_data): context["list_index"] = idx if isinstance(peer_data, dict): @@ -643,6 +576,7 @@ async def create_related_nodes( context=context, branch=branch, default_schema_kind=default_schema_kind, + parameters=parameters, ) nodes.append(node) return nodes @@ -668,6 +602,7 @@ async def create_related_nodes( context=context, branch=branch, default_schema_kind=default_schema_kind, + parameters=parameters, ) nodes.append(node) diff --git a/infrahub_sdk/spec/processors/__init__.py b/infrahub_sdk/spec/processors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/infrahub_sdk/spec/processors/data_processor.py b/infrahub_sdk/spec/processors/data_processor.py new file mode 100644 index 00000000..0b007fec --- /dev/null +++ b/infrahub_sdk/spec/processors/data_processor.py @@ -0,0 +1,10 @@ +from abc import ABC, abstractmethod +from typing import Any + + +class DataProcessor(ABC): + """Abstract base class for data processing strategies""" + + @abstractmethod + async def process_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Process the data according to the strategy""" diff --git a/infrahub_sdk/spec/processors/factory.py b/infrahub_sdk/spec/processors/factory.py new file mode 100644 index 00000000..80f80efc --- /dev/null +++ b/infrahub_sdk/spec/processors/factory.py @@ -0,0 +1,34 @@ +from collections.abc import Sequence +from typing import Any + +from ..models import InfrahubObjectParameters +from .data_processor import DataProcessor +from .range_expand_processor import RangeExpandDataProcessor + +PROCESSOR_PER_KIND: dict[str, DataProcessor] = {} + + +class DataProcessorFactory: + """Factory to create appropriate data processor based on strategy""" + + @classmethod + def get_processors(cls, kind: str, parameters: InfrahubObjectParameters) -> Sequence[DataProcessor]: + processors: list[DataProcessor] = [] + if parameters.expand_range: + processors.append(RangeExpandDataProcessor()) + if kind in PROCESSOR_PER_KIND: + processors.append(PROCESSOR_PER_KIND[kind]) + + return processors + + @classmethod + async def process_data( + cls, + kind: str, + data: list[dict[str, Any]], + parameters: InfrahubObjectParameters, + ) -> list[dict[str, Any]]: + processors = cls.get_processors(kind=kind, parameters=parameters) + for processor in processors: + data = await processor.process_data(data=data) + return data diff --git a/infrahub_sdk/spec/processors/range_expand_processor.py b/infrahub_sdk/spec/processors/range_expand_processor.py new file mode 100644 index 00000000..53e159e4 --- /dev/null +++ b/infrahub_sdk/spec/processors/range_expand_processor.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import copy +import logging +import re +from typing import Any + +from ...exceptions import ValidationError +from ..range_expansion import MATCH_PATTERN, range_expansion +from .data_processor import DataProcessor + +log = logging.getLogger("infrahub_sdk") + + +class RangeExpandDataProcessor(DataProcessor): + """Process data with range expansion""" + + @classmethod + async def process_data( + cls, + data: list[dict[str, Any]], + ) -> list[dict[str, Any]]: + """Expand any item in data with range pattern in any value. Supports multiple fields, requires equal expansion length.""" + range_pattern = re.compile(MATCH_PATTERN) + expanded = [] + for item in data: + # Find all fields to expand + expand_fields = {} + for key, value in item.items(): + if isinstance(value, str) and range_pattern.search(value): + try: + expand_fields[key] = range_expansion(value) + except (ValueError, TypeError, KeyError): + # If expansion fails, treat as no expansion + log.debug( + f"Range expansion failed for value '{value}' in key '{key}'. Treating as no expansion." + ) + expand_fields[key] = [value] + if not expand_fields: + expanded.append(item) + continue + # Check all expanded lists have the same length + lengths = [len(v) for v in expand_fields.values()] + if len(set(lengths)) > 1: + raise ValidationError( + identifier="range_expansion", + message=f"Range expansion mismatch: fields expanded to different lengths: {lengths}", + ) + n = lengths[0] + # Zip expanded values and produce new items + for i in range(n): + new_item = copy.deepcopy(item) + for key, values in expand_fields.items(): + new_item[key] = values[i] + expanded.append(new_item) + return expanded diff --git a/poetry.lock b/poetry.lock index 5115d361..e1402209 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,6 +11,7 @@ files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] +markers = {dev = "python_version >= \"3.12\""} [[package]] name = "anyio" @@ -72,7 +73,7 @@ description = "Programmatic startup/shutdown of ASGI apps." optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "asgi-lifespan-2.1.0.tar.gz", hash = "sha256:5e2effaf0bfe39829cf2d64e7ecc47c7d86d676a6599f7afba378c31f5e3a308"}, {file = "asgi_lifespan-2.1.0-py3-none-any.whl", hash = "sha256:ed840706680e28428c01e14afb3875d7d76d3206f3d5b2f2294e059b5c23804f"}, @@ -115,19 +116,6 @@ six = ">=1.12.0" astroid = ["astroid (>=1,<2) ; python_version < \"3\"", "astroid (>=2,<4) ; python_version >= \"3\""] test = ["astroid (>=1,<2) ; python_version < \"3\"", "astroid (>=2,<4) ; python_version >= \"3\"", "pytest"] -[[package]] -name = "async-timeout" -version = "5.0.1" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, - {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, -] - [[package]] name = "attrs" version = "25.3.0" @@ -135,7 +123,7 @@ description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, @@ -221,7 +209,7 @@ description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "cachetools-6.2.0-py3-none-any.whl", hash = "sha256:1c76a8960c0041fcc21097e357f882197c79da0dbff766e7317890a65d7d8ba6"}, {file = "cachetools-6.2.0.tar.gz", hash = "sha256:38b328c0889450f05f5e120f56ab68c8abaf424e1275522b138ffc93253f7e32"}, @@ -374,7 +362,7 @@ description = "Pickler class to extend the standard pickle.Pickler functionality optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "cloudpickle-3.1.1-py3-none-any.whl", hash = "sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e"}, {file = "cloudpickle-3.1.1.tar.gz", hash = "sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64"}, @@ -391,7 +379,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "extra == \"ctl\" or extra == \"all\" or sys_platform == \"win32\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\" or python_version >= \"3.10\""} +markers = {main = "extra == \"ctl\" or extra == \"all\" or sys_platform == \"win32\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\" or python_version >= \"3.12\""} [[package]] name = "coolname" @@ -400,7 +388,7 @@ description = "Random name and slug generator" optional = false python-versions = "*" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "coolname-2.2.0-py2.py3-none-any.whl", hash = "sha256:4d1563186cfaf71b394d5df4c744f8c41303b6846413645e31d31915cdeb13e8"}, {file = "coolname-2.2.0.tar.gz", hash = "sha256:6c5d5731759104479e7ca195a9b64f7900ac5bead40183c09323c7d0be9e75c7"}, @@ -531,7 +519,7 @@ description = "Date parsing library designed to parse dates from HTML pages" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "dateparser-1.2.2-py3-none-any.whl", hash = "sha256:5a5d7211a09013499867547023a2a0c91d5a27d15dd4dbcea676ea9fe66f2482"}, {file = "dateparser-1.2.2.tar.gz", hash = "sha256:986316f17cb8cdc23ea8ce563027c5ef12fc725b6fb1d137c14ca08777c5ecf7"}, @@ -579,7 +567,7 @@ description = "A Python library for the Docker Engine API." optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0"}, {file = "docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c"}, @@ -727,7 +715,7 @@ files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] -markers = {main = "python_version < \"3.11\""} +markers = {main = "python_version < \"3.11\"", dev = "python_version < \"3.11\" or python_version >= \"3.12\""} [package.extras] test = ["pytest (>=6)"] @@ -769,7 +757,7 @@ description = "FastAPI framework, high performance, easy to learn, fast to code, optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565"}, {file = "fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143"}, @@ -809,7 +797,7 @@ description = "File-system specification" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "fsspec-2025.7.0-py3-none-any.whl", hash = "sha256:8b012e39f63c7d5f10474de957f3ab793b47b45ae7d39f2fb735f8bbe25c0e21"}, {file = "fsspec-2025.7.0.tar.gz", hash = "sha256:786120687ffa54b8283d942929540d8bc5ccfa820deb555a2b5d0ed2b737bf58"}, @@ -875,7 +863,7 @@ description = "Simple Python interface for Graphviz" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42"}, {file = "graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78"}, @@ -893,7 +881,7 @@ description = "Signatures for entire Python programs. Extract the structure, the optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "griffe-1.13.0-py3-none-any.whl", hash = "sha256:470fde5b735625ac0a36296cd194617f039e9e83e301fcbd493e2b58382d0559"}, {file = "griffe-1.13.0.tar.gz", hash = "sha256:246ea436a5e78f7fbf5f24ca8a727bb4d2a4b442a2959052eea3d0bfe9a076e0"}, @@ -921,7 +909,7 @@ description = "Pure-Python HTTP/2 protocol implementation" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd"}, {file = "h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1"}, @@ -938,7 +926,7 @@ description = "Pure-Python HPACK header encoding" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, @@ -999,7 +987,7 @@ description = "Python humanize utilities" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "humanize-4.13.0-py3-none-any.whl", hash = "sha256:b810820b31891813b1673e8fec7f1ed3312061eab2f26e3fa192c393d11ed25f"}, {file = "humanize-4.13.0.tar.gz", hash = "sha256:78f79e68f76f0b04d711c4e55d32bebef5be387148862cb1ef83d2b58e7935a0"}, @@ -1015,7 +1003,7 @@ description = "Pure-Python HTTP/2 framing" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"}, {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, @@ -1058,6 +1046,7 @@ description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "python_version == \"3.9\" or python_version >= \"3.12\"" files = [ {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, @@ -1101,20 +1090,20 @@ type = ["pytest-mypy"] [[package]] name = "infrahub-testcontainers" -version = "1.4.1" +version = "1.5.0b2" description = "Testcontainers instance for Infrahub to easily build integration tests" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ - {file = "infrahub_testcontainers-1.4.1-py3-none-any.whl", hash = "sha256:2da7ec2999344ef95cbafde3bec3617abd826c2262a3c4f11d1b8490e7c7b778"}, - {file = "infrahub_testcontainers-1.4.1.tar.gz", hash = "sha256:01f25f2554e78797207513c28a68da0cb3fcdf3f249a66a6ac25afa7559fa0a8"}, + {file = "infrahub_testcontainers-1.5.0b2-py3-none-any.whl", hash = "sha256:0c1138cf7b1cf85d991258f291e7120b325b0fbed46968352c9561348fc152fb"}, + {file = "infrahub_testcontainers-1.5.0b2.tar.gz", hash = "sha256:000cf0136ac8f7d43d3e6d9090ae6a15814f847f4bdfd370e9bcb91f0042ea6e"}, ] [package.dependencies] httpx = ">=0.28.1,<0.29.0" -prefect-client = "3.4.13" +prefect-client = "3.4.23" psutil = "*" pydantic = ">=2.10.6,<3.0.0" pytest = "*" @@ -1265,7 +1254,7 @@ description = "Apply JSON-Patches (RFC 6902)" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, @@ -1281,7 +1270,7 @@ description = "Identify specific nodes in a JSON document (RFC 6901)" optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, @@ -1294,7 +1283,7 @@ description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63"}, {file = "jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85"}, @@ -1317,7 +1306,7 @@ description = "The JSON Schema meta-schemas and vocabularies, exposed as a Regis optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af"}, {file = "jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608"}, @@ -1593,7 +1582,7 @@ description = "OpenTelemetry Python API" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "opentelemetry_api-1.36.0-py3-none-any.whl", hash = "sha256:02f20bcacf666e1333b6b1f04e647dc1d5111f86b8e510238fcc56d7762cda8c"}, {file = "opentelemetry_api-1.36.0.tar.gz", hash = "sha256:9a72572b9c416d004d492cbc6e61962c0501eaf945ece9b5a0f56597d8348aa0"}, @@ -1610,7 +1599,7 @@ description = "Fast, correct Python JSON library supporting dataclasses, datetim optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "orjson-3.11.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:29cb1f1b008d936803e2da3d7cba726fc47232c45df531b29edf0b232dd737e7"}, {file = "orjson-3.11.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97dceed87ed9139884a55db8722428e27bd8452817fbf1869c58b49fecab1120"}, @@ -1745,7 +1734,7 @@ description = "Python datetimes made easy" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\" and python_version < \"3.13\"" +markers = "python_version == \"3.12\"" files = [ {file = "pendulum-3.1.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:aa545a59e6517cf43597455a6fb44daa4a6e08473d67a7ad34e4fa951efb9620"}, {file = "pendulum-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:299df2da6c490ede86bb8d58c65e33d7a2a42479d21475a54b467b03ccb88531"}, @@ -1925,15 +1914,15 @@ virtualenv = ">=20.10.0" [[package]] name = "prefect-client" -version = "3.4.13" +version = "3.4.23" description = "Workflow orchestration and management." optional = false python-versions = "<3.14,>=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ - {file = "prefect_client-3.4.13-py3-none-any.whl", hash = "sha256:24d0599c1c3135b8fd55c195312d1b6020d146762b2b046ea2902478f6402cd5"}, - {file = "prefect_client-3.4.13.tar.gz", hash = "sha256:96572081a14892d7d02667e9e4fa3a3634f5421ab6389bc8aa1e18f475b9e3f2"}, + {file = "prefect_client-3.4.23-py3-none-any.whl", hash = "sha256:6d3ac95ced68a3d5d461e13f90df35871ce90e58194b8a354f840c6a8e6c1fa1"}, + {file = "prefect_client-3.4.23.tar.gz", hash = "sha256:23d035c1e5a9df0c69c701c5f6ad3f8b80530c19a2383b4ca319a2397fe09ac4"}, ] [package.dependencies] @@ -1976,7 +1965,7 @@ toml = ">=0.10.0" typing-extensions = ">=4.10.0,<5.0.0" uvicorn = ">=0.14.0,<0.29.0 || >0.29.0" websockets = ">=13.0,<16.0" -whenever = {version = ">=0.7.3,<0.9.0", markers = "python_version >= \"3.13\""} +whenever = {version = ">=0.7.3,<0.10.0", markers = "python_version >= \"3.13\""} [package.extras] notifications = ["apprise (>=1.1.0,<2.0.0)"] @@ -1988,7 +1977,7 @@ description = "Python client for the Prometheus monitoring system." optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "prometheus_client-0.22.1-py3-none-any.whl", hash = "sha256:cca895342e308174341b2cbf99a56bef291fbc0ef7b9e5412a0f26d653ba7094"}, {file = "prometheus_client-0.22.1.tar.gz", hash = "sha256:190f1331e783cf21eb60bca559354e0a4d4378facecf78f5428c39b675d20d28"}, @@ -2020,7 +2009,7 @@ description = "Cross-platform lib for process and system monitoring in Python. optional = false python-versions = ">=3.6" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25"}, {file = "psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da"}, @@ -2133,6 +2122,7 @@ files = [ {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, ] +markers = {dev = "python_version >= \"3.12\""} [package.dependencies] annotated-types = ">=0.6.0" @@ -2252,6 +2242,7 @@ files = [ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, ] +markers = {dev = "python_version >= \"3.12\""} [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" @@ -2263,7 +2254,7 @@ description = "Extra Pydantic types." optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "pydantic_extra_types-2.10.5-py3-none-any.whl", hash = "sha256:b60c4e23d573a69a4f1a16dd92888ecc0ef34fb0e655b4f305530377fa70e7a8"}, {file = "pydantic_extra_types-2.10.5.tar.gz", hash = "sha256:1dcfa2c0cf741a422f088e0dbb4690e7bfadaaf050da3d6f80d6c3cf58a2bad8"}, @@ -2292,6 +2283,7 @@ files = [ {file = "pydantic_settings-2.5.2-py3-none-any.whl", hash = "sha256:2c912e55fd5794a59bf8c832b9de832dcfdf4778d79ff79b708744eed499a907"}, {file = "pydantic_settings-2.5.2.tar.gz", hash = "sha256:f90b139682bee4d2065273d5185d71d37ea46cfe57e1b5ae184fc6a0b2484ca0"}, ] +markers = {dev = "python_version >= \"3.12\""} [package.dependencies] pydantic = ">=2.7.0" @@ -2455,7 +2447,7 @@ description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -2475,6 +2467,7 @@ files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, ] +markers = {dev = "python_version >= \"3.12\""} [package.extras] cli = ["click (>=5.0)"] @@ -2486,7 +2479,7 @@ description = "A Python slugify application that also handles Unicode" optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, @@ -2505,15 +2498,12 @@ description = "Proxy (SOCKS4, SOCKS5, HTTP CONNECT) client for Python" optional = false python-versions = ">=3.8.0" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "python_socks-2.7.2-py3-none-any.whl", hash = "sha256:d311aefbacc0ddfaa1fa1c32096c436d4fe75b899c24d78e677e1b0623c52c48"}, {file = "python_socks-2.7.2.tar.gz", hash = "sha256:4c845d4700352bc7e7382f302dfc6baf0af0de34d2a6d70ba356b2539d4dbb62"}, ] -[package.dependencies] -async-timeout = {version = ">=4.0", optional = true, markers = "python_version < \"3.11\" and extra == \"asyncio\""} - [package.extras] anyio = ["anyio (>=3.3.4,<5.0.0)"] asyncio = ["async-timeout (>=4.0) ; python_version < \"3.11\""] @@ -2527,7 +2517,7 @@ description = "World timezone definitions, modern and historical" optional = false python-versions = "*" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, @@ -2537,7 +2527,7 @@ files = [ name = "pywin32" version = "308" description = "Python for Window Extensions" -optional = false +optional = true python-versions = "*" groups = ["main", "dev"] files = [ @@ -2560,7 +2550,7 @@ files = [ {file = "pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341"}, {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, ] -markers = {main = "platform_python_implementation != \"PyPy\" and (extra == \"ctl\" or extra == \"all\") and platform_system == \"Windows\"", dev = "python_version >= \"3.10\" and sys_platform == \"win32\""} +markers = {main = "platform_python_implementation != \"PyPy\" and (extra == \"ctl\" or extra == \"all\") and platform_system == \"Windows\"", dev = "python_version >= \"3.12\" and sys_platform == \"win32\""} [[package]] name = "pyyaml" @@ -2649,7 +2639,7 @@ description = "JSON Referencing + Python" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, @@ -2667,7 +2657,7 @@ description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "regex-2025.7.34-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d856164d25e2b3b07b779bfed813eb4b6b6ce73c2fd818d46f47c1eb5cd79bd6"}, {file = "regex-2025.7.34-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d15a9da5fad793e35fb7be74eec450d968e05d2e294f3e0e77ab03fa7234a83"}, @@ -2787,7 +2777,7 @@ description = "A pure python RFC3339 validator" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, @@ -2823,7 +2813,7 @@ description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef"}, {file = "rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be"}, @@ -2989,7 +2979,7 @@ description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip pres optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "ruamel.yaml-0.18.15-py3-none-any.whl", hash = "sha256:148f6488d698b7a5eded5ea793a025308b25eca97208181b6a026037f391f701"}, {file = "ruamel.yaml-0.18.15.tar.gz", hash = "sha256:dbfca74b018c4c3fba0b9cc9ee33e53c371194a9000e694995e620490fd40700"}, @@ -3009,7 +2999,7 @@ description = "C version of reader, parser and emitter for ruamel.yaml derived f optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.10\"" +markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.12\"" files = [ {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}, @@ -3017,7 +3007,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, @@ -3026,7 +3015,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, @@ -3035,7 +3023,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, @@ -3044,7 +3031,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, @@ -3053,7 +3039,6 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, @@ -3151,7 +3136,7 @@ description = "The little ASGI library that shines." optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51"}, {file = "starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9"}, @@ -3171,7 +3156,7 @@ description = "Python library for throwaway instances of anything that can run i optional = false python-versions = "<4.0,>=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "testcontainers-4.8.2-py3-none-any.whl", hash = "sha256:9e19af077cd96e1957c13ee466f1f32905bc6c5bc1bc98643eb18be1a989bfb0"}, {file = "testcontainers-4.8.2.tar.gz", hash = "sha256:dd4a6a2ea09e3c3ecd39e180b6548105929d0bb78d665ce9919cb3f8c98f9853"}, @@ -3225,7 +3210,7 @@ description = "The most basic Text::Unidecode port" optional = false python-versions = "*" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, @@ -3235,14 +3220,14 @@ files = [ name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -optional = false +optional = true python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" groups = ["main", "dev"] files = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] -markers = {main = "extra == \"ctl\" or extra == \"all\"", dev = "python_version >= \"3.10\""} +markers = {main = "extra == \"ctl\" or extra == \"all\"", dev = "python_version >= \"3.12\""} [[package]] name = "tomli" @@ -3373,6 +3358,7 @@ files = [ {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, {file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"}, ] +markers = {dev = "python_version >= \"3.12\""} [package.dependencies] typing-extensions = ">=4.12.0" @@ -3388,7 +3374,7 @@ files = [ {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, ] -markers = {main = "sys_platform == \"win32\"", dev = "(platform_system == \"Windows\" or sys_platform == \"win32\" or python_version == \"3.12\" or python_version == \"3.11\" or python_version == \"3.10\") and python_version >= \"3.10\""} +markers = {main = "sys_platform == \"win32\"", dev = "python_version >= \"3.12\" and (platform_system == \"Windows\" or sys_platform == \"win32\" or python_version == \"3.12\")"} [[package]] name = "tzlocal" @@ -3397,7 +3383,7 @@ description = "tzinfo object for the local timezone" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d"}, {file = "tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd"}, @@ -3522,7 +3508,7 @@ description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a"}, {file = "uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01"}, @@ -3531,7 +3517,6 @@ files = [ [package.dependencies] click = ">=7.0" h11 = ">=0.8" -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] @@ -3577,7 +3562,7 @@ description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, @@ -3742,7 +3727,7 @@ description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version >= \"3.10\"" +markers = "python_version >= \"3.12\"" files = [ {file = "wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8"}, {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d"}, @@ -3837,6 +3822,7 @@ description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "python_version == \"3.9\" or python_version >= \"3.12\"" files = [ {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, @@ -3858,4 +3844,4 @@ tests = ["Jinja2", "pytest", "pyyaml", "rich"] [metadata] lock-version = "2.1" python-versions = "^3.9, <3.14" -content-hash = "e46a650fcc8ef743c26c20c668b3256db096d06c20dbd17161d18d47f24e5a93" +content-hash = "62ddd6975004d3f3fde5eb25a937802773a8108bd0eadd84afeb3c505e8d4b9d" diff --git a/pyproject.toml b/pyproject.toml index 1590dfc3..37e1a418 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "infrahub-sdk" -version = "1.14.0" +version = "1.15.0" description = "Python Client to interact with Infrahub" authors = ["OpsMill "] readme = "README.md" @@ -66,7 +66,7 @@ pytest-xdist = "^3.3.1" types-python-slugify = "^8.0.0.3" invoke = "^2.2.0" towncrier = "^24.8.0" -infrahub-testcontainers = { version = "^1.4.0", python = ">=3.10" } +infrahub-testcontainers = { version = "1.5.0b2", python = ">=3.12" } astroid = "~3.1" [tool.poetry.extras] diff --git a/tests/unit/ctl/conftest.py b/tests/unit/ctl/conftest.py index e63efec1..30ce70d7 100644 --- a/tests/unit/ctl/conftest.py +++ b/tests/unit/ctl/conftest.py @@ -36,6 +36,8 @@ async def mock_branches_list_query(httpx_mock: HTTPXMock) -> HTTPXMock: "origin_branch": "main", "branched_from": "2023-02-17T09:30:17.811719Z", "has_schema_changes": False, + "graph_version": 99, + "status": "OPEN", }, { "id": "7d9f817a-b958-4e76-8528-8afd0c689ada", @@ -45,6 +47,8 @@ async def mock_branches_list_query(httpx_mock: HTTPXMock) -> HTTPXMock: "origin_branch": "main", "branched_from": "2023-02-17T09:30:17.811719Z", "has_schema_changes": True, + "graph_version": None, + "status": "NEED_UPGRADE_REBASE", }, ] } diff --git a/tests/unit/sdk/conftest.py b/tests/unit/sdk/conftest.py index 92749412..efa4f845 100644 --- a/tests/unit/sdk/conftest.py +++ b/tests/unit/sdk/conftest.py @@ -862,7 +862,7 @@ async def rfile_schema() -> NodeSchemaAPI: "name": "TransformJinja2", "namespace": "Core", "default_filter": "name__value", - "display_label": ["label__value"], + "display_labels": ["label__value"], "branch": BranchSupportType.AWARE.value, "attributes": [ {"name": "name", "kind": "String", "unique": True}, @@ -1498,6 +1498,8 @@ async def mock_branches_list_query(httpx_mock: HTTPXMock) -> HTTPXMock: "origin_branch": "main", "branched_from": "2023-02-17T09:30:17.811719Z", "has_schema_changes": False, + "graph_version": 99, + "status": "OPEN", }, { "id": "7d9f817a-b958-4e76-8528-8afd0c689ada", @@ -1507,6 +1509,8 @@ async def mock_branches_list_query(httpx_mock: HTTPXMock) -> HTTPXMock: "origin_branch": "main", "branched_from": "2023-02-17T09:30:17.811719Z", "has_schema_changes": True, + "graph_version": None, + "status": "NEED_UPGRADE_REBASE", }, ] } @@ -1878,6 +1882,12 @@ async def mock_schema_query_01(httpx_mock: HTTPXMock, schema_query_01_data: dict return httpx_mock +@pytest.fixture +async def client_with_schema_01(client: InfrahubClient, schema_query_01_data: dict) -> InfrahubClient: + client.schema.set_cache(schema=schema_query_01_data, branch="main") + return client + + @pytest.fixture async def mock_schema_query_02(httpx_mock: HTTPXMock, schema_query_02_data: dict) -> HTTPXMock: httpx_mock.add_response( diff --git a/tests/unit/sdk/spec/test_object.py b/tests/unit/sdk/spec/test_object.py index faf862b0..570f14c5 100644 --- a/tests/unit/sdk/spec/test_object.py +++ b/tests/unit/sdk/spec/test_object.py @@ -5,11 +5,9 @@ import pytest from infrahub_sdk.exceptions import ValidationError -from infrahub_sdk.spec.object import ObjectFile, ObjectStrategy, RelationshipDataFormat, get_relationship_info +from infrahub_sdk.spec.object import ObjectFile, RelationshipDataFormat, get_relationship_info if TYPE_CHECKING: - from pytest_httpx import HTTPXMock - from infrahub_sdk.client import InfrahubClient @@ -40,7 +38,7 @@ def location_bad_syntax02(root_location: dict) -> dict: data = [{"name": "Mexico", "notvalidattribute": "notvalidattribute", "type": "Country"}] location = root_location.copy() location["spec"]["data"] = data - location["spec"]["strategy"] = ObjectStrategy.RANGE_EXPAND + location["spec"]["parameters"] = {"expand_range": True} return location @@ -54,7 +52,7 @@ def location_expansion(root_location: dict) -> dict: ] location = root_location.copy() location["spec"]["data"] = data - location["spec"]["strategy"] = ObjectStrategy.RANGE_EXPAND + location["spec"]["parameters"] = {"expand_range": True} return location @@ -68,7 +66,7 @@ def no_location_expansion(root_location: dict) -> dict: ] location = root_location.copy() location["spec"]["data"] = data - location["spec"]["strategy"] = ObjectStrategy.NORMAL + location["spec"]["parameters"] = {"expand_range": False} return location @@ -83,7 +81,7 @@ def location_expansion_multiple_ranges(root_location: dict) -> dict: ] location = root_location.copy() location["spec"]["data"] = data - location["spec"]["strategy"] = ObjectStrategy.RANGE_EXPAND + location["spec"]["parameters"] = {"expand_range": True} return location @@ -98,11 +96,12 @@ def location_expansion_multiple_ranges_bad_syntax(root_location: dict) -> dict: ] location = root_location.copy() location["spec"]["data"] = data - location["spec"]["strategy"] = ObjectStrategy.RANGE_EXPAND + location["spec"]["parameters"] = {"expand_range": True} return location -async def test_validate_object(client: InfrahubClient, mock_schema_query_01: HTTPXMock, location_mexico_01) -> None: +async def test_validate_object(client: InfrahubClient, schema_query_01_data: dict, location_mexico_01) -> None: + client.schema.set_cache(schema=schema_query_01_data, branch="main") obj = ObjectFile(location="some/path", content=location_mexico_01) await obj.validate_format(client=client) @@ -110,8 +109,9 @@ async def test_validate_object(client: InfrahubClient, mock_schema_query_01: HTT async def test_validate_object_bad_syntax01( - client: InfrahubClient, mock_schema_query_01: HTTPXMock, location_bad_syntax01 + client: InfrahubClient, schema_query_01_data: dict, location_bad_syntax01 ) -> None: + client.schema.set_cache(schema=schema_query_01_data, branch="main") obj = ObjectFile(location="some/path", content=location_bad_syntax01) with pytest.raises(ValidationError) as exc: await obj.validate_format(client=client) @@ -119,21 +119,17 @@ async def test_validate_object_bad_syntax01( assert "name" in str(exc.value) -async def test_validate_object_bad_syntax02( - client: InfrahubClient, mock_schema_query_01: HTTPXMock, location_bad_syntax02 -) -> None: +async def test_validate_object_bad_syntax02(client_with_schema_01: InfrahubClient, location_bad_syntax02) -> None: obj = ObjectFile(location="some/path", content=location_bad_syntax02) with pytest.raises(ValidationError) as exc: - await obj.validate_format(client=client) + await obj.validate_format(client=client_with_schema_01) assert "notvalidattribute" in str(exc.value) -async def test_validate_object_expansion( - client: InfrahubClient, mock_schema_query_01: HTTPXMock, location_expansion -) -> None: +async def test_validate_object_expansion(client_with_schema_01: InfrahubClient, location_expansion) -> None: obj = ObjectFile(location="some/path", content=location_expansion) - await obj.validate_format(client=client) + await obj.validate_format(client=client_with_schema_01) assert obj.spec.kind == "BuiltinLocation" assert len(obj.spec.data) == 5 @@ -141,22 +137,20 @@ async def test_validate_object_expansion( assert obj.spec.data[4]["name"] == "AMS5" -async def test_validate_no_object_expansion( - client: InfrahubClient, mock_schema_query_01: HTTPXMock, no_location_expansion -) -> None: +async def test_validate_no_object_expansion(client_with_schema_01: InfrahubClient, no_location_expansion) -> None: obj = ObjectFile(location="some/path", content=no_location_expansion) - await obj.validate_format(client=client) + await obj.validate_format(client=client_with_schema_01) assert obj.spec.kind == "BuiltinLocation" - assert obj.spec.strategy == ObjectStrategy.NORMAL + assert not obj.spec.parameters.expand_range assert len(obj.spec.data) == 1 assert obj.spec.data[0]["name"] == "AMS[1-5]" async def test_validate_object_expansion_multiple_ranges( - client: InfrahubClient, mock_schema_query_01: HTTPXMock, location_expansion_multiple_ranges + client_with_schema_01: InfrahubClient, location_expansion_multiple_ranges ) -> None: obj = ObjectFile(location="some/path", content=location_expansion_multiple_ranges) - await obj.validate_format(client=client) + await obj.validate_format(client=client_with_schema_01) assert obj.spec.kind == "BuiltinLocation" assert len(obj.spec.data) == 5 @@ -167,11 +161,11 @@ async def test_validate_object_expansion_multiple_ranges( async def test_validate_object_expansion_multiple_ranges_bad_syntax( - client: InfrahubClient, mock_schema_query_01: HTTPXMock, location_expansion_multiple_ranges_bad_syntax + client_with_schema_01: InfrahubClient, location_expansion_multiple_ranges_bad_syntax ) -> None: obj = ObjectFile(location="some/path", content=location_expansion_multiple_ranges_bad_syntax) with pytest.raises(ValidationError) as exc: - await obj.validate_format(client=client) + await obj.validate_format(client=client_with_schema_01) assert "Range expansion mismatch" in str(exc.value) @@ -217,41 +211,13 @@ async def test_validate_object_expansion_multiple_ranges_bad_syntax( @pytest.mark.parametrize("data,is_valid,format", get_relationship_info_testdata) async def test_get_relationship_info_tags( - client: InfrahubClient, - mock_schema_query_01: HTTPXMock, + client_with_schema_01: InfrahubClient, data: dict | list, is_valid: bool, format: RelationshipDataFormat, ) -> None: - location_schema = await client.schema.get(kind="BuiltinLocation") + location_schema = await client_with_schema_01.schema.get(kind="BuiltinLocation") - rel_info = await get_relationship_info(client, location_schema, "tags", data) + rel_info = await get_relationship_info(client_with_schema_01, location_schema, "tags", data) assert rel_info.is_valid == is_valid assert rel_info.format == format - - -async def test_invalid_object_expansion_processor( - client: InfrahubClient, mock_schema_query_01: HTTPXMock, location_expansion -) -> None: - obj = ObjectFile(location="some/path", content=location_expansion) - - from infrahub_sdk.spec.object import DataProcessorFactory, ObjectStrategy # noqa: PLC0415 - - # Patch _processors to remove the invalid strategy - original_processors = DataProcessorFactory._processors.copy() - try: - DataProcessorFactory._processors[ObjectStrategy.RANGE_EXPAND] = None - with pytest.raises(ValueError) as exc: - await obj.validate_format(client=client) - assert "Unknown strategy" in str(exc.value) - finally: - DataProcessorFactory._processors = original_processors - - -async def test_invalid_object_expansion_strategy(client: InfrahubClient, location_expansion) -> None: - location_expansion["spec"]["strategy"] = "InvalidStrategy" - obj = ObjectFile(location="some/path", content=location_expansion) - - with pytest.raises(ValidationError) as exc: - await obj.validate_format(client=client) - assert "Input should be" in str(exc.value) diff --git a/tests/unit/sdk/test_client.py b/tests/unit/sdk/test_client.py index 82bda107..2d5de0ad 100644 --- a/tests/unit/sdk/test_client.py +++ b/tests/unit/sdk/test_client.py @@ -607,7 +607,7 @@ async def test_allocate_next_ip_address( method="POST", json={ "data": { - "IPAddressPoolGetResource": { + "InfrahubIPAddressPoolGetResource": { "ok": True, "node": { "id": "17da1246-54f1-a9c0-2784-179f0ec5b128", @@ -708,7 +708,7 @@ async def test_allocate_next_ip_prefix( method="POST", json={ "data": { - "IPPrefixPoolGetResource": { + "InfrahubIPPrefixPoolGetResource": { "ok": True, "node": { "id": "7d9bd8d-8fc2-70b0-278a-179f425e25cb",