infrahub-server 1.6.0b0__py3-none-any.whl → 1.6.2__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/oauth2.py +33 -6
- infrahub/api/oidc.py +36 -6
- infrahub/auth.py +11 -0
- infrahub/auth_pkce.py +41 -0
- infrahub/config.py +9 -3
- infrahub/core/branch/models.py +3 -2
- infrahub/core/branch/tasks.py +6 -1
- infrahub/core/changelog/models.py +2 -2
- infrahub/core/constants/__init__.py +1 -0
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/integrity/object_conflict/conflict_recorder.py +1 -1
- infrahub/core/manager.py +36 -31
- infrahub/core/migrations/graph/__init__.py +4 -0
- infrahub/core/migrations/graph/m041_deleted_dup_edges.py +30 -12
- infrahub/core/migrations/graph/m047_backfill_or_null_display_label.py +606 -0
- infrahub/core/migrations/graph/m048_undelete_rel_props.py +161 -0
- infrahub/core/models.py +5 -6
- infrahub/core/node/__init__.py +16 -13
- infrahub/core/node/create.py +36 -8
- infrahub/core/node/proposed_change.py +5 -3
- infrahub/core/node/standard.py +1 -1
- infrahub/core/protocols.py +1 -7
- infrahub/core/query/attribute.py +1 -1
- infrahub/core/query/node.py +9 -5
- infrahub/core/relationship/model.py +21 -4
- infrahub/core/schema/generic_schema.py +1 -1
- infrahub/core/schema/manager.py +8 -3
- infrahub/core/schema/schema_branch.py +35 -16
- infrahub/core/validators/attribute/choices.py +2 -2
- infrahub/core/validators/determiner.py +3 -6
- infrahub/database/__init__.py +1 -1
- infrahub/git/base.py +2 -3
- infrahub/git/models.py +13 -0
- infrahub/git/tasks.py +23 -19
- infrahub/git/utils.py +16 -9
- infrahub/graphql/app.py +6 -6
- infrahub/graphql/loaders/peers.py +6 -0
- infrahub/graphql/mutations/action.py +15 -7
- infrahub/graphql/mutations/hfid.py +1 -1
- infrahub/graphql/mutations/profile.py +8 -1
- infrahub/graphql/mutations/repository.py +3 -3
- infrahub/graphql/mutations/schema.py +4 -4
- infrahub/graphql/mutations/webhook.py +2 -2
- infrahub/graphql/queries/resource_manager.py +2 -3
- infrahub/graphql/queries/search.py +2 -3
- 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/log.py +1 -1
- infrahub/message_bus/messages/__init__.py +0 -12
- infrahub/profiles/node_applier.py +9 -0
- infrahub/proposed_change/branch_diff.py +1 -1
- infrahub/proposed_change/tasks.py +1 -1
- infrahub/repositories/create_repository.py +3 -3
- infrahub/task_manager/models.py +1 -1
- infrahub/task_manager/task.py +5 -5
- infrahub/trigger/setup.py +6 -9
- infrahub/utils.py +18 -0
- infrahub/validators/tasks.py +1 -1
- infrahub/workers/infrahub_async.py +7 -6
- infrahub_sdk/client.py +113 -1
- infrahub_sdk/ctl/AGENTS.md +67 -0
- infrahub_sdk/ctl/branch.py +175 -1
- infrahub_sdk/ctl/check.py +3 -3
- infrahub_sdk/ctl/cli_commands.py +9 -9
- infrahub_sdk/ctl/generator.py +2 -2
- infrahub_sdk/ctl/graphql.py +1 -2
- infrahub_sdk/ctl/importer.py +1 -2
- infrahub_sdk/ctl/repository.py +6 -49
- infrahub_sdk/ctl/task.py +2 -4
- infrahub_sdk/ctl/utils.py +2 -2
- 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 +0 -1
- infrahub_sdk/node/constants.py +3 -1
- infrahub_sdk/node/node.py +303 -3
- infrahub_sdk/node/related_node.py +1 -2
- infrahub_sdk/node/relationship.py +1 -2
- infrahub_sdk/protocols_base.py +0 -1
- infrahub_sdk/pytest_plugin/AGENTS.md +67 -0
- infrahub_sdk/schema/__init__.py +0 -3
- infrahub_sdk/timestamp.py +7 -7
- {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/METADATA +2 -3
- {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/RECORD +91 -86
- {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/WHEEL +1 -1
- infrahub_testcontainers/container.py +2 -2
- {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/entry_points.txt +0 -0
- {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/licenses/LICENSE.txt +0 -0
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
|
"""
|
|
@@ -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_commands.py
CHANGED
|
@@ -8,7 +8,7 @@ import platform
|
|
|
8
8
|
import sys
|
|
9
9
|
from collections.abc import Callable
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import TYPE_CHECKING, Any
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
12
|
|
|
13
13
|
import typer
|
|
14
14
|
import ujson
|
|
@@ -78,13 +78,13 @@ console = Console()
|
|
|
78
78
|
@catch_exception(console=console)
|
|
79
79
|
def check(
|
|
80
80
|
check_name: str = typer.Argument(default="", help="Name of the Python check"),
|
|
81
|
-
branch:
|
|
81
|
+
branch: str | None = None,
|
|
82
82
|
path: str = typer.Option(".", help="Root directory"),
|
|
83
83
|
debug: bool = False,
|
|
84
84
|
format_json: bool = False,
|
|
85
85
|
_: str = CONFIG_PARAM,
|
|
86
86
|
list_available: bool = typer.Option(False, "--list", help="Show available Python checks"),
|
|
87
|
-
variables:
|
|
87
|
+
variables: list[str] | None = typer.Argument(
|
|
88
88
|
None, help="Variables to pass along with the query. Format key=value key=value."
|
|
89
89
|
),
|
|
90
90
|
) -> None:
|
|
@@ -106,12 +106,12 @@ def check(
|
|
|
106
106
|
@catch_exception(console=console)
|
|
107
107
|
async def generator(
|
|
108
108
|
generator_name: str = typer.Argument(default="", help="Name of the Generator"),
|
|
109
|
-
branch:
|
|
109
|
+
branch: str | None = None,
|
|
110
110
|
path: str = typer.Option(".", help="Root directory"),
|
|
111
111
|
debug: bool = False,
|
|
112
112
|
_: str = CONFIG_PARAM,
|
|
113
113
|
list_available: bool = typer.Option(False, "--list", help="Show available Generators"),
|
|
114
|
-
variables:
|
|
114
|
+
variables: list[str] | None = typer.Argument(
|
|
115
115
|
None, help="Variables to pass along with the query. Format key=value key=value."
|
|
116
116
|
),
|
|
117
117
|
) -> None:
|
|
@@ -134,13 +134,13 @@ async def run(
|
|
|
134
134
|
debug: bool = False,
|
|
135
135
|
_: str = CONFIG_PARAM,
|
|
136
136
|
branch: str = typer.Option(None, help="Branch on which to run the script."),
|
|
137
|
-
concurrent:
|
|
137
|
+
concurrent: int | None = typer.Option(
|
|
138
138
|
None,
|
|
139
139
|
help="Maximum number of requests to execute at the same time.",
|
|
140
140
|
envvar="INFRAHUB_MAX_CONCURRENT_EXECUTION",
|
|
141
141
|
),
|
|
142
142
|
timeout: int = typer.Option(60, help="Timeout in sec", envvar="INFRAHUB_TIMEOUT"),
|
|
143
|
-
variables:
|
|
143
|
+
variables: list[str] | None = typer.Argument(
|
|
144
144
|
None, help="Variables to pass along with the query. Format key=value key=value."
|
|
145
145
|
),
|
|
146
146
|
) -> None:
|
|
@@ -251,7 +251,7 @@ async def _run_transform(
|
|
|
251
251
|
@catch_exception(console=console)
|
|
252
252
|
async def render(
|
|
253
253
|
transform_name: str = typer.Argument(default="", help="Name of the Python transformation", show_default=False),
|
|
254
|
-
variables:
|
|
254
|
+
variables: list[str] | None = typer.Argument(
|
|
255
255
|
None, help="Variables to pass along with the query. Format key=value key=value."
|
|
256
256
|
),
|
|
257
257
|
branch: str = typer.Option(None, help="Branch on which to render the transform."),
|
|
@@ -301,7 +301,7 @@ async def render(
|
|
|
301
301
|
@catch_exception(console=console)
|
|
302
302
|
def transform(
|
|
303
303
|
transform_name: str = typer.Argument(default="", help="Name of the Python transformation", show_default=False),
|
|
304
|
-
variables:
|
|
304
|
+
variables: list[str] | None = typer.Argument(
|
|
305
305
|
None, help="Variables to pass along with the query. Format key=value key=value."
|
|
306
306
|
),
|
|
307
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
|
|
@@ -115,7 +114,7 @@ async def export_schema(
|
|
|
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."),
|
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
|
|
@@ -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
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
from pathlib import Path
|
|
5
|
-
from typing import Optional
|
|
6
4
|
|
|
7
5
|
import typer
|
|
8
6
|
import yaml
|
|
9
|
-
from copier import run_copy
|
|
10
7
|
from pydantic import ValidationError
|
|
11
8
|
from rich.console import Console
|
|
12
9
|
from rich.table import Table
|
|
@@ -109,7 +106,7 @@ async def add(
|
|
|
109
106
|
name: str,
|
|
110
107
|
location: str,
|
|
111
108
|
description: str = "",
|
|
112
|
-
username:
|
|
109
|
+
username: str | None = None,
|
|
113
110
|
password: str = "",
|
|
114
111
|
ref: str = "",
|
|
115
112
|
read_only: bool = False,
|
|
@@ -155,7 +152,7 @@ async def add(
|
|
|
155
152
|
|
|
156
153
|
@app.command()
|
|
157
154
|
async def list(
|
|
158
|
-
branch:
|
|
155
|
+
branch: str | None = typer.Option(None, help="Branch on which to list repositories."),
|
|
159
156
|
debug: bool = False,
|
|
160
157
|
_: str = CONFIG_PARAM,
|
|
161
158
|
) -> None:
|
|
@@ -208,49 +205,9 @@ async def list(
|
|
|
208
205
|
|
|
209
206
|
|
|
210
207
|
@app.command()
|
|
211
|
-
async def init(
|
|
212
|
-
directory: Path = typer.Argument(help="Directory path for the new project."),
|
|
213
|
-
template: str = typer.Option(
|
|
214
|
-
default="https://github.com/opsmill/infrahub-template.git",
|
|
215
|
-
help="Template to use for the new repository. Can be a local path or a git repository URL.",
|
|
216
|
-
),
|
|
217
|
-
data: Optional[Path] = typer.Option(default=None, help="Path to YAML file containing answers to CLI prompt."),
|
|
218
|
-
vcs_ref: Optional[str] = typer.Option(
|
|
219
|
-
default="HEAD",
|
|
220
|
-
help="VCS reference to use for the template. Defaults to HEAD.",
|
|
221
|
-
),
|
|
222
|
-
trust: Optional[bool] = typer.Option(
|
|
223
|
-
default=False,
|
|
224
|
-
help="Trust the template repository. If set, the template will be cloned without verification.",
|
|
225
|
-
),
|
|
226
|
-
_: str = CONFIG_PARAM,
|
|
227
|
-
) -> None:
|
|
208
|
+
async def init() -> None:
|
|
228
209
|
"""Initialize a new Infrahub repository."""
|
|
229
210
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
with Path.open(data, encoding="utf-8") as file:
|
|
234
|
-
config_data = yaml.safe_load(file)
|
|
235
|
-
typer.echo(f"Loaded config: {config_data}")
|
|
236
|
-
except Exception as exc:
|
|
237
|
-
typer.echo(f"Error loading YAML file: {exc}", err=True)
|
|
238
|
-
raise typer.Exit(code=1)
|
|
239
|
-
|
|
240
|
-
# Allow template to be a local path or a URL
|
|
241
|
-
template_source = template or ""
|
|
242
|
-
if template and Path(template).exists():
|
|
243
|
-
template_source = str(Path(template).resolve())
|
|
244
|
-
|
|
245
|
-
try:
|
|
246
|
-
await asyncio.to_thread(
|
|
247
|
-
run_copy,
|
|
248
|
-
template_source,
|
|
249
|
-
str(directory),
|
|
250
|
-
data=config_data,
|
|
251
|
-
vcs_ref=vcs_ref,
|
|
252
|
-
unsafe=trust,
|
|
253
|
-
)
|
|
254
|
-
except Exception as e:
|
|
255
|
-
typer.echo(f"Error running copier: {e}", err=True)
|
|
256
|
-
raise typer.Exit(code=1)
|
|
211
|
+
console.print("The copier tool is not included in the Infrahub SDK CLI due to license restrictions,")
|
|
212
|
+
console.print("please run the following command to create a new Infrahub repository project:\n")
|
|
213
|
+
console.print("uv tool run --from 'copier' copier copy https://github.com/opsmill/infrahub-template <project-name>")
|
infrahub_sdk/ctl/task.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Optional
|
|
4
|
-
|
|
5
3
|
import typer
|
|
6
4
|
from rich.console import Console
|
|
7
5
|
from rich.table import Table
|
|
@@ -75,8 +73,8 @@ async def list_tasks(
|
|
|
75
73
|
state: list[str] = typer.Option(
|
|
76
74
|
None, "--state", "-s", help="Filter by task state. Can be provided multiple times."
|
|
77
75
|
),
|
|
78
|
-
limit:
|
|
79
|
-
offset:
|
|
76
|
+
limit: int | None = typer.Option(None, help="Maximum number of tasks to retrieve."),
|
|
77
|
+
offset: int | None = typer.Option(None, help="Offset for pagination."),
|
|
80
78
|
include_related_nodes: bool = typer.Option(False, help="Include related nodes in the output."),
|
|
81
79
|
include_logs: bool = typer.Option(False, help="Include task logs in the output."),
|
|
82
80
|
json_output: bool = typer.Option(False, "--json", help="Output the result as JSON."),
|
infrahub_sdk/ctl/utils.py
CHANGED
|
@@ -6,7 +6,7 @@ import traceback
|
|
|
6
6
|
from collections.abc import Callable, Coroutine
|
|
7
7
|
from functools import wraps
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import TYPE_CHECKING, Any, NoReturn,
|
|
9
|
+
from typing import TYPE_CHECKING, Any, NoReturn, TypeVar
|
|
10
10
|
|
|
11
11
|
import typer
|
|
12
12
|
from click.exceptions import Exit
|
|
@@ -149,7 +149,7 @@ def print_graphql_errors(console: Console, errors: list) -> None:
|
|
|
149
149
|
console.print(f"[red]{escape(str(error))}")
|
|
150
150
|
|
|
151
151
|
|
|
152
|
-
def parse_cli_vars(variables:
|
|
152
|
+
def parse_cli_vars(variables: list[str] | None) -> dict[str, str]:
|
|
153
153
|
if not variables:
|
|
154
154
|
return {}
|
|
155
155
|
|
infrahub_sdk/ctl/validate.py
CHANGED
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Optional
|
|
6
5
|
|
|
7
6
|
import typer
|
|
8
7
|
import ujson
|
|
@@ -58,7 +57,7 @@ async def validate_schema(schema: Path, _: str = CONFIG_PARAM) -> None:
|
|
|
58
57
|
@catch_exception(console=console)
|
|
59
58
|
def validate_graphql(
|
|
60
59
|
query: str,
|
|
61
|
-
variables:
|
|
60
|
+
variables: list[str] | None = typer.Argument(
|
|
62
61
|
None, help="Variables to pass along with the query. Format key=value key=value."
|
|
63
62
|
),
|
|
64
63
|
debug: bool = typer.Option(False, help="Display more troubleshooting information."),
|
infrahub_sdk/diff.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
)
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
6
5
|
|
|
7
6
|
from typing_extensions import NotRequired, TypedDict
|
|
8
7
|
|
|
8
|
+
from infrahub_sdk.graphql.query import Query
|
|
9
|
+
|
|
9
10
|
|
|
10
11
|
class NodeDiff(TypedDict):
|
|
11
12
|
branch: str
|
|
@@ -35,6 +36,19 @@ class NodeDiffPeer(TypedDict):
|
|
|
35
36
|
summary: NodeDiffSummary
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
class DiffTreeData(TypedDict):
|
|
40
|
+
num_added: int
|
|
41
|
+
num_updated: int
|
|
42
|
+
num_removed: int
|
|
43
|
+
num_conflicts: int
|
|
44
|
+
to_time: str
|
|
45
|
+
from_time: str
|
|
46
|
+
base_branch: str
|
|
47
|
+
diff_branch: str
|
|
48
|
+
name: NotRequired[str | None]
|
|
49
|
+
nodes: list[NodeDiff]
|
|
50
|
+
|
|
51
|
+
|
|
38
52
|
def get_diff_summary_query() -> str:
|
|
39
53
|
return """
|
|
40
54
|
query GetDiffTree($branch_name: String!, $name: String, $from_time: DateTime, $to_time: DateTime) {
|
|
@@ -125,3 +139,66 @@ def diff_tree_node_to_node_diff(node_dict: dict[str, Any], branch_name: str) ->
|
|
|
125
139
|
display_label=str(node_dict.get("label")),
|
|
126
140
|
elements=element_diffs,
|
|
127
141
|
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_diff_tree_query() -> Query:
|
|
145
|
+
node_structure = {
|
|
146
|
+
"uuid": None,
|
|
147
|
+
"kind": None,
|
|
148
|
+
"status": None,
|
|
149
|
+
"label": None,
|
|
150
|
+
"num_added": None,
|
|
151
|
+
"num_updated": None,
|
|
152
|
+
"num_removed": None,
|
|
153
|
+
"attributes": {
|
|
154
|
+
"name": None,
|
|
155
|
+
"status": None,
|
|
156
|
+
"num_added": None,
|
|
157
|
+
"num_updated": None,
|
|
158
|
+
"num_removed": None,
|
|
159
|
+
},
|
|
160
|
+
"relationships": {
|
|
161
|
+
"name": None,
|
|
162
|
+
"status": None,
|
|
163
|
+
"cardinality": None,
|
|
164
|
+
"num_added": None,
|
|
165
|
+
"num_updated": None,
|
|
166
|
+
"num_removed": None,
|
|
167
|
+
"elements": {
|
|
168
|
+
"status": None,
|
|
169
|
+
"num_added": None,
|
|
170
|
+
"num_updated": None,
|
|
171
|
+
"num_removed": None,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return Query(
|
|
177
|
+
name="GetDiffTree",
|
|
178
|
+
query={
|
|
179
|
+
"DiffTree": {
|
|
180
|
+
"@filters": {
|
|
181
|
+
"branch": "$branch_name",
|
|
182
|
+
"name": "$name",
|
|
183
|
+
"from_time": "$from_time",
|
|
184
|
+
"to_time": "$to_time",
|
|
185
|
+
},
|
|
186
|
+
"name": None,
|
|
187
|
+
"to_time": None,
|
|
188
|
+
"from_time": None,
|
|
189
|
+
"base_branch": None,
|
|
190
|
+
"diff_branch": None,
|
|
191
|
+
"num_added": None,
|
|
192
|
+
"num_updated": None,
|
|
193
|
+
"num_removed": None,
|
|
194
|
+
"num_conflicts": None,
|
|
195
|
+
"nodes": node_structure,
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
variables={
|
|
199
|
+
"branch_name": str,
|
|
200
|
+
"name": str | None,
|
|
201
|
+
"from_time": datetime | None,
|
|
202
|
+
"to_time": datetime | None,
|
|
203
|
+
},
|
|
204
|
+
)
|
|
@@ -1 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
VARIABLE_TYPE_MAPPING = (
|
|
4
|
+
(str, "String!"),
|
|
5
|
+
(str | None, "String"),
|
|
6
|
+
(int, "Int!"),
|
|
7
|
+
(int | None, "Int"),
|
|
8
|
+
(float, "Float!"),
|
|
9
|
+
(float | None, "Float"),
|
|
10
|
+
(bool, "Boolean!"),
|
|
11
|
+
(bool | None, "Boolean"),
|
|
12
|
+
(datetime, "DateTime!"),
|
|
13
|
+
(datetime | None, "DateTime"),
|
|
14
|
+
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
from datetime import datetime
|
|
4
5
|
from enum import Enum
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
@@ -66,7 +67,10 @@ def convert_to_graphql_as_string(value: Any, convert_enum: bool = False) -> str:
|
|
|
66
67
|
return str(value)
|
|
67
68
|
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
GRAPHQL_VARIABLE_TYPES = type[str | int | float | bool | datetime | None]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def render_variables_to_string(data: dict[str, GRAPHQL_VARIABLE_TYPES]) -> str:
|
|
70
74
|
"""Render a dict into a variable string that will be used in a GraphQL Query.
|
|
71
75
|
|
|
72
76
|
The $ sign will be automatically added to the name of the query.
|
infrahub_sdk/node/attribute.py
CHANGED
|
@@ -53,7 +53,6 @@ class Attribute:
|
|
|
53
53
|
self.is_inherited: bool | None = data.get("is_inherited")
|
|
54
54
|
self.updated_at: str | None = data.get("updated_at")
|
|
55
55
|
|
|
56
|
-
self.is_visible: bool | None = data.get("is_visible")
|
|
57
56
|
self.is_protected: bool | None = data.get("is_protected")
|
|
58
57
|
|
|
59
58
|
self.source: NodeProperty | None = None
|
infrahub_sdk/node/constants.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ipaddress
|
|
2
2
|
import re
|
|
3
3
|
|
|
4
|
-
PROPERTIES_FLAG = ["
|
|
4
|
+
PROPERTIES_FLAG = ["is_protected", "updated_at"]
|
|
5
5
|
PROPERTIES_OBJECT = ["source", "owner"]
|
|
6
6
|
SAFE_VALUE = re.compile(r"(^[\. /:a-zA-Z0-9_-]+$)|(^$)")
|
|
7
7
|
|
|
@@ -17,4 +17,6 @@ ARTIFACT_DEFINITION_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE = (
|
|
|
17
17
|
"calling generate is only supported for CoreArtifactDefinition nodes"
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
+
HIERARCHY_FETCH_FEATURE_NOT_SUPPORTED_MESSAGE = "Hierarchical fields are not supported for this node."
|
|
21
|
+
|
|
20
22
|
HFID_STR_SEPARATOR = "__"
|