infrahub-server 1.5.4__py3-none-any.whl → 1.6.0__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/api/artifact.py +5 -3
- infrahub/auth.py +5 -6
- infrahub/cli/db.py +3 -3
- infrahub/cli/db_commands/clean_duplicate_schema_fields.py +2 -2
- infrahub/cli/dev.py +30 -0
- infrahub/config.py +62 -14
- infrahub/constants/database.py +5 -5
- infrahub/core/branch/models.py +24 -6
- infrahub/core/constants/__init__.py +1 -0
- infrahub/core/diff/model/diff.py +2 -2
- infrahub/core/graph/constraints.py +2 -2
- infrahub/core/manager.py +191 -60
- infrahub/core/merge.py +29 -2
- infrahub/core/migrations/shared.py +2 -2
- infrahub/core/models.py +5 -6
- infrahub/core/node/__init__.py +12 -6
- infrahub/core/node/create.py +36 -8
- infrahub/core/node/ipam.py +4 -4
- infrahub/core/node/node_property_attribute.py +2 -2
- infrahub/core/node/standard.py +1 -1
- infrahub/core/query/attribute.py +1 -1
- infrahub/core/query/branch.py +11 -0
- infrahub/core/query/node.py +9 -5
- infrahub/core/query/standard_node.py +3 -0
- infrahub/core/relationship/model.py +15 -10
- infrahub/core/schema/__init__.py +3 -3
- infrahub/core/schema/generic_schema.py +1 -1
- infrahub/core/schema/schema_branch.py +35 -16
- infrahub/core/task/user_task.py +2 -2
- infrahub/core/validators/determiner.py +3 -6
- infrahub/core/validators/enum.py +2 -2
- infrahub/database/__init__.py +1 -1
- infrahub/dependencies/interface.py +2 -2
- infrahub/events/constants.py +2 -2
- infrahub/git/base.py +42 -1
- infrahub/git/models.py +2 -1
- infrahub/git/repository.py +5 -1
- infrahub/git/tasks.py +28 -1
- infrahub/git/utils.py +9 -0
- infrahub/graphql/analyzer.py +4 -4
- infrahub/graphql/loaders/peers.py +6 -0
- infrahub/graphql/mutations/computed_attribute.py +1 -1
- infrahub/graphql/mutations/convert_object_type.py +1 -1
- infrahub/graphql/mutations/display_label.py +1 -1
- infrahub/graphql/mutations/hfid.py +1 -1
- infrahub/graphql/mutations/ipam.py +1 -1
- infrahub/graphql/mutations/profile.py +9 -1
- infrahub/graphql/mutations/relationship.py +2 -2
- infrahub/graphql/mutations/resource_manager.py +1 -1
- infrahub/graphql/queries/__init__.py +2 -1
- infrahub/graphql/queries/branch.py +58 -3
- infrahub/graphql/queries/ipam.py +9 -4
- infrahub/graphql/queries/resource_manager.py +7 -11
- infrahub/graphql/queries/search.py +5 -6
- infrahub/graphql/resolvers/ipam.py +20 -0
- infrahub/graphql/resolvers/many_relationship.py +12 -11
- infrahub/graphql/resolvers/resolver.py +6 -2
- infrahub/graphql/resolvers/single_relationship.py +1 -11
- infrahub/graphql/schema.py +2 -0
- infrahub/graphql/types/__init__.py +3 -1
- infrahub/graphql/types/branch.py +98 -2
- infrahub/lock.py +6 -6
- infrahub/log.py +1 -1
- infrahub/message_bus/messages/__init__.py +0 -12
- infrahub/patch/constants.py +2 -2
- infrahub/profiles/node_applier.py +9 -0
- infrahub/proposed_change/tasks.py +1 -1
- infrahub/task_manager/task.py +4 -4
- infrahub/telemetry/constants.py +2 -2
- infrahub/trigger/models.py +2 -2
- infrahub/trigger/setup.py +6 -9
- infrahub/utils.py +19 -1
- infrahub/validators/tasks.py +1 -1
- infrahub/workers/infrahub_async.py +39 -1
- infrahub_sdk/async_typer.py +2 -1
- infrahub_sdk/batch.py +2 -2
- infrahub_sdk/client.py +121 -10
- infrahub_sdk/config.py +2 -2
- infrahub_sdk/ctl/branch.py +176 -2
- infrahub_sdk/ctl/check.py +3 -3
- infrahub_sdk/ctl/cli.py +2 -2
- infrahub_sdk/ctl/cli_commands.py +10 -9
- infrahub_sdk/ctl/generator.py +2 -2
- infrahub_sdk/ctl/graphql.py +3 -4
- infrahub_sdk/ctl/importer.py +2 -3
- infrahub_sdk/ctl/repository.py +5 -6
- infrahub_sdk/ctl/task.py +2 -4
- infrahub_sdk/ctl/utils.py +4 -4
- infrahub_sdk/ctl/validate.py +1 -2
- infrahub_sdk/diff.py +80 -3
- infrahub_sdk/graphql/constants.py +14 -1
- infrahub_sdk/graphql/renderers.py +5 -1
- infrahub_sdk/node/attribute.py +10 -10
- infrahub_sdk/node/constants.py +2 -3
- infrahub_sdk/node/node.py +54 -11
- infrahub_sdk/node/related_node.py +1 -2
- infrahub_sdk/node/relationship.py +1 -2
- infrahub_sdk/object_store.py +4 -4
- infrahub_sdk/operation.py +2 -2
- infrahub_sdk/protocols_base.py +0 -1
- infrahub_sdk/protocols_generator/generator.py +1 -1
- infrahub_sdk/pytest_plugin/items/jinja2_transform.py +1 -1
- infrahub_sdk/pytest_plugin/models.py +1 -1
- infrahub_sdk/pytest_plugin/plugin.py +1 -1
- infrahub_sdk/query_groups.py +2 -2
- infrahub_sdk/schema/__init__.py +10 -14
- infrahub_sdk/schema/main.py +2 -2
- infrahub_sdk/schema/repository.py +2 -2
- infrahub_sdk/spec/object.py +2 -2
- infrahub_sdk/spec/range_expansion.py +1 -1
- infrahub_sdk/template/__init__.py +2 -1
- infrahub_sdk/transfer/importer/json.py +3 -3
- infrahub_sdk/types.py +2 -2
- infrahub_sdk/utils.py +2 -2
- {infrahub_server-1.5.4.dist-info → infrahub_server-1.6.0.dist-info}/METADATA +58 -59
- {infrahub_server-1.5.4.dist-info → infrahub_server-1.6.0.dist-info}/RECORD +239 -245
- {infrahub_server-1.5.4.dist-info → infrahub_server-1.6.0.dist-info}/WHEEL +1 -1
- infrahub_server-1.6.0.dist-info/entry_points.txt +12 -0
- infrahub_testcontainers/container.py +2 -2
- infrahub_testcontainers/docker-compose-cluster.test.yml +1 -1
- infrahub_testcontainers/docker-compose.test.yml +1 -1
- infrahub/core/schema/generated/__init__.py +0 -0
- infrahub/core/schema/generated/attribute_schema.py +0 -133
- infrahub/core/schema/generated/base_node_schema.py +0 -111
- infrahub/core/schema/generated/genericnode_schema.py +0 -30
- infrahub/core/schema/generated/node_schema.py +0 -40
- infrahub/core/schema/generated/relationship_schema.py +0 -141
- infrahub_server-1.5.4.dist-info/entry_points.txt +0 -13
- {infrahub_server-1.5.4.dist-info → infrahub_server-1.6.0.dist-info/licenses}/LICENSE.txt +0 -0
infrahub_sdk/client.py
CHANGED
|
@@ -5,14 +5,13 @@ import copy
|
|
|
5
5
|
import logging
|
|
6
6
|
import time
|
|
7
7
|
import warnings
|
|
8
|
-
from collections.abc import Coroutine, Mapping, MutableMapping
|
|
8
|
+
from collections.abc import Callable, Coroutine, Mapping, MutableMapping
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
from functools import wraps
|
|
11
11
|
from time import sleep
|
|
12
12
|
from typing import (
|
|
13
13
|
TYPE_CHECKING,
|
|
14
14
|
Any,
|
|
15
|
-
Callable,
|
|
16
15
|
Literal,
|
|
17
16
|
TypedDict,
|
|
18
17
|
TypeVar,
|
|
@@ -35,7 +34,7 @@ from .config import Config
|
|
|
35
34
|
from .constants import InfrahubClientMode
|
|
36
35
|
from .convert_object_type import CONVERT_OBJECT_MUTATION, ConversionFieldInput
|
|
37
36
|
from .data import RepositoryBranchInfo, RepositoryData
|
|
38
|
-
from .diff import NodeDiff, diff_tree_node_to_node_diff, get_diff_summary_query
|
|
37
|
+
from .diff import DiffTreeData, NodeDiff, diff_tree_node_to_node_diff, get_diff_summary_query, get_diff_tree_query
|
|
39
38
|
from .exceptions import (
|
|
40
39
|
AuthenticationError,
|
|
41
40
|
Error,
|
|
@@ -300,7 +299,7 @@ class BaseClient:
|
|
|
300
299
|
if prefix_length:
|
|
301
300
|
input_data["prefix_length"] = prefix_length
|
|
302
301
|
if member_type:
|
|
303
|
-
if member_type not in
|
|
302
|
+
if member_type not in {"prefix", "address"}:
|
|
304
303
|
raise ValueError("member_type possible values are 'prefix' or 'address'")
|
|
305
304
|
input_data["member_type"] = member_type
|
|
306
305
|
if prefix_type:
|
|
@@ -957,7 +956,7 @@ class InfrahubClient(BaseClient):
|
|
|
957
956
|
try:
|
|
958
957
|
resp = await self._post(url=url, payload=payload, headers=headers, timeout=timeout)
|
|
959
958
|
|
|
960
|
-
if raise_for_error in
|
|
959
|
+
if raise_for_error in {None, True}:
|
|
961
960
|
resp.raise_for_status()
|
|
962
961
|
|
|
963
962
|
retry = False
|
|
@@ -971,7 +970,7 @@ class InfrahubClient(BaseClient):
|
|
|
971
970
|
self.log.error(f"Unable to connect to {self.address} .. ")
|
|
972
971
|
raise
|
|
973
972
|
except httpx.HTTPStatusError as exc:
|
|
974
|
-
if exc.response.status_code in
|
|
973
|
+
if exc.response.status_code in {401, 403}:
|
|
975
974
|
response = decode_json(response=exc.response)
|
|
976
975
|
errors = response.get("errors", [])
|
|
977
976
|
messages = [error.get("message") for error in errors]
|
|
@@ -1209,7 +1208,7 @@ class InfrahubClient(BaseClient):
|
|
|
1209
1208
|
timeout=timeout or self.default_timeout,
|
|
1210
1209
|
)
|
|
1211
1210
|
|
|
1212
|
-
if raise_for_error in
|
|
1211
|
+
if raise_for_error in {None, True}:
|
|
1213
1212
|
resp.raise_for_status()
|
|
1214
1213
|
|
|
1215
1214
|
return decode_json(response=resp)
|
|
@@ -1283,6 +1282,62 @@ class InfrahubClient(BaseClient):
|
|
|
1283
1282
|
|
|
1284
1283
|
return node_diffs
|
|
1285
1284
|
|
|
1285
|
+
async def get_diff_tree(
|
|
1286
|
+
self,
|
|
1287
|
+
branch: str,
|
|
1288
|
+
name: str | None = None,
|
|
1289
|
+
from_time: datetime | None = None,
|
|
1290
|
+
to_time: datetime | None = None,
|
|
1291
|
+
timeout: int | None = None,
|
|
1292
|
+
tracker: str | None = None,
|
|
1293
|
+
) -> DiffTreeData | None:
|
|
1294
|
+
"""Get complete diff tree with metadata and nodes.
|
|
1295
|
+
|
|
1296
|
+
Returns None if no diff exists.
|
|
1297
|
+
"""
|
|
1298
|
+
query = get_diff_tree_query()
|
|
1299
|
+
input_data = {"branch_name": branch}
|
|
1300
|
+
if name:
|
|
1301
|
+
input_data["name"] = name
|
|
1302
|
+
if from_time and to_time and from_time > to_time:
|
|
1303
|
+
raise ValueError("from_time must be <= to_time")
|
|
1304
|
+
if from_time:
|
|
1305
|
+
input_data["from_time"] = from_time.isoformat()
|
|
1306
|
+
if to_time:
|
|
1307
|
+
input_data["to_time"] = to_time.isoformat()
|
|
1308
|
+
|
|
1309
|
+
response = await self.execute_graphql(
|
|
1310
|
+
query=query.render(),
|
|
1311
|
+
branch_name=branch,
|
|
1312
|
+
timeout=timeout,
|
|
1313
|
+
tracker=tracker,
|
|
1314
|
+
variables=input_data,
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
diff_tree = response["DiffTree"]
|
|
1318
|
+
if diff_tree is None:
|
|
1319
|
+
return None
|
|
1320
|
+
|
|
1321
|
+
# Convert nodes to NodeDiff objects
|
|
1322
|
+
node_diffs: list[NodeDiff] = []
|
|
1323
|
+
if "nodes" in diff_tree:
|
|
1324
|
+
for node_dict in diff_tree["nodes"]:
|
|
1325
|
+
node_diff = diff_tree_node_to_node_diff(node_dict=node_dict, branch_name=branch)
|
|
1326
|
+
node_diffs.append(node_diff)
|
|
1327
|
+
|
|
1328
|
+
return DiffTreeData(
|
|
1329
|
+
num_added=diff_tree.get("num_added") or 0,
|
|
1330
|
+
num_updated=diff_tree.get("num_updated") or 0,
|
|
1331
|
+
num_removed=diff_tree.get("num_removed") or 0,
|
|
1332
|
+
num_conflicts=diff_tree.get("num_conflicts") or 0,
|
|
1333
|
+
to_time=diff_tree["to_time"],
|
|
1334
|
+
from_time=diff_tree["from_time"],
|
|
1335
|
+
base_branch=diff_tree["base_branch"],
|
|
1336
|
+
diff_branch=diff_tree["diff_branch"],
|
|
1337
|
+
name=diff_tree.get("name"),
|
|
1338
|
+
nodes=node_diffs,
|
|
1339
|
+
)
|
|
1340
|
+
|
|
1286
1341
|
@overload
|
|
1287
1342
|
async def allocate_next_ip_address(
|
|
1288
1343
|
self,
|
|
@@ -1818,7 +1873,7 @@ class InfrahubClientSync(BaseClient):
|
|
|
1818
1873
|
try:
|
|
1819
1874
|
resp = self._post(url=url, payload=payload, headers=headers, timeout=timeout)
|
|
1820
1875
|
|
|
1821
|
-
if raise_for_error in
|
|
1876
|
+
if raise_for_error in {None, True}:
|
|
1822
1877
|
resp.raise_for_status()
|
|
1823
1878
|
|
|
1824
1879
|
retry = False
|
|
@@ -1832,7 +1887,7 @@ class InfrahubClientSync(BaseClient):
|
|
|
1832
1887
|
self.log.error(f"Unable to connect to {self.address} .. ")
|
|
1833
1888
|
raise
|
|
1834
1889
|
except httpx.HTTPStatusError as exc:
|
|
1835
|
-
if exc.response.status_code in
|
|
1890
|
+
if exc.response.status_code in {401, 403}:
|
|
1836
1891
|
response = decode_json(response=exc.response)
|
|
1837
1892
|
errors = response.get("errors", [])
|
|
1838
1893
|
messages = [error.get("message") for error in errors]
|
|
@@ -2447,7 +2502,7 @@ class InfrahubClientSync(BaseClient):
|
|
|
2447
2502
|
timeout=timeout or self.default_timeout,
|
|
2448
2503
|
)
|
|
2449
2504
|
|
|
2450
|
-
if raise_for_error in
|
|
2505
|
+
if raise_for_error in {None, True}:
|
|
2451
2506
|
resp.raise_for_status()
|
|
2452
2507
|
|
|
2453
2508
|
return decode_json(response=resp)
|
|
@@ -2521,6 +2576,62 @@ class InfrahubClientSync(BaseClient):
|
|
|
2521
2576
|
|
|
2522
2577
|
return node_diffs
|
|
2523
2578
|
|
|
2579
|
+
def get_diff_tree(
|
|
2580
|
+
self,
|
|
2581
|
+
branch: str,
|
|
2582
|
+
name: str | None = None,
|
|
2583
|
+
from_time: datetime | None = None,
|
|
2584
|
+
to_time: datetime | None = None,
|
|
2585
|
+
timeout: int | None = None,
|
|
2586
|
+
tracker: str | None = None,
|
|
2587
|
+
) -> DiffTreeData | None:
|
|
2588
|
+
"""Get complete diff tree with metadata and nodes.
|
|
2589
|
+
|
|
2590
|
+
Returns None if no diff exists.
|
|
2591
|
+
"""
|
|
2592
|
+
query = get_diff_tree_query()
|
|
2593
|
+
input_data = {"branch_name": branch}
|
|
2594
|
+
if name:
|
|
2595
|
+
input_data["name"] = name
|
|
2596
|
+
if from_time and to_time and from_time > to_time:
|
|
2597
|
+
raise ValueError("from_time must be <= to_time")
|
|
2598
|
+
if from_time:
|
|
2599
|
+
input_data["from_time"] = from_time.isoformat()
|
|
2600
|
+
if to_time:
|
|
2601
|
+
input_data["to_time"] = to_time.isoformat()
|
|
2602
|
+
|
|
2603
|
+
response = self.execute_graphql(
|
|
2604
|
+
query=query.render(),
|
|
2605
|
+
branch_name=branch,
|
|
2606
|
+
timeout=timeout,
|
|
2607
|
+
tracker=tracker,
|
|
2608
|
+
variables=input_data,
|
|
2609
|
+
)
|
|
2610
|
+
|
|
2611
|
+
diff_tree = response["DiffTree"]
|
|
2612
|
+
if diff_tree is None:
|
|
2613
|
+
return None
|
|
2614
|
+
|
|
2615
|
+
# Convert nodes to NodeDiff objects
|
|
2616
|
+
node_diffs: list[NodeDiff] = []
|
|
2617
|
+
if "nodes" in diff_tree:
|
|
2618
|
+
for node_dict in diff_tree["nodes"]:
|
|
2619
|
+
node_diff = diff_tree_node_to_node_diff(node_dict=node_dict, branch_name=branch)
|
|
2620
|
+
node_diffs.append(node_diff)
|
|
2621
|
+
|
|
2622
|
+
return DiffTreeData(
|
|
2623
|
+
num_added=diff_tree.get("num_added") or 0,
|
|
2624
|
+
num_updated=diff_tree.get("num_updated") or 0,
|
|
2625
|
+
num_removed=diff_tree.get("num_removed") or 0,
|
|
2626
|
+
num_conflicts=diff_tree.get("num_conflicts") or 0,
|
|
2627
|
+
to_time=diff_tree["to_time"],
|
|
2628
|
+
from_time=diff_tree["from_time"],
|
|
2629
|
+
base_branch=diff_tree["base_branch"],
|
|
2630
|
+
diff_branch=diff_tree["diff_branch"],
|
|
2631
|
+
name=diff_tree.get("name"),
|
|
2632
|
+
nodes=node_diffs,
|
|
2633
|
+
)
|
|
2634
|
+
|
|
2524
2635
|
@overload
|
|
2525
2636
|
def allocate_next_ip_address(
|
|
2526
2637
|
self,
|
infrahub_sdk/config.py
CHANGED
|
@@ -173,7 +173,7 @@ class Config(ConfigBase):
|
|
|
173
173
|
# When using structlog the logger doesn't expose the expected methods by looking at the
|
|
174
174
|
# object to pydantic rejects them. This is a workaround to allow structlog to be used
|
|
175
175
|
# as a logger
|
|
176
|
-
return self.log # type: ignore
|
|
176
|
+
return self.log # type: ignore[return-value]
|
|
177
177
|
|
|
178
178
|
@model_validator(mode="before")
|
|
179
179
|
@classmethod
|
|
@@ -194,7 +194,7 @@ class Config(ConfigBase):
|
|
|
194
194
|
"log": self.log,
|
|
195
195
|
}
|
|
196
196
|
covered_keys = list(config.keys())
|
|
197
|
-
for field in Config.model_fields
|
|
197
|
+
for field in Config.model_fields:
|
|
198
198
|
if field not in covered_keys:
|
|
199
199
|
config[field] = deepcopy(getattr(self, field))
|
|
200
200
|
|
infrahub_sdk/ctl/branch.py
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
2
5
|
|
|
3
6
|
import typer
|
|
4
7
|
from rich.console import Console
|
|
5
8
|
from rich.table import Table
|
|
6
9
|
|
|
7
10
|
from ..async_typer import AsyncTyper
|
|
8
|
-
from ..
|
|
11
|
+
from ..branch import BranchData
|
|
12
|
+
from ..diff import DiffTreeData
|
|
13
|
+
from ..protocols import CoreProposedChange
|
|
14
|
+
from ..utils import calculate_time_diff, decode_json
|
|
9
15
|
from .client import initialize_client
|
|
10
16
|
from .parameters import CONFIG_PARAM
|
|
11
17
|
from .utils import catch_exception
|
|
12
18
|
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from ..client import InfrahubClient
|
|
21
|
+
|
|
13
22
|
app = AsyncTyper()
|
|
14
23
|
console = Console()
|
|
15
24
|
|
|
@@ -18,6 +27,108 @@ DEFAULT_CONFIG_FILE = "infrahubctl.toml"
|
|
|
18
27
|
ENVVAR_CONFIG_FILE = "INFRAHUBCTL_CONFIG"
|
|
19
28
|
|
|
20
29
|
|
|
30
|
+
def format_timestamp(timestamp: str) -> str:
|
|
31
|
+
"""Format ISO timestamp to 'YYYY-MM-DD HH:MM:SS'.
|
|
32
|
+
Args:
|
|
33
|
+
timestamp (str): ISO fromatted timestamp
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
(str): the datetime as string formatted as 'YYYY-MM-DD HH:MM:SS'
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
Any execptions returned from formatting the timestamp are propogated to the caller
|
|
40
|
+
"""
|
|
41
|
+
dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
|
|
42
|
+
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def check_git_files_changed(client: "InfrahubClient", branch: str) -> bool:
|
|
46
|
+
"""Check if there are any Git file changes in a branch.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
client: Infrahub client instance
|
|
50
|
+
branch: Branch name to check
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
True if files have changed, False otherwise
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
Any exceptions from the API call are propagated to the caller
|
|
57
|
+
"""
|
|
58
|
+
url = f"{client.address}/api/diff/files?branch={branch}"
|
|
59
|
+
resp = await client._get(url=url, timeout=client.default_timeout)
|
|
60
|
+
resp.raise_for_status()
|
|
61
|
+
data = decode_json(response=resp)
|
|
62
|
+
|
|
63
|
+
# Check if any repository has files
|
|
64
|
+
if branch in data:
|
|
65
|
+
for repo_data in data[branch].values():
|
|
66
|
+
if isinstance(repo_data, dict) and "files" in repo_data and len(repo_data["files"]) > 0:
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def generate_branch_report_table(
|
|
73
|
+
branch: BranchData, diff_tree: DiffTreeData | None, git_files_changed: bool | None
|
|
74
|
+
) -> Table:
|
|
75
|
+
branch_table = Table(show_header=False, box=None)
|
|
76
|
+
branch_table.add_column(justify="left")
|
|
77
|
+
branch_table.add_column(justify="right")
|
|
78
|
+
|
|
79
|
+
branch_table.add_row("Created at", format_timestamp(branch.branched_from))
|
|
80
|
+
|
|
81
|
+
status_value = branch.status.value if hasattr(branch.status, "value") else str(branch.status)
|
|
82
|
+
branch_table.add_row("Status", status_value)
|
|
83
|
+
|
|
84
|
+
branch_table.add_row("Synced with Git", "Yes" if branch.sync_with_git else "No")
|
|
85
|
+
|
|
86
|
+
if git_files_changed is not None:
|
|
87
|
+
branch_table.add_row("Git files changed", "Yes" if git_files_changed else "No")
|
|
88
|
+
else:
|
|
89
|
+
branch_table.add_row("Git files changed", "N/A")
|
|
90
|
+
|
|
91
|
+
branch_table.add_row("Has schema changes", "Yes" if branch.has_schema_changes else "No")
|
|
92
|
+
|
|
93
|
+
if diff_tree:
|
|
94
|
+
branch_table.add_row("Diff last updated", format_timestamp(diff_tree["to_time"]))
|
|
95
|
+
branch_table.add_row("Amount of additions", str(diff_tree["num_added"]))
|
|
96
|
+
branch_table.add_row("Amount of deletions", str(diff_tree["num_removed"]))
|
|
97
|
+
branch_table.add_row("Amount of updates", str(diff_tree["num_updated"]))
|
|
98
|
+
branch_table.add_row("Amount of conflicts", str(diff_tree["num_conflicts"]))
|
|
99
|
+
else:
|
|
100
|
+
branch_table.add_row("Diff last updated", "No diff available")
|
|
101
|
+
branch_table.add_row("Amount of additions", "-")
|
|
102
|
+
branch_table.add_row("Amount of deletions", "-")
|
|
103
|
+
branch_table.add_row("Amount of updates", "-")
|
|
104
|
+
branch_table.add_row("Amount of conflicts", "-")
|
|
105
|
+
|
|
106
|
+
return branch_table
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def generate_proposed_change_tables(proposed_changes: list[CoreProposedChange]) -> list[Table]:
|
|
110
|
+
proposed_change_tables: list[Table] = []
|
|
111
|
+
|
|
112
|
+
for pc in proposed_changes:
|
|
113
|
+
# Create proposal table
|
|
114
|
+
proposed_change_table = Table(show_header=False, box=None)
|
|
115
|
+
proposed_change_table.add_column(justify="left")
|
|
116
|
+
proposed_change_table.add_column(justify="right")
|
|
117
|
+
|
|
118
|
+
# Extract data from node
|
|
119
|
+
proposed_change_table.add_row("Name", pc.name.value)
|
|
120
|
+
proposed_change_table.add_row("State", str(pc.state.value))
|
|
121
|
+
proposed_change_table.add_row("Is draft", "Yes" if pc.is_draft.value else "No")
|
|
122
|
+
proposed_change_table.add_row("Created by", pc.created_by.peer.name.value) # type: ignore[union-attr]
|
|
123
|
+
proposed_change_table.add_row("Created at", format_timestamp(str(pc.created_by.updated_at)))
|
|
124
|
+
proposed_change_table.add_row("Approvals", str(len(pc.approved_by.peers)))
|
|
125
|
+
proposed_change_table.add_row("Rejections", str(len(pc.rejected_by.peers)))
|
|
126
|
+
|
|
127
|
+
proposed_change_tables.append(proposed_change_table)
|
|
128
|
+
|
|
129
|
+
return proposed_change_tables
|
|
130
|
+
|
|
131
|
+
|
|
21
132
|
@app.callback()
|
|
22
133
|
def callback() -> None:
|
|
23
134
|
"""
|
|
@@ -49,7 +160,7 @@ async def list_branch(_: str = CONFIG_PARAM) -> None:
|
|
|
49
160
|
table.add_column("Status")
|
|
50
161
|
|
|
51
162
|
# identify the default branch and always print it first
|
|
52
|
-
default_branch =
|
|
163
|
+
default_branch = next(branch for branch in branches.values() if branch.is_default)
|
|
53
164
|
table.add_row(
|
|
54
165
|
default_branch.name,
|
|
55
166
|
default_branch.description or " - ",
|
|
@@ -143,3 +254,66 @@ async def validate(branch_name: str, _: str = CONFIG_PARAM) -> None:
|
|
|
143
254
|
client = initialize_client()
|
|
144
255
|
await client.branch.validate(branch_name=branch_name)
|
|
145
256
|
console.print(f"Branch '{branch_name}' is valid.")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@app.command()
|
|
260
|
+
@catch_exception(console=console)
|
|
261
|
+
async def report(
|
|
262
|
+
branch_name: str = typer.Argument(..., help="Branch name to generate report for"),
|
|
263
|
+
update_diff: bool = typer.Option(False, "--update-diff", help="Update diff before generating report"),
|
|
264
|
+
_: str = CONFIG_PARAM,
|
|
265
|
+
) -> None:
|
|
266
|
+
"""Generate branch cleanup status report."""
|
|
267
|
+
|
|
268
|
+
client = initialize_client()
|
|
269
|
+
|
|
270
|
+
# Fetch branch metadata first (needed for diff creation)
|
|
271
|
+
branch = await client.branch.get(branch_name=branch_name)
|
|
272
|
+
|
|
273
|
+
if branch.is_default:
|
|
274
|
+
console.print("[red]Cannot create a report for the default branch!")
|
|
275
|
+
sys.exit(1)
|
|
276
|
+
|
|
277
|
+
# Update diff if requested
|
|
278
|
+
if update_diff:
|
|
279
|
+
console.print("Updating diff...")
|
|
280
|
+
# Create diff from branch creation to now
|
|
281
|
+
from_time = datetime.fromisoformat(branch.branched_from.replace("Z", "+00:00"))
|
|
282
|
+
to_time = datetime.now(timezone.utc)
|
|
283
|
+
await client.create_diff(
|
|
284
|
+
branch=branch_name,
|
|
285
|
+
name=f"report-{branch_name}",
|
|
286
|
+
from_time=from_time,
|
|
287
|
+
to_time=to_time,
|
|
288
|
+
)
|
|
289
|
+
console.print("Diff updated\n")
|
|
290
|
+
|
|
291
|
+
diff_tree = await client.get_diff_tree(branch=branch_name)
|
|
292
|
+
|
|
293
|
+
git_files_changed = await check_git_files_changed(client, branch=branch_name)
|
|
294
|
+
|
|
295
|
+
proposed_changes = await client.filters(
|
|
296
|
+
kind=CoreProposedChange, # type: ignore[type-abstract]
|
|
297
|
+
source_branch__value=branch_name,
|
|
298
|
+
include=["created_by"],
|
|
299
|
+
prefetch_relationships=True,
|
|
300
|
+
property=True,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
branch_table = generate_branch_report_table(branch=branch, diff_tree=diff_tree, git_files_changed=git_files_changed)
|
|
304
|
+
proposed_change_tables = generate_proposed_change_tables(proposed_changes=proposed_changes)
|
|
305
|
+
|
|
306
|
+
console.print()
|
|
307
|
+
console.print(f"[bold]Branch: {branch_name}[/bold]")
|
|
308
|
+
|
|
309
|
+
console.print(branch_table)
|
|
310
|
+
console.print()
|
|
311
|
+
|
|
312
|
+
if not proposed_changes:
|
|
313
|
+
console.print("No proposed changes for this branch")
|
|
314
|
+
console.print()
|
|
315
|
+
|
|
316
|
+
for proposed_change, proposed_change_table in zip(proposed_changes, proposed_change_tables, strict=True):
|
|
317
|
+
console.print(f"Proposed change: {proposed_change.name.value}")
|
|
318
|
+
console.print(proposed_change_table)
|
|
319
|
+
console.print()
|
infrahub_sdk/ctl/check.py
CHANGED
|
@@ -5,7 +5,7 @@ import sys
|
|
|
5
5
|
from asyncio import run as aiorun
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
9
|
|
|
10
10
|
import typer
|
|
11
11
|
from rich.console import Console
|
|
@@ -49,8 +49,8 @@ def run(
|
|
|
49
49
|
format_json: bool,
|
|
50
50
|
list_available: bool,
|
|
51
51
|
variables: dict[str, str],
|
|
52
|
-
name:
|
|
53
|
-
branch:
|
|
52
|
+
name: str | None = None,
|
|
53
|
+
branch: str | None = None,
|
|
54
54
|
) -> None:
|
|
55
55
|
"""Locate and execute all checks under the defined path."""
|
|
56
56
|
|
infrahub_sdk/ctl/cli.py
CHANGED
|
@@ -4,8 +4,8 @@ try:
|
|
|
4
4
|
from .cli_commands import app
|
|
5
5
|
except ImportError as exc:
|
|
6
6
|
sys.exit(
|
|
7
|
-
f"Module {exc.name} is not available, install the 'ctl' extra of the infrahub-sdk package,
|
|
8
|
-
"
|
|
7
|
+
f"Module {exc.name} is not available, install the 'ctl' extra of the infrahub-sdk package, "
|
|
8
|
+
f"`pip install 'infrahub-sdk[ctl]'` or run `uv sync --extra ctl`."
|
|
9
9
|
)
|
|
10
10
|
|
|
11
11
|
__all__ = ["app"]
|
infrahub_sdk/ctl/cli_commands.py
CHANGED
|
@@ -6,8 +6,9 @@ import importlib
|
|
|
6
6
|
import logging
|
|
7
7
|
import platform
|
|
8
8
|
import sys
|
|
9
|
+
from collections.abc import Callable
|
|
9
10
|
from pathlib import Path
|
|
10
|
-
from typing import TYPE_CHECKING, Any
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
11
12
|
|
|
12
13
|
import typer
|
|
13
14
|
import ujson
|
|
@@ -77,13 +78,13 @@ console = Console()
|
|
|
77
78
|
@catch_exception(console=console)
|
|
78
79
|
def check(
|
|
79
80
|
check_name: str = typer.Argument(default="", help="Name of the Python check"),
|
|
80
|
-
branch:
|
|
81
|
+
branch: str | None = None,
|
|
81
82
|
path: str = typer.Option(".", help="Root directory"),
|
|
82
83
|
debug: bool = False,
|
|
83
84
|
format_json: bool = False,
|
|
84
85
|
_: str = CONFIG_PARAM,
|
|
85
86
|
list_available: bool = typer.Option(False, "--list", help="Show available Python checks"),
|
|
86
|
-
variables:
|
|
87
|
+
variables: list[str] | None = typer.Argument(
|
|
87
88
|
None, help="Variables to pass along with the query. Format key=value key=value."
|
|
88
89
|
),
|
|
89
90
|
) -> None:
|
|
@@ -105,12 +106,12 @@ def check(
|
|
|
105
106
|
@catch_exception(console=console)
|
|
106
107
|
async def generator(
|
|
107
108
|
generator_name: str = typer.Argument(default="", help="Name of the Generator"),
|
|
108
|
-
branch:
|
|
109
|
+
branch: str | None = None,
|
|
109
110
|
path: str = typer.Option(".", help="Root directory"),
|
|
110
111
|
debug: bool = False,
|
|
111
112
|
_: str = CONFIG_PARAM,
|
|
112
113
|
list_available: bool = typer.Option(False, "--list", help="Show available Generators"),
|
|
113
|
-
variables:
|
|
114
|
+
variables: list[str] | None = typer.Argument(
|
|
114
115
|
None, help="Variables to pass along with the query. Format key=value key=value."
|
|
115
116
|
),
|
|
116
117
|
) -> None:
|
|
@@ -133,13 +134,13 @@ async def run(
|
|
|
133
134
|
debug: bool = False,
|
|
134
135
|
_: str = CONFIG_PARAM,
|
|
135
136
|
branch: str = typer.Option(None, help="Branch on which to run the script."),
|
|
136
|
-
concurrent:
|
|
137
|
+
concurrent: int | None = typer.Option(
|
|
137
138
|
None,
|
|
138
139
|
help="Maximum number of requests to execute at the same time.",
|
|
139
140
|
envvar="INFRAHUB_MAX_CONCURRENT_EXECUTION",
|
|
140
141
|
),
|
|
141
142
|
timeout: int = typer.Option(60, help="Timeout in sec", envvar="INFRAHUB_TIMEOUT"),
|
|
142
|
-
variables:
|
|
143
|
+
variables: list[str] | None = typer.Argument(
|
|
143
144
|
None, help="Variables to pass along with the query. Format key=value key=value."
|
|
144
145
|
),
|
|
145
146
|
) -> None:
|
|
@@ -250,7 +251,7 @@ async def _run_transform(
|
|
|
250
251
|
@catch_exception(console=console)
|
|
251
252
|
async def render(
|
|
252
253
|
transform_name: str = typer.Argument(default="", help="Name of the Python transformation", show_default=False),
|
|
253
|
-
variables:
|
|
254
|
+
variables: list[str] | None = typer.Argument(
|
|
254
255
|
None, help="Variables to pass along with the query. Format key=value key=value."
|
|
255
256
|
),
|
|
256
257
|
branch: str = typer.Option(None, help="Branch on which to render the transform."),
|
|
@@ -300,7 +301,7 @@ async def render(
|
|
|
300
301
|
@catch_exception(console=console)
|
|
301
302
|
def transform(
|
|
302
303
|
transform_name: str = typer.Argument(default="", help="Name of the Python transformation", show_default=False),
|
|
303
|
-
variables:
|
|
304
|
+
variables: list[str] | None = typer.Argument(
|
|
304
305
|
None, help="Variables to pass along with the query. Format key=value key=value."
|
|
305
306
|
),
|
|
306
307
|
branch: str = typer.Option(None, help="Branch on which to run the transformation"),
|
infrahub_sdk/ctl/generator.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
6
|
import typer
|
|
7
7
|
from rich.console import Console
|
|
@@ -22,7 +22,7 @@ async def run(
|
|
|
22
22
|
debug: bool,
|
|
23
23
|
list_available: bool,
|
|
24
24
|
branch: str | None = None,
|
|
25
|
-
variables:
|
|
25
|
+
variables: list[str] | None = None,
|
|
26
26
|
) -> None:
|
|
27
27
|
init_logging(debug=debug)
|
|
28
28
|
repository_config = get_repository_config(find_repository_config_file())
|
infrahub_sdk/ctl/graphql.py
CHANGED
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
import ast
|
|
4
4
|
from collections import defaultdict
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Optional
|
|
7
6
|
|
|
8
7
|
import typer
|
|
9
8
|
from ariadne_codegen.client_generators.package import PackageGenerator, get_package_generator
|
|
@@ -108,14 +107,14 @@ async def export_schema(
|
|
|
108
107
|
schema_text = await client.schema.get_graphql_schema()
|
|
109
108
|
|
|
110
109
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
-
destination.write_text(schema_text)
|
|
110
|
+
destination.write_text(schema_text, encoding="utf-8")
|
|
112
111
|
console.print(f"[green]Schema exported to {destination}")
|
|
113
112
|
|
|
114
113
|
|
|
115
114
|
@app.command()
|
|
116
115
|
@catch_exception(console=console)
|
|
117
116
|
async def generate_return_types(
|
|
118
|
-
query:
|
|
117
|
+
query: Path | None = typer.Argument(
|
|
119
118
|
None, help="Location of the GraphQL query file(s). Defaults to current directory if not specified."
|
|
120
119
|
),
|
|
121
120
|
schema: Path = typer.Option("schema.graphql", help="Path to the GraphQL schema file."),
|
|
@@ -180,5 +179,5 @@ async def generate_return_types(
|
|
|
180
179
|
|
|
181
180
|
generate_result_types(directory=directory, package=package_generator, fragment=module_fragment)
|
|
182
181
|
|
|
183
|
-
for file_name in package_generator._result_types_files
|
|
182
|
+
for file_name in package_generator._result_types_files:
|
|
184
183
|
console.print(f"[green]Generated {file_name} in {directory}")
|
infrahub_sdk/ctl/importer.py
CHANGED
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from asyncio import run as aiorun
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Optional
|
|
6
5
|
|
|
7
6
|
import typer
|
|
8
7
|
from rich.console import Console
|
|
@@ -16,7 +15,7 @@ from .parameters import CONFIG_PARAM
|
|
|
16
15
|
|
|
17
16
|
def local_directory() -> Path:
|
|
18
17
|
# We use a function here to avoid failure when generating the documentation due to directory name
|
|
19
|
-
return Path
|
|
18
|
+
return Path.cwd()
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
def load(
|
|
@@ -27,7 +26,7 @@ def load(
|
|
|
27
26
|
quiet: bool = typer.Option(False, help="No console output"),
|
|
28
27
|
_: str = CONFIG_PARAM,
|
|
29
28
|
branch: str = typer.Option(None, help="Branch from which to export"),
|
|
30
|
-
concurrent:
|
|
29
|
+
concurrent: int | None = typer.Option(
|
|
31
30
|
None,
|
|
32
31
|
help="Maximum number of requests to execute at the same time.",
|
|
33
32
|
envvar="INFRAHUB_MAX_CONCURRENT_EXECUTION",
|
infrahub_sdk/ctl/repository.py
CHANGED
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Optional
|
|
6
5
|
|
|
7
6
|
import typer
|
|
8
7
|
import yaml
|
|
@@ -109,7 +108,7 @@ async def add(
|
|
|
109
108
|
name: str,
|
|
110
109
|
location: str,
|
|
111
110
|
description: str = "",
|
|
112
|
-
username:
|
|
111
|
+
username: str | None = None,
|
|
113
112
|
password: str = "",
|
|
114
113
|
ref: str = "",
|
|
115
114
|
read_only: bool = False,
|
|
@@ -155,7 +154,7 @@ async def add(
|
|
|
155
154
|
|
|
156
155
|
@app.command()
|
|
157
156
|
async def list(
|
|
158
|
-
branch:
|
|
157
|
+
branch: str | None = typer.Option(None, help="Branch on which to list repositories."),
|
|
159
158
|
debug: bool = False,
|
|
160
159
|
_: str = CONFIG_PARAM,
|
|
161
160
|
) -> None:
|
|
@@ -214,12 +213,12 @@ async def init(
|
|
|
214
213
|
default="https://github.com/opsmill/infrahub-template.git",
|
|
215
214
|
help="Template to use for the new repository. Can be a local path or a git repository URL.",
|
|
216
215
|
),
|
|
217
|
-
data:
|
|
218
|
-
vcs_ref:
|
|
216
|
+
data: Path | None = typer.Option(default=None, help="Path to YAML file containing answers to CLI prompt."),
|
|
217
|
+
vcs_ref: str | None = typer.Option(
|
|
219
218
|
default="HEAD",
|
|
220
219
|
help="VCS reference to use for the template. Defaults to HEAD.",
|
|
221
220
|
),
|
|
222
|
-
trust:
|
|
221
|
+
trust: bool | None = typer.Option(
|
|
223
222
|
default=False,
|
|
224
223
|
help="Trust the template repository. If set, the template will be cloned without verification.",
|
|
225
224
|
),
|