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,332 @@
|
|
|
1
|
+
"""Load output format manifest and drive CLI -o/--output behavior."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
from ._output import (
|
|
16
|
+
_as_table_rows,
|
|
17
|
+
_infer_table_columns,
|
|
18
|
+
emit,
|
|
19
|
+
emit_table,
|
|
20
|
+
)
|
|
21
|
+
from ._sdk_dispatch import unwrap_list_payload
|
|
22
|
+
from ._typing import ClickDecorator
|
|
23
|
+
|
|
24
|
+
_MANIFEST_PATH = Path(__file__).with_name("output_formats.yaml")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class FormatConfig:
|
|
29
|
+
fields: list[str] | None = None
|
|
30
|
+
infer_fields: bool = False
|
|
31
|
+
mode: str | None = None
|
|
32
|
+
hide_null_values: bool = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class CommandOutputSpec:
|
|
37
|
+
command_key: str
|
|
38
|
+
supported_formats: tuple[str, ...]
|
|
39
|
+
default_tty: str
|
|
40
|
+
default_non_tty: str
|
|
41
|
+
fixed_default: str | None
|
|
42
|
+
name_field: str
|
|
43
|
+
table: FormatConfig | None
|
|
44
|
+
wide: FormatConfig | None
|
|
45
|
+
raw: FormatConfig | None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def command_key(action: str, resource: str | None = None) -> str:
|
|
49
|
+
if resource:
|
|
50
|
+
return f"{action} {resource}"
|
|
51
|
+
return action
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@functools.lru_cache(maxsize=1)
|
|
55
|
+
def _load_manifest() -> dict[str, Any]:
|
|
56
|
+
with _MANIFEST_PATH.open(encoding="utf-8") as handle:
|
|
57
|
+
data = yaml.safe_load(handle)
|
|
58
|
+
if not isinstance(data, dict):
|
|
59
|
+
raise ValueError(f"Invalid manifest: {_MANIFEST_PATH}")
|
|
60
|
+
return data
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _merge_profile(entry: dict[str, Any], profiles: dict[str, Any]) -> dict[str, Any]:
|
|
64
|
+
profile_name = entry.get("profile")
|
|
65
|
+
merged: dict[str, Any] = {}
|
|
66
|
+
if profile_name:
|
|
67
|
+
profile = profiles.get(profile_name)
|
|
68
|
+
if isinstance(profile, dict):
|
|
69
|
+
merged.update(profile)
|
|
70
|
+
for key, value in entry.items():
|
|
71
|
+
if key != "profile":
|
|
72
|
+
merged[key] = value
|
|
73
|
+
return merged
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _parse_format_config(data: dict[str, Any] | None) -> FormatConfig | None:
|
|
77
|
+
if not data:
|
|
78
|
+
return None
|
|
79
|
+
raw_fields = data.get("fields")
|
|
80
|
+
infer = raw_fields == "*"
|
|
81
|
+
fields: list[str] | None
|
|
82
|
+
if isinstance(raw_fields, list):
|
|
83
|
+
fields = [str(item) for item in raw_fields]
|
|
84
|
+
else:
|
|
85
|
+
fields = None
|
|
86
|
+
return FormatConfig(
|
|
87
|
+
fields=fields,
|
|
88
|
+
infer_fields=infer,
|
|
89
|
+
mode=data.get("mode"),
|
|
90
|
+
hide_null_values=bool(data.get("hide_null_values")),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _build_spec(command_key_value: str, entry: dict[str, Any]) -> CommandOutputSpec:
|
|
95
|
+
supported = entry.get("supported_formats")
|
|
96
|
+
if not isinstance(supported, list) or not supported:
|
|
97
|
+
raise ValueError(f"Command {command_key_value!r} has no supported_formats")
|
|
98
|
+
|
|
99
|
+
default_tty = str(entry.get("default_tty") or entry.get("default") or "table")
|
|
100
|
+
default_non_tty = str(entry.get("default_non_tty") or entry.get("default") or "yaml")
|
|
101
|
+
fixed_default = str(entry["default"]) if "default" in entry else None
|
|
102
|
+
name_field = str(entry.get("name_field") or "name")
|
|
103
|
+
|
|
104
|
+
return CommandOutputSpec(
|
|
105
|
+
command_key=command_key_value,
|
|
106
|
+
supported_formats=tuple(str(fmt) for fmt in supported),
|
|
107
|
+
default_tty=default_tty,
|
|
108
|
+
default_non_tty=default_non_tty,
|
|
109
|
+
fixed_default=fixed_default,
|
|
110
|
+
name_field=name_field,
|
|
111
|
+
table=_parse_format_config(entry.get("table") if isinstance(entry.get("table"), dict) else None),
|
|
112
|
+
wide=_parse_format_config(entry.get("wide") if isinstance(entry.get("wide"), dict) else None),
|
|
113
|
+
raw=_parse_format_config(entry.get("raw") if isinstance(entry.get("raw"), dict) else None),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@functools.cache
|
|
118
|
+
def get_command_spec(key: str) -> CommandOutputSpec:
|
|
119
|
+
"""Return merged output spec for a command key."""
|
|
120
|
+
manifest = _load_manifest()
|
|
121
|
+
profiles = manifest.get("profiles", {})
|
|
122
|
+
commands = manifest.get("commands", {})
|
|
123
|
+
|
|
124
|
+
if key in commands and not key.startswith("_"):
|
|
125
|
+
entry = _merge_profile(dict(commands[key]), profiles)
|
|
126
|
+
return _build_spec(key, entry)
|
|
127
|
+
|
|
128
|
+
action = key.split(" ", 1)[0]
|
|
129
|
+
generated_defaults = commands.get("_generated_defaults", {})
|
|
130
|
+
if isinstance(generated_defaults, dict) and action in generated_defaults:
|
|
131
|
+
base = dict(generated_defaults[action])
|
|
132
|
+
entry = _merge_profile(base, profiles)
|
|
133
|
+
return _build_spec(key, entry)
|
|
134
|
+
|
|
135
|
+
entry = _merge_profile({"profile": "list_wide"}, profiles)
|
|
136
|
+
return _build_spec(key, entry)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def supported_formats_help(spec: CommandOutputSpec) -> str:
|
|
140
|
+
formats = [fmt for fmt in spec.supported_formats if fmt != "jsonpath"]
|
|
141
|
+
return ", ".join(formats)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def output_formats_help(spec: CommandOutputSpec) -> str:
|
|
145
|
+
"""Help text listing base formats plus jsonpath field extraction."""
|
|
146
|
+
listed = supported_formats_help(spec)
|
|
147
|
+
return f"{listed}, or jsonpath=<path>"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def default_format_help(spec: CommandOutputSpec) -> str:
|
|
151
|
+
if spec.fixed_default:
|
|
152
|
+
return spec.fixed_default
|
|
153
|
+
return f"{spec.default_tty} on TTY, {spec.default_non_tty} otherwise"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def output_option_help(spec: CommandOutputSpec) -> str:
|
|
157
|
+
return (
|
|
158
|
+
f"Output format: {output_formats_help(spec)} "
|
|
159
|
+
f"to extract properties by dot-path. "
|
|
160
|
+
f"Default: {default_format_help(spec)}."
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def is_valid_format(value: str, spec: CommandOutputSpec) -> bool:
|
|
165
|
+
return value in spec.supported_formats or value.startswith("jsonpath=")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def validate_output_format(
|
|
169
|
+
value: str | None,
|
|
170
|
+
spec: CommandOutputSpec,
|
|
171
|
+
*,
|
|
172
|
+
ctx: click.Context | None = None,
|
|
173
|
+
param: click.Parameter | None = None,
|
|
174
|
+
) -> str | None:
|
|
175
|
+
if value is None:
|
|
176
|
+
return value
|
|
177
|
+
if is_valid_format(value, spec):
|
|
178
|
+
return value
|
|
179
|
+
valid = output_formats_help(spec)
|
|
180
|
+
raise click.BadParameter(
|
|
181
|
+
f"{value!r} is not a valid output format for {spec.command_key!r}. " f"Valid values: {valid}.",
|
|
182
|
+
ctx=ctx,
|
|
183
|
+
param=param,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def resolve_effective_format(
|
|
188
|
+
cli_fmt: str | None,
|
|
189
|
+
ctx_fmt: str | None,
|
|
190
|
+
spec: CommandOutputSpec,
|
|
191
|
+
) -> str:
|
|
192
|
+
for candidate in (cli_fmt, ctx_fmt):
|
|
193
|
+
if candidate is None:
|
|
194
|
+
continue
|
|
195
|
+
if is_valid_format(candidate, spec):
|
|
196
|
+
return candidate
|
|
197
|
+
if spec.fixed_default:
|
|
198
|
+
return spec.fixed_default
|
|
199
|
+
env = os.environ.get("OCMO_OUTPUT")
|
|
200
|
+
if env:
|
|
201
|
+
if is_valid_format(env, spec):
|
|
202
|
+
return env
|
|
203
|
+
print(
|
|
204
|
+
f"Warning: OCMO_OUTPUT={env!r} is not valid for {spec.command_key!r}; ignored.",
|
|
205
|
+
file=sys.stderr,
|
|
206
|
+
)
|
|
207
|
+
return spec.default_tty if sys.stdout.isatty() else spec.default_non_tty
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _combined_command_spec(command_keys: tuple[str, ...]) -> CommandOutputSpec:
|
|
211
|
+
specs = [get_command_spec(key) for key in command_keys]
|
|
212
|
+
supported = tuple(dict.fromkeys(fmt for spec in specs for fmt in spec.supported_formats))
|
|
213
|
+
if not supported:
|
|
214
|
+
raise ValueError(f"No supported formats for command keys: {command_keys!r}")
|
|
215
|
+
primary = specs[0]
|
|
216
|
+
return CommandOutputSpec(
|
|
217
|
+
command_key=" / ".join(command_keys),
|
|
218
|
+
supported_formats=supported,
|
|
219
|
+
default_tty=primary.default_tty,
|
|
220
|
+
default_non_tty=primary.default_non_tty,
|
|
221
|
+
fixed_default=primary.fixed_default,
|
|
222
|
+
name_field=primary.name_field,
|
|
223
|
+
table=primary.table,
|
|
224
|
+
wide=primary.wide,
|
|
225
|
+
raw=primary.raw,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def manifest_output_option(command_key_value: str) -> ClickDecorator:
|
|
230
|
+
"""Standard -o / --output flag configured from the manifest."""
|
|
231
|
+
return manifest_output_option_for_keys(command_key_value)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def manifest_output_option_for_keys(*command_key_values: str) -> ClickDecorator:
|
|
235
|
+
"""``-o`` / ``--output`` flag validating against a union of command specs."""
|
|
236
|
+
spec = (
|
|
237
|
+
get_command_spec(command_key_values[0])
|
|
238
|
+
if len(command_key_values) == 1
|
|
239
|
+
else _combined_command_spec(command_key_values)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def _callback(
|
|
243
|
+
ctx: click.Context,
|
|
244
|
+
param: click.Parameter,
|
|
245
|
+
value: str | None,
|
|
246
|
+
) -> str | None:
|
|
247
|
+
return validate_output_format(value, spec, ctx=ctx, param=param)
|
|
248
|
+
|
|
249
|
+
return click.option(
|
|
250
|
+
"-o",
|
|
251
|
+
"--output",
|
|
252
|
+
"output_fmt",
|
|
253
|
+
default=None,
|
|
254
|
+
metavar="FORMAT",
|
|
255
|
+
help=output_option_help(spec),
|
|
256
|
+
callback=_callback,
|
|
257
|
+
is_eager=False,
|
|
258
|
+
expose_value=True,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def columns_for_format(
|
|
263
|
+
spec: CommandOutputSpec,
|
|
264
|
+
fmt: str,
|
|
265
|
+
rows: list[dict[str, Any]],
|
|
266
|
+
) -> list[str]:
|
|
267
|
+
config = spec.table if fmt == "table" else spec.wide if fmt == "wide" else None
|
|
268
|
+
if config is None:
|
|
269
|
+
return _infer_table_columns(rows)
|
|
270
|
+
if config.infer_fields or not config.fields:
|
|
271
|
+
return _infer_table_columns(rows)
|
|
272
|
+
return list(config.fields)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _rows_from_data(
|
|
276
|
+
data: Any,
|
|
277
|
+
*,
|
|
278
|
+
command_key: str | None = None,
|
|
279
|
+
wide: bool = False,
|
|
280
|
+
) -> list[dict[str, Any]]:
|
|
281
|
+
rows = unwrap_list_payload(data)
|
|
282
|
+
if rows is None:
|
|
283
|
+
rows = _as_table_rows(data)
|
|
284
|
+
if command_key == "get globalpermission":
|
|
285
|
+
from ._globalpermission_output import global_permission_table_rows
|
|
286
|
+
|
|
287
|
+
return global_permission_table_rows(rows, wide=wide)
|
|
288
|
+
if command_key == "whoami":
|
|
289
|
+
from ._whoami_output import whoami_table_rows
|
|
290
|
+
|
|
291
|
+
return whoami_table_rows(data)
|
|
292
|
+
if command_key == "resolve parameters":
|
|
293
|
+
from ._resolve_parameters_output import resolve_parameters_table_rows
|
|
294
|
+
|
|
295
|
+
return resolve_parameters_table_rows(data)
|
|
296
|
+
return rows
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def emit_command_output(
|
|
300
|
+
command_key_value: str,
|
|
301
|
+
data: Any,
|
|
302
|
+
fmt: str | None,
|
|
303
|
+
*,
|
|
304
|
+
ctx_fmt: str | None = None,
|
|
305
|
+
columns_override: list[str] | None = None,
|
|
306
|
+
) -> None:
|
|
307
|
+
"""Emit command result using manifest-driven format resolution."""
|
|
308
|
+
from ._output import _emit_identifiers
|
|
309
|
+
|
|
310
|
+
spec = get_command_spec(command_key_value)
|
|
311
|
+
effective_fmt = resolve_effective_format(fmt, ctx_fmt, spec)
|
|
312
|
+
|
|
313
|
+
if effective_fmt in ("table", "wide"):
|
|
314
|
+
rows = _rows_from_data(
|
|
315
|
+
data,
|
|
316
|
+
command_key=command_key_value,
|
|
317
|
+
wide=effective_fmt == "wide",
|
|
318
|
+
)
|
|
319
|
+
columns = columns_override or columns_for_format(spec, effective_fmt, rows)
|
|
320
|
+
emit_table(rows, columns)
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
if effective_fmt == "name":
|
|
324
|
+
rows = _rows_from_data(data, command_key=command_key_value)
|
|
325
|
+
_emit_identifiers(rows if rows else data, spec.name_field)
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
emit(data, effective_fmt)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def uses_document_profile(command_key_value: str) -> bool:
|
|
332
|
+
return "raw" in get_command_spec(command_key_value).supported_formats
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Resolve-draft API call returning a lazy :class:`ocmo.resolve.ResolveResult`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def call_resolve_draft(
|
|
9
|
+
view: Any,
|
|
10
|
+
path: str,
|
|
11
|
+
*,
|
|
12
|
+
content: str,
|
|
13
|
+
cast: str | None = None,
|
|
14
|
+
trace_only: bool = False,
|
|
15
|
+
no_creds: bool = False,
|
|
16
|
+
params: dict[str, str] | None = None,
|
|
17
|
+
cast_options: dict[str, str] | None = None,
|
|
18
|
+
) -> Any:
|
|
19
|
+
"""POST draft YAML to ``~resolve-draft`` and build a :class:`ResolveResult`."""
|
|
20
|
+
from ocmo.client import _build_params, _encode_resolve_path
|
|
21
|
+
from ocmo.resolve import build_resolve_result
|
|
22
|
+
|
|
23
|
+
client = view._client
|
|
24
|
+
wire_cast = "json" if cast == "python" else cast
|
|
25
|
+
query = _build_params(
|
|
26
|
+
params=params,
|
|
27
|
+
cast_options=cast_options,
|
|
28
|
+
extra={
|
|
29
|
+
"cast": wire_cast,
|
|
30
|
+
"trace_only": str(trace_only).lower() if trace_only else None,
|
|
31
|
+
"no-creds": str(no_creds).lower() if no_creds else None,
|
|
32
|
+
},
|
|
33
|
+
)
|
|
34
|
+
url = f"/ns/{view._namespace}/~resolve-draft/{_encode_resolve_path(path)}"
|
|
35
|
+
resp = client._transport.request(
|
|
36
|
+
"POST",
|
|
37
|
+
url,
|
|
38
|
+
params=query,
|
|
39
|
+
content=content,
|
|
40
|
+
headers={"Content-Type": "application/yaml"},
|
|
41
|
+
)
|
|
42
|
+
cache_status = resp.headers.get("X-Ocmo-Resolve-Cache")
|
|
43
|
+
return build_resolve_result(
|
|
44
|
+
resp.json(),
|
|
45
|
+
http=client._http,
|
|
46
|
+
transport=client._transport,
|
|
47
|
+
server_origin=client._config.server,
|
|
48
|
+
cast=cast,
|
|
49
|
+
cache_status=cache_status,
|
|
50
|
+
)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Shared Click options for ``ocmo resolve`` and ``ocmo resolve draft``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from ._options import resolve_output_option
|
|
11
|
+
from ._typing import ClickDecorator
|
|
12
|
+
|
|
13
|
+
_HOOK_TIMEOUT_DEFAULT = 60
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def resolve_options(
|
|
17
|
+
*,
|
|
18
|
+
include_version: bool = True,
|
|
19
|
+
file_required: bool = False,
|
|
20
|
+
output_command_key: str = "resolve",
|
|
21
|
+
) -> ClickDecorator:
|
|
22
|
+
"""Click decorator factory for shared resolve flags."""
|
|
23
|
+
|
|
24
|
+
def decorator(cmd: Callable[..., Any]) -> Callable[..., Any]:
|
|
25
|
+
return add_resolve_options(
|
|
26
|
+
cmd,
|
|
27
|
+
include_version=include_version,
|
|
28
|
+
file_required=file_required,
|
|
29
|
+
output_command_key=output_command_key,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return decorator
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def add_resolve_options(
|
|
36
|
+
cmd: Callable[..., Any],
|
|
37
|
+
*,
|
|
38
|
+
include_version: bool = True,
|
|
39
|
+
file_required: bool = False,
|
|
40
|
+
output_command_key: str = "resolve",
|
|
41
|
+
) -> Callable[..., Any]:
|
|
42
|
+
"""Attach resolve output, cast, filesystem, hook, and optional draft-file flags."""
|
|
43
|
+
cmd = click.option(
|
|
44
|
+
"--print-hooks",
|
|
45
|
+
"print_hooks",
|
|
46
|
+
is_flag=True,
|
|
47
|
+
default=False,
|
|
48
|
+
help="Print effective hook commands and exit without resolving.",
|
|
49
|
+
)(cmd)
|
|
50
|
+
cmd = click.option(
|
|
51
|
+
"--trust-hooks",
|
|
52
|
+
"trust_hooks_sha",
|
|
53
|
+
default=None,
|
|
54
|
+
metavar="SHA256",
|
|
55
|
+
help="Trust a specific hook configuration SHA-256 (for unattended pipelines).",
|
|
56
|
+
)(cmd)
|
|
57
|
+
cmd = click.option(
|
|
58
|
+
"--hook-timeout",
|
|
59
|
+
default=_HOOK_TIMEOUT_DEFAULT,
|
|
60
|
+
show_default=True,
|
|
61
|
+
help="Hook execution timeout in seconds.",
|
|
62
|
+
)(cmd)
|
|
63
|
+
cmd = click.option(
|
|
64
|
+
"--exec-hooks",
|
|
65
|
+
"exec_hooks",
|
|
66
|
+
is_flag=True,
|
|
67
|
+
default=False,
|
|
68
|
+
help="Execute resolver hooks (default: off; see OCMO_EXEC_HOOKS).",
|
|
69
|
+
)(cmd)
|
|
70
|
+
if include_version:
|
|
71
|
+
cmd = click.option(
|
|
72
|
+
"--version",
|
|
73
|
+
"-V",
|
|
74
|
+
"version_flag",
|
|
75
|
+
default=None,
|
|
76
|
+
help="Version / tag to resolve.",
|
|
77
|
+
)(cmd)
|
|
78
|
+
cmd = click.option(
|
|
79
|
+
"--property",
|
|
80
|
+
"prop_path",
|
|
81
|
+
default=None,
|
|
82
|
+
metavar="PATH",
|
|
83
|
+
help=("Extract a json/yaml property (dot notation); with -O/--output-dir " "writes only that value."),
|
|
84
|
+
)(cmd)
|
|
85
|
+
cmd = click.option(
|
|
86
|
+
"--trace-only",
|
|
87
|
+
"trace_only",
|
|
88
|
+
is_flag=True,
|
|
89
|
+
default=False,
|
|
90
|
+
help="Print resolve trace metadata only; perform no artifact downloads.",
|
|
91
|
+
)(cmd)
|
|
92
|
+
cmd = click.option(
|
|
93
|
+
"--skip-existing",
|
|
94
|
+
is_flag=True,
|
|
95
|
+
default=False,
|
|
96
|
+
help="Skip existing files that differ instead of failing.",
|
|
97
|
+
)(cmd)
|
|
98
|
+
cmd = click.option(
|
|
99
|
+
"--rewrite",
|
|
100
|
+
is_flag=True,
|
|
101
|
+
default=False,
|
|
102
|
+
help="Overwrite existing files that differ from the resolved content.",
|
|
103
|
+
)(cmd)
|
|
104
|
+
cmd = click.option(
|
|
105
|
+
"--output-dir",
|
|
106
|
+
default=None,
|
|
107
|
+
metavar="DIR",
|
|
108
|
+
help="Write each item to DIR/<item.name>. Required for folder resolve.",
|
|
109
|
+
)(cmd)
|
|
110
|
+
cmd = click.option(
|
|
111
|
+
"-O",
|
|
112
|
+
"--output-file",
|
|
113
|
+
"output_file",
|
|
114
|
+
default=None,
|
|
115
|
+
metavar="FILE",
|
|
116
|
+
help="Write single resolved item to FILE.",
|
|
117
|
+
)(cmd)
|
|
118
|
+
cmd = click.option(
|
|
119
|
+
"--cast-option",
|
|
120
|
+
"cast_options",
|
|
121
|
+
multiple=True,
|
|
122
|
+
metavar="KEY=VALUE",
|
|
123
|
+
help="Cast options. May be repeated.",
|
|
124
|
+
)(cmd)
|
|
125
|
+
cmd = click.option(
|
|
126
|
+
"--param",
|
|
127
|
+
"params",
|
|
128
|
+
multiple=True,
|
|
129
|
+
metavar="KEY=VALUE",
|
|
130
|
+
help="Dynamic resolve parameters. May be repeated.",
|
|
131
|
+
)(cmd)
|
|
132
|
+
cmd = click.option("--cast", default=None, help="Cast format (json, yaml, raw, …).")(cmd)
|
|
133
|
+
cmd = resolve_output_option(output_command_key)(cmd)
|
|
134
|
+
if file_required:
|
|
135
|
+
cmd = click.option(
|
|
136
|
+
"-f",
|
|
137
|
+
"--file",
|
|
138
|
+
"file_path",
|
|
139
|
+
required=True,
|
|
140
|
+
metavar="FILE|-",
|
|
141
|
+
help="Draft config YAML from file ('-' for stdin).",
|
|
142
|
+
)(cmd)
|
|
143
|
+
return cmd
|