infrahub-server 1.4.9__py3-none-any.whl → 1.5.0b0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. infrahub/actions/tasks.py +200 -16
  2. infrahub/api/artifact.py +3 -0
  3. infrahub/api/query.py +2 -0
  4. infrahub/api/schema.py +3 -0
  5. infrahub/auth.py +5 -5
  6. infrahub/cli/db.py +2 -2
  7. infrahub/config.py +7 -2
  8. infrahub/core/attribute.py +22 -19
  9. infrahub/core/branch/models.py +2 -2
  10. infrahub/core/branch/needs_rebase_status.py +11 -0
  11. infrahub/core/branch/tasks.py +2 -2
  12. infrahub/core/constants/__init__.py +1 -0
  13. infrahub/core/convert_object_type/object_conversion.py +201 -0
  14. infrahub/core/convert_object_type/repository_conversion.py +89 -0
  15. infrahub/core/convert_object_type/schema_mapping.py +27 -3
  16. infrahub/core/diff/query/artifact.py +12 -9
  17. infrahub/core/graph/__init__.py +1 -1
  18. infrahub/core/initialization.py +2 -2
  19. infrahub/core/manager.py +3 -81
  20. infrahub/core/migrations/graph/__init__.py +2 -0
  21. infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +166 -0
  22. infrahub/core/node/__init__.py +26 -3
  23. infrahub/core/node/create.py +79 -38
  24. infrahub/core/node/lock_utils.py +98 -0
  25. infrahub/core/property.py +11 -0
  26. infrahub/core/protocols.py +1 -0
  27. infrahub/core/query/attribute.py +27 -15
  28. infrahub/core/query/node.py +47 -184
  29. infrahub/core/query/relationship.py +43 -26
  30. infrahub/core/query/subquery.py +0 -8
  31. infrahub/core/relationship/model.py +59 -19
  32. infrahub/core/schema/attribute_schema.py +0 -2
  33. infrahub/core/schema/definitions/core/repository.py +7 -0
  34. infrahub/core/schema/relationship_schema.py +0 -1
  35. infrahub/core/schema/schema_branch.py +3 -2
  36. infrahub/generators/models.py +31 -12
  37. infrahub/generators/tasks.py +3 -1
  38. infrahub/git/base.py +38 -1
  39. infrahub/graphql/api/dependencies.py +2 -4
  40. infrahub/graphql/api/endpoints.py +2 -2
  41. infrahub/graphql/app.py +2 -4
  42. infrahub/graphql/initialization.py +2 -3
  43. infrahub/graphql/manager.py +212 -137
  44. infrahub/graphql/middleware.py +12 -0
  45. infrahub/graphql/mutations/branch.py +11 -0
  46. infrahub/graphql/mutations/computed_attribute.py +110 -3
  47. infrahub/graphql/mutations/convert_object_type.py +34 -13
  48. infrahub/graphql/mutations/ipam.py +21 -8
  49. infrahub/graphql/mutations/main.py +37 -153
  50. infrahub/graphql/mutations/profile.py +195 -0
  51. infrahub/graphql/mutations/proposed_change.py +2 -1
  52. infrahub/graphql/mutations/repository.py +22 -83
  53. infrahub/graphql/mutations/webhook.py +1 -1
  54. infrahub/graphql/registry.py +173 -0
  55. infrahub/graphql/schema.py +4 -1
  56. infrahub/lock.py +52 -26
  57. infrahub/locks/__init__.py +0 -0
  58. infrahub/locks/tasks.py +37 -0
  59. infrahub/patch/plan_writer.py +2 -2
  60. infrahub/profiles/__init__.py +0 -0
  61. infrahub/profiles/node_applier.py +101 -0
  62. infrahub/profiles/queries/__init__.py +0 -0
  63. infrahub/profiles/queries/get_profile_data.py +99 -0
  64. infrahub/profiles/tasks.py +63 -0
  65. infrahub/repositories/__init__.py +0 -0
  66. infrahub/repositories/create_repository.py +113 -0
  67. infrahub/tasks/registry.py +6 -4
  68. infrahub/webhook/models.py +1 -1
  69. infrahub/workflows/catalogue.py +38 -3
  70. infrahub/workflows/models.py +17 -2
  71. infrahub_sdk/branch.py +5 -8
  72. infrahub_sdk/client.py +364 -84
  73. infrahub_sdk/convert_object_type.py +61 -0
  74. infrahub_sdk/ctl/check.py +2 -3
  75. infrahub_sdk/ctl/cli_commands.py +16 -12
  76. infrahub_sdk/ctl/config.py +8 -2
  77. infrahub_sdk/ctl/generator.py +2 -3
  78. infrahub_sdk/ctl/repository.py +39 -1
  79. infrahub_sdk/ctl/schema.py +12 -1
  80. infrahub_sdk/ctl/utils.py +4 -0
  81. infrahub_sdk/ctl/validate.py +5 -3
  82. infrahub_sdk/diff.py +4 -5
  83. infrahub_sdk/exceptions.py +2 -0
  84. infrahub_sdk/graphql.py +7 -2
  85. infrahub_sdk/node/attribute.py +2 -0
  86. infrahub_sdk/node/node.py +28 -20
  87. infrahub_sdk/playback.py +1 -2
  88. infrahub_sdk/protocols.py +40 -6
  89. infrahub_sdk/pytest_plugin/plugin.py +7 -4
  90. infrahub_sdk/pytest_plugin/utils.py +40 -0
  91. infrahub_sdk/repository.py +1 -2
  92. infrahub_sdk/schema/main.py +1 -0
  93. infrahub_sdk/spec/object.py +43 -4
  94. infrahub_sdk/spec/range_expansion.py +118 -0
  95. infrahub_sdk/timestamp.py +18 -6
  96. {infrahub_server-1.4.9.dist-info → infrahub_server-1.5.0b0.dist-info}/METADATA +20 -24
  97. {infrahub_server-1.4.9.dist-info → infrahub_server-1.5.0b0.dist-info}/RECORD +102 -84
  98. infrahub_testcontainers/models.py +2 -2
  99. infrahub_testcontainers/performance_test.py +4 -4
  100. infrahub/core/convert_object_type/conversion.py +0 -134
  101. {infrahub_server-1.4.9.dist-info → infrahub_server-1.5.0b0.dist-info}/LICENSE.txt +0 -0
  102. {infrahub_server-1.4.9.dist-info → infrahub_server-1.5.0b0.dist-info}/WHEEL +0 -0
  103. {infrahub_server-1.4.9.dist-info → infrahub_server-1.5.0b0.dist-info}/entry_points.txt +0 -0
infrahub_sdk/protocols.py CHANGED
@@ -233,6 +233,10 @@ class CoreWebhook(CoreNode):
233
233
  validate_certificates: BooleanOptional
234
234
 
235
235
 
236
+ class CoreWeightedPoolResource(CoreNode):
237
+ allocation_weight: IntegerOptional
238
+
239
+
236
240
  class LineageOwner(CoreNode):
237
241
  pass
238
242
 
@@ -321,6 +325,7 @@ class CoreCheckDefinition(CoreTaskTarget):
321
325
 
322
326
 
323
327
  class CoreCustomWebhook(CoreWebhook, CoreTaskTarget):
328
+ shared_key: StringOptional
324
329
  transformation: RelatedNode
325
330
 
326
331
 
@@ -405,12 +410,12 @@ class CoreGraphQLQueryGroup(CoreGroup):
405
410
 
406
411
 
407
412
  class CoreGroupAction(CoreAction):
408
- add_members: Boolean
413
+ member_action: Dropdown
409
414
  group: RelatedNode
410
415
 
411
416
 
412
417
  class CoreGroupTriggerRule(CoreTriggerRule):
413
- members_added: Boolean
418
+ member_update: Dropdown
414
419
  group: RelatedNode
415
420
 
416
421
 
@@ -442,7 +447,7 @@ class CoreNodeTriggerAttributeMatch(CoreNodeTriggerMatch):
442
447
 
443
448
  class CoreNodeTriggerRelationshipMatch(CoreNodeTriggerMatch):
444
449
  relationship_name: String
445
- added: Boolean
450
+ modification_type: Dropdown
446
451
  peer: StringOptional
447
452
 
448
453
 
@@ -457,6 +462,7 @@ class CoreNumberPool(CoreResourcePool, LineageSource):
457
462
  node_attribute: String
458
463
  start_range: Integer
459
464
  end_range: Integer
465
+ pool_type: Enum
460
466
 
461
467
 
462
468
  class CoreObjectPermission(CoreBasePermission):
@@ -481,7 +487,10 @@ class CoreProposedChange(CoreTaskTarget):
481
487
  source_branch: String
482
488
  destination_branch: String
483
489
  state: Enum
490
+ is_draft: Boolean
491
+ total_comments: IntegerOptional
484
492
  approved_by: RelationshipManager
493
+ rejected_by: RelationshipManager
485
494
  reviewers: RelationshipManager
486
495
  created_by: RelatedNode
487
496
  comments: RelationshipManager
@@ -555,6 +564,14 @@ class InternalAccountToken(CoreNode):
555
564
  account: RelatedNode
556
565
 
557
566
 
567
+ class InternalIPPrefixAvailable(BuiltinIPPrefix):
568
+ pass
569
+
570
+
571
+ class InternalIPRangeAvailable(BuiltinIPAddress):
572
+ last_address: IPHost
573
+
574
+
558
575
  class InternalRefreshToken(CoreNode):
559
576
  expiration: DateTime
560
577
  account: RelatedNode
@@ -766,6 +783,10 @@ class CoreWebhookSync(CoreNodeSync):
766
783
  validate_certificates: BooleanOptional
767
784
 
768
785
 
786
+ class CoreWeightedPoolResourceSync(CoreNodeSync):
787
+ allocation_weight: IntegerOptional
788
+
789
+
769
790
  class LineageOwnerSync(CoreNodeSync):
770
791
  pass
771
792
 
@@ -854,6 +875,7 @@ class CoreCheckDefinitionSync(CoreTaskTargetSync):
854
875
 
855
876
 
856
877
  class CoreCustomWebhookSync(CoreWebhookSync, CoreTaskTargetSync):
878
+ shared_key: StringOptional
857
879
  transformation: RelatedNodeSync
858
880
 
859
881
 
@@ -938,12 +960,12 @@ class CoreGraphQLQueryGroupSync(CoreGroupSync):
938
960
 
939
961
 
940
962
  class CoreGroupActionSync(CoreActionSync):
941
- add_members: Boolean
963
+ member_action: Dropdown
942
964
  group: RelatedNodeSync
943
965
 
944
966
 
945
967
  class CoreGroupTriggerRuleSync(CoreTriggerRuleSync):
946
- members_added: Boolean
968
+ member_update: Dropdown
947
969
  group: RelatedNodeSync
948
970
 
949
971
 
@@ -975,7 +997,7 @@ class CoreNodeTriggerAttributeMatchSync(CoreNodeTriggerMatchSync):
975
997
 
976
998
  class CoreNodeTriggerRelationshipMatchSync(CoreNodeTriggerMatchSync):
977
999
  relationship_name: String
978
- added: Boolean
1000
+ modification_type: Dropdown
979
1001
  peer: StringOptional
980
1002
 
981
1003
 
@@ -990,6 +1012,7 @@ class CoreNumberPoolSync(CoreResourcePoolSync, LineageSourceSync):
990
1012
  node_attribute: String
991
1013
  start_range: Integer
992
1014
  end_range: Integer
1015
+ pool_type: Enum
993
1016
 
994
1017
 
995
1018
  class CoreObjectPermissionSync(CoreBasePermissionSync):
@@ -1014,7 +1037,10 @@ class CoreProposedChangeSync(CoreTaskTargetSync):
1014
1037
  source_branch: String
1015
1038
  destination_branch: String
1016
1039
  state: Enum
1040
+ is_draft: Boolean
1041
+ total_comments: IntegerOptional
1017
1042
  approved_by: RelationshipManagerSync
1043
+ rejected_by: RelationshipManagerSync
1018
1044
  reviewers: RelationshipManagerSync
1019
1045
  created_by: RelatedNodeSync
1020
1046
  comments: RelationshipManagerSync
@@ -1088,6 +1114,14 @@ class InternalAccountTokenSync(CoreNodeSync):
1088
1114
  account: RelatedNodeSync
1089
1115
 
1090
1116
 
1117
+ class InternalIPPrefixAvailableSync(BuiltinIPPrefixSync):
1118
+ pass
1119
+
1120
+
1121
+ class InternalIPRangeAvailableSync(BuiltinIPAddressSync):
1122
+ last_address: IPHost
1123
+
1124
+
1091
1125
  class InternalRefreshTokenSync(CoreNodeSync):
1092
1126
  expiration: DateTime
1093
1127
  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")
@@ -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
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import copy
4
+ import re
3
5
  from enum import Enum
4
6
  from typing import TYPE_CHECKING, Any
5
7
 
@@ -8,6 +10,7 @@ from pydantic import BaseModel, Field
8
10
  from ..exceptions import ObjectValidationError, ValidationError
9
11
  from ..schema import GenericSchemaAPI, RelationshipKind, RelationshipSchema
10
12
  from ..yaml import InfrahubFile, InfrahubFileKind
13
+ from .range_expansion import MATCH_PATTERN, range_expansion
11
14
 
12
15
  if TYPE_CHECKING:
13
16
  from ..client import InfrahubClient
@@ -164,6 +167,37 @@ async def get_relationship_info(
164
167
  return info
165
168
 
166
169
 
170
+ def expand_data_with_ranges(data: list[dict[str, Any]]) -> list[dict[str, Any]]:
171
+ """Expand any item in self.data with range pattern in any value. Supports multiple fields, requires equal expansion length."""
172
+ range_pattern = re.compile(MATCH_PATTERN)
173
+ expanded = []
174
+ for item in data:
175
+ # Find all fields to expand
176
+ expand_fields = {}
177
+ for key, value in item.items():
178
+ if isinstance(value, str) and range_pattern.search(value):
179
+ try:
180
+ expand_fields[key] = range_expansion(value)
181
+ except Exception:
182
+ # If expansion fails, treat as no expansion
183
+ expand_fields[key] = [value]
184
+ if not expand_fields:
185
+ expanded.append(item)
186
+ continue
187
+ # Check all expanded lists have the same length
188
+ lengths = [len(v) for v in expand_fields.values()]
189
+ if len(set(lengths)) > 1:
190
+ raise ValidationError(f"Range expansion mismatch: fields expanded to different lengths: {lengths}")
191
+ n = lengths[0]
192
+ # Zip expanded values and produce new items
193
+ for i in range(n):
194
+ new_item = copy.deepcopy(item)
195
+ for key, values in expand_fields.items():
196
+ new_item[key] = values[i]
197
+ expanded.append(new_item)
198
+ return expanded
199
+
200
+
167
201
  class InfrahubObjectFileData(BaseModel):
168
202
  kind: str
169
203
  data: list[dict[str, Any]] = Field(default_factory=list)
@@ -171,7 +205,9 @@ class InfrahubObjectFileData(BaseModel):
171
205
  async def validate_format(self, client: InfrahubClient, branch: str | None = None) -> list[ObjectValidationError]:
172
206
  errors: list[ObjectValidationError] = []
173
207
  schema = await client.schema.get(kind=self.kind, branch=branch)
174
- for idx, item in enumerate(self.data):
208
+ expanded_data = expand_data_with_ranges(self.data)
209
+ self.data = expanded_data
210
+ for idx, item in enumerate(expanded_data):
175
211
  errors.extend(
176
212
  await self.validate_object(
177
213
  client=client,
@@ -186,7 +222,8 @@ class InfrahubObjectFileData(BaseModel):
186
222
 
187
223
  async def process(self, client: InfrahubClient, branch: str | None = None) -> None:
188
224
  schema = await client.schema.get(kind=self.kind, branch=branch)
189
- for idx, item in enumerate(self.data):
225
+ expanded_data = expand_data_with_ranges(self.data)
226
+ for idx, item in enumerate(expanded_data):
190
227
  await self.create_node(
191
228
  client=client,
192
229
  schema=schema,
@@ -311,7 +348,8 @@ class InfrahubObjectFileData(BaseModel):
311
348
  rel_info.find_matching_relationship(peer_schema=peer_schema)
312
349
  context.update(rel_info.get_context(value="placeholder"))
313
350
 
314
- for idx, peer_data in enumerate(data["data"]):
351
+ expanded_data = expand_data_with_ranges(data=data["data"])
352
+ for idx, peer_data in enumerate(expanded_data):
315
353
  context["list_index"] = idx
316
354
  errors.extend(
317
355
  await cls.validate_object(
@@ -525,7 +563,8 @@ class InfrahubObjectFileData(BaseModel):
525
563
  rel_info.find_matching_relationship(peer_schema=peer_schema)
526
564
  context.update(rel_info.get_context(value=parent_node.id))
527
565
 
528
- for idx, peer_data in enumerate(data["data"]):
566
+ expanded_data = expand_data_with_ranges(data=data["data"])
567
+ for idx, peer_data in enumerate(expanded_data):
529
568
  context["list_index"] = idx
530
569
  if isinstance(peer_data, dict):
531
570
  node = await cls.create_node(
@@ -0,0 +1,118 @@
1
+ import itertools
2
+ import re
3
+
4
+ MATCH_PATTERN = r"(\[[\w,-]+\])"
5
+
6
+
7
+ def _escape_brackets(s: str) -> str:
8
+ return s.replace("\\[", "__LBRACK__").replace("\\]", "__RBRACK__")
9
+
10
+
11
+ def _unescape_brackets(s: str) -> str:
12
+ return s.replace("__LBRACK__", "[").replace("__RBRACK__", "]")
13
+
14
+
15
+ def _char_range_expand(char_range_str: str) -> list[str]:
16
+ """Expands a string of numbers or single-character letters."""
17
+ expanded_values: list[str] = []
18
+ # Special case: if no dash and no comma, and multiple characters, error if not all alphanumeric
19
+ if "," not in char_range_str and "-" not in char_range_str and len(char_range_str) > 1:
20
+ if not char_range_str.isalnum():
21
+ raise ValueError(f"Invalid non-alphanumeric range: [{char_range_str}]")
22
+ return list(char_range_str)
23
+
24
+ for value in char_range_str.split(","):
25
+ if not value:
26
+ # Malformed: empty part in comma-separated list
27
+ return [f"[{char_range_str}]"]
28
+ if "-" in value:
29
+ start_char, end_char = value.split("-", 1)
30
+ if not start_char or not end_char:
31
+ expanded_values.append(f"[{char_range_str}]")
32
+ return expanded_values
33
+ # Check if it's a numeric range
34
+ if start_char.isdigit() and end_char.isdigit():
35
+ start_num = int(start_char)
36
+ end_num = int(end_char)
37
+ step = 1 if start_num <= end_num else -1
38
+ expanded_values.extend(str(i) for i in range(start_num, end_num + step, step))
39
+ # Check if it's an alphabetical range (single character)
40
+ elif len(start_char) == 1 and len(end_char) == 1 and start_char.isalpha() and end_char.isalpha():
41
+ start_ord = ord(start_char)
42
+ end_ord = ord(end_char)
43
+ step = 1 if start_ord <= end_ord else -1
44
+ is_upper = start_char.isupper()
45
+ for i in range(start_ord, end_ord + step, step):
46
+ char = chr(i)
47
+ expanded_values.append(char.upper() if is_upper else char)
48
+ else:
49
+ # Mixed or unsupported range type, append as-is
50
+ expanded_values.append(value)
51
+ else:
52
+ # If the value is a single character or valid alphanumeric string, append
53
+ if not value.isalnum():
54
+ raise ValueError(f"Invalid non-alphanumeric value: [{value}]")
55
+ expanded_values.append(value)
56
+ return expanded_values
57
+
58
+
59
+ def _extract_constants(pattern: str, re_compiled: re.Pattern) -> tuple[list[int], list[list[str]]]:
60
+ cartesian_list = []
61
+ interface_constant = [0]
62
+ for match in re_compiled.finditer(pattern):
63
+ interface_constant.append(match.start())
64
+ interface_constant.append(match.end())
65
+ cartesian_list.append(_char_range_expand(match.group()[1:-1]))
66
+ return interface_constant, cartesian_list
67
+
68
+
69
+ def _expand_interfaces(pattern: str, interface_constant: list[int], cartesian_list: list[list[str]]) -> list[str]:
70
+ def _pairwise(lst: list[int]) -> list[tuple[int, int]]:
71
+ it = iter(lst)
72
+ return list(zip(it, it))
73
+
74
+ if interface_constant[-1] < len(pattern):
75
+ interface_constant.append(len(pattern))
76
+ interface_constant_out = _pairwise(interface_constant)
77
+ expanded_interfaces = []
78
+ for element in itertools.product(*cartesian_list):
79
+ current_interface = ""
80
+ for count, item in enumerate(interface_constant_out):
81
+ current_interface += pattern[item[0] : item[1]]
82
+ if count < len(element):
83
+ current_interface += element[count]
84
+ expanded_interfaces.append(_unescape_brackets(current_interface))
85
+ return expanded_interfaces
86
+
87
+
88
+ def range_expansion(interface_pattern: str) -> list[str]:
89
+ """Expand string pattern into a list of strings, supporting both
90
+ number and single-character alphabet ranges. Heavily inspired by
91
+ Netutils interface_range_expansion but adapted to support letters.
92
+
93
+ Args:
94
+ interface_pattern: The string pattern that will be parsed to create the list of interfaces.
95
+
96
+ Returns:
97
+ Contains the expanded list of interfaces.
98
+
99
+ Examples:
100
+ >>> from infrahub_sdk.spec.range_expansion import range_expansion
101
+ >>> range_expansion("Device [A-C]")
102
+ ['Device A', 'Device B', 'Device C']
103
+ >>> range_expansion("FastEthernet[1-2]/0/[10-15]")
104
+ ['FastEthernet1/0/10', 'FastEthernet1/0/11', 'FastEthernet1/0/12',
105
+ 'FastEthernet1/0/13', 'FastEthernet1/0/14', 'FastEthernet1/0/15',
106
+ 'FastEthernet2/0/10', 'FastEthernet2/0/11', 'FastEthernet2/0/12',
107
+ 'FastEthernet2/0/13', 'FastEthernet2/0/14', 'FastEthernet2/0/15']
108
+ >>> range_expansion("GigabitEthernet[a-c]/0/1")
109
+ ['GigabitEtherneta/0/1', 'GigabitEthernetb/0/1', 'GigabitEthernetc/0/1']
110
+ >>> range_expansion("Eth[a,c,e]/0/1")
111
+ ['Etha/0/1', 'Ethc/0/1', 'Ethe/0/1']
112
+ """
113
+ pattern_escaped = _escape_brackets(interface_pattern)
114
+ re_compiled = re.compile(MATCH_PATTERN)
115
+ if not re_compiled.search(pattern_escaped):
116
+ return [_unescape_brackets(pattern_escaped)]
117
+ interface_constant, cartesian_list = _extract_constants(pattern_escaped, re_compiled)
118
+ return _expand_interfaces(pattern_escaped, interface_constant, cartesian_list)
infrahub_sdk/timestamp.py CHANGED
@@ -3,14 +3,22 @@ from __future__ import annotations
3
3
  import re
4
4
  import warnings
5
5
  from datetime import datetime, timezone
6
- from typing import Literal
6
+ from typing import Literal, TypedDict
7
7
 
8
+ from typing_extensions import NotRequired
8
9
  from whenever import Date, Instant, LocalDateTime, OffsetDateTime, Time, ZonedDateTime
9
10
 
10
11
  from .exceptions import TimestampFormatError
11
12
 
12
13
  UTC = timezone.utc # Required for older versions of Python
13
14
 
15
+
16
+ class SubstractParams(TypedDict):
17
+ seconds: NotRequired[float]
18
+ minutes: NotRequired[float]
19
+ hours: NotRequired[float]
20
+
21
+
14
22
  REGEX_MAPPING = {
15
23
  "seconds": r"(\d+)(s|sec|second|seconds)",
16
24
  "minutes": r"(\d+)(m|min|minute|minutes)",
@@ -43,8 +51,7 @@ class Timestamp:
43
51
  @classmethod
44
52
  def _parse_string(cls, value: str) -> ZonedDateTime:
45
53
  try:
46
- zoned_date = ZonedDateTime.parse_common_iso(value)
47
- return zoned_date
54
+ return ZonedDateTime.parse_common_iso(value)
48
55
  except ValueError:
49
56
  pass
50
57
 
@@ -73,14 +80,19 @@ class Timestamp:
73
80
  except ValueError:
74
81
  pass
75
82
 
76
- params: dict[str, float] = {}
83
+ params: SubstractParams = {}
77
84
  for key, regex in REGEX_MAPPING.items():
78
85
  match = re.search(regex, value)
79
86
  if match:
80
- params[key] = float(match.group(1))
87
+ if key == "seconds":
88
+ params["seconds"] = float(match.group(1))
89
+ elif key == "minutes":
90
+ params["minutes"] = float(match.group(1))
91
+ elif key == "hours":
92
+ params["hours"] = float(match.group(1))
81
93
 
82
94
  if params:
83
- return ZonedDateTime.now("UTC").subtract(**params) # type: ignore[call-overload]
95
+ return ZonedDateTime.now("UTC").subtract(**params)
84
96
 
85
97
  raise TimestampFormatError(f"Invalid time format for {value}")
86
98
 
@@ -1,16 +1,14 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: infrahub-server
3
- Version: 1.4.9
3
+ Version: 1.5.0b0
4
4
  Summary: Infrahub is taking a new approach to Infrastructure Management by providing a new generation of datastore to organize and control all the data that defines how an infrastructure should run.
5
5
  License: Apache-2.0
6
6
  Author: OpsMill
7
7
  Author-email: info@opsmill.com
8
- Requires-Python: >=3.10,<3.13
8
+ Requires-Python: >=3.12,<3.13
9
9
  Classifier: Intended Audience :: Developers
10
10
  Classifier: License :: OSI Approved :: Apache Software License
11
11
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.10
13
- Classifier: Programming Language :: Python :: 3.11
14
12
  Classifier: Programming Language :: Python :: 3.12
15
13
  Requires-Dist: Jinja2 (>=3,<4)
16
14
  Requires-Dist: aio-pika (>=9.4,<9.5)
@@ -34,15 +32,14 @@ Requires-Dist: neo4j (>=5.28,<5.29)
34
32
  Requires-Dist: neo4j-rust-ext (>=5.28,<5.29)
35
33
  Requires-Dist: netaddr (==1.3.0)
36
34
  Requires-Dist: netutils (==1.12.0)
37
- Requires-Dist: numpy (>=1.24.2,<2.0.0) ; python_version >= "3.9" and python_version < "3.12"
38
- Requires-Dist: numpy (>=1.26.2,<2.0.0) ; python_version >= "3.12"
35
+ Requires-Dist: numpy (>=1.26.2,<2.0.0)
39
36
  Requires-Dist: opentelemetry-exporter-otlp-proto-grpc (==1.28.1)
40
37
  Requires-Dist: opentelemetry-exporter-otlp-proto-http (==1.28.1)
41
38
  Requires-Dist: opentelemetry-instrumentation-aio-pika (==0.49b1)
42
39
  Requires-Dist: opentelemetry-instrumentation-fastapi (==0.49b1)
43
- Requires-Dist: prefect (==3.4.13)
40
+ Requires-Dist: prefect (==3.4.22)
44
41
  Requires-Dist: prefect-redis (==0.2.4)
45
- Requires-Dist: pyarrow (>=14,<15)
42
+ Requires-Dist: pyarrow (>=14)
46
43
  Requires-Dist: pydantic (>=2.10,<2.11)
47
44
  Requires-Dist: pydantic-settings (>=2.8,<2.9)
48
45
  Requires-Dist: pyjwt (>=2.8,<2.9)
@@ -53,7 +50,7 @@ Requires-Dist: redis[hiredis] (>=6.0.0,<7.0.0)
53
50
  Requires-Dist: rich (>=13,<14)
54
51
  Requires-Dist: starlette-exporter (>=0.23,<0.24)
55
52
  Requires-Dist: structlog (==24.1.0)
56
- Requires-Dist: toml (>=0.10,<0.11)
53
+ Requires-Dist: tomli (>=1.1.0) ; python_version < "3.11"
57
54
  Requires-Dist: typer (==0.12.5)
58
55
  Requires-Dist: ujson (>=5,<6)
59
56
  Requires-Dist: uvicorn[standard] (>=0.32,<0.33)
@@ -66,42 +63,41 @@ Description-Content-Type: text/markdown
66
63
  <h1 align="center">
67
64
  <a href=""><img src="docs/static/img/infrahub-hori.svg" alt="Infrahub" width="350"></a>
68
65
  </h1>
69
- <h3 align="center">Simplify Infrastructure Automation</h2>
70
66
 
71
67
  <p align="center">
72
68
  <a href="https://www.linkedin.com/company/opsmill">
73
- <img src="https://img.shields.io/badge/linkedin-blue?logo=linkedin"/>
69
+ <img src="https://img.shields.io/badge/linkedin-blue?logo=LinkedIn"/>
74
70
  </a>
75
71
  <a href="https://discord.gg/opsmill">
76
72
  <img src="https://img.shields.io/badge/Discord-7289DA?&logo=discord&logoColor=white"/>
77
73
  </a>
78
74
  </p>
79
75
 
80
- Infrahub from [OpsMill](https://opsmill.com) is taking a new approach to Infrastructure Management by providing a new generation of datastore to organize and control all the data that defines how an infrastructure should run. Infrahub offers a central hub to manage the data, templates and playbooks that powers your infrastructure by combining the version control and branch management capabilities similar to Git with the flexible data model and UI of a graph database.
76
+ Infrahub is a graph-based data management platform with built-in version control, CI workflows, peer review, and API access. It's purpose-built to power network, data center, and cloud automation.
81
77
 
82
- If you just want to try Infrahub out, you can use our [Infrahub Sandbox](https://sandbox.infrahub.app/) to get started.
78
+ ## The data foundation for modern automation
83
79
 
84
- ![infrahub screenshot](docs/docs/media/infrahub-readme.gif)
80
+ Infrahub provides a single platform that unifies infrastructure data with business logic, enforces consistency, and integrates with automation tools and AI workflows.
85
81
 
86
- ## Why Use Infrahub?
82
+ With Infrahub as the data foundation in your automation stack, you can move faster, reduce risk, and deliver infrastructure as a reliable service.
87
83
 
88
- **Unified Source of Truth** - Infrahub is a single source of truth for all your infrastructure and network data. It provides a unified view of your infrastructure, allowing you to manage your infrastructure in a more efficient and effective way. Infrahub allows unidirectional and bi-directional [data synchronization](https://docs.infrahub.app/sync/sync/) between other internal systems and Infrahub. The data can be accessed via WebUI, API and SDK, along with SSO and RBAC for access control.
84
+ ## Top ways to use Infrahub
89
85
 
90
- **Flexible Schema** - Infrahub provides a flexible schema for your infrastructure data and related business information, allowing you to define your own data model and customize it to your needs. Get started quickly with our [schema library](https://github.com/opsmill/schema-library) or build your own.
86
+ ### Unify infrastructure data
91
87
 
92
- **Version Control** - Infrahub provides a version control system for your infrastructure data, allowing you to track changes and revert to previous versions if needed. Immutable history of all changes to the data and artifacts is maintained, allowing you to audit and review changes to your infrastructure.
88
+ Sync network and infrastructure device, service, and policy data into a unified source of truth, with rich metadata and robust UI and API access.
93
89
 
94
- **CI Pipeline and Validation** - Infrahub provides a CI pipeline and validation system for your infrastructure data, allowing you to ensure that your infrastructure is always in a valid state. Infrahub was designed with infrastructure-as-code workflows in mind, removing fragility and complexity of combining together multiple tools and projects to achieve the same goal.
90
+ ### Automate at scale
95
91
 
96
- ## Infrahub Use Cases
92
+ Generate, validate, and deploy configurations with unified data. Support full lifecycle management—provisioning, upgrades, decommissioning—across vendors and sites.
97
93
 
98
- **Service Catalog** - Infrahub acts as the underlying system to provide infrastructure-as-a-service, allowing you to manage your services and lifecycle them as the services evolve.
94
+ ### Enable self-service
99
95
 
100
- **Infrastructure Automation** - Provide infrastructure and network automation workflows with Infrahub rendering configurations and artifacts via Jinja2 and python,then passing to deployment tools such as [Nornir](https://www.opsmill.com/simplifying-network-automation-workflows-with-infrahub-nornir-and-jinja2/), [Ansible](https://docs.infrahub.app/ansible), Terraform, or vendor-specific tools.
96
+ Expose automation through catalogs and APIs so application, platform, and ops teams can request infrastructure directly. Speed time to delivery, reduce errors, and make infrastructure more responsive to the business.
101
97
 
102
- **Inventory Management** - Infrahub serves as a centralized inventory system for your infrastructure, allowing you to manage your inventory and track changes to your infrastructure. It provides a WebUI and API for other teams to self-service the information needed to allow the organization to operate.
98
+ ### Build an AIOps knowledge graph
103
99
 
104
- **DCIM and IPAM** - Infrahub provides centralized DCIM and IPAM systems for your infrastructure, capable of handling complex cases such as overlapping IP addresses and VLANs, automation-friendly, branch-aware allocation of resources via Infrahub's [Resource Manager](https://docs.infrahub.app/python-sdk/guides/resource-manager), and more.
100
+ Model dependencies and relationships across infrastructure. Provide the data foundation for AI-driven reasoning, troubleshooting, and predictive operations.
105
101
 
106
102
  ## Quick Start
107
103