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,203 @@
|
|
|
1
|
+
"""Parse and validate structured (-f) bodies for SDK BODY_PAYLOAD operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"BODY_PAYLOADS",
|
|
10
|
+
"format_usage_error",
|
|
11
|
+
"merge_tag_address_version",
|
|
12
|
+
"parse_structured_content",
|
|
13
|
+
"prepare_body_payload",
|
|
14
|
+
"prepare_untag_body_payload",
|
|
15
|
+
"sdk_path_for_body_payload",
|
|
16
|
+
"validate_create_request",
|
|
17
|
+
"validate_tag_request",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
from ._exit import USAGE_ERROR
|
|
21
|
+
from ._sdk_dispatch import address_keyword_for_op
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from ocmo._facade_meta import BODY_PAYLOADS
|
|
25
|
+
except ImportError: # pragma: no cover - CLI tests may run without SDK on path quirks
|
|
26
|
+
BODY_PAYLOADS = {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_CREATE_REQUIREMENTS: dict[str, tuple[tuple[str, ...], str]] = {
|
|
30
|
+
"create_namespace": (
|
|
31
|
+
("name",),
|
|
32
|
+
"create namespace requires a name (ADDRESS).",
|
|
33
|
+
),
|
|
34
|
+
"create_global_permission": (
|
|
35
|
+
("namespace",),
|
|
36
|
+
"create global permission requires namespace in -f " "(and optionally rule id via ADDRESS).",
|
|
37
|
+
),
|
|
38
|
+
"create_lock": (
|
|
39
|
+
("reason",),
|
|
40
|
+
"create lock requires --reason with a lock rationale.",
|
|
41
|
+
),
|
|
42
|
+
"replace_lock": (
|
|
43
|
+
("reason",),
|
|
44
|
+
"update lock requires --reason with a lock rationale.",
|
|
45
|
+
),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_ADDRESS_REQUIRED_OPS = frozenset({"create_lock"})
|
|
49
|
+
|
|
50
|
+
# BODY_PAYLOAD ops where ADDRESS is a positional tree path, not a body field.
|
|
51
|
+
_BODY_PAYLOAD_PATH_ARG_OPS = frozenset(
|
|
52
|
+
{
|
|
53
|
+
"create_lock",
|
|
54
|
+
"replace_lock",
|
|
55
|
+
"rotate_resolver_token",
|
|
56
|
+
"set_tag",
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Extra CLI kwargs that must not be merged into the rule/document body.
|
|
61
|
+
_BODY_PAYLOAD_EXCLUDED_EXTRA: dict[str, frozenset[str]] = {
|
|
62
|
+
"create_global_permission": frozenset({"position"}),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def parse_structured_content(content: str) -> dict[str, Any]:
|
|
67
|
+
"""Parse YAML or JSON file/stdin content into an object dict."""
|
|
68
|
+
import yaml # deferred
|
|
69
|
+
|
|
70
|
+
text = content.strip()
|
|
71
|
+
if not text:
|
|
72
|
+
return {}
|
|
73
|
+
try:
|
|
74
|
+
data = json.loads(text)
|
|
75
|
+
except json.JSONDecodeError:
|
|
76
|
+
data = yaml.safe_load(text)
|
|
77
|
+
if data is None:
|
|
78
|
+
return {}
|
|
79
|
+
if not isinstance(data, dict):
|
|
80
|
+
raise ValueError("Body must be a YAML/JSON object (mapping), not a scalar or list.")
|
|
81
|
+
return data
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def prepare_body_payload(
|
|
85
|
+
op_id: str,
|
|
86
|
+
*,
|
|
87
|
+
address: str | None,
|
|
88
|
+
content: str | None,
|
|
89
|
+
extra: dict[str, Any] | None = None,
|
|
90
|
+
address_version: str | None = None,
|
|
91
|
+
) -> dict[str, Any]:
|
|
92
|
+
"""Build a dict suitable for SDK ``body=`` on BODY_PAYLOAD create/update ops."""
|
|
93
|
+
if op_id not in BODY_PAYLOADS:
|
|
94
|
+
return {}
|
|
95
|
+
|
|
96
|
+
payload: dict[str, Any] = {}
|
|
97
|
+
if content:
|
|
98
|
+
payload = parse_structured_content(content)
|
|
99
|
+
|
|
100
|
+
if address is not None and op_id not in _BODY_PAYLOAD_PATH_ARG_OPS:
|
|
101
|
+
address_key = address_keyword_for_op(op_id)
|
|
102
|
+
payload.setdefault(address_key, address)
|
|
103
|
+
|
|
104
|
+
if extra:
|
|
105
|
+
excluded = _BODY_PAYLOAD_EXCLUDED_EXTRA.get(op_id, frozenset())
|
|
106
|
+
for key, value in extra.items():
|
|
107
|
+
if key in excluded:
|
|
108
|
+
continue
|
|
109
|
+
if value is not None and value is not False:
|
|
110
|
+
payload.setdefault(key, value)
|
|
111
|
+
|
|
112
|
+
if op_id == "set_tag":
|
|
113
|
+
payload = merge_tag_address_version(payload, address_version)
|
|
114
|
+
|
|
115
|
+
return payload
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def prepare_untag_body_payload(tag: str) -> dict[str, Any]:
|
|
119
|
+
"""POST body to remove a tag (``set_tag`` with ``version: null``)."""
|
|
120
|
+
return {"tag": tag, "version": None}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def validate_tag_request(
|
|
124
|
+
op_id: str,
|
|
125
|
+
*,
|
|
126
|
+
action: str | None = None,
|
|
127
|
+
payload: dict[str, Any] | None = None,
|
|
128
|
+
extra: dict[str, Any] | None = None,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Fail fast when tag/untag commands omit required --tag."""
|
|
131
|
+
import sys
|
|
132
|
+
|
|
133
|
+
if action == "untag":
|
|
134
|
+
tag = (extra or {}).get("tag")
|
|
135
|
+
message = "untag item requires --tag with the version tag name to remove."
|
|
136
|
+
elif op_id == "set_tag":
|
|
137
|
+
tag = (payload or {}).get("tag")
|
|
138
|
+
message = "tag item requires --tag with a version tag name."
|
|
139
|
+
else:
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
if tag not in (None, ""):
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
print(f"Error: {message}", file=sys.stderr)
|
|
146
|
+
raise SystemExit(USAGE_ERROR)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def merge_tag_address_version(
|
|
150
|
+
payload: dict[str, Any],
|
|
151
|
+
version: str | None,
|
|
152
|
+
) -> dict[str, Any]:
|
|
153
|
+
"""Fold ADDRESS@version into TagPayload when not already set in the body."""
|
|
154
|
+
if not version or payload.get("version") not in (None, ""):
|
|
155
|
+
return payload
|
|
156
|
+
merged = dict(payload)
|
|
157
|
+
merged["version"] = int(version) if version.isdigit() else version
|
|
158
|
+
return merged
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def validate_create_request(
|
|
162
|
+
op_id: str,
|
|
163
|
+
*,
|
|
164
|
+
address: str | None,
|
|
165
|
+
payload: dict[str, Any],
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Fail fast with actionable CLI messages before calling the API."""
|
|
168
|
+
import sys
|
|
169
|
+
|
|
170
|
+
if op_id in _ADDRESS_REQUIRED_OPS and not address:
|
|
171
|
+
print(
|
|
172
|
+
"Error: create lock requires ADDRESS (tree path to lock).",
|
|
173
|
+
file=sys.stderr,
|
|
174
|
+
)
|
|
175
|
+
raise SystemExit(USAGE_ERROR)
|
|
176
|
+
|
|
177
|
+
spec = _CREATE_REQUIREMENTS.get(op_id)
|
|
178
|
+
if spec is None:
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
required_fields, message = spec
|
|
182
|
+
if all(payload.get(field) not in (None, "") for field in required_fields):
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
print(f"Error: {message}", file=sys.stderr)
|
|
186
|
+
raise SystemExit(USAGE_ERROR)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def format_usage_error(exc: ValueError) -> str:
|
|
190
|
+
return f"Invalid -f body: {exc}"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def sdk_path_for_body_payload(
|
|
194
|
+
op_id: str,
|
|
195
|
+
path: str | None,
|
|
196
|
+
body_payload: dict[str, Any] | None,
|
|
197
|
+
) -> str | None:
|
|
198
|
+
"""Return the path argument for ``build_sdk_call`` (omit when merged into body)."""
|
|
199
|
+
if body_payload is None:
|
|
200
|
+
return path
|
|
201
|
+
if op_id in _BODY_PAYLOAD_PATH_ARG_OPS:
|
|
202
|
+
return path
|
|
203
|
+
return None
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Click group helpers."""
|
|
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 ._lazy_mixin import ClickLazyMixin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DefaultCommandGroup(click.Group):
|
|
14
|
+
"""Group that routes unknown first tokens to a default subcommand."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, *args: Any, default_command: str, **kwargs: Any) -> None:
|
|
17
|
+
super().__init__(*args, **kwargs)
|
|
18
|
+
self.default_command = default_command
|
|
19
|
+
|
|
20
|
+
def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
|
|
21
|
+
if args and args[0] not in self.commands and not args[0].startswith("-"):
|
|
22
|
+
args = [self.default_command, *args]
|
|
23
|
+
return super().parse_args(ctx, args)
|
|
24
|
+
|
|
25
|
+
def _default_command(self, ctx: click.Context) -> click.Command | None:
|
|
26
|
+
cmd = self.get_command(ctx, self.default_command)
|
|
27
|
+
if cmd is None:
|
|
28
|
+
return None
|
|
29
|
+
return cmd
|
|
30
|
+
|
|
31
|
+
def resolve_command(
|
|
32
|
+
self,
|
|
33
|
+
ctx: click.Context,
|
|
34
|
+
args: list[str],
|
|
35
|
+
) -> tuple[str | None, click.Command | None, list[str]]:
|
|
36
|
+
if not args:
|
|
37
|
+
return None, None, args
|
|
38
|
+
|
|
39
|
+
first = args[0]
|
|
40
|
+
if first in self.commands:
|
|
41
|
+
return super().resolve_command(ctx, args)
|
|
42
|
+
|
|
43
|
+
if ctx.token_normalize_func is not None:
|
|
44
|
+
normalized = ctx.token_normalize_func(first)
|
|
45
|
+
if normalized in self.commands:
|
|
46
|
+
return super().resolve_command(ctx, args)
|
|
47
|
+
|
|
48
|
+
default_cmd = self._default_command(ctx)
|
|
49
|
+
if default_cmd is None:
|
|
50
|
+
return super().resolve_command(ctx, args)
|
|
51
|
+
|
|
52
|
+
if first.startswith("-") or first not in self.commands:
|
|
53
|
+
return self.default_command, default_cmd, args
|
|
54
|
+
|
|
55
|
+
return super().resolve_command(ctx, args)
|
|
56
|
+
|
|
57
|
+
def shell_complete(
|
|
58
|
+
self,
|
|
59
|
+
ctx: click.Context,
|
|
60
|
+
incomplete: str,
|
|
61
|
+
) -> list[click.shell_completion.CompletionItem]:
|
|
62
|
+
results = super().shell_complete(ctx, incomplete)
|
|
63
|
+
default_cmd = self._default_command(ctx)
|
|
64
|
+
if default_cmd is None:
|
|
65
|
+
return results
|
|
66
|
+
|
|
67
|
+
seen = {item.value for item in results}
|
|
68
|
+
for item in default_cmd.shell_complete(ctx, incomplete):
|
|
69
|
+
if item.value not in seen:
|
|
70
|
+
results.append(item)
|
|
71
|
+
seen.add(item.value)
|
|
72
|
+
return results
|
|
73
|
+
|
|
74
|
+
def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
75
|
+
"""Include the default subcommand's options in group help."""
|
|
76
|
+
self.format_usage(ctx, formatter)
|
|
77
|
+
self.format_help_text(ctx, formatter)
|
|
78
|
+
default_cmd = self.get_command(ctx, self.default_command)
|
|
79
|
+
if default_cmd is not None:
|
|
80
|
+
with formatter.section("Options"):
|
|
81
|
+
opts: list[tuple[str, str]] = []
|
|
82
|
+
for param in default_cmd.get_params(ctx):
|
|
83
|
+
record = param.get_help_record(ctx)
|
|
84
|
+
if record is not None:
|
|
85
|
+
opts.append(record)
|
|
86
|
+
if opts:
|
|
87
|
+
formatter.write_dl(opts)
|
|
88
|
+
self.format_commands(ctx, formatter)
|
|
89
|
+
self.format_epilog(ctx, formatter)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ResourceAliasGroup(click.Group):
|
|
93
|
+
"""Group that lists resource aliases on one help line per canonical command."""
|
|
94
|
+
|
|
95
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
96
|
+
super().__init__(*args, **kwargs)
|
|
97
|
+
self._alias_to_canonical: dict[str, str] = {}
|
|
98
|
+
self._aliases_for_resource: dict[str, list[str]] = {}
|
|
99
|
+
|
|
100
|
+
def add_resource_command(
|
|
101
|
+
self,
|
|
102
|
+
cmd: click.Command,
|
|
103
|
+
*,
|
|
104
|
+
canonical: str,
|
|
105
|
+
aliases: list[str] | None = None,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Register a command under its canonical name and optional aliases."""
|
|
108
|
+
self.add_command(cmd, name=canonical)
|
|
109
|
+
alias_list = aliases or []
|
|
110
|
+
if alias_list:
|
|
111
|
+
self._aliases_for_resource[canonical] = alias_list
|
|
112
|
+
for alias in alias_list:
|
|
113
|
+
self._alias_to_canonical[alias] = canonical
|
|
114
|
+
self.add_command(cmd, name=alias)
|
|
115
|
+
|
|
116
|
+
def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
117
|
+
commands: list[tuple[str, click.Command]] = []
|
|
118
|
+
for subcommand in self.list_commands(ctx):
|
|
119
|
+
if subcommand in self._alias_to_canonical:
|
|
120
|
+
continue
|
|
121
|
+
cmd = self.get_command(ctx, subcommand)
|
|
122
|
+
if cmd is None or cmd.hidden:
|
|
123
|
+
continue
|
|
124
|
+
commands.append((subcommand, cmd))
|
|
125
|
+
|
|
126
|
+
if not commands:
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
limit = formatter.width - 6 - max(len(name) for name, _ in commands)
|
|
130
|
+
|
|
131
|
+
rows: list[tuple[str, str]] = []
|
|
132
|
+
for subcommand, cmd in commands:
|
|
133
|
+
help_text = cmd.get_short_help_str(limit=limit)
|
|
134
|
+
aliases = self._aliases_for_resource.get(subcommand, [])
|
|
135
|
+
if aliases:
|
|
136
|
+
suffix = f" (aliases: {', '.join(aliases)})"
|
|
137
|
+
help_text = f"{help_text}{suffix}" if help_text else suffix.strip()
|
|
138
|
+
rows.append((subcommand, help_text))
|
|
139
|
+
|
|
140
|
+
with formatter.section("Commands"):
|
|
141
|
+
formatter.write_dl(rows)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def ensure_resource_alias_group(
|
|
145
|
+
root: click.Group,
|
|
146
|
+
name: str,
|
|
147
|
+
*,
|
|
148
|
+
help: str,
|
|
149
|
+
) -> ResourceAliasGroup:
|
|
150
|
+
"""Return an existing relocate group or create a ``ResourceAliasGroup`` stub."""
|
|
151
|
+
group = root.commands.get(name)
|
|
152
|
+
if isinstance(group, ResourceAliasGroup):
|
|
153
|
+
return group
|
|
154
|
+
if isinstance(group, click.Group):
|
|
155
|
+
return group # type: ignore[return-value]
|
|
156
|
+
|
|
157
|
+
@click.group(name, cls=ResourceAliasGroup, help=help)
|
|
158
|
+
@click.pass_context
|
|
159
|
+
def group_cmd(ctx: click.Context) -> None:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
root.add_command(group_cmd)
|
|
163
|
+
return group_cmd
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def attach_resource_command(
|
|
167
|
+
group: click.Group,
|
|
168
|
+
cmd: click.Command,
|
|
169
|
+
*,
|
|
170
|
+
canonical: str,
|
|
171
|
+
aliases: list[str] | None = None,
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Register a command on a group, using alias metadata when supported."""
|
|
174
|
+
if isinstance(group, ResourceAliasGroup):
|
|
175
|
+
group.add_resource_command(cmd, canonical=canonical, aliases=aliases)
|
|
176
|
+
elif isinstance(group, click.Group):
|
|
177
|
+
group.add_command(cmd, name=canonical)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class ConfigPathHelpGroup(click.Group):
|
|
181
|
+
"""Group that appends the local CLI config file path to help."""
|
|
182
|
+
|
|
183
|
+
def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
184
|
+
super().format_help(ctx, formatter)
|
|
185
|
+
from ._config import format_config_path_note
|
|
186
|
+
|
|
187
|
+
formatter.write_paragraph()
|
|
188
|
+
formatter.write_text(format_config_path_note())
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class LazyGroup(ClickLazyMixin, click.Group):
|
|
192
|
+
"""Group stub that builds the real group on first use."""
|
|
193
|
+
|
|
194
|
+
def __init__(
|
|
195
|
+
self,
|
|
196
|
+
name: str,
|
|
197
|
+
*,
|
|
198
|
+
help: str,
|
|
199
|
+
build_fn: Callable[[], click.Group],
|
|
200
|
+
) -> None:
|
|
201
|
+
super().__init__(name, help=help)
|
|
202
|
+
self._build_fn = build_fn
|
|
203
|
+
self._real: click.Group | None = None
|
|
204
|
+
|
|
205
|
+
def _lazy_target(self) -> click.Group:
|
|
206
|
+
return self._ensure()
|
|
207
|
+
|
|
208
|
+
def _ensure(self) -> click.Group:
|
|
209
|
+
if self._real is None:
|
|
210
|
+
self._real = self._build_fn()
|
|
211
|
+
return self._real
|
|
212
|
+
|
|
213
|
+
def list_commands(self, ctx: click.Context) -> list[str]:
|
|
214
|
+
return self._ensure().list_commands(ctx)
|
|
215
|
+
|
|
216
|
+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
|
|
217
|
+
return self._ensure().get_command(ctx, cmd_name)
|
|
218
|
+
|
|
219
|
+
def resolve_command(
|
|
220
|
+
self,
|
|
221
|
+
ctx: click.Context,
|
|
222
|
+
args: list[str],
|
|
223
|
+
) -> tuple[str | None, click.Command | None, list[str]]:
|
|
224
|
+
return self._ensure().resolve_command(ctx, args)
|
|
225
|
+
|
|
226
|
+
def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
227
|
+
self._ensure().format_help(ctx, formatter)
|