change: Implement 3: Complete the native-process CLI, JSON request handling, status vocabulary and exit codes
Repository Validate / repository-checks (pull_request) Successful in 3s

Completed the native-process CLI for `python -m tools.example`. Added `tools/envelope.py` with the fixed status vocabulary (`ok`, `dry_run`, `invalid_request`, `no_format`, `no_size`, `access_error`, `partial_download`) mapped to exit codes 0/0/2/3/4/5/6 and the `Response`/`ResponseBuilder` JSON envelope (keys: status, source, request, items, error). Extended `tools/errors.py` with request errors (`RequestError`, `RequestDocumentError`, `RequestFieldError`, `UnsupportedFormatError`) and `ACCEPTED_FORMATS`. Rewrote `tools/example.py` as a plain executable CLI accepting either a JSON request document or flags (--topic, --width, --height, --format png|ico, --source, --count, --download, --output-dir, --resize, --dry-run), with a custom parser that turns usage errors into `invalid_request` envelopes, unknown sources reported together with the available source ids, unsupported formats rejected with the accepted values, and format/size filtering, download and partial-download handling mapped onto the shared status table. `examples/input.json` is now the canonical offline dry_run request, and `tests/test_cli.py` (new) plus a rewritten `tests/test_example.py` cover the status/exit-code table, single-JSON-document output, help text, unknown source/unsupported format rejection and the download branches.

Tests:
Facade checks run from /workspace with the prepared .venv: `python -m compileall tools tests` (tools.validate) OK; `pytest` (tools.test) 76 passed; `python -m tools.example examples/input.json` (tools.smoke) exit 0 with a single `dry_run` JSON document; `python -m tools.example --help` exits 0 and documents every parameter plus the JSON document contract. Origin target branch `origin/task/1-task` (1f929c5) is an ancestor of the work branch, so the target is integrated and the checks above ran on the integrated revision. No dependency changes were needed, so no prepare_environment update was required.

Progress:
Implemented envelope status/exit-code table and response builder, request errors, the full flag/JSON-document CLI in tools/example.py, canonical offline examples/input.json, and CLI tests; facade validate/test/smoke and --help all pass (76 tests).

Published by developer-agent through Tool Backend.

Closes #5
This commit is contained in:
Tool Backend
2026-11-03 01:48:00 +00:00
parent 1f929c5d38
commit f20e58e85c
7 changed files with 1244 additions and 219 deletions
+74
View File
@@ -185,3 +185,77 @@ class HostNotAllowedError(SourceAccessError):
url=str(url),
details={"allowed_hosts": list(self.allowed)},
)
# ---------------------------------------------------------------------------
# Request errors: invalid command lines and JSON request documents
# ---------------------------------------------------------------------------
#: Image formats the CLI accepts.
ACCEPTED_FORMATS: tuple[str, ...] = ("png", "ico")
class RequestError(ToolkitError):
"""Base class for every unusable request (CLI flags or request document)."""
code = "request_invalid"
def __init__(
self,
message: str,
*,
code: str | None = None,
details: Mapping[str, Any] | None = None,
field: str | None = None,
accepted_fields: Iterable[str] | None = None,
accepted_formats: Iterable[str] | None = None,
) -> None:
merged: dict[str, Any] = dict(details or {})
if field is not None:
merged["field"] = field
if accepted_fields is not None:
merged["accepted_fields"] = list(accepted_fields)
if accepted_formats is not None:
merged["accepted_formats"] = list(accepted_formats)
super().__init__(message, code=code, details=merged)
self.field = field
self.accepted_fields: tuple[str, ...] = tuple(accepted_fields or ())
self.accepted_formats: tuple[str, ...] = tuple(accepted_formats or ())
class RequestDocumentError(RequestError):
"""Raised when the JSON request document cannot be read or decoded."""
code = "request_document"
class RequestFieldError(RequestError):
"""Raised when a single request field is missing, unknown or malformed."""
code = "request_field"
class UnsupportedFormatError(RequestError):
"""Raised for a format outside :data:`ACCEPTED_FORMATS`."""
code = "request_format_unsupported"
def __init__(self, value: Any, accepted: Iterable[str] = ACCEPTED_FORMATS) -> None:
accepted = tuple(accepted)
self.value = str(value)
super().__init__(
f"unsupported image format {self.value!r}; "
f"accepted formats: {', '.join(accepted)}",
field="format",
accepted_formats=accepted,
details={"requested_format": self.value},
)
__all__ += [
"ACCEPTED_FORMATS",
"RequestDocumentError",
"RequestError",
"RequestFieldError",
"UnsupportedFormatError",
]