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
ocmo_cli/_import_plan.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Import plan building and validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import fnmatch
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
from ._import_metadata import import_file_text, metadata_tree_path, read_file_metadata
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ocmo import NamespaceView
|
|
14
|
+
|
|
15
|
+
VALID_IMPORT_TYPES = frozenset({"config", "template", "secret", "resolver"})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def try_parse_config_document(content_bytes: bytes) -> tuple[dict[str, Any] | None, bool]:
|
|
19
|
+
"""Return ``(document, is_json)`` when *content_bytes* is a YAML/JSON object."""
|
|
20
|
+
text = content_bytes.decode("utf-8", errors="replace")
|
|
21
|
+
try:
|
|
22
|
+
doc = json.loads(text)
|
|
23
|
+
if isinstance(doc, dict):
|
|
24
|
+
return doc, True
|
|
25
|
+
except json.JSONDecodeError:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
import yaml # deferred
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
doc = yaml.safe_load(text)
|
|
32
|
+
except Exception:
|
|
33
|
+
return None, False
|
|
34
|
+
if isinstance(doc, dict):
|
|
35
|
+
return doc, False
|
|
36
|
+
return None, False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def classify_file_kind(content_bytes: bytes) -> str:
|
|
40
|
+
"""Classify a source file as ``config`` or ``template`` by parsing content."""
|
|
41
|
+
doc, _ = try_parse_config_document(content_bytes)
|
|
42
|
+
return "config" if doc is not None else "template"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_type_override(spec: str) -> tuple[str, str]:
|
|
46
|
+
"""Parse ``GLOB=TYPE`` from ``--type-override``."""
|
|
47
|
+
if "=" not in spec:
|
|
48
|
+
raise ValueError(f"expected GLOB=TYPE, got {spec!r}")
|
|
49
|
+
pattern, item_type = spec.split("=", 1)
|
|
50
|
+
pattern = pattern.strip()
|
|
51
|
+
item_type = item_type.strip().lower()
|
|
52
|
+
if not pattern:
|
|
53
|
+
raise ValueError(f"type override pattern must not be empty in {spec!r}")
|
|
54
|
+
_validate_import_type(item_type)
|
|
55
|
+
return pattern, item_type
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_type_map(path: Path) -> list[tuple[str, str]]:
|
|
59
|
+
"""Load glob → item-type mappings from a YAML file."""
|
|
60
|
+
import yaml # deferred
|
|
61
|
+
|
|
62
|
+
raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
63
|
+
if isinstance(raw, dict) and "types" in raw:
|
|
64
|
+
raw = raw["types"]
|
|
65
|
+
if not isinstance(raw, dict):
|
|
66
|
+
raise ValueError(f"type map {path} must be a YAML mapping")
|
|
67
|
+
|
|
68
|
+
mappings: list[tuple[str, str]] = []
|
|
69
|
+
for pattern, item_type in raw.items():
|
|
70
|
+
pattern = str(pattern).strip()
|
|
71
|
+
item_type = str(item_type).strip().lower()
|
|
72
|
+
if not pattern:
|
|
73
|
+
raise ValueError(f"type map {path} contains an empty pattern")
|
|
74
|
+
_validate_import_type(item_type)
|
|
75
|
+
mappings.append((pattern, item_type))
|
|
76
|
+
return mappings
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def explicit_import_type(
|
|
80
|
+
rel_str: str,
|
|
81
|
+
*,
|
|
82
|
+
type_map: list[tuple[str, str]],
|
|
83
|
+
type_overrides: list[tuple[str, str]],
|
|
84
|
+
) -> str | None:
|
|
85
|
+
"""Return the explicitly mapped type for *rel_str*, if any."""
|
|
86
|
+
resolved: str | None = None
|
|
87
|
+
for pattern, item_type in [*type_map, *type_overrides]:
|
|
88
|
+
if _matches_glob(rel_str, pattern):
|
|
89
|
+
resolved = item_type
|
|
90
|
+
return resolved
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def resolve_import_type(
|
|
94
|
+
rel_str: str,
|
|
95
|
+
*,
|
|
96
|
+
type_map: list[tuple[str, str]],
|
|
97
|
+
type_overrides: list[tuple[str, str]],
|
|
98
|
+
metadata_type: str | None,
|
|
99
|
+
content_bytes: bytes,
|
|
100
|
+
) -> str:
|
|
101
|
+
"""Pick the import item type for a source file."""
|
|
102
|
+
resolved = explicit_import_type(
|
|
103
|
+
rel_str,
|
|
104
|
+
type_map=type_map,
|
|
105
|
+
type_overrides=type_overrides,
|
|
106
|
+
)
|
|
107
|
+
if resolved:
|
|
108
|
+
return resolved
|
|
109
|
+
if metadata_type:
|
|
110
|
+
_validate_import_type(metadata_type)
|
|
111
|
+
return metadata_type
|
|
112
|
+
return classify_file_kind(content_bytes)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _validate_import_type(item_type: str) -> None:
|
|
116
|
+
if item_type not in VALID_IMPORT_TYPES:
|
|
117
|
+
allowed = ", ".join(sorted(VALID_IMPORT_TYPES))
|
|
118
|
+
raise ValueError(f"invalid item type {item_type!r}; expected one of: {allowed}")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _matches_glob(rel_str: str, pattern: str) -> bool:
|
|
122
|
+
return fnmatch.fnmatch(rel_str, pattern) or fnmatch.fnmatch(Path(rel_str).name, pattern)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def annotate_plan_conflicts(
|
|
126
|
+
plan: list[dict[str, Any]],
|
|
127
|
+
*,
|
|
128
|
+
namespace: str,
|
|
129
|
+
update: bool,
|
|
130
|
+
view: NamespaceView | None = None,
|
|
131
|
+
) -> list[str]:
|
|
132
|
+
"""Mark conflicting entries and return human-readable conflict messages."""
|
|
133
|
+
messages: list[str] = []
|
|
134
|
+
seen_paths: dict[str, str] = {}
|
|
135
|
+
|
|
136
|
+
for entry in plan:
|
|
137
|
+
entry.setdefault("status", "ok")
|
|
138
|
+
entry.setdefault("action", "create")
|
|
139
|
+
entry.pop("conflict", None)
|
|
140
|
+
|
|
141
|
+
if entry.get("conflict_reason"):
|
|
142
|
+
entry["status"] = "conflict"
|
|
143
|
+
entry["conflict"] = entry["conflict_reason"]
|
|
144
|
+
messages.append(f"{entry['source_rel']}: {entry['conflict_reason']}")
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
tree_path = entry["tree_path"]
|
|
148
|
+
rel = str(entry["source_rel"])
|
|
149
|
+
if tree_path in seen_paths:
|
|
150
|
+
reason = f"tree path {tree_path!r} is already mapped from {seen_paths[tree_path]!r}"
|
|
151
|
+
entry["status"] = "conflict"
|
|
152
|
+
entry["conflict"] = reason
|
|
153
|
+
messages.append(f"{rel}: {reason}")
|
|
154
|
+
continue
|
|
155
|
+
seen_paths[tree_path] = rel
|
|
156
|
+
|
|
157
|
+
meta_ns = entry.get("metadata_namespace")
|
|
158
|
+
if meta_ns and meta_ns != namespace:
|
|
159
|
+
reason = f"metadata namespace {meta_ns!r} does not match {namespace!r}"
|
|
160
|
+
entry["status"] = "conflict"
|
|
161
|
+
entry["conflict"] = reason
|
|
162
|
+
messages.append(f"{rel}: {reason}")
|
|
163
|
+
|
|
164
|
+
if view is not None:
|
|
165
|
+
exists = _item_exists(view, tree_path)
|
|
166
|
+
if exists:
|
|
167
|
+
entry["action"] = "update"
|
|
168
|
+
if not update:
|
|
169
|
+
reason = f"item {tree_path!r} already exists (use --update)"
|
|
170
|
+
entry["status"] = "conflict"
|
|
171
|
+
entry["conflict"] = reason
|
|
172
|
+
messages.append(f"{rel}: {reason}")
|
|
173
|
+
|
|
174
|
+
return messages
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def build_metadata_entry(
|
|
178
|
+
*,
|
|
179
|
+
file_path: Path,
|
|
180
|
+
rel: Path,
|
|
181
|
+
rel_str: str,
|
|
182
|
+
target_prefix: str,
|
|
183
|
+
content_bytes: bytes,
|
|
184
|
+
type_map: list[tuple[str, str]],
|
|
185
|
+
type_overrides: list[tuple[str, str]],
|
|
186
|
+
) -> dict[str, Any] | None:
|
|
187
|
+
"""Build one import plan entry from export metadata on *file_path*."""
|
|
188
|
+
meta = read_file_metadata(file_path)
|
|
189
|
+
item_path = meta.get("path", "").strip()
|
|
190
|
+
if not item_path:
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
metadata_type = meta.get("node_type", "").strip() or None
|
|
194
|
+
node_type = resolve_import_type(
|
|
195
|
+
rel_str,
|
|
196
|
+
type_map=type_map,
|
|
197
|
+
type_overrides=type_overrides,
|
|
198
|
+
metadata_type=metadata_type,
|
|
199
|
+
content_bytes=content_bytes,
|
|
200
|
+
)
|
|
201
|
+
tree_path = metadata_tree_path(item_path, target_prefix)
|
|
202
|
+
body = import_file_text(content_bytes)
|
|
203
|
+
|
|
204
|
+
entry: dict[str, Any] = {
|
|
205
|
+
"kind": node_type,
|
|
206
|
+
"source_rel": rel,
|
|
207
|
+
"source_abs": file_path,
|
|
208
|
+
"tree_path": tree_path,
|
|
209
|
+
"metadata_namespace": meta.get("namespace", "").strip() or None,
|
|
210
|
+
"content": body,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return entry
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _item_exists(view: NamespaceView, path: str) -> bool:
|
|
217
|
+
try:
|
|
218
|
+
view.get_item(path=path)
|
|
219
|
+
return True
|
|
220
|
+
except Exception:
|
|
221
|
+
return False
|
ocmo_cli/_item_output.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Tree item output formatting (config, template, secret, resolver)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ._output import (
|
|
9
|
+
_parse_path,
|
|
10
|
+
_print_jsonpath_value,
|
|
11
|
+
_resolve_path,
|
|
12
|
+
as_dict,
|
|
13
|
+
format_datetime,
|
|
14
|
+
sanitize_for_output,
|
|
15
|
+
yaml_dumps,
|
|
16
|
+
)
|
|
17
|
+
from ._resolve_output import _err_meta, resolve_output_format
|
|
18
|
+
|
|
19
|
+
DOCUMENT_NODE_TYPES = frozenset({"config", "template", "secret", "resolver"})
|
|
20
|
+
|
|
21
|
+
DOCUMENT_OUTPUT_RESOURCES = frozenset(
|
|
22
|
+
{
|
|
23
|
+
"config",
|
|
24
|
+
"template",
|
|
25
|
+
"secret",
|
|
26
|
+
"resolver",
|
|
27
|
+
"globalpermission",
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_METADATA_KEYS = ("path", "name", "node_type", "author")
|
|
32
|
+
_VERSION_METADATA_KEYS = ("version", "updater", "updated_at")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def uses_item_output(op_id: str, action: str, resource: str, result: Any | None) -> bool:
|
|
36
|
+
"""True when API result should use resolve-style raw/json/yaml output."""
|
|
37
|
+
if action in ("create", "update") and resource in DOCUMENT_OUTPUT_RESOURCES:
|
|
38
|
+
return True
|
|
39
|
+
if op_id == "get_item" and result is not None:
|
|
40
|
+
return node_type_of(result) in DOCUMENT_NODE_TYPES
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def node_type_of(result: Any) -> str | None:
|
|
45
|
+
if hasattr(result, "node_type"):
|
|
46
|
+
value = result.node_type
|
|
47
|
+
return str(value) if value else None
|
|
48
|
+
if isinstance(result, dict):
|
|
49
|
+
value = result.get("node_type")
|
|
50
|
+
return str(value) if value else None
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_field(obj: Any, key: str) -> Any:
|
|
55
|
+
if isinstance(obj, dict):
|
|
56
|
+
return obj.get(key)
|
|
57
|
+
return getattr(obj, key, None)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _version_data(obj: Any) -> Any | None:
|
|
61
|
+
return _get_field(obj, "version_data")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def item_version_is_deleted(result: Any) -> bool:
|
|
65
|
+
"""True when the fetched item points at a soft-deleted version."""
|
|
66
|
+
version_data = _version_data(result)
|
|
67
|
+
if version_data is None:
|
|
68
|
+
return False
|
|
69
|
+
return bool(_get_field(version_data, "deleted_at"))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def deleted_version_edit_error(path: str, item: Any) -> str:
|
|
73
|
+
"""Human-readable error for attempting to edit a deleted version."""
|
|
74
|
+
version_data = _version_data(item)
|
|
75
|
+
version = _get_field(version_data, "version") if version_data else None
|
|
76
|
+
if version is not None:
|
|
77
|
+
return f"Cannot edit deleted version {version} of {path!r}."
|
|
78
|
+
return f"Cannot edit deleted version of {path!r}."
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def item_metadata_rows(result: Any, *, resource: str | None = None) -> list[tuple[str, Any]]:
|
|
82
|
+
rows: list[tuple[str, Any]] = []
|
|
83
|
+
|
|
84
|
+
if resource == "namespace":
|
|
85
|
+
for key in ("name", "description", "permissions_tag", "webhooks_tag", "git_sync_tag"):
|
|
86
|
+
value = _get_field(result, key)
|
|
87
|
+
if value is not None and value != "":
|
|
88
|
+
rows.append((key, value))
|
|
89
|
+
return rows
|
|
90
|
+
|
|
91
|
+
if resource == "lock":
|
|
92
|
+
for key in ("path", "reason", "locked_by"):
|
|
93
|
+
value = _get_field(result, key)
|
|
94
|
+
if value is not None and value != "":
|
|
95
|
+
rows.append((key, value))
|
|
96
|
+
expires_at = _get_field(result, "expires_at")
|
|
97
|
+
if expires_at:
|
|
98
|
+
rows.append(("expires_at", format_datetime(expires_at)))
|
|
99
|
+
return rows
|
|
100
|
+
|
|
101
|
+
if resource == "globalpermission":
|
|
102
|
+
for key in ("id", "position", "created_at", "updated_at"):
|
|
103
|
+
value = _get_field(result, key)
|
|
104
|
+
if value is not None and value != "":
|
|
105
|
+
if key in ("created_at", "updated_at"):
|
|
106
|
+
value = format_datetime(value) or value
|
|
107
|
+
rows.append((key, value))
|
|
108
|
+
return rows
|
|
109
|
+
|
|
110
|
+
for key in _METADATA_KEYS:
|
|
111
|
+
value = _get_field(result, key)
|
|
112
|
+
if value is not None and value != "":
|
|
113
|
+
rows.append((key, value))
|
|
114
|
+
|
|
115
|
+
version_data = _version_data(result)
|
|
116
|
+
if version_data is not None:
|
|
117
|
+
deleted_at = _get_field(version_data, "deleted_at")
|
|
118
|
+
version = _get_field(version_data, "version")
|
|
119
|
+
if version is not None and version != "":
|
|
120
|
+
rows.append(("version", version))
|
|
121
|
+
|
|
122
|
+
if deleted_at:
|
|
123
|
+
formatted_deleted_at = format_datetime(deleted_at)
|
|
124
|
+
if formatted_deleted_at:
|
|
125
|
+
rows.append(("deleted_at", formatted_deleted_at))
|
|
126
|
+
deleted_by = _get_field(version_data, "updater")
|
|
127
|
+
if deleted_by:
|
|
128
|
+
rows.append(("deleted_by", deleted_by))
|
|
129
|
+
else:
|
|
130
|
+
for key in _VERSION_METADATA_KEYS[1:]:
|
|
131
|
+
value = _get_field(version_data, key)
|
|
132
|
+
if value is not None and value != "":
|
|
133
|
+
if key == "updated_at":
|
|
134
|
+
value = format_datetime(value) or value
|
|
135
|
+
rows.append((key, value))
|
|
136
|
+
|
|
137
|
+
description = _get_field(result, "description")
|
|
138
|
+
if description:
|
|
139
|
+
rows.append(("description", description))
|
|
140
|
+
|
|
141
|
+
return rows
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def item_body(result: Any, *, resource: str | None = None) -> str:
|
|
145
|
+
"""Return the document body for stdout in raw mode."""
|
|
146
|
+
if resource == "globalpermission":
|
|
147
|
+
rule = _get_field(result, "rule")
|
|
148
|
+
if rule is None:
|
|
149
|
+
return ""
|
|
150
|
+
payload = sanitize_for_output(as_dict(rule, fallback_vars=False) or rule)
|
|
151
|
+
return yaml_dumps(payload)
|
|
152
|
+
|
|
153
|
+
if resource in ("namespace", "lock"):
|
|
154
|
+
payload = sanitize_for_output(as_dict(result, fallback_vars=False) or result)
|
|
155
|
+
return yaml_dumps(payload)
|
|
156
|
+
|
|
157
|
+
node_type = node_type_of(result)
|
|
158
|
+
if node_type == "resolver":
|
|
159
|
+
body = _get_field(result, "configuration")
|
|
160
|
+
return body if isinstance(body, str) else ""
|
|
161
|
+
version_data = _version_data(result)
|
|
162
|
+
if version_data is not None:
|
|
163
|
+
data = _get_field(version_data, "data")
|
|
164
|
+
return data if isinstance(data, str) else ""
|
|
165
|
+
return ""
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def emit_item_metadata(result: Any, *, no_color: bool = False, resource: str | None = None) -> None:
|
|
169
|
+
for key, value in item_metadata_rows(result, resource=resource):
|
|
170
|
+
_err_meta(f"# {key}: {value}", no_color=no_color)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def emit_item_raw(result: Any, *, no_color: bool = False, resource: str | None = None) -> None:
|
|
174
|
+
emit_item_metadata(result, no_color=no_color, resource=resource)
|
|
175
|
+
text = item_body(result, resource=resource)
|
|
176
|
+
print(text, end="" if text.endswith("\n") else "\n")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def emit_item_result(
|
|
180
|
+
result: Any,
|
|
181
|
+
fmt: str,
|
|
182
|
+
*,
|
|
183
|
+
no_color: bool = False,
|
|
184
|
+
resource: str | None = None,
|
|
185
|
+
) -> None:
|
|
186
|
+
"""Emit one tree item in resolve-compatible output formats."""
|
|
187
|
+
if fmt == "raw":
|
|
188
|
+
emit_item_raw(result, no_color=no_color, resource=resource)
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
if fmt == "name":
|
|
192
|
+
name = _get_field(result, "path") or _get_field(result, "name")
|
|
193
|
+
if name:
|
|
194
|
+
print(name)
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
payload = sanitize_for_output(as_dict(result, fallback_vars=False) or result)
|
|
198
|
+
|
|
199
|
+
if fmt == "json":
|
|
200
|
+
print(json.dumps(payload, default=str, indent=2))
|
|
201
|
+
elif fmt == "yaml":
|
|
202
|
+
print(yaml_dumps(payload), end="")
|
|
203
|
+
elif fmt.startswith("jsonpath="):
|
|
204
|
+
value = _resolve_path(payload, _parse_path(fmt[9:]))
|
|
205
|
+
_print_jsonpath_value(value)
|
|
206
|
+
else:
|
|
207
|
+
print(payload)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def item_output_format(
|
|
211
|
+
cli_fmt: str | None,
|
|
212
|
+
ctx_fmt: str | None,
|
|
213
|
+
*,
|
|
214
|
+
command_key: str = "get item",
|
|
215
|
+
) -> str:
|
|
216
|
+
"""Effective output format for tree item commands (default: raw)."""
|
|
217
|
+
return resolve_output_format(cli_fmt, ctx_fmt, command_key=command_key)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def item_output_includes_token_in_payload(fmt: str, field: str | None) -> bool:
|
|
221
|
+
if field:
|
|
222
|
+
return field in ("token1", "token")
|
|
223
|
+
return fmt in ("yaml", "json")
|
ocmo_cli/_lazy_mixin.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Shared lazy-delegation helpers for startup-safe Click stubs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ClickLazyMixin:
|
|
11
|
+
"""Delegate common Click entry points to a lazily loaded target."""
|
|
12
|
+
|
|
13
|
+
def _lazy_target(self) -> click.Command | click.Group:
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
|
|
16
|
+
def make_context(
|
|
17
|
+
self,
|
|
18
|
+
info_name: str | None,
|
|
19
|
+
args: list[str],
|
|
20
|
+
parent: click.Context | None = None,
|
|
21
|
+
**extra: Any,
|
|
22
|
+
) -> click.Context:
|
|
23
|
+
return self._lazy_target().make_context(info_name, args, parent=parent, **extra)
|
|
24
|
+
|
|
25
|
+
def invoke(self, ctx: click.Context) -> Any:
|
|
26
|
+
return self._lazy_target().invoke(ctx)
|
|
27
|
+
|
|
28
|
+
def shell_complete(self, ctx: click.Context, incomplete: str) -> list[click.shell_completion.CompletionItem]:
|
|
29
|
+
return self._lazy_target().shell_complete(ctx, incomplete)
|