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,310 @@
|
|
|
1
|
+
"""Map CLI addresses to SDK method calls for generated commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
# First positional argument name when address is provided (by op_id).
|
|
9
|
+
_POSITIONAL_ADDRESS: dict[str, str] = {
|
|
10
|
+
"show_namespace": "namespace",
|
|
11
|
+
"delete_namespace": "namespace",
|
|
12
|
+
"update_namespace": "namespace",
|
|
13
|
+
"get_global_audit_event": "event_id",
|
|
14
|
+
"get_namespace_audit_event": "event_id",
|
|
15
|
+
"get_global_permission": "rule_id",
|
|
16
|
+
"delete_global_permission": "rule_id",
|
|
17
|
+
"update_global_permission": "rule_id",
|
|
18
|
+
"move_global_permission": "rule_id",
|
|
19
|
+
"get_lock": "path",
|
|
20
|
+
"create_lock": "path",
|
|
21
|
+
"replace_lock": "path",
|
|
22
|
+
"delete_lock": "path",
|
|
23
|
+
"set_tag": "path",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Keyword name for address when not using default ``path``.
|
|
27
|
+
_KEYWORD_ADDRESS: dict[str, str] = {
|
|
28
|
+
"create_namespace": "name",
|
|
29
|
+
"create_global_permission": "id",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Create/update ops whose ADDRESS is not a tree path (skip segment validation).
|
|
33
|
+
NON_TREE_ADDRESS_OPS = frozenset(
|
|
34
|
+
{
|
|
35
|
+
"create_namespace",
|
|
36
|
+
"create_global_permission",
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Operations with multiple positional arguments after kwargs are assembled.
|
|
41
|
+
_MULTI_POSITIONAL: dict[str, list[str]] = {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class SdkExtraParam:
|
|
46
|
+
"""Extra CLI flag mapped to an SDK keyword argument."""
|
|
47
|
+
|
|
48
|
+
sdk_name: str
|
|
49
|
+
click_name: str
|
|
50
|
+
help: str
|
|
51
|
+
type_: type | None = str
|
|
52
|
+
is_flag: bool = False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_SDK_PARAM_CATALOG: dict[str, SdkExtraParam] = {
|
|
56
|
+
"q": SdkExtraParam("q", "q", "Search query string."),
|
|
57
|
+
"types": SdkExtraParam("types", "types", "Filter by node types (comma-separated)."),
|
|
58
|
+
"limit": SdkExtraParam("limit", "limit", "Maximum number of results.", int),
|
|
59
|
+
"offset": SdkExtraParam("offset", "offset", "Result offset for pagination.", int),
|
|
60
|
+
"object_id": SdkExtraParam("object_id", "object-id", "Audit object ID."),
|
|
61
|
+
"object_type": SdkExtraParam("object_type", "object-type", "Audit object type."),
|
|
62
|
+
"search": SdkExtraParam("search", "search", "Free-text search filter."),
|
|
63
|
+
"from_": SdkExtraParam("from_", "from", "Range start (ISO timestamp)."),
|
|
64
|
+
"to": SdkExtraParam("to", "to", "Range end (ISO timestamp)."),
|
|
65
|
+
"bucket_seconds": SdkExtraParam("bucket_seconds", "bucket-seconds", "Bucket size in seconds.", int),
|
|
66
|
+
"tag": SdkExtraParam("tag", "tag", "Version tag name."),
|
|
67
|
+
"no_creds": SdkExtraParam("no_creds", "no-creds", "Omit credential-backed parameters.", is_flag=True),
|
|
68
|
+
"auth_id": SdkExtraParam("auth_id", "auth-id", "Filter by author ID."),
|
|
69
|
+
"auth_email": SdkExtraParam("auth_email", "auth-email", "Filter by author email."),
|
|
70
|
+
"event_id": SdkExtraParam("event_id", "event-id", "Filter by event ID."),
|
|
71
|
+
"event_kind": SdkExtraParam("event_kind", "event-kind", "Filter by event kind."),
|
|
72
|
+
"token_number": SdkExtraParam("token_number", "token-number", "Resolver token number.", int),
|
|
73
|
+
"reason": SdkExtraParam("reason", "reason", "Lock rationale."),
|
|
74
|
+
"expires_at": SdkExtraParam("expires_at", "expires-at", "Lock expiry time (ISO-8601 UTC)."),
|
|
75
|
+
"position": SdkExtraParam(
|
|
76
|
+
"position",
|
|
77
|
+
"position",
|
|
78
|
+
"Sort position for the new rule (applied via move after create).",
|
|
79
|
+
float,
|
|
80
|
+
),
|
|
81
|
+
"description": SdkExtraParam(
|
|
82
|
+
"description",
|
|
83
|
+
"description",
|
|
84
|
+
"Human-readable namespace description.",
|
|
85
|
+
),
|
|
86
|
+
"tagged_only": SdkExtraParam(
|
|
87
|
+
"tagged_only",
|
|
88
|
+
"tagged-only",
|
|
89
|
+
"Show only versions that have tags.",
|
|
90
|
+
is_flag=True,
|
|
91
|
+
),
|
|
92
|
+
"preview": SdkExtraParam(
|
|
93
|
+
"preview",
|
|
94
|
+
"preview",
|
|
95
|
+
"Show what would be deleted without removing anything.",
|
|
96
|
+
is_flag=True,
|
|
97
|
+
),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Per-operation extra SDK kwargs exposed as CLI flags.
|
|
101
|
+
_OP_EXTRA_PARAMS: dict[str, list[str]] = {
|
|
102
|
+
"search_root": ["q", "types", "limit", "offset"],
|
|
103
|
+
"search_path": ["q", "types", "limit", "offset"],
|
|
104
|
+
"namespace_audit_timeline": ["object_id", "object_type", "search", "limit", "offset"],
|
|
105
|
+
"namespace_audit_resolve_series": ["object_id", "object_type", "from_", "to", "bucket_seconds"],
|
|
106
|
+
"list_namespace_audit": [
|
|
107
|
+
"auth_id",
|
|
108
|
+
"auth_email",
|
|
109
|
+
"object_id",
|
|
110
|
+
"object_type",
|
|
111
|
+
"event_id",
|
|
112
|
+
"event_kind",
|
|
113
|
+
"search",
|
|
114
|
+
"from_",
|
|
115
|
+
"to",
|
|
116
|
+
"limit",
|
|
117
|
+
"offset",
|
|
118
|
+
],
|
|
119
|
+
"list_global_audit": [
|
|
120
|
+
"auth_id",
|
|
121
|
+
"auth_email",
|
|
122
|
+
"object_id",
|
|
123
|
+
"object_type",
|
|
124
|
+
"event_id",
|
|
125
|
+
"event_kind",
|
|
126
|
+
"search",
|
|
127
|
+
"from_",
|
|
128
|
+
"to",
|
|
129
|
+
"limit",
|
|
130
|
+
"offset",
|
|
131
|
+
],
|
|
132
|
+
"set_tag": ["tag"],
|
|
133
|
+
"rotate_resolver_token": ["token_number"],
|
|
134
|
+
"create_lock": ["reason", "expires_at"],
|
|
135
|
+
"replace_lock": ["reason", "expires_at"],
|
|
136
|
+
"create_namespace": ["description"],
|
|
137
|
+
"create_global_permission": ["position"],
|
|
138
|
+
"list_item_versions": ["limit", "offset", "tagged_only"],
|
|
139
|
+
"delete_item": ["preview"],
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Operations that accept version/tag via address@VER or --version.
|
|
143
|
+
VERSION_ADDRESS_OPS = frozenset(
|
|
144
|
+
{
|
|
145
|
+
"get_item",
|
|
146
|
+
"delete_item",
|
|
147
|
+
"propagate_config",
|
|
148
|
+
"get_config_data_schema",
|
|
149
|
+
"set_tag",
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# ADDRESS@VER is folded into the request body (not an SDK ``version`` kwarg).
|
|
154
|
+
VERSION_BODY_OPS = frozenset(
|
|
155
|
+
{
|
|
156
|
+
"set_tag",
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# ADDRESS@VER is a filter/query for these ops, not an SDK ``version`` argument.
|
|
161
|
+
VERSION_FILTER_OPS = frozenset(
|
|
162
|
+
{
|
|
163
|
+
"list_item_versions",
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Operations that do not accept a tree path / address argument.
|
|
168
|
+
NO_ADDRESS_OPS = frozenset(
|
|
169
|
+
{
|
|
170
|
+
"namespace_audit_timeline",
|
|
171
|
+
"namespace_audit_resolve_series",
|
|
172
|
+
"search_root",
|
|
173
|
+
"list_namespaces",
|
|
174
|
+
"list_global_audit",
|
|
175
|
+
"list_namespace_audit",
|
|
176
|
+
"list_global_permissions",
|
|
177
|
+
"list_locks",
|
|
178
|
+
"list_cast_formats",
|
|
179
|
+
"health",
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def address_optional_for_command(
|
|
185
|
+
op_ids: list[str],
|
|
186
|
+
*,
|
|
187
|
+
action: str,
|
|
188
|
+
resource: str,
|
|
189
|
+
) -> bool:
|
|
190
|
+
"""True when ADDRESS may be omitted (list-without-path or scoped list ops)."""
|
|
191
|
+
if action == "create" and resource == "globalpermission":
|
|
192
|
+
return True
|
|
193
|
+
if action == "get" and resource in ("item", "cast"):
|
|
194
|
+
return True
|
|
195
|
+
return any(op_id in NO_ADDRESS_OPS for op_id in op_ids)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def address_required_for_op(op_id: str, *, action: str, resource: str) -> bool:
|
|
199
|
+
"""True when the resolved SDK operation needs a CLI ADDRESS."""
|
|
200
|
+
if action == "create" and resource == "globalpermission":
|
|
201
|
+
return False
|
|
202
|
+
return op_id not in NO_ADDRESS_OPS
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def extra_params_for_ops(op_ids: list[str]) -> list[SdkExtraParam]:
|
|
206
|
+
"""Return sorted unique extra parameter specs for a generated command."""
|
|
207
|
+
seen: dict[str, SdkExtraParam] = {}
|
|
208
|
+
for op_id in op_ids:
|
|
209
|
+
for name in _OP_EXTRA_PARAMS.get(op_id, []):
|
|
210
|
+
seen[name] = _SDK_PARAM_CATALOG[name]
|
|
211
|
+
return [seen[k] for k in sorted(seen)]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def pick_op_id(
|
|
215
|
+
op_ids: list[str],
|
|
216
|
+
*,
|
|
217
|
+
address: str | None,
|
|
218
|
+
namespace: str | None,
|
|
219
|
+
ops_meta: dict[str, dict[str, Any]],
|
|
220
|
+
) -> str:
|
|
221
|
+
"""Choose the SDK operation when several map to the same CLI command."""
|
|
222
|
+
if len(op_ids) == 1:
|
|
223
|
+
return op_ids[0]
|
|
224
|
+
|
|
225
|
+
candidates = list(op_ids)
|
|
226
|
+
|
|
227
|
+
# Mixed client/namespace audit & permission ops: prefer scope matching -n.
|
|
228
|
+
if namespace:
|
|
229
|
+
scoped = [o for o in candidates if ops_meta.get(o, {}).get("scope") == "namespace"]
|
|
230
|
+
if scoped:
|
|
231
|
+
candidates = scoped
|
|
232
|
+
else:
|
|
233
|
+
scoped = [o for o in candidates if ops_meta.get(o, {}).get("scope") == "client"]
|
|
234
|
+
if scoped:
|
|
235
|
+
candidates = scoped
|
|
236
|
+
|
|
237
|
+
if address:
|
|
238
|
+
singles = [
|
|
239
|
+
o
|
|
240
|
+
for o in candidates
|
|
241
|
+
if o.startswith(("show_", "get_", "search_path", "navigate_path")) or o.endswith("_path")
|
|
242
|
+
]
|
|
243
|
+
if singles:
|
|
244
|
+
return singles[0]
|
|
245
|
+
non_list = [o for o in candidates if not o.startswith("list_") and not o.endswith("_root")]
|
|
246
|
+
return non_list[0] if non_list else candidates[0]
|
|
247
|
+
|
|
248
|
+
lists = [o for o in candidates if o.startswith("list_") or o.endswith("_root")]
|
|
249
|
+
return lists[0] if lists else candidates[0]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def address_keyword_for_op(op_id: str) -> str:
|
|
253
|
+
"""SDK keyword that receives the CLI ADDRESS for ``op_id``."""
|
|
254
|
+
return _KEYWORD_ADDRESS.get(op_id, "path")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def build_sdk_call(
|
|
258
|
+
op_id: str,
|
|
259
|
+
*,
|
|
260
|
+
path: str | None,
|
|
261
|
+
version: str | None,
|
|
262
|
+
content: str | None,
|
|
263
|
+
extra: dict[str, Any] | None = None,
|
|
264
|
+
) -> tuple[list[Any], dict[str, Any]]:
|
|
265
|
+
"""Return (positional_args, keyword_args) for an SDK facade method."""
|
|
266
|
+
from ocmo._facade_meta import DOCUMENT_BODY_OPS
|
|
267
|
+
|
|
268
|
+
args: list[Any] = []
|
|
269
|
+
kwargs: dict[str, Any] = {}
|
|
270
|
+
|
|
271
|
+
if path is not None:
|
|
272
|
+
if op_id in _POSITIONAL_ADDRESS:
|
|
273
|
+
args.append(path)
|
|
274
|
+
else:
|
|
275
|
+
kw = address_keyword_for_op(op_id)
|
|
276
|
+
kwargs[kw] = path
|
|
277
|
+
|
|
278
|
+
if version is not None and op_id not in VERSION_FILTER_OPS and op_id not in VERSION_BODY_OPS:
|
|
279
|
+
kwargs["version"] = version
|
|
280
|
+
|
|
281
|
+
if content is not None:
|
|
282
|
+
if op_id in DOCUMENT_BODY_OPS:
|
|
283
|
+
kwargs["content"] = content
|
|
284
|
+
else:
|
|
285
|
+
kwargs["body"] = content
|
|
286
|
+
|
|
287
|
+
if extra:
|
|
288
|
+
for key, value in extra.items():
|
|
289
|
+
if value is not None and value is not False:
|
|
290
|
+
kwargs[key] = value
|
|
291
|
+
|
|
292
|
+
multi = _MULTI_POSITIONAL.get(op_id)
|
|
293
|
+
if multi:
|
|
294
|
+
for name in multi:
|
|
295
|
+
if name in kwargs:
|
|
296
|
+
args.append(kwargs.pop(name))
|
|
297
|
+
|
|
298
|
+
return args, kwargs
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def unwrap_list_payload(data: Any) -> list[dict[str, Any]] | None:
|
|
302
|
+
"""Extract a list of row dicts from common paginated API responses."""
|
|
303
|
+
if isinstance(data, list):
|
|
304
|
+
return [r for r in data if isinstance(r, dict)]
|
|
305
|
+
if isinstance(data, dict):
|
|
306
|
+
for key in ("items", "children", "versions", "locks", "rules", "formats"):
|
|
307
|
+
value = data.get(key)
|
|
308
|
+
if isinstance(value, list):
|
|
309
|
+
return [r for r in value if isinstance(r, dict)]
|
|
310
|
+
return None
|
ocmo_cli/_typing.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Shared typing helpers for Click decorators and deferred SDK imports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any, Protocol, TypeAlias, TypeVar
|
|
7
|
+
|
|
8
|
+
ClickDecorator: TypeAlias = Callable[[Callable[..., Any]], Callable[..., Any]]
|
|
9
|
+
|
|
10
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ResolvedArtifact(Protocol):
|
|
14
|
+
name: str | None
|
|
15
|
+
version: int | None
|
|
16
|
+
format: str | None
|
|
17
|
+
checksum: str | None
|
|
18
|
+
trace: dict[str, Any]
|
|
19
|
+
url: str | None
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def bytes(self) -> bytes: ...
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def text(self) -> str: ...
|
|
26
|
+
|
|
27
|
+
def get(self, path: str, *, default: Any = ...) -> Any: ...
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Helpers for ``ocmo get version`` address and query handling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def apply_version_address_query(extra: dict[str, Any], version: str | None) -> None:
|
|
9
|
+
"""Map ADDRESS@VER suffix to list_item_versions query parameters.
|
|
10
|
+
|
|
11
|
+
``@latest``, ``@stable``, and other tag names are passed as ``q`` (search),
|
|
12
|
+
not as implicit ``limit=1`` — a tag may point at an older version.
|
|
13
|
+
"""
|
|
14
|
+
if not version or extra.get("q") is not None:
|
|
15
|
+
return
|
|
16
|
+
extra.setdefault("q", version)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def emit_item_versions_output(
|
|
20
|
+
view: Any,
|
|
21
|
+
path: str,
|
|
22
|
+
*,
|
|
23
|
+
version: str | None,
|
|
24
|
+
output_fmt: str | None,
|
|
25
|
+
ctx_fmt: str | None,
|
|
26
|
+
field: str | None = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Emit version history for *path* using ``get version`` output formatting."""
|
|
29
|
+
from ._output import as_dict, extract_field, sanitize_for_output
|
|
30
|
+
from ._output_manifest import emit_command_output
|
|
31
|
+
|
|
32
|
+
list_kwargs: dict[str, Any] = {}
|
|
33
|
+
apply_version_address_query(list_kwargs, version)
|
|
34
|
+
result = view.list_item_versions(path=path, **list_kwargs)
|
|
35
|
+
data = as_dict(result)
|
|
36
|
+
if field:
|
|
37
|
+
extract_field(sanitize_for_output(data), field)
|
|
38
|
+
return
|
|
39
|
+
emit_command_output("get version", data, output_fmt, ctx_fmt=ctx_fmt)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Output helpers for ``ocmo whoami``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ._output import as_dict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def whoami_table_row(data: Any) -> dict[str, Any]:
|
|
11
|
+
"""Flatten whoami API payload for table output."""
|
|
12
|
+
payload = as_dict(data, fallback_vars=False)
|
|
13
|
+
if not payload:
|
|
14
|
+
return {}
|
|
15
|
+
|
|
16
|
+
row: dict[str, Any] = {
|
|
17
|
+
"auth_type": payload.get("auth_type"),
|
|
18
|
+
"identifier": payload.get("identifier"),
|
|
19
|
+
"display_name": payload.get("display_name"),
|
|
20
|
+
"access_scope": payload.get("access_scope"),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
user_details = payload.get("user_details")
|
|
24
|
+
if isinstance(user_details, dict):
|
|
25
|
+
row["email"] = user_details.get("email")
|
|
26
|
+
row["is_global_admin"] = user_details.get("is_global_admin")
|
|
27
|
+
row["claims"] = user_details.get("claims")
|
|
28
|
+
return row
|
|
29
|
+
|
|
30
|
+
resolver_details = payload.get("resolver_details")
|
|
31
|
+
if isinstance(resolver_details, dict):
|
|
32
|
+
row["namespace"] = resolver_details.get("namespace")
|
|
33
|
+
row["name"] = resolver_details.get("name")
|
|
34
|
+
row["token_number"] = resolver_details.get("token_number")
|
|
35
|
+
|
|
36
|
+
return row
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def whoami_table_rows(data: Any) -> list[dict[str, Any]]:
|
|
40
|
+
if isinstance(data, list):
|
|
41
|
+
return [whoami_table_row(item) for item in data]
|
|
42
|
+
row = whoami_table_row(data)
|
|
43
|
+
return [row] if row else []
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Commands package.
|
ocmo_cli/commands/api.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""ocmo api — escape hatch to invoke any SDK operation directly.
|
|
2
|
+
|
|
3
|
+
This is the guarantee that no API capability is ever unreachable from the CLI.
|
|
4
|
+
Prints raw JSON. Bypasses all ergonomic layers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Any, NoReturn
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from .._client import OcmoCtx
|
|
15
|
+
from .._errors import handle_sdk_error
|
|
16
|
+
from .._output import as_dict, emit
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@click.command("api", hidden=True)
|
|
20
|
+
@click.argument("operation_id")
|
|
21
|
+
@click.option(
|
|
22
|
+
"--param", "-p", "params", multiple=True, metavar="KEY=VALUE", help="Operation parameters (may be repeated)."
|
|
23
|
+
)
|
|
24
|
+
@click.option(
|
|
25
|
+
"-f", "--file", "file_path", default=None, metavar="FILE|-", help="Request body from file ('-' for stdin)."
|
|
26
|
+
)
|
|
27
|
+
@click.option("-n", "--namespace", default=None)
|
|
28
|
+
@click.pass_obj
|
|
29
|
+
def api_cmd(
|
|
30
|
+
ctx: OcmoCtx,
|
|
31
|
+
operation_id: str,
|
|
32
|
+
params: tuple[str, ...],
|
|
33
|
+
file_path: str | None,
|
|
34
|
+
namespace: str | None,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Invoke any API operation directly by operation_id.
|
|
37
|
+
|
|
38
|
+
This is the escape hatch for operations marked `skip` in commands.yaml,
|
|
39
|
+
and for advanced usage. Output is always raw JSON.
|
|
40
|
+
|
|
41
|
+
\b
|
|
42
|
+
Examples:
|
|
43
|
+
ocmo api whoami
|
|
44
|
+
ocmo api get_item --param path=app/web -n prod
|
|
45
|
+
ocmo api create_config --param path=app/new -f body.yaml -n prod
|
|
46
|
+
"""
|
|
47
|
+
kwargs: dict[str, Any] = {}
|
|
48
|
+
|
|
49
|
+
# Parse --param
|
|
50
|
+
for p in params:
|
|
51
|
+
if "=" not in p:
|
|
52
|
+
print(f"Error: --param must be KEY=VALUE, got {p!r}", file=sys.stderr)
|
|
53
|
+
raise SystemExit(2)
|
|
54
|
+
k, v = p.split("=", 1)
|
|
55
|
+
kwargs[k] = v
|
|
56
|
+
|
|
57
|
+
# Read body
|
|
58
|
+
if file_path:
|
|
59
|
+
if file_path == "-":
|
|
60
|
+
kwargs["content"] = sys.stdin.read()
|
|
61
|
+
else:
|
|
62
|
+
with open(file_path) as f:
|
|
63
|
+
kwargs["content"] = f.read()
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
result = _dispatch(ctx, operation_id, namespace, kwargs)
|
|
67
|
+
if result is not None:
|
|
68
|
+
if isinstance(result, list):
|
|
69
|
+
emit([as_dict(item) for item in result], "json")
|
|
70
|
+
else:
|
|
71
|
+
payload = as_dict(result, fallback_vars=False) or result
|
|
72
|
+
emit(payload, "json")
|
|
73
|
+
except KeyboardInterrupt:
|
|
74
|
+
raise SystemExit(130)
|
|
75
|
+
except SystemExit:
|
|
76
|
+
raise
|
|
77
|
+
except Exception as exc:
|
|
78
|
+
handle_sdk_error(exc)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _dispatch(ctx: OcmoCtx, op_id: str, namespace: str | None, kwargs: dict[str, Any]) -> Any:
|
|
82
|
+
"""Find the SDK method and call it."""
|
|
83
|
+
from pathlib import Path
|
|
84
|
+
|
|
85
|
+
import yaml # deferred
|
|
86
|
+
|
|
87
|
+
ops_path = Path(__file__).parent.parent.parent.parent / "sdk" / "operations.yaml"
|
|
88
|
+
scope = "namespace"
|
|
89
|
+
if ops_path.exists():
|
|
90
|
+
with ops_path.open() as f:
|
|
91
|
+
ops = yaml.safe_load(f) or {}
|
|
92
|
+
op_cfg = ops.get("operations", {}).get(op_id, {})
|
|
93
|
+
scope = op_cfg.get("scope", "namespace")
|
|
94
|
+
|
|
95
|
+
if scope == "client":
|
|
96
|
+
client = ctx.client()
|
|
97
|
+
method = getattr(client, op_id, None)
|
|
98
|
+
if method is None:
|
|
99
|
+
_no_such_op(op_id)
|
|
100
|
+
assert method is not None
|
|
101
|
+
return method(**kwargs)
|
|
102
|
+
else:
|
|
103
|
+
ns = namespace or ctx.namespace
|
|
104
|
+
if ns:
|
|
105
|
+
view = ctx.ns(ns)
|
|
106
|
+
else:
|
|
107
|
+
# Try client first, then ask for namespace
|
|
108
|
+
client = ctx.client()
|
|
109
|
+
method = getattr(client, op_id, None)
|
|
110
|
+
if method:
|
|
111
|
+
return method(**kwargs)
|
|
112
|
+
ctx.require_namespace(namespace) # will abort with error
|
|
113
|
+
raise SystemExit(2) # unreachable but satisfies type checker
|
|
114
|
+
|
|
115
|
+
method = getattr(view, op_id, None)
|
|
116
|
+
if method is None:
|
|
117
|
+
_no_such_op(op_id)
|
|
118
|
+
assert method is not None
|
|
119
|
+
return method(**kwargs)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _no_such_op(op_id: str) -> NoReturn:
|
|
123
|
+
print(f"Error: unknown operation {op_id!r}.", file=sys.stderr)
|
|
124
|
+
print("Run `ocmo api --help` or check the SDK documentation for valid operation IDs.", file=sys.stderr)
|
|
125
|
+
raise SystemExit(2)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""ocmo api-health — check API dependency health."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .._client_commands import client_command
|
|
6
|
+
|
|
7
|
+
api_health_cmd = client_command(
|
|
8
|
+
"api-health",
|
|
9
|
+
help="Check API dependency health (database, cache, and related backends).",
|
|
10
|
+
manifest_key="api-health",
|
|
11
|
+
method="health",
|
|
12
|
+
)
|