infrahub-server 1.2.0rc0__py3-none-any.whl → 1.2.1__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 (365) hide show
  1. infrahub/api/dependencies.py +6 -6
  2. infrahub/api/diff/validation_models.py +7 -7
  3. infrahub/api/schema.py +1 -1
  4. infrahub/artifacts/models.py +5 -3
  5. infrahub/artifacts/tasks.py +3 -5
  6. infrahub/cli/__init__.py +13 -9
  7. infrahub/cli/constants.py +3 -0
  8. infrahub/cli/db.py +165 -183
  9. infrahub/cli/upgrade.py +146 -0
  10. infrahub/computed_attribute/gather.py +185 -0
  11. infrahub/computed_attribute/models.py +240 -12
  12. infrahub/computed_attribute/tasks.py +77 -441
  13. infrahub/computed_attribute/triggers.py +13 -47
  14. infrahub/config.py +43 -32
  15. infrahub/context.py +14 -0
  16. infrahub/core/account.py +4 -4
  17. infrahub/core/attribute.py +58 -58
  18. infrahub/core/branch/tasks.py +74 -22
  19. infrahub/core/changelog/diff.py +95 -36
  20. infrahub/core/changelog/models.py +217 -43
  21. infrahub/core/constants/__init__.py +28 -0
  22. infrahub/core/constants/infrahubkind.py +2 -0
  23. infrahub/core/constants/schema.py +2 -0
  24. infrahub/core/constraint/node/runner.py +9 -8
  25. infrahub/core/diff/branch_differ.py +10 -10
  26. infrahub/core/diff/enricher/cardinality_one.py +5 -0
  27. infrahub/core/diff/enricher/hierarchy.py +17 -4
  28. infrahub/core/diff/enricher/labels.py +5 -0
  29. infrahub/core/diff/enricher/path_identifier.py +4 -0
  30. infrahub/core/diff/ipam_diff_parser.py +4 -5
  31. infrahub/core/diff/model/diff.py +27 -27
  32. infrahub/core/diff/model/path.py +32 -9
  33. infrahub/core/diff/parent_node_adder.py +78 -0
  34. infrahub/core/diff/payload_builder.py +13 -2
  35. infrahub/core/diff/query/filters.py +2 -2
  36. infrahub/core/diff/query/merge.py +20 -17
  37. infrahub/core/diff/query/save.py +188 -182
  38. infrahub/core/diff/query/summary_counts_enricher.py +51 -4
  39. infrahub/core/diff/query_parser.py +4 -4
  40. infrahub/core/diff/repository/deserializer.py +8 -3
  41. infrahub/core/diff/repository/repository.py +156 -38
  42. infrahub/core/diff/tasks.py +4 -4
  43. infrahub/core/graph/__init__.py +1 -1
  44. infrahub/core/graph/index.py +3 -0
  45. infrahub/core/initialization.py +1 -10
  46. infrahub/core/ipam/constants.py +3 -4
  47. infrahub/core/ipam/reconciler.py +12 -12
  48. infrahub/core/ipam/utilization.py +10 -13
  49. infrahub/core/manager.py +36 -36
  50. infrahub/core/merge.py +7 -7
  51. infrahub/core/migrations/__init__.py +2 -3
  52. infrahub/core/migrations/graph/__init__.py +12 -3
  53. infrahub/core/migrations/graph/m017_add_core_profile.py +1 -5
  54. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +4 -4
  55. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +256 -0
  56. infrahub/core/migrations/graph/m020_duplicate_edges.py +160 -0
  57. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +51 -0
  58. infrahub/core/migrations/graph/m022_add_generate_template_attr.py +48 -0
  59. infrahub/core/migrations/graph/m023_deduplicate_cardinality_one_relationships.py +96 -0
  60. infrahub/core/migrations/query/attribute_add.py +2 -2
  61. infrahub/core/migrations/query/node_duplicate.py +43 -26
  62. infrahub/core/migrations/query/schema_attribute_update.py +2 -2
  63. infrahub/core/migrations/schema/models.py +19 -4
  64. infrahub/core/migrations/schema/node_remove.py +26 -12
  65. infrahub/core/migrations/schema/tasks.py +2 -2
  66. infrahub/core/migrations/shared.py +16 -16
  67. infrahub/core/models.py +15 -6
  68. infrahub/core/node/__init__.py +43 -39
  69. infrahub/core/node/base.py +2 -4
  70. infrahub/core/node/constraints/attribute_uniqueness.py +2 -2
  71. infrahub/core/node/constraints/grouped_uniqueness.py +99 -47
  72. infrahub/core/node/constraints/interface.py +1 -2
  73. infrahub/core/node/delete_validator.py +3 -5
  74. infrahub/core/node/ipam.py +4 -4
  75. infrahub/core/node/permissions.py +7 -7
  76. infrahub/core/node/resource_manager/ip_address_pool.py +6 -6
  77. infrahub/core/node/resource_manager/ip_prefix_pool.py +6 -6
  78. infrahub/core/node/resource_manager/number_pool.py +3 -3
  79. infrahub/core/path.py +12 -12
  80. infrahub/core/property.py +11 -11
  81. infrahub/core/protocols.py +7 -0
  82. infrahub/core/protocols_base.py +21 -21
  83. infrahub/core/query/__init__.py +33 -33
  84. infrahub/core/query/attribute.py +6 -4
  85. infrahub/core/query/diff.py +3 -3
  86. infrahub/core/query/node.py +82 -32
  87. infrahub/core/query/relationship.py +228 -40
  88. infrahub/core/query/resource_manager.py +2 -0
  89. infrahub/core/query/standard_node.py +3 -3
  90. infrahub/core/query/subquery.py +9 -9
  91. infrahub/core/registry.py +13 -15
  92. infrahub/core/relationship/constraints/count.py +3 -4
  93. infrahub/core/relationship/constraints/peer_kind.py +3 -4
  94. infrahub/core/relationship/constraints/profiles_kind.py +2 -2
  95. infrahub/core/relationship/model.py +51 -59
  96. infrahub/core/schema/attribute_schema.py +16 -8
  97. infrahub/core/schema/basenode_schema.py +105 -44
  98. infrahub/core/schema/computed_attribute.py +3 -3
  99. infrahub/core/schema/definitions/core/__init__.py +147 -0
  100. infrahub/core/schema/definitions/core/account.py +171 -0
  101. infrahub/core/schema/definitions/core/artifact.py +136 -0
  102. infrahub/core/schema/definitions/core/builtin.py +24 -0
  103. infrahub/core/schema/definitions/core/check.py +68 -0
  104. infrahub/core/schema/definitions/core/core.py +17 -0
  105. infrahub/core/schema/definitions/core/generator.py +100 -0
  106. infrahub/core/schema/definitions/core/graphql_query.py +79 -0
  107. infrahub/core/schema/definitions/core/group.py +108 -0
  108. infrahub/core/schema/definitions/core/ipam.py +193 -0
  109. infrahub/core/schema/definitions/core/lineage.py +19 -0
  110. infrahub/core/schema/definitions/core/menu.py +48 -0
  111. infrahub/core/schema/definitions/core/permission.py +163 -0
  112. infrahub/core/schema/definitions/core/profile.py +18 -0
  113. infrahub/core/schema/definitions/core/propose_change.py +97 -0
  114. infrahub/core/schema/definitions/core/propose_change_comment.py +193 -0
  115. infrahub/core/schema/definitions/core/propose_change_validator.py +328 -0
  116. infrahub/core/schema/definitions/core/repository.py +286 -0
  117. infrahub/core/schema/definitions/core/resource_pool.py +170 -0
  118. infrahub/core/schema/definitions/core/template.py +27 -0
  119. infrahub/core/schema/definitions/core/transform.py +96 -0
  120. infrahub/core/schema/definitions/core/webhook.py +134 -0
  121. infrahub/core/schema/definitions/internal.py +16 -16
  122. infrahub/core/schema/dropdown.py +3 -4
  123. infrahub/core/schema/generated/attribute_schema.py +15 -18
  124. infrahub/core/schema/generated/base_node_schema.py +12 -14
  125. infrahub/core/schema/generated/node_schema.py +3 -5
  126. infrahub/core/schema/generated/relationship_schema.py +9 -11
  127. infrahub/core/schema/generic_schema.py +2 -2
  128. infrahub/core/schema/manager.py +20 -9
  129. infrahub/core/schema/node_schema.py +4 -2
  130. infrahub/core/schema/relationship_schema.py +14 -6
  131. infrahub/core/schema/schema_branch.py +292 -144
  132. infrahub/core/schema/schema_branch_computed.py +41 -4
  133. infrahub/core/task/task.py +3 -3
  134. infrahub/core/task/user_task.py +15 -15
  135. infrahub/core/timestamp.py +3 -3
  136. infrahub/core/utils.py +20 -18
  137. infrahub/core/validators/__init__.py +1 -3
  138. infrahub/core/validators/aggregated_checker.py +2 -2
  139. infrahub/core/validators/attribute/choices.py +2 -2
  140. infrahub/core/validators/attribute/enum.py +2 -2
  141. infrahub/core/validators/attribute/kind.py +2 -2
  142. infrahub/core/validators/attribute/length.py +2 -2
  143. infrahub/core/validators/attribute/optional.py +2 -2
  144. infrahub/core/validators/attribute/regex.py +2 -2
  145. infrahub/core/validators/attribute/unique.py +2 -2
  146. infrahub/core/validators/checks_runner.py +25 -2
  147. infrahub/core/validators/determiner.py +1 -3
  148. infrahub/core/validators/interface.py +6 -2
  149. infrahub/core/validators/model.py +22 -3
  150. infrahub/core/validators/models/validate_migration.py +17 -4
  151. infrahub/core/validators/node/attribute.py +2 -2
  152. infrahub/core/validators/node/generate_profile.py +2 -2
  153. infrahub/core/validators/node/hierarchy.py +3 -5
  154. infrahub/core/validators/node/inherit_from.py +27 -5
  155. infrahub/core/validators/node/relationship.py +2 -2
  156. infrahub/core/validators/relationship/count.py +4 -4
  157. infrahub/core/validators/relationship/optional.py +2 -2
  158. infrahub/core/validators/relationship/peer.py +2 -2
  159. infrahub/core/validators/shared.py +2 -2
  160. infrahub/core/validators/tasks.py +8 -0
  161. infrahub/core/validators/uniqueness/checker.py +22 -21
  162. infrahub/core/validators/uniqueness/index.py +2 -2
  163. infrahub/core/validators/uniqueness/model.py +11 -11
  164. infrahub/database/__init__.py +27 -22
  165. infrahub/database/metrics.py +7 -1
  166. infrahub/dependencies/builder/constraint/grouped/node_runner.py +1 -3
  167. infrahub/dependencies/builder/diff/deserializer.py +3 -1
  168. infrahub/dependencies/builder/diff/enricher/hierarchy.py +3 -1
  169. infrahub/dependencies/builder/diff/parent_node_adder.py +8 -0
  170. infrahub/dependencies/component/registry.py +2 -2
  171. infrahub/events/__init__.py +25 -2
  172. infrahub/events/artifact_action.py +64 -0
  173. infrahub/events/branch_action.py +33 -22
  174. infrahub/events/generator.py +71 -0
  175. infrahub/events/group_action.py +51 -21
  176. infrahub/events/models.py +18 -19
  177. infrahub/events/node_action.py +88 -37
  178. infrahub/events/repository_action.py +5 -18
  179. infrahub/events/schema_action.py +4 -9
  180. infrahub/events/utils.py +16 -0
  181. infrahub/events/validator_action.py +55 -0
  182. infrahub/exceptions.py +32 -24
  183. infrahub/generators/models.py +2 -3
  184. infrahub/generators/tasks.py +24 -4
  185. infrahub/git/base.py +7 -7
  186. infrahub/git/integrator.py +48 -24
  187. infrahub/git/models.py +101 -9
  188. infrahub/git/repository.py +3 -3
  189. infrahub/git/tasks.py +408 -6
  190. infrahub/git/utils.py +48 -0
  191. infrahub/git/worktree.py +1 -2
  192. infrahub/git_credential/askpass.py +1 -2
  193. infrahub/graphql/analyzer.py +12 -0
  194. infrahub/graphql/app.py +13 -15
  195. infrahub/graphql/context.py +39 -0
  196. infrahub/graphql/initialization.py +3 -0
  197. infrahub/graphql/loaders/node.py +2 -12
  198. infrahub/graphql/loaders/peers.py +77 -0
  199. infrahub/graphql/loaders/shared.py +13 -0
  200. infrahub/graphql/manager.py +17 -19
  201. infrahub/graphql/mutations/artifact_definition.py +5 -5
  202. infrahub/graphql/mutations/branch.py +26 -1
  203. infrahub/graphql/mutations/computed_attribute.py +9 -5
  204. infrahub/graphql/mutations/diff.py +23 -11
  205. infrahub/graphql/mutations/diff_conflict.py +5 -0
  206. infrahub/graphql/mutations/generator.py +83 -0
  207. infrahub/graphql/mutations/graphql_query.py +5 -5
  208. infrahub/graphql/mutations/ipam.py +54 -74
  209. infrahub/graphql/mutations/main.py +195 -132
  210. infrahub/graphql/mutations/menu.py +7 -7
  211. infrahub/graphql/mutations/models.py +2 -4
  212. infrahub/graphql/mutations/node_getter/by_default_filter.py +10 -10
  213. infrahub/graphql/mutations/node_getter/by_hfid.py +1 -3
  214. infrahub/graphql/mutations/node_getter/by_id.py +1 -3
  215. infrahub/graphql/mutations/node_getter/interface.py +1 -2
  216. infrahub/graphql/mutations/proposed_change.py +7 -7
  217. infrahub/graphql/mutations/relationship.py +93 -19
  218. infrahub/graphql/mutations/repository.py +8 -8
  219. infrahub/graphql/mutations/resource_manager.py +3 -3
  220. infrahub/graphql/mutations/schema.py +19 -4
  221. infrahub/graphql/mutations/webhook.py +137 -0
  222. infrahub/graphql/parser.py +4 -4
  223. infrahub/graphql/permissions.py +1 -10
  224. infrahub/graphql/queries/diff/tree.py +19 -14
  225. infrahub/graphql/queries/event.py +5 -2
  226. infrahub/graphql/queries/ipam.py +2 -2
  227. infrahub/graphql/queries/relationship.py +2 -2
  228. infrahub/graphql/queries/search.py +2 -2
  229. infrahub/graphql/resolvers/many_relationship.py +264 -0
  230. infrahub/graphql/resolvers/resolver.py +13 -110
  231. infrahub/graphql/schema.py +2 -0
  232. infrahub/graphql/subscription/graphql_query.py +2 -0
  233. infrahub/graphql/types/context.py +12 -0
  234. infrahub/graphql/types/event.py +84 -17
  235. infrahub/graphql/types/node.py +2 -2
  236. infrahub/graphql/utils.py +2 -2
  237. infrahub/groups/ancestors.py +29 -0
  238. infrahub/groups/parsers.py +107 -0
  239. infrahub/lock.py +20 -20
  240. infrahub/menu/constants.py +0 -1
  241. infrahub/menu/generator.py +9 -21
  242. infrahub/menu/menu.py +17 -38
  243. infrahub/menu/models.py +117 -16
  244. infrahub/menu/repository.py +111 -0
  245. infrahub/menu/utils.py +5 -8
  246. infrahub/message_bus/__init__.py +11 -13
  247. infrahub/message_bus/messages/__init__.py +1 -21
  248. infrahub/message_bus/messages/check_generator_run.py +3 -3
  249. infrahub/message_bus/messages/finalize_validator_execution.py +3 -0
  250. infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +6 -0
  251. infrahub/message_bus/messages/request_generatordefinition_check.py +2 -0
  252. infrahub/message_bus/messages/send_echo_request.py +1 -1
  253. infrahub/message_bus/operations/__init__.py +1 -10
  254. infrahub/message_bus/operations/check/__init__.py +2 -2
  255. infrahub/message_bus/operations/check/generator.py +1 -0
  256. infrahub/message_bus/operations/event/__init__.py +2 -2
  257. infrahub/message_bus/operations/event/worker.py +0 -3
  258. infrahub/message_bus/operations/finalize/validator.py +51 -1
  259. infrahub/message_bus/operations/requests/__init__.py +0 -2
  260. infrahub/message_bus/operations/requests/generator_definition.py +21 -23
  261. infrahub/message_bus/operations/requests/proposed_change.py +14 -10
  262. infrahub/permissions/globals.py +15 -0
  263. infrahub/pools/number.py +2 -4
  264. infrahub/proposed_change/models.py +3 -0
  265. infrahub/proposed_change/tasks.py +58 -45
  266. infrahub/pytest_plugin.py +13 -10
  267. infrahub/server.py +2 -3
  268. infrahub/services/__init__.py +2 -2
  269. infrahub/services/adapters/cache/__init__.py +4 -6
  270. infrahub/services/adapters/cache/nats.py +4 -5
  271. infrahub/services/adapters/cache/redis.py +3 -7
  272. infrahub/services/adapters/event/__init__.py +1 -1
  273. infrahub/services/adapters/message_bus/__init__.py +3 -3
  274. infrahub/services/adapters/message_bus/local.py +2 -2
  275. infrahub/services/adapters/message_bus/nats.py +4 -4
  276. infrahub/services/adapters/message_bus/rabbitmq.py +4 -4
  277. infrahub/services/adapters/workflow/local.py +2 -2
  278. infrahub/services/component.py +5 -5
  279. infrahub/services/protocols.py +7 -7
  280. infrahub/services/scheduler.py +1 -3
  281. infrahub/task_manager/event.py +102 -9
  282. infrahub/task_manager/models.py +27 -7
  283. infrahub/tasks/artifact.py +7 -6
  284. infrahub/telemetry/__init__.py +0 -0
  285. infrahub/telemetry/constants.py +9 -0
  286. infrahub/telemetry/database.py +86 -0
  287. infrahub/telemetry/models.py +65 -0
  288. infrahub/telemetry/task_manager.py +77 -0
  289. infrahub/{tasks/telemetry.py → telemetry/tasks.py} +49 -56
  290. infrahub/telemetry/utils.py +11 -0
  291. infrahub/trace.py +4 -4
  292. infrahub/transformations/tasks.py +2 -2
  293. infrahub/trigger/catalogue.py +4 -6
  294. infrahub/trigger/constants.py +0 -8
  295. infrahub/trigger/models.py +54 -5
  296. infrahub/trigger/setup.py +90 -0
  297. infrahub/trigger/tasks.py +35 -84
  298. infrahub/utils.py +11 -1
  299. infrahub/validators/__init__.py +0 -0
  300. infrahub/validators/events.py +42 -0
  301. infrahub/validators/tasks.py +41 -0
  302. infrahub/webhook/gather.py +17 -0
  303. infrahub/webhook/models.py +176 -44
  304. infrahub/webhook/tasks.py +154 -155
  305. infrahub/webhook/triggers.py +31 -7
  306. infrahub/workers/infrahub_async.py +2 -2
  307. infrahub/workers/utils.py +2 -2
  308. infrahub/workflows/catalogue.py +86 -35
  309. infrahub/workflows/initialization.py +8 -2
  310. infrahub/workflows/models.py +27 -1
  311. infrahub/workflows/utils.py +10 -1
  312. infrahub_sdk/client.py +35 -8
  313. infrahub_sdk/config.py +3 -0
  314. infrahub_sdk/context.py +13 -0
  315. infrahub_sdk/ctl/branch.py +3 -2
  316. infrahub_sdk/ctl/cli_commands.py +5 -1
  317. infrahub_sdk/ctl/utils.py +0 -16
  318. infrahub_sdk/exceptions.py +12 -0
  319. infrahub_sdk/generator.py +4 -1
  320. infrahub_sdk/graphql.py +45 -13
  321. infrahub_sdk/node.py +71 -22
  322. infrahub_sdk/protocols.py +21 -8
  323. infrahub_sdk/protocols_base.py +32 -11
  324. infrahub_sdk/query_groups.py +6 -35
  325. infrahub_sdk/schema/__init__.py +55 -26
  326. infrahub_sdk/schema/main.py +8 -0
  327. infrahub_sdk/task/__init__.py +11 -0
  328. infrahub_sdk/task/constants.py +3 -0
  329. infrahub_sdk/task/exceptions.py +25 -0
  330. infrahub_sdk/task/manager.py +551 -0
  331. infrahub_sdk/task/models.py +74 -0
  332. infrahub_sdk/testing/schemas/animal.py +9 -0
  333. infrahub_sdk/timestamp.py +142 -33
  334. infrahub_sdk/utils.py +29 -1
  335. {infrahub_server-1.2.0rc0.dist-info → infrahub_server-1.2.1.dist-info}/METADATA +8 -6
  336. {infrahub_server-1.2.0rc0.dist-info → infrahub_server-1.2.1.dist-info}/RECORD +349 -293
  337. {infrahub_server-1.2.0rc0.dist-info → infrahub_server-1.2.1.dist-info}/entry_points.txt +1 -0
  338. infrahub_testcontainers/constants.py +2 -0
  339. infrahub_testcontainers/container.py +157 -12
  340. infrahub_testcontainers/docker-compose.test.yml +31 -6
  341. infrahub_testcontainers/helpers.py +18 -73
  342. infrahub_testcontainers/host.py +41 -0
  343. infrahub_testcontainers/measurements.py +93 -0
  344. infrahub_testcontainers/models.py +38 -0
  345. infrahub_testcontainers/performance_test.py +166 -0
  346. infrahub_testcontainers/plugin.py +136 -0
  347. infrahub_testcontainers/prometheus.yml +30 -0
  348. infrahub/core/schema/definitions/core.py +0 -2286
  349. infrahub/message_bus/messages/check_repository_checkdefinition.py +0 -20
  350. infrahub/message_bus/messages/check_repository_mergeconflicts.py +0 -16
  351. infrahub/message_bus/messages/check_repository_usercheck.py +0 -26
  352. infrahub/message_bus/messages/event_branch_create.py +0 -11
  353. infrahub/message_bus/messages/event_branch_delete.py +0 -11
  354. infrahub/message_bus/messages/event_branch_rebased.py +0 -9
  355. infrahub/message_bus/messages/event_node_mutated.py +0 -15
  356. infrahub/message_bus/messages/event_schema_update.py +0 -9
  357. infrahub/message_bus/messages/request_repository_checks.py +0 -12
  358. infrahub/message_bus/messages/request_repository_userchecks.py +0 -18
  359. infrahub/message_bus/operations/check/repository.py +0 -293
  360. infrahub/message_bus/operations/event/node.py +0 -20
  361. infrahub/message_bus/operations/event/schema.py +0 -17
  362. infrahub/message_bus/operations/requests/repository.py +0 -133
  363. infrahub/webhook/constants.py +0 -1
  364. {infrahub_server-1.2.0rc0.dist-info → infrahub_server-1.2.1.dist-info}/LICENSE.txt +0 -0
  365. {infrahub_server-1.2.0rc0.dist-info → infrahub_server-1.2.1.dist-info}/WHEEL +0 -0
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Optional
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  from infrahub_sdk.batch import InfrahubBatch
6
6
  from prefect import flow, task
@@ -33,7 +33,7 @@ async def schema_apply_migrations(message: SchemaApplyMigrationData, service: In
33
33
  for migration in message.migrations:
34
34
  log.info(f"Preparing migration for {migration.migration_name!r} ({migration.routing_key})")
35
35
 
36
- new_node_schema: Optional[MainSchemaTypes] = None
36
+ new_node_schema: MainSchemaTypes | None = None
37
37
 
38
38
  if message.new_schema.has(name=migration.path.schema_kind):
39
39
  new_node_schema = message.new_schema.get(name=migration.path.schema_kind)
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any, Optional, Sequence, Union
3
+ from typing import TYPE_CHECKING, Any, Sequence
4
4
 
5
5
  from pydantic import BaseModel, ConfigDict, Field
6
6
  from typing_extensions import Self
@@ -43,13 +43,11 @@ class SchemaMigration(BaseModel):
43
43
  name: str = Field(..., description="Name of the migration")
44
44
  queries: Sequence[type[MigrationQuery]] = Field(..., description="List of queries to execute for this migration")
45
45
 
46
- new_node_schema: Optional[Union[NodeSchema, GenericSchema]] = None
47
- previous_node_schema: Optional[Union[NodeSchema, GenericSchema]] = None
46
+ new_node_schema: NodeSchema | GenericSchema | None = None
47
+ previous_node_schema: NodeSchema | GenericSchema | None = None
48
48
  schema_path: SchemaPath
49
49
 
50
- async def execute(
51
- self, db: InfrahubDatabase, branch: Branch, at: Optional[Union[Timestamp, str]] = None
52
- ) -> MigrationResult:
50
+ async def execute(self, db: InfrahubDatabase, branch: Branch, at: Timestamp | str | None = None) -> MigrationResult:
53
51
  async with db.start_transaction() as ts:
54
52
  result = MigrationResult()
55
53
 
@@ -65,13 +63,13 @@ class SchemaMigration(BaseModel):
65
63
  return result
66
64
 
67
65
  @property
68
- def new_schema(self) -> Union[NodeSchema, GenericSchema]:
66
+ def new_schema(self) -> NodeSchema | GenericSchema:
69
67
  if self.new_node_schema:
70
68
  return self.new_node_schema
71
69
  raise ValueError("new_node_schema hasn't been initialized")
72
70
 
73
71
  @property
74
- def previous_schema(self) -> Union[NodeSchema, GenericSchema]:
72
+ def previous_schema(self) -> NodeSchema | GenericSchema:
75
73
  if self.previous_node_schema:
76
74
  return self.previous_node_schema
77
75
  raise ValueError("previous_node_schema hasn't been initialized")
@@ -120,15 +118,17 @@ class GraphMigration(BaseModel):
120
118
 
121
119
  async def execute(self, db: InfrahubDatabase) -> MigrationResult:
122
120
  async with db.start_transaction() as ts:
123
- result = MigrationResult()
121
+ return await self.do_execute(db=ts)
124
122
 
125
- for migration_query in self.queries:
126
- try:
127
- query = await migration_query.init(db=ts)
128
- await query.execute(db=ts)
129
- except Exception as exc:
130
- result.errors.append(str(exc))
131
- return result
123
+ async def do_execute(self, db: InfrahubDatabase) -> MigrationResult:
124
+ result = MigrationResult()
125
+ for migration_query in self.queries:
126
+ try:
127
+ query = await migration_query.init(db=db)
128
+ await query.execute(db=db)
129
+ except Exception as exc:
130
+ result.errors.append(str(exc))
131
+ return result
132
132
 
133
133
  return result
134
134
 
infrahub/core/models.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import hashlib
4
- from typing import TYPE_CHECKING, Any, Optional
4
+ from typing import TYPE_CHECKING, Any
5
5
 
6
6
  from infrahub_sdk.utils import compare_lists, deep_merge_dict, duplicates, intersection
7
7
  from pydantic import BaseModel, ConfigDict, Field
@@ -125,7 +125,7 @@ class SchemaUpdateValidationError(BaseModel):
125
125
  model_config = ConfigDict(extra="forbid")
126
126
  path: SchemaPath
127
127
  error: UpdateValidationErrorType
128
- message: Optional[str] = None
128
+ message: str | None = None
129
129
 
130
130
  def to_string(self) -> str:
131
131
  return f"{self.error.value!r}: {self.path.schema_kind} {self.path.field_name} {self.message}"
@@ -181,6 +181,15 @@ class SchemaUpdateValidationResult(BaseModel):
181
181
 
182
182
  for schema_name, schema_diff in self.diff.changed.items():
183
183
  schema_node = schema.get(name=schema_name, duplicate=False)
184
+ if "inherit_from" in schema_diff.changed:
185
+ self.migrations.append(
186
+ SchemaUpdateMigrationInfo(
187
+ path=SchemaPath( # type: ignore[call-arg]
188
+ schema_kind=schema_name, path_type=SchemaPathType.NODE
189
+ ),
190
+ migration_name="node.inherit_from.update",
191
+ )
192
+ )
184
193
 
185
194
  # Nothing to do today if we add a new attribute to a node in the schema
186
195
  # for node_field_name, _ in schema_diff.added.items():
@@ -341,9 +350,9 @@ class SchemaUpdateValidationResult(BaseModel):
341
350
 
342
351
  class HashableModelDiff(BaseModel):
343
352
  model_config = ConfigDict(extra="forbid")
344
- added: dict[str, Optional[HashableModelDiff]] = Field(default_factory=dict)
345
- changed: dict[str, Optional[HashableModelDiff]] = Field(default_factory=dict)
346
- removed: dict[str, Optional[HashableModelDiff]] = Field(default_factory=dict)
353
+ added: dict[str, HashableModelDiff | None] = Field(default_factory=dict)
354
+ changed: dict[str, HashableModelDiff | None] = Field(default_factory=dict)
355
+ removed: dict[str, HashableModelDiff | None] = Field(default_factory=dict)
347
356
 
348
357
  @property
349
358
  def has_diff(self) -> bool:
@@ -353,7 +362,7 @@ class HashableModelDiff(BaseModel):
353
362
  class HashableModel(BaseModel):
354
363
  model_config = ConfigDict(extra="forbid")
355
364
 
356
- id: Optional[str] = None
365
+ id: str | None = None
357
366
  state: HashableModelState = HashableModelState.PRESENT
358
367
 
359
368
  _exclude_from_hash: list[str] = []
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from enum import Enum
4
- from typing import TYPE_CHECKING, Any, Optional, Sequence, TypeVar, Union, overload
4
+ from typing import TYPE_CHECKING, Any, Sequence, TypeVar, overload
5
5
 
6
6
  from infrahub_sdk.utils import is_valid_uuid
7
7
  from infrahub_sdk.uuidt import UUIDT
@@ -9,6 +9,7 @@ from infrahub_sdk.uuidt import UUIDT
9
9
  from infrahub.core import registry
10
10
  from infrahub.core.changelog.models import NodeChangelog
11
11
  from infrahub.core.constants import (
12
+ GLOBAL_BRANCH_NAME,
12
13
  OBJECT_TEMPLATE_NAME_ATTR,
13
14
  OBJECT_TEMPLATE_RELATIONSHIP_NAME,
14
15
  BranchSupportType,
@@ -28,6 +29,7 @@ from infrahub.types import ATTRIBUTE_TYPES
28
29
 
29
30
  from ...graphql.constants import KIND_GRAPHQL_FIELD_NAME
30
31
  from ...graphql.models import OrderModel
32
+ from ..query.relationship import RelationshipDeleteAllQuery
31
33
  from ..relationship import RelationshipManager
32
34
  from ..utils import update_relationships_to
33
35
  from .base import BaseNode, BaseNodeMeta, BaseNodeOptions
@@ -61,7 +63,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
61
63
  _meta.default_filter = default_filter
62
64
  super().__init_subclass_with_meta__(_meta=_meta, **options)
63
65
 
64
- def get_schema(self) -> Union[NodeSchema, ProfileSchema, TemplateSchema]:
66
+ def get_schema(self) -> NodeSchema | ProfileSchema | TemplateSchema:
65
67
  return self._schema
66
68
 
67
69
  def get_kind(self) -> str:
@@ -78,17 +80,18 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
78
80
  def get_updated_at(self) -> Timestamp | None:
79
81
  return self._updated_at
80
82
 
81
- async def get_hfid(self, db: InfrahubDatabase, include_kind: bool = False) -> Optional[list[str]]:
83
+ async def get_hfid(self, db: InfrahubDatabase, include_kind: bool = False) -> list[str] | None:
82
84
  """Return the Human friendly id of the node."""
83
85
  if not self._schema.human_friendly_id:
84
86
  return None
85
87
 
86
- hfid = [await self.get_path_value(db=db, path=item) for item in self._schema.human_friendly_id]
88
+ hfid_values = [await self.get_path_value(db=db, path=item) for item in self._schema.human_friendly_id]
89
+ hfid = [value for value in hfid_values if value is not None]
87
90
  if include_kind:
88
91
  return [self.get_kind()] + hfid
89
92
  return hfid
90
93
 
91
- async def get_hfid_as_string(self, db: InfrahubDatabase, include_kind: bool = False) -> Optional[str]:
94
+ async def get_hfid_as_string(self, db: InfrahubDatabase, include_kind: bool = False) -> str | None:
92
95
  """Return the Human friendly id of the node in string format separated with a dunder (__) ."""
93
96
  hfid = await self.get_hfid(db=db, include_kind=include_kind)
94
97
  if not hfid:
@@ -158,18 +161,18 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
158
161
 
159
162
  return f"{self.get_kind()}(ID: {str(self.id)})"
160
163
 
161
- def __init__(self, schema: Union[NodeSchema, ProfileSchema, TemplateSchema], branch: Branch, at: Timestamp):
162
- self._schema: Union[NodeSchema, ProfileSchema, TemplateSchema] = schema
164
+ def __init__(self, schema: NodeSchema | ProfileSchema | TemplateSchema, branch: Branch, at: Timestamp):
165
+ self._schema: NodeSchema | ProfileSchema | TemplateSchema = schema
163
166
  self._branch: Branch = branch
164
167
  self._at: Timestamp = at
165
168
  self._existing: bool = False
166
169
 
167
- self._updated_at: Optional[Timestamp] = None
170
+ self._updated_at: Timestamp | None = None
168
171
  self.id: str = None
169
172
  self.db_id: str = None
170
173
 
171
- self._source: Optional[Node] = None
172
- self._owner: Optional[Node] = None
174
+ self._source: Node | None = None
175
+ self._owner: Node | None = None
173
176
  self._is_protected: bool = None
174
177
  self._computed_jinja2_attributes: list[str] = []
175
178
 
@@ -189,10 +192,10 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
189
192
  @classmethod
190
193
  async def init(
191
194
  cls,
192
- schema: Union[NodeSchema, ProfileSchema, TemplateSchema, str],
195
+ schema: NodeSchema | ProfileSchema | TemplateSchema | str,
193
196
  db: InfrahubDatabase,
194
- branch: Optional[Union[Branch, str]] = ...,
195
- at: Optional[Union[Timestamp, str]] = ...,
197
+ branch: Branch | str | None = ...,
198
+ at: Timestamp | str | None = ...,
196
199
  ) -> Self: ...
197
200
 
198
201
  @overload
@@ -201,17 +204,17 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
201
204
  cls,
202
205
  schema: type[SchemaProtocol],
203
206
  db: InfrahubDatabase,
204
- branch: Optional[Union[Branch, str]] = ...,
205
- at: Optional[Union[Timestamp, str]] = ...,
207
+ branch: Branch | str | None = ...,
208
+ at: Timestamp | str | None = ...,
206
209
  ) -> SchemaProtocol: ...
207
210
 
208
211
  @classmethod
209
212
  async def init(
210
213
  cls,
211
- schema: Union[NodeSchema, ProfileSchema, TemplateSchema, str, type[SchemaProtocol]],
214
+ schema: NodeSchema | ProfileSchema | TemplateSchema | str | type[SchemaProtocol],
212
215
  db: InfrahubDatabase,
213
- branch: Optional[Union[Branch, str]] = None,
214
- at: Optional[Union[Timestamp, str]] = None,
216
+ branch: Branch | str | None = None,
217
+ at: Timestamp | str | None = None,
215
218
  ) -> Self | SchemaProtocol:
216
219
  attrs: dict[str, Any] = {}
217
220
 
@@ -308,13 +311,13 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
308
311
  for attribute_name in template._attributes:
309
312
  if attribute_name in list(fields) + [OBJECT_TEMPLATE_NAME_ATTR]:
310
313
  continue
311
- fields[attribute_name] = {"value": getattr(template, attribute_name).value}
314
+ fields[attribute_name] = {"value": getattr(template, attribute_name).value, "source": template.id}
312
315
 
313
316
  for relationship_name in template._relationships:
314
317
  relationship_schema = template._schema.get_relationship(name=relationship_name)
315
318
  if (
316
319
  relationship_name in list(fields)
317
- or relationship_schema.kind != RelationshipKind.ATTRIBUTE
320
+ or relationship_schema.kind not in [RelationshipKind.ATTRIBUTE, RelationshipKind.GENERIC]
318
321
  or relationship_name == OBJECT_TEMPLATE_RELATIONSHIP_NAME
319
322
  ):
320
323
  continue
@@ -544,7 +547,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
544
547
  )
545
548
  return attr
546
549
 
547
- async def process_label(self, db: Optional[InfrahubDatabase] = None) -> None: # noqa: ARG002
550
+ async def process_label(self, db: InfrahubDatabase | None = None) -> None: # noqa: ARG002
548
551
  # If there label and name are both defined for this node
549
552
  # if label is not define, we'll automatically populate it with a human friendy vesion of name
550
553
  if not self._existing and hasattr(self, "label") and hasattr(self, "name"):
@@ -552,7 +555,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
552
555
  self.label.value = " ".join([word.title() for word in self.name.value.split("_")])
553
556
  self.label.is_default = False
554
557
 
555
- async def new(self, db: InfrahubDatabase, id: Optional[str] = None, **kwargs: Any) -> Self:
558
+ async def new(self, db: InfrahubDatabase, id: str | None = None, **kwargs: Any) -> Self:
556
559
  if id and not is_valid_uuid(id):
557
560
  raise ValidationError({"id": f"{id} is not a valid UUID"})
558
561
  if id:
@@ -575,9 +578,9 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
575
578
  async def load(
576
579
  self,
577
580
  db: InfrahubDatabase,
578
- id: Optional[str] = None,
579
- db_id: Optional[str] = None,
580
- updated_at: Optional[Union[Timestamp, str]] = None,
581
+ id: str | None = None,
582
+ db_id: str | None = None,
583
+ updated_at: Timestamp | str | None = None,
581
584
  **kwargs: Any,
582
585
  ) -> Self:
583
586
  self.id = id
@@ -628,7 +631,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
628
631
  return node_changelog
629
632
 
630
633
  async def _update(
631
- self, db: InfrahubDatabase, at: Optional[Timestamp] = None, fields: list[str] | None = None
634
+ self, db: InfrahubDatabase, at: Timestamp | None = None, fields: list[str] | None = None
632
635
  ) -> NodeChangelog:
633
636
  """Update the node in the database if needed."""
634
637
 
@@ -650,7 +653,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
650
653
  processed_relationships.append(name)
651
654
  rel: RelationshipManager = getattr(self, name)
652
655
  updated_relationship = await rel.save(at=update_at, db=db)
653
- node_changelog.add_relationship(relationship=updated_relationship)
656
+ node_changelog.add_relationship(relationship_changelog=updated_relationship)
654
657
 
655
658
  if len(processed_relationships) != len(self._relationships):
656
659
  # Analyze if the node has a parent and add it to the changelog if missing
@@ -663,7 +666,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
663
666
  node_changelog.display_label = await self.render_display_label(db=db)
664
667
  return node_changelog
665
668
 
666
- async def save(self, db: InfrahubDatabase, at: Optional[Timestamp] = None, fields: list[str] | None = None) -> Self:
669
+ async def save(self, db: InfrahubDatabase, at: Timestamp | None = None, fields: list[str] | None = None) -> Self:
667
670
  """Create or Update the Node in the database."""
668
671
 
669
672
  save_at = Timestamp(at)
@@ -675,7 +678,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
675
678
  self._node_changelog = await self._create(at=save_at, db=db)
676
679
  return self
677
680
 
678
- async def delete(self, db: InfrahubDatabase, at: Optional[Timestamp] = None) -> None:
681
+ async def delete(self, db: InfrahubDatabase, at: Timestamp | None = None) -> None:
679
682
  """Delete the Node in the database."""
680
683
 
681
684
  delete_at = Timestamp(at)
@@ -690,16 +693,17 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
690
693
  if deleted_attribute:
691
694
  node_changelog.add_attribute(attribute=deleted_attribute)
692
695
 
693
- # Go over the list of relationships and update them one by one
694
- for name in self._relationships:
695
- rel: RelationshipManager = getattr(self, name)
696
- updated_relationship = await rel.delete(at=delete_at, db=db)
697
- node_changelog.add_relationship(relationship=updated_relationship)
698
-
699
- # Need to check if there are some unidirectional relationship as well
700
- # For example, if we delete a tag, we must check the permissions and update all the relationships pointing at it
701
696
  branch = self.get_branch_based_on_support_type()
702
697
 
698
+ delete_query = await RelationshipDeleteAllQuery.init(
699
+ db=db, node_id=self.get_id(), branch=branch, at=delete_at, branch_agnostic=branch.name == GLOBAL_BRANCH_NAME
700
+ )
701
+ await delete_query.execute(db=db)
702
+
703
+ deleted_relationships_changelogs = delete_query.get_deleted_relationships_changelog(self._schema)
704
+ for relationship_changelog in deleted_relationships_changelogs:
705
+ node_changelog.add_relationship(relationship_changelog=relationship_changelog)
706
+
703
707
  # Update the relationship to the branch itself
704
708
  query = await NodeGetListQuery.init(
705
709
  db=db,
@@ -767,7 +771,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
767
771
  response[field_name] = None
768
772
  continue
769
773
 
770
- field: Optional[BaseAttribute] = getattr(self, field_name, None)
774
+ field: BaseAttribute | None = getattr(self, field_name, None)
771
775
 
772
776
  if not field:
773
777
  response[field_name] = None
@@ -829,7 +833,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
829
833
 
830
834
  return changed
831
835
 
832
- async def render_display_label(self, db: Optional[InfrahubDatabase] = None) -> str: # noqa: ARG002
836
+ async def render_display_label(self, db: InfrahubDatabase | None = None) -> str: # noqa: ARG002
833
837
  if not self._schema.display_labels:
834
838
  return repr(self)
835
839
 
@@ -1,13 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Optional
4
-
5
3
  from ..utils import SubclassWithMeta, SubclassWithMeta_Meta
6
4
 
7
5
 
8
6
  class BaseOptions:
9
- name: Optional[str] = None
10
- description: Optional[str] = None
7
+ name: str | None = None
8
+ description: str | None = None
11
9
 
12
10
  _frozen: bool = False
13
11
 
@@ -1,4 +1,4 @@
1
- from typing import TYPE_CHECKING, Optional
1
+ from typing import TYPE_CHECKING
2
2
 
3
3
  from infrahub.core import registry
4
4
  from infrahub.core.branch import Branch
@@ -18,7 +18,7 @@ class NodeAttributeUniquenessConstraint(NodeConstraintInterface):
18
18
  self.db = db
19
19
  self.branch = branch
20
20
 
21
- async def check(self, node: Node, at: Optional[Timestamp] = None, filters: Optional[list[str]] = None) -> None:
21
+ async def check(self, node: Node, at: Timestamp | None = None, filters: list[str] | None = None) -> None:
22
22
  at = Timestamp(at)
23
23
  node_schema = node.get_schema()
24
24
  for unique_attr in node_schema.unique_attributes:
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Iterable, Optional
3
+ from typing import TYPE_CHECKING, Iterable
4
4
 
5
5
  from infrahub.core import registry
6
6
  from infrahub.core.constants import NULL_VALUE
@@ -9,6 +9,11 @@ from infrahub.core.schema import (
9
9
  SchemaAttributePath,
10
10
  SchemaAttributePathValue,
11
11
  )
12
+ from infrahub.core.schema.basenode_schema import (
13
+ SchemaUniquenessConstraintPath,
14
+ UniquenessConstraintType,
15
+ UniquenessConstraintViolation,
16
+ )
12
17
  from infrahub.core.validators.uniqueness.index import UniquenessQueryResultsIndex
13
18
  from infrahub.core.validators.uniqueness.model import (
14
19
  NodeUniquenessQueryRequest,
@@ -16,7 +21,7 @@ from infrahub.core.validators.uniqueness.model import (
16
21
  QueryRelationshipAttributePath,
17
22
  )
18
23
  from infrahub.core.validators.uniqueness.query import NodeUniqueAttributeConstraintQuery
19
- from infrahub.exceptions import ValidationError
24
+ from infrahub.exceptions import HFIDViolatedError, ValidationError
20
25
 
21
26
  from .interface import NodeConstraintInterface
22
27
 
@@ -39,15 +44,15 @@ class NodeGroupedUniquenessConstraint(NodeConstraintInterface):
39
44
  self,
40
45
  updated_node: Node,
41
46
  node_schema: MainSchemaTypes,
42
- path_groups: list[list[SchemaAttributePath]],
43
- filters: Optional[list[str]] = None,
47
+ uniqueness_constraint_paths: list[SchemaUniquenessConstraintPath],
48
+ filters: list[str] | None = None,
44
49
  ) -> NodeUniquenessQueryRequest:
45
50
  query_request = NodeUniquenessQueryRequest(kind=node_schema.kind)
46
- for path_group in path_groups:
51
+ for uniqueness_constraint_path in uniqueness_constraint_paths:
47
52
  include_in_query = not filters
48
53
  query_relationship_paths: set[QueryRelationshipAttributePath] = set()
49
54
  query_attribute_paths: set[QueryAttributePath] = set()
50
- for attribute_path in path_group:
55
+ for attribute_path in uniqueness_constraint_path.attributes_paths:
51
56
  if attribute_path.related_schema and attribute_path.relationship_schema:
52
57
  if filters and attribute_path.relationship_schema.name in filters:
53
58
  include_in_query = True
@@ -118,71 +123,118 @@ class NodeGroupedUniquenessConstraint(NodeConstraintInterface):
118
123
  )
119
124
  return node_value_combination
120
125
 
121
- def _check_one_constraint_group(
122
- self, schema_attribute_path_values: list[SchemaAttributePathValue], results_index: UniquenessQueryResultsIndex
123
- ) -> None:
124
- # constraint cannot be violated if this node is missing any values
125
- if any(sapv.value is None for sapv in schema_attribute_path_values):
126
- return
127
-
128
- matching_node_ids = results_index.get_node_ids_for_value_group(schema_attribute_path_values)
129
- if not matching_node_ids:
130
- return
131
- uniqueness_constraint_fields = []
132
- for sapv in schema_attribute_path_values:
133
- if sapv.relationship_schema:
134
- uniqueness_constraint_fields.append(sapv.relationship_schema.name)
135
- elif sapv.attribute_schema:
136
- uniqueness_constraint_fields.append(sapv.attribute_schema.name)
137
- uniqueness_constraint_string = "-".join(uniqueness_constraint_fields)
138
- error_msg = f"Violates uniqueness constraint '{uniqueness_constraint_string}'"
139
- errors = [ValidationError({field_name: error_msg}) for field_name in uniqueness_constraint_fields]
140
- raise ValidationError(errors)
141
-
142
- async def _check_results(
126
+ async def _get_violations(
143
127
  self,
144
128
  updated_node: Node,
145
- path_groups: list[list[SchemaAttributePath]],
129
+ uniqueness_constraint_paths: list[SchemaUniquenessConstraintPath],
146
130
  query_results: Iterable[QueryResult],
147
- ) -> None:
131
+ ) -> list[UniquenessConstraintViolation]:
148
132
  results_index = UniquenessQueryResultsIndex(
149
133
  query_results=query_results, exclude_node_ids={updated_node.get_id()}
150
134
  )
151
- for path_group in path_groups:
135
+ violations = []
136
+ for uniqueness_constraint_path in uniqueness_constraint_paths:
137
+ # path_group = one constraint (that can contain multiple items)
152
138
  schema_attribute_path_values = await self._get_node_attribute_path_values(
153
- updated_node=updated_node, path_group=path_group
139
+ updated_node=updated_node, path_group=uniqueness_constraint_path.attributes_paths
154
140
  )
155
- self._check_one_constraint_group(
156
- schema_attribute_path_values=schema_attribute_path_values, results_index=results_index
141
+
142
+ # constraint cannot be violated if this node is missing any values
143
+ if any(sapv.value is None for sapv in schema_attribute_path_values):
144
+ continue
145
+
146
+ matching_node_ids = results_index.get_node_ids_for_value_group(schema_attribute_path_values)
147
+ if not matching_node_ids:
148
+ continue
149
+
150
+ uniqueness_constraint_fields = []
151
+ for sapv in schema_attribute_path_values:
152
+ if sapv.relationship_schema:
153
+ uniqueness_constraint_fields.append(sapv.relationship_schema.name)
154
+ elif sapv.attribute_schema:
155
+ uniqueness_constraint_fields.append(sapv.attribute_schema.name)
156
+
157
+ violations.append(
158
+ UniquenessConstraintViolation(
159
+ nodes_ids=matching_node_ids,
160
+ fields=uniqueness_constraint_fields,
161
+ typ=uniqueness_constraint_path.typ,
162
+ )
157
163
  )
158
164
 
159
- async def _check_one_schema(
165
+ return violations
166
+
167
+ async def _get_single_schema_violations(
160
168
  self,
161
169
  node: Node,
162
170
  node_schema: MainSchemaTypes,
163
- at: Optional[Timestamp] = None,
164
- filters: Optional[list[str]] = None,
165
- ) -> None:
171
+ at: Timestamp | None = None,
172
+ filters: list[str] | None = None,
173
+ ) -> list[UniquenessConstraintViolation]:
166
174
  schema_branch = self.db.schema.get_schema_branch(name=self.branch.name)
167
- path_groups = node_schema.get_unique_constraint_schema_attribute_paths(schema_branch=schema_branch)
175
+
176
+ uniqueness_constraint_paths = node_schema.get_unique_constraint_schema_attribute_paths(
177
+ schema_branch=schema_branch
178
+ )
168
179
  query_request = await self._build_query_request(
169
- updated_node=node, node_schema=node_schema, path_groups=path_groups, filters=filters
180
+ updated_node=node,
181
+ node_schema=node_schema,
182
+ uniqueness_constraint_paths=uniqueness_constraint_paths,
183
+ filters=filters,
170
184
  )
171
185
  if not query_request:
172
- return
186
+ return []
187
+
173
188
  query = await NodeUniqueAttributeConstraintQuery.init(
174
189
  db=self.db, branch=self.branch, at=at, query_request=query_request, min_count_required=0
175
190
  )
176
191
  await query.execute(db=self.db)
177
- await self._check_results(updated_node=node, path_groups=path_groups, query_results=query.get_results())
192
+ return await self._get_violations(
193
+ updated_node=node,
194
+ uniqueness_constraint_paths=uniqueness_constraint_paths,
195
+ query_results=query.get_results(),
196
+ )
197
+
198
+ async def check(self, node: Node, at: Timestamp | None = None, filters: list[str] | None = None) -> None:
199
+ def _frozen_constraints(schema: MainSchemaTypes) -> frozenset[frozenset[str]]:
200
+ if not schema.uniqueness_constraints:
201
+ return frozenset()
202
+ return frozenset(frozenset(uc) for uc in schema.uniqueness_constraints)
178
203
 
179
- async def check(self, node: Node, at: Optional[Timestamp] = None, filters: Optional[list[str]] = None) -> None:
180
204
  node_schema = node.get_schema()
181
- schemas_to_check: list[MainSchemaTypes] = [node_schema]
205
+ include_node_schema = True
206
+ frozen_node_constraints = _frozen_constraints(node_schema)
207
+ schemas_to_check: list[MainSchemaTypes] = []
182
208
  if node_schema.inherit_from:
183
209
  for parent_schema_name in node_schema.inherit_from:
184
210
  parent_schema = self.schema_branch.get(name=parent_schema_name, duplicate=False)
185
- if parent_schema.uniqueness_constraints:
186
- schemas_to_check.append(parent_schema)
211
+ if not parent_schema.uniqueness_constraints:
212
+ continue
213
+ schemas_to_check.append(parent_schema)
214
+ frozen_parent_constraints = _frozen_constraints(parent_schema)
215
+ if frozen_node_constraints <= frozen_parent_constraints:
216
+ include_node_schema = False
217
+
218
+ if include_node_schema:
219
+ schemas_to_check.append(node_schema)
220
+
221
+ violations = []
187
222
  for schema in schemas_to_check:
188
- await self._check_one_schema(node=node, node_schema=schema, at=at, filters=filters)
223
+ schema_violations = await self._get_single_schema_violations(
224
+ node=node, node_schema=schema, at=at, filters=filters
225
+ )
226
+ violations.extend(schema_violations)
227
+
228
+ is_hfid_violated = any(violation.typ == UniquenessConstraintType.HFID for violation in violations)
229
+
230
+ for violation in violations:
231
+ if violation.typ == UniquenessConstraintType.STANDARD or (
232
+ violation.typ == UniquenessConstraintType.SUBSET_OF_HFID and not is_hfid_violated
233
+ ):
234
+ error_msg = f"Violates uniqueness constraint '{'-'.join(violation.fields)}'"
235
+ raise ValidationError(error_msg)
236
+
237
+ for violation in violations:
238
+ if violation.typ == UniquenessConstraintType.HFID:
239
+ error_msg = f"Violates uniqueness constraint '{'-'.join(violation.fields)}'"
240
+ raise HFIDViolatedError(error_msg, matching_nodes_ids=violation.nodes_ids)
@@ -1,5 +1,4 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Optional
3
2
 
4
3
  from infrahub.core.node import Node
5
4
  from infrahub.core.timestamp import Timestamp
@@ -7,4 +6,4 @@ from infrahub.core.timestamp import Timestamp
7
6
 
8
7
  class NodeConstraintInterface(ABC):
9
8
  @abstractmethod
10
- async def check(self, node: Node, at: Optional[Timestamp] = None, filters: Optional[list[str]] = None) -> None: ...
9
+ async def check(self, node: Node, at: Timestamp | None = None, filters: list[str] | None = None) -> None: ...
@@ -1,6 +1,6 @@
1
1
  from collections import defaultdict
2
2
  from enum import Enum
3
- from typing import Iterable, Optional, Union
3
+ from typing import Iterable
4
4
 
5
5
  from infrahub.core import registry
6
6
  from infrahub.core.branch import Branch
@@ -124,16 +124,14 @@ class NodeDeleteValidator:
124
124
  self._all_schemas_map = schema_branch.get_all(duplicate=False)
125
125
  self.index: NodeDeleteIndex = NodeDeleteIndex(all_schemas_map=self._all_schemas_map)
126
126
 
127
- async def get_ids_to_delete(self, nodes: Iterable[Node], at: Optional[Union[Timestamp, str]] = None) -> set[str]:
127
+ async def get_ids_to_delete(self, nodes: Iterable[Node], at: Timestamp | str | None = None) -> set[str]:
128
128
  start_schemas = {node.get_schema() for node in nodes}
129
129
  self.index.index(start_schemas=start_schemas)
130
130
  at = Timestamp(at)
131
131
 
132
132
  return await self._analyze_delete_dependencies(start_nodes=nodes, at=at)
133
133
 
134
- async def _analyze_delete_dependencies(
135
- self, start_nodes: Iterable[Node], at: Optional[Union[Timestamp, str]]
136
- ) -> set[str]:
134
+ async def _analyze_delete_dependencies(self, start_nodes: Iterable[Node], at: Timestamp | str | None) -> set[str]:
137
135
  full_relationship_identifiers = self.index.get_relationship_identifiers()
138
136
  if not full_relationship_identifiers:
139
137
  return {node.get_id() for node in start_nodes}