infrahub-server 1.5.0b1__py3-none-any.whl → 1.5.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 (171) hide show
  1. infrahub/api/dependencies.py +4 -13
  2. infrahub/api/internal.py +2 -0
  3. infrahub/api/oauth2.py +13 -19
  4. infrahub/api/oidc.py +15 -21
  5. infrahub/api/schema.py +24 -3
  6. infrahub/api/transformation.py +22 -20
  7. infrahub/artifacts/models.py +2 -1
  8. infrahub/auth.py +137 -3
  9. infrahub/cli/__init__.py +2 -0
  10. infrahub/cli/db.py +158 -155
  11. infrahub/cli/dev.py +118 -0
  12. infrahub/cli/tasks.py +46 -0
  13. infrahub/cli/upgrade.py +56 -9
  14. infrahub/computed_attribute/tasks.py +20 -8
  15. infrahub/core/attribute.py +10 -2
  16. infrahub/core/branch/enums.py +1 -1
  17. infrahub/core/branch/models.py +7 -3
  18. infrahub/core/branch/tasks.py +68 -7
  19. infrahub/core/constants/__init__.py +3 -0
  20. infrahub/core/diff/calculator.py +2 -2
  21. infrahub/core/diff/query/artifact.py +1 -0
  22. infrahub/core/diff/query/delete_query.py +9 -5
  23. infrahub/core/diff/query/field_summary.py +1 -0
  24. infrahub/core/diff/query/merge.py +39 -23
  25. infrahub/core/graph/__init__.py +1 -1
  26. infrahub/core/initialization.py +5 -2
  27. infrahub/core/migrations/__init__.py +3 -0
  28. infrahub/core/migrations/exceptions.py +4 -0
  29. infrahub/core/migrations/graph/__init__.py +12 -13
  30. infrahub/core/migrations/graph/load_schema_branch.py +21 -0
  31. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +1 -1
  32. infrahub/core/migrations/graph/m037_index_attr_vals.py +11 -30
  33. infrahub/core/migrations/graph/m039_ipam_reconcile.py +9 -7
  34. infrahub/core/migrations/graph/m040_duplicated_attributes.py +81 -0
  35. infrahub/core/migrations/graph/m041_deleted_dup_edges.py +149 -0
  36. infrahub/core/migrations/graph/m042_profile_attrs_in_db.py +147 -0
  37. infrahub/core/migrations/graph/m043_create_hfid_display_label_in_db.py +164 -0
  38. infrahub/core/migrations/graph/m044_backfill_hfid_display_label_in_db.py +864 -0
  39. infrahub/core/migrations/query/__init__.py +7 -8
  40. infrahub/core/migrations/query/attribute_add.py +8 -6
  41. infrahub/core/migrations/query/attribute_remove.py +134 -0
  42. infrahub/core/migrations/runner.py +54 -0
  43. infrahub/core/migrations/schema/attribute_kind_update.py +9 -3
  44. infrahub/core/migrations/schema/attribute_supports_profile.py +90 -0
  45. infrahub/core/migrations/schema/node_attribute_add.py +30 -2
  46. infrahub/core/migrations/schema/node_attribute_remove.py +13 -109
  47. infrahub/core/migrations/schema/node_kind_update.py +2 -1
  48. infrahub/core/migrations/schema/node_remove.py +2 -1
  49. infrahub/core/migrations/schema/placeholder_dummy.py +3 -2
  50. infrahub/core/migrations/shared.py +62 -14
  51. infrahub/core/models.py +2 -2
  52. infrahub/core/node/__init__.py +42 -12
  53. infrahub/core/node/create.py +46 -63
  54. infrahub/core/node/lock_utils.py +70 -44
  55. infrahub/core/node/resource_manager/ip_address_pool.py +2 -1
  56. infrahub/core/node/resource_manager/ip_prefix_pool.py +2 -1
  57. infrahub/core/node/resource_manager/number_pool.py +2 -1
  58. infrahub/core/query/attribute.py +55 -0
  59. infrahub/core/query/diff.py +61 -16
  60. infrahub/core/query/ipam.py +16 -4
  61. infrahub/core/query/node.py +51 -43
  62. infrahub/core/query/relationship.py +1 -0
  63. infrahub/core/relationship/model.py +10 -5
  64. infrahub/core/schema/__init__.py +56 -0
  65. infrahub/core/schema/attribute_schema.py +4 -0
  66. infrahub/core/schema/definitions/core/check.py +1 -1
  67. infrahub/core/schema/definitions/core/transform.py +1 -1
  68. infrahub/core/schema/definitions/internal.py +2 -2
  69. infrahub/core/schema/generated/attribute_schema.py +2 -2
  70. infrahub/core/schema/manager.py +22 -1
  71. infrahub/core/schema/schema_branch.py +180 -22
  72. infrahub/core/schema/schema_branch_display.py +12 -0
  73. infrahub/core/schema/schema_branch_hfid.py +6 -0
  74. infrahub/core/validators/uniqueness/checker.py +2 -1
  75. infrahub/database/__init__.py +0 -13
  76. infrahub/database/graph.py +21 -0
  77. infrahub/display_labels/tasks.py +13 -7
  78. infrahub/events/branch_action.py +27 -1
  79. infrahub/generators/tasks.py +3 -7
  80. infrahub/git/base.py +4 -1
  81. infrahub/git/integrator.py +1 -1
  82. infrahub/git/models.py +2 -1
  83. infrahub/git/repository.py +22 -5
  84. infrahub/git/tasks.py +66 -10
  85. infrahub/git/utils.py +123 -1
  86. infrahub/graphql/analyzer.py +9 -0
  87. infrahub/graphql/api/endpoints.py +14 -4
  88. infrahub/graphql/manager.py +4 -9
  89. infrahub/graphql/mutations/branch.py +5 -0
  90. infrahub/graphql/mutations/convert_object_type.py +11 -1
  91. infrahub/graphql/mutations/display_label.py +17 -10
  92. infrahub/graphql/mutations/hfid.py +17 -10
  93. infrahub/graphql/mutations/ipam.py +54 -35
  94. infrahub/graphql/mutations/main.py +27 -28
  95. infrahub/graphql/mutations/proposed_change.py +6 -0
  96. infrahub/graphql/schema_sort.py +170 -0
  97. infrahub/graphql/types/branch.py +4 -1
  98. infrahub/graphql/types/enums.py +3 -0
  99. infrahub/hfid/tasks.py +13 -7
  100. infrahub/lock.py +52 -12
  101. infrahub/message_bus/types.py +3 -1
  102. infrahub/permissions/constants.py +2 -0
  103. infrahub/profiles/queries/get_profile_data.py +4 -5
  104. infrahub/proposed_change/tasks.py +66 -23
  105. infrahub/server.py +6 -2
  106. infrahub/services/__init__.py +2 -2
  107. infrahub/services/adapters/http/__init__.py +5 -0
  108. infrahub/services/adapters/workflow/worker.py +14 -3
  109. infrahub/task_manager/event.py +5 -0
  110. infrahub/task_manager/models.py +7 -0
  111. infrahub/task_manager/task.py +73 -0
  112. infrahub/trigger/setup.py +13 -4
  113. infrahub/trigger/tasks.py +3 -0
  114. infrahub/workers/dependencies.py +10 -1
  115. infrahub/workers/infrahub_async.py +10 -2
  116. infrahub/workflows/catalogue.py +8 -0
  117. infrahub/workflows/initialization.py +5 -0
  118. infrahub/workflows/utils.py +2 -1
  119. infrahub_sdk/analyzer.py +1 -1
  120. infrahub_sdk/batch.py +2 -2
  121. infrahub_sdk/branch.py +14 -2
  122. infrahub_sdk/checks.py +1 -1
  123. infrahub_sdk/client.py +15 -14
  124. infrahub_sdk/config.py +29 -2
  125. infrahub_sdk/ctl/branch.py +3 -0
  126. infrahub_sdk/ctl/cli_commands.py +2 -0
  127. infrahub_sdk/ctl/exceptions.py +1 -1
  128. infrahub_sdk/ctl/schema.py +22 -7
  129. infrahub_sdk/ctl/task.py +110 -0
  130. infrahub_sdk/exceptions.py +18 -18
  131. infrahub_sdk/graphql/query.py +2 -2
  132. infrahub_sdk/node/attribute.py +1 -1
  133. infrahub_sdk/node/property.py +1 -1
  134. infrahub_sdk/node/related_node.py +3 -3
  135. infrahub_sdk/node/relationship.py +4 -6
  136. infrahub_sdk/object_store.py +2 -2
  137. infrahub_sdk/operation.py +1 -1
  138. infrahub_sdk/protocols_generator/generator.py +1 -1
  139. infrahub_sdk/pytest_plugin/exceptions.py +9 -9
  140. infrahub_sdk/pytest_plugin/items/base.py +1 -1
  141. infrahub_sdk/pytest_plugin/items/check.py +1 -1
  142. infrahub_sdk/pytest_plugin/items/python_transform.py +1 -1
  143. infrahub_sdk/repository.py +1 -1
  144. infrahub_sdk/schema/__init__.py +33 -5
  145. infrahub_sdk/spec/models.py +7 -0
  146. infrahub_sdk/spec/object.py +41 -102
  147. infrahub_sdk/spec/processors/__init__.py +0 -0
  148. infrahub_sdk/spec/processors/data_processor.py +10 -0
  149. infrahub_sdk/spec/processors/factory.py +34 -0
  150. infrahub_sdk/spec/processors/range_expand_processor.py +56 -0
  151. infrahub_sdk/task/exceptions.py +4 -4
  152. infrahub_sdk/task/manager.py +2 -2
  153. infrahub_sdk/task/models.py +6 -4
  154. infrahub_sdk/timestamp.py +1 -1
  155. infrahub_sdk/transfer/exporter/json.py +1 -1
  156. infrahub_sdk/transfer/importer/json.py +1 -1
  157. infrahub_sdk/transforms.py +1 -1
  158. {infrahub_server-1.5.0b1.dist-info → infrahub_server-1.5.1.dist-info}/METADATA +4 -2
  159. {infrahub_server-1.5.0b1.dist-info → infrahub_server-1.5.1.dist-info}/RECORD +168 -152
  160. infrahub_testcontainers/container.py +144 -6
  161. infrahub_testcontainers/docker-compose-cluster.test.yml +5 -0
  162. infrahub_testcontainers/docker-compose.test.yml +5 -0
  163. infrahub_testcontainers/helpers.py +19 -4
  164. infrahub_testcontainers/models.py +8 -6
  165. infrahub_testcontainers/performance_test.py +6 -4
  166. infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +0 -166
  167. infrahub/core/migrations/graph/m041_create_hfid_display_label_in_db.py +0 -97
  168. infrahub/core/migrations/graph/m042_backfill_hfid_display_label_in_db.py +0 -86
  169. {infrahub_server-1.5.0b1.dist-info → infrahub_server-1.5.1.dist-info}/LICENSE.txt +0 -0
  170. {infrahub_server-1.5.0b1.dist-info → infrahub_server-1.5.1.dist-info}/WHEEL +0 -0
  171. {infrahub_server-1.5.0b1.dist-info → infrahub_server-1.5.1.dist-info}/entry_points.txt +0 -0
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
17
17
  class RelatedNodeBase:
18
18
  """Base class for representing a related node in a relationship."""
19
19
 
20
- def __init__(self, branch: str, schema: RelationshipSchemaAPI, data: Any | dict, name: str | None = None):
20
+ def __init__(self, branch: str, schema: RelationshipSchemaAPI, data: Any | dict, name: str | None = None) -> None:
21
21
  """
22
22
  Args:
23
23
  branch (str): The branch where the related node resides.
@@ -189,7 +189,7 @@ class RelatedNode(RelatedNodeBase):
189
189
  schema: RelationshipSchemaAPI,
190
190
  data: Any | dict,
191
191
  name: str | None = None,
192
- ):
192
+ ) -> None:
193
193
  """
194
194
  Args:
195
195
  client (InfrahubClient): The client used to interact with the backend asynchronously.
@@ -236,7 +236,7 @@ class RelatedNodeSync(RelatedNodeBase):
236
236
  schema: RelationshipSchemaAPI,
237
237
  data: Any | dict,
238
238
  name: str | None = None,
239
- ):
239
+ ) -> None:
240
240
  """
241
241
  Args:
242
242
  client (InfrahubClientSync): The client used to interact with the backend synchronously.
@@ -4,7 +4,6 @@ from collections import defaultdict
4
4
  from collections.abc import Iterable
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
- from ..batch import InfrahubBatch
8
7
  from ..exceptions import (
9
8
  Error,
10
9
  UninitializedError,
@@ -22,7 +21,7 @@ if TYPE_CHECKING:
22
21
  class RelationshipManagerBase:
23
22
  """Base class for RelationshipManager and RelationshipManagerSync"""
24
23
 
25
- def __init__(self, name: str, branch: str, schema: RelationshipSchemaAPI):
24
+ def __init__(self, name: str, branch: str, schema: RelationshipSchemaAPI) -> None:
26
25
  """
27
26
  Args:
28
27
  name (str): The name of the relationship.
@@ -108,7 +107,7 @@ class RelationshipManager(RelationshipManagerBase):
108
107
  branch: str,
109
108
  schema: RelationshipSchemaAPI,
110
109
  data: Any | dict,
111
- ):
110
+ ) -> None:
112
111
  """
113
112
  Args:
114
113
  name (str): The name of the relationship.
@@ -166,7 +165,7 @@ class RelationshipManager(RelationshipManagerBase):
166
165
  raise Error("Unable to fetch the peer, id and/or typename are not defined")
167
166
  ids_per_kind_map[peer.typename].append(peer.id)
168
167
 
169
- batch = InfrahubBatch(max_concurrent_execution=self.client.max_concurrent_execution)
168
+ batch = await self.client.create_batch()
170
169
  for kind, ids in ids_per_kind_map.items():
171
170
  batch.add(
172
171
  task=self.client.filters,
@@ -231,7 +230,7 @@ class RelationshipManagerSync(RelationshipManagerBase):
231
230
  branch: str,
232
231
  schema: RelationshipSchemaAPI,
233
232
  data: Any | dict,
234
- ):
233
+ ) -> None:
235
234
  """
236
235
  Args:
237
236
  name (str): The name of the relationship.
@@ -289,7 +288,6 @@ class RelationshipManagerSync(RelationshipManagerBase):
289
288
  raise Error("Unable to fetch the peer, id and/or typename are not defined")
290
289
  ids_per_kind_map[peer.typename].append(peer.id)
291
290
 
292
- # Unlike Async, no need to create a new batch from scratch because we are not using a semaphore
293
291
  batch = self.client.create_batch()
294
292
  for kind, ids in ids_per_kind_map.items():
295
293
  batch.add(
@@ -16,7 +16,7 @@ class ObjectStoreBase:
16
16
 
17
17
 
18
18
  class ObjectStore(ObjectStoreBase):
19
- def __init__(self, client: InfrahubClient):
19
+ def __init__(self, client: InfrahubClient) -> None:
20
20
  self.client = client
21
21
 
22
22
  async def get(self, identifier: str, tracker: str | None = None) -> str:
@@ -64,7 +64,7 @@ class ObjectStore(ObjectStoreBase):
64
64
 
65
65
 
66
66
  class ObjectStoreSync(ObjectStoreBase):
67
- def __init__(self, client: InfrahubClientSync):
67
+ def __init__(self, client: InfrahubClientSync) -> None:
68
68
  self.client = client
69
69
 
70
70
  def get(self, identifier: str, tracker: str | None = None) -> str:
infrahub_sdk/operation.py CHANGED
@@ -19,7 +19,7 @@ class InfrahubOperation:
19
19
  convert_query_response: bool,
20
20
  branch: str,
21
21
  root_directory: str,
22
- ):
22
+ ) -> None:
23
23
  self.branch = branch
24
24
  self.convert_query_response = convert_query_response
25
25
  self.root_directory = root_directory or os.getcwd()
@@ -34,7 +34,7 @@ def move_to_end_of_list(lst: list, item: str) -> list:
34
34
 
35
35
 
36
36
  class CodeGenerator:
37
- def __init__(self, schema: dict[str, MainSchemaTypesAll]):
37
+ def __init__(self, schema: dict[str, MainSchemaTypesAll]) -> None:
38
38
  self.generics: dict[str, GenericSchemaAPI | GenericSchema] = {}
39
39
  self.nodes: dict[str, NodeSchemaAPI | NodeSchema] = {}
40
40
  self.profiles: dict[str, ProfileSchemaAPI] = {}
@@ -7,37 +7,37 @@ class Error(Exception):
7
7
 
8
8
 
9
9
  class InvalidResourceConfigError(Error):
10
- def __init__(self, resource_name: str):
10
+ def __init__(self, resource_name: str) -> None:
11
11
  super().__init__(f"Improperly configured resource with name '{resource_name}'.")
12
12
 
13
13
 
14
14
  class DirectoryNotFoundError(Error):
15
- def __init__(self, name: str, message: str = ""):
15
+ def __init__(self, name: str, message: str = "") -> None:
16
16
  self.message = message or f"Unable to find directory {name!r}."
17
17
  super().__init__(self.message)
18
18
 
19
19
 
20
20
  class FileNotValidError(Error):
21
- def __init__(self, name: str, message: str = ""):
21
+ def __init__(self, name: str, message: str = "") -> None:
22
22
  self.message = message or f"Unable to access file {name!r}."
23
23
  super().__init__(self.message)
24
24
 
25
25
 
26
26
  class OutputMatchError(Error):
27
- def __init__(self, name: str, message: str = "", differences: str = ""):
27
+ def __init__(self, name: str, message: str = "", differences: str = "") -> None:
28
28
  self.message = message or f"Rendered output does not match expected output for {name!r}."
29
29
  self.differences = differences
30
30
  super().__init__(self.message)
31
31
 
32
32
 
33
33
  class Jinja2TransformError(Error):
34
- def __init__(self, name: str, message: str = ""):
34
+ def __init__(self, name: str, message: str = "") -> None:
35
35
  self.message = message or f"Unexpected error happened while processing {name!r}."
36
36
  super().__init__(self.message)
37
37
 
38
38
 
39
39
  class Jinja2TransformUndefinedError(Error):
40
- def __init__(self, name: str, rtb: Traceback, errors: list[tuple[Frame, Syntax]], message: str = ""):
40
+ def __init__(self, name: str, rtb: Traceback, errors: list[tuple[Frame, Syntax]], message: str = "") -> None:
41
41
  self.rtb = rtb
42
42
  self.errors = errors
43
43
  self.message = message or f"Unable to render Jinja2 transform {name!r}."
@@ -45,18 +45,18 @@ class Jinja2TransformUndefinedError(Error):
45
45
 
46
46
 
47
47
  class CheckDefinitionError(Error):
48
- def __init__(self, name: str, message: str = ""):
48
+ def __init__(self, name: str, message: str = "") -> None:
49
49
  self.message = message or f"Check {name!r} is not properly defined."
50
50
  super().__init__(self.message)
51
51
 
52
52
 
53
53
  class CheckResultError(Error):
54
- def __init__(self, name: str, message: str = ""):
54
+ def __init__(self, name: str, message: str = "") -> None:
55
55
  self.message = message or f"Unexpected result for check {name!r}."
56
56
  super().__init__(self.message)
57
57
 
58
58
 
59
59
  class PythonTransformDefinitionError(Error):
60
- def __init__(self, name: str, message: str = ""):
60
+ def __init__(self, name: str, message: str = "") -> None:
61
61
  self.message = message or f"Python transform {name!r} is not properly defined."
62
62
  super().__init__(self.message)
@@ -25,7 +25,7 @@ class InfrahubItem(pytest.Item):
25
25
  resource_config: InfrahubRepositoryConfigElement,
26
26
  test: InfrahubTest,
27
27
  **kwargs: dict[str, Any],
28
- ):
28
+ ) -> None:
29
29
  super().__init__(*args, **kwargs) # type: ignore[arg-type]
30
30
  self.resource_name: str = resource_name
31
31
  self.resource_config: InfrahubRepositoryConfigElement = resource_config
@@ -27,7 +27,7 @@ class InfrahubCheckItem(InfrahubItem):
27
27
  resource_config: InfrahubRepositoryConfigElement,
28
28
  test: InfrahubTest,
29
29
  **kwargs: dict[str, Any],
30
- ):
30
+ ) -> None:
31
31
  super().__init__(*args, resource_name=resource_name, resource_config=resource_config, test=test, **kwargs)
32
32
 
33
33
  self.check_instance: InfrahubCheck
@@ -28,7 +28,7 @@ class InfrahubPythonTransformItem(InfrahubItem):
28
28
  resource_config: InfrahubRepositoryConfigElement,
29
29
  test: InfrahubTest,
30
30
  **kwargs: dict[str, Any],
31
- ):
31
+ ) -> None:
32
32
  super().__init__(*args, resource_name=resource_name, resource_config=resource_config, test=test, **kwargs)
33
33
 
34
34
  self.transform_instance: InfrahubTransform
@@ -7,7 +7,7 @@ from dulwich.repo import Repo
7
7
 
8
8
 
9
9
  class GitRepoManager:
10
- def __init__(self, root_directory: str, branch: str = "main"):
10
+ def __init__(self, root_directory: str, branch: str = "main") -> None:
11
11
  self.root_directory = root_directory
12
12
  self.branch = branch
13
13
  self.git: Repo = self.initialize_repo()
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import inspect
4
5
  import json
5
6
  import warnings
6
7
  from collections.abc import MutableMapping
@@ -90,11 +91,31 @@ MainSchemaTypesAll: TypeAlias = Union[
90
91
  ]
91
92
 
92
93
 
94
+ class SchemaWarningType(Enum):
95
+ DEPRECATION = "deprecation"
96
+
97
+
98
+ class SchemaWarningKind(BaseModel):
99
+ kind: str = Field(..., description="The kind impacted by the warning")
100
+ field: str | None = Field(default=None, description="The attribute or relationship impacted by the warning")
101
+
102
+ @property
103
+ def display(self) -> str:
104
+ suffix = f".{self.field}" if self.field else ""
105
+ return f"{self.kind}{suffix}"
106
+
107
+
108
+ class SchemaWarning(BaseModel):
109
+ type: SchemaWarningType = Field(..., description="The type of warning")
110
+ kinds: list[SchemaWarningKind] = Field(default_factory=list, description="The kinds impacted by the warning")
111
+ message: str = Field(..., description="The message that describes the warning")
112
+
113
+
93
114
  class InfrahubSchemaBase:
94
115
  client: InfrahubClient | InfrahubClientSync
95
116
  cache: dict[str, BranchSchema]
96
117
 
97
- def __init__(self, client: InfrahubClient | InfrahubClientSync):
118
+ def __init__(self, client: InfrahubClient | InfrahubClientSync) -> None:
98
119
  self.client = client
99
120
  self.cache = {}
100
121
 
@@ -169,7 +190,9 @@ class InfrahubSchemaBase:
169
190
  def _validate_load_schema_response(response: httpx.Response) -> SchemaLoadResponse:
170
191
  if response.status_code == httpx.codes.OK:
171
192
  status = response.json()
172
- return SchemaLoadResponse(hash=status["hash"], previous_hash=status["previous_hash"])
193
+ return SchemaLoadResponse(
194
+ hash=status["hash"], previous_hash=status["previous_hash"], warnings=status.get("warnings") or []
195
+ )
173
196
 
174
197
  if response.status_code in [
175
198
  httpx.codes.BAD_REQUEST,
@@ -185,12 +208,16 @@ class InfrahubSchemaBase:
185
208
 
186
209
  @staticmethod
187
210
  def _get_schema_name(schema: type[SchemaType | SchemaTypeSync] | str) -> str:
188
- if hasattr(schema, "_is_runtime_protocol") and schema._is_runtime_protocol: # type: ignore[union-attr]
189
- return schema.__name__ # type: ignore[union-attr]
190
-
191
211
  if isinstance(schema, str):
192
212
  return schema
193
213
 
214
+ if hasattr(schema, "_is_runtime_protocol") and getattr(schema, "_is_runtime_protocol", None):
215
+ if inspect.iscoroutinefunction(schema.save):
216
+ return schema.__name__
217
+ if schema.__name__[-4:] == "Sync":
218
+ return schema.__name__[:-4]
219
+ return schema.__name__
220
+
194
221
  raise ValueError("schema must be a protocol or a string")
195
222
 
196
223
  @staticmethod
@@ -802,6 +829,7 @@ class SchemaLoadResponse(BaseModel):
802
829
  hash: str = Field(default="", description="The new hash for the entire schema")
803
830
  previous_hash: str = Field(default="", description="The previous hash for the entire schema")
804
831
  errors: dict = Field(default_factory=dict, description="Errors reported by the server")
832
+ warnings: list[SchemaWarning] = Field(default_factory=list, description="Warnings reported by the server")
805
833
 
806
834
  @property
807
835
  def schema_updated(self) -> bool:
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class InfrahubObjectParameters(BaseModel):
7
+ expand_range: bool = False
@@ -1,17 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
- import copy
4
- import re
5
- from abc import ABC, abstractmethod
6
3
  from enum import Enum
7
- from typing import TYPE_CHECKING, Any, ClassVar
4
+ from typing import TYPE_CHECKING, Any
8
5
 
9
6
  from pydantic import BaseModel, Field
10
7
 
11
8
  from ..exceptions import ObjectValidationError, ValidationError
12
9
  from ..schema import GenericSchemaAPI, RelationshipKind, RelationshipSchema
13
10
  from ..yaml import InfrahubFile, InfrahubFileKind
14
- from .range_expansion import MATCH_PATTERN, range_expansion
11
+ from .models import InfrahubObjectParameters
12
+ from .processors.factory import DataProcessorFactory
15
13
 
16
14
  if TYPE_CHECKING:
17
15
  from ..client import InfrahubClient
@@ -46,11 +44,6 @@ class RelationshipDataFormat(str, Enum):
46
44
  MANY_REF = "many_ref_list"
47
45
 
48
46
 
49
- class ObjectStrategy(str, Enum):
50
- NORMAL = "normal"
51
- RANGE_EXPAND = "range_expand"
52
-
53
-
54
47
  class RelationshipInfo(BaseModel):
55
48
  name: str
56
49
  rel_schema: RelationshipSchema
@@ -173,97 +166,21 @@ async def get_relationship_info(
173
166
  return info
174
167
 
175
168
 
176
- def expand_data_with_ranges(data: list[dict[str, Any]]) -> list[dict[str, Any]]:
177
- """Expand any item in data with range pattern in any value. Supports multiple fields, requires equal expansion length."""
178
- range_pattern = re.compile(MATCH_PATTERN)
179
- expanded = []
180
- for item in data:
181
- # Find all fields to expand
182
- expand_fields = {}
183
- for key, value in item.items():
184
- if isinstance(value, str) and range_pattern.search(value):
185
- try:
186
- expand_fields[key] = range_expansion(value)
187
- except Exception:
188
- # If expansion fails, treat as no expansion
189
- expand_fields[key] = [value]
190
- if not expand_fields:
191
- expanded.append(item)
192
- continue
193
- # Check all expanded lists have the same length
194
- lengths = [len(v) for v in expand_fields.values()]
195
- if len(set(lengths)) > 1:
196
- raise ValidationError(f"Range expansion mismatch: fields expanded to different lengths: {lengths}")
197
- n = lengths[0]
198
- # Zip expanded values and produce new items
199
- for i in range(n):
200
- new_item = copy.deepcopy(item)
201
- for key, values in expand_fields.items():
202
- new_item[key] = values[i]
203
- expanded.append(new_item)
204
- return expanded
205
-
206
-
207
- class DataProcessor(ABC):
208
- """Abstract base class for data processing strategies"""
209
-
210
- @abstractmethod
211
- def process_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]:
212
- """Process the data according to the strategy"""
213
-
214
-
215
- class SingleDataProcessor(DataProcessor):
216
- """Process data without any expansion"""
217
-
218
- def process_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]:
219
- return data
220
-
221
-
222
- class RangeExpandDataProcessor(DataProcessor):
223
- """Process data with range expansion"""
224
-
225
- def process_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]:
226
- return expand_data_with_ranges(data)
227
-
228
-
229
- class DataProcessorFactory:
230
- """Factory to create appropriate data processor based on strategy"""
231
-
232
- _processors: ClassVar[dict[ObjectStrategy, type[DataProcessor]]] = {
233
- ObjectStrategy.NORMAL: SingleDataProcessor,
234
- ObjectStrategy.RANGE_EXPAND: RangeExpandDataProcessor,
235
- }
236
-
237
- @classmethod
238
- def get_processor(cls, strategy: ObjectStrategy) -> DataProcessor:
239
- processor_class = cls._processors.get(strategy)
240
- if not processor_class:
241
- raise ValueError(
242
- f"Unknown strategy: {strategy} - no processor found. Valid strategies are: {list(cls._processors.keys())}"
243
- )
244
- return processor_class()
245
-
246
- @classmethod
247
- def register_processor(cls, strategy: ObjectStrategy, processor_class: type[DataProcessor]) -> None:
248
- """Register a new processor for a strategy - useful for future extensions"""
249
- cls._processors[strategy] = processor_class
250
-
251
-
252
169
  class InfrahubObjectFileData(BaseModel):
253
170
  kind: str
254
- strategy: ObjectStrategy = ObjectStrategy.NORMAL
171
+ parameters: InfrahubObjectParameters = Field(default_factory=InfrahubObjectParameters)
255
172
  data: list[dict[str, Any]] = Field(default_factory=list)
256
173
 
257
- def _get_processed_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]:
174
+ async def _get_processed_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]:
258
175
  """Get data processed according to the strategy"""
259
- processor = DataProcessorFactory.get_processor(self.strategy)
260
- return processor.process_data(data)
176
+
177
+ return await DataProcessorFactory.process_data(kind=self.kind, parameters=self.parameters, data=data)
261
178
 
262
179
  async def validate_format(self, client: InfrahubClient, branch: str | None = None) -> list[ObjectValidationError]:
263
180
  errors: list[ObjectValidationError] = []
264
181
  schema = await client.schema.get(kind=self.kind, branch=branch)
265
182
 
266
- processed_data = self._get_processed_data(data=self.data)
183
+ processed_data = await self._get_processed_data(data=self.data)
267
184
  self.data = processed_data
268
185
 
269
186
  for idx, item in enumerate(processed_data):
@@ -275,14 +192,14 @@ class InfrahubObjectFileData(BaseModel):
275
192
  data=item,
276
193
  branch=branch,
277
194
  default_schema_kind=self.kind,
278
- strategy=self.strategy, # Pass strategy down
195
+ parameters=self.parameters,
279
196
  )
280
197
  )
281
198
  return errors
282
199
 
283
200
  async def process(self, client: InfrahubClient, branch: str | None = None) -> None:
284
201
  schema = await client.schema.get(kind=self.kind, branch=branch)
285
- processed_data = self._get_processed_data(data=self.data)
202
+ processed_data = await self._get_processed_data(data=self.data)
286
203
 
287
204
  for idx, item in enumerate(processed_data):
288
205
  await self.create_node(
@@ -292,6 +209,7 @@ class InfrahubObjectFileData(BaseModel):
292
209
  position=[idx + 1],
293
210
  branch=branch,
294
211
  default_schema_kind=self.kind,
212
+ parameters=self.parameters,
295
213
  )
296
214
 
297
215
  @classmethod
@@ -304,8 +222,9 @@ class InfrahubObjectFileData(BaseModel):
304
222
  context: dict | None = None,
305
223
  branch: str | None = None,
306
224
  default_schema_kind: str | None = None,
307
- strategy: ObjectStrategy = ObjectStrategy.NORMAL,
225
+ parameters: InfrahubObjectParameters | None = None,
308
226
  ) -> list[ObjectValidationError]:
227
+ parameters = parameters or InfrahubObjectParameters()
309
228
  errors: list[ObjectValidationError] = []
310
229
  context = context.copy() if context else {}
311
230
 
@@ -354,7 +273,7 @@ class InfrahubObjectFileData(BaseModel):
354
273
  context=context,
355
274
  branch=branch,
356
275
  default_schema_kind=default_schema_kind,
357
- strategy=strategy,
276
+ parameters=parameters,
358
277
  )
359
278
  )
360
279
 
@@ -370,8 +289,9 @@ class InfrahubObjectFileData(BaseModel):
370
289
  context: dict | None = None,
371
290
  branch: str | None = None,
372
291
  default_schema_kind: str | None = None,
373
- strategy: ObjectStrategy = ObjectStrategy.NORMAL,
292
+ parameters: InfrahubObjectParameters | None = None,
374
293
  ) -> list[ObjectValidationError]:
294
+ parameters = parameters or InfrahubObjectParameters()
375
295
  context = context.copy() if context else {}
376
296
  errors: list[ObjectValidationError] = []
377
297
 
@@ -399,6 +319,7 @@ class InfrahubObjectFileData(BaseModel):
399
319
  context=context,
400
320
  branch=branch,
401
321
  default_schema_kind=default_schema_kind,
322
+ parameters=parameters,
402
323
  )
403
324
  )
404
325
  return errors
@@ -412,11 +333,11 @@ class InfrahubObjectFileData(BaseModel):
412
333
  rel_info.find_matching_relationship(peer_schema=peer_schema)
413
334
  context.update(rel_info.get_context(value="placeholder"))
414
335
 
415
- # Use strategy-aware data processing
416
- processor = DataProcessorFactory.get_processor(strategy)
417
- expanded_data = processor.process_data(data["data"])
336
+ processed_data = await DataProcessorFactory.process_data(
337
+ kind=peer_kind, data=data["data"], parameters=parameters
338
+ )
418
339
 
419
- for idx, peer_data in enumerate(expanded_data):
340
+ for idx, peer_data in enumerate(processed_data):
420
341
  context["list_index"] = idx
421
342
  errors.extend(
422
343
  await cls.validate_object(
@@ -427,7 +348,7 @@ class InfrahubObjectFileData(BaseModel):
427
348
  context=context,
428
349
  branch=branch,
429
350
  default_schema_kind=default_schema_kind,
430
- strategy=strategy,
351
+ parameters=parameters,
431
352
  )
432
353
  )
433
354
  return errors
@@ -452,6 +373,7 @@ class InfrahubObjectFileData(BaseModel):
452
373
  context=context,
453
374
  branch=branch,
454
375
  default_schema_kind=default_schema_kind,
376
+ parameters=parameters,
455
377
  )
456
378
  )
457
379
  return errors
@@ -478,7 +400,9 @@ class InfrahubObjectFileData(BaseModel):
478
400
  context: dict | None = None,
479
401
  branch: str | None = None,
480
402
  default_schema_kind: str | None = None,
403
+ parameters: InfrahubObjectParameters | None = None,
481
404
  ) -> InfrahubNode:
405
+ parameters = parameters or InfrahubObjectParameters()
482
406
  context = context.copy() if context else {}
483
407
 
484
408
  errors = await cls.validate_object(
@@ -489,6 +413,7 @@ class InfrahubObjectFileData(BaseModel):
489
413
  context=context,
490
414
  branch=branch,
491
415
  default_schema_kind=default_schema_kind,
416
+ parameters=parameters,
492
417
  )
493
418
  if errors:
494
419
  messages = [str(error) for error in errors]
@@ -545,6 +470,9 @@ class InfrahubObjectFileData(BaseModel):
545
470
  data=value,
546
471
  branch=branch,
547
472
  default_schema_kind=default_schema_kind,
473
+ parameters=InfrahubObjectParameters(**value.get("parameters"))
474
+ if "parameters" in value
475
+ else None,
548
476
  )
549
477
  clean_data[key] = nodes
550
478
 
@@ -583,6 +511,9 @@ class InfrahubObjectFileData(BaseModel):
583
511
  context=context,
584
512
  branch=branch,
585
513
  default_schema_kind=default_schema_kind,
514
+ parameters=InfrahubObjectParameters(**data[rel].get("parameters"))
515
+ if "parameters" in data[rel]
516
+ else None,
586
517
  )
587
518
 
588
519
  return node
@@ -598,7 +529,9 @@ class InfrahubObjectFileData(BaseModel):
598
529
  context: dict | None = None,
599
530
  branch: str | None = None,
600
531
  default_schema_kind: str | None = None,
532
+ parameters: InfrahubObjectParameters | None = None,
601
533
  ) -> list[InfrahubNode]:
534
+ parameters = parameters or InfrahubObjectParameters()
602
535
  nodes: list[InfrahubNode] = []
603
536
  context = context.copy() if context else {}
604
537
 
@@ -618,6 +551,7 @@ class InfrahubObjectFileData(BaseModel):
618
551
  context=context,
619
552
  branch=branch,
620
553
  default_schema_kind=default_schema_kind,
554
+ parameters=parameters,
621
555
  )
622
556
  return [new_node]
623
557
 
@@ -631,7 +565,10 @@ class InfrahubObjectFileData(BaseModel):
631
565
  rel_info.find_matching_relationship(peer_schema=peer_schema)
632
566
  context.update(rel_info.get_context(value=parent_node.id))
633
567
 
634
- expanded_data = expand_data_with_ranges(data=data["data"])
568
+ expanded_data = await DataProcessorFactory.process_data(
569
+ kind=peer_kind, data=data["data"], parameters=parameters
570
+ )
571
+
635
572
  for idx, peer_data in enumerate(expanded_data):
636
573
  context["list_index"] = idx
637
574
  if isinstance(peer_data, dict):
@@ -643,6 +580,7 @@ class InfrahubObjectFileData(BaseModel):
643
580
  context=context,
644
581
  branch=branch,
645
582
  default_schema_kind=default_schema_kind,
583
+ parameters=parameters,
646
584
  )
647
585
  nodes.append(node)
648
586
  return nodes
@@ -668,6 +606,7 @@ class InfrahubObjectFileData(BaseModel):
668
606
  context=context,
669
607
  branch=branch,
670
608
  default_schema_kind=default_schema_kind,
609
+ parameters=parameters,
671
610
  )
672
611
  nodes.append(node)
673
612
 
File without changes
@@ -0,0 +1,10 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any
3
+
4
+
5
+ class DataProcessor(ABC):
6
+ """Abstract base class for data processing strategies"""
7
+
8
+ @abstractmethod
9
+ async def process_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]:
10
+ """Process the data according to the strategy"""
@@ -0,0 +1,34 @@
1
+ from collections.abc import Sequence
2
+ from typing import Any
3
+
4
+ from ..models import InfrahubObjectParameters
5
+ from .data_processor import DataProcessor
6
+ from .range_expand_processor import RangeExpandDataProcessor
7
+
8
+ PROCESSOR_PER_KIND: dict[str, DataProcessor] = {}
9
+
10
+
11
+ class DataProcessorFactory:
12
+ """Factory to create appropriate data processor based on strategy"""
13
+
14
+ @classmethod
15
+ def get_processors(cls, kind: str, parameters: InfrahubObjectParameters) -> Sequence[DataProcessor]:
16
+ processors: list[DataProcessor] = []
17
+ if parameters.expand_range:
18
+ processors.append(RangeExpandDataProcessor())
19
+ if kind in PROCESSOR_PER_KIND:
20
+ processors.append(PROCESSOR_PER_KIND[kind])
21
+
22
+ return processors
23
+
24
+ @classmethod
25
+ async def process_data(
26
+ cls,
27
+ kind: str,
28
+ data: list[dict[str, Any]],
29
+ parameters: InfrahubObjectParameters,
30
+ ) -> list[dict[str, Any]]:
31
+ processors = cls.get_processors(kind=kind, parameters=parameters)
32
+ for processor in processors:
33
+ data = await processor.process_data(data=data)
34
+ return data