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
@@ -0,0 +1,228 @@
1
+ """Resolve command output formatting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+ from typing import Any
9
+
10
+ from ._output import (
11
+ _parse_path,
12
+ _print_jsonpath_value,
13
+ _resolve_path,
14
+ sanitize_for_output,
15
+ yaml_dumps,
16
+ )
17
+ from ._typing import ResolvedArtifact
18
+
19
+ _METADATA_FIELDS = ("name", "version", "format", "checksum")
20
+ _TRACE_BASE_INDENT = 2
21
+
22
+
23
+ def resolve_output_format(
24
+ cli_fmt: str | None,
25
+ ctx_fmt: str | None,
26
+ *,
27
+ command_key: str = "resolve",
28
+ ) -> str:
29
+ """Pick the effective resolve/document output format."""
30
+ from ._output_manifest import get_command_spec, resolve_effective_format
31
+
32
+ return resolve_effective_format(cli_fmt, ctx_fmt, get_command_spec(command_key))
33
+
34
+
35
+ def _stderr_color_enabled(*, no_color: bool = False) -> bool:
36
+ if no_color or os.environ.get("NO_COLOR") or os.environ.get("OCMO_NO_COLOR"):
37
+ return False
38
+ return sys.stderr.isatty()
39
+
40
+
41
+ def _item_metadata(item: ResolvedArtifact) -> list[tuple[str, Any]]:
42
+ rows: list[tuple[str, Any]] = []
43
+ for key in _METADATA_FIELDS:
44
+ value = getattr(item, key, None)
45
+ if value is not None and value != "":
46
+ rows.append((key, value))
47
+ return rows
48
+
49
+
50
+ def _trace_metadata_lines(item: ResolvedArtifact) -> list[str]:
51
+ trace = sanitize_for_output(getattr(item, "trace", {}) or {})
52
+ if not trace:
53
+ return []
54
+ yaml_text = yaml_dumps(trace).rstrip("\n")
55
+ if not yaml_text or yaml_text == "{}":
56
+ return []
57
+ lines = ["# trace:"]
58
+ for line in yaml_text.splitlines():
59
+ if not line.strip():
60
+ continue
61
+ yaml_indent = len(line) - len(line.lstrip(" "))
62
+ content = line.lstrip(" ")
63
+ prefix = "# " + (" " * (_TRACE_BASE_INDENT + yaml_indent))
64
+ lines.append(f"{prefix}{content}")
65
+ return lines
66
+
67
+
68
+ def _item_content(item: ResolvedArtifact) -> str:
69
+ """Resolved artifact bytes as text for json/yaml output."""
70
+ return item.text
71
+
72
+
73
+ def _item_payload(item: ResolvedArtifact, *, include_data: bool) -> dict[str, Any]:
74
+ payload: dict[str, Any] = {
75
+ "name": getattr(item, "name", None),
76
+ "version": getattr(item, "version", None),
77
+ "format": getattr(item, "format", None),
78
+ "checksum": getattr(item, "checksum", None),
79
+ "trace": sanitize_for_output(getattr(item, "trace", {}) or {}),
80
+ }
81
+ if include_data:
82
+ payload["data"] = _item_content(item)
83
+ else:
84
+ payload["url"] = getattr(item, "url", None)
85
+ return payload
86
+
87
+
88
+ def _write_report_payload(outcomes: list[Any]) -> dict[str, Any]:
89
+ from ._resolve_write import ResolveWriteOutcome
90
+
91
+ items: list[dict[str, Any]] = []
92
+ for outcome in outcomes:
93
+ if not isinstance(outcome, ResolveWriteOutcome):
94
+ continue
95
+ item = outcome.item
96
+ entry: dict[str, Any] = {
97
+ "name": getattr(item, "name", None),
98
+ "version": getattr(item, "version", None),
99
+ "format": getattr(item, "format", None),
100
+ "checksum": getattr(item, "checksum", None),
101
+ "trace": sanitize_for_output(getattr(item, "trace", {}) or {}),
102
+ "path": str(outcome.path),
103
+ "result": outcome.result,
104
+ }
105
+ if outcome.detail:
106
+ entry["detail"] = outcome.detail
107
+ items.append(entry)
108
+ return {"items": items, "length": len(items)}
109
+
110
+
111
+ def emit_write_outcome_jsonpath(outcome: Any, expr: str) -> None:
112
+ """Print one filesystem write outcome field selected by jsonpath."""
113
+ payload = _write_report_payload([outcome])
114
+ value = _resolve_path(payload, _parse_path(expr))
115
+ _print_jsonpath_value(value)
116
+
117
+
118
+ def emit_write_outcome(
119
+ outcome: Any,
120
+ fmt: str,
121
+ *,
122
+ no_color: bool = False,
123
+ ) -> None:
124
+ """Report one filesystem write outcome."""
125
+ emit_write_report([outcome], fmt, no_color=no_color)
126
+
127
+
128
+ def emit_write_report(
129
+ outcomes: list[Any],
130
+ fmt: str,
131
+ *,
132
+ no_color: bool = False,
133
+ ) -> None:
134
+ """Report filesystem write outcomes to stderr (raw) or stdout (json/yaml)."""
135
+ from ._resolve_write import ResolveWriteOutcome
136
+
137
+ if fmt in ("json", "yaml"):
138
+ payload = _write_report_payload(outcomes)
139
+ if fmt == "json":
140
+ print(json.dumps(payload, default=str, indent=2))
141
+ else:
142
+ print(yaml_dumps(payload), end="")
143
+ return
144
+
145
+ for outcome in outcomes:
146
+ if not isinstance(outcome, ResolveWriteOutcome):
147
+ continue
148
+ path = outcome.path
149
+ if outcome.result == "created":
150
+ _err_meta(f"created {path}", no_color=no_color)
151
+ elif outcome.result == "rewritten":
152
+ _err_meta(f"rewritten {path}", no_color=no_color)
153
+ elif outcome.result == "skipped":
154
+ reason = outcome.detail or "unchanged"
155
+ _err_meta(f"skipped {path} ({reason})", no_color=no_color)
156
+ elif outcome.result == "failed":
157
+ reason = outcome.detail or "failed"
158
+ _err_meta(f"failed {path}: {reason}", no_color=no_color)
159
+
160
+
161
+ def _response_payload(items: list[Any], *, include_data: bool) -> dict[str, Any]:
162
+ return {
163
+ "items": [_item_payload(item, include_data=include_data) for item in items],
164
+ "length": len(items),
165
+ }
166
+
167
+
168
+ def _err_meta(line: str, *, no_color: bool = False) -> None:
169
+ if _stderr_color_enabled(no_color=no_color):
170
+ from rich.console import Console
171
+
172
+ Console(stderr=True, highlight=False).print(line, style="bright_black")
173
+ else:
174
+ print(line, file=sys.stderr)
175
+
176
+
177
+ def emit_resolve_item_metadata(item: ResolvedArtifact, *, no_color: bool = False) -> None:
178
+ """Write one item's resolve metadata to stderr as ``# key: value`` lines."""
179
+ for key, value in _item_metadata(item):
180
+ _err_meta(f"# {key}: {value}", no_color=no_color)
181
+ for line in _trace_metadata_lines(item):
182
+ _err_meta(line, no_color=no_color)
183
+
184
+
185
+ def emit_resolve_raw(items: list[ResolvedArtifact], *, no_color: bool = False) -> None:
186
+ """Metadata on stderr, artifact content on stdout, blank line between items."""
187
+ for index, item in enumerate(items):
188
+ if index > 0:
189
+ print()
190
+ emit_resolve_item_metadata(item, no_color=no_color)
191
+ text = item.text
192
+ print(text, end="" if text.endswith("\n") else "\n")
193
+
194
+
195
+ def emit_resolve_results(
196
+ items: list[ResolvedArtifact],
197
+ fmt: str,
198
+ *,
199
+ no_color: bool = False,
200
+ include_data: bool = True,
201
+ ) -> None:
202
+ """Emit resolved items in the requested resolve output format."""
203
+ if fmt == "raw":
204
+ if not include_data:
205
+ for item in items:
206
+ emit_resolve_item_metadata(item, no_color=no_color)
207
+ return
208
+ emit_resolve_raw(items, no_color=no_color)
209
+ return
210
+
211
+ if fmt == "name":
212
+ for item in items:
213
+ name = getattr(item, "name", None)
214
+ if name:
215
+ print(name)
216
+ return
217
+
218
+ payload = _response_payload(items, include_data=include_data)
219
+
220
+ if fmt == "json":
221
+ print(json.dumps(payload, default=str, indent=2))
222
+ elif fmt == "yaml":
223
+ print(yaml_dumps(payload), end="")
224
+ elif fmt.startswith("jsonpath="):
225
+ value = _resolve_path(payload, _parse_path(fmt[9:]))
226
+ _print_jsonpath_value(value)
227
+ else:
228
+ print(payload)
@@ -0,0 +1,72 @@
1
+ """Output helpers for ``ocmo resolve parameters``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ._output import as_dict
8
+
9
+
10
+ def parameter_config_value(param: dict[str, Any]) -> Any:
11
+ """Value as declared in the ocmo configuration (type-specific)."""
12
+ ptype = param.get("type") or ""
13
+ if ptype == "projected":
14
+ return param.get("selector")
15
+ if ptype == "dynamic":
16
+ return param.get("declared_default")
17
+ if ptype == "secret":
18
+ return param.get("secret_reference")
19
+ return None
20
+
21
+
22
+ def parameter_resolved_value(param: dict[str, Any]) -> Any:
23
+ """Effective value after evaluation, transformers, and secret masking."""
24
+ return param.get("effective_value")
25
+
26
+
27
+ def filter_parameters_data(data: Any, types: tuple[str, ...]) -> Any:
28
+ """Return payload with ``parameters`` filtered to the given types (CLI-side)."""
29
+ if not types:
30
+ return data
31
+
32
+ type_set = {item.lower() for item in types}
33
+ payload = as_dict(data, fallback_vars=False)
34
+ if not payload:
35
+ return data
36
+
37
+ parameters = payload.get("parameters") or {}
38
+ if not isinstance(parameters, dict):
39
+ return data
40
+
41
+ filtered = {
42
+ name: parameters[name]
43
+ for name in parameters
44
+ if (as_dict(parameters[name], fallback_vars=False).get("type") or "").lower() in type_set
45
+ }
46
+ return {**payload, "parameters": filtered}
47
+
48
+
49
+ def resolve_parameters_table_rows(data: Any) -> list[dict[str, Any]]:
50
+ """Flatten resolve-parameters API payload into table rows."""
51
+ payload = as_dict(data, fallback_vars=False)
52
+ parameters = payload.get("parameters") or {}
53
+ if not isinstance(parameters, dict):
54
+ return []
55
+
56
+ rows: list[dict[str, Any]] = []
57
+ for name in sorted(parameters):
58
+ param = as_dict(parameters[name], fallback_vars=False)
59
+ transformers = param.get("transformers_applied") or []
60
+ if not isinstance(transformers, list):
61
+ transformers = []
62
+ rows.append(
63
+ {
64
+ "name": name,
65
+ "type": param.get("type") or "",
66
+ "value": parameter_config_value(param),
67
+ "resolved_value": parameter_resolved_value(param),
68
+ "description": param.get("description") or "",
69
+ "transformers": ", ".join(str(item) for item in transformers),
70
+ }
71
+ )
72
+ return rows
@@ -0,0 +1,89 @@
1
+ """Resolve-series time range and bucket helpers (mirrors frontend resolveStatsChart)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime, timedelta
6
+ from typing import Any, cast
7
+
8
+ MINUTE = 60
9
+ HOUR = 60 * MINUTE
10
+ DAY = 24 * HOUR
11
+
12
+ DEFAULT_RESOLVE_RANGE = 30 * DAY
13
+ MIN_BUCKET_SECONDS = 1800
14
+
15
+
16
+ def pick_resolve_bucket_seconds(range_seconds: float) -> int:
17
+ """Pick bucket size from visible range duration (see frontend thresholds)."""
18
+ if range_seconds > 15 * DAY:
19
+ return DAY
20
+ if range_seconds > 5 * DAY:
21
+ return 12 * HOUR
22
+ if range_seconds > 3 * DAY:
23
+ return 4 * HOUR
24
+ if range_seconds > 1 * DAY:
25
+ return 2 * HOUR
26
+ if range_seconds >= 0.5 * DAY:
27
+ return HOUR
28
+ return 30 * MINUTE
29
+
30
+
31
+ def parse_cli_timestamp(value: str) -> datetime:
32
+ """Parse an ISO-8601 timestamp from a CLI flag."""
33
+ text = value.strip()
34
+ if text.endswith("Z"):
35
+ text = f"{text[:-1]}+00:00"
36
+ parsed = datetime.fromisoformat(text)
37
+ if parsed.tzinfo is None:
38
+ return parsed.replace(tzinfo=UTC)
39
+ return parsed.astimezone(UTC)
40
+
41
+
42
+ def resolve_time_range(
43
+ from_value: str | None,
44
+ to_value: str | None,
45
+ ) -> tuple[datetime, datetime]:
46
+ """Default to the last 30 days ending now; override with --from / --to."""
47
+ end = parse_cli_timestamp(to_value) if to_value else datetime.now(UTC)
48
+ start = parse_cli_timestamp(from_value) if from_value else end - timedelta(seconds=DEFAULT_RESOLVE_RANGE)
49
+ if start >= end:
50
+ raise ValueError("--from must be earlier than --to.")
51
+ return start, end
52
+
53
+
54
+ def resolve_bucket_seconds(
55
+ start: datetime,
56
+ end: datetime,
57
+ *,
58
+ override: int | None,
59
+ ) -> int:
60
+ if override is not None:
61
+ if override < MIN_BUCKET_SECONDS:
62
+ raise ValueError(f"--bucket-seconds must be at least {MIN_BUCKET_SECONDS}.")
63
+ return override
64
+ range_seconds = (end - start).total_seconds()
65
+ return max(pick_resolve_bucket_seconds(range_seconds), MIN_BUCKET_SECONDS)
66
+
67
+
68
+ def series_to_dict(result: Any) -> dict[str, Any]:
69
+ if hasattr(result, "to_dict"):
70
+ return cast(dict[str, Any], result.to_dict())
71
+ if isinstance(result, dict):
72
+ return result
73
+ return cast(dict[str, Any], vars(result))
74
+
75
+
76
+ def bucket_values(data: dict[str, Any]) -> tuple[list[dict[str, Any]], list[int], list[int], list[int]]:
77
+ raw_buckets = data.get("buckets") or []
78
+ buckets: list[dict[str, Any]] = []
79
+ for item in raw_buckets:
80
+ if isinstance(item, dict):
81
+ buckets.append(item)
82
+ elif hasattr(item, "to_dict"):
83
+ buckets.append(item.to_dict())
84
+ else:
85
+ buckets.append(vars(item))
86
+ direct = [int(b.get("direct") or 0) for b in buckets]
87
+ nested = [int(b.get("nested") or 0) for b in buckets]
88
+ errors = [int(b.get("errors") or 0) for b in buckets]
89
+ return buckets, direct, nested, errors