infrahub-server 1.2.12__py3-none-any.whl → 1.3.0__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 (205) hide show
  1. infrahub/actions/constants.py +130 -0
  2. infrahub/actions/gather.py +114 -0
  3. infrahub/actions/models.py +243 -0
  4. infrahub/actions/parsers.py +104 -0
  5. infrahub/actions/schema.py +393 -0
  6. infrahub/actions/tasks.py +119 -0
  7. infrahub/actions/triggers.py +21 -0
  8. infrahub/branch/__init__.py +0 -0
  9. infrahub/branch/tasks.py +29 -0
  10. infrahub/branch/triggers.py +22 -0
  11. infrahub/cli/db.py +3 -4
  12. infrahub/computed_attribute/gather.py +3 -1
  13. infrahub/computed_attribute/tasks.py +23 -29
  14. infrahub/core/account.py +24 -47
  15. infrahub/core/attribute.py +13 -15
  16. infrahub/core/constants/__init__.py +10 -0
  17. infrahub/core/constants/infrahubkind.py +9 -0
  18. infrahub/core/constraint/node/runner.py +3 -1
  19. infrahub/core/convert_object_type/__init__.py +0 -0
  20. infrahub/core/convert_object_type/conversion.py +124 -0
  21. infrahub/core/convert_object_type/schema_mapping.py +56 -0
  22. infrahub/core/diff/coordinator.py +8 -1
  23. infrahub/core/diff/query/all_conflicts.py +1 -5
  24. infrahub/core/diff/query/artifact.py +10 -20
  25. infrahub/core/diff/query/delete_query.py +8 -4
  26. infrahub/core/diff/query/diff_get.py +3 -6
  27. infrahub/core/diff/query/field_specifiers.py +1 -1
  28. infrahub/core/diff/query/field_summary.py +2 -4
  29. infrahub/core/diff/query/merge.py +72 -125
  30. infrahub/core/diff/query/save.py +28 -43
  31. infrahub/core/diff/query/summary_counts_enricher.py +34 -54
  32. infrahub/core/diff/query/time_range_query.py +0 -1
  33. infrahub/core/diff/repository/repository.py +4 -0
  34. infrahub/core/manager.py +14 -11
  35. infrahub/core/migrations/graph/m003_relationship_parent_optional.py +1 -2
  36. infrahub/core/migrations/graph/m012_convert_account_generic.py +1 -1
  37. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +2 -6
  38. infrahub/core/migrations/graph/m015_diff_format_update.py +1 -2
  39. infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +1 -2
  40. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +11 -22
  41. infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -6
  42. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +1 -2
  43. infrahub/core/migrations/graph/m023_deduplicate_cardinality_one_relationships.py +2 -2
  44. infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +1 -2
  45. infrahub/core/migrations/graph/m028_delete_diffs.py +1 -2
  46. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +30 -48
  47. infrahub/core/migrations/graph/m030_illegal_edges.py +1 -2
  48. infrahub/core/migrations/query/attribute_add.py +1 -2
  49. infrahub/core/migrations/query/attribute_rename.py +6 -11
  50. infrahub/core/migrations/query/delete_element_in_schema.py +19 -17
  51. infrahub/core/migrations/query/node_duplicate.py +19 -21
  52. infrahub/core/migrations/query/relationship_duplicate.py +19 -18
  53. infrahub/core/migrations/schema/node_attribute_remove.py +4 -8
  54. infrahub/core/migrations/schema/node_remove.py +19 -20
  55. infrahub/core/models.py +29 -2
  56. infrahub/core/node/__init__.py +131 -28
  57. infrahub/core/node/base.py +1 -1
  58. infrahub/core/node/create.py +211 -0
  59. infrahub/core/node/resource_manager/number_pool.py +31 -5
  60. infrahub/core/node/standard.py +6 -1
  61. infrahub/core/path.py +15 -1
  62. infrahub/core/protocols.py +57 -0
  63. infrahub/core/protocols_base.py +3 -0
  64. infrahub/core/query/__init__.py +2 -2
  65. infrahub/core/query/delete.py +3 -3
  66. infrahub/core/query/diff.py +19 -32
  67. infrahub/core/query/ipam.py +10 -20
  68. infrahub/core/query/node.py +29 -47
  69. infrahub/core/query/relationship.py +55 -34
  70. infrahub/core/query/resource_manager.py +1 -2
  71. infrahub/core/query/standard_node.py +19 -5
  72. infrahub/core/query/subquery.py +2 -4
  73. infrahub/core/relationship/constraints/count.py +10 -9
  74. infrahub/core/relationship/constraints/interface.py +2 -1
  75. infrahub/core/relationship/constraints/peer_kind.py +2 -1
  76. infrahub/core/relationship/constraints/peer_parent.py +56 -0
  77. infrahub/core/relationship/constraints/peer_relatives.py +72 -0
  78. infrahub/core/relationship/constraints/profiles_kind.py +1 -1
  79. infrahub/core/relationship/model.py +4 -1
  80. infrahub/core/schema/__init__.py +2 -1
  81. infrahub/core/schema/attribute_parameters.py +160 -0
  82. infrahub/core/schema/attribute_schema.py +130 -7
  83. infrahub/core/schema/basenode_schema.py +27 -3
  84. infrahub/core/schema/definitions/core/__init__.py +29 -1
  85. infrahub/core/schema/definitions/core/group.py +45 -0
  86. infrahub/core/schema/definitions/core/resource_pool.py +9 -0
  87. infrahub/core/schema/definitions/internal.py +43 -5
  88. infrahub/core/schema/generated/attribute_schema.py +16 -3
  89. infrahub/core/schema/generated/relationship_schema.py +11 -1
  90. infrahub/core/schema/manager.py +7 -2
  91. infrahub/core/schema/schema_branch.py +104 -9
  92. infrahub/core/validators/__init__.py +15 -2
  93. infrahub/core/validators/attribute/choices.py +1 -3
  94. infrahub/core/validators/attribute/enum.py +1 -3
  95. infrahub/core/validators/attribute/kind.py +1 -3
  96. infrahub/core/validators/attribute/length.py +13 -7
  97. infrahub/core/validators/attribute/min_max.py +118 -0
  98. infrahub/core/validators/attribute/number_pool.py +106 -0
  99. infrahub/core/validators/attribute/optional.py +1 -4
  100. infrahub/core/validators/attribute/regex.py +5 -6
  101. infrahub/core/validators/attribute/unique.py +1 -3
  102. infrahub/core/validators/determiner.py +18 -2
  103. infrahub/core/validators/enum.py +12 -0
  104. infrahub/core/validators/node/hierarchy.py +3 -6
  105. infrahub/core/validators/query.py +1 -3
  106. infrahub/core/validators/relationship/count.py +6 -12
  107. infrahub/core/validators/relationship/optional.py +2 -4
  108. infrahub/core/validators/relationship/peer.py +177 -12
  109. infrahub/core/validators/tasks.py +1 -1
  110. infrahub/core/validators/uniqueness/query.py +5 -9
  111. infrahub/database/__init__.py +12 -4
  112. infrahub/database/validation.py +1 -2
  113. infrahub/dependencies/builder/constraint/grouped/node_runner.py +4 -0
  114. infrahub/dependencies/builder/constraint/relationship_manager/peer_parent.py +8 -0
  115. infrahub/dependencies/builder/constraint/relationship_manager/peer_relatives.py +8 -0
  116. infrahub/dependencies/builder/constraint/schema/aggregated.py +2 -0
  117. infrahub/dependencies/builder/constraint/schema/relationship_peer.py +8 -0
  118. infrahub/dependencies/builder/diff/deserializer.py +1 -1
  119. infrahub/dependencies/registry.py +4 -0
  120. infrahub/events/group_action.py +1 -0
  121. infrahub/events/models.py +1 -1
  122. infrahub/git/base.py +5 -3
  123. infrahub/git/integrator.py +96 -5
  124. infrahub/git/tasks.py +1 -0
  125. infrahub/graphql/analyzer.py +139 -18
  126. infrahub/graphql/manager.py +4 -0
  127. infrahub/graphql/mutations/action.py +164 -0
  128. infrahub/graphql/mutations/convert_object_type.py +71 -0
  129. infrahub/graphql/mutations/main.py +24 -175
  130. infrahub/graphql/mutations/proposed_change.py +20 -17
  131. infrahub/graphql/mutations/relationship.py +32 -0
  132. infrahub/graphql/mutations/resource_manager.py +63 -7
  133. infrahub/graphql/queries/convert_object_type_mapping.py +34 -0
  134. infrahub/graphql/queries/resource_manager.py +7 -1
  135. infrahub/graphql/resolvers/many_relationship.py +1 -1
  136. infrahub/graphql/resolvers/resolver.py +2 -2
  137. infrahub/graphql/resolvers/single_relationship.py +1 -1
  138. infrahub/graphql/schema.py +6 -0
  139. infrahub/menu/menu.py +34 -2
  140. infrahub/message_bus/messages/__init__.py +0 -10
  141. infrahub/message_bus/operations/__init__.py +0 -8
  142. infrahub/message_bus/operations/refresh/registry.py +3 -6
  143. infrahub/patch/queries/delete_duplicated_edges.py +10 -15
  144. infrahub/pools/models.py +14 -0
  145. infrahub/pools/number.py +5 -3
  146. infrahub/pools/registration.py +22 -0
  147. infrahub/pools/tasks.py +126 -0
  148. infrahub/prefect_server/models.py +1 -19
  149. infrahub/proposed_change/models.py +68 -3
  150. infrahub/proposed_change/tasks.py +911 -34
  151. infrahub/schema/__init__.py +0 -0
  152. infrahub/schema/tasks.py +27 -0
  153. infrahub/schema/triggers.py +23 -0
  154. infrahub/task_manager/models.py +10 -6
  155. infrahub/trigger/catalogue.py +6 -0
  156. infrahub/trigger/models.py +23 -6
  157. infrahub/trigger/setup.py +26 -2
  158. infrahub/trigger/tasks.py +4 -2
  159. infrahub/types.py +6 -0
  160. infrahub/webhook/tasks.py +4 -8
  161. infrahub/workflows/catalogue.py +103 -1
  162. infrahub_sdk/client.py +43 -10
  163. infrahub_sdk/ctl/generator.py +4 -4
  164. infrahub_sdk/ctl/repository.py +1 -1
  165. infrahub_sdk/node/__init__.py +39 -0
  166. infrahub_sdk/node/attribute.py +122 -0
  167. infrahub_sdk/node/constants.py +21 -0
  168. infrahub_sdk/{node.py → node/node.py} +158 -803
  169. infrahub_sdk/node/parsers.py +15 -0
  170. infrahub_sdk/node/property.py +24 -0
  171. infrahub_sdk/node/related_node.py +266 -0
  172. infrahub_sdk/node/relationship.py +302 -0
  173. infrahub_sdk/protocols.py +112 -0
  174. infrahub_sdk/protocols_base.py +34 -2
  175. infrahub_sdk/pytest_plugin/items/python_transform.py +2 -1
  176. infrahub_sdk/query_groups.py +17 -5
  177. infrahub_sdk/schema/main.py +1 -0
  178. infrahub_sdk/schema/repository.py +16 -0
  179. infrahub_sdk/spec/object.py +1 -1
  180. infrahub_sdk/store.py +1 -1
  181. infrahub_sdk/testing/schemas/car_person.py +1 -0
  182. infrahub_sdk/utils.py +7 -20
  183. infrahub_sdk/yaml.py +6 -5
  184. {infrahub_server-1.2.12.dist-info → infrahub_server-1.3.0.dist-info}/METADATA +3 -3
  185. {infrahub_server-1.2.12.dist-info → infrahub_server-1.3.0.dist-info}/RECORD +192 -166
  186. infrahub_testcontainers/container.py +0 -1
  187. infrahub_testcontainers/docker-compose.test.yml +1 -1
  188. infrahub_testcontainers/helpers.py +8 -2
  189. infrahub/message_bus/messages/check_generator_run.py +0 -26
  190. infrahub/message_bus/messages/finalize_validator_execution.py +0 -15
  191. infrahub/message_bus/messages/proposed_change/base_with_diff.py +0 -16
  192. infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +0 -11
  193. infrahub/message_bus/messages/request_generatordefinition_check.py +0 -20
  194. infrahub/message_bus/messages/request_proposedchange_pipeline.py +0 -23
  195. infrahub/message_bus/operations/check/__init__.py +0 -3
  196. infrahub/message_bus/operations/check/generator.py +0 -156
  197. infrahub/message_bus/operations/finalize/__init__.py +0 -3
  198. infrahub/message_bus/operations/finalize/validator.py +0 -133
  199. infrahub/message_bus/operations/requests/__init__.py +0 -9
  200. infrahub/message_bus/operations/requests/generator_definition.py +0 -140
  201. infrahub/message_bus/operations/requests/proposed_change.py +0 -629
  202. /infrahub/{message_bus/messages/proposed_change → actions}/__init__.py +0 -0
  203. {infrahub_server-1.2.12.dist-info → infrahub_server-1.3.0.dist-info}/LICENSE.txt +0 -0
  204. {infrahub_server-1.2.12.dist-info → infrahub_server-1.3.0.dist-info}/WHEEL +0 -0
  205. {infrahub_server-1.2.12.dist-info → infrahub_server-1.3.0.dist-info}/entry_points.txt +0 -0
@@ -47,7 +47,6 @@ class RelationshipDuplicateQuery(Query):
47
47
  @staticmethod
48
48
  def _render_sub_query_per_rel_type(rel_name: str, rel_type: str, direction: GraphRelDirection) -> str:
49
49
  subquery = [
50
- f"WITH peer_node, {rel_name}, active_rel, new_rel",
51
50
  f"WITH peer_node, {rel_name}, active_rel, new_rel",
52
51
  f'WHERE type({rel_name}) = "{rel_type}"',
53
52
  ]
@@ -61,28 +60,32 @@ class RelationshipDuplicateQuery(Query):
61
60
  return "\n".join(subquery)
62
61
 
63
62
  @classmethod
64
- def _render_sub_query_out(cls) -> str:
63
+ def _render_sub_query_out(cls) -> tuple[str, str]:
64
+ rel_name = "rel_outband"
65
+ sub_query_out_args = f"peer_node, {rel_name}, active_rel, new_rel"
65
66
  sub_queries_out = [
66
67
  cls._render_sub_query_per_rel_type(
67
- rel_name="rel_outband", rel_type=rel_type, direction=GraphRelDirection.OUTBOUND
68
+ rel_name=rel_name, rel_type=rel_type, direction=GraphRelDirection.OUTBOUND
68
69
  )
69
70
  for rel_type, rel_def in GraphRelationshipRelationships.model_fields.items()
70
71
  if rel_def.default.direction in [GraphRelDirection.OUTBOUND, GraphRelDirection.EITHER]
71
72
  ]
72
73
  sub_query_out = "\nUNION\n".join(sub_queries_out)
73
- return sub_query_out
74
+ return sub_query_out, sub_query_out_args
74
75
 
75
76
  @classmethod
76
- def _render_sub_query_in(cls) -> str:
77
+ def _render_sub_query_in(cls) -> tuple[str, str]:
78
+ rel_name = "rel_inband"
79
+ sub_query_in_args = f"peer_node, {rel_name}, active_rel, new_rel"
77
80
  sub_queries_in = [
78
81
  cls._render_sub_query_per_rel_type(
79
- rel_name="rel_inband", rel_type=rel_type, direction=GraphRelDirection.INBOUND
82
+ rel_name=rel_name, rel_type=rel_type, direction=GraphRelDirection.INBOUND
80
83
  )
81
84
  for rel_type, rel_def in GraphRelationshipRelationships.model_fields.items()
82
85
  if rel_def.default.direction in [GraphRelDirection.INBOUND, GraphRelDirection.EITHER]
83
86
  ]
84
87
  sub_query_in = "\nUNION\n".join(sub_queries_in)
85
- return sub_query_in
88
+ return sub_query_in, sub_query_in_args
86
89
 
87
90
  async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
88
91
  branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string())
@@ -109,15 +112,13 @@ class RelationshipDuplicateQuery(Query):
109
112
  "from": self.at.to_string(),
110
113
  }
111
114
 
112
- sub_query_out = self._render_sub_query_out()
113
- sub_query_in = self._render_sub_query_in()
115
+ sub_query_out, sub_query_out_args = self._render_sub_query_out()
116
+ sub_query_in, sub_query_in_args = self._render_sub_query_in()
114
117
 
115
118
  self.add_to_query(self.render_match())
116
119
 
117
- # ruff: noqa: E501
118
120
  query = """
119
- CALL {
120
- WITH source, rel, destination
121
+ CALL (source, rel, destination) {
121
122
  MATCH path = (source)-[r1:IS_RELATED]-(rel)-[r2:IS_RELATED]-(destination)
122
123
  WHERE all(r IN relationships(path) WHERE %(branch_filter)s)
123
124
  RETURN rel as rel1, r1 as r11, r2 as r12
@@ -130,8 +131,7 @@ class RelationshipDuplicateQuery(Query):
130
131
  WITH DISTINCT(active_rel) as active_rel, new_rel
131
132
  // Process Inbound Relationship
132
133
  MATCH (active_rel)<-[]-(peer)
133
- CALL {
134
- WITH active_rel, peer
134
+ CALL (active_rel, peer) {
135
135
  MATCH (active_rel)<-[r]-(peer)
136
136
  WHERE %(branch_filter)s
137
137
  RETURN active_rel as n1, r as rel_inband1, peer as p1
@@ -140,7 +140,7 @@ class RelationshipDuplicateQuery(Query):
140
140
  }
141
141
  WITH n1 as active_rel, rel_inband1 as rel_inband, p1 as peer_node, new_rel
142
142
  WHERE rel_inband.status = "active"
143
- CALL {
143
+ CALL (%(sub_query_in_args)s) {
144
144
  %(sub_query_in)s
145
145
  }
146
146
  WITH p2 as peer_node, rel_inband, active_rel, new_rel
@@ -150,8 +150,7 @@ class RelationshipDuplicateQuery(Query):
150
150
  WITH DISTINCT(active_rel) as active_rel, new_rel
151
151
  // Process Outbound Relationship
152
152
  MATCH (active_rel)-[]->(peer)
153
- CALL {
154
- WITH active_rel, peer
153
+ CALL (active_rel, peer) {
155
154
  MATCH (active_rel)-[r]->(peer)
156
155
  WHERE %(branch_filter)s
157
156
  RETURN active_rel as n1, r as rel_outband1, peer as p1
@@ -160,7 +159,7 @@ class RelationshipDuplicateQuery(Query):
160
159
  }
161
160
  WITH n1 as active_rel, rel_outband1 as rel_outband, p1 as peer_node, new_rel
162
161
  WHERE rel_outband.status = "active"
163
- CALL {
162
+ CALL (%(sub_query_out_args)s) {
164
163
  %(sub_query_out)s
165
164
  }
166
165
  WITH p2 as peer_node, rel_outband, active_rel, new_rel
@@ -172,5 +171,7 @@ class RelationshipDuplicateQuery(Query):
172
171
  "branch_filter": branch_filter,
173
172
  "sub_query_out": sub_query_out,
174
173
  "sub_query_in": sub_query_in,
174
+ "sub_query_in_args": sub_query_in_args,
175
+ "sub_query_out_args": sub_query_out_args,
175
176
  }
176
177
  self.add_to_query(query)
@@ -50,7 +50,6 @@ class NodeAttributeRemoveMigrationQuery01(AttributeMigrationQuery):
50
50
 
51
51
  def render_sub_query_per_rel_type(rel_type: str, rel_def: FieldInfo) -> str:
52
52
  subquery = [
53
- "WITH peer_node, rb, active_attr",
54
53
  "WITH peer_node, rb, active_attr",
55
54
  f'WHERE type(rb) = "{rel_type}"',
56
55
  ]
@@ -75,8 +74,7 @@ class NodeAttributeRemoveMigrationQuery01(AttributeMigrationQuery):
75
74
  MATCH (node:%(node_kind)s)
76
75
  WHERE (size($kinds_to_ignore) = 0 OR NOT any(l IN labels(node) WHERE l IN $kinds_to_ignore))
77
76
  AND exists((node)-[:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name }))
78
- CALL {
79
- WITH node
77
+ CALL (node) {
80
78
  MATCH (root:Root)<-[r:IS_PART_OF]-(node)
81
79
  WHERE %(branch_filter)s
82
80
  RETURN node as n1, r as r1
@@ -86,8 +84,7 @@ class NodeAttributeRemoveMigrationQuery01(AttributeMigrationQuery):
86
84
  WITH n1 as active_node, r1 as rb
87
85
  WHERE rb.status = "active"
88
86
  // Find all the attributes that need to be updated
89
- CALL {
90
- WITH active_node
87
+ CALL (active_node) {
91
88
  MATCH (active_node)-[r:HAS_ATTRIBUTE]-(attr:Attribute { name: $attr_name })
92
89
  WHERE %(branch_filter)s
93
90
  RETURN active_node as n1, r as r1, attr as attr1
@@ -98,8 +95,7 @@ class NodeAttributeRemoveMigrationQuery01(AttributeMigrationQuery):
98
95
  WHERE rb.status = "active"
99
96
  WITH active_attr
100
97
  MATCH (active_attr)-[]-(peer)
101
- CALL {
102
- WITH active_attr, peer
98
+ CALL (active_attr, peer) {
103
99
  MATCH (active_attr)-[r]-(peer)
104
100
  WHERE %(branch_filter)s
105
101
  RETURN active_attr as a1, r as r1, peer as p1
@@ -108,7 +104,7 @@ class NodeAttributeRemoveMigrationQuery01(AttributeMigrationQuery):
108
104
  }
109
105
  WITH a1 as active_attr, r1 as rb, p1 as peer_node
110
106
  WHERE rb.status = "active"
111
- CALL {
107
+ CALL (peer_node, rb, active_attr) {
112
108
  %(sub_query_all)s
113
109
  }
114
110
  WITH p2 as peer_node, rb, active_attr
@@ -21,7 +21,6 @@ class NodeRemoveMigrationBaseQuery(MigrationQuery):
21
21
  rel_def: FieldInfo,
22
22
  ) -> str:
23
23
  subquery = [
24
- f"WITH peer_node, {rel_name}, active_node",
25
24
  f"WITH peer_node, {rel_name}, active_node",
26
25
  f'WHERE type({rel_name}) = "{rel_type}"',
27
26
  ]
@@ -59,12 +58,10 @@ class NodeRemoveMigrationBaseQuery(MigrationQuery):
59
58
 
60
59
  node_remove_query = self.render_node_remove_query(branch_filter=branch_filter)
61
60
 
62
- # ruff: noqa: E501
63
61
  query = """
64
62
  // Find all the active nodes
65
63
  MATCH (node:%(node_kind)s)
66
- CALL {
67
- WITH node
64
+ CALL (node) {
68
65
  MATCH (root:Root)<-[r:IS_PART_OF]-(node)
69
66
  WHERE %(branch_filter)s
70
67
  RETURN node as n1, r as r1
@@ -91,13 +88,12 @@ class NodeRemoveMigrationQueryIn(NodeRemoveMigrationBaseQuery):
91
88
  insert_return: bool = False
92
89
 
93
90
  def render_node_remove_query(self, branch_filter: str) -> str:
94
- sub_query = self.render_sub_query_in()
91
+ sub_query, sub_query_args = self.render_sub_query_in()
95
92
  query = """
96
93
  // Process Inbound Relationship
97
94
  WITH active_node
98
95
  MATCH (active_node)<-[]-(peer)
99
- CALL {
100
- WITH active_node, peer
96
+ CALL (active_node, peer) {
101
97
  MATCH (active_node)-[r]->(peer)
102
98
  WHERE %(branch_filter)s
103
99
  RETURN active_node as n1, r as rel_inband1, peer as p1
@@ -106,27 +102,29 @@ class NodeRemoveMigrationQueryIn(NodeRemoveMigrationBaseQuery):
106
102
  }
107
103
  WITH n1 as active_node, rel_inband1 as rel_inband, p1 as peer_node
108
104
  WHERE rel_inband.status = "active"
109
- CALL {
105
+ CALL (%(sub_query_args)s) {
110
106
  %(sub_query)s
111
107
  }
112
108
  WITH p2 as peer_node, rel_inband, active_node
113
109
  FOREACH (i in CASE WHEN rel_inband.branch IN ["-global-", $branch] THEN [1] ELSE [] END |
114
110
  SET rel_inband.to = $current_time
115
111
  )
116
- """ % {"sub_query": sub_query, "branch_filter": branch_filter}
112
+ """ % {"sub_query": sub_query, "sub_query_args": sub_query_args, "branch_filter": branch_filter}
117
113
  return query
118
114
 
119
- def render_sub_query_in(self) -> str:
115
+ def render_sub_query_in(self) -> tuple[str, str]:
116
+ rel_name = "rel_inband"
117
+ sub_query_in_args = f"peer_node, {rel_name}, active_node"
120
118
  sub_queries_in = [
121
119
  self.render_sub_query_per_rel_type(
122
- rel_name="rel_inband",
120
+ rel_name=rel_name,
123
121
  rel_type=rel_type,
124
122
  rel_def=rel_def,
125
123
  )
126
124
  for rel_type, rel_def in GraphNodeRelationships.model_fields.items()
127
125
  ]
128
126
  sub_query_in = "\nUNION\n".join(sub_queries_in)
129
- return sub_query_in
127
+ return sub_query_in, sub_query_in_args
130
128
 
131
129
  def get_nbr_migrations_executed(self) -> int:
132
130
  return 0
@@ -137,13 +135,12 @@ class NodeRemoveMigrationQueryOut(NodeRemoveMigrationBaseQuery):
137
135
  insert_return: bool = False
138
136
 
139
137
  def render_node_remove_query(self, branch_filter: str) -> str:
140
- sub_query = self.render_sub_query_out()
138
+ sub_query, sub_query_args = self.render_sub_query_out()
141
139
  query = """
142
140
  // Process Outbound Relationship
143
141
  WITH active_node
144
142
  MATCH (active_node)-[]->(peer)
145
- CALL {
146
- WITH active_node, peer
143
+ CALL (active_node, peer) {
147
144
  MATCH (active_node)-[r]->(peer)
148
145
  WHERE %(branch_filter)s
149
146
  RETURN active_node as n1, r as rel_outband1, peer as p1
@@ -152,27 +149,29 @@ class NodeRemoveMigrationQueryOut(NodeRemoveMigrationBaseQuery):
152
149
  }
153
150
  WITH n1 as active_node, rel_outband1 as rel_outband, p1 as peer_node
154
151
  WHERE rel_outband.status = "active"
155
- CALL {
152
+ CALL (%(sub_query_args)s) {
156
153
  %(sub_query)s
157
154
  }
158
155
  FOREACH (i in CASE WHEN rel_outband.branch IN ["-global-", $branch] THEN [1] ELSE [] END |
159
156
  SET rel_outband.to = $current_time
160
157
  )
161
- """ % {"sub_query": sub_query, "branch_filter": branch_filter}
158
+ """ % {"sub_query": sub_query, "sub_query_args": sub_query_args, "branch_filter": branch_filter}
162
159
 
163
160
  return query
164
161
 
165
- def render_sub_query_out(self) -> str:
162
+ def render_sub_query_out(self) -> tuple[str, str]:
163
+ rel_name = "rel_outband"
164
+ sub_query_out_args = f"peer_node, {rel_name}, active_node"
166
165
  sub_queries_out = [
167
166
  self.render_sub_query_per_rel_type(
168
- rel_name="rel_outband",
167
+ rel_name=rel_name,
169
168
  rel_type=rel_type,
170
169
  rel_def=rel_def,
171
170
  )
172
171
  for rel_type, rel_def in GraphNodeRelationships.model_fields.items()
173
172
  ]
174
173
  sub_query_out = "\nUNION\n".join(sub_queries_out)
175
- return sub_query_out
174
+ return sub_query_out, sub_query_out_args
176
175
 
177
176
  def get_nbr_migrations_executed(self) -> int:
178
177
  return self.num_of_results
infrahub/core/models.py CHANGED
@@ -20,6 +20,7 @@ if TYPE_CHECKING:
20
20
  from infrahub.core.schema.schema_branch import SchemaBranch
21
21
 
22
22
  GENERIC_ATTRIBUTES_TO_IGNORE = ["namespace", "name", "branch"]
23
+ PROPERTY_NAMES_TO_IGNORE = ["regex", "min_length", "max_length"]
23
24
 
24
25
 
25
26
  class NodeKind(BaseModel):
@@ -252,11 +253,37 @@ class SchemaUpdateValidationResult(BaseModel):
252
253
  if not sub_field_diff:
253
254
  raise ValueError("sub_field_diff must be defined, unexpected situation")
254
255
 
255
- for prop_name in sub_field_diff.changed:
256
+ for prop_name, prop_diff in sub_field_diff.changed.items():
257
+ if prop_name in PROPERTY_NAMES_TO_IGNORE:
258
+ continue
259
+
256
260
  field_info = field.model_fields[prop_name]
257
261
  field_update = str(field_info.json_schema_extra.get("update")) # type: ignore[union-attr]
258
262
 
259
- schema_path = SchemaPath( # type: ignore[call-arg]
263
+ if isinstance(prop_diff, HashableModelDiff):
264
+ for param_field_name in prop_diff.changed:
265
+ # override field_update if this field has its own json_schema_extra.update
266
+ try:
267
+ prop_field = getattr(field, prop_name)
268
+ param_field_info = prop_field.model_fields[param_field_name]
269
+ param_field_update = str(param_field_info.json_schema_extra.get("update"))
270
+ except (AttributeError, KeyError):
271
+ param_field_update = None
272
+
273
+ schema_path = SchemaPath(
274
+ schema_kind=schema.kind,
275
+ path_type=path_type,
276
+ field_name=field_name,
277
+ property_name=f"{prop_name}.{param_field_name}",
278
+ )
279
+
280
+ self._process_field(
281
+ schema_path=schema_path,
282
+ field_update=param_field_update or field_update,
283
+ )
284
+ continue
285
+
286
+ schema_path = SchemaPath(
260
287
  schema_kind=schema.kind,
261
288
  path_type=path_type,
262
289
  field_name=field_name,
@@ -7,6 +7,7 @@ from infrahub_sdk.template import Jinja2Template
7
7
  from infrahub_sdk.utils import is_valid_uuid
8
8
  from infrahub_sdk.uuidt import UUIDT
9
9
 
10
+ from infrahub import lock
10
11
  from infrahub.core import registry
11
12
  from infrahub.core.changelog.models import NodeChangelog
12
13
  from infrahub.core.constants import (
@@ -16,15 +17,25 @@ from infrahub.core.constants import (
16
17
  BranchSupportType,
17
18
  ComputedAttributeKind,
18
19
  InfrahubKind,
20
+ NumberPoolType,
19
21
  RelationshipCardinality,
20
22
  RelationshipKind,
21
23
  )
22
24
  from infrahub.core.constants.schema import SchemaElementPathType
23
25
  from infrahub.core.protocols import CoreNumberPool, CoreObjectTemplate
24
26
  from infrahub.core.query.node import NodeCheckIDQuery, NodeCreateAllQuery, NodeDeleteQuery, NodeGetListQuery
25
- from infrahub.core.schema import AttributeSchema, NodeSchema, ProfileSchema, RelationshipSchema, TemplateSchema
27
+ from infrahub.core.schema import (
28
+ AttributeSchema,
29
+ NodeSchema,
30
+ NonGenericSchemaTypes,
31
+ ProfileSchema,
32
+ RelationshipSchema,
33
+ TemplateSchema,
34
+ )
35
+ from infrahub.core.schema.attribute_parameters import NumberPoolParameters
26
36
  from infrahub.core.timestamp import Timestamp
27
37
  from infrahub.exceptions import InitializationError, NodeNotFoundError, PoolExhaustedError, ValidationError
38
+ from infrahub.pools.models import NumberPoolLockDefinition
28
39
  from infrahub.types import ATTRIBUTE_TYPES
29
40
 
30
41
  from ...graphql.constants import KIND_GRAPHQL_FIELD_NAME
@@ -66,7 +77,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
66
77
  _meta.default_filter = default_filter
67
78
  super().__init_subclass_with_meta__(_meta=_meta, **options)
68
79
 
69
- def get_schema(self) -> NodeSchema | ProfileSchema | TemplateSchema:
80
+ def get_schema(self) -> NonGenericSchemaTypes:
70
81
  return self._schema
71
82
 
72
83
  def get_kind(self) -> str:
@@ -247,6 +258,12 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
247
258
  within the create code.
248
259
  """
249
260
 
261
+ number_pool_parameters: NumberPoolParameters | None = None
262
+ if attribute.schema.kind == "NumberPool" and isinstance(attribute.schema.parameters, NumberPoolParameters):
263
+ attribute.from_pool = {"id": attribute.schema.parameters.number_pool_id}
264
+ attribute.is_default = False
265
+ number_pool_parameters = attribute.schema.parameters
266
+
250
267
  if not attribute.from_pool:
251
268
  return
252
269
 
@@ -255,19 +272,25 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
255
272
  db=db, id=attribute.from_pool["id"], kind=CoreNumberPool
256
273
  )
257
274
  except NodeNotFoundError:
258
- errors.append(
259
- ValidationError(
260
- {f"{attribute.name}.from_pool": f"The pool requested {attribute.from_pool} was not found."}
275
+ if number_pool_parameters:
276
+ number_pool = await self._fetch_or_create_number_pool(
277
+ db=db, attribute=attribute, number_pool_parameters=number_pool_parameters
261
278
  )
262
- )
263
- return
279
+
280
+ else:
281
+ errors.append(
282
+ ValidationError(
283
+ {f"{attribute.name}.from_pool": f"The pool requested {attribute.from_pool} was not found."}
284
+ )
285
+ )
286
+ return
264
287
 
265
288
  if (
266
289
  number_pool.node.value in [self._schema.kind] + self._schema.inherit_from
267
290
  and number_pool.node_attribute.value == attribute.name
268
291
  ):
269
292
  try:
270
- next_free = await number_pool.get_resource(db=db, branch=self._branch, node=self)
293
+ next_free = await number_pool.get_resource(db=db, branch=self._branch, node=self, attribute=attribute)
271
294
  except PoolExhaustedError:
272
295
  errors.append(
273
296
  ValidationError({f"{attribute.name}.from_pool": f"The pool {number_pool.node.value} is exhausted."})
@@ -285,6 +308,50 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
285
308
  )
286
309
  )
287
310
 
311
+ async def _fetch_or_create_number_pool(
312
+ self, db: InfrahubDatabase, attribute: BaseAttribute, number_pool_parameters: NumberPoolParameters
313
+ ) -> CoreNumberPool:
314
+ number_pool_from_db: CoreNumberPool | None = None
315
+ lock_definition = NumberPoolLockDefinition(pool_id=str(number_pool_parameters.number_pool_id))
316
+ async with lock.registry.get(
317
+ name=lock_definition.lock_name, namespace=lock_definition.namespace_name, local=False
318
+ ):
319
+ try:
320
+ number_pool_from_db = await registry.manager.get_one_by_id_or_default_filter(
321
+ db=db, id=str(number_pool_parameters.number_pool_id), kind=CoreNumberPool
322
+ )
323
+ except NodeNotFoundError:
324
+ schema = db.schema.get_node_schema(name="CoreNumberPool", duplicate=False)
325
+
326
+ pool_node = self._schema.kind
327
+ schema_attribute = self._schema.get_attribute(attribute.schema.name)
328
+ if schema_attribute.inherited:
329
+ for generic_name in self._schema.inherit_from:
330
+ generic_node = db.schema.get_generic_schema(name=generic_name, duplicate=False)
331
+ if attribute.schema.name in generic_node.attribute_names:
332
+ pool_node = generic_node.kind
333
+ break
334
+
335
+ number_pool = await Node.init(db=db, schema=schema, branch=self._branch)
336
+ await number_pool.new(
337
+ db=db,
338
+ id=number_pool_parameters.number_pool_id,
339
+ name=f"{pool_node}.{attribute.schema.name} [{number_pool_parameters.number_pool_id}]",
340
+ node=pool_node,
341
+ node_attribute=attribute.schema.name,
342
+ start_range=number_pool_parameters.start_range,
343
+ end_range=number_pool_parameters.end_range,
344
+ pool_type=NumberPoolType.SCHEMA.value,
345
+ )
346
+ await number_pool.save(db=db)
347
+
348
+ # Do a lookup of the number pool to get the correct mapped type from the registry
349
+ # without this we don't get access to the .get_resource() method.
350
+ created_pool: CoreNumberPool = number_pool_from_db or await registry.manager.get_one_by_id_or_default_filter(
351
+ db=db, id=number_pool.id, kind=CoreNumberPool
352
+ )
353
+ return created_pool
354
+
288
355
  async def handle_object_template(self, fields: dict, db: InfrahubDatabase, errors: list) -> None:
289
356
  """Fill the `fields` parameters with values from an object template if one is in use."""
290
357
  object_template_field = fields.get(OBJECT_TEMPLATE_RELATIONSHIP_NAME)
@@ -369,6 +436,9 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
369
436
  self._computed_jinja2_attributes.append(mandatory_attr)
370
437
  continue
371
438
 
439
+ if mandatory_attribute.kind == "NumberPool":
440
+ continue
441
+
372
442
  errors.append(
373
443
  ValidationError({mandatory_attr: f"{mandatory_attr} is mandatory for {self.get_kind()}"})
374
444
  )
@@ -385,6 +455,21 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
385
455
  # -------------------------------------------
386
456
  # Generate Attribute and Relationship and assign them
387
457
  # -------------------------------------------
458
+ errors.extend(await self._process_fields_relationships(fields=fields, db=db))
459
+ errors.extend(await self._process_fields_attributes(fields=fields, db=db))
460
+
461
+ if errors:
462
+ raise ValidationError(errors)
463
+
464
+ # Check if any post processor have been defined
465
+ # A processor can be used for example to assigne a default value
466
+ for name in self._attributes + self._relationships:
467
+ if hasattr(self, f"process_{name}"):
468
+ await getattr(self, f"process_{name}")(db=db)
469
+
470
+ async def _process_fields_relationships(self, fields: dict, db: InfrahubDatabase) -> list[ValidationError]:
471
+ errors: list[ValidationError] = []
472
+
388
473
  for rel_schema in self._schema.relationships:
389
474
  self._relationships.append(rel_schema.name)
390
475
 
@@ -406,6 +491,11 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
406
491
  except ValidationError as exc:
407
492
  errors.append(exc)
408
493
 
494
+ return errors
495
+
496
+ async def _process_fields_attributes(self, fields: dict, db: InfrahubDatabase) -> list[ValidationError]:
497
+ errors: list[ValidationError] = []
498
+
409
499
  for attr_schema in self._schema.attributes:
410
500
  self._attributes.append(attr_schema.name)
411
501
  if not self._existing and attr_schema.name in self._computed_jinja2_attributes:
@@ -434,14 +524,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
434
524
  except ValidationError as exc:
435
525
  errors.append(exc)
436
526
 
437
- if errors:
438
- raise ValidationError(errors)
439
-
440
- # Check if any post processor have been defined
441
- # A processor can be used for example to assigne a default value
442
- for name in self._attributes + self._relationships:
443
- if hasattr(self, f"process_{name}"):
444
- await getattr(self, f"process_{name}")(db=db)
527
+ return errors
445
528
 
446
529
  async def _process_macros(self, db: InfrahubDatabase) -> None:
447
530
  schema_branch = db.schema.get_schema_branch(self._branch.name)
@@ -474,17 +557,21 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
474
557
  relationship_attribute: RelationshipManager = getattr(
475
558
  self, attribute_path.active_relationship_schema.name
476
559
  )
477
- peer = await relationship_attribute.get_peer(db=db, raise_on_error=True)
478
-
479
- related_node = await registry.manager.get_one_by_id_or_default_filter(
480
- db=db, id=peer.id, kind=attribute_path.active_relationship_schema.peer, branch=self._branch.name
481
- )
560
+ if peer := await relationship_attribute.get_peer(db=db, raise_on_error=False):
561
+ related_node = await registry.manager.get_one_by_id_or_default_filter(
562
+ db=db,
563
+ id=peer.id,
564
+ kind=attribute_path.active_relationship_schema.peer,
565
+ branch=self._branch.name,
566
+ )
482
567
 
483
- attribute: BaseAttribute = getattr(
484
- getattr(related_node, attribute_path.active_attribute_schema.name),
485
- attribute_path.active_attribute_property_name,
486
- )
487
- variables[variable] = attribute
568
+ attribute: BaseAttribute = getattr(
569
+ getattr(related_node, attribute_path.active_attribute_schema.name),
570
+ attribute_path.active_attribute_property_name,
571
+ )
572
+ variables[variable] = attribute
573
+ else:
574
+ variables[variable] = None
488
575
 
489
576
  elif attribute_path.is_type_attribute:
490
577
  attribute = getattr(
@@ -872,9 +959,11 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
872
959
  if relationship.kind == RelationshipKind.PARENT:
873
960
  return relationship.name
874
961
 
875
- async def get_object_template(self, db: InfrahubDatabase) -> Node | None:
962
+ async def get_object_template(self, db: InfrahubDatabase) -> CoreObjectTemplate | None:
876
963
  object_template: RelationshipManager = getattr(self, OBJECT_TEMPLATE_RELATIONSHIP_NAME, None)
877
- return await object_template.get_peer(db=db) if object_template is not None else None
964
+ return (
965
+ await object_template.get_peer(db=db, peer_type=CoreObjectTemplate) if object_template is not None else None
966
+ )
878
967
 
879
968
  def get_relationships(
880
969
  self, kind: RelationshipKind, exclude: Sequence[str] | None = None
@@ -888,3 +977,17 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
888
977
  for relationship in self.get_schema().relationships
889
978
  if relationship.name not in exclude and relationship.kind == kind
890
979
  ]
980
+
981
+ def validate_relationships(self) -> None:
982
+ for name in self._relationships:
983
+ relm: RelationshipManager = getattr(self, name)
984
+ relm.validate()
985
+
986
+ async def get_parent_relationship_peer(self, db: InfrahubDatabase, name: str) -> Node | None:
987
+ """When a node has a parent relationship of a given name, this method returns the peer of that relationship."""
988
+ relationship = self.get_schema().get_relationship(name=name)
989
+ if relationship.kind != RelationshipKind.PARENT:
990
+ raise ValueError(f"Relationship '{name}' is not of kind 'parent'")
991
+
992
+ relm: RelationshipManager = getattr(self, name)
993
+ return await relm.get_peer(db=db)
@@ -52,7 +52,7 @@ class BaseNodeOptions(BaseOptions):
52
52
 
53
53
 
54
54
  class ObjectNodeMeta(BaseNodeMeta):
55
- def __new__(mcs, name_, bases, namespace, **options): # noqa: N804
55
+ def __new__(mcs, name_, bases, namespace, **options):
56
56
  # Note: it's safe to pass options as keyword arguments as they are still type-checked by NodeOptions.
57
57
 
58
58
  # We create this type, to then overload it with the dataclass attrs