infrahub-server 1.2.6__py3-none-any.whl → 1.2.7__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 (45) hide show
  1. infrahub/cli/db.py +2 -0
  2. infrahub/cli/patch.py +153 -0
  3. infrahub/computed_attribute/models.py +81 -1
  4. infrahub/computed_attribute/tasks.py +34 -53
  5. infrahub/core/node/__init__.py +4 -1
  6. infrahub/core/query/ipam.py +7 -5
  7. infrahub/patch/__init__.py +0 -0
  8. infrahub/patch/constants.py +13 -0
  9. infrahub/patch/edge_adder.py +64 -0
  10. infrahub/patch/edge_deleter.py +33 -0
  11. infrahub/patch/edge_updater.py +28 -0
  12. infrahub/patch/models.py +98 -0
  13. infrahub/patch/plan_reader.py +107 -0
  14. infrahub/patch/plan_writer.py +92 -0
  15. infrahub/patch/queries/__init__.py +0 -0
  16. infrahub/patch/queries/base.py +17 -0
  17. infrahub/patch/runner.py +254 -0
  18. infrahub/patch/vertex_adder.py +61 -0
  19. infrahub/patch/vertex_deleter.py +33 -0
  20. infrahub/patch/vertex_updater.py +28 -0
  21. infrahub_sdk/checks.py +1 -1
  22. infrahub_sdk/ctl/cli_commands.py +2 -2
  23. infrahub_sdk/ctl/menu.py +56 -13
  24. infrahub_sdk/ctl/object.py +55 -5
  25. infrahub_sdk/ctl/utils.py +22 -1
  26. infrahub_sdk/exceptions.py +19 -1
  27. infrahub_sdk/node.py +42 -26
  28. infrahub_sdk/protocols_generator/__init__.py +0 -0
  29. infrahub_sdk/protocols_generator/constants.py +28 -0
  30. infrahub_sdk/{code_generator.py → protocols_generator/generator.py} +47 -34
  31. infrahub_sdk/protocols_generator/template.j2 +114 -0
  32. infrahub_sdk/schema/__init__.py +110 -74
  33. infrahub_sdk/schema/main.py +36 -2
  34. infrahub_sdk/schema/repository.py +2 -0
  35. infrahub_sdk/spec/menu.py +3 -3
  36. infrahub_sdk/spec/object.py +522 -41
  37. infrahub_sdk/testing/docker.py +4 -5
  38. infrahub_sdk/testing/schemas/animal.py +7 -0
  39. infrahub_sdk/yaml.py +63 -7
  40. {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.7.dist-info}/METADATA +1 -1
  41. {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.7.dist-info}/RECORD +44 -27
  42. infrahub_sdk/ctl/constants.py +0 -115
  43. {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.7.dist-info}/LICENSE.txt +0 -0
  44. {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.7.dist-info}/WHEEL +0 -0
  45. {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.7.dist-info}/entry_points.txt +0 -0
@@ -1,20 +1,363 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from enum import Enum
3
4
  from typing import TYPE_CHECKING, Any
4
5
 
5
6
  from pydantic import BaseModel, Field
6
7
 
8
+ from ..exceptions import ObjectValidationError, ValidationError
9
+ from ..schema import GenericSchemaAPI, RelationshipKind, RelationshipSchema
7
10
  from ..yaml import InfrahubFile, InfrahubFileKind
8
11
 
9
12
  if TYPE_CHECKING:
10
13
  from ..client import InfrahubClient
11
- from ..schema import MainSchemaTypesAPI
14
+ from ..node import InfrahubNode
15
+ from ..schema import MainSchemaTypesAPI, RelationshipSchema
16
+
17
+
18
+ def validate_list_of_scalars(value: list[Any]) -> bool:
19
+ return all(isinstance(item, (str, int, float, bool)) for item in value)
20
+
21
+
22
+ def validate_list_of_hfids(value: list[Any]) -> bool:
23
+ return all(isinstance(item, (str, list)) for item in value)
24
+
25
+
26
+ def validate_list_of_data_dicts(value: list[Any]) -> bool:
27
+ return all(isinstance(item, dict) and "data" in item for item in value)
28
+
29
+
30
+ def validate_list_of_objects(value: list[Any]) -> bool:
31
+ return all(isinstance(item, dict) for item in value)
32
+
33
+
34
+ class RelationshipDataFormat(str, Enum):
35
+ UNKNOWN = "unknown"
36
+
37
+ ONE_REF = "one_ref"
38
+ ONE_OBJ = "one_obj"
39
+
40
+ MANY_OBJ_DICT_LIST = "many_obj_dict_list"
41
+ MANY_OBJ_LIST_DICT = "many_obj_list_dict"
42
+ MANY_REF = "many_ref_list"
43
+
44
+
45
+ class RelationshipInfo(BaseModel):
46
+ name: str
47
+ rel_schema: RelationshipSchema
48
+ peer_kind: str
49
+ peer_rel: RelationshipSchema | None = None
50
+ reason_relationship_not_valid: str | None = None
51
+ format: RelationshipDataFormat = RelationshipDataFormat.UNKNOWN
52
+
53
+ @property
54
+ def is_bidirectional(self) -> bool:
55
+ """Indicate if a relationship with the same identifier exists on the other side"""
56
+ return bool(self.peer_rel)
57
+
58
+ @property
59
+ def is_mandatory(self) -> bool:
60
+ if not self.peer_rel:
61
+ return False
62
+ # For hierarchical node, currently the relationship to the parent is always optional in the schema even if it's mandatory
63
+ # In order to build the tree from top to bottom, we need to consider it as mandatory
64
+ # While it should technically work bottom-up, it created some unexpected behavior while loading the menu
65
+ if self.peer_rel.cardinality == "one" and self.peer_rel.kind == RelationshipKind.HIERARCHY:
66
+ return True
67
+ return not self.peer_rel.optional
68
+
69
+ @property
70
+ def is_valid(self) -> bool:
71
+ return not self.reason_relationship_not_valid
72
+
73
+ @property
74
+ def is_reference(self) -> bool:
75
+ return self.format in [RelationshipDataFormat.ONE_REF, RelationshipDataFormat.MANY_REF]
76
+
77
+ def get_context(self, value: Any) -> dict:
78
+ """Return a dict to insert to the context if the relationship is mandatory"""
79
+ if self.peer_rel and self.is_mandatory and self.peer_rel.cardinality == "one":
80
+ return {self.peer_rel.name: value}
81
+ if self.peer_rel and self.is_mandatory and self.peer_rel.cardinality == "many":
82
+ return {self.peer_rel.name: [value]}
83
+ return {}
84
+
85
+ def find_matching_relationship(
86
+ self, peer_schema: MainSchemaTypesAPI, force: bool = False
87
+ ) -> RelationshipSchema | None:
88
+ """Find the matching relationship on the other side of the relationship"""
89
+ if self.peer_rel and not force:
90
+ return self.peer_rel
91
+
92
+ try:
93
+ self.peer_rel = peer_schema.get_matching_relationship(
94
+ id=self.rel_schema.identifier or "", direction=self.rel_schema.direction
95
+ )
96
+ except ValueError:
97
+ pass
98
+
99
+ return self.peer_rel
100
+
101
+
102
+ async def get_relationship_info(
103
+ client: InfrahubClient, schema: MainSchemaTypesAPI, name: str, value: Any, branch: str | None = None
104
+ ) -> RelationshipInfo:
105
+ """
106
+ Get the relationship info for a given relationship name.
107
+ """
108
+ rel_schema = schema.get_relationship(name=name)
109
+
110
+ info = RelationshipInfo(name=name, peer_kind=rel_schema.peer, rel_schema=rel_schema)
111
+
112
+ if isinstance(value, dict) and "data" not in value:
113
+ info.reason_relationship_not_valid = f"Relationship {name} must be a dict with 'data'"
114
+ return info
115
+
116
+ if isinstance(value, dict) and "kind" in value:
117
+ info.peer_kind = value["kind"]
118
+
119
+ peer_schema = await client.schema.get(kind=info.peer_kind, branch=branch)
120
+
121
+ try:
122
+ info.peer_rel = peer_schema.get_matching_relationship(
123
+ id=rel_schema.identifier or "", direction=rel_schema.direction
124
+ )
125
+ except ValueError:
126
+ pass
127
+
128
+ if rel_schema.cardinality == "one" and isinstance(value, list):
129
+ # validate the list is composed of string
130
+ if validate_list_of_scalars(value):
131
+ info.format = RelationshipDataFormat.ONE_REF
132
+ else:
133
+ info.reason_relationship_not_valid = "Too many objects provided for a relationship of cardinality one"
134
+
135
+ elif rel_schema.cardinality == "one" and isinstance(value, str):
136
+ info.format = RelationshipDataFormat.ONE_REF
137
+
138
+ elif rel_schema.cardinality == "one" and isinstance(value, dict) and "data" in value:
139
+ info.format = RelationshipDataFormat.ONE_OBJ
140
+
141
+ elif (
142
+ rel_schema.cardinality == "many"
143
+ and isinstance(value, dict)
144
+ and "data" in value
145
+ and validate_list_of_objects(value["data"])
146
+ ):
147
+ # Initial format, we need to support it for backward compatibility for menu
148
+ # it's helpful if there is only one type of object to manage
149
+ info.format = RelationshipDataFormat.MANY_OBJ_DICT_LIST
150
+
151
+ elif rel_schema.cardinality == "many" and isinstance(value, dict) and "data" not in value:
152
+ info.reason_relationship_not_valid = "Invalid structure for a relationship of cardinality many,"
153
+ " either provide a dict with data as a list or a list of objects"
154
+
155
+ elif rel_schema.cardinality == "many" and isinstance(value, list):
156
+ if validate_list_of_data_dicts(value):
157
+ info.format = RelationshipDataFormat.MANY_OBJ_LIST_DICT
158
+ elif validate_list_of_hfids(value):
159
+ info.format = RelationshipDataFormat.MANY_REF
160
+ else:
161
+ info.reason_relationship_not_valid = "Invalid structure for a relationship of cardinality many,"
162
+ " either provide a list of dict with data or a list of hfids"
163
+
164
+ return info
12
165
 
13
166
 
14
167
  class InfrahubObjectFileData(BaseModel):
15
168
  kind: str
16
169
  data: list[dict[str, Any]] = Field(default_factory=list)
17
170
 
171
+ async def validate_format(self, client: InfrahubClient, branch: str | None = None) -> list[ObjectValidationError]:
172
+ errors: list[ObjectValidationError] = []
173
+ schema = await client.schema.get(kind=self.kind, branch=branch)
174
+ for idx, item in enumerate(self.data):
175
+ errors.extend(
176
+ await self.validate_object(
177
+ client=client,
178
+ position=[idx + 1],
179
+ schema=schema,
180
+ data=item,
181
+ branch=branch,
182
+ default_schema_kind=self.kind,
183
+ )
184
+ )
185
+ return errors
186
+
187
+ async def process(self, client: InfrahubClient, branch: str | None = None) -> None:
188
+ schema = await client.schema.get(kind=self.kind, branch=branch)
189
+ for idx, item in enumerate(self.data):
190
+ await self.create_node(
191
+ client=client,
192
+ schema=schema,
193
+ data=item,
194
+ position=[idx + 1],
195
+ branch=branch,
196
+ default_schema_kind=self.kind,
197
+ )
198
+
199
+ @classmethod
200
+ async def validate_object(
201
+ cls,
202
+ client: InfrahubClient,
203
+ schema: MainSchemaTypesAPI,
204
+ data: dict,
205
+ position: list[int | str],
206
+ context: dict | None = None,
207
+ branch: str | None = None,
208
+ default_schema_kind: str | None = None,
209
+ ) -> list[ObjectValidationError]:
210
+ errors: list[ObjectValidationError] = []
211
+ context = context.copy() if context else {}
212
+
213
+ # First validate if all mandatory fields are present
214
+ for element in schema.mandatory_input_names:
215
+ if not any([element in data.keys(), element in context.keys()]):
216
+ errors.append(ObjectValidationError(position=position + [element], message=f"{element} is mandatory"))
217
+
218
+ # Validate if all attributes are valid
219
+ for key, value in data.items():
220
+ if key not in schema.attribute_names and key not in schema.relationship_names:
221
+ errors.append(
222
+ ObjectValidationError(
223
+ position=position + [key],
224
+ message=f"{key} is not a valid attribute or relationship for {schema.kind}",
225
+ )
226
+ )
227
+
228
+ if key in schema.attribute_names:
229
+ if not isinstance(value, (str, int, float, bool, list, dict)):
230
+ errors.append(
231
+ ObjectValidationError(
232
+ position=position + [key],
233
+ message=f"{key} must be a string, int, float, bool, list, or dict",
234
+ )
235
+ )
236
+
237
+ if key in schema.relationship_names:
238
+ rel_info = await get_relationship_info(
239
+ client=client, schema=schema, name=key, value=value, branch=branch
240
+ )
241
+ if not rel_info.is_valid:
242
+ errors.append(
243
+ ObjectValidationError(
244
+ position=position + [key],
245
+ message=rel_info.reason_relationship_not_valid or "Invalid relationship",
246
+ )
247
+ )
248
+
249
+ errors.extend(
250
+ await cls.validate_related_nodes(
251
+ client=client,
252
+ position=position + [key],
253
+ rel_info=rel_info,
254
+ data=value,
255
+ context=context,
256
+ branch=branch,
257
+ default_schema_kind=default_schema_kind,
258
+ )
259
+ )
260
+
261
+ return errors
262
+
263
+ @classmethod
264
+ async def validate_related_nodes(
265
+ cls,
266
+ client: InfrahubClient,
267
+ position: list[int | str],
268
+ rel_info: RelationshipInfo,
269
+ data: dict | list[dict],
270
+ context: dict | None = None,
271
+ branch: str | None = None,
272
+ default_schema_kind: str | None = None,
273
+ ) -> list[ObjectValidationError]:
274
+ context = context.copy() if context else {}
275
+ errors: list[ObjectValidationError] = []
276
+
277
+ if isinstance(data, (list, str)) and rel_info.format == RelationshipDataFormat.ONE_REF:
278
+ return errors
279
+
280
+ if isinstance(data, list) and rel_info.format == RelationshipDataFormat.MANY_REF:
281
+ return errors
282
+
283
+ if isinstance(data, dict) and rel_info.format == RelationshipDataFormat.ONE_OBJ:
284
+ peer_kind = data.get("kind") or rel_info.peer_kind
285
+ peer_schema = await cls.get_peer_schema(
286
+ client=client, peer_kind=peer_kind, branch=branch, default_schema_kind=default_schema_kind
287
+ )
288
+
289
+ rel_info.find_matching_relationship(peer_schema=peer_schema)
290
+ context.update(rel_info.get_context(value="placeholder"))
291
+
292
+ errors.extend(
293
+ await cls.validate_object(
294
+ client=client,
295
+ position=position,
296
+ schema=peer_schema,
297
+ data=data["data"],
298
+ context=context,
299
+ branch=branch,
300
+ default_schema_kind=default_schema_kind,
301
+ )
302
+ )
303
+ return errors
304
+
305
+ if isinstance(data, dict) and rel_info.format == RelationshipDataFormat.MANY_OBJ_DICT_LIST:
306
+ peer_kind = data.get("kind") or rel_info.peer_kind
307
+ peer_schema = await cls.get_peer_schema(
308
+ client=client, peer_kind=peer_kind, branch=branch, default_schema_kind=default_schema_kind
309
+ )
310
+
311
+ rel_info.find_matching_relationship(peer_schema=peer_schema)
312
+ context.update(rel_info.get_context(value="placeholder"))
313
+
314
+ for idx, peer_data in enumerate(data["data"]):
315
+ context["list_index"] = idx
316
+ errors.extend(
317
+ await cls.validate_object(
318
+ client=client,
319
+ position=position + [idx + 1],
320
+ schema=peer_schema,
321
+ data=peer_data,
322
+ context=context,
323
+ branch=branch,
324
+ default_schema_kind=default_schema_kind,
325
+ )
326
+ )
327
+ return errors
328
+
329
+ if isinstance(data, list) and rel_info.format == RelationshipDataFormat.MANY_OBJ_LIST_DICT:
330
+ for idx, item in enumerate(data):
331
+ context["list_index"] = idx
332
+ peer_kind = item.get("kind") or rel_info.peer_kind
333
+ peer_schema = await cls.get_peer_schema(
334
+ client=client, peer_kind=peer_kind, branch=branch, default_schema_kind=default_schema_kind
335
+ )
336
+
337
+ rel_info.find_matching_relationship(peer_schema=peer_schema)
338
+ context.update(rel_info.get_context(value="placeholder"))
339
+
340
+ errors.extend(
341
+ await cls.validate_object(
342
+ client=client,
343
+ position=position + [idx + 1],
344
+ schema=peer_schema,
345
+ data=item["data"],
346
+ context=context,
347
+ branch=branch,
348
+ default_schema_kind=default_schema_kind,
349
+ )
350
+ )
351
+ return errors
352
+
353
+ errors.append(
354
+ ObjectValidationError(
355
+ position=position,
356
+ message=f"Relationship {rel_info.rel_schema.name} doesn't have the right format {rel_info.rel_schema.cardinality} / {type(data)}",
357
+ )
358
+ )
359
+ return errors
360
+
18
361
  @classmethod
19
362
  def enrich_node(cls, data: dict, context: dict) -> dict: # noqa: ARG003
20
363
  return data
@@ -25,35 +368,82 @@ class InfrahubObjectFileData(BaseModel):
25
368
  client: InfrahubClient,
26
369
  schema: MainSchemaTypesAPI,
27
370
  data: dict,
371
+ position: list[int | str],
28
372
  context: dict | None = None,
29
373
  branch: str | None = None,
30
374
  default_schema_kind: str | None = None,
31
- ) -> None:
32
- # First validate of all mandatory fields are present
33
- for element in schema.mandatory_attribute_names + schema.mandatory_relationship_names:
34
- if element not in data.keys():
35
- raise ValueError(f"{element} is mandatory")
375
+ ) -> InfrahubNode:
376
+ context = context.copy() if context else {}
377
+
378
+ errors = await cls.validate_object(
379
+ client=client,
380
+ position=position,
381
+ schema=schema,
382
+ data=data,
383
+ context=context,
384
+ branch=branch,
385
+ default_schema_kind=default_schema_kind,
386
+ )
387
+ if errors:
388
+ messages = [str(error) for error in errors]
389
+ raise ObjectValidationError(position=position, message="Object is not valid - " + ", ".join(messages))
36
390
 
37
391
  clean_data: dict[str, Any] = {}
38
392
 
393
+ # List of relationships that need to be processed after the current object has been created
39
394
  remaining_rels = []
395
+ rels_info: dict[str, RelationshipInfo] = {}
396
+
40
397
  for key, value in data.items():
41
398
  if key in schema.attribute_names:
42
399
  clean_data[key] = value
400
+ continue
43
401
 
44
402
  if key in schema.relationship_names:
45
- rel_schema = schema.get_relationship(name=key)
403
+ rel_info = await get_relationship_info(
404
+ client=client, schema=schema, name=key, value=value, branch=branch
405
+ )
406
+ rels_info[key] = rel_info
46
407
 
47
- if isinstance(value, dict) and "data" not in value:
48
- raise ValueError(f"Relationship {key} must be a dict with 'data'")
408
+ if not rel_info.is_valid:
409
+ client.log.info(rel_info.reason_relationship_not_valid)
410
+ continue
49
411
 
50
- # This is a simple implementation for now, need to revisit once we have the integration tests
51
- if isinstance(value, (list)):
412
+ # We need to determine if the related object depend on this object or if this is the other way around.
413
+ # - if the relationship is bidirectional and is mandatory on the other side, then we need to create this object First
414
+ # - if the relationship is bidirectional and is not mandatory on the other side, then we need should create the related object First
415
+ # - if the relationship is not bidirectional, then we need to create the related object First
416
+ if rel_info.is_reference and isinstance(value, list):
52
417
  clean_data[key] = value
53
- elif rel_schema.cardinality == "one" and isinstance(value, str):
418
+ elif rel_info.format == RelationshipDataFormat.ONE_REF and isinstance(value, str):
54
419
  clean_data[key] = [value]
55
- else:
420
+ elif not rel_info.is_reference and rel_info.is_bidirectional and rel_info.is_mandatory:
56
421
  remaining_rels.append(key)
422
+ elif not rel_info.is_reference and not rel_info.is_mandatory:
423
+ if rel_info.format == RelationshipDataFormat.ONE_OBJ:
424
+ nodes = await cls.create_related_nodes(
425
+ client=client,
426
+ position=position,
427
+ rel_info=rel_info,
428
+ data=value,
429
+ branch=branch,
430
+ default_schema_kind=default_schema_kind,
431
+ )
432
+ clean_data[key] = nodes[0]
433
+
434
+ else:
435
+ nodes = await cls.create_related_nodes(
436
+ client=client,
437
+ position=position,
438
+ rel_info=rel_info,
439
+ data=value,
440
+ branch=branch,
441
+ default_schema_kind=default_schema_kind,
442
+ )
443
+ clean_data[key] = nodes
444
+
445
+ else:
446
+ raise ValueError(f"Situation unaccounted for: {rel_info}")
57
447
 
58
448
  if context:
59
449
  clean_context = {
@@ -67,54 +457,136 @@ class InfrahubObjectFileData(BaseModel):
67
457
 
68
458
  node = await client.create(kind=schema.kind, branch=branch, data=clean_data)
69
459
  await node.save(allow_upsert=True)
460
+
70
461
  display_label = node.get_human_friendly_id_as_string() or f"{node.get_kind()} : {node.id}"
71
462
  client.log.info(f"Node: {display_label}")
72
463
 
73
464
  for rel in remaining_rels:
74
- # identify what is the name of the relationship on the other side
75
- if not isinstance(data[rel], dict) and "data" in data[rel]:
76
- raise ValueError(f"relationship {rel} must be a dict with 'data'")
465
+ context = {}
466
+
467
+ # If there is a peer relationship, we add the node id to the context
468
+ rel_info = rels_info[rel]
469
+ context.update(rel_info.get_context(value=node.id))
470
+
471
+ await cls.create_related_nodes(
472
+ client=client,
473
+ parent_node=node,
474
+ rel_info=rel_info,
475
+ position=position,
476
+ data=data[rel],
477
+ context=context,
478
+ branch=branch,
479
+ default_schema_kind=default_schema_kind,
480
+ )
481
+
482
+ return node
483
+
484
+ @classmethod
485
+ async def create_related_nodes(
486
+ cls,
487
+ client: InfrahubClient,
488
+ rel_info: RelationshipInfo,
489
+ position: list[int | str],
490
+ data: dict | list[dict],
491
+ parent_node: InfrahubNode | None = None,
492
+ context: dict | None = None,
493
+ branch: str | None = None,
494
+ default_schema_kind: str | None = None,
495
+ ) -> list[InfrahubNode]:
496
+ nodes: list[InfrahubNode] = []
497
+ context = context.copy() if context else {}
77
498
 
78
- rel_schema = schema.get_relationship(name=rel)
79
- peer_kind = data[rel].get("kind", default_schema_kind) or rel_schema.peer
499
+ if isinstance(data, dict) and rel_info.format == RelationshipDataFormat.ONE_OBJ:
500
+ peer_kind = data.get("kind") or rel_info.peer_kind
80
501
  peer_schema = await client.schema.get(kind=peer_kind, branch=branch)
81
502
 
82
- if rel_schema.identifier is None:
83
- raise ValueError("identifier must be defined")
503
+ if parent_node:
504
+ rel_info.find_matching_relationship(peer_schema=peer_schema)
505
+ context.update(rel_info.get_context(value=parent_node.id))
84
506
 
85
- peer_rel = peer_schema.get_matching_relationship(id=rel_schema.identifier, direction=rel_schema.direction)
507
+ new_node = await cls.create_node(
508
+ client=client,
509
+ schema=peer_schema,
510
+ position=position,
511
+ data=data["data"],
512
+ context=context,
513
+ branch=branch,
514
+ default_schema_kind=default_schema_kind,
515
+ )
516
+ return [new_node]
86
517
 
87
- rel_data = data[rel]["data"]
88
- context = {}
89
- if peer_rel:
90
- context[peer_rel.name] = node.id
518
+ if isinstance(data, dict) and rel_info.format == RelationshipDataFormat.MANY_OBJ_DICT_LIST:
519
+ peer_kind = data.get("kind") or rel_info.peer_kind
520
+ peer_schema = await cls.get_peer_schema(
521
+ client=client, peer_kind=peer_kind, branch=branch, default_schema_kind=default_schema_kind
522
+ )
91
523
 
92
- if rel_schema.cardinality == "one" and isinstance(rel_data, dict):
93
- await cls.create_node(
94
- client=client,
95
- schema=peer_schema,
96
- data=rel_data,
97
- context=context,
98
- branch=branch,
99
- default_schema_kind=default_schema_kind,
100
- )
524
+ if parent_node:
525
+ rel_info.find_matching_relationship(peer_schema=peer_schema)
526
+ context.update(rel_info.get_context(value=parent_node.id))
101
527
 
102
- elif rel_schema.cardinality == "many" and isinstance(rel_data, list):
103
- for idx, peer_data in enumerate(rel_data):
104
- context["list_index"] = idx
105
- await cls.create_node(
528
+ for idx, peer_data in enumerate(data["data"]):
529
+ context["list_index"] = idx
530
+ if isinstance(peer_data, dict):
531
+ node = await cls.create_node(
106
532
  client=client,
107
533
  schema=peer_schema,
534
+ position=position + [rel_info.name, idx + 1],
108
535
  data=peer_data,
109
536
  context=context,
110
537
  branch=branch,
111
538
  default_schema_kind=default_schema_kind,
112
539
  )
113
- else:
114
- raise ValueError(
115
- f"Relationship {rel_schema.name} doesn't have the right format {rel_schema.cardinality} / {type(rel_data)}"
540
+ nodes.append(node)
541
+ return nodes
542
+
543
+ if isinstance(data, list) and rel_info.format == RelationshipDataFormat.MANY_OBJ_LIST_DICT:
544
+ for idx, item in enumerate(data):
545
+ context["list_index"] = idx
546
+
547
+ peer_kind = item.get("kind") or rel_info.peer_kind
548
+ peer_schema = await cls.get_peer_schema(
549
+ client=client, peer_kind=peer_kind, branch=branch, default_schema_kind=default_schema_kind
116
550
  )
117
551
 
552
+ if parent_node:
553
+ rel_info.find_matching_relationship(peer_schema=peer_schema)
554
+ context.update(rel_info.get_context(value=parent_node.id))
555
+
556
+ node = await cls.create_node(
557
+ client=client,
558
+ schema=peer_schema,
559
+ position=position + [rel_info.name, idx + 1],
560
+ data=item["data"],
561
+ context=context,
562
+ branch=branch,
563
+ default_schema_kind=default_schema_kind,
564
+ )
565
+ nodes.append(node)
566
+
567
+ return nodes
568
+
569
+ raise ValueError(
570
+ f"Relationship {rel_info.rel_schema.name} doesn't have the right format {rel_info.rel_schema.cardinality} / {type(data)}"
571
+ )
572
+
573
+ @classmethod
574
+ async def get_peer_schema(
575
+ cls, client: InfrahubClient, peer_kind: str, branch: str | None = None, default_schema_kind: str | None = None
576
+ ) -> MainSchemaTypesAPI:
577
+ peer_schema = await client.schema.get(kind=peer_kind, branch=branch)
578
+ if not isinstance(peer_schema, GenericSchemaAPI):
579
+ return peer_schema
580
+
581
+ if not default_schema_kind:
582
+ raise ValueError(f"Found a peer schema as a generic {peer_kind} but no default value was provided")
583
+
584
+ # if the initial peer_kind was a generic, we try the default_schema_kind
585
+ peer_schema = await client.schema.get(kind=default_schema_kind, branch=branch)
586
+ if isinstance(peer_schema, GenericSchemaAPI):
587
+ raise ValueError(f"Default schema kind {default_schema_kind} can't be a generic")
588
+ return peer_schema
589
+
118
590
 
119
591
  class ObjectFile(InfrahubFile):
120
592
  _spec: InfrahubObjectFileData | None = None
@@ -130,3 +602,12 @@ class ObjectFile(InfrahubFile):
130
602
  if self.kind != InfrahubFileKind.OBJECT:
131
603
  raise ValueError("File is not an Infrahub Object file")
132
604
  self._spec = InfrahubObjectFileData(**self.data.spec)
605
+
606
+ async def validate_format(self, client: InfrahubClient, branch: str | None = None) -> None:
607
+ self.validate_content()
608
+ errors = await self.spec.validate_format(client=client, branch=branch)
609
+ if errors:
610
+ raise ValidationError(identifier=str(self.location), messages=[str(error) for error in errors])
611
+
612
+ async def process(self, client: InfrahubClient, branch: str | None = None) -> None:
613
+ await self.spec.process(client=client, branch=branch)
@@ -8,13 +8,16 @@ from packaging.version import InvalidVersion, Version
8
8
 
9
9
  from .. import Config, InfrahubClient, InfrahubClientSync
10
10
 
11
- INFRAHUB_VERSION = os.getenv("INFRAHUB_TESTING_IMAGE_VER", "latest")
11
+ INFRAHUB_VERSION = os.getenv("INFRAHUB_TESTING_IMAGE_VER")
12
12
 
13
13
 
14
14
  def skip_version(min_infrahub_version: str | None = None, max_infrahub_version: str | None = None) -> bool:
15
15
  """
16
16
  Check if a test should be skipped depending on infrahub version.
17
17
  """
18
+ if INFRAHUB_VERSION is None:
19
+ return True
20
+
18
21
  try:
19
22
  version = Version(INFRAHUB_VERSION)
20
23
  except InvalidVersion:
@@ -31,10 +34,6 @@ def skip_version(min_infrahub_version: str | None = None, max_infrahub_version:
31
34
 
32
35
 
33
36
  class TestInfrahubDockerClient(TestInfrahubDocker):
34
- @pytest.fixture(scope="class")
35
- def infrahub_version(self) -> str:
36
- return INFRAHUB_VERSION
37
-
38
37
  @pytest.fixture(scope="class")
39
38
  def client(self, infrahub_port: int) -> InfrahubClient:
40
39
  return InfrahubClient(
@@ -20,6 +20,7 @@ TESTING_ANIMAL = f"{NAMESPACE}Animal"
20
20
  TESTING_CAT = f"{NAMESPACE}Cat"
21
21
  TESTING_DOG = f"{NAMESPACE}Dog"
22
22
  TESTING_PERSON = f"{NAMESPACE}Person"
23
+ BUILTIN_TAG = "BuiltinTag"
23
24
 
24
25
 
25
26
  class SchemaAnimal:
@@ -125,6 +126,12 @@ class SchemaAnimal:
125
126
  cardinality="many",
126
127
  direction=RelationshipDirection.INBOUND,
127
128
  ),
129
+ Rel(
130
+ name="tags",
131
+ optional=True,
132
+ peer=BUILTIN_TAG,
133
+ cardinality="many",
134
+ ),
128
135
  ],
129
136
  )
130
137