infrahub-server 1.6.0__py3-none-any.whl → 1.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- infrahub/api/oauth2.py +33 -6
- infrahub/api/oidc.py +36 -6
- infrahub/auth.py +11 -0
- infrahub/auth_pkce.py +41 -0
- infrahub/config.py +8 -2
- infrahub/core/branch/models.py +3 -2
- infrahub/core/changelog/models.py +2 -2
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/integrity/object_conflict/conflict_recorder.py +1 -1
- infrahub/core/migrations/graph/__init__.py +2 -0
- infrahub/core/migrations/graph/m047_backfill_or_null_display_label.py +606 -0
- infrahub/core/node/__init__.py +5 -8
- infrahub/core/node/proposed_change.py +5 -3
- infrahub/core/relationship/model.py +9 -3
- infrahub/core/schema/manager.py +8 -3
- infrahub/core/validators/attribute/choices.py +2 -2
- infrahub/git/models.py +13 -0
- infrahub/git/tasks.py +23 -19
- infrahub/git/utils.py +16 -9
- infrahub/graphql/app.py +6 -6
- infrahub/graphql/mutations/action.py +15 -7
- infrahub/graphql/mutations/hfid.py +1 -1
- infrahub/graphql/mutations/repository.py +3 -3
- infrahub/graphql/mutations/schema.py +4 -4
- infrahub/graphql/mutations/webhook.py +2 -2
- infrahub/proposed_change/branch_diff.py +1 -1
- infrahub/repositories/create_repository.py +3 -3
- infrahub/task_manager/models.py +1 -1
- infrahub/task_manager/task.py +3 -3
- infrahub/validators/tasks.py +1 -1
- infrahub_sdk/ctl/AGENTS.md +67 -0
- infrahub_sdk/ctl/repository.py +4 -46
- infrahub_sdk/node/constants.py +2 -0
- infrahub_sdk/node/node.py +303 -3
- infrahub_sdk/pytest_plugin/AGENTS.md +67 -0
- infrahub_sdk/timestamp.py +7 -7
- {infrahub_server-1.6.0.dist-info → infrahub_server-1.6.1.dist-info}/METADATA +2 -3
- {infrahub_server-1.6.0.dist-info → infrahub_server-1.6.1.dist-info}/RECORD +41 -37
- {infrahub_server-1.6.0.dist-info → infrahub_server-1.6.1.dist-info}/WHEEL +0 -0
- {infrahub_server-1.6.0.dist-info → infrahub_server-1.6.1.dist-info}/entry_points.txt +0 -0
- {infrahub_server-1.6.0.dist-info → infrahub_server-1.6.1.dist-info}/licenses/LICENSE.txt +0 -0
infrahub_sdk/ctl/repository.py
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
|
|
6
5
|
import typer
|
|
7
6
|
import yaml
|
|
8
|
-
from copier import run_copy
|
|
9
7
|
from pydantic import ValidationError
|
|
10
8
|
from rich.console import Console
|
|
11
9
|
from rich.table import Table
|
|
@@ -207,49 +205,9 @@ async def list(
|
|
|
207
205
|
|
|
208
206
|
|
|
209
207
|
@app.command()
|
|
210
|
-
async def init(
|
|
211
|
-
directory: Path = typer.Argument(help="Directory path for the new project."),
|
|
212
|
-
template: str = typer.Option(
|
|
213
|
-
default="https://github.com/opsmill/infrahub-template.git",
|
|
214
|
-
help="Template to use for the new repository. Can be a local path or a git repository URL.",
|
|
215
|
-
),
|
|
216
|
-
data: Path | None = typer.Option(default=None, help="Path to YAML file containing answers to CLI prompt."),
|
|
217
|
-
vcs_ref: str | None = typer.Option(
|
|
218
|
-
default="HEAD",
|
|
219
|
-
help="VCS reference to use for the template. Defaults to HEAD.",
|
|
220
|
-
),
|
|
221
|
-
trust: bool | None = typer.Option(
|
|
222
|
-
default=False,
|
|
223
|
-
help="Trust the template repository. If set, the template will be cloned without verification.",
|
|
224
|
-
),
|
|
225
|
-
_: str = CONFIG_PARAM,
|
|
226
|
-
) -> None:
|
|
208
|
+
async def init() -> None:
|
|
227
209
|
"""Initialize a new Infrahub repository."""
|
|
228
210
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
with Path.open(data, encoding="utf-8") as file:
|
|
233
|
-
config_data = yaml.safe_load(file)
|
|
234
|
-
typer.echo(f"Loaded config: {config_data}")
|
|
235
|
-
except Exception as exc:
|
|
236
|
-
typer.echo(f"Error loading YAML file: {exc}", err=True)
|
|
237
|
-
raise typer.Exit(code=1)
|
|
238
|
-
|
|
239
|
-
# Allow template to be a local path or a URL
|
|
240
|
-
template_source = template or ""
|
|
241
|
-
if template and Path(template).exists():
|
|
242
|
-
template_source = str(Path(template).resolve())
|
|
243
|
-
|
|
244
|
-
try:
|
|
245
|
-
await asyncio.to_thread(
|
|
246
|
-
run_copy,
|
|
247
|
-
template_source,
|
|
248
|
-
str(directory),
|
|
249
|
-
data=config_data,
|
|
250
|
-
vcs_ref=vcs_ref,
|
|
251
|
-
unsafe=trust,
|
|
252
|
-
)
|
|
253
|
-
except Exception as e:
|
|
254
|
-
typer.echo(f"Error running copier: {e}", err=True)
|
|
255
|
-
raise typer.Exit(code=1)
|
|
211
|
+
console.print("The copier tool is not included in the Infrahub SDK CLI due to license restrictions,")
|
|
212
|
+
console.print("please run the following command to create a new Infrahub repository project:\n")
|
|
213
|
+
console.print("uv tool run --from 'copier' copier copy https://github.com/opsmill/infrahub-template <project-name>")
|
infrahub_sdk/node/constants.py
CHANGED
|
@@ -17,4 +17,6 @@ ARTIFACT_DEFINITION_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE = (
|
|
|
17
17
|
"calling generate is only supported for CoreArtifactDefinition nodes"
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
+
HIERARCHY_FETCH_FEATURE_NOT_SUPPORTED_MESSAGE = "Hierarchical fields are not supported for this node."
|
|
21
|
+
|
|
20
22
|
HFID_STR_SEPARATOR = "__"
|
infrahub_sdk/node/node.py
CHANGED
|
@@ -7,7 +7,14 @@ from typing import TYPE_CHECKING, Any
|
|
|
7
7
|
from ..constants import InfrahubClientMode
|
|
8
8
|
from ..exceptions import FeatureNotSupportedError, NodeNotFoundError, ResourceNotDefinedError, SchemaNotFoundError
|
|
9
9
|
from ..graphql import Mutation, Query
|
|
10
|
-
from ..schema import
|
|
10
|
+
from ..schema import (
|
|
11
|
+
GenericSchemaAPI,
|
|
12
|
+
ProfileSchemaAPI,
|
|
13
|
+
RelationshipCardinality,
|
|
14
|
+
RelationshipKind,
|
|
15
|
+
RelationshipSchemaAPI,
|
|
16
|
+
TemplateSchemaAPI,
|
|
17
|
+
)
|
|
11
18
|
from ..utils import compare_lists, generate_short_id
|
|
12
19
|
from .attribute import Attribute
|
|
13
20
|
from .constants import (
|
|
@@ -56,9 +63,20 @@ class InfrahubNodeBase:
|
|
|
56
63
|
self._attributes = [item.name for item in self._schema.attributes]
|
|
57
64
|
self._relationships = [item.name for item in self._schema.relationships]
|
|
58
65
|
|
|
59
|
-
|
|
66
|
+
# GenericSchemaAPI doesn't have inherit_from, so we need to check the type first
|
|
67
|
+
if isinstance(schema, GenericSchemaAPI):
|
|
68
|
+
self._artifact_support = False
|
|
69
|
+
else:
|
|
70
|
+
inherit_from = getattr(schema, "inherit_from", None) or []
|
|
71
|
+
self._artifact_support = "CoreArtifactTarget" in inherit_from
|
|
60
72
|
self._artifact_definition_support = schema.kind == "CoreArtifactDefinition"
|
|
61
73
|
|
|
74
|
+
# Check if this node is hierarchical (supports parent/children and ancestors/descendants)
|
|
75
|
+
if not isinstance(schema, (ProfileSchemaAPI, GenericSchemaAPI, TemplateSchemaAPI)):
|
|
76
|
+
self._hierarchy_support = getattr(schema, "hierarchy", None) is not None
|
|
77
|
+
else:
|
|
78
|
+
self._hierarchy_support = False
|
|
79
|
+
|
|
62
80
|
if not self.id:
|
|
63
81
|
self._existing = False
|
|
64
82
|
|
|
@@ -384,6 +402,10 @@ class InfrahubNodeBase:
|
|
|
384
402
|
if not self._artifact_support:
|
|
385
403
|
raise FeatureNotSupportedError(message)
|
|
386
404
|
|
|
405
|
+
def _validate_hierarchy_support(self, message: str) -> None:
|
|
406
|
+
if not self._hierarchy_support:
|
|
407
|
+
raise FeatureNotSupportedError(message)
|
|
408
|
+
|
|
387
409
|
def _validate_artifact_definition_support(self, message: str) -> None:
|
|
388
410
|
if not self._artifact_definition_support:
|
|
389
411
|
raise FeatureNotSupportedError(message)
|
|
@@ -479,6 +501,7 @@ class InfrahubNode(InfrahubNodeBase):
|
|
|
479
501
|
|
|
480
502
|
self._relationship_cardinality_many_data: dict[str, RelationshipManager] = {}
|
|
481
503
|
self._relationship_cardinality_one_data: dict[str, RelatedNode] = {}
|
|
504
|
+
self._hierarchical_data: dict[str, RelatedNode | RelationshipManager] = {}
|
|
482
505
|
|
|
483
506
|
super().__init__(schema=schema, branch=branch or client.default_branch, data=data)
|
|
484
507
|
|
|
@@ -532,6 +555,75 @@ class InfrahubNode(InfrahubNodeBase):
|
|
|
532
555
|
schema=rel_schema,
|
|
533
556
|
data=rel_data,
|
|
534
557
|
)
|
|
558
|
+
# Initialize parent, children, ancestors and descendants for hierarchical nodes
|
|
559
|
+
if self._hierarchy_support:
|
|
560
|
+
# Create pseudo-schema for parent (cardinality one)
|
|
561
|
+
parent_schema = RelationshipSchemaAPI(
|
|
562
|
+
name="parent",
|
|
563
|
+
peer=self._schema.hierarchy, # type: ignore[union-attr, arg-type]
|
|
564
|
+
kind=RelationshipKind.HIERARCHY,
|
|
565
|
+
cardinality="one",
|
|
566
|
+
optional=True,
|
|
567
|
+
)
|
|
568
|
+
parent_data = data.get("parent", None) if isinstance(data, dict) else None
|
|
569
|
+
self._hierarchical_data["parent"] = RelatedNode(
|
|
570
|
+
name="parent",
|
|
571
|
+
client=self._client,
|
|
572
|
+
branch=self._branch,
|
|
573
|
+
schema=parent_schema,
|
|
574
|
+
data=parent_data,
|
|
575
|
+
)
|
|
576
|
+
# Create pseudo-schema for children (many cardinality)
|
|
577
|
+
children_schema = RelationshipSchemaAPI(
|
|
578
|
+
name="children",
|
|
579
|
+
peer=self._schema.hierarchy, # type: ignore[union-attr, arg-type]
|
|
580
|
+
kind=RelationshipKind.HIERARCHY,
|
|
581
|
+
cardinality="many",
|
|
582
|
+
optional=True,
|
|
583
|
+
)
|
|
584
|
+
children_data = data.get("children", None) if isinstance(data, dict) else None
|
|
585
|
+
self._hierarchical_data["children"] = RelationshipManager(
|
|
586
|
+
name="children",
|
|
587
|
+
client=self._client,
|
|
588
|
+
node=self,
|
|
589
|
+
branch=self._branch,
|
|
590
|
+
schema=children_schema,
|
|
591
|
+
data=children_data,
|
|
592
|
+
)
|
|
593
|
+
# Create pseudo-schema for ancestors (read-only, many cardinality)
|
|
594
|
+
ancestors_schema = RelationshipSchemaAPI(
|
|
595
|
+
name="ancestors",
|
|
596
|
+
peer=self._schema.hierarchy, # type: ignore[union-attr, arg-type]
|
|
597
|
+
cardinality="many",
|
|
598
|
+
read_only=True,
|
|
599
|
+
optional=True,
|
|
600
|
+
)
|
|
601
|
+
ancestors_data = data.get("ancestors", None) if isinstance(data, dict) else None
|
|
602
|
+
self._hierarchical_data["ancestors"] = RelationshipManager(
|
|
603
|
+
name="ancestors",
|
|
604
|
+
client=self._client,
|
|
605
|
+
node=self,
|
|
606
|
+
branch=self._branch,
|
|
607
|
+
schema=ancestors_schema,
|
|
608
|
+
data=ancestors_data,
|
|
609
|
+
)
|
|
610
|
+
# Create pseudo-schema for descendants (read-only, many cardinality)
|
|
611
|
+
descendants_schema = RelationshipSchemaAPI(
|
|
612
|
+
name="descendants",
|
|
613
|
+
peer=self._schema.hierarchy, # type: ignore[union-attr, arg-type]
|
|
614
|
+
cardinality="many",
|
|
615
|
+
read_only=True,
|
|
616
|
+
optional=True,
|
|
617
|
+
)
|
|
618
|
+
descendants_data = data.get("descendants", None) if isinstance(data, dict) else None
|
|
619
|
+
self._hierarchical_data["descendants"] = RelationshipManager(
|
|
620
|
+
name="descendants",
|
|
621
|
+
client=self._client,
|
|
622
|
+
node=self,
|
|
623
|
+
branch=self._branch,
|
|
624
|
+
schema=descendants_schema,
|
|
625
|
+
data=descendants_data,
|
|
626
|
+
)
|
|
535
627
|
|
|
536
628
|
def __getattr__(self, name: str) -> Attribute | RelationshipManager | RelatedNode:
|
|
537
629
|
if "_attribute_data" in self.__dict__ and name in self._attribute_data:
|
|
@@ -540,6 +632,8 @@ class InfrahubNode(InfrahubNodeBase):
|
|
|
540
632
|
return self._relationship_cardinality_many_data[name]
|
|
541
633
|
if "_relationship_cardinality_one_data" in self.__dict__ and name in self._relationship_cardinality_one_data:
|
|
542
634
|
return self._relationship_cardinality_one_data[name]
|
|
635
|
+
if "_hierarchical_data" in self.__dict__ and name in self._hierarchical_data:
|
|
636
|
+
return self._hierarchical_data[name]
|
|
543
637
|
|
|
544
638
|
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
|
|
545
639
|
|
|
@@ -628,6 +722,57 @@ class InfrahubNode(InfrahubNodeBase):
|
|
|
628
722
|
|
|
629
723
|
self._client.store.set(node=self)
|
|
630
724
|
|
|
725
|
+
async def _process_hierarchical_fields(
|
|
726
|
+
self,
|
|
727
|
+
data: dict[str, Any],
|
|
728
|
+
include: list[str] | None = None,
|
|
729
|
+
exclude: list[str] | None = None,
|
|
730
|
+
prefetch_relationships: bool = False,
|
|
731
|
+
insert_alias: bool = False,
|
|
732
|
+
property: bool = False,
|
|
733
|
+
) -> None:
|
|
734
|
+
"""Process hierarchical fields (parent, children, ancestors, descendants) for hierarchical nodes."""
|
|
735
|
+
if not self._hierarchy_support:
|
|
736
|
+
return
|
|
737
|
+
|
|
738
|
+
for hierarchical_name in ["parent", "children", "ancestors", "descendants"]:
|
|
739
|
+
if exclude and hierarchical_name in exclude:
|
|
740
|
+
continue
|
|
741
|
+
|
|
742
|
+
# Only include if explicitly requested or if prefetch_relationships is True
|
|
743
|
+
should_fetch = prefetch_relationships or (include is not None and hierarchical_name in include)
|
|
744
|
+
if not should_fetch:
|
|
745
|
+
continue
|
|
746
|
+
|
|
747
|
+
peer_schema = await self._client.schema.get(kind=self._schema.hierarchy, branch=self._branch) # type: ignore[union-attr, arg-type]
|
|
748
|
+
peer_node = InfrahubNode(client=self._client, schema=peer_schema, branch=self._branch)
|
|
749
|
+
# Exclude hierarchical fields from peer data to prevent infinite recursion
|
|
750
|
+
peer_exclude = list(exclude) if exclude else []
|
|
751
|
+
peer_exclude.extend(["parent", "children", "ancestors", "descendants"])
|
|
752
|
+
peer_data = await peer_node.generate_query_data_node(
|
|
753
|
+
exclude=peer_exclude,
|
|
754
|
+
property=property,
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
# Parent is cardinality one, others are cardinality many
|
|
758
|
+
if hierarchical_name == "parent":
|
|
759
|
+
hierarchical_data = RelatedNode._generate_query_data(peer_data=peer_data, property=property)
|
|
760
|
+
# Use fragment for hierarchical fields similar to hierarchy relationships
|
|
761
|
+
data_node = hierarchical_data["node"]
|
|
762
|
+
hierarchical_data["node"] = {}
|
|
763
|
+
hierarchical_data["node"][f"...on {self._schema.hierarchy}"] = data_node # type: ignore[union-attr]
|
|
764
|
+
else:
|
|
765
|
+
hierarchical_data = RelationshipManager._generate_query_data(peer_data=peer_data, property=property)
|
|
766
|
+
# Use fragment for hierarchical fields similar to hierarchy relationships
|
|
767
|
+
data_node = hierarchical_data["edges"]["node"]
|
|
768
|
+
hierarchical_data["edges"]["node"] = {}
|
|
769
|
+
hierarchical_data["edges"]["node"][f"...on {self._schema.hierarchy}"] = data_node # type: ignore[union-attr]
|
|
770
|
+
|
|
771
|
+
data[hierarchical_name] = hierarchical_data
|
|
772
|
+
|
|
773
|
+
if insert_alias:
|
|
774
|
+
data[hierarchical_name]["@alias"] = f"__alias__{self._schema.kind}__{hierarchical_name}"
|
|
775
|
+
|
|
631
776
|
async def generate_query_data(
|
|
632
777
|
self,
|
|
633
778
|
filters: dict[str, Any] | None = None,
|
|
@@ -755,6 +900,7 @@ class InfrahubNode(InfrahubNodeBase):
|
|
|
755
900
|
property=property,
|
|
756
901
|
)
|
|
757
902
|
|
|
903
|
+
rel_data: dict[str, Any]
|
|
758
904
|
if rel_schema and rel_schema.cardinality == "one":
|
|
759
905
|
rel_data = RelatedNode._generate_query_data(peer_data=peer_data, property=property)
|
|
760
906
|
# Nodes involved in a hierarchy are required to inherit from a common ancestor node, and graphql
|
|
@@ -767,12 +913,24 @@ class InfrahubNode(InfrahubNodeBase):
|
|
|
767
913
|
rel_data["node"][f"...on {rel_schema.peer}"] = data_node
|
|
768
914
|
elif rel_schema and rel_schema.cardinality == "many":
|
|
769
915
|
rel_data = RelationshipManager._generate_query_data(peer_data=peer_data, property=property)
|
|
916
|
+
else:
|
|
917
|
+
continue
|
|
770
918
|
|
|
771
919
|
data[rel_name] = rel_data
|
|
772
920
|
|
|
773
921
|
if insert_alias:
|
|
774
922
|
data[rel_name]["@alias"] = f"__alias__{self._schema.kind}__{rel_name}"
|
|
775
923
|
|
|
924
|
+
# Add parent, children, ancestors and descendants for hierarchical nodes
|
|
925
|
+
await self._process_hierarchical_fields(
|
|
926
|
+
data=data,
|
|
927
|
+
include=include,
|
|
928
|
+
exclude=exclude,
|
|
929
|
+
prefetch_relationships=prefetch_relationships,
|
|
930
|
+
insert_alias=insert_alias,
|
|
931
|
+
property=property,
|
|
932
|
+
)
|
|
933
|
+
|
|
776
934
|
return data
|
|
777
935
|
|
|
778
936
|
async def add_relationships(self, relation_to_update: str, related_nodes: list[str]) -> None:
|
|
@@ -1132,6 +1290,7 @@ class InfrahubNodeSync(InfrahubNodeBase):
|
|
|
1132
1290
|
|
|
1133
1291
|
self._relationship_cardinality_many_data: dict[str, RelationshipManagerSync] = {}
|
|
1134
1292
|
self._relationship_cardinality_one_data: dict[str, RelatedNodeSync] = {}
|
|
1293
|
+
self._hierarchical_data: dict[str, RelatedNodeSync | RelationshipManagerSync] = {}
|
|
1135
1294
|
|
|
1136
1295
|
super().__init__(schema=schema, branch=branch or client.default_branch, data=data)
|
|
1137
1296
|
|
|
@@ -1186,6 +1345,79 @@ class InfrahubNodeSync(InfrahubNodeBase):
|
|
|
1186
1345
|
data=rel_data,
|
|
1187
1346
|
)
|
|
1188
1347
|
|
|
1348
|
+
# Initialize parent, children, ancestors and descendants for hierarchical nodes
|
|
1349
|
+
if self._hierarchy_support:
|
|
1350
|
+
# Create pseudo-schema for parent (cardinality one)
|
|
1351
|
+
parent_schema = RelationshipSchemaAPI(
|
|
1352
|
+
name="parent",
|
|
1353
|
+
peer=self._schema.hierarchy, # type: ignore[union-attr, arg-type]
|
|
1354
|
+
kind=RelationshipKind.HIERARCHY,
|
|
1355
|
+
cardinality="one",
|
|
1356
|
+
optional=True,
|
|
1357
|
+
)
|
|
1358
|
+
parent_data = data.get("parent", None) if isinstance(data, dict) else None
|
|
1359
|
+
self._hierarchical_data["parent"] = RelatedNodeSync(
|
|
1360
|
+
name="parent",
|
|
1361
|
+
client=self._client,
|
|
1362
|
+
branch=self._branch,
|
|
1363
|
+
schema=parent_schema,
|
|
1364
|
+
data=parent_data,
|
|
1365
|
+
)
|
|
1366
|
+
|
|
1367
|
+
# Create pseudo-schema for children (many cardinality)
|
|
1368
|
+
children_schema = RelationshipSchemaAPI(
|
|
1369
|
+
name="children",
|
|
1370
|
+
peer=self._schema.hierarchy, # type: ignore[union-attr, arg-type]
|
|
1371
|
+
kind=RelationshipKind.HIERARCHY,
|
|
1372
|
+
cardinality="many",
|
|
1373
|
+
optional=True,
|
|
1374
|
+
)
|
|
1375
|
+
children_data = data.get("children", None) if isinstance(data, dict) else None
|
|
1376
|
+
self._hierarchical_data["children"] = RelationshipManagerSync(
|
|
1377
|
+
name="children",
|
|
1378
|
+
client=self._client,
|
|
1379
|
+
node=self,
|
|
1380
|
+
branch=self._branch,
|
|
1381
|
+
schema=children_schema,
|
|
1382
|
+
data=children_data,
|
|
1383
|
+
)
|
|
1384
|
+
|
|
1385
|
+
# Create pseudo-schema for ancestors (read-only, many cardinality)
|
|
1386
|
+
ancestors_schema = RelationshipSchemaAPI(
|
|
1387
|
+
name="ancestors",
|
|
1388
|
+
peer=self._schema.hierarchy, # type: ignore[union-attr, arg-type]
|
|
1389
|
+
cardinality="many",
|
|
1390
|
+
read_only=True,
|
|
1391
|
+
optional=True,
|
|
1392
|
+
)
|
|
1393
|
+
ancestors_data = data.get("ancestors", None) if isinstance(data, dict) else None
|
|
1394
|
+
self._hierarchical_data["ancestors"] = RelationshipManagerSync(
|
|
1395
|
+
name="ancestors",
|
|
1396
|
+
client=self._client,
|
|
1397
|
+
node=self,
|
|
1398
|
+
branch=self._branch,
|
|
1399
|
+
schema=ancestors_schema,
|
|
1400
|
+
data=ancestors_data,
|
|
1401
|
+
)
|
|
1402
|
+
|
|
1403
|
+
# Create pseudo-schema for descendants (read-only, many cardinality)
|
|
1404
|
+
descendants_schema = RelationshipSchemaAPI(
|
|
1405
|
+
name="descendants",
|
|
1406
|
+
peer=self._schema.hierarchy, # type: ignore[union-attr, arg-type]
|
|
1407
|
+
cardinality="many",
|
|
1408
|
+
read_only=True,
|
|
1409
|
+
optional=True,
|
|
1410
|
+
)
|
|
1411
|
+
descendants_data = data.get("descendants", None) if isinstance(data, dict) else None
|
|
1412
|
+
self._hierarchical_data["descendants"] = RelationshipManagerSync(
|
|
1413
|
+
name="descendants",
|
|
1414
|
+
client=self._client,
|
|
1415
|
+
node=self,
|
|
1416
|
+
branch=self._branch,
|
|
1417
|
+
schema=descendants_schema,
|
|
1418
|
+
data=descendants_data,
|
|
1419
|
+
)
|
|
1420
|
+
|
|
1189
1421
|
def __getattr__(self, name: str) -> Attribute | RelationshipManagerSync | RelatedNodeSync:
|
|
1190
1422
|
if "_attribute_data" in self.__dict__ and name in self._attribute_data:
|
|
1191
1423
|
return self._attribute_data[name]
|
|
@@ -1193,6 +1425,8 @@ class InfrahubNodeSync(InfrahubNodeBase):
|
|
|
1193
1425
|
return self._relationship_cardinality_many_data[name]
|
|
1194
1426
|
if "_relationship_cardinality_one_data" in self.__dict__ and name in self._relationship_cardinality_one_data:
|
|
1195
1427
|
return self._relationship_cardinality_one_data[name]
|
|
1428
|
+
if "_hierarchical_data" in self.__dict__ and name in self._hierarchical_data:
|
|
1429
|
+
return self._hierarchical_data[name]
|
|
1196
1430
|
|
|
1197
1431
|
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
|
|
1198
1432
|
|
|
@@ -1274,6 +1508,57 @@ class InfrahubNodeSync(InfrahubNodeBase):
|
|
|
1274
1508
|
|
|
1275
1509
|
self._client.store.set(node=self)
|
|
1276
1510
|
|
|
1511
|
+
def _process_hierarchical_fields(
|
|
1512
|
+
self,
|
|
1513
|
+
data: dict[str, Any],
|
|
1514
|
+
include: list[str] | None = None,
|
|
1515
|
+
exclude: list[str] | None = None,
|
|
1516
|
+
prefetch_relationships: bool = False,
|
|
1517
|
+
insert_alias: bool = False,
|
|
1518
|
+
property: bool = False,
|
|
1519
|
+
) -> None:
|
|
1520
|
+
"""Process hierarchical fields (parent, children, ancestors, descendants) for hierarchical nodes."""
|
|
1521
|
+
if not self._hierarchy_support:
|
|
1522
|
+
return
|
|
1523
|
+
|
|
1524
|
+
for hierarchical_name in ["parent", "children", "ancestors", "descendants"]:
|
|
1525
|
+
if exclude and hierarchical_name in exclude:
|
|
1526
|
+
continue
|
|
1527
|
+
|
|
1528
|
+
# Only include if explicitly requested or if prefetch_relationships is True
|
|
1529
|
+
should_fetch = prefetch_relationships or (include is not None and hierarchical_name in include)
|
|
1530
|
+
if not should_fetch:
|
|
1531
|
+
continue
|
|
1532
|
+
|
|
1533
|
+
peer_schema = self._client.schema.get(kind=self._schema.hierarchy, branch=self._branch) # type: ignore[union-attr, arg-type]
|
|
1534
|
+
peer_node = InfrahubNodeSync(client=self._client, schema=peer_schema, branch=self._branch)
|
|
1535
|
+
# Exclude hierarchical fields from peer data to prevent infinite recursion
|
|
1536
|
+
peer_exclude = list(exclude) if exclude else []
|
|
1537
|
+
peer_exclude.extend(["parent", "children", "ancestors", "descendants"])
|
|
1538
|
+
peer_data = peer_node.generate_query_data_node(
|
|
1539
|
+
exclude=peer_exclude,
|
|
1540
|
+
property=property,
|
|
1541
|
+
)
|
|
1542
|
+
|
|
1543
|
+
# Parent is cardinality one, others are cardinality many
|
|
1544
|
+
if hierarchical_name == "parent":
|
|
1545
|
+
hierarchical_data = RelatedNodeSync._generate_query_data(peer_data=peer_data, property=property)
|
|
1546
|
+
# Use fragment for hierarchical fields similar to hierarchy relationships
|
|
1547
|
+
data_node = hierarchical_data["node"]
|
|
1548
|
+
hierarchical_data["node"] = {}
|
|
1549
|
+
hierarchical_data["node"][f"...on {self._schema.hierarchy}"] = data_node # type: ignore[union-attr]
|
|
1550
|
+
else:
|
|
1551
|
+
hierarchical_data = RelationshipManagerSync._generate_query_data(peer_data=peer_data, property=property)
|
|
1552
|
+
# Use fragment for hierarchical fields similar to hierarchy relationships
|
|
1553
|
+
data_node = hierarchical_data["edges"]["node"]
|
|
1554
|
+
hierarchical_data["edges"]["node"] = {}
|
|
1555
|
+
hierarchical_data["edges"]["node"][f"...on {self._schema.hierarchy}"] = data_node # type: ignore[union-attr]
|
|
1556
|
+
|
|
1557
|
+
data[hierarchical_name] = hierarchical_data
|
|
1558
|
+
|
|
1559
|
+
if insert_alias:
|
|
1560
|
+
data[hierarchical_name]["@alias"] = f"__alias__{self._schema.kind}__{hierarchical_name}"
|
|
1561
|
+
|
|
1277
1562
|
def generate_query_data(
|
|
1278
1563
|
self,
|
|
1279
1564
|
filters: dict[str, Any] | None = None,
|
|
@@ -1396,8 +1681,11 @@ class InfrahubNodeSync(InfrahubNodeBase):
|
|
|
1396
1681
|
if rel_schema and should_fetch_relationship:
|
|
1397
1682
|
peer_schema = self._client.schema.get(kind=rel_schema.peer, branch=self._branch)
|
|
1398
1683
|
peer_node = InfrahubNodeSync(client=self._client, schema=peer_schema, branch=self._branch)
|
|
1399
|
-
peer_data = peer_node.generate_query_data_node(
|
|
1684
|
+
peer_data = peer_node.generate_query_data_node(
|
|
1685
|
+
property=property,
|
|
1686
|
+
)
|
|
1400
1687
|
|
|
1688
|
+
rel_data: dict[str, Any]
|
|
1401
1689
|
if rel_schema and rel_schema.cardinality == "one":
|
|
1402
1690
|
rel_data = RelatedNodeSync._generate_query_data(peer_data=peer_data, property=property)
|
|
1403
1691
|
# Nodes involved in a hierarchy are required to inherit from a common ancestor node, and graphql
|
|
@@ -1410,12 +1698,24 @@ class InfrahubNodeSync(InfrahubNodeBase):
|
|
|
1410
1698
|
rel_data["node"][f"...on {rel_schema.peer}"] = data_node
|
|
1411
1699
|
elif rel_schema and rel_schema.cardinality == "many":
|
|
1412
1700
|
rel_data = RelationshipManagerSync._generate_query_data(peer_data=peer_data, property=property)
|
|
1701
|
+
else:
|
|
1702
|
+
continue
|
|
1413
1703
|
|
|
1414
1704
|
data[rel_name] = rel_data
|
|
1415
1705
|
|
|
1416
1706
|
if insert_alias:
|
|
1417
1707
|
data[rel_name]["@alias"] = f"__alias__{self._schema.kind}__{rel_name}"
|
|
1418
1708
|
|
|
1709
|
+
# Add parent, children, ancestors and descendants for hierarchical nodes
|
|
1710
|
+
self._process_hierarchical_fields(
|
|
1711
|
+
data=data,
|
|
1712
|
+
include=include,
|
|
1713
|
+
exclude=exclude,
|
|
1714
|
+
prefetch_relationships=prefetch_relationships,
|
|
1715
|
+
insert_alias=insert_alias,
|
|
1716
|
+
property=property,
|
|
1717
|
+
)
|
|
1718
|
+
|
|
1419
1719
|
return data
|
|
1420
1720
|
|
|
1421
1721
|
def add_relationships(
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# infrahub_sdk/pytest_plugin/AGENTS.md
|
|
2
|
+
|
|
3
|
+
Custom pytest plugin for testing Infrahub resources via YAML test files.
|
|
4
|
+
|
|
5
|
+
## YAML Test Format
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
infrahub_tests:
|
|
9
|
+
- resource: Check # Check, GraphQLQuery, Jinja2Transform, PythonTransform
|
|
10
|
+
resource_name: my_check
|
|
11
|
+
tests:
|
|
12
|
+
- name: test_success_case
|
|
13
|
+
spec:
|
|
14
|
+
kind: check-smoke # See test kinds below
|
|
15
|
+
input:
|
|
16
|
+
data: {...}
|
|
17
|
+
output:
|
|
18
|
+
passed: true
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Test Kinds
|
|
22
|
+
|
|
23
|
+
| Resource | Smoke | Unit | Integration |
|
|
24
|
+
| -------- | ----- | ---- | ----------- |
|
|
25
|
+
| Check | `check-smoke` | `check-unit-process` | `check-integration` |
|
|
26
|
+
| GraphQL | `graphql-query-smoke` | - | `graphql-query-integration` |
|
|
27
|
+
| Jinja2 | `jinja2-transform-smoke` | `jinja2-transform-unit-render` | `jinja2-transform-integration` |
|
|
28
|
+
| Python | `python-transform-smoke` | `python-transform-unit-process` | `python-transform-integration` |
|
|
29
|
+
|
|
30
|
+
## Plugin Structure
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
infrahub_sdk/pytest_plugin/
|
|
34
|
+
├── plugin.py # Pytest hooks (pytest_collect_file, etc.)
|
|
35
|
+
├── loader.py # YAML loading, ITEMS_MAPPING
|
|
36
|
+
├── models.py # Pydantic schemas for test files
|
|
37
|
+
└── items/ # Test item implementations
|
|
38
|
+
├── base.py # InfrahubItem base class
|
|
39
|
+
└── check.py # Check-specific items
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Adding New Test Item
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# 1. Create item class in items/
|
|
46
|
+
class MyCustomItem(InfrahubItem):
|
|
47
|
+
def runtest(self):
|
|
48
|
+
result = self.process(self.test.input)
|
|
49
|
+
assert result == self.test.output
|
|
50
|
+
|
|
51
|
+
# 2. Register in loader.py
|
|
52
|
+
ITEMS_MAPPING = {
|
|
53
|
+
"my-custom-test": MyCustomItem,
|
|
54
|
+
...
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Boundaries
|
|
59
|
+
|
|
60
|
+
✅ **Always**
|
|
61
|
+
|
|
62
|
+
- Register new items in `ITEMS_MAPPING`
|
|
63
|
+
- Inherit from `InfrahubItem` base class
|
|
64
|
+
|
|
65
|
+
🚫 **Never**
|
|
66
|
+
|
|
67
|
+
- Forget to add new test kinds to `ITEMS_MAPPING`
|
infrahub_sdk/timestamp.py
CHANGED
|
@@ -6,7 +6,7 @@ from datetime import datetime, timezone
|
|
|
6
6
|
from typing import Literal, TypedDict
|
|
7
7
|
|
|
8
8
|
from typing_extensions import NotRequired
|
|
9
|
-
from whenever import Date, Instant,
|
|
9
|
+
from whenever import Date, Instant, OffsetDateTime, PlainDateTime, Time, ZonedDateTime
|
|
10
10
|
|
|
11
11
|
from .exceptions import TimestampFormatError
|
|
12
12
|
|
|
@@ -51,30 +51,30 @@ class Timestamp:
|
|
|
51
51
|
@classmethod
|
|
52
52
|
def _parse_string(cls, value: str) -> ZonedDateTime:
|
|
53
53
|
try:
|
|
54
|
-
return ZonedDateTime.
|
|
54
|
+
return ZonedDateTime.parse_iso(value)
|
|
55
55
|
except ValueError:
|
|
56
56
|
pass
|
|
57
57
|
|
|
58
58
|
try:
|
|
59
|
-
instant_date = Instant.
|
|
59
|
+
instant_date = Instant.parse_iso(value)
|
|
60
60
|
return instant_date.to_tz("UTC")
|
|
61
61
|
except ValueError:
|
|
62
62
|
pass
|
|
63
63
|
|
|
64
64
|
try:
|
|
65
|
-
|
|
66
|
-
return
|
|
65
|
+
plain_date_time = PlainDateTime.parse_iso(value)
|
|
66
|
+
return plain_date_time.assume_utc().to_tz("UTC")
|
|
67
67
|
except ValueError:
|
|
68
68
|
pass
|
|
69
69
|
|
|
70
70
|
try:
|
|
71
|
-
offset_date_time = OffsetDateTime.
|
|
71
|
+
offset_date_time = OffsetDateTime.parse_iso(value)
|
|
72
72
|
return offset_date_time.to_tz("UTC")
|
|
73
73
|
except ValueError:
|
|
74
74
|
pass
|
|
75
75
|
|
|
76
76
|
try:
|
|
77
|
-
date = Date.
|
|
77
|
+
date = Date.parse_iso(value)
|
|
78
78
|
local_date = date.at(Time(12, 00))
|
|
79
79
|
return local_date.assume_tz("UTC", disambiguate="compatible")
|
|
80
80
|
except ValueError:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: infrahub-server
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.1
|
|
4
4
|
Summary: Infrahub is taking a new approach to Infrastructure Management by providing a new generation of datastore to organize and control all the data that defines how an infrastructure should run.
|
|
5
5
|
Project-URL: Homepage, https://opsmill.com
|
|
6
6
|
Project-URL: Repository, https://github.com/opsmill/infrahub
|
|
@@ -21,7 +21,6 @@ Requires-Dist: bcrypt<4.2,>=4.1
|
|
|
21
21
|
Requires-Dist: boto3==1.34.129
|
|
22
22
|
Requires-Dist: cachetools-async==0.0.5
|
|
23
23
|
Requires-Dist: click==8.1.7
|
|
24
|
-
Requires-Dist: copier==9.8.0
|
|
25
24
|
Requires-Dist: deepdiff==8.6.1
|
|
26
25
|
Requires-Dist: dulwich==0.22.7
|
|
27
26
|
Requires-Dist: email-validator<2.2,>=2.1
|
|
@@ -60,7 +59,7 @@ Requires-Dist: tomli>=1.1.0; python_version <= '3.11'
|
|
|
60
59
|
Requires-Dist: typer==0.19.2
|
|
61
60
|
Requires-Dist: ujson<6,>=5
|
|
62
61
|
Requires-Dist: uvicorn[standard]<0.33,>=0.32
|
|
63
|
-
Requires-Dist: whenever==0.
|
|
62
|
+
Requires-Dist: whenever==0.9.3
|
|
64
63
|
Description-Content-Type: text/markdown
|
|
65
64
|
|
|
66
65
|
<h1 align="center">
|