infrahub-server 1.1.6__py3-none-any.whl → 1.2.0rc0__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 (346) hide show
  1. infrahub/api/artifact.py +16 -4
  2. infrahub/api/dependencies.py +8 -0
  3. infrahub/api/oauth2.py +0 -1
  4. infrahub/api/oidc.py +0 -1
  5. infrahub/api/query.py +18 -7
  6. infrahub/api/schema.py +32 -6
  7. infrahub/api/transformation.py +12 -5
  8. infrahub/{message_bus/messages/check_artifact_create.py → artifacts/models.py} +2 -4
  9. infrahub/{message_bus/operations/check/artifact.py → artifacts/tasks.py} +26 -25
  10. infrahub/cli/__init__.py +0 -2
  11. infrahub/cli/db.py +6 -7
  12. infrahub/cli/events.py +8 -3
  13. infrahub/cli/git_agent.py +9 -7
  14. infrahub/cli/tasks.py +4 -6
  15. infrahub/computed_attribute/tasks.py +63 -17
  16. infrahub/computed_attribute/triggers.py +90 -0
  17. infrahub/config.py +1 -1
  18. infrahub/context.py +39 -0
  19. infrahub/core/account.py +5 -8
  20. infrahub/core/attribute.py +53 -21
  21. infrahub/core/branch/models.py +4 -4
  22. infrahub/core/branch/tasks.py +89 -130
  23. infrahub/core/changelog/__init__.py +0 -0
  24. infrahub/core/changelog/diff.py +232 -0
  25. infrahub/core/changelog/models.py +488 -0
  26. infrahub/core/constants/__init__.py +19 -2
  27. infrahub/core/constants/infrahubkind.py +1 -0
  28. infrahub/core/diff/combiner.py +12 -8
  29. infrahub/core/diff/coordinator.py +49 -70
  30. infrahub/core/diff/data_check_synchronizer.py +86 -7
  31. infrahub/core/diff/enricher/aggregated.py +3 -3
  32. infrahub/core/diff/enricher/cardinality_one.py +2 -7
  33. infrahub/core/diff/enricher/hierarchy.py +5 -3
  34. infrahub/core/diff/enricher/labels.py +14 -4
  35. infrahub/core/diff/enricher/path_identifier.py +3 -9
  36. infrahub/core/diff/enricher/summary_counts.py +3 -1
  37. infrahub/core/diff/merger/merger.py +8 -4
  38. infrahub/core/diff/model/path.py +47 -29
  39. infrahub/core/diff/query/all_conflicts.py +6 -3
  40. infrahub/core/diff/query/artifact.py +1 -1
  41. infrahub/core/diff/query/delete_query.py +1 -1
  42. infrahub/core/diff/query/diff_get.py +3 -2
  43. infrahub/core/diff/query/diff_summary.py +1 -1
  44. infrahub/core/diff/query/field_specifiers.py +3 -1
  45. infrahub/core/diff/query/field_summary.py +3 -2
  46. infrahub/core/diff/query/filters.py +12 -1
  47. infrahub/core/diff/query/get_conflict_query.py +1 -1
  48. infrahub/core/diff/query/has_conflicts_query.py +6 -3
  49. infrahub/core/diff/query/merge.py +3 -3
  50. infrahub/core/diff/query/{drop_tracking_id.py → merge_tracking_id.py} +4 -4
  51. infrahub/core/diff/query/roots_metadata.py +9 -2
  52. infrahub/core/diff/query/save.py +151 -66
  53. infrahub/core/diff/query/summary_counts_enricher.py +220 -0
  54. infrahub/core/diff/query/time_range_query.py +3 -2
  55. infrahub/core/diff/query/update_conflict_query.py +1 -1
  56. infrahub/core/diff/query_parser.py +49 -24
  57. infrahub/core/diff/repository/deserializer.py +24 -25
  58. infrahub/core/diff/repository/repository.py +76 -20
  59. infrahub/core/diff/tasks.py +9 -8
  60. infrahub/core/enums.py +1 -1
  61. infrahub/core/integrity/object_conflict/conflict_recorder.py +1 -1
  62. infrahub/core/ipam/reconciler.py +1 -1
  63. infrahub/core/ipam/tasks.py +2 -3
  64. infrahub/core/manager.py +18 -13
  65. infrahub/core/merge.py +5 -2
  66. infrahub/core/migrations/graph/m001_add_version_to_graph.py +1 -1
  67. infrahub/core/migrations/graph/m002_attribute_is_default.py +2 -2
  68. infrahub/core/migrations/graph/m003_relationship_parent_optional.py +2 -2
  69. infrahub/core/migrations/graph/m004_add_attr_documentation.py +1 -1
  70. infrahub/core/migrations/graph/m005_add_rel_read_only.py +1 -1
  71. infrahub/core/migrations/graph/m006_add_rel_on_delete.py +1 -1
  72. infrahub/core/migrations/graph/m007_add_rel_allow_override.py +1 -1
  73. infrahub/core/migrations/graph/m008_add_human_friendly_id.py +1 -1
  74. infrahub/core/migrations/graph/m009_add_generate_profile_attr.py +1 -1
  75. infrahub/core/migrations/graph/m010_add_generate_profile_attr_generic.py +1 -1
  76. infrahub/core/migrations/graph/m011_remove_profile_relationship_schema.py +2 -2
  77. infrahub/core/migrations/graph/m012_convert_account_generic.py +12 -23
  78. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +7 -11
  79. infrahub/core/migrations/graph/m014_remove_index_attr_value.py +2 -2
  80. infrahub/core/migrations/graph/m015_diff_format_update.py +1 -1
  81. infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +1 -1
  82. infrahub/core/migrations/graph/m017_add_core_profile.py +1 -1
  83. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +2 -2
  84. infrahub/core/migrations/query/attribute_add.py +1 -1
  85. infrahub/core/migrations/query/attribute_rename.py +1 -1
  86. infrahub/core/migrations/query/delete_element_in_schema.py +1 -1
  87. infrahub/core/migrations/query/node_duplicate.py +1 -1
  88. infrahub/core/migrations/query/relationship_duplicate.py +1 -1
  89. infrahub/core/migrations/query/schema_attribute_update.py +1 -1
  90. infrahub/core/migrations/schema/node_attribute_remove.py +1 -1
  91. infrahub/core/migrations/schema/node_remove.py +1 -1
  92. infrahub/core/migrations/schema/tasks.py +5 -5
  93. infrahub/core/migrations/shared.py +4 -4
  94. infrahub/core/models.py +7 -8
  95. infrahub/core/node/__init__.py +161 -40
  96. infrahub/core/node/base.py +1 -1
  97. infrahub/core/node/constraints/grouped_uniqueness.py +9 -2
  98. infrahub/core/node/delete_validator.py +4 -4
  99. infrahub/core/node/ipam.py +13 -8
  100. infrahub/core/node/permissions.py +4 -0
  101. infrahub/core/node/resource_manager/ip_prefix_pool.py +8 -5
  102. infrahub/core/node/standard.py +3 -5
  103. infrahub/core/property.py +1 -1
  104. infrahub/core/protocols.py +4 -0
  105. infrahub/core/protocols_base.py +4 -2
  106. infrahub/core/query/__init__.py +2 -5
  107. infrahub/core/query/attribute.py +9 -9
  108. infrahub/core/query/branch.py +5 -5
  109. infrahub/core/query/delete.py +1 -1
  110. infrahub/core/query/diff.py +45 -7
  111. infrahub/core/query/ipam.py +4 -4
  112. infrahub/core/query/node.py +19 -14
  113. infrahub/core/query/relationship.py +10 -11
  114. infrahub/core/query/resource_manager.py +13 -11
  115. infrahub/core/query/standard_node.py +6 -6
  116. infrahub/core/query/task.py +3 -3
  117. infrahub/core/query/task_log.py +1 -1
  118. infrahub/core/query/utils.py +5 -5
  119. infrahub/core/registry.py +0 -2
  120. infrahub/core/relationship/constraints/count.py +1 -1
  121. infrahub/core/relationship/constraints/peer_kind.py +1 -1
  122. infrahub/core/relationship/model.py +66 -26
  123. infrahub/core/schema/__init__.py +6 -4
  124. infrahub/core/schema/basenode_schema.py +1 -3
  125. infrahub/core/schema/definitions/core.py +14 -2
  126. infrahub/core/schema/definitions/internal.py +16 -0
  127. infrahub/core/schema/generated/genericnode_schema.py +5 -0
  128. infrahub/core/schema/generated/node_schema.py +5 -0
  129. infrahub/core/schema/generic_schema.py +5 -1
  130. infrahub/core/schema/manager.py +45 -42
  131. infrahub/core/schema/node_schema.py +4 -0
  132. infrahub/core/schema/profile_schema.py +4 -0
  133. infrahub/core/schema/relationship_schema.py +2 -2
  134. infrahub/core/schema/schema_branch.py +248 -14
  135. infrahub/core/schema/template_schema.py +36 -0
  136. infrahub/core/task/user_task.py +7 -5
  137. infrahub/core/timestamp.py +1 -1
  138. infrahub/core/utils.py +3 -2
  139. infrahub/core/validators/attribute/choices.py +1 -1
  140. infrahub/core/validators/attribute/enum.py +1 -1
  141. infrahub/core/validators/attribute/kind.py +1 -1
  142. infrahub/core/validators/attribute/length.py +1 -1
  143. infrahub/core/validators/attribute/optional.py +1 -1
  144. infrahub/core/validators/attribute/regex.py +1 -1
  145. infrahub/core/validators/attribute/unique.py +1 -1
  146. infrahub/core/validators/checks_runner.py +37 -0
  147. infrahub/core/validators/node/generate_profile.py +1 -1
  148. infrahub/core/validators/node/hierarchy.py +1 -1
  149. infrahub/core/validators/query.py +1 -1
  150. infrahub/core/validators/relationship/count.py +1 -1
  151. infrahub/core/validators/relationship/optional.py +1 -1
  152. infrahub/core/validators/relationship/peer.py +1 -1
  153. infrahub/core/validators/tasks.py +8 -6
  154. infrahub/core/validators/uniqueness/query.py +20 -17
  155. infrahub/database/__init__.py +15 -2
  156. infrahub/database/memgraph.py +1 -1
  157. infrahub/dependencies/builder/constraint/grouped/node_runner.py +0 -2
  158. infrahub/dependencies/builder/diff/combiner.py +1 -1
  159. infrahub/dependencies/builder/diff/conflicts_enricher.py +1 -1
  160. infrahub/dependencies/builder/diff/coordinator.py +0 -2
  161. infrahub/dependencies/builder/diff/deserializer.py +1 -1
  162. infrahub/dependencies/builder/diff/enricher/summary_counts.py +1 -1
  163. infrahub/events/branch_action.py +47 -21
  164. infrahub/events/group_action.py +73 -0
  165. infrahub/events/models.py +159 -51
  166. infrahub/events/node_action.py +74 -8
  167. infrahub/events/repository_action.py +8 -8
  168. infrahub/events/schema_action.py +21 -8
  169. infrahub/generators/tasks.py +12 -13
  170. infrahub/git/base.py +3 -5
  171. infrahub/git/constants.py +0 -1
  172. infrahub/git/integrator.py +36 -35
  173. infrahub/git/repository.py +7 -8
  174. infrahub/git/tasks.py +43 -107
  175. infrahub/git_credential/helper.py +2 -3
  176. infrahub/graphql/analyzer.py +572 -11
  177. infrahub/graphql/app.py +34 -26
  178. infrahub/graphql/auth/query_permission_checker/anonymous_checker.py +5 -5
  179. infrahub/graphql/auth/query_permission_checker/default_branch_checker.py +4 -4
  180. infrahub/graphql/auth/query_permission_checker/merge_operation_checker.py +4 -4
  181. infrahub/graphql/auth/query_permission_checker/object_permission_checker.py +28 -35
  182. infrahub/graphql/auth/query_permission_checker/super_admin_checker.py +5 -5
  183. infrahub/graphql/enums.py +1 -1
  184. infrahub/graphql/initialization.py +5 -1
  185. infrahub/graphql/loaders/node.py +2 -2
  186. infrahub/graphql/manager.py +59 -54
  187. infrahub/graphql/mutations/account.py +20 -13
  188. infrahub/graphql/mutations/artifact_definition.py +16 -12
  189. infrahub/graphql/mutations/branch.py +61 -40
  190. infrahub/graphql/mutations/computed_attribute.py +19 -13
  191. infrahub/graphql/mutations/diff.py +37 -9
  192. infrahub/graphql/mutations/diff_conflict.py +9 -8
  193. infrahub/graphql/mutations/graphql_query.py +19 -11
  194. infrahub/graphql/mutations/ipam.py +21 -19
  195. infrahub/graphql/mutations/main.py +197 -44
  196. infrahub/graphql/mutations/menu.py +8 -8
  197. infrahub/graphql/mutations/proposed_change.py +36 -28
  198. infrahub/graphql/mutations/relationship.py +302 -105
  199. infrahub/graphql/mutations/repository.py +41 -35
  200. infrahub/graphql/mutations/resource_manager.py +26 -26
  201. infrahub/graphql/mutations/schema.py +51 -33
  202. infrahub/graphql/mutations/tasks.py +16 -10
  203. infrahub/graphql/parser.py +1 -1
  204. infrahub/graphql/permissions.py +6 -4
  205. infrahub/graphql/queries/account.py +22 -18
  206. infrahub/graphql/queries/branch.py +6 -4
  207. infrahub/graphql/queries/diff/tree.py +48 -42
  208. infrahub/graphql/queries/event.py +112 -0
  209. infrahub/graphql/queries/internal.py +3 -3
  210. infrahub/graphql/queries/ipam.py +23 -18
  211. infrahub/graphql/queries/relationship.py +11 -10
  212. infrahub/graphql/queries/resource_manager.py +43 -27
  213. infrahub/graphql/queries/search.py +9 -8
  214. infrahub/graphql/queries/status.py +12 -9
  215. infrahub/graphql/queries/task.py +11 -9
  216. infrahub/graphql/resolvers/resolver.py +69 -43
  217. infrahub/graphql/resolvers/single_relationship.py +16 -10
  218. infrahub/graphql/schema.py +2 -0
  219. infrahub/graphql/subscription/__init__.py +1 -1
  220. infrahub/graphql/subscription/events.py +1 -1
  221. infrahub/graphql/subscription/graphql_query.py +8 -8
  222. infrahub/graphql/types/branch.py +2 -2
  223. infrahub/graphql/types/common.py +6 -1
  224. infrahub/graphql/types/enums.py +2 -0
  225. infrahub/graphql/types/event.py +100 -0
  226. infrahub/graphql/types/interface.py +2 -2
  227. infrahub/graphql/types/node.py +3 -3
  228. infrahub/graphql/types/permission.py +2 -2
  229. infrahub/graphql/types/relationship.py +3 -3
  230. infrahub/graphql/types/standard_node.py +9 -11
  231. infrahub/graphql/utils.py +28 -182
  232. infrahub/groups/tasks.py +2 -3
  233. infrahub/lock.py +1 -1
  234. infrahub/menu/constants.py +1 -0
  235. infrahub/menu/generator.py +14 -3
  236. infrahub/menu/menu.py +116 -127
  237. infrahub/menu/models.py +4 -4
  238. infrahub/message_bus/messages/__init__.py +0 -4
  239. infrahub/message_bus/messages/event_branch_merge.py +3 -0
  240. infrahub/message_bus/messages/request_proposedchange_pipeline.py +2 -0
  241. infrahub/message_bus/operations/__init__.py +3 -5
  242. infrahub/message_bus/operations/check/__init__.py +2 -2
  243. infrahub/message_bus/operations/check/generator.py +1 -3
  244. infrahub/message_bus/operations/check/repository.py +1 -1
  245. infrahub/message_bus/operations/event/branch.py +7 -3
  246. infrahub/message_bus/operations/event/schema.py +1 -1
  247. infrahub/message_bus/operations/finalize/validator.py +1 -1
  248. infrahub/message_bus/operations/git/file.py +2 -2
  249. infrahub/message_bus/operations/git/repository.py +1 -1
  250. infrahub/message_bus/operations/requests/__init__.py +0 -2
  251. infrahub/message_bus/operations/requests/generator_definition.py +1 -1
  252. infrahub/message_bus/operations/requests/proposed_change.py +26 -11
  253. infrahub/message_bus/operations/requests/repository.py +2 -2
  254. infrahub/message_bus/operations/send/echo.py +1 -1
  255. infrahub/message_bus/types.py +1 -1
  256. infrahub/permissions/__init__.py +2 -1
  257. infrahub/permissions/types.py +26 -0
  258. infrahub/pools/prefix.py +29 -165
  259. infrahub/prefect_server/__init__.py +0 -0
  260. infrahub/prefect_server/app.py +18 -0
  261. infrahub/prefect_server/database.py +20 -0
  262. infrahub/prefect_server/events.py +28 -0
  263. infrahub/prefect_server/models.py +46 -0
  264. infrahub/proposed_change/models.py +15 -1
  265. infrahub/proposed_change/tasks.py +173 -35
  266. infrahub/pytest_plugin.py +4 -4
  267. infrahub/server.py +12 -11
  268. infrahub/services/__init__.py +147 -62
  269. infrahub/services/adapters/cache/__init__.py +7 -5
  270. infrahub/services/adapters/cache/nats.py +40 -22
  271. infrahub/services/adapters/cache/redis.py +0 -4
  272. infrahub/services/adapters/event/__init__.py +10 -18
  273. infrahub/services/adapters/http/__init__.py +0 -5
  274. infrahub/services/adapters/http/httpx.py +22 -15
  275. infrahub/services/adapters/message_bus/__init__.py +23 -6
  276. infrahub/services/adapters/message_bus/local.py +8 -6
  277. infrahub/services/adapters/message_bus/nats.py +12 -6
  278. infrahub/services/adapters/message_bus/rabbitmq.py +22 -9
  279. infrahub/services/adapters/workflow/__init__.py +11 -8
  280. infrahub/services/adapters/workflow/local.py +28 -7
  281. infrahub/services/adapters/workflow/worker.py +23 -7
  282. infrahub/services/component.py +38 -35
  283. infrahub/services/scheduler.py +32 -29
  284. infrahub/storage.py +2 -4
  285. infrahub/task_manager/constants.py +1 -1
  286. infrahub/task_manager/event.py +182 -0
  287. infrahub/task_manager/models.py +125 -1
  288. infrahub/task_manager/task.py +1 -1
  289. infrahub/tasks/artifact.py +14 -16
  290. infrahub/tasks/registry.py +1 -1
  291. infrahub/tasks/telemetry.py +13 -14
  292. infrahub/transformations/tasks.py +3 -5
  293. infrahub/trigger/__init__.py +0 -0
  294. infrahub/trigger/catalogue.py +15 -0
  295. infrahub/trigger/constants.py +9 -0
  296. infrahub/trigger/models.py +69 -0
  297. infrahub/trigger/tasks.py +85 -0
  298. infrahub/types.py +1 -1
  299. infrahub/utils.py +1 -1
  300. infrahub/webhook/constants.py +0 -2
  301. infrahub/webhook/models.py +8 -2
  302. infrahub/webhook/tasks.py +20 -73
  303. infrahub/webhook/triggers.py +20 -0
  304. infrahub/workers/infrahub_async.py +36 -25
  305. infrahub/workers/utils.py +63 -0
  306. infrahub/workflows/catalogue.py +13 -37
  307. infrahub/workflows/initialization.py +6 -8
  308. infrahub/workflows/models.py +3 -5
  309. infrahub/workflows/utils.py +1 -1
  310. infrahub_sdk/ctl/check.py +3 -3
  311. infrahub_sdk/ctl/cli_commands.py +11 -10
  312. infrahub_sdk/ctl/exceptions.py +0 -6
  313. infrahub_sdk/ctl/exporter.py +1 -1
  314. infrahub_sdk/ctl/generator.py +5 -5
  315. infrahub_sdk/ctl/importer.py +3 -2
  316. infrahub_sdk/ctl/menu.py +1 -1
  317. infrahub_sdk/ctl/object.py +1 -1
  318. infrahub_sdk/ctl/repository.py +23 -15
  319. infrahub_sdk/ctl/schema.py +2 -2
  320. infrahub_sdk/ctl/utils.py +4 -3
  321. infrahub_sdk/ctl/validate.py +2 -1
  322. infrahub_sdk/exceptions.py +6 -0
  323. infrahub_sdk/generator.py +3 -0
  324. infrahub_sdk/node.py +2 -2
  325. infrahub_sdk/schema/__init__.py +14 -2
  326. infrahub_sdk/schema/main.py +7 -0
  327. infrahub_sdk/utils.py +11 -1
  328. infrahub_sdk/yaml.py +2 -3
  329. {infrahub_server-1.1.6.dist-info → infrahub_server-1.2.0rc0.dist-info}/METADATA +46 -12
  330. {infrahub_server-1.1.6.dist-info → infrahub_server-1.2.0rc0.dist-info}/RECORD +338 -321
  331. infrahub_testcontainers/container.py +14 -6
  332. infrahub_testcontainers/docker-compose.test.yml +24 -5
  333. infrahub_testcontainers/haproxy.cfg +43 -0
  334. infrahub_testcontainers/helpers.py +85 -1
  335. infrahub/core/branch/constants.py +0 -2
  336. infrahub/graphql/query.py +0 -52
  337. infrahub/message_bus/messages/request_artifactdefinition_check.py +0 -17
  338. infrahub/message_bus/operations/requests/artifact_definition.py +0 -148
  339. infrahub/schema/constants.py +0 -1
  340. infrahub/schema/tasks.py +0 -76
  341. infrahub/services/adapters/database/__init__.py +0 -9
  342. infrahub_sdk/ctl/_file.py +0 -13
  343. /infrahub/{schema → artifacts}/__init__.py +0 -0
  344. {infrahub_server-1.1.6.dist-info → infrahub_server-1.2.0rc0.dist-info}/LICENSE.txt +0 -0
  345. {infrahub_server-1.1.6.dist-info → infrahub_server-1.2.0rc0.dist-info}/WHEEL +0 -0
  346. {infrahub_server-1.1.6.dist-info → infrahub_server-1.2.0rc0.dist-info}/entry_points.txt +0 -0
@@ -12,6 +12,7 @@ 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,
15
16
  RESERVED_ATTR_GEN_NAMES,
16
17
  RESERVED_ATTR_REL_NAMES,
17
18
  RESTRICTED_NAMESPACES,
@@ -43,6 +44,7 @@ from infrahub.core.schema import (
43
44
  RelationshipSchema,
44
45
  SchemaAttributePath,
45
46
  SchemaRoot,
47
+ TemplateSchema,
46
48
  )
47
49
  from infrahub.core.schema.definitions.core import core_profile_schema_definition
48
50
  from infrahub.core.validators import CONSTRAINT_VALIDATOR_MAP
@@ -62,9 +64,6 @@ if TYPE_CHECKING:
62
64
  from pydantic import ValidationInfo
63
65
 
64
66
 
65
- # pylint: disable=redefined-builtin,too-many-public-methods,too-many-lines
66
-
67
-
68
67
  class SchemaBranch:
69
68
  def __init__(
70
69
  self,
@@ -78,19 +77,21 @@ class SchemaBranch:
78
77
  self.nodes: dict[str, str] = {}
79
78
  self.generics: dict[str, str] = {}
80
79
  self.profiles: dict[str, str] = {}
80
+ self.templates: dict[str, str] = {}
81
81
  self.computed_attributes = computed_attributes or ComputedAttributes()
82
82
 
83
83
  if data:
84
84
  self.nodes = data.get("nodes", {})
85
85
  self.generics = data.get("generics", {})
86
86
  self.profiles = data.get("profiles", {})
87
+ self.templates = data.get("templates", {})
87
88
 
88
89
  @classmethod
89
90
  def __get_validators__(cls) -> Iterator[Callable[..., Any]]: # noqa: PLW3201
90
91
  yield cls.validate
91
92
 
92
93
  @classmethod
93
- def validate(cls, v: Any, info: ValidationInfo) -> Self: # pylint: disable=unused-argument
94
+ def validate(cls, v: Any, info: ValidationInfo) -> Self: # noqa: ARG003
94
95
  if isinstance(v, cls):
95
96
  return v
96
97
  if isinstance(v, dict):
@@ -109,6 +110,10 @@ class SchemaBranch:
109
110
  def profile_names(self) -> list[str]:
110
111
  return list(self.profiles.keys())
111
112
 
113
+ @property
114
+ def template_names(self) -> list[str]:
115
+ return list(self.templates.keys())
116
+
112
117
  def get_all_kind_id_map(self, exclude_profiles: bool = False) -> dict[str, str]:
113
118
  kind_id_map = {}
114
119
  if exclude_profiles:
@@ -124,7 +129,7 @@ class SchemaBranch:
124
129
 
125
130
  @property
126
131
  def all_names(self) -> list[str]:
127
- return self.node_names + self.generic_names + self.profile_names
132
+ return self.node_names + self.generic_names + self.profile_names + self.template_names
128
133
 
129
134
  def get_hash(self) -> str:
130
135
  """Calculate the hash for this objects based on the content of nodes and generics.
@@ -142,13 +147,14 @@ class SchemaBranch:
142
147
  return SchemaBranchHash(main=self.get_hash(), nodes=self.nodes, generics=self.generics)
143
148
 
144
149
  def to_dict(self) -> dict[str, Any]:
145
- return {"nodes": self.nodes, "generics": self.generics, "profiles": self.profiles}
150
+ return {"nodes": self.nodes, "generics": self.generics, "profiles": self.profiles, "templates": self.templates}
146
151
 
147
152
  def to_dict_schema_object(self, duplicate: bool = False) -> dict[str, dict[str, MainSchemaTypes]]:
148
153
  return {
149
154
  "nodes": {name: self.get(name, duplicate=duplicate) for name in self.nodes},
150
155
  "profiles": {name: self.get(name, duplicate=duplicate) for name in self.profiles},
151
156
  "generics": {name: self.get(name, duplicate=duplicate) for name in self.generics},
157
+ "templates": {name: self.get(name, duplicate=duplicate) for name in self.templates},
152
158
  }
153
159
 
154
160
  @classmethod
@@ -157,10 +163,11 @@ class SchemaBranch:
157
163
  "nodes": NodeSchema,
158
164
  "generics": GenericSchema,
159
165
  "profiles": ProfileSchema,
166
+ "templates": TemplateSchema,
160
167
  }
161
168
 
162
169
  cache: dict[str, MainSchemaTypes] = {}
163
- nodes: dict[str, dict[str, str]] = {"nodes": {}, "generics": {}, "profiles": {}}
170
+ nodes: dict[str, dict[str, str]] = {"nodes": {}, "generics": {}, "profiles": {}, "templates": {}}
164
171
 
165
172
  for node_type, node_class in type_mapping.items():
166
173
  for node_name, node_data in data[node_type].items():
@@ -281,6 +288,8 @@ class SchemaBranch:
281
288
  self.generics[name] = schema_hash
282
289
  elif "Profile" in schema.__class__.__name__:
283
290
  self.profiles[name] = schema_hash
291
+ elif "Template" in schema.__class__.__name__:
292
+ self.templates[name] = schema_hash
284
293
 
285
294
  return schema_hash
286
295
 
@@ -292,6 +301,7 @@ class SchemaBranch:
292
301
 
293
302
  If duplicate is set to false, the real object will be returned.
294
303
  """
304
+
295
305
  key = None
296
306
  if name in self.nodes:
297
307
  key = self.nodes[name]
@@ -299,6 +309,8 @@ class SchemaBranch:
299
309
  key = self.generics[name]
300
310
  elif name in self.profiles:
301
311
  key = self.profiles[name]
312
+ elif name in self.templates:
313
+ key = self.templates[name]
302
314
 
303
315
  if key and duplicate:
304
316
  return self._cache[key].duplicate()
@@ -330,6 +342,13 @@ class SchemaBranch:
330
342
  raise ValueError(f"{name!r} is not of type ProfileSchema")
331
343
  return item
332
344
 
345
+ def get_template(self, name: str, duplicate: bool = True) -> TemplateSchema:
346
+ """Access a specific TemplateSchema, defined by its kind."""
347
+ item = self.get(name=name, duplicate=duplicate)
348
+ if not isinstance(item, TemplateSchema):
349
+ raise ValueError(f"{name!r} is not of type TemplateSchema")
350
+ return item
351
+
333
352
  def delete(self, name: str) -> None:
334
353
  if name in self.nodes:
335
354
  del self.nodes[name]
@@ -337,6 +356,8 @@ class SchemaBranch:
337
356
  del self.generics[name]
338
357
  elif name in self.profiles:
339
358
  del self.profiles[name]
359
+ elif name in self.templates:
360
+ del self.templates[name]
340
361
  else:
341
362
  raise SchemaNotFoundError(
342
363
  branch_name=self.name, identifier=name, message=f"Unable to find the schema {name!r} in the registry"
@@ -431,7 +452,7 @@ class SchemaBranch:
431
452
 
432
453
  def generate_fields_for_display_label(self, name: str) -> Optional[dict]:
433
454
  node = self.get(name=name, duplicate=False)
434
- if isinstance(node, (NodeSchema, ProfileSchema)):
455
+ if isinstance(node, NodeSchema | ProfileSchema | TemplateSchema):
435
456
  return node.generate_fields_for_display_label()
436
457
 
437
458
  fields: dict[str, Union[str, None, dict[str, None]]] = {}
@@ -490,6 +511,8 @@ class SchemaBranch:
490
511
  self.process_inheritance()
491
512
  self.process_hierarchy()
492
513
  self.process_branch_support()
514
+ self.manage_object_template_schemas()
515
+ self.manage_object_template_relationships()
493
516
  self.manage_profile_schemas()
494
517
  self.manage_profile_relationships()
495
518
  self.add_hierarchy_generic()
@@ -520,6 +543,9 @@ class SchemaBranch:
520
543
  self.process_relationships()
521
544
  self.process_human_friendly_id()
522
545
 
546
+ def _generate_identifier_string(self, node_kind: str, peer_kind: str) -> str:
547
+ return "__".join(sorted([node_kind, peer_kind])).lower()
548
+
523
549
  def generate_identifiers(self) -> None:
524
550
  """Generate the identifier for all relationships if it's not already present."""
525
551
  for name in self.all_names:
@@ -532,7 +558,7 @@ class SchemaBranch:
532
558
  for rel in node.relationships:
533
559
  if rel.identifier:
534
560
  continue
535
- rel.identifier = str("__".join(sorted([node.kind, rel.peer]))).lower()
561
+ rel.identifier = self._generate_identifier_string(node.kind, rel.peer)
536
562
  self.set(name=name, schema=node)
537
563
 
538
564
  def validate_identifiers(self) -> None:
@@ -1267,7 +1293,6 @@ class SchemaBranch:
1267
1293
 
1268
1294
  if either node on a relationship support branch, the relationship must be branch aware.
1269
1295
  """
1270
- # pylint: disable=too-many-branches
1271
1296
 
1272
1297
  for name in self.all_names:
1273
1298
  node = self.get(name=name, duplicate=False)
@@ -1329,7 +1354,6 @@ class SchemaBranch:
1329
1354
 
1330
1355
  def process_cardinality_counts(self) -> None:
1331
1356
  """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
1357
 
1334
1358
  for name in self.all_names:
1335
1359
  node = self.get(name=name, duplicate=False)
@@ -1416,7 +1440,6 @@ class SchemaBranch:
1416
1440
  self.set(name=name, schema=node)
1417
1441
 
1418
1442
  def cleanup_inherited_elements(self) -> None:
1419
- # pylint: disable=too-many-branches
1420
1443
  for name in self.node_names:
1421
1444
  node = self.get_node(name=name, duplicate=False)
1422
1445
 
@@ -1642,7 +1665,7 @@ class SchemaBranch:
1642
1665
 
1643
1666
  if new_used_by_profile:
1644
1667
  core_profile_schema = self.get(name=InfrahubKind.PROFILE, duplicate=True)
1645
- core_profile_schema.used_by = sorted(list(profile_schema_kinds))
1668
+ core_profile_schema.used_by = sorted(profile_schema_kinds)
1646
1669
  self.set(name=InfrahubKind.PROFILE, schema=core_profile_schema)
1647
1670
 
1648
1671
  if self.has(name=InfrahubKind.NODE):
@@ -1653,7 +1676,7 @@ class SchemaBranch:
1653
1676
  if new_used_by_node:
1654
1677
  core_node_schema = self.get(name=InfrahubKind.NODE, duplicate=True)
1655
1678
  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))
1679
+ core_node_schema.used_by = sorted(updated_used_by_node)
1657
1680
  self.set(name=InfrahubKind.NODE, schema=core_node_schema)
1658
1681
 
1659
1682
  def manage_profile_relationships(self) -> None:
@@ -1746,3 +1769,214 @@ class SchemaBranch:
1746
1769
  profile.attributes.append(attr)
1747
1770
 
1748
1771
  return profile
1772
+
1773
+ def _get_object_template_kind(self, node_kind: str) -> str:
1774
+ return f"Template{node_kind}"
1775
+
1776
+ def manage_object_template_relationships(self) -> None:
1777
+ """Add an `object_template` relationship to all nodes that can be created from object templates.
1778
+
1779
+ This relationship allows to record from which template an object has been created.
1780
+ """
1781
+ for node_name in self.node_names + self.generic_names:
1782
+ node = self.get(name=node_name, duplicate=False)
1783
+
1784
+ if (
1785
+ node.namespace in RESTRICTED_NAMESPACES
1786
+ or not node.generate_template
1787
+ or node.state == HashableModelState.ABSENT
1788
+ ):
1789
+ continue
1790
+
1791
+ template_rel_settings: dict[str, Any] = {
1792
+ "name": "object_template",
1793
+ "identifier": "node__objecttemplate",
1794
+ "peer": self._get_object_template_kind(node.kind),
1795
+ "kind": RelationshipKind.TEMPLATE,
1796
+ "cardinality": RelationshipCardinality.ONE,
1797
+ "branch": BranchSupportType.AWARE,
1798
+ "order_weight": 1,
1799
+ }
1800
+
1801
+ # Add relationship between node and template
1802
+ if "object_template" not in node.relationship_names:
1803
+ node_schema = self.get(name=node_name, duplicate=True)
1804
+
1805
+ node_schema.relationships.append(RelationshipSchema(**template_rel_settings))
1806
+ self.set(name=node_name, schema=node_schema)
1807
+ else:
1808
+ has_changes: bool = False
1809
+ rel_template = node.get_relationship(name="object_template")
1810
+ for name, value in template_rel_settings.items():
1811
+ if getattr(rel_template, name) != value:
1812
+ has_changes = True
1813
+
1814
+ if not has_changes:
1815
+ continue
1816
+
1817
+ node_schema = self.get(name=node_name, duplicate=True)
1818
+ rel_template = node_schema.get_relationship(name="object_template")
1819
+ for name, value in template_rel_settings.items():
1820
+ if getattr(rel_template, name) != value:
1821
+ setattr(rel_template, name, value)
1822
+
1823
+ self.set(name=node_name, schema=node_schema)
1824
+
1825
+ def add_relationships_to_template(self, node: NodeSchema) -> None:
1826
+ template_schema = self.get(name=self._get_object_template_kind(node_kind=node.kind), duplicate=False)
1827
+
1828
+ for relationship in node.relationships:
1829
+ if relationship.peer in [
1830
+ InfrahubKind.GENERICGROUP,
1831
+ InfrahubKind.PROFILE,
1832
+ ] or relationship.kind not in [
1833
+ RelationshipKind.COMPONENT,
1834
+ RelationshipKind.PARENT,
1835
+ RelationshipKind.ATTRIBUTE,
1836
+ ]:
1837
+ continue
1838
+
1839
+ rel_template_peer = (
1840
+ self._get_object_template_kind(node_kind=relationship.peer)
1841
+ if relationship.kind != RelationshipKind.ATTRIBUTE
1842
+ else relationship.peer
1843
+ )
1844
+ template_schema.relationships.append(
1845
+ RelationshipSchema(
1846
+ name=relationship.name,
1847
+ peer=rel_template_peer,
1848
+ kind=relationship.kind,
1849
+ optional=relationship.kind in [RelationshipKind.COMPONENT, RelationshipKind.ATTRIBUTE],
1850
+ cardinality=relationship.cardinality,
1851
+ branch=relationship.branch,
1852
+ identifier=self._generate_identifier_string(template_schema.kind, rel_template_peer),
1853
+ min_count=relationship.min_count,
1854
+ max_count=relationship.max_count,
1855
+ )
1856
+ )
1857
+
1858
+ def generate_object_template_from_node(self, node: NodeSchema) -> TemplateSchema:
1859
+ core_template_schema = self.get(name=InfrahubKind.OBJECTTEMPLATE, duplicate=False)
1860
+ core_name_attr = core_template_schema.get_attribute(name=OBJECT_TEMPLATE_NAME_ATTR)
1861
+ template_name_attr = AttributeSchema(
1862
+ **core_name_attr.model_dump(exclude=["id", "inherited"]),
1863
+ )
1864
+ template_name_attr.branch = node.branch
1865
+
1866
+ template = TemplateSchema(
1867
+ name=node.kind,
1868
+ namespace="Template",
1869
+ label=f"Object template {node.label}",
1870
+ description=f"Object template for {node.kind}",
1871
+ branch=node.branch,
1872
+ include_in_menu=True,
1873
+ display_labels=["template_name__value"],
1874
+ inherit_from=[InfrahubKind.LINEAGESOURCE, InfrahubKind.OBJECTTEMPLATE, InfrahubKind.NODE],
1875
+ human_friendly_id=["template_name__value"],
1876
+ default_filter="template_name__value",
1877
+ attributes=[template_name_attr],
1878
+ relationships=[
1879
+ RelationshipSchema(
1880
+ name="related_nodes",
1881
+ identifier="node__objecttemplate",
1882
+ peer=node.kind,
1883
+ kind=RelationshipKind.TEMPLATE,
1884
+ cardinality=RelationshipCardinality.MANY,
1885
+ branch=BranchSupportType.AWARE,
1886
+ )
1887
+ ],
1888
+ )
1889
+
1890
+ for node_attr in node.attributes:
1891
+ if node_attr.unique:
1892
+ continue
1893
+
1894
+ attr = AttributeSchema(
1895
+ optional=True, **node_attr.model_dump(exclude=["id", "unique", "optional", "read_only", "inherited"])
1896
+ )
1897
+ template.attributes.append(attr)
1898
+
1899
+ return template
1900
+
1901
+ def identify_required_object_templates(
1902
+ self, node_schema: NodeSchema, identified: set[NodeSchema]
1903
+ ) -> set[NodeSchema]:
1904
+ """Identify all templates required to turn a given node into a template."""
1905
+ if node_schema in identified:
1906
+ return identified
1907
+
1908
+ identified.add(node_schema)
1909
+
1910
+ for relationship in node_schema.relationships:
1911
+ if relationship.peer in [
1912
+ InfrahubKind.GENERICGROUP,
1913
+ InfrahubKind.PROFILE,
1914
+ ] or relationship.kind not in [RelationshipKind.COMPONENT, RelationshipKind.PARENT]:
1915
+ continue
1916
+
1917
+ peer_schema = self.get(name=relationship.peer, duplicate=False)
1918
+ if not isinstance(peer_schema, NodeSchema) or peer_schema in identified:
1919
+ continue
1920
+
1921
+ identified |= self.identify_required_object_templates(node_schema=peer_schema, identified=identified)
1922
+
1923
+ return identified
1924
+
1925
+ def manage_object_template_schemas(self) -> None:
1926
+ need_templates: set[NodeSchema] = set()
1927
+ template_schema_kinds: set[str] = set()
1928
+
1929
+ for node_name in self.node_names + self.generic_names:
1930
+ node = self.get(name=node_name, duplicate=False)
1931
+
1932
+ # Delete old object templates if schemas were removed
1933
+ if (
1934
+ node.namespace in RESTRICTED_NAMESPACES
1935
+ or not node.generate_template
1936
+ or node.state == HashableModelState.ABSENT
1937
+ ):
1938
+ try:
1939
+ self.delete(name=self._get_object_template_kind(node_kind=node.kind))
1940
+ except SchemaNotFoundError:
1941
+ ...
1942
+ continue
1943
+
1944
+ need_templates |= self.identify_required_object_templates(node_schema=node, identified=need_templates)
1945
+
1946
+ # Generate templates with their attributes
1947
+ for node in need_templates:
1948
+ template = self.generate_object_template_from_node(node=node)
1949
+ self.set(name=template.kind, schema=template)
1950
+ template_schema_kinds.add(template.kind)
1951
+
1952
+ # Go back on templates and add relationships to them
1953
+ for node in need_templates:
1954
+ self.add_relationships_to_template(node=node)
1955
+
1956
+ for previous_template in list(self.templates.keys()):
1957
+ # Ensure that we remove previous object template schemas if a node has been renamed
1958
+ if previous_template not in template_schema_kinds:
1959
+ self.delete(name=previous_template)
1960
+
1961
+ if not template_schema_kinds:
1962
+ return
1963
+
1964
+ core_template_schema = self.get(name=InfrahubKind.OBJECTTEMPLATE, duplicate=False)
1965
+ current_used_by_template = set(core_template_schema.used_by)
1966
+ new_used_by_template = template_schema_kinds - current_used_by_template
1967
+
1968
+ if new_used_by_template:
1969
+ core_template_schema = self.get(name=InfrahubKind.OBJECTTEMPLATE, duplicate=True)
1970
+ core_template_schema.used_by = sorted(template_schema_kinds)
1971
+ self.set(name=InfrahubKind.OBJECTTEMPLATE, schema=core_template_schema)
1972
+
1973
+ if self.has(name=InfrahubKind.NODE):
1974
+ core_node_schema = self.get(name=InfrahubKind.NODE, duplicate=False)
1975
+ current_used_by_node = set(core_node_schema.used_by)
1976
+ new_used_by_node = template_schema_kinds - current_used_by_node
1977
+
1978
+ if new_used_by_node:
1979
+ core_node_schema = self.get(name=InfrahubKind.NODE, duplicate=True)
1980
+ updated_used_by_node = set(chain(template_schema_kinds, set(core_node_schema.used_by)))
1981
+ core_node_schema.used_by = sorted(updated_used_by_node)
1982
+ self.set(name=InfrahubKind.NODE, schema=core_node_schema)
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import Field
4
+
5
+ from infrahub.core.constants import InfrahubKind
6
+ from infrahub.core.schema.basenode_schema import BaseNodeSchema
7
+
8
+
9
+ class TemplateSchema(BaseNodeSchema):
10
+ inherit_from: list[str] = Field(
11
+ default_factory=list, description="List of Generic Kind that this template is inheriting from"
12
+ )
13
+
14
+ @property
15
+ def is_node_schema(self) -> bool:
16
+ return False
17
+
18
+ @property
19
+ def is_generic_schema(self) -> bool:
20
+ return False
21
+
22
+ @property
23
+ def is_profile_schema(self) -> bool:
24
+ return False
25
+
26
+ @property
27
+ def is_template_schema(self) -> bool:
28
+ return True
29
+
30
+ def get_labels(self) -> list[str]:
31
+ """Return the labels for this object, composed of the kind and the list of Generic this object is inheriting from."""
32
+
33
+ labels: list[str] = [self.kind] + self.inherit_from
34
+ if self.namespace not in ["Schema", "Internal"] and InfrahubKind.GENERICGROUP not in self.inherit_from:
35
+ labels.append(InfrahubKind.OBJECTTEMPLATE)
36
+ return labels
@@ -80,14 +80,16 @@ class UserTask:
80
80
 
81
81
  @classmethod
82
82
  def from_graphql_context(
83
- cls, title: str, context: GraphqlContext, logger: Optional[Union[BoundLogger, InfrahubLogger]] = None
83
+ cls, title: str, graphql_context: GraphqlContext, logger: Optional[Union[BoundLogger, InfrahubLogger]] = None
84
84
  ) -> Self:
85
- if not context.db or not context.account_session:
85
+ if not graphql_context.db or not graphql_context.account_session:
86
86
  raise ValueError("db and account_session must be provided to initialize a GraphQLTaskReport")
87
87
 
88
- if not logger and context.service and context.service.log:
89
- logger = context.service.log
90
- return cls(title=title, account_id=context.account_session.account_id, db=context.db, logger=logger)
88
+ if not logger and graphql_context.service and graphql_context.service.log:
89
+ logger = graphql_context.service.log
90
+ return cls(
91
+ title=title, account_id=graphql_context.account_session.account_id, db=graphql_context.db, logger=logger
92
+ )
91
93
 
92
94
  async def __aenter__(self) -> Self:
93
95
  await self.create_task()
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
9
9
 
10
10
 
11
11
  class Timestamp(BaseTimestamp):
12
- async def to_graphql(self, *args: Any, **kwargs: Any) -> DateTime: # pylint: disable=unused-argument
12
+ async def to_graphql(self, *args: Any, **kwargs: Any) -> DateTime: # noqa: ARG002
13
13
  return self.obj
14
14
 
15
15
  def get_query_filter_path(self, rel_name: str = "r") -> tuple[str, dict]:
infrahub/core/utils.py CHANGED
@@ -72,6 +72,7 @@ async def update_relationships_to(ids: list[str], db: InfrahubDatabase, to: Time
72
72
  query = """
73
73
  MATCH ()-[r]->()
74
74
  WHERE %(id_func)s(r) IN $ids
75
+ AND r.to IS NULL
75
76
  SET r.to = $to
76
77
  RETURN %(id_func)s(r)
77
78
  """ % {"id_func": db.get_id_function_name()}
@@ -184,7 +185,7 @@ def parse_node_kind(kind: str) -> NodeKind:
184
185
  def convert_ip_to_binary_str(
185
186
  obj: Union[ipaddress.IPv6Network, ipaddress.IPv4Network, ipaddress.IPv4Interface, ipaddress.IPv6Interface],
186
187
  ) -> str:
187
- if isinstance(obj, (ipaddress.IPv6Network, ipaddress.IPv4Network)):
188
+ if isinstance(obj, ipaddress.IPv6Network | ipaddress.IPv4Network):
188
189
  prefix_bin = f"{int(obj.network_address):b}"
189
190
  return prefix_bin.zfill(obj.max_prefixlen)
190
191
 
@@ -204,7 +205,7 @@ def build_regex_attr(value: str | int | bool) -> str:
204
205
  """
205
206
  if isinstance(value, str):
206
207
  return f'"{value}"'
207
- if isinstance(value, (bool, int)):
208
+ if isinstance(value, bool | int):
208
209
  value_str = str(value).lower()
209
210
  return rf'(?<=[^\w"\d]){value_str}(?=[^\w"\d])'
210
211
 
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
18
18
  class AttributeChoicesUpdateValidatorQuery(AttributeSchemaValidatorQuery):
19
19
  name: str = "attribute_constraints_choices_validator"
20
20
 
21
- async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None:
21
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
22
22
  if self.attribute_schema.choices is None:
23
23
  return
24
24
 
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
18
18
  class AttributeEnumUpdateValidatorQuery(AttributeSchemaValidatorQuery):
19
19
  name: str = "attribute_constraints_enum_validator"
20
20
 
21
- async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None:
21
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
22
22
  if self.attribute_schema.enum is None:
23
23
  return
24
24
 
@@ -28,7 +28,7 @@ class NodeAttributeValue:
28
28
  class AttributeKindUpdateValidatorQuery(AttributeSchemaValidatorQuery):
29
29
  name: str = "attribute_constraints_kind_validator"
30
30
 
31
- async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None:
31
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
32
32
  branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string())
33
33
  self.params.update(branch_params)
34
34
 
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
18
18
  class AttributeLengthUpdateValidatorQuery(AttributeSchemaValidatorQuery):
19
19
  name: str = "attribute_constraints_length_validator"
20
20
 
21
- async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None:
21
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
22
22
  branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string())
23
23
  self.params.update(branch_params)
24
24
 
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
18
18
  class AttributeOptionalUpdateValidatorQuery(AttributeSchemaValidatorQuery):
19
19
  name: str = "attribute_constraints_optional_validator"
20
20
 
21
- async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None:
21
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
22
22
  branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string())
23
23
  self.params.update(branch_params)
24
24
 
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
18
18
  class AttributeRegexUpdateValidatorQuery(AttributeSchemaValidatorQuery):
19
19
  name: str = "attribute_constraints_regex_validator"
20
20
 
21
- async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None:
21
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
22
22
  branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string())
23
23
  self.params.update(branch_params)
24
24
 
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
18
18
  class AttributeUniqueUpdateValidatorQuery(AttributeSchemaValidatorQuery):
19
19
  name: str = "attribute_constraints_unique_validator"
20
20
 
21
- async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None:
21
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
22
22
  branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string(), is_isolated=False)
23
23
  self.params.update(branch_params)
24
24
 
@@ -0,0 +1,37 @@
1
+ import asyncio
2
+ from typing import Any, Coroutine
3
+
4
+ from infrahub_sdk.node import InfrahubNode
5
+
6
+ from infrahub.core.constants import ValidatorConclusion, ValidatorState
7
+ from infrahub.core.timestamp import Timestamp
8
+
9
+
10
+ async def run_checks_and_update_validator(
11
+ checks: list[Coroutine[Any, None, ValidatorConclusion]], validator: InfrahubNode
12
+ ) -> None:
13
+ """
14
+ Execute a list of checks coroutines, and set validator fields accordingly.
15
+ Tasks are retrieved by completion order so as soon as we detect a failing check,
16
+ we set validator conclusion to failure.
17
+ """
18
+
19
+ # First set validator to in progress, then wait for results
20
+ validator.state.value = ValidatorState.IN_PROGRESS.value
21
+ validator.started_at.value = Timestamp().to_string()
22
+ validator.completed_at.value = ""
23
+ await validator.save()
24
+
25
+ for earliest_task in asyncio.as_completed(checks):
26
+ result = await earliest_task
27
+ if validator.conclusion.value != ValidatorConclusion.FAILURE.value and result == ValidatorConclusion.FAILURE:
28
+ validator.conclusion.value = ValidatorConclusion.FAILURE.value
29
+ await validator.save()
30
+ # Continue to iterate to wait for the end of all checks
31
+
32
+ validator.state.value = ValidatorState.COMPLETED.value
33
+ validator.completed_at.value = Timestamp().to_string()
34
+ if validator.conclusion.value != ValidatorConclusion.FAILURE.value:
35
+ validator.conclusion.value = ValidatorConclusion.SUCCESS.value
36
+
37
+ await validator.save()
@@ -28,7 +28,7 @@ class NodeGenerateProfileValidatorQuery(SchemaValidatorQuery):
28
28
  super().__init__(*args, **kwargs)
29
29
  self.profile_kind = f"Profile{self.node_schema.kind}"
30
30
 
31
- async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None:
31
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
32
32
  branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at)
33
33
  self.params.update(branch_params)
34
34
 
@@ -31,7 +31,7 @@ class NodeHierarchyUpdateValidatorQuery(SchemaValidatorQuery):
31
31
  self.check_parent = check_parent
32
32
  super().__init__(**kwargs)
33
33
 
34
- async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None:
34
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
35
35
  if self.check_children and self.check_parent:
36
36
  raise RuntimeError("Cannot check children and parent at same time")
37
37
  if self.check_children:
@@ -14,7 +14,7 @@ if TYPE_CHECKING:
14
14
  class NodeNotPresentValidatorQuery(SchemaValidatorQuery):
15
15
  name: str = "node_not_present_validator"
16
16
 
17
- async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None:
17
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
18
18
  branch_filter, branch_params = self.branch.get_query_filter_path(at=self.at.to_string())
19
19
  self.params.update(branch_params)
20
20