Coverage for polar/exceptions.py: 93%
65 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 12:42 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 12:42 +0000
1from collections.abc import Sequence
2from typing import Any, ClassVar, Literal, LiteralString, NotRequired, TypedDict
4from pydantic import BaseModel, Field, create_model
5from pydantic_core import ErrorDetails, InitErrorDetails, PydanticCustomError
6from pydantic_core import ValidationError as PydanticValidationError
8from polar.config import settings
11class PolarError(Exception):
12 """
13 Base exception class for all errors raised by Polar.
15 A custom exception handler for FastAPI takes care
16 of catching and returning a proper HTTP error from them.
18 Args:
19 message: The error message that'll be displayed to the user.
20 status_code: The status code of the HTTP response. Defaults to 500.
21 headers: Additional headers to be included in the response.
22 """
24 _schema: ClassVar[type[BaseModel] | None] = None
26 def __init__(
27 self,
28 message: str,
29 status_code: int = 500,
30 headers: dict[str, str] | None = None,
31 ) -> None:
32 super().__init__(message)
33 self.message = message
34 self.status_code = status_code
35 self.headers = headers
37 @classmethod
38 def schema(cls) -> type[BaseModel]:
39 if cls._schema is not None:
40 return cls._schema
42 error_literal = Literal[cls.__name__] # type: ignore
44 model = create_model(
45 cls.__name__,
46 error=(error_literal, Field(examples=[cls.__name__])),
47 detail=(str, ...),
48 )
49 cls._schema = model
50 return cls._schema
53class PolarTaskError(PolarError):
54 """
55 Base exception class for errors raised by tasks.
57 Args:
58 message: The error message.
59 """
61 def __init__(self, message: str) -> None:
62 super().__init__(message)
65class PolarRedirectionError(PolarError):
66 """
67 Exception class for errors
68 that should be displayed nicely to the user through our UI.
70 A specific exception handler will redirect to `/error` page in the client app.
72 Args:
73 return_to: Target URL of the *Go back* button on the error page.
74 """
76 def __init__(
77 self, message: str, status_code: int = 400, return_to: str | None = None
78 ) -> None:
79 self.return_to = return_to
80 super().__init__(message, status_code)
83class BadRequest(PolarError):
84 def __init__(self, message: str = "Bad request", status_code: int = 400) -> None:
85 super().__init__(message, status_code)
88class NotPermitted(PolarError):
89 def __init__(self, message: str = "Not permitted", status_code: int = 403) -> None:
90 super().__init__(message, status_code)
93class Unauthorized(PolarError):
94 def __init__(self, message: str = "Unauthorized", status_code: int = 401) -> None:
95 super().__init__(
96 message,
97 status_code,
98 headers={
99 "WWW-Authenticate": f'Bearer realm="{settings.WWW_AUTHENTICATE_REALM}"'
100 },
101 )
104class InternalServerError(PolarError):
105 def __init__(
106 self, message: str = "Internal Server Error", status_code: int = 500
107 ) -> None:
108 super().__init__(message, status_code)
111class ResourceNotFound(PolarError):
112 def __init__(self, message: str = "Not found", status_code: int = 404) -> None:
113 super().__init__(message, status_code)
116class ResourceNotModified(Exception):
117 # Handled separately to avoid any content being returned
118 """304 Not Modified."""
120 def __init__(self) -> None:
121 self.status_code = 304
124class ResourceUnavailable(PolarError):
125 def __init__(self, message: str = "Unavailable", status_code: int = 410) -> None:
126 super().__init__(message, status_code)
129class ResourceAlreadyExists(PolarError):
130 def __init__(self, message: str = "Already exists", status_code: int = 409) -> None:
131 super().__init__(message, status_code)
134class PaymentNotReady(PolarError):
135 def __init__(
136 self,
137 message: str = "Organization is not ready to accept payments",
138 status_code: int = 403,
139 ) -> None:
140 super().__init__(message, status_code)
143class ValidationError(TypedDict):
144 loc: tuple[int | str, ...]
145 msg: LiteralString
146 type: LiteralString
147 input: Any
148 ctx: NotRequired[dict[str, Any]]
149 url: NotRequired[str]
152class PolarRequestValidationError(PolarError):
153 def __init__(self, errors: Sequence[ValidationError]) -> None:
154 self._errors = errors
156 def errors(self) -> list[ErrorDetails]:
157 pydantic_errors: list[InitErrorDetails] = []
158 for error in self._errors:
159 pydantic_errors.append(
160 {
161 "type": PydanticCustomError(error["type"], error["msg"]),
162 "loc": error["loc"],
163 "input": error["input"],
164 }
165 )
166 pydantic_error = PydanticValidationError.from_exception_data(
167 self.__class__.__name__, pydantic_errors
168 )
169 return pydantic_error.errors()