infrahub-server 1.2.5__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 (57) 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/manager.py +15 -2
  6. infrahub/core/node/__init__.py +4 -1
  7. infrahub/core/query/ipam.py +7 -5
  8. infrahub/core/registry.py +2 -3
  9. infrahub/core/schema/schema_branch.py +34 -37
  10. infrahub/database/__init__.py +2 -0
  11. infrahub/graphql/manager.py +10 -0
  12. infrahub/graphql/mutations/main.py +4 -5
  13. infrahub/graphql/mutations/resource_manager.py +3 -3
  14. infrahub/patch/__init__.py +0 -0
  15. infrahub/patch/constants.py +13 -0
  16. infrahub/patch/edge_adder.py +64 -0
  17. infrahub/patch/edge_deleter.py +33 -0
  18. infrahub/patch/edge_updater.py +28 -0
  19. infrahub/patch/models.py +98 -0
  20. infrahub/patch/plan_reader.py +107 -0
  21. infrahub/patch/plan_writer.py +92 -0
  22. infrahub/patch/queries/__init__.py +0 -0
  23. infrahub/patch/queries/base.py +17 -0
  24. infrahub/patch/runner.py +254 -0
  25. infrahub/patch/vertex_adder.py +61 -0
  26. infrahub/patch/vertex_deleter.py +33 -0
  27. infrahub/patch/vertex_updater.py +28 -0
  28. infrahub/tasks/registry.py +4 -1
  29. infrahub_sdk/checks.py +1 -1
  30. infrahub_sdk/ctl/cli_commands.py +2 -2
  31. infrahub_sdk/ctl/menu.py +56 -13
  32. infrahub_sdk/ctl/object.py +55 -5
  33. infrahub_sdk/ctl/utils.py +22 -1
  34. infrahub_sdk/exceptions.py +19 -1
  35. infrahub_sdk/node.py +42 -26
  36. infrahub_sdk/protocols_generator/__init__.py +0 -0
  37. infrahub_sdk/protocols_generator/constants.py +28 -0
  38. infrahub_sdk/{code_generator.py → protocols_generator/generator.py} +47 -34
  39. infrahub_sdk/protocols_generator/template.j2 +114 -0
  40. infrahub_sdk/schema/__init__.py +110 -74
  41. infrahub_sdk/schema/main.py +36 -2
  42. infrahub_sdk/schema/repository.py +2 -0
  43. infrahub_sdk/spec/menu.py +3 -3
  44. infrahub_sdk/spec/object.py +522 -41
  45. infrahub_sdk/testing/docker.py +4 -5
  46. infrahub_sdk/testing/schemas/animal.py +7 -0
  47. infrahub_sdk/yaml.py +63 -7
  48. {infrahub_server-1.2.5.dist-info → infrahub_server-1.2.7.dist-info}/METADATA +1 -1
  49. {infrahub_server-1.2.5.dist-info → infrahub_server-1.2.7.dist-info}/RECORD +56 -39
  50. infrahub_testcontainers/container.py +52 -2
  51. infrahub_testcontainers/docker-compose.test.yml +27 -0
  52. infrahub_testcontainers/performance_test.py +1 -1
  53. infrahub_testcontainers/plugin.py +1 -1
  54. infrahub_sdk/ctl/constants.py +0 -115
  55. {infrahub_server-1.2.5.dist-info → infrahub_server-1.2.7.dist-info}/LICENSE.txt +0 -0
  56. {infrahub_server-1.2.5.dist-info → infrahub_server-1.2.7.dist-info}/WHEEL +0 -0
  57. {infrahub_server-1.2.5.dist-info → infrahub_server-1.2.7.dist-info}/entry_points.txt +0 -0
infrahub_sdk/node.py CHANGED
@@ -479,7 +479,7 @@ class RelationshipManagerBase:
479
479
  """
480
480
  data: dict[str, Any] = {
481
481
  "count": None,
482
- "edges": {"node": {"id": None, "display_label": None, "__typename": None}},
482
+ "edges": {"node": {"id": None, "hfid": None, "display_label": None, "__typename": None}},
483
483
  }
484
484
 
485
485
  properties: dict[str, Any] = {}
@@ -569,7 +569,9 @@ class RelationshipManager(RelationshipManagerBase):
569
569
  raise UninitializedError("Must call fetch() on RelationshipManager before editing members")
570
570
  new_node = RelatedNode(schema=self.schema, client=self.client, branch=self.branch, data=data)
571
571
 
572
- if new_node.id and new_node.id not in self.peer_ids:
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
+ ):
573
575
  self.peers.append(new_node)
574
576
  self._has_update = True
575
577
 
@@ -591,6 +593,14 @@ class RelationshipManager(RelationshipManagerBase):
591
593
  self.peers.pop(idx)
592
594
  self._has_update = True
593
595
 
596
+ elif node_to_remove.hfid and node_to_remove.hfid in self.peer_hfids:
597
+ idx = self.peer_hfids.index(node_to_remove.hfid)
598
+ if self.peers[idx].hfid != node_to_remove.hfid:
599
+ raise IndexError(f"Unexpected situation, the node with the index {idx} should be {node_to_remove.hfid}")
600
+
601
+ self.peers.pop(idx)
602
+ self._has_update = True
603
+
594
604
 
595
605
  class RelationshipManagerSync(RelationshipManagerBase):
596
606
  """Manages relationships of a node in a synchronous context."""
@@ -664,7 +674,9 @@ class RelationshipManagerSync(RelationshipManagerBase):
664
674
  raise UninitializedError("Must call fetch() on RelationshipManager before editing members")
665
675
  new_node = RelatedNodeSync(schema=self.schema, client=self.client, branch=self.branch, data=data)
666
676
 
667
- if new_node.id and new_node.id not in self.peer_ids:
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
+ ):
668
680
  self.peers.append(new_node)
669
681
  self._has_update = True
670
682
 
@@ -682,6 +694,13 @@ class RelationshipManagerSync(RelationshipManagerBase):
682
694
  idx = self.peer_ids.index(node_to_remove.id)
683
695
  if self.peers[idx].id != node_to_remove.id:
684
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}")
685
704
 
686
705
  self.peers.pop(idx)
687
706
  self._has_update = True
@@ -793,13 +812,12 @@ class InfrahubNodeBase:
793
812
  return self.get_human_friendly_id_as_string(include_kind=True)
794
813
 
795
814
  def _init_attributes(self, data: dict | None = None) -> None:
796
- for attr_name in self._attributes:
797
- attr_schema = [attr for attr in self._schema.attributes if attr.name == attr_name][0]
798
- attr_data = data.get(attr_name, None) if isinstance(data, dict) else None
815
+ for attr_schema in self._schema.attributes:
816
+ attr_data = data.get(attr_schema.name, None) if isinstance(data, dict) else None
799
817
  setattr(
800
818
  self,
801
- attr_name,
802
- Attribute(name=attr_name, schema=attr_schema, data=attr_data),
819
+ attr_schema.name,
820
+ Attribute(name=attr_schema.name, schema=attr_schema, data=attr_data),
803
821
  )
804
822
 
805
823
  def _get_request_context(self, request_context: RequestContext | None = None) -> dict[str, Any] | None:
@@ -1147,24 +1165,23 @@ class InfrahubNode(InfrahubNodeBase):
1147
1165
  return cls(client=client, schema=schema, branch=branch, data=cls._strip_alias(data))
1148
1166
 
1149
1167
  def _init_relationships(self, data: dict | None = None) -> None:
1150
- for rel_name in self._relationships:
1151
- rel_schema = [rel for rel in self._schema.relationships if rel.name == rel_name][0]
1152
- rel_data = data.get(rel_name, None) if isinstance(data, dict) else None
1168
+ for rel_schema in self._schema.relationships:
1169
+ rel_data = data.get(rel_schema.name, None) if isinstance(data, dict) else None
1153
1170
 
1154
1171
  if rel_schema.cardinality == "one":
1155
- setattr(self, f"_{rel_name}", None)
1172
+ setattr(self, f"_{rel_schema.name}", None)
1156
1173
  setattr(
1157
1174
  self.__class__,
1158
- rel_name,
1159
- generate_relationship_property(name=rel_name, node=self),
1175
+ rel_schema.name,
1176
+ generate_relationship_property(name=rel_schema.name, node=self),
1160
1177
  )
1161
- setattr(self, rel_name, rel_data)
1178
+ setattr(self, rel_schema.name, rel_data)
1162
1179
  else:
1163
1180
  setattr(
1164
1181
  self,
1165
- rel_name,
1182
+ rel_schema.name,
1166
1183
  RelationshipManager(
1167
- name=rel_name,
1184
+ name=rel_schema.name,
1168
1185
  client=self._client,
1169
1186
  node=self,
1170
1187
  branch=self._branch,
@@ -1679,24 +1696,23 @@ class InfrahubNodeSync(InfrahubNodeBase):
1679
1696
  return cls(client=client, schema=schema, branch=branch, data=cls._strip_alias(data))
1680
1697
 
1681
1698
  def _init_relationships(self, data: dict | None = None) -> None:
1682
- for rel_name in self._relationships:
1683
- rel_schema = [rel for rel in self._schema.relationships if rel.name == rel_name][0]
1684
- rel_data = data.get(rel_name, None) if isinstance(data, dict) else None
1699
+ for rel_schema in self._schema.relationships:
1700
+ rel_data = data.get(rel_schema.name, None) if isinstance(data, dict) else None
1685
1701
 
1686
1702
  if rel_schema.cardinality == "one":
1687
- setattr(self, f"_{rel_name}", None)
1703
+ setattr(self, f"_{rel_schema.name}", None)
1688
1704
  setattr(
1689
1705
  self.__class__,
1690
- rel_name,
1691
- generate_relationship_property(name=rel_name, node=self),
1706
+ rel_schema.name,
1707
+ generate_relationship_property(name=rel_schema.name, node=self),
1692
1708
  )
1693
- setattr(self, rel_name, rel_data)
1709
+ setattr(self, rel_schema.name, rel_data)
1694
1710
  else:
1695
1711
  setattr(
1696
1712
  self,
1697
- rel_name,
1713
+ rel_schema.name,
1698
1714
  RelationshipManagerSync(
1699
- name=rel_name,
1715
+ name=rel_schema.name,
1700
1716
  client=self._client,
1701
1717
  node=self,
1702
1718
  branch=self._branch,
File without changes
@@ -0,0 +1,28 @@
1
+ TEMPLATE_FILE_NAME = "template.j2"
2
+
3
+ ATTRIBUTE_KIND_MAP = {
4
+ "ID": "String",
5
+ "Text": "String",
6
+ "TextArea": "String",
7
+ "DateTime": "DateTime",
8
+ "Email": "String",
9
+ "Password": "String",
10
+ "HashedPassword": "HashedPassword",
11
+ "URL": "URL",
12
+ "File": "String",
13
+ "MacAddress": "MacAddress",
14
+ "Color": "String",
15
+ "Dropdown": "Dropdown",
16
+ "Number": "Integer",
17
+ "Bandwidth": "Integer",
18
+ "IPHost": "IPHost",
19
+ "IPNetwork": "IPNetwork",
20
+ "Boolean": "Boolean",
21
+ "Checkbox": "Boolean",
22
+ "List": "ListAttribute",
23
+ "JSON": "JSONAttribute",
24
+ "Any": "AnyAttribute",
25
+ }
26
+
27
+ # The order of the classes in the list determines the order of the classes in the generated code
28
+ CORE_BASE_CLASS_TO_SYNCIFY = ["CoreProfile", "CoreObjectTemplate", "CoreNode"]
@@ -1,13 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Mapping
4
- from typing import Any
4
+ from pathlib import Path
5
5
 
6
6
  import jinja2
7
7
 
8
- from . import protocols as sdk_protocols
9
- from .ctl.constants import PROTOCOLS_TEMPLATE
10
- from .schema import (
8
+ from .. import protocols as sdk_protocols
9
+ from ..schema import (
11
10
  AttributeSchemaAPI,
12
11
  GenericSchema,
13
12
  GenericSchemaAPI,
@@ -16,31 +15,22 @@ from .schema import (
16
15
  NodeSchemaAPI,
17
16
  ProfileSchemaAPI,
18
17
  RelationshipSchemaAPI,
18
+ TemplateSchemaAPI,
19
19
  )
20
+ from .constants import ATTRIBUTE_KIND_MAP, CORE_BASE_CLASS_TO_SYNCIFY, TEMPLATE_FILE_NAME
20
21
 
21
- ATTRIBUTE_KIND_MAP = {
22
- "ID": "String",
23
- "Text": "String",
24
- "TextArea": "String",
25
- "DateTime": "DateTime",
26
- "Email": "String",
27
- "Password": "String",
28
- "HashedPassword": "HashedPassword",
29
- "URL": "URL",
30
- "File": "String",
31
- "MacAddress": "MacAddress",
32
- "Color": "String",
33
- "Dropdown": "Dropdown",
34
- "Number": "Integer",
35
- "Bandwidth": "Integer",
36
- "IPHost": "IPHost",
37
- "IPNetwork": "IPNetwork",
38
- "Boolean": "Boolean",
39
- "Checkbox": "Boolean",
40
- "List": "ListAttribute",
41
- "JSON": "JSONAttribute",
42
- "Any": "AnyAttribute",
43
- }
22
+
23
+ def load_template() -> str:
24
+ path = Path(__file__).parent / TEMPLATE_FILE_NAME
25
+ return path.read_text()
26
+
27
+
28
+ def move_to_end_of_list(lst: list, item: str) -> list:
29
+ """Move an item to the end of a list if it exists in the list"""
30
+ if item in lst:
31
+ lst.remove(item)
32
+ lst.append(item)
33
+ return lst
44
34
 
45
35
 
46
36
  class CodeGenerator:
@@ -48,6 +38,7 @@ class CodeGenerator:
48
38
  self.generics: dict[str, GenericSchemaAPI | GenericSchema] = {}
49
39
  self.nodes: dict[str, NodeSchemaAPI | NodeSchema] = {}
50
40
  self.profiles: dict[str, ProfileSchemaAPI] = {}
41
+ self.templates: dict[str, TemplateSchemaAPI] = {}
51
42
 
52
43
  for name, schema_type in schema.items():
53
44
  if isinstance(schema_type, (GenericSchemaAPI, GenericSchema)):
@@ -56,6 +47,8 @@ class CodeGenerator:
56
47
  self.nodes[name] = schema_type
57
48
  if isinstance(schema_type, ProfileSchemaAPI):
58
49
  self.profiles[name] = schema_type
50
+ if isinstance(schema_type, TemplateSchemaAPI):
51
+ self.templates[name] = schema_type
59
52
 
60
53
  self.base_protocols = [
61
54
  e
@@ -71,29 +64,49 @@ class CodeGenerator:
71
64
  self.sorted_profiles = self._sort_and_filter_models(
72
65
  self.profiles, filters=["CoreProfile"] + self.base_protocols
73
66
  )
67
+ self.sorted_templates = self._sort_and_filter_models(
68
+ self.templates, filters=["CoreObjectTemplate"] + self.base_protocols
69
+ )
74
70
 
75
71
  def render(self, sync: bool = True) -> str:
76
72
  jinja2_env = jinja2.Environment(loader=jinja2.BaseLoader(), trim_blocks=True, lstrip_blocks=True)
77
- jinja2_env.filters["inheritance"] = self._jinja2_filter_inheritance
78
73
  jinja2_env.filters["render_attribute"] = self._jinja2_filter_render_attribute
79
74
  jinja2_env.filters["render_relationship"] = self._jinja2_filter_render_relationship
75
+ jinja2_env.filters["syncify"] = self._jinja2_filter_syncify
80
76
 
81
- template = jinja2_env.from_string(PROTOCOLS_TEMPLATE)
77
+ template = jinja2_env.from_string(load_template())
82
78
  return template.render(
83
79
  generics=self.sorted_generics,
84
80
  nodes=self.sorted_nodes,
85
81
  profiles=self.sorted_profiles,
82
+ templates=self.sorted_templates,
86
83
  base_protocols=self.base_protocols,
84
+ core_node_name="CoreNodeSync" if sync else "CoreNode",
87
85
  sync=sync,
88
86
  )
89
87
 
90
88
  @staticmethod
91
- def _jinja2_filter_inheritance(value: dict[str, Any]) -> str:
92
- inherit_from: list[str] = value.get("inherit_from", [])
89
+ def _jinja2_filter_syncify(value: str | list, sync: bool = False) -> str | list:
90
+ """Filter to help with the convertion to sync
91
+
92
+ If a string is provided, append Sync to the end of the string
93
+ If a list is provided, search for CoreNode and replace it with CoreNodeSync
94
+ """
95
+ if isinstance(value, list):
96
+ # Order the list based on the CORE_BASE_CLASS_TO_SYNCIFY list to ensure the base classes are always last
97
+ for item in CORE_BASE_CLASS_TO_SYNCIFY:
98
+ value = move_to_end_of_list(value, item)
99
+
100
+ if not sync:
101
+ return value
102
+
103
+ if isinstance(value, str):
104
+ return f"{value}Sync"
105
+
106
+ if isinstance(value, list):
107
+ return [f"{item}Sync" if item in CORE_BASE_CLASS_TO_SYNCIFY else item for item in value]
93
108
 
94
- if not inherit_from:
95
- return "CoreNode"
96
- return ", ".join(inherit_from)
109
+ return value
97
110
 
98
111
  @staticmethod
99
112
  def _jinja2_filter_render_attribute(value: AttributeSchemaAPI) -> str:
@@ -0,0 +1,114 @@
1
+ #
2
+ # Generated by "infrahubctl protocols"
3
+ #
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import TYPE_CHECKING, Optional
8
+
9
+ from infrahub_sdk.protocols import {{ "CoreNode" | syncify(sync) }}, {{ base_protocols | join(', ') }}
10
+
11
+ if TYPE_CHECKING:
12
+ from infrahub_sdk.node import {{ "RelatedNode" | syncify(sync) }}, {{ "RelationshipManager" | syncify(sync) }}
13
+ from infrahub_sdk.protocols_base import (
14
+ AnyAttribute,
15
+ AnyAttributeOptional,
16
+ String,
17
+ StringOptional,
18
+ Integer,
19
+ IntegerOptional,
20
+ Boolean,
21
+ BooleanOptional,
22
+ DateTime,
23
+ DateTimeOptional,
24
+ Dropdown,
25
+ DropdownOptional,
26
+ HashedPassword,
27
+ HashedPasswordOptional,
28
+ MacAddress,
29
+ MacAddressOptional,
30
+ IPHost,
31
+ IPHostOptional,
32
+ IPNetwork,
33
+ IPNetworkOptional,
34
+ JSONAttribute,
35
+ JSONAttributeOptional,
36
+ ListAttribute,
37
+ ListAttributeOptional,
38
+ URL,
39
+ URLOptional,
40
+ )
41
+
42
+ {% for generic in generics %}
43
+
44
+ class {{ generic.namespace + generic.name }}({{core_node_name}}):
45
+ {% if not generic.attributes|default([]) and not generic.relationships|default([]) %}
46
+ pass
47
+ {% endif %}
48
+ {% for attribute in generic.attributes | sort(attribute='name') | default([]) %}
49
+ {{ attribute | render_attribute }}
50
+ {% endfor %}
51
+ {% for relationship in generic.relationships | sort(attribute='name') | default([]) %}
52
+ {{ relationship | render_relationship(sync) }}
53
+ {% endfor %}
54
+ {% if generic.hierarchical | default(false) %}
55
+ parent: {{ "RelatedNode" | syncify(sync) }}
56
+ children: {{ "RelationshipManager" | syncify(sync) }}
57
+ {% endif %}
58
+ {% endfor %}
59
+
60
+
61
+ {% for node in nodes %}
62
+
63
+ class {{ node.namespace + node.name }}({{ node.inherit_from | syncify(sync) | join(", ") or core_node_name }}):
64
+ {% if not node.attributes|default([]) and not node.relationships|default([]) %}
65
+ pass
66
+ {% endif %}
67
+ {% for attribute in node.attributes | sort(attribute='name') | default([]) %}
68
+ {{ attribute | render_attribute }}
69
+ {% endfor %}
70
+ {% for relationship in node.relationships | sort(attribute='name') | default([]) %}
71
+ {{ relationship | render_relationship(sync) }}
72
+ {% endfor %}
73
+ {% if node.hierarchical | default(false) %}
74
+ parent: {{ "RelatedNode" | syncify(sync) }}
75
+ children: {{ "RelationshipManager" | syncify(sync) }}
76
+ {% endif %}
77
+
78
+ {% endfor %}
79
+
80
+
81
+ {% for node in profiles %}
82
+
83
+ class {{ node.namespace + node.name }}({{ node.inherit_from | syncify(sync) | join(", ") or core_node_name }}):
84
+ {% if not node.attributes|default([]) and not node.relationships|default([]) %}
85
+ pass
86
+ {% endif %}
87
+ {% for attribute in node.attributes | sort(attribute='name') | default([]) %}
88
+ {{ attribute | render_attribute }}
89
+ {% endfor %}
90
+ {% for relationship in node.relationships | sort(attribute='name') | default([]) %}
91
+ {{ relationship | render_relationship(sync) }}
92
+ {% endfor %}
93
+ {% if node.hierarchical | default(false) %}
94
+ parent: {{ "RelatedNode" | syncify(sync) }}
95
+ children: {{ "RelationshipManager" | syncify(sync) }}
96
+ {% endif %}
97
+
98
+ {% endfor %}
99
+
100
+
101
+ {% for node in templates %}
102
+
103
+ class {{ node.namespace + node.name }}({{ node.inherit_from | syncify(sync) | join(", ") or core_node_name }}):
104
+ {% if not node.attributes|default([]) and not node.relationships|default([]) %}
105
+ pass
106
+ {% endif %}
107
+ {% for attribute in node.attributes | sort(attribute='name') | default([]) %}
108
+ {{ attribute | render_attribute }}
109
+ {% endfor %}
110
+ {% for relationship in node.relationships | sort(attribute='name') | default([]) %}
111
+ {{ relationship | render_relationship(sync) }}
112
+ {% endfor %}
113
+
114
+ {% endfor %}