fastapi-mongo-base 1.3.5__tar.gz → 1.3.7__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.5/src/fastapi_mongo_base.egg-info → fastapi_mongo_base-1.3.7}/PKG-INFO +1 -1
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/pyproject.toml +1 -1
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/core/app_factory.py +7 -1
- fastapi_mongo_base-1.3.7/src/fastapi_mongo_base/core/error_responses.py +137 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/core/exceptions.py +64 -28
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/routes.py +8 -3
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/utils/usso_routes.py +3 -3
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7/src/fastapi_mongo_base.egg-info}/PKG-INFO +1 -1
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base.egg-info/SOURCES.txt +2 -0
- fastapi_mongo_base-1.3.7/tests/test_openapi_errors.py +116 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/LICENSE.txt +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/MANIFEST.in +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/README.md +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/docs/boilerplate.md +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/docs/changelog.md +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/docs/contributing.md +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/docs/endpoints.md +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/docs/extra.css +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/docs/index.md +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/docs/installation.md +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/docs/license.md +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/docs/quickstart.md +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/docs/settings.md +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/docs/tutorial.md +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/docs/usage.md +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/mkdocs.yml +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/pytest.ini +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/setup.cfg +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/__init__.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/core/__init__.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/core/config.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/core/db.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/core/enums.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/core/errors/mongodb_errors.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/core/errors/resource_errors.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/models.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/py.typed +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/schemas.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/sql.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/tasks.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/utils/__init__.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/utils/basic.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/utils/bsontools.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/utils/timezone.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base.egg-info/dependency_links.txt +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base.egg-info/requires.txt +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base.egg-info/top_level.txt +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/tests/__init__.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/tests/app/__init__.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/tests/app/logs/app.log +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/tests/app/main.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/tests/app/server.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/tests/conftest.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/tests/test_apis.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/tests/test_app.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/tests/test_mongodb_errors.py +0 -0
- {fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/tests/test_resource_errors.py +0 -0
{fastapi_mongo_base-1.3.5/src/fastapi_mongo_base.egg-info → fastapi_mongo_base-1.3.7}/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.7
|
|
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.7"
|
|
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.5 → fastapi_mongo_base-1.3.7}/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.5 → fastapi_mongo_base-1.3.7}/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
|
|
@@ -110,40 +117,75 @@ def base_http_exception_handler(
|
|
|
110
117
|
else:
|
|
111
118
|
message = exc.message
|
|
112
119
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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)
|
|
122
169
|
|
|
123
170
|
|
|
124
171
|
def pydantic_exception_handler(
|
|
125
|
-
request: Request, exc:
|
|
172
|
+
request: Request, exc: RequestValidationError | ResponseValidationError
|
|
126
173
|
) -> JSONResponse:
|
|
127
174
|
"""
|
|
128
175
|
Handle Pydantic validation errors and return JSON response.
|
|
129
176
|
|
|
130
177
|
Args:
|
|
131
178
|
request: FastAPI request object.
|
|
132
|
-
exc:
|
|
179
|
+
exc: RequestValidationError or ResponseValidationError instance.
|
|
133
180
|
|
|
134
181
|
Returns:
|
|
135
182
|
JSONResponse with validation error details.
|
|
136
183
|
|
|
137
184
|
"""
|
|
138
185
|
logging.debug("pydantic_exception_handler: %s\n%s", request.url, exc)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
"message": str(exc),
|
|
143
|
-
"error": "Exception",
|
|
144
|
-
"errors": json.loads(json_advanced.dumps(exc.errors())),
|
|
145
|
-
},
|
|
146
|
-
)
|
|
186
|
+
|
|
187
|
+
status_code = 500 if isinstance(exc, ResponseValidationError) else 422
|
|
188
|
+
return _validation_error_response(request, exc.errors(), status_code)
|
|
147
189
|
|
|
148
190
|
|
|
149
191
|
async def request_validation_exception_handler(
|
|
@@ -185,11 +227,7 @@ async def request_validation_exception_handler(
|
|
|
185
227
|
dict(request.headers),
|
|
186
228
|
)
|
|
187
229
|
|
|
188
|
-
|
|
189
|
-
request_validation_exception_handler as default_handler,
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
return await default_handler(request, exc)
|
|
230
|
+
return _validation_error_response(request, exc.errors())
|
|
193
231
|
|
|
194
232
|
|
|
195
233
|
def mongodb_exception_handler(
|
|
@@ -253,10 +291,8 @@ def general_exception_handler(
|
|
|
253
291
|
except ImportError:
|
|
254
292
|
pass
|
|
255
293
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
content={"message": str(exc), "error": "Exception"},
|
|
259
|
-
)
|
|
294
|
+
content = InternalErrorResponseModel(message=str(exc)).model_dump()
|
|
295
|
+
return JSONResponse(status_code=500, content=content)
|
|
260
296
|
|
|
261
297
|
|
|
262
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.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/utils/usso_routes.py
RENAMED
|
@@ -188,7 +188,7 @@ class AbstractUSSORouterBase(AbstractBaseRouter):
|
|
|
188
188
|
if item is None:
|
|
189
189
|
raise exceptions.BaseHTTPException(
|
|
190
190
|
status_code=404,
|
|
191
|
-
|
|
191
|
+
error_code="item_not_found",
|
|
192
192
|
message={
|
|
193
193
|
"en": f"{self.model.__name__.capitalize()} not found"
|
|
194
194
|
},
|
|
@@ -223,7 +223,7 @@ class AbstractUSSORouterBase(AbstractBaseRouter):
|
|
|
223
223
|
if filters.get("__deny__"):
|
|
224
224
|
raise exceptions.BaseHTTPException(
|
|
225
225
|
status_code=403,
|
|
226
|
-
|
|
226
|
+
error_code="forbidden",
|
|
227
227
|
message={
|
|
228
228
|
"en": "You are not authorized to access this resource"
|
|
229
229
|
},
|
|
@@ -448,7 +448,7 @@ class AbstractOwnedUSSORouter(AbstractUSSORouterBase):
|
|
|
448
448
|
):
|
|
449
449
|
raise exceptions.BaseHTTPException(
|
|
450
450
|
status_code=400,
|
|
451
|
-
|
|
451
|
+
error_code="workspace_required",
|
|
452
452
|
message={
|
|
453
453
|
"en": "User must belong to a workspace for this resource"
|
|
454
454
|
},
|
{fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7/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.7
|
|
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.5 → fastapi_mongo_base-1.3.7}/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.5 → fastapi_mongo_base-1.3.7}/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.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/utils/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/utils/bsontools.py
RENAMED
|
File without changes
|
{fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base/utils/timezone.py
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/src/fastapi_mongo_base.egg-info/requires.txt
RENAMED
|
File without changes
|
{fastapi_mongo_base-1.3.5 → fastapi_mongo_base-1.3.7}/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
|