4ce4204b6c
CI / Lint (push) Failing after 2s
CI / Build (push) Has been skipped
SDK CI / PHP SDK (push) Failing after 1s
Split PHP SDK / PHP SDK tests (push) Failing after 1s
CI / Test (push) Failing after 1s
CI / Dashboard (push) Failing after 0s
SDK CI / JavaScript SDK (push) Failing after 0s
SDK CI / Python SDK (push) Failing after 2s
SDK CI / Java SDK (push) Failing after 1s
Split PHP SDK / Mirror sdk/php -> rmyndharis/openwa-php (push) Has been skipped
CI / Test (PostgreSQL migrations) (push) Failing after 7m47s
CI / Docker Build (push) Has been skipped
100 lines
3.1 KiB
Python
100 lines
3.1 KiB
Python
"""Typed error hierarchy for the OpenWA Python SDK.
|
|
|
|
The OpenWA API returns NestJS-default errors of the shape::
|
|
|
|
{"statusCode": int, "message": str | list[str], "error": str}
|
|
|
|
This module maps that to a typed, ergonomic error tree so callers can
|
|
``isinstance``-check or branch on ``.status``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
|
|
class OpenWAError(Exception):
|
|
"""Base class for every error raised by the SDK."""
|
|
|
|
|
|
class OpenWAApiError(OpenWAError):
|
|
"""Raised when the API responds with a non-2xx status.
|
|
|
|
Attributes:
|
|
status: HTTP status code.
|
|
body: Parsed JSON body if available, otherwise the raw text.
|
|
error_kind: Value of the ``error`` field in the NestJS envelope.
|
|
"""
|
|
|
|
def __init__(self, message: str, status: int, body: Any = None, error_kind: str | None = None) -> None:
|
|
super().__init__(message)
|
|
self.status = status
|
|
self.body = body
|
|
self.error_kind = error_kind
|
|
|
|
@classmethod
|
|
def from_response(cls, status_code: int, text: str, context: str) -> "OpenWAApiError":
|
|
import json
|
|
|
|
body: Any = None
|
|
if text:
|
|
try:
|
|
body = json.loads(text)
|
|
except ValueError:
|
|
body = text
|
|
envelope = body if isinstance(body, dict) and "statusCode" in body else None
|
|
raw_message = envelope.get("message") if envelope else body
|
|
if isinstance(raw_message, list):
|
|
message_text = ", ".join(str(m) for m in raw_message)
|
|
elif isinstance(raw_message, str):
|
|
message_text = raw_message
|
|
else:
|
|
message_text = str(raw_message)
|
|
message = f"OpenWA API {status_code} — {context}: {message_text}"
|
|
return classify(status_code, message, body, envelope.get("error") if envelope else None)
|
|
|
|
|
|
class OpenWAAuthError(OpenWAApiError):
|
|
"""401 Unauthorized — missing or invalid API key."""
|
|
|
|
|
|
class OpenWAForbiddenError(OpenWAApiError):
|
|
"""403 Forbidden — insufficient role."""
|
|
|
|
|
|
class OpenWANotFoundError(OpenWAApiError):
|
|
"""404 Not Found."""
|
|
|
|
|
|
class OpenWAConflictError(OpenWAApiError):
|
|
"""409 Conflict — typically an engine-not-ready condition."""
|
|
|
|
|
|
class OpenWARateLimitError(OpenWAApiError):
|
|
"""429 Too Many Requests."""
|
|
|
|
|
|
class OpenWANotImplementedError(OpenWAApiError):
|
|
"""501 Not Implemented — the active engine does not support this operation."""
|
|
|
|
|
|
class OpenWATimeoutError(OpenWAError):
|
|
"""Raised when a request exceeds the configured timeout."""
|
|
|
|
def __init__(self, timeout: float) -> None:
|
|
super().__init__(f"Request timed out after {timeout}s")
|
|
self.timeout = timeout
|
|
|
|
|
|
def classify(status: int, message: str, body: Any, error_kind: str | None) -> OpenWAApiError:
|
|
"""Pick the most specific :class:`OpenWAApiError` subclass for a status."""
|
|
cls = {
|
|
401: OpenWAAuthError,
|
|
403: OpenWAForbiddenError,
|
|
404: OpenWANotFoundError,
|
|
409: OpenWAConflictError,
|
|
429: OpenWARateLimitError,
|
|
501: OpenWANotImplementedError,
|
|
}.get(status, OpenWAApiError)
|
|
return cls(message, status, body, error_kind)
|