ocmo-cli 0.8.19__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.
- LICENSE +201 -0
- NOTICE +16 -0
- ocmo_cli/__init__.py +3 -0
- ocmo_cli/__main__.py +4 -0
- ocmo_cli/_address.py +182 -0
- ocmo_cli/_audit_item.py +33 -0
- ocmo_cli/_auth_resolve.py +66 -0
- ocmo_cli/_body_payload.py +203 -0
- ocmo_cli/_click_groups.py +227 -0
- ocmo_cli/_client.py +351 -0
- ocmo_cli/_client_commands.py +35 -0
- ocmo_cli/_command_registry.py +29 -0
- ocmo_cli/_commands_map.py +65 -0
- ocmo_cli/_config.py +183 -0
- ocmo_cli/_config_content.py +43 -0
- ocmo_cli/_delete_item_output.py +129 -0
- ocmo_cli/_diff_input.py +144 -0
- ocmo_cli/_dry_run.py +292 -0
- ocmo_cli/_errors.py +151 -0
- ocmo_cli/_exit.py +13 -0
- ocmo_cli/_export_metadata.py +47 -0
- ocmo_cli/_generated_registry.py +45 -0
- ocmo_cli/_get_cast_output.py +52 -0
- ocmo_cli/_get_item_list.py +23 -0
- ocmo_cli/_globalpermission_output.py +98 -0
- ocmo_cli/_import_metadata.py +140 -0
- ocmo_cli/_import_plan.py +221 -0
- ocmo_cli/_item_output.py +223 -0
- ocmo_cli/_lazy_mixin.py +29 -0
- ocmo_cli/_ls_wide.py +286 -0
- ocmo_cli/_mutating.py +39 -0
- ocmo_cli/_notice.py +17 -0
- ocmo_cli/_options.py +77 -0
- ocmo_cli/_output.py +424 -0
- ocmo_cli/_output_manifest.py +332 -0
- ocmo_cli/_resolve_draft.py +50 -0
- ocmo_cli/_resolve_options.py +143 -0
- ocmo_cli/_resolve_output.py +228 -0
- ocmo_cli/_resolve_parameters_output.py +72 -0
- ocmo_cli/_resolve_series.py +89 -0
- ocmo_cli/_resolve_series_chart.py +409 -0
- ocmo_cli/_resolve_write.py +185 -0
- ocmo_cli/_resolver_output.py +34 -0
- ocmo_cli/_resource_aliases.py +22 -0
- ocmo_cli/_sdk_dispatch.py +310 -0
- ocmo_cli/_typing.py +27 -0
- ocmo_cli/_version_output.py +39 -0
- ocmo_cli/_whoami_output.py +43 -0
- ocmo_cli/commands/__init__.py +1 -0
- ocmo_cli/commands/api.py +125 -0
- ocmo_cli/commands/api_health.py +12 -0
- ocmo_cli/commands/apply.py +153 -0
- ocmo_cli/commands/auth.py +786 -0
- ocmo_cli/commands/can_i.py +51 -0
- ocmo_cli/commands/completion.py +59 -0
- ocmo_cli/commands/config_cmd.py +235 -0
- ocmo_cli/commands/describe.py +109 -0
- ocmo_cli/commands/diff.py +86 -0
- ocmo_cli/commands/edit.py +200 -0
- ocmo_cli/commands/export.py +242 -0
- ocmo_cli/commands/generated.py +907 -0
- ocmo_cli/commands/gp_move.py +108 -0
- ocmo_cli/commands/import_.py +595 -0
- ocmo_cli/commands/item_relocate.py +187 -0
- ocmo_cli/commands/ls.py +481 -0
- ocmo_cli/commands/resolve.py +658 -0
- ocmo_cli/commands/resolve_draft.py +102 -0
- ocmo_cli/commands/resolve_parameters.py +65 -0
- ocmo_cli/commands/resolve_series.py +109 -0
- ocmo_cli/commands/schema.py +112 -0
- ocmo_cli/commands/timeline.py +135 -0
- ocmo_cli/commands/version_cmd.py +102 -0
- ocmo_cli/commands/whoami.py +12 -0
- ocmo_cli/main.py +241 -0
- ocmo_cli/output_formats.yaml +295 -0
- ocmo_cli-0.8.19.dist-info/METADATA +169 -0
- ocmo_cli-0.8.19.dist-info/RECORD +81 -0
- ocmo_cli-0.8.19.dist-info/WHEEL +4 -0
- ocmo_cli-0.8.19.dist-info/entry_points.txt +2 -0
- ocmo_cli-0.8.19.dist-info/licenses/LICENSE +201 -0
- ocmo_cli-0.8.19.dist-info/licenses/NOTICE +16 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Normalize config document bodies before upload."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ._output import yaml_dumps
|
|
9
|
+
|
|
10
|
+
_CONFIG_METADATA_KEY = "_ocmo"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def prepare_config_apply_content(content: str, *, source_name: str) -> str:
|
|
14
|
+
"""Convert JSON config sources to YAML syntax for storage."""
|
|
15
|
+
if not _is_json_config_source(source_name, content):
|
|
16
|
+
return content
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
doc = json.loads(content)
|
|
20
|
+
except json.JSONDecodeError:
|
|
21
|
+
return content
|
|
22
|
+
|
|
23
|
+
if isinstance(doc, dict):
|
|
24
|
+
_inject_json_cast_metadata(doc)
|
|
25
|
+
|
|
26
|
+
return yaml_dumps(doc)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _is_json_config_source(source_name: str, content: str) -> bool:
|
|
30
|
+
if source_name != "<stdin>":
|
|
31
|
+
return source_name.lower().endswith(".json")
|
|
32
|
+
stripped = content.lstrip()
|
|
33
|
+
return stripped.startswith("{") or stripped.startswith("[")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _inject_json_cast_metadata(doc: dict[str, Any]) -> None:
|
|
37
|
+
"""Preserve JSON resolve semantics when the on-disk source was JSON."""
|
|
38
|
+
existing = doc.get(_CONFIG_METADATA_KEY)
|
|
39
|
+
cast_block = {"format": "json"}
|
|
40
|
+
if existing is None:
|
|
41
|
+
doc[_CONFIG_METADATA_KEY] = {"cast": cast_block}
|
|
42
|
+
elif isinstance(existing, dict) and "cast" not in existing:
|
|
43
|
+
doc[_CONFIG_METADATA_KEY] = {**existing, "cast": cast_block}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Output formatting for ``ocmo delete item``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ._output import emit, emit_table
|
|
9
|
+
from ._output_manifest import get_command_spec, resolve_effective_format
|
|
10
|
+
|
|
11
|
+
_DELETE_PREVIEW_RE = re.compile(r"^[^:]+::\s*(\w+)::\s*(.+)$")
|
|
12
|
+
|
|
13
|
+
_TYPE_BY_LABEL = {
|
|
14
|
+
"folder": "folder",
|
|
15
|
+
"config": "config",
|
|
16
|
+
"template": "template",
|
|
17
|
+
"secret": "secret",
|
|
18
|
+
"resolver": "resolver",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _split_path_version(raw_path: str) -> tuple[str, int | None]:
|
|
23
|
+
if "@" not in raw_path:
|
|
24
|
+
return raw_path, None
|
|
25
|
+
path, suffix = raw_path.rsplit("@", 1)
|
|
26
|
+
if suffix.isdigit():
|
|
27
|
+
return path, int(suffix)
|
|
28
|
+
return raw_path, None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_delete_preview_line(line: str) -> dict[str, Any]:
|
|
32
|
+
"""Parse API delete lines like ``my-ns:: Config:: app/foo``."""
|
|
33
|
+
trimmed = line.strip()
|
|
34
|
+
match = _DELETE_PREVIEW_RE.match(trimmed)
|
|
35
|
+
if not match:
|
|
36
|
+
path = trimmed
|
|
37
|
+
node_type = "config"
|
|
38
|
+
else:
|
|
39
|
+
node_type = _TYPE_BY_LABEL.get(match[1].lower(), "config")
|
|
40
|
+
path = match[2]
|
|
41
|
+
|
|
42
|
+
path, version_number = _split_path_version(path)
|
|
43
|
+
name = path.rsplit("/", 1)[-1] if path else path
|
|
44
|
+
entry: dict[str, Any] = {"path": path, "name": name, "node_type": node_type}
|
|
45
|
+
if version_number is not None:
|
|
46
|
+
entry["version"] = version_number
|
|
47
|
+
return entry
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_delete_preview_lines(lines: list[str]) -> list[dict[str, Any]]:
|
|
51
|
+
return [parse_delete_preview_line(line) for line in lines]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _normalize_path(path: str) -> str:
|
|
55
|
+
return path.strip("/")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_folder_delete(entries: list[dict[str, Any]], target_path: str | None) -> bool:
|
|
59
|
+
if not target_path:
|
|
60
|
+
return False
|
|
61
|
+
norm = _normalize_path(target_path)
|
|
62
|
+
return any(
|
|
63
|
+
entry.get("node_type") == "folder" and _normalize_path(entry.get("path") or "") == norm for entry in entries
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def delete_item_table_rows(entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
68
|
+
rows: list[dict[str, Any]] = []
|
|
69
|
+
for entry in entries:
|
|
70
|
+
row: dict[str, Any] = {
|
|
71
|
+
"type": entry.get("node_type") or "",
|
|
72
|
+
"path": entry.get("path") or "",
|
|
73
|
+
}
|
|
74
|
+
if entry.get("version") is not None:
|
|
75
|
+
row["version"] = entry["version"]
|
|
76
|
+
rows.append(row)
|
|
77
|
+
rows.sort(key=lambda row: (row["path"], row.get("version") or 0))
|
|
78
|
+
return rows
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def delete_item_table_columns(rows: list[dict[str, Any]]) -> list[str]:
|
|
82
|
+
if any(row.get("version") is not None for row in rows):
|
|
83
|
+
return ["type", "version", "path"]
|
|
84
|
+
return ["type", "path"]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def emit_delete_item_output(
|
|
88
|
+
result: Any,
|
|
89
|
+
*,
|
|
90
|
+
target_path: str | None,
|
|
91
|
+
version: str | None,
|
|
92
|
+
output_fmt: str | None,
|
|
93
|
+
ctx_fmt: str | None,
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Render delete results as ``ocmo tree`` (folders) or ``ocmo ls`` (other items)."""
|
|
96
|
+
from .commands.ls import _build_tree_hierarchy, _render_plain_tree
|
|
97
|
+
|
|
98
|
+
command_key = "delete item"
|
|
99
|
+
spec = get_command_spec(command_key)
|
|
100
|
+
effective_fmt = resolve_effective_format(output_fmt, ctx_fmt, spec)
|
|
101
|
+
|
|
102
|
+
delete_lines = list(getattr(result, "delete", None) or [])
|
|
103
|
+
entries = parse_delete_preview_lines(delete_lines)
|
|
104
|
+
|
|
105
|
+
use_tree = effective_fmt == "table" and version is None and is_folder_delete(entries, target_path)
|
|
106
|
+
|
|
107
|
+
if use_tree:
|
|
108
|
+
tree_rows = _build_tree_hierarchy(entries)
|
|
109
|
+
_render_plain_tree(tree_rows, prefix="", connector="", use_emoji=False)
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
rows = delete_item_table_rows(entries)
|
|
113
|
+
|
|
114
|
+
if effective_fmt == "table":
|
|
115
|
+
emit_table(rows, delete_item_table_columns(rows))
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
if effective_fmt == "name":
|
|
119
|
+
for entry in entries:
|
|
120
|
+
print(entry.get("path") or "")
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
if effective_fmt == "path":
|
|
124
|
+
for entry in entries:
|
|
125
|
+
print(entry.get("path") or "")
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
payload = {"delete": entries}
|
|
129
|
+
emit(payload, effective_fmt)
|
ocmo_cli/_diff_input.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Parse ``ocmo diff`` addresses and render unified diff output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import difflib
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ocmo._generated.types import UNSET
|
|
10
|
+
|
|
11
|
+
from ._address import AddressError, _validate_path, parse_address
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class DiffSpec:
|
|
16
|
+
path: str
|
|
17
|
+
from_ref: str | None = None
|
|
18
|
+
to_ref: str | None = None
|
|
19
|
+
to_path: str | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def diff_sdk_kwargs(spec: DiffSpec, *, reveal: bool = False) -> tuple[str, dict[str, Any]]:
|
|
23
|
+
"""Return ``(path, kwargs)`` for ``NamespaceView.diff_item``."""
|
|
24
|
+
kwargs: dict[str, Any] = {}
|
|
25
|
+
if spec.from_ref is not None:
|
|
26
|
+
kwargs["from_"] = spec.from_ref
|
|
27
|
+
if spec.to_ref is not None:
|
|
28
|
+
kwargs["to"] = spec.to_ref
|
|
29
|
+
if spec.to_path is not None:
|
|
30
|
+
kwargs["to_path"] = spec.to_path
|
|
31
|
+
if reveal:
|
|
32
|
+
kwargs["reveal"] = True
|
|
33
|
+
return spec.path, kwargs
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_diff_spec(
|
|
37
|
+
addresses: tuple[str, ...],
|
|
38
|
+
*,
|
|
39
|
+
version_flag: str | None = None,
|
|
40
|
+
from_version: str | None = None,
|
|
41
|
+
to_version: str | None = None,
|
|
42
|
+
) -> DiffSpec:
|
|
43
|
+
"""Resolve diff operands from positional addresses and version flags."""
|
|
44
|
+
if not addresses:
|
|
45
|
+
raise AddressError("At least one address is required.")
|
|
46
|
+
if len(addresses) > 2:
|
|
47
|
+
raise AddressError("Expected one or two addresses.")
|
|
48
|
+
|
|
49
|
+
if len(addresses) == 2:
|
|
50
|
+
if version_flag:
|
|
51
|
+
raise AddressError(
|
|
52
|
+
"Cannot use --version with two addresses; " "use @suffix or --from-version / --to-version."
|
|
53
|
+
)
|
|
54
|
+
if ".." in addresses[0] or ".." in addresses[1]:
|
|
55
|
+
raise AddressError("Version range (..) is only supported with a single address.")
|
|
56
|
+
from_path, from_ref = parse_address(addresses[0])
|
|
57
|
+
to_path, to_ref = parse_address(addresses[1])
|
|
58
|
+
return DiffSpec(
|
|
59
|
+
path=from_path,
|
|
60
|
+
from_ref=from_version or from_ref,
|
|
61
|
+
to_ref=to_version or to_ref,
|
|
62
|
+
to_path=to_path if to_path != from_path else None,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
raw = addresses[0]
|
|
66
|
+
if "@" in raw:
|
|
67
|
+
path, version_part = raw.split("@", 1)
|
|
68
|
+
if ".." in version_part:
|
|
69
|
+
if version_flag:
|
|
70
|
+
raise AddressError(
|
|
71
|
+
f"Conflicting version specifications: address suffix '@{version_part}' "
|
|
72
|
+
f"and --version '{version_flag}'. Provide only one."
|
|
73
|
+
)
|
|
74
|
+
_validate_path(path)
|
|
75
|
+
range_from, range_to = _parse_version_range(version_part)
|
|
76
|
+
return DiffSpec(
|
|
77
|
+
path=path,
|
|
78
|
+
from_ref=from_version or range_from,
|
|
79
|
+
to_ref=to_version or range_to,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
path, version = parse_address(raw, version_flag=version_flag)
|
|
83
|
+
return DiffSpec(
|
|
84
|
+
path=path,
|
|
85
|
+
from_ref=from_version,
|
|
86
|
+
to_ref=to_version or version,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _parse_version_range(version_part: str) -> tuple[str, str]:
|
|
91
|
+
left, sep, right = version_part.partition("..")
|
|
92
|
+
if not sep or not left or not right:
|
|
93
|
+
raise AddressError(f"Invalid version range '@{version_part}'; expected path@FROM..TO.")
|
|
94
|
+
return left, right
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def diff_side_label(side: Any) -> str:
|
|
98
|
+
requested = getattr(side, "requested", None) or ""
|
|
99
|
+
path = getattr(side, "path", "")
|
|
100
|
+
if requested:
|
|
101
|
+
return f"{path}@{requested}"
|
|
102
|
+
version = getattr(side, "version", None)
|
|
103
|
+
if version is not None:
|
|
104
|
+
return f"{path}@v{version}"
|
|
105
|
+
return path
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _side_text(side: Any) -> str:
|
|
109
|
+
data = getattr(side, "data", UNSET)
|
|
110
|
+
if data is UNSET or data is None:
|
|
111
|
+
return ""
|
|
112
|
+
return str(data)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def render_unified_diff(from_text: str, to_text: str, *, from_label: str, to_label: str) -> str:
|
|
116
|
+
if from_text == to_text:
|
|
117
|
+
return "No differences\n"
|
|
118
|
+
lines = difflib.unified_diff(
|
|
119
|
+
from_text.splitlines(keepends=True),
|
|
120
|
+
to_text.splitlines(keepends=True),
|
|
121
|
+
fromfile=from_label,
|
|
122
|
+
tofile=to_label,
|
|
123
|
+
)
|
|
124
|
+
rendered = "".join(lines)
|
|
125
|
+
return rendered or "No differences\n"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def render_diff_response(result: Any) -> str:
|
|
129
|
+
"""Build unified diff text from an SDK ``DiffResponseSchema``."""
|
|
130
|
+
if getattr(result, "decryption_required", False):
|
|
131
|
+
return ""
|
|
132
|
+
|
|
133
|
+
from_text = _side_text(result.from_side)
|
|
134
|
+
to_text = _side_text(result.to_side)
|
|
135
|
+
|
|
136
|
+
if getattr(result, "identical", False) is True:
|
|
137
|
+
return "No differences\n"
|
|
138
|
+
|
|
139
|
+
return render_unified_diff(
|
|
140
|
+
from_text,
|
|
141
|
+
to_text,
|
|
142
|
+
from_label=diff_side_label(result.from_side),
|
|
143
|
+
to_label=diff_side_label(result.to_side),
|
|
144
|
+
)
|
ocmo_cli/_dry_run.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""User-facing dry-run plan messages for CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def quote(value: str) -> str:
|
|
9
|
+
return f"'{value}'"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def namespace_clause(namespace: str | None, *, client_scope: bool) -> str:
|
|
13
|
+
if client_scope or not namespace:
|
|
14
|
+
return ""
|
|
15
|
+
return f" in namespace {quote(namespace)}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def versioned_path(path: str | None, version: str | None) -> str:
|
|
19
|
+
if not path:
|
|
20
|
+
return "?"
|
|
21
|
+
if version:
|
|
22
|
+
return f"{quote(path)}@{version}"
|
|
23
|
+
return quote(path)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def file_clause(file_path: str | None) -> str:
|
|
27
|
+
if not file_path:
|
|
28
|
+
return ""
|
|
29
|
+
label = "<stdin>" if file_path == "-" else file_path
|
|
30
|
+
return f" from {quote(label)}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def body_dict(kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
34
|
+
body = kwargs.get("body")
|
|
35
|
+
return body if isinstance(body, dict) else {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def primary_target(
|
|
39
|
+
op_id: str,
|
|
40
|
+
*,
|
|
41
|
+
path: str | None,
|
|
42
|
+
args: list[Any],
|
|
43
|
+
kwargs: dict[str, Any],
|
|
44
|
+
) -> str | None:
|
|
45
|
+
if path:
|
|
46
|
+
return path
|
|
47
|
+
if args:
|
|
48
|
+
return str(args[0])
|
|
49
|
+
for key in ("path", "name", "id", "namespace"):
|
|
50
|
+
value = kwargs.get(key)
|
|
51
|
+
if value not in (None, ""):
|
|
52
|
+
return str(value)
|
|
53
|
+
payload = body_dict(kwargs)
|
|
54
|
+
for key in ("name", "id", "path"):
|
|
55
|
+
value = payload.get(key)
|
|
56
|
+
if value not in (None, ""):
|
|
57
|
+
return str(value)
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def format_generated_dry_run(
|
|
62
|
+
*,
|
|
63
|
+
op_id: str,
|
|
64
|
+
action: str,
|
|
65
|
+
resource: str,
|
|
66
|
+
path: str | None,
|
|
67
|
+
version: str | None,
|
|
68
|
+
namespace: str | None,
|
|
69
|
+
args: list[Any],
|
|
70
|
+
kwargs: dict[str, Any],
|
|
71
|
+
client_scope: bool,
|
|
72
|
+
file_path: str | None = None,
|
|
73
|
+
gp_create_position: float | None = None,
|
|
74
|
+
cast_format: str | None = None,
|
|
75
|
+
) -> list[str]:
|
|
76
|
+
"""Return human-readable dry-run plan lines (without the ``[dry-run]`` prefix)."""
|
|
77
|
+
ns = namespace_clause(namespace, client_scope=client_scope)
|
|
78
|
+
target = primary_target(op_id, path=path, args=args, kwargs=kwargs)
|
|
79
|
+
payload = body_dict(kwargs)
|
|
80
|
+
lines: list[str] = []
|
|
81
|
+
|
|
82
|
+
if op_id == "create_namespace":
|
|
83
|
+
name = target or "?"
|
|
84
|
+
line = f"Would create namespace {quote(name)}."
|
|
85
|
+
description = payload.get("description")
|
|
86
|
+
if description:
|
|
87
|
+
line += f" Description: {description!r}."
|
|
88
|
+
lines.append(line)
|
|
89
|
+
return lines
|
|
90
|
+
|
|
91
|
+
if op_id == "update_namespace":
|
|
92
|
+
lines.append(f"Would update namespace {quote(target or '?')}.")
|
|
93
|
+
return lines
|
|
94
|
+
|
|
95
|
+
if op_id == "delete_namespace":
|
|
96
|
+
lines.append(f"Would delete namespace {quote(target or '?')}.")
|
|
97
|
+
return lines
|
|
98
|
+
|
|
99
|
+
if op_id == "create_global_permission":
|
|
100
|
+
rule_id = target or payload.get("id") or "?"
|
|
101
|
+
lines.append(f"Would create global permission rule {quote(str(rule_id))}{file_clause(file_path)}.")
|
|
102
|
+
if gp_create_position is not None:
|
|
103
|
+
lines.append(f"Would set rule position to {gp_create_position}.")
|
|
104
|
+
return lines
|
|
105
|
+
|
|
106
|
+
if op_id == "update_global_permission":
|
|
107
|
+
lines.append(f"Would update global permission rule {quote(target or '?')}{file_clause(file_path)}.")
|
|
108
|
+
return lines
|
|
109
|
+
|
|
110
|
+
if op_id == "delete_global_permission":
|
|
111
|
+
lines.append(f"Would delete global permission rule {quote(target or '?')}.")
|
|
112
|
+
return lines
|
|
113
|
+
|
|
114
|
+
if op_id == "move_global_permission":
|
|
115
|
+
position = payload.get("position", kwargs.get("position"))
|
|
116
|
+
pos_text = f" to position {position}" if position is not None else ""
|
|
117
|
+
lines.append(f"Would move global permission rule {quote(target or '?')}{pos_text}.")
|
|
118
|
+
return lines
|
|
119
|
+
|
|
120
|
+
if op_id == "move_item":
|
|
121
|
+
destination = payload.get("target_path") or kwargs.get("target_path")
|
|
122
|
+
dest = str(destination or "?")
|
|
123
|
+
lines.append(f"Would move item {quote(target or '?')} to {quote(dest)}{ns}.")
|
|
124
|
+
lines.append(f"After move, item will be available at {quote(dest)}.")
|
|
125
|
+
return lines
|
|
126
|
+
|
|
127
|
+
if op_id == "copy_item":
|
|
128
|
+
destination = payload.get("target_path") or kwargs.get("target_path")
|
|
129
|
+
dest = str(destination or "?")
|
|
130
|
+
tag = kwargs.get("tag_to_copy")
|
|
131
|
+
tag_text = f" (tag {quote(str(tag))})" if tag else ""
|
|
132
|
+
lines.append(f"Would copy item {quote(target or '?')} to {quote(dest)}" f"{tag_text}{ns}.")
|
|
133
|
+
lines.append(f"After copy, item will be available at {quote(dest)}.")
|
|
134
|
+
return lines
|
|
135
|
+
|
|
136
|
+
if op_id == "set_tag":
|
|
137
|
+
if action == "untag":
|
|
138
|
+
tag = kwargs.get("tag")
|
|
139
|
+
body = kwargs.get("body")
|
|
140
|
+
if tag is None and isinstance(body, dict):
|
|
141
|
+
tag = body.get("tag")
|
|
142
|
+
lines.append(f"Would remove tag {quote(str(tag or '?'))} from item {quote(target or '?')}{ns}.")
|
|
143
|
+
return lines
|
|
144
|
+
tag = kwargs.get("tag")
|
|
145
|
+
body = kwargs.get("body")
|
|
146
|
+
if tag is None and isinstance(body, dict):
|
|
147
|
+
tag = body.get("tag")
|
|
148
|
+
lines.append(f"Would tag item {quote(target or '?')} as {quote(str(tag or '?'))}{ns}.")
|
|
149
|
+
return lines
|
|
150
|
+
|
|
151
|
+
if op_id == "rotate_resolver_token":
|
|
152
|
+
token_number = kwargs.get("token_number")
|
|
153
|
+
token_text = f" (token #{token_number})" if token_number is not None else ""
|
|
154
|
+
lines.append(f"Would rotate resolver token for {quote(target or '?')}{token_text}{ns}.")
|
|
155
|
+
return lines
|
|
156
|
+
|
|
157
|
+
if op_id == "create_lock":
|
|
158
|
+
reason = payload.get("reason") or kwargs.get("reason")
|
|
159
|
+
reason_text = f" (reason: {reason!r})" if reason else ""
|
|
160
|
+
expires = payload.get("expires_at") or kwargs.get("expires_at")
|
|
161
|
+
expires_text = f", expires {expires}" if expires else ""
|
|
162
|
+
lines.append(f"Would create lock on {quote(target or '?')}{reason_text}{expires_text}{ns}.")
|
|
163
|
+
return lines
|
|
164
|
+
|
|
165
|
+
if op_id == "replace_lock":
|
|
166
|
+
reason = payload.get("reason") or kwargs.get("reason")
|
|
167
|
+
reason_text = f" (reason: {reason!r})" if reason else ""
|
|
168
|
+
expires = payload.get("expires_at") or kwargs.get("expires_at")
|
|
169
|
+
expires_text = f", expires {expires}" if expires else ""
|
|
170
|
+
lines.append(f"Would update lock on {quote(target or '?')}{reason_text}{expires_text}{ns}.")
|
|
171
|
+
return lines
|
|
172
|
+
|
|
173
|
+
if op_id == "delete_lock":
|
|
174
|
+
lines.append(f"Would remove lock on {quote(target or '?')}{ns}.")
|
|
175
|
+
return lines
|
|
176
|
+
|
|
177
|
+
if op_id == "search_root" and resource == "item":
|
|
178
|
+
types = kwargs.get("types") or []
|
|
179
|
+
limit = kwargs.get("limit")
|
|
180
|
+
type_text = f" (types: {', '.join(types)})" if types else ""
|
|
181
|
+
limit_text = f", limit {limit}" if limit is not None else ""
|
|
182
|
+
lines.append(f"Would list items{type_text}{limit_text}{ns}.")
|
|
183
|
+
return lines
|
|
184
|
+
|
|
185
|
+
if op_id == "list_cast_formats" and resource == "cast":
|
|
186
|
+
if cast_format:
|
|
187
|
+
lines.append(f"Would show cast format schema for {quote(cast_format)}{ns}.")
|
|
188
|
+
else:
|
|
189
|
+
lines.append(f"Would list cast formats{ns}.")
|
|
190
|
+
return lines
|
|
191
|
+
|
|
192
|
+
if op_id == "propagate_config":
|
|
193
|
+
lines.append(f"Would propagate config {versioned_path(target, version)} to descendants{ns}.")
|
|
194
|
+
return lines
|
|
195
|
+
|
|
196
|
+
if op_id == "delete_item":
|
|
197
|
+
if version:
|
|
198
|
+
lines.append(
|
|
199
|
+
f"Would delete version {quote(version)} of item {quote(target or '?')}{ns} " f"(not the item itself)."
|
|
200
|
+
)
|
|
201
|
+
else:
|
|
202
|
+
lines.append(f"Would delete item {quote(target or '?')}{ns}.")
|
|
203
|
+
return lines
|
|
204
|
+
|
|
205
|
+
resource_label = _RESOURCE_LABELS.get(resource, resource)
|
|
206
|
+
if action == "create":
|
|
207
|
+
lines.append(
|
|
208
|
+
f"Would create {resource_label} {versioned_path(target, version)}" f"{file_clause(file_path)}{ns}."
|
|
209
|
+
)
|
|
210
|
+
return lines
|
|
211
|
+
|
|
212
|
+
if action == "update":
|
|
213
|
+
lines.append(
|
|
214
|
+
f"Would update {resource_label} {versioned_path(target, version)}" f"{file_clause(file_path)}{ns}."
|
|
215
|
+
)
|
|
216
|
+
return lines
|
|
217
|
+
|
|
218
|
+
if action == "delete":
|
|
219
|
+
lines.append(f"Would delete {resource_label} {versioned_path(target, version)}{ns}.")
|
|
220
|
+
return lines
|
|
221
|
+
|
|
222
|
+
lines.append(f"Would {action} {resource_label} {versioned_path(target, version)}{ns}.")
|
|
223
|
+
return lines
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def format_apply_dry_run(
|
|
227
|
+
*,
|
|
228
|
+
kind: str,
|
|
229
|
+
path: str,
|
|
230
|
+
source_name: str,
|
|
231
|
+
namespace: str | None,
|
|
232
|
+
) -> str:
|
|
233
|
+
ns = namespace_clause(namespace, client_scope=False)
|
|
234
|
+
return f"Would create or update {kind} {quote(path)} from {quote(source_name)}{ns}."
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def format_resolve_dry_run(
|
|
238
|
+
*,
|
|
239
|
+
path: str,
|
|
240
|
+
namespace: str,
|
|
241
|
+
cast: str | None = None,
|
|
242
|
+
parameters: dict[str, Any] | None = None,
|
|
243
|
+
) -> list[str]:
|
|
244
|
+
lines = [f"Would resolve {quote(path)} in namespace {quote(namespace)}."]
|
|
245
|
+
if cast:
|
|
246
|
+
lines.append(f"Cast output as {cast}.")
|
|
247
|
+
if parameters:
|
|
248
|
+
params = ", ".join(f"{key}={value!r}" for key, value in sorted(parameters.items()))
|
|
249
|
+
lines.append(f"Parameters: {params}.")
|
|
250
|
+
return lines
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def format_describe_dry_run(*, path: str, namespace: str | None, char_count: int) -> str:
|
|
254
|
+
ns = namespace_clause(namespace, client_scope=False)
|
|
255
|
+
return f"Would update description for {quote(path)}{ns} ({char_count} characters)."
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def format_edit_dry_run(*, resource: str, path: str, namespace: str | None) -> str:
|
|
259
|
+
ns = namespace_clause(namespace, client_scope=False)
|
|
260
|
+
label = _RESOURCE_LABELS.get(resource, resource)
|
|
261
|
+
return f"Would edit {label} {quote(path)} via $EDITOR{ns}."
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def format_export_dry_run(*, item_path: str, dest_file: str, namespace: str) -> str:
|
|
265
|
+
return f"Would export {quote(item_path)} from namespace {quote(namespace)} " f"to {quote(dest_file)}."
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def format_import_dry_run_header(count: int) -> str:
|
|
269
|
+
return f"Would import {count} item{'s' if count != 1 else ''}:"
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
_RESOURCE_LABELS: dict[str, str] = {
|
|
273
|
+
"namespace": "namespace",
|
|
274
|
+
"globalpermission": "global permission rule",
|
|
275
|
+
"config": "config",
|
|
276
|
+
"template": "template",
|
|
277
|
+
"secret": "secret",
|
|
278
|
+
"resolver": "resolver",
|
|
279
|
+
"item": "item",
|
|
280
|
+
"lock": "lock",
|
|
281
|
+
"token": "resolver token",
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def emit_dry_run_plan(lines: list[str] | str) -> None:
|
|
286
|
+
"""Print one or more dry-run plan lines to stderr."""
|
|
287
|
+
from ._output import status # deferred
|
|
288
|
+
|
|
289
|
+
if isinstance(lines, str):
|
|
290
|
+
lines = [lines]
|
|
291
|
+
for line in lines:
|
|
292
|
+
status(f"[dry-run] {line}")
|