infrahub-server 1.6.3__py3-none-any.whl → 1.7.0b0__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 (161) hide show
  1. infrahub/actions/tasks.py +4 -2
  2. infrahub/api/schema.py +3 -1
  3. infrahub/artifacts/tasks.py +1 -0
  4. infrahub/auth.py +2 -2
  5. infrahub/cli/db.py +6 -6
  6. infrahub/computed_attribute/gather.py +3 -4
  7. infrahub/computed_attribute/tasks.py +23 -6
  8. infrahub/config.py +8 -0
  9. infrahub/constants/enums.py +12 -0
  10. infrahub/core/account.py +5 -8
  11. infrahub/core/attribute.py +106 -108
  12. infrahub/core/branch/models.py +44 -71
  13. infrahub/core/branch/tasks.py +5 -3
  14. infrahub/core/changelog/diff.py +1 -20
  15. infrahub/core/changelog/models.py +0 -7
  16. infrahub/core/constants/__init__.py +17 -0
  17. infrahub/core/constants/database.py +0 -1
  18. infrahub/core/constants/schema.py +0 -1
  19. infrahub/core/convert_object_type/repository_conversion.py +3 -4
  20. infrahub/core/diff/data_check_synchronizer.py +3 -2
  21. infrahub/core/diff/enricher/cardinality_one.py +1 -1
  22. infrahub/core/diff/merger/merger.py +27 -1
  23. infrahub/core/diff/merger/serializer.py +3 -10
  24. infrahub/core/diff/model/diff.py +1 -1
  25. infrahub/core/diff/query/merge.py +376 -135
  26. infrahub/core/graph/__init__.py +1 -1
  27. infrahub/core/graph/constraints.py +2 -2
  28. infrahub/core/graph/schema.py +2 -12
  29. infrahub/core/manager.py +132 -126
  30. infrahub/core/metadata/__init__.py +0 -0
  31. infrahub/core/metadata/interface.py +37 -0
  32. infrahub/core/metadata/model.py +31 -0
  33. infrahub/core/metadata/query/__init__.py +0 -0
  34. infrahub/core/metadata/query/node_metadata.py +301 -0
  35. infrahub/core/migrations/graph/__init__.py +4 -0
  36. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +3 -8
  37. infrahub/core/migrations/graph/m017_add_core_profile.py +5 -2
  38. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +2 -1
  39. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +0 -10
  40. infrahub/core/migrations/graph/m020_duplicate_edges.py +0 -8
  41. infrahub/core/migrations/graph/m025_uniqueness_nulls.py +2 -1
  42. infrahub/core/migrations/graph/m026_0000_prefix_fix.py +2 -1
  43. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +0 -1
  44. infrahub/core/migrations/graph/m031_check_number_attributes.py +2 -2
  45. infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +2 -1
  46. infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +38 -0
  47. infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +168 -0
  48. infrahub/core/migrations/query/attribute_add.py +17 -6
  49. infrahub/core/migrations/query/attribute_remove.py +19 -5
  50. infrahub/core/migrations/query/attribute_rename.py +21 -5
  51. infrahub/core/migrations/query/node_duplicate.py +19 -4
  52. infrahub/core/migrations/schema/attribute_kind_update.py +25 -7
  53. infrahub/core/migrations/schema/attribute_supports_profile.py +3 -1
  54. infrahub/core/migrations/schema/models.py +3 -0
  55. infrahub/core/migrations/schema/node_attribute_add.py +4 -1
  56. infrahub/core/migrations/schema/node_remove.py +24 -2
  57. infrahub/core/migrations/schema/tasks.py +4 -1
  58. infrahub/core/migrations/shared.py +13 -6
  59. infrahub/core/models.py +6 -6
  60. infrahub/core/node/__init__.py +156 -57
  61. infrahub/core/node/create.py +7 -3
  62. infrahub/core/node/standard.py +100 -14
  63. infrahub/core/property.py +0 -1
  64. infrahub/core/protocols_base.py +6 -2
  65. infrahub/core/query/__init__.py +6 -7
  66. infrahub/core/query/attribute.py +161 -46
  67. infrahub/core/query/branch.py +57 -69
  68. infrahub/core/query/diff.py +4 -4
  69. infrahub/core/query/node.py +618 -180
  70. infrahub/core/query/relationship.py +449 -300
  71. infrahub/core/query/standard_node.py +25 -5
  72. infrahub/core/query/utils.py +2 -4
  73. infrahub/core/relationship/constraints/profiles_removal.py +168 -0
  74. infrahub/core/relationship/model.py +293 -139
  75. infrahub/core/schema/attribute_parameters.py +1 -28
  76. infrahub/core/schema/attribute_schema.py +17 -11
  77. infrahub/core/schema/manager.py +63 -43
  78. infrahub/core/schema/relationship_schema.py +6 -2
  79. infrahub/core/schema/schema_branch.py +48 -76
  80. infrahub/core/task/task.py +4 -2
  81. infrahub/core/utils.py +0 -22
  82. infrahub/core/validators/attribute/kind.py +2 -5
  83. infrahub/core/validators/determiner.py +3 -3
  84. infrahub/database/__init__.py +3 -3
  85. infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
  86. infrahub/dependencies/builder/constraint/relationship_manager/profiles_removal.py +8 -0
  87. infrahub/dependencies/registry.py +2 -0
  88. infrahub/display_labels/tasks.py +12 -3
  89. infrahub/git/integrator.py +18 -18
  90. infrahub/git/tasks.py +1 -1
  91. infrahub/graphql/app.py +2 -2
  92. infrahub/graphql/constants.py +3 -0
  93. infrahub/graphql/context.py +1 -1
  94. infrahub/graphql/initialization.py +11 -0
  95. infrahub/graphql/loaders/account.py +134 -0
  96. infrahub/graphql/loaders/node.py +5 -12
  97. infrahub/graphql/loaders/peers.py +5 -7
  98. infrahub/graphql/manager.py +158 -18
  99. infrahub/graphql/metadata.py +91 -0
  100. infrahub/graphql/models.py +33 -3
  101. infrahub/graphql/mutations/account.py +5 -5
  102. infrahub/graphql/mutations/attribute.py +0 -2
  103. infrahub/graphql/mutations/branch.py +9 -5
  104. infrahub/graphql/mutations/computed_attribute.py +1 -1
  105. infrahub/graphql/mutations/display_label.py +1 -1
  106. infrahub/graphql/mutations/hfid.py +1 -1
  107. infrahub/graphql/mutations/ipam.py +4 -6
  108. infrahub/graphql/mutations/main.py +9 -4
  109. infrahub/graphql/mutations/profile.py +16 -22
  110. infrahub/graphql/mutations/proposed_change.py +4 -4
  111. infrahub/graphql/mutations/relationship.py +40 -10
  112. infrahub/graphql/mutations/repository.py +14 -12
  113. infrahub/graphql/mutations/schema.py +2 -2
  114. infrahub/graphql/queries/branch.py +62 -6
  115. infrahub/graphql/queries/diff/tree.py +5 -5
  116. infrahub/graphql/resolvers/account_metadata.py +84 -0
  117. infrahub/graphql/resolvers/ipam.py +6 -8
  118. infrahub/graphql/resolvers/many_relationship.py +77 -35
  119. infrahub/graphql/resolvers/resolver.py +16 -12
  120. infrahub/graphql/resolvers/single_relationship.py +87 -23
  121. infrahub/graphql/subscription/graphql_query.py +2 -0
  122. infrahub/graphql/types/__init__.py +0 -1
  123. infrahub/graphql/types/attribute.py +10 -5
  124. infrahub/graphql/types/branch.py +40 -53
  125. infrahub/graphql/types/enums.py +3 -0
  126. infrahub/graphql/types/metadata.py +28 -0
  127. infrahub/graphql/types/node.py +22 -2
  128. infrahub/graphql/types/relationship.py +10 -2
  129. infrahub/graphql/types/standard_node.py +4 -3
  130. infrahub/hfid/tasks.py +12 -3
  131. infrahub/profiles/gather.py +56 -0
  132. infrahub/profiles/mandatory_fields_checker.py +116 -0
  133. infrahub/profiles/models.py +66 -0
  134. infrahub/profiles/node_applier.py +153 -12
  135. infrahub/profiles/queries/get_profile_data.py +143 -31
  136. infrahub/profiles/tasks.py +79 -27
  137. infrahub/profiles/triggers.py +22 -0
  138. infrahub/proposed_change/tasks.py +4 -1
  139. infrahub/tasks/artifact.py +1 -0
  140. infrahub/transformations/tasks.py +2 -2
  141. infrahub/trigger/catalogue.py +2 -0
  142. infrahub/trigger/models.py +1 -0
  143. infrahub/trigger/setup.py +3 -3
  144. infrahub/trigger/tasks.py +3 -0
  145. infrahub/validators/tasks.py +1 -0
  146. infrahub/webhook/models.py +1 -1
  147. infrahub/webhook/tasks.py +1 -1
  148. infrahub/workers/dependencies.py +9 -3
  149. infrahub/workers/infrahub_async.py +13 -4
  150. infrahub/workflows/catalogue.py +19 -0
  151. infrahub_sdk/node/constants.py +1 -0
  152. infrahub_sdk/node/related_node.py +13 -4
  153. infrahub_sdk/node/relationship.py +8 -0
  154. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/METADATA +17 -16
  155. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/RECORD +161 -143
  156. infrahub_testcontainers/container.py +3 -3
  157. infrahub_testcontainers/docker-compose-cluster.test.yml +7 -7
  158. infrahub_testcontainers/docker-compose.test.yml +13 -5
  159. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/WHEEL +0 -0
  160. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/entry_points.txt +0 -0
  161. {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0b0.dist-info}/licenses/LICENSE.txt +0 -0
@@ -1,9 +1,11 @@
1
+ # This entire file and possibly everything in this module should be deleted. - Patrick (2025-12-11)
2
+
1
3
  from typing import Any
2
4
 
3
5
  from pydantic import ConfigDict, Field
4
6
 
5
7
  from infrahub.core.constants import TaskConclusion
6
- from infrahub.core.node.standard import StandardNode
8
+ from infrahub.core.node.standard import StandardNode, StandardNodeQueryFields
7
9
  from infrahub.core.protocols import CoreNode
8
10
  from infrahub.core.query.standard_node import StandardNodeQuery
9
11
  from infrahub.core.query.task import TaskNodeCreateQuery, TaskNodeQuery, TaskNodeQueryWithLogs
@@ -70,7 +72,7 @@ class Task(StandardNode):
70
72
  logs = [
71
73
  {
72
74
  "node": await TaskLog.from_db(result, extras={"task_id": task_result.get("uuid")}).to_graphql(
73
- fields=log_fields
75
+ fields=StandardNodeQueryFields(node=log_fields)
74
76
  )
75
77
  }
76
78
  for result in logs_results
infrahub/core/utils.py CHANGED
@@ -62,28 +62,6 @@ async def delete_all_relationships_for_branch(branch_name: str, db: InfrahubData
62
62
  await db.execute_query(query=query, params=params, name="delete_all_relationships_for_branch")
63
63
 
64
64
 
65
- async def update_relationships_to(
66
- ids: list[str], db: InfrahubDatabase, to: Timestamp | None = None
67
- ) -> list[Record] | None:
68
- """Update the "to" field on one or multiple relationships."""
69
- if not ids:
70
- return None
71
-
72
- to = Timestamp(to)
73
-
74
- query = """
75
- MATCH ()-[r]->()
76
- WHERE %(id_func)s(r) IN $ids
77
- AND r.to IS NULL
78
- SET r.to = $to
79
- RETURN %(id_func)s(r)
80
- """ % {"id_func": db.get_id_function_name()}
81
-
82
- params = {"to": to.to_string(), "ids": [db.to_database_id(_id) for _id in ids]}
83
-
84
- return await db.execute_query(query=query, params=params, name="update_relationships_to")
85
-
86
-
87
65
  async def get_paths_between_nodes(
88
66
  db: InfrahubDatabase,
89
67
  source_id: str,
@@ -36,7 +36,7 @@ class AttributeKindUpdateValidatorQuery(AttributeSchemaValidatorQuery):
36
36
  self.params["null_value"] = NULL_VALUE
37
37
 
38
38
  query = """
39
- MATCH (n:%(node_kinds)s)
39
+ MATCH p = (n:%(node_kind)s)
40
40
  CALL (n) {
41
41
  MATCH path = (root:Root)<-[rr:IS_PART_OF]-(n)-[ra:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name } )-[rv:HAS_VALUE]-(av:AttributeValue)
42
42
  WHERE all(
@@ -51,10 +51,7 @@ class AttributeKindUpdateValidatorQuery(AttributeSchemaValidatorQuery):
51
51
  WHERE all(r in relationships(full_path) WHERE r.status = "active")
52
52
  AND attribute_value IS NOT NULL
53
53
  AND attribute_value <> $null_value
54
- """ % {
55
- "branch_filter": branch_filter,
56
- "node_kinds": f"{self.node_schema.kind}|Profile{self.node_schema.kind}|Template{self.node_schema.kind}",
57
- }
54
+ """ % {"branch_filter": branch_filter, "node_kind": self.node_schema.kind}
58
55
 
59
56
  self.add_to_query(query)
60
57
  self.return_labels = ["node.uuid", "attribute_value", "value_relationship.branch as value_branch"]
@@ -97,7 +97,7 @@ class ConstraintValidatorDeterminer:
97
97
  self, schema: MainSchemaTypes
98
98
  ) -> list[SchemaUpdateConstraintInfo]:
99
99
  constraints: list[SchemaUpdateConstraintInfo] = []
100
- for prop_name, prop_field_info in schema.model_fields.items():
100
+ for prop_name, prop_field_info in schema.__class__.model_fields.items():
101
101
  if (
102
102
  prop_name in ["attributes", "relationships"]
103
103
  or not prop_field_info.json_schema_extra
@@ -158,10 +158,10 @@ class ConstraintValidatorDeterminer:
158
158
  ) -> list[SchemaUpdateConstraintInfo]:
159
159
  constraints: list[SchemaUpdateConstraintInfo] = []
160
160
  prop_details_list: list[tuple[str, FieldInfo, Any]] = []
161
- for p_name, p_info in field.model_fields.items():
161
+ for p_name, p_info in field.__class__.model_fields.items():
162
162
  p_value = getattr(field, p_name)
163
163
  if isinstance(p_value, AttributeParameters):
164
- for parameter_name, parameter_field_info in p_value.model_fields.items():
164
+ for parameter_name, parameter_field_info in p_value.__class__.model_fields.items():
165
165
  parameter_value = getattr(p_value, parameter_name)
166
166
  prop_details_list.append((f"{p_name}.{parameter_name}", parameter_field_info, parameter_value))
167
167
  else:
@@ -13,7 +13,7 @@ from neo4j import (
13
13
  AsyncResult,
14
14
  AsyncSession,
15
15
  AsyncTransaction,
16
- NotificationDisabledCategory,
16
+ NotificationDisabledClassification,
17
17
  NotificationMinimumSeverity,
18
18
  Query,
19
19
  Record,
@@ -492,8 +492,8 @@ async def get_db(retry: int = 0) -> AsyncDriver:
492
492
  auth=(config.SETTINGS.database.username, config.SETTINGS.database.password),
493
493
  encrypted=config.SETTINGS.database.tls_enabled,
494
494
  trusted_certificates=trusted_certificates,
495
- notifications_disabled_categories=[
496
- NotificationDisabledCategory.UNRECOGNIZED,
495
+ notifications_disabled_classifications=[
496
+ NotificationDisabledClassification.UNRECOGNIZED,
497
497
  ],
498
498
  notifications_min_severity=NotificationMinimumSeverity.WARNING,
499
499
  )
@@ -7,6 +7,7 @@ from ..relationship_manager.peer_kind import RelationshipPeerKindConstraintDepen
7
7
  from ..relationship_manager.peer_parent import RelationshipPeerParentConstraintDependency
8
8
  from ..relationship_manager.peer_relatives import RelationshipPeerRelativesConstraintDependency
9
9
  from ..relationship_manager.profiles_kind import RelationshipProfilesKindConstraintDependency
10
+ from ..relationship_manager.profiles_removal import RelationshipProfileRemovalConstraintDependency
10
11
 
11
12
 
12
13
  class NodeConstraintRunnerDependency(DependencyBuilder[NodeConstraintRunner]):
@@ -20,6 +21,7 @@ class NodeConstraintRunnerDependency(DependencyBuilder[NodeConstraintRunner]):
20
21
  RelationshipPeerKindConstraintDependency.build(context=context),
21
22
  RelationshipCountConstraintDependency.build(context=context),
22
23
  RelationshipProfilesKindConstraintDependency.build(context=context),
24
+ RelationshipProfileRemovalConstraintDependency.build(context=context),
23
25
  RelationshipPeerParentConstraintDependency.build(context=context),
24
26
  RelationshipPeerRelativesConstraintDependency.build(context=context),
25
27
  ],
@@ -0,0 +1,8 @@
1
+ from infrahub.core.relationship.constraints.profiles_removal import RelationshipProfileRemovalConstraint
2
+ from infrahub.dependencies.interface import DependencyBuilder, DependencyBuilderContext
3
+
4
+
5
+ class RelationshipProfileRemovalConstraintDependency(DependencyBuilder[RelationshipProfileRemovalConstraint]):
6
+ @classmethod
7
+ def build(cls, context: DependencyBuilderContext) -> RelationshipProfileRemovalConstraint:
8
+ return RelationshipProfileRemovalConstraint(db=context.db, branch=context.branch)
@@ -6,6 +6,7 @@ from .builder.constraint.relationship_manager.peer_kind import RelationshipPeerK
6
6
  from .builder.constraint.relationship_manager.peer_parent import RelationshipPeerParentConstraintDependency
7
7
  from .builder.constraint.relationship_manager.peer_relatives import RelationshipPeerRelativesConstraintDependency
8
8
  from .builder.constraint.relationship_manager.profiles_kind import RelationshipProfilesKindConstraintDependency
9
+ from .builder.constraint.relationship_manager.profiles_removal import RelationshipProfileRemovalConstraintDependency
9
10
  from .builder.constraint.schema.aggregated import AggregatedSchemaConstraintsDependency
10
11
  from .builder.constraint.schema.attribute_regex import SchemaAttributeRegexConstraintDependency
11
12
  from .builder.constraint.schema.attribute_uniqueness import SchemaAttributeUniqueConstraintDependency
@@ -38,6 +39,7 @@ def build_component_registry() -> ComponentDependencyRegistry:
38
39
  component_registry.track_dependency(NodeGroupedUniquenessConstraintDependency)
39
40
  component_registry.track_dependency(RelationshipCountConstraintDependency)
40
41
  component_registry.track_dependency(RelationshipProfilesKindConstraintDependency)
42
+ component_registry.track_dependency(RelationshipProfileRemovalConstraintDependency)
41
43
  component_registry.track_dependency(RelationshipPeerKindConstraintDependency)
42
44
  component_registry.track_dependency(RelationshipPeerParentConstraintDependency)
43
45
  component_registry.track_dependency(RelationshipPeerRelativesConstraintDependency)
@@ -21,9 +21,11 @@ UPDATE_DISPLAY_LABEL = """
21
21
  mutation UpdateDisplayLabel(
22
22
  $id: String!,
23
23
  $kind: String!,
24
- $value: String!
24
+ $value: String!,
25
+ $context_account_id: String!
25
26
  ) {
26
27
  InfrahubUpdateDisplayLabel(
28
+ context: {account: {id: $context_account_id}},
27
29
  data: {id: $id, value: $value, kind: $kind}
28
30
  ) {
29
31
  ok
@@ -41,6 +43,7 @@ async def display_label_jinja2_update_value(
41
43
  obj: DisplayLabelJinja2GraphQLResponse,
42
44
  node_kind: str,
43
45
  template: Jinja2Template,
46
+ context: InfrahubContext,
44
47
  ) -> None:
45
48
  log = get_run_logger()
46
49
  client = get_client()
@@ -55,7 +58,12 @@ async def display_label_jinja2_update_value(
55
58
  try:
56
59
  await client.execute_graphql(
57
60
  query=UPDATE_DISPLAY_LABEL,
58
- variables={"id": obj.node_id, "kind": node_kind, "value": value},
61
+ variables={
62
+ "id": obj.node_id,
63
+ "kind": node_kind,
64
+ "value": value,
65
+ "context_account_id": context.account.account_id,
66
+ },
59
67
  branch_name=branch_name,
60
68
  )
61
69
  log.info(f"Updating {node_kind}.display_label='{value}' ({obj.node_id})")
@@ -74,7 +82,7 @@ async def process_display_label(
74
82
  node_kind: str,
75
83
  object_id: str,
76
84
  target_kind: str,
77
- context: InfrahubContext, # noqa: ARG001
85
+ context: InfrahubContext,
78
86
  ) -> None:
79
87
  log = get_run_logger()
80
88
  client = get_client()
@@ -114,6 +122,7 @@ async def process_display_label(
114
122
  obj=node,
115
123
  node_kind=node_schema.kind,
116
124
  template=jinja_template,
125
+ context=context,
117
126
  )
118
127
 
119
128
  _ = [response async for _, response in batch.execute()]
@@ -180,31 +180,31 @@ class InfrahubRepositoryIntegrator(InfrahubRepositoryBase):
180
180
  self.create_commit_worktree(commit)
181
181
  await self._update_sync_status(branch_name=infrahub_branch_name, status=RepositorySyncStatus.SYNCING)
182
182
 
183
- config_file = await self.get_repository_config(branch_name=infrahub_branch_name, commit=commit) # type: ignore[misc]
183
+ config_file = await self.get_repository_config(branch_name=infrahub_branch_name, commit=commit) # type: ignore[call-overload]
184
184
  sync_status = RepositorySyncStatus.IN_SYNC if config_file else RepositorySyncStatus.ERROR_IMPORT
185
185
 
186
186
  error: Exception | None = None
187
187
 
188
188
  try:
189
189
  if config_file:
190
- await self.import_schema_files(branch_name=infrahub_branch_name, commit=commit, config_file=config_file) # type: ignore[misc]
190
+ await self.import_schema_files(branch_name=infrahub_branch_name, commit=commit, config_file=config_file) # type: ignore[call-overload]
191
191
  await self.import_all_graphql_query(
192
192
  branch_name=infrahub_branch_name, commit=commit, config_file=config_file
193
- ) # type: ignore[misc]
193
+ ) # type: ignore[call-overload]
194
194
  await self.import_objects(
195
195
  branch_name=infrahub_branch_name,
196
196
  commit=commit,
197
197
  config_file=config_file,
198
- ) # type: ignore[misc]
199
- await self.import_all_python_files( # type: ignore[call-overload]
198
+ ) # type: ignore[call-overload]
199
+ await self.import_all_python_files(
200
200
  branch_name=infrahub_branch_name, commit=commit, config_file=config_file
201
- ) # type: ignore[misc]
201
+ ) # type: ignore[call-overload]
202
202
  await self.import_jinja2_transforms(
203
203
  branch_name=infrahub_branch_name, commit=commit, config_file=config_file
204
- ) # type: ignore[misc]
204
+ ) # type: ignore[call-overload]
205
205
  await self.import_artifact_definitions(
206
206
  branch_name=infrahub_branch_name, commit=commit, config_file=config_file
207
- ) # type: ignore[misc]
207
+ ) # type: ignore[call-overload]
208
208
 
209
209
  except Exception as exc:
210
210
  sync_status = RepositorySyncStatus.ERROR_IMPORT
@@ -634,7 +634,7 @@ class InfrahubRepositoryIntegrator(InfrahubRepositoryBase):
634
634
  module=module,
635
635
  file_path=file_info.relative_path_file,
636
636
  check_definition=check,
637
- ) # type: ignore[misc]
637
+ ) # type: ignore[call-overload]
638
638
  )
639
639
 
640
640
  local_check_definitions = {check.name: check for check in checks}
@@ -797,7 +797,7 @@ class InfrahubRepositoryIntegrator(InfrahubRepositoryBase):
797
797
  module=module,
798
798
  file_path=file_info.relative_path_file,
799
799
  transform=transform,
800
- ) # type: ignore[misc]
800
+ ) # type: ignore[call-overload]
801
801
  )
802
802
 
803
803
  local_transform_definitions = {transform.name: transform for transform in transforms}
@@ -1156,9 +1156,9 @@ class InfrahubRepositoryIntegrator(InfrahubRepositoryBase):
1156
1156
  ) -> None:
1157
1157
  await add_tags(branches=[branch_name], nodes=[str(self.id)])
1158
1158
 
1159
- await self.import_python_check_definitions(branch_name=branch_name, commit=commit, config_file=config_file) # type: ignore[misc]
1160
- await self.import_python_transforms(branch_name=branch_name, commit=commit, config_file=config_file) # type: ignore[misc]
1161
- await self.import_generator_definitions(branch_name=branch_name, commit=commit, config_file=config_file) # type: ignore[misc]
1159
+ await self.import_python_check_definitions(branch_name=branch_name, commit=commit, config_file=config_file) # type: ignore[call-overload]
1160
+ await self.import_python_transforms(branch_name=branch_name, commit=commit, config_file=config_file) # type: ignore[call-overload]
1161
+ await self.import_generator_definitions(branch_name=branch_name, commit=commit, config_file=config_file) # type: ignore[call-overload]
1162
1162
 
1163
1163
  @task(name="jinja2-template-render", task_run_name="Render Jinja2 template", cache_policy=NONE)
1164
1164
  async def render_jinja2_template(self, commit: str, location: str, data: dict) -> str:
@@ -1324,7 +1324,7 @@ class InfrahubRepositoryIntegrator(InfrahubRepositoryBase):
1324
1324
  if transformation.typename == InfrahubKind.TRANSFORMJINJA2:
1325
1325
  artifact_content = await self.render_jinja2_template.with_options(
1326
1326
  timeout_seconds=transformation.timeout.value
1327
- )(commit=commit, location=transformation.template_path.value, data=response) # type: ignore[misc]
1327
+ )(commit=commit, location=transformation.template_path.value, data=response) # type: ignore[call-overload]
1328
1328
  elif transformation.typename == InfrahubKind.TRANSFORMPYTHON:
1329
1329
  transformation_location = f"{transformation.file_path.value}::{transformation.class_name.value}"
1330
1330
  artifact_content = await self.execute_python_transform.with_options(
@@ -1336,7 +1336,7 @@ class InfrahubRepositoryIntegrator(InfrahubRepositoryBase):
1336
1336
  location=transformation_location,
1337
1337
  data=response,
1338
1338
  convert_query_response=transformation.convert_query_response.value,
1339
- ) # type: ignore[misc]
1339
+ ) # type: ignore[call-overload]
1340
1340
 
1341
1341
  if definition.content_type.value == ContentType.APPLICATION_JSON.value and isinstance(artifact_content, dict):
1342
1342
  artifact_content_str = ujson.dumps(artifact_content, indent=2)
@@ -1387,7 +1387,7 @@ class InfrahubRepositoryIntegrator(InfrahubRepositoryBase):
1387
1387
  if message.transform_type == InfrahubKind.TRANSFORMJINJA2:
1388
1388
  artifact_content = await self.render_jinja2_template.with_options(timeout_seconds=message.timeout)(
1389
1389
  commit=message.commit, location=message.transform_location, data=response
1390
- ) # type: ignore[misc]
1390
+ ) # type: ignore[call-overload]
1391
1391
  elif message.transform_type == InfrahubKind.TRANSFORMPYTHON:
1392
1392
  artifact_content = await self.execute_python_transform.with_options(timeout_seconds=message.timeout)(
1393
1393
  client=self.sdk,
@@ -1396,7 +1396,7 @@ class InfrahubRepositoryIntegrator(InfrahubRepositoryBase):
1396
1396
  location=message.transform_location,
1397
1397
  data=response,
1398
1398
  convert_query_response=message.convert_query_response,
1399
- ) # type: ignore[misc]
1399
+ ) # type: ignore[call-overload]
1400
1400
 
1401
1401
  if message.content_type == ContentType.APPLICATION_JSON.value and isinstance(artifact_content, dict):
1402
1402
  artifact_content_str = ujson.dumps(artifact_content, indent=2)
@@ -1421,7 +1421,7 @@ class InfrahubRepositoryIntegrator(InfrahubRepositoryBase):
1421
1421
  artifact.status.value = ArtifactStatus.READY.value
1422
1422
  if artifact.name.value != message.artifact_name:
1423
1423
  artifact.name.value = message.artifact_name
1424
- await artifact.save()
1424
+ await artifact.save(request_context=message.context.to_request_context())
1425
1425
 
1426
1426
  event_class = ArtifactCreatedEvent if artifact_created else ArtifactUpdatedEvent
1427
1427
 
infrahub/git/tasks.py CHANGED
@@ -950,7 +950,7 @@ async def run_user_check(model: UserCheckData) -> ValidatorConclusion:
950
950
  client=client,
951
951
  commit=model.commit,
952
952
  params=model.variables,
953
- ) # type: ignore[misc]
953
+ ) # type: ignore[call-overload]
954
954
  if check_run.passed:
955
955
  conclusion = ValidatorConclusion.SUCCESS
956
956
  severity = "info"
infrahub/graphql/app.py CHANGED
@@ -172,9 +172,9 @@ class InfrahubGraphQLApp:
172
172
 
173
173
  response = handler(request)
174
174
  if isawaitable(response):
175
- return await response
175
+ return await cast("Awaitable[Response]", response)
176
176
 
177
- return response
177
+ return cast("Response", response)
178
178
 
179
179
  async def _handle_http_request(
180
180
  self, request: Request, db: InfrahubDatabase, branch: Branch, account_session: AccountSession
@@ -1 +1,4 @@
1
1
  KIND_GRAPHQL_FIELD_NAME = "__kind__"
2
+
3
+ NODE_METADATA_TYPE = "InfrahubNodeMetadata"
4
+ RELATIONSHIP_METADATA_TYPE = "InfrahubRelationshipMetadata"
@@ -14,7 +14,7 @@ if TYPE_CHECKING:
14
14
 
15
15
  async def apply_external_context(graphql_context: GraphqlContext, context_input: ContextInput | None) -> None:
16
16
  """Applies context provided by an external mutation to the GraphQL context"""
17
- if not context_input or not context_input.account:
17
+ if not context_input or not context_input.account or not context_input.account.id:
18
18
  return
19
19
 
20
20
  if graphql_context.active_account_session.account_id == context_input.account.id:
@@ -7,9 +7,11 @@ from starlette.background import BackgroundTasks
7
7
 
8
8
  from infrahub.context import InfrahubContext
9
9
  from infrahub.core import registry
10
+ from infrahub.core.constants import SYSTEM_USER_ID
10
11
  from infrahub.core.timestamp import Timestamp
11
12
  from infrahub.exceptions import InitializationError
12
13
  from infrahub.graphql.registry import registry as graphql_registry
14
+ from infrahub.graphql.resolvers.account_metadata import AccountMetadataResolver
13
15
  from infrahub.graphql.resolvers.many_relationship import ManyRelationshipResolver
14
16
  from infrahub.graphql.resolvers.single_relationship import SingleRelationshipResolver
15
17
  from infrahub.permissions import PermissionManager
@@ -37,6 +39,7 @@ class GraphqlContext:
37
39
  types: dict
38
40
  single_relationship_resolver: SingleRelationshipResolver
39
41
  many_relationship_resolver: ManyRelationshipResolver
42
+ account_metadata_resolver: AccountMetadataResolver
40
43
  service: InfrahubServices | None = None
41
44
  at: Timestamp | None = None
42
45
  related_node_ids: set | None = None
@@ -75,6 +78,13 @@ class GraphqlContext:
75
78
  def get_context(self) -> InfrahubContext:
76
79
  return InfrahubContext.init(branch=self.branch, account=self.active_account_session)
77
80
 
81
+ @property
82
+ def assigned_user_id(self) -> str:
83
+ """Return the user ID to be used for assignments in this context."""
84
+ if self.account_session and self.account_session.account_id:
85
+ return self.account_session.account_id
86
+ return SYSTEM_USER_ID
87
+
78
88
 
79
89
  async def prepare_graphql_params(
80
90
  db: InfrahubDatabase,
@@ -113,6 +123,7 @@ async def prepare_graphql_params(
113
123
  branch=branch,
114
124
  single_relationship_resolver=SingleRelationshipResolver(),
115
125
  many_relationship_resolver=ManyRelationshipResolver(),
126
+ account_metadata_resolver=AccountMetadataResolver(),
116
127
  at=Timestamp(at),
117
128
  types=gqlm.get_graphql_types(),
118
129
  related_node_ids=set(),
@@ -0,0 +1,134 @@
1
+ """DataLoader for resolving account IDs to full account data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from aiodataloader import DataLoader
9
+
10
+ from infrahub.core.constants import SYSTEM_USER_ID, InfrahubKind
11
+ from infrahub.core.manager import NodeManager
12
+ from infrahub.graphql.constants import KIND_GRAPHQL_FIELD_NAME
13
+
14
+ if TYPE_CHECKING:
15
+ from infrahub.core.branch.models import Branch
16
+ from infrahub.core.timestamp import Timestamp
17
+ from infrahub.database import InfrahubDatabase
18
+
19
+ SYSTEM_ACCOUNT_DISPLAY_LABEL = "[Infrahub System]"
20
+ UNKNOWN_ACCOUNT_DISPLAY_LABEL = "[Unknown Account]"
21
+
22
+
23
+ @dataclass
24
+ class AccountLoaderParams:
25
+ branch: Branch
26
+ at: Timestamp | None
27
+ fields: dict[str, Any] = field(default_factory=dict)
28
+
29
+ def __hash__(self) -> int:
30
+ # Fields are not included in hash as they vary per query but we want to reuse loader
31
+ at_str = self.at.to_string() if self.at else ""
32
+ return hash(f"{at_str}|{self.branch.name}")
33
+
34
+
35
+ class AccountDataLoader(DataLoader[str, dict[str, Any] | None]):
36
+ """DataLoader for batch-loading account data by ID.
37
+
38
+ Handles SYSTEM_USER_ID specially by returning synthetic system account data.
39
+ All lookups are branch-agnostic since accounts don't vary by branch.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ db: InfrahubDatabase,
45
+ params: AccountLoaderParams,
46
+ *args: Any,
47
+ **kwargs: Any,
48
+ ) -> None:
49
+ super().__init__(*args, **kwargs)
50
+ self.db = db
51
+ self.params = params
52
+
53
+ def _build_system_account_response(self, fields: dict[str, Any]) -> dict[str, Any]:
54
+ """Build response for SYSTEM_USER_ID based on requested fields."""
55
+ # Always include __kind__ for GraphQL interface type resolution
56
+ response: dict[str, Any] = {KIND_GRAPHQL_FIELD_NAME: InfrahubKind.ACCOUNT}
57
+
58
+ for field_name in fields:
59
+ if field_name == "id":
60
+ response["id"] = SYSTEM_USER_ID
61
+ elif field_name == "display_label":
62
+ response["display_label"] = SYSTEM_ACCOUNT_DISPLAY_LABEL
63
+ elif field_name == "__typename":
64
+ response["__typename"] = InfrahubKind.ACCOUNT
65
+ elif field_name == "name":
66
+ response["name"] = {"value": SYSTEM_ACCOUNT_DISPLAY_LABEL}
67
+ elif field_name == "label":
68
+ response["label"] = {"value": SYSTEM_ACCOUNT_DISPLAY_LABEL}
69
+ elif field_name == "description":
70
+ response["description"] = {"value": None}
71
+ else:
72
+ # For any other field, return None
73
+ response[field_name] = None
74
+
75
+ return response
76
+
77
+ def _build_unknown_account_response(self, account_id: str, fields: dict[str, Any]) -> dict[str, Any]:
78
+ """Build response for an unknown/deleted account based on requested fields."""
79
+ # Always include __kind__ for GraphQL interface type resolution
80
+ response: dict[str, Any] = {KIND_GRAPHQL_FIELD_NAME: InfrahubKind.ACCOUNT}
81
+
82
+ for field_name in fields:
83
+ if field_name == "id":
84
+ response["id"] = account_id
85
+ elif field_name == "display_label":
86
+ response["display_label"] = UNKNOWN_ACCOUNT_DISPLAY_LABEL
87
+ elif field_name == "__typename":
88
+ response["__typename"] = InfrahubKind.ACCOUNT
89
+ elif field_name == "name":
90
+ response["name"] = {"value": UNKNOWN_ACCOUNT_DISPLAY_LABEL}
91
+ elif field_name == "label":
92
+ response["label"] = {"value": UNKNOWN_ACCOUNT_DISPLAY_LABEL}
93
+ elif field_name == "description":
94
+ response["description"] = {"value": None}
95
+ else:
96
+ response[field_name] = None
97
+
98
+ return response
99
+
100
+ async def batch_load_fn(self, keys: list[str]) -> list[dict[str, Any] | None]:
101
+ # Separate system user from real account IDs
102
+ real_account_ids = [k for k in keys if k != SYSTEM_USER_ID]
103
+
104
+ # Batch load real accounts
105
+ accounts_by_id: dict[str, dict[str, Any]] = {}
106
+ if real_account_ids:
107
+ async with self.db.start_session(read_only=True) as db:
108
+ nodes = await NodeManager.get_many(
109
+ db=db,
110
+ ids=real_account_ids,
111
+ fields=self.params.fields,
112
+ at=self.params.at,
113
+ branch=self.params.branch,
114
+ branch_agnostic=True, # Accounts are always branch-agnostic
115
+ )
116
+
117
+ for node_id, node in nodes.items():
118
+ accounts_by_id[node_id] = await node.to_graphql(
119
+ db=db,
120
+ fields=self.params.fields,
121
+ )
122
+
123
+ # Build results in same order as keys
124
+ results: list[dict[str, Any] | None] = []
125
+ for key in keys:
126
+ if key == SYSTEM_USER_ID:
127
+ results.append(self._build_system_account_response(self.params.fields))
128
+ elif key in accounts_by_id:
129
+ results.append(accounts_by_id[key])
130
+ else:
131
+ # Account not found - return placeholder
132
+ results.append(self._build_unknown_account_response(key, self.params.fields))
133
+
134
+ return results
@@ -1,11 +1,11 @@
1
- from dataclasses import dataclass
1
+ from dataclasses import dataclass, field
2
2
  from typing import Any
3
3
 
4
4
  from aiodataloader import DataLoader
5
5
 
6
- from infrahub.auth import AccountSession
7
6
  from infrahub.core.branch.models import Branch
8
7
  from infrahub.core.manager import NodeManager
8
+ from infrahub.core.metadata.model import MetadataQueryOptions
9
9
  from infrahub.core.node import Node
10
10
  from infrahub.core.timestamp import Timestamp
11
11
  from infrahub.database import InfrahubDatabase
@@ -18,10 +18,8 @@ class GetManyParams:
18
18
  branch: Branch | str
19
19
  fields: dict | None = None
20
20
  at: Timestamp | str | None = None
21
- include_source: bool = False
22
- include_owner: bool = False
21
+ include_metadata: MetadataQueryOptions = field(default_factory=MetadataQueryOptions)
23
22
  prefetch_relationships: bool = False
24
- account: AccountSession | None = None
25
23
  branch_agnostic: bool = False
26
24
 
27
25
  def __hash__(self) -> int:
@@ -30,16 +28,13 @@ class GetManyParams:
30
28
  frozen_fields = to_frozen_set(self.fields)
31
29
  timestamp = Timestamp(self.at)
32
30
  branch = self.branch.name if isinstance(self.branch, Branch) else self.branch
33
- account_id = self.account.account_id if isinstance(self.account, AccountSession) else None
34
31
  hash_str = "|".join(
35
32
  [
36
33
  str(hash(frozen_fields)),
37
34
  timestamp.to_string(),
38
35
  branch,
39
- str(self.include_source),
40
- str(self.include_owner),
36
+ str(hash(self.include_metadata)),
41
37
  str(self.prefetch_relationships),
42
- str(account_id),
43
38
  str(self.branch_agnostic),
44
39
  ]
45
40
  )
@@ -60,10 +55,8 @@ class NodeDataLoader(DataLoader[str, Node | None]):
60
55
  fields=self.query_params.fields,
61
56
  at=self.query_params.at,
62
57
  branch=self.query_params.branch,
63
- include_source=self.query_params.include_source,
64
- include_owner=self.query_params.include_owner,
58
+ include_metadata=self.query_params.include_metadata,
65
59
  prefetch_relationships=self.query_params.prefetch_relationships,
66
- account=self.query_params.account,
67
60
  branch_agnostic=self.query_params.branch_agnostic,
68
61
  )
69
62
  results = []
@@ -1,10 +1,11 @@
1
- from dataclasses import dataclass
1
+ from dataclasses import dataclass, field
2
2
  from typing import Any
3
3
 
4
4
  from aiodataloader import DataLoader
5
5
 
6
6
  from infrahub.core.branch.models import Branch
7
7
  from infrahub.core.manager import NodeManager
8
+ from infrahub.core.metadata.model import MetadataQueryOptions
8
9
  from infrahub.core.relationship.model import Relationship
9
10
  from infrahub.core.schema.relationship_schema import RelationshipSchema
10
11
  from infrahub.core.timestamp import Timestamp
@@ -22,8 +23,7 @@ class QueryPeerParams:
22
23
  fields: dict | None = None
23
24
  at: Timestamp | str | None = None
24
25
  branch_agnostic: bool = False
25
- include_source: bool = False
26
- include_owner: bool = False
26
+ include_metadata: MetadataQueryOptions = field(default_factory=MetadataQueryOptions)
27
27
 
28
28
  def __hash__(self) -> int:
29
29
  frozen_fields: frozenset | None = None
@@ -41,8 +41,7 @@ class QueryPeerParams:
41
41
  self.schema.name,
42
42
  str(self.source_kind),
43
43
  str(self.branch_agnostic),
44
- str(self.include_source),
45
- str(self.include_owner),
44
+ str(hash(self.include_metadata)),
46
45
  ]
47
46
  )
48
47
  return hash(hash_str)
@@ -66,9 +65,8 @@ class PeerRelationshipsDataLoader(DataLoader[str, list[Relationship]]):
66
65
  at=self.query_params.at,
67
66
  branch=self.query_params.branch,
68
67
  branch_agnostic=self.query_params.branch_agnostic,
68
+ include_metadata=self.query_params.include_metadata,
69
69
  fetch_peers=True,
70
- include_source=self.query_params.include_source,
71
- include_owner=self.query_params.include_owner,
72
70
  )
73
71
  peer_rels_by_node_id: dict[str, list[Relationship]] = {}
74
72
  for rel in peer_rels: