infrahub-server 1.2.11__py3-none-any.whl → 1.3.0b1__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 (147) hide show
  1. infrahub/actions/constants.py +86 -0
  2. infrahub/actions/gather.py +114 -0
  3. infrahub/actions/models.py +241 -0
  4. infrahub/actions/parsers.py +104 -0
  5. infrahub/actions/schema.py +382 -0
  6. infrahub/actions/tasks.py +126 -0
  7. infrahub/actions/triggers.py +21 -0
  8. infrahub/cli/db.py +1 -2
  9. infrahub/core/account.py +24 -47
  10. infrahub/core/attribute.py +13 -15
  11. infrahub/core/constants/__init__.py +5 -0
  12. infrahub/core/constants/infrahubkind.py +9 -0
  13. infrahub/core/convert_object_type/__init__.py +0 -0
  14. infrahub/core/convert_object_type/conversion.py +122 -0
  15. infrahub/core/convert_object_type/schema_mapping.py +56 -0
  16. infrahub/core/diff/query/all_conflicts.py +1 -5
  17. infrahub/core/diff/query/artifact.py +10 -20
  18. infrahub/core/diff/query/diff_get.py +3 -6
  19. infrahub/core/diff/query/field_summary.py +2 -4
  20. infrahub/core/diff/query/merge.py +70 -123
  21. infrahub/core/diff/query/save.py +20 -32
  22. infrahub/core/diff/query/summary_counts_enricher.py +34 -54
  23. infrahub/core/manager.py +14 -11
  24. infrahub/core/migrations/graph/m003_relationship_parent_optional.py +1 -2
  25. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +2 -4
  26. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +11 -22
  27. infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -6
  28. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +1 -2
  29. infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +1 -2
  30. infrahub/core/migrations/query/attribute_add.py +1 -2
  31. infrahub/core/migrations/query/attribute_rename.py +5 -10
  32. infrahub/core/migrations/query/delete_element_in_schema.py +19 -17
  33. infrahub/core/migrations/query/node_duplicate.py +19 -21
  34. infrahub/core/migrations/query/relationship_duplicate.py +19 -17
  35. infrahub/core/migrations/schema/node_attribute_remove.py +4 -8
  36. infrahub/core/migrations/schema/node_remove.py +19 -19
  37. infrahub/core/models.py +29 -2
  38. infrahub/core/node/__init__.py +90 -18
  39. infrahub/core/node/create.py +211 -0
  40. infrahub/core/node/resource_manager/number_pool.py +31 -5
  41. infrahub/core/node/standard.py +6 -1
  42. infrahub/core/protocols.py +56 -0
  43. infrahub/core/protocols_base.py +3 -0
  44. infrahub/core/query/__init__.py +2 -2
  45. infrahub/core/query/diff.py +19 -32
  46. infrahub/core/query/ipam.py +10 -20
  47. infrahub/core/query/node.py +28 -46
  48. infrahub/core/query/relationship.py +53 -32
  49. infrahub/core/query/resource_manager.py +1 -2
  50. infrahub/core/query/subquery.py +2 -4
  51. infrahub/core/relationship/model.py +3 -0
  52. infrahub/core/schema/__init__.py +2 -1
  53. infrahub/core/schema/attribute_parameters.py +160 -0
  54. infrahub/core/schema/attribute_schema.py +111 -8
  55. infrahub/core/schema/basenode_schema.py +25 -1
  56. infrahub/core/schema/definitions/core/__init__.py +29 -1
  57. infrahub/core/schema/definitions/core/group.py +45 -0
  58. infrahub/core/schema/definitions/internal.py +27 -4
  59. infrahub/core/schema/generated/attribute_schema.py +16 -3
  60. infrahub/core/schema/manager.py +3 -0
  61. infrahub/core/schema/schema_branch.py +67 -7
  62. infrahub/core/validators/__init__.py +13 -1
  63. infrahub/core/validators/attribute/choices.py +1 -3
  64. infrahub/core/validators/attribute/enum.py +1 -3
  65. infrahub/core/validators/attribute/kind.py +1 -3
  66. infrahub/core/validators/attribute/length.py +13 -7
  67. infrahub/core/validators/attribute/min_max.py +118 -0
  68. infrahub/core/validators/attribute/number_pool.py +106 -0
  69. infrahub/core/validators/attribute/optional.py +1 -4
  70. infrahub/core/validators/attribute/regex.py +5 -6
  71. infrahub/core/validators/attribute/unique.py +1 -3
  72. infrahub/core/validators/determiner.py +18 -2
  73. infrahub/core/validators/enum.py +12 -0
  74. infrahub/core/validators/node/hierarchy.py +3 -6
  75. infrahub/core/validators/query.py +1 -3
  76. infrahub/core/validators/relationship/count.py +6 -12
  77. infrahub/core/validators/relationship/optional.py +2 -4
  78. infrahub/core/validators/relationship/peer.py +3 -8
  79. infrahub/core/validators/uniqueness/query.py +5 -9
  80. infrahub/database/__init__.py +11 -2
  81. infrahub/events/group_action.py +1 -0
  82. infrahub/git/base.py +5 -3
  83. infrahub/git/integrator.py +102 -3
  84. infrahub/graphql/analyzer.py +139 -18
  85. infrahub/graphql/manager.py +4 -0
  86. infrahub/graphql/mutations/action.py +164 -0
  87. infrahub/graphql/mutations/convert_object_type.py +62 -0
  88. infrahub/graphql/mutations/main.py +24 -175
  89. infrahub/graphql/mutations/proposed_change.py +20 -17
  90. infrahub/graphql/mutations/resource_manager.py +62 -6
  91. infrahub/graphql/queries/convert_object_type_mapping.py +36 -0
  92. infrahub/graphql/queries/resource_manager.py +7 -1
  93. infrahub/graphql/schema.py +6 -0
  94. infrahub/menu/menu.py +31 -0
  95. infrahub/message_bus/messages/__init__.py +0 -10
  96. infrahub/message_bus/operations/__init__.py +0 -8
  97. infrahub/patch/queries/consolidate_duplicated_nodes.py +3 -6
  98. infrahub/patch/queries/delete_duplicated_edges.py +5 -10
  99. infrahub/pools/number.py +5 -3
  100. infrahub/prefect_server/models.py +1 -19
  101. infrahub/proposed_change/models.py +68 -3
  102. infrahub/proposed_change/tasks.py +907 -30
  103. infrahub/task_manager/models.py +10 -6
  104. infrahub/trigger/catalogue.py +2 -0
  105. infrahub/trigger/models.py +18 -2
  106. infrahub/trigger/tasks.py +3 -1
  107. infrahub/types.py +6 -0
  108. infrahub/workflows/catalogue.py +76 -0
  109. infrahub_sdk/client.py +43 -10
  110. infrahub_sdk/node/__init__.py +39 -0
  111. infrahub_sdk/node/attribute.py +122 -0
  112. infrahub_sdk/node/constants.py +21 -0
  113. infrahub_sdk/{node.py → node/node.py} +50 -749
  114. infrahub_sdk/node/parsers.py +15 -0
  115. infrahub_sdk/node/property.py +24 -0
  116. infrahub_sdk/node/related_node.py +266 -0
  117. infrahub_sdk/node/relationship.py +302 -0
  118. infrahub_sdk/protocols.py +112 -0
  119. infrahub_sdk/protocols_base.py +34 -2
  120. infrahub_sdk/query_groups.py +13 -2
  121. infrahub_sdk/schema/main.py +1 -0
  122. infrahub_sdk/schema/repository.py +16 -0
  123. infrahub_sdk/spec/object.py +1 -1
  124. infrahub_sdk/store.py +1 -1
  125. infrahub_sdk/testing/schemas/car_person.py +1 -0
  126. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/METADATA +4 -4
  127. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/RECORD +134 -122
  128. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/WHEEL +1 -1
  129. infrahub_testcontainers/container.py +0 -1
  130. infrahub_testcontainers/docker-compose.test.yml +1 -1
  131. infrahub_testcontainers/helpers.py +8 -2
  132. infrahub/message_bus/messages/check_generator_run.py +0 -26
  133. infrahub/message_bus/messages/finalize_validator_execution.py +0 -15
  134. infrahub/message_bus/messages/proposed_change/base_with_diff.py +0 -16
  135. infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +0 -11
  136. infrahub/message_bus/messages/request_generatordefinition_check.py +0 -20
  137. infrahub/message_bus/messages/request_proposedchange_pipeline.py +0 -23
  138. infrahub/message_bus/operations/check/__init__.py +0 -3
  139. infrahub/message_bus/operations/check/generator.py +0 -156
  140. infrahub/message_bus/operations/finalize/__init__.py +0 -3
  141. infrahub/message_bus/operations/finalize/validator.py +0 -133
  142. infrahub/message_bus/operations/requests/__init__.py +0 -9
  143. infrahub/message_bus/operations/requests/generator_definition.py +0 -140
  144. infrahub/message_bus/operations/requests/proposed_change.py +0 -629
  145. /infrahub/{message_bus/messages/proposed_change → actions}/__init__.py +0 -0
  146. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/LICENSE.txt +0 -0
  147. {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/entry_points.txt +0 -0
@@ -1,709 +1,75 @@
1
1
  from __future__ import annotations
2
2
 
3
- import ipaddress
4
- import re
5
- from collections.abc import Iterable
6
3
  from copy import copy
7
- from typing import TYPE_CHECKING, Any, Callable, Union, get_args
4
+ from typing import TYPE_CHECKING, Any
8
5
 
9
- from .constants import InfrahubClientMode
10
- from .exceptions import (
11
- Error,
6
+ from ..constants import InfrahubClientMode
7
+ from ..exceptions import (
12
8
  FeatureNotSupportedError,
13
9
  NodeNotFoundError,
14
- UninitializedError,
15
10
  )
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
11
+ from ..graphql import Mutation, Query
12
+ from ..schema import GenericSchemaAPI, RelationshipCardinality, RelationshipKind
13
+ from ..utils import compare_lists, generate_short_id, get_flat_value
14
+ from .attribute import Attribute
15
+ from .constants import (
16
+ ARTIFACT_DEFINITION_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE,
17
+ ARTIFACT_FETCH_FEATURE_NOT_SUPPORTED_MESSAGE,
18
+ ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE,
19
+ PROPERTIES_OBJECT,
20
+ )
21
+ from .related_node import RelatedNode, RelatedNodeBase, RelatedNodeSync
22
+ from .relationship import RelationshipManager, RelationshipManagerBase, RelationshipManagerSync
20
23
 
21
24
  if TYPE_CHECKING:
22
25
  from typing_extensions import Self
23
26
 
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}
27
+ from ..client import InfrahubClient, InfrahubClientSync
28
+ from ..context import RequestContext
29
+ from ..schema import MainSchemaTypesAPI
30
+ from ..types import Order
76
31
 
77
- self._properties_flag = PROPERTIES_FLAG
78
- self._properties_object = PROPERTIES_OBJECT
79
- self._properties = self._properties_flag + self._properties_object
80
32
 
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]
33
+ def generate_relationship_property(node: InfrahubNode | InfrahubNodeSync, name: str) -> property:
34
+ """Generates a property that stores values under a private non-public name.
110
35
 
111
- @property
112
- def value(self) -> Any:
113
- return self._value
36
+ Args:
37
+ node (Union[InfrahubNode, InfrahubNodeSync]): The node instance.
38
+ name (str): The name of the relationship property.
114
39
 
115
- @value.setter
116
- def value(self, value: Any) -> None:
117
- self._value = value
118
- self.value_has_been_mutated = True
40
+ Returns:
41
+ A property object for managing the relationship.
119
42
 
120
- def _generate_input_data(self) -> dict | None:
121
- data: dict[str, Any] = {}
122
- variables: dict[str, Any] = {}
43
+ """
44
+ internal_name = "_" + name.lower()
45
+ external_name = name
123
46
 
124
- if self.value is None:
125
- return data
47
+ def prop_getter(self: InfrahubNodeBase) -> Any:
48
+ return getattr(self, internal_name)
126
49
 
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}
50
+ def prop_setter(self: InfrahubNodeBase, value: Any) -> None:
51
+ if isinstance(value, RelatedNodeBase) or value is None:
52
+ setattr(self, internal_name, value)
138
53
  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)
54
+ schema = [rel for rel in self._schema.relationships if rel.name == external_name][0]
55
+ if isinstance(node, InfrahubNode):
56
+ setattr(
57
+ self,
58
+ internal_name,
59
+ RelatedNode(
60
+ name=external_name, branch=node._branch, client=node._client, schema=schema, data=value
61
+ ),
536
62
  )
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)
63
+ else:
64
+ setattr(
65
+ self,
66
+ internal_name,
67
+ RelatedNodeSync(
68
+ name=external_name, branch=node._branch, client=node._client, schema=schema, data=value
69
+ ),
541
70
  )
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
71
 
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
72
+ return property(prop_getter, prop_setter)
707
73
 
708
74
 
709
75
  class InfrahubNodeBase:
@@ -2179,68 +1545,3 @@ class InfrahubNodeSync(InfrahubNodeBase):
2179
1545
  if response[graphql_query_name].get("count", 0):
2180
1546
  return [edge["node"] for edge in response[graphql_query_name]["edges"]]
2181
1547
  return []
2182
-
2183
-
2184
- class NodeProperty:
2185
- """Represents a property of a node, typically used for metadata like display labels."""
2186
-
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.
2213
-
2214
- Returns:
2215
- A property object for managing the relationship.
2216
-
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)