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
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:
@@ -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",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user