Source code for rational.errors
"""Exceptions that cross the client/server boundary as structured, safe errors."""
from __future__ import annotations
from typing import Any
[docs]
class RationalError(Exception):
"""Base for errors whose ``message`` is safe to show to the user."""
status: int = 400
code: str = "error"
def __init__(
self, message: str = "", *, code: str | None = None, status: int | None = None, details: Any = None
) -> None:
super().__init__(message or self.__class__.__name__)
self.message = message or self.default_message()
if code is not None:
self.code = code
if status is not None:
self.status = status
self.details = details
def default_message(self) -> str:
return "Request failed"
def to_json(self) -> dict[str, Any]:
out: dict[str, Any] = {"code": self.code, "message": self.message}
if self.details is not None:
out["details"] = self.details
return out
[docs]
class RemoteError(RationalError):
"""Raise from services or handlers to report a user-facing failure."""
[docs]
class FieldError(InvalidInput):
"""Reject one or more form fields from a handler or service::
raise FieldError("email", "This email is already registered")
raise FieldError({"password": "Too short", "confirm": "Does not match"})
Each message is shown under the matching form field; the JSON API answers 422.
"""
def __init__(self, field: str | dict[str, str], message: str = "") -> None:
errors = field if isinstance(field, dict) else {field: message}
super().__init__(details=[{"field": f, "message": m} for f, m in errors.items()])
[docs]
class Unauthorized(RationalError):
status = 401
code = "unauthorized"
def default_message(self) -> str:
return "Authentication required"
[docs]
class Forbidden(RationalError):
status = 403
code = "forbidden"
def default_message(self) -> str:
return "Not allowed"
[docs]
class NotFound(RationalError):
status = 404
code = "not_found"
def default_message(self) -> str:
return "Not found"
[docs]
class Redirect(Exception): # noqa: N818 - control flow, not an error
"""Raise during a page render or an event to send the browser elsewhere."""
def __init__(self, url: str, status: int = 303) -> None:
super().__init__(url)
self.url = url
self.status = status
[docs]
def redirect(url: str) -> Redirect:
"""``raise redirect("/login")``"""
return Redirect(url)