infrahub-server 1.2.0b1__py3-none-any.whl → 1.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. infrahub/api/dependencies.py +6 -6
  2. infrahub/api/diff/validation_models.py +7 -7
  3. infrahub/api/schema.py +1 -1
  4. infrahub/artifacts/models.py +1 -3
  5. infrahub/artifacts/tasks.py +1 -3
  6. infrahub/cli/__init__.py +13 -9
  7. infrahub/cli/constants.py +3 -0
  8. infrahub/cli/db.py +165 -183
  9. infrahub/cli/upgrade.py +146 -0
  10. infrahub/computed_attribute/gather.py +185 -0
  11. infrahub/computed_attribute/models.py +239 -11
  12. infrahub/computed_attribute/tasks.py +77 -442
  13. infrahub/computed_attribute/triggers.py +11 -45
  14. infrahub/config.py +43 -32
  15. infrahub/context.py +14 -0
  16. infrahub/core/account.py +4 -4
  17. infrahub/core/attribute.py +57 -57
  18. infrahub/core/branch/tasks.py +12 -9
  19. infrahub/core/changelog/diff.py +16 -8
  20. infrahub/core/changelog/models.py +189 -26
  21. infrahub/core/constants/__init__.py +5 -1
  22. infrahub/core/constants/infrahubkind.py +2 -0
  23. infrahub/core/constraint/node/runner.py +9 -8
  24. infrahub/core/diff/branch_differ.py +10 -10
  25. infrahub/core/diff/ipam_diff_parser.py +4 -5
  26. infrahub/core/diff/model/diff.py +27 -27
  27. infrahub/core/diff/model/path.py +3 -3
  28. infrahub/core/diff/query/merge.py +20 -17
  29. infrahub/core/diff/query_parser.py +4 -4
  30. infrahub/core/graph/__init__.py +1 -1
  31. infrahub/core/initialization.py +1 -10
  32. infrahub/core/ipam/constants.py +3 -4
  33. infrahub/core/ipam/reconciler.py +12 -12
  34. infrahub/core/ipam/utilization.py +10 -13
  35. infrahub/core/manager.py +34 -34
  36. infrahub/core/merge.py +7 -7
  37. infrahub/core/migrations/__init__.py +2 -3
  38. infrahub/core/migrations/graph/__init__.py +9 -4
  39. infrahub/core/migrations/graph/m017_add_core_profile.py +1 -5
  40. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +4 -4
  41. infrahub/core/migrations/graph/m020_duplicate_edges.py +160 -0
  42. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +51 -0
  43. infrahub/core/migrations/graph/{m020_add_generate_template_attr.py → m022_add_generate_template_attr.py} +3 -3
  44. infrahub/core/migrations/graph/m023_deduplicate_cardinality_one_relationships.py +96 -0
  45. infrahub/core/migrations/query/attribute_add.py +2 -2
  46. infrahub/core/migrations/query/node_duplicate.py +18 -21
  47. infrahub/core/migrations/query/schema_attribute_update.py +2 -2
  48. infrahub/core/migrations/schema/models.py +19 -4
  49. infrahub/core/migrations/schema/tasks.py +2 -2
  50. infrahub/core/migrations/shared.py +16 -16
  51. infrahub/core/models.py +15 -6
  52. infrahub/core/node/__init__.py +29 -28
  53. infrahub/core/node/base.py +2 -4
  54. infrahub/core/node/constraints/attribute_uniqueness.py +2 -2
  55. infrahub/core/node/constraints/grouped_uniqueness.py +99 -47
  56. infrahub/core/node/constraints/interface.py +1 -2
  57. infrahub/core/node/delete_validator.py +3 -5
  58. infrahub/core/node/ipam.py +4 -4
  59. infrahub/core/node/permissions.py +7 -7
  60. infrahub/core/node/resource_manager/ip_address_pool.py +6 -6
  61. infrahub/core/node/resource_manager/ip_prefix_pool.py +6 -6
  62. infrahub/core/node/resource_manager/number_pool.py +3 -3
  63. infrahub/core/path.py +12 -12
  64. infrahub/core/property.py +11 -11
  65. infrahub/core/protocols.py +5 -0
  66. infrahub/core/protocols_base.py +21 -21
  67. infrahub/core/query/__init__.py +33 -33
  68. infrahub/core/query/attribute.py +6 -4
  69. infrahub/core/query/diff.py +3 -3
  70. infrahub/core/query/node.py +82 -32
  71. infrahub/core/query/relationship.py +24 -24
  72. infrahub/core/query/resource_manager.py +2 -0
  73. infrahub/core/query/standard_node.py +3 -3
  74. infrahub/core/query/subquery.py +9 -9
  75. infrahub/core/registry.py +13 -15
  76. infrahub/core/relationship/constraints/count.py +3 -4
  77. infrahub/core/relationship/constraints/peer_kind.py +3 -4
  78. infrahub/core/relationship/constraints/profiles_kind.py +2 -2
  79. infrahub/core/relationship/model.py +40 -46
  80. infrahub/core/schema/attribute_schema.py +9 -9
  81. infrahub/core/schema/basenode_schema.py +93 -44
  82. infrahub/core/schema/computed_attribute.py +3 -3
  83. infrahub/core/schema/definitions/core/__init__.py +13 -19
  84. infrahub/core/schema/definitions/core/account.py +151 -148
  85. infrahub/core/schema/definitions/core/artifact.py +122 -113
  86. infrahub/core/schema/definitions/core/builtin.py +19 -16
  87. infrahub/core/schema/definitions/core/check.py +61 -53
  88. infrahub/core/schema/definitions/core/core.py +17 -0
  89. infrahub/core/schema/definitions/core/generator.py +89 -85
  90. infrahub/core/schema/definitions/core/graphql_query.py +72 -70
  91. infrahub/core/schema/definitions/core/group.py +96 -93
  92. infrahub/core/schema/definitions/core/ipam.py +176 -235
  93. infrahub/core/schema/definitions/core/lineage.py +18 -16
  94. infrahub/core/schema/definitions/core/menu.py +42 -40
  95. infrahub/core/schema/definitions/core/permission.py +144 -142
  96. infrahub/core/schema/definitions/core/profile.py +16 -27
  97. infrahub/core/schema/definitions/core/propose_change.py +88 -79
  98. infrahub/core/schema/definitions/core/propose_change_comment.py +170 -165
  99. infrahub/core/schema/definitions/core/propose_change_validator.py +290 -288
  100. infrahub/core/schema/definitions/core/repository.py +231 -225
  101. infrahub/core/schema/definitions/core/resource_pool.py +156 -166
  102. infrahub/core/schema/definitions/core/template.py +27 -12
  103. infrahub/core/schema/definitions/core/transform.py +85 -76
  104. infrahub/core/schema/definitions/core/webhook.py +127 -101
  105. infrahub/core/schema/definitions/internal.py +16 -16
  106. infrahub/core/schema/dropdown.py +3 -4
  107. infrahub/core/schema/generated/attribute_schema.py +15 -18
  108. infrahub/core/schema/generated/base_node_schema.py +12 -14
  109. infrahub/core/schema/generated/node_schema.py +3 -5
  110. infrahub/core/schema/generated/relationship_schema.py +9 -11
  111. infrahub/core/schema/generic_schema.py +2 -2
  112. infrahub/core/schema/manager.py +20 -9
  113. infrahub/core/schema/node_schema.py +4 -2
  114. infrahub/core/schema/relationship_schema.py +7 -7
  115. infrahub/core/schema/schema_branch.py +276 -138
  116. infrahub/core/schema/schema_branch_computed.py +41 -4
  117. infrahub/core/task/task.py +3 -3
  118. infrahub/core/task/user_task.py +15 -15
  119. infrahub/core/utils.py +20 -18
  120. infrahub/core/validators/__init__.py +1 -3
  121. infrahub/core/validators/aggregated_checker.py +2 -2
  122. infrahub/core/validators/attribute/choices.py +2 -2
  123. infrahub/core/validators/attribute/enum.py +2 -2
  124. infrahub/core/validators/attribute/kind.py +2 -2
  125. infrahub/core/validators/attribute/length.py +2 -2
  126. infrahub/core/validators/attribute/optional.py +2 -2
  127. infrahub/core/validators/attribute/regex.py +2 -2
  128. infrahub/core/validators/attribute/unique.py +2 -2
  129. infrahub/core/validators/checks_runner.py +25 -2
  130. infrahub/core/validators/determiner.py +1 -3
  131. infrahub/core/validators/interface.py +6 -2
  132. infrahub/core/validators/model.py +22 -3
  133. infrahub/core/validators/models/validate_migration.py +17 -4
  134. infrahub/core/validators/node/attribute.py +2 -2
  135. infrahub/core/validators/node/generate_profile.py +2 -2
  136. infrahub/core/validators/node/hierarchy.py +3 -5
  137. infrahub/core/validators/node/inherit_from.py +27 -5
  138. infrahub/core/validators/node/relationship.py +2 -2
  139. infrahub/core/validators/relationship/count.py +4 -4
  140. infrahub/core/validators/relationship/optional.py +2 -2
  141. infrahub/core/validators/relationship/peer.py +2 -2
  142. infrahub/core/validators/shared.py +2 -2
  143. infrahub/core/validators/tasks.py +8 -0
  144. infrahub/core/validators/uniqueness/checker.py +22 -21
  145. infrahub/core/validators/uniqueness/index.py +2 -2
  146. infrahub/core/validators/uniqueness/model.py +11 -11
  147. infrahub/database/__init__.py +26 -22
  148. infrahub/database/metrics.py +7 -1
  149. infrahub/dependencies/builder/constraint/grouped/node_runner.py +1 -3
  150. infrahub/dependencies/component/registry.py +2 -2
  151. infrahub/events/__init__.py +25 -2
  152. infrahub/events/artifact_action.py +13 -25
  153. infrahub/events/branch_action.py +26 -18
  154. infrahub/events/generator.py +71 -0
  155. infrahub/events/group_action.py +10 -24
  156. infrahub/events/models.py +10 -16
  157. infrahub/events/node_action.py +87 -32
  158. infrahub/events/repository_action.py +5 -18
  159. infrahub/events/schema_action.py +4 -9
  160. infrahub/events/utils.py +16 -0
  161. infrahub/events/validator_action.py +55 -0
  162. infrahub/exceptions.py +23 -24
  163. infrahub/generators/models.py +1 -3
  164. infrahub/git/base.py +7 -7
  165. infrahub/git/integrator.py +26 -25
  166. infrahub/git/models.py +22 -9
  167. infrahub/git/repository.py +3 -3
  168. infrahub/git/tasks.py +67 -49
  169. infrahub/git/utils.py +48 -0
  170. infrahub/git/worktree.py +1 -2
  171. infrahub/git_credential/askpass.py +1 -2
  172. infrahub/graphql/analyzer.py +12 -0
  173. infrahub/graphql/app.py +13 -15
  174. infrahub/graphql/context.py +6 -0
  175. infrahub/graphql/initialization.py +3 -0
  176. infrahub/graphql/loaders/node.py +2 -12
  177. infrahub/graphql/loaders/peers.py +77 -0
  178. infrahub/graphql/loaders/shared.py +13 -0
  179. infrahub/graphql/manager.py +13 -10
  180. infrahub/graphql/mutations/artifact_definition.py +5 -5
  181. infrahub/graphql/mutations/computed_attribute.py +4 -5
  182. infrahub/graphql/mutations/graphql_query.py +5 -5
  183. infrahub/graphql/mutations/ipam.py +50 -70
  184. infrahub/graphql/mutations/main.py +164 -141
  185. infrahub/graphql/mutations/menu.py +5 -5
  186. infrahub/graphql/mutations/models.py +2 -4
  187. infrahub/graphql/mutations/node_getter/by_default_filter.py +10 -10
  188. infrahub/graphql/mutations/node_getter/by_hfid.py +1 -3
  189. infrahub/graphql/mutations/node_getter/by_id.py +1 -3
  190. infrahub/graphql/mutations/node_getter/interface.py +1 -2
  191. infrahub/graphql/mutations/proposed_change.py +7 -7
  192. infrahub/graphql/mutations/relationship.py +67 -35
  193. infrahub/graphql/mutations/repository.py +8 -8
  194. infrahub/graphql/mutations/resource_manager.py +3 -3
  195. infrahub/graphql/mutations/schema.py +4 -4
  196. infrahub/graphql/mutations/webhook.py +137 -0
  197. infrahub/graphql/parser.py +4 -4
  198. infrahub/graphql/queries/diff/tree.py +4 -4
  199. infrahub/graphql/queries/ipam.py +2 -2
  200. infrahub/graphql/queries/relationship.py +2 -2
  201. infrahub/graphql/queries/search.py +2 -2
  202. infrahub/graphql/resolvers/many_relationship.py +264 -0
  203. infrahub/graphql/resolvers/resolver.py +13 -110
  204. infrahub/graphql/subscription/graphql_query.py +2 -0
  205. infrahub/graphql/types/event.py +20 -11
  206. infrahub/graphql/types/node.py +2 -2
  207. infrahub/graphql/utils.py +2 -2
  208. infrahub/groups/ancestors.py +29 -0
  209. infrahub/groups/parsers.py +107 -0
  210. infrahub/menu/generator.py +7 -7
  211. infrahub/menu/menu.py +0 -10
  212. infrahub/menu/models.py +117 -16
  213. infrahub/menu/repository.py +111 -0
  214. infrahub/menu/utils.py +5 -8
  215. infrahub/message_bus/messages/__init__.py +1 -11
  216. infrahub/message_bus/messages/check_generator_run.py +2 -0
  217. infrahub/message_bus/messages/finalize_validator_execution.py +3 -0
  218. infrahub/message_bus/messages/request_generatordefinition_check.py +2 -0
  219. infrahub/message_bus/operations/__init__.py +0 -2
  220. infrahub/message_bus/operations/check/generator.py +1 -0
  221. infrahub/message_bus/operations/event/__init__.py +2 -2
  222. infrahub/message_bus/operations/finalize/validator.py +51 -1
  223. infrahub/message_bus/operations/requests/generator_definition.py +19 -19
  224. infrahub/message_bus/operations/requests/proposed_change.py +3 -1
  225. infrahub/pools/number.py +2 -4
  226. infrahub/proposed_change/tasks.py +37 -28
  227. infrahub/pytest_plugin.py +13 -10
  228. infrahub/server.py +1 -2
  229. infrahub/services/adapters/event/__init__.py +1 -1
  230. infrahub/task_manager/event.py +23 -9
  231. infrahub/tasks/artifact.py +2 -4
  232. infrahub/telemetry/__init__.py +0 -0
  233. infrahub/telemetry/constants.py +9 -0
  234. infrahub/telemetry/database.py +86 -0
  235. infrahub/telemetry/models.py +65 -0
  236. infrahub/telemetry/task_manager.py +77 -0
  237. infrahub/{tasks/telemetry.py → telemetry/tasks.py} +49 -56
  238. infrahub/telemetry/utils.py +11 -0
  239. infrahub/trace.py +4 -4
  240. infrahub/transformations/tasks.py +2 -2
  241. infrahub/trigger/catalogue.py +2 -5
  242. infrahub/trigger/constants.py +0 -8
  243. infrahub/trigger/models.py +14 -1
  244. infrahub/trigger/setup.py +90 -0
  245. infrahub/trigger/tasks.py +35 -90
  246. infrahub/utils.py +11 -1
  247. infrahub/validators/__init__.py +0 -0
  248. infrahub/validators/events.py +42 -0
  249. infrahub/validators/tasks.py +41 -0
  250. infrahub/webhook/gather.py +17 -0
  251. infrahub/webhook/models.py +22 -5
  252. infrahub/webhook/tasks.py +44 -19
  253. infrahub/webhook/triggers.py +22 -5
  254. infrahub/workers/infrahub_async.py +2 -2
  255. infrahub/workers/utils.py +2 -2
  256. infrahub/workflows/catalogue.py +28 -20
  257. infrahub/workflows/initialization.py +1 -3
  258. infrahub/workflows/models.py +1 -1
  259. infrahub/workflows/utils.py +10 -1
  260. infrahub_sdk/client.py +27 -8
  261. infrahub_sdk/config.py +3 -0
  262. infrahub_sdk/context.py +13 -0
  263. infrahub_sdk/exceptions.py +6 -0
  264. infrahub_sdk/generator.py +4 -1
  265. infrahub_sdk/graphql.py +45 -13
  266. infrahub_sdk/node.py +69 -20
  267. infrahub_sdk/protocols_base.py +32 -11
  268. infrahub_sdk/query_groups.py +6 -35
  269. infrahub_sdk/schema/__init__.py +55 -26
  270. infrahub_sdk/schema/main.py +8 -0
  271. infrahub_sdk/task/__init__.py +10 -0
  272. infrahub_sdk/task/manager.py +12 -6
  273. infrahub_sdk/testing/schemas/animal.py +9 -0
  274. infrahub_sdk/timestamp.py +12 -4
  275. {infrahub_server-1.2.0b1.dist-info → infrahub_server-1.2.1.dist-info}/METADATA +3 -2
  276. {infrahub_server-1.2.0b1.dist-info → infrahub_server-1.2.1.dist-info}/RECORD +289 -260
  277. {infrahub_server-1.2.0b1.dist-info → infrahub_server-1.2.1.dist-info}/entry_points.txt +1 -0
  278. infrahub_testcontainers/constants.py +2 -0
  279. infrahub_testcontainers/container.py +157 -12
  280. infrahub_testcontainers/docker-compose.test.yml +31 -6
  281. infrahub_testcontainers/helpers.py +18 -73
  282. infrahub_testcontainers/host.py +41 -0
  283. infrahub_testcontainers/measurements.py +93 -0
  284. infrahub_testcontainers/models.py +38 -0
  285. infrahub_testcontainers/performance_test.py +166 -0
  286. infrahub_testcontainers/plugin.py +136 -0
  287. infrahub_testcontainers/prometheus.yml +30 -0
  288. infrahub/message_bus/messages/event_branch_create.py +0 -11
  289. infrahub/message_bus/messages/event_branch_delete.py +0 -11
  290. infrahub/message_bus/messages/event_branch_rebased.py +0 -9
  291. infrahub/message_bus/messages/event_node_mutated.py +0 -15
  292. infrahub/message_bus/messages/event_schema_update.py +0 -9
  293. infrahub/message_bus/operations/event/node.py +0 -20
  294. infrahub/message_bus/operations/event/schema.py +0 -17
  295. infrahub/webhook/constants.py +0 -1
  296. {infrahub_server-1.2.0b1.dist-info → infrahub_server-1.2.1.dist-info}/LICENSE.txt +0 -0
  297. {infrahub_server-1.2.0b1.dist-info → infrahub_server-1.2.1.dist-info}/WHEEL +0 -0
@@ -1,4 +1,4 @@
1
- from typing import TYPE_CHECKING, Any, Optional
1
+ from typing import TYPE_CHECKING, Any, Self
2
2
 
3
3
  from graphene import Boolean, Field, InputObjectType, Mutation, String
4
4
  from graphql import GraphQLResolveInfo
@@ -31,7 +31,7 @@ if TYPE_CHECKING:
31
31
 
32
32
  class InfrahubProposedChangeMutation(InfrahubMutationMixin, Mutation):
33
33
  @classmethod
34
- def __init_subclass_with_meta__(cls, schema: NodeSchema = None, _meta=None, **options):
34
+ def __init_subclass_with_meta__(cls, schema: NodeSchema = None, _meta=None, **options) -> None:
35
35
  # Make sure schema is a valid NodeSchema Node Class
36
36
  if not isinstance(schema, NodeSchema):
37
37
  raise ValueError(f"You need to pass a valid NodeSchema in '{cls.__name__}.Meta', received '{schema}'")
@@ -49,8 +49,8 @@ class InfrahubProposedChangeMutation(InfrahubMutationMixin, Mutation):
49
49
  info: GraphQLResolveInfo,
50
50
  data: InputObjectType,
51
51
  branch: Branch,
52
- database: Optional[InfrahubDatabase] = None, # noqa: ARG003
53
- ):
52
+ database: InfrahubDatabase | None = None, # noqa: ARG003
53
+ ) -> tuple[Node, Self]:
54
54
  graphql_context: GraphqlContext = info.context
55
55
 
56
56
  async with graphql_context.db.start_transaction() as dbt:
@@ -86,9 +86,9 @@ class InfrahubProposedChangeMutation(InfrahubMutationMixin, Mutation):
86
86
  info: GraphQLResolveInfo,
87
87
  data: InputObjectType,
88
88
  branch: Branch,
89
- database: Optional[InfrahubDatabase] = None, # noqa: ARG003
90
- node: Optional[Node] = None, # noqa: ARG003
91
- ):
89
+ database: InfrahubDatabase | None = None, # noqa: ARG003
90
+ node: Node | None = None, # noqa: ARG003
91
+ ) -> tuple[Node, Self]:
92
92
  graphql_context: GraphqlContext = info.context
93
93
 
94
94
  obj = await NodeManager.get_one_by_id_or_default_filter(
@@ -8,14 +8,12 @@ from infrahub_sdk.utils import compare_lists
8
8
 
9
9
  from infrahub import config
10
10
  from infrahub.core.account import GlobalPermission, ObjectPermission
11
- from infrahub.core.changelog.models import NodeChangelog
11
+ from infrahub.core.changelog.models import NodeChangelog, RelationshipChangelogGetter
12
12
  from infrahub.core.constants import (
13
13
  InfrahubKind,
14
- MutationAction,
15
14
  PermissionAction,
16
15
  PermissionDecision,
17
16
  RelationshipCardinality,
18
- RelationshipHierarchyDirection,
19
17
  )
20
18
  from infrahub.core.manager import NodeManager
21
19
  from infrahub.core.query.node import NodeGetKindQuery
@@ -24,14 +22,15 @@ from infrahub.core.query.relationship import (
24
22
  RelationshipPeerData,
25
23
  )
26
24
  from infrahub.core.relationship import Relationship
27
- from infrahub.core.schema import NodeSchema
28
25
  from infrahub.database import retry_db_transaction
29
- from infrahub.events import EventMeta, NodeMutatedEvent
26
+ from infrahub.events import EventMeta
30
27
  from infrahub.events.group_action import GroupMemberAddedEvent, GroupMemberRemovedEvent
31
28
  from infrahub.events.models import EventNode
29
+ from infrahub.events.node_action import NodeUpdatedEvent
32
30
  from infrahub.exceptions import NodeNotFoundError, ValidationError
33
31
  from infrahub.graphql.context import apply_external_context
34
32
  from infrahub.graphql.types.context import ContextInput
33
+ from infrahub.groups.ancestors import collect_ancestors
35
34
  from infrahub.permissions import get_global_permission_for_kind
36
35
 
37
36
  from ..types import RelatedNodeInput
@@ -119,7 +118,12 @@ class RelationshipAdd(Mutation):
119
118
 
120
119
  if config.SETTINGS.broker.enable and graphql_context.background and node_changelog.has_changes:
121
120
  if group_event_type == GroupUpdateType.MEMBERS:
122
- ancestors = await _collect_ancestors(info=info, node_kind=source.get_schema().kind, node_id=source.id)
121
+ ancestors = await collect_ancestors(
122
+ db=graphql_context.db,
123
+ branch=graphql_context.branch,
124
+ node_kind=source.get_schema().kind,
125
+ node_id=source.id,
126
+ )
123
127
  group_add_event = GroupMemberAddedEvent(
124
128
  node_id=source.id,
125
129
  kind=source.get_schema().kind,
@@ -137,7 +141,9 @@ class RelationshipAdd(Mutation):
137
141
  node_kind_map = await node_kind_query.get_node_kind_map()
138
142
 
139
143
  for node_id, node_kind in node_kind_map.items():
140
- ancestors = await _collect_ancestors(info=info, node_kind=node_kind, node_id=node_id)
144
+ ancestors = await collect_ancestors(
145
+ db=graphql_context.db, branch=graphql_context.branch, node_kind=node_kind, node_id=node_id
146
+ )
141
147
  group_add_event = GroupMemberAddedEvent(
142
148
  node_id=node_id,
143
149
  kind=node_kind,
@@ -148,15 +154,33 @@ class RelationshipAdd(Mutation):
148
154
  graphql_context.background.add_task(graphql_context.active_service.event.send, group_add_event)
149
155
 
150
156
  else:
151
- event = NodeMutatedEvent(
157
+ main_event = NodeUpdatedEvent(
152
158
  kind=source.get_schema().kind,
153
159
  node_id=source.id,
154
- data=node_changelog,
155
- action=MutationAction.UPDATED,
160
+ changelog=node_changelog,
156
161
  fields=[relationship_name],
157
162
  meta=EventMeta(branch=graphql_context.branch, context=graphql_context.get_context()),
158
163
  )
159
- graphql_context.background.add_task(graphql_context.active_service.event.send, event)
164
+ relationship_changelogs = RelationshipChangelogGetter(
165
+ db=graphql_context.db, branch=graphql_context.branch
166
+ )
167
+ node_changelogs = await relationship_changelogs.get_changelogs(primary_changelog=node_changelog)
168
+
169
+ events = [main_event]
170
+
171
+ for node_changelog in node_changelogs:
172
+ meta = EventMeta.from_parent(parent=main_event)
173
+ event = NodeUpdatedEvent(
174
+ kind=node_changelog.node_kind,
175
+ node_id=node_changelog.node_id,
176
+ changelog=node_changelog,
177
+ fields=node_changelog.updated_fields,
178
+ meta=meta,
179
+ )
180
+ events.append(event)
181
+
182
+ for event in events:
183
+ graphql_context.background.add_task(graphql_context.active_service.event.send, event)
160
184
 
161
185
  return cls(ok=True)
162
186
 
@@ -216,7 +240,12 @@ class RelationshipRemove(Mutation):
216
240
 
217
241
  if config.SETTINGS.broker.enable and graphql_context.background and node_changelog.has_changes:
218
242
  if group_event_type == GroupUpdateType.MEMBERS:
219
- ancestors = await _collect_ancestors(info=info, node_kind=source.get_schema().kind, node_id=source.id)
243
+ ancestors = await collect_ancestors(
244
+ db=graphql_context.db,
245
+ branch=graphql_context.branch,
246
+ node_kind=source.get_schema().kind,
247
+ node_id=source.id,
248
+ )
220
249
  group_remove_event = GroupMemberRemovedEvent(
221
250
  node_id=source.id,
222
251
  kind=source.get_schema().kind,
@@ -233,7 +262,9 @@ class RelationshipRemove(Mutation):
233
262
  node_kind_map = await node_kind_query.get_node_kind_map()
234
263
 
235
264
  for node_id, node_kind in node_kind_map.items():
236
- ancestors = await _collect_ancestors(info=info, node_kind=node_kind, node_id=node_id)
265
+ ancestors = await collect_ancestors(
266
+ db=graphql_context.db, branch=graphql_context.branch, node_kind=node_kind, node_id=node_id
267
+ )
237
268
  group_remove_event = GroupMemberRemovedEvent(
238
269
  node_id=node_id,
239
270
  kind=node_kind,
@@ -244,15 +275,34 @@ class RelationshipRemove(Mutation):
244
275
  graphql_context.active_service.event.send, group_remove_event
245
276
  )
246
277
  else:
247
- event = NodeMutatedEvent(
278
+ main_event = NodeUpdatedEvent(
248
279
  kind=source.get_schema().kind,
249
280
  node_id=source.id,
250
- data=node_changelog,
251
- action=MutationAction.UPDATED,
281
+ changelog=node_changelog,
252
282
  fields=[relationship_name],
253
283
  meta=EventMeta(branch=graphql_context.branch, context=graphql_context.get_context()),
254
284
  )
255
- graphql_context.background.add_task(graphql_context.active_service.event.send, event)
285
+
286
+ relationship_changelogs = RelationshipChangelogGetter(
287
+ db=graphql_context.db, branch=graphql_context.branch
288
+ )
289
+ node_changelogs = await relationship_changelogs.get_changelogs(primary_changelog=node_changelog)
290
+
291
+ events = [main_event]
292
+
293
+ for node_changelog in node_changelogs:
294
+ meta = EventMeta.from_parent(parent=main_event)
295
+ event = NodeUpdatedEvent(
296
+ kind=node_changelog.node_kind,
297
+ node_id=node_changelog.node_id,
298
+ changelog=node_changelog,
299
+ fields=node_changelog.updated_fields,
300
+ meta=meta,
301
+ )
302
+ events.append(event)
303
+
304
+ for event in events:
305
+ graphql_context.background.add_task(graphql_context.active_service.event.send, event)
256
306
 
257
307
  return cls(ok=True)
258
308
 
@@ -356,24 +406,6 @@ async def _validate_peer_types(
356
406
  )
357
407
 
358
408
 
359
- async def _collect_ancestors(info: GraphQLResolveInfo, node_kind: str, node_id: str) -> list[EventNode]:
360
- graphql_context: GraphqlContext = info.context
361
- schema = graphql_context.db.schema.get(name=node_kind, branch=graphql_context.branch)
362
-
363
- if not isinstance(schema, NodeSchema):
364
- return []
365
-
366
- ancestors = await NodeManager.query_hierarchy(
367
- db=graphql_context.db,
368
- branch=graphql_context.branch,
369
- direction=RelationshipHierarchyDirection.ANCESTORS,
370
- id=node_id,
371
- node_schema=schema,
372
- filters={"id": None},
373
- )
374
- return [EventNode(id=ancestor.get_id(), kind=ancestor.get_kind()) for ancestor in ancestors.values()]
375
-
376
-
377
409
  async def _collect_current_peers(
378
410
  info: GraphQLResolveInfo, data: RelationshipNodesInput, source_node: Node
379
411
  ) -> dict[str, RelationshipPeerData]:
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
- from typing import TYPE_CHECKING, Any, Optional, cast
4
+ from typing import TYPE_CHECKING, Any, Self, cast
5
5
 
6
6
  import httpx
7
7
  from graphene import Boolean, Field, InputObjectType, Mutation, String
@@ -45,7 +45,7 @@ log = get_logger()
45
45
 
46
46
  class InfrahubRepositoryMutation(InfrahubMutationMixin, Mutation):
47
47
  @classmethod
48
- def __init_subclass_with_meta__(cls, schema: Optional[NodeSchema] = None, _meta=None, **options):
48
+ def __init_subclass_with_meta__(cls, schema: NodeSchema | None = None, _meta=None, **options) -> None:
49
49
  # Make sure schema is a valid NodeSchema Node Class
50
50
  if not isinstance(schema, NodeSchema):
51
51
  raise ValueError(f"You need to pass a valid NodeSchema in '{cls.__name__}.Meta', received '{schema}'")
@@ -63,8 +63,8 @@ class InfrahubRepositoryMutation(InfrahubMutationMixin, Mutation):
63
63
  info: GraphQLResolveInfo,
64
64
  data: InputObjectType,
65
65
  branch: Branch,
66
- database: Optional[InfrahubDatabase] = None, # noqa: ARG003
67
- ):
66
+ database: InfrahubDatabase | None = None, # noqa: ARG003
67
+ ) -> tuple[Node, Self]:
68
68
  graphql_context: GraphqlContext = info.context
69
69
 
70
70
  cleanup_payload(data)
@@ -149,9 +149,9 @@ class InfrahubRepositoryMutation(InfrahubMutationMixin, Mutation):
149
149
  info: GraphQLResolveInfo,
150
150
  data: InputObjectType,
151
151
  branch: Branch,
152
- database: Optional[InfrahubDatabase] = None, # noqa: ARG003
153
- node: Optional[Node] = None,
154
- ):
152
+ database: InfrahubDatabase | None = None, # noqa: ARG003
153
+ node: Node | None = None,
154
+ ) -> tuple[Node, Self]:
155
155
  graphql_context: GraphqlContext = info.context
156
156
 
157
157
  cleanup_payload(data)
@@ -219,7 +219,7 @@ def cleanup_payload(data: InputObjectType | dict[str, Any]) -> None:
219
219
  ):
220
220
  url = httpx.URL(data["location"]["value"])
221
221
  if url.host in config.SETTINGS.git.append_git_suffix:
222
- data["location"]["value"] = f'{data["location"]["value"]}.git'
222
+ data["location"]["value"] = f"{data['location']['value']}.git"
223
223
 
224
224
 
225
225
  class ProcessRepository(Mutation):
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
- from graphene import Boolean, Field, InputField, InputObjectType, Int, Mutation, String
5
+ from graphene import Boolean, Field, InputField, InputObjectType, Int, List, Mutation, String
6
6
  from graphene.types.generic import GenericScalar
7
7
  from typing_extensions import Self
8
8
 
@@ -30,7 +30,7 @@ if TYPE_CHECKING:
30
30
 
31
31
  class IPPrefixPoolGetResourceInput(InputObjectType):
32
32
  id = InputField(String(required=False), description="ID of the pool to allocate from")
33
- hfid = InputField(String(required=False), description="HFID of the pool to allocate from")
33
+ hfid = InputField(List(of_type=String, required=False), description="HFID of the pool to allocate from")
34
34
  identifier = InputField(String(required=False), description="Identifier for the allocated resource")
35
35
  prefix_length = InputField(Int(required=False), description="Size of the prefix to allocate")
36
36
  member_type = InputField(String(required=False), description="Type of members for the newly created prefix")
@@ -40,7 +40,7 @@ class IPPrefixPoolGetResourceInput(InputObjectType):
40
40
 
41
41
  class IPAddressPoolGetResourceInput(InputObjectType):
42
42
  id = InputField(String(required=False), description="ID of the pool to allocate from")
43
- hfid = InputField(String(required=False), description="HFID of the pool to allocate from")
43
+ hfid = InputField(List(of_type=String, required=False), description="HFID of the pool to allocate from")
44
44
  identifier = InputField(String(required=False), description="Identifier for the allocated resource")
45
45
  prefix_length = InputField(
46
46
  Int(required=False), description="Size of the prefix mask to allocate on the new IP address"
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Self, Union
3
+ from typing import TYPE_CHECKING, Self
4
4
 
5
5
  from graphene import Boolean, Field, InputObjectType, Mutation, String
6
6
 
@@ -267,21 +267,21 @@ class SchemaEnumRemove(Mutation):
267
267
  return {"ok": True}
268
268
 
269
269
 
270
- def validate_kind_dropdown(kind: Union[GenericSchema, NodeSchema], attribute: str) -> None:
270
+ def validate_kind_dropdown(kind: GenericSchema | NodeSchema, attribute: str) -> None:
271
271
  validate_kind(kind=kind, attribute=attribute)
272
272
  matching_attribute = [attrib for attrib in kind.attributes if attrib.name == attribute]
273
273
  if matching_attribute and matching_attribute[0].kind != "Dropdown":
274
274
  raise ValidationError(f"Attribute {attribute} on {kind.kind} is not a Dropdown")
275
275
 
276
276
 
277
- def validate_kind_enum(kind: Union[GenericSchema, NodeSchema], attribute: str) -> None:
277
+ def validate_kind_enum(kind: GenericSchema | NodeSchema, attribute: str) -> None:
278
278
  validate_kind(kind=kind, attribute=attribute)
279
279
  matching_attribute = [attrib for attrib in kind.attributes if attrib.name == attribute]
280
280
  if not matching_attribute[0].enum:
281
281
  raise ValidationError(f"Attribute {attribute} on {kind.kind} is not an enum")
282
282
 
283
283
 
284
- def validate_kind(kind: Union[GenericSchema, NodeSchema], attribute: str) -> None:
284
+ def validate_kind(kind: GenericSchema | NodeSchema, attribute: str) -> None:
285
285
  if kind.namespace in RESTRICTED_NAMESPACES:
286
286
  raise ValidationError(f"Operation not allowed for {kind.kind} in restricted namespace {kind.namespace}")
287
287
  if attribute not in kind.attribute_names:
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING, Any, Self, cast
5
+
6
+ from graphene import InputObjectType, Mutation
7
+
8
+ from infrahub.core.manager import NodeManager
9
+ from infrahub.core.protocols import CoreWebhook
10
+ from infrahub.core.schema import NodeSchema
11
+ from infrahub.database import retry_db_transaction
12
+ from infrahub.events.utils import get_all_infrahub_node_kind_events
13
+ from infrahub.exceptions import ValidationError
14
+ from infrahub.log import get_logger
15
+
16
+ from .main import InfrahubMutationMixin, InfrahubMutationOptions
17
+
18
+ if TYPE_CHECKING:
19
+ from graphql import GraphQLResolveInfo
20
+
21
+ from infrahub.core.branch import Branch
22
+ from infrahub.core.node import Node
23
+ from infrahub.database import InfrahubDatabase
24
+ from infrahub.graphql.initialization import GraphqlContext
25
+
26
+ log = get_logger()
27
+
28
+
29
+ @dataclass
30
+ class StringValue:
31
+ value: str
32
+
33
+
34
+ @dataclass
35
+ class WebhookCreate:
36
+ event_type: StringValue | None = field(default=None)
37
+ node_kind: StringValue | None = field(default=None)
38
+
39
+
40
+ class WebhookUpdate(WebhookCreate):
41
+ id: str | None = field(default=None)
42
+ hfid: list[str] | None = field(default=None)
43
+
44
+
45
+ class InfrahubWebhookMutation(InfrahubMutationMixin, Mutation):
46
+ _meta: InfrahubMutationOptions
47
+
48
+ @classmethod
49
+ def __init_subclass_with_meta__(
50
+ cls, schema: NodeSchema | None = None, _meta: InfrahubMutationOptions | None = None, **options: dict[str, Any]
51
+ ) -> None:
52
+ # Make sure schema is a valid NodeSchema Node Class
53
+ if not isinstance(schema, NodeSchema):
54
+ raise ValueError(f"You need to pass a valid NodeSchema in '{cls.__name__}.Meta', received '{schema}'")
55
+
56
+ if not _meta:
57
+ _meta = InfrahubMutationOptions(cls)
58
+
59
+ _meta.schema = schema
60
+
61
+ super().__init_subclass_with_meta__(_meta=_meta, **options)
62
+
63
+ @classmethod
64
+ async def mutate_create(
65
+ cls,
66
+ info: GraphQLResolveInfo,
67
+ data: InputObjectType,
68
+ branch: Branch,
69
+ database: InfrahubDatabase | None = None,
70
+ ) -> tuple[Node, Self]:
71
+ graphql_context: GraphqlContext = info.context
72
+
73
+ input_data = cast("WebhookCreate", data)
74
+
75
+ _validate_input(graphql_context=graphql_context, branch=branch, input_data=input_data)
76
+
77
+ obj, result = await super().mutate_create(info=info, data=data, branch=branch, database=database)
78
+
79
+ return obj, result
80
+
81
+ @classmethod
82
+ @retry_db_transaction(name="object_update")
83
+ async def mutate_update(
84
+ cls,
85
+ info: GraphQLResolveInfo,
86
+ data: InputObjectType,
87
+ branch: Branch,
88
+ database: InfrahubDatabase | None = None,
89
+ node: Node | None = None,
90
+ ) -> tuple[Node, Self]:
91
+ graphql_context: GraphqlContext = info.context
92
+
93
+ db = database or graphql_context.db
94
+
95
+ input_data = cast("WebhookUpdate", data)
96
+
97
+ _validate_input(graphql_context=graphql_context, branch=branch, input_data=input_data)
98
+
99
+ obj = node or await NodeManager.find_object(
100
+ db=db,
101
+ kind=cls._meta.active_schema.kind,
102
+ id=input_data.id,
103
+ hfid=input_data.hfid,
104
+ branch=branch,
105
+ )
106
+
107
+ webhook = cast(CoreWebhook, obj)
108
+
109
+ event_type = input_data.event_type.value if input_data.event_type else webhook.event_type.value.value
110
+ node_kind = input_data.node_kind.value if input_data.node_kind else webhook.node_kind.value
111
+
112
+ if event_type and node_kind:
113
+ updated_data = WebhookUpdate(
114
+ event_type=StringValue(value=event_type), node_kind=StringValue(value=node_kind)
115
+ )
116
+ _validate_input(graphql_context=graphql_context, branch=branch, input_data=updated_data)
117
+
118
+ try:
119
+ obj, result = await cls._call_mutate_update(info=info, data=data, db=db, branch=branch, obj=obj)
120
+ except ValidationError as exc:
121
+ raise ValueError(str(exc)) from exc
122
+
123
+ return obj, result
124
+
125
+
126
+ def _validate_input(graphql_context: GraphqlContext, branch: Branch, input_data: WebhookCreate) -> None:
127
+ if input_data.node_kind:
128
+ # Validate that the requested node_kind exists, will raise an error if not
129
+ graphql_context.db.schema.get(name=input_data.node_kind.value, branch=branch, duplicate=False)
130
+
131
+ if input_data.event_type:
132
+ # If the event type is not set then all events are applicable, this will mean that some events
133
+ # would be filtered out, as they won't match the node.
134
+ if input_data.event_type.value not in get_all_infrahub_node_kind_events():
135
+ raise ValidationError(
136
+ input_value=f"Defining a node_kind is not valid for {input_data.event_type.value} events"
137
+ )
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass, field
4
- from typing import TYPE_CHECKING, Optional
4
+ from typing import TYPE_CHECKING
5
5
 
6
6
  from graphql.language import (
7
7
  DirectiveNode,
@@ -172,13 +172,13 @@ class GraphQLExtractor:
172
172
  return fields
173
173
 
174
174
  async def extract_fields(
175
- self, selection_set: Optional[SelectionSetNode], path: str = "/"
176
- ) -> Optional[dict[str, Optional[dict]]]:
175
+ self, selection_set: SelectionSetNode | None, path: str = "/"
176
+ ) -> dict[str, dict | None] | None:
177
177
  """Extract fields and apply Directives"""
178
178
  if not selection_set:
179
179
  return None
180
180
 
181
- fields: dict[str, Optional[dict]] = {}
181
+ fields: dict[str, dict | None] = {}
182
182
  for node in selection_set.selections:
183
183
  sub_selection_set = getattr(node, "selection_set", None)
184
184
  if isinstance(node, FieldNode):
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any, Optional, Union
3
+ from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from graphene import Argument, Boolean, DateTime, Field, InputObjectType, Int, List, NonNull, ObjectType, String
6
6
  from graphene import Enum as GrapheneEnum
@@ -324,7 +324,7 @@ class DiffTreeResolver:
324
324
 
325
325
  async def to_graphql(
326
326
  self, fields: dict[str, dict], diff_object: Any | None
327
- ) -> Optional[Union[list[dict[str, Any]], dict[str, Any]]]:
327
+ ) -> list[dict[str, Any]] | dict[str, Any] | None:
328
328
  if diff_object is None:
329
329
  return None
330
330
  if isinstance(diff_object, list):
@@ -387,7 +387,7 @@ class DiffTreeResolver:
387
387
  root_node_uuids: list[str] | None = None,
388
388
  limit: int | None = None,
389
389
  offset: int | None = None,
390
- ) -> Optional[Union[list[dict[str, Any]], dict[str, Any]]]:
390
+ ) -> list[dict[str, Any]] | dict[str, Any] | None:
391
391
  component_registry = get_component_registry()
392
392
  graphql_context: GraphqlContext = info.context
393
393
  base_branch = await registry.get_branch(db=graphql_context.db, branch=registry.default_branch)
@@ -459,7 +459,7 @@ class DiffTreeResolver:
459
459
  from_time: datetime | None = None,
460
460
  to_time: datetime | None = None,
461
461
  filters: dict | None = None,
462
- ) -> Optional[Union[list[dict[str, Any]], dict[str, Any]]]:
462
+ ) -> list[dict[str, Any]] | dict[str, Any] | None:
463
463
  component_registry = get_component_registry()
464
464
  graphql_context: GraphqlContext = info.context
465
465
  base_branch = await registry.get_branch(db=graphql_context.db, branch=registry.default_branch)
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ipaddress
4
- from typing import TYPE_CHECKING, Optional
4
+ from typing import TYPE_CHECKING
5
5
 
6
6
  from graphene import Field, Int, ObjectType, String
7
7
  from netaddr import IPSet
@@ -27,7 +27,7 @@ class IPAddressGetNextAvailable(ObjectType):
27
27
  root: dict, # noqa: ARG004
28
28
  info: GraphQLResolveInfo,
29
29
  prefix_id: str,
30
- prefix_length: Optional[int] = None,
30
+ prefix_length: int | None = None,
31
31
  ) -> dict[str, str]:
32
32
  graphql_context: GraphqlContext = info.context
33
33
 
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any, Optional
3
+ from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from graphene import Field, Int, List, NonNull, ObjectType, String
6
6
  from infrahub_sdk.utils import extract_fields_first_node
@@ -25,7 +25,7 @@ class Relationships(ObjectType):
25
25
  ids: list[str],
26
26
  limit: int = 10,
27
27
  offset: int = 0,
28
- excluded_namespaces: Optional[list[str]] = None,
28
+ excluded_namespaces: list[str] | None = None,
29
29
  ) -> dict[str, Any]:
30
30
  graphql_context: GraphqlContext = info.context
31
31
 
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ipaddress
4
- from typing import TYPE_CHECKING, Any, Optional
4
+ from typing import TYPE_CHECKING, Any
5
5
 
6
6
  from graphene import Boolean, Field, Int, List, NonNull, ObjectType, String
7
7
  from infrahub_sdk.utils import extract_fields_first_node, is_valid_uuid
@@ -110,7 +110,7 @@ async def search_resolver(
110
110
  fields = await extract_fields_first_node(info)
111
111
 
112
112
  if is_valid_uuid(q):
113
- matching: Optional[CoreNode] = await NodeManager.get_one(
113
+ matching: CoreNode | None = await NodeManager.get_one(
114
114
  db=graphql_context.db, branch=graphql_context.branch, at=graphql_context.at, id=q
115
115
  )
116
116
  if matching: