Skip to content

Commit 1588fe7

Browse files
committed
Add menu and object cli commands
1 parent 2b48f61 commit 1588fe7

File tree

6 files changed

+256
-1
lines changed

6 files changed

+256
-1
lines changed

infrahub_sdk/ctl/cli_commands.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from infrahub_sdk.ctl.client import initialize_client, initialize_client_sync
2323
from infrahub_sdk.ctl.exceptions import QueryNotFoundError
2424
from infrahub_sdk.ctl.generator import run as run_generator
25+
from infrahub_sdk.ctl.menu import app as menu_app
26+
from infrahub_sdk.ctl.object import app as object_app
2527
from infrahub_sdk.ctl.render import list_jinja2_transforms
2628
from infrahub_sdk.ctl.repository import app as repository_app
2729
from infrahub_sdk.ctl.repository import get_repository_config
@@ -50,14 +52,17 @@
5052
app.add_typer(schema_app, name="schema")
5153
app.add_typer(validate_app, name="validate")
5254
app.add_typer(repository_app, name="repository")
55+
app.add_typer(menu_app, name="menu")
56+
app.add_typer(object_app, name="object")
57+
5358
app.command(name="dump")(dump)
5459
app.command(name="load")(load)
5560

5661
console = Console()
5762

5863

5964
@app.command(name="check")
60-
@catch_exception(console=console)
65+
# @catch_exception(console=console)
6166
def check(
6267
check_name: str = typer.Argument(default="", help="Name of the Python check"),
6368
branch: Optional[str] = None,

infrahub_sdk/ctl/menu.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from pathlib import Path
2+
3+
import typer
4+
from rich.console import Console
5+
6+
from infrahub_sdk.async_typer import AsyncTyper
7+
from infrahub_sdk.ctl.client import initialize_client
8+
from infrahub_sdk.ctl.utils import catch_exception, init_logging
9+
from infrahub_sdk.spec.menu import MenuFile
10+
11+
from .parameters import CONFIG_PARAM
12+
from .utils import load_yamlfile_from_disk_and_exit
13+
14+
app = AsyncTyper()
15+
console = Console()
16+
17+
18+
@app.callback()
19+
def callback() -> None:
20+
"""
21+
Manage the menu in a remote Infrahub instance.
22+
"""
23+
24+
25+
@app.command()
26+
@catch_exception(console=console)
27+
async def load(
28+
menus: list[Path],
29+
debug: bool = False,
30+
branch: str = typer.Option("main", help="Branch on which to load the menu."),
31+
_: str = CONFIG_PARAM,
32+
) -> None:
33+
"""Load one or multiple menu files into Infrahub."""
34+
35+
init_logging(debug=debug)
36+
37+
files = load_yamlfile_from_disk_and_exit(paths=menus, file_type=MenuFile, console=console)
38+
client = await initialize_client()
39+
40+
default_kind = "CoreMenuItem"
41+
42+
for file in files:
43+
file.validate_content()
44+
if not file.spec.kind:
45+
file.spec.kind = default_kind
46+
47+
schema = await client.schema.get(kind=file.spec.kind, branch=branch)
48+
49+
for item in file.spec.data:
50+
await file.spec.create_node(
51+
client=client, schema=schema, data=item, branch=branch, default_schema_kind=default_kind
52+
)

infrahub_sdk/ctl/object.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from pathlib import Path
2+
3+
import typer
4+
from rich.console import Console
5+
6+
from infrahub_sdk.async_typer import AsyncTyper
7+
from infrahub_sdk.ctl.client import initialize_client
8+
from infrahub_sdk.ctl.exceptions import FileNotValidError
9+
from infrahub_sdk.ctl.utils import catch_exception, init_logging
10+
from infrahub_sdk.spec.object import ObjectFile
11+
12+
from .parameters import CONFIG_PARAM
13+
from .utils import load_yamlfile_from_disk_and_exit
14+
15+
app = AsyncTyper()
16+
console = Console()
17+
18+
19+
@app.callback()
20+
def callback() -> None:
21+
"""
22+
Manage objects in a remote Infrahub instance.
23+
"""
24+
25+
26+
@app.command()
27+
@catch_exception(console=console)
28+
async def load(
29+
paths: list[Path],
30+
debug: bool = False,
31+
branch: str = typer.Option("main", help="Branch on which to load the objects."),
32+
_: str = CONFIG_PARAM,
33+
) -> None:
34+
"""Load one or multiple objects files into Infrahub."""
35+
36+
init_logging(debug=debug)
37+
38+
files = load_yamlfile_from_disk_and_exit(paths=paths, file_type=ObjectFile, console=console)
39+
client = await initialize_client()
40+
41+
for file in files:
42+
file.validate_content()
43+
if not file.spec.kind:
44+
raise FileNotValidError(name=str(file.location), message="kind must be specified.")
45+
schema = await client.schema.get(kind=file.spec.kind, branch=branch)
46+
47+
for item in file.spec.data:
48+
await file.spec.create_node(client=client, schema=schema, data=item, branch=branch)

infrahub_sdk/spec/__init__.py

Whitespace-only changes.

infrahub_sdk/spec/menu.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from typing import Optional
2+
3+
from infrahub_sdk.yaml import InfrahubFile, InfrahubFileKind
4+
5+
from .object import InfrahubObjectFileData
6+
7+
8+
class InfrahubMenuFileData(InfrahubObjectFileData):
9+
@classmethod
10+
def enrich_node(cls, data: dict) -> dict:
11+
if "kind" in data and "path" not in data:
12+
data["path"] = "/objects/" + data["kind"]
13+
return data
14+
15+
16+
class MenuFile(InfrahubFile):
17+
_spec: Optional[InfrahubMenuFileData] = None
18+
19+
@property
20+
def spec(self) -> InfrahubMenuFileData:
21+
if not self._spec:
22+
self._spec = InfrahubMenuFileData(**self.data.spec)
23+
return self._spec
24+
25+
def validate_content(self) -> None:
26+
super().validate_content()
27+
if self.kind != InfrahubFileKind.MENU:
28+
raise ValueError("File is not an Infrahub Menu file")
29+
self._spec = InfrahubMenuFileData(**self.data.spec)

infrahub_sdk/spec/object.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
from typing import Any, Optional
2+
3+
from pydantic import BaseModel, Field
4+
5+
from infrahub_sdk.client import InfrahubClient
6+
from infrahub_sdk.schema import MainSchemaTypes
7+
from infrahub_sdk.yaml import InfrahubFile, InfrahubFileKind
8+
9+
10+
class InfrahubObjectFileData(BaseModel):
11+
kind: str | None = None
12+
data: list[dict[str, Any]] = Field(default_factory=list)
13+
14+
@classmethod
15+
def enrich_node(cls, data: dict) -> dict:
16+
return data
17+
18+
@classmethod
19+
async def create_node(
20+
cls,
21+
client: InfrahubClient,
22+
schema: MainSchemaTypes,
23+
data: dict,
24+
context: Optional[dict] = None,
25+
branch: Optional[str] = None,
26+
default_schema_kind: Optional[str] = None,
27+
) -> None:
28+
# First validate of all mandatory fields are present
29+
for element in schema.mandatory_attribute_names + schema.mandatory_relationship_names:
30+
if element not in data.keys():
31+
raise ValueError(f"{element} is mandatory")
32+
33+
clean_data: dict[str, Any] = {}
34+
35+
remaining_rels = []
36+
for key, value in data.items():
37+
if key in schema.attribute_names:
38+
# NOTE we could validate the format of the data but the API will do it as well
39+
clean_data[key] = value
40+
41+
if key in schema.relationship_names:
42+
rel_schema = schema.get_relationship(name=key)
43+
44+
if not isinstance(value, dict) and "data" in value:
45+
raise ValueError(f"relationship {key} must be a dict with 'data'")
46+
47+
if rel_schema.cardinality == "one" or rel_schema.optional is False:
48+
raise ValueError(
49+
"Not supported yet, we need to have a way to define connect object before they exist"
50+
)
51+
# clean_data[key] = value[data]
52+
remaining_rels.append(key)
53+
54+
if context:
55+
clean_data.update(context)
56+
57+
clean_data = cls.enrich_node(data=clean_data)
58+
59+
node = await client.create(kind=schema.kind, branch=branch, data=clean_data)
60+
await node.save(allow_upsert=True)
61+
print(f"Created Node: {node.get_human_friendly_id_as_string()}")
62+
63+
for rel in remaining_rels:
64+
# identify what is the name of the relationship on the other side
65+
if not isinstance(data[rel], dict) and "data" in data[rel]:
66+
raise ValueError(f"relationship {rel} must be a dict with 'data'")
67+
68+
rel_schema = schema.get_relationship(name=rel)
69+
peer_kind = data[rel].get("kind", default_schema_kind) or rel_schema.peer
70+
peer_schema = await client.schema.get(kind=peer_kind, branch=branch)
71+
72+
if rel_schema.identifier is None:
73+
raise ValueError("identifier must be defined")
74+
75+
peer_rel = peer_schema.get_relationship_by_identifier(id=rel_schema.identifier)
76+
77+
rel_data = data[rel]["data"]
78+
context = {}
79+
if peer_rel:
80+
context[peer_rel.name] = node.id
81+
82+
if rel_schema.cardinality == "one" and isinstance(rel_data, dict):
83+
await cls.create_node(
84+
client=client,
85+
schema=peer_schema,
86+
data=rel_data,
87+
context=context,
88+
branch=branch,
89+
default_schema_kind=default_schema_kind,
90+
)
91+
92+
elif rel_schema.cardinality == "many" and isinstance(rel_data, list):
93+
for peer_data in rel_data:
94+
await cls.create_node(
95+
client=client,
96+
schema=peer_schema,
97+
data=peer_data,
98+
context=context,
99+
branch=branch,
100+
default_schema_kind=default_schema_kind,
101+
)
102+
else:
103+
raise ValueError(
104+
f"Relationship {rel_schema.name} doesn't have the right format {rel_schema.cardinality} / {type(rel_data)}"
105+
)
106+
107+
108+
class ObjectFile(InfrahubFile):
109+
_spec: Optional[InfrahubObjectFileData] = None
110+
111+
@property
112+
def spec(self) -> InfrahubObjectFileData:
113+
if not self._spec:
114+
self._spec = InfrahubObjectFileData(**self.data.spec)
115+
return self._spec
116+
117+
def validate_content(self) -> None:
118+
super().validate_content()
119+
if self.kind != InfrahubFileKind.OBJECT:
120+
raise ValueError("File is not an Infrahub Object file")
121+
self._spec = InfrahubObjectFileData(**self.data.spec)

0 commit comments

Comments
 (0)