infrahub-server 1.2.9rc0__py3-none-any.whl → 1.3.0a0__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 (166) hide show
  1. infrahub/actions/constants.py +86 -0
  2. infrahub/actions/gather.py +114 -0
  3. infrahub/actions/models.py +241 -0
  4. infrahub/actions/parsers.py +104 -0
  5. infrahub/actions/schema.py +382 -0
  6. infrahub/actions/tasks.py +126 -0
  7. infrahub/actions/triggers.py +21 -0
  8. infrahub/cli/db.py +1 -2
  9. infrahub/computed_attribute/models.py +13 -0
  10. infrahub/computed_attribute/tasks.py +48 -26
  11. infrahub/config.py +9 -0
  12. infrahub/core/account.py +24 -47
  13. infrahub/core/attribute.py +53 -14
  14. infrahub/core/branch/models.py +8 -9
  15. infrahub/core/branch/tasks.py +0 -2
  16. infrahub/core/constants/infrahubkind.py +8 -0
  17. infrahub/core/constraint/node/runner.py +1 -1
  18. infrahub/core/convert_object_type/__init__.py +0 -0
  19. infrahub/core/convert_object_type/conversion.py +122 -0
  20. infrahub/core/convert_object_type/schema_mapping.py +56 -0
  21. infrahub/core/diff/calculator.py +65 -11
  22. infrahub/core/diff/combiner.py +38 -31
  23. infrahub/core/diff/coordinator.py +44 -28
  24. infrahub/core/diff/data_check_synchronizer.py +3 -2
  25. infrahub/core/diff/enricher/hierarchy.py +36 -27
  26. infrahub/core/diff/ipam_diff_parser.py +5 -4
  27. infrahub/core/diff/merger/merger.py +46 -16
  28. infrahub/core/diff/merger/serializer.py +1 -0
  29. infrahub/core/diff/model/field_specifiers_map.py +64 -0
  30. infrahub/core/diff/model/path.py +58 -58
  31. infrahub/core/diff/parent_node_adder.py +14 -16
  32. infrahub/core/diff/query/all_conflicts.py +1 -5
  33. infrahub/core/diff/query/artifact.py +10 -20
  34. infrahub/core/diff/query/diff_get.py +3 -6
  35. infrahub/core/diff/query/drop_nodes.py +42 -0
  36. infrahub/core/diff/query/field_specifiers.py +8 -7
  37. infrahub/core/diff/query/field_summary.py +2 -4
  38. infrahub/core/diff/query/filters.py +15 -1
  39. infrahub/core/diff/query/merge.py +284 -101
  40. infrahub/core/diff/query/save.py +26 -34
  41. infrahub/core/diff/query/summary_counts_enricher.py +34 -54
  42. infrahub/core/diff/query_parser.py +55 -65
  43. infrahub/core/diff/repository/deserializer.py +38 -24
  44. infrahub/core/diff/repository/repository.py +31 -12
  45. infrahub/core/diff/tasks.py +3 -3
  46. infrahub/core/graph/__init__.py +1 -1
  47. infrahub/core/manager.py +14 -11
  48. infrahub/core/migrations/graph/__init__.py +2 -0
  49. infrahub/core/migrations/graph/m003_relationship_parent_optional.py +1 -2
  50. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +2 -4
  51. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +11 -22
  52. infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -6
  53. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +1 -2
  54. infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +1 -2
  55. infrahub/core/migrations/graph/m027_delete_isolated_nodes.py +50 -0
  56. infrahub/core/migrations/graph/m028_delete_diffs.py +38 -0
  57. infrahub/core/migrations/query/attribute_add.py +1 -2
  58. infrahub/core/migrations/query/attribute_rename.py +3 -6
  59. infrahub/core/migrations/query/delete_element_in_schema.py +3 -6
  60. infrahub/core/migrations/query/node_duplicate.py +3 -6
  61. infrahub/core/migrations/query/relationship_duplicate.py +3 -6
  62. infrahub/core/migrations/schema/node_attribute_remove.py +3 -6
  63. infrahub/core/migrations/schema/node_remove.py +3 -6
  64. infrahub/core/models.py +29 -2
  65. infrahub/core/node/__init__.py +18 -4
  66. infrahub/core/node/create.py +211 -0
  67. infrahub/core/protocols.py +51 -0
  68. infrahub/core/protocols_base.py +3 -0
  69. infrahub/core/query/__init__.py +2 -2
  70. infrahub/core/query/branch.py +27 -17
  71. infrahub/core/query/diff.py +186 -81
  72. infrahub/core/query/ipam.py +10 -20
  73. infrahub/core/query/node.py +65 -49
  74. infrahub/core/query/relationship.py +156 -58
  75. infrahub/core/query/resource_manager.py +1 -2
  76. infrahub/core/query/subquery.py +4 -6
  77. infrahub/core/relationship/model.py +4 -1
  78. infrahub/core/schema/__init__.py +2 -1
  79. infrahub/core/schema/attribute_parameters.py +36 -0
  80. infrahub/core/schema/attribute_schema.py +83 -8
  81. infrahub/core/schema/basenode_schema.py +25 -1
  82. infrahub/core/schema/definitions/core/__init__.py +21 -0
  83. infrahub/core/schema/definitions/internal.py +13 -3
  84. infrahub/core/schema/generated/attribute_schema.py +9 -3
  85. infrahub/core/schema/schema_branch.py +15 -7
  86. infrahub/core/validators/__init__.py +5 -1
  87. infrahub/core/validators/attribute/choices.py +1 -2
  88. infrahub/core/validators/attribute/enum.py +1 -2
  89. infrahub/core/validators/attribute/kind.py +1 -2
  90. infrahub/core/validators/attribute/length.py +13 -6
  91. infrahub/core/validators/attribute/optional.py +1 -2
  92. infrahub/core/validators/attribute/regex.py +5 -5
  93. infrahub/core/validators/attribute/unique.py +1 -3
  94. infrahub/core/validators/determiner.py +18 -2
  95. infrahub/core/validators/enum.py +7 -0
  96. infrahub/core/validators/node/hierarchy.py +3 -6
  97. infrahub/core/validators/query.py +1 -3
  98. infrahub/core/validators/relationship/count.py +6 -12
  99. infrahub/core/validators/relationship/optional.py +2 -4
  100. infrahub/core/validators/relationship/peer.py +3 -8
  101. infrahub/core/validators/tasks.py +1 -1
  102. infrahub/core/validators/uniqueness/query.py +12 -9
  103. infrahub/database/__init__.py +1 -3
  104. infrahub/events/group_action.py +1 -0
  105. infrahub/graphql/analyzer.py +139 -18
  106. infrahub/graphql/app.py +1 -1
  107. infrahub/graphql/loaders/node.py +1 -1
  108. infrahub/graphql/loaders/peers.py +1 -1
  109. infrahub/graphql/manager.py +4 -0
  110. infrahub/graphql/mutations/action.py +164 -0
  111. infrahub/graphql/mutations/convert_object_type.py +62 -0
  112. infrahub/graphql/mutations/main.py +24 -175
  113. infrahub/graphql/mutations/proposed_change.py +21 -18
  114. infrahub/graphql/queries/convert_object_type_mapping.py +36 -0
  115. infrahub/graphql/queries/diff/tree.py +2 -1
  116. infrahub/graphql/queries/relationship.py +1 -1
  117. infrahub/graphql/resolvers/many_relationship.py +4 -4
  118. infrahub/graphql/resolvers/resolver.py +4 -4
  119. infrahub/graphql/resolvers/single_relationship.py +2 -2
  120. infrahub/graphql/schema.py +6 -0
  121. infrahub/graphql/subscription/graphql_query.py +2 -2
  122. infrahub/graphql/types/branch.py +1 -1
  123. infrahub/menu/menu.py +31 -0
  124. infrahub/message_bus/messages/__init__.py +0 -10
  125. infrahub/message_bus/operations/__init__.py +0 -8
  126. infrahub/message_bus/operations/refresh/registry.py +1 -1
  127. infrahub/patch/queries/consolidate_duplicated_nodes.py +3 -6
  128. infrahub/patch/queries/delete_duplicated_edges.py +5 -10
  129. infrahub/prefect_server/models.py +1 -19
  130. infrahub/proposed_change/models.py +68 -3
  131. infrahub/proposed_change/tasks.py +907 -30
  132. infrahub/task_manager/models.py +10 -6
  133. infrahub/telemetry/database.py +1 -1
  134. infrahub/telemetry/tasks.py +1 -1
  135. infrahub/trigger/catalogue.py +2 -0
  136. infrahub/trigger/models.py +29 -3
  137. infrahub/trigger/setup.py +51 -15
  138. infrahub/trigger/tasks.py +4 -5
  139. infrahub/types.py +1 -1
  140. infrahub/webhook/models.py +2 -1
  141. infrahub/workflows/catalogue.py +85 -0
  142. infrahub/workflows/initialization.py +1 -3
  143. infrahub_sdk/timestamp.py +2 -2
  144. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/METADATA +4 -4
  145. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/RECORD +153 -146
  146. infrahub_testcontainers/container.py +0 -1
  147. infrahub_testcontainers/docker-compose.test.yml +4 -4
  148. infrahub_testcontainers/helpers.py +8 -2
  149. infrahub_testcontainers/performance_test.py +6 -3
  150. infrahub/message_bus/messages/check_generator_run.py +0 -26
  151. infrahub/message_bus/messages/finalize_validator_execution.py +0 -15
  152. infrahub/message_bus/messages/proposed_change/base_with_diff.py +0 -16
  153. infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +0 -11
  154. infrahub/message_bus/messages/request_generatordefinition_check.py +0 -20
  155. infrahub/message_bus/messages/request_proposedchange_pipeline.py +0 -23
  156. infrahub/message_bus/operations/check/__init__.py +0 -3
  157. infrahub/message_bus/operations/check/generator.py +0 -156
  158. infrahub/message_bus/operations/finalize/__init__.py +0 -3
  159. infrahub/message_bus/operations/finalize/validator.py +0 -133
  160. infrahub/message_bus/operations/requests/__init__.py +0 -9
  161. infrahub/message_bus/operations/requests/generator_definition.py +0 -140
  162. infrahub/message_bus/operations/requests/proposed_change.py +0 -629
  163. /infrahub/{message_bus/messages/proposed_change → actions}/__init__.py +0 -0
  164. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/LICENSE.txt +0 -0
  165. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/WHEEL +0 -0
  166. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/entry_points.txt +0 -0
@@ -14,7 +14,7 @@ from infrahub.core.registry import registry
14
14
  from infrahub.events import BranchDeletedEvent
15
15
  from infrahub.git.repository import get_initialized_repo
16
16
  from infrahub.services import InfrahubServices # noqa: TC001 needed for prefect flow
17
- from infrahub.trigger.models import TriggerType
17
+ from infrahub.trigger.models import TriggerSetupReport, TriggerType
18
18
  from infrahub.trigger.setup import setup_triggers
19
19
  from infrahub.workflows.catalogue import (
20
20
  COMPUTED_ATTRIBUTE_PROCESS_JINJA2,
@@ -25,7 +25,11 @@ from infrahub.workflows.catalogue import (
25
25
  from infrahub.workflows.utils import add_tags, wait_for_schema_to_converge
26
26
 
27
27
  from .gather import gather_trigger_computed_attribute_jinja2, gather_trigger_computed_attribute_python
28
- from .models import ComputedAttrJinja2GraphQL, ComputedAttrJinja2GraphQLResponse, PythonTransformTarget
28
+ from .models import (
29
+ ComputedAttrJinja2GraphQL,
30
+ ComputedAttrJinja2GraphQLResponse,
31
+ PythonTransformTarget,
32
+ )
29
33
 
30
34
  if TYPE_CHECKING:
31
35
  from infrahub.core.schema.computed_attribute import ComputedAttribute
@@ -159,10 +163,10 @@ async def trigger_update_python_computed_attributes(
159
163
 
160
164
 
161
165
  @flow(
162
- name="process_computed_attribute_value_jinja2",
163
- flow_run_name="Update value for computed attribute {attribute_name}",
166
+ name="computed-attribute-jinja2-update-value",
167
+ flow_run_name="Update value for computed attribute {node_kind}:{attribute_name}",
164
168
  )
165
- async def update_computed_attribute_value_jinja2(
169
+ async def computed_attribute_jinja2_update_value(
166
170
  branch_name: str,
167
171
  obj: ComputedAttrJinja2GraphQLResponse,
168
172
  node_kind: str,
@@ -246,7 +250,7 @@ async def process_jinja2(
246
250
  batch = await service.client.create_batch()
247
251
  for node in found:
248
252
  batch.add(
249
- task=update_computed_attribute_value_jinja2,
253
+ task=computed_attribute_jinja2_update_value,
250
254
  branch_name=branch_name,
251
255
  obj=node,
252
256
  node_kind=node_schema.kind,
@@ -302,26 +306,33 @@ async def computed_attribute_setup_jinja2(
302
306
 
303
307
  triggers = await gather_trigger_computed_attribute_jinja2()
304
308
 
305
- for trigger in triggers:
306
- if event_name != BranchDeletedEvent.event_name and trigger.branch == branch_name:
307
- await service.workflow.submit_workflow(
308
- workflow=TRIGGER_UPDATE_JINJA_COMPUTED_ATTRIBUTES,
309
- context=context,
310
- parameters={
311
- "branch_name": trigger.branch,
312
- "computed_attribute_name": trigger.computed_attribute.attribute.name,
313
- "computed_attribute_kind": trigger.computed_attribute.kind,
314
- },
315
- )
316
-
317
309
  # Configure all ComputedAttrJinja2Trigger in Prefect
318
310
  async with get_client(sync_client=False) as prefect_client:
319
- await setup_triggers(
311
+ report: TriggerSetupReport = await setup_triggers(
320
312
  client=prefect_client,
321
313
  triggers=triggers,
322
314
  trigger_type=TriggerType.COMPUTED_ATTR_JINJA2,
315
+ force_update=False,
323
316
  ) # type: ignore[misc]
324
317
 
318
+ # Since we can have multiple trigger per NodeKind
319
+ # we need to extract the list of unique node that should be processed
320
+ unique_nodes: set[tuple[str, str, str]] = {
321
+ (trigger.branch, trigger.computed_attribute.kind, trigger.computed_attribute.attribute.name) # type: ignore[attr-defined]
322
+ for trigger in report.updated + report.created
323
+ }
324
+ for branch, kind, attribute_name in unique_nodes:
325
+ if event_name != BranchDeletedEvent.event_name and branch == branch_name:
326
+ await service.workflow.submit_workflow(
327
+ workflow=TRIGGER_UPDATE_JINJA_COMPUTED_ATTRIBUTES,
328
+ context=context,
329
+ parameters={
330
+ "branch_name": branch,
331
+ "computed_attribute_name": attribute_name,
332
+ "computed_attribute_kind": kind,
333
+ },
334
+ )
335
+
325
336
  log.info(f"{len(triggers)} Computed Attribute for Jinja2 automation configuration completed")
326
337
 
327
338
 
@@ -347,18 +358,29 @@ async def computed_attribute_setup_python(
347
358
 
348
359
  triggers_python, triggers_python_query = await gather_trigger_computed_attribute_python(db=db)
349
360
 
350
- for trigger in triggers_python:
351
- if event_name != BranchDeletedEvent.event_name and trigger.branch == branch_name:
352
- log.info(
353
- f"Triggering update for {trigger.computed_attribute.computed_attribute.attribute.name} on {branch_name}"
354
- )
361
+ # Since we can have multiple trigger per NodeKind
362
+ # we need to extract the list of unique node that should be processed
363
+ # also
364
+ # Because the automation in Prefect doesn't capture all information about the computed attribute
365
+ # we can't tell right now if a given computed attribute has changed and need to be updated
366
+ unique_nodes: set[tuple[str, str, str]] = {
367
+ (
368
+ trigger.branch,
369
+ trigger.computed_attribute.computed_attribute.kind,
370
+ trigger.computed_attribute.computed_attribute.attribute.name,
371
+ )
372
+ for trigger in triggers_python
373
+ }
374
+ for branch, kind, attribute_name in unique_nodes:
375
+ if event_name != BranchDeletedEvent.event_name and branch == branch_name:
376
+ log.info(f"Triggering update for {kind}.{attribute_name} on {branch}")
355
377
  await service.workflow.submit_workflow(
356
378
  workflow=TRIGGER_UPDATE_PYTHON_COMPUTED_ATTRIBUTES,
357
379
  context=context,
358
380
  parameters={
359
381
  "branch_name": branch_name,
360
- "computed_attribute_name": trigger.computed_attribute.computed_attribute.attribute.name,
361
- "computed_attribute_kind": trigger.computed_attribute.computed_attribute.kind,
382
+ "computed_attribute_name": attribute_name,
383
+ "computed_attribute_kind": kind,
362
384
  },
363
385
  )
364
386
 
infrahub/config.py CHANGED
@@ -269,6 +269,7 @@ class DatabaseSettings(BaseSettings):
269
269
  address: str = "localhost"
270
270
  port: int = 7687
271
271
  database: str | None = Field(default=None, pattern=VALID_DATABASE_NAME_REGEX, description="Name of the database")
272
+ policy: str | None = Field(default=None, description="Routing policy for database connections")
272
273
  tls_enabled: bool = Field(default=False, description="Indicates if TLS is enabled for the connection")
273
274
  tls_insecure: bool = Field(default=False, description="Indicates if TLS certificates are verified")
274
275
  tls_ca_file: str | None = Field(default=None, description="File path to CA cert or bundle in PEM format")
@@ -293,6 +294,14 @@ class DatabaseSettings(BaseSettings):
293
294
  default=0.01, ge=0, description="Delay to add when max_concurrent_queries is reached."
294
295
  )
295
296
 
297
+ @property
298
+ def database_uri(self) -> str:
299
+ """Constructs the database URI based on the configuration settings."""
300
+ base_uri = f"{self.protocol}://{self.address}:{self.port}"
301
+ if self.policy is not None:
302
+ return f"{base_uri}?policy={self.policy}"
303
+ return base_uri
304
+
296
305
  @property
297
306
  def database_name(self) -> str:
298
307
  return self.database or self.db_type.value
infrahub/core/account.py CHANGED
@@ -69,8 +69,7 @@ class AccountGlobalPermissionQuery(Query):
69
69
  query = """
70
70
  MATCH (account:%(generic_account_node)s)
71
71
  WHERE account.uuid = $account_id
72
- CALL {
73
- WITH account
72
+ CALL (account) {
74
73
  MATCH (account)-[r:IS_PART_OF]-(root:Root)
75
74
  WHERE %(branch_filter)s
76
75
  RETURN account as account1, r as r1
@@ -80,8 +79,7 @@ class AccountGlobalPermissionQuery(Query):
80
79
  WITH account, r1 as r
81
80
  WHERE r.status = "active"
82
81
  WITH account
83
- CALL {
84
- WITH account
82
+ CALL (account) {
85
83
  MATCH (account)-[r1:IS_RELATED]->(:Relationship {name: "group_member"})<-[r2:IS_RELATED]-(account_group:%(account_group_node)s)
86
84
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
87
85
  WITH account_group, r1, r2, (r1.status = "active" AND r2.status = "active") AS is_active
@@ -92,8 +90,7 @@ class AccountGlobalPermissionQuery(Query):
92
90
  }
93
91
  WITH account_group
94
92
 
95
- CALL {
96
- WITH account_group
93
+ CALL (account_group) {
97
94
  MATCH (account_group)-[r1:IS_RELATED]->(:Relationship {name: "role__accountgroups"})<-[r2:IS_RELATED]-(account_role:%(account_role_node)s)
98
95
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
99
96
  WITH account_role, r1, r2, (r1.status = "active" AND r2.status = "active") AS is_active
@@ -104,8 +101,7 @@ class AccountGlobalPermissionQuery(Query):
104
101
  }
105
102
  WITH account_role
106
103
 
107
- CALL {
108
- WITH account_role
104
+ CALL (account_role) {
109
105
  MATCH (account_role)-[r1:IS_RELATED]->(:Relationship {name: "role__permissions"})<-[r2:IS_RELATED]-(global_permission:%(global_permission_node)s)
110
106
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
111
107
  WITH global_permission, r1, r2, (r1.status = "active" AND r2.status = "active") AS is_active
@@ -116,7 +112,7 @@ class AccountGlobalPermissionQuery(Query):
116
112
  }
117
113
  WITH global_permission
118
114
 
119
- CALL {
115
+ CALL (global_permission) {
120
116
  WITH global_permission
121
117
  MATCH (global_permission)-[r1:HAS_ATTRIBUTE]->(:Attribute {name: "action"})-[r2:HAS_VALUE]->(global_permission_action:AttributeValue)
122
118
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
@@ -127,8 +123,7 @@ class AccountGlobalPermissionQuery(Query):
127
123
  WITH global_permission, global_permission_action, is_active AS gpa_is_active
128
124
  WHERE gpa_is_active = TRUE
129
125
 
130
- CALL {
131
- WITH global_permission
126
+ CALL (global_permission) {
132
127
  MATCH (global_permission)-[r1:HAS_ATTRIBUTE]->(:Attribute {name: "decision"})-[r2:HAS_VALUE]->(global_permission_decision:AttributeValue)
133
128
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
134
129
  RETURN global_permission_decision, (r1.status = "active" AND r2.status = "active") AS is_active
@@ -183,8 +178,7 @@ class AccountObjectPermissionQuery(Query):
183
178
  query = """
184
179
  MATCH (account:%(generic_account_node)s)
185
180
  WHERE account.uuid = $account_id
186
- CALL {
187
- WITH account
181
+ CALL (account) {
188
182
  MATCH (account)-[r:IS_PART_OF]-(root:Root)
189
183
  WHERE %(branch_filter)s
190
184
  RETURN account as account1, r as r1
@@ -194,8 +188,7 @@ class AccountObjectPermissionQuery(Query):
194
188
  WITH account, r1 as r
195
189
  WHERE r.status = "active"
196
190
  WITH account
197
- CALL {
198
- WITH account
191
+ CALL (account) {
199
192
  MATCH (account)-[r1:IS_RELATED]->(:Relationship {name: "group_member"})<-[r2:IS_RELATED]-(account_group:%(account_group_node)s)
200
193
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
201
194
  WITH account_group, r1, r2, (r1.status = "active" AND r2.status = "active") AS is_active
@@ -206,8 +199,7 @@ class AccountObjectPermissionQuery(Query):
206
199
  }
207
200
  WITH account_group
208
201
 
209
- CALL {
210
- WITH account_group
202
+ CALL (account_group) {
211
203
  MATCH (account_group)-[r1:IS_RELATED]->(:Relationship {name: "role__accountgroups"})<-[r2:IS_RELATED]-(account_role:%(account_role_node)s)
212
204
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
213
205
  WITH account_role, r1, r2, (r1.status = "active" AND r2.status = "active") AS is_active
@@ -218,8 +210,7 @@ class AccountObjectPermissionQuery(Query):
218
210
  }
219
211
  WITH account_role
220
212
 
221
- CALL {
222
- WITH account_role
213
+ CALL (account_role) {
223
214
  MATCH (account_role)-[r1:IS_RELATED]->(:Relationship {name: "role__permissions"})<-[r2:IS_RELATED]-(object_permission:%(object_permission_node)s)
224
215
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
225
216
  WITH object_permission, r1, r2, (r1.status = "active" AND r2.status = "active") AS is_active
@@ -230,8 +221,7 @@ class AccountObjectPermissionQuery(Query):
230
221
  }
231
222
  WITH object_permission
232
223
 
233
- CALL {
234
- WITH object_permission
224
+ CALL (object_permission) {
235
225
  MATCH (object_permission)-[r1:HAS_ATTRIBUTE]->(:Attribute {name: "namespace"})-[r2:HAS_VALUE]->(object_permission_namespace:AttributeValue)
236
226
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
237
227
  RETURN object_permission_namespace, (r1.status = "active" AND r2.status = "active") AS is_active
@@ -240,8 +230,7 @@ class AccountObjectPermissionQuery(Query):
240
230
  }
241
231
  WITH object_permission, object_permission_namespace, is_active AS opn_is_active
242
232
  WHERE opn_is_active = TRUE
243
- CALL {
244
- WITH object_permission
233
+ CALL (object_permission) {
245
234
  MATCH (object_permission)-[r1:HAS_ATTRIBUTE]->(:Attribute {name: "name"})-[r2:HAS_VALUE]->(object_permission_name:AttributeValue)
246
235
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
247
236
  RETURN object_permission_name, (r1.status = "active" AND r2.status = "active") AS is_active
@@ -250,8 +239,7 @@ class AccountObjectPermissionQuery(Query):
250
239
  }
251
240
  WITH object_permission, object_permission_namespace, object_permission_name, is_active AS opn_is_active
252
241
  WHERE opn_is_active = TRUE
253
- CALL {
254
- WITH object_permission
242
+ CALL (object_permission) {
255
243
  MATCH (object_permission)-[r1:HAS_ATTRIBUTE]->(:Attribute {name: "action"})-[r2:HAS_VALUE]->(object_permission_action:AttributeValue)
256
244
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
257
245
  RETURN object_permission_action, (r1.status = "active" AND r2.status = "active") AS is_active
@@ -260,8 +248,7 @@ class AccountObjectPermissionQuery(Query):
260
248
  }
261
249
  WITH object_permission, object_permission_namespace, object_permission_name, object_permission_action, is_active AS opa_is_active
262
250
  WHERE opa_is_active = TRUE
263
- CALL {
264
- WITH object_permission
251
+ CALL (object_permission) {
265
252
  MATCH (object_permission)-[r1:HAS_ATTRIBUTE]->(:Attribute {name: "decision"})-[r2:HAS_VALUE]->(object_permission_decision:AttributeValue)
266
253
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
267
254
  RETURN object_permission_decision, (r1.status = "active" AND r2.status = "active") AS is_active
@@ -336,8 +323,7 @@ class AccountRoleGlobalPermissionQuery(Query):
336
323
  query = """
337
324
  MATCH (account_role:%(account_role_node)s)
338
325
  WHERE account_role.uuid = $role_id
339
- CALL {
340
- WITH account_role
326
+ CALL (account_role) {
341
327
  MATCH (account_role)-[r:IS_PART_OF]-(root:Root)
342
328
  WHERE %(branch_filter)s
343
329
  RETURN account_role as account_role1, r as r1
@@ -348,8 +334,7 @@ class AccountRoleGlobalPermissionQuery(Query):
348
334
  WHERE r.status = "active"
349
335
  WITH account_role
350
336
 
351
- CALL {
352
- WITH account_role
337
+ CALL (account_role) {
353
338
  MATCH (account_role)-[r1:IS_RELATED]->(:Relationship {name: "role__permissions"})<-[r2:IS_RELATED]-(global_permission:%(global_permission_node)s)
354
339
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
355
340
  WITH global_permission, r1, r2, (r1.status = "active" AND r2.status = "active") AS is_active
@@ -360,8 +345,7 @@ class AccountRoleGlobalPermissionQuery(Query):
360
345
  }
361
346
  WITH global_permission
362
347
 
363
- CALL {
364
- WITH global_permission
348
+ CALL (global_permission) {
365
349
  MATCH (global_permission)-[r1:HAS_ATTRIBUTE]->(:Attribute {name: "action"})-[r2:HAS_VALUE]->(global_permission_action:AttributeValue)
366
350
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
367
351
  RETURN global_permission_action, (r1.status = "active" AND r2.status = "active") AS is_active
@@ -371,8 +355,7 @@ class AccountRoleGlobalPermissionQuery(Query):
371
355
  WITH global_permission, global_permission_action, is_active AS gpa_is_active
372
356
  WHERE gpa_is_active = TRUE
373
357
 
374
- CALL {
375
- WITH global_permission
358
+ CALL (global_permission) {
376
359
  MATCH (global_permission)-[r1:HAS_ATTRIBUTE]->(:Attribute {name: "decision"})-[r2:HAS_VALUE]->(global_permission_decision:AttributeValue)
377
360
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
378
361
  RETURN global_permission_decision, (r1.status = "active" AND r2.status = "active") AS is_active
@@ -425,8 +408,7 @@ class AccountRoleObjectPermissionQuery(Query):
425
408
  query = """
426
409
  MATCH (account_role:%(account_role_node)s)
427
410
  WHERE account_role.uuid = $role_id
428
- CALL {
429
- WITH account_role
411
+ CALL (account_role) {
430
412
  MATCH (account_role)-[r:IS_PART_OF]-(root:Root)
431
413
  WHERE %(branch_filter)s
432
414
  RETURN account_role as account_role1, r as r1
@@ -437,8 +419,7 @@ class AccountRoleObjectPermissionQuery(Query):
437
419
  WHERE r.status = "active"
438
420
  WITH account_role
439
421
 
440
- CALL {
441
- WITH account_role
422
+ CALL (account_role) {
442
423
  MATCH (account_role)-[r1:IS_RELATED]->(:Relationship {name: "role__permissions"})<-[r2:IS_RELATED]-(object_permission:%(object_permission_node)s)
443
424
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
444
425
  WITH object_permission, r1, r2, (r1.status = "active" AND r2.status = "active") AS is_active
@@ -449,8 +430,7 @@ class AccountRoleObjectPermissionQuery(Query):
449
430
  }
450
431
  WITH object_permission
451
432
 
452
- CALL {
453
- WITH object_permission
433
+ CALL (object_permission) {
454
434
  MATCH (object_permission)-[r1:HAS_ATTRIBUTE]->(:Attribute {name: "namespace"})-[r2:HAS_VALUE]->(object_permission_namespace:AttributeValue)
455
435
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
456
436
  RETURN object_permission_namespace, (r1.status = "active" AND r2.status = "active") AS is_active
@@ -459,8 +439,7 @@ class AccountRoleObjectPermissionQuery(Query):
459
439
  }
460
440
  WITH object_permission, object_permission_namespace, is_active AS opn_is_active
461
441
  WHERE opn_is_active = TRUE
462
- CALL {
463
- WITH object_permission
442
+ CALL (object_permission) {
464
443
  MATCH (object_permission)-[r1:HAS_ATTRIBUTE]->(:Attribute {name: "name"})-[r2:HAS_VALUE]->(object_permission_name:AttributeValue)
465
444
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
466
445
  RETURN object_permission_name, (r1.status = "active" AND r2.status = "active") AS is_active
@@ -469,8 +448,7 @@ class AccountRoleObjectPermissionQuery(Query):
469
448
  }
470
449
  WITH object_permission, object_permission_namespace, object_permission_name, is_active AS opn_is_active
471
450
  WHERE opn_is_active = TRUE
472
- CALL {
473
- WITH object_permission
451
+ CALL (object_permission) {
474
452
  MATCH (object_permission)-[r1:HAS_ATTRIBUTE]->(:Attribute {name: "action"})-[r2:HAS_VALUE]->(object_permission_action:AttributeValue)
475
453
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
476
454
  RETURN object_permission_action, (r1.status = "active" AND r2.status = "active") AS is_active
@@ -479,8 +457,7 @@ class AccountRoleObjectPermissionQuery(Query):
479
457
  }
480
458
  WITH object_permission, object_permission_namespace, object_permission_name, object_permission_action, is_active AS opa_is_active
481
459
  WHERE opa_is_active = TRUE
482
- CALL {
483
- WITH object_permission
460
+ CALL (object_permission) {
484
461
  MATCH (object_permission)-[r1:HAS_ATTRIBUTE]->(:Attribute {name: "decision"})-[r2:HAS_VALUE]->(object_permission_decision:AttributeValue)
485
462
  WHERE all(r IN [r1, r2] WHERE (%(branch_filter)s))
486
463
  RETURN object_permission_decision, (r1.status = "active" AND r2.status = "active") AS is_active
@@ -232,7 +232,7 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
232
232
  Raises:
233
233
  ValidationError: Content of the attribute value is not valid
234
234
  """
235
- if schema.regex:
235
+ if regex := schema.get_regex():
236
236
  if schema.kind == "List":
237
237
  validation_values = [str(entry) for entry in value]
238
238
  else:
@@ -240,22 +240,20 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
240
240
 
241
241
  for validation_value in validation_values:
242
242
  try:
243
- is_valid = re.match(pattern=schema.regex, string=str(validation_value))
243
+ is_valid = re.match(pattern=regex, string=str(validation_value))
244
244
  except re.error as exc:
245
- raise ValidationError(
246
- {name: f"The regex defined in the schema is not valid ({schema.regex!r})"}
247
- ) from exc
245
+ raise ValidationError({name: f"The regex defined in the schema is not valid ({regex!r})"}) from exc
248
246
 
249
247
  if not is_valid:
250
- raise ValidationError({name: f"{validation_value} must conform with the regex: {schema.regex!r}"})
248
+ raise ValidationError({name: f"{validation_value} must conform with the regex: {regex!r}"})
251
249
 
252
- if schema.min_length:
253
- if len(value) < schema.min_length:
254
- raise ValidationError({name: f"{value} must have a minimum length of {schema.min_length!r}"})
250
+ if min_length := schema.get_min_length():
251
+ if len(value) < min_length:
252
+ raise ValidationError({name: f"{value} must have a minimum length of {min_length!r}"})
255
253
 
256
- if schema.max_length:
257
- if len(value) > schema.max_length:
258
- raise ValidationError({name: f"{value} must have a maximum length of {schema.max_length!r}"})
254
+ if max_length := schema.get_max_length():
255
+ if len(value) > max_length:
256
+ raise ValidationError({name: f"{value} must have a maximum length of {max_length!r}"})
259
257
 
260
258
  if schema.enum:
261
259
  try:
@@ -782,6 +780,10 @@ class Dropdown(BaseAttribute):
782
780
 
783
781
  return ""
784
782
 
783
+ @staticmethod
784
+ def get_allowed_property_in_path() -> list[str]:
785
+ return ["color", "description", "label", "value"]
786
+
785
787
  @classmethod
786
788
  def validate_content(cls, value: Any, name: str, schema: AttributeSchema) -> None:
787
789
  """Validate the content of the dropdown."""
@@ -817,7 +819,18 @@ class IPNetwork(BaseAttribute):
817
819
 
818
820
  @staticmethod
819
821
  def get_allowed_property_in_path() -> list[str]:
820
- return ["value", "version", "binary_address", "prefixlen"]
822
+ return [
823
+ "binary_address",
824
+ "broadcast_address",
825
+ "hostmask",
826
+ "netmask",
827
+ "num_addresses",
828
+ "prefixlen",
829
+ "value",
830
+ "version",
831
+ "with_hostmask",
832
+ "with_netmask",
833
+ ]
821
834
 
822
835
  @property
823
836
  def obj(self) -> ipaddress.IPv4Network | ipaddress.IPv6Network:
@@ -950,7 +963,17 @@ class IPHost(BaseAttribute):
950
963
 
951
964
  @staticmethod
952
965
  def get_allowed_property_in_path() -> list[str]:
953
- return ["value", "version", "binary_address"]
966
+ return [
967
+ "binary_address",
968
+ "hostmask",
969
+ "ip",
970
+ "netmask",
971
+ "prefixlen",
972
+ "value",
973
+ "version",
974
+ "with_hostmask",
975
+ "with_netmask",
976
+ ]
954
977
 
955
978
  @property
956
979
  def obj(self) -> ipaddress.IPv4Interface | ipaddress.IPv6Interface:
@@ -1170,6 +1193,22 @@ class MacAddress(BaseAttribute):
1170
1193
  """Serialize the value as standard EUI-48 or EUI-64 before storing it in the database."""
1171
1194
  return str(netaddr.EUI(addr=self.value))
1172
1195
 
1196
+ @staticmethod
1197
+ def get_allowed_property_in_path() -> list[str]:
1198
+ return [
1199
+ "bare",
1200
+ "binary",
1201
+ "dot_notation",
1202
+ "ei",
1203
+ "eui48",
1204
+ "eui64",
1205
+ "oui",
1206
+ "semicolon_notation",
1207
+ "split_notation",
1208
+ "value",
1209
+ "version",
1210
+ ]
1211
+
1173
1212
 
1174
1213
  class MacAddressOptional(MacAddress):
1175
1214
  value: str | None
@@ -295,6 +295,7 @@ class Branch(StandardNode):
295
295
  is_isolated: bool = True,
296
296
  branch_agnostic: bool = False,
297
297
  variable_name: str = "r",
298
+ params_prefix: str = "",
298
299
  ) -> tuple[str, dict]:
299
300
  """
300
301
  Generate a CYPHER Query filter based on a path to query a part of the graph at a specific time and on a specific branch.
@@ -306,30 +307,28 @@ class Branch(StandardNode):
306
307
 
307
308
  There is a currently an assumption that the relationship in the path will be named 'r'
308
309
  """
309
-
310
+ pp = params_prefix
310
311
  params: dict[str, Any] = {}
311
312
  at = Timestamp(at)
312
313
  at_str = at.to_string()
313
314
  if branch_agnostic:
314
- filter_str = (
315
- f"{variable_name}.from <= $time1 AND ({variable_name}.to IS NULL or {variable_name}.to >= $time1)"
316
- )
317
- params["time1"] = at_str
315
+ filter_str = f"{variable_name}.from <= ${pp}time1 AND ({variable_name}.to IS NULL or {variable_name}.to >= ${pp}time1)"
316
+ params[f"{pp}time1"] = at_str
318
317
  return filter_str, params
319
318
 
320
319
  branches_times = self.get_branches_and_times_to_query_global(at=at_str, is_isolated=is_isolated)
321
320
 
322
321
  for idx, (branch_name, time_to_query) in enumerate(branches_times.items()):
323
- params[f"branch{idx}"] = list(branch_name)
324
- params[f"time{idx}"] = time_to_query
322
+ params[f"{pp}branch{idx}"] = list(branch_name)
323
+ params[f"{pp}time{idx}"] = time_to_query
325
324
 
326
325
  filters = []
327
326
  for idx in range(len(branches_times)):
328
327
  filters.append(
329
- f"({variable_name}.branch IN $branch{idx} AND {variable_name}.from <= $time{idx} AND {variable_name}.to IS NULL)"
328
+ f"({variable_name}.branch IN ${pp}branch{idx} AND {variable_name}.from <= ${pp}time{idx} AND {variable_name}.to IS NULL)"
330
329
  )
331
330
  filters.append(
332
- f"({variable_name}.branch IN $branch{idx} AND {variable_name}.from <= $time{idx} AND {variable_name}.to >= $time{idx})"
331
+ f"({variable_name}.branch IN ${pp}branch{idx} AND {variable_name}.from <= ${pp}time{idx} AND {variable_name}.to >= ${pp}time{idx})"
333
332
  )
334
333
 
335
334
  filter_str = "(" + "\n OR ".join(filters) + ")"
@@ -212,8 +212,6 @@ async def merge_branch(
212
212
 
213
213
  merger: BranchMerger | None = None
214
214
  async with lock.registry.global_graph_lock():
215
- # await update_diff(model=RequestDiffUpdate(branch_name=obj.name))
216
-
217
215
  diff_repository = await component_registry.get_component(DiffRepository, db=db, branch=obj)
218
216
  diff_coordinator = await component_registry.get_component(DiffCoordinator, db=db, branch=obj)
219
217
  diff_merger = await component_registry.get_component(DiffMerger, db=db, branch=obj)
@@ -2,6 +2,7 @@ ACCOUNT = "CoreAccount"
2
2
  ACCOUNTGROUP = "CoreAccountGroup"
3
3
  ACCOUNTROLE = "CoreAccountRole"
4
4
  ACCOUNTTOKEN = "InternalAccountToken"
5
+ ACTION = "CoreAction"
5
6
  ARTIFACT = "CoreArtifact"
6
7
  ARTIFACTCHECK = "CoreArtifactCheck"
7
8
  ARTIFACTDEFINITION = "CoreArtifactDefinition"
@@ -21,6 +22,7 @@ DATAVALIDATOR = "CoreDataValidator"
21
22
  FILECHECK = "CoreFileCheck"
22
23
  FILETHREAD = "CoreFileThread"
23
24
  GENERICACCOUNT = "CoreGenericAccount"
25
+ GENERATORACTION = "CoreGeneratorAction"
24
26
  GENERATORCHECK = "CoreGeneratorCheck"
25
27
  GENERATORDEFINITION = "CoreGeneratorDefinition"
26
28
  GENERATORINSTANCE = "CoreGeneratorInstance"
@@ -30,6 +32,8 @@ GENERICGROUP = "CoreGroup"
30
32
  GLOBALPERMISSION = "CoreGlobalPermission"
31
33
  GRAPHQLQUERY = "CoreGraphQLQuery"
32
34
  GRAPHQLQUERYGROUP = "CoreGraphQLQueryGroup"
35
+ GROUPACTION = "CoreGroupAction"
36
+ GROUPTRIGGERRULE = "CoreGroupTriggerRule"
33
37
  IPNAMESPACE = "BuiltinIPNamespace"
34
38
  IPADDRESS = "BuiltinIPAddress"
35
39
  IPADDRESSPOOL = "CoreIPAddressPool"
@@ -39,6 +43,9 @@ MENU = "CoreMenu"
39
43
  MENUITEM = "CoreMenuItem"
40
44
  NAMESPACE = "IpamNamespace"
41
45
  NODE = "CoreNode"
46
+ NODETRIGGERRULE = "CoreNodeTriggerRule"
47
+ NODETRIGGERATTRIBUTEMATCH = "CoreNodeTriggerAttributeMatch"
48
+ NODETRIGGERRELATIONSHIPMATCH = "CoreNodeTriggerRelationshipMatch"
42
49
  NUMBERPOOL = "CoreNumberPool"
43
50
  LINEAGEOWNER = "LineageOwner"
44
51
  LINEAGESOURCE = "LineageSource"
@@ -67,6 +74,7 @@ THREADCOMMENT = "CoreThreadComment"
67
74
  TRANSFORM = "CoreTransformation"
68
75
  TRANSFORMJINJA2 = "CoreTransformJinja2"
69
76
  TRANSFORMPYTHON = "CoreTransformPython"
77
+ TRIGGERRULE = "CoreTriggerRule"
70
78
  USERVALIDATOR = "CoreUserValidator"
71
79
  VALIDATOR = "CoreValidator"
72
80
  WEBHOOK = "CoreWebhook"
@@ -26,7 +26,7 @@ class NodeConstraintRunner:
26
26
  async def check(
27
27
  self, node: Node, field_filters: list[str] | None = None, skip_uniqueness_check: bool = False
28
28
  ) -> None:
29
- async with self.db.start_session() as db:
29
+ async with self.db.start_session(read_only=False) as db:
30
30
  await node.resolve_relationships(db=db)
31
31
 
32
32
  if not skip_uniqueness_check:
File without changes