"""HTTP client for the m2-ledger service (contracts/ledger-api.md).""" from __future__ import annotations import httpx DEFAULT_TIMEOUT = 10.0 class LedgerError(Exception): """Any ledger error not covered by a more specific exception.""" def __init__(self, message: str, status_code: int | None = None, body: dict | None = None): super().__init__(message) self.status_code = status_code self.body = body or {} class InsufficientFunds(LedgerError): """Raised on 402 — insufficient balance, nothing written.""" class DuplicateRef(LedgerError): """Raised on 409 — same ref already settled/refunded (idempotency guard).""" class LedgerClient: def __init__(self, base_url: str, api_key: str, timeout: float = DEFAULT_TIMEOUT): self._client = httpx.Client( base_url=base_url, headers={"X-API-Key": api_key}, timeout=timeout, ) def close(self) -> None: self._client.close() def __enter__(self) -> "LedgerClient": return self def __exit__(self, *exc_info) -> None: self.close() def _raise_for_error(self, response: httpx.Response) -> None: try: body = response.json() except ValueError: body = {} message = body.get("error", response.text) if response.status_code == 402: raise InsufficientFunds(message, response.status_code, body) if response.status_code == 409: raise DuplicateRef(message, response.status_code, body) raise LedgerError(message, response.status_code, body) def balance(self, operator_id: str) -> dict: response = self._client.get(f"/balance/{operator_id}") if response.status_code != 200: self._raise_for_error(response) return response.json() def install( self, buyer: str, seller: str, amount: int, ref: str, platform_pct: float | None = None, ) -> dict: body = {"buyer": buyer, "seller": seller, "amount": amount, "ref": ref} if platform_pct is not None: body["platform_pct"] = platform_pct response = self._client.post("/tx/install", json=body) if response.status_code != 200: self._raise_for_error(response) return response.json()["grant"] def refund(self, ref: str) -> list: response = self._client.post("/tx/refund", json={"ref": ref}) if response.status_code != 200: self._raise_for_error(response) return response.json()["tx_ids"] def transactions(self, **filters) -> list: response = self._client.get("/tx", params=filters) if response.status_code != 200: self._raise_for_error(response) return response.json()["transactions"] def license(self, operator_id: str, listing_id: str) -> dict | None: response = self._client.get(f"/license/{operator_id}/{listing_id}") if response.status_code == 404: return None if response.status_code != 200: self._raise_for_error(response) return response.json()["grant"]