change: Implement 1: Bootstrap the toolkit tree, project config and initial dependency lock #11

Merged
ai-reviewer merged 1 commits from change/3-implement-1-bootstrap-the-toolkit-tree-project-config-and-init into task/1-task 2026-11-02 23:53:57 +00:00
9 changed files with 438 additions and 2 deletions
+9
View File
@@ -0,0 +1,9 @@
{
"tasks": {
"projects/product-54354138-1#3": {
"events": [
"Created tools package (tools/__init__.py, tools/example.py), examples/input.json, tests/test_example.py, .gitignore, .env.example; updated pyproject.toml (metadata, setuptools packages, dev pytest extra, pytest pythonpath/testpaths, requires-python >=3.11 due to approved-mirror exceptiongroup constraint) and generated uv.lock via prepare_environment update. Facade validate/test/smoke and --help all pass locally."
]
}
}
}
+3
View File
@@ -0,0 +1,3 @@
# Optional environment overrides for the toolkit CLI.
# The CLI is offline and does not require any variable to be set.
LOG_LEVEL=info
+7
View File
@@ -0,0 +1,7 @@
__pycache__/
*.py[cod]
.venv/
.pytest_cache/
build/
dist/
*.egg-info/
+9
View File
@@ -0,0 +1,9 @@
{
"version": 1,
"mode": "validate",
"dry_run": true,
"command": "example",
"arguments": {
"request": "examples/input.json"
}
}
+17 -2
View File
@@ -1,8 +1,23 @@
# Initial declaration; add project dependencies and development tools here.
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "product-54354138-1-tools"
version = "0.1.0"
description = "Offline toolkit for validating and running structured JSON request documents."
readme = "README.md"
requires-python = ">=3.11"
dependencies = []
[project.optional-dependencies]
dev = ["pytest"]
dev = ["pytest>=8.0"]
[tool.setuptools]
packages = ["tools"]
[tool.pytest.ini_options]
minversion = "8.0"
pythonpath = ["."]
testpaths = ["tests"]
addopts = "-ra"
+118
View File
@@ -0,0 +1,118 @@
"""Tests for the placeholder toolkit CLI and its request document."""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
import pytest
from tools import example
REPO_ROOT = Path(__file__).resolve().parents[1]
EXAMPLE_REQUEST = REPO_ROOT / "examples" / "input.json"
def run_cli(*args: str) -> subprocess.CompletedProcess[str]:
"""Run the CLI in a subprocess so stdout/exit codes are asserted end to end."""
return subprocess.run(
[sys.executable, "-m", "tools.example", *args],
cwd=REPO_ROOT,
capture_output=True,
text=True,
check=False,
)
def test_example_request_document_is_valid_json_object() -> None:
document = json.loads(EXAMPLE_REQUEST.read_text(encoding="utf-8"))
assert isinstance(document, dict)
assert set(example.REQUIRED_FIELDS) <= set(document)
assert document["dry_run"] is True
assert document["mode"] == "validate"
def test_example_request_document_passes_validation() -> None:
request = example.validate_request(example.load_request(EXAMPLE_REQUEST))
assert request["mode"] == "validate"
assert request["dry_run"] is True
assert request["arguments"]["request"] == "examples/input.json"
def test_cli_prints_single_json_envelope_and_exits_zero() -> None:
result = run_cli("examples/input.json")
assert result.returncode == 0, result.stderr
assert result.stderr == ""
lines = [line for line in result.stdout.splitlines() if line.strip()]
assert len(lines) == 1
envelope = json.loads(lines[0])
assert envelope == {
"status": "ok",
"command": "example",
"mode": "validate",
"dry_run": True,
"version": 1,
"request": "examples/input.json",
}
def test_cli_help_exits_zero() -> None:
result = run_cli("--help")
assert result.returncode == 0
assert "REQUEST" in result.stdout
def test_cli_reports_missing_request_document(tmp_path: Path) -> None:
missing = tmp_path / "missing.json"
result = run_cli(str(missing))
assert result.returncode == example.EXIT_INVALID_REQUEST
envelope = json.loads(result.stdout)
assert envelope["status"] == "error"
assert envelope["error"]["code"] == "request_not_found"
def test_cli_reports_malformed_json(tmp_path: Path) -> None:
malformed = tmp_path / "malformed.json"
malformed.write_text("{not json", encoding="utf-8")
result = run_cli(str(malformed))
assert result.returncode == example.EXIT_INVALID_REQUEST
assert json.loads(result.stdout)["error"]["code"] == "request_not_json"
@pytest.mark.parametrize(
("document", "code"),
[
([], "request_not_object"),
({"mode": "validate", "dry_run": True, "command": "example", "arguments": {}}, "request_field_missing"),
({"version": 1, "mode": "validate", "dry_run": False, "command": "example", "arguments": {"request": "x"}}, "request_not_dry_run"),
({"version": 1, "mode": "execute", "dry_run": True, "command": "example", "arguments": {"request": "x"}}, "request_mode_unsupported"),
({"version": 1, "mode": "validate", "dry_run": True, "command": "example", "arguments": {}}, "request_field_missing"),
({"version": 1, "mode": "validate", "dry_run": True, "command": "", "arguments": {"request": "x"}}, "request_field_invalid"),
],
)
def test_validate_request_rejects_invalid_documents(document: object, code: str) -> None:
with pytest.raises(example.RequestError) as excinfo:
example.validate_request(document)
assert excinfo.value.code == code
def test_envelope_is_deterministic() -> None:
request = example.validate_request(example.load_request(EXAMPLE_REQUEST))
first = example.build_envelope(request, "examples/input.json")
second = example.build_envelope(request, "examples/input.json")
assert first == second
assert json.dumps(first, sort_keys=True) == json.dumps(second, sort_keys=True)
+12
View File
@@ -0,0 +1,12 @@
"""Importable toolkit package for the request-document CLI.
The package intentionally exposes only its version metadata here; the
executable behaviour lives in :mod:`tools.example` (and in the modules that
later increments add).
"""
from __future__ import annotations
__all__ = ["__version__"]
__version__ = "0.1.0"
+200
View File
@@ -0,0 +1,200 @@
"""Placeholder toolkit entry point that loads and validates a JSON request.
This increment only establishes the stable command-line surface: read a single
JSON request document, validate its shape, and print one structured JSON
envelope. Later modules extend the same document format and envelope without
changing the invocation contract, so the module stays deterministic and works
fully offline.
Usage::
python -m tools.example examples/input.json
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any, Mapping, Sequence
__all__ = [
"EXIT_INVALID_REQUEST",
"EXIT_OK",
"EXIT_USAGE",
"RequestError",
"build_envelope",
"load_request",
"main",
"validate_request",
]
EXIT_OK = 0
EXIT_INVALID_REQUEST = 1
EXIT_USAGE = 2
#: Fields every request document must provide.
REQUIRED_FIELDS = ("version", "mode", "dry_run", "command", "arguments")
#: Modes understood by this increment of the toolkit.
SUPPORTED_MODES = frozenset({"validate"})
PROG = "tools.example"
class RequestError(ValueError):
"""Raised when a request document is missing or malformed."""
def __init__(self, code: str, message: str) -> None:
super().__init__(message)
self.code = code
self.message = message
def load_request(path: str | Path) -> Any:
"""Read and decode the JSON document stored at ``path``."""
try:
raw = Path(path).read_text(encoding="utf-8")
except FileNotFoundError as exc:
raise RequestError("request_not_found", f"request document not found: {path}") from exc
except IsADirectoryError as exc:
raise RequestError("request_not_a_file", f"request document is not a file: {path}") from exc
except OSError as exc:
raise RequestError("request_unreadable", f"request document could not be read: {path}") from exc
except UnicodeDecodeError as exc:
raise RequestError("request_not_utf8", f"request document is not valid UTF-8: {path}") from exc
try:
return json.loads(raw)
except json.JSONDecodeError as exc:
raise RequestError(
"request_not_json",
f"request document is not valid JSON: {path} (line {exc.lineno}, column {exc.colno})",
) from exc
def _require(mapping: Mapping[str, Any], key: str, expected: type | tuple[type, ...], label: str) -> Any:
if key not in mapping:
raise RequestError("request_field_missing", f"request field {label} is required")
value = mapping[key]
if isinstance(value, bool) and bool not in (
expected if isinstance(expected, tuple) else (expected,)
):
raise RequestError("request_field_invalid", f"request field {label} must be {_describe(expected)}")
if not isinstance(value, expected):
raise RequestError("request_field_invalid", f"request field {label} must be {_describe(expected)}")
return value
def _describe(expected: type | tuple[type, ...]) -> str:
names = expected if isinstance(expected, tuple) else (expected,)
return " or ".join(sorted({name.__name__ for name in names}))
def validate_request(document: Any) -> dict[str, Any]:
"""Validate ``document`` and return the normalised request mapping."""
if not isinstance(document, dict):
raise RequestError("request_not_object", "request document must be a JSON object")
for key in REQUIRED_FIELDS:
if key not in document:
raise RequestError("request_field_missing", f"request field '{key}' is required")
version = _require(document, "version", int, "version")
if version < 1:
raise RequestError("request_field_invalid", "request field 'version' must be a positive integer")
mode = _require(document, "mode", str, "mode")
if mode not in SUPPORTED_MODES:
supported = ", ".join(sorted(SUPPORTED_MODES))
raise RequestError(
"request_mode_unsupported",
f"request field 'mode' must be one of: {supported}",
)
dry_run = _require(document, "dry_run", bool, "dry_run")
if not dry_run:
raise RequestError(
"request_not_dry_run",
"this increment only supports validate-only dry_run requests",
)
command = _require(document, "command", str, "command")
if not command.strip():
raise RequestError("request_field_invalid", "request field 'command' must not be empty")
arguments = _require(document, "arguments", dict, "arguments")
request_path = _require(arguments, "request", str, "arguments.request")
if not request_path.strip():
raise RequestError("request_field_invalid", "request field 'arguments.request' must not be empty")
return {
"version": version,
"mode": mode,
"dry_run": dry_run,
"command": command,
"arguments": dict(arguments),
}
def build_envelope(request: Mapping[str, Any], source: str) -> dict[str, Any]:
"""Build the deterministic success envelope for a validated request."""
return {
"status": "ok",
"command": request["command"],
"mode": request["mode"],
"dry_run": request["dry_run"],
"version": request["version"],
"request": source,
}
def build_error_envelope(error: RequestError, source: str) -> dict[str, Any]:
"""Build the deterministic failure envelope for an invalid request."""
return {
"status": "error",
"error": {"code": error.code, "message": error.message},
"request": source,
}
def emit(envelope: Mapping[str, Any]) -> None:
"""Print exactly one JSON document to standard output."""
print(json.dumps(envelope, sort_keys=True, ensure_ascii=True))
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog=PROG,
description=(
"Load and validate a JSON request document. The command is offline "
"and prints one structured JSON envelope."
),
)
parser.add_argument(
"request",
metavar="REQUEST",
help="path to the JSON request document (for example examples/input.json)",
)
return parser
def main(argv: Sequence[str] | None = None) -> int:
"""Run the placeholder CLI and return the process exit code."""
parser = _build_parser()
args = parser.parse_args(list(sys.argv[1:] if argv is None else argv))
try:
document = load_request(args.request)
request = validate_request(document)
except RequestError as error:
emit(build_error_envelope(error, args.request))
return EXIT_INVALID_REQUEST
emit(build_envelope(request, args.request))
return EXIT_OK
if __name__ == "__main__":
raise SystemExit(main())
Generated
+63
View File
@@ -0,0 +1,63 @@
version = 1
revision = 5
requires-python = ">=3.11"
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://git.p2prod.ru/api/packages/dependency-mirror/pypi/simple" }
wheels = [
{ url = "https://git.p2prod.ru/api/packages/dependency-mirror/pypi/files/colorama/0.4.6/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" },
]
[[package]]
name = "iniconfig"
version = "2.3.1"
source = { registry = "https://git.p2prod.ru/api/packages/dependency-mirror/pypi/simple" }
wheels = [
{ url = "https://git.p2prod.ru/api/packages/dependency-mirror/pypi/files/iniconfig/2.3.1/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7" },
]
[[package]]
name = "packaging"
version = "26.3"
source = { registry = "https://git.p2prod.ru/api/packages/dependency-mirror/pypi/simple" }
wheels = [
{ url = "https://git.p2prod.ru/api/packages/dependency-mirror/pypi/files/packaging/26.3/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c" },
]
[[package]]
name = "pluggy"
version = "1.7.0"
source = { registry = "https://git.p2prod.ru/api/packages/dependency-mirror/pypi/simple" }
wheels = [
{ url = "https://git.p2prod.ru/api/packages/dependency-mirror/pypi/files/pluggy/1.7.0/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec" },
]
[[package]]
name = "product-54354138-1-tools"
version = "0.1.0"
source = { editable = "." }
[package.optional-dependencies]
dev = [
{ name = "pytest" },
]
[package.metadata]
requires-dist = [{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }]
provides-extras = ["dev"]
[[package]]
name = "pytest"
version = "8.3.3"
source = { registry = "https://git.p2prod.ru/api/packages/dependency-mirror/pypi/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
]
wheels = [
{ url = "https://git.p2prod.ru/api/packages/dependency-mirror/pypi/files/pytest/8.3.3/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" },
]