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
infrahub_sdk/client.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import copy
5
5
  import logging
6
+ import time
6
7
  from collections.abc import Coroutine, MutableMapping
7
8
  from functools import wraps
8
9
  from time import sleep
@@ -38,6 +39,7 @@ from .exceptions import (
38
39
  NodeNotFoundError,
39
40
  ServerNotReachableError,
40
41
  ServerNotResponsiveError,
42
+ URLNotFoundError,
41
43
  )
42
44
  from .graphql import Mutation, Query
43
45
  from .node import (
@@ -58,6 +60,8 @@ from .utils import decode_json, get_user_permissions, is_valid_uuid
58
60
  if TYPE_CHECKING:
59
61
  from types import TracebackType
60
62
 
63
+ from .context import RequestContext
64
+
61
65
 
62
66
  SchemaType = TypeVar("SchemaType", bound=CoreNode)
63
67
  SchemaTypeSync = TypeVar("SchemaTypeSync", bound=CoreNodeSync)
@@ -139,6 +143,7 @@ class BaseClient:
139
143
  self.identifier = self.config.identifier
140
144
  self.group_context: InfrahubGroupContext | InfrahubGroupContextSync
141
145
  self._initialize()
146
+ self._request_context: RequestContext | None = None
142
147
 
143
148
  def _initialize(self) -> None:
144
149
  """Sets the properties for each version of the client"""
@@ -153,6 +158,14 @@ class BaseClient:
153
158
  if variables:
154
159
  print(f"VARIABLES:\n{ujson.dumps(variables, indent=4)}\n")
155
160
 
161
+ @property
162
+ def request_context(self) -> RequestContext | None:
163
+ return self._request_context
164
+
165
+ @request_context.setter
166
+ def request_context(self, request_context: RequestContext) -> None:
167
+ self._request_context = request_context
168
+
156
169
  def start_tracking(
157
170
  self,
158
171
  identifier: str | None = None,
@@ -457,7 +470,7 @@ class InfrahubClient(BaseClient):
457
470
  hfid: list[str] | None = None,
458
471
  include: list[str] | None = None,
459
472
  exclude: list[str] | None = None,
460
- populate_store: bool = False,
473
+ populate_store: bool = True,
461
474
  fragment: bool = False,
462
475
  prefetch_relationships: bool = False,
463
476
  property: bool = False,
@@ -611,7 +624,7 @@ class InfrahubClient(BaseClient):
611
624
  at: Timestamp | None = None,
612
625
  branch: str | None = None,
613
626
  timeout: int | None = None,
614
- populate_store: bool = False,
627
+ populate_store: bool = True,
615
628
  offset: int | None = None,
616
629
  limit: int | None = None,
617
630
  include: list[str] | None = None,
@@ -707,7 +720,7 @@ class InfrahubClient(BaseClient):
707
720
  at: Timestamp | None = None,
708
721
  branch: str | None = None,
709
722
  timeout: int | None = None,
710
- populate_store: bool = False,
723
+ populate_store: bool = True,
711
724
  offset: int | None = None,
712
725
  limit: int | None = None,
713
726
  include: list[str] | None = None,
@@ -880,7 +893,8 @@ class InfrahubClient(BaseClient):
880
893
 
881
894
  retry = True
882
895
  resp = None
883
- while retry:
896
+ start_time = time.time()
897
+ while retry and time.time() - start_time < self.config.max_retry_duration:
884
898
  retry = self.retry_on_failure
885
899
  try:
886
900
  resp = await self._post(url=url, payload=payload, headers=headers, timeout=timeout)
@@ -904,6 +918,8 @@ class InfrahubClient(BaseClient):
904
918
  errors = response.get("errors", [])
905
919
  messages = [error.get("message") for error in errors]
906
920
  raise AuthenticationError(" | ".join(messages)) from exc
921
+ if exc.response.status_code == 404:
922
+ raise URLNotFoundError(url=url)
907
923
 
908
924
  if not resp:
909
925
  raise Error("Unexpected situation, resp hasn't been initialized.")
@@ -1621,7 +1637,8 @@ class InfrahubClientSync(BaseClient):
1621
1637
 
1622
1638
  retry = True
1623
1639
  resp = None
1624
- while retry:
1640
+ start_time = time.time()
1641
+ while retry and time.time() - start_time < self.config.max_retry_duration:
1625
1642
  retry = self.retry_on_failure
1626
1643
  try:
1627
1644
  resp = self._post(url=url, payload=payload, headers=headers, timeout=timeout)
@@ -1645,6 +1662,8 @@ class InfrahubClientSync(BaseClient):
1645
1662
  errors = response.get("errors", [])
1646
1663
  messages = [error.get("message") for error in errors]
1647
1664
  raise AuthenticationError(" | ".join(messages)) from exc
1665
+ if exc.response.status_code == 404:
1666
+ raise URLNotFoundError(url=url)
1648
1667
 
1649
1668
  if not resp:
1650
1669
  raise Error("Unexpected situation, resp hasn't been initialized.")
@@ -1726,7 +1745,7 @@ class InfrahubClientSync(BaseClient):
1726
1745
  at: Timestamp | None = None,
1727
1746
  branch: str | None = None,
1728
1747
  timeout: int | None = None,
1729
- populate_store: bool = False,
1748
+ populate_store: bool = True,
1730
1749
  offset: int | None = None,
1731
1750
  limit: int | None = None,
1732
1751
  include: list[str] | None = None,
@@ -1857,7 +1876,7 @@ class InfrahubClientSync(BaseClient):
1857
1876
  at: Timestamp | None = None,
1858
1877
  branch: str | None = None,
1859
1878
  timeout: int | None = None,
1860
- populate_store: bool = False,
1879
+ populate_store: bool = True,
1861
1880
  offset: int | None = None,
1862
1881
  limit: int | None = None,
1863
1882
  include: list[str] | None = None,
@@ -2110,7 +2129,7 @@ class InfrahubClientSync(BaseClient):
2110
2129
  hfid: list[str] | None = None,
2111
2130
  include: list[str] | None = None,
2112
2131
  exclude: list[str] | None = None,
2113
- populate_store: bool = False,
2132
+ populate_store: bool = True,
2114
2133
  fragment: bool = False,
2115
2134
  prefetch_relationships: bool = False,
2116
2135
  property: bool = False,
infrahub_sdk/config.py CHANGED
@@ -56,6 +56,9 @@ class ConfigBase(BaseSettings):
56
56
  pagination_size: int = Field(default=50, description="Page size for queries to the server")
57
57
  retry_delay: int = Field(default=5, description="Number of seconds to wait until attempting a retry.")
58
58
  retry_on_failure: bool = Field(default=False, description="Retry operation in case of failure")
59
+ max_retry_duration: int = Field(
60
+ default=300, description="Maximum duration until we stop attempting to retry if enabled."
61
+ )
59
62
  schema_converge_timeout: int = Field(
60
63
  default=60, description="Number of seconds to wait for schema to have converged"
61
64
  )
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class ContextAccount(BaseModel):
7
+ id: str = Field(..., description="The ID of the account")
8
+
9
+
10
+ class RequestContext(BaseModel):
11
+ """The context can be used to override settings such as the account within mutations."""
12
+
13
+ account: ContextAccount | None = Field(default=None, description="Account tied to the context")
@@ -121,6 +121,12 @@ class AuthenticationError(Error):
121
121
  super().__init__(self.message)
122
122
 
123
123
 
124
+ class URLNotFoundError(Error):
125
+ def __init__(self, url: str):
126
+ self.message = f"`{url}` not found."
127
+ super().__init__(self.message)
128
+
129
+
124
130
  class FeatureNotSupportedError(Error):
125
131
  """Raised when trying to use a method on a node that doesn't support it."""
126
132
 
infrahub_sdk/generator.py CHANGED
@@ -11,6 +11,7 @@ from .exceptions import UninitializedError
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  from .client import InfrahubClient
14
+ from .context import RequestContext
14
15
  from .node import InfrahubNode
15
16
  from .store import NodeStore
16
17
 
@@ -29,6 +30,7 @@ class InfrahubGenerator:
29
30
  params: dict | None = None,
30
31
  convert_query_response: bool = False,
31
32
  logger: logging.Logger | None = None,
33
+ request_context: RequestContext | None = None,
32
34
  ) -> None:
33
35
  self.query = query
34
36
  self.branch = branch
@@ -44,6 +46,7 @@ class InfrahubGenerator:
44
46
  self.infrahub_node = infrahub_node
45
47
  self.convert_query_response = convert_query_response
46
48
  self.logger = logger if logger else logging.getLogger("infrahub.tasks")
49
+ self.request_context = request_context
47
50
 
48
51
  @property
49
52
  def store(self) -> NodeStore:
@@ -122,7 +125,7 @@ class InfrahubGenerator:
122
125
  await self._init_client.schema.all(branch=self.branch_name)
123
126
 
124
127
  for kind in data:
125
- if kind in self._init_client.schema.cache[self.branch_name]:
128
+ if kind in self._init_client.schema.cache[self.branch_name].nodes.keys():
126
129
  for result in data[kind].get("edges", []):
127
130
  node = await self.infrahub_node.from_graphql(
128
131
  client=self._init_client, branch=self.branch_name, data=result
infrahub_sdk/graphql.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from enum import Enum
3
4
  from typing import Any
4
5
 
5
6
  from pydantic import BaseModel
@@ -7,19 +8,30 @@ from pydantic import BaseModel
7
8
  VARIABLE_TYPE_MAPPING = ((str, "String!"), (int, "Int!"), (float, "Float!"), (bool, "Boolean!"))
8
9
 
9
10
 
10
- def convert_to_graphql_as_string(value: str | bool | list) -> str:
11
+ def convert_to_graphql_as_string(value: str | bool | list | BaseModel | Enum | Any, convert_enum: bool = False) -> str: # noqa: PLR0911
11
12
  if isinstance(value, str) and value.startswith("$"):
12
13
  return value
14
+ if isinstance(value, Enum):
15
+ if convert_enum:
16
+ return convert_to_graphql_as_string(value=value.value, convert_enum=True)
17
+ return value.name
13
18
  if isinstance(value, str):
14
19
  return f'"{value}"'
15
20
  if isinstance(value, bool):
16
21
  return repr(value).lower()
17
22
  if isinstance(value, list):
18
- values_as_string = [convert_to_graphql_as_string(item) for item in value]
23
+ values_as_string = [convert_to_graphql_as_string(value=item, convert_enum=convert_enum) for item in value]
19
24
  return "[" + ", ".join(values_as_string) + "]"
20
25
  if isinstance(value, BaseModel):
21
26
  data = value.model_dump()
22
- return "{ " + ", ".join(f"{key}: {convert_to_graphql_as_string(val)}" for key, val in data.items()) + " }"
27
+ return (
28
+ "{ "
29
+ + ", ".join(
30
+ f"{key}: {convert_to_graphql_as_string(value=val, convert_enum=convert_enum)}"
31
+ for key, val in data.items()
32
+ )
33
+ + " }"
34
+ )
23
35
 
24
36
  return str(value)
25
37
 
@@ -38,7 +50,7 @@ def render_variables_to_string(data: dict[str, type[str | int | float | bool]])
38
50
  return ", ".join([f"{key}: {value}" for key, value in vars_dict.items()])
39
51
 
40
52
 
41
- def render_query_block(data: dict, offset: int = 4, indentation: int = 4) -> list[str]:
53
+ def render_query_block(data: dict, offset: int = 4, indentation: int = 4, convert_enum: bool = False) -> list[str]:
42
54
  FILTERS_KEY = "@filters"
43
55
  ALIAS_KEY = "@alias"
44
56
  KEYWORDS_TO_SKIP = [FILTERS_KEY, ALIAS_KEY]
@@ -60,25 +72,36 @@ def render_query_block(data: dict, offset: int = 4, indentation: int = 4) -> lis
60
72
 
61
73
  if value.get(FILTERS_KEY):
62
74
  filters_str = ", ".join(
63
- [f"{key2}: {convert_to_graphql_as_string(value2)}" for key2, value2 in value[FILTERS_KEY].items()]
75
+ [
76
+ f"{key2}: {convert_to_graphql_as_string(value=value2, convert_enum=convert_enum)}"
77
+ for key2, value2 in value[FILTERS_KEY].items()
78
+ ]
64
79
  )
65
80
  lines.append(f"{offset_str}{key_str}({filters_str}) " + "{")
66
81
  else:
67
82
  lines.append(f"{offset_str}{key_str} " + "{")
68
83
 
69
- lines.extend(render_query_block(data=value, offset=offset + indentation, indentation=indentation))
84
+ lines.extend(
85
+ render_query_block(
86
+ data=value, offset=offset + indentation, indentation=indentation, convert_enum=convert_enum
87
+ )
88
+ )
70
89
  lines.append(offset_str + "}")
71
90
 
72
91
  return lines
73
92
 
74
93
 
75
- def render_input_block(data: dict, offset: int = 4, indentation: int = 4) -> list[str]:
94
+ def render_input_block(data: dict, offset: int = 4, indentation: int = 4, convert_enum: bool = False) -> list[str]:
76
95
  offset_str = " " * offset
77
96
  lines = []
78
97
  for key, value in data.items():
79
98
  if isinstance(value, dict):
80
99
  lines.append(f"{offset_str}{key}: " + "{")
81
- lines.extend(render_input_block(data=value, offset=offset + indentation, indentation=indentation))
100
+ lines.extend(
101
+ render_input_block(
102
+ data=value, offset=offset + indentation, indentation=indentation, convert_enum=convert_enum
103
+ )
104
+ )
82
105
  lines.append(offset_str + "}")
83
106
  elif isinstance(value, list):
84
107
  lines.append(f"{offset_str}{key}: " + "[")
@@ -90,14 +113,17 @@ def render_input_block(data: dict, offset: int = 4, indentation: int = 4) -> lis
90
113
  data=item,
91
114
  offset=offset + indentation + indentation,
92
115
  indentation=indentation,
116
+ convert_enum=convert_enum,
93
117
  )
94
118
  )
95
119
  lines.append(f"{offset_str}{' ' * indentation}" + "},")
96
120
  else:
97
- lines.append(f"{offset_str}{' ' * indentation}{convert_to_graphql_as_string(item)},")
121
+ lines.append(
122
+ f"{offset_str}{' ' * indentation}{convert_to_graphql_as_string(value=item, convert_enum=convert_enum)},"
123
+ )
98
124
  lines.append(offset_str + "]")
99
125
  else:
100
- lines.append(f"{offset_str}{key}: {convert_to_graphql_as_string(value)}")
126
+ lines.append(f"{offset_str}{key}: {convert_to_graphql_as_string(value=value, convert_enum=convert_enum)}")
101
127
  return lines
102
128
 
103
129
 
@@ -127,9 +153,13 @@ class BaseGraphQLQuery:
127
153
  class Query(BaseGraphQLQuery):
128
154
  query_type = "query"
129
155
 
130
- def render(self) -> str:
156
+ def render(self, convert_enum: bool = False) -> str:
131
157
  lines = [self.render_first_line()]
132
- lines.extend(render_query_block(data=self.query, indentation=self.indentation, offset=self.indentation))
158
+ lines.extend(
159
+ render_query_block(
160
+ data=self.query, indentation=self.indentation, offset=self.indentation, convert_enum=convert_enum
161
+ )
162
+ )
133
163
  lines.append("}")
134
164
 
135
165
  return "\n" + "\n".join(lines) + "\n"
@@ -143,7 +173,7 @@ class Mutation(BaseGraphQLQuery):
143
173
  self.mutation = mutation
144
174
  super().__init__(*args, **kwargs)
145
175
 
146
- def render(self) -> str:
176
+ def render(self, convert_enum: bool = False) -> str:
147
177
  lines = [self.render_first_line()]
148
178
  lines.append(" " * self.indentation + f"{self.mutation}(")
149
179
  lines.extend(
@@ -151,6 +181,7 @@ class Mutation(BaseGraphQLQuery):
151
181
  data=self.input_data,
152
182
  indentation=self.indentation,
153
183
  offset=self.indentation * 2,
184
+ convert_enum=convert_enum,
154
185
  )
155
186
  )
156
187
  lines.append(" " * self.indentation + "){")
@@ -159,6 +190,7 @@ class Mutation(BaseGraphQLQuery):
159
190
  data=self.query,
160
191
  indentation=self.indentation,
161
192
  offset=self.indentation * 2,
193
+ convert_enum=convert_enum,
162
194
  )
163
195
  )
164
196
  lines.append(" " * self.indentation + "}")
infrahub_sdk/node.py CHANGED
@@ -22,6 +22,7 @@ if TYPE_CHECKING:
22
22
  from typing_extensions import Self
23
23
 
24
24
  from .client import InfrahubClient, InfrahubClientSync
25
+ from .context import RequestContext
25
26
  from .schema import AttributeSchemaAPI, MainSchemaTypesAPI, RelationshipSchemaAPI
26
27
  from .types import Order
27
28
 
@@ -187,6 +188,7 @@ class RelatedNodeBase:
187
188
  if node_data:
188
189
  self._id = node_data.get("id", None)
189
190
  self._hfid = node_data.get("hfid", None)
191
+ self._kind = node_data.get("kind", None)
190
192
  self._display_label = node_data.get("display_label", None)
191
193
  self._typename = node_data.get("__typename", None)
192
194
 
@@ -255,6 +257,8 @@ class RelatedNodeBase:
255
257
  data["id"] = self.id
256
258
  elif self.hfid is not None:
257
259
  data["hfid"] = self.hfid
260
+ if self._kind is not None:
261
+ data["kind"] = self._kind
258
262
 
259
263
  for prop_name in self._properties:
260
264
  if getattr(self, prop_name) is not None:
@@ -766,6 +770,16 @@ class InfrahubNodeBase:
766
770
  Attribute(name=attr_name, schema=attr_schema, data=attr_data),
767
771
  )
768
772
 
773
+ def _get_request_context(self, request_context: RequestContext | None = None) -> dict[str, Any] | None:
774
+ if request_context:
775
+ return request_context.model_dump(exclude_none=True)
776
+
777
+ client: InfrahubClient | InfrahubClientSync | None = getattr(self, "_client", None)
778
+ if not client or not client.request_context:
779
+ return None
780
+
781
+ return client.request_context.model_dump(exclude_none=True)
782
+
769
783
  def _init_relationships(self, data: dict | None = None) -> None:
770
784
  pass
771
785
 
@@ -794,7 +808,12 @@ class InfrahubNodeBase:
794
808
  def get_raw_graphql_data(self) -> dict | None:
795
809
  return self._data
796
810
 
797
- def _generate_input_data(self, exclude_unmodified: bool = False, exclude_hfid: bool = False) -> dict[str, dict]: # noqa: C901
811
+ def _generate_input_data( # noqa: C901
812
+ self,
813
+ exclude_unmodified: bool = False,
814
+ exclude_hfid: bool = False,
815
+ request_context: RequestContext | None = None,
816
+ ) -> dict[str, dict]:
798
817
  """Generate a dictionary that represent the input data required by a mutation.
799
818
 
800
819
  Returns:
@@ -872,7 +891,15 @@ class InfrahubNodeBase:
872
891
  elif self.hfid is not None and not exclude_hfid:
873
892
  data["hfid"] = self.hfid
874
893
 
875
- return {"data": {"data": data}, "variables": variables, "mutation_variables": mutation_variables}
894
+ mutation_payload = {"data": data}
895
+ if context_data := self._get_request_context(request_context=request_context):
896
+ mutation_payload["context"] = context_data
897
+
898
+ return {
899
+ "data": mutation_payload,
900
+ "variables": variables,
901
+ "mutation_variables": mutation_variables,
902
+ }
876
903
 
877
904
  @staticmethod
878
905
  def _strip_unmodified_dict(data: dict, original_data: dict, variables: dict, item: str) -> None:
@@ -1129,8 +1156,11 @@ class InfrahubNode(InfrahubNodeBase):
1129
1156
  content = await self._client.object_store.get(identifier=artifact.storage_id.value) # type: ignore[attr-defined]
1130
1157
  return content
1131
1158
 
1132
- async def delete(self, timeout: int | None = None) -> None:
1159
+ async def delete(self, timeout: int | None = None, request_context: RequestContext | None = None) -> None:
1133
1160
  input_data = {"data": {"id": self.id}}
1161
+ if context_data := self._get_request_context(request_context=request_context):
1162
+ input_data["context"] = context_data
1163
+
1134
1164
  mutation_query = {"ok": None}
1135
1165
  query = Mutation(
1136
1166
  mutation=f"{self._schema.kind}Delete",
@@ -1145,12 +1175,16 @@ class InfrahubNode(InfrahubNodeBase):
1145
1175
  )
1146
1176
 
1147
1177
  async def save(
1148
- self, allow_upsert: bool = False, update_group_context: bool | None = None, timeout: int | None = None
1178
+ self,
1179
+ allow_upsert: bool = False,
1180
+ update_group_context: bool | None = None,
1181
+ timeout: int | None = None,
1182
+ request_context: RequestContext | None = None,
1149
1183
  ) -> None:
1150
1184
  if self._existing is False or allow_upsert is True:
1151
- await self.create(allow_upsert=allow_upsert, timeout=timeout)
1185
+ await self.create(allow_upsert=allow_upsert, timeout=timeout, request_context=request_context)
1152
1186
  else:
1153
- await self.update(timeout=timeout)
1187
+ await self.update(timeout=timeout, request_context=request_context)
1154
1188
 
1155
1189
  if update_group_context is None and self._client.mode == InfrahubClientMode.TRACKING:
1156
1190
  update_group_context = True
@@ -1379,15 +1413,17 @@ class InfrahubNode(InfrahubNodeBase):
1379
1413
  await related_node.fetch(timeout=timeout)
1380
1414
  setattr(self, rel_name, related_node)
1381
1415
 
1382
- async def create(self, allow_upsert: bool = False, timeout: int | None = None) -> None:
1416
+ async def create(
1417
+ self, allow_upsert: bool = False, timeout: int | None = None, request_context: RequestContext | None = None
1418
+ ) -> None:
1383
1419
  mutation_query = self._generate_mutation_query()
1384
1420
 
1385
1421
  if allow_upsert:
1386
- input_data = self._generate_input_data(exclude_hfid=False)
1422
+ input_data = self._generate_input_data(exclude_hfid=False, request_context=request_context)
1387
1423
  mutation_name = f"{self._schema.kind}Upsert"
1388
1424
  tracker = f"mutation-{str(self._schema.kind).lower()}-upsert"
1389
1425
  else:
1390
- input_data = self._generate_input_data(exclude_hfid=True)
1426
+ input_data = self._generate_input_data(exclude_hfid=True, request_context=request_context)
1391
1427
  mutation_name = f"{self._schema.kind}Create"
1392
1428
  tracker = f"mutation-{str(self._schema.kind).lower()}-create"
1393
1429
  query = Mutation(
@@ -1405,8 +1441,10 @@ class InfrahubNode(InfrahubNodeBase):
1405
1441
  )
1406
1442
  await self._process_mutation_result(mutation_name=mutation_name, response=response, timeout=timeout)
1407
1443
 
1408
- async def update(self, do_full_update: bool = False, timeout: int | None = None) -> None:
1409
- input_data = self._generate_input_data(exclude_unmodified=not do_full_update)
1444
+ async def update(
1445
+ self, do_full_update: bool = False, timeout: int | None = None, request_context: RequestContext | None = None
1446
+ ) -> None:
1447
+ input_data = self._generate_input_data(exclude_unmodified=not do_full_update, request_context=request_context)
1410
1448
  mutation_query = self._generate_mutation_query()
1411
1449
  mutation_name = f"{self._schema.kind}Update"
1412
1450
 
@@ -1645,8 +1683,11 @@ class InfrahubNodeSync(InfrahubNodeBase):
1645
1683
  content = self._client.object_store.get(identifier=artifact.storage_id.value) # type: ignore[attr-defined]
1646
1684
  return content
1647
1685
 
1648
- def delete(self, timeout: int | None = None) -> None:
1686
+ def delete(self, timeout: int | None = None, request_context: RequestContext | None = None) -> None:
1649
1687
  input_data = {"data": {"id": self.id}}
1688
+ if context_data := self._get_request_context(request_context=request_context):
1689
+ input_data["context"] = context_data
1690
+
1650
1691
  mutation_query = {"ok": None}
1651
1692
  query = Mutation(
1652
1693
  mutation=f"{self._schema.kind}Delete",
@@ -1661,12 +1702,16 @@ class InfrahubNodeSync(InfrahubNodeBase):
1661
1702
  )
1662
1703
 
1663
1704
  def save(
1664
- self, allow_upsert: bool = False, update_group_context: bool | None = None, timeout: int | None = None
1705
+ self,
1706
+ allow_upsert: bool = False,
1707
+ update_group_context: bool | None = None,
1708
+ timeout: int | None = None,
1709
+ request_context: RequestContext | None = None,
1665
1710
  ) -> None:
1666
1711
  if self._existing is False or allow_upsert is True:
1667
- self.create(allow_upsert=allow_upsert, timeout=timeout)
1712
+ self.create(allow_upsert=allow_upsert, timeout=timeout, request_context=request_context)
1668
1713
  else:
1669
- self.update(timeout=timeout)
1714
+ self.update(timeout=timeout, request_context=request_context)
1670
1715
 
1671
1716
  if update_group_context is None and self._client.mode == InfrahubClientMode.TRACKING:
1672
1717
  update_group_context = True
@@ -1890,15 +1935,17 @@ class InfrahubNodeSync(InfrahubNodeBase):
1890
1935
  related_node.fetch(timeout=timeout)
1891
1936
  setattr(self, rel_name, related_node)
1892
1937
 
1893
- def create(self, allow_upsert: bool = False, timeout: int | None = None) -> None:
1938
+ def create(
1939
+ self, allow_upsert: bool = False, timeout: int | None = None, request_context: RequestContext | None = None
1940
+ ) -> None:
1894
1941
  mutation_query = self._generate_mutation_query()
1895
1942
 
1896
1943
  if allow_upsert:
1897
- input_data = self._generate_input_data(exclude_hfid=False)
1944
+ input_data = self._generate_input_data(exclude_hfid=False, request_context=request_context)
1898
1945
  mutation_name = f"{self._schema.kind}Upsert"
1899
1946
  tracker = f"mutation-{str(self._schema.kind).lower()}-upsert"
1900
1947
  else:
1901
- input_data = self._generate_input_data(exclude_hfid=True)
1948
+ input_data = self._generate_input_data(exclude_hfid=True, request_context=request_context)
1902
1949
  mutation_name = f"{self._schema.kind}Create"
1903
1950
  tracker = f"mutation-{str(self._schema.kind).lower()}-create"
1904
1951
  query = Mutation(
@@ -1917,8 +1964,10 @@ class InfrahubNodeSync(InfrahubNodeBase):
1917
1964
  )
1918
1965
  self._process_mutation_result(mutation_name=mutation_name, response=response, timeout=timeout)
1919
1966
 
1920
- def update(self, do_full_update: bool = False, timeout: int | None = None) -> None:
1921
- input_data = self._generate_input_data(exclude_unmodified=not do_full_update)
1967
+ def update(
1968
+ self, do_full_update: bool = False, timeout: int | None = None, request_context: RequestContext | None = None
1969
+ ) -> None:
1970
+ input_data = self._generate_input_data(exclude_unmodified=not do_full_update, request_context=request_context)
1922
1971
  mutation_query = self._generate_mutation_query()
1923
1972
  mutation_name = f"{self._schema.kind}Update"
1924
1973
 
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
5
5
  if TYPE_CHECKING:
6
6
  import ipaddress
7
7
 
8
+ from .context import RequestContext
8
9
  from .schema import MainSchemaTypes
9
10
 
10
11
 
@@ -169,13 +170,23 @@ class CoreNodeBase(Protocol):
169
170
 
170
171
  @runtime_checkable
171
172
  class CoreNode(CoreNodeBase, Protocol):
172
- async def save(self, allow_upsert: bool = False, update_group_context: bool | None = None) -> None: ...
173
+ async def save(
174
+ self,
175
+ allow_upsert: bool = False,
176
+ update_group_context: bool | None = None,
177
+ timeout: int | None = None,
178
+ request_context: RequestContext | None = None,
179
+ ) -> None: ...
173
180
 
174
- async def delete(self) -> None: ...
181
+ async def delete(self, timeout: int | None = None, request_context: RequestContext | None = None) -> None: ...
175
182
 
176
- async def update(self, do_full_update: bool) -> None: ...
183
+ async def update(
184
+ self, do_full_update: bool, timeout: int | None = None, request_context: RequestContext | None = None
185
+ ) -> None: ...
177
186
 
178
- async def create(self, allow_upsert: bool = False) -> None: ...
187
+ async def create(
188
+ self, allow_upsert: bool = False, timeout: int | None = None, request_context: RequestContext | None = None
189
+ ) -> None: ...
179
190
 
180
191
  async def add_relationships(self, relation_to_update: str, related_nodes: list[str]) -> None: ...
181
192
 
@@ -184,13 +195,23 @@ class CoreNode(CoreNodeBase, Protocol):
184
195
 
185
196
  @runtime_checkable
186
197
  class CoreNodeSync(CoreNodeBase, Protocol):
187
- def save(self, allow_upsert: bool = False, update_group_context: bool | None = None) -> None: ...
188
-
189
- def delete(self) -> None: ...
190
-
191
- def update(self, do_full_update: bool) -> None: ...
192
-
193
- def create(self, allow_upsert: bool = False) -> None: ...
198
+ def save(
199
+ self,
200
+ allow_upsert: bool = False,
201
+ update_group_context: bool | None = None,
202
+ timeout: int | None = None,
203
+ request_context: RequestContext | None = None,
204
+ ) -> None: ...
205
+
206
+ def delete(self, timeout: int | None = None, request_context: RequestContext | None = None) -> None: ...
207
+
208
+ def update(
209
+ self, do_full_update: bool, timeout: int | None = None, request_context: RequestContext | None = None
210
+ ) -> None: ...
211
+
212
+ def create(
213
+ self, allow_upsert: bool = False, timeout: int | None = None, request_context: RequestContext | None = None
214
+ ) -> None: ...
194
215
 
195
216
  def add_relationships(self, relation_to_update: str, related_nodes: list[str]) -> None: ...
196
217