sample API scripts

"""
Bas-Relief Public API (Python) — production sample

Recommended: provide your API key via environment variable.

Windows (PowerShell):
  $env:BASRELIEF_API_KEY = "YOUR_API_KEY"

macOS/Linux (bash/zsh):
  export BASRELIEF_API_KEY="YOUR_API_KEY"
"""

import base64
import json
import os
import sys
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional
from urllib.error import HTTPError, URLError
from urllib.parse import urljoin, urlparse
from urllib.request import Request, urlopen

DEFAULT_BASE_URL = "https://bas-relief.ai"
BASE_URL: str = DEFAULT_BASE_URL
APPROACH: str = "A"

# =====================
# EDIT THESE SETTINGS
# =====================

# Recommended: set env var BASRELIEF_API_KEY instead of committing a key into code.
API_KEY: str = os.getenv("BASRELIEF_API_KEY", "").strip()


# Job inputs
SUBJECT: str = "YOUR_SUBJECT"  # e.g. cats, flowers, predators, plane-tree
TEXT_TOP: str = ""
TEXT_CENTER: str = "YOUR_TEXT"
TEXT_BOTTOM: str = ""
TEXT_BOTTOM2: str = ""

LOCALE: str = "en" # If you enter subject in another language, change locale accordingly.
METHOD_NO: str = "C"

# Style: choose ONE of these options
STYLE_NO: int = 41 # e.g. 1, 2, 3, ...
CUSTOM_STYLE_TEXT: Optional[str] = None  # e.g. "baroque, ornate"

# Size (optional)
WIDTH: Optional[int] = 1024
HEIGHT: Optional[int] = 1024

# Optional: mask image path (png/jpg/webp). Set to None to not upload a mask.
# Examples:
#   MASK_PATH = str(Path.home() / "mask.png")
#   MASK_PATH = "/absolute/path/to/mask.png"  # macOS/Linux
MASK_PATH: Optional[str] = None

# Optional: client reference / correlation id
CLIENT_REFERENCE: Optional[str] = None

# Download folder
OUTDIR: str = str(Path.cwd() / "downloads")

# Polling behavior
TIMEOUT_SECONDS: int = 15 * 60
POLL_SECONDS: float = 2.0


@dataclass(frozen=True)
class ApiConfig:
    base_url: str
    api_key: str
    timeout_seconds: int
    poll_seconds: float


class ApiError(RuntimeError):
    pass


def _json_request(
    method: str,
    url: str,
    *,
    headers: Dict[str, str],
    body: Optional[Dict[str, Any]] = None,
    timeout: int = 60,
) -> Any:
    data = None
    req_headers = {"Accept": "application/json", **headers}
    if body is not None:
        data = json.dumps(body).encode("utf-8")
        req_headers["Content-Type"] = "application/json"

    req = Request(url, method=method, headers=req_headers, data=data)
    try:
        with urlopen(req, timeout=timeout) as resp:
            raw = resp.read()
            if not raw:
                return None
            return json.loads(raw.decode("utf-8"))
    except HTTPError as e:
        raw = None
        try:
            raw = e.read()
        except Exception:
            pass
        detail = None
        if raw:
            try:
                detail = json.loads(raw.decode("utf-8"))
            except Exception:
                detail = raw.decode("utf-8", errors="replace")
        raise ApiError(f"HTTP {e.code} calling {url}: {detail}") from e
    except URLError as e:
        raise ApiError(f"Network error calling {url}: {e}") from e


def _download_file(
    url: str,
    dest_path: Path,
    *,
    headers: Dict[str, str],
    timeout: int = 120,
) -> None:
    dest_path.parent.mkdir(parents=True, exist_ok=True)
    req = Request(url, method="GET", headers=headers)
    try:
        with urlopen(req, timeout=timeout) as resp:
            dest_path.write_bytes(resp.read())
    except HTTPError as e:
        raise ApiError(f"HTTP {e.code} downloading {url}") from e
    except URLError as e:
        raise ApiError(f"Network error downloading {url}: {e}") from e


def _normalize_base_url(base_url: str) -> str:
    base_url = (base_url or "").strip()
    if not base_url:
        return DEFAULT_BASE_URL
    return base_url.rstrip("/")


def _artifact_filename(artifact: Dict[str, Any]) -> str:
    fn = (artifact.get("filename") or "").strip()
    if fn:
        return fn
    url = (artifact.get("url") or "").strip()
    if url:
        p = urlparse(url)
        name = Path(p.path).name
        if name:
            return name
    return "artifact.bin"


def _read_mask_as_base64(mask_path: Path) -> Dict[str, str]:
    if not mask_path.exists():
        raise ApiError(f"Mask file not found: {mask_path}")

    ext = mask_path.suffix.lower().lstrip(".")
    if ext == "jpeg":
        ext = "jpg"
    if ext not in {"png", "jpg", "webp"}:
        raise ApiError(f"Unsupported mask extension: .{ext} (use png/jpg/webp)")

    raw = mask_path.read_bytes()
    return {
        "mask_image_base64": base64.b64encode(raw).decode("ascii"),
        "mask_image_ext": ext,
    }


def create_job(cfg: ApiConfig, payload: Dict[str, Any]) -> Dict[str, Any]:
    url = f"{cfg.base_url}/v1/jobs"
    headers = {"X-API-Key": cfg.api_key}
    return _json_request("POST", url, headers=headers, body=payload, timeout=60)


def get_job_status(cfg: ApiConfig, job_id: str) -> Dict[str, Any]:
    url = f"{cfg.base_url}/v1/jobs/{job_id}"
    headers = {"X-API-Key": cfg.api_key}
    return _json_request("GET", url, headers=headers, timeout=30)


def list_outputs(cfg: ApiConfig, job_id: str) -> List[Dict[str, Any]]:
    url = f"{cfg.base_url}/v1/jobs/{job_id}/outputs"
    headers = {"X-API-Key": cfg.api_key}
    data = _json_request("GET", url, headers=headers, timeout=60)
    if not isinstance(data, list):
        raise ApiError(f"Unexpected outputs response type: {type(data)}")
    return data


def wait_for_terminal(cfg: ApiConfig, job_id: str) -> Dict[str, Any]:
    deadline = time.time() + cfg.timeout_seconds
    last_status = None

    while time.time() < deadline:
        status = get_job_status(cfg, job_id)
        s = str(status.get("status") or "").lower()
        msg = status.get("message")
        progress = status.get("progress")

        if s != last_status:
            print(f"status={s} message={msg} progress={progress}")
            last_status = s
        else:
            print(".", end="", flush=True)

        if s in {"done", "completed", "success"}:
            print("")
            return status
        if s in {"failed", "error", "canceled", "cancelled"}:
            print("")
            raise ApiError(f"Job ended with status={s} message={msg}")

        time.sleep(cfg.poll_seconds)

    raise ApiError(f"Timed out waiting for job {job_id} after {cfg.timeout_seconds}s")


def main() -> int:
    base_url = _normalize_base_url(BASE_URL)
    api_key = (API_KEY or "").strip()
    if not api_key:
        print(
            "Missing API key. Set BASRELIEF_API_KEY env var.",
            file=sys.stderr,
        )
        return 2

    cfg = ApiConfig(
        base_url=base_url,
        api_key=api_key,
        timeout_seconds=TIMEOUT_SECONDS,
        poll_seconds=POLL_SECONDS,
    )

    style: Dict[str, Any]
    if CUSTOM_STYLE_TEXT is not None:
        style = {"custom": True, "style_no": None, "custom_text": CUSTOM_STYLE_TEXT}
    else:
        style = {"custom": False, "style_no": int(STYLE_NO), "custom_text": None}

    payload: Dict[str, Any] = {
        "subject": SUBJECT,
        "text_top": TEXT_TOP,
        "text_center": TEXT_CENTER,
        "text_bottom": TEXT_BOTTOM,
        "text_bottom2": TEXT_BOTTOM2,
        "style": style,
        "method_no": METHOD_NO,
        "approach": APPROACH,
        "locale": LOCALE,
        "width": WIDTH,
        "height": HEIGHT,
        "client_reference": CLIENT_REFERENCE,
    }

    payload = {k: v for k, v in payload.items() if v is not None}

    if MASK_PATH:
        payload.update(_read_mask_as_base64(Path(MASK_PATH)))

    print(f"POST {cfg.base_url}/v1/jobs")
    job = create_job(cfg, payload)
    job_id = job.get("job_id")
    if not job_id:
        raise ApiError(f"Create job response missing job_id: {job}")

    print(f"Created job_id={job_id}")
    wait_for_terminal(cfg, str(job_id))

    outputs = list_outputs(cfg, str(job_id))
    outdir = Path(OUTDIR) / str(job_id)
    outdir.mkdir(parents=True, exist_ok=True)

    headers = {"X-API-Key": cfg.api_key}
    print(f"Downloading {len(outputs)} artifact(s) to: {outdir}")
    for art in outputs:
        url = (art.get("url") or "").strip()
        if not url:
            continue
        if not url.lower().startswith("http"):
            url = urljoin(cfg.base_url + "/", url.lstrip("/"))

        filename = _artifact_filename(art)
        dest = outdir / filename
        kind = art.get("kind")
        print(f"- {kind}: {filename}")
        _download_file(url, dest, headers=headers)

    print("Done.")
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except ApiError as e:
        print(f"ERROR: {e}", file=sys.stderr)
        raise SystemExit(1)