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,187 @@
|
|
|
1
|
+
"""Hand-written ``ocmo move item`` and ``ocmo copy item`` commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from .._address import (
|
|
10
|
+
parse_address_or_exit,
|
|
11
|
+
parse_simple_address_or_exit,
|
|
12
|
+
resolve_relocate_target,
|
|
13
|
+
)
|
|
14
|
+
from .._click_groups import attach_resource_command, ensure_resource_alias_group
|
|
15
|
+
from .._client import OcmoCtx
|
|
16
|
+
from .._dry_run import format_generated_dry_run
|
|
17
|
+
from .._errors import sdk_command
|
|
18
|
+
from .._exit import USAGE_ERROR
|
|
19
|
+
from .._mutating import run_mutating
|
|
20
|
+
from .._options import dry_run_option, namespace_option, yes_option
|
|
21
|
+
from .._output import err, status
|
|
22
|
+
|
|
23
|
+
_MOVE_ITEM_HELP = """\
|
|
24
|
+
Move a tree item or folder subtree to a new path.
|
|
25
|
+
|
|
26
|
+
TARGET uses Unix-style semantics: a trailing ``/`` places the source inside that
|
|
27
|
+
directory (keeping its leaf name); otherwise TARGET is the exact destination path.
|
|
28
|
+
|
|
29
|
+
\b
|
|
30
|
+
Examples:
|
|
31
|
+
ocmo -n prod move item app/web app/archive/web
|
|
32
|
+
ocmo -n prod move item b/c/d a/
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
_COPY_ITEM_HELP = """\
|
|
36
|
+
Copy a tree item or folder subtree to a new path.
|
|
37
|
+
|
|
38
|
+
When SOURCE includes ``@tag`` or ``@version``, that version is copied; otherwise
|
|
39
|
+
the API default tag (``latest``) is used.
|
|
40
|
+
|
|
41
|
+
TARGET uses Unix-style semantics: a trailing ``/`` places the source inside that
|
|
42
|
+
directory (keeping its leaf name); otherwise TARGET is the exact destination path.
|
|
43
|
+
|
|
44
|
+
\b
|
|
45
|
+
Examples:
|
|
46
|
+
ocmo -n prod copy item app/web app/staging/web
|
|
47
|
+
ocmo -n prod copy item app/web@stable app/staging/
|
|
48
|
+
ocmo -n prod copy item b/c/d a/
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _parse_source_target(
|
|
53
|
+
source: str,
|
|
54
|
+
target: str,
|
|
55
|
+
*,
|
|
56
|
+
allow_source_version: bool,
|
|
57
|
+
) -> tuple[str, str | None, str]:
|
|
58
|
+
source_path, source_version = parse_address_or_exit(source)
|
|
59
|
+
target_path, target_version = parse_simple_address_or_exit(target)
|
|
60
|
+
if target_version:
|
|
61
|
+
err("TARGET does not support @version.")
|
|
62
|
+
raise SystemExit(USAGE_ERROR)
|
|
63
|
+
|
|
64
|
+
if source_version and not allow_source_version:
|
|
65
|
+
err("SOURCE does not support @version or @tag for move.")
|
|
66
|
+
raise SystemExit(USAGE_ERROR)
|
|
67
|
+
|
|
68
|
+
return source_path, source_version, target_path
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _relocate_confirm_message(action: str, source_path: str, destination_path: str) -> str:
|
|
72
|
+
return f"{action.capitalize()} item {source_path!r} to {destination_path!r}. Continue?"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _relocate_success_message(action: str, source_path: str, destination_path: str) -> str:
|
|
76
|
+
past = "moved" if action == "move" else "copied"
|
|
77
|
+
return f"Item {source_path!r} was {past} to {destination_path!r}."
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _run_item_relocate(
|
|
81
|
+
ctx: OcmoCtx,
|
|
82
|
+
*,
|
|
83
|
+
op_id: str,
|
|
84
|
+
action: str,
|
|
85
|
+
source: str,
|
|
86
|
+
target: str,
|
|
87
|
+
namespace: str | None,
|
|
88
|
+
dry_run: bool,
|
|
89
|
+
yes: bool,
|
|
90
|
+
allow_source_version: bool,
|
|
91
|
+
) -> None:
|
|
92
|
+
source_path, source_version, target_path = _parse_source_target(
|
|
93
|
+
source,
|
|
94
|
+
target,
|
|
95
|
+
allow_source_version=allow_source_version,
|
|
96
|
+
)
|
|
97
|
+
destination_path = resolve_relocate_target(source_path, target_path)
|
|
98
|
+
|
|
99
|
+
sdk_kwargs: dict[str, Any] = {"target_path": destination_path}
|
|
100
|
+
if op_id == "copy_item" and source_version:
|
|
101
|
+
sdk_kwargs["tag_to_copy"] = source_version
|
|
102
|
+
|
|
103
|
+
ns = ctx.require_namespace(namespace)
|
|
104
|
+
|
|
105
|
+
def _action() -> None:
|
|
106
|
+
view = ctx.namespace_view(namespace)
|
|
107
|
+
if op_id == "move_item":
|
|
108
|
+
view.move_item(source_path, **sdk_kwargs)
|
|
109
|
+
else:
|
|
110
|
+
view.copy_item(source_path, **sdk_kwargs)
|
|
111
|
+
status(_relocate_success_message(action, source_path, destination_path))
|
|
112
|
+
|
|
113
|
+
run_mutating(
|
|
114
|
+
ctx,
|
|
115
|
+
dry_run=dry_run,
|
|
116
|
+
yes=yes,
|
|
117
|
+
plan_lines=format_generated_dry_run(
|
|
118
|
+
op_id=op_id,
|
|
119
|
+
action=action,
|
|
120
|
+
resource="item",
|
|
121
|
+
path=source_path,
|
|
122
|
+
version=source_version if op_id == "copy_item" else None,
|
|
123
|
+
namespace=ns,
|
|
124
|
+
args=[],
|
|
125
|
+
kwargs=sdk_kwargs,
|
|
126
|
+
client_scope=False,
|
|
127
|
+
),
|
|
128
|
+
confirm_message=_relocate_confirm_message(action, source_path, destination_path),
|
|
129
|
+
action=_action,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _build_relocate_command(action: str) -> click.Command:
|
|
134
|
+
help_text = _MOVE_ITEM_HELP if action == "move" else _COPY_ITEM_HELP
|
|
135
|
+
op_id = "move_item" if action == "move" else "copy_item"
|
|
136
|
+
allow_source_version = action == "copy"
|
|
137
|
+
|
|
138
|
+
@click.command("item", help=help_text)
|
|
139
|
+
@click.argument("source")
|
|
140
|
+
@click.argument("target")
|
|
141
|
+
@namespace_option()
|
|
142
|
+
@yes_option()
|
|
143
|
+
@dry_run_option()
|
|
144
|
+
@click.pass_obj
|
|
145
|
+
@sdk_command
|
|
146
|
+
def relocate_cmd(
|
|
147
|
+
ctx: OcmoCtx,
|
|
148
|
+
source: str,
|
|
149
|
+
target: str,
|
|
150
|
+
namespace: str | None,
|
|
151
|
+
dry_run: bool,
|
|
152
|
+
yes: bool,
|
|
153
|
+
) -> None:
|
|
154
|
+
_run_item_relocate(
|
|
155
|
+
ctx,
|
|
156
|
+
op_id=op_id,
|
|
157
|
+
action=action,
|
|
158
|
+
source=source,
|
|
159
|
+
target=target,
|
|
160
|
+
namespace=namespace,
|
|
161
|
+
dry_run=dry_run,
|
|
162
|
+
yes=yes,
|
|
163
|
+
allow_source_version=allow_source_version,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
return relocate_cmd
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
move_item_cmd = _build_relocate_command("move")
|
|
170
|
+
copy_item_cmd = _build_relocate_command("copy")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def register_item_relocate_commands(root: click.Group) -> None:
|
|
174
|
+
"""Register hand-written ``move item`` / ``copy item`` commands."""
|
|
175
|
+
move_group = ensure_resource_alias_group(
|
|
176
|
+
root,
|
|
177
|
+
"move",
|
|
178
|
+
help="Move items within the tree.",
|
|
179
|
+
)
|
|
180
|
+
attach_resource_command(move_group, move_item_cmd, canonical="item", aliases=[])
|
|
181
|
+
|
|
182
|
+
copy_group = ensure_resource_alias_group(
|
|
183
|
+
root,
|
|
184
|
+
"copy",
|
|
185
|
+
help="Copy items within the tree.",
|
|
186
|
+
)
|
|
187
|
+
attach_resource_command(copy_group, copy_item_cmd, canonical="item", aliases=[])
|
ocmo_cli/commands/ls.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
"""ocmo ls / ocmo tree — list or render the item tree under a path."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from .._address import parse_address_or_exit
|
|
11
|
+
from .._client import OcmoCtx
|
|
12
|
+
from .._errors import sdk_command
|
|
13
|
+
from .._exit import USAGE_ERROR
|
|
14
|
+
from .._ls_wide import (
|
|
15
|
+
LS_WIDE_SORT_CHOICES,
|
|
16
|
+
basic_ls_row,
|
|
17
|
+
enrich_ls_rows,
|
|
18
|
+
sort_wide_rows,
|
|
19
|
+
)
|
|
20
|
+
from .._options import namespace_option, output_option
|
|
21
|
+
from .._output import as_dict, emit, emit_table, err
|
|
22
|
+
from .._output_manifest import columns_for_format, get_command_spec, resolve_effective_format
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from ocmo import NamespaceView, OcmoClient
|
|
26
|
+
|
|
27
|
+
_LS_HELP = """\
|
|
28
|
+
List the direct children of a tree path, or the namespace root when ADDRESS is omitted.
|
|
29
|
+
|
|
30
|
+
\b
|
|
31
|
+
Examples:
|
|
32
|
+
ocmo -n prod ls
|
|
33
|
+
ocmo -n prod ls app/
|
|
34
|
+
ocmo -n prod ls app/ -R
|
|
35
|
+
ocmo -n prod ls app/ --hide-folders
|
|
36
|
+
ocmo -n prod ls --hide-system
|
|
37
|
+
ocmo -n prod ls -o json
|
|
38
|
+
ocmo -n prod ls -o wide
|
|
39
|
+
ocmo -n prod ls -o wide --sort updated
|
|
40
|
+
ocmo -n prod ls -o wide --sort created
|
|
41
|
+
ocmo -n prod ls -o name
|
|
42
|
+
ocmo -n prod ls -o path
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
_TREE_HELP = """\
|
|
46
|
+
Render the subtree under a path in tree format.
|
|
47
|
+
|
|
48
|
+
\b
|
|
49
|
+
Examples:
|
|
50
|
+
ocmo -n prod tree
|
|
51
|
+
ocmo -n prod tree app/
|
|
52
|
+
ocmo -n prod tree --depth 2
|
|
53
|
+
ocmo -n prod tree --hide-system
|
|
54
|
+
ocmo -n prod tree --emoji
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
_TREE_TYPE_EMOJI: dict[str, str] = {
|
|
58
|
+
"folder": "📁",
|
|
59
|
+
"config": "📄",
|
|
60
|
+
"template": "📜",
|
|
61
|
+
"secret": "🔒",
|
|
62
|
+
"resolver": "⚡",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _navigate(
|
|
67
|
+
view: NamespaceView,
|
|
68
|
+
address: str,
|
|
69
|
+
*,
|
|
70
|
+
recursive: bool,
|
|
71
|
+
limit: int | None,
|
|
72
|
+
) -> tuple[str, dict[str, Any]]:
|
|
73
|
+
kwargs: dict[str, Any] = {}
|
|
74
|
+
if recursive:
|
|
75
|
+
kwargs["recursive"] = True
|
|
76
|
+
if limit is not None:
|
|
77
|
+
kwargs["limit"] = limit
|
|
78
|
+
|
|
79
|
+
if address:
|
|
80
|
+
path, _version = parse_address_or_exit(address)
|
|
81
|
+
result = view.navigate_path(path=path, **kwargs)
|
|
82
|
+
else:
|
|
83
|
+
path = ""
|
|
84
|
+
result = view.navigate_root(**kwargs)
|
|
85
|
+
|
|
86
|
+
data = as_dict(result)
|
|
87
|
+
return path, data
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@click.command("ls", help=_LS_HELP)
|
|
91
|
+
@click.argument("address", default="", required=False)
|
|
92
|
+
@namespace_option()
|
|
93
|
+
@output_option("ls")
|
|
94
|
+
@click.option(
|
|
95
|
+
"-R",
|
|
96
|
+
"--recursive",
|
|
97
|
+
is_flag=True,
|
|
98
|
+
default=False,
|
|
99
|
+
help="Fetch the full subtree (not just direct children).",
|
|
100
|
+
)
|
|
101
|
+
@click.option(
|
|
102
|
+
"--limit",
|
|
103
|
+
default=None,
|
|
104
|
+
type=int,
|
|
105
|
+
help="Maximum number of items to return.",
|
|
106
|
+
)
|
|
107
|
+
@click.option(
|
|
108
|
+
"--hide-folders",
|
|
109
|
+
is_flag=True,
|
|
110
|
+
default=False,
|
|
111
|
+
help="Omit folder nodes from the listing.",
|
|
112
|
+
)
|
|
113
|
+
@click.option(
|
|
114
|
+
"--hide-system",
|
|
115
|
+
is_flag=True,
|
|
116
|
+
default=False,
|
|
117
|
+
help="Omit built-in namespace items (_permissions, _webhooks, …).",
|
|
118
|
+
)
|
|
119
|
+
@click.option(
|
|
120
|
+
"--sort",
|
|
121
|
+
"sort_by",
|
|
122
|
+
type=click.Choice(LS_WIDE_SORT_CHOICES, case_sensitive=False),
|
|
123
|
+
default=None,
|
|
124
|
+
help="Sort wide output by path, type, created, or update time (requires -o wide).",
|
|
125
|
+
)
|
|
126
|
+
@click.pass_obj
|
|
127
|
+
@sdk_command
|
|
128
|
+
def ls_cmd(
|
|
129
|
+
ctx: OcmoCtx,
|
|
130
|
+
address: str,
|
|
131
|
+
namespace: str | None,
|
|
132
|
+
output_fmt: str | None,
|
|
133
|
+
recursive: bool,
|
|
134
|
+
limit: int | None,
|
|
135
|
+
hide_folders: bool,
|
|
136
|
+
hide_system: bool,
|
|
137
|
+
sort_by: str | None,
|
|
138
|
+
) -> None:
|
|
139
|
+
view = ctx.namespace_view(namespace)
|
|
140
|
+
client = ctx.client()
|
|
141
|
+
fmt = resolve_effective_format(output_fmt, ctx.output, get_command_spec("ls"))
|
|
142
|
+
_validate_ls_sort(sort_by, fmt)
|
|
143
|
+
|
|
144
|
+
path, data = _navigate(view, address, recursive=recursive, limit=limit)
|
|
145
|
+
system_paths = _load_system_paths(client) if hide_system else frozenset()
|
|
146
|
+
rows = _filter_navigation_rows(
|
|
147
|
+
_navigation_rows(data),
|
|
148
|
+
hide_folders=hide_folders,
|
|
149
|
+
hide_system=hide_system,
|
|
150
|
+
system_paths=system_paths,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if fmt == "wide":
|
|
154
|
+
_render_wide_table(
|
|
155
|
+
client,
|
|
156
|
+
view,
|
|
157
|
+
ctx.require_namespace(namespace),
|
|
158
|
+
rows,
|
|
159
|
+
sort_by=sort_by or "path",
|
|
160
|
+
command_name="ls",
|
|
161
|
+
)
|
|
162
|
+
elif fmt == "table":
|
|
163
|
+
_render_table(rows, command_name="ls")
|
|
164
|
+
elif fmt in ("name", "path"):
|
|
165
|
+
emit(rows, fmt)
|
|
166
|
+
elif fmt == "json":
|
|
167
|
+
import json as _json
|
|
168
|
+
|
|
169
|
+
payload = _filter_navigate_data(
|
|
170
|
+
data,
|
|
171
|
+
hide_folders=hide_folders,
|
|
172
|
+
hide_system=hide_system,
|
|
173
|
+
system_paths=system_paths,
|
|
174
|
+
)
|
|
175
|
+
print(_json.dumps(payload, default=str, indent=2))
|
|
176
|
+
elif fmt == "yaml" or (fmt and fmt.startswith("jsonpath=")):
|
|
177
|
+
emit(
|
|
178
|
+
_filter_navigate_data(
|
|
179
|
+
data,
|
|
180
|
+
hide_folders=hide_folders,
|
|
181
|
+
hide_system=hide_system,
|
|
182
|
+
system_paths=system_paths,
|
|
183
|
+
),
|
|
184
|
+
fmt,
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
err(f"Unsupported output format for ocmo ls: {fmt!r}")
|
|
188
|
+
raise SystemExit(USAGE_ERROR)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@click.command("tree", help=_TREE_HELP)
|
|
192
|
+
@click.argument("address", default="", required=False)
|
|
193
|
+
@namespace_option()
|
|
194
|
+
@click.option(
|
|
195
|
+
"--depth",
|
|
196
|
+
default=None,
|
|
197
|
+
type=click.IntRange(1),
|
|
198
|
+
help="Maximum nesting depth to display (default: unlimited).",
|
|
199
|
+
)
|
|
200
|
+
@click.option(
|
|
201
|
+
"--hide-system",
|
|
202
|
+
is_flag=True,
|
|
203
|
+
default=False,
|
|
204
|
+
help="Omit built-in namespace items (_permissions, _webhooks, …).",
|
|
205
|
+
)
|
|
206
|
+
@click.option(
|
|
207
|
+
"--emoji",
|
|
208
|
+
is_flag=True,
|
|
209
|
+
default=False,
|
|
210
|
+
help="Show item-type emojis before names instead of [type] suffixes.",
|
|
211
|
+
)
|
|
212
|
+
@click.pass_obj
|
|
213
|
+
@sdk_command
|
|
214
|
+
def tree_cmd(
|
|
215
|
+
ctx: OcmoCtx,
|
|
216
|
+
address: str,
|
|
217
|
+
namespace: str | None,
|
|
218
|
+
depth: int | None,
|
|
219
|
+
hide_system: bool,
|
|
220
|
+
emoji: bool,
|
|
221
|
+
) -> None:
|
|
222
|
+
if ctx.output is not None:
|
|
223
|
+
err("ocmo tree does not support -o/--output; output is always a tree view.")
|
|
224
|
+
raise SystemExit(USAGE_ERROR)
|
|
225
|
+
|
|
226
|
+
view = ctx.namespace_view(namespace)
|
|
227
|
+
client = ctx.client()
|
|
228
|
+
|
|
229
|
+
path, data = _navigate(view, address, recursive=True, limit=None)
|
|
230
|
+
system_paths = _load_system_paths(client) if hide_system else frozenset()
|
|
231
|
+
rows = _filter_navigation_rows(
|
|
232
|
+
_navigation_rows(data),
|
|
233
|
+
hide_folders=False,
|
|
234
|
+
hide_system=hide_system,
|
|
235
|
+
system_paths=system_paths,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
tree_rows = _build_tree_hierarchy(
|
|
239
|
+
rows,
|
|
240
|
+
root_prefix=path.strip("/") if address else "",
|
|
241
|
+
)
|
|
242
|
+
_render_plain_tree(tree_rows, prefix="", connector="", max_depth=depth, use_emoji=emoji)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _validate_ls_sort(sort_by: str | None, fmt: str | None) -> None:
|
|
246
|
+
if sort_by is not None and fmt != "wide":
|
|
247
|
+
err("--sort is only supported with -o wide.")
|
|
248
|
+
raise SystemExit(USAGE_ERROR)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
_DEFAULT_BUILTIN_NAMESPACE_PATHS = frozenset(
|
|
252
|
+
{
|
|
253
|
+
"_permissions",
|
|
254
|
+
"_permissions.schema",
|
|
255
|
+
"_webhooks",
|
|
256
|
+
"_webhooks.schema",
|
|
257
|
+
"_webhooks_secret",
|
|
258
|
+
"_git_sync",
|
|
259
|
+
"_git_sync.schema",
|
|
260
|
+
"_git_sync_secret",
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _load_system_paths(client: OcmoClient) -> frozenset[str]:
|
|
266
|
+
"""Return built-in namespace tree paths from ``/api/version`` when available."""
|
|
267
|
+
version_info = None
|
|
268
|
+
if hasattr(client, "version_info"):
|
|
269
|
+
try:
|
|
270
|
+
version_info = client.version_info()
|
|
271
|
+
except Exception:
|
|
272
|
+
version_info = None
|
|
273
|
+
if not version_info:
|
|
274
|
+
return _DEFAULT_BUILTIN_NAMESPACE_PATHS
|
|
275
|
+
|
|
276
|
+
payload = version_info.get("builtin_namespace_paths") if isinstance(version_info, dict) else None
|
|
277
|
+
if not isinstance(payload, dict):
|
|
278
|
+
return _DEFAULT_BUILTIN_NAMESPACE_PATHS
|
|
279
|
+
|
|
280
|
+
order = payload.get("order")
|
|
281
|
+
if isinstance(order, list) and order:
|
|
282
|
+
return frozenset(str(path).strip("/") for path in order if str(path).strip("/"))
|
|
283
|
+
|
|
284
|
+
paths: set[str] = set()
|
|
285
|
+
for section in ("config", "secret", "schema"):
|
|
286
|
+
values = payload.get(section)
|
|
287
|
+
if isinstance(values, list):
|
|
288
|
+
paths.update(str(path).strip("/") for path in values if str(path).strip("/"))
|
|
289
|
+
return frozenset(paths) if paths else _DEFAULT_BUILTIN_NAMESPACE_PATHS
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _node_path(node: Any) -> str:
|
|
293
|
+
item = node if isinstance(node, dict) else vars(node)
|
|
294
|
+
return str(item.get("path") or "").strip("/")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _node_type(node: Any) -> str:
|
|
298
|
+
item = node if isinstance(node, dict) else vars(node)
|
|
299
|
+
return item.get("node_type") or ""
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _filter_navigation_rows(
|
|
303
|
+
rows: Sequence[Any],
|
|
304
|
+
*,
|
|
305
|
+
hide_folders: bool,
|
|
306
|
+
hide_system: bool,
|
|
307
|
+
system_paths: frozenset[str],
|
|
308
|
+
) -> list[Any]:
|
|
309
|
+
if not hide_folders and not hide_system:
|
|
310
|
+
return list(rows)
|
|
311
|
+
filtered: list[Any] = []
|
|
312
|
+
for node in rows:
|
|
313
|
+
if hide_folders and _node_type(node) == "folder":
|
|
314
|
+
continue
|
|
315
|
+
if hide_system and _node_path(node) in system_paths:
|
|
316
|
+
continue
|
|
317
|
+
item = node if isinstance(node, dict) else dict(vars(node))
|
|
318
|
+
children = item.get("children") or []
|
|
319
|
+
if children:
|
|
320
|
+
item = {
|
|
321
|
+
**item,
|
|
322
|
+
"children": _filter_navigation_rows(
|
|
323
|
+
children,
|
|
324
|
+
hide_folders=hide_folders,
|
|
325
|
+
hide_system=hide_system,
|
|
326
|
+
system_paths=system_paths,
|
|
327
|
+
),
|
|
328
|
+
}
|
|
329
|
+
filtered.append(item)
|
|
330
|
+
return filtered
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _filter_navigate_data(
|
|
334
|
+
data: dict[str, Any],
|
|
335
|
+
*,
|
|
336
|
+
hide_folders: bool,
|
|
337
|
+
hide_system: bool,
|
|
338
|
+
system_paths: frozenset[str],
|
|
339
|
+
) -> dict[str, Any]:
|
|
340
|
+
if not hide_folders and not hide_system:
|
|
341
|
+
return data
|
|
342
|
+
payload = dict(data)
|
|
343
|
+
payload["children"] = _filter_navigation_rows(
|
|
344
|
+
payload.get("children") or [],
|
|
345
|
+
hide_folders=hide_folders,
|
|
346
|
+
hide_system=hide_system,
|
|
347
|
+
system_paths=system_paths,
|
|
348
|
+
)
|
|
349
|
+
item = payload.get("item")
|
|
350
|
+
if payload.get("is_leaf") and item:
|
|
351
|
+
if hide_folders and _node_type(item) == "folder":
|
|
352
|
+
payload["item"] = None
|
|
353
|
+
elif hide_system and _node_path(item) in system_paths:
|
|
354
|
+
payload["item"] = None
|
|
355
|
+
if payload.get("children"):
|
|
356
|
+
payload["children_count"] = len(payload["children"])
|
|
357
|
+
elif (hide_folders or hide_system) and not payload.get("is_leaf"):
|
|
358
|
+
payload["children_count"] = 0
|
|
359
|
+
return payload
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _navigation_rows(data: dict[str, Any]) -> list[Any]:
|
|
363
|
+
"""Return the nodes to list for a navigate response.
|
|
364
|
+
|
|
365
|
+
Folder paths expose direct children; leaf paths expose the matched item itself.
|
|
366
|
+
"""
|
|
367
|
+
children = data.get("children") or []
|
|
368
|
+
if children:
|
|
369
|
+
return children
|
|
370
|
+
item = data.get("item")
|
|
371
|
+
if data.get("is_leaf") and item:
|
|
372
|
+
return [item]
|
|
373
|
+
return []
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _build_tree_hierarchy(nodes: Sequence[Any], *, root_prefix: str = "") -> list[dict[str, Any]]:
|
|
377
|
+
"""Nest flat navigate rows (recursive API) into a path hierarchy."""
|
|
378
|
+
root_prefix = root_prefix.strip("/")
|
|
379
|
+
|
|
380
|
+
prepared: list[dict[str, Any]] = []
|
|
381
|
+
for raw in nodes:
|
|
382
|
+
item = raw if isinstance(raw, dict) else dict(vars(raw))
|
|
383
|
+
path = (item.get("path") or "").strip("/")
|
|
384
|
+
if not path or (root_prefix and path == root_prefix):
|
|
385
|
+
continue
|
|
386
|
+
prepared.append({**item, "children": []})
|
|
387
|
+
|
|
388
|
+
if not prepared:
|
|
389
|
+
return []
|
|
390
|
+
|
|
391
|
+
by_path = {node["path"]: node for node in prepared}
|
|
392
|
+
roots: list[dict[str, Any]] = []
|
|
393
|
+
|
|
394
|
+
for node in sorted(prepared, key=lambda item: item["path"]):
|
|
395
|
+
path = node["path"]
|
|
396
|
+
parent_path = path.rsplit("/", 1)[0] if "/" in path else None
|
|
397
|
+
|
|
398
|
+
if parent_path and parent_path in by_path:
|
|
399
|
+
by_path[parent_path]["children"].append(node)
|
|
400
|
+
continue
|
|
401
|
+
|
|
402
|
+
if root_prefix:
|
|
403
|
+
if parent_path == root_prefix:
|
|
404
|
+
roots.append(node)
|
|
405
|
+
else:
|
|
406
|
+
roots.append(node)
|
|
407
|
+
elif parent_path is None:
|
|
408
|
+
roots.append(node)
|
|
409
|
+
else:
|
|
410
|
+
roots.append(node)
|
|
411
|
+
|
|
412
|
+
def _sort_children(items: list[dict[str, Any]]) -> None:
|
|
413
|
+
items.sort(key=lambda item: item.get("path") or "")
|
|
414
|
+
for item in items:
|
|
415
|
+
if item["children"]:
|
|
416
|
+
_sort_children(item["children"])
|
|
417
|
+
|
|
418
|
+
_sort_children(roots)
|
|
419
|
+
return roots
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _tree_node_label(node: dict[str, Any], *, use_emoji: bool) -> str:
|
|
423
|
+
node_name = node.get("name") or node.get("path") or str(node)
|
|
424
|
+
node_type = node.get("node_type") or ""
|
|
425
|
+
if use_emoji:
|
|
426
|
+
icon = _TREE_TYPE_EMOJI.get(node_type, "")
|
|
427
|
+
return f"{icon} {node_name}" if icon else node_name
|
|
428
|
+
type_str = f" [{node_type}]" if node_type else ""
|
|
429
|
+
return f"{node_name}{type_str}"
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _render_plain_tree(
|
|
433
|
+
children: Sequence[Any],
|
|
434
|
+
prefix: str,
|
|
435
|
+
connector: str = "",
|
|
436
|
+
*,
|
|
437
|
+
max_depth: int | None = None,
|
|
438
|
+
current_depth: int = 1,
|
|
439
|
+
use_emoji: bool = False,
|
|
440
|
+
) -> None:
|
|
441
|
+
for i, item in enumerate(children):
|
|
442
|
+
is_last = i == len(children) - 1
|
|
443
|
+
branch = "└── " if is_last else "├── "
|
|
444
|
+
node = item if isinstance(item, dict) else vars(item)
|
|
445
|
+
print(f"{prefix}{connector}{branch}{_tree_node_label(node, use_emoji=use_emoji)}")
|
|
446
|
+
sub = node.get("children") or []
|
|
447
|
+
if sub and (max_depth is None or current_depth < max_depth):
|
|
448
|
+
child_prefix = prefix + connector + (" " if is_last else "│ ")
|
|
449
|
+
_render_plain_tree(
|
|
450
|
+
sub,
|
|
451
|
+
prefix=child_prefix,
|
|
452
|
+
max_depth=max_depth,
|
|
453
|
+
current_depth=current_depth + 1,
|
|
454
|
+
use_emoji=use_emoji,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _render_table(children: Sequence[Any], *, command_name: str) -> None:
|
|
459
|
+
rows = []
|
|
460
|
+
for item in children:
|
|
461
|
+
node = item if isinstance(item, dict) else vars(item)
|
|
462
|
+
rows.append(basic_ls_row(node))
|
|
463
|
+
rows.sort(key=lambda row: row["path"])
|
|
464
|
+
spec = get_command_spec(command_name)
|
|
465
|
+
emit_table(rows, columns_for_format(spec, "table", rows))
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _render_wide_table(
|
|
469
|
+
client: OcmoClient,
|
|
470
|
+
view: NamespaceView,
|
|
471
|
+
namespace: str,
|
|
472
|
+
children: Sequence[Any],
|
|
473
|
+
*,
|
|
474
|
+
sort_by: str = "path",
|
|
475
|
+
command_name: str,
|
|
476
|
+
) -> None:
|
|
477
|
+
nodes = [item if isinstance(item, dict) else vars(item) for item in children]
|
|
478
|
+
rows = enrich_ls_rows(client=client, view=view, namespace=namespace, nodes=nodes)
|
|
479
|
+
sort_wide_rows(rows, sort_by)
|
|
480
|
+
spec = get_command_spec(command_name)
|
|
481
|
+
emit_table(rows, columns_for_format(spec, "wide", rows))
|