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,102 @@
|
|
|
1
|
+
"""ocmo resolve draft — resolve unsaved config YAML without persisting it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from .._address import parse_simple_address_or_exit, reject_version
|
|
11
|
+
from .._client import OcmoCtx
|
|
12
|
+
from .._errors import sdk_command
|
|
13
|
+
from .._options import namespace_option
|
|
14
|
+
from .._resolve_draft import call_resolve_draft
|
|
15
|
+
from .._resolve_options import resolve_options
|
|
16
|
+
|
|
17
|
+
_DRAFT_HELP = """\
|
|
18
|
+
Resolve unsaved draft YAML at ADDRESS without persisting it.
|
|
19
|
+
|
|
20
|
+
Uses the same output, cast, parameter, filesystem, and hook options as
|
|
21
|
+
``ocmo resolve``. Draft content is read from ``-f/--file`` (or stdin via ``-``).
|
|
22
|
+
|
|
23
|
+
\b
|
|
24
|
+
Examples:
|
|
25
|
+
ocmo -n prod resolve draft app/web -f draft.yaml
|
|
26
|
+
ocmo -n prod resolve draft app/web -f - --cast json < draft.yaml
|
|
27
|
+
ocmo -n prod resolve draft app/web -f draft.yaml --trace-only -o yaml
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.command("draft", help=_DRAFT_HELP)
|
|
32
|
+
@click.argument("address")
|
|
33
|
+
@namespace_option()
|
|
34
|
+
@resolve_options(include_version=False, file_required=True)
|
|
35
|
+
@click.pass_obj
|
|
36
|
+
@sdk_command
|
|
37
|
+
def resolve_draft_cmd(
|
|
38
|
+
ctx: OcmoCtx,
|
|
39
|
+
address: str,
|
|
40
|
+
namespace: str | None,
|
|
41
|
+
output_fmt: str | None,
|
|
42
|
+
cast: str | None,
|
|
43
|
+
params: tuple[str, ...],
|
|
44
|
+
cast_options: tuple[str, ...],
|
|
45
|
+
output_file: str | None,
|
|
46
|
+
output_dir: str | None,
|
|
47
|
+
rewrite: bool,
|
|
48
|
+
skip_existing: bool,
|
|
49
|
+
trace_only: bool,
|
|
50
|
+
prop_path: str | None,
|
|
51
|
+
exec_hooks: bool,
|
|
52
|
+
hook_timeout: int,
|
|
53
|
+
trust_hooks_sha: str | None,
|
|
54
|
+
print_hooks: bool,
|
|
55
|
+
file_path: str,
|
|
56
|
+
) -> None:
|
|
57
|
+
from .resolve import _parse_kv_args, _print_hooks, run_resolve_pipeline
|
|
58
|
+
|
|
59
|
+
path, version = parse_simple_address_or_exit(address)
|
|
60
|
+
reject_version(version, command="resolve draft")
|
|
61
|
+
|
|
62
|
+
content = sys.stdin.read() if file_path == "-" else Path(file_path).read_text()
|
|
63
|
+
|
|
64
|
+
params_dict = _parse_kv_args(params)
|
|
65
|
+
cast_options_dict = _parse_kv_args(cast_options)
|
|
66
|
+
|
|
67
|
+
view = ctx.namespace_view(namespace)
|
|
68
|
+
ns = ctx.require_namespace(namespace)
|
|
69
|
+
|
|
70
|
+
if print_hooks:
|
|
71
|
+
trace_only = True
|
|
72
|
+
|
|
73
|
+
result = call_resolve_draft(
|
|
74
|
+
view,
|
|
75
|
+
path,
|
|
76
|
+
content=content,
|
|
77
|
+
cast=cast,
|
|
78
|
+
trace_only=trace_only,
|
|
79
|
+
params=params_dict or None,
|
|
80
|
+
cast_options=cast_options_dict or None,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if print_hooks:
|
|
84
|
+
_print_hooks(result)
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
run_resolve_pipeline(
|
|
88
|
+
ctx,
|
|
89
|
+
result,
|
|
90
|
+
path=path,
|
|
91
|
+
ns=ns,
|
|
92
|
+
output_fmt=output_fmt,
|
|
93
|
+
output_file=output_file,
|
|
94
|
+
output_dir=output_dir,
|
|
95
|
+
rewrite=rewrite,
|
|
96
|
+
skip_existing=skip_existing,
|
|
97
|
+
trace_only=trace_only,
|
|
98
|
+
prop_path=prop_path,
|
|
99
|
+
exec_hooks=exec_hooks,
|
|
100
|
+
hook_timeout=hook_timeout,
|
|
101
|
+
trust_hooks_sha=trust_hooks_sha,
|
|
102
|
+
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""ocmo resolve parameters — effective parameter values for a config."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from .._address import parse_address_or_exit
|
|
10
|
+
from .._client import OcmoCtx
|
|
11
|
+
from .._errors import sdk_command
|
|
12
|
+
from .._options import namespace_option, output_option
|
|
13
|
+
from .._output_manifest import emit_command_output
|
|
14
|
+
from .._resolve_parameters_output import filter_parameters_data
|
|
15
|
+
|
|
16
|
+
_COMMAND_KEY = "resolve parameters"
|
|
17
|
+
|
|
18
|
+
_PARAMETER_TYPE_CHOICES = ("projected", "dynamic", "secret")
|
|
19
|
+
|
|
20
|
+
_PARAMETERS_HELP = """\
|
|
21
|
+
Show effective parameter values for a config (debug).
|
|
22
|
+
|
|
23
|
+
\b
|
|
24
|
+
Examples:
|
|
25
|
+
ocmo -n prod resolve parameters app/web
|
|
26
|
+
ocmo -n prod resolve parameters app/web@stable -o yaml
|
|
27
|
+
ocmo -n prod resolve parameters app/web --type secret
|
|
28
|
+
ocmo -n prod resolve parameters app/web --type projected --type dynamic
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@click.command("parameters", help=_PARAMETERS_HELP)
|
|
33
|
+
@click.argument("address")
|
|
34
|
+
@namespace_option()
|
|
35
|
+
@output_option(_COMMAND_KEY)
|
|
36
|
+
@click.option("--version", "-V", "version_flag", default=None, help="Version / tag to resolve.")
|
|
37
|
+
@click.option(
|
|
38
|
+
"--type",
|
|
39
|
+
"param_types",
|
|
40
|
+
multiple=True,
|
|
41
|
+
type=click.Choice(_PARAMETER_TYPE_CHOICES, case_sensitive=False),
|
|
42
|
+
help="Filter parameters by type (repeatable).",
|
|
43
|
+
)
|
|
44
|
+
@click.pass_obj
|
|
45
|
+
@sdk_command
|
|
46
|
+
def resolve_parameters_cmd(
|
|
47
|
+
ctx: OcmoCtx,
|
|
48
|
+
address: str,
|
|
49
|
+
namespace: str | None,
|
|
50
|
+
output_fmt: str | None,
|
|
51
|
+
version_flag: str | None,
|
|
52
|
+
param_types: tuple[str, ...],
|
|
53
|
+
) -> None:
|
|
54
|
+
path, version = parse_address_or_exit(address, version_flag=version_flag)
|
|
55
|
+
|
|
56
|
+
view = ctx.namespace_view(namespace)
|
|
57
|
+
|
|
58
|
+
kwargs: dict[str, Any] = {}
|
|
59
|
+
if version:
|
|
60
|
+
kwargs["version"] = version
|
|
61
|
+
|
|
62
|
+
result = view.resolve_parameters(path, **kwargs)
|
|
63
|
+
data = filter_parameters_data(result, param_types)
|
|
64
|
+
|
|
65
|
+
emit_command_output(_COMMAND_KEY, data, output_fmt, ctx_fmt=ctx.output)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""ocmo resolve-series audit — time-bucketed resolve statistics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from .._audit_item import resolve_audit_item
|
|
8
|
+
from .._client import OcmoCtx
|
|
9
|
+
from .._errors import sdk_command
|
|
10
|
+
from .._exit import USAGE_ERROR
|
|
11
|
+
from .._options import namespace_option, output_option
|
|
12
|
+
from .._output import emit, err
|
|
13
|
+
from .._output_manifest import emit_command_output, get_command_spec, resolve_effective_format
|
|
14
|
+
from .._resolve_series import resolve_bucket_seconds, resolve_time_range, series_to_dict
|
|
15
|
+
from .._resolve_series_chart import print_resolve_series_chart
|
|
16
|
+
|
|
17
|
+
_COMMAND_KEY = "resolve-series audit"
|
|
18
|
+
|
|
19
|
+
_AUDIT_HELP = """\
|
|
20
|
+
Show time-bucketed direct, nested, and error resolve counts for a tree item.
|
|
21
|
+
|
|
22
|
+
\b
|
|
23
|
+
Examples:
|
|
24
|
+
ocmo -n prod resolve-series audit app/web
|
|
25
|
+
ocmo -n prod resolve-series audit app/web -o json
|
|
26
|
+
ocmo -n prod resolve-series audit app/web --from 2026-07-01T00:00:00Z
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@click.group("resolve-series", help="Resolve statistics over time.")
|
|
31
|
+
def resolve_series_group() -> None:
|
|
32
|
+
"""Resolve-series commands."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@resolve_series_group.command("audit", help=_AUDIT_HELP)
|
|
36
|
+
@click.argument("address")
|
|
37
|
+
@click.option(
|
|
38
|
+
"--from",
|
|
39
|
+
"from_value",
|
|
40
|
+
default=None,
|
|
41
|
+
metavar="ISO",
|
|
42
|
+
help="Range start (ISO-8601 UTC). Default: 30 days before --to.",
|
|
43
|
+
)
|
|
44
|
+
@click.option(
|
|
45
|
+
"--to",
|
|
46
|
+
"to_value",
|
|
47
|
+
default=None,
|
|
48
|
+
metavar="ISO",
|
|
49
|
+
help="Range end (ISO-8601 UTC). Default: now.",
|
|
50
|
+
)
|
|
51
|
+
@click.option(
|
|
52
|
+
"--bucket-seconds",
|
|
53
|
+
type=int,
|
|
54
|
+
default=None,
|
|
55
|
+
help="Bucket size in seconds (minimum 1800). Default: auto from range.",
|
|
56
|
+
)
|
|
57
|
+
@namespace_option()
|
|
58
|
+
@output_option(_COMMAND_KEY)
|
|
59
|
+
@click.pass_obj
|
|
60
|
+
@sdk_command
|
|
61
|
+
def resolve_series_audit_cmd(
|
|
62
|
+
ctx: OcmoCtx,
|
|
63
|
+
address: str,
|
|
64
|
+
from_value: str | None,
|
|
65
|
+
to_value: str | None,
|
|
66
|
+
bucket_seconds: int | None,
|
|
67
|
+
namespace: str | None,
|
|
68
|
+
output_fmt: str | None,
|
|
69
|
+
) -> None:
|
|
70
|
+
view, path, node_type = resolve_audit_item(
|
|
71
|
+
ctx,
|
|
72
|
+
address,
|
|
73
|
+
namespace,
|
|
74
|
+
command="resolve-series audit",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
start, end = resolve_time_range(from_value, to_value)
|
|
79
|
+
bucket = resolve_bucket_seconds(start, end, override=bucket_seconds)
|
|
80
|
+
except ValueError as exc:
|
|
81
|
+
err(str(exc))
|
|
82
|
+
raise SystemExit(USAGE_ERROR) from exc
|
|
83
|
+
|
|
84
|
+
result = view.namespace_audit_resolve_series(
|
|
85
|
+
object_id=path,
|
|
86
|
+
object_type=node_type,
|
|
87
|
+
from_=start,
|
|
88
|
+
to=end,
|
|
89
|
+
bucket_seconds=bucket,
|
|
90
|
+
)
|
|
91
|
+
data = series_to_dict(result)
|
|
92
|
+
|
|
93
|
+
spec = get_command_spec(_COMMAND_KEY)
|
|
94
|
+
effective_fmt = resolve_effective_format(output_fmt, ctx.output, spec)
|
|
95
|
+
|
|
96
|
+
if effective_fmt == "chart":
|
|
97
|
+
print_resolve_series_chart(
|
|
98
|
+
data,
|
|
99
|
+
node_type=node_type,
|
|
100
|
+
range_start=start,
|
|
101
|
+
range_end=end,
|
|
102
|
+
)
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
if effective_fmt in ("json", "yaml"):
|
|
106
|
+
emit(data, effective_fmt)
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
emit_command_output(_COMMAND_KEY, data, output_fmt, ctx_fmt=ctx.output)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""ocmo schema — render JSON Schemas for OCMO metadata and resource configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from .._client import OcmoCtx
|
|
12
|
+
from .._errors import sdk_command
|
|
13
|
+
from .._options import namespace_option, output_option
|
|
14
|
+
from .._output import as_dict
|
|
15
|
+
from .._output_manifest import emit_command_output
|
|
16
|
+
|
|
17
|
+
_VALID_RESOURCES = "ocmo, config, resolver"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class _SchemaSpec:
|
|
22
|
+
aliases: tuple[str, ...]
|
|
23
|
+
requires_address: bool
|
|
24
|
+
forbids_address: bool
|
|
25
|
+
usage_error: str
|
|
26
|
+
fetch: Callable[[OcmoCtx, str | None, str | None], Any]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _fetch_ocmo_schema(ctx: OcmoCtx, _namespace: str | None, _address: str | None) -> Any:
|
|
30
|
+
return ctx.client().get_config_metadata_schema() # type: ignore[no-untyped-call]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _fetch_resolver_schema(ctx: OcmoCtx, _namespace: str | None, _address: str | None) -> Any:
|
|
34
|
+
return ctx.client().get_resolver_configuration_schema() # type: ignore[no-untyped-call]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _fetch_config_schema(ctx: OcmoCtx, namespace: str | None, address: str | None) -> Any:
|
|
38
|
+
if not address:
|
|
39
|
+
raise click.UsageError(
|
|
40
|
+
"ADDRESS is required for RESOURCE 'config'. " "Use: ocmo -n <namespace> schema config <path>"
|
|
41
|
+
)
|
|
42
|
+
return ctx.namespace_view(namespace).get_config_data_schema(path=address)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_SCHEMA_DISPATCH: dict[str, _SchemaSpec] = {
|
|
46
|
+
"ocmo": _SchemaSpec(
|
|
47
|
+
aliases=("ocmo",),
|
|
48
|
+
requires_address=False,
|
|
49
|
+
forbids_address=True,
|
|
50
|
+
usage_error="ADDRESS is not used with RESOURCE 'ocmo'. Use: ocmo schema ocmo",
|
|
51
|
+
fetch=_fetch_ocmo_schema,
|
|
52
|
+
),
|
|
53
|
+
"config": _SchemaSpec(
|
|
54
|
+
aliases=("config", "cfg", "configs"),
|
|
55
|
+
requires_address=True,
|
|
56
|
+
forbids_address=False,
|
|
57
|
+
usage_error="",
|
|
58
|
+
fetch=_fetch_config_schema,
|
|
59
|
+
),
|
|
60
|
+
"resolver": _SchemaSpec(
|
|
61
|
+
aliases=("resolver", "rsv", "resolvers"),
|
|
62
|
+
requires_address=False,
|
|
63
|
+
forbids_address=True,
|
|
64
|
+
usage_error="ADDRESS is not used with RESOURCE 'resolver'. Use: ocmo schema resolver",
|
|
65
|
+
fetch=_fetch_resolver_schema,
|
|
66
|
+
),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_SCHEMA_LOOKUP: dict[str, _SchemaSpec] = {alias: spec for spec in _SCHEMA_DISPATCH.values() for alias in spec.aliases}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@click.command("schema", help="Render the JSON Schema for a resource type.")
|
|
73
|
+
@click.argument("resource_type", metavar="RESOURCE", required=False)
|
|
74
|
+
@click.argument("address", required=False)
|
|
75
|
+
@namespace_option()
|
|
76
|
+
@output_option("schema")
|
|
77
|
+
@click.pass_obj
|
|
78
|
+
@sdk_command
|
|
79
|
+
def schema_cmd(
|
|
80
|
+
ctx: OcmoCtx,
|
|
81
|
+
resource_type: str | None,
|
|
82
|
+
address: str | None,
|
|
83
|
+
namespace: str | None,
|
|
84
|
+
output_fmt: str | None,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Render the JSON Schema for a resource type.
|
|
87
|
+
|
|
88
|
+
\b
|
|
89
|
+
Resource types:
|
|
90
|
+
ocmo — ``_ocmo`` metadata block schema
|
|
91
|
+
config / cfg — config data schema (requires ADDRESS and -n)
|
|
92
|
+
resolver / rsv — resolver configuration schema
|
|
93
|
+
|
|
94
|
+
\b
|
|
95
|
+
Examples:
|
|
96
|
+
ocmo schema ocmo # _ocmo block schema
|
|
97
|
+
ocmo -n prod schema config app/web # data schema for a specific config
|
|
98
|
+
ocmo schema resolver # resolver configuration schema
|
|
99
|
+
"""
|
|
100
|
+
if not resource_type:
|
|
101
|
+
raise click.UsageError(f"RESOURCE type is required. Valid types: {_VALID_RESOURCES}")
|
|
102
|
+
|
|
103
|
+
spec = _SCHEMA_LOOKUP.get(resource_type.lower())
|
|
104
|
+
if spec is None:
|
|
105
|
+
raise click.UsageError(f"Unknown resource type {resource_type!r}. Valid: {_VALID_RESOURCES}")
|
|
106
|
+
|
|
107
|
+
if spec.forbids_address and address:
|
|
108
|
+
raise click.UsageError(spec.usage_error)
|
|
109
|
+
|
|
110
|
+
schema = spec.fetch(ctx, namespace, address)
|
|
111
|
+
data = as_dict(schema)
|
|
112
|
+
emit_command_output("schema", data, output_fmt, ctx_fmt=ctx.output)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""ocmo timeline audit — item modification history as human-readable notes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from .._audit_item import resolve_audit_item
|
|
11
|
+
from .._client import OcmoCtx
|
|
12
|
+
from .._errors import sdk_command
|
|
13
|
+
from .._exit import USAGE_ERROR
|
|
14
|
+
from .._options import namespace_option
|
|
15
|
+
from .._output import as_dict, err, format_datetime
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ocmo import NamespaceView
|
|
19
|
+
|
|
20
|
+
_TIMELINE_PAGE_SIZE = 100
|
|
21
|
+
|
|
22
|
+
_AUDIT_HELP = """\
|
|
23
|
+
Show modification history for a tree item as human-readable notes.
|
|
24
|
+
|
|
25
|
+
\b
|
|
26
|
+
Examples:
|
|
27
|
+
ocmo -n prod timeline audit app/web
|
|
28
|
+
ocmo -n prod timeline audit app/web --search stable
|
|
29
|
+
ocmo -n prod timeline audit app/web --limit 10
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _reject_output_format(ctx: OcmoCtx) -> None:
|
|
34
|
+
if ctx.output:
|
|
35
|
+
raise click.UsageError("timeline audit does not support -o/--output or OCMO_OUTPUT for this command.")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _fetch_timeline_entries(
|
|
39
|
+
view: NamespaceView,
|
|
40
|
+
*,
|
|
41
|
+
path: str,
|
|
42
|
+
node_type: str,
|
|
43
|
+
search: str | None,
|
|
44
|
+
limit: int | None = None,
|
|
45
|
+
) -> list[Any]:
|
|
46
|
+
entries: list[Any] = []
|
|
47
|
+
offset = 0
|
|
48
|
+
while True:
|
|
49
|
+
if limit is not None:
|
|
50
|
+
remaining = limit - len(entries)
|
|
51
|
+
if remaining <= 0:
|
|
52
|
+
break
|
|
53
|
+
page_limit = min(_TIMELINE_PAGE_SIZE, remaining)
|
|
54
|
+
else:
|
|
55
|
+
page_limit = _TIMELINE_PAGE_SIZE
|
|
56
|
+
|
|
57
|
+
page = view.namespace_audit_timeline(
|
|
58
|
+
object_id=path,
|
|
59
|
+
object_type=node_type,
|
|
60
|
+
search=search,
|
|
61
|
+
limit=page_limit,
|
|
62
|
+
offset=offset,
|
|
63
|
+
)
|
|
64
|
+
items = list(getattr(page, "items", []) or [])
|
|
65
|
+
entries.extend(items)
|
|
66
|
+
if limit is not None and len(entries) >= limit:
|
|
67
|
+
return entries[:limit]
|
|
68
|
+
total = int(getattr(page, "count", len(entries)))
|
|
69
|
+
if not items or offset + len(items) >= total:
|
|
70
|
+
break
|
|
71
|
+
offset += len(items)
|
|
72
|
+
return entries
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _entry_message(entry: Any) -> tuple[str, str]:
|
|
76
|
+
data = as_dict(entry)
|
|
77
|
+
occurred_at = format_datetime(data.get("occurred_at"))
|
|
78
|
+
message = str(data.get("message") or "").strip()
|
|
79
|
+
return occurred_at, message
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@click.group("timeline", help="Item-scoped audit timelines.")
|
|
83
|
+
def timeline_group() -> None:
|
|
84
|
+
"""Timeline commands."""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@timeline_group.command("audit", help=_AUDIT_HELP)
|
|
88
|
+
@click.argument("address")
|
|
89
|
+
@click.option("--search", default=None, help="Filter notes (matches audit timeline search).")
|
|
90
|
+
@click.option("--limit", type=int, default=None, help="Maximum number of timeline events to show.")
|
|
91
|
+
@namespace_option()
|
|
92
|
+
@click.pass_obj
|
|
93
|
+
@sdk_command
|
|
94
|
+
def timeline_audit_cmd(
|
|
95
|
+
ctx: OcmoCtx,
|
|
96
|
+
address: str,
|
|
97
|
+
search: str | None,
|
|
98
|
+
limit: int | None,
|
|
99
|
+
namespace: str | None,
|
|
100
|
+
) -> None:
|
|
101
|
+
_reject_output_format(ctx)
|
|
102
|
+
|
|
103
|
+
view, path, node_type = resolve_audit_item(
|
|
104
|
+
ctx,
|
|
105
|
+
address,
|
|
106
|
+
namespace,
|
|
107
|
+
command="timeline audit",
|
|
108
|
+
allow_version_flag=True,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if limit is not None and limit < 1:
|
|
112
|
+
err("--limit must be a positive integer.")
|
|
113
|
+
raise SystemExit(USAGE_ERROR)
|
|
114
|
+
|
|
115
|
+
entries = _fetch_timeline_entries(
|
|
116
|
+
view,
|
|
117
|
+
path=path,
|
|
118
|
+
node_type=node_type,
|
|
119
|
+
search=search,
|
|
120
|
+
limit=limit,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if not entries:
|
|
124
|
+
if sys.stdout.isatty():
|
|
125
|
+
print("No audit events found.")
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
for entry in entries:
|
|
129
|
+
occurred_at, message = _entry_message(entry)
|
|
130
|
+
if occurred_at and message:
|
|
131
|
+
print(f"{occurred_at} {message}")
|
|
132
|
+
elif message:
|
|
133
|
+
print(message)
|
|
134
|
+
elif occurred_at:
|
|
135
|
+
print(occurred_at)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""ocmo version — print CLI, SDK, and server versions with a compatibility verdict."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from .. import __version__ as CLI_VERSION
|
|
10
|
+
from .._client import OcmoCtx
|
|
11
|
+
from .._errors import sdk_command
|
|
12
|
+
from .._exit import FAILURE
|
|
13
|
+
from .._output import as_dict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command("version")
|
|
17
|
+
@click.option(
|
|
18
|
+
"--skip-server", is_flag=True, default=False, help="Print CLI and SDK versions only; do not contact the server."
|
|
19
|
+
)
|
|
20
|
+
@click.option("--notice", is_flag=True, default=False, help="Print the bundled product NOTICE text.")
|
|
21
|
+
@click.pass_obj
|
|
22
|
+
@sdk_command
|
|
23
|
+
def version_cmd(ctx: OcmoCtx, skip_server: bool, notice: bool) -> None:
|
|
24
|
+
"""Print CLI, SDK, and server versions with a compatibility verdict."""
|
|
25
|
+
try:
|
|
26
|
+
import ocmo as _sdk
|
|
27
|
+
|
|
28
|
+
sdk_version = _sdk.__version__
|
|
29
|
+
except Exception:
|
|
30
|
+
sdk_version = "unknown"
|
|
31
|
+
|
|
32
|
+
if notice:
|
|
33
|
+
try:
|
|
34
|
+
from .._notice import load_notice_text
|
|
35
|
+
|
|
36
|
+
notice_text = load_notice_text()
|
|
37
|
+
except Exception as exc:
|
|
38
|
+
click.echo(f"Failed to load NOTICE: {exc}", err=True)
|
|
39
|
+
sys.exit(FAILURE)
|
|
40
|
+
print(notice_text, end="" if notice_text.endswith("\n") else "\n")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
if skip_server:
|
|
44
|
+
_print_versions(CLI_VERSION, sdk_version, server_version="(skipped)", verdict="")
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
server_version = "(unavailable)"
|
|
48
|
+
verdict = ""
|
|
49
|
+
try:
|
|
50
|
+
info = ctx.client().version() # type: ignore[no-untyped-call]
|
|
51
|
+
data = as_dict(info)
|
|
52
|
+
server_version = data.get("version", "(unknown)")
|
|
53
|
+
verdict = _compatibility_verdict(sdk_version, server_version)
|
|
54
|
+
except SystemExit:
|
|
55
|
+
raise
|
|
56
|
+
except Exception as exc:
|
|
57
|
+
err_str = str(exc)
|
|
58
|
+
if "OCMO_SERVER" in err_str or "required" in err_str.lower():
|
|
59
|
+
server_version = "(server not configured)"
|
|
60
|
+
else:
|
|
61
|
+
server_version = f"(error: {exc})"
|
|
62
|
+
verdict = ""
|
|
63
|
+
|
|
64
|
+
_print_versions(CLI_VERSION, sdk_version, server_version, verdict)
|
|
65
|
+
|
|
66
|
+
if "MAJOR MISMATCH" in verdict:
|
|
67
|
+
sys.exit(FAILURE)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _print_versions(cli: str, sdk: str, server_version: str, verdict: str) -> None:
|
|
71
|
+
print(f"ocmo CLI: {cli}")
|
|
72
|
+
print(f"ocmo SDK: {sdk}")
|
|
73
|
+
print(f"Server: {server_version}")
|
|
74
|
+
if verdict:
|
|
75
|
+
print(f"Compat: {verdict}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _compatibility_verdict(sdk_version: str, server_version: str) -> str:
|
|
79
|
+
sdk_parts = _parse_semver(sdk_version)
|
|
80
|
+
srv_parts = _parse_semver(server_version)
|
|
81
|
+
if not sdk_parts or not srv_parts:
|
|
82
|
+
return "WARNING: could not parse one or both versions"
|
|
83
|
+
sdk_major, sdk_minor, _ = sdk_parts
|
|
84
|
+
srv_major, srv_minor, _ = srv_parts
|
|
85
|
+
if sdk_major != srv_major:
|
|
86
|
+
return f"MAJOR MISMATCH — SDK major {sdk_major} != server major {srv_major} (not compatible)"
|
|
87
|
+
diff = abs(sdk_minor - srv_minor)
|
|
88
|
+
if diff > 2:
|
|
89
|
+
return (
|
|
90
|
+
f"WARNING: minor version gap {diff} exceeds the ±2 compatibility window "
|
|
91
|
+
f"(SDK {sdk_version}, server {server_version})"
|
|
92
|
+
)
|
|
93
|
+
return "OK"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _parse_semver(v: str) -> tuple[int, int, int] | None:
|
|
97
|
+
import re
|
|
98
|
+
|
|
99
|
+
m = re.match(r"^(\d+)\.(\d+)\.(\d+)", v)
|
|
100
|
+
if not m:
|
|
101
|
+
return None
|
|
102
|
+
return int(m.group(1)), int(m.group(2)), int(m.group(3))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""ocmo whoami — show current identity."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .._client_commands import client_command
|
|
6
|
+
|
|
7
|
+
whoami_cmd = client_command(
|
|
8
|
+
"whoami",
|
|
9
|
+
help="Show the identity of the current authenticated principal.",
|
|
10
|
+
manifest_key="whoami",
|
|
11
|
+
method="whoami",
|
|
12
|
+
)
|