#!/usr/bin/env python3 """Signed POST /v1/predict client for local or integration testing. Uses the same fingerprint + HMAC chain as the server (see src.core.security). Example : poetry run python scripts/call_predict.py --image D:\\photos\\sample.jpg """ from __future__ import annotations import argparse import asyncio import mimetypes import sys import time from io import BytesIO from pathlib import Path import httpx from starlette.datastructures import UploadFile from src.core.security import extract_image_fingerprint, generate_hmac_chain # --- DEVICE CONFIGURATION --- API_KEY = "sk_ca9f2fac38dda360a74d7fb50ff5adcf" SECRET_KEY = "f892ea5db6ced1b58baa5000e98abb052ec54e403f601435a509ecdd81499fb3" DEVICE_ID = "DEV-2ZJ7-626O-8ZF4" # --- API CONFIGURATION --- BASE_URL = "http://127.0.0.1:7860" ENDPOINT_PATH = "/v1/predict" async def _signed_headers( *, image_bytes: bytes, filename: str, device_id: str, secret: str, api_key: str, path: str, ) -> dict[str, str]: upload = UploadFile(filename=filename, file=BytesIO(image_bytes)) fingerprint = await extract_image_fingerprint(upload) ts = str(int(time.time())) parts = [ ts.encode("utf-8"), b"POST", path.encode("utf-8"), device_id.encode("utf-8"), fingerprint, ] signature = generate_hmac_chain(parts, secret.encode("utf-8")).hex() return { "X-API-Key": api_key, "X-Device-ID": device_id, "X-Timestamp": ts, "X-Signature": signature, } def main() -> None: parser = argparse.ArgumentParser( description="Call POST /v1/predict with v2-style signing. " "Defaults use DEVICE_ID / API_KEY / SECRET_KEY / BASE_URL from this file.", ) parser.add_argument("--base-url", default=BASE_URL, help="API origin (no trailing slash)") parser.add_argument("--image", type=Path, required=True, help="Path to JPEG/PNG/WEBP/BMP/TIFF image") parser.add_argument("--api-key", default=API_KEY, help="X-API-Key (default: scripts/call_predict.py constant)") parser.add_argument("--device-id", default=DEVICE_ID, help="Device id (default: scripts/call_predict.py constant)") parser.add_argument("--secret", default=SECRET_KEY, help="HMAC secret hex (default: scripts/call_predict.py constant)") args = parser.parse_args() image_path: Path = args.image if not image_path.is_file(): print(f"Error: image not found: {image_path}", file=sys.stderr) raise SystemExit(1) image_bytes = image_path.read_bytes() headers = asyncio.run( _signed_headers( image_bytes=image_bytes, filename=image_path.name, device_id=args.device_id, secret=args.secret, api_key=args.api_key, path=ENDPOINT_PATH, ) ) content_type, _ = mimetypes.guess_type(str(image_path)) if not content_type: content_type = "application/octet-stream" url = f"{args.base_url.rstrip('/')}{ENDPOINT_PATH}" with httpx.Client(timeout=300.0) as client: response = client.post( url, headers=headers, files={"image": (image_path.name, image_bytes, content_type)}, ) print(response.status_code, response.reason_phrase) try: print(response.json()) except Exception: print(response.text) if __name__ == "__main__": main()