infrahub-server 1.3.0a0__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.
- infrahub/core/attribute.py +3 -3
- infrahub/core/constants/__init__.py +5 -0
- infrahub/core/constants/infrahubkind.py +2 -0
- infrahub/core/migrations/query/attribute_rename.py +2 -4
- infrahub/core/migrations/query/delete_element_in_schema.py +16 -11
- infrahub/core/migrations/query/node_duplicate.py +16 -15
- infrahub/core/migrations/query/relationship_duplicate.py +16 -11
- infrahub/core/migrations/schema/node_attribute_remove.py +1 -2
- infrahub/core/migrations/schema/node_remove.py +16 -13
- infrahub/core/node/__init__.py +72 -14
- infrahub/core/node/resource_manager/ip_address_pool.py +6 -2
- infrahub/core/node/resource_manager/ip_prefix_pool.py +6 -2
- infrahub/core/node/resource_manager/number_pool.py +31 -5
- infrahub/core/node/standard.py +6 -1
- infrahub/core/protocols.py +9 -0
- infrahub/core/query/relationship.py +2 -4
- infrahub/core/schema/attribute_parameters.py +129 -5
- infrahub/core/schema/attribute_schema.py +38 -10
- infrahub/core/schema/definitions/core/__init__.py +16 -2
- infrahub/core/schema/definitions/core/group.py +45 -0
- infrahub/core/schema/definitions/core/resource_pool.py +20 -0
- infrahub/core/schema/definitions/internal.py +16 -3
- infrahub/core/schema/generated/attribute_schema.py +12 -5
- infrahub/core/schema/manager.py +3 -0
- infrahub/core/schema/schema_branch.py +55 -0
- infrahub/core/validators/__init__.py +8 -0
- infrahub/core/validators/attribute/choices.py +0 -1
- infrahub/core/validators/attribute/enum.py +0 -1
- infrahub/core/validators/attribute/kind.py +0 -1
- infrahub/core/validators/attribute/length.py +0 -1
- infrahub/core/validators/attribute/min_max.py +118 -0
- infrahub/core/validators/attribute/number_pool.py +106 -0
- infrahub/core/validators/attribute/optional.py +0 -2
- infrahub/core/validators/attribute/regex.py +0 -1
- infrahub/core/validators/enum.py +5 -0
- infrahub/database/__init__.py +15 -3
- infrahub/git/base.py +5 -3
- infrahub/git/integrator.py +102 -3
- infrahub/graphql/mutations/resource_manager.py +62 -6
- infrahub/graphql/queries/resource_manager.py +7 -1
- infrahub/graphql/queries/task.py +10 -0
- infrahub/graphql/types/task_log.py +3 -2
- infrahub/menu/menu.py +3 -3
- infrahub/pools/number.py +5 -3
- infrahub/task_manager/task.py +44 -4
- infrahub/types.py +6 -0
- infrahub_sdk/client.py +43 -10
- infrahub_sdk/node/__init__.py +39 -0
- infrahub_sdk/node/attribute.py +122 -0
- infrahub_sdk/node/constants.py +21 -0
- infrahub_sdk/{node.py → node/node.py} +50 -749
- infrahub_sdk/node/parsers.py +15 -0
- infrahub_sdk/node/property.py +24 -0
- infrahub_sdk/node/related_node.py +266 -0
- infrahub_sdk/node/relationship.py +302 -0
- infrahub_sdk/protocols.py +112 -0
- infrahub_sdk/protocols_base.py +34 -2
- infrahub_sdk/query_groups.py +13 -2
- infrahub_sdk/schema/main.py +1 -0
- infrahub_sdk/schema/repository.py +16 -0
- infrahub_sdk/spec/object.py +1 -1
- infrahub_sdk/store.py +1 -1
- infrahub_sdk/testing/schemas/car_person.py +1 -0
- {infrahub_server-1.3.0a0.dist-info → infrahub_server-1.3.0b1.dist-info}/METADATA +3 -3
- {infrahub_server-1.3.0a0.dist-info → infrahub_server-1.3.0b1.dist-info}/RECORD +68 -59
- {infrahub_server-1.3.0a0.dist-info → infrahub_server-1.3.0b1.dist-info}/WHEEL +1 -1
- {infrahub_server-1.3.0a0.dist-info → infrahub_server-1.3.0b1.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.3.0a0.dist-info → infrahub_server-1.3.0b1.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
|