From 356bfb22792da79e1dfbdec0425189e959c62158 Mon Sep 17 00:00:00 2001 From: Damien Garros Date: Sun, 13 Oct 2024 19:15:12 +0200 Subject: [PATCH 1/3] Add infrahubctl menu load command to load menu items from a file --- infrahub_sdk/ctl/cli_commands.py | 16 +++- infrahub_sdk/ctl/menu.py | 60 +++++++++++++++ infrahub_sdk/ctl/object.py | 48 ++++++++++++ infrahub_sdk/ctl/schema.py | 46 +---------- infrahub_sdk/ctl/utils.py | 29 ++++++- infrahub_sdk/protocols.py | 2 + infrahub_sdk/spec/__init__.py | 0 infrahub_sdk/spec/menu.py | 33 ++++++++ infrahub_sdk/spec/object.py | 128 +++++++++++++++++++++++++++++++ infrahub_sdk/yaml.py | 76 +++++++++++++++++- 10 files changed, 388 insertions(+), 50 deletions(-) create mode 100644 infrahub_sdk/ctl/menu.py create mode 100644 infrahub_sdk/ctl/object.py create mode 100644 infrahub_sdk/spec/__init__.py create mode 100644 infrahub_sdk/spec/menu.py create mode 100644 infrahub_sdk/spec/object.py diff --git a/infrahub_sdk/ctl/cli_commands.py b/infrahub_sdk/ctl/cli_commands.py index 498e7b0d..dc0b7525 100644 --- a/infrahub_sdk/ctl/cli_commands.py +++ b/infrahub_sdk/ctl/cli_commands.py @@ -22,13 +22,19 @@ from infrahub_sdk.ctl.client import initialize_client, initialize_client_sync from infrahub_sdk.ctl.exceptions import QueryNotFoundError from infrahub_sdk.ctl.generator import run as run_generator +from infrahub_sdk.ctl.menu import app as menu_app +from infrahub_sdk.ctl.object import app as object_app from infrahub_sdk.ctl.render import list_jinja2_transforms from infrahub_sdk.ctl.repository import app as repository_app from infrahub_sdk.ctl.repository import get_repository_config from infrahub_sdk.ctl.schema import app as schema_app -from infrahub_sdk.ctl.schema import load_schemas_from_disk_and_exit from infrahub_sdk.ctl.transform import list_transforms -from infrahub_sdk.ctl.utils import catch_exception, execute_graphql_query, parse_cli_vars +from infrahub_sdk.ctl.utils import ( + catch_exception, + execute_graphql_query, + load_yamlfile_from_disk_and_exit, + parse_cli_vars, +) from infrahub_sdk.ctl.validate import app as validate_app from infrahub_sdk.exceptions import GraphQLError, InfrahubTransformNotFoundError from infrahub_sdk.jinja2 import identify_faulty_jinja_code @@ -39,6 +45,7 @@ ) from infrahub_sdk.transforms import get_transform_class_instance from infrahub_sdk.utils import get_branch, write_to_file +from infrahub_sdk.yaml import SchemaFile from .exporter import dump from .importer import load @@ -50,6 +57,9 @@ app.add_typer(schema_app, name="schema") app.add_typer(validate_app, name="validate") app.add_typer(repository_app, name="repository") +app.add_typer(menu_app, name="menu") +app.add_typer(object_app, name="object", hidden=True) + app.command(name="dump")(dump) app.command(name="load")(load) @@ -338,7 +348,7 @@ def protocols( # noqa: PLR0915 schema: dict[str, MainSchemaTypes] = {} if schemas: - schemas_data = load_schemas_from_disk_and_exit(schemas=schemas) + schemas_data = load_yamlfile_from_disk_and_exit(paths=schemas, file_type=SchemaFile, console=console) for data in schemas_data: data.load_content() diff --git a/infrahub_sdk/ctl/menu.py b/infrahub_sdk/ctl/menu.py new file mode 100644 index 00000000..8c66694d --- /dev/null +++ b/infrahub_sdk/ctl/menu.py @@ -0,0 +1,60 @@ +import logging +from pathlib import Path + +import typer +from rich.console import Console + +from infrahub_sdk.async_typer import AsyncTyper +from infrahub_sdk.ctl.client import initialize_client +from infrahub_sdk.ctl.utils import catch_exception, init_logging +from infrahub_sdk.spec.menu import MenuFile + +from .parameters import CONFIG_PARAM +from .utils import load_yamlfile_from_disk_and_exit + +app = AsyncTyper() +console = Console() + + +@app.callback() +def callback() -> None: + """ + Manage the menu in a remote Infrahub instance. + """ + + +@app.command() +@catch_exception(console=console) +async def load( + menus: list[Path], + debug: bool = False, + branch: str = typer.Option("main", help="Branch on which to load the menu."), + _: str = CONFIG_PARAM, +) -> None: + """Load one or multiple menu files into Infrahub.""" + + init_logging(debug=debug) + + logging.getLogger("infrahub_sdk").setLevel(logging.INFO) + + files = load_yamlfile_from_disk_and_exit(paths=menus, file_type=MenuFile, console=console) + client = await initialize_client() + + default_kind = "CoreMenuItem" + + for file in files: + file.validate_content() + if not file.spec.kind: + file.spec.kind = default_kind + + schema = await client.schema.get(kind=file.spec.kind, branch=branch) + + for idx, item in enumerate(file.spec.data): + await file.spec.create_node( + client=client, + schema=schema, + data=item, + branch=branch, + default_schema_kind=default_kind, + context={"list_index": idx}, + ) diff --git a/infrahub_sdk/ctl/object.py b/infrahub_sdk/ctl/object.py new file mode 100644 index 00000000..34141f0f --- /dev/null +++ b/infrahub_sdk/ctl/object.py @@ -0,0 +1,48 @@ +from pathlib import Path + +import typer +from rich.console import Console + +from infrahub_sdk.async_typer import AsyncTyper +from infrahub_sdk.ctl.client import initialize_client +from infrahub_sdk.ctl.exceptions import FileNotValidError +from infrahub_sdk.ctl.utils import catch_exception, init_logging +from infrahub_sdk.spec.object import ObjectFile + +from .parameters import CONFIG_PARAM +from .utils import load_yamlfile_from_disk_and_exit + +app = AsyncTyper() +console = Console() + + +@app.callback() +def callback() -> None: + """ + Manage objects in a remote Infrahub instance. + """ + + +@app.command() +@catch_exception(console=console) +async def load( + paths: list[Path], + debug: bool = False, + branch: str = typer.Option("main", help="Branch on which to load the objects."), + _: str = CONFIG_PARAM, +) -> None: + """Load one or multiple objects files into Infrahub.""" + + init_logging(debug=debug) + + files = load_yamlfile_from_disk_and_exit(paths=paths, file_type=ObjectFile, console=console) + client = await initialize_client() + + for file in files: + file.validate_content() + if not file.spec.kind: + raise FileNotValidError(name=str(file.location), message="kind must be specified.") + schema = await client.schema.get(kind=file.spec.kind, branch=branch) + + for item in file.spec.data: + await file.spec.create_node(client=client, schema=schema, data=item, branch=branch) diff --git a/infrahub_sdk/ctl/schema.py b/infrahub_sdk/ctl/schema.py index 57ecc379..d1bd9109 100644 --- a/infrahub_sdk/ctl/schema.py +++ b/infrahub_sdk/ctl/schema.py @@ -11,13 +11,12 @@ from infrahub_sdk import InfrahubClient from infrahub_sdk.async_typer import AsyncTyper from infrahub_sdk.ctl.client import initialize_client -from infrahub_sdk.ctl.exceptions import FileNotValidError from infrahub_sdk.ctl.utils import catch_exception, init_logging from infrahub_sdk.queries import SCHEMA_HASH_SYNC_STATUS -from infrahub_sdk.utils import find_files from infrahub_sdk.yaml import SchemaFile from .parameters import CONFIG_PARAM +from .utils import load_yamlfile_from_disk_and_exit app = AsyncTyper() console = Console() @@ -30,45 +29,6 @@ def callback() -> None: """ -def load_schemas_from_disk(schemas: list[Path]) -> list[SchemaFile]: - schemas_data: list[SchemaFile] = [] - for schema in schemas: - if schema.is_file(): - schema_file = SchemaFile(location=schema) - schema_file.load_content() - schemas_data.append(schema_file) - elif schema.is_dir(): - files = find_files(extension=["yaml", "yml", "json"], directory=schema) - for item in files: - schema_file = SchemaFile(location=item) - schema_file.load_content() - schemas_data.append(schema_file) - else: - raise FileNotValidError(name=schema, message=f"Schema path: {schema} does not exist!") - - return schemas_data - - -def load_schemas_from_disk_and_exit(schemas: list[Path]) -> list[SchemaFile]: - has_error = False - try: - schemas_data = load_schemas_from_disk(schemas=schemas) - except FileNotValidError as exc: - console.print(f"[red]{exc.message}") - raise typer.Exit(1) from exc - - for schema_file in schemas_data: - if schema_file.valid and schema_file.content: - continue - console.print(f"[red]{schema_file.error_message} ({schema_file.location})") - has_error = True - - if has_error: - raise typer.Exit(1) - - return schemas_data - - def validate_schema_content_and_exit(client: InfrahubClient, schemas: list[SchemaFile]) -> None: has_error: bool = False for schema_file in schemas: @@ -153,7 +113,7 @@ async def load( init_logging(debug=debug) - schemas_data = load_schemas_from_disk_and_exit(schemas=schemas) + schemas_data = load_yamlfile_from_disk_and_exit(paths=schemas, file_type=SchemaFile, console=console) schema_definition = "schema" if len(schemas_data) == 1 else "schemas" client = await initialize_client() validate_schema_content_and_exit(client=client, schemas=schemas_data) @@ -203,7 +163,7 @@ async def check( init_logging(debug=debug) - schemas_data = load_schemas_from_disk_and_exit(schemas=schemas) + schemas_data = load_yamlfile_from_disk_and_exit(paths=schemas, file_type=SchemaFile, console=console) client = await initialize_client() validate_schema_content_and_exit(client=client, schemas=schemas_data) diff --git a/infrahub_sdk/ctl/utils.py b/infrahub_sdk/ctl/utils.py index 73eb3e6d..d3663bb7 100644 --- a/infrahub_sdk/ctl/utils.py +++ b/infrahub_sdk/ctl/utils.py @@ -3,7 +3,7 @@ import traceback from functools import wraps from pathlib import Path -from typing import Any, Callable, Optional, Union +from typing import Any, Callable, Optional, TypeVar, Union import pendulum import typer @@ -14,7 +14,7 @@ from rich.logging import RichHandler from rich.markup import escape -from infrahub_sdk.ctl.exceptions import QueryNotFoundError +from infrahub_sdk.ctl.exceptions import FileNotValidError, QueryNotFoundError from infrahub_sdk.exceptions import ( AuthenticationError, Error, @@ -25,9 +25,12 @@ ServerNotResponsiveError, ) from infrahub_sdk.schema import InfrahubRepositoryConfig +from infrahub_sdk.yaml import YamlFile from .client import initialize_client_sync +YamlFileVar = TypeVar("YamlFileVar", bound=YamlFile) + def init_logging(debug: bool = False) -> None: logging.getLogger("infrahub_sdk").setLevel(logging.CRITICAL) @@ -179,3 +182,25 @@ def get_fixtures_dir() -> Path: """Get the directory which stores fixtures that are common to multiple unit/integration tests.""" here = Path(__file__).resolve().parent return here.parent.parent / "tests" / "fixtures" + + +def load_yamlfile_from_disk_and_exit( + paths: list[Path], file_type: type[YamlFileVar], console: Console +) -> list[YamlFileVar]: + has_error = False + try: + data_files = file_type.load_from_disk(paths=paths) + except FileNotValidError as exc: + console.print(f"[red]{exc.message}") + raise typer.Exit(1) from exc + + for data_file in data_files: + if data_file.valid and data_file.content: + continue + console.print(f"[red]{data_file.error_message} ({data_file.location})") + has_error = True + + if has_error: + raise typer.Exit(1) + + return data_files diff --git a/infrahub_sdk/protocols.py b/infrahub_sdk/protocols.py index fe8a2649..821757d1 100644 --- a/infrahub_sdk/protocols.py +++ b/infrahub_sdk/protocols.py @@ -143,6 +143,7 @@ class CoreMenu(CoreNode): namespace: String name: String label: StringOptional + kind: StringOptional path: StringOptional description: StringOptional icon: StringOptional @@ -607,6 +608,7 @@ class CoreMenuSync(CoreNodeSync): namespace: String name: String label: StringOptional + kind: StringOptional path: StringOptional description: StringOptional icon: StringOptional diff --git a/infrahub_sdk/spec/__init__.py b/infrahub_sdk/spec/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/infrahub_sdk/spec/menu.py b/infrahub_sdk/spec/menu.py new file mode 100644 index 00000000..0afaa395 --- /dev/null +++ b/infrahub_sdk/spec/menu.py @@ -0,0 +1,33 @@ +from typing import Optional + +from infrahub_sdk.yaml import InfrahubFile, InfrahubFileKind + +from .object import InfrahubObjectFileData + + +class InfrahubMenuFileData(InfrahubObjectFileData): + @classmethod + def enrich_node(cls, data: dict, context: dict) -> dict: + if "kind" in data and "path" not in data: + data["path"] = "/objects/" + data["kind"] + + if "list_index" in context and "order_weight" not in data: + data["order_weight"] = (context["list_index"] + 1) * 1000 + + return data + + +class MenuFile(InfrahubFile): + _spec: Optional[InfrahubMenuFileData] = None + + @property + def spec(self) -> InfrahubMenuFileData: + if not self._spec: + self._spec = InfrahubMenuFileData(**self.data.spec) + return self._spec + + def validate_content(self) -> None: + super().validate_content() + if self.kind != InfrahubFileKind.MENU: + raise ValueError("File is not an Infrahub Menu file") + self._spec = InfrahubMenuFileData(**self.data.spec) diff --git a/infrahub_sdk/spec/object.py b/infrahub_sdk/spec/object.py new file mode 100644 index 00000000..6ba6342b --- /dev/null +++ b/infrahub_sdk/spec/object.py @@ -0,0 +1,128 @@ +from typing import Any, Optional + +from pydantic import BaseModel, Field + +from infrahub_sdk.client import InfrahubClient +from infrahub_sdk.schema import MainSchemaTypes +from infrahub_sdk.yaml import InfrahubFile, InfrahubFileKind + + +class InfrahubObjectFileData(BaseModel): + kind: str | None = None + data: list[dict[str, Any]] = Field(default_factory=list) + + @classmethod + def enrich_node(cls, data: dict, context: dict) -> dict: + return data + + @classmethod + async def create_node( + cls, + client: InfrahubClient, + schema: MainSchemaTypes, + data: dict, + context: Optional[dict] = None, + branch: Optional[str] = None, + default_schema_kind: Optional[str] = None, + ) -> None: + # First validate of all mandatory fields are present + for element in schema.mandatory_attribute_names + schema.mandatory_relationship_names: + if element not in data.keys(): + raise ValueError(f"{element} is mandatory") + + clean_data: dict[str, Any] = {} + + remaining_rels = [] + for key, value in data.items(): + if key in schema.attribute_names: + # NOTE we could validate the format of the data but the API will do it as well + clean_data[key] = value + + if key in schema.relationship_names: + rel_schema = schema.get_relationship(name=key) + + if not isinstance(value, dict) and "data" in value: + raise ValueError(f"relationship {key} must be a dict with 'data'") + + if rel_schema.cardinality == "one" or rel_schema.optional is False: + raise ValueError( + "Not supported yet, we need to have a way to define connect object before they exist" + ) + # clean_data[key] = value[data] + remaining_rels.append(key) + + if context: + clean_context = { + ckey: cvalue + for ckey, cvalue in context.items() + if ckey in schema.relationship_names + schema.attribute_names + } + clean_data.update(clean_context) + + clean_data = cls.enrich_node(data=clean_data, context=context or {}) + + node = await client.create(kind=schema.kind, branch=branch, data=clean_data) + await node.save(allow_upsert=True) + display_label = node.get_human_friendly_id_as_string() or f"{node.get_kind()} : {node.id}" + client.log.info(f"Node: {display_label}") + + for rel in remaining_rels: + # identify what is the name of the relationship on the other side + if not isinstance(data[rel], dict) and "data" in data[rel]: + raise ValueError(f"relationship {rel} must be a dict with 'data'") + + rel_schema = schema.get_relationship(name=rel) + peer_kind = data[rel].get("kind", default_schema_kind) or rel_schema.peer + peer_schema = await client.schema.get(kind=peer_kind, branch=branch) + + if rel_schema.identifier is None: + raise ValueError("identifier must be defined") + + peer_rel = peer_schema.get_relationship_by_identifier(id=rel_schema.identifier) + + rel_data = data[rel]["data"] + context = {} + if peer_rel: + context[peer_rel.name] = node.id + + if rel_schema.cardinality == "one" and isinstance(rel_data, dict): + await cls.create_node( + client=client, + schema=peer_schema, + data=rel_data, + context=context, + branch=branch, + default_schema_kind=default_schema_kind, + ) + + elif rel_schema.cardinality == "many" and isinstance(rel_data, list): + for idx, peer_data in enumerate(rel_data): + context["list_index"] = idx + await cls.create_node( + client=client, + schema=peer_schema, + data=peer_data, + context=context, + branch=branch, + default_schema_kind=default_schema_kind, + ) + else: + raise ValueError( + f"Relationship {rel_schema.name} doesn't have the right format {rel_schema.cardinality} / {type(rel_data)}" + ) + + +class ObjectFile(InfrahubFile): + _spec: Optional[InfrahubObjectFileData] = None + + @property + def spec(self) -> InfrahubObjectFileData: + if not self._spec: + self._spec = InfrahubObjectFileData(**self.data.spec) + return self._spec + + def validate_content(self) -> None: + super().validate_content() + if self.kind != InfrahubFileKind.OBJECT: + raise ValueError("File is not an Infrahub Object file") + self._spec = InfrahubObjectFileData(**self.data.spec) diff --git a/infrahub_sdk/yaml.py b/infrahub_sdk/yaml.py index aa6da1a1..9abc9dc0 100644 --- a/infrahub_sdk/yaml.py +++ b/infrahub_sdk/yaml.py @@ -1,17 +1,40 @@ +from enum import Enum from pathlib import Path from typing import Optional import yaml -from pydantic import BaseModel +from pydantic import BaseModel, Field +from typing_extensions import Self +from infrahub_sdk.ctl.exceptions import FileNotValidError +from infrahub_sdk.utils import find_files -class SchemaFile(BaseModel): + +class InfrahubFileApiVersion(str, Enum): + V1 = "infrahub.app/v1" + + +class InfrahubFileKind(str, Enum): + MENU = "Menu" + OBJECT = "Object" + + +class InfrahubFileData(BaseModel): + api_version: InfrahubFileApiVersion = Field(InfrahubFileApiVersion.V1, alias="apiVersion") + kind: InfrahubFileKind + spec: dict + metadata: Optional[dict] = Field(default_factory=dict) + + +class LocalFile(BaseModel): identifier: Optional[str] = None location: Path content: Optional[dict] = None valid: bool = True error_message: Optional[str] = None + +class YamlFile(LocalFile): def load_content(self) -> None: try: self.content = yaml.safe_load(self.location.read_text()) @@ -23,3 +46,52 @@ def load_content(self) -> None: if not self.content: self.error_message = "Empty YAML/JSON file" self.valid = False + + def validate_content(self) -> None: + pass + + @classmethod + def load_from_disk(cls, paths: list[Path]) -> list[Self]: + yaml_files: list[Self] = [] + for file_path in paths: + if file_path.is_file(): + yaml_file = cls(location=file_path) + yaml_file.load_content() + yaml_files.append(yaml_file) + elif file_path.is_dir(): + files = find_files(extension=["yaml", "yml", "json"], directory=file_path) + for item in files: + yaml_file = cls(location=item) + yaml_file.load_content() + yaml_files.append(yaml_file) + else: + raise FileNotValidError(name=str(file_path), message=f"{file_path} does not exist!") + + return yaml_files + + +class InfrahubFile(YamlFile): + _data: Optional[InfrahubFileData] = None + + @property + def data(self) -> InfrahubFileData: + if not self._data: + raise ValueError("_data hasn't been initialized yet") + return self._data + + @property + def version(self) -> InfrahubFileApiVersion: + return self.data.api_version + + @property + def kind(self) -> InfrahubFileKind: + return self.data.kind + + def validate_content(self) -> None: + if not self.content: + raise ValueError("Content hasn't been loaded yet") + self._data = InfrahubFileData(**self.content) + + +class SchemaFile(YamlFile): + pass From 258b3ff6452394c0c4920e3a07fedb552a2ce7d5 Mon Sep 17 00:00:00 2001 From: Damien Garros Date: Sun, 13 Oct 2024 19:17:41 +0200 Subject: [PATCH 2/3] Replace pipe None with optional for Python 3.9 --- infrahub_sdk/spec/object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrahub_sdk/spec/object.py b/infrahub_sdk/spec/object.py index 6ba6342b..9d2115dc 100644 --- a/infrahub_sdk/spec/object.py +++ b/infrahub_sdk/spec/object.py @@ -8,7 +8,7 @@ class InfrahubObjectFileData(BaseModel): - kind: str | None = None + kind: Optional[str] = None data: list[dict[str, Any]] = Field(default_factory=list) @classmethod From cd83cde45da01865f7d67e9e382960b7956ace36 Mon Sep 17 00:00:00 2001 From: Damien Garros Date: Mon, 14 Oct 2024 21:51:36 +0200 Subject: [PATCH 3/3] Improve support for relationship & cleanup --- infrahub_sdk/ctl/menu.py | 7 +------ infrahub_sdk/ctl/object.py | 6 +++--- infrahub_sdk/spec/menu.py | 2 ++ infrahub_sdk/spec/object.py | 20 ++++++++++---------- 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/infrahub_sdk/ctl/menu.py b/infrahub_sdk/ctl/menu.py index 8c66694d..fd0edc30 100644 --- a/infrahub_sdk/ctl/menu.py +++ b/infrahub_sdk/ctl/menu.py @@ -40,13 +40,8 @@ async def load( files = load_yamlfile_from_disk_and_exit(paths=menus, file_type=MenuFile, console=console) client = await initialize_client() - default_kind = "CoreMenuItem" - for file in files: file.validate_content() - if not file.spec.kind: - file.spec.kind = default_kind - schema = await client.schema.get(kind=file.spec.kind, branch=branch) for idx, item in enumerate(file.spec.data): @@ -55,6 +50,6 @@ async def load( schema=schema, data=item, branch=branch, - default_schema_kind=default_kind, + default_schema_kind=file.spec.kind, context={"list_index": idx}, ) diff --git a/infrahub_sdk/ctl/object.py b/infrahub_sdk/ctl/object.py index 34141f0f..1e43b356 100644 --- a/infrahub_sdk/ctl/object.py +++ b/infrahub_sdk/ctl/object.py @@ -1,3 +1,4 @@ +import logging from pathlib import Path import typer @@ -5,7 +6,6 @@ from infrahub_sdk.async_typer import AsyncTyper from infrahub_sdk.ctl.client import initialize_client -from infrahub_sdk.ctl.exceptions import FileNotValidError from infrahub_sdk.ctl.utils import catch_exception, init_logging from infrahub_sdk.spec.object import ObjectFile @@ -35,13 +35,13 @@ async def load( init_logging(debug=debug) + logging.getLogger("infrahub_sdk").setLevel(logging.INFO) + files = load_yamlfile_from_disk_and_exit(paths=paths, file_type=ObjectFile, console=console) client = await initialize_client() for file in files: file.validate_content() - if not file.spec.kind: - raise FileNotValidError(name=str(file.location), message="kind must be specified.") schema = await client.schema.get(kind=file.spec.kind, branch=branch) for item in file.spec.data: diff --git a/infrahub_sdk/spec/menu.py b/infrahub_sdk/spec/menu.py index 0afaa395..bd1433ab 100644 --- a/infrahub_sdk/spec/menu.py +++ b/infrahub_sdk/spec/menu.py @@ -6,6 +6,8 @@ class InfrahubMenuFileData(InfrahubObjectFileData): + kind: str = "CoreMenuItem" + @classmethod def enrich_node(cls, data: dict, context: dict) -> dict: if "kind" in data and "path" not in data: diff --git a/infrahub_sdk/spec/object.py b/infrahub_sdk/spec/object.py index 9d2115dc..05bde271 100644 --- a/infrahub_sdk/spec/object.py +++ b/infrahub_sdk/spec/object.py @@ -8,7 +8,7 @@ class InfrahubObjectFileData(BaseModel): - kind: Optional[str] = None + kind: str data: list[dict[str, Any]] = Field(default_factory=list) @classmethod @@ -35,21 +35,21 @@ async def create_node( remaining_rels = [] for key, value in data.items(): if key in schema.attribute_names: - # NOTE we could validate the format of the data but the API will do it as well clean_data[key] = value if key in schema.relationship_names: rel_schema = schema.get_relationship(name=key) - if not isinstance(value, dict) and "data" in value: - raise ValueError(f"relationship {key} must be a dict with 'data'") + if isinstance(value, dict) and "data" not in value: + raise ValueError(f"Relationship {key} must be a dict with 'data'") - if rel_schema.cardinality == "one" or rel_schema.optional is False: - raise ValueError( - "Not supported yet, we need to have a way to define connect object before they exist" - ) - # clean_data[key] = value[data] - remaining_rels.append(key) + # This is a simple implementation for now, need to revisit once we have the integration tests + if isinstance(value, (list)): + clean_data[key] = value + elif rel_schema.cardinality == "one" and isinstance(value, str): + clean_data[key] = [value] + else: + remaining_rels.append(key) if context: clean_context = {