"""Python standard-library client. No third-party packages required. python examples/translate_game.py --input strings.json --output translated.json Rerun with the same output path to resume an existing job. """ import argparse import json from pathlib import Path import time from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen def api(base, path, payload=None, method=None): data = json.dumps(payload, ensure_ascii=False).encode("utf-8") if payload is not None else None request = Request(base + path, data=data, headers={"Content-Type": "application/json"}, method=method) try: with urlopen(request, timeout=45) as response: return json.load(response) except HTTPError as exc: raise RuntimeError(f"API HTTP {exc.code}: {exc.read().decode('utf-8', errors='replace')}") from exc def save(path, value): path.parent.mkdir(parents=True, exist_ok=True) temporary = path.with_suffix(path.suffix + ".tmp") temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8") temporary.replace(path) def main(): parser = argparse.ArgumentParser() parser.add_argument("--base", default="http://127.0.0.1:8787") parser.add_argument("--input", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--project", help="Existing project ID; otherwise create one") parser.add_argument("--project-file", type=Path, help="Project settings JSON used when creating a project") parser.add_argument("--context", default="Fantasy RPG. Characters speak naturally. Preserve names consistently.") parser.add_argument("--target", default="Russian") parser.add_argument("--retry", action="store_true", help="Retry failed items from the saved job") parser.add_argument("--new-job", action="store_true", help="Ignore saved job and translate using current project settings") args = parser.parse_args() base = args.base.rstrip("/") items = json.loads(args.input.read_text(encoding="utf-8")) if not isinstance(items, list) or not all(isinstance(x, dict) and isinstance(x.get("id"), str) and isinstance(x.get("text"), str) for x in items): parser.error("Input must be an array of {id: string, text: string}") if not 1 <= len(items) <= 500: parser.error("Send 1..500 items per saved job; split larger files") state_path = args.output.with_suffix(args.output.suffix + ".job.json") if state_path.exists() and not args.new_job: state = json.loads(state_path.read_text(encoding="utf-8")) if state["base"] != base or state["items"] != items or (args.project and args.project != state["project_id"]): parser.error("Saved job belongs to a different input, project or server; use --new-job") job = api(base, "/jobs/" + state["job_id"]) if args.retry and job["status"] in {"failed", "completed_with_errors"}: job = api(base, "/jobs/" + job["id"] + "/retry", {}) else: project_id = args.project if not project_id: project_config = json.loads(args.project_file.read_text(encoding="utf-8")) if args.project_file else { "name": args.input.stem, "target_language": args.target, "context": args.context, "glossary": {"Moonstone": "Лунный камень"}} project = api(base, "/projects", project_config) project_id = project["id"] job = api(base, f"/projects/{project_id}/jobs", {"items": items, "batch_size": 10}) save(state_path, {"base": base, "project_id": project_id, "job_id": job["id"], "items": items}) print(f"Project: {job['project_id']} Job: {job['id']}") while job["status"] in {"queued", "running"}: print(f"Status: {job['status']} {job['progress']}", flush=True) time.sleep(1) job = api(base, "/jobs/" + job["id"]) save(args.output, job) print(f"Saved {args.output}: {job['status']}") if job["status"] != "completed": raise SystemExit("Some items failed. See output; rerun with --retry after fixing the cause.") if __name__ == "__main__": try: main() except (RuntimeError, URLError, OSError, ValueError) as exc: raise SystemExit(str(exc)) from exc