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,907 @@
|
|
|
1
|
+
"""Generated command groups built from commands.yaml at import time.
|
|
2
|
+
|
|
3
|
+
For each unique action in commands.yaml that is NOT hand-written, a Click Group
|
|
4
|
+
is created (e.g. `get`, `create`, `delete`, ...). Within each group, a subcommand
|
|
5
|
+
is created for each resource type (e.g. `get config`, `get namespace`, ...).
|
|
6
|
+
|
|
7
|
+
Since commands.yaml is a committed file, this is equivalent to code generation
|
|
8
|
+
in terms of determinism and startup behaviour.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, cast
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
|
|
19
|
+
from .._body_payload import (
|
|
20
|
+
BODY_PAYLOADS,
|
|
21
|
+
format_usage_error,
|
|
22
|
+
prepare_body_payload,
|
|
23
|
+
prepare_untag_body_payload,
|
|
24
|
+
sdk_path_for_body_payload,
|
|
25
|
+
validate_create_request,
|
|
26
|
+
validate_tag_request,
|
|
27
|
+
)
|
|
28
|
+
from .._click_groups import ResourceAliasGroup
|
|
29
|
+
from .._client import OcmoCtx
|
|
30
|
+
from .._item_output import (
|
|
31
|
+
emit_item_result,
|
|
32
|
+
item_output_format,
|
|
33
|
+
item_output_includes_token_in_payload,
|
|
34
|
+
uses_item_output,
|
|
35
|
+
)
|
|
36
|
+
from .._options import (
|
|
37
|
+
get_cast_output_option,
|
|
38
|
+
get_item_output_option,
|
|
39
|
+
namespace_option,
|
|
40
|
+
output_option,
|
|
41
|
+
tree_version_option,
|
|
42
|
+
)
|
|
43
|
+
from .._output_manifest import (
|
|
44
|
+
command_key as output_command_key,
|
|
45
|
+
)
|
|
46
|
+
from .._output_manifest import (
|
|
47
|
+
emit_command_output,
|
|
48
|
+
get_command_spec,
|
|
49
|
+
resolve_effective_format,
|
|
50
|
+
)
|
|
51
|
+
from .._sdk_dispatch import (
|
|
52
|
+
NO_ADDRESS_OPS as _NO_ADDRESS_OPS,
|
|
53
|
+
)
|
|
54
|
+
from .._sdk_dispatch import (
|
|
55
|
+
NON_TREE_ADDRESS_OPS as _NON_TREE_ADDRESS_OPS,
|
|
56
|
+
)
|
|
57
|
+
from .._sdk_dispatch import (
|
|
58
|
+
VERSION_ADDRESS_OPS as _VERSION_ADDRESS_OPS,
|
|
59
|
+
)
|
|
60
|
+
from .._sdk_dispatch import (
|
|
61
|
+
VERSION_FILTER_OPS as _VERSION_FILTER_OPS,
|
|
62
|
+
)
|
|
63
|
+
from .._sdk_dispatch import (
|
|
64
|
+
address_optional_for_command,
|
|
65
|
+
address_required_for_op,
|
|
66
|
+
build_sdk_call,
|
|
67
|
+
extra_params_for_ops,
|
|
68
|
+
pick_op_id,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _op_scope(op_id: str, ops_meta: dict[str, Any]) -> str:
|
|
73
|
+
return str(ops_meta.get(op_id, {}).get("scope", "namespace"))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _command_is_client_scoped(op_ids: list[str], ops_meta: dict[str, Any]) -> bool:
|
|
77
|
+
"""True when every mapped operation for this command is client-scoped."""
|
|
78
|
+
if not op_ids:
|
|
79
|
+
return False
|
|
80
|
+
return all(_op_scope(op_id, ops_meta) == "client" for op_id in op_ids)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _reject_namespace_if_client_scoped(
|
|
84
|
+
*,
|
|
85
|
+
op_ids: list[str],
|
|
86
|
+
ops_meta: dict[str, Any],
|
|
87
|
+
namespace_explicit: bool,
|
|
88
|
+
) -> None:
|
|
89
|
+
from .._exit import USAGE_ERROR # deferred
|
|
90
|
+
|
|
91
|
+
if not _command_is_client_scoped(op_ids, ops_meta):
|
|
92
|
+
return
|
|
93
|
+
if not namespace_explicit:
|
|
94
|
+
return
|
|
95
|
+
print(
|
|
96
|
+
"Error: -n/--namespace is not valid for this command.",
|
|
97
|
+
file=sys.stderr,
|
|
98
|
+
)
|
|
99
|
+
raise SystemExit(USAGE_ERROR)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _namespace_explicit_on_cli(
|
|
103
|
+
click_ctx: click.Context,
|
|
104
|
+
*,
|
|
105
|
+
use_namespace_option: bool,
|
|
106
|
+
) -> bool:
|
|
107
|
+
"""True when -n/--namespace was passed on the CLI (not from env/config alone)."""
|
|
108
|
+
from click.core import ParameterSource
|
|
109
|
+
|
|
110
|
+
if use_namespace_option:
|
|
111
|
+
source = click_ctx.get_parameter_source("namespace")
|
|
112
|
+
if source == ParameterSource.COMMANDLINE:
|
|
113
|
+
return True
|
|
114
|
+
ctx: click.Context | None = click_ctx
|
|
115
|
+
while ctx is not None:
|
|
116
|
+
try:
|
|
117
|
+
source = ctx.get_parameter_source("namespace")
|
|
118
|
+
except LookupError:
|
|
119
|
+
source = None
|
|
120
|
+
if source == ParameterSource.COMMANDLINE:
|
|
121
|
+
return True
|
|
122
|
+
ctx = ctx.parent
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
from .._generated_registry import action_help as _action_help # noqa: E402
|
|
127
|
+
|
|
128
|
+
_RESOURCE_HELP: dict[str, str] = {
|
|
129
|
+
"namespace": "Namespace metadata.",
|
|
130
|
+
"item": "A tree item (config, template, secret, resolver, or folder). "
|
|
131
|
+
"Address may include @version or @tag (e.g. app/web@2, app/web@stable).",
|
|
132
|
+
"config": "A configuration item.",
|
|
133
|
+
"template": "A template item.",
|
|
134
|
+
"secret": "A secret item.",
|
|
135
|
+
"resolver": "A resolver item.",
|
|
136
|
+
"folder": "A folder in the tree.",
|
|
137
|
+
"version": "Version history for an item.",
|
|
138
|
+
"lock": "An active edit lock.",
|
|
139
|
+
"audit": "Audit log events.",
|
|
140
|
+
"globalpermission": "A global permission rule.",
|
|
141
|
+
"cast": "Available resolve cast formats.",
|
|
142
|
+
"tree": "Tree navigation and search results.",
|
|
143
|
+
"parameters": "Dynamic resolve parameters.",
|
|
144
|
+
"draft": "A resolve draft.",
|
|
145
|
+
"token": "A resolver authentication token.",
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_ACTION_RESOURCE_HELP: dict[tuple[str, str], str] = {
|
|
149
|
+
("get", "namespace"): (
|
|
150
|
+
"List namespaces or show one. ADDRESS is the namespace name (omit to list all). "
|
|
151
|
+
"Default table shows name and description; use -o wide for all fields."
|
|
152
|
+
),
|
|
153
|
+
("create", "namespace"): ("Create a namespace. ADDRESS is the name; use --description for details."),
|
|
154
|
+
("update", "namespace"): "Update namespace metadata. ADDRESS is the namespace name.",
|
|
155
|
+
("delete", "namespace"): "Delete a namespace. ADDRESS is the namespace name.",
|
|
156
|
+
("get", "version"): (
|
|
157
|
+
"List version history for a tree item. ADDRESS is the item path, optionally "
|
|
158
|
+
"with @version or @tag (e.g. app/web@26, app/web@stable). Use --limit to "
|
|
159
|
+
"cap results and --tagged-only to omit untagged versions."
|
|
160
|
+
),
|
|
161
|
+
("get", "item"): (
|
|
162
|
+
"Show a tree item or list items in the namespace. ADDRESS is the item path "
|
|
163
|
+
"with optional @version or @tag (omit to list). Use --limit to cap list "
|
|
164
|
+
"results and --type to filter by item type (repeatable)."
|
|
165
|
+
),
|
|
166
|
+
("get", "cast"): (
|
|
167
|
+
"List resolve cast formats or show one format's option schema. "
|
|
168
|
+
"ADDRESS is the format name (e.g. yaml, json; omit to list all)."
|
|
169
|
+
),
|
|
170
|
+
("get", "audit"): (
|
|
171
|
+
"List audit events or show one. ADDRESS is the event id " "(full UUID or a unique hex prefix; omit to list)."
|
|
172
|
+
),
|
|
173
|
+
("get", "lock"): "List locks or show one. ADDRESS is the tree path (omit to list all).",
|
|
174
|
+
("create", "lock"): ("Create a lock. ADDRESS is the tree path; use --reason and optionally --expires-at."),
|
|
175
|
+
("update", "lock"): ("Update a lock. ADDRESS is the tree path; use --reason and optionally --expires-at."),
|
|
176
|
+
("delete", "lock"): "Remove a lock. ADDRESS is the tree path.",
|
|
177
|
+
("get", "globalpermission"): (
|
|
178
|
+
"List global permission rules or show one. "
|
|
179
|
+
"ADDRESS is the rule id (user-defined id or UUID; omit to list all)."
|
|
180
|
+
),
|
|
181
|
+
("create", "globalpermission"): (
|
|
182
|
+
"Create a rule. ADDRESS is optional rule id; use -f for the rule body "
|
|
183
|
+
"and --position to reorder after create."
|
|
184
|
+
),
|
|
185
|
+
("update", "globalpermission"): (
|
|
186
|
+
"Update a rule. ADDRESS is the rule id (user-defined id or UUID); use -f for the rule body."
|
|
187
|
+
),
|
|
188
|
+
("delete", "globalpermission"): ("Delete a rule. ADDRESS is the rule id (user-defined id or UUID)."),
|
|
189
|
+
("move", "globalpermission"): (
|
|
190
|
+
"Reorder a rule. ADDRESS is the rule id (user-defined id or UUID); "
|
|
191
|
+
"use --position to set the new sort position."
|
|
192
|
+
),
|
|
193
|
+
("rotate", "token"): (
|
|
194
|
+
"Rotate a resolver access token. ADDRESS is the resolver path; "
|
|
195
|
+
"--token-number selects which token slot to rotate."
|
|
196
|
+
),
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _resource_command_help(action: str, resource: str) -> str:
|
|
201
|
+
return _ACTION_RESOURCE_HELP.get(
|
|
202
|
+
(action, resource),
|
|
203
|
+
_RESOURCE_HELP.get(resource, f"{action.capitalize()} {resource}."),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# Resource type aliases (canonical → set of aliases)
|
|
208
|
+
from .._resource_aliases import RESOURCE_ALIASES # noqa: E402
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _load_commands_yaml() -> dict[str, Any]:
|
|
212
|
+
from ocmo_cli._commands_map import OPERATIONS # static Python dict, no YAML at startup
|
|
213
|
+
|
|
214
|
+
return OPERATIONS
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _entries_for_action(action: str) -> list[tuple[str, str, dict[str, Any]]]:
|
|
218
|
+
operations = _load_commands_yaml()
|
|
219
|
+
if action == "untag":
|
|
220
|
+
set_tag_cfg = operations.get("set_tag")
|
|
221
|
+
if isinstance(set_tag_cfg, dict) and not set_tag_cfg.get("hand_written") and not set_tag_cfg.get("skip"):
|
|
222
|
+
return [("set_tag", "item", dict(set_tag_cfg))]
|
|
223
|
+
entries: list[tuple[str, str, dict[str, Any]]] = []
|
|
224
|
+
for op_id, config in operations.items():
|
|
225
|
+
if not isinstance(config, dict):
|
|
226
|
+
continue
|
|
227
|
+
if config.get("hand_written") or config.get("skip"):
|
|
228
|
+
continue
|
|
229
|
+
entry_action = config.get("action")
|
|
230
|
+
resource = config.get("resource")
|
|
231
|
+
if entry_action != action or not resource:
|
|
232
|
+
continue
|
|
233
|
+
# Hand-written ``resolve`` command owns config resolve; only draft/parameters are generated.
|
|
234
|
+
if entry_action == "resolve":
|
|
235
|
+
continue
|
|
236
|
+
entries.append((op_id, resource, config))
|
|
237
|
+
return entries
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def build_action_group(action: str) -> click.Group:
|
|
241
|
+
"""Build one generated Click command group."""
|
|
242
|
+
entries = _entries_for_action(action)
|
|
243
|
+
if not entries:
|
|
244
|
+
raise ValueError(f"no generated operations for action {action!r}")
|
|
245
|
+
return _build_action_group(action, entries)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def build_generated_groups() -> list[click.Group]:
|
|
249
|
+
"""Build and return all generated Click command groups."""
|
|
250
|
+
from .._generated_registry import generated_action_names
|
|
251
|
+
|
|
252
|
+
return [build_action_group(action) for action in generated_action_names()]
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def build_resolve_subcommands() -> list[click.Command]:
|
|
256
|
+
"""Subcommands merged into the hand-written ``resolve`` group."""
|
|
257
|
+
operations = _load_commands_yaml()
|
|
258
|
+
by_resource: dict[str, list[tuple[str, dict[str, Any]]]] = {}
|
|
259
|
+
for op_id, config in operations.items():
|
|
260
|
+
if not isinstance(config, dict) or config.get("hand_written") or config.get("skip"):
|
|
261
|
+
continue
|
|
262
|
+
if config.get("action") != "resolve":
|
|
263
|
+
continue
|
|
264
|
+
resource = config.get("resource")
|
|
265
|
+
if not resource:
|
|
266
|
+
continue
|
|
267
|
+
by_resource.setdefault(resource, []).append((op_id, config))
|
|
268
|
+
|
|
269
|
+
commands: list[click.Command] = []
|
|
270
|
+
for resource, resource_entries in sorted(by_resource.items()):
|
|
271
|
+
op_ids = [op_id for op_id, _ in resource_entries]
|
|
272
|
+
config = resource_entries[0][1]
|
|
273
|
+
commands.append(_build_resource_command("resolve", resource, op_ids, config))
|
|
274
|
+
return commands
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _build_action_group(action: str, entries: list[tuple[str, str, dict[str, Any]]]) -> click.Group:
|
|
278
|
+
"""Build a Click Group for an action (e.g. 'get', 'create', 'delete')."""
|
|
279
|
+
|
|
280
|
+
group_help = _action_help(action)
|
|
281
|
+
|
|
282
|
+
@click.group(action, cls=ResourceAliasGroup, help=group_help)
|
|
283
|
+
@click.pass_context
|
|
284
|
+
def action_group(ctx: click.Context) -> None:
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
# Group operations by resource (multiple op_ids may share action+resource).
|
|
288
|
+
by_resource: dict[str, list[tuple[str, dict[str, Any]]]] = {}
|
|
289
|
+
for op_id, resource, config in entries:
|
|
290
|
+
by_resource.setdefault(resource, []).append((op_id, config))
|
|
291
|
+
|
|
292
|
+
for resource, resource_entries in sorted(by_resource.items()):
|
|
293
|
+
op_ids = [op_id for op_id, _ in resource_entries]
|
|
294
|
+
config = resource_entries[0][1]
|
|
295
|
+
cmd = _build_resource_command(action, resource, op_ids, config)
|
|
296
|
+
action_group.add_resource_command(
|
|
297
|
+
cmd,
|
|
298
|
+
canonical=resource,
|
|
299
|
+
aliases=RESOURCE_ALIASES.get(resource, []),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return action_group
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _build_resource_command(action: str, resource: str, op_ids: list[str], config: dict[str, Any]) -> click.Command:
|
|
306
|
+
"""Build a Click Command for action+resource, wired to the SDK method."""
|
|
307
|
+
|
|
308
|
+
confirm_mode = config.get("confirm")
|
|
309
|
+
|
|
310
|
+
is_rotate_token = action == "rotate" and resource == "token"
|
|
311
|
+
is_mutating = action in ("create", "update", "delete", "move", "copy", "tag", "untag", "rotate", "propagate")
|
|
312
|
+
|
|
313
|
+
resource_help = _resource_command_help(action, resource)
|
|
314
|
+
extra_specs = extra_params_for_ops(op_ids)
|
|
315
|
+
supports_version_address = any(op_id in _VERSION_ADDRESS_OPS for op_id in op_ids) or any(
|
|
316
|
+
op_id in _VERSION_FILTER_OPS for op_id in op_ids
|
|
317
|
+
)
|
|
318
|
+
supports_file_body = (
|
|
319
|
+
action not in ("tag", "untag", "delete", "propagate", "move", "copy")
|
|
320
|
+
and not is_rotate_token
|
|
321
|
+
and not (action == "create" and resource in ("namespace", "lock"))
|
|
322
|
+
and not (action == "update" and resource == "lock")
|
|
323
|
+
)
|
|
324
|
+
output_key = output_command_key(action, resource)
|
|
325
|
+
address_required = not address_optional_for_command(
|
|
326
|
+
op_ids,
|
|
327
|
+
action=action,
|
|
328
|
+
resource=resource,
|
|
329
|
+
)
|
|
330
|
+
get_item_list_mode = action == "get" and resource == "item"
|
|
331
|
+
get_cast_dual_mode = action == "get" and resource == "cast"
|
|
332
|
+
ops_meta = _load_ops_yaml()
|
|
333
|
+
use_namespace_option = not _command_is_client_scoped(op_ids, ops_meta)
|
|
334
|
+
|
|
335
|
+
@click.pass_context
|
|
336
|
+
def resource_cmd(
|
|
337
|
+
click_ctx: click.Context,
|
|
338
|
+
/,
|
|
339
|
+
address: str | None,
|
|
340
|
+
output_fmt: str | None = None,
|
|
341
|
+
field: str | None = None,
|
|
342
|
+
version_flag: str | None = None,
|
|
343
|
+
list_limit: int | None = None,
|
|
344
|
+
item_types: tuple[str, ...] = (),
|
|
345
|
+
dry_run: bool = False,
|
|
346
|
+
yes: bool = False,
|
|
347
|
+
file_path: str | None = None,
|
|
348
|
+
_op_ids: list[str] = op_ids,
|
|
349
|
+
_action: str = action,
|
|
350
|
+
_resource: str = resource,
|
|
351
|
+
_confirm_mode: str | None = confirm_mode,
|
|
352
|
+
_extra_specs: list[Any] = extra_specs,
|
|
353
|
+
**_ignored: Any,
|
|
354
|
+
) -> None:
|
|
355
|
+
ocmo_ctx = click_ctx.obj
|
|
356
|
+
namespace = click_ctx.params.get("namespace")
|
|
357
|
+
namespace_explicit = _namespace_explicit_on_cli(
|
|
358
|
+
click_ctx,
|
|
359
|
+
use_namespace_option=use_namespace_option,
|
|
360
|
+
)
|
|
361
|
+
sdk_extra = {
|
|
362
|
+
spec.sdk_name: click_ctx.params.get(spec.sdk_name)
|
|
363
|
+
for spec in _extra_specs
|
|
364
|
+
if click_ctx.params.get(spec.sdk_name) is not None
|
|
365
|
+
}
|
|
366
|
+
if _action == "get" and _resource == "item":
|
|
367
|
+
if list_limit is not None:
|
|
368
|
+
sdk_extra["limit"] = list_limit
|
|
369
|
+
_execute_generated(
|
|
370
|
+
ctx=ocmo_ctx,
|
|
371
|
+
op_ids=_op_ids,
|
|
372
|
+
action=_action,
|
|
373
|
+
resource=_resource,
|
|
374
|
+
address=address,
|
|
375
|
+
namespace=namespace,
|
|
376
|
+
output_fmt=output_fmt,
|
|
377
|
+
field=field,
|
|
378
|
+
version_flag=version_flag,
|
|
379
|
+
dry_run=dry_run or (ocmo_ctx and ocmo_ctx.dry_run),
|
|
380
|
+
yes=yes or (ocmo_ctx and ocmo_ctx.yes),
|
|
381
|
+
file_path=file_path,
|
|
382
|
+
confirm_mode=_confirm_mode,
|
|
383
|
+
sdk_extra=sdk_extra,
|
|
384
|
+
namespace_explicit=namespace_explicit,
|
|
385
|
+
item_types=item_types,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Apply Click decorators inside-out: options/arguments first, command last.
|
|
389
|
+
for spec in reversed(extra_specs):
|
|
390
|
+
opt_name = f"--{spec.click_name}"
|
|
391
|
+
if spec.is_flag:
|
|
392
|
+
resource_cmd = click.option(
|
|
393
|
+
opt_name,
|
|
394
|
+
spec.sdk_name,
|
|
395
|
+
is_flag=True,
|
|
396
|
+
default=False,
|
|
397
|
+
help=spec.help,
|
|
398
|
+
)(resource_cmd)
|
|
399
|
+
elif spec.type_ is int:
|
|
400
|
+
token_required = is_rotate_token and spec.sdk_name == "token_number"
|
|
401
|
+
resource_cmd = click.option(
|
|
402
|
+
opt_name,
|
|
403
|
+
spec.sdk_name,
|
|
404
|
+
type=int,
|
|
405
|
+
required=token_required,
|
|
406
|
+
default=None,
|
|
407
|
+
help=spec.help,
|
|
408
|
+
)(resource_cmd)
|
|
409
|
+
elif spec.type_ is float:
|
|
410
|
+
resource_cmd = click.option(
|
|
411
|
+
opt_name,
|
|
412
|
+
spec.sdk_name,
|
|
413
|
+
type=float,
|
|
414
|
+
default=None,
|
|
415
|
+
help=spec.help,
|
|
416
|
+
)(resource_cmd)
|
|
417
|
+
else:
|
|
418
|
+
tag_required = action in ("tag", "untag") and spec.sdk_name == "tag"
|
|
419
|
+
if tag_required:
|
|
420
|
+
resource_cmd = click.option(
|
|
421
|
+
opt_name,
|
|
422
|
+
spec.sdk_name,
|
|
423
|
+
required=True,
|
|
424
|
+
help=spec.help,
|
|
425
|
+
)(resource_cmd)
|
|
426
|
+
else:
|
|
427
|
+
resource_cmd = click.option(
|
|
428
|
+
opt_name,
|
|
429
|
+
spec.sdk_name,
|
|
430
|
+
default=None,
|
|
431
|
+
help=spec.help,
|
|
432
|
+
)(resource_cmd)
|
|
433
|
+
if not is_rotate_token:
|
|
434
|
+
resource_cmd = click.option(
|
|
435
|
+
"--field",
|
|
436
|
+
default=None,
|
|
437
|
+
metavar="PATH",
|
|
438
|
+
help="Extract a specific dot-path field from the response (e.g. version_data.data).",
|
|
439
|
+
)(resource_cmd)
|
|
440
|
+
if is_mutating:
|
|
441
|
+
if supports_file_body:
|
|
442
|
+
resource_cmd = click.option(
|
|
443
|
+
"-f", "--file", "file_path", default=None, help="YAML/JSON body from file ('-' for stdin)."
|
|
444
|
+
)(resource_cmd)
|
|
445
|
+
if confirm_mode == "destructive":
|
|
446
|
+
resource_cmd = click.option("--yes", "-y", is_flag=True, default=False, help="Skip confirmation prompts.")(
|
|
447
|
+
resource_cmd
|
|
448
|
+
)
|
|
449
|
+
resource_cmd = click.option("--dry-run", is_flag=True, default=False, help="Print plan without sending.")(
|
|
450
|
+
resource_cmd
|
|
451
|
+
)
|
|
452
|
+
if get_item_list_mode:
|
|
453
|
+
from .._get_item_list import GET_ITEM_TYPE_CHOICES
|
|
454
|
+
|
|
455
|
+
resource_cmd = click.option(
|
|
456
|
+
"--limit",
|
|
457
|
+
"list_limit",
|
|
458
|
+
type=int,
|
|
459
|
+
default=None,
|
|
460
|
+
help="Maximum number of items to return.",
|
|
461
|
+
)(resource_cmd)
|
|
462
|
+
resource_cmd = click.option(
|
|
463
|
+
"--type",
|
|
464
|
+
"item_types",
|
|
465
|
+
multiple=True,
|
|
466
|
+
type=click.Choice(GET_ITEM_TYPE_CHOICES, case_sensitive=False),
|
|
467
|
+
help=("Filter by item type (repeatable). " "Default: config, template, secret, resolver."),
|
|
468
|
+
)(resource_cmd)
|
|
469
|
+
resource_cmd = get_item_output_option()(resource_cmd)
|
|
470
|
+
elif get_cast_dual_mode:
|
|
471
|
+
resource_cmd = get_cast_output_option()(resource_cmd)
|
|
472
|
+
elif not is_rotate_token:
|
|
473
|
+
resource_cmd = output_option(output_key)(resource_cmd)
|
|
474
|
+
if supports_version_address:
|
|
475
|
+
resource_cmd = tree_version_option()(resource_cmd)
|
|
476
|
+
if use_namespace_option:
|
|
477
|
+
resource_cmd = namespace_option()(resource_cmd)
|
|
478
|
+
resource_cmd = click.argument("address", required=address_required)(resource_cmd)
|
|
479
|
+
return click.command(resource, help=resource_help)(resource_cmd)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _execute_generated(
|
|
483
|
+
*,
|
|
484
|
+
ctx: OcmoCtx,
|
|
485
|
+
op_ids: list[str],
|
|
486
|
+
action: str,
|
|
487
|
+
resource: str,
|
|
488
|
+
address: str | None,
|
|
489
|
+
namespace: str | None,
|
|
490
|
+
output_fmt: str | None,
|
|
491
|
+
field: str | None = None,
|
|
492
|
+
version_flag: str | None = None,
|
|
493
|
+
dry_run: bool,
|
|
494
|
+
yes: bool,
|
|
495
|
+
file_path: str | None,
|
|
496
|
+
confirm_mode: str | None,
|
|
497
|
+
sdk_extra: dict[str, Any] | None = None,
|
|
498
|
+
namespace_explicit: bool = False,
|
|
499
|
+
item_types: tuple[str, ...] = (),
|
|
500
|
+
) -> None:
|
|
501
|
+
from .._address import AddressError, parse_address, parse_simple_address # deferred
|
|
502
|
+
from .._errors import handle_sdk_error # deferred
|
|
503
|
+
from .._exit import USAGE_ERROR # deferred
|
|
504
|
+
from .._output import as_dict, extract_field, sanitize_for_output, status # deferred # deferred
|
|
505
|
+
from .._output import confirm as _confirm
|
|
506
|
+
from .._resolver_output import print_resolver_token # deferred
|
|
507
|
+
|
|
508
|
+
list_mode = action == "get" and resource == "item" and not address
|
|
509
|
+
cast_list_mode = action == "get" and resource == "cast" and not address
|
|
510
|
+
cast_show_mode = action == "get" and resource == "cast" and bool(address)
|
|
511
|
+
cast_format: str | None = None
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
ops_meta = _load_ops_yaml()
|
|
515
|
+
_reject_namespace_if_client_scoped(
|
|
516
|
+
op_ids=op_ids,
|
|
517
|
+
ops_meta=ops_meta,
|
|
518
|
+
namespace_explicit=namespace_explicit,
|
|
519
|
+
)
|
|
520
|
+
if list_mode:
|
|
521
|
+
op_id = "search_root"
|
|
522
|
+
elif cast_list_mode or cast_show_mode:
|
|
523
|
+
op_id = "list_cast_formats"
|
|
524
|
+
else:
|
|
525
|
+
op_id = pick_op_id(op_ids, address=address, namespace=namespace, ops_meta=ops_meta)
|
|
526
|
+
|
|
527
|
+
path: str | None = None
|
|
528
|
+
version: str | None = None
|
|
529
|
+
audit_show_mode = action == "get" and resource == "audit" and bool(address)
|
|
530
|
+
if address and not (cast_list_mode or cast_show_mode or audit_show_mode):
|
|
531
|
+
parser = parse_simple_address if op_id in _NON_TREE_ADDRESS_OPS else parse_address
|
|
532
|
+
try:
|
|
533
|
+
path, version = parser(address, version_flag=version_flag)
|
|
534
|
+
except AddressError as e:
|
|
535
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
536
|
+
raise SystemExit(USAGE_ERROR)
|
|
537
|
+
elif cast_show_mode:
|
|
538
|
+
assert address is not None
|
|
539
|
+
try:
|
|
540
|
+
cast_format, version = parse_simple_address(
|
|
541
|
+
address,
|
|
542
|
+
version_flag=version_flag,
|
|
543
|
+
)
|
|
544
|
+
except AddressError as e:
|
|
545
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
546
|
+
raise SystemExit(USAGE_ERROR)
|
|
547
|
+
if version:
|
|
548
|
+
print(
|
|
549
|
+
"Error: cast format ADDRESS does not support @version or --version.",
|
|
550
|
+
file=sys.stderr,
|
|
551
|
+
)
|
|
552
|
+
raise SystemExit(USAGE_ERROR)
|
|
553
|
+
elif audit_show_mode:
|
|
554
|
+
assert address is not None
|
|
555
|
+
try:
|
|
556
|
+
path, version = parse_simple_address(
|
|
557
|
+
address,
|
|
558
|
+
version_flag=version_flag,
|
|
559
|
+
)
|
|
560
|
+
except AddressError as e:
|
|
561
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
562
|
+
raise SystemExit(USAGE_ERROR)
|
|
563
|
+
if version:
|
|
564
|
+
print(
|
|
565
|
+
"Error: audit event ADDRESS does not support @version or --version.",
|
|
566
|
+
file=sys.stderr,
|
|
567
|
+
)
|
|
568
|
+
raise SystemExit(USAGE_ERROR)
|
|
569
|
+
|
|
570
|
+
# Some operations use query-only parameters and ignore tree addresses.
|
|
571
|
+
if op_id in _NO_ADDRESS_OPS:
|
|
572
|
+
path = None
|
|
573
|
+
version = None
|
|
574
|
+
|
|
575
|
+
if path is None and address_required_for_op(
|
|
576
|
+
op_id,
|
|
577
|
+
action=action,
|
|
578
|
+
resource=resource,
|
|
579
|
+
):
|
|
580
|
+
print(
|
|
581
|
+
f"Error: ADDRESS is required for ocmo {action} {resource}.",
|
|
582
|
+
file=sys.stderr,
|
|
583
|
+
)
|
|
584
|
+
raise SystemExit(USAGE_ERROR)
|
|
585
|
+
|
|
586
|
+
merged_extra = dict(sdk_extra or {})
|
|
587
|
+
if list_mode:
|
|
588
|
+
from .._get_item_list import prepare_get_item_list_extra # deferred
|
|
589
|
+
|
|
590
|
+
merged_extra = prepare_get_item_list_extra(merged_extra, item_types=item_types)
|
|
591
|
+
if op_id == "list_item_versions":
|
|
592
|
+
from .._version_output import apply_version_address_query # deferred
|
|
593
|
+
|
|
594
|
+
apply_version_address_query(merged_extra, version)
|
|
595
|
+
version = None
|
|
596
|
+
gp_create_position: float | None = None
|
|
597
|
+
if op_id == "create_global_permission":
|
|
598
|
+
raw_position = merged_extra.pop("position", None)
|
|
599
|
+
if raw_position is not None:
|
|
600
|
+
gp_create_position = float(raw_position)
|
|
601
|
+
content: str | None = None
|
|
602
|
+
if file_path:
|
|
603
|
+
content = sys.stdin.read() if file_path == "-" else Path(file_path).read_text()
|
|
604
|
+
|
|
605
|
+
body_payload: dict[str, Any] | None = None
|
|
606
|
+
untag_mode = action == "untag" and op_id == "set_tag"
|
|
607
|
+
if untag_mode:
|
|
608
|
+
tag_name = merged_extra.pop("tag", None)
|
|
609
|
+
validate_tag_request("set_tag", action="untag", extra={"tag": tag_name})
|
|
610
|
+
body_payload = prepare_untag_body_payload(str(tag_name))
|
|
611
|
+
merged_extra = {}
|
|
612
|
+
elif op_id in BODY_PAYLOADS:
|
|
613
|
+
try:
|
|
614
|
+
body_payload = prepare_body_payload(
|
|
615
|
+
op_id,
|
|
616
|
+
address=path,
|
|
617
|
+
content=content,
|
|
618
|
+
extra=merged_extra,
|
|
619
|
+
address_version=version,
|
|
620
|
+
)
|
|
621
|
+
except ValueError as exc:
|
|
622
|
+
print(f"Error: {format_usage_error(exc)}", file=sys.stderr)
|
|
623
|
+
raise SystemExit(USAGE_ERROR)
|
|
624
|
+
if action == "create" or op_id == "replace_lock":
|
|
625
|
+
validate_create_request(op_id, address=path, payload=body_payload)
|
|
626
|
+
content = None
|
|
627
|
+
merged_extra = {}
|
|
628
|
+
|
|
629
|
+
if op_id == "set_tag":
|
|
630
|
+
validate_tag_request(op_id, payload=body_payload, extra=merged_extra)
|
|
631
|
+
|
|
632
|
+
delete_preview = False
|
|
633
|
+
if op_id == "delete_item":
|
|
634
|
+
delete_preview = bool(merged_extra.pop("preview", False))
|
|
635
|
+
|
|
636
|
+
if dry_run:
|
|
637
|
+
args, kwargs = build_sdk_call(
|
|
638
|
+
op_id,
|
|
639
|
+
path=sdk_path_for_body_payload(op_id, path, body_payload),
|
|
640
|
+
version=version,
|
|
641
|
+
content=content,
|
|
642
|
+
extra=merged_extra,
|
|
643
|
+
)
|
|
644
|
+
if body_payload is not None:
|
|
645
|
+
kwargs["body"] = body_payload
|
|
646
|
+
from .._dry_run import emit_dry_run_plan, format_generated_dry_run # deferred
|
|
647
|
+
|
|
648
|
+
display_ns = namespace if isinstance(namespace, str) and namespace else None
|
|
649
|
+
if display_ns is None and ctx is not None:
|
|
650
|
+
ctx_ns = getattr(ctx, "namespace", None)
|
|
651
|
+
if isinstance(ctx_ns, str) and ctx_ns:
|
|
652
|
+
display_ns = ctx_ns
|
|
653
|
+
client_scope = ops_meta.get(op_id, {}).get("scope") == "client"
|
|
654
|
+
emit_dry_run_plan(
|
|
655
|
+
format_generated_dry_run(
|
|
656
|
+
op_id=op_id,
|
|
657
|
+
action=action,
|
|
658
|
+
resource=resource,
|
|
659
|
+
path=path,
|
|
660
|
+
version=version,
|
|
661
|
+
namespace=display_ns,
|
|
662
|
+
args=args,
|
|
663
|
+
kwargs=kwargs,
|
|
664
|
+
client_scope=client_scope,
|
|
665
|
+
file_path=file_path,
|
|
666
|
+
gp_create_position=gp_create_position,
|
|
667
|
+
cast_format=cast_format,
|
|
668
|
+
)
|
|
669
|
+
)
|
|
670
|
+
return
|
|
671
|
+
|
|
672
|
+
if confirm_mode == "destructive" and not yes and not delete_preview:
|
|
673
|
+
confirm_message = _destructive_confirm_message(
|
|
674
|
+
op_id=op_id,
|
|
675
|
+
action=action,
|
|
676
|
+
resource=resource,
|
|
677
|
+
path=path,
|
|
678
|
+
version=version,
|
|
679
|
+
)
|
|
680
|
+
if not _confirm(confirm_message, yes=False):
|
|
681
|
+
status("Aborted.")
|
|
682
|
+
raise SystemExit(0)
|
|
683
|
+
|
|
684
|
+
args, kwargs = build_sdk_call(
|
|
685
|
+
op_id,
|
|
686
|
+
path=sdk_path_for_body_payload(op_id, path, body_payload),
|
|
687
|
+
version=version,
|
|
688
|
+
content=content,
|
|
689
|
+
extra=merged_extra,
|
|
690
|
+
)
|
|
691
|
+
if op_id == "delete_item":
|
|
692
|
+
kwargs["preview"] = delete_preview
|
|
693
|
+
if body_payload is not None:
|
|
694
|
+
kwargs["body"] = body_payload
|
|
695
|
+
result = _call_sdk_method(ctx, op_id, namespace, args, kwargs)
|
|
696
|
+
|
|
697
|
+
if op_id == "set_tag" and path is not None:
|
|
698
|
+
from .._version_output import emit_item_versions_output
|
|
699
|
+
|
|
700
|
+
ns_view = ctx.namespace_view(namespace)
|
|
701
|
+
emit_item_versions_output(
|
|
702
|
+
ns_view,
|
|
703
|
+
path,
|
|
704
|
+
version=version,
|
|
705
|
+
output_fmt=output_fmt,
|
|
706
|
+
ctx_fmt=ctx.output if ctx else None,
|
|
707
|
+
field=field,
|
|
708
|
+
)
|
|
709
|
+
return
|
|
710
|
+
|
|
711
|
+
if op_id == "delete_item" and result is not None:
|
|
712
|
+
from .._delete_item_output import emit_delete_item_output
|
|
713
|
+
|
|
714
|
+
if field:
|
|
715
|
+
data = sanitize_for_output(as_dict(result, fallback_vars=False) or result)
|
|
716
|
+
extract_field(data, field)
|
|
717
|
+
else:
|
|
718
|
+
emit_delete_item_output(
|
|
719
|
+
result,
|
|
720
|
+
target_path=path,
|
|
721
|
+
version=version,
|
|
722
|
+
output_fmt=output_fmt,
|
|
723
|
+
ctx_fmt=ctx.output if ctx else None,
|
|
724
|
+
)
|
|
725
|
+
return
|
|
726
|
+
|
|
727
|
+
if op_id == "rotate_resolver_token":
|
|
728
|
+
if result is None or not print_resolver_token(result):
|
|
729
|
+
print(
|
|
730
|
+
"Error: resolver token was not returned by the API.",
|
|
731
|
+
file=sys.stderr,
|
|
732
|
+
)
|
|
733
|
+
raise SystemExit(USAGE_ERROR)
|
|
734
|
+
return
|
|
735
|
+
|
|
736
|
+
if op_id == "create_global_permission" and gp_create_position is not None and result is not None:
|
|
737
|
+
rule_id = _created_resource_label(result)
|
|
738
|
+
current_position = getattr(result, "position", None)
|
|
739
|
+
if current_position != gp_create_position and rule_id is not None:
|
|
740
|
+
result = _call_sdk_method(
|
|
741
|
+
ctx,
|
|
742
|
+
"move_global_permission",
|
|
743
|
+
namespace,
|
|
744
|
+
[rule_id],
|
|
745
|
+
{"body": {"position": gp_create_position}},
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
if action in ("create", "update"):
|
|
749
|
+
label = path or _created_resource_label(result)
|
|
750
|
+
if label:
|
|
751
|
+
verb = "Created" if action == "create" else "Updated"
|
|
752
|
+
status(f"{verb} {resource} {label!r}.")
|
|
753
|
+
|
|
754
|
+
output_key = (
|
|
755
|
+
"get item list"
|
|
756
|
+
if list_mode
|
|
757
|
+
else "get cast list"
|
|
758
|
+
if cast_list_mode
|
|
759
|
+
else output_command_key(action, resource)
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
if result is not None and (cast_list_mode or cast_show_mode):
|
|
763
|
+
from .._get_cast_output import CastFormatNotFoundError, cast_list_rows, cast_show_payload
|
|
764
|
+
|
|
765
|
+
if cast_list_mode:
|
|
766
|
+
data = cast_list_rows(as_dict(result, fallback_vars=False) or result)
|
|
767
|
+
else:
|
|
768
|
+
try:
|
|
769
|
+
data = cast_show_payload(
|
|
770
|
+
as_dict(result, fallback_vars=False) or result,
|
|
771
|
+
cast_format or "",
|
|
772
|
+
)
|
|
773
|
+
except CastFormatNotFoundError as exc:
|
|
774
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
775
|
+
raise SystemExit(USAGE_ERROR) from exc
|
|
776
|
+
if field:
|
|
777
|
+
extract_field(data, field)
|
|
778
|
+
else:
|
|
779
|
+
emit_command_output(
|
|
780
|
+
output_key,
|
|
781
|
+
data,
|
|
782
|
+
output_fmt,
|
|
783
|
+
ctx_fmt=ctx.output if ctx else None,
|
|
784
|
+
)
|
|
785
|
+
return
|
|
786
|
+
|
|
787
|
+
if result is not None and uses_item_output(op_id, action, resource, result):
|
|
788
|
+
fmt = item_output_format(
|
|
789
|
+
output_fmt,
|
|
790
|
+
ctx.output if ctx else None,
|
|
791
|
+
command_key=output_key,
|
|
792
|
+
)
|
|
793
|
+
if field:
|
|
794
|
+
data = sanitize_for_output(as_dict(result, fallback_vars=False) or result)
|
|
795
|
+
extract_field(data, field)
|
|
796
|
+
else:
|
|
797
|
+
emit_item_result(
|
|
798
|
+
result,
|
|
799
|
+
fmt,
|
|
800
|
+
no_color=bool(ctx and ctx.no_color),
|
|
801
|
+
resource=resource,
|
|
802
|
+
)
|
|
803
|
+
if op_id == "create_resolver" and not item_output_includes_token_in_payload(fmt, field):
|
|
804
|
+
print_resolver_token(result)
|
|
805
|
+
return
|
|
806
|
+
|
|
807
|
+
if result is not None:
|
|
808
|
+
if isinstance(result, list):
|
|
809
|
+
data = [sanitize_for_output(as_dict(r)) for r in result]
|
|
810
|
+
else:
|
|
811
|
+
data = sanitize_for_output(as_dict(result, fallback_vars=False) or result)
|
|
812
|
+
|
|
813
|
+
if field:
|
|
814
|
+
extract_field(data, field)
|
|
815
|
+
else:
|
|
816
|
+
emit_command_output(
|
|
817
|
+
output_key,
|
|
818
|
+
data,
|
|
819
|
+
output_fmt,
|
|
820
|
+
ctx_fmt=ctx.output if ctx else None,
|
|
821
|
+
)
|
|
822
|
+
if op_id == "create_resolver" and not _structured_output_includes_token(
|
|
823
|
+
resolve_effective_format(
|
|
824
|
+
output_fmt,
|
|
825
|
+
ctx.output if ctx else None,
|
|
826
|
+
get_command_spec(output_key),
|
|
827
|
+
),
|
|
828
|
+
field,
|
|
829
|
+
):
|
|
830
|
+
print_resolver_token(result)
|
|
831
|
+
|
|
832
|
+
except KeyboardInterrupt:
|
|
833
|
+
raise SystemExit(130)
|
|
834
|
+
except SystemExit:
|
|
835
|
+
raise
|
|
836
|
+
except Exception as exc:
|
|
837
|
+
handle_sdk_error(exc)
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def _destructive_confirm_message(
|
|
841
|
+
*,
|
|
842
|
+
op_id: str,
|
|
843
|
+
action: str,
|
|
844
|
+
resource: str,
|
|
845
|
+
path: str | None,
|
|
846
|
+
version: str | None,
|
|
847
|
+
) -> str:
|
|
848
|
+
if op_id == "delete_item" and version:
|
|
849
|
+
return f"This will delete version {version} for item {path!r}. Continue?"
|
|
850
|
+
return f"This will {action} {resource} {path!r}. Continue?"
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def _created_resource_label(result: Any) -> str | None:
|
|
854
|
+
for key in ("name", "path", "id"):
|
|
855
|
+
value = getattr(result, key, None)
|
|
856
|
+
if value is not None and value != "":
|
|
857
|
+
return str(value)
|
|
858
|
+
return None
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def _call_sdk_method(
|
|
862
|
+
ctx: OcmoCtx,
|
|
863
|
+
op_id: str,
|
|
864
|
+
namespace: str | None,
|
|
865
|
+
args: list[Any],
|
|
866
|
+
kwargs: dict[str, Any],
|
|
867
|
+
) -> Any:
|
|
868
|
+
"""Dispatch to the correct SDK method based on operation scope."""
|
|
869
|
+
ops_yaml = _load_ops_yaml()
|
|
870
|
+
scope = ops_yaml.get(op_id, {}).get("scope", "namespace")
|
|
871
|
+
|
|
872
|
+
if scope == "client":
|
|
873
|
+
client = ctx.client()
|
|
874
|
+
method = getattr(client, op_id, None)
|
|
875
|
+
if method is None:
|
|
876
|
+
print(f"Error: SDK method {op_id!r} not found on client.", file=sys.stderr)
|
|
877
|
+
raise SystemExit(1)
|
|
878
|
+
return method(*args, **kwargs)
|
|
879
|
+
|
|
880
|
+
view = ctx.namespace_view(namespace)
|
|
881
|
+
method = getattr(view, op_id, None)
|
|
882
|
+
if method is None:
|
|
883
|
+
print(f"Error: SDK method {op_id!r} not found on namespace view.", file=sys.stderr)
|
|
884
|
+
raise SystemExit(1)
|
|
885
|
+
return method(*args, **kwargs)
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def _structured_output_includes_token(fmt: str, field: str | None) -> bool:
|
|
889
|
+
"""True when stdout already carries the resolver token in structured output."""
|
|
890
|
+
if field:
|
|
891
|
+
return field in ("token1", "token")
|
|
892
|
+
if fmt in ("yaml", "json", "table"):
|
|
893
|
+
return True
|
|
894
|
+
return fmt.startswith("jsonpath=token") or fmt.startswith("jsonpath=token1")
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def _load_ops_yaml() -> dict[str, Any]:
|
|
898
|
+
from pathlib import Path
|
|
899
|
+
|
|
900
|
+
import yaml # deferred
|
|
901
|
+
|
|
902
|
+
ops_path = Path(__file__).parent.parent.parent.parent / "sdk" / "operations.yaml"
|
|
903
|
+
if not ops_path.exists():
|
|
904
|
+
return {}
|
|
905
|
+
with ops_path.open() as f:
|
|
906
|
+
data = yaml.safe_load(f) or {}
|
|
907
|
+
return cast(dict[str, Any], data.get("operations", {}))
|