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.
Files changed (81) hide show
  1. LICENSE +201 -0
  2. NOTICE +16 -0
  3. ocmo_cli/__init__.py +3 -0
  4. ocmo_cli/__main__.py +4 -0
  5. ocmo_cli/_address.py +182 -0
  6. ocmo_cli/_audit_item.py +33 -0
  7. ocmo_cli/_auth_resolve.py +66 -0
  8. ocmo_cli/_body_payload.py +203 -0
  9. ocmo_cli/_click_groups.py +227 -0
  10. ocmo_cli/_client.py +351 -0
  11. ocmo_cli/_client_commands.py +35 -0
  12. ocmo_cli/_command_registry.py +29 -0
  13. ocmo_cli/_commands_map.py +65 -0
  14. ocmo_cli/_config.py +183 -0
  15. ocmo_cli/_config_content.py +43 -0
  16. ocmo_cli/_delete_item_output.py +129 -0
  17. ocmo_cli/_diff_input.py +144 -0
  18. ocmo_cli/_dry_run.py +292 -0
  19. ocmo_cli/_errors.py +151 -0
  20. ocmo_cli/_exit.py +13 -0
  21. ocmo_cli/_export_metadata.py +47 -0
  22. ocmo_cli/_generated_registry.py +45 -0
  23. ocmo_cli/_get_cast_output.py +52 -0
  24. ocmo_cli/_get_item_list.py +23 -0
  25. ocmo_cli/_globalpermission_output.py +98 -0
  26. ocmo_cli/_import_metadata.py +140 -0
  27. ocmo_cli/_import_plan.py +221 -0
  28. ocmo_cli/_item_output.py +223 -0
  29. ocmo_cli/_lazy_mixin.py +29 -0
  30. ocmo_cli/_ls_wide.py +286 -0
  31. ocmo_cli/_mutating.py +39 -0
  32. ocmo_cli/_notice.py +17 -0
  33. ocmo_cli/_options.py +77 -0
  34. ocmo_cli/_output.py +424 -0
  35. ocmo_cli/_output_manifest.py +332 -0
  36. ocmo_cli/_resolve_draft.py +50 -0
  37. ocmo_cli/_resolve_options.py +143 -0
  38. ocmo_cli/_resolve_output.py +228 -0
  39. ocmo_cli/_resolve_parameters_output.py +72 -0
  40. ocmo_cli/_resolve_series.py +89 -0
  41. ocmo_cli/_resolve_series_chart.py +409 -0
  42. ocmo_cli/_resolve_write.py +185 -0
  43. ocmo_cli/_resolver_output.py +34 -0
  44. ocmo_cli/_resource_aliases.py +22 -0
  45. ocmo_cli/_sdk_dispatch.py +310 -0
  46. ocmo_cli/_typing.py +27 -0
  47. ocmo_cli/_version_output.py +39 -0
  48. ocmo_cli/_whoami_output.py +43 -0
  49. ocmo_cli/commands/__init__.py +1 -0
  50. ocmo_cli/commands/api.py +125 -0
  51. ocmo_cli/commands/api_health.py +12 -0
  52. ocmo_cli/commands/apply.py +153 -0
  53. ocmo_cli/commands/auth.py +786 -0
  54. ocmo_cli/commands/can_i.py +51 -0
  55. ocmo_cli/commands/completion.py +59 -0
  56. ocmo_cli/commands/config_cmd.py +235 -0
  57. ocmo_cli/commands/describe.py +109 -0
  58. ocmo_cli/commands/diff.py +86 -0
  59. ocmo_cli/commands/edit.py +200 -0
  60. ocmo_cli/commands/export.py +242 -0
  61. ocmo_cli/commands/generated.py +907 -0
  62. ocmo_cli/commands/gp_move.py +108 -0
  63. ocmo_cli/commands/import_.py +595 -0
  64. ocmo_cli/commands/item_relocate.py +187 -0
  65. ocmo_cli/commands/ls.py +481 -0
  66. ocmo_cli/commands/resolve.py +658 -0
  67. ocmo_cli/commands/resolve_draft.py +102 -0
  68. ocmo_cli/commands/resolve_parameters.py +65 -0
  69. ocmo_cli/commands/resolve_series.py +109 -0
  70. ocmo_cli/commands/schema.py +112 -0
  71. ocmo_cli/commands/timeline.py +135 -0
  72. ocmo_cli/commands/version_cmd.py +102 -0
  73. ocmo_cli/commands/whoami.py +12 -0
  74. ocmo_cli/main.py +241 -0
  75. ocmo_cli/output_formats.yaml +295 -0
  76. ocmo_cli-0.8.19.dist-info/METADATA +169 -0
  77. ocmo_cli-0.8.19.dist-info/RECORD +81 -0
  78. ocmo_cli-0.8.19.dist-info/WHEEL +4 -0
  79. ocmo_cli-0.8.19.dist-info/entry_points.txt +2 -0
  80. ocmo_cli-0.8.19.dist-info/licenses/LICENSE +201 -0
  81. ocmo_cli-0.8.19.dist-info/licenses/NOTICE +16 -0
ocmo_cli/_ls_wide.py ADDED
@@ -0,0 +1,286 @@
1
+ """Wide-format enrichment for ``ocmo ls``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime
6
+ from collections.abc import Sequence
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from ._output import as_dict, format_datetime
10
+
11
+ if TYPE_CHECKING:
12
+ from ocmo import NamespaceView, OcmoClient
13
+
14
+ LS_WIDE_SORT_CHOICES = ("path", "type", "created", "updated")
15
+
16
+ _PERM_ORDER = ("r", "w", "d", "x", "t", "D", "a")
17
+
18
+ _NODE_PROBE_OPS: dict[str, dict[str, str | None]] = {
19
+ "config": {
20
+ "r": "config:read",
21
+ "w": "config:write",
22
+ "d": "config:delete",
23
+ "x": "config:resolve",
24
+ "t": "config:tag",
25
+ "D": "config:describe",
26
+ "a": "config:audit",
27
+ },
28
+ "template": {
29
+ "r": "template:read",
30
+ "w": "template:write",
31
+ "d": "template:delete",
32
+ "x": None,
33
+ "t": "template:tag",
34
+ "D": "template:describe",
35
+ "a": "template:audit",
36
+ },
37
+ "secret": {
38
+ "r": "secret:read",
39
+ "w": "secret:write",
40
+ "d": "secret:delete",
41
+ "x": "secret:resolve",
42
+ "t": "secret:tag",
43
+ "D": "secret:describe",
44
+ "a": "secret:audit",
45
+ },
46
+ "resolver": {
47
+ "r": "resolver:read",
48
+ "w": "resolver:write",
49
+ "d": "resolver:delete",
50
+ "x": None,
51
+ "t": None,
52
+ "D": None,
53
+ "a": "resolver:audit",
54
+ },
55
+ "folder": {
56
+ "r": None,
57
+ "w": None,
58
+ "d": None,
59
+ "x": None,
60
+ "t": None,
61
+ "D": "folder:describe",
62
+ "a": "folder:audit",
63
+ },
64
+ }
65
+
66
+ _VERSIONED_NODE_TYPES = frozenset({"config", "template", "secret"})
67
+ _ITEM_METADATA_NODE_TYPES = frozenset({"resolver", "folder"})
68
+
69
+ LS_TABLE_COLUMNS = ("type", "path")
70
+ LS_WIDE_COLUMNS = ("permissions", "versions", "author", "created", "updated", "type", "path")
71
+
72
+
73
+ def basic_ls_row(node: dict[str, Any]) -> dict[str, Any]:
74
+ return {
75
+ "path": node.get("path") or "",
76
+ "type": node.get("node_type") or "",
77
+ }
78
+
79
+
80
+ def probe_operations_for_node(node_type: str) -> list[str]:
81
+ ops = _NODE_PROBE_OPS.get(node_type, {})
82
+ return [op for letter in _PERM_ORDER if (op := ops.get(letter))]
83
+
84
+
85
+ def format_permission_string(node_type: str, allowed: dict[str, bool]) -> str:
86
+ if node_type == "folder":
87
+ return _format_folder_permissions(allowed)
88
+ ops = _NODE_PROBE_OPS.get(node_type, {})
89
+ chars: list[str] = []
90
+ for letter in _PERM_ORDER:
91
+ operation = ops.get(letter)
92
+ if operation is None:
93
+ chars.append("-")
94
+ elif allowed.get(operation):
95
+ chars.append(letter)
96
+ else:
97
+ chars.append("-")
98
+ return "".join(chars)
99
+
100
+
101
+ def _format_folder_permissions(allowed: dict[str, bool]) -> str:
102
+ """Folders always show read; unsettable actions use ``-`` or ``?`` (resolve)."""
103
+ chars: list[str] = []
104
+ for letter in _PERM_ORDER:
105
+ if letter == "r":
106
+ chars.append("r")
107
+ elif letter in ("w", "d", "t"):
108
+ chars.append("-")
109
+ elif letter == "x":
110
+ chars.append("?")
111
+ elif letter == "D":
112
+ chars.append("D" if allowed.get("folder:describe") else "-")
113
+ elif letter == "a":
114
+ chars.append("a" if allowed.get("folder:audit") else "-")
115
+ return "".join(chars)
116
+
117
+
118
+ def _node_dict(node: Any) -> dict[str, Any]:
119
+ return as_dict(node)
120
+
121
+
122
+ def _response_dict(result: Any) -> dict[str, Any]:
123
+ return as_dict(result)
124
+
125
+
126
+ def _fetch_permissions(client: OcmoClient, *, namespace: str, path: str, node_type: str) -> str:
127
+ operations = probe_operations_for_node(node_type)
128
+ if not operations:
129
+ return "-" * len(_PERM_ORDER)
130
+ try:
131
+ result = client.can_i(operations=operations, namespace=namespace, resource=path)
132
+ allowed = _response_dict(result).get("allowed") or {}
133
+ except Exception:
134
+ return "-" * len(_PERM_ORDER)
135
+ return format_permission_string(node_type, allowed)
136
+
137
+
138
+ def _parse_sort_datetime(value: Any) -> datetime.datetime | None:
139
+ if value is None:
140
+ return None
141
+ if isinstance(value, datetime.datetime):
142
+ return value
143
+ if isinstance(value, str) and value.strip() and value != "-":
144
+ try:
145
+ from dateutil.parser import isoparse
146
+
147
+ return isoparse(value)
148
+ except Exception:
149
+ return None
150
+ return None
151
+
152
+
153
+ def _format_timestamp(value: Any) -> str:
154
+ return format_datetime(value) or "-"
155
+
156
+
157
+ def _fetch_versioned_fields(
158
+ view: NamespaceView,
159
+ *,
160
+ path: str,
161
+ ) -> tuple[str, str, str, str, datetime.datetime | None, datetime.datetime | None]:
162
+ """Versioned items: author/created from first version, updated from latest."""
163
+ try:
164
+ latest_result = view.list_item_versions(path=path, limit=1)
165
+ data = _response_dict(latest_result)
166
+ count = data.get("versions_count")
167
+ versions = data.get("versions") or []
168
+ versions_text = str(count) if count is not None else "-"
169
+ if not versions:
170
+ return versions_text, "-", "-", "-", None, None
171
+
172
+ latest = _node_dict(versions[0])
173
+ updated_raw = latest.get("updated_at")
174
+ updated = _format_timestamp(updated_raw)
175
+ updated_at = _parse_sort_datetime(updated_raw)
176
+
177
+ first = latest
178
+ if isinstance(count, int) and count > 1:
179
+ first_result = view.list_item_versions(path=path, limit=1, offset=count - 1)
180
+ first_versions = _response_dict(first_result).get("versions") or []
181
+ if first_versions:
182
+ first = _node_dict(first_versions[0])
183
+
184
+ created_raw = first.get("updated_at")
185
+ created = _format_timestamp(created_raw)
186
+ created_at = _parse_sort_datetime(created_raw)
187
+ author = first.get("updater") or "-"
188
+ return versions_text, author, created, updated, created_at, updated_at
189
+ except Exception:
190
+ return "-", "-", "-", "-", None, None
191
+
192
+
193
+ def _fetch_item_metadata_fields(
194
+ view: NamespaceView,
195
+ *,
196
+ path: str,
197
+ ) -> tuple[str, str, str, str, datetime.datetime | None, datetime.datetime | None]:
198
+ """Non-versioned items: use ``created_at`` / ``updated_at`` from ``get_item`` when present."""
199
+ try:
200
+ result = view.get_item(path=path)
201
+ data = _response_dict(result)
202
+ author = data.get("author") or "-"
203
+ created_raw = data.get("created_at")
204
+ created = _format_timestamp(created_raw)
205
+ created_at = _parse_sort_datetime(created_raw)
206
+ updated_raw = data.get("updated_at")
207
+ updated = _format_timestamp(updated_raw)
208
+ updated_at = _parse_sort_datetime(updated_raw)
209
+ return "-", author, created, updated, created_at, updated_at
210
+ except Exception:
211
+ return "-", "-", "-", "-", None, None
212
+
213
+
214
+ def _fetch_metadata_fields(
215
+ view: NamespaceView,
216
+ *,
217
+ path: str,
218
+ node_type: str,
219
+ ) -> tuple[str, str, str, str, datetime.datetime | None, datetime.datetime | None]:
220
+ if node_type in _VERSIONED_NODE_TYPES:
221
+ return _fetch_versioned_fields(view, path=path)
222
+ if node_type in _ITEM_METADATA_NODE_TYPES:
223
+ return _fetch_item_metadata_fields(view, path=path)
224
+ return "-", "-", "-", "-", None, None
225
+
226
+
227
+ def _sort_by_timestamp(rows: list[dict[str, Any]], field: str) -> None:
228
+ rows.sort(
229
+ key=lambda row: (
230
+ row.get(field) is None,
231
+ -(row[field].timestamp() if row.get(field) is not None else 0),
232
+ row.get("path") or "",
233
+ ),
234
+ )
235
+
236
+
237
+ def sort_wide_rows(rows: list[dict[str, Any]], sort_by: str) -> None:
238
+ """Sort wide rows in place."""
239
+ if sort_by == "type":
240
+ rows.sort(key=lambda row: (row.get("type") or "", row.get("path") or ""))
241
+ elif sort_by == "created":
242
+ _sort_by_timestamp(rows, "_created_at")
243
+ elif sort_by == "updated":
244
+ _sort_by_timestamp(rows, "_updated_at")
245
+ else:
246
+ rows.sort(key=lambda row: row.get("path") or "")
247
+
248
+
249
+ def enrich_ls_rows(
250
+ *,
251
+ client: OcmoClient,
252
+ view: NamespaceView,
253
+ namespace: str,
254
+ nodes: Sequence[Any],
255
+ ) -> list[dict[str, Any]]:
256
+ """Build wide-format rows with per-item permission and version probes."""
257
+ rows: list[dict[str, Any]] = []
258
+ for node in nodes:
259
+ item = _node_dict(node)
260
+ path = item.get("path") or ""
261
+ node_type = item.get("node_type") or ""
262
+ permissions = _fetch_permissions(
263
+ client,
264
+ namespace=namespace,
265
+ path=path,
266
+ node_type=node_type,
267
+ )
268
+ versions, author, created, updated, created_at, updated_at = _fetch_metadata_fields(
269
+ view,
270
+ path=path,
271
+ node_type=node_type,
272
+ )
273
+ rows.append(
274
+ {
275
+ "permissions": permissions,
276
+ "versions": versions,
277
+ "author": author,
278
+ "created": created,
279
+ "updated": updated,
280
+ "_created_at": created_at,
281
+ "_updated_at": updated_at,
282
+ "type": node_type,
283
+ "path": path,
284
+ }
285
+ )
286
+ return rows
ocmo_cli/_mutating.py ADDED
@@ -0,0 +1,39 @@
1
+ """Shared dry-run and confirmation flow for mutating CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+
7
+ from ._client import OcmoCtx
8
+ from ._output import confirm, err, status
9
+
10
+
11
+ def run_mutating(
12
+ ctx: OcmoCtx,
13
+ *,
14
+ dry_run: bool,
15
+ yes: bool,
16
+ plan_lines: list[str] | None,
17
+ confirm_message: str | None,
18
+ action: Callable[[], None],
19
+ abort_exit_code: int = 0,
20
+ abort_via_err: bool = False,
21
+ ) -> None:
22
+ """Run a mutating action with optional dry-run plan and confirmation."""
23
+ if ctx.dry_run or dry_run:
24
+ if plan_lines:
25
+ from ._dry_run import emit_dry_run_plan # deferred
26
+
27
+ for line in plan_lines:
28
+ emit_dry_run_plan(line)
29
+ return
30
+
31
+ if confirm_message and not (yes or ctx.yes):
32
+ if not confirm(confirm_message, yes=False):
33
+ if abort_via_err:
34
+ err("Aborted.")
35
+ else:
36
+ status("Aborted.")
37
+ raise SystemExit(abort_exit_code)
38
+
39
+ action()
ocmo_cli/_notice.py ADDED
@@ -0,0 +1,17 @@
1
+ """Bundled product NOTICE for the CLI (no API access required)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from functools import lru_cache
6
+ from pathlib import Path
7
+
8
+ LICENSE_NAME = "Apache License, Version 2.0"
9
+ LICENSE_SPDX = "Apache-2.0"
10
+ _NOTICE_PATH = Path(__file__).resolve().parents[1] / "NOTICE"
11
+
12
+
13
+ @lru_cache(maxsize=1)
14
+ def load_notice_text() -> str:
15
+ if not _NOTICE_PATH.is_file():
16
+ raise FileNotFoundError(f"NOTICE file not found: {_NOTICE_PATH}")
17
+ return _NOTICE_PATH.read_text(encoding="utf-8").strip() + "\n"
ocmo_cli/_options.py ADDED
@@ -0,0 +1,77 @@
1
+ """Shared click option factories — keep flags consistent across all commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from ._output_manifest import manifest_output_option
8
+ from ._typing import ClickDecorator
9
+
10
+
11
+ def output_option(command_key: str) -> ClickDecorator:
12
+ """Standard -o / --output flag with manifest-driven choices."""
13
+ return manifest_output_option(command_key)
14
+
15
+
16
+ def get_item_output_option() -> ClickDecorator:
17
+ """``-o`` for ``get item`` (single-item document + namespace list modes)."""
18
+ from ._output_manifest import manifest_output_option_for_keys
19
+
20
+ return manifest_output_option_for_keys("get item", "get item list")
21
+
22
+
23
+ def get_cast_output_option() -> ClickDecorator:
24
+ """``-o`` for ``get cast`` (format list + per-format schema show modes)."""
25
+ from ._output_manifest import manifest_output_option_for_keys
26
+
27
+ return manifest_output_option_for_keys("get cast", "get cast list")
28
+
29
+
30
+ def ls_output_option() -> ClickDecorator:
31
+ return manifest_output_option("ls")
32
+
33
+
34
+ def can_i_output_option() -> ClickDecorator:
35
+ return manifest_output_option("can-i")
36
+
37
+
38
+ def resolve_output_option(command_key: str = "resolve") -> ClickDecorator:
39
+ return manifest_output_option(command_key)
40
+
41
+
42
+ def tree_version_option() -> ClickDecorator:
43
+ """Version/tag selector for tree item addresses."""
44
+ return click.option(
45
+ "--version",
46
+ "-V",
47
+ "version_flag",
48
+ default=None,
49
+ metavar="VER",
50
+ help=(
51
+ "Version or tag (latest, stable, <n>, or custom tag). " "May also be appended to the address as path@VER."
52
+ ),
53
+ )
54
+
55
+
56
+ def namespace_option() -> ClickDecorator:
57
+ return click.option(
58
+ "-n",
59
+ "--namespace",
60
+ default=None,
61
+ metavar="NS",
62
+ help="Namespace (overrides context and OCMO_NAMESPACE).",
63
+ )
64
+
65
+
66
+ def yes_option(*, help: str = "Skip confirmation prompts.") -> ClickDecorator:
67
+ return click.option(
68
+ "--yes",
69
+ "-y",
70
+ is_flag=True,
71
+ default=False,
72
+ help=help,
73
+ )
74
+
75
+
76
+ def dry_run_option(*, help: str = "Print plan without sending.") -> ClickDecorator:
77
+ return click.option("--dry-run", is_flag=True, default=False, help=help)