infrahub-server 1.4.10__py3-none-any.whl → 1.5.0b1__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 (178) hide show
  1. infrahub/actions/tasks.py +208 -16
  2. infrahub/api/artifact.py +3 -0
  3. infrahub/api/diff/diff.py +1 -1
  4. infrahub/api/query.py +2 -0
  5. infrahub/api/schema.py +3 -0
  6. infrahub/auth.py +5 -5
  7. infrahub/cli/db.py +26 -2
  8. infrahub/cli/db_commands/clean_duplicate_schema_fields.py +212 -0
  9. infrahub/config.py +7 -2
  10. infrahub/core/attribute.py +25 -22
  11. infrahub/core/branch/models.py +2 -2
  12. infrahub/core/branch/needs_rebase_status.py +11 -0
  13. infrahub/core/branch/tasks.py +4 -3
  14. infrahub/core/changelog/models.py +4 -12
  15. infrahub/core/constants/__init__.py +1 -0
  16. infrahub/core/constants/infrahubkind.py +1 -0
  17. infrahub/core/convert_object_type/object_conversion.py +201 -0
  18. infrahub/core/convert_object_type/repository_conversion.py +89 -0
  19. infrahub/core/convert_object_type/schema_mapping.py +27 -3
  20. infrahub/core/diff/model/path.py +4 -0
  21. infrahub/core/diff/payload_builder.py +1 -1
  22. infrahub/core/diff/query/artifact.py +1 -1
  23. infrahub/core/graph/__init__.py +1 -1
  24. infrahub/core/initialization.py +2 -2
  25. infrahub/core/ipam/utilization.py +1 -1
  26. infrahub/core/manager.py +9 -84
  27. infrahub/core/migrations/graph/__init__.py +6 -0
  28. infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +166 -0
  29. infrahub/core/migrations/graph/m041_create_hfid_display_label_in_db.py +97 -0
  30. infrahub/core/migrations/graph/m042_backfill_hfid_display_label_in_db.py +86 -0
  31. infrahub/core/migrations/schema/node_attribute_add.py +5 -2
  32. infrahub/core/migrations/shared.py +5 -6
  33. infrahub/core/node/__init__.py +165 -42
  34. infrahub/core/node/constraints/attribute_uniqueness.py +3 -1
  35. infrahub/core/node/create.py +67 -35
  36. infrahub/core/node/lock_utils.py +98 -0
  37. infrahub/core/node/node_property_attribute.py +230 -0
  38. infrahub/core/node/standard.py +1 -1
  39. infrahub/core/property.py +11 -0
  40. infrahub/core/protocols.py +8 -1
  41. infrahub/core/query/attribute.py +27 -15
  42. infrahub/core/query/node.py +61 -185
  43. infrahub/core/query/relationship.py +43 -26
  44. infrahub/core/query/subquery.py +0 -8
  45. infrahub/core/registry.py +2 -2
  46. infrahub/core/relationship/constraints/count.py +1 -1
  47. infrahub/core/relationship/model.py +60 -20
  48. infrahub/core/schema/attribute_schema.py +0 -2
  49. infrahub/core/schema/basenode_schema.py +42 -2
  50. infrahub/core/schema/definitions/core/__init__.py +2 -0
  51. infrahub/core/schema/definitions/core/generator.py +2 -0
  52. infrahub/core/schema/definitions/core/group.py +16 -2
  53. infrahub/core/schema/definitions/core/repository.py +7 -0
  54. infrahub/core/schema/definitions/internal.py +14 -1
  55. infrahub/core/schema/generated/base_node_schema.py +6 -1
  56. infrahub/core/schema/node_schema.py +5 -2
  57. infrahub/core/schema/relationship_schema.py +0 -1
  58. infrahub/core/schema/schema_branch.py +137 -2
  59. infrahub/core/schema/schema_branch_display.py +123 -0
  60. infrahub/core/schema/schema_branch_hfid.py +114 -0
  61. infrahub/core/validators/aggregated_checker.py +1 -1
  62. infrahub/core/validators/determiner.py +12 -1
  63. infrahub/core/validators/relationship/peer.py +1 -1
  64. infrahub/core/validators/tasks.py +1 -1
  65. infrahub/display_labels/__init__.py +0 -0
  66. infrahub/display_labels/gather.py +48 -0
  67. infrahub/display_labels/models.py +240 -0
  68. infrahub/display_labels/tasks.py +186 -0
  69. infrahub/display_labels/triggers.py +22 -0
  70. infrahub/events/group_action.py +1 -1
  71. infrahub/events/node_action.py +1 -1
  72. infrahub/generators/constants.py +7 -0
  73. infrahub/generators/models.py +38 -12
  74. infrahub/generators/tasks.py +34 -16
  75. infrahub/git/base.py +38 -1
  76. infrahub/git/integrator.py +22 -14
  77. infrahub/graphql/analyzer.py +1 -1
  78. infrahub/graphql/api/dependencies.py +2 -4
  79. infrahub/graphql/api/endpoints.py +2 -2
  80. infrahub/graphql/app.py +2 -4
  81. infrahub/graphql/initialization.py +2 -3
  82. infrahub/graphql/manager.py +212 -137
  83. infrahub/graphql/middleware.py +12 -0
  84. infrahub/graphql/mutations/branch.py +11 -0
  85. infrahub/graphql/mutations/computed_attribute.py +110 -3
  86. infrahub/graphql/mutations/convert_object_type.py +34 -13
  87. infrahub/graphql/mutations/display_label.py +111 -0
  88. infrahub/graphql/mutations/generator.py +25 -7
  89. infrahub/graphql/mutations/hfid.py +118 -0
  90. infrahub/graphql/mutations/ipam.py +21 -8
  91. infrahub/graphql/mutations/main.py +37 -153
  92. infrahub/graphql/mutations/profile.py +195 -0
  93. infrahub/graphql/mutations/proposed_change.py +2 -1
  94. infrahub/graphql/mutations/relationship.py +2 -2
  95. infrahub/graphql/mutations/repository.py +22 -83
  96. infrahub/graphql/mutations/resource_manager.py +2 -2
  97. infrahub/graphql/mutations/schema.py +5 -5
  98. infrahub/graphql/mutations/webhook.py +1 -1
  99. infrahub/graphql/queries/resource_manager.py +1 -1
  100. infrahub/graphql/registry.py +173 -0
  101. infrahub/graphql/resolvers/resolver.py +2 -0
  102. infrahub/graphql/schema.py +8 -1
  103. infrahub/groups/tasks.py +1 -1
  104. infrahub/hfid/__init__.py +0 -0
  105. infrahub/hfid/gather.py +48 -0
  106. infrahub/hfid/models.py +240 -0
  107. infrahub/hfid/tasks.py +185 -0
  108. infrahub/hfid/triggers.py +22 -0
  109. infrahub/lock.py +67 -30
  110. infrahub/locks/__init__.py +0 -0
  111. infrahub/locks/tasks.py +37 -0
  112. infrahub/middleware.py +26 -1
  113. infrahub/patch/plan_writer.py +2 -2
  114. infrahub/profiles/__init__.py +0 -0
  115. infrahub/profiles/node_applier.py +101 -0
  116. infrahub/profiles/queries/__init__.py +0 -0
  117. infrahub/profiles/queries/get_profile_data.py +99 -0
  118. infrahub/profiles/tasks.py +63 -0
  119. infrahub/proposed_change/tasks.py +10 -1
  120. infrahub/repositories/__init__.py +0 -0
  121. infrahub/repositories/create_repository.py +113 -0
  122. infrahub/server.py +16 -3
  123. infrahub/services/__init__.py +8 -5
  124. infrahub/tasks/registry.py +6 -4
  125. infrahub/trigger/catalogue.py +4 -0
  126. infrahub/trigger/models.py +2 -0
  127. infrahub/trigger/tasks.py +3 -0
  128. infrahub/webhook/models.py +1 -1
  129. infrahub/workflows/catalogue.py +110 -3
  130. infrahub/workflows/initialization.py +16 -0
  131. infrahub/workflows/models.py +17 -2
  132. infrahub_sdk/branch.py +5 -8
  133. infrahub_sdk/checks.py +1 -1
  134. infrahub_sdk/client.py +364 -84
  135. infrahub_sdk/convert_object_type.py +61 -0
  136. infrahub_sdk/ctl/check.py +2 -3
  137. infrahub_sdk/ctl/cli_commands.py +18 -12
  138. infrahub_sdk/ctl/config.py +8 -2
  139. infrahub_sdk/ctl/generator.py +6 -3
  140. infrahub_sdk/ctl/graphql.py +184 -0
  141. infrahub_sdk/ctl/repository.py +39 -1
  142. infrahub_sdk/ctl/schema.py +18 -3
  143. infrahub_sdk/ctl/utils.py +4 -0
  144. infrahub_sdk/ctl/validate.py +5 -3
  145. infrahub_sdk/diff.py +4 -5
  146. infrahub_sdk/exceptions.py +2 -0
  147. infrahub_sdk/generator.py +7 -1
  148. infrahub_sdk/graphql/__init__.py +12 -0
  149. infrahub_sdk/graphql/constants.py +1 -0
  150. infrahub_sdk/graphql/plugin.py +85 -0
  151. infrahub_sdk/graphql/query.py +77 -0
  152. infrahub_sdk/{graphql.py → graphql/renderers.py} +88 -75
  153. infrahub_sdk/graphql/utils.py +40 -0
  154. infrahub_sdk/node/attribute.py +2 -0
  155. infrahub_sdk/node/node.py +28 -20
  156. infrahub_sdk/playback.py +1 -2
  157. infrahub_sdk/protocols.py +54 -6
  158. infrahub_sdk/pytest_plugin/plugin.py +7 -4
  159. infrahub_sdk/pytest_plugin/utils.py +40 -0
  160. infrahub_sdk/repository.py +1 -2
  161. infrahub_sdk/schema/__init__.py +38 -0
  162. infrahub_sdk/schema/main.py +1 -0
  163. infrahub_sdk/schema/repository.py +8 -0
  164. infrahub_sdk/spec/object.py +120 -7
  165. infrahub_sdk/spec/range_expansion.py +118 -0
  166. infrahub_sdk/timestamp.py +18 -6
  167. infrahub_sdk/transforms.py +1 -1
  168. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/METADATA +9 -11
  169. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/RECORD +177 -134
  170. infrahub_testcontainers/container.py +1 -1
  171. infrahub_testcontainers/docker-compose-cluster.test.yml +1 -1
  172. infrahub_testcontainers/docker-compose.test.yml +1 -1
  173. infrahub_testcontainers/models.py +2 -2
  174. infrahub_testcontainers/performance_test.py +4 -4
  175. infrahub/core/convert_object_type/conversion.py +0 -134
  176. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/LICENSE.txt +0 -0
  177. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/WHEEL +0 -0
  178. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b1.dist-info}/entry_points.txt +0 -0
infrahub_sdk/protocols.py CHANGED
@@ -131,6 +131,7 @@ class CoreGenericRepository(CoreNode):
131
131
  queries: RelationshipManager
132
132
  checks: RelationshipManager
133
133
  generators: RelationshipManager
134
+ groups_objects: RelationshipManager
134
135
 
135
136
 
136
137
  class CoreGroup(CoreNode):
@@ -233,6 +234,10 @@ class CoreWebhook(CoreNode):
233
234
  validate_certificates: BooleanOptional
234
235
 
235
236
 
237
+ class CoreWeightedPoolResource(CoreNode):
238
+ allocation_weight: IntegerOptional
239
+
240
+
236
241
  class LineageOwner(CoreNode):
237
242
  pass
238
243
 
@@ -321,6 +326,7 @@ class CoreCheckDefinition(CoreTaskTarget):
321
326
 
322
327
 
323
328
  class CoreCustomWebhook(CoreWebhook, CoreTaskTarget):
329
+ shared_key: StringOptional
324
330
  transformation: RelatedNode
325
331
 
326
332
 
@@ -350,6 +356,10 @@ class CoreGeneratorAction(CoreAction):
350
356
  generator: RelatedNode
351
357
 
352
358
 
359
+ class CoreGeneratorAwareGroup(CoreGroup):
360
+ pass
361
+
362
+
353
363
  class CoreGeneratorCheck(CoreCheck):
354
364
  instance: String
355
365
 
@@ -361,6 +371,8 @@ class CoreGeneratorDefinition(CoreTaskTarget):
361
371
  file_path: String
362
372
  class_name: String
363
373
  convert_query_response: BooleanOptional
374
+ execute_in_proposed_change: BooleanOptional
375
+ execute_after_merge: BooleanOptional
364
376
  query: RelatedNode
365
377
  repository: RelatedNode
366
378
  targets: RelatedNode
@@ -405,12 +417,12 @@ class CoreGraphQLQueryGroup(CoreGroup):
405
417
 
406
418
 
407
419
  class CoreGroupAction(CoreAction):
408
- add_members: Boolean
420
+ member_action: Dropdown
409
421
  group: RelatedNode
410
422
 
411
423
 
412
424
  class CoreGroupTriggerRule(CoreTriggerRule):
413
- members_added: Boolean
425
+ member_update: Dropdown
414
426
  group: RelatedNode
415
427
 
416
428
 
@@ -442,7 +454,7 @@ class CoreNodeTriggerAttributeMatch(CoreNodeTriggerMatch):
442
454
 
443
455
  class CoreNodeTriggerRelationshipMatch(CoreNodeTriggerMatch):
444
456
  relationship_name: String
445
- added: Boolean
457
+ modification_type: Dropdown
446
458
  peer: StringOptional
447
459
 
448
460
 
@@ -457,6 +469,7 @@ class CoreNumberPool(CoreResourcePool, LineageSource):
457
469
  node_attribute: String
458
470
  start_range: Integer
459
471
  end_range: Integer
472
+ pool_type: Enum
460
473
 
461
474
 
462
475
  class CoreObjectPermission(CoreBasePermission):
@@ -481,7 +494,10 @@ class CoreProposedChange(CoreTaskTarget):
481
494
  source_branch: String
482
495
  destination_branch: String
483
496
  state: Enum
497
+ is_draft: Boolean
498
+ total_comments: IntegerOptional
484
499
  approved_by: RelationshipManager
500
+ rejected_by: RelationshipManager
485
501
  reviewers: RelationshipManager
486
502
  created_by: RelatedNode
487
503
  comments: RelationshipManager
@@ -555,6 +571,14 @@ class InternalAccountToken(CoreNode):
555
571
  account: RelatedNode
556
572
 
557
573
 
574
+ class InternalIPPrefixAvailable(BuiltinIPPrefix):
575
+ pass
576
+
577
+
578
+ class InternalIPRangeAvailable(BuiltinIPAddress):
579
+ last_address: IPHost
580
+
581
+
558
582
  class InternalRefreshToken(CoreNode):
559
583
  expiration: DateTime
560
584
  account: RelatedNode
@@ -664,6 +688,7 @@ class CoreGenericRepositorySync(CoreNodeSync):
664
688
  queries: RelationshipManagerSync
665
689
  checks: RelationshipManagerSync
666
690
  generators: RelationshipManagerSync
691
+ groups_objects: RelationshipManagerSync
667
692
 
668
693
 
669
694
  class CoreGroupSync(CoreNodeSync):
@@ -766,6 +791,10 @@ class CoreWebhookSync(CoreNodeSync):
766
791
  validate_certificates: BooleanOptional
767
792
 
768
793
 
794
+ class CoreWeightedPoolResourceSync(CoreNodeSync):
795
+ allocation_weight: IntegerOptional
796
+
797
+
769
798
  class LineageOwnerSync(CoreNodeSync):
770
799
  pass
771
800
 
@@ -854,6 +883,7 @@ class CoreCheckDefinitionSync(CoreTaskTargetSync):
854
883
 
855
884
 
856
885
  class CoreCustomWebhookSync(CoreWebhookSync, CoreTaskTargetSync):
886
+ shared_key: StringOptional
857
887
  transformation: RelatedNodeSync
858
888
 
859
889
 
@@ -883,6 +913,10 @@ class CoreGeneratorActionSync(CoreActionSync):
883
913
  generator: RelatedNodeSync
884
914
 
885
915
 
916
+ class CoreGeneratorAwareGroupSync(CoreGroupSync):
917
+ pass
918
+
919
+
886
920
  class CoreGeneratorCheckSync(CoreCheckSync):
887
921
  instance: String
888
922
 
@@ -894,6 +928,8 @@ class CoreGeneratorDefinitionSync(CoreTaskTargetSync):
894
928
  file_path: String
895
929
  class_name: String
896
930
  convert_query_response: BooleanOptional
931
+ execute_in_proposed_change: BooleanOptional
932
+ execute_after_merge: BooleanOptional
897
933
  query: RelatedNodeSync
898
934
  repository: RelatedNodeSync
899
935
  targets: RelatedNodeSync
@@ -938,12 +974,12 @@ class CoreGraphQLQueryGroupSync(CoreGroupSync):
938
974
 
939
975
 
940
976
  class CoreGroupActionSync(CoreActionSync):
941
- add_members: Boolean
977
+ member_action: Dropdown
942
978
  group: RelatedNodeSync
943
979
 
944
980
 
945
981
  class CoreGroupTriggerRuleSync(CoreTriggerRuleSync):
946
- members_added: Boolean
982
+ member_update: Dropdown
947
983
  group: RelatedNodeSync
948
984
 
949
985
 
@@ -975,7 +1011,7 @@ class CoreNodeTriggerAttributeMatchSync(CoreNodeTriggerMatchSync):
975
1011
 
976
1012
  class CoreNodeTriggerRelationshipMatchSync(CoreNodeTriggerMatchSync):
977
1013
  relationship_name: String
978
- added: Boolean
1014
+ modification_type: Dropdown
979
1015
  peer: StringOptional
980
1016
 
981
1017
 
@@ -990,6 +1026,7 @@ class CoreNumberPoolSync(CoreResourcePoolSync, LineageSourceSync):
990
1026
  node_attribute: String
991
1027
  start_range: Integer
992
1028
  end_range: Integer
1029
+ pool_type: Enum
993
1030
 
994
1031
 
995
1032
  class CoreObjectPermissionSync(CoreBasePermissionSync):
@@ -1014,7 +1051,10 @@ class CoreProposedChangeSync(CoreTaskTargetSync):
1014
1051
  source_branch: String
1015
1052
  destination_branch: String
1016
1053
  state: Enum
1054
+ is_draft: Boolean
1055
+ total_comments: IntegerOptional
1017
1056
  approved_by: RelationshipManagerSync
1057
+ rejected_by: RelationshipManagerSync
1018
1058
  reviewers: RelationshipManagerSync
1019
1059
  created_by: RelatedNodeSync
1020
1060
  comments: RelationshipManagerSync
@@ -1088,6 +1128,14 @@ class InternalAccountTokenSync(CoreNodeSync):
1088
1128
  account: RelatedNodeSync
1089
1129
 
1090
1130
 
1131
+ class InternalIPPrefixAvailableSync(BuiltinIPPrefixSync):
1132
+ pass
1133
+
1134
+
1135
+ class InternalIPRangeAvailableSync(BuiltinIPAddressSync):
1136
+ last_address: IPHost
1137
+
1138
+
1091
1139
  class InternalRefreshTokenSync(CoreNodeSync):
1092
1140
  expiration: DateTime
1093
1141
  account: RelatedNodeSync
@@ -9,7 +9,7 @@ from pytest import exit as exit_test
9
9
  from .. import InfrahubClientSync
10
10
  from ..utils import is_valid_url
11
11
  from .loader import InfrahubYamlFile
12
- from .utils import load_repository_config
12
+ from .utils import find_repository_config_file, load_repository_config
13
13
 
14
14
 
15
15
  def pytest_addoption(parser: Parser) -> None:
@@ -18,9 +18,9 @@ def pytest_addoption(parser: Parser) -> None:
18
18
  "--infrahub-repo-config",
19
19
  action="store",
20
20
  dest="infrahub_repo_config",
21
- default=".infrahub.yml",
21
+ default=None,
22
22
  metavar="INFRAHUB_REPO_CONFIG_FILE",
23
- help="Infrahub configuration file for the repository (default: %(default)s)",
23
+ help="Infrahub configuration file for the repository (.infrahub.yml or .infrahub.yaml)",
24
24
  )
25
25
  group.addoption(
26
26
  "--infrahub-address",
@@ -63,7 +63,10 @@ def pytest_addoption(parser: Parser) -> None:
63
63
 
64
64
 
65
65
  def pytest_sessionstart(session: Session) -> None:
66
- session.infrahub_config_path = Path(session.config.option.infrahub_repo_config) # type: ignore[attr-defined]
66
+ if session.config.option.infrahub_repo_config:
67
+ session.infrahub_config_path = Path(session.config.option.infrahub_repo_config) # type: ignore[attr-defined]
68
+ else:
69
+ session.infrahub_config_path = find_repository_config_file() # type: ignore[attr-defined]
67
70
 
68
71
  if session.infrahub_config_path.is_file(): # type: ignore[attr-defined]
69
72
  session.infrahub_repo_config = load_repository_config(repo_config_file=session.infrahub_config_path) # type: ignore[attr-defined]
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pathlib import Path
2
4
 
3
5
  import yaml
@@ -6,7 +8,45 @@ from ..schema.repository import InfrahubRepositoryConfig
6
8
  from .exceptions import FileNotValidError
7
9
 
8
10
 
11
+ def find_repository_config_file(base_path: Path | None = None) -> Path:
12
+ """Find the repository config file, checking for both .yml and .yaml extensions.
13
+
14
+ Args:
15
+ base_path: Base directory to search in. If None, uses current directory.
16
+
17
+ Returns:
18
+ Path to the config file.
19
+
20
+ Raises:
21
+ FileNotFoundError: If neither .infrahub.yml nor .infrahub.yaml exists.
22
+ """
23
+ if base_path is None:
24
+ base_path = Path()
25
+
26
+ yml_path = base_path / ".infrahub.yml"
27
+ yaml_path = base_path / ".infrahub.yaml"
28
+
29
+ # Prefer .yml if both exist
30
+ if yml_path.exists():
31
+ return yml_path
32
+ if yaml_path.exists():
33
+ return yaml_path
34
+ # For backward compatibility, return .yml path for error messages
35
+ return yml_path
36
+
37
+
9
38
  def load_repository_config(repo_config_file: Path) -> InfrahubRepositoryConfig:
39
+ # If the file doesn't exist, try to find it with alternate extension
40
+ if not repo_config_file.exists():
41
+ if repo_config_file.name == ".infrahub.yml":
42
+ alt_path = repo_config_file.parent / ".infrahub.yaml"
43
+ if alt_path.exists():
44
+ repo_config_file = alt_path
45
+ elif repo_config_file.name == ".infrahub.yaml":
46
+ alt_path = repo_config_file.parent / ".infrahub.yml"
47
+ if alt_path.exists():
48
+ repo_config_file = alt_path
49
+
10
50
  if not repo_config_file.is_file():
11
51
  raise FileNotFoundError(repo_config_file)
12
52
 
@@ -29,5 +29,4 @@ class GitRepoManager:
29
29
 
30
30
  @property
31
31
  def active_branch(self) -> str | None:
32
- active_branch = porcelain.active_branch(self.root_directory).decode("utf-8")
33
- return active_branch
32
+ return porcelain.active_branch(self.root_directory).decode("utf-8")
@@ -474,6 +474,25 @@ class InfrahubSchema(InfrahubSchemaBase):
474
474
 
475
475
  return branch_schema.nodes
476
476
 
477
+ async def get_graphql_schema(self, branch: str | None = None) -> str:
478
+ """Get the GraphQL schema as a string.
479
+
480
+ Args:
481
+ branch: The branch to get the schema for. Defaults to default_branch.
482
+
483
+ Returns:
484
+ The GraphQL schema as a string.
485
+ """
486
+ branch = branch or self.client.default_branch
487
+ url = f"{self.client.address}/schema.graphql?branch={branch}"
488
+
489
+ response = await self.client._get(url=url)
490
+
491
+ if response.status_code != 200:
492
+ raise ValueError(f"Failed to fetch GraphQL schema: HTTP {response.status_code} - {response.text}")
493
+
494
+ return response.text
495
+
477
496
  async def _fetch(self, branch: str, namespaces: list[str] | None = None) -> BranchSchema:
478
497
  url_parts = [("branch", branch)]
479
498
  if namespaces:
@@ -697,6 +716,25 @@ class InfrahubSchemaSync(InfrahubSchemaBase):
697
716
 
698
717
  return branch_schema.nodes
699
718
 
719
+ def get_graphql_schema(self, branch: str | None = None) -> str:
720
+ """Get the GraphQL schema as a string.
721
+
722
+ Args:
723
+ branch: The branch to get the schema for. Defaults to default_branch.
724
+
725
+ Returns:
726
+ The GraphQL schema as a string.
727
+ """
728
+ branch = branch or self.client.default_branch
729
+ url = f"{self.client.address}/schema.graphql?branch={branch}"
730
+
731
+ response = self.client._get(url=url)
732
+
733
+ if response.status_code != 200:
734
+ raise ValueError(f"Failed to fetch GraphQL schema: HTTP {response.status_code} - {response.text}")
735
+
736
+ return response.text
737
+
700
738
  def _fetch(self, branch: str, namespaces: list[str] | None = None) -> BranchSchema:
701
739
  url_parts = [("branch", branch)]
702
740
  if namespaces:
@@ -267,6 +267,7 @@ class BaseSchema(BaseModel):
267
267
  description: str | None = None
268
268
  include_in_menu: bool | None = None
269
269
  menu_placement: str | None = None
270
+ display_label: str | None = None
270
271
  display_labels: list[str] | None = None
271
272
  human_friendly_id: list[str] | None = None
272
273
  icon: str | None = None
@@ -96,6 +96,14 @@ class InfrahubGeneratorDefinitionConfig(InfrahubRepositoryConfigElement):
96
96
  default=False,
97
97
  description="Decide if the generator should convert the result of the GraphQL query to SDK InfrahubNode objects.",
98
98
  )
99
+ execute_in_proposed_change: bool = Field(
100
+ default=True,
101
+ description="Decide if the generator should execute in a proposed change.",
102
+ )
103
+ execute_after_merge: bool = Field(
104
+ default=True,
105
+ description="Decide if the generator should execute after a merge.",
106
+ )
99
107
 
100
108
  def load_class(self, import_root: str | None = None, relative_path: str | None = None) -> type[InfrahubGenerator]:
101
109
  module = import_module(module_path=self.file_path, import_root=import_root, relative_path=relative_path)
@@ -1,13 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import copy
4
+ import re
5
+ from abc import ABC, abstractmethod
3
6
  from enum import Enum
4
- from typing import TYPE_CHECKING, Any
7
+ from typing import TYPE_CHECKING, Any, ClassVar
5
8
 
6
9
  from pydantic import BaseModel, Field
7
10
 
8
11
  from ..exceptions import ObjectValidationError, ValidationError
9
12
  from ..schema import GenericSchemaAPI, RelationshipKind, RelationshipSchema
10
13
  from ..yaml import InfrahubFile, InfrahubFileKind
14
+ from .range_expansion import MATCH_PATTERN, range_expansion
11
15
 
12
16
  if TYPE_CHECKING:
13
17
  from ..client import InfrahubClient
@@ -42,6 +46,11 @@ class RelationshipDataFormat(str, Enum):
42
46
  MANY_REF = "many_ref_list"
43
47
 
44
48
 
49
+ class ObjectStrategy(str, Enum):
50
+ NORMAL = "normal"
51
+ RANGE_EXPAND = "range_expand"
52
+
53
+
45
54
  class RelationshipInfo(BaseModel):
46
55
  name: str
47
56
  rel_schema: RelationshipSchema
@@ -164,14 +173,100 @@ async def get_relationship_info(
164
173
  return info
165
174
 
166
175
 
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
+
167
252
  class InfrahubObjectFileData(BaseModel):
168
253
  kind: str
254
+ strategy: ObjectStrategy = ObjectStrategy.NORMAL
169
255
  data: list[dict[str, Any]] = Field(default_factory=list)
170
256
 
257
+ def _get_processed_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]:
258
+ """Get data processed according to the strategy"""
259
+ processor = DataProcessorFactory.get_processor(self.strategy)
260
+ return processor.process_data(data)
261
+
171
262
  async def validate_format(self, client: InfrahubClient, branch: str | None = None) -> list[ObjectValidationError]:
172
263
  errors: list[ObjectValidationError] = []
173
264
  schema = await client.schema.get(kind=self.kind, branch=branch)
174
- for idx, item in enumerate(self.data):
265
+
266
+ processed_data = self._get_processed_data(data=self.data)
267
+ self.data = processed_data
268
+
269
+ for idx, item in enumerate(processed_data):
175
270
  errors.extend(
176
271
  await self.validate_object(
177
272
  client=client,
@@ -180,13 +275,16 @@ class InfrahubObjectFileData(BaseModel):
180
275
  data=item,
181
276
  branch=branch,
182
277
  default_schema_kind=self.kind,
278
+ strategy=self.strategy, # Pass strategy down
183
279
  )
184
280
  )
185
281
  return errors
186
282
 
187
283
  async def process(self, client: InfrahubClient, branch: str | None = None) -> None:
188
284
  schema = await client.schema.get(kind=self.kind, branch=branch)
189
- for idx, item in enumerate(self.data):
285
+ processed_data = self._get_processed_data(data=self.data)
286
+
287
+ for idx, item in enumerate(processed_data):
190
288
  await self.create_node(
191
289
  client=client,
192
290
  schema=schema,
@@ -206,6 +304,7 @@ class InfrahubObjectFileData(BaseModel):
206
304
  context: dict | None = None,
207
305
  branch: str | None = None,
208
306
  default_schema_kind: str | None = None,
307
+ strategy: ObjectStrategy = ObjectStrategy.NORMAL,
209
308
  ) -> list[ObjectValidationError]:
210
309
  errors: list[ObjectValidationError] = []
211
310
  context = context.copy() if context else {}
@@ -255,6 +354,7 @@ class InfrahubObjectFileData(BaseModel):
255
354
  context=context,
256
355
  branch=branch,
257
356
  default_schema_kind=default_schema_kind,
357
+ strategy=strategy,
258
358
  )
259
359
  )
260
360
 
@@ -270,6 +370,7 @@ class InfrahubObjectFileData(BaseModel):
270
370
  context: dict | None = None,
271
371
  branch: str | None = None,
272
372
  default_schema_kind: str | None = None,
373
+ strategy: ObjectStrategy = ObjectStrategy.NORMAL,
273
374
  ) -> list[ObjectValidationError]:
274
375
  context = context.copy() if context else {}
275
376
  errors: list[ObjectValidationError] = []
@@ -311,7 +412,11 @@ class InfrahubObjectFileData(BaseModel):
311
412
  rel_info.find_matching_relationship(peer_schema=peer_schema)
312
413
  context.update(rel_info.get_context(value="placeholder"))
313
414
 
314
- for idx, peer_data in enumerate(data["data"]):
415
+ # Use strategy-aware data processing
416
+ processor = DataProcessorFactory.get_processor(strategy)
417
+ expanded_data = processor.process_data(data["data"])
418
+
419
+ for idx, peer_data in enumerate(expanded_data):
315
420
  context["list_index"] = idx
316
421
  errors.extend(
317
422
  await cls.validate_object(
@@ -322,6 +427,7 @@ class InfrahubObjectFileData(BaseModel):
322
427
  context=context,
323
428
  branch=branch,
324
429
  default_schema_kind=default_schema_kind,
430
+ strategy=strategy,
325
431
  )
326
432
  )
327
433
  return errors
@@ -525,7 +631,8 @@ class InfrahubObjectFileData(BaseModel):
525
631
  rel_info.find_matching_relationship(peer_schema=peer_schema)
526
632
  context.update(rel_info.get_context(value=parent_node.id))
527
633
 
528
- for idx, peer_data in enumerate(data["data"]):
634
+ expanded_data = expand_data_with_ranges(data=data["data"])
635
+ for idx, peer_data in enumerate(expanded_data):
529
636
  context["list_index"] = idx
530
637
  if isinstance(peer_data, dict):
531
638
  node = await cls.create_node(
@@ -594,14 +701,20 @@ class ObjectFile(InfrahubFile):
594
701
  @property
595
702
  def spec(self) -> InfrahubObjectFileData:
596
703
  if not self._spec:
597
- self._spec = InfrahubObjectFileData(**self.data.spec)
704
+ try:
705
+ self._spec = InfrahubObjectFileData(**self.data.spec)
706
+ except Exception as exc:
707
+ raise ValidationError(identifier=str(self.location), message=str(exc))
598
708
  return self._spec
599
709
 
600
710
  def validate_content(self) -> None:
601
711
  super().validate_content()
602
712
  if self.kind != InfrahubFileKind.OBJECT:
603
713
  raise ValueError("File is not an Infrahub Object file")
604
- self._spec = InfrahubObjectFileData(**self.data.spec)
714
+ try:
715
+ self._spec = InfrahubObjectFileData(**self.data.spec)
716
+ except Exception as exc:
717
+ raise ValidationError(identifier=str(self.location), message=str(exc))
605
718
 
606
719
  async def validate_format(self, client: InfrahubClient, branch: str | None = None) -> None:
607
720
  self.validate_content()