Coverage for polar/exceptions.py: 76%
71 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-12-05 17:15 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-12-05 17:15 +0000
1from collections.abc import Sequence 1ba
2from typing import Any, ClassVar, Literal, LiteralString, NotRequired, TypedDict 1ba
4from pydantic import BaseModel, Field, create_model 1ba
5from pydantic_core import ErrorDetails, InitErrorDetails, PydanticCustomError 1ba
6from pydantic_core import ValidationError as PydanticValidationError 1ba
8from polar.config import settings 1ba
11class PolarError(Exception): 1ba
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 1ba
26 def __init__( 1ba
27 self,
28 message: str,
29 status_code: int = 500,
30 headers: dict[str, str] | None = None,
31 ) -> None:
32 super().__init__(message) 1c
33 self.message = message 1c
34 self.status_code = status_code 1c
35 self.headers = headers 1c
37 @classmethod 1ba
38 def schema(cls) -> type[BaseModel]: 1ba
39 if cls._schema is not None: 1a
40 return cls._schema 1a
42 error_literal = Literal[cls.__name__] # type: ignore 1a
44 model = create_model( 1a
45 cls.__name__,
46 error=(error_literal, Field(examples=[cls.__name__])),
47 detail=(str, ...),
48 )
49 cls._schema = model 1a
50 return cls._schema 1a
53class PolarTaskError(PolarError): 1ba
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: 1ba
62 super().__init__(message)
65class PolarRedirectionError(PolarError): 1ba
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__( 1ba
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): 1ba
84 def __init__(self, message: str = "Bad request", status_code: int = 400) -> None: 1ba
85 super().__init__(message, status_code)
88class NotPermitted(PolarError): 1ba
89 def __init__(self, message: str = "Not permitted", status_code: int = 403) -> None: 1ba
90 super().__init__(message, status_code)
93class Unauthorized(PolarError): 1ba
94 def __init__(self, message: str = "Unauthorized", status_code: int = 401) -> None: 1ba
95 super().__init__( 1c
96 message,
97 status_code,
98 headers={
99 "WWW-Authenticate": f'Bearer realm="{settings.WWW_AUTHENTICATE_REALM}"'
100 },
101 )
104class InternalServerError(PolarError): 1ba
105 def __init__( 1ba
106 self, message: str = "Internal Server Error", status_code: int = 500
107 ) -> None:
108 super().__init__(message, status_code)
111class ResourceNotFound(PolarError): 1ba
112 def __init__(self, message: str = "Not found", status_code: int = 404) -> None: 1ba
113 super().__init__(message, status_code) 1c
116class ResourceNotModified(Exception): 1ba
117 # Handled separately to avoid any content being returned
118 """304 Not Modified."""
120 def __init__(self) -> None: 1ba
121 self.status_code = 304
124class ResourceUnavailable(PolarError): 1ba
125 def __init__(self, message: str = "Unavailable", status_code: int = 410) -> None: 1ba
126 super().__init__(message, status_code)
129class ResourceAlreadyExists(PolarError): 1ba
130 def __init__(self, message: str = "Already exists", status_code: int = 409) -> None: 1ba
131 super().__init__(message, status_code)
134class PaymentNotReady(PolarError): 1ba
135 def __init__( 1ba
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): 1ba
144 loc: tuple[int | str, ...] 1ba
145 msg: LiteralString 1ba
146 type: LiteralString 1ba
147 input: Any 1ba
148 ctx: NotRequired[dict[str, Any]] 1ba
149 url: NotRequired[str] 1ba
152class PolarRequestValidationError(PolarError): 1ba
153 def __init__(self, errors: Sequence[ValidationError]) -> None: 1ba
154 self._errors = errors
156 def errors(self) -> list[ErrorDetails]: 1ba
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()