awesome-errors 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. awesome_errors/__init__.py +232 -0
  2. awesome_errors/analysis/__init__.py +8 -0
  3. awesome_errors/analysis/decorators.py +251 -0
  4. awesome_errors/analysis/error_analyzer.py +463 -0
  5. awesome_errors/client/__init__.py +7 -0
  6. awesome_errors/client/exceptions.py +97 -0
  7. awesome_errors/client/response_parser.py +175 -0
  8. awesome_errors/converters/__init__.py +31 -0
  9. awesome_errors/converters/generic.py +23 -0
  10. awesome_errors/converters/pydantic_converter.py +75 -0
  11. awesome_errors/converters/python_converter.py +80 -0
  12. awesome_errors/converters/sql_converter.py +258 -0
  13. awesome_errors/converters/universal_converter.py +123 -0
  14. awesome_errors/core/__init__.py +0 -0
  15. awesome_errors/core/error_codes.py +104 -0
  16. awesome_errors/core/error_response.py +61 -0
  17. awesome_errors/core/exceptions.py +613 -0
  18. awesome_errors/core/renderers.py +115 -0
  19. awesome_errors/i18n/__init__.py +0 -0
  20. awesome_errors/i18n/locales/en/errors.json +32 -0
  21. awesome_errors/i18n/locales/test/errors.json +4 -0
  22. awesome_errors/i18n/locales/uk/errors.json +26 -0
  23. awesome_errors/i18n/translator.py +150 -0
  24. awesome_errors/integrations/__init__.py +11 -0
  25. awesome_errors/integrations/fastapi_auto_docs.py +220 -0
  26. awesome_errors/litestar_utils.py +55 -0
  27. awesome_errors/middleware/__init__.py +0 -0
  28. awesome_errors/middleware/fastapi.py +247 -0
  29. awesome_errors/middleware/litestar.py +333 -0
  30. awesome_errors/py.typed +1 -0
  31. awesome_errors/websocket/__init__.py +35 -0
  32. awesome_errors/websocket/error_handler.py +231 -0
  33. awesome_errors/websocket/exceptions.py +286 -0
  34. awesome_errors-0.3.0.dist-info/METADATA +239 -0
  35. awesome_errors-0.3.0.dist-info/RECORD +37 -0
  36. awesome_errors-0.3.0.dist-info/WHEEL +4 -0
  37. awesome_errors-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,247 @@
1
+ """FastAPI integration for awesome-errors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import traceback
7
+ from typing import Any, Callable, Dict, Optional, cast
8
+
9
+ from fastapi import FastAPI, Request
10
+ from fastapi.exceptions import HTTPException, RequestValidationError
11
+ from fastapi.responses import JSONResponse
12
+ from sqlalchemy.exc import SQLAlchemyError
13
+ from starlette.exceptions import HTTPException as StarletteHTTPException
14
+
15
+ from ..core.error_codes import ErrorCode
16
+ from ..core.exceptions import AppError, ValidationError
17
+ from ..core.renderers import ErrorResponseFormat, ErrorResponseRenderer, RenderResult
18
+ from ..converters.sql_converter import SQLErrorConverter
19
+ from ..i18n.translator import ErrorTranslator
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class ErrorHandlerMiddleware:
25
+ """FastAPI middleware that converts exceptions into structured responses."""
26
+
27
+ def __init__(
28
+ self,
29
+ app: FastAPI,
30
+ translator: Optional[ErrorTranslator] = None,
31
+ debug: bool = False,
32
+ log_errors: bool = True,
33
+ locales_dir: Optional[str] = None,
34
+ default_locale: str = "en",
35
+ response_format: ErrorResponseFormat = ErrorResponseFormat.LEGACY,
36
+ problem_type_resolver: Optional[Callable[[AppError], str]] = None,
37
+ problem_extension_builder: Optional[
38
+ Callable[[AppError], Dict[str, object]]
39
+ ] = None,
40
+ message_resolver: Optional[
41
+ Callable[[AppError, Optional[str], Optional[ErrorTranslator]], str]
42
+ ] = None,
43
+ ) -> None:
44
+ self.app = app
45
+
46
+ if translator is None:
47
+ from pathlib import Path
48
+
49
+ locales_path = Path(locales_dir) if locales_dir else None
50
+ self.translator = ErrorTranslator(
51
+ locales_dir=locales_path, default_locale=default_locale
52
+ )
53
+ else:
54
+ self.translator = translator
55
+
56
+ self.message_resolver = message_resolver
57
+ self.debug = debug
58
+ self.log_errors = log_errors
59
+ self.renderer = ErrorResponseRenderer(
60
+ format=response_format,
61
+ problem_type_resolver=problem_type_resolver,
62
+ problem_extension_builder=problem_extension_builder,
63
+ )
64
+
65
+ self._register_handlers()
66
+
67
+ def _register_handlers(self) -> None:
68
+ self.app.add_exception_handler(AppError, cast(Any, self._handle_app_error))
69
+ self.app.add_exception_handler(
70
+ RequestValidationError, cast(Any, self._handle_validation_error)
71
+ )
72
+ self.app.add_exception_handler(HTTPException, cast(Any, self._handle_http_exception))
73
+ self.app.add_exception_handler(
74
+ StarletteHTTPException, cast(Any, self._handle_http_exception)
75
+ )
76
+ self.app.add_exception_handler(SQLAlchemyError, cast(Any, self._handle_sqlalchemy_error))
77
+ self.app.add_exception_handler(Exception, cast(Any, self._handle_generic_error))
78
+
79
+ async def _handle_app_error(self, request: Request, exc: AppError) -> JSONResponse:
80
+ locale = self._get_locale(request)
81
+ translated_message = self._resolve_message(exc, locale)
82
+
83
+ if self.log_errors:
84
+ logger.error(
85
+ f"App error: {exc.code.value} - {exc.message}",
86
+ extra={
87
+ "error_code": exc.code.value,
88
+ "details": exc.details,
89
+ "request_id": exc.request_id,
90
+ },
91
+ )
92
+
93
+ if exc.status_code >= 500:
94
+ self._print_stacktrace(
95
+ "500 ERROR",
96
+ Error_Code=exc.code.value,
97
+ Message=exc.message,
98
+ Request_ID=exc.request_id,
99
+ Details=exc.details,
100
+ )
101
+
102
+ rendered: RenderResult = self.renderer.render(
103
+ exc, message=translated_message, request=request
104
+ )
105
+
106
+ return JSONResponse(
107
+ content=rendered.payload,
108
+ status_code=exc.status_code,
109
+ media_type=rendered.media_type,
110
+ headers={"X-Request-ID": exc.request_id or "unknown"},
111
+ )
112
+
113
+ async def _handle_validation_error(
114
+ self, request: Request, exc: RequestValidationError
115
+ ) -> JSONResponse:
116
+ details = {"errors": exc.errors()}
117
+
118
+ error = ValidationError(
119
+ message="Request validation failed",
120
+ code=ErrorCode.VALIDATION_FAILED,
121
+ )
122
+ error.details = details
123
+
124
+ return await self._handle_app_error(request, error)
125
+
126
+ async def _handle_http_exception(
127
+ self, request: Request, exc: HTTPException
128
+ ) -> JSONResponse:
129
+ status_to_code = {
130
+ 400: ErrorCode.INVALID_INPUT,
131
+ 401: ErrorCode.AUTH_REQUIRED,
132
+ 403: ErrorCode.AUTH_PERMISSION_DENIED,
133
+ 404: ErrorCode.RESOURCE_NOT_FOUND,
134
+ 422: ErrorCode.VALIDATION_FAILED,
135
+ }
136
+
137
+ error_code = status_to_code.get(exc.status_code, ErrorCode.UNKNOWN_ERROR)
138
+ details = {"http_detail": exc.detail}
139
+
140
+ error = AppError(
141
+ code=error_code,
142
+ message=str(exc.detail),
143
+ status_code=exc.status_code,
144
+ details=details,
145
+ )
146
+
147
+ if exc.status_code >= 500:
148
+ self._print_stacktrace(
149
+ "500 HTTP ERROR",
150
+ HTTP_Status=exc.status_code,
151
+ Error_Code=error_code.value,
152
+ Message=exc.detail,
153
+ Details=details,
154
+ )
155
+
156
+ return await self._handle_app_error(request, error)
157
+
158
+ async def _handle_sqlalchemy_error(
159
+ self, request: Request, exc: SQLAlchemyError
160
+ ) -> JSONResponse:
161
+ error = SQLErrorConverter.convert(exc)
162
+ return await self._handle_app_error(request, error)
163
+
164
+ async def _handle_generic_error(
165
+ self, request: Request, exc: Exception
166
+ ) -> JSONResponse:
167
+ if self.log_errors:
168
+ logger.exception("Unhandled exception")
169
+
170
+ self._print_stacktrace(
171
+ "UNHANDLED ERROR",
172
+ Exception_Type=type(exc).__name__,
173
+ Exception_Message=str(exc),
174
+ )
175
+
176
+ error = AppError(
177
+ code=ErrorCode.INTERNAL_ERROR,
178
+ message="An internal error occurred" if not self.debug else str(exc),
179
+ status_code=500,
180
+ )
181
+
182
+ if self.debug:
183
+ error.details["traceback"] = traceback.format_exc()
184
+
185
+ return await self._handle_app_error(request, error)
186
+
187
+ def _get_locale(self, request: Request) -> Optional[str]:
188
+ accept_language = request.headers.get("Accept-Language", "")
189
+ if accept_language:
190
+ return accept_language.split(",")[0].split("-")[0]
191
+ return None
192
+
193
+ def _resolve_message(self, error: AppError, locale: Optional[str]) -> str:
194
+ if self.message_resolver:
195
+ return self.message_resolver(error, locale, self.translator)
196
+
197
+ translated = self.translator.translate(
198
+ error.code.value,
199
+ locale=locale,
200
+ params=error.details,
201
+ )
202
+ if translated == error.code.value:
203
+ return error.message
204
+ return translated
205
+
206
+ def _print_stacktrace(self, error_type: str, **kwargs: object) -> None:
207
+ logger.error(f"\n=== {error_type} STACKTRACE ===")
208
+ for key, value in kwargs.items():
209
+ logger.error(f"{key}: {value}")
210
+ logger.error("Stacktrace:")
211
+ traceback.print_exc(limit=30)
212
+ logger.error(f"=== END {error_type} STACKTRACE ===\n")
213
+
214
+
215
+ def setup_error_handling(
216
+ app: FastAPI,
217
+ translator: Optional[ErrorTranslator] = None,
218
+ debug: bool = False,
219
+ log_errors: bool = True,
220
+ locales_dir: Optional[str] = None,
221
+ default_locale: str = "en",
222
+ custom_translations: Optional[Dict[str, Dict[str, str]]] = None,
223
+ response_format: ErrorResponseFormat = ErrorResponseFormat.LEGACY,
224
+ problem_type_resolver: Optional[Callable[[AppError], str]] = None,
225
+ problem_extension_builder: Optional[Callable[[AppError], Dict[str, object]]] = None,
226
+ message_resolver: Optional[
227
+ Callable[[AppError, Optional[str], Optional[ErrorTranslator]], str]
228
+ ] = None,
229
+ ) -> ErrorHandlerMiddleware:
230
+ middleware = ErrorHandlerMiddleware(
231
+ app=app,
232
+ translator=translator,
233
+ debug=debug,
234
+ log_errors=log_errors,
235
+ locales_dir=locales_dir,
236
+ default_locale=default_locale,
237
+ response_format=response_format,
238
+ problem_type_resolver=problem_type_resolver,
239
+ problem_extension_builder=problem_extension_builder,
240
+ message_resolver=message_resolver,
241
+ )
242
+
243
+ if custom_translations:
244
+ for locale, translations in custom_translations.items():
245
+ middleware.translator.add_translations(locale, translations, persist=False)
246
+
247
+ return middleware
@@ -0,0 +1,333 @@
1
+ """Litestar integration helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import traceback
7
+ from typing import TYPE_CHECKING, Callable, Dict, Mapping, Optional, Tuple, Type
8
+
9
+ if TYPE_CHECKING:
10
+ from litestar import Request
11
+ from litestar import Litestar
12
+ from litestar.exceptions import HTTPException, ValidationException
13
+ from litestar.response import Response
14
+ from litestar.types import ExceptionHandler
15
+
16
+ from ..core.error_codes import ErrorCode
17
+ from ..core.exceptions import AppError, ValidationError as CoreValidationError
18
+ from ..core.renderers import ErrorResponseFormat, ErrorResponseRenderer, RenderResult
19
+ from ..converters.sql_converter import SQLErrorConverter
20
+ from ..i18n.translator import ErrorTranslator
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def create_litestar_exception_handlers(
26
+ *,
27
+ translator: Optional[ErrorTranslator] = None,
28
+ debug: bool = False,
29
+ log_errors: bool = True,
30
+ log_level_resolver: Optional[Callable[[AppError], Optional[int]]] = None,
31
+ suppress_error_codes: Optional[Tuple[ErrorCode | str, ...]] = None,
32
+ locales_dir: Optional[str] = None,
33
+ default_locale: str = "en",
34
+ response_format: ErrorResponseFormat = ErrorResponseFormat.RFC7807,
35
+ problem_type_resolver: Optional[Callable[[AppError], str]] = None,
36
+ problem_extension_builder: Optional[Callable[[AppError], Dict[str, object]]] = None,
37
+ message_resolver: Optional[
38
+ Callable[[AppError, Optional[str], Optional[ErrorTranslator]], str]
39
+ ] = None,
40
+ ) -> Dict[Type[Exception], "ExceptionHandler"]:
41
+ """Return a mapping of exception handlers configured for Litestar."""
42
+
43
+ if translator is None:
44
+ from pathlib import Path
45
+
46
+ locales_path = Path(locales_dir) if locales_dir else None
47
+ translator = ErrorTranslator(
48
+ locales_dir=locales_path, default_locale=default_locale
49
+ )
50
+
51
+ renderer = ErrorResponseRenderer(
52
+ format=response_format,
53
+ problem_type_resolver=problem_type_resolver,
54
+ problem_extension_builder=problem_extension_builder,
55
+ )
56
+
57
+ from litestar.exceptions import HTTPException, ValidationException # type: ignore
58
+ from litestar.response import Response # type: ignore
59
+
60
+ def resolve_message(error: AppError, locale: Optional[str]) -> str:
61
+ if message_resolver:
62
+ return message_resolver(error, locale, translator)
63
+
64
+ translated = translator.translate(
65
+ error.code.value,
66
+ locale=locale,
67
+ params=error.details,
68
+ )
69
+ if translated == error.code.value:
70
+ return error.message
71
+ return translated
72
+
73
+ suppressed_codes = {
74
+ code.value if isinstance(code, ErrorCode) else str(code)
75
+ for code in (suppress_error_codes or ())
76
+ }
77
+
78
+ def handle_app_error(request: "Request", exc: AppError) -> "Response":
79
+ locale = request.headers.get("Accept-Language")
80
+ if locale:
81
+ locale = locale.split(",")[0].split("-")[0]
82
+
83
+ if log_errors:
84
+ if exc.code.value not in suppressed_codes:
85
+ level = log_level_resolver(exc) if log_level_resolver else logging.ERROR
86
+ if level is not None:
87
+ logger.log(
88
+ level,
89
+ f"App error: {exc.code.value} - {exc.message}",
90
+ extra={
91
+ "error_code": exc.code.value,
92
+ "details": exc.details,
93
+ "request_id": exc.request_id,
94
+ },
95
+ )
96
+
97
+ if exc.status_code >= 500:
98
+ _print_stacktrace(
99
+ "500 ERROR",
100
+ Error_Code=exc.code.value,
101
+ Message=exc.message,
102
+ Request_ID=exc.request_id,
103
+ Details=exc.details,
104
+ )
105
+
106
+ rendered: RenderResult = renderer.render(
107
+ exc, message=resolve_message(exc, locale), request=request
108
+ )
109
+
110
+ return Response(
111
+ content=rendered.payload,
112
+ status_code=exc.status_code,
113
+ media_type=rendered.media_type,
114
+ headers={"X-Request-ID": exc.request_id or "unknown"},
115
+ )
116
+
117
+ def handle_validation_error(
118
+ request: "Request", exc: "ValidationException"
119
+ ) -> "Response":
120
+ error = CoreValidationError(
121
+ message="Request validation failed",
122
+ code=ErrorCode.VALIDATION_FAILED,
123
+ )
124
+ errors = exc.extra or []
125
+ first_error = next(
126
+ (err for err in errors if isinstance(err, dict) and "path" in err),
127
+ None,
128
+ )
129
+ path = first_error.get("path") if first_error else None
130
+ if path is None:
131
+ path = getattr(exc, "path", None)
132
+ error.details = {
133
+ "errors": errors,
134
+ "path": path,
135
+ }
136
+ return handle_app_error(request, error)
137
+
138
+ def handle_http_exception(request: "Request", exc: "HTTPException") -> "Response":
139
+ status_to_code = {
140
+ 400: ErrorCode.INVALID_INPUT,
141
+ 401: ErrorCode.AUTH_REQUIRED,
142
+ 403: ErrorCode.AUTH_PERMISSION_DENIED,
143
+ 404: ErrorCode.RESOURCE_NOT_FOUND,
144
+ 422: ErrorCode.VALIDATION_FAILED,
145
+ }
146
+
147
+ error_code = status_to_code.get(exc.status_code, ErrorCode.UNKNOWN_ERROR)
148
+ error = AppError(
149
+ code=error_code,
150
+ message=str(exc.detail or exc.extra or exc.__class__.__name__),
151
+ status_code=exc.status_code,
152
+ details={
153
+ "detail": exc.detail,
154
+ "extra": exc.extra,
155
+ },
156
+ )
157
+
158
+ if exc.status_code >= 500:
159
+ _print_stacktrace(
160
+ "500 HTTP ERROR",
161
+ HTTP_Status=exc.status_code,
162
+ Error_Code=error_code.value,
163
+ Message=exc.detail,
164
+ )
165
+
166
+ return handle_app_error(request, error)
167
+
168
+ def handle_sqlalchemy_error(request: "Request", exc: Exception) -> "Response":
169
+ error = SQLErrorConverter.convert(exc)
170
+ return handle_app_error(request, error)
171
+
172
+ def handle_generic_error(request: "Request", exc: Exception) -> "Response":
173
+ if log_errors:
174
+ logger.exception("Unhandled exception")
175
+
176
+ _print_stacktrace(
177
+ "UNHANDLED ERROR",
178
+ Exception_Type=type(exc).__name__,
179
+ Exception_Message=str(exc),
180
+ )
181
+
182
+ error = AppError(
183
+ code=ErrorCode.INTERNAL_ERROR,
184
+ message="An internal error occurred" if not debug else str(exc),
185
+ status_code=500,
186
+ )
187
+
188
+ if debug:
189
+ error.details["traceback"] = traceback.format_exc()
190
+
191
+ return handle_app_error(request, error)
192
+
193
+ handlers: Dict[Type[Exception], "ExceptionHandler"] = {
194
+ AppError: handle_app_error,
195
+ ValidationException: handle_validation_error,
196
+ HTTPException: handle_http_exception,
197
+ Exception: handle_generic_error,
198
+ }
199
+
200
+ try:
201
+ from sqlalchemy.exc import SQLAlchemyError # type: ignore
202
+
203
+ handlers[SQLAlchemyError] = handle_sqlalchemy_error # type: ignore
204
+ except Exception: # pragma: no cover - SQLAlchemy already a dependency
205
+ pass
206
+
207
+ return handlers
208
+
209
+
210
+ def _print_stacktrace(error_type: str, **kwargs: object) -> None:
211
+ logger.error(f"\n=== {error_type} STACKTRACE ===")
212
+ for key, value in kwargs.items():
213
+ logger.error(f"{key}: {value}")
214
+ logger.error("Stacktrace:")
215
+ traceback.print_exc(limit=30)
216
+ logger.error(f"=== END {error_type} STACKTRACE ===\n")
217
+
218
+
219
+ _DEFAULT_PROBLEM_DETAILS: Dict[int, Tuple[str, str, str]] = {
220
+ 400: ("VALIDATION_FAILED", "Request validation failed", "Bad Request"),
221
+ 401: ("AUTH_REQUIRED", "Authentication required", "Unauthorized"),
222
+ 403: ("AUTH_PERMISSION_DENIED", "Access denied", "Forbidden"),
223
+ 404: ("RESOURCE_NOT_FOUND", "Resource not found", "Not Found"),
224
+ 409: ("DB_DUPLICATE_ENTRY", "Resource conflict", "Conflict"),
225
+ 422: ("VALIDATION_FAILED", "Unprocessable entity", "Unprocessable Entity"),
226
+ 429: ("RATE_LIMIT_EXCEEDED", "Too many requests", "Too Many Requests"),
227
+ 500: ("INTERNAL_ERROR", "Internal server error", "Internal Server Error"),
228
+ }
229
+
230
+
231
+ def apply_litestar_openapi_problem_details(
232
+ app: "Litestar",
233
+ *,
234
+ service_name: str,
235
+ status_defaults: Optional[Mapping[int, Tuple[str, str, str]]] = None,
236
+ example_instance: str = "/docs/openapi.json",
237
+ ) -> None:
238
+ """Ensure generated OpenAPI documentation reflects RFC 7807 error payloads."""
239
+ try: # pragma: no cover - optional dependency
240
+ from litestar.openapi.spec.enums import OpenAPIFormat, OpenAPIType
241
+ from litestar.openapi.spec.media_type import OpenAPIMediaType
242
+ from litestar.openapi.spec.schema import Schema
243
+ except ImportError as exc: # pragma: no cover
244
+ raise ImportError("litestar must be installed to use this helper") from exc
245
+
246
+ try:
247
+ schema = app.openapi_schema
248
+ except Exception: # pragma: no cover - OpenAPI disabled or misconfigured
249
+ return
250
+
251
+ if not schema or not getattr(schema, "paths", None):
252
+ return
253
+
254
+ defaults = dict(_DEFAULT_PROBLEM_DETAILS)
255
+ if status_defaults:
256
+ defaults.update(status_defaults)
257
+
258
+ problem_schema = Schema(
259
+ type=OpenAPIType.OBJECT,
260
+ required=[
261
+ "type",
262
+ "title",
263
+ "status",
264
+ "detail",
265
+ "instance",
266
+ "code",
267
+ "timestamp",
268
+ "request_id",
269
+ ],
270
+ properties={
271
+ "type": Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URI),
272
+ "title": Schema(type=OpenAPIType.STRING),
273
+ "status": Schema(type=OpenAPIType.INTEGER),
274
+ "detail": Schema(type=OpenAPIType.STRING),
275
+ "instance": Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.URI),
276
+ "code": Schema(type=OpenAPIType.STRING),
277
+ "timestamp": Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.DATE_TIME),
278
+ "request_id": Schema(type=OpenAPIType.STRING),
279
+ "service": Schema(type=OpenAPIType.STRING),
280
+ "details": Schema(type=OpenAPIType.OBJECT, additional_properties=True),
281
+ },
282
+ additional_properties=True,
283
+ description="RFC 7807 compatible error payload produced by awesome-errors.",
284
+ )
285
+
286
+ operations = ("delete", "get", "head", "options", "patch", "post", "put", "trace")
287
+
288
+ for path_item in (schema.paths or {}).values():
289
+ for operation_name in operations:
290
+ operation = getattr(path_item, operation_name, None)
291
+ if not operation or not getattr(operation, "responses", None):
292
+ continue
293
+
294
+ for status, response in operation.responses.items():
295
+ try:
296
+ status_code = int(status)
297
+ except (TypeError, ValueError):
298
+ continue
299
+
300
+ if status_code < 400:
301
+ continue
302
+
303
+ error_code, detail, title = defaults.get(
304
+ status_code,
305
+ ("UNKNOWN_ERROR", "An unexpected error occurred", "Error"),
306
+ )
307
+
308
+ example_payload = {
309
+ "type": f"urn:{service_name}:error:{error_code.lower()}",
310
+ "title": title,
311
+ "status": status_code,
312
+ "detail": detail,
313
+ "instance": example_instance,
314
+ "code": error_code,
315
+ "timestamp": "2024-01-08T12:00:00Z",
316
+ "request_id": "req_example123",
317
+ "service": service_name,
318
+ "details": {"info": "Example payload"},
319
+ }
320
+
321
+ existing = getattr(response, "content", None) or {}
322
+ media_type = existing.get("application/problem+json")
323
+ if media_type:
324
+ media_type.schema = problem_schema
325
+ media_type.example = example_payload
326
+ continue
327
+
328
+ response.content = {
329
+ "application/problem+json": OpenAPIMediaType(
330
+ schema=problem_schema,
331
+ example=example_payload,
332
+ )
333
+ }
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,35 @@
1
+ """WebSocket support for awesome-errors"""
2
+
3
+ from .exceptions import (
4
+ # Base classes
5
+ WebSocketError,
6
+ JSONRPCErrorCode,
7
+ # Specific errors
8
+ WebSocketAuthError,
9
+ WebSocketTokenExpiredError,
10
+ WebSocketRateLimitError,
11
+ WebSocketValidationError,
12
+ WebSocketMethodNotFoundError,
13
+ WebSocketInternalError,
14
+ )
15
+
16
+ from .error_handler import (
17
+ WebSocketErrorHandler,
18
+ setup_websocket_error_handling,
19
+ )
20
+
21
+ __all__ = [
22
+ # Base classes
23
+ "WebSocketError",
24
+ "JSONRPCErrorCode",
25
+ # Specific errors
26
+ "WebSocketAuthError",
27
+ "WebSocketTokenExpiredError",
28
+ "WebSocketRateLimitError",
29
+ "WebSocketValidationError",
30
+ "WebSocketMethodNotFoundError",
31
+ "WebSocketInternalError",
32
+ # Error handling
33
+ "WebSocketErrorHandler",
34
+ "setup_websocket_error_handling",
35
+ ]