Skip to content

Commit c22ab24

Browse files
authored
Merge pull request #442 from opsmill/develop
Merge stable into develop
2 parents 9c70cb0 + b8186de commit c22ab24

34 files changed

+1186
-845
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,17 @@ jobs:
8686

8787

8888
markdown-lint:
89-
if: needs.files-changed.outputs.documentation == 'true'
89+
if: |
90+
needs.files-changed.outputs.documentation == 'true' ||
91+
needs.files-changed.outputs.github_workflows == 'true'
9092
needs: ["files-changed"]
9193
runs-on: "ubuntu-latest"
9294
timeout-minutes: 5
9395
steps:
9496
- name: "Check out repository code"
9597
uses: "actions/checkout@v4"
9698
- name: "Linting: markdownlint"
97-
uses: DavidAnson/markdownlint-cli2-action@v19
99+
uses: DavidAnson/markdownlint-cli2-action@v20
98100
with:
99101
config: .markdownlint.yaml
100102
globs: |

.markdownlint.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ MD034: false # no-bare-urls
1111
MD041: false # allow 1st line to not be a top-level heading (required for Towncrier)
1212
MD045: false # no alt text around images
1313
MD047: false # single trailing newline
14+
MD059: # Link descriptions that are prohibited
15+
prohibited_texts: []

changelog/+040fc56b.housekeeping.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Refactor InfrahubNode to avoid the creation of a dynamic Python class for each object defined

changelog/+543efbd1.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added NumberPool as a new attribute kind, for support in Infrahub 1.3

docs/docs/python-sdk/topics/object_file.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ Multiple object files can be loaded at once by specifying the path to multiple f
3838
The `object load` command will create/update the objects using an `Upsert` operation. All objects previously loaded will NOT be deleted in the Infrahub instance.
3939
Also, if some objects present in different files are identical and dependent on each other, the `object load` command will NOT calculate the dependencies between the objects and as such it's the responsibility of the users to execute the command in the right order.
4040

41+
> Object files can also be loaded into Infrahub when using external Git repositories. To see how to do this, please refer to the [.infrahub.yml](https://docs.infrahub.app/topics/infrahub-yml#objects) documentation.
42+
4143
### Validate the format of object files
4244

4345
The object file can be validated using the `infrahubctl object validate` command.

infrahub_sdk/client.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,11 +172,18 @@ def start_tracking(
172172
params: dict[str, Any] | None = None,
173173
delete_unused_nodes: bool = False,
174174
group_type: str | None = None,
175+
group_params: dict[str, Any] | None = None,
176+
branch: str | None = None,
175177
) -> Self:
176178
self.mode = InfrahubClientMode.TRACKING
177179
identifier = identifier or self.identifier or "python-sdk"
178180
self.set_context_properties(
179-
identifier=identifier, params=params, delete_unused_nodes=delete_unused_nodes, group_type=group_type
181+
identifier=identifier,
182+
params=params,
183+
delete_unused_nodes=delete_unused_nodes,
184+
group_type=group_type,
185+
group_params=group_params,
186+
branch=branch,
180187
)
181188
return self
182189

@@ -187,14 +194,22 @@ def set_context_properties(
187194
delete_unused_nodes: bool = True,
188195
reset: bool = True,
189196
group_type: str | None = None,
197+
group_params: dict[str, Any] | None = None,
198+
branch: str | None = None,
190199
) -> None:
191200
if reset:
192201
if isinstance(self, InfrahubClient):
193202
self.group_context = InfrahubGroupContext(self)
194203
elif isinstance(self, InfrahubClientSync):
195204
self.group_context = InfrahubGroupContextSync(self)
205+
196206
self.group_context.set_properties(
197-
identifier=identifier, params=params, delete_unused_nodes=delete_unused_nodes, group_type=group_type
207+
identifier=identifier,
208+
params=params,
209+
delete_unused_nodes=delete_unused_nodes,
210+
group_type=group_type,
211+
group_params=group_params,
212+
branch=branch,
198213
)
199214

200215
def _graphql_url(

infrahub_sdk/ctl/generator.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,20 +74,20 @@ async def run(
7474
targets = await client.get(
7575
kind="CoreGroup", branch=branch, include=["members"], name__value=generator_config.targets
7676
)
77-
await targets.members.fetch()
77+
await targets._get_relationship_many(name="members").fetch()
7878

79-
if not targets.members.peers:
79+
if not targets._get_relationship_many(name="members").peers:
8080
console.print(
8181
f"[red]No members found within '{generator_config.targets}', not running generator '{generator_name}'"
8282
)
8383
return
8484

85-
for member in targets.members.peers:
85+
for member in targets._get_relationship_many(name="members").peers:
8686
check_parameter = {}
8787
if identifier:
8888
attribute = getattr(member.peer, identifier)
8989
check_parameter = {identifier: attribute.value}
90-
params = {"name": member.peer.name.value}
90+
params = {"name": member.peer._get_attribute(name="name").value}
9191
generator = generator_class(
9292
query=generator_config.query,
9393
client=client,

infrahub_sdk/node/__init__.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from __future__ import annotations
2+
3+
from .constants import (
4+
ARTIFACT_DEFINITION_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE,
5+
ARTIFACT_FETCH_FEATURE_NOT_SUPPORTED_MESSAGE,
6+
ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE,
7+
HFID_STR_SEPARATOR,
8+
IP_TYPES,
9+
PROPERTIES_FLAG,
10+
PROPERTIES_OBJECT,
11+
SAFE_VALUE,
12+
)
13+
from .node import InfrahubNode, InfrahubNodeBase, InfrahubNodeSync
14+
from .parsers import parse_human_friendly_id
15+
from .property import NodeProperty
16+
from .related_node import RelatedNode, RelatedNodeBase, RelatedNodeSync
17+
from .relationship import RelationshipManager, RelationshipManagerBase, RelationshipManagerSync
18+
19+
__all__ = [
20+
"ARTIFACT_DEFINITION_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE",
21+
"ARTIFACT_FETCH_FEATURE_NOT_SUPPORTED_MESSAGE",
22+
"ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE",
23+
"HFID_STR_SEPARATOR",
24+
"IP_TYPES",
25+
"PROPERTIES_FLAG",
26+
"PROPERTIES_OBJECT",
27+
"SAFE_VALUE",
28+
"InfrahubNode",
29+
"InfrahubNodeBase",
30+
"InfrahubNodeSync",
31+
"NodeProperty",
32+
"RelatedNode",
33+
"RelatedNodeBase",
34+
"RelatedNodeSync",
35+
"RelationshipManager",
36+
"RelationshipManagerBase",
37+
"RelationshipManagerSync",
38+
"parse_human_friendly_id",
39+
]

infrahub_sdk/node/attribute.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from __future__ import annotations
2+
3+
import ipaddress
4+
from typing import TYPE_CHECKING, Any, Callable, get_args
5+
6+
from ..protocols_base import CoreNodeBase
7+
from ..uuidt import UUIDT
8+
from .constants import IP_TYPES, PROPERTIES_FLAG, PROPERTIES_OBJECT, SAFE_VALUE
9+
from .property import NodeProperty
10+
11+
if TYPE_CHECKING:
12+
from ..schema import AttributeSchemaAPI
13+
14+
15+
class Attribute:
16+
"""Represents an attribute of a Node, including its schema, value, and properties."""
17+
18+
def __init__(self, name: str, schema: AttributeSchemaAPI, data: Any | dict):
19+
"""
20+
Args:
21+
name (str): The name of the attribute.
22+
schema (AttributeSchema): The schema defining the attribute.
23+
data (Union[Any, dict]): The data for the attribute, either in raw form or as a dictionary.
24+
"""
25+
self.name = name
26+
self._schema = schema
27+
28+
if not isinstance(data, dict) or "value" not in data.keys():
29+
data = {"value": data}
30+
31+
self._properties_flag = PROPERTIES_FLAG
32+
self._properties_object = PROPERTIES_OBJECT
33+
self._properties = self._properties_flag + self._properties_object
34+
35+
self._read_only = ["updated_at", "is_inherited"]
36+
37+
self.id: str | None = data.get("id", None)
38+
39+
self._value: Any | None = data.get("value", None)
40+
self.value_has_been_mutated = False
41+
self.is_default: bool | None = data.get("is_default", None)
42+
self.is_from_profile: bool | None = data.get("is_from_profile", None)
43+
44+
if self._value:
45+
value_mapper: dict[str, Callable] = {
46+
"IPHost": ipaddress.ip_interface,
47+
"IPNetwork": ipaddress.ip_network,
48+
}
49+
mapper = value_mapper.get(schema.kind, lambda value: value)
50+
self._value = mapper(data.get("value"))
51+
52+
self.is_inherited: bool | None = data.get("is_inherited", None)
53+
self.updated_at: str | None = data.get("updated_at", None)
54+
55+
self.is_visible: bool | None = data.get("is_visible", None)
56+
self.is_protected: bool | None = data.get("is_protected", None)
57+
58+
self.source: NodeProperty | None = None
59+
self.owner: NodeProperty | None = None
60+
61+
for prop_name in self._properties_object:
62+
if data.get(prop_name):
63+
setattr(self, prop_name, NodeProperty(data=data.get(prop_name))) # type: ignore[arg-type]
64+
65+
@property
66+
def value(self) -> Any:
67+
return self._value
68+
69+
@value.setter
70+
def value(self, value: Any) -> None:
71+
self._value = value
72+
self.value_has_been_mutated = True
73+
74+
def _generate_input_data(self) -> dict | None:
75+
data: dict[str, Any] = {}
76+
variables: dict[str, Any] = {}
77+
78+
if self.value is None:
79+
return data
80+
81+
if isinstance(self.value, str):
82+
if SAFE_VALUE.match(self.value):
83+
data["value"] = self.value
84+
else:
85+
var_name = f"value_{UUIDT.new().hex}"
86+
variables[var_name] = self.value
87+
data["value"] = f"${var_name}"
88+
elif isinstance(self.value, get_args(IP_TYPES)):
89+
data["value"] = self.value.with_prefixlen
90+
elif isinstance(self.value, CoreNodeBase) and self.value.is_resource_pool():
91+
data["from_pool"] = {"id": self.value.id}
92+
else:
93+
data["value"] = self.value
94+
95+
for prop_name in self._properties_flag:
96+
if getattr(self, prop_name) is not None:
97+
data[prop_name] = getattr(self, prop_name)
98+
99+
for prop_name in self._properties_object:
100+
if getattr(self, prop_name) is not None:
101+
data[prop_name] = getattr(self, prop_name)._generate_input_data()
102+
103+
return {"data": data, "variables": variables}
104+
105+
def _generate_query_data(self, property: bool = False) -> dict | None:
106+
data: dict[str, Any] = {"value": None}
107+
108+
if property:
109+
data.update({"is_default": None, "is_from_profile": None})
110+
111+
for prop_name in self._properties_flag:
112+
data[prop_name] = None
113+
for prop_name in self._properties_object:
114+
data[prop_name] = {"id": None, "display_label": None, "__typename": None}
115+
116+
return data
117+
118+
def _generate_mutation_query(self) -> dict[str, Any]:
119+
if isinstance(self.value, CoreNodeBase) and self.value.is_resource_pool():
120+
# If it points to a pool, ask for the value of the pool allocated resource
121+
return {self.name: {"value": None}}
122+
return {}

infrahub_sdk/node/constants.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import ipaddress
2+
import re
3+
from typing import Union
4+
5+
PROPERTIES_FLAG = ["is_visible", "is_protected"]
6+
PROPERTIES_OBJECT = ["source", "owner"]
7+
SAFE_VALUE = re.compile(r"(^[\. /:a-zA-Z0-9_-]+$)|(^$)")
8+
9+
IP_TYPES = Union[ipaddress.IPv4Interface, ipaddress.IPv6Interface, ipaddress.IPv4Network, ipaddress.IPv6Network]
10+
11+
ARTIFACT_FETCH_FEATURE_NOT_SUPPORTED_MESSAGE = (
12+
"calling artifact_fetch is only supported for nodes that are Artifact Definition target"
13+
)
14+
ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE = (
15+
"calling artifact_generate is only supported for nodes that are Artifact Definition targets"
16+
)
17+
ARTIFACT_DEFINITION_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE = (
18+
"calling generate is only supported for CoreArtifactDefinition nodes"
19+
)
20+
21+
HFID_STR_SEPARATOR = "__"

0 commit comments

Comments
 (0)