48 lines
1.9 KiB
Python
48 lines
1.9 KiB
Python
"""Machine-readable error codes for user-facing API failures.
|
|||
|
|
|
||
|
|
The dashboard is multilingual (app/static/i18n.js) but the API is not: every
|
||
|
|
message produced here stays English. What travels alongside it is a stable
|
||
|
|
`code` the client maps onto its own translated string (`error.<code>`), falling
|
||
|
|
back to `message` for any code it doesn't recognize — so a non-dashboard
|
||
|
|
consumer (curl, tests, a future client) still gets something readable without
|
||
|
|
having to know the code table.
|
||
|
|
|
||
|
|
`detail` is therefore an object rather than the FastAPI-default bare string:
|
||
|
|
|
||
|
|
{"code": "insufficient_balance", "message": "insufficient balance", "params": {}}
|
||
|
|
|
||
|
|
`params` carries the values interpolated into the message (amounts, limits) so
|
||
|
|
the translated string can place them wherever its own grammar needs them,
|
||
|
|
instead of the client having to parse them back out of the English text.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
from fastapi import HTTPException
|
||
|
|
|
||
|
|
|
||
|
|
class ApiError(Exception):
|
||
|
|
"""Domain-layer error carrying the code the client will translate.
|
||
|
|
|
||
|
|
Subclassed per domain (BetError, WithdrawalError) so services keep raising
|
||
|
|
their own exception type. `str(exc)` is still the plain English message.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, code: str, message: str, **params: Any) -> None:
|
||
|
|
super().__init__(message)
|
||
|
|
self.code = code
|
||
|
|
self.message = message
|
||
|
|
self.params = params
|
||
|
|
|
||
|
|
def as_detail(self) -> dict[str, Any]:
|
||
|
|
return {"code": self.code, "message": self.message, "params": self.params}
|
||
|
|
|
||
|
|
|
||
|
|
def http_error(status_code: int, code: str, message: str, **params: Any) -> HTTPException:
|
||
|
|
"""HTTPException whose detail is the structured object described above."""
|
||
|
|
return HTTPException(status_code, ApiError(code, message, **params).as_detail())
|
||
|
|
|
||
|
|
|
||
|
|
def from_api_error(status_code: int, exc: ApiError) -> HTTPException:
|
||
|
|
return HTTPException(status_code, exc.as_detail())
|