infrahub-server 1.3.0a0__py3-none-any.whl → 1.3.0b2__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 (123) hide show
  1. infrahub/actions/tasks.py +4 -11
  2. infrahub/branch/__init__.py +0 -0
  3. infrahub/branch/tasks.py +29 -0
  4. infrahub/branch/triggers.py +22 -0
  5. infrahub/cli/db.py +2 -2
  6. infrahub/computed_attribute/gather.py +3 -1
  7. infrahub/computed_attribute/tasks.py +23 -29
  8. infrahub/core/attribute.py +3 -3
  9. infrahub/core/constants/__init__.py +10 -0
  10. infrahub/core/constants/database.py +1 -0
  11. infrahub/core/constants/infrahubkind.py +2 -0
  12. infrahub/core/convert_object_type/conversion.py +1 -1
  13. infrahub/core/diff/query/save.py +67 -40
  14. infrahub/core/diff/query/time_range_query.py +0 -1
  15. infrahub/core/graph/__init__.py +1 -1
  16. infrahub/core/migrations/graph/__init__.py +6 -0
  17. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +0 -2
  18. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +662 -0
  19. infrahub/core/migrations/graph/m030_illegal_edges.py +82 -0
  20. infrahub/core/migrations/query/attribute_add.py +13 -9
  21. infrahub/core/migrations/query/attribute_rename.py +2 -4
  22. infrahub/core/migrations/query/delete_element_in_schema.py +16 -11
  23. infrahub/core/migrations/query/node_duplicate.py +16 -15
  24. infrahub/core/migrations/query/relationship_duplicate.py +16 -12
  25. infrahub/core/migrations/schema/node_attribute_remove.py +1 -2
  26. infrahub/core/migrations/schema/node_remove.py +16 -14
  27. infrahub/core/node/__init__.py +74 -14
  28. infrahub/core/node/base.py +1 -1
  29. infrahub/core/node/resource_manager/ip_address_pool.py +6 -2
  30. infrahub/core/node/resource_manager/ip_prefix_pool.py +6 -2
  31. infrahub/core/node/resource_manager/number_pool.py +31 -5
  32. infrahub/core/node/standard.py +6 -1
  33. infrahub/core/path.py +1 -1
  34. infrahub/core/protocols.py +10 -0
  35. infrahub/core/query/node.py +1 -1
  36. infrahub/core/query/relationship.py +4 -6
  37. infrahub/core/query/standard_node.py +19 -5
  38. infrahub/core/relationship/constraints/peer_relatives.py +72 -0
  39. infrahub/core/relationship/model.py +1 -1
  40. infrahub/core/schema/attribute_parameters.py +129 -5
  41. infrahub/core/schema/attribute_schema.py +62 -14
  42. infrahub/core/schema/basenode_schema.py +2 -2
  43. infrahub/core/schema/definitions/core/__init__.py +16 -2
  44. infrahub/core/schema/definitions/core/group.py +45 -0
  45. infrahub/core/schema/definitions/core/resource_pool.py +29 -0
  46. infrahub/core/schema/definitions/internal.py +25 -4
  47. infrahub/core/schema/generated/attribute_schema.py +12 -5
  48. infrahub/core/schema/generated/relationship_schema.py +6 -1
  49. infrahub/core/schema/manager.py +7 -2
  50. infrahub/core/schema/schema_branch.py +69 -5
  51. infrahub/core/validators/__init__.py +8 -0
  52. infrahub/core/validators/attribute/choices.py +0 -1
  53. infrahub/core/validators/attribute/enum.py +0 -1
  54. infrahub/core/validators/attribute/kind.py +0 -1
  55. infrahub/core/validators/attribute/length.py +0 -1
  56. infrahub/core/validators/attribute/min_max.py +118 -0
  57. infrahub/core/validators/attribute/number_pool.py +106 -0
  58. infrahub/core/validators/attribute/optional.py +0 -2
  59. infrahub/core/validators/attribute/regex.py +0 -1
  60. infrahub/core/validators/enum.py +5 -0
  61. infrahub/core/validators/tasks.py +1 -1
  62. infrahub/database/__init__.py +16 -4
  63. infrahub/database/validation.py +100 -0
  64. infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
  65. infrahub/dependencies/builder/constraint/relationship_manager/peer_relatives.py +8 -0
  66. infrahub/dependencies/builder/diff/deserializer.py +1 -1
  67. infrahub/dependencies/registry.py +2 -0
  68. infrahub/events/models.py +1 -1
  69. infrahub/git/base.py +5 -3
  70. infrahub/git/integrator.py +102 -3
  71. infrahub/graphql/mutations/main.py +1 -1
  72. infrahub/graphql/mutations/resource_manager.py +54 -6
  73. infrahub/graphql/queries/resource_manager.py +7 -1
  74. infrahub/graphql/queries/task.py +10 -0
  75. infrahub/graphql/resolvers/many_relationship.py +1 -1
  76. infrahub/graphql/resolvers/resolver.py +2 -2
  77. infrahub/graphql/resolvers/single_relationship.py +1 -1
  78. infrahub/graphql/types/task_log.py +3 -2
  79. infrahub/menu/menu.py +8 -7
  80. infrahub/message_bus/operations/refresh/registry.py +3 -3
  81. infrahub/patch/queries/delete_duplicated_edges.py +40 -29
  82. infrahub/pools/number.py +5 -3
  83. infrahub/pools/registration.py +22 -0
  84. infrahub/pools/tasks.py +56 -0
  85. infrahub/schema/__init__.py +0 -0
  86. infrahub/schema/tasks.py +27 -0
  87. infrahub/schema/triggers.py +23 -0
  88. infrahub/task_manager/task.py +44 -4
  89. infrahub/trigger/catalogue.py +4 -0
  90. infrahub/trigger/models.py +5 -4
  91. infrahub/trigger/setup.py +26 -2
  92. infrahub/trigger/tasks.py +1 -1
  93. infrahub/types.py +6 -0
  94. infrahub/webhook/tasks.py +6 -9
  95. infrahub/workflows/catalogue.py +27 -1
  96. infrahub_sdk/client.py +43 -10
  97. infrahub_sdk/node/__init__.py +39 -0
  98. infrahub_sdk/node/attribute.py +122 -0
  99. infrahub_sdk/node/constants.py +21 -0
  100. infrahub_sdk/{node.py → node/node.py} +50 -749
  101. infrahub_sdk/node/parsers.py +15 -0
  102. infrahub_sdk/node/property.py +24 -0
  103. infrahub_sdk/node/related_node.py +266 -0
  104. infrahub_sdk/node/relationship.py +302 -0
  105. infrahub_sdk/protocols.py +112 -0
  106. infrahub_sdk/protocols_base.py +34 -2
  107. infrahub_sdk/query_groups.py +13 -2
  108. infrahub_sdk/schema/main.py +1 -0
  109. infrahub_sdk/schema/repository.py +16 -0
  110. infrahub_sdk/spec/object.py +1 -1
  111. infrahub_sdk/store.py +1 -1
  112. infrahub_sdk/testing/schemas/car_person.py +1 -0
  113. {infrahub_server-1.3.0a0.dist-info → infrahub_server-1.3.0b2.dist-info}/METADATA +3 -3
  114. {infrahub_server-1.3.0a0.dist-info → infrahub_server-1.3.0b2.dist-info}/RECORD +122 -100
  115. {infrahub_server-1.3.0a0.dist-info → infrahub_server-1.3.0b2.dist-info}/WHEEL +1 -1
  116. infrahub_testcontainers/container.py +239 -64
  117. infrahub_testcontainers/docker-compose-cluster.test.yml +321 -0
  118. infrahub_testcontainers/docker-compose.test.yml +1 -0
  119. infrahub_testcontainers/helpers.py +15 -1
  120. infrahub_testcontainers/plugin.py +9 -0
  121. infrahub/patch/queries/consolidate_duplicated_nodes.py +0 -106
  122. {infrahub_server-1.3.0a0.dist-info → infrahub_server-1.3.0b2.dist-info}/LICENSE.txt +0 -0
  123. {infrahub_server-1.3.0a0.dist-info → infrahub_server-1.3.0b2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from .constants import HFID_STR_SEPARATOR
4
+
5
+
6
+ def parse_human_friendly_id(hfid: str | list[str]) -> tuple[str | None, list[str]]:
7
+ """Parse a human friendly ID into a kind and an identifier."""
8
+ if isinstance(hfid, str):
9
+ hfid_parts = hfid.split(HFID_STR_SEPARATOR)
10
+ if len(hfid_parts) == 1:
11
+ return None, hfid_parts
12
+ return hfid_parts[0], hfid_parts[1:]
13
+ if isinstance(hfid, list):
14
+ return None, hfid
15
+ raise ValueError(f"Invalid human friendly ID: {hfid}")
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class NodeProperty:
5
+ """Represents a property of a node, typically used for metadata like display labels."""
6
+
7
+ def __init__(self, data: dict | str):
8
+ """
9
+ Args:
10
+ data (Union[dict, str]): Data representing the node property.
11
+ """
12
+ self.id = None
13
+ self.display_label = None
14
+ self.typename = None
15
+
16
+ if isinstance(data, str):
17
+ self.id = data
18
+ elif isinstance(data, dict):
19
+ self.id = data.get("id", None)
20
+ self.display_label = data.get("display_label", None)
21
+ self.typename = data.get("__typename", None)
22
+
23
+ def _generate_input_data(self) -> str | None:
24
+ return self.id
@@ -0,0 +1,266 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from ..exceptions import (
6
+ Error,
7
+ )
8
+ from ..protocols_base import CoreNodeBase
9
+ from .constants import PROPERTIES_FLAG, PROPERTIES_OBJECT
10
+
11
+ if TYPE_CHECKING:
12
+ from ..client import InfrahubClient, InfrahubClientSync
13
+ from ..schema import RelationshipSchemaAPI
14
+ from .node import InfrahubNode, InfrahubNodeSync
15
+
16
+
17
+ class RelatedNodeBase:
18
+ """Base class for representing a related node in a relationship."""
19
+
20
+ def __init__(self, branch: str, schema: RelationshipSchemaAPI, data: Any | dict, name: str | None = None):
21
+ """
22
+ Args:
23
+ branch (str): The branch where the related node resides.
24
+ schema (RelationshipSchema): The schema of the relationship.
25
+ data (Union[Any, dict]): Data representing the related node.
26
+ name (Optional[str]): The name of the related node.
27
+ """
28
+ self.schema = schema
29
+ self.name = name
30
+
31
+ self._branch = branch
32
+
33
+ self._properties_flag = PROPERTIES_FLAG
34
+ self._properties_object = PROPERTIES_OBJECT
35
+ self._properties = self._properties_flag + self._properties_object
36
+
37
+ self._peer = None
38
+ self._id: str | None = None
39
+ self._hfid: list[str] | None = None
40
+ self._display_label: str | None = None
41
+ self._typename: str | None = None
42
+
43
+ if isinstance(data, (CoreNodeBase)):
44
+ self._peer = data
45
+ for prop in self._properties:
46
+ setattr(self, prop, None)
47
+
48
+ elif isinstance(data, list):
49
+ data = {"hfid": data}
50
+ elif not isinstance(data, dict):
51
+ data = {"id": data}
52
+
53
+ if isinstance(data, dict):
54
+ # To support both with and without pagination, we split data into node_data and properties_data
55
+ # We should probably clean that once we'll remove the code without pagination.
56
+ node_data = data.get("node", data)
57
+ properties_data = data.get("properties", data)
58
+
59
+ if node_data:
60
+ self._id = node_data.get("id", None)
61
+ self._hfid = node_data.get("hfid", None)
62
+ self._kind = node_data.get("kind", None)
63
+ self._display_label = node_data.get("display_label", None)
64
+ self._typename = node_data.get("__typename", None)
65
+
66
+ self.updated_at: str | None = data.get("updated_at", data.get("_relation__updated_at", None))
67
+
68
+ # FIXME, we won't need that once we are only supporting paginated results
69
+ if self._typename and self._typename.startswith("Related"):
70
+ self._typename = self._typename[7:]
71
+
72
+ for prop in self._properties:
73
+ prop_data = properties_data.get(prop, properties_data.get(f"_relation__{prop}", None))
74
+ if prop_data and isinstance(prop_data, dict) and "id" in prop_data:
75
+ setattr(self, prop, prop_data["id"])
76
+ elif prop_data and isinstance(prop_data, (str, bool)):
77
+ setattr(self, prop, prop_data)
78
+ else:
79
+ setattr(self, prop, None)
80
+
81
+ @property
82
+ def id(self) -> str | None:
83
+ if self._peer:
84
+ return self._peer.id
85
+ return self._id
86
+
87
+ @property
88
+ def hfid(self) -> list[Any] | None:
89
+ if self._peer:
90
+ return self._peer.hfid
91
+ return self._hfid
92
+
93
+ @property
94
+ def hfid_str(self) -> str | None:
95
+ if self._peer and self.hfid:
96
+ return self._peer.get_human_friendly_id_as_string(include_kind=True)
97
+ return None
98
+
99
+ @property
100
+ def is_resource_pool(self) -> bool:
101
+ if self._peer:
102
+ return self._peer.is_resource_pool()
103
+ return False
104
+
105
+ @property
106
+ def initialized(self) -> bool:
107
+ return bool(self.id) or bool(self.hfid)
108
+
109
+ @property
110
+ def display_label(self) -> str | None:
111
+ if self._peer:
112
+ return self._peer.display_label
113
+ return self._display_label
114
+
115
+ @property
116
+ def typename(self) -> str | None:
117
+ if self._peer:
118
+ return self._peer.typename
119
+ return self._typename
120
+
121
+ def _generate_input_data(self, allocate_from_pool: bool = False) -> dict[str, Any]:
122
+ data: dict[str, Any] = {}
123
+
124
+ if self.is_resource_pool and allocate_from_pool:
125
+ return {"from_pool": {"id": self.id}}
126
+
127
+ if self.id is not None:
128
+ data["id"] = self.id
129
+ elif self.hfid is not None:
130
+ data["hfid"] = self.hfid
131
+ if self._kind is not None:
132
+ data["kind"] = self._kind
133
+
134
+ for prop_name in self._properties:
135
+ if getattr(self, prop_name) is not None:
136
+ data[f"_relation__{prop_name}"] = getattr(self, prop_name)
137
+
138
+ return data
139
+
140
+ def _generate_mutation_query(self) -> dict[str, Any]:
141
+ if self.name and self.is_resource_pool:
142
+ # If a related node points to a pool, ask for the ID of the pool allocated resource
143
+ return {self.name: {"node": {"id": None, "display_label": None, "__typename": None}}}
144
+ return {}
145
+
146
+ @classmethod
147
+ def _generate_query_data(cls, peer_data: dict[str, Any] | None = None, property: bool = False) -> dict:
148
+ """Generates the basic structure of a GraphQL query for a single relationship.
149
+
150
+ Args:
151
+ peer_data (dict[str, Union[Any, Dict]], optional): Additional data to be included in the query for the node.
152
+ This is used to add extra fields when prefetching related node data.
153
+
154
+ Returns:
155
+ Dict: A dictionary representing the basic structure of a GraphQL query, including the node's ID, display label,
156
+ and typename. The method also includes additional properties and any peer_data provided.
157
+ """
158
+ data: dict[str, Any] = {"node": {"id": None, "hfid": None, "display_label": None, "__typename": None}}
159
+ properties: dict[str, Any] = {}
160
+
161
+ if property:
162
+ for prop_name in PROPERTIES_FLAG:
163
+ properties[prop_name] = None
164
+ for prop_name in PROPERTIES_OBJECT:
165
+ properties[prop_name] = {"id": None, "display_label": None, "__typename": None}
166
+
167
+ if properties:
168
+ data["properties"] = properties
169
+ if peer_data:
170
+ data["node"].update(peer_data)
171
+
172
+ return data
173
+
174
+
175
+ class RelatedNode(RelatedNodeBase):
176
+ """Represents a RelatedNodeBase in an asynchronous context."""
177
+
178
+ def __init__(
179
+ self,
180
+ client: InfrahubClient,
181
+ branch: str,
182
+ schema: RelationshipSchemaAPI,
183
+ data: Any | dict,
184
+ name: str | None = None,
185
+ ):
186
+ """
187
+ Args:
188
+ client (InfrahubClient): The client used to interact with the backend asynchronously.
189
+ branch (str): The branch where the related node resides.
190
+ schema (RelationshipSchema): The schema of the relationship.
191
+ data (Union[Any, dict]): Data representing the related node.
192
+ name (Optional[str]): The name of the related node.
193
+ """
194
+ self._client = client
195
+ super().__init__(branch=branch, schema=schema, data=data, name=name)
196
+
197
+ async def fetch(self, timeout: int | None = None) -> None:
198
+ if not self.id or not self.typename:
199
+ raise Error("Unable to fetch the peer, id and/or typename are not defined")
200
+
201
+ self._peer = await self._client.get(
202
+ kind=self.typename, id=self.id, populate_store=True, branch=self._branch, timeout=timeout
203
+ )
204
+
205
+ @property
206
+ def peer(self) -> InfrahubNode:
207
+ return self.get()
208
+
209
+ def get(self) -> InfrahubNode:
210
+ if self._peer:
211
+ return self._peer # type: ignore[return-value]
212
+
213
+ if self.id and self.typename:
214
+ return self._client.store.get(key=self.id, kind=self.typename, branch=self._branch) # type: ignore[return-value]
215
+
216
+ if self.hfid_str:
217
+ return self._client.store.get(key=self.hfid_str, branch=self._branch) # type: ignore[return-value]
218
+
219
+ raise ValueError("Node must have at least one identifier (ID or HFID) to query it.")
220
+
221
+
222
+ class RelatedNodeSync(RelatedNodeBase):
223
+ """Represents a related node in a synchronous context."""
224
+
225
+ def __init__(
226
+ self,
227
+ client: InfrahubClientSync,
228
+ branch: str,
229
+ schema: RelationshipSchemaAPI,
230
+ data: Any | dict,
231
+ name: str | None = None,
232
+ ):
233
+ """
234
+ Args:
235
+ client (InfrahubClientSync): The client used to interact with the backend synchronously.
236
+ branch (str): The branch where the related node resides.
237
+ schema (RelationshipSchema): The schema of the relationship.
238
+ data (Union[Any, dict]): Data representing the related node.
239
+ name (Optional[str]): The name of the related node.
240
+ """
241
+ self._client = client
242
+ super().__init__(branch=branch, schema=schema, data=data, name=name)
243
+
244
+ def fetch(self, timeout: int | None = None) -> None:
245
+ if not self.id or not self.typename:
246
+ raise Error("Unable to fetch the peer, id and/or typename are not defined")
247
+
248
+ self._peer = self._client.get(
249
+ kind=self.typename, id=self.id, populate_store=True, branch=self._branch, timeout=timeout
250
+ )
251
+
252
+ @property
253
+ def peer(self) -> InfrahubNodeSync:
254
+ return self.get()
255
+
256
+ def get(self) -> InfrahubNodeSync:
257
+ if self._peer:
258
+ return self._peer # type: ignore[return-value]
259
+
260
+ if self.id and self.typename:
261
+ return self._client.store.get(key=self.id, kind=self.typename, branch=self._branch) # type: ignore[return-value]
262
+
263
+ if self.hfid_str:
264
+ return self._client.store.get(key=self.hfid_str, branch=self._branch) # type: ignore[return-value]
265
+
266
+ raise ValueError("Node must have at least one identifier (ID or HFID) to query it.")
@@ -0,0 +1,302 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from ..exceptions import (
7
+ UninitializedError,
8
+ )
9
+ from .constants import PROPERTIES_FLAG, PROPERTIES_OBJECT
10
+ from .related_node import RelatedNode, RelatedNodeSync
11
+
12
+ if TYPE_CHECKING:
13
+ from ..client import InfrahubClient, InfrahubClientSync
14
+ from ..schema import RelationshipSchemaAPI
15
+ from .node import InfrahubNode, InfrahubNodeSync
16
+
17
+
18
+ class RelationshipManagerBase:
19
+ """Base class for RelationshipManager and RelationshipManagerSync"""
20
+
21
+ def __init__(self, name: str, branch: str, schema: RelationshipSchemaAPI):
22
+ """
23
+ Args:
24
+ name (str): The name of the relationship.
25
+ branch (str): The branch where the relationship resides.
26
+ schema (RelationshipSchema): The schema of the relationship.
27
+ """
28
+ self.initialized: bool = False
29
+ self._has_update: bool = False
30
+ self.name = name
31
+ self.schema = schema
32
+ self.branch = branch
33
+
34
+ self._properties_flag = PROPERTIES_FLAG
35
+ self._properties_object = PROPERTIES_OBJECT
36
+ self._properties = self._properties_flag + self._properties_object
37
+
38
+ self.peers: list[RelatedNode | RelatedNodeSync] = []
39
+
40
+ @property
41
+ def peer_ids(self) -> list[str]:
42
+ return [peer.id for peer in self.peers if peer.id]
43
+
44
+ @property
45
+ def peer_hfids(self) -> list[list[Any]]:
46
+ return [peer.hfid for peer in self.peers if peer.hfid]
47
+
48
+ @property
49
+ def peer_hfids_str(self) -> list[str]:
50
+ return [peer.hfid_str for peer in self.peers if peer.hfid_str]
51
+
52
+ @property
53
+ def has_update(self) -> bool:
54
+ return self._has_update
55
+
56
+ def _generate_input_data(self, allocate_from_pool: bool = False) -> list[dict]:
57
+ return [peer._generate_input_data(allocate_from_pool=allocate_from_pool) for peer in self.peers]
58
+
59
+ def _generate_mutation_query(self) -> dict[str, Any]:
60
+ # Does nothing for now
61
+ return {}
62
+
63
+ @classmethod
64
+ def _generate_query_data(cls, peer_data: dict[str, Any] | None = None, property: bool = False) -> dict:
65
+ """Generates the basic structure of a GraphQL query for relationships with multiple nodes.
66
+
67
+ Args:
68
+ peer_data (dict[str, Union[Any, Dict]], optional): Additional data to be included in the query for each node.
69
+ This is used to add extra fields when prefetching related node data in many-to-many relationships.
70
+
71
+ Returns:
72
+ Dict: A dictionary representing the basic structure of a GraphQL query for multiple related nodes.
73
+ It includes count, edges, and node information (ID, display label, and typename), along with additional properties
74
+ and any peer_data provided.
75
+ """
76
+ data: dict[str, Any] = {
77
+ "count": None,
78
+ "edges": {"node": {"id": None, "hfid": None, "display_label": None, "__typename": None}},
79
+ }
80
+
81
+ properties: dict[str, Any] = {}
82
+ if property:
83
+ for prop_name in PROPERTIES_FLAG:
84
+ properties[prop_name] = None
85
+ for prop_name in PROPERTIES_OBJECT:
86
+ properties[prop_name] = {"id": None, "display_label": None, "__typename": None}
87
+
88
+ if properties:
89
+ data["edges"]["properties"] = properties
90
+ if peer_data:
91
+ data["edges"]["node"].update(peer_data)
92
+
93
+ return data
94
+
95
+
96
+ class RelationshipManager(RelationshipManagerBase):
97
+ """Manages relationships of a node in an asynchronous context."""
98
+
99
+ def __init__(
100
+ self,
101
+ name: str,
102
+ client: InfrahubClient,
103
+ node: InfrahubNode,
104
+ branch: str,
105
+ schema: RelationshipSchemaAPI,
106
+ data: Any | dict,
107
+ ):
108
+ """
109
+ Args:
110
+ name (str): The name of the relationship.
111
+ client (InfrahubClient): The client used to interact with the backend.
112
+ node (InfrahubNode): The node to which the relationship belongs.
113
+ branch (str): The branch where the relationship resides.
114
+ schema (RelationshipSchema): The schema of the relationship.
115
+ data (Union[Any, dict]): Initial data for the relationships.
116
+ """
117
+ self.client = client
118
+ self.node = node
119
+
120
+ super().__init__(name=name, schema=schema, branch=branch)
121
+
122
+ self.initialized = data is not None
123
+ self._has_update = False
124
+
125
+ if data is None:
126
+ return
127
+
128
+ if isinstance(data, list):
129
+ for item in data:
130
+ self.peers.append(
131
+ RelatedNode(name=name, client=self.client, branch=self.branch, schema=schema, data=item)
132
+ )
133
+ elif isinstance(data, dict) and "edges" in data:
134
+ for item in data["edges"]:
135
+ self.peers.append(
136
+ RelatedNode(name=name, client=self.client, branch=self.branch, schema=schema, data=item)
137
+ )
138
+ else:
139
+ raise ValueError(f"Unexpected format for {name} found a {type(data)}, {data}")
140
+
141
+ def __getitem__(self, item: int) -> RelatedNode:
142
+ return self.peers[item] # type: ignore[return-value]
143
+
144
+ async def fetch(self) -> None:
145
+ if not self.initialized:
146
+ exclude = self.node._schema.relationship_names + self.node._schema.attribute_names
147
+ exclude.remove(self.schema.name)
148
+ node = await self.client.get(
149
+ kind=self.node._schema.kind,
150
+ id=self.node.id,
151
+ branch=self.branch,
152
+ include=[self.schema.name],
153
+ exclude=exclude,
154
+ )
155
+ rm = getattr(node, self.schema.name)
156
+ self.peers = rm.peers
157
+ self.initialized = True
158
+
159
+ for peer in self.peers:
160
+ await peer.fetch() # type: ignore[misc]
161
+
162
+ def add(self, data: str | RelatedNode | dict) -> None:
163
+ """Add a new peer to this relationship."""
164
+ if not self.initialized:
165
+ raise UninitializedError("Must call fetch() on RelationshipManager before editing members")
166
+ new_node = RelatedNode(schema=self.schema, client=self.client, branch=self.branch, data=data)
167
+
168
+ if (new_node.id and new_node.id not in self.peer_ids) or (
169
+ new_node.hfid and new_node.hfid not in self.peer_hfids
170
+ ):
171
+ self.peers.append(new_node)
172
+ self._has_update = True
173
+
174
+ def extend(self, data: Iterable[str | RelatedNode | dict]) -> None:
175
+ """Add new peers to this relationship."""
176
+ for d in data:
177
+ self.add(d)
178
+
179
+ def remove(self, data: str | RelatedNode | dict) -> None:
180
+ if not self.initialized:
181
+ raise UninitializedError("Must call fetch() on RelationshipManager before editing members")
182
+ node_to_remove = RelatedNode(schema=self.schema, client=self.client, branch=self.branch, data=data)
183
+
184
+ if node_to_remove.id and node_to_remove.id in self.peer_ids:
185
+ idx = self.peer_ids.index(node_to_remove.id)
186
+ if self.peers[idx].id != node_to_remove.id:
187
+ raise IndexError(f"Unexpected situation, the node with the index {idx} should be {node_to_remove.id}")
188
+
189
+ self.peers.pop(idx)
190
+ self._has_update = True
191
+
192
+ elif node_to_remove.hfid and node_to_remove.hfid in self.peer_hfids:
193
+ idx = self.peer_hfids.index(node_to_remove.hfid)
194
+ if self.peers[idx].hfid != node_to_remove.hfid:
195
+ raise IndexError(f"Unexpected situation, the node with the index {idx} should be {node_to_remove.hfid}")
196
+
197
+ self.peers.pop(idx)
198
+ self._has_update = True
199
+
200
+
201
+ class RelationshipManagerSync(RelationshipManagerBase):
202
+ """Manages relationships of a node in a synchronous context."""
203
+
204
+ def __init__(
205
+ self,
206
+ name: str,
207
+ client: InfrahubClientSync,
208
+ node: InfrahubNodeSync,
209
+ branch: str,
210
+ schema: RelationshipSchemaAPI,
211
+ data: Any | dict,
212
+ ):
213
+ """
214
+ Args:
215
+ name (str): The name of the relationship.
216
+ client (InfrahubClientSync): The client used to interact with the backend synchronously.
217
+ node (InfrahubNodeSync): The node to which the relationship belongs.
218
+ branch (str): The branch where the relationship resides.
219
+ schema (RelationshipSchema): The schema of the relationship.
220
+ data (Union[Any, dict]): Initial data for the relationships.
221
+ """
222
+ self.client = client
223
+ self.node = node
224
+
225
+ super().__init__(name=name, schema=schema, branch=branch)
226
+
227
+ self.initialized = data is not None
228
+ self._has_update = False
229
+
230
+ if data is None:
231
+ return
232
+
233
+ if isinstance(data, list):
234
+ for item in data:
235
+ self.peers.append(
236
+ RelatedNodeSync(name=name, client=self.client, branch=self.branch, schema=schema, data=item)
237
+ )
238
+ elif isinstance(data, dict) and "edges" in data:
239
+ for item in data["edges"]:
240
+ self.peers.append(
241
+ RelatedNodeSync(name=name, client=self.client, branch=self.branch, schema=schema, data=item)
242
+ )
243
+ else:
244
+ raise ValueError(f"Unexpected format for {name} found a {type(data)}, {data}")
245
+
246
+ def __getitem__(self, item: int) -> RelatedNodeSync:
247
+ return self.peers[item] # type: ignore[return-value]
248
+
249
+ def fetch(self) -> None:
250
+ if not self.initialized:
251
+ exclude = self.node._schema.relationship_names + self.node._schema.attribute_names
252
+ exclude.remove(self.schema.name)
253
+ node = self.client.get(
254
+ kind=self.node._schema.kind,
255
+ id=self.node.id,
256
+ branch=self.branch,
257
+ include=[self.schema.name],
258
+ exclude=exclude,
259
+ )
260
+ rm = getattr(node, self.schema.name)
261
+ self.peers = rm.peers
262
+ self.initialized = True
263
+
264
+ for peer in self.peers:
265
+ peer.fetch()
266
+
267
+ def add(self, data: str | RelatedNodeSync | dict) -> None:
268
+ """Add a new peer to this relationship."""
269
+ if not self.initialized:
270
+ raise UninitializedError("Must call fetch() on RelationshipManager before editing members")
271
+ new_node = RelatedNodeSync(schema=self.schema, client=self.client, branch=self.branch, data=data)
272
+
273
+ if (new_node.id and new_node.id not in self.peer_ids) or (
274
+ new_node.hfid and new_node.hfid not in self.peer_hfids
275
+ ):
276
+ self.peers.append(new_node)
277
+ self._has_update = True
278
+
279
+ def extend(self, data: Iterable[str | RelatedNodeSync | dict]) -> None:
280
+ """Add new peers to this relationship."""
281
+ for d in data:
282
+ self.add(d)
283
+
284
+ def remove(self, data: str | RelatedNodeSync | dict) -> None:
285
+ if not self.initialized:
286
+ raise UninitializedError("Must call fetch() on RelationshipManager before editing members")
287
+ node_to_remove = RelatedNodeSync(schema=self.schema, client=self.client, branch=self.branch, data=data)
288
+
289
+ if node_to_remove.id and node_to_remove.id in self.peer_ids:
290
+ idx = self.peer_ids.index(node_to_remove.id)
291
+ if self.peers[idx].id != node_to_remove.id:
292
+ raise IndexError(f"Unexpected situation, the node with the index {idx} should be {node_to_remove.id}")
293
+ self.peers.pop(idx)
294
+ self._has_update = True
295
+
296
+ elif node_to_remove.hfid and node_to_remove.hfid in self.peer_hfids:
297
+ idx = self.peer_hfids.index(node_to_remove.hfid)
298
+ if self.peers[idx].hfid != node_to_remove.hfid:
299
+ raise IndexError(f"Unexpected situation, the node with the index {idx} should be {node_to_remove.hfid}")
300
+
301
+ self.peers.pop(idx)
302
+ self._has_update = True