infrahub-server 1.1.9__py3-none-any.whl → 1.2.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 (467) hide show
  1. infrahub/api/artifact.py +16 -4
  2. infrahub/api/dependencies.py +14 -6
  3. infrahub/api/diff/validation_models.py +7 -7
  4. infrahub/api/oauth2.py +0 -1
  5. infrahub/api/oidc.py +0 -1
  6. infrahub/api/query.py +18 -7
  7. infrahub/api/schema.py +33 -7
  8. infrahub/api/transformation.py +12 -5
  9. infrahub/{message_bus/messages/check_artifact_create.py → artifacts/models.py} +6 -6
  10. infrahub/{message_bus/operations/check/artifact.py → artifacts/tasks.py} +27 -28
  11. infrahub/cli/__init__.py +12 -10
  12. infrahub/cli/constants.py +3 -0
  13. infrahub/cli/db.py +166 -185
  14. infrahub/cli/events.py +8 -3
  15. infrahub/cli/git_agent.py +9 -7
  16. infrahub/cli/tasks.py +4 -6
  17. infrahub/cli/upgrade.py +146 -0
  18. infrahub/computed_attribute/gather.py +174 -0
  19. infrahub/computed_attribute/models.py +202 -11
  20. infrahub/computed_attribute/tasks.py +103 -421
  21. infrahub/computed_attribute/triggers.py +56 -0
  22. infrahub/config.py +33 -33
  23. infrahub/context.py +53 -0
  24. infrahub/core/account.py +9 -12
  25. infrahub/core/attribute.py +104 -75
  26. infrahub/core/branch/models.py +4 -4
  27. infrahub/core/branch/tasks.py +133 -125
  28. infrahub/core/changelog/__init__.py +0 -0
  29. infrahub/core/changelog/diff.py +291 -0
  30. infrahub/core/changelog/models.py +662 -0
  31. infrahub/core/constants/__init__.py +47 -2
  32. infrahub/core/constants/infrahubkind.py +3 -0
  33. infrahub/core/constants/schema.py +2 -0
  34. infrahub/core/constraint/node/runner.py +2 -2
  35. infrahub/core/diff/branch_differ.py +10 -10
  36. infrahub/core/diff/combiner.py +1 -1
  37. infrahub/core/diff/enricher/cardinality_one.py +1 -1
  38. infrahub/core/diff/enricher/hierarchy.py +5 -3
  39. infrahub/core/diff/enricher/labels.py +1 -1
  40. infrahub/core/diff/enricher/path_identifier.py +1 -2
  41. infrahub/core/diff/enricher/summary_counts.py +107 -0
  42. infrahub/core/diff/ipam_diff_parser.py +4 -5
  43. infrahub/core/diff/merger/merger.py +3 -1
  44. infrahub/core/diff/model/diff.py +27 -27
  45. infrahub/core/diff/model/path.py +13 -13
  46. infrahub/core/diff/query/all_conflicts.py +1 -1
  47. infrahub/core/diff/query/artifact.py +1 -1
  48. infrahub/core/diff/query/delete_query.py +1 -1
  49. infrahub/core/diff/query/diff_get.py +1 -1
  50. infrahub/core/diff/query/diff_summary.py +1 -1
  51. infrahub/core/diff/query/field_specifiers.py +1 -1
  52. infrahub/core/diff/query/field_summary.py +1 -1
  53. infrahub/core/diff/query/filters.py +2 -2
  54. infrahub/core/diff/query/get_conflict_query.py +1 -1
  55. infrahub/core/diff/query/has_conflicts_query.py +1 -1
  56. infrahub/core/diff/query/merge.py +3 -3
  57. infrahub/core/diff/query/merge_tracking_id.py +1 -1
  58. infrahub/core/diff/query/roots_metadata.py +1 -1
  59. infrahub/core/diff/query/save.py +3 -3
  60. infrahub/core/diff/query/summary_counts_enricher.py +2 -2
  61. infrahub/core/diff/query/time_range_query.py +1 -1
  62. infrahub/core/diff/query/update_conflict_query.py +1 -1
  63. infrahub/core/diff/query_parser.py +4 -4
  64. infrahub/core/diff/repository/deserializer.py +1 -1
  65. infrahub/core/diff/tasks.py +9 -8
  66. infrahub/core/enums.py +1 -1
  67. infrahub/core/initialization.py +1 -10
  68. infrahub/core/integrity/object_conflict/conflict_recorder.py +1 -1
  69. infrahub/core/ipam/constants.py +3 -4
  70. infrahub/core/ipam/reconciler.py +13 -13
  71. infrahub/core/ipam/tasks.py +2 -3
  72. infrahub/core/ipam/utilization.py +10 -13
  73. infrahub/core/manager.py +52 -47
  74. infrahub/core/merge.py +12 -9
  75. infrahub/core/migrations/__init__.py +1 -3
  76. infrahub/core/migrations/graph/__init__.py +5 -3
  77. infrahub/core/migrations/graph/m001_add_version_to_graph.py +1 -1
  78. infrahub/core/migrations/graph/m002_attribute_is_default.py +2 -2
  79. infrahub/core/migrations/graph/m003_relationship_parent_optional.py +2 -2
  80. infrahub/core/migrations/graph/m004_add_attr_documentation.py +1 -1
  81. infrahub/core/migrations/graph/m005_add_rel_read_only.py +1 -1
  82. infrahub/core/migrations/graph/m006_add_rel_on_delete.py +1 -1
  83. infrahub/core/migrations/graph/m007_add_rel_allow_override.py +1 -1
  84. infrahub/core/migrations/graph/m008_add_human_friendly_id.py +1 -1
  85. infrahub/core/migrations/graph/m009_add_generate_profile_attr.py +1 -1
  86. infrahub/core/migrations/graph/m010_add_generate_profile_attr_generic.py +1 -1
  87. infrahub/core/migrations/graph/m011_remove_profile_relationship_schema.py +2 -2
  88. infrahub/core/migrations/graph/m012_convert_account_generic.py +12 -23
  89. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +7 -11
  90. infrahub/core/migrations/graph/m014_remove_index_attr_value.py +2 -2
  91. infrahub/core/migrations/graph/m015_diff_format_update.py +1 -1
  92. infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +1 -1
  93. infrahub/core/migrations/graph/m017_add_core_profile.py +2 -6
  94. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +3 -3
  95. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +4 -4
  96. infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -3
  97. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +2 -2
  98. infrahub/core/migrations/graph/m022_add_generate_template_attr.py +48 -0
  99. infrahub/core/migrations/query/attribute_add.py +3 -3
  100. infrahub/core/migrations/query/attribute_rename.py +1 -1
  101. infrahub/core/migrations/query/delete_element_in_schema.py +1 -1
  102. infrahub/core/migrations/query/node_duplicate.py +1 -1
  103. infrahub/core/migrations/query/relationship_duplicate.py +1 -1
  104. infrahub/core/migrations/query/schema_attribute_update.py +3 -3
  105. infrahub/core/migrations/schema/models.py +19 -4
  106. infrahub/core/migrations/schema/node_attribute_remove.py +1 -1
  107. infrahub/core/migrations/schema/node_remove.py +1 -1
  108. infrahub/core/migrations/schema/tasks.py +7 -7
  109. infrahub/core/migrations/shared.py +10 -12
  110. infrahub/core/models.py +13 -14
  111. infrahub/core/node/__init__.py +172 -57
  112. infrahub/core/node/base.py +3 -5
  113. infrahub/core/node/constraints/attribute_uniqueness.py +2 -2
  114. infrahub/core/node/constraints/grouped_uniqueness.py +5 -5
  115. infrahub/core/node/constraints/interface.py +1 -2
  116. infrahub/core/node/delete_validator.py +7 -9
  117. infrahub/core/node/ipam.py +10 -10
  118. infrahub/core/node/permissions.py +7 -7
  119. infrahub/core/node/resource_manager/ip_address_pool.py +6 -6
  120. infrahub/core/node/resource_manager/ip_prefix_pool.py +14 -11
  121. infrahub/core/node/resource_manager/number_pool.py +3 -3
  122. infrahub/core/node/standard.py +3 -5
  123. infrahub/core/path.py +12 -12
  124. infrahub/core/property.py +12 -12
  125. infrahub/core/protocols.py +11 -0
  126. infrahub/core/protocols_base.py +25 -23
  127. infrahub/core/query/__init__.py +35 -38
  128. infrahub/core/query/attribute.py +13 -13
  129. infrahub/core/query/branch.py +5 -5
  130. infrahub/core/query/delete.py +1 -1
  131. infrahub/core/query/diff.py +7 -7
  132. infrahub/core/query/ipam.py +4 -4
  133. infrahub/core/query/node.py +23 -24
  134. infrahub/core/query/relationship.py +143 -46
  135. infrahub/core/query/resource_manager.py +10 -10
  136. infrahub/core/query/standard_node.py +9 -9
  137. infrahub/core/query/subquery.py +9 -9
  138. infrahub/core/query/task.py +3 -3
  139. infrahub/core/query/task_log.py +1 -1
  140. infrahub/core/query/utils.py +5 -5
  141. infrahub/core/registry.py +13 -17
  142. infrahub/core/relationship/constraints/count.py +4 -5
  143. infrahub/core/relationship/constraints/peer_kind.py +4 -5
  144. infrahub/core/relationship/constraints/profiles_kind.py +2 -2
  145. infrahub/core/relationship/model.py +103 -67
  146. infrahub/core/schema/__init__.py +6 -4
  147. infrahub/core/schema/attribute_schema.py +16 -8
  148. infrahub/core/schema/basenode_schema.py +38 -26
  149. infrahub/core/schema/computed_attribute.py +3 -3
  150. infrahub/core/schema/definitions/core/__init__.py +147 -0
  151. infrahub/core/schema/definitions/core/account.py +171 -0
  152. infrahub/core/schema/definitions/core/artifact.py +136 -0
  153. infrahub/core/schema/definitions/core/builtin.py +24 -0
  154. infrahub/core/schema/definitions/core/check.py +68 -0
  155. infrahub/core/schema/definitions/core/core.py +17 -0
  156. infrahub/core/schema/definitions/core/generator.py +100 -0
  157. infrahub/core/schema/definitions/core/graphql_query.py +79 -0
  158. infrahub/core/schema/definitions/core/group.py +108 -0
  159. infrahub/core/schema/definitions/core/ipam.py +193 -0
  160. infrahub/core/schema/definitions/core/lineage.py +19 -0
  161. infrahub/core/schema/definitions/core/menu.py +48 -0
  162. infrahub/core/schema/definitions/core/permission.py +163 -0
  163. infrahub/core/schema/definitions/core/profile.py +18 -0
  164. infrahub/core/schema/definitions/core/propose_change.py +97 -0
  165. infrahub/core/schema/definitions/core/propose_change_comment.py +193 -0
  166. infrahub/core/schema/definitions/core/propose_change_validator.py +328 -0
  167. infrahub/core/schema/definitions/core/repository.py +286 -0
  168. infrahub/core/schema/definitions/core/resource_pool.py +170 -0
  169. infrahub/core/schema/definitions/core/template.py +27 -0
  170. infrahub/core/schema/definitions/core/transform.py +96 -0
  171. infrahub/core/schema/definitions/core/webhook.py +134 -0
  172. infrahub/core/schema/definitions/internal.py +32 -16
  173. infrahub/core/schema/dropdown.py +3 -4
  174. infrahub/core/schema/generated/attribute_schema.py +15 -18
  175. infrahub/core/schema/generated/base_node_schema.py +12 -14
  176. infrahub/core/schema/generated/genericnode_schema.py +5 -0
  177. infrahub/core/schema/generated/node_schema.py +8 -5
  178. infrahub/core/schema/generated/relationship_schema.py +9 -11
  179. infrahub/core/schema/generic_schema.py +6 -2
  180. infrahub/core/schema/manager.py +46 -43
  181. infrahub/core/schema/node_schema.py +6 -2
  182. infrahub/core/schema/profile_schema.py +4 -0
  183. infrahub/core/schema/relationship_schema.py +15 -7
  184. infrahub/core/schema/schema_branch.py +423 -89
  185. infrahub/core/schema/schema_branch_computed.py +41 -4
  186. infrahub/core/schema/template_schema.py +36 -0
  187. infrahub/core/task/task.py +3 -3
  188. infrahub/core/task/user_task.py +21 -19
  189. infrahub/core/timestamp.py +3 -3
  190. infrahub/core/utils.py +12 -12
  191. infrahub/core/validators/__init__.py +1 -3
  192. infrahub/core/validators/aggregated_checker.py +2 -2
  193. infrahub/core/validators/attribute/choices.py +3 -3
  194. infrahub/core/validators/attribute/enum.py +3 -3
  195. infrahub/core/validators/attribute/kind.py +3 -3
  196. infrahub/core/validators/attribute/length.py +3 -3
  197. infrahub/core/validators/attribute/optional.py +3 -3
  198. infrahub/core/validators/attribute/regex.py +3 -3
  199. infrahub/core/validators/attribute/unique.py +3 -3
  200. infrahub/core/validators/checks_runner.py +60 -0
  201. infrahub/core/validators/determiner.py +1 -3
  202. infrahub/core/validators/model.py +1 -3
  203. infrahub/core/validators/models/validate_migration.py +17 -4
  204. infrahub/core/validators/node/attribute.py +2 -2
  205. infrahub/core/validators/node/generate_profile.py +3 -3
  206. infrahub/core/validators/node/hierarchy.py +3 -3
  207. infrahub/core/validators/node/inherit_from.py +2 -2
  208. infrahub/core/validators/node/relationship.py +2 -2
  209. infrahub/core/validators/query.py +1 -1
  210. infrahub/core/validators/relationship/count.py +5 -5
  211. infrahub/core/validators/relationship/optional.py +3 -3
  212. infrahub/core/validators/relationship/peer.py +3 -3
  213. infrahub/core/validators/shared.py +2 -2
  214. infrahub/core/validators/tasks.py +8 -6
  215. infrahub/core/validators/uniqueness/checker.py +5 -6
  216. infrahub/core/validators/uniqueness/index.py +2 -2
  217. infrahub/core/validators/uniqueness/model.py +11 -11
  218. infrahub/core/validators/uniqueness/query.py +1 -1
  219. infrahub/database/__init__.py +19 -23
  220. infrahub/database/memgraph.py +1 -1
  221. infrahub/dependencies/builder/diff/combiner.py +1 -1
  222. infrahub/dependencies/builder/diff/conflicts_enricher.py +1 -1
  223. infrahub/dependencies/builder/diff/deserializer.py +1 -1
  224. infrahub/dependencies/builder/diff/enricher/summary_counts.py +8 -0
  225. infrahub/dependencies/builder/diff/parent_node_adder.py +1 -1
  226. infrahub/dependencies/component/registry.py +2 -2
  227. infrahub/events/__init__.py +25 -2
  228. infrahub/events/artifact_action.py +64 -0
  229. infrahub/events/branch_action.py +57 -20
  230. infrahub/events/generator.py +71 -0
  231. infrahub/events/group_action.py +103 -0
  232. infrahub/events/models.py +160 -53
  233. infrahub/events/node_action.py +140 -23
  234. infrahub/events/repository_action.py +7 -20
  235. infrahub/events/schema_action.py +18 -10
  236. infrahub/events/utils.py +16 -0
  237. infrahub/events/validator_action.py +55 -0
  238. infrahub/exceptions.py +12 -3
  239. infrahub/generators/models.py +2 -3
  240. infrahub/generators/tasks.py +34 -15
  241. infrahub/git/base.py +10 -12
  242. infrahub/git/constants.py +0 -1
  243. infrahub/git/integrator.py +82 -57
  244. infrahub/git/models.py +101 -9
  245. infrahub/git/repository.py +9 -10
  246. infrahub/git/tasks.py +450 -112
  247. infrahub/git/utils.py +48 -0
  248. infrahub/git/worktree.py +1 -2
  249. infrahub/git_credential/askpass.py +1 -2
  250. infrahub/git_credential/helper.py +2 -3
  251. infrahub/graphql/analyzer.py +572 -11
  252. infrahub/graphql/app.py +47 -41
  253. infrahub/graphql/auth/query_permission_checker/anonymous_checker.py +5 -5
  254. infrahub/graphql/auth/query_permission_checker/default_branch_checker.py +4 -4
  255. infrahub/graphql/auth/query_permission_checker/merge_operation_checker.py +4 -4
  256. infrahub/graphql/auth/query_permission_checker/object_permission_checker.py +28 -35
  257. infrahub/graphql/auth/query_permission_checker/super_admin_checker.py +5 -5
  258. infrahub/graphql/context.py +39 -0
  259. infrahub/graphql/enums.py +1 -1
  260. infrahub/graphql/initialization.py +5 -1
  261. infrahub/graphql/loaders/node.py +1 -1
  262. infrahub/graphql/loaders/shared.py +1 -1
  263. infrahub/graphql/manager.py +75 -72
  264. infrahub/graphql/mutations/account.py +20 -13
  265. infrahub/graphql/mutations/artifact_definition.py +18 -14
  266. infrahub/graphql/mutations/branch.py +86 -40
  267. infrahub/graphql/mutations/computed_attribute.py +26 -18
  268. infrahub/graphql/mutations/diff.py +17 -8
  269. infrahub/graphql/mutations/diff_conflict.py +14 -8
  270. infrahub/graphql/mutations/generator.py +83 -0
  271. infrahub/graphql/mutations/graphql_query.py +21 -13
  272. infrahub/graphql/mutations/ipam.py +41 -39
  273. infrahub/graphql/mutations/main.py +226 -66
  274. infrahub/graphql/mutations/menu.py +12 -12
  275. infrahub/graphql/mutations/models.py +2 -4
  276. infrahub/graphql/mutations/node_getter/by_default_filter.py +1 -3
  277. infrahub/graphql/mutations/node_getter/by_hfid.py +1 -3
  278. infrahub/graphql/mutations/node_getter/by_id.py +1 -3
  279. infrahub/graphql/mutations/node_getter/interface.py +1 -2
  280. infrahub/graphql/mutations/proposed_change.py +39 -31
  281. infrahub/graphql/mutations/relationship.py +372 -129
  282. infrahub/graphql/mutations/repository.py +46 -40
  283. infrahub/graphql/mutations/resource_manager.py +26 -26
  284. infrahub/graphql/mutations/schema.py +70 -37
  285. infrahub/graphql/mutations/tasks.py +10 -7
  286. infrahub/graphql/mutations/webhook.py +137 -0
  287. infrahub/graphql/parser.py +5 -5
  288. infrahub/graphql/permissions.py +3 -10
  289. infrahub/graphql/queries/account.py +22 -18
  290. infrahub/graphql/queries/branch.py +6 -4
  291. infrahub/graphql/queries/diff/tree.py +67 -56
  292. infrahub/graphql/queries/event.py +115 -0
  293. infrahub/graphql/queries/internal.py +3 -3
  294. infrahub/graphql/queries/ipam.py +25 -20
  295. infrahub/graphql/queries/relationship.py +13 -12
  296. infrahub/graphql/queries/resource_manager.py +37 -25
  297. infrahub/graphql/queries/search.py +11 -10
  298. infrahub/graphql/queries/status.py +12 -9
  299. infrahub/graphql/queries/task.py +11 -9
  300. infrahub/graphql/resolvers/many_relationship.py +15 -15
  301. infrahub/graphql/resolvers/resolver.py +58 -37
  302. infrahub/graphql/resolvers/single_relationship.py +16 -10
  303. infrahub/graphql/schema.py +4 -0
  304. infrahub/graphql/subscription/__init__.py +1 -1
  305. infrahub/graphql/subscription/events.py +1 -1
  306. infrahub/graphql/subscription/graphql_query.py +8 -8
  307. infrahub/graphql/types/branch.py +2 -2
  308. infrahub/graphql/types/common.py +6 -1
  309. infrahub/graphql/types/context.py +12 -0
  310. infrahub/graphql/types/enums.py +2 -0
  311. infrahub/graphql/types/event.py +167 -0
  312. infrahub/graphql/types/interface.py +2 -2
  313. infrahub/graphql/types/node.py +5 -5
  314. infrahub/graphql/types/permission.py +2 -2
  315. infrahub/graphql/types/relationship.py +3 -3
  316. infrahub/graphql/types/standard_node.py +9 -11
  317. infrahub/graphql/utils.py +30 -184
  318. infrahub/groups/ancestors.py +29 -0
  319. infrahub/groups/parsers.py +107 -0
  320. infrahub/groups/tasks.py +2 -3
  321. infrahub/lock.py +21 -21
  322. infrahub/menu/generator.py +7 -8
  323. infrahub/menu/menu.py +107 -139
  324. infrahub/menu/models.py +121 -20
  325. infrahub/menu/repository.py +111 -0
  326. infrahub/menu/utils.py +5 -8
  327. infrahub/message_bus/__init__.py +11 -13
  328. infrahub/message_bus/messages/__init__.py +1 -25
  329. infrahub/message_bus/messages/check_generator_run.py +3 -3
  330. infrahub/message_bus/messages/event_branch_merge.py +3 -0
  331. infrahub/message_bus/messages/finalize_validator_execution.py +3 -0
  332. infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +6 -0
  333. infrahub/message_bus/messages/request_generatordefinition_check.py +2 -0
  334. infrahub/message_bus/messages/request_proposedchange_pipeline.py +2 -0
  335. infrahub/message_bus/messages/send_echo_request.py +1 -1
  336. infrahub/message_bus/operations/__init__.py +4 -15
  337. infrahub/message_bus/operations/check/__init__.py +2 -2
  338. infrahub/message_bus/operations/check/generator.py +2 -3
  339. infrahub/message_bus/operations/event/__init__.py +2 -2
  340. infrahub/message_bus/operations/event/branch.py +7 -3
  341. infrahub/message_bus/operations/event/worker.py +0 -3
  342. infrahub/message_bus/operations/finalize/validator.py +52 -2
  343. infrahub/message_bus/operations/git/file.py +2 -2
  344. infrahub/message_bus/operations/git/repository.py +1 -1
  345. infrahub/message_bus/operations/requests/__init__.py +0 -4
  346. infrahub/message_bus/operations/requests/generator_definition.py +22 -24
  347. infrahub/message_bus/operations/requests/proposed_change.py +39 -20
  348. infrahub/message_bus/operations/send/echo.py +1 -1
  349. infrahub/message_bus/types.py +1 -1
  350. infrahub/permissions/globals.py +15 -0
  351. infrahub/pools/number.py +2 -4
  352. infrahub/pools/prefix.py +29 -165
  353. infrahub/prefect_server/__init__.py +0 -0
  354. infrahub/prefect_server/app.py +18 -0
  355. infrahub/prefect_server/database.py +20 -0
  356. infrahub/prefect_server/events.py +28 -0
  357. infrahub/prefect_server/models.py +46 -0
  358. infrahub/proposed_change/models.py +18 -1
  359. infrahub/proposed_change/tasks.py +204 -53
  360. infrahub/pytest_plugin.py +13 -10
  361. infrahub/server.py +13 -12
  362. infrahub/services/__init__.py +148 -63
  363. infrahub/services/adapters/cache/__init__.py +11 -11
  364. infrahub/services/adapters/cache/nats.py +42 -25
  365. infrahub/services/adapters/cache/redis.py +3 -11
  366. infrahub/services/adapters/event/__init__.py +11 -19
  367. infrahub/services/adapters/http/__init__.py +0 -5
  368. infrahub/services/adapters/http/httpx.py +22 -15
  369. infrahub/services/adapters/message_bus/__init__.py +25 -8
  370. infrahub/services/adapters/message_bus/local.py +9 -7
  371. infrahub/services/adapters/message_bus/nats.py +14 -8
  372. infrahub/services/adapters/message_bus/rabbitmq.py +23 -10
  373. infrahub/services/adapters/workflow/__init__.py +11 -8
  374. infrahub/services/adapters/workflow/local.py +27 -6
  375. infrahub/services/adapters/workflow/worker.py +23 -7
  376. infrahub/services/component.py +43 -40
  377. infrahub/services/protocols.py +7 -7
  378. infrahub/services/scheduler.py +30 -29
  379. infrahub/storage.py +2 -4
  380. infrahub/task_manager/constants.py +1 -1
  381. infrahub/task_manager/event.py +275 -0
  382. infrahub/task_manager/models.py +147 -3
  383. infrahub/task_manager/task.py +1 -1
  384. infrahub/tasks/artifact.py +20 -21
  385. infrahub/tasks/registry.py +1 -1
  386. infrahub/telemetry/__init__.py +0 -0
  387. infrahub/telemetry/constants.py +9 -0
  388. infrahub/telemetry/database.py +86 -0
  389. infrahub/telemetry/models.py +65 -0
  390. infrahub/telemetry/task_manager.py +77 -0
  391. infrahub/telemetry/tasks.py +119 -0
  392. infrahub/telemetry/utils.py +11 -0
  393. infrahub/transformations/tasks.py +5 -7
  394. infrahub/trigger/__init__.py +0 -0
  395. infrahub/trigger/catalogue.py +13 -0
  396. infrahub/trigger/constants.py +1 -0
  397. infrahub/trigger/models.py +118 -0
  398. infrahub/trigger/setup.py +90 -0
  399. infrahub/trigger/tasks.py +36 -0
  400. infrahub/types.py +1 -1
  401. infrahub/utils.py +12 -2
  402. infrahub/validators/__init__.py +0 -0
  403. infrahub/validators/events.py +42 -0
  404. infrahub/validators/tasks.py +41 -0
  405. infrahub/webhook/gather.py +17 -0
  406. infrahub/webhook/models.py +180 -42
  407. infrahub/webhook/tasks.py +149 -203
  408. infrahub/webhook/triggers.py +44 -0
  409. infrahub/workers/infrahub_async.py +38 -27
  410. infrahub/workers/utils.py +63 -0
  411. infrahub/workflows/catalogue.py +98 -71
  412. infrahub/workflows/initialization.py +12 -8
  413. infrahub/workflows/models.py +29 -5
  414. infrahub/workflows/utils.py +11 -2
  415. infrahub_sdk/client.py +19 -0
  416. infrahub_sdk/context.py +13 -0
  417. infrahub_sdk/ctl/branch.py +3 -2
  418. infrahub_sdk/ctl/utils.py +0 -16
  419. infrahub_sdk/exceptions.py +6 -0
  420. infrahub_sdk/generator.py +3 -0
  421. infrahub_sdk/graphql.py +45 -13
  422. infrahub_sdk/node.py +66 -20
  423. infrahub_sdk/protocols.py +21 -8
  424. infrahub_sdk/protocols_base.py +32 -11
  425. infrahub_sdk/schema/__init__.py +14 -2
  426. infrahub_sdk/schema/main.py +7 -0
  427. infrahub_sdk/task/__init__.py +11 -0
  428. infrahub_sdk/task/constants.py +3 -0
  429. infrahub_sdk/task/exceptions.py +25 -0
  430. infrahub_sdk/task/manager.py +551 -0
  431. infrahub_sdk/task/models.py +74 -0
  432. infrahub_sdk/timestamp.py +142 -33
  433. infrahub_sdk/utils.py +29 -1
  434. {infrahub_server-1.1.9.dist-info → infrahub_server-1.2.0.dist-info}/METADATA +8 -6
  435. infrahub_server-1.2.0.dist-info/RECORD +746 -0
  436. infrahub_testcontainers/container.py +5 -6
  437. infrahub_testcontainers/docker-compose.test.yml +2 -2
  438. infrahub_testcontainers/helpers.py +5 -1
  439. infrahub/core/branch/constants.py +0 -2
  440. infrahub/core/schema/definitions/core.py +0 -2275
  441. infrahub/graphql/query.py +0 -52
  442. infrahub/message_bus/messages/check_repository_checkdefinition.py +0 -20
  443. infrahub/message_bus/messages/check_repository_mergeconflicts.py +0 -16
  444. infrahub/message_bus/messages/check_repository_usercheck.py +0 -26
  445. infrahub/message_bus/messages/event_branch_create.py +0 -11
  446. infrahub/message_bus/messages/event_branch_delete.py +0 -11
  447. infrahub/message_bus/messages/event_branch_rebased.py +0 -9
  448. infrahub/message_bus/messages/event_node_mutated.py +0 -15
  449. infrahub/message_bus/messages/event_schema_update.py +0 -9
  450. infrahub/message_bus/messages/request_artifactdefinition_check.py +0 -17
  451. infrahub/message_bus/messages/request_repository_checks.py +0 -12
  452. infrahub/message_bus/messages/request_repository_userchecks.py +0 -18
  453. infrahub/message_bus/operations/check/repository.py +0 -293
  454. infrahub/message_bus/operations/event/node.py +0 -20
  455. infrahub/message_bus/operations/event/schema.py +0 -17
  456. infrahub/message_bus/operations/requests/artifact_definition.py +0 -148
  457. infrahub/message_bus/operations/requests/repository.py +0 -133
  458. infrahub/schema/constants.py +0 -1
  459. infrahub/schema/tasks.py +0 -76
  460. infrahub/services/adapters/database/__init__.py +0 -9
  461. infrahub/tasks/telemetry.py +0 -127
  462. infrahub/webhook/constants.py +0 -3
  463. infrahub_server-1.1.9.dist-info/RECORD +0 -688
  464. /infrahub/{schema → artifacts}/__init__.py +0 -0
  465. {infrahub_server-1.1.9.dist-info → infrahub_server-1.2.0.dist-info}/LICENSE.txt +0 -0
  466. {infrahub_server-1.1.9.dist-info → infrahub_server-1.2.0.dist-info}/WHEEL +0 -0
  467. {infrahub_server-1.1.9.dist-info → infrahub_server-1.2.0.dist-info}/entry_points.txt +0 -0
@@ -3,8 +3,8 @@ from __future__ import annotations
3
3
  import copy
4
4
  import hashlib
5
5
  from collections import defaultdict
6
- from itertools import chain
7
- from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, Union
6
+ from itertools import chain, combinations
7
+ from typing import Any
8
8
 
9
9
  from infrahub_sdk.topological_sort import DependencyCycleExistsError, topological_sort
10
10
  from infrahub_sdk.utils import compare_lists, deep_merge_dict, duplicates, intersection
@@ -12,6 +12,8 @@ from typing_extensions import Self
12
12
 
13
13
  from infrahub.computed_attribute.constants import VALID_KINDS as VALID_COMPUTED_ATTRIBUTE_KINDS
14
14
  from infrahub.core.constants import (
15
+ OBJECT_TEMPLATE_NAME_ATTR,
16
+ OBJECT_TEMPLATE_RELATIONSHIP_NAME,
15
17
  RESERVED_ATTR_GEN_NAMES,
16
18
  RESERVED_ATTR_REL_NAMES,
17
19
  RESTRICTED_NAMESPACES,
@@ -43,6 +45,7 @@ from infrahub.core.schema import (
43
45
  RelationshipSchema,
44
46
  SchemaAttributePath,
45
47
  SchemaRoot,
48
+ TemplateSchema,
46
49
  )
47
50
  from infrahub.core.schema.definitions.core import core_profile_schema_definition
48
51
  from infrahub.core.validators import CONSTRAINT_VALIDATOR_MAP
@@ -53,17 +56,12 @@ from infrahub.types import ATTRIBUTE_TYPES
53
56
  from infrahub.utils import format_label
54
57
  from infrahub.visuals import select_color
55
58
 
59
+ from ..constants.schema import PARENT_CHILD_IDENTIFIER
56
60
  from .constants import INTERNAL_SCHEMA_NODE_KINDS, SchemaNamespace
57
61
  from .schema_branch_computed import ComputedAttributes
58
62
 
59
63
  log = get_logger()
60
64
 
61
- if TYPE_CHECKING:
62
- from pydantic import ValidationInfo
63
-
64
-
65
- # pylint: disable=redefined-builtin,too-many-public-methods,too-many-lines
66
-
67
65
 
68
66
  class SchemaBranch:
69
67
  def __init__(
@@ -73,28 +71,26 @@ class SchemaBranch:
73
71
  data: dict[str, dict[str, str]] | None = None,
74
72
  computed_attributes: ComputedAttributes | None = None,
75
73
  ):
76
- self._cache: dict[str, Union[NodeSchema, GenericSchema]] = cache
74
+ self._cache: dict[str, NodeSchema | GenericSchema] = cache
77
75
  self.name: str | None = name
78
76
  self.nodes: dict[str, str] = {}
79
77
  self.generics: dict[str, str] = {}
80
78
  self.profiles: dict[str, str] = {}
79
+ self.templates: dict[str, str] = {}
81
80
  self.computed_attributes = computed_attributes or ComputedAttributes()
82
81
 
83
82
  if data:
84
83
  self.nodes = data.get("nodes", {})
85
84
  self.generics = data.get("generics", {})
86
85
  self.profiles = data.get("profiles", {})
86
+ self.templates = data.get("templates", {})
87
87
 
88
88
  @classmethod
89
- def __get_validators__(cls) -> Iterator[Callable[..., Any]]: # noqa: PLW3201
90
- yield cls.validate
91
-
92
- @classmethod
93
- def validate(cls, v: Any, info: ValidationInfo) -> Self: # pylint: disable=unused-argument
94
- if isinstance(v, cls):
95
- return v
96
- if isinstance(v, dict):
97
- return cls.from_dict_schema_object(data=v)
89
+ def validate(cls, data: Any) -> Self: # noqa: ARG003
90
+ if isinstance(data, cls):
91
+ return data
92
+ if isinstance(data, dict):
93
+ return cls.from_dict_schema_object(data=data)
98
94
  raise ValueError("must be a class or a dict")
99
95
 
100
96
  @property
@@ -105,14 +101,22 @@ class SchemaBranch:
105
101
  def generic_names(self) -> list[str]:
106
102
  return list(self.generics.keys())
107
103
 
104
+ @property
105
+ def generic_names_without_templates(self) -> list[str]:
106
+ return [g for g in self.generic_names if not g.startswith("Template")]
107
+
108
108
  @property
109
109
  def profile_names(self) -> list[str]:
110
110
  return list(self.profiles.keys())
111
111
 
112
- def get_all_kind_id_map(self, exclude_profiles: bool = False) -> dict[str, str]:
112
+ @property
113
+ def template_names(self) -> list[str]:
114
+ return list(self.templates.keys())
115
+
116
+ def get_all_kind_id_map(self, nodes_and_generics_only: bool = False) -> dict[str, str]:
113
117
  kind_id_map = {}
114
- if exclude_profiles:
115
- names = self.node_names + self.generic_names
118
+ if nodes_and_generics_only:
119
+ names = self.node_names + self.generic_names_without_templates
116
120
  else:
117
121
  names = self.all_names
118
122
  for name in names:
@@ -124,7 +128,7 @@ class SchemaBranch:
124
128
 
125
129
  @property
126
130
  def all_names(self) -> list[str]:
127
- return self.node_names + self.generic_names + self.profile_names
131
+ return self.node_names + self.generic_names + self.profile_names + self.template_names
128
132
 
129
133
  def get_hash(self) -> str:
130
134
  """Calculate the hash for this objects based on the content of nodes and generics.
@@ -142,13 +146,14 @@ class SchemaBranch:
142
146
  return SchemaBranchHash(main=self.get_hash(), nodes=self.nodes, generics=self.generics)
143
147
 
144
148
  def to_dict(self) -> dict[str, Any]:
145
- return {"nodes": self.nodes, "generics": self.generics, "profiles": self.profiles}
149
+ return {"nodes": self.nodes, "generics": self.generics, "profiles": self.profiles, "templates": self.templates}
146
150
 
147
151
  def to_dict_schema_object(self, duplicate: bool = False) -> dict[str, dict[str, MainSchemaTypes]]:
148
152
  return {
149
153
  "nodes": {name: self.get(name, duplicate=duplicate) for name in self.nodes},
150
154
  "profiles": {name: self.get(name, duplicate=duplicate) for name in self.profiles},
151
155
  "generics": {name: self.get(name, duplicate=duplicate) for name in self.generics},
156
+ "templates": {name: self.get(name, duplicate=duplicate) for name in self.templates},
152
157
  }
153
158
 
154
159
  @classmethod
@@ -157,10 +162,11 @@ class SchemaBranch:
157
162
  "nodes": NodeSchema,
158
163
  "generics": GenericSchema,
159
164
  "profiles": ProfileSchema,
165
+ "templates": TemplateSchema,
160
166
  }
161
167
 
162
168
  cache: dict[str, MainSchemaTypes] = {}
163
- nodes: dict[str, dict[str, str]] = {"nodes": {}, "generics": {}, "profiles": {}}
169
+ nodes: dict[str, dict[str, str]] = {"nodes": {}, "generics": {}, "profiles": {}, "templates": {}}
164
170
 
165
171
  for node_type, node_class in type_mapping.items():
166
172
  for node_name, node_data in data[node_type].items():
@@ -174,8 +180,8 @@ class SchemaBranch:
174
180
 
175
181
  def diff(self, other: SchemaBranch) -> SchemaDiff:
176
182
  # Identify the nodes or generics that have been added or removed
177
- local_kind_id_map = self.get_all_kind_id_map(exclude_profiles=True)
178
- other_kind_id_map = other.get_all_kind_id_map(exclude_profiles=True)
183
+ local_kind_id_map = self.get_all_kind_id_map(nodes_and_generics_only=True)
184
+ other_kind_id_map = other.get_all_kind_id_map(nodes_and_generics_only=True)
179
185
  clean_local_ids = [id for id in local_kind_id_map.values() if id is not None]
180
186
  clean_other_ids = [id for id in other_kind_id_map.values() if id is not None]
181
187
  shared_ids = intersection(list1=clean_local_ids, list2=clean_other_ids)
@@ -229,12 +235,6 @@ class SchemaBranch:
229
235
  other_item = schema.get(name=item_kind)
230
236
  self.set(name=item_kind, schema=other_item)
231
237
 
232
- # for item_kind in local_only:
233
- # if item_kind in self.nodes:
234
- # del self.nodes[item_kind]
235
- # else:
236
- # del self.generics[item_kind]
237
-
238
238
  def validate_node_deletions(self, diff: SchemaDiff) -> None:
239
239
  """Given a diff, check if a deleted node is still used in relationships of other nodes."""
240
240
  removed_schema_names = set(diff.removed.keys())
@@ -256,7 +256,7 @@ class SchemaBranch:
256
256
  result.validate_all(migration_map=MIGRATION_MAP, validator_map=CONSTRAINT_VALIDATOR_MAP)
257
257
  return result
258
258
 
259
- def duplicate(self, name: Optional[str] = None) -> SchemaBranch:
259
+ def duplicate(self, name: str | None = None) -> SchemaBranch:
260
260
  """Duplicate the current object but conserve the same cache."""
261
261
  return self.__class__(
262
262
  name=name,
@@ -281,6 +281,8 @@ class SchemaBranch:
281
281
  self.generics[name] = schema_hash
282
282
  elif "Profile" in schema.__class__.__name__:
283
283
  self.profiles[name] = schema_hash
284
+ elif "Template" in schema.__class__.__name__:
285
+ self.templates[name] = schema_hash
284
286
 
285
287
  return schema_hash
286
288
 
@@ -292,6 +294,7 @@ class SchemaBranch:
292
294
 
293
295
  If duplicate is set to false, the real object will be returned.
294
296
  """
297
+
295
298
  key = None
296
299
  if name in self.nodes:
297
300
  key = self.nodes[name]
@@ -299,6 +302,8 @@ class SchemaBranch:
299
302
  key = self.generics[name]
300
303
  elif name in self.profiles:
301
304
  key = self.profiles[name]
305
+ elif name in self.templates:
306
+ key = self.templates[name]
302
307
 
303
308
  if key and duplicate:
304
309
  return self._cache[key].duplicate()
@@ -330,6 +335,13 @@ class SchemaBranch:
330
335
  raise ValueError(f"{name!r} is not of type ProfileSchema")
331
336
  return item
332
337
 
338
+ def get_template(self, name: str, duplicate: bool = True) -> TemplateSchema:
339
+ """Access a specific TemplateSchema, defined by its kind."""
340
+ item = self.get(name=name, duplicate=duplicate)
341
+ if not isinstance(item, TemplateSchema):
342
+ raise ValueError(f"{name!r} is not of type TemplateSchema")
343
+ return item
344
+
333
345
  def delete(self, name: str) -> None:
334
346
  if name in self.nodes:
335
347
  del self.nodes[name]
@@ -337,6 +349,8 @@ class SchemaBranch:
337
349
  del self.generics[name]
338
350
  elif name in self.profiles:
339
351
  del self.profiles[name]
352
+ elif name in self.templates:
353
+ del self.templates[name]
340
354
  else:
341
355
  raise SchemaNotFoundError(
342
356
  branch_name=self.name, identifier=name, message=f"Unable to find the schema {name!r} in the registry"
@@ -412,7 +426,7 @@ class SchemaBranch:
412
426
  return list(namespaces.values())
413
427
 
414
428
  def get_schemas_for_namespaces(
415
- self, namespaces: Optional[list[str]] = None, include_internal: bool = False
429
+ self, namespaces: list[str] | None = None, include_internal: bool = False
416
430
  ) -> list[MainSchemaTypes]:
417
431
  """Retrive everything in a single dictionary."""
418
432
  all_schemas = self.get_all(include_internal=include_internal, duplicate=False)
@@ -429,12 +443,12 @@ class SchemaBranch:
429
443
  nodes.append(self.get(name=node_name, duplicate=True))
430
444
  return nodes
431
445
 
432
- def generate_fields_for_display_label(self, name: str) -> Optional[dict]:
446
+ def generate_fields_for_display_label(self, name: str) -> dict | None:
433
447
  node = self.get(name=name, duplicate=False)
434
- if isinstance(node, (NodeSchema, ProfileSchema)):
448
+ if isinstance(node, NodeSchema | ProfileSchema | TemplateSchema):
435
449
  return node.generate_fields_for_display_label()
436
450
 
437
- fields: dict[str, Union[str, None, dict[str, None]]] = {}
451
+ fields: dict[str, str | None | dict[str, None]] = {}
438
452
  if isinstance(node, GenericSchema):
439
453
  for child_node_name in node.used_by:
440
454
  child_node = self.get(name=child_node_name, duplicate=False)
@@ -490,6 +504,8 @@ class SchemaBranch:
490
504
  self.process_inheritance()
491
505
  self.process_hierarchy()
492
506
  self.process_branch_support()
507
+ self.manage_object_template_schemas()
508
+ self.manage_object_template_relationships()
493
509
  self.manage_profile_schemas()
494
510
  self.manage_profile_relationships()
495
511
  self.add_hierarchy_generic()
@@ -520,6 +536,9 @@ class SchemaBranch:
520
536
  self.process_relationships()
521
537
  self.process_human_friendly_id()
522
538
 
539
+ def _generate_identifier_string(self, node_kind: str, peer_kind: str) -> str:
540
+ return "__".join(sorted([node_kind, peer_kind])).lower()
541
+
523
542
  def generate_identifiers(self) -> None:
524
543
  """Generate the identifier for all relationships if it's not already present."""
525
544
  for name in self.all_names:
@@ -532,7 +551,7 @@ class SchemaBranch:
532
551
  for rel in node.relationships:
533
552
  if rel.identifier:
534
553
  continue
535
- rel.identifier = str("__".join(sorted([node.kind, rel.peer]))).lower()
554
+ rel.identifier = self._generate_identifier_string(node.kind, rel.peer)
536
555
  self.set(name=name, schema=node)
537
556
 
538
557
  def validate_identifiers(self) -> None:
@@ -594,7 +613,7 @@ class SchemaBranch:
594
613
  node_schema: BaseNodeSchema,
595
614
  path: str,
596
615
  allowed_path_types: SchemaElementPathType,
597
- element_name: Optional[str] = None,
616
+ element_name: str | None = None,
598
617
  ) -> SchemaAttributePath:
599
618
  error_header = f"{node_schema.kind}"
600
619
  error_header += f".{element_name}" if element_name else ""
@@ -660,7 +679,7 @@ class SchemaBranch:
660
679
  return schema_attribute_path
661
680
 
662
681
  def sync_uniqueness_constraints_and_unique_attributes(self) -> None:
663
- for name in self.generic_names + self.node_names:
682
+ for name in self.generic_names_without_templates + self.node_names:
664
683
  node_schema = self.get(name=name, duplicate=False)
665
684
 
666
685
  if not node_schema.unique_attributes and not node_schema.uniqueness_constraints:
@@ -696,10 +715,12 @@ class SchemaBranch:
696
715
  for attr_name in attrs_to_make_unique:
697
716
  attr_schema = node_schema.get_attribute(name=attr_name)
698
717
  attr_schema.unique = True
718
+
699
719
  if attrs_to_add_to_constraints:
700
720
  node_schema.uniqueness_constraints = (node_schema.uniqueness_constraints or []) + sorted(
701
721
  [[f"{attr_name}__value"] for attr_name in attrs_to_add_to_constraints]
702
722
  )
723
+
703
724
  self.set(name=name, schema=node_schema)
704
725
 
705
726
  def validate_uniqueness_constraints(self) -> None:
@@ -775,7 +796,7 @@ class SchemaBranch:
775
796
  )
776
797
 
777
798
  def validate_default_values(self) -> None:
778
- for name in self.generic_names + self.node_names:
799
+ for name in self.generic_names_without_templates + self.node_names:
779
800
  node_schema = self.get(name=name, duplicate=False)
780
801
  for node_attr in node_schema.local_attributes:
781
802
  if node_attr.default_value is None:
@@ -794,15 +815,30 @@ class SchemaBranch:
794
815
  f"{node_schema.namespace}{node_schema.name}: default value {exc.message}"
795
816
  ) from exc
796
817
 
818
+ def _is_attr_combination_unique(self, attrs_paths: list[str], uniqueness_constraints: list[list[str]]) -> bool:
819
+ """
820
+ Return whether at least one combination of any length of `attrs_paths` is equal to a uniqueness constraint.
821
+ """
822
+
823
+ unique_constraint_group_sets = [set(ucg) for ucg in uniqueness_constraints]
824
+ for i in range(1, len(attrs_paths) + 1):
825
+ for attr_combo in combinations(attrs_paths, i):
826
+ if any(ucg == set(attr_combo) for ucg in unique_constraint_group_sets):
827
+ return True
828
+ return False
829
+
797
830
  def validate_human_friendly_id(self) -> None:
798
- for name in self.generic_names + self.node_names:
831
+ for name in self.generic_names_without_templates + self.node_names:
799
832
  node_schema = self.get(name=name, duplicate=False)
800
- hf_attr_names = set()
801
833
 
802
834
  if not node_schema.human_friendly_id:
803
835
  continue
804
836
 
805
837
  allowed_types = SchemaElementPathType.ATTR_WITH_PROP | SchemaElementPathType.REL_ONE_MANDATORY_ATTR
838
+
839
+ # Mapping relationship identifiers -> list of attributes paths
840
+ rel_schemas_to_paths: dict[str, tuple[MainSchemaTypes, list[str]]] = {}
841
+
806
842
  for hfid_path in node_schema.human_friendly_id:
807
843
  schema_path = self.validate_schema_path(
808
844
  node_schema=node_schema,
@@ -811,12 +847,23 @@ class SchemaBranch:
811
847
  element_name="human_friendly_id",
812
848
  )
813
849
 
814
- if schema_path.is_type_attribute:
815
- hf_attr_names.add(schema_path.attribute_schema.name)
850
+ if schema_path.is_type_relationship:
851
+ # Construct the name without relationship prefix to match with how it would be defined in peer schema uniqueness constraint
852
+ rel_identifier = schema_path.relationship_schema.identifier
853
+ if rel_identifier not in rel_schemas_to_paths:
854
+ rel_schemas_to_paths[rel_identifier] = (schema_path.related_schema, [])
855
+ rel_schemas_to_paths[rel_identifier][1].append(schema_path.attribute_path_as_str)
856
+
857
+ # For every relationship referred within hfid, check whether the combination of attributes is unique is the peer schema node
858
+ for related_schema, attrs_paths in rel_schemas_to_paths.values():
859
+ if not self._is_attr_combination_unique(attrs_paths, related_schema.uniqueness_constraints):
860
+ raise ValidationError(
861
+ f"HFID of {node_schema.kind} refers peer {related_schema.kind} with a non-unique combination of attributes {attrs_paths}"
862
+ )
816
863
 
817
864
  def validate_required_relationships(self) -> None:
818
865
  reverse_dependency_map: dict[str, set[str]] = {}
819
- for name in self.node_names + self.generic_names:
866
+ for name in self.node_names + self.generic_names_without_templates:
820
867
  node_schema = self.get(name=name, duplicate=False)
821
868
  for relationship_schema in node_schema.relationships:
822
869
  if relationship_schema.optional:
@@ -834,7 +881,7 @@ class SchemaBranch:
834
881
  def validate_parent_component(self) -> None:
835
882
  # {parent_kind: {component_kind_1, component_kind_2, ...}}
836
883
  dependency_map: dict[str, set[str]] = defaultdict(set)
837
- for name in self.generic_names + self.node_names:
884
+ for name in self.generic_names_without_templates + self.node_names:
838
885
  node_schema = self.get(name=name, duplicate=False)
839
886
 
840
887
  parent_relationships: list[RelationshipSchema] = []
@@ -873,7 +920,7 @@ class SchemaBranch:
873
920
  raise ValueError(f"Cycles exist among parents and components in schema: {exc.get_cycle_strings()}") from exc
874
921
 
875
922
  def _validate_parents_one_schema(
876
- self, node_schema: Union[NodeSchema, GenericSchema], parent_relationships: list[RelationshipSchema]
923
+ self, node_schema: NodeSchema | GenericSchema, parent_relationships: list[RelationshipSchema]
877
924
  ) -> None:
878
925
  if not parent_relationships:
879
926
  return
@@ -934,9 +981,9 @@ class SchemaBranch:
934
981
  for rel in node.relationships:
935
982
  if rel.peer in [InfrahubKind.GENERICGROUP]:
936
983
  continue
937
- if not self.has(rel.peer):
984
+ if not self.has(rel.peer) or self.get(rel.peer, duplicate=False).state == HashableModelState.ABSENT:
938
985
  raise ValueError(
939
- f"{node.kind}: Relationship {rel.name!r} is referencing an invalid peer {rel.peer!r}"
986
+ f"{node.kind}: Relationship {rel.name!r} is referring an invalid peer {rel.peer!r}"
940
987
  ) from None
941
988
 
942
989
  def validate_computed_attributes(self) -> None:
@@ -1103,7 +1150,7 @@ class SchemaBranch:
1103
1150
  for name in self.all_names:
1104
1151
  node = self.get(name=name, duplicate=False)
1105
1152
 
1106
- schema_to_update: Optional[Union[NodeSchema, GenericSchema]] = None
1153
+ schema_to_update: NodeSchema | GenericSchema | None = None
1107
1154
  for relationship in node.relationships:
1108
1155
  if relationship.on_delete is not None:
1109
1156
  continue
@@ -1120,49 +1167,57 @@ class SchemaBranch:
1120
1167
  self.set(name=schema_to_update.kind, schema=schema_to_update)
1121
1168
 
1122
1169
  def process_human_friendly_id(self) -> None:
1123
- for name in self.generic_names + self.node_names:
1170
+ for name in self.generic_names_without_templates + self.node_names:
1124
1171
  node = self.get(name=name, duplicate=False)
1125
1172
 
1126
1173
  # If human_friendly_id IS NOT defined
1127
1174
  # but some the model has some unique attribute, we generate a human_friendly_id
1128
1175
  # If human_friendly_id IS defined
1129
1176
  # but no unique attributes and no uniquess constraints, we add a uniqueness_constraint
1130
- if not node.human_friendly_id and node.unique_attributes:
1131
- for attr in node.unique_attributes:
1132
- node = self.get(name=name, duplicate=True)
1133
- node.human_friendly_id = [f"{attr.name}__value"]
1134
- self.set(name=node.kind, schema=node)
1135
- break
1136
- continue
1137
-
1138
- # if no human_friendly_id and a uniqueness_constraint with a single attribute exists
1139
- # then use that attribute as the human_friendly_id
1140
- if not node.human_friendly_id and node.uniqueness_constraints:
1141
- for constraint_paths in node.uniqueness_constraints:
1142
- if len(constraint_paths) > 1:
1143
- continue
1144
- constraint_path = constraint_paths[0]
1145
- schema_path = node.parse_schema_path(path=constraint_path, schema=node)
1146
- if (
1147
- schema_path.is_type_attribute
1148
- and schema_path.attribute_property_name == "value"
1149
- and schema_path.attribute_schema
1150
- ):
1177
+ if not node.human_friendly_id:
1178
+ if node.unique_attributes:
1179
+ for attr in node.unique_attributes:
1151
1180
  node = self.get(name=name, duplicate=True)
1152
- node.human_friendly_id = [f"{schema_path.attribute_schema.name}__value"]
1181
+ node.human_friendly_id = [f"{attr.name}__value"]
1153
1182
  self.set(name=node.kind, schema=node)
1154
1183
  break
1184
+ continue
1155
1185
 
1156
- if node.human_friendly_id and not node.uniqueness_constraints:
1157
- uniqueness_constraints: list[str] = []
1186
+ # if no human_friendly_id and a uniqueness_constraint with a single attribute exists
1187
+ # then use that attribute as the human_friendly_id
1188
+ if node.uniqueness_constraints:
1189
+ for constraint_paths in node.uniqueness_constraints:
1190
+ if len(constraint_paths) > 1:
1191
+ continue
1192
+ constraint_path = constraint_paths[0]
1193
+ schema_path = node.parse_schema_path(path=constraint_path, schema=node)
1194
+ if (
1195
+ schema_path.is_type_attribute
1196
+ and schema_path.attribute_property_name == "value"
1197
+ and schema_path.attribute_schema
1198
+ ):
1199
+ node = self.get(name=name, duplicate=True)
1200
+ node.human_friendly_id = [f"{schema_path.attribute_schema.name}__value"]
1201
+ self.set(name=node.kind, schema=node)
1202
+ break
1203
+
1204
+ # Add hfid to uniqueness constraint
1205
+ if node.human_friendly_id:
1206
+ uniqueness_constraint: list[str] = []
1158
1207
  for item in node.human_friendly_id:
1159
1208
  schema_attribute_path = node.parse_schema_path(path=item, schema=self)
1160
1209
  if schema_attribute_path.is_type_attribute:
1161
- uniqueness_constraints.append(item)
1210
+ uniqueness_constraint.append(item)
1162
1211
  elif schema_attribute_path.is_type_relationship:
1163
- uniqueness_constraints.append(schema_attribute_path.relationship_schema.name)
1212
+ uniqueness_constraint.append(schema_attribute_path.relationship_schema.name)
1164
1213
 
1165
- node.uniqueness_constraints = [uniqueness_constraints]
1214
+ node = self.get(name=name, duplicate=True)
1215
+ # Make sure there is no duplicate regarding generics values.
1216
+ if node.uniqueness_constraints:
1217
+ if uniqueness_constraint not in node.uniqueness_constraints:
1218
+ node.uniqueness_constraints.append(uniqueness_constraint)
1219
+ else:
1220
+ node.uniqueness_constraints = [uniqueness_constraint]
1166
1221
  self.set(name=node.kind, schema=node)
1167
1222
 
1168
1223
  def process_hierarchy(self) -> None:
@@ -1267,7 +1322,6 @@ class SchemaBranch:
1267
1322
 
1268
1323
  if either node on a relationship support branch, the relationship must be branch aware.
1269
1324
  """
1270
- # pylint: disable=too-many-branches
1271
1325
 
1272
1326
  for name in self.all_names:
1273
1327
  node = self.get(name=name, duplicate=False)
@@ -1329,7 +1383,6 @@ class SchemaBranch:
1329
1383
 
1330
1384
  def process_cardinality_counts(self) -> None:
1331
1385
  """Ensure that all relationships with a cardinality of ONE have a min_count and max_count of 1."""
1332
- # pylint: disable=too-many-branches
1333
1386
 
1334
1387
  for name in self.all_names:
1335
1388
  node = self.get(name=name, duplicate=False)
@@ -1416,7 +1469,6 @@ class SchemaBranch:
1416
1469
  self.set(name=name, schema=node)
1417
1470
 
1418
1471
  def cleanup_inherited_elements(self) -> None:
1419
- # pylint: disable=too-many-branches
1420
1472
  for name in self.node_names:
1421
1473
  node = self.get_node(name=name, duplicate=False)
1422
1474
 
@@ -1512,7 +1564,7 @@ class SchemaBranch:
1512
1564
  def _get_hierarchy_child_rel(self, peer: str, hierarchical: str | None, read_only: bool) -> RelationshipSchema:
1513
1565
  return RelationshipSchema(
1514
1566
  name="children",
1515
- identifier="parent__child",
1567
+ identifier=PARENT_CHILD_IDENTIFIER,
1516
1568
  peer=peer,
1517
1569
  kind=RelationshipKind.HIERARCHY,
1518
1570
  cardinality=RelationshipCardinality.MANY,
@@ -1527,7 +1579,7 @@ class SchemaBranch:
1527
1579
  ) -> RelationshipSchema:
1528
1580
  return RelationshipSchema(
1529
1581
  name="parent",
1530
- identifier="parent__child",
1582
+ identifier=PARENT_CHILD_IDENTIFIER,
1531
1583
  peer=peer,
1532
1584
  kind=RelationshipKind.HIERARCHY,
1533
1585
  cardinality=RelationshipCardinality.ONE,
@@ -1606,11 +1658,10 @@ class SchemaBranch:
1606
1658
  if not self.has(name=InfrahubKind.PROFILE):
1607
1659
  # TODO: This logic is actually only for testing purposes as since 1.0.9 CoreProfile is loaded in db.
1608
1660
  # Ideally, we would remove this and instead load CoreProfile properly within tests.
1609
- core_profile_schema = GenericSchema(**core_profile_schema_definition)
1610
- self.set(name=core_profile_schema.kind, schema=core_profile_schema)
1661
+ self.set(name=core_profile_schema_definition.kind, schema=core_profile_schema_definition)
1611
1662
 
1612
1663
  profile_schema_kinds = set()
1613
- for node_name in self.node_names + self.generic_names:
1664
+ for node_name in self.node_names + self.generic_names_without_templates:
1614
1665
  node = self.get(name=node_name, duplicate=False)
1615
1666
  if (
1616
1667
  node.namespace in RESTRICTED_NAMESPACES
@@ -1642,7 +1693,7 @@ class SchemaBranch:
1642
1693
 
1643
1694
  if new_used_by_profile:
1644
1695
  core_profile_schema = self.get(name=InfrahubKind.PROFILE, duplicate=True)
1645
- core_profile_schema.used_by = sorted(list(profile_schema_kinds))
1696
+ core_profile_schema.used_by = sorted(profile_schema_kinds)
1646
1697
  self.set(name=InfrahubKind.PROFILE, schema=core_profile_schema)
1647
1698
 
1648
1699
  if self.has(name=InfrahubKind.NODE):
@@ -1653,7 +1704,7 @@ class SchemaBranch:
1653
1704
  if new_used_by_node:
1654
1705
  core_node_schema = self.get(name=InfrahubKind.NODE, duplicate=True)
1655
1706
  updated_used_by_node = set(chain(profile_schema_kinds, set(core_node_schema.used_by)))
1656
- core_node_schema.used_by = sorted(list(updated_used_by_node))
1707
+ core_node_schema.used_by = sorted(updated_used_by_node)
1657
1708
  self.set(name=InfrahubKind.NODE, schema=core_node_schema)
1658
1709
 
1659
1710
  def manage_profile_relationships(self) -> None:
@@ -1746,3 +1797,286 @@ class SchemaBranch:
1746
1797
  profile.attributes.append(attr)
1747
1798
 
1748
1799
  return profile
1800
+
1801
+ def _get_object_template_kind(self, node_kind: str) -> str:
1802
+ return f"Template{node_kind}"
1803
+
1804
+ def manage_object_template_relationships(self) -> None:
1805
+ """Add an `object_template` relationship to all nodes that can be created from object templates.
1806
+
1807
+ This relationship allows to record from which template an object has been created.
1808
+ """
1809
+ for node_name in self.node_names + self.generic_names:
1810
+ node = self.get(name=node_name, duplicate=False)
1811
+
1812
+ if (
1813
+ node.namespace in RESTRICTED_NAMESPACES
1814
+ or not node.generate_template
1815
+ or node.state == HashableModelState.ABSENT
1816
+ ):
1817
+ continue
1818
+
1819
+ template_rel_settings: dict[str, Any] = {
1820
+ "name": OBJECT_TEMPLATE_RELATIONSHIP_NAME,
1821
+ "identifier": "node__objecttemplate",
1822
+ "peer": self._get_object_template_kind(node.kind),
1823
+ "kind": RelationshipKind.TEMPLATE,
1824
+ "cardinality": RelationshipCardinality.ONE,
1825
+ "branch": BranchSupportType.AWARE,
1826
+ "order_weight": 1,
1827
+ }
1828
+
1829
+ # Add relationship between node and template
1830
+ if OBJECT_TEMPLATE_RELATIONSHIP_NAME not in node.relationship_names:
1831
+ node_schema = self.get(name=node_name, duplicate=True)
1832
+
1833
+ node_schema.relationships.append(RelationshipSchema(**template_rel_settings))
1834
+ self.set(name=node_name, schema=node_schema)
1835
+ else:
1836
+ has_changes: bool = False
1837
+ rel_template = node.get_relationship(name=OBJECT_TEMPLATE_RELATIONSHIP_NAME)
1838
+ for name, value in template_rel_settings.items():
1839
+ if getattr(rel_template, name) != value:
1840
+ has_changes = True
1841
+
1842
+ if not has_changes:
1843
+ continue
1844
+
1845
+ node_schema = self.get(name=node_name, duplicate=True)
1846
+ rel_template = node_schema.get_relationship(name=OBJECT_TEMPLATE_RELATIONSHIP_NAME)
1847
+ for name, value in template_rel_settings.items():
1848
+ if getattr(rel_template, name) != value:
1849
+ setattr(rel_template, name, value)
1850
+
1851
+ self.set(name=node_name, schema=node_schema)
1852
+
1853
+ def add_relationships_to_template(self, node: NodeSchema) -> None:
1854
+ template_schema = self.get(name=self._get_object_template_kind(node_kind=node.kind), duplicate=False)
1855
+ if template_schema.is_generic_schema:
1856
+ return
1857
+
1858
+ # Remove previous relationships to account for new ones
1859
+ template_schema.relationships = [
1860
+ r for r in template_schema.relationships if r.kind == RelationshipKind.TEMPLATE
1861
+ ]
1862
+ # Tell if the user explicitely requested this template
1863
+ is_autogenerated_subtemplate = node.generate_template is False
1864
+
1865
+ for relationship in node.relationships:
1866
+ if relationship.peer in [InfrahubKind.GENERICGROUP, InfrahubKind.PROFILE] or relationship.kind not in [
1867
+ RelationshipKind.COMPONENT,
1868
+ RelationshipKind.PARENT,
1869
+ RelationshipKind.ATTRIBUTE,
1870
+ RelationshipKind.GENERIC,
1871
+ ]:
1872
+ continue
1873
+
1874
+ rel_template_peer = (
1875
+ self._get_object_template_kind(node_kind=relationship.peer)
1876
+ if relationship.kind not in [RelationshipKind.ATTRIBUTE, RelationshipKind.GENERIC]
1877
+ else relationship.peer
1878
+ )
1879
+ template_schema.relationships.append(
1880
+ RelationshipSchema(
1881
+ name=relationship.name,
1882
+ peer=rel_template_peer,
1883
+ kind=relationship.kind,
1884
+ optional=relationship.optional
1885
+ if is_autogenerated_subtemplate
1886
+ else relationship.kind != RelationshipKind.PARENT,
1887
+ cardinality=relationship.cardinality,
1888
+ direction=relationship.direction,
1889
+ branch=relationship.branch,
1890
+ identifier=f"template_{relationship.identifier}"
1891
+ if relationship.identifier
1892
+ else self._generate_identifier_string(template_schema.kind, rel_template_peer),
1893
+ min_count=relationship.min_count,
1894
+ max_count=relationship.max_count,
1895
+ label=f"{relationship.name} template".title()
1896
+ if relationship.kind in [RelationshipKind.COMPONENT, RelationshipKind.PARENT]
1897
+ else relationship.name.title(),
1898
+ )
1899
+ )
1900
+
1901
+ if relationship.kind == RelationshipKind.PARENT:
1902
+ template_schema.human_friendly_id = [
1903
+ f"{relationship.name}__template_name__value"
1904
+ ] + template_schema.human_friendly_id
1905
+ template_schema.uniqueness_constraints[0].append(relationship.name)
1906
+
1907
+ def generate_object_template_from_node(
1908
+ self, node: NodeSchema | GenericSchema, need_templates: set[NodeSchema | GenericSchema]
1909
+ ) -> TemplateSchema | GenericSchema:
1910
+ # Tell if the user explicitely requested this template
1911
+ is_autogenerated_subtemplate = node.generate_template is False
1912
+
1913
+ core_template_schema = (
1914
+ self.get(name=InfrahubKind.OBJECTCOMPONENTTEMPLATE, duplicate=False)
1915
+ if is_autogenerated_subtemplate
1916
+ else self.get(name=InfrahubKind.OBJECTTEMPLATE, duplicate=False)
1917
+ )
1918
+ core_name_attr = core_template_schema.get_attribute(name=OBJECT_TEMPLATE_NAME_ATTR)
1919
+ template_name_attr = AttributeSchema(
1920
+ **core_name_attr.model_dump(exclude=["id", "inherited"]),
1921
+ )
1922
+ template_name_attr.branch = node.branch
1923
+
1924
+ template: TemplateSchema | GenericSchema
1925
+ need_template_kinds = [n.kind for n in need_templates]
1926
+
1927
+ if node.is_generic_schema:
1928
+ # When needing a template for a generic, we generate an empty shell mostly to make sure that schemas (including the GraphQL one) will
1929
+ # look right. We don't really care about applying inheritance of fields as it was already processed and actual templates will have the
1930
+ # correct attributes and relationships
1931
+ template = GenericSchema(
1932
+ name=node.kind,
1933
+ namespace="Template",
1934
+ label=f"Generic object template {node.label}",
1935
+ description=f"Generic object template for generic {node.kind}",
1936
+ generate_profile=False,
1937
+ branch=node.branch,
1938
+ include_in_menu=False,
1939
+ attributes=[template_name_attr],
1940
+ )
1941
+
1942
+ for used in node.used_by:
1943
+ if used in need_template_kinds:
1944
+ template.used_by.append(self._get_object_template_kind(node_kind=used))
1945
+
1946
+ return template
1947
+
1948
+ template = TemplateSchema(
1949
+ name=node.kind,
1950
+ namespace="Template",
1951
+ label=f"Object template {node.label}",
1952
+ description=f"Object template for {node.kind}",
1953
+ branch=node.branch,
1954
+ include_in_menu=False,
1955
+ display_labels=["template_name__value"],
1956
+ human_friendly_id=["template_name__value"],
1957
+ uniqueness_constraints=[["template_name__value"]],
1958
+ inherit_from=[InfrahubKind.LINEAGESOURCE, InfrahubKind.NODE, core_template_schema.kind],
1959
+ default_filter="template_name__value",
1960
+ attributes=[template_name_attr],
1961
+ relationships=[
1962
+ RelationshipSchema(
1963
+ name="related_nodes",
1964
+ identifier="node__objecttemplate",
1965
+ peer=node.kind,
1966
+ kind=RelationshipKind.TEMPLATE,
1967
+ cardinality=RelationshipCardinality.MANY,
1968
+ branch=BranchSupportType.AWARE,
1969
+ )
1970
+ ],
1971
+ )
1972
+
1973
+ for inherited in node.inherit_from:
1974
+ if inherited in need_template_kinds:
1975
+ template.inherit_from.append(self._get_object_template_kind(node_kind=inherited))
1976
+
1977
+ for node_attr in node.attributes:
1978
+ if node_attr.unique or node_attr.read_only:
1979
+ continue
1980
+
1981
+ attr = AttributeSchema(
1982
+ optional=node_attr.optional if is_autogenerated_subtemplate else True,
1983
+ **node_attr.model_dump(exclude=["id", "unique", "optional", "read_only", "inherited"]),
1984
+ )
1985
+ template.attributes.append(attr)
1986
+
1987
+ return template
1988
+
1989
+ def identify_required_object_templates(
1990
+ self, node_schema: NodeSchema | GenericSchema, identified: set[NodeSchema | GenericSchema]
1991
+ ) -> set[NodeSchema]:
1992
+ """Identify all templates required to turn a given node into a template."""
1993
+ if node_schema in identified or node_schema.state == HashableModelState.ABSENT:
1994
+ return identified
1995
+
1996
+ identified.add(node_schema)
1997
+
1998
+ for relationship in node_schema.relationships:
1999
+ if (
2000
+ relationship.peer in [InfrahubKind.GENERICGROUP, InfrahubKind.PROFILE]
2001
+ or (relationship.kind == RelationshipKind.PARENT and node_schema.generate_template)
2002
+ or relationship.kind not in [RelationshipKind.PARENT, RelationshipKind.COMPONENT]
2003
+ ):
2004
+ continue
2005
+
2006
+ peer_schema = self.get(name=relationship.peer, duplicate=False)
2007
+ if not isinstance(peer_schema, NodeSchema | GenericSchema) or peer_schema in identified:
2008
+ continue
2009
+ # In a context of a generic, we won't be able to create objects out of it, so any kind of nodes implementing the generic is a valid
2010
+ # option, we therefore need to have a template for each of those nodes
2011
+ if isinstance(peer_schema, GenericSchema) and peer_schema.used_by:
2012
+ if relationship.kind != RelationshipKind.PARENT or not any(
2013
+ u in [i.kind for i in identified] for u in peer_schema.used_by
2014
+ ):
2015
+ for used_by in peer_schema.used_by:
2016
+ identified |= self.identify_required_object_templates(
2017
+ node_schema=self.get(name=used_by, duplicate=False), identified=identified
2018
+ )
2019
+
2020
+ identified |= self.identify_required_object_templates(node_schema=peer_schema, identified=identified)
2021
+
2022
+ return identified
2023
+
2024
+ def manage_object_template_schemas(self) -> None:
2025
+ need_templates: set[NodeSchema | GenericSchema] = set()
2026
+ template_schema_kinds: set[str] = set()
2027
+
2028
+ for node_name in self.node_names + self.generic_names_without_templates:
2029
+ node = self.get(name=node_name, duplicate=False)
2030
+
2031
+ # Delete old object templates if schemas were removed
2032
+ if (
2033
+ node.namespace in RESTRICTED_NAMESPACES
2034
+ or not node.generate_template
2035
+ or node.state == HashableModelState.ABSENT
2036
+ ):
2037
+ try:
2038
+ node.relationships = [r for r in node.relationships if r.name != OBJECT_TEMPLATE_RELATIONSHIP_NAME]
2039
+ self.delete(name=self._get_object_template_kind(node_kind=node.kind))
2040
+ except SchemaNotFoundError:
2041
+ ...
2042
+ continue
2043
+
2044
+ need_templates |= self.identify_required_object_templates(node_schema=node, identified=need_templates)
2045
+
2046
+ # Generate templates with their attributes
2047
+ for node in need_templates:
2048
+ template = self.generate_object_template_from_node(node=node, need_templates=need_templates)
2049
+ self.set(name=template.kind, schema=template)
2050
+ template_schema_kinds.add(template.kind)
2051
+
2052
+ # Go back on templates and add relationships to them
2053
+ for node in need_templates:
2054
+ self.add_relationships_to_template(node=node)
2055
+
2056
+ for previous_template in list(self.templates.keys()):
2057
+ # Ensure that we remove previous object template schemas if a node has been renamed
2058
+ if previous_template not in template_schema_kinds:
2059
+ self.delete(name=previous_template)
2060
+
2061
+ if not template_schema_kinds:
2062
+ return
2063
+
2064
+ core_template_schema = self.get(name=InfrahubKind.OBJECTTEMPLATE, duplicate=False)
2065
+ current_used_by_template = set(core_template_schema.used_by)
2066
+ new_used_by_template = template_schema_kinds - current_used_by_template
2067
+
2068
+ if new_used_by_template:
2069
+ core_template_schema = self.get(name=InfrahubKind.OBJECTTEMPLATE, duplicate=True)
2070
+ core_template_schema.used_by = sorted(template_schema_kinds)
2071
+ self.set(name=InfrahubKind.OBJECTTEMPLATE, schema=core_template_schema)
2072
+
2073
+ if self.has(name=InfrahubKind.NODE):
2074
+ core_node_schema = self.get(name=InfrahubKind.NODE, duplicate=False)
2075
+ current_used_by_node = set(core_node_schema.used_by)
2076
+ new_used_by_node = template_schema_kinds - current_used_by_node
2077
+
2078
+ if new_used_by_node:
2079
+ core_node_schema = self.get(name=InfrahubKind.NODE, duplicate=True)
2080
+ updated_used_by_node = set(chain(template_schema_kinds, set(core_node_schema.used_by)))
2081
+ core_node_schema.used_by = sorted(updated_used_by_node)
2082
+ self.set(name=InfrahubKind.NODE, schema=core_node_schema)