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,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
|