infrahub-server 1.2.12__py3-none-any.whl → 1.3.0__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.
Files changed (205) hide show
  1. infrahub/actions/constants.py +130 -0
  2. infrahub/actions/gather.py +114 -0
  3. infrahub/actions/models.py +243 -0
  4. infrahub/actions/parsers.py +104 -0
  5. infrahub/actions/schema.py +393 -0
  6. infrahub/actions/tasks.py +119 -0
  7. infrahub/actions/triggers.py +21 -0
  8. infrahub/branch/__init__.py +0 -0
  9. infrahub/branch/tasks.py +29 -0
  10. infrahub/branch/triggers.py +22 -0
  11. infrahub/cli/db.py +3 -4
  12. infrahub/computed_attribute/gather.py +3 -1
  13. infrahub/computed_attribute/tasks.py +23 -29
  14. infrahub/core/account.py +24 -47
  15. infrahub/core/attribute.py +13 -15
  16. infrahub/core/constants/__init__.py +10 -0
  17. infrahub/core/constants/infrahubkind.py +9 -0
  18. infrahub/core/constraint/node/runner.py +3 -1
  19. infrahub/core/convert_object_type/__init__.py +0 -0
  20. infrahub/core/convert_object_type/conversion.py +124 -0
  21. infrahub/core/convert_object_type/schema_mapping.py +56 -0
  22. infrahub/core/diff/coordinator.py +8 -1
  23. infrahub/core/diff/query/all_conflicts.py +1 -5
  24. infrahub/core/diff/query/artifact.py +10 -20
  25. infrahub/core/diff/query/delete_query.py +8 -4
  26. infrahub/core/diff/query/diff_get.py +3 -6
  27. infrahub/core/diff/query/field_specifiers.py +1 -1
  28. infrahub/core/diff/query/field_summary.py +2 -4
  29. infrahub/core/diff/query/merge.py +72 -125
  30. infrahub/core/diff/query/save.py +28 -43
  31. infrahub/core/diff/query/summary_counts_enricher.py +34 -54
  32. infrahub/core/diff/query/time_range_query.py +0 -1
  33. infrahub/core/diff/repository/repository.py +4 -0
  34. infrahub/core/manager.py +14 -11
  35. infrahub/core/migrations/graph/m003_relationship_parent_optional.py +1 -2
  36. infrahub/core/migrations/graph/m012_convert_account_generic.py +1 -1
  37. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +2 -6
  38. infrahub/core/migrations/graph/m015_diff_format_update.py +1 -2
  39. infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +1 -2
  40. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +11 -22
  41. infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -6
  42. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +1 -2
  43. infrahub/core/migrations/graph/m023_deduplicate_cardinality_one_relationships.py +2 -2
  44. infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +1 -2
  45. infrahub/core/migrations/graph/m028_delete_diffs.py +1 -2
  46. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +30 -48
  47. infrahub/core/migrations/graph/m030_illegal_edges.py +1 -2
  48. infrahub/core/migrations/query/attribute_add.py +1 -2
  49. infrahub/core/migrations/query/attribute_rename.py +6 -11
  50. infrahub/core/migrations/query/delete_element_in_schema.py +19 -17
  51. infrahub/core/migrations/query/node_duplicate.py +19 -21
  52. infrahub/core/migrations/query/relationship_duplicate.py +19 -18
  53. infrahub/core/migrations/schema/node_attribute_remove.py +4 -8
  54. infrahub/core/migrations/schema/node_remove.py +19 -20
  55. infrahub/core/models.py +29 -2
  56. infrahub/core/node/__init__.py +131 -28
  57. infrahub/core/node/base.py +1 -1
  58. infrahub/core/node/create.py +211 -0
  59. infrahub/core/node/resource_manager/number_pool.py +31 -5
  60. infrahub/core/node/standard.py +6 -1
  61. infrahub/core/path.py +15 -1
  62. infrahub/core/protocols.py +57 -0
  63. infrahub/core/protocols_base.py +3 -0
  64. infrahub/core/query/__init__.py +2 -2
  65. infrahub/core/query/delete.py +3 -3
  66. infrahub/core/query/diff.py +19 -32
  67. infrahub/core/query/ipam.py +10 -20
  68. infrahub/core/query/node.py +29 -47
  69. infrahub/core/query/relationship.py +55 -34
  70. infrahub/core/query/resource_manager.py +1 -2
  71. infrahub/core/query/standard_node.py +19 -5
  72. infrahub/core/query/subquery.py +2 -4
  73. infrahub/core/relationship/constraints/count.py +10 -9
  74. infrahub/core/relationship/constraints/interface.py +2 -1
  75. infrahub/core/relationship/constraints/peer_kind.py +2 -1
  76. infrahub/core/relationship/constraints/peer_parent.py +56 -0
  77. infrahub/core/relationship/constraints/peer_relatives.py +72 -0
  78. infrahub/core/relationship/constraints/profiles_kind.py +1 -1
  79. infrahub/core/relationship/model.py +4 -1
  80. infrahub/core/schema/__init__.py +2 -1
  81. infrahub/core/schema/attribute_parameters.py +160 -0
  82. infrahub/core/schema/attribute_schema.py +130 -7
  83. infrahub/core/schema/basenode_schema.py +27 -3
  84. infrahub/core/schema/definitions/core/__init__.py +29 -1
  85. infrahub/core/schema/definitions/core/group.py +45 -0
  86. infrahub/core/schema/definitions/core/resource_pool.py +9 -0
  87. infrahub/core/schema/definitions/internal.py +43 -5
  88. infrahub/core/schema/generated/attribute_schema.py +16 -3
  89. infrahub/core/schema/generated/relationship_schema.py +11 -1
  90. infrahub/core/schema/manager.py +7 -2
  91. infrahub/core/schema/schema_branch.py +104 -9
  92. infrahub/core/validators/__init__.py +15 -2
  93. infrahub/core/validators/attribute/choices.py +1 -3
  94. infrahub/core/validators/attribute/enum.py +1 -3
  95. infrahub/core/validators/attribute/kind.py +1 -3
  96. infrahub/core/validators/attribute/length.py +13 -7
  97. infrahub/core/validators/attribute/min_max.py +118 -0
  98. infrahub/core/validators/attribute/number_pool.py +106 -0
  99. infrahub/core/validators/attribute/optional.py +1 -4
  100. infrahub/core/validators/attribute/regex.py +5 -6
  101. infrahub/core/validators/attribute/unique.py +1 -3
  102. infrahub/core/validators/determiner.py +18 -2
  103. infrahub/core/validators/enum.py +12 -0
  104. infrahub/core/validators/node/hierarchy.py +3 -6
  105. infrahub/core/validators/query.py +1 -3
  106. infrahub/core/validators/relationship/count.py +6 -12
  107. infrahub/core/validators/relationship/optional.py +2 -4
  108. infrahub/core/validators/relationship/peer.py +177 -12
  109. infrahub/core/validators/tasks.py +1 -1
  110. infrahub/core/validators/uniqueness/query.py +5 -9
  111. infrahub/database/__init__.py +12 -4
  112. infrahub/database/validation.py +1 -2
  113. infrahub/dependencies/builder/constraint/grouped/node_runner.py +4 -0
  114. infrahub/dependencies/builder/constraint/relationship_manager/peer_parent.py +8 -0
  115. infrahub/dependencies/builder/constraint/relationship_manager/peer_relatives.py +8 -0
  116. infrahub/dependencies/builder/constraint/schema/aggregated.py +2 -0
  117. infrahub/dependencies/builder/constraint/schema/relationship_peer.py +8 -0
  118. infrahub/dependencies/builder/diff/deserializer.py +1 -1
  119. infrahub/dependencies/registry.py +4 -0
  120. infrahub/events/group_action.py +1 -0
  121. infrahub/events/models.py +1 -1
  122. infrahub/git/base.py +5 -3
  123. infrahub/git/integrator.py +96 -5
  124. infrahub/git/tasks.py +1 -0
  125. infrahub/graphql/analyzer.py +139 -18
  126. infrahub/graphql/manager.py +4 -0
  127. infrahub/graphql/mutations/action.py +164 -0
  128. infrahub/graphql/mutations/convert_object_type.py +71 -0
  129. infrahub/graphql/mutations/main.py +24 -175
  130. infrahub/graphql/mutations/proposed_change.py +20 -17
  131. infrahub/graphql/mutations/relationship.py +32 -0
  132. infrahub/graphql/mutations/resource_manager.py +63 -7
  133. infrahub/graphql/queries/convert_object_type_mapping.py +34 -0
  134. infrahub/graphql/queries/resource_manager.py +7 -1
  135. infrahub/graphql/resolvers/many_relationship.py +1 -1
  136. infrahub/graphql/resolvers/resolver.py +2 -2
  137. infrahub/graphql/resolvers/single_relationship.py +1 -1
  138. infrahub/graphql/schema.py +6 -0
  139. infrahub/menu/menu.py +34 -2
  140. infrahub/message_bus/messages/__init__.py +0 -10
  141. infrahub/message_bus/operations/__init__.py +0 -8
  142. infrahub/message_bus/operations/refresh/registry.py +3 -6
  143. infrahub/patch/queries/delete_duplicated_edges.py +10 -15
  144. infrahub/pools/models.py +14 -0
  145. infrahub/pools/number.py +5 -3
  146. infrahub/pools/registration.py +22 -0
  147. infrahub/pools/tasks.py +126 -0
  148. infrahub/prefect_server/models.py +1 -19
  149. infrahub/proposed_change/models.py +68 -3
  150. infrahub/proposed_change/tasks.py +911 -34
  151. infrahub/schema/__init__.py +0 -0
  152. infrahub/schema/tasks.py +27 -0
  153. infrahub/schema/triggers.py +23 -0
  154. infrahub/task_manager/models.py +10 -6
  155. infrahub/trigger/catalogue.py +6 -0
  156. infrahub/trigger/models.py +23 -6
  157. infrahub/trigger/setup.py +26 -2
  158. infrahub/trigger/tasks.py +4 -2
  159. infrahub/types.py +6 -0
  160. infrahub/webhook/tasks.py +4 -8
  161. infrahub/workflows/catalogue.py +103 -1
  162. infrahub_sdk/client.py +43 -10
  163. infrahub_sdk/ctl/generator.py +4 -4
  164. infrahub_sdk/ctl/repository.py +1 -1
  165. infrahub_sdk/node/__init__.py +39 -0
  166. infrahub_sdk/node/attribute.py +122 -0
  167. infrahub_sdk/node/constants.py +21 -0
  168. infrahub_sdk/{node.py → node/node.py} +158 -803
  169. infrahub_sdk/node/parsers.py +15 -0
  170. infrahub_sdk/node/property.py +24 -0
  171. infrahub_sdk/node/related_node.py +266 -0
  172. infrahub_sdk/node/relationship.py +302 -0
  173. infrahub_sdk/protocols.py +112 -0
  174. infrahub_sdk/protocols_base.py +34 -2
  175. infrahub_sdk/pytest_plugin/items/python_transform.py +2 -1
  176. infrahub_sdk/query_groups.py +17 -5
  177. infrahub_sdk/schema/main.py +1 -0
  178. infrahub_sdk/schema/repository.py +16 -0
  179. infrahub_sdk/spec/object.py +1 -1
  180. infrahub_sdk/store.py +1 -1
  181. infrahub_sdk/testing/schemas/car_person.py +1 -0
  182. infrahub_sdk/utils.py +7 -20
  183. infrahub_sdk/yaml.py +6 -5
  184. {infrahub_server-1.2.12.dist-info → infrahub_server-1.3.0.dist-info}/METADATA +3 -3
  185. {infrahub_server-1.2.12.dist-info → infrahub_server-1.3.0.dist-info}/RECORD +192 -166
  186. infrahub_testcontainers/container.py +0 -1
  187. infrahub_testcontainers/docker-compose.test.yml +1 -1
  188. infrahub_testcontainers/helpers.py +8 -2
  189. infrahub/message_bus/messages/check_generator_run.py +0 -26
  190. infrahub/message_bus/messages/finalize_validator_execution.py +0 -15
  191. infrahub/message_bus/messages/proposed_change/base_with_diff.py +0 -16
  192. infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +0 -11
  193. infrahub/message_bus/messages/request_generatordefinition_check.py +0 -20
  194. infrahub/message_bus/messages/request_proposedchange_pipeline.py +0 -23
  195. infrahub/message_bus/operations/check/__init__.py +0 -3
  196. infrahub/message_bus/operations/check/generator.py +0 -156
  197. infrahub/message_bus/operations/finalize/__init__.py +0 -3
  198. infrahub/message_bus/operations/finalize/validator.py +0 -133
  199. infrahub/message_bus/operations/requests/__init__.py +0 -9
  200. infrahub/message_bus/operations/requests/generator_definition.py +0 -140
  201. infrahub/message_bus/operations/requests/proposed_change.py +0 -629
  202. /infrahub/{message_bus/messages/proposed_change → actions}/__init__.py +0 -0
  203. {infrahub_server-1.2.12.dist-info → infrahub_server-1.3.0.dist-info}/LICENSE.txt +0 -0
  204. {infrahub_server-1.2.12.dist-info → infrahub_server-1.3.0.dist-info}/WHEEL +0 -0
  205. {infrahub_server-1.2.12.dist-info → infrahub_server-1.3.0.dist-info}/entry_points.txt +0 -0
@@ -1,709 +1,31 @@
1
1
  from __future__ import annotations
2
2
 
3
- import ipaddress
4
- import re
5
3
  from collections.abc import Iterable
6
4
  from copy import copy
7
- from typing import TYPE_CHECKING, Any, Callable, Union, get_args
8
-
9
- from .constants import InfrahubClientMode
10
- from .exceptions import (
11
- Error,
12
- FeatureNotSupportedError,
13
- NodeNotFoundError,
14
- UninitializedError,
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from ..constants import InfrahubClientMode
8
+ from ..exceptions import FeatureNotSupportedError, NodeNotFoundError, ResourceNotDefinedError, SchemaNotFoundError
9
+ from ..graphql import Mutation, Query
10
+ from ..schema import GenericSchemaAPI, RelationshipCardinality, RelationshipKind
11
+ from ..utils import compare_lists, generate_short_id, get_flat_value
12
+ from .attribute import Attribute
13
+ from .constants import (
14
+ ARTIFACT_DEFINITION_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE,
15
+ ARTIFACT_FETCH_FEATURE_NOT_SUPPORTED_MESSAGE,
16
+ ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE,
17
+ PROPERTIES_OBJECT,
15
18
  )
16
- from .graphql import Mutation, Query
17
- from .schema import GenericSchemaAPI, RelationshipCardinality, RelationshipKind
18
- from .utils import compare_lists, generate_short_id, get_flat_value
19
- from .uuidt import UUIDT
19
+ from .related_node import RelatedNode, RelatedNodeBase, RelatedNodeSync
20
+ from .relationship import RelationshipManager, RelationshipManagerBase, RelationshipManagerSync
20
21
 
21
22
  if TYPE_CHECKING:
22
23
  from typing_extensions import Self
23
24
 
24
- from .client import InfrahubClient, InfrahubClientSync
25
- from .context import RequestContext
26
- from .schema import AttributeSchemaAPI, MainSchemaTypesAPI, RelationshipSchemaAPI
27
- from .types import Order
28
-
29
-
30
- PROPERTIES_FLAG = ["is_visible", "is_protected"]
31
- PROPERTIES_OBJECT = ["source", "owner"]
32
- SAFE_VALUE = re.compile(r"(^[\. /:a-zA-Z0-9_-]+$)|(^$)")
33
-
34
- IP_TYPES = Union[ipaddress.IPv4Interface, ipaddress.IPv6Interface, ipaddress.IPv4Network, ipaddress.IPv6Network]
35
-
36
- ARTIFACT_FETCH_FEATURE_NOT_SUPPORTED_MESSAGE = (
37
- "calling artifact_fetch is only supported for nodes that are Artifact Definition target"
38
- )
39
- ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE = (
40
- "calling artifact_generate is only supported for nodes that are Artifact Definition targets"
41
- )
42
- ARTIFACT_DEFINITION_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE = (
43
- "calling generate is only supported for CoreArtifactDefinition nodes"
44
- )
45
-
46
- HFID_STR_SEPARATOR = "__"
47
-
48
-
49
- def parse_human_friendly_id(hfid: str | list[str]) -> tuple[str | None, list[str]]:
50
- """Parse a human friendly ID into a kind and an identifier."""
51
- if isinstance(hfid, str):
52
- hfid_parts = hfid.split(HFID_STR_SEPARATOR)
53
- if len(hfid_parts) == 1:
54
- return None, hfid_parts
55
- return hfid_parts[0], hfid_parts[1:]
56
- if isinstance(hfid, list):
57
- return None, hfid
58
- raise ValueError(f"Invalid human friendly ID: {hfid}")
59
-
60
-
61
- class Attribute:
62
- """Represents an attribute of a Node, including its schema, value, and properties."""
63
-
64
- def __init__(self, name: str, schema: AttributeSchemaAPI, data: Any | dict):
65
- """
66
- Args:
67
- name (str): The name of the attribute.
68
- schema (AttributeSchema): The schema defining the attribute.
69
- data (Union[Any, dict]): The data for the attribute, either in raw form or as a dictionary.
70
- """
71
- self.name = name
72
- self._schema = schema
73
-
74
- if not isinstance(data, dict) or "value" not in data.keys():
75
- data = {"value": data}
76
-
77
- self._properties_flag = PROPERTIES_FLAG
78
- self._properties_object = PROPERTIES_OBJECT
79
- self._properties = self._properties_flag + self._properties_object
80
-
81
- self._read_only = ["updated_at", "is_inherited"]
82
-
83
- self.id: str | None = data.get("id", None)
84
-
85
- self._value: Any | None = data.get("value", None)
86
- self.value_has_been_mutated = False
87
- self.is_default: bool | None = data.get("is_default", None)
88
- self.is_from_profile: bool | None = data.get("is_from_profile", None)
89
-
90
- if self._value:
91
- value_mapper: dict[str, Callable] = {
92
- "IPHost": ipaddress.ip_interface,
93
- "IPNetwork": ipaddress.ip_network,
94
- }
95
- mapper = value_mapper.get(schema.kind, lambda value: value)
96
- self._value = mapper(data.get("value"))
97
-
98
- self.is_inherited: bool | None = data.get("is_inherited", None)
99
- self.updated_at: str | None = data.get("updated_at", None)
100
-
101
- self.is_visible: bool | None = data.get("is_visible", None)
102
- self.is_protected: bool | None = data.get("is_protected", None)
103
-
104
- self.source: NodeProperty | None = None
105
- self.owner: NodeProperty | None = None
106
-
107
- for prop_name in self._properties_object:
108
- if data.get(prop_name):
109
- setattr(self, prop_name, NodeProperty(data=data.get(prop_name))) # type: ignore[arg-type]
110
-
111
- @property
112
- def value(self) -> Any:
113
- return self._value
114
-
115
- @value.setter
116
- def value(self, value: Any) -> None:
117
- self._value = value
118
- self.value_has_been_mutated = True
119
-
120
- def _generate_input_data(self) -> dict | None:
121
- data: dict[str, Any] = {}
122
- variables: dict[str, Any] = {}
123
-
124
- if self.value is None:
125
- return data
126
-
127
- if isinstance(self.value, str):
128
- if SAFE_VALUE.match(self.value):
129
- data["value"] = self.value
130
- else:
131
- var_name = f"value_{UUIDT.new().hex}"
132
- variables[var_name] = self.value
133
- data["value"] = f"${var_name}"
134
- elif isinstance(self.value, get_args(IP_TYPES)):
135
- data["value"] = self.value.with_prefixlen
136
- elif isinstance(self.value, InfrahubNodeBase) and self.value.is_resource_pool():
137
- data["from_pool"] = {"id": self.value.id}
138
- else:
139
- data["value"] = self.value
140
-
141
- for prop_name in self._properties_flag:
142
- if getattr(self, prop_name) is not None:
143
- data[prop_name] = getattr(self, prop_name)
144
-
145
- for prop_name in self._properties_object:
146
- if getattr(self, prop_name) is not None:
147
- data[prop_name] = getattr(self, prop_name)._generate_input_data()
148
-
149
- return {"data": data, "variables": variables}
150
-
151
- def _generate_query_data(self, property: bool = False) -> dict | None:
152
- data: dict[str, Any] = {"value": None}
153
-
154
- if property:
155
- data.update({"is_default": None, "is_from_profile": None})
156
-
157
- for prop_name in self._properties_flag:
158
- data[prop_name] = None
159
- for prop_name in self._properties_object:
160
- data[prop_name] = {"id": None, "display_label": None, "__typename": None}
161
-
162
- return data
163
-
164
- def _generate_mutation_query(self) -> dict[str, Any]:
165
- if isinstance(self.value, InfrahubNodeBase) and self.value.is_resource_pool():
166
- # If it points to a pool, ask for the value of the pool allocated resource
167
- return {self.name: {"value": None}}
168
- return {}
169
-
170
-
171
- class RelatedNodeBase:
172
- """Base class for representing a related node in a relationship."""
173
-
174
- def __init__(self, branch: str, schema: RelationshipSchemaAPI, data: Any | dict, name: str | None = None):
175
- """
176
- Args:
177
- branch (str): The branch where the related node resides.
178
- schema (RelationshipSchema): The schema of the relationship.
179
- data (Union[Any, dict]): Data representing the related node.
180
- name (Optional[str]): The name of the related node.
181
- """
182
- self.schema = schema
183
- self.name = name
184
-
185
- self._branch = branch
186
-
187
- self._properties_flag = PROPERTIES_FLAG
188
- self._properties_object = PROPERTIES_OBJECT
189
- self._properties = self._properties_flag + self._properties_object
190
-
191
- self._peer = None
192
- self._id: str | None = None
193
- self._hfid: list[str] | None = None
194
- self._display_label: str | None = None
195
- self._typename: str | None = None
196
-
197
- if isinstance(data, (InfrahubNode, InfrahubNodeSync)):
198
- self._peer = data
199
- for prop in self._properties:
200
- setattr(self, prop, None)
201
- elif isinstance(data, list):
202
- data = {"hfid": data}
203
- elif not isinstance(data, dict):
204
- data = {"id": data}
205
-
206
- if isinstance(data, dict):
207
- # To support both with and without pagination, we split data into node_data and properties_data
208
- # We should probably clean that once we'll remove the code without pagination.
209
- node_data = data.get("node", data)
210
- properties_data = data.get("properties", data)
211
-
212
- if node_data:
213
- self._id = node_data.get("id", None)
214
- self._hfid = node_data.get("hfid", None)
215
- self._kind = node_data.get("kind", None)
216
- self._display_label = node_data.get("display_label", None)
217
- self._typename = node_data.get("__typename", None)
218
-
219
- self.updated_at: str | None = data.get("updated_at", data.get("_relation__updated_at", None))
220
-
221
- # FIXME, we won't need that once we are only supporting paginated results
222
- if self._typename and self._typename.startswith("Related"):
223
- self._typename = self._typename[7:]
224
-
225
- for prop in self._properties:
226
- prop_data = properties_data.get(prop, properties_data.get(f"_relation__{prop}", None))
227
- if prop_data and isinstance(prop_data, dict) and "id" in prop_data:
228
- setattr(self, prop, prop_data["id"])
229
- elif prop_data and isinstance(prop_data, (str, bool)):
230
- setattr(self, prop, prop_data)
231
- else:
232
- setattr(self, prop, None)
233
-
234
- @property
235
- def id(self) -> str | None:
236
- if self._peer:
237
- return self._peer.id
238
- return self._id
239
-
240
- @property
241
- def hfid(self) -> list[Any] | None:
242
- if self._peer:
243
- return self._peer.hfid
244
- return self._hfid
245
-
246
- @property
247
- def hfid_str(self) -> str | None:
248
- if self._peer and self.hfid:
249
- return self._peer.get_human_friendly_id_as_string(include_kind=True)
250
- return None
251
-
252
- @property
253
- def is_resource_pool(self) -> bool:
254
- if self._peer:
255
- return self._peer.is_resource_pool()
256
- return False
257
-
258
- @property
259
- def initialized(self) -> bool:
260
- return bool(self.id) or bool(self.hfid)
261
-
262
- @property
263
- def display_label(self) -> str | None:
264
- if self._peer:
265
- return self._peer.display_label
266
- return self._display_label
267
-
268
- @property
269
- def typename(self) -> str | None:
270
- if self._peer:
271
- return self._peer.typename
272
- return self._typename
273
-
274
- def _generate_input_data(self, allocate_from_pool: bool = False) -> dict[str, Any]:
275
- data: dict[str, Any] = {}
276
-
277
- if self.is_resource_pool and allocate_from_pool:
278
- return {"from_pool": {"id": self.id}}
279
-
280
- if self.id is not None:
281
- data["id"] = self.id
282
- elif self.hfid is not None:
283
- data["hfid"] = self.hfid
284
- if self._kind is not None:
285
- data["kind"] = self._kind
286
-
287
- for prop_name in self._properties:
288
- if getattr(self, prop_name) is not None:
289
- data[f"_relation__{prop_name}"] = getattr(self, prop_name)
290
-
291
- return data
292
-
293
- def _generate_mutation_query(self) -> dict[str, Any]:
294
- if self.name and self.is_resource_pool:
295
- # If a related node points to a pool, ask for the ID of the pool allocated resource
296
- return {self.name: {"node": {"id": None, "display_label": None, "__typename": None}}}
297
- return {}
298
-
299
- @classmethod
300
- def _generate_query_data(cls, peer_data: dict[str, Any] | None = None, property: bool = False) -> dict:
301
- """Generates the basic structure of a GraphQL query for a single relationship.
302
-
303
- Args:
304
- peer_data (dict[str, Union[Any, Dict]], optional): Additional data to be included in the query for the node.
305
- This is used to add extra fields when prefetching related node data.
306
-
307
- Returns:
308
- Dict: A dictionary representing the basic structure of a GraphQL query, including the node's ID, display label,
309
- and typename. The method also includes additional properties and any peer_data provided.
310
- """
311
- data: dict[str, Any] = {"node": {"id": None, "hfid": None, "display_label": None, "__typename": None}}
312
- properties: dict[str, Any] = {}
313
-
314
- if property:
315
- for prop_name in PROPERTIES_FLAG:
316
- properties[prop_name] = None
317
- for prop_name in PROPERTIES_OBJECT:
318
- properties[prop_name] = {"id": None, "display_label": None, "__typename": None}
319
-
320
- if properties:
321
- data["properties"] = properties
322
- if peer_data:
323
- data["node"].update(peer_data)
324
-
325
- return data
326
-
327
-
328
- class RelatedNode(RelatedNodeBase):
329
- """Represents a RelatedNodeBase in an asynchronous context."""
330
-
331
- def __init__(
332
- self,
333
- client: InfrahubClient,
334
- branch: str,
335
- schema: RelationshipSchemaAPI,
336
- data: Any | dict,
337
- name: str | None = None,
338
- ):
339
- """
340
- Args:
341
- client (InfrahubClient): The client used to interact with the backend asynchronously.
342
- branch (str): The branch where the related node resides.
343
- schema (RelationshipSchema): The schema of the relationship.
344
- data (Union[Any, dict]): Data representing the related node.
345
- name (Optional[str]): The name of the related node.
346
- """
347
- self._client = client
348
- super().__init__(branch=branch, schema=schema, data=data, name=name)
349
-
350
- async def fetch(self, timeout: int | None = None) -> None:
351
- if not self.id or not self.typename:
352
- raise Error("Unable to fetch the peer, id and/or typename are not defined")
353
-
354
- self._peer = await self._client.get(
355
- kind=self.typename, id=self.id, populate_store=True, branch=self._branch, timeout=timeout
356
- )
357
-
358
- @property
359
- def peer(self) -> InfrahubNode:
360
- return self.get()
361
-
362
- def get(self) -> InfrahubNode:
363
- if self._peer:
364
- return self._peer # type: ignore[return-value]
365
-
366
- if self.id and self.typename:
367
- return self._client.store.get(key=self.id, kind=self.typename, branch=self._branch) # type: ignore[return-value]
368
-
369
- if self.hfid_str:
370
- return self._client.store.get(key=self.hfid_str, branch=self._branch) # type: ignore[return-value]
371
-
372
- raise ValueError("Node must have at least one identifier (ID or HFID) to query it.")
373
-
374
-
375
- class RelatedNodeSync(RelatedNodeBase):
376
- """Represents a related node in a synchronous context."""
377
-
378
- def __init__(
379
- self,
380
- client: InfrahubClientSync,
381
- branch: str,
382
- schema: RelationshipSchemaAPI,
383
- data: Any | dict,
384
- name: str | None = None,
385
- ):
386
- """
387
- Args:
388
- client (InfrahubClientSync): The client used to interact with the backend synchronously.
389
- branch (str): The branch where the related node resides.
390
- schema (RelationshipSchema): The schema of the relationship.
391
- data (Union[Any, dict]): Data representing the related node.
392
- name (Optional[str]): The name of the related node.
393
- """
394
- self._client = client
395
- super().__init__(branch=branch, schema=schema, data=data, name=name)
396
-
397
- def fetch(self, timeout: int | None = None) -> None:
398
- if not self.id or not self.typename:
399
- raise Error("Unable to fetch the peer, id and/or typename are not defined")
400
-
401
- self._peer = self._client.get(
402
- kind=self.typename, id=self.id, populate_store=True, branch=self._branch, timeout=timeout
403
- )
404
-
405
- @property
406
- def peer(self) -> InfrahubNodeSync:
407
- return self.get()
408
-
409
- def get(self) -> InfrahubNodeSync:
410
- if self._peer:
411
- return self._peer # type: ignore[return-value]
412
-
413
- if self.id and self.typename:
414
- return self._client.store.get(key=self.id, kind=self.typename, branch=self._branch) # type: ignore[return-value]
415
-
416
- if self.hfid_str:
417
- return self._client.store.get(key=self.hfid_str, branch=self._branch) # type: ignore[return-value]
418
-
419
- raise ValueError("Node must have at least one identifier (ID or HFID) to query it.")
420
-
421
-
422
- class RelationshipManagerBase:
423
- """Base class for RelationshipManager and RelationshipManagerSync"""
424
-
425
- def __init__(self, name: str, branch: str, schema: RelationshipSchemaAPI):
426
- """
427
- Args:
428
- name (str): The name of the relationship.
429
- branch (str): The branch where the relationship resides.
430
- schema (RelationshipSchema): The schema of the relationship.
431
- """
432
- self.initialized: bool = False
433
- self._has_update: bool = False
434
- self.name = name
435
- self.schema = schema
436
- self.branch = branch
437
-
438
- self._properties_flag = PROPERTIES_FLAG
439
- self._properties_object = PROPERTIES_OBJECT
440
- self._properties = self._properties_flag + self._properties_object
441
-
442
- self.peers: list[RelatedNode | RelatedNodeSync] = []
443
-
444
- @property
445
- def peer_ids(self) -> list[str]:
446
- return [peer.id for peer in self.peers if peer.id]
447
-
448
- @property
449
- def peer_hfids(self) -> list[list[Any]]:
450
- return [peer.hfid for peer in self.peers if peer.hfid]
451
-
452
- @property
453
- def peer_hfids_str(self) -> list[str]:
454
- return [peer.hfid_str for peer in self.peers if peer.hfid_str]
455
-
456
- @property
457
- def has_update(self) -> bool:
458
- return self._has_update
459
-
460
- def _generate_input_data(self, allocate_from_pool: bool = False) -> list[dict]:
461
- return [peer._generate_input_data(allocate_from_pool=allocate_from_pool) for peer in self.peers]
462
-
463
- def _generate_mutation_query(self) -> dict[str, Any]:
464
- # Does nothing for now
465
- return {}
466
-
467
- @classmethod
468
- def _generate_query_data(cls, peer_data: dict[str, Any] | None = None, property: bool = False) -> dict:
469
- """Generates the basic structure of a GraphQL query for relationships with multiple nodes.
470
-
471
- Args:
472
- peer_data (dict[str, Union[Any, Dict]], optional): Additional data to be included in the query for each node.
473
- This is used to add extra fields when prefetching related node data in many-to-many relationships.
474
-
475
- Returns:
476
- Dict: A dictionary representing the basic structure of a GraphQL query for multiple related nodes.
477
- It includes count, edges, and node information (ID, display label, and typename), along with additional properties
478
- and any peer_data provided.
479
- """
480
- data: dict[str, Any] = {
481
- "count": None,
482
- "edges": {"node": {"id": None, "hfid": None, "display_label": None, "__typename": None}},
483
- }
484
-
485
- properties: dict[str, Any] = {}
486
- if property:
487
- for prop_name in PROPERTIES_FLAG:
488
- properties[prop_name] = None
489
- for prop_name in PROPERTIES_OBJECT:
490
- properties[prop_name] = {"id": None, "display_label": None, "__typename": None}
491
-
492
- if properties:
493
- data["edges"]["properties"] = properties
494
- if peer_data:
495
- data["edges"]["node"].update(peer_data)
496
-
497
- return data
498
-
499
-
500
- class RelationshipManager(RelationshipManagerBase):
501
- """Manages relationships of a node in an asynchronous context."""
502
-
503
- def __init__(
504
- self,
505
- name: str,
506
- client: InfrahubClient,
507
- node: InfrahubNode,
508
- branch: str,
509
- schema: RelationshipSchemaAPI,
510
- data: Any | dict,
511
- ):
512
- """
513
- Args:
514
- name (str): The name of the relationship.
515
- client (InfrahubClient): The client used to interact with the backend.
516
- node (InfrahubNode): The node to which the relationship belongs.
517
- branch (str): The branch where the relationship resides.
518
- schema (RelationshipSchema): The schema of the relationship.
519
- data (Union[Any, dict]): Initial data for the relationships.
520
- """
521
- self.client = client
522
- self.node = node
523
-
524
- super().__init__(name=name, schema=schema, branch=branch)
525
-
526
- self.initialized = data is not None
527
- self._has_update = False
528
-
529
- if data is None:
530
- return
531
-
532
- if isinstance(data, list):
533
- for item in data:
534
- self.peers.append(
535
- RelatedNode(name=name, client=self.client, branch=self.branch, schema=schema, data=item)
536
- )
537
- elif isinstance(data, dict) and "edges" in data:
538
- for item in data["edges"]:
539
- self.peers.append(
540
- RelatedNode(name=name, client=self.client, branch=self.branch, schema=schema, data=item)
541
- )
542
- else:
543
- raise ValueError(f"Unexpected format for {name} found a {type(data)}, {data}")
544
-
545
- def __getitem__(self, item: int) -> RelatedNode:
546
- return self.peers[item] # type: ignore[return-value]
547
-
548
- async def fetch(self) -> None:
549
- if not self.initialized:
550
- exclude = self.node._schema.relationship_names + self.node._schema.attribute_names
551
- exclude.remove(self.schema.name)
552
- node = await self.client.get(
553
- kind=self.node._schema.kind,
554
- id=self.node.id,
555
- branch=self.branch,
556
- include=[self.schema.name],
557
- exclude=exclude,
558
- )
559
- rm = getattr(node, self.schema.name)
560
- self.peers = rm.peers
561
- self.initialized = True
562
-
563
- for peer in self.peers:
564
- await peer.fetch() # type: ignore[misc]
565
-
566
- def add(self, data: str | RelatedNode | dict) -> None:
567
- """Add a new peer to this relationship."""
568
- if not self.initialized:
569
- raise UninitializedError("Must call fetch() on RelationshipManager before editing members")
570
- new_node = RelatedNode(schema=self.schema, client=self.client, branch=self.branch, data=data)
571
-
572
- if (new_node.id and new_node.id not in self.peer_ids) or (
573
- new_node.hfid and new_node.hfid not in self.peer_hfids
574
- ):
575
- self.peers.append(new_node)
576
- self._has_update = True
577
-
578
- def extend(self, data: Iterable[str | RelatedNode | dict]) -> None:
579
- """Add new peers to this relationship."""
580
- for d in data:
581
- self.add(d)
582
-
583
- def remove(self, data: str | RelatedNode | dict) -> None:
584
- if not self.initialized:
585
- raise UninitializedError("Must call fetch() on RelationshipManager before editing members")
586
- node_to_remove = RelatedNode(schema=self.schema, client=self.client, branch=self.branch, data=data)
587
-
588
- if node_to_remove.id and node_to_remove.id in self.peer_ids:
589
- idx = self.peer_ids.index(node_to_remove.id)
590
- if self.peers[idx].id != node_to_remove.id:
591
- raise IndexError(f"Unexpected situation, the node with the index {idx} should be {node_to_remove.id}")
592
-
593
- self.peers.pop(idx)
594
- self._has_update = True
595
-
596
- elif node_to_remove.hfid and node_to_remove.hfid in self.peer_hfids:
597
- idx = self.peer_hfids.index(node_to_remove.hfid)
598
- if self.peers[idx].hfid != node_to_remove.hfid:
599
- raise IndexError(f"Unexpected situation, the node with the index {idx} should be {node_to_remove.hfid}")
600
-
601
- self.peers.pop(idx)
602
- self._has_update = True
603
-
604
-
605
- class RelationshipManagerSync(RelationshipManagerBase):
606
- """Manages relationships of a node in a synchronous context."""
607
-
608
- def __init__(
609
- self,
610
- name: str,
611
- client: InfrahubClientSync,
612
- node: InfrahubNodeSync,
613
- branch: str,
614
- schema: RelationshipSchemaAPI,
615
- data: Any | dict,
616
- ):
617
- """
618
- Args:
619
- name (str): The name of the relationship.
620
- client (InfrahubClientSync): The client used to interact with the backend synchronously.
621
- node (InfrahubNodeSync): The node to which the relationship belongs.
622
- branch (str): The branch where the relationship resides.
623
- schema (RelationshipSchema): The schema of the relationship.
624
- data (Union[Any, dict]): Initial data for the relationships.
625
- """
626
- self.client = client
627
- self.node = node
628
-
629
- super().__init__(name=name, schema=schema, branch=branch)
630
-
631
- self.initialized = data is not None
632
- self._has_update = False
633
-
634
- if data is None:
635
- return
636
-
637
- if isinstance(data, list):
638
- for item in data:
639
- self.peers.append(
640
- RelatedNodeSync(name=name, client=self.client, branch=self.branch, schema=schema, data=item)
641
- )
642
- elif isinstance(data, dict) and "edges" in data:
643
- for item in data["edges"]:
644
- self.peers.append(
645
- RelatedNodeSync(name=name, client=self.client, branch=self.branch, schema=schema, data=item)
646
- )
647
- else:
648
- raise ValueError(f"Unexpected format for {name} found a {type(data)}, {data}")
649
-
650
- def __getitem__(self, item: int) -> RelatedNodeSync:
651
- return self.peers[item] # type: ignore[return-value]
652
-
653
- def fetch(self) -> None:
654
- if not self.initialized:
655
- exclude = self.node._schema.relationship_names + self.node._schema.attribute_names
656
- exclude.remove(self.schema.name)
657
- node = self.client.get(
658
- kind=self.node._schema.kind,
659
- id=self.node.id,
660
- branch=self.branch,
661
- include=[self.schema.name],
662
- exclude=exclude,
663
- )
664
- rm = getattr(node, self.schema.name)
665
- self.peers = rm.peers
666
- self.initialized = True
667
-
668
- for peer in self.peers:
669
- peer.fetch()
670
-
671
- def add(self, data: str | RelatedNodeSync | dict) -> None:
672
- """Add a new peer to this relationship."""
673
- if not self.initialized:
674
- raise UninitializedError("Must call fetch() on RelationshipManager before editing members")
675
- new_node = RelatedNodeSync(schema=self.schema, client=self.client, branch=self.branch, data=data)
676
-
677
- if (new_node.id and new_node.id not in self.peer_ids) or (
678
- new_node.hfid and new_node.hfid not in self.peer_hfids
679
- ):
680
- self.peers.append(new_node)
681
- self._has_update = True
682
-
683
- def extend(self, data: Iterable[str | RelatedNodeSync | dict]) -> None:
684
- """Add new peers to this relationship."""
685
- for d in data:
686
- self.add(d)
687
-
688
- def remove(self, data: str | RelatedNodeSync | dict) -> None:
689
- if not self.initialized:
690
- raise UninitializedError("Must call fetch() on RelationshipManager before editing members")
691
- node_to_remove = RelatedNodeSync(schema=self.schema, client=self.client, branch=self.branch, data=data)
692
-
693
- if node_to_remove.id and node_to_remove.id in self.peer_ids:
694
- idx = self.peer_ids.index(node_to_remove.id)
695
- if self.peers[idx].id != node_to_remove.id:
696
- raise IndexError(f"Unexpected situation, the node with the index {idx} should be {node_to_remove.id}")
697
- self.peers.pop(idx)
698
- self._has_update = True
699
-
700
- elif node_to_remove.hfid and node_to_remove.hfid in self.peer_hfids:
701
- idx = self.peer_hfids.index(node_to_remove.hfid)
702
- if self.peers[idx].hfid != node_to_remove.hfid:
703
- raise IndexError(f"Unexpected situation, the node with the index {idx} should be {node_to_remove.hfid}")
704
-
705
- self.peers.pop(idx)
706
- self._has_update = True
25
+ from ..client import InfrahubClient, InfrahubClientSync
26
+ from ..context import RequestContext
27
+ from ..schema import MainSchemaTypesAPI
28
+ from ..types import Order
707
29
 
708
30
 
709
31
  class InfrahubNodeBase:
@@ -720,6 +42,7 @@ class InfrahubNodeBase:
720
42
  self._data = data
721
43
  self._branch = branch
722
44
  self._existing: bool = True
45
+ self._attribute_data: dict[str, Attribute] = {}
723
46
 
724
47
  # Generate a unique ID only to be used inside the SDK
725
48
  # The format if this ID is purposely different from the ID used by the API
@@ -814,12 +137,18 @@ class InfrahubNodeBase:
814
137
  def _init_attributes(self, data: dict | None = None) -> None:
815
138
  for attr_schema in self._schema.attributes:
816
139
  attr_data = data.get(attr_schema.name, None) if isinstance(data, dict) else None
817
- setattr(
818
- self,
819
- attr_schema.name,
820
- Attribute(name=attr_schema.name, schema=attr_schema, data=attr_data),
140
+ self._attribute_data[attr_schema.name] = Attribute(
141
+ name=attr_schema.name, schema=attr_schema, data=attr_data
821
142
  )
822
143
 
144
+ def __setattr__(self, name: str, value: Any) -> None:
145
+ """Set values for attributes that exist or revert to normal behaviour"""
146
+ if "_attribute_data" in self.__dict__ and name in self._attribute_data:
147
+ self._attribute_data[name].value = value
148
+ return
149
+
150
+ super().__setattr__(name, value)
151
+
823
152
  def _get_request_context(self, request_context: RequestContext | None = None) -> dict[str, Any] | None:
824
153
  if request_context:
825
154
  return request_context.model_dump(exclude_none=True)
@@ -1121,6 +450,12 @@ class InfrahubNodeBase:
1121
450
  }}
1122
451
  """
1123
452
 
453
+ def _get_attribute(self, name: str) -> Attribute:
454
+ if name in self._attribute_data:
455
+ return self._attribute_data[name]
456
+
457
+ raise ResourceNotDefinedError(message=f"The node doesn't have an attribute for {name}")
458
+
1124
459
 
1125
460
  class InfrahubNode(InfrahubNodeBase):
1126
461
  """Represents a Infrahub node in an asynchronous context."""
@@ -1140,11 +475,13 @@ class InfrahubNode(InfrahubNodeBase):
1140
475
  data: Optional data to initialize the node.
1141
476
  """
1142
477
  self._client = client
1143
- self.__class__ = type(f"{schema.kind}InfrahubNode", (self.__class__,), {})
1144
478
 
1145
479
  if isinstance(data, dict) and isinstance(data.get("node"), dict):
1146
480
  data = data.get("node")
1147
481
 
482
+ self._relationship_cardinality_many_data: dict[str, RelationshipManager] = {}
483
+ self._relationship_cardinality_one_data: dict[str, RelatedNode] = {}
484
+
1148
485
  super().__init__(schema=schema, branch=branch or client.default_branch, data=data)
1149
486
 
1150
487
  @classmethod
@@ -1169,27 +506,46 @@ class InfrahubNode(InfrahubNodeBase):
1169
506
  rel_data = data.get(rel_schema.name, None) if isinstance(data, dict) else None
1170
507
 
1171
508
  if rel_schema.cardinality == "one":
1172
- setattr(self, f"_{rel_schema.name}", None)
1173
- setattr(
1174
- self.__class__,
1175
- rel_schema.name,
1176
- generate_relationship_property(name=rel_schema.name, node=self),
509
+ self._relationship_cardinality_one_data[rel_schema.name] = RelatedNode(
510
+ name=rel_schema.name, branch=self._branch, client=self._client, schema=rel_schema, data=rel_data
1177
511
  )
1178
- setattr(self, rel_schema.name, rel_data)
1179
512
  else:
1180
- setattr(
1181
- self,
1182
- rel_schema.name,
1183
- RelationshipManager(
1184
- name=rel_schema.name,
1185
- client=self._client,
1186
- node=self,
1187
- branch=self._branch,
1188
- schema=rel_schema,
1189
- data=rel_data,
1190
- ),
513
+ self._relationship_cardinality_many_data[rel_schema.name] = RelationshipManager(
514
+ name=rel_schema.name,
515
+ client=self._client,
516
+ node=self,
517
+ branch=self._branch,
518
+ schema=rel_schema,
519
+ data=rel_data,
1191
520
  )
1192
521
 
522
+ def __getattr__(self, name: str) -> Attribute | RelationshipManager | RelatedNode:
523
+ if "_attribute_data" in self.__dict__ and name in self._attribute_data:
524
+ return self._attribute_data[name]
525
+ if "_relationship_cardinality_many_data" in self.__dict__ and name in self._relationship_cardinality_many_data:
526
+ return self._relationship_cardinality_many_data[name]
527
+ if "_relationship_cardinality_one_data" in self.__dict__ and name in self._relationship_cardinality_one_data:
528
+ return self._relationship_cardinality_one_data[name]
529
+
530
+ raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
531
+
532
+ def __setattr__(self, name: str, value: Any) -> None:
533
+ """Set values for relationship names that exist or revert to normal behaviour"""
534
+ if "_relationship_cardinality_one_data" in self.__dict__ and name in self._relationship_cardinality_one_data:
535
+ rel_schemas = [rel_schema for rel_schema in self._schema.relationships if rel_schema.name == name]
536
+ if not rel_schemas:
537
+ raise SchemaNotFoundError(
538
+ identifier=self._schema.kind,
539
+ message=f"Unable to find relationship schema for '{name}' on {self._schema.kind}",
540
+ )
541
+ rel_schema = rel_schemas[0]
542
+ self._relationship_cardinality_one_data[name] = RelatedNode(
543
+ name=rel_schema.name, branch=self._branch, client=self._client, schema=rel_schema, data=value
544
+ )
545
+ return
546
+
547
+ super().__setattr__(name, value)
548
+
1193
549
  async def generate(self, nodes: list[str] | None = None) -> None:
1194
550
  self._validate_artifact_definition_support(ARTIFACT_DEFINITION_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE)
1195
551
 
@@ -1202,14 +558,14 @@ class InfrahubNode(InfrahubNodeBase):
1202
558
  self._validate_artifact_support(ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE)
1203
559
 
1204
560
  artifact = await self._client.get(kind="CoreArtifact", name__value=name, object__ids=[self.id])
1205
- await artifact.definition.fetch() # type: ignore[attr-defined]
1206
- await artifact.definition.peer.generate([artifact.id]) # type: ignore[attr-defined]
561
+ await artifact._get_relationship_one(name="definition").fetch()
562
+ await artifact._get_relationship_one(name="definition").peer.generate([artifact.id])
1207
563
 
1208
564
  async def artifact_fetch(self, name: str) -> str | dict[str, Any]:
1209
565
  self._validate_artifact_support(ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE)
1210
566
 
1211
567
  artifact = await self._client.get(kind="CoreArtifact", name__value=name, object__ids=[self.id])
1212
- content = await self._client.object_store.get(identifier=artifact.storage_id.value) # type: ignore[attr-defined]
568
+ content = await self._client.object_store.get(identifier=artifact._get_attribute(name="storage_id").value)
1213
569
  return content
1214
570
 
1215
571
  async def delete(self, timeout: int | None = None, request_context: RequestContext | None = None) -> None:
@@ -1652,6 +1008,27 @@ class InfrahubNode(InfrahubNodeBase):
1652
1008
  return [edge["node"] for edge in response[graphql_query_name]["edges"]]
1653
1009
  return []
1654
1010
 
1011
+ def _get_relationship_many(self, name: str) -> RelationshipManager:
1012
+ if name in self._relationship_cardinality_many_data:
1013
+ return self._relationship_cardinality_many_data[name]
1014
+
1015
+ raise ResourceNotDefinedError(message=f"The node doesn't have a cardinality=many relationship for {name}")
1016
+
1017
+ def _get_relationship_one(self, name: str) -> RelatedNode:
1018
+ if name in self._relationship_cardinality_one_data:
1019
+ return self._relationship_cardinality_one_data[name]
1020
+
1021
+ raise ResourceNotDefinedError(message=f"The node doesn't have a cardinality=one relationship for {name}")
1022
+
1023
+ def __dir__(self) -> Iterable[str]:
1024
+ base = list(super().__dir__())
1025
+ return sorted(
1026
+ base
1027
+ + list(self._attribute_data.keys())
1028
+ + list(self._relationship_cardinality_many_data.keys())
1029
+ + list(self._relationship_cardinality_one_data.keys())
1030
+ )
1031
+
1655
1032
 
1656
1033
  class InfrahubNodeSync(InfrahubNodeBase):
1657
1034
  """Represents a Infrahub node in a synchronous context."""
@@ -1670,12 +1047,14 @@ class InfrahubNodeSync(InfrahubNodeBase):
1670
1047
  branch (Optional[str]): The branch where the node resides.
1671
1048
  data (Optional[dict]): Optional data to initialize the node.
1672
1049
  """
1673
- self.__class__ = type(f"{schema.kind}InfrahubNodeSync", (self.__class__,), {})
1674
1050
  self._client = client
1675
1051
 
1676
1052
  if isinstance(data, dict) and isinstance(data.get("node"), dict):
1677
1053
  data = data.get("node")
1678
1054
 
1055
+ self._relationship_cardinality_many_data: dict[str, RelationshipManagerSync] = {}
1056
+ self._relationship_cardinality_one_data: dict[str, RelatedNodeSync] = {}
1057
+
1679
1058
  super().__init__(schema=schema, branch=branch or client.default_branch, data=data)
1680
1059
 
1681
1060
  @classmethod
@@ -1700,26 +1079,46 @@ class InfrahubNodeSync(InfrahubNodeBase):
1700
1079
  rel_data = data.get(rel_schema.name, None) if isinstance(data, dict) else None
1701
1080
 
1702
1081
  if rel_schema.cardinality == "one":
1703
- setattr(self, f"_{rel_schema.name}", None)
1704
- setattr(
1705
- self.__class__,
1706
- rel_schema.name,
1707
- generate_relationship_property(name=rel_schema.name, node=self),
1082
+ self._relationship_cardinality_one_data[rel_schema.name] = RelatedNodeSync(
1083
+ name=rel_schema.name, branch=self._branch, client=self._client, schema=rel_schema, data=rel_data
1708
1084
  )
1709
- setattr(self, rel_schema.name, rel_data)
1085
+
1710
1086
  else:
1711
- setattr(
1712
- self,
1713
- rel_schema.name,
1714
- RelationshipManagerSync(
1715
- name=rel_schema.name,
1716
- client=self._client,
1717
- node=self,
1718
- branch=self._branch,
1719
- schema=rel_schema,
1720
- data=rel_data,
1721
- ),
1087
+ self._relationship_cardinality_many_data[rel_schema.name] = RelationshipManagerSync(
1088
+ name=rel_schema.name,
1089
+ client=self._client,
1090
+ node=self,
1091
+ branch=self._branch,
1092
+ schema=rel_schema,
1093
+ data=rel_data,
1094
+ )
1095
+
1096
+ def __getattr__(self, name: str) -> Attribute | RelationshipManagerSync | RelatedNodeSync:
1097
+ if "_attribute_data" in self.__dict__ and name in self._attribute_data:
1098
+ return self._attribute_data[name]
1099
+ if "_relationship_cardinality_many_data" in self.__dict__ and name in self._relationship_cardinality_many_data:
1100
+ return self._relationship_cardinality_many_data[name]
1101
+ if "_relationship_cardinality_one_data" in self.__dict__ and name in self._relationship_cardinality_one_data:
1102
+ return self._relationship_cardinality_one_data[name]
1103
+
1104
+ raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
1105
+
1106
+ def __setattr__(self, name: str, value: Any) -> None:
1107
+ """Set values for relationship names that exist or revert to normal behaviour"""
1108
+ if "_relationship_cardinality_one_data" in self.__dict__ and name in self._relationship_cardinality_one_data:
1109
+ rel_schemas = [rel_schema for rel_schema in self._schema.relationships if rel_schema.name == name]
1110
+ if not rel_schemas:
1111
+ raise SchemaNotFoundError(
1112
+ identifier=self._schema.kind,
1113
+ message=f"Unable to find relationship schema for '{name}' on {self._schema.kind}",
1722
1114
  )
1115
+ rel_schema = rel_schemas[0]
1116
+ self._relationship_cardinality_one_data[name] = RelatedNodeSync(
1117
+ name=rel_schema.name, branch=self._branch, client=self._client, schema=rel_schema, data=value
1118
+ )
1119
+ return
1120
+
1121
+ super().__setattr__(name, value)
1723
1122
 
1724
1123
  def generate(self, nodes: list[str] | None = None) -> None:
1725
1124
  self._validate_artifact_definition_support(ARTIFACT_DEFINITION_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE)
@@ -1731,13 +1130,13 @@ class InfrahubNodeSync(InfrahubNodeBase):
1731
1130
  def artifact_generate(self, name: str) -> None:
1732
1131
  self._validate_artifact_support(ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE)
1733
1132
  artifact = self._client.get(kind="CoreArtifact", name__value=name, object__ids=[self.id])
1734
- artifact.definition.fetch() # type: ignore[attr-defined]
1735
- artifact.definition.peer.generate([artifact.id]) # type: ignore[attr-defined]
1133
+ artifact._get_relationship_one(name="definition").fetch()
1134
+ artifact._get_relationship_one(name="definition").peer.generate([artifact.id])
1736
1135
 
1737
1136
  def artifact_fetch(self, name: str) -> str | dict[str, Any]:
1738
1137
  self._validate_artifact_support(ARTIFACT_FETCH_FEATURE_NOT_SUPPORTED_MESSAGE)
1739
1138
  artifact = self._client.get(kind="CoreArtifact", name__value=name, object__ids=[self.id])
1740
- content = self._client.object_store.get(identifier=artifact.storage_id.value) # type: ignore[attr-defined]
1139
+ content = self._client.object_store.get(identifier=artifact._get_attribute(name="storage_id").value)
1741
1140
  return content
1742
1141
 
1743
1142
  def delete(self, timeout: int | None = None, request_context: RequestContext | None = None) -> None:
@@ -2180,67 +1579,23 @@ class InfrahubNodeSync(InfrahubNodeBase):
2180
1579
  return [edge["node"] for edge in response[graphql_query_name]["edges"]]
2181
1580
  return []
2182
1581
 
1582
+ def _get_relationship_many(self, name: str) -> RelationshipManager | RelationshipManagerSync:
1583
+ if name in self._relationship_cardinality_many_data:
1584
+ return self._relationship_cardinality_many_data[name]
2183
1585
 
2184
- class NodeProperty:
2185
- """Represents a property of a node, typically used for metadata like display labels."""
1586
+ raise ResourceNotDefinedError(message=f"The node doesn't have a cardinality=many relationship for {name}")
2186
1587
 
2187
- def __init__(self, data: dict | str):
2188
- """
2189
- Args:
2190
- data (Union[dict, str]): Data representing the node property.
2191
- """
2192
- self.id = None
2193
- self.display_label = None
2194
- self.typename = None
2195
-
2196
- if isinstance(data, str):
2197
- self.id = data
2198
- elif isinstance(data, dict):
2199
- self.id = data.get("id", None)
2200
- self.display_label = data.get("display_label", None)
2201
- self.typename = data.get("__typename", None)
2202
-
2203
- def _generate_input_data(self) -> str | None:
2204
- return self.id
2205
-
2206
-
2207
- def generate_relationship_property(node: InfrahubNode | InfrahubNodeSync, name: str) -> property:
2208
- """Generates a property that stores values under a private non-public name.
2209
-
2210
- Args:
2211
- node (Union[InfrahubNode, InfrahubNodeSync]): The node instance.
2212
- name (str): The name of the relationship property.
1588
+ def _get_relationship_one(self, name: str) -> RelatedNode | RelatedNodeSync:
1589
+ if name in self._relationship_cardinality_one_data:
1590
+ return self._relationship_cardinality_one_data[name]
2213
1591
 
2214
- Returns:
2215
- A property object for managing the relationship.
1592
+ raise ResourceNotDefinedError(message=f"The node doesn't have a cardinality=one relationship for {name}")
2216
1593
 
2217
- """
2218
- internal_name = "_" + name.lower()
2219
- external_name = name
2220
-
2221
- def prop_getter(self: InfrahubNodeBase) -> Any:
2222
- return getattr(self, internal_name)
2223
-
2224
- def prop_setter(self: InfrahubNodeBase, value: Any) -> None:
2225
- if isinstance(value, RelatedNodeBase) or value is None:
2226
- setattr(self, internal_name, value)
2227
- else:
2228
- schema = [rel for rel in self._schema.relationships if rel.name == external_name][0]
2229
- if isinstance(node, InfrahubNode):
2230
- setattr(
2231
- self,
2232
- internal_name,
2233
- RelatedNode(
2234
- name=external_name, branch=node._branch, client=node._client, schema=schema, data=value
2235
- ),
2236
- )
2237
- else:
2238
- setattr(
2239
- self,
2240
- internal_name,
2241
- RelatedNodeSync(
2242
- name=external_name, branch=node._branch, client=node._client, schema=schema, data=value
2243
- ),
2244
- )
2245
-
2246
- return property(prop_getter, prop_setter)
1594
+ def __dir__(self) -> Iterable[str]:
1595
+ base = list(super().__dir__())
1596
+ return sorted(
1597
+ base
1598
+ + list(self._attribute_data.keys())
1599
+ + list(self._relationship_cardinality_many_data.keys())
1600
+ + list(self._relationship_cardinality_one_data.keys())
1601
+ )