fastapi-mongo-base 1.3.4__tar.gz → 1.3.6__tar.gz
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.
- {fastapi_mongo_base-1.3.4/src/fastapi_mongo_base.egg-info → fastapi_mongo_base-1.3.6}/PKG-INFO +1 -1
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/pyproject.toml +1 -1
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/core/app_factory.py +7 -1
- fastapi_mongo_base-1.3.6/src/fastapi_mongo_base/core/error_responses.py +137 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/core/exceptions.py +71 -30
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/routes.py +8 -3
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6/src/fastapi_mongo_base.egg-info}/PKG-INFO +1 -1
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base.egg-info/SOURCES.txt +2 -0
- fastapi_mongo_base-1.3.6/tests/test_openapi_errors.py +116 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/LICENSE.txt +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/MANIFEST.in +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/README.md +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/boilerplate.md +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/changelog.md +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/contributing.md +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/endpoints.md +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/extra.css +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/index.md +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/installation.md +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/license.md +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/quickstart.md +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/settings.md +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/tutorial.md +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/usage.md +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/mkdocs.yml +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/pytest.ini +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/setup.cfg +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/__init__.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/core/__init__.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/core/config.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/core/db.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/core/enums.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/core/errors/mongodb_errors.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/core/errors/resource_errors.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/models.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/py.typed +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/schemas.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/sql.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/tasks.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/utils/__init__.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/utils/basic.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/utils/bsontools.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/utils/timezone.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/utils/usso_routes.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base.egg-info/dependency_links.txt +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base.egg-info/requires.txt +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base.egg-info/top_level.txt +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/__init__.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/app/__init__.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/app/logs/app.log +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/app/main.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/app/server.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/conftest.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/test_apis.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/test_app.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/test_mongodb_errors.py +0 -0
- {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/test_resource_errors.py +0 -0
{fastapi_mongo_base-1.3.4/src/fastapi_mongo_base.egg-info → fastapi_mongo_base-1.3.6}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-mongo-base
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.6
|
|
4
4
|
Summary: A simple boilerplate application, including models and schemas and abstract router, for FastAPI with MongoDB
|
|
5
5
|
Author-email: Mahdi Kiani <mahdikiany@gmail.com>
|
|
6
6
|
Maintainer-email: Mahdi Kiani <mahdikiany@gmail.com>
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fastapi-mongo-base"
|
|
7
|
-
version = "1.3.
|
|
7
|
+
version = "1.3.6"
|
|
8
8
|
description = "A simple boilerplate application, including models and schemas and abstract router, for FastAPI with MongoDB"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
{fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/core/app_factory.py
RENAMED
|
@@ -12,6 +12,7 @@ from fastapi.responses import RedirectResponse
|
|
|
12
12
|
from fastapi.staticfiles import StaticFiles
|
|
13
13
|
|
|
14
14
|
from . import config, db, exceptions
|
|
15
|
+
from .error_responses import COMMON_ERROR_RESPONSES, setup_openapi_errors
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def health(request: fastapi.Request) -> dict[str, str]:
|
|
@@ -258,7 +259,10 @@ def create_app(
|
|
|
258
259
|
license_info=license_info,
|
|
259
260
|
)
|
|
260
261
|
|
|
261
|
-
app = fastapi.FastAPI(
|
|
262
|
+
app = fastapi.FastAPI(
|
|
263
|
+
**data,
|
|
264
|
+
responses=kwargs.pop("responses", COMMON_ERROR_RESPONSES),
|
|
265
|
+
)
|
|
262
266
|
|
|
263
267
|
app = configure_app(
|
|
264
268
|
app=app,
|
|
@@ -356,4 +360,6 @@ def configure_app(
|
|
|
356
360
|
name="coverage",
|
|
357
361
|
)
|
|
358
362
|
|
|
363
|
+
setup_openapi_errors(app)
|
|
364
|
+
|
|
359
365
|
return app
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""OpenAPI error response schemas aligned with exception handlers."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import fastapi
|
|
6
|
+
from fastapi.openapi.utils import get_openapi
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class APIErrorResponseModel(BaseModel):
|
|
11
|
+
"""Structured error response for BaseHTTPException handlers."""
|
|
12
|
+
|
|
13
|
+
model_config = ConfigDict(extra="allow")
|
|
14
|
+
|
|
15
|
+
message: dict[str, str]
|
|
16
|
+
error_code: str
|
|
17
|
+
detail: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ValidationReason(BaseModel):
|
|
21
|
+
"""Single field validation failure."""
|
|
22
|
+
|
|
23
|
+
model_config = ConfigDict(extra="allow")
|
|
24
|
+
|
|
25
|
+
field: str
|
|
26
|
+
type: str | None = None
|
|
27
|
+
msg: str | None = None
|
|
28
|
+
input: Any = None
|
|
29
|
+
ctx: dict[str, Any] | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ValidationErrorResponseModel(BaseModel):
|
|
33
|
+
"""Structured validation error response."""
|
|
34
|
+
|
|
35
|
+
message: dict[str, str]
|
|
36
|
+
error_code: str = "validation_error"
|
|
37
|
+
detail: None = None
|
|
38
|
+
reasons: list[ValidationReason] = Field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class InternalErrorResponseModel(BaseModel):
|
|
42
|
+
"""Fallback response for unhandled exceptions."""
|
|
43
|
+
|
|
44
|
+
message: str
|
|
45
|
+
error: str = "Exception"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
COMMON_ERROR_RESPONSES: dict[int, dict[str, Any]] = {
|
|
49
|
+
400: {
|
|
50
|
+
"model": APIErrorResponseModel,
|
|
51
|
+
"description": "Bad request",
|
|
52
|
+
},
|
|
53
|
+
401: {
|
|
54
|
+
"model": APIErrorResponseModel,
|
|
55
|
+
"description": "Unauthorized",
|
|
56
|
+
},
|
|
57
|
+
402: {
|
|
58
|
+
"model": APIErrorResponseModel,
|
|
59
|
+
"description": "Payment required",
|
|
60
|
+
},
|
|
61
|
+
403: {
|
|
62
|
+
"model": APIErrorResponseModel,
|
|
63
|
+
"description": "Forbidden",
|
|
64
|
+
},
|
|
65
|
+
404: {
|
|
66
|
+
"model": APIErrorResponseModel,
|
|
67
|
+
"description": "Not found",
|
|
68
|
+
},
|
|
69
|
+
409: {
|
|
70
|
+
"model": APIErrorResponseModel,
|
|
71
|
+
"description": "Conflict",
|
|
72
|
+
},
|
|
73
|
+
410: {
|
|
74
|
+
"model": APIErrorResponseModel,
|
|
75
|
+
"description": "Gone",
|
|
76
|
+
},
|
|
77
|
+
422: {
|
|
78
|
+
"model": ValidationErrorResponseModel,
|
|
79
|
+
"description": "Validation error",
|
|
80
|
+
},
|
|
81
|
+
423: {
|
|
82
|
+
"model": APIErrorResponseModel,
|
|
83
|
+
"description": "Locked",
|
|
84
|
+
},
|
|
85
|
+
500: {
|
|
86
|
+
"model": APIErrorResponseModel,
|
|
87
|
+
"description": "Internal server error",
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _patch_validation_responses(schema: dict[str, Any]) -> None:
|
|
93
|
+
validation_ref = {
|
|
94
|
+
"$ref": "#/components/schemas/ValidationErrorResponseModel"
|
|
95
|
+
}
|
|
96
|
+
for path_item in schema.get("paths", {}).values():
|
|
97
|
+
for operation in path_item.values():
|
|
98
|
+
if not isinstance(operation, dict):
|
|
99
|
+
continue
|
|
100
|
+
responses = operation.get("responses")
|
|
101
|
+
if not isinstance(responses, dict) or "422" not in responses:
|
|
102
|
+
continue
|
|
103
|
+
responses["422"] = {
|
|
104
|
+
"description": "Validation error",
|
|
105
|
+
"content": {
|
|
106
|
+
"application/json": {
|
|
107
|
+
"schema": validation_ref,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def setup_openapi_errors(app: fastapi.FastAPI) -> None:
|
|
114
|
+
"""
|
|
115
|
+
Replace default FastAPI validation error schemas in OpenAPI.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
app: FastAPI application instance.
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def custom_openapi() -> dict[str, Any]:
|
|
123
|
+
if app.openapi_schema:
|
|
124
|
+
return app.openapi_schema
|
|
125
|
+
|
|
126
|
+
schema = get_openapi(
|
|
127
|
+
title=app.title,
|
|
128
|
+
version=app.version,
|
|
129
|
+
openapi_version=app.openapi_version,
|
|
130
|
+
description=app.description,
|
|
131
|
+
routes=app.routes,
|
|
132
|
+
)
|
|
133
|
+
_patch_validation_responses(schema)
|
|
134
|
+
app.openapi_schema = schema
|
|
135
|
+
return app.openapi_schema
|
|
136
|
+
|
|
137
|
+
app.openapi = custom_openapi
|
{fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/core/exceptions.py
RENAMED
|
@@ -15,6 +15,13 @@ from fastapi.exceptions import (
|
|
|
15
15
|
from fastapi.responses import JSONResponse
|
|
16
16
|
from pydantic import ValidationError
|
|
17
17
|
|
|
18
|
+
from fastapi_mongo_base.core.error_responses import (
|
|
19
|
+
APIErrorResponseModel,
|
|
20
|
+
InternalErrorResponseModel,
|
|
21
|
+
ValidationErrorResponseModel,
|
|
22
|
+
ValidationReason,
|
|
23
|
+
)
|
|
24
|
+
|
|
18
25
|
try:
|
|
19
26
|
from usso.integrations.fastapi import (
|
|
20
27
|
EXCEPTION_HANDLERS as usso_exception_handler, # noqa: N811
|
|
@@ -100,45 +107,85 @@ def base_http_exception_handler(
|
|
|
100
107
|
logging.debug("base_http_exception_handler: %s\n%s", request.url, exc)
|
|
101
108
|
|
|
102
109
|
if request.headers.get("accept-language"):
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
locales = request.headers.get("accept-language").split(",")
|
|
111
|
+
msg = {}
|
|
112
|
+
for locale in locales:
|
|
113
|
+
lang = locale.split("-")[0]
|
|
114
|
+
if lang in exc.message:
|
|
115
|
+
msg[lang] = exc.message.get(lang)
|
|
116
|
+
message = msg
|
|
105
117
|
else:
|
|
106
118
|
message = exc.message
|
|
107
119
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
content = APIErrorResponseModel(
|
|
121
|
+
message=message,
|
|
122
|
+
error_code=exc.error_code,
|
|
123
|
+
detail=exc.detail,
|
|
124
|
+
**exc.data,
|
|
125
|
+
).model_dump()
|
|
126
|
+
return JSONResponse(status_code=exc.status_code, content=content)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _resolve_validation_message(request: Request) -> dict[str, str]:
|
|
130
|
+
message = {
|
|
131
|
+
"en": "Validation error",
|
|
132
|
+
"fa": "اطلاعات وارد شده صحیح نیست",
|
|
133
|
+
}
|
|
134
|
+
if request.headers.get("accept-language"):
|
|
135
|
+
locales = request.headers.get("accept-language").split(",")
|
|
136
|
+
filtered_message = {}
|
|
137
|
+
for locale in locales:
|
|
138
|
+
lang = locale.split("-")[0]
|
|
139
|
+
if lang in message:
|
|
140
|
+
filtered_message[lang] = message[lang]
|
|
141
|
+
return filtered_message or message
|
|
142
|
+
return message
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _format_validation_reasons(errors: list[dict]) -> list[dict]:
|
|
146
|
+
reasons_by_field: dict[str, dict] = {}
|
|
147
|
+
for error in json.loads(json_advanced.dumps(errors)):
|
|
148
|
+
loc = error.pop("loc", ())
|
|
149
|
+
field = str(loc[-1]) if loc else "unknown"
|
|
150
|
+
error.pop("url", None)
|
|
151
|
+
error["field"] = field
|
|
152
|
+
reasons_by_field.setdefault(field, error)
|
|
153
|
+
return list(reasons_by_field.values())
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _validation_error_response(
|
|
157
|
+
request: Request,
|
|
158
|
+
errors: list[dict],
|
|
159
|
+
status_code: int = 422,
|
|
160
|
+
) -> JSONResponse:
|
|
161
|
+
content = ValidationErrorResponseModel(
|
|
162
|
+
message=_resolve_validation_message(request),
|
|
163
|
+
reasons=[
|
|
164
|
+
ValidationReason(**reason)
|
|
165
|
+
for reason in _format_validation_reasons(errors)
|
|
166
|
+
],
|
|
167
|
+
).model_dump()
|
|
168
|
+
return JSONResponse(status_code=status_code, content=content)
|
|
117
169
|
|
|
118
170
|
|
|
119
171
|
def pydantic_exception_handler(
|
|
120
|
-
request: Request, exc:
|
|
172
|
+
request: Request, exc: RequestValidationError | ResponseValidationError
|
|
121
173
|
) -> JSONResponse:
|
|
122
174
|
"""
|
|
123
175
|
Handle Pydantic validation errors and return JSON response.
|
|
124
176
|
|
|
125
177
|
Args:
|
|
126
178
|
request: FastAPI request object.
|
|
127
|
-
exc:
|
|
179
|
+
exc: RequestValidationError or ResponseValidationError instance.
|
|
128
180
|
|
|
129
181
|
Returns:
|
|
130
182
|
JSONResponse with validation error details.
|
|
131
183
|
|
|
132
184
|
"""
|
|
133
185
|
logging.debug("pydantic_exception_handler: %s\n%s", request.url, exc)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
"message": str(exc),
|
|
138
|
-
"error": "Exception",
|
|
139
|
-
"errors": json.loads(json_advanced.dumps(exc.errors())),
|
|
140
|
-
},
|
|
141
|
-
)
|
|
186
|
+
|
|
187
|
+
status_code = 500 if isinstance(exc, ResponseValidationError) else 422
|
|
188
|
+
return _validation_error_response(request, exc.errors(), status_code)
|
|
142
189
|
|
|
143
190
|
|
|
144
191
|
async def request_validation_exception_handler(
|
|
@@ -180,11 +227,7 @@ async def request_validation_exception_handler(
|
|
|
180
227
|
dict(request.headers),
|
|
181
228
|
)
|
|
182
229
|
|
|
183
|
-
|
|
184
|
-
request_validation_exception_handler as default_handler,
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
return await default_handler(request, exc)
|
|
230
|
+
return _validation_error_response(request, exc.errors())
|
|
188
231
|
|
|
189
232
|
|
|
190
233
|
def mongodb_exception_handler(
|
|
@@ -248,10 +291,8 @@ def general_exception_handler(
|
|
|
248
291
|
except ImportError:
|
|
249
292
|
pass
|
|
250
293
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
content={"message": str(exc), "error": "Exception"},
|
|
254
|
-
)
|
|
294
|
+
content = InternalErrorResponseModel(message=str(exc)).model_dump()
|
|
295
|
+
return JSONResponse(status_code=500, content=content)
|
|
255
296
|
|
|
256
297
|
|
|
257
298
|
# A dictionary for dynamic registration
|
|
@@ -11,6 +11,7 @@ from fastapi import APIRouter, BackgroundTasks, Query, Request
|
|
|
11
11
|
from pydantic import BaseModel
|
|
12
12
|
|
|
13
13
|
from .core.config import Settings
|
|
14
|
+
from .core.error_responses import COMMON_ERROR_RESPONSES
|
|
14
15
|
from .core.exceptions import BaseHTTPException
|
|
15
16
|
from .models import BaseEntity
|
|
16
17
|
from .schemas import BaseEntitySchema, PaginatedResponse
|
|
@@ -90,9 +91,13 @@ class AbstractBaseRouter(metaclass=singleton.Singleton):
|
|
|
90
91
|
if tags is None:
|
|
91
92
|
tags = [self.model.__name__]
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
prefix
|
|
95
|
-
|
|
94
|
+
router_kwargs = {
|
|
95
|
+
"prefix": prefix,
|
|
96
|
+
"tags": cast(list[str | Enum], tags),
|
|
97
|
+
"responses": kwargs.pop("responses", COMMON_ERROR_RESPONSES),
|
|
98
|
+
**kwargs,
|
|
99
|
+
}
|
|
100
|
+
self.router = APIRouter(**router_kwargs)
|
|
96
101
|
|
|
97
102
|
self.config_schemas(self.schema, **kwargs)
|
|
98
103
|
self.config_routes(**kwargs)
|
{fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6/src/fastapi_mongo_base.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-mongo-base
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.6
|
|
4
4
|
Summary: A simple boilerplate application, including models and schemas and abstract router, for FastAPI with MongoDB
|
|
5
5
|
Author-email: Mahdi Kiani <mahdikiany@gmail.com>
|
|
6
6
|
Maintainer-email: Mahdi Kiani <mahdikiany@gmail.com>
|
{fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base.egg-info/SOURCES.txt
RENAMED
|
@@ -33,6 +33,7 @@ src/fastapi_mongo_base/core/app_factory.py
|
|
|
33
33
|
src/fastapi_mongo_base/core/config.py
|
|
34
34
|
src/fastapi_mongo_base/core/db.py
|
|
35
35
|
src/fastapi_mongo_base/core/enums.py
|
|
36
|
+
src/fastapi_mongo_base/core/error_responses.py
|
|
36
37
|
src/fastapi_mongo_base/core/exceptions.py
|
|
37
38
|
src/fastapi_mongo_base/core/errors/mongodb_errors.py
|
|
38
39
|
src/fastapi_mongo_base/core/errors/resource_errors.py
|
|
@@ -46,6 +47,7 @@ tests/conftest.py
|
|
|
46
47
|
tests/test_apis.py
|
|
47
48
|
tests/test_app.py
|
|
48
49
|
tests/test_mongodb_errors.py
|
|
50
|
+
tests/test_openapi_errors.py
|
|
49
51
|
tests/test_resource_errors.py
|
|
50
52
|
tests/app/__init__.py
|
|
51
53
|
tests/app/main.py
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Tests for OpenAPI error response schemas."""
|
|
2
|
+
|
|
3
|
+
from fastapi.testclient import TestClient
|
|
4
|
+
|
|
5
|
+
from fastapi_mongo_base.core.error_responses import (
|
|
6
|
+
APIErrorResponseModel,
|
|
7
|
+
ValidationErrorResponseModel,
|
|
8
|
+
)
|
|
9
|
+
from fastapi_mongo_base.core.exceptions import BaseHTTPException
|
|
10
|
+
|
|
11
|
+
from .app.server import app as fastapi_app
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_openapi_documents_error_schemas() -> None:
|
|
15
|
+
"""OpenAPI should document custom error response schemas."""
|
|
16
|
+
schema = fastapi_app.openapi()
|
|
17
|
+
|
|
18
|
+
assert "APIErrorResponseModel" in schema["components"]["schemas"]
|
|
19
|
+
assert "ValidationErrorResponseModel" in schema["components"]["schemas"]
|
|
20
|
+
|
|
21
|
+
list_get = schema["paths"]["/test"]["get"]["responses"]
|
|
22
|
+
assert "404" in list_get
|
|
23
|
+
assert (
|
|
24
|
+
list_get["404"]["content"]["application/json"]["schema"]["$ref"]
|
|
25
|
+
== "#/components/schemas/APIErrorResponseModel"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
create_post = schema["paths"]["/test"]["post"]["responses"]
|
|
29
|
+
assert "422" in create_post
|
|
30
|
+
assert (
|
|
31
|
+
create_post["422"]["content"]["application/json"]["schema"]["$ref"]
|
|
32
|
+
== "#/components/schemas/ValidationErrorResponseModel"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_openapi_error_models_match_handler_output() -> None:
|
|
37
|
+
"""Documented schemas should include fields returned by handlers."""
|
|
38
|
+
api_schema = APIErrorResponseModel.model_json_schema()
|
|
39
|
+
assert "message" in api_schema["properties"]
|
|
40
|
+
assert "error_code" in api_schema["properties"]
|
|
41
|
+
assert "detail" in api_schema["properties"]
|
|
42
|
+
|
|
43
|
+
validation_schema = ValidationErrorResponseModel.model_json_schema()
|
|
44
|
+
assert "reasons" in validation_schema["properties"]
|
|
45
|
+
assert validation_schema["properties"]["error_code"]["default"] == (
|
|
46
|
+
"validation_error"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_runtime_validation_error_matches_openapi_schema() -> None:
|
|
51
|
+
"""Validation handler response should match documented error shape."""
|
|
52
|
+
client = TestClient(fastapi_app)
|
|
53
|
+
response = client.post("/test", json={})
|
|
54
|
+
|
|
55
|
+
assert response.status_code == 422
|
|
56
|
+
body = response.json()
|
|
57
|
+
assert body["error_code"] == "validation_error"
|
|
58
|
+
assert "message" in body
|
|
59
|
+
assert "reasons" in body
|
|
60
|
+
assert isinstance(body["reasons"], list)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_runtime_base_http_exception_matches_openapi_schema() -> None:
|
|
64
|
+
"""BaseHTTPException handler response should match documented shape."""
|
|
65
|
+
from starlette.requests import Request
|
|
66
|
+
|
|
67
|
+
from fastapi_mongo_base.core.exceptions import base_http_exception_handler
|
|
68
|
+
|
|
69
|
+
request = Request({
|
|
70
|
+
"type": "http",
|
|
71
|
+
"headers": [],
|
|
72
|
+
"method": "GET",
|
|
73
|
+
"path": "/test/not-a-real-uid",
|
|
74
|
+
})
|
|
75
|
+
exc = BaseHTTPException(
|
|
76
|
+
status_code=404,
|
|
77
|
+
error_code="item_not_found",
|
|
78
|
+
message={"en": "Testentity not found"},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
response = base_http_exception_handler(request, exc)
|
|
82
|
+
body = response.body.decode()
|
|
83
|
+
|
|
84
|
+
assert response.status_code == 404
|
|
85
|
+
assert "item_not_found" in body
|
|
86
|
+
assert "Testentity not found" in body
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_custom_route_documents_api_error_response() -> None:
|
|
90
|
+
"""App-level responses should document APIErrorResponseModel on routes."""
|
|
91
|
+
from fastapi import FastAPI
|
|
92
|
+
|
|
93
|
+
from fastapi_mongo_base.core.app_factory import setup_exception_handlers
|
|
94
|
+
from fastapi_mongo_base.core.error_responses import (
|
|
95
|
+
COMMON_ERROR_RESPONSES,
|
|
96
|
+
setup_openapi_errors,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
app = FastAPI(responses=COMMON_ERROR_RESPONSES)
|
|
100
|
+
setup_exception_handlers(app=app)
|
|
101
|
+
setup_openapi_errors(app)
|
|
102
|
+
|
|
103
|
+
@app.get("/missing")
|
|
104
|
+
def raise_not_found() -> None:
|
|
105
|
+
raise BaseHTTPException(
|
|
106
|
+
status_code=404,
|
|
107
|
+
error_code="item_not_found",
|
|
108
|
+
message={"en": "Item not found"},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
schema = app.openapi()
|
|
112
|
+
responses = schema["paths"]["/missing"]["get"]["responses"]
|
|
113
|
+
assert (
|
|
114
|
+
responses["404"]["content"]["application/json"]["schema"]["$ref"]
|
|
115
|
+
== "#/components/schemas/APIErrorResponseModel"
|
|
116
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/core/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/utils/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/utils/bsontools.py
RENAMED
|
File without changes
|
{fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/utils/timezone.py
RENAMED
|
File without changes
|
{fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/utils/usso_routes.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base.egg-info/requires.txt
RENAMED
|
File without changes
|
{fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|