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.
- infrahub/actions/tasks.py +200 -16
- infrahub/api/artifact.py +3 -0
- infrahub/api/query.py +2 -0
- infrahub/api/schema.py +3 -0
- infrahub/auth.py +5 -5
- infrahub/cli/db.py +2 -2
- infrahub/config.py +7 -2
- infrahub/core/attribute.py +22 -19
- infrahub/core/branch/models.py +2 -2
- infrahub/core/branch/needs_rebase_status.py +11 -0
- infrahub/core/branch/tasks.py +2 -2
- infrahub/core/constants/__init__.py +1 -0
- infrahub/core/convert_object_type/object_conversion.py +201 -0
- infrahub/core/convert_object_type/repository_conversion.py +89 -0
- infrahub/core/convert_object_type/schema_mapping.py +27 -3
- infrahub/core/diff/query/artifact.py +12 -9
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/initialization.py +2 -2
- infrahub/core/manager.py +3 -81
- infrahub/core/migrations/graph/__init__.py +2 -0
- infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +166 -0
- infrahub/core/node/__init__.py +26 -3
- infrahub/core/node/create.py +79 -38
- infrahub/core/node/lock_utils.py +98 -0
- infrahub/core/property.py +11 -0
- infrahub/core/protocols.py +1 -0
- infrahub/core/query/attribute.py +27 -15
- infrahub/core/query/node.py +47 -184
- infrahub/core/query/relationship.py +43 -26
- infrahub/core/query/subquery.py +0 -8
- infrahub/core/relationship/model.py +59 -19
- infrahub/core/schema/attribute_schema.py +0 -2
- infrahub/core/schema/definitions/core/repository.py +7 -0
- infrahub/core/schema/relationship_schema.py +0 -1
- infrahub/core/schema/schema_branch.py +3 -2
- infrahub/generators/models.py +31 -12
- infrahub/generators/tasks.py +3 -1
- infrahub/git/base.py +38 -1
- infrahub/graphql/api/dependencies.py +2 -4
- infrahub/graphql/api/endpoints.py +2 -2
- infrahub/graphql/app.py +2 -4
- infrahub/graphql/initialization.py +2 -3
- infrahub/graphql/manager.py +212 -137
- infrahub/graphql/middleware.py +12 -0
- infrahub/graphql/mutations/branch.py +11 -0
- infrahub/graphql/mutations/computed_attribute.py +110 -3
- infrahub/graphql/mutations/convert_object_type.py +34 -13
- infrahub/graphql/mutations/ipam.py +21 -8
- infrahub/graphql/mutations/main.py +37 -153
- infrahub/graphql/mutations/profile.py +195 -0
- infrahub/graphql/mutations/proposed_change.py +2 -1
- infrahub/graphql/mutations/repository.py +22 -83
- infrahub/graphql/mutations/webhook.py +1 -1
- infrahub/graphql/registry.py +173 -0
- infrahub/graphql/schema.py +4 -1
- infrahub/lock.py +52 -26
- infrahub/locks/__init__.py +0 -0
- infrahub/locks/tasks.py +37 -0
- infrahub/patch/plan_writer.py +2 -2
- infrahub/profiles/__init__.py +0 -0
- infrahub/profiles/node_applier.py +101 -0
- infrahub/profiles/queries/__init__.py +0 -0
- infrahub/profiles/queries/get_profile_data.py +99 -0
- infrahub/profiles/tasks.py +63 -0
- infrahub/repositories/__init__.py +0 -0
- infrahub/repositories/create_repository.py +113 -0
- infrahub/tasks/registry.py +6 -4
- infrahub/webhook/models.py +1 -1
- infrahub/workflows/catalogue.py +38 -3
- infrahub/workflows/models.py +17 -2
- infrahub_sdk/branch.py +5 -8
- infrahub_sdk/client.py +364 -84
- infrahub_sdk/convert_object_type.py +61 -0
- infrahub_sdk/ctl/check.py +2 -3
- infrahub_sdk/ctl/cli_commands.py +16 -12
- infrahub_sdk/ctl/config.py +8 -2
- infrahub_sdk/ctl/generator.py +2 -3
- infrahub_sdk/ctl/repository.py +39 -1
- infrahub_sdk/ctl/schema.py +12 -1
- infrahub_sdk/ctl/utils.py +4 -0
- infrahub_sdk/ctl/validate.py +5 -3
- infrahub_sdk/diff.py +4 -5
- infrahub_sdk/exceptions.py +2 -0
- infrahub_sdk/graphql.py +7 -2
- infrahub_sdk/node/attribute.py +2 -0
- infrahub_sdk/node/node.py +28 -20
- infrahub_sdk/playback.py +1 -2
- infrahub_sdk/protocols.py +40 -6
- infrahub_sdk/pytest_plugin/plugin.py +7 -4
- infrahub_sdk/pytest_plugin/utils.py +40 -0
- infrahub_sdk/repository.py +1 -2
- infrahub_sdk/schema/main.py +1 -0
- infrahub_sdk/spec/object.py +43 -4
- infrahub_sdk/spec/range_expansion.py +118 -0
- infrahub_sdk/timestamp.py +18 -6
- {infrahub_server-1.4.9.dist-info → infrahub_server-1.5.0b0.dist-info}/METADATA +20 -24
- {infrahub_server-1.4.9.dist-info → infrahub_server-1.5.0b0.dist-info}/RECORD +102 -84
- infrahub_testcontainers/models.py +2 -2
- infrahub_testcontainers/performance_test.py +4 -4
- infrahub/core/convert_object_type/conversion.py +0 -134
- {infrahub_server-1.4.9.dist-info → infrahub_server-1.5.0b0.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.4.9.dist-info → infrahub_server-1.5.0b0.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
413
|
+
member_action: Dropdown
|
|
409
414
|
group: RelatedNode
|
|
410
415
|
|
|
411
416
|
|
|
412
417
|
class CoreGroupTriggerRule(CoreTriggerRule):
|
|
413
|
-
|
|
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
|
-
|
|
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
|
-
|
|
963
|
+
member_action: Dropdown
|
|
942
964
|
group: RelatedNodeSync
|
|
943
965
|
|
|
944
966
|
|
|
945
967
|
class CoreGroupTriggerRuleSync(CoreTriggerRuleSync):
|
|
946
|
-
|
|
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
|
-
|
|
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=
|
|
21
|
+
default=None,
|
|
22
22
|
metavar="INFRAHUB_REPO_CONFIG_FILE",
|
|
23
|
-
help="Infrahub configuration file for the repository (
|
|
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
|
-
|
|
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
|
|
infrahub_sdk/repository.py
CHANGED
infrahub_sdk/schema/main.py
CHANGED
|
@@ -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
|
infrahub_sdk/spec/object.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
83
|
+
params: SubstractParams = {}
|
|
77
84
|
for key, regex in REGEX_MAPPING.items():
|
|
78
85
|
match = re.search(regex, value)
|
|
79
86
|
if match:
|
|
80
|
-
|
|
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)
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
40
|
+
Requires-Dist: prefect (==3.4.22)
|
|
44
41
|
Requires-Dist: prefect-redis (==0.2.4)
|
|
45
|
-
Requires-Dist: pyarrow (>=14
|
|
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:
|
|
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=
|
|
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
|
|
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
|
-
|
|
78
|
+
## The data foundation for modern automation
|
|
83
79
|
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
+
## Top ways to use Infrahub
|
|
89
85
|
|
|
90
|
-
|
|
86
|
+
### Unify infrastructure data
|
|
91
87
|
|
|
92
|
-
|
|
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
|
-
|
|
90
|
+
### Automate at scale
|
|
95
91
|
|
|
96
|
-
|
|
92
|
+
Generate, validate, and deploy configurations with unified data. Support full lifecycle management—provisioning, upgrades, decommissioning—across vendors and sites.
|
|
97
93
|
|
|
98
|
-
|
|
94
|
+
### Enable self-service
|
|
99
95
|
|
|
100
|
-
|
|
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
|
-
|
|
98
|
+
### Build an AIOps knowledge graph
|
|
103
99
|
|
|
104
|
-
|
|
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
|
|