everypixel-cli 0.1.0__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.
- everypixel_cli/__init__.py +1 -0
- everypixel_cli/__main__.py +5 -0
- everypixel_cli/application/__init__.py +10 -0
- everypixel_cli/application/models.py +59 -0
- everypixel_cli/application/serialization.py +20 -0
- everypixel_cli/application/services.py +1376 -0
- everypixel_cli/cli.py +1520 -0
- everypixel_cli/client.py +159 -0
- everypixel_cli/config.py +221 -0
- everypixel_cli/errors.py +262 -0
- everypixel_cli/files.py +298 -0
- everypixel_cli/mcp_server.py +828 -0
- everypixel_cli/openapi.py +338 -0
- everypixel_cli/output.py +159 -0
- everypixel_cli/resources/__init__.py +1 -0
- everypixel_cli/resources/openapi.json +5402 -0
- everypixel_cli/schemas.py +846 -0
- everypixel_cli-0.1.0.dist-info/METADATA +454 -0
- everypixel_cli-0.1.0.dist-info/RECORD +22 -0
- everypixel_cli-0.1.0.dist-info/WHEEL +4 -0
- everypixel_cli-0.1.0.dist-info/entry_points.txt +3 -0
- everypixel_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""OpenAPI schema support for the generic `run` command.
|
|
2
|
+
|
|
3
|
+
The CLI tries live API schema first, then cache, then the bundled snapshot.
|
|
4
|
+
This keeps `everypixel run ...` usable offline and up to date when API exists.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
from contextlib import suppress
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from importlib.resources import files
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from jsonschema import Draft202012Validator
|
|
18
|
+
from jsonschema.exceptions import ValidationError as JSONSchemaValidationError
|
|
19
|
+
from jsonschema.exceptions import best_match
|
|
20
|
+
from referencing import Registry, Resource
|
|
21
|
+
from referencing.jsonschema import DRAFT202012
|
|
22
|
+
|
|
23
|
+
from .config import config_dir
|
|
24
|
+
from .errors import CLIError, EXIT_VALIDATION, FileWriteError
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
SCHEMA_TTL_SECONDS = 24 * 60 * 60
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Operation:
|
|
32
|
+
"""Resolved OpenAPI operation: HTTP method, path, and schema fragment."""
|
|
33
|
+
|
|
34
|
+
method: str
|
|
35
|
+
path: str
|
|
36
|
+
schema: dict[str, Any]
|
|
37
|
+
document: dict[str, Any] | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def schema_cache_path(base_url: str) -> Path:
|
|
41
|
+
"""Return the OpenAPI schema cache path for a base URL."""
|
|
42
|
+
|
|
43
|
+
safe = base_url.replace("://", "_").replace("/", "_").replace(":", "_")
|
|
44
|
+
return config_dir() / "schemas" / f"{safe}.json"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def read_cached_schema(
|
|
48
|
+
base_url: str, *, ttl_seconds: int = SCHEMA_TTL_SECONDS
|
|
49
|
+
) -> dict[str, Any] | None:
|
|
50
|
+
"""Read a fresh cached schema or return None."""
|
|
51
|
+
|
|
52
|
+
path = schema_cache_path(base_url)
|
|
53
|
+
try:
|
|
54
|
+
if not path.exists():
|
|
55
|
+
return None
|
|
56
|
+
if time.time() - path.stat().st_mtime > ttl_seconds:
|
|
57
|
+
return None
|
|
58
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
59
|
+
except (OSError, ValueError):
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def write_cached_schema(base_url: str, schema: dict[str, Any]) -> Path:
|
|
64
|
+
"""Save an OpenAPI schema to local cache."""
|
|
65
|
+
|
|
66
|
+
path = schema_cache_path(base_url)
|
|
67
|
+
try:
|
|
68
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
path.write_text(
|
|
70
|
+
json.dumps(schema, ensure_ascii=False, indent=2),
|
|
71
|
+
encoding="utf-8",
|
|
72
|
+
)
|
|
73
|
+
except OSError as exc:
|
|
74
|
+
raise FileWriteError(
|
|
75
|
+
"Unable to write OpenAPI schema cache",
|
|
76
|
+
details={"path": str(path)},
|
|
77
|
+
) from exc
|
|
78
|
+
return path
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def bundled_schema() -> dict[str, Any]:
|
|
82
|
+
"""Read the bundled OpenAPI snapshot from package resources."""
|
|
83
|
+
|
|
84
|
+
resource = files("everypixel_cli.resources").joinpath("openapi.json")
|
|
85
|
+
return json.loads(resource.read_text(encoding="utf-8"))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def load_schema(client, *, refresh: bool = False) -> tuple[dict[str, Any], str]:
|
|
89
|
+
"""Load schema from cache/live/bundled and return its source."""
|
|
90
|
+
|
|
91
|
+
if not refresh:
|
|
92
|
+
cached = read_cached_schema(client.base_url)
|
|
93
|
+
if cached is not None:
|
|
94
|
+
return cached, "cache"
|
|
95
|
+
try:
|
|
96
|
+
live = client.request("GET", "/v1/openapi.json", auth_required=False)
|
|
97
|
+
except CLIError:
|
|
98
|
+
cached = read_cached_schema(
|
|
99
|
+
client.base_url, ttl_seconds=10 * 365 * 24 * 60 * 60
|
|
100
|
+
)
|
|
101
|
+
if cached is not None:
|
|
102
|
+
return cached, "cache_stale"
|
|
103
|
+
return bundled_schema(), "bundled"
|
|
104
|
+
with suppress(FileWriteError):
|
|
105
|
+
write_cached_schema(client.base_url, live)
|
|
106
|
+
return live, "live"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def refresh_schema(client) -> tuple[dict[str, Any], Path]:
|
|
110
|
+
"""Force-load live OpenAPI schema and update cache."""
|
|
111
|
+
|
|
112
|
+
schema = client.request("GET", "/v1/openapi.json", auth_required=False)
|
|
113
|
+
path = write_cached_schema(client.base_url, schema)
|
|
114
|
+
return schema, path
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def find_operation(
|
|
118
|
+
schema: dict[str, Any], endpoint: str, requested_method: str | None = None
|
|
119
|
+
) -> Operation | None:
|
|
120
|
+
"""Find an operation by path, short endpoint name, or operationId."""
|
|
121
|
+
|
|
122
|
+
paths = schema.get("paths")
|
|
123
|
+
if not isinstance(paths, dict):
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
normalized_endpoint = endpoint.strip()
|
|
127
|
+
if normalized_endpoint.startswith("/"):
|
|
128
|
+
candidates = [normalized_endpoint]
|
|
129
|
+
else:
|
|
130
|
+
candidates = [f"/v1/{normalized_endpoint}", normalized_endpoint]
|
|
131
|
+
|
|
132
|
+
requested = requested_method.lower() if requested_method else None
|
|
133
|
+
for path, operations in paths.items():
|
|
134
|
+
if not isinstance(operations, dict):
|
|
135
|
+
continue
|
|
136
|
+
for method, operation_schema in operations.items():
|
|
137
|
+
if requested and method.lower() != requested:
|
|
138
|
+
continue
|
|
139
|
+
if (
|
|
140
|
+
path in candidates
|
|
141
|
+
or path.rstrip("/").split("/")[-1] == normalized_endpoint
|
|
142
|
+
):
|
|
143
|
+
return Operation(
|
|
144
|
+
method=method.upper(),
|
|
145
|
+
path=path,
|
|
146
|
+
schema=operation_schema,
|
|
147
|
+
document=schema,
|
|
148
|
+
)
|
|
149
|
+
operation_id = (
|
|
150
|
+
operation_schema.get("operationId")
|
|
151
|
+
if isinstance(operation_schema, dict)
|
|
152
|
+
else None
|
|
153
|
+
)
|
|
154
|
+
if operation_id == normalized_endpoint:
|
|
155
|
+
return Operation(
|
|
156
|
+
method=method.upper(),
|
|
157
|
+
path=path,
|
|
158
|
+
schema=operation_schema,
|
|
159
|
+
document=schema,
|
|
160
|
+
)
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def operation_help(
|
|
165
|
+
operation: Operation | None,
|
|
166
|
+
*,
|
|
167
|
+
endpoint: str,
|
|
168
|
+
method: str,
|
|
169
|
+
path: str,
|
|
170
|
+
schema_source: str | None,
|
|
171
|
+
) -> dict[str, Any]:
|
|
172
|
+
"""Build a help payload for a resolved OpenAPI operation."""
|
|
173
|
+
|
|
174
|
+
schema = operation.schema if operation else None
|
|
175
|
+
return {
|
|
176
|
+
"endpoint": endpoint,
|
|
177
|
+
"schema_source": schema_source,
|
|
178
|
+
"method": method,
|
|
179
|
+
"path": path,
|
|
180
|
+
"summary": schema.get("summary") if isinstance(schema, dict) else None,
|
|
181
|
+
"operation_id": schema.get("operationId") if isinstance(schema, dict) else None,
|
|
182
|
+
"parameters": summarize_parameters(schema),
|
|
183
|
+
"request_body": summarize_request_body(schema),
|
|
184
|
+
"schema": schema,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def summarize_parameters(schema: dict[str, Any] | None) -> list[dict[str, Any]]:
|
|
189
|
+
"""Simplify OpenAPI parameters for user output."""
|
|
190
|
+
|
|
191
|
+
if not isinstance(schema, dict) or not isinstance(schema.get("parameters"), list):
|
|
192
|
+
return []
|
|
193
|
+
parameters = []
|
|
194
|
+
for item in schema["parameters"]:
|
|
195
|
+
if isinstance(item, dict):
|
|
196
|
+
parameters.append(
|
|
197
|
+
{
|
|
198
|
+
"name": item.get("name"),
|
|
199
|
+
"in": item.get("in"),
|
|
200
|
+
"required": item.get("required", False),
|
|
201
|
+
"schema": item.get("schema"),
|
|
202
|
+
"description": item.get("description"),
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
return parameters
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def summarize_request_body(schema: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
209
|
+
"""Simplify requestBody description for user output."""
|
|
210
|
+
|
|
211
|
+
if not isinstance(schema, dict):
|
|
212
|
+
return None
|
|
213
|
+
request_body = schema.get("requestBody")
|
|
214
|
+
if not isinstance(request_body, dict):
|
|
215
|
+
return None
|
|
216
|
+
content = request_body.get("content")
|
|
217
|
+
if not isinstance(content, dict):
|
|
218
|
+
return {"required": request_body.get("required", False), "content_types": []}
|
|
219
|
+
return {
|
|
220
|
+
"required": request_body.get("required", False),
|
|
221
|
+
"content_types": [
|
|
222
|
+
{
|
|
223
|
+
"content_type": content_type,
|
|
224
|
+
"schema": content_schema.get("schema")
|
|
225
|
+
if isinstance(content_schema, dict)
|
|
226
|
+
else None,
|
|
227
|
+
}
|
|
228
|
+
for content_type, content_schema in content.items()
|
|
229
|
+
],
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def validate_payload_against_operation(
|
|
234
|
+
operation: Operation | None, payload: dict[str, Any]
|
|
235
|
+
) -> None:
|
|
236
|
+
"""Validate a payload against an OpenAPI 3.1 JSON request schema."""
|
|
237
|
+
|
|
238
|
+
if operation is None:
|
|
239
|
+
return
|
|
240
|
+
schema = json_request_body_schema(operation.schema)
|
|
241
|
+
if schema is None:
|
|
242
|
+
return
|
|
243
|
+
validator = openapi_validator(schema, operation.document)
|
|
244
|
+
error = best_match(validator.iter_errors(payload))
|
|
245
|
+
if error is None:
|
|
246
|
+
return
|
|
247
|
+
message, details = format_openapi_validation_error(error)
|
|
248
|
+
raise CLIError(
|
|
249
|
+
message,
|
|
250
|
+
code="validation_error",
|
|
251
|
+
exit_code=EXIT_VALIDATION,
|
|
252
|
+
details=details,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def json_request_body_schema(operation_schema: dict[str, Any]) -> dict[str, Any] | None:
|
|
257
|
+
"""Return the application/json schema for an OpenAPI operation."""
|
|
258
|
+
|
|
259
|
+
request_body = operation_schema.get("requestBody")
|
|
260
|
+
if not isinstance(request_body, dict):
|
|
261
|
+
return None
|
|
262
|
+
content = request_body.get("content")
|
|
263
|
+
if not isinstance(content, dict):
|
|
264
|
+
return None
|
|
265
|
+
json_content = content.get("application/json")
|
|
266
|
+
if not isinstance(json_content, dict):
|
|
267
|
+
return None
|
|
268
|
+
schema = json_content.get("schema")
|
|
269
|
+
return schema if isinstance(schema, dict) else None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def openapi_validator(
|
|
273
|
+
schema: dict[str, Any], document: dict[str, Any] | None
|
|
274
|
+
) -> Draft202012Validator:
|
|
275
|
+
"""Build a JSON Schema validator with local OpenAPI references available."""
|
|
276
|
+
|
|
277
|
+
if document is None:
|
|
278
|
+
return Draft202012Validator(schema)
|
|
279
|
+
document_uri = "urn:everypixel:openapi"
|
|
280
|
+
resource = Resource.from_contents(
|
|
281
|
+
document,
|
|
282
|
+
default_specification=DRAFT202012,
|
|
283
|
+
)
|
|
284
|
+
registry = Registry().with_resource(document_uri, resource)
|
|
285
|
+
return Draft202012Validator(
|
|
286
|
+
absolute_document_refs(schema, document_uri),
|
|
287
|
+
registry=registry,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def absolute_document_refs(value: Any, document_uri: str) -> Any:
|
|
292
|
+
"""Point fragment refs from a detached request schema at its document."""
|
|
293
|
+
|
|
294
|
+
if isinstance(value, dict):
|
|
295
|
+
result = {
|
|
296
|
+
key: absolute_document_refs(item, document_uri)
|
|
297
|
+
for key, item in value.items()
|
|
298
|
+
}
|
|
299
|
+
ref = result.get("$ref")
|
|
300
|
+
if isinstance(ref, str) and ref.startswith("#/"):
|
|
301
|
+
result["$ref"] = f"{document_uri}{ref}"
|
|
302
|
+
return result
|
|
303
|
+
if isinstance(value, list):
|
|
304
|
+
return [absolute_document_refs(item, document_uri) for item in value]
|
|
305
|
+
return value
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def format_openapi_validation_error(
|
|
309
|
+
error: JSONSchemaValidationError,
|
|
310
|
+
) -> tuple[str, dict[str, Any]]:
|
|
311
|
+
"""Return a stable error without echoing arbitrary user payload values."""
|
|
312
|
+
|
|
313
|
+
path = ".".join(str(part) for part in error.absolute_path)
|
|
314
|
+
label = path or "payload"
|
|
315
|
+
rule = str(error.validator)
|
|
316
|
+
if error.validator == "required" and isinstance(error.validator_value, list):
|
|
317
|
+
instance = error.instance if isinstance(error.instance, dict) else {}
|
|
318
|
+
missing = [key for key in error.validator_value if key not in instance]
|
|
319
|
+
message = f"Missing required input(s): {', '.join(map(str, missing))}"
|
|
320
|
+
elif error.validator == "type":
|
|
321
|
+
message = f"Invalid type for input '{label}': expected {error.validator_value}"
|
|
322
|
+
elif error.validator == "enum":
|
|
323
|
+
message = f"Invalid value for input '{label}': expected an allowed enum value"
|
|
324
|
+
elif error.validator in {"oneOf", "anyOf"}:
|
|
325
|
+
message = "Payload does not match an allowed request schema"
|
|
326
|
+
elif error.validator in {
|
|
327
|
+
"minimum",
|
|
328
|
+
"maximum",
|
|
329
|
+
"exclusiveMinimum",
|
|
330
|
+
"exclusiveMaximum",
|
|
331
|
+
}:
|
|
332
|
+
message = f"Input '{label}' is outside the allowed range"
|
|
333
|
+
else:
|
|
334
|
+
message = f"Input '{label}' does not satisfy the OpenAPI schema"
|
|
335
|
+
details = {"rule": rule}
|
|
336
|
+
if path:
|
|
337
|
+
details["path"] = path
|
|
338
|
+
return message, details
|
everypixel_cli/output.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""CLI output rendering in JSON or human-readable mode."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
from .errors import CLIError, JqExpressionError, SerializationError, serialize_error
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def apply_jq(data: Any, expr: str | None) -> Any:
|
|
16
|
+
"""Apply a jq expression or simple dot selector fallback."""
|
|
17
|
+
|
|
18
|
+
if not expr:
|
|
19
|
+
return data
|
|
20
|
+
try:
|
|
21
|
+
import jq # type: ignore
|
|
22
|
+
except ModuleNotFoundError:
|
|
23
|
+
return apply_simple_selector(data, expr)
|
|
24
|
+
try:
|
|
25
|
+
return jq.compile(expr).input(data).first()
|
|
26
|
+
except Exception as exc: # jq does not expose stable public exception classes.
|
|
27
|
+
raise JqExpressionError(
|
|
28
|
+
"Unable to apply jq expression", details={"expression": expr}
|
|
29
|
+
) from exc
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def apply_simple_selector(data: Any, expr: str) -> Any:
|
|
33
|
+
"""Minimal fallback for `.field.subfield` expressions."""
|
|
34
|
+
|
|
35
|
+
if not expr.startswith("."):
|
|
36
|
+
raise JqExpressionError(
|
|
37
|
+
"Unable to apply jq expression", details={"expression": expr}
|
|
38
|
+
)
|
|
39
|
+
current = data
|
|
40
|
+
try:
|
|
41
|
+
for part in expr[1:].split("."):
|
|
42
|
+
if not part:
|
|
43
|
+
continue
|
|
44
|
+
if not isinstance(current, dict):
|
|
45
|
+
raise TypeError("selector target is not an object")
|
|
46
|
+
current = current[part]
|
|
47
|
+
return current
|
|
48
|
+
except (KeyError, TypeError) as exc:
|
|
49
|
+
raise JqExpressionError(
|
|
50
|
+
"Unable to apply jq expression", details={"expression": expr}
|
|
51
|
+
) from exc
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def emit_json(data: Any, jq_expr: str | None = None) -> None:
|
|
55
|
+
"""Print JSON output."""
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
selected = apply_jq(data, jq_expr)
|
|
59
|
+
print(json.dumps(selected, ensure_ascii=False, indent=2))
|
|
60
|
+
except CLIError:
|
|
61
|
+
raise
|
|
62
|
+
except (TypeError, ValueError) as exc:
|
|
63
|
+
raise SerializationError("Unable to serialize CLI response") from exc
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def emit_human(data: Any, *, title: str | None = None, no_color: bool = False) -> None:
|
|
67
|
+
"""Print output using Rich tables or a simple fallback."""
|
|
68
|
+
|
|
69
|
+
console = Console(no_color=no_color)
|
|
70
|
+
if title:
|
|
71
|
+
console.print(f"[bold]{title}[/bold]")
|
|
72
|
+
if isinstance(data, dict) and render_known_table(console, data):
|
|
73
|
+
return
|
|
74
|
+
if isinstance(data, dict):
|
|
75
|
+
table = Table(show_header=False, box=None)
|
|
76
|
+
table.add_column("Key", style="cyan")
|
|
77
|
+
table.add_column("Value")
|
|
78
|
+
for key, value in data.items():
|
|
79
|
+
table.add_row(
|
|
80
|
+
str(key),
|
|
81
|
+
json.dumps(value, ensure_ascii=False)
|
|
82
|
+
if isinstance(value, (dict, list))
|
|
83
|
+
else str(value),
|
|
84
|
+
)
|
|
85
|
+
console.print(table)
|
|
86
|
+
else:
|
|
87
|
+
console.print(data)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def render_known_table(console: Console, data: dict[str, Any]) -> bool:
|
|
91
|
+
"""Render specialized tables for known API responses."""
|
|
92
|
+
|
|
93
|
+
if isinstance(data.get("keywords"), list):
|
|
94
|
+
table = Table(title="Keywords")
|
|
95
|
+
table.add_column("Keyword", style="cyan")
|
|
96
|
+
table.add_column("Score", justify="right")
|
|
97
|
+
for item in data["keywords"]:
|
|
98
|
+
if isinstance(item, dict):
|
|
99
|
+
table.add_row(
|
|
100
|
+
str(item.get("keyword", "")), format_score(item.get("score"))
|
|
101
|
+
)
|
|
102
|
+
console.print(table)
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
if isinstance(data.get("faces"), list):
|
|
106
|
+
table = Table(title="Faces")
|
|
107
|
+
table.add_column("#", justify="right")
|
|
108
|
+
table.add_column("Score", justify="right")
|
|
109
|
+
table.add_column("BBox")
|
|
110
|
+
table.add_column("Age")
|
|
111
|
+
table.add_column("Gender")
|
|
112
|
+
for index, item in enumerate(data["faces"], start=1):
|
|
113
|
+
if isinstance(item, dict):
|
|
114
|
+
table.add_row(
|
|
115
|
+
str(index),
|
|
116
|
+
format_score(item.get("score")),
|
|
117
|
+
json.dumps(item.get("bbox", ""), ensure_ascii=False),
|
|
118
|
+
str(item.get("age", "")),
|
|
119
|
+
str(item.get("gender", "")),
|
|
120
|
+
)
|
|
121
|
+
console.print(table)
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
if isinstance(data.get("quality"), dict):
|
|
125
|
+
table = Table(title="Quality")
|
|
126
|
+
table.add_column("Metric", style="cyan")
|
|
127
|
+
table.add_column("Value", justify="right")
|
|
128
|
+
for key, value in data["quality"].items():
|
|
129
|
+
table.add_row(
|
|
130
|
+
str(key),
|
|
131
|
+
format_score(value) if isinstance(value, float) else str(value),
|
|
132
|
+
)
|
|
133
|
+
console.print(table)
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def format_score(value: Any) -> str:
|
|
140
|
+
"""Format score/metric values for human output."""
|
|
141
|
+
|
|
142
|
+
if isinstance(value, float):
|
|
143
|
+
return f"{value:.4f}"
|
|
144
|
+
if value is None:
|
|
145
|
+
return ""
|
|
146
|
+
return str(value)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def emit_error(error: CLIError, *, output_json: bool, no_color: bool = False) -> None:
|
|
150
|
+
"""Print a JSON error to stdout or a human error to stderr."""
|
|
151
|
+
|
|
152
|
+
if output_json:
|
|
153
|
+
# JSON output has one machine-readable stream, including failures.
|
|
154
|
+
print(serialize_error(error))
|
|
155
|
+
return
|
|
156
|
+
console = Console(stderr=True, no_color=no_color)
|
|
157
|
+
console.print(Text(f"Error [{error.code}]: {error.message}", style="red"))
|
|
158
|
+
for key, value in error.details.items():
|
|
159
|
+
console.print(f"{key.replace('_', ' ').title()}: {value}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Bundled package resources."""
|