merge: cli-ledger-client (US1 wave 1)
This commit is contained in:
commit
0960f062b2
1 changed files with 96 additions and 0 deletions
96
cli/src/m2_market/ledger.py
Normal file
96
cli/src/m2_market/ledger.py
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
"""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"]
|
||||||
Loading…
Reference in a new issue