"""Server-only Python client. Persist retries in your own durable outbox.""" import json import re import urllib.request import urllib.error from urllib.parse import urlparse class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None class WingsFlowError(Exception): def __init__(self, status, detail, request_id=None): super().__init__(detail.get("message", "API request failed")) self.status, self.code, self.request_id = status, detail.get("code"), request_id self.retryable = detail.get("retryable", False) class WingsFlowClient: def __init__(self, base_url, api_key, timeout=10): u=urlparse(base_url) if u.scheme != "https" and not (u.scheme == "http" and u.hostname in ("localhost", "127.0.0.1")): raise ValueError("HTTPS required") if u.username or u.password or u.query or u.fragment: raise ValueError("Clean API base URL required") if not re.fullmatch(r"wf_(test|live)_[0-9a-f-]+\.[A-Za-z0-9_-]+", api_key): raise ValueError("Invalid API credential") self.base, self.key, self.timeout=base_url.rstrip("/"), api_key, timeout self.opener=urllib.request.build_opener(NoRedirect()) def request(self, method, path, body=None, idempotency_key=None, if_match=None): if not path.startswith("/") or path.startswith("//") or ".." in path or "://" in path: raise ValueError("Relative API path required") if method != "GET" and not idempotency_key: raise ValueError("Persist an idempotency key first") headers={"Authorization":"Bearer "+self.key,"Accept":"application/json"} if body is not None: headers["Content-Type"]="application/json" if idempotency_key: headers["Idempotency-Key"]=idempotency_key if if_match: headers["If-Match"]=if_match req=urllib.request.Request(self.base+path, data=json.dumps(body).encode() if body is not None else None, headers=headers, method=method) try: with self.opener.open(req,timeout=self.timeout) as response: return json.load(response) except urllib.error.HTTPError as e: try: detail=json.loads(e.read()).get("error",{}) except (ValueError,UnicodeError): detail={"message":"Non-JSON API error; retain request identity"} error=WingsFlowError(e.code,detail,e.headers.get("x-request-id")) error.retry_after=e.headers.get("retry-after") raise error from None