fastapi-error-map 0.9.7__tar.gz → 0.9.9__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_error_map-0.9.7 → fastapi_error_map-0.9.9}/.github/workflows/ci.yaml +5 -4
- fastapi_error_map-0.9.9/.github/workflows/publish.yaml +59 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/.github/workflows/test-compatibility.yaml +5 -3
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/.pre-commit-config.yaml +3 -3
- fastapi_error_map-0.9.9/Makefile +24 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/PKG-INFO +18 -9
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/README.md +15 -6
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/fastapi_error_map/error_handling.py +8 -2
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/fastapi_error_map/routing.py +16 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/fastapi_error_map/rules.py +8 -3
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/noxfile.py +1 -1
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/pyproject.toml +7 -4
- fastapi_error_map-0.9.9/tests/integration/conftest.py +19 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/tests/integration/test_example.py +5 -8
- fastapi_error_map-0.9.9/tests/integration/test_exclude_none.py +85 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/tests/integration/test_routing.py +4 -9
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/tests/integration/test_threadpool.py +15 -18
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/tests/unit/test_error_handling.py +5 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/tests/unit/test_rules.py +67 -0
- fastapi_error_map-0.9.9/uv.lock +1731 -0
- fastapi_error_map-0.9.7/Makefile +0 -23
- fastapi_error_map-0.9.7/tests/integration/conftest.py +0 -25
- fastapi_error_map-0.9.7/uv.lock +0 -1535
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/.gitignore +0 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/LICENSE +0 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/docs/example-openapi.png +0 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/examples/__init__.py +0 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/examples/errors.py +0 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/examples/main.py +0 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/fastapi_error_map/__init__.py +0 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/fastapi_error_map/openapi.py +0 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/fastapi_error_map/py.typed +0 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/fastapi_error_map/translator_policy.py +0 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/fastapi_error_map/translators.py +0 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/tests/__init__.py +0 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/tests/integration/__init__.py +0 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/tests/unit/__init__.py +0 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/tests/unit/error_stubs.py +0 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/tests/unit/test_openapi.py +0 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/tests/unit/test_translators.py +0 -0
- {fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/tests/unit/translator_stubs.py +0 -0
|
@@ -7,21 +7,22 @@ jobs:
|
|
|
7
7
|
runs-on: ubuntu-latest
|
|
8
8
|
|
|
9
9
|
steps:
|
|
10
|
-
-
|
|
10
|
+
- name: Checkout
|
|
11
|
+
uses: actions/checkout@v6
|
|
11
12
|
|
|
12
13
|
- name: Set up Python
|
|
13
|
-
uses: actions/setup-python@
|
|
14
|
+
uses: actions/setup-python@v6
|
|
14
15
|
with:
|
|
15
16
|
python-version: "3.9"
|
|
16
17
|
|
|
17
18
|
- name: Install uv
|
|
18
|
-
uses: astral-sh/setup-uv@
|
|
19
|
+
uses: astral-sh/setup-uv@v7
|
|
19
20
|
|
|
20
21
|
- name: Install dependencies
|
|
21
22
|
run: uv sync --locked --group all
|
|
22
23
|
|
|
23
24
|
- name: Lint code
|
|
24
|
-
run: uv run make
|
|
25
|
+
run: uv run make lint
|
|
25
26
|
|
|
26
27
|
- name: Run tests for Codecov
|
|
27
28
|
run: uv run pytest --cov=fastapi_error_map --cov-branch --cov-report=xml
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: Publish to PyPI on GitHub Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [ published ]
|
|
6
|
+
|
|
7
|
+
env:
|
|
8
|
+
UV_PYTHON_DOWNLOADS: 0
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
name: Build distribution
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
permissions: { }
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout
|
|
17
|
+
uses: actions/checkout@v6
|
|
18
|
+
with:
|
|
19
|
+
persist-credentials: false
|
|
20
|
+
|
|
21
|
+
- name: Set up Python
|
|
22
|
+
uses: actions/setup-python@v6
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.13"
|
|
25
|
+
|
|
26
|
+
- name: Install uv
|
|
27
|
+
uses: astral-sh/setup-uv@v7
|
|
28
|
+
|
|
29
|
+
- name: Build
|
|
30
|
+
run: uv build
|
|
31
|
+
|
|
32
|
+
- name: Upload artifact
|
|
33
|
+
uses: actions/upload-artifact@v6
|
|
34
|
+
with:
|
|
35
|
+
name: python-package-distributions
|
|
36
|
+
path: dist/*
|
|
37
|
+
|
|
38
|
+
publish:
|
|
39
|
+
name: Publish to PyPI (OIDC)
|
|
40
|
+
needs: build
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
environment:
|
|
43
|
+
name: pypi
|
|
44
|
+
permissions:
|
|
45
|
+
id-token: write
|
|
46
|
+
contents: read
|
|
47
|
+
|
|
48
|
+
steps:
|
|
49
|
+
- name: Download artifact
|
|
50
|
+
uses: actions/download-artifact@v7
|
|
51
|
+
with:
|
|
52
|
+
name: python-package-distributions
|
|
53
|
+
path: dist/
|
|
54
|
+
|
|
55
|
+
- name: Install uv
|
|
56
|
+
uses: astral-sh/setup-uv@v7
|
|
57
|
+
|
|
58
|
+
- name: Publish
|
|
59
|
+
run: uv publish
|
{fastapi_error_map-0.9.7 → fastapi_error_map-0.9.9}/.github/workflows/test-compatibility.yaml
RENAMED
|
@@ -23,14 +23,16 @@ jobs:
|
|
|
23
23
|
- "3.14"
|
|
24
24
|
|
|
25
25
|
steps:
|
|
26
|
-
-
|
|
26
|
+
- name: Checkout
|
|
27
|
+
uses: actions/checkout@v6
|
|
28
|
+
|
|
27
29
|
- name: Set up ${{ matrix.python-version }} on ${{ matrix.os }}
|
|
28
|
-
uses: actions/setup-python@
|
|
30
|
+
uses: actions/setup-python@v6
|
|
29
31
|
with:
|
|
30
32
|
python-version: ${{ matrix.python-version }}
|
|
31
33
|
|
|
32
34
|
- name: Install uv
|
|
33
|
-
uses: astral-sh/setup-uv@
|
|
35
|
+
uses: astral-sh/setup-uv@v7
|
|
34
36
|
|
|
35
37
|
- name: Run tests
|
|
36
38
|
run: uvx nox -s compatibility
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Make config
|
|
2
|
+
.SILENT:
|
|
3
|
+
MAKEFLAGS += --no-print-directory
|
|
4
|
+
|
|
5
|
+
# Code quality
|
|
6
|
+
.PHONY: lint test check coverage
|
|
7
|
+
lint:
|
|
8
|
+
ruff check --fix
|
|
9
|
+
ruff format
|
|
10
|
+
mypy
|
|
11
|
+
|
|
12
|
+
test:
|
|
13
|
+
pytest -v
|
|
14
|
+
|
|
15
|
+
check: lint test
|
|
16
|
+
|
|
17
|
+
coverage: check
|
|
18
|
+
coverage html
|
|
19
|
+
|
|
20
|
+
# Project structure visualization
|
|
21
|
+
.PHONY: pycache-del
|
|
22
|
+
pycache-del:
|
|
23
|
+
find . -type d -name '__pycache__' -prune -exec rm -rf {} +; \
|
|
24
|
+
find . -type f \( -name '*.pyc' -o -name '*.pyo' \) -delete
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-error-map
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.9
|
|
4
4
|
Summary: Elegant per-endpoint error handling for FastAPI that keeps OpenAPI in sync
|
|
5
5
|
Project-URL: Homepage, https://github.com/ivan-borovets/fastapi-error-map
|
|
6
6
|
Project-URL: Repository, https://github.com/ivan-borovets/fastapi-error-map
|
|
@@ -21,14 +21,14 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Requires-Python: >=3.9
|
|
24
|
-
Requires-Dist: fastapi
|
|
25
|
-
Requires-Dist: orjson
|
|
24
|
+
Requires-Dist: fastapi<1.0,>=0.100
|
|
25
|
+
Requires-Dist: orjson>=3.11.4
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
|
|
28
28
|
## FastAPI Error Map
|
|
29
29
|
|
|
30
|
-
[](https://badge.fury.io/py/fastapi-error-map)
|
|
31
|
+

|
|
32
32
|
[](https://codecov.io/gh/ivan-borovets/fastapi-error-map)
|
|
33
33
|

|
|
34
34
|

|
|
@@ -169,10 +169,13 @@ Parameters of `rule(...)`, * — required:
|
|
|
169
169
|
|
|
170
170
|
#### 🧩 Matching semantics
|
|
171
171
|
|
|
172
|
-
`error_map`
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
172
|
+
`error_map` resolves exceptions using Python's Method Resolution Order (MRO).
|
|
173
|
+
The most specific exception type is matched first.
|
|
174
|
+
If no exact match is found, parent classes are checked in MRO order.
|
|
175
|
+
|
|
176
|
+
For example, if you map `BaseError` and raise `ChildError(BaseError)`,
|
|
177
|
+
the rule for `BaseError` will apply — unless a specific rule for
|
|
178
|
+
`ChildError` is defined, in which case it takes precedence.
|
|
176
179
|
|
|
177
180
|
### 🧰 Custom Translators
|
|
178
181
|
|
|
@@ -271,6 +274,7 @@ In addition to `error_map`, you can also pass:
|
|
|
271
274
|
warn_on_unmapped=...,
|
|
272
275
|
default_client_error_translator=...,
|
|
273
276
|
default_server_error_translator=...,
|
|
277
|
+
exclude_none=...,
|
|
274
278
|
)
|
|
275
279
|
```
|
|
276
280
|
|
|
@@ -299,6 +303,11 @@ When an error occurs, `fastapi-error-map` processes it as follows:
|
|
|
299
303
|
- Otherwise, `default_on_error` is used if provided
|
|
300
304
|
- If neither is set, nothing is called
|
|
301
305
|
|
|
306
|
+
4. `exclude_none`:
|
|
307
|
+
- If `True`, fields with value `None` are omitted from the serialized
|
|
308
|
+
error response body
|
|
309
|
+
- If `False` (default), `None` values are included as `null`
|
|
310
|
+
|
|
302
311
|
#### 🧾 OpenAPI: `responses` Takes Priority
|
|
303
312
|
|
|
304
313
|
If you explicitly pass the `responses=...` parameter to `.get(...)` / `.post(...)`, it overrides the schema generation
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
## FastAPI Error Map
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/py/fastapi-error-map)
|
|
4
|
+

|
|
5
5
|
[](https://codecov.io/gh/ivan-borovets/fastapi-error-map)
|
|
6
6
|

|
|
7
7
|

|
|
@@ -142,10 +142,13 @@ Parameters of `rule(...)`, * — required:
|
|
|
142
142
|
|
|
143
143
|
#### 🧩 Matching semantics
|
|
144
144
|
|
|
145
|
-
`error_map`
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
`error_map` resolves exceptions using Python's Method Resolution Order (MRO).
|
|
146
|
+
The most specific exception type is matched first.
|
|
147
|
+
If no exact match is found, parent classes are checked in MRO order.
|
|
148
|
+
|
|
149
|
+
For example, if you map `BaseError` and raise `ChildError(BaseError)`,
|
|
150
|
+
the rule for `BaseError` will apply — unless a specific rule for
|
|
151
|
+
`ChildError` is defined, in which case it takes precedence.
|
|
149
152
|
|
|
150
153
|
### 🧰 Custom Translators
|
|
151
154
|
|
|
@@ -244,6 +247,7 @@ In addition to `error_map`, you can also pass:
|
|
|
244
247
|
warn_on_unmapped=...,
|
|
245
248
|
default_client_error_translator=...,
|
|
246
249
|
default_server_error_translator=...,
|
|
250
|
+
exclude_none=...,
|
|
247
251
|
)
|
|
248
252
|
```
|
|
249
253
|
|
|
@@ -272,6 +276,11 @@ When an error occurs, `fastapi-error-map` processes it as follows:
|
|
|
272
276
|
- Otherwise, `default_on_error` is used if provided
|
|
273
277
|
- If neither is set, nothing is called
|
|
274
278
|
|
|
279
|
+
4. `exclude_none`:
|
|
280
|
+
- If `True`, fields with value `None` are omitted from the serialized
|
|
281
|
+
error response body
|
|
282
|
+
- If `False` (default), `None` values are included as `null`
|
|
283
|
+
|
|
275
284
|
#### 🧾 OpenAPI: `responses` Takes Priority
|
|
276
285
|
|
|
277
286
|
If you explicitly pass the `responses=...` parameter to `.get(...)` / `.post(...)`, it overrides the schema generation
|
|
@@ -19,6 +19,7 @@ def wrap_with_error_handling(
|
|
|
19
19
|
default_client_error_translator: ErrorTranslator[Any],
|
|
20
20
|
default_server_error_translator: ErrorTranslator[Any],
|
|
21
21
|
default_on_error: Optional[Callable[[Exception], Union[Awaitable[None], None]]],
|
|
22
|
+
exclude_none: bool = False,
|
|
22
23
|
) -> Callable[..., Any]:
|
|
23
24
|
is_coro = inspect.iscoroutinefunction(func)
|
|
24
25
|
|
|
@@ -27,7 +28,7 @@ def wrap_with_error_handling(
|
|
|
27
28
|
try:
|
|
28
29
|
if is_coro:
|
|
29
30
|
return await func(*args, **kwargs)
|
|
30
|
-
return func
|
|
31
|
+
return await run_in_threadpool(func, *args, **kwargs)
|
|
31
32
|
except Exception as error:
|
|
32
33
|
return await handle_with_error_map(
|
|
33
34
|
error=error,
|
|
@@ -36,6 +37,7 @@ def wrap_with_error_handling(
|
|
|
36
37
|
default_client_error_translator=default_client_error_translator,
|
|
37
38
|
default_server_error_translator=default_server_error_translator,
|
|
38
39
|
default_on_error=default_on_error,
|
|
40
|
+
exclude_none=exclude_none,
|
|
39
41
|
)
|
|
40
42
|
|
|
41
43
|
return wrapped
|
|
@@ -49,6 +51,7 @@ async def handle_with_error_map(
|
|
|
49
51
|
default_client_error_translator: ErrorTranslator[Any],
|
|
50
52
|
default_server_error_translator: ErrorTranslator[Any],
|
|
51
53
|
default_on_error: Optional[Callable[[Exception], Union[Awaitable[None], None]]],
|
|
54
|
+
exclude_none: bool,
|
|
52
55
|
) -> ORJSONResponse:
|
|
53
56
|
try:
|
|
54
57
|
rule = resolve_rule_for_error(
|
|
@@ -75,5 +78,8 @@ async def handle_with_error_map(
|
|
|
75
78
|
content = rule.translator.from_error(error)
|
|
76
79
|
return ORJSONResponse(
|
|
77
80
|
status_code=rule.status,
|
|
78
|
-
content=jsonable_encoder(
|
|
81
|
+
content=jsonable_encoder(
|
|
82
|
+
content,
|
|
83
|
+
exclude_none=exclude_none,
|
|
84
|
+
),
|
|
79
85
|
)
|
|
@@ -48,6 +48,7 @@ class ErrorAwareRoute(APIRoute):
|
|
|
48
48
|
warn_on_unmapped: bool = True,
|
|
49
49
|
default_client_error_translator: Optional[ErrorTranslator[Any]] = None,
|
|
50
50
|
default_server_error_translator: Optional[ErrorTranslator[Any]] = None,
|
|
51
|
+
exclude_none: bool = False,
|
|
51
52
|
# --- FastAPI ---
|
|
52
53
|
response_model: Any = Default(None),
|
|
53
54
|
status_code: Optional[int] = None,
|
|
@@ -98,6 +99,7 @@ class ErrorAwareRoute(APIRoute):
|
|
|
98
99
|
default_on_error=self.default_on_error,
|
|
99
100
|
default_client_error_translator=self.default_client_error_translator,
|
|
100
101
|
default_server_error_translator=self.default_server_error_translator,
|
|
102
|
+
exclude_none=exclude_none,
|
|
101
103
|
)
|
|
102
104
|
responses = {
|
|
103
105
|
**build_openapi_responses(
|
|
@@ -405,6 +407,7 @@ class ErrorAwareRouter(APIRouter):
|
|
|
405
407
|
warn_on_unmapped: bool = True,
|
|
406
408
|
default_client_error_translator: Optional[ErrorTranslator[Any]] = None,
|
|
407
409
|
default_server_error_translator: Optional[ErrorTranslator[Any]] = None,
|
|
410
|
+
exclude_none: bool = False,
|
|
408
411
|
# --- FastAPI ---
|
|
409
412
|
response_model: Any = Default(None),
|
|
410
413
|
status_code: Optional[int] = None,
|
|
@@ -462,6 +465,7 @@ class ErrorAwareRouter(APIRouter):
|
|
|
462
465
|
default_on_error=default_on_error,
|
|
463
466
|
default_client_error_translator=default_client_error_translator,
|
|
464
467
|
default_server_error_translator=default_server_error_translator,
|
|
468
|
+
exclude_none=exclude_none,
|
|
465
469
|
response_model=response_model,
|
|
466
470
|
status_code=status_code,
|
|
467
471
|
tags=current_tags,
|
|
@@ -528,6 +532,7 @@ class ErrorAwareRouter(APIRouter):
|
|
|
528
532
|
warn_on_unmapped: bool = True,
|
|
529
533
|
default_client_error_translator: Optional[ErrorTranslator[Any]] = None,
|
|
530
534
|
default_server_error_translator: Optional[ErrorTranslator[Any]] = None,
|
|
535
|
+
exclude_none: bool = False,
|
|
531
536
|
# --- FastAPI ---
|
|
532
537
|
response_model: Any = Default(None),
|
|
533
538
|
status_code: Optional[int] = None,
|
|
@@ -564,6 +569,7 @@ class ErrorAwareRouter(APIRouter):
|
|
|
564
569
|
default_on_error=default_on_error,
|
|
565
570
|
default_client_error_translator=default_client_error_translator,
|
|
566
571
|
default_server_error_translator=default_server_error_translator,
|
|
572
|
+
exclude_none=exclude_none,
|
|
567
573
|
response_model=response_model,
|
|
568
574
|
status_code=status_code,
|
|
569
575
|
tags=tags,
|
|
@@ -612,6 +618,7 @@ class ErrorAwareRouter(APIRouter):
|
|
|
612
618
|
warn_on_unmapped: bool = True,
|
|
613
619
|
default_client_error_translator: Optional[ErrorTranslator[Any]] = None,
|
|
614
620
|
default_server_error_translator: Optional[ErrorTranslator[Any]] = None,
|
|
621
|
+
exclude_none: bool = False,
|
|
615
622
|
# --- FastAPI ---
|
|
616
623
|
response_model: Annotated[
|
|
617
624
|
Any,
|
|
@@ -957,6 +964,7 @@ class ErrorAwareRouter(APIRouter):
|
|
|
957
964
|
default_on_error=default_on_error,
|
|
958
965
|
default_client_error_translator=default_client_error_translator,
|
|
959
966
|
default_server_error_translator=default_server_error_translator,
|
|
967
|
+
exclude_none=exclude_none,
|
|
960
968
|
response_model=response_model,
|
|
961
969
|
status_code=status_code,
|
|
962
970
|
tags=tags,
|
|
@@ -1002,6 +1010,7 @@ class ErrorAwareRouter(APIRouter):
|
|
|
1002
1010
|
warn_on_unmapped: bool = True,
|
|
1003
1011
|
default_client_error_translator: Optional[ErrorTranslator[Any]] = None,
|
|
1004
1012
|
default_server_error_translator: Optional[ErrorTranslator[Any]] = None,
|
|
1013
|
+
exclude_none: bool = False,
|
|
1005
1014
|
# --- FastAPI ---
|
|
1006
1015
|
response_model: Annotated[
|
|
1007
1016
|
Any,
|
|
@@ -1352,6 +1361,7 @@ class ErrorAwareRouter(APIRouter):
|
|
|
1352
1361
|
default_on_error=default_on_error,
|
|
1353
1362
|
default_client_error_translator=default_client_error_translator,
|
|
1354
1363
|
default_server_error_translator=default_server_error_translator,
|
|
1364
|
+
exclude_none=exclude_none,
|
|
1355
1365
|
response_model=response_model,
|
|
1356
1366
|
status_code=status_code,
|
|
1357
1367
|
tags=tags,
|
|
@@ -1397,6 +1407,7 @@ class ErrorAwareRouter(APIRouter):
|
|
|
1397
1407
|
warn_on_unmapped: bool = True,
|
|
1398
1408
|
default_client_error_translator: Optional[ErrorTranslator[Any]] = None,
|
|
1399
1409
|
default_server_error_translator: Optional[ErrorTranslator[Any]] = None,
|
|
1410
|
+
exclude_none: bool = False,
|
|
1400
1411
|
# --- FastAPI ---
|
|
1401
1412
|
response_model: Annotated[
|
|
1402
1413
|
Any,
|
|
@@ -1747,6 +1758,7 @@ class ErrorAwareRouter(APIRouter):
|
|
|
1747
1758
|
default_on_error=default_on_error,
|
|
1748
1759
|
default_client_error_translator=default_client_error_translator,
|
|
1749
1760
|
default_server_error_translator=default_server_error_translator,
|
|
1761
|
+
exclude_none=exclude_none,
|
|
1750
1762
|
response_model=response_model,
|
|
1751
1763
|
status_code=status_code,
|
|
1752
1764
|
tags=tags,
|
|
@@ -1792,6 +1804,7 @@ class ErrorAwareRouter(APIRouter):
|
|
|
1792
1804
|
warn_on_unmapped: bool = True,
|
|
1793
1805
|
default_client_error_translator: Optional[ErrorTranslator[Any]] = None,
|
|
1794
1806
|
default_server_error_translator: Optional[ErrorTranslator[Any]] = None,
|
|
1807
|
+
exclude_none: bool = False,
|
|
1795
1808
|
# --- FastAPI ---
|
|
1796
1809
|
response_model: Annotated[
|
|
1797
1810
|
Any,
|
|
@@ -2142,6 +2155,7 @@ class ErrorAwareRouter(APIRouter):
|
|
|
2142
2155
|
default_on_error=default_on_error,
|
|
2143
2156
|
default_client_error_translator=default_client_error_translator,
|
|
2144
2157
|
default_server_error_translator=default_server_error_translator,
|
|
2158
|
+
exclude_none=exclude_none,
|
|
2145
2159
|
response_model=response_model,
|
|
2146
2160
|
status_code=status_code,
|
|
2147
2161
|
tags=tags,
|
|
@@ -2187,6 +2201,7 @@ class ErrorAwareRouter(APIRouter):
|
|
|
2187
2201
|
warn_on_unmapped: bool = True,
|
|
2188
2202
|
default_client_error_translator: Optional[ErrorTranslator[Any]] = None,
|
|
2189
2203
|
default_server_error_translator: Optional[ErrorTranslator[Any]] = None,
|
|
2204
|
+
exclude_none: bool = False,
|
|
2190
2205
|
# --- FastAPI ---
|
|
2191
2206
|
response_model: Annotated[
|
|
2192
2207
|
Any,
|
|
@@ -2532,6 +2547,7 @@ class ErrorAwareRouter(APIRouter):
|
|
|
2532
2547
|
default_on_error=default_on_error,
|
|
2533
2548
|
default_client_error_translator=default_client_error_translator,
|
|
2534
2549
|
default_server_error_translator=default_server_error_translator,
|
|
2550
|
+
exclude_none=exclude_none,
|
|
2535
2551
|
response_model=response_model,
|
|
2536
2552
|
status_code=status_code,
|
|
2537
2553
|
tags=tags,
|
|
@@ -63,9 +63,14 @@ def resolve_rule_for_error(
|
|
|
63
63
|
Callable[[Exception], Union[Awaitable[None], None]]
|
|
64
64
|
] = None,
|
|
65
65
|
) -> ResolvedRule:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
status_or_rule: Union[int, Rule, None] = None
|
|
67
|
+
for cls in type(error).mro():
|
|
68
|
+
if not issubclass(cls, Exception):
|
|
69
|
+
continue
|
|
70
|
+
if cls in error_map:
|
|
71
|
+
status_or_rule = error_map[cls]
|
|
72
|
+
break
|
|
73
|
+
if status_or_rule is None:
|
|
69
74
|
raise RuntimeError(f"No rule defined for {type(error).__name__}") from error
|
|
70
75
|
|
|
71
76
|
if isinstance(status_or_rule, int):
|
|
@@ -11,4 +11,4 @@ def compatibility(session, fastapi):
|
|
|
11
11
|
else:
|
|
12
12
|
session.run("uv", "add", f"fastapi=={fastapi}", "--active", external=True)
|
|
13
13
|
|
|
14
|
-
session.run("uv", "run", "--active", "make", "
|
|
14
|
+
session.run("uv", "run", "--active", "make", "test", external=True)
|
|
@@ -7,7 +7,7 @@ sources = ["src"]
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "fastapi-error-map"
|
|
10
|
-
version = "0.9.
|
|
10
|
+
version = "0.9.9"
|
|
11
11
|
description = "Elegant per-endpoint error handling for FastAPI that keeps OpenAPI in sync"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
license = { text = "Apache-2.0" }
|
|
@@ -31,8 +31,8 @@ classifiers = [
|
|
|
31
31
|
"Framework :: FastAPI",
|
|
32
32
|
]
|
|
33
33
|
dependencies = [
|
|
34
|
-
"fastapi
|
|
35
|
-
"orjson
|
|
34
|
+
"fastapi>=0.100,<1.0",
|
|
35
|
+
"orjson>=3.11.4",
|
|
36
36
|
]
|
|
37
37
|
[dependency-groups]
|
|
38
38
|
dev = [
|
|
@@ -44,7 +44,7 @@ dev = [
|
|
|
44
44
|
test = [
|
|
45
45
|
"coverage==7.10.1",
|
|
46
46
|
"httpx==0.28.1",
|
|
47
|
-
"nox==2025.
|
|
47
|
+
"nox==2025.11.12",
|
|
48
48
|
"pytest==8.4.1",
|
|
49
49
|
"pytest-asyncio==1.1.0",
|
|
50
50
|
"pytest-cov==6.2.1",
|
|
@@ -92,6 +92,7 @@ parallel = true
|
|
|
92
92
|
branch = true
|
|
93
93
|
|
|
94
94
|
[tool.mypy]
|
|
95
|
+
python_version = "3.9"
|
|
95
96
|
files = [
|
|
96
97
|
"examples",
|
|
97
98
|
"src",
|
|
@@ -110,9 +111,11 @@ testpaths = ["tests", ]
|
|
|
110
111
|
markers = ["slow", ]
|
|
111
112
|
addopts = "-m 'not slow'"
|
|
112
113
|
asyncio_default_fixture_loop_scope = "function"
|
|
114
|
+
asyncio_mode = "auto"
|
|
113
115
|
|
|
114
116
|
[tool.ruff]
|
|
115
117
|
line-length = 88
|
|
118
|
+
target-version = "py39"
|
|
116
119
|
preview = true # experimental
|
|
117
120
|
|
|
118
121
|
[tool.ruff.format]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from collections.abc import AsyncIterator
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import pytest
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def app() -> FastAPI:
|
|
10
|
+
return FastAPI()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
async def client(app: FastAPI) -> AsyncIterator[httpx.AsyncClient]:
|
|
15
|
+
async with httpx.AsyncClient(
|
|
16
|
+
transport=httpx.ASGITransport(app=app),
|
|
17
|
+
base_url="http://test",
|
|
18
|
+
) as client:
|
|
19
|
+
yield client
|
|
@@ -1,22 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import httpx
|
|
3
2
|
import pytest
|
|
4
3
|
|
|
5
4
|
from examples.main import create_app
|
|
6
|
-
from tests.integration.conftest import AsgiClientFactory
|
|
7
|
-
|
|
8
|
-
if TYPE_CHECKING:
|
|
9
|
-
import httpx
|
|
10
5
|
|
|
11
6
|
|
|
12
7
|
@pytest.mark.asyncio
|
|
13
8
|
async def test_check_stock(
|
|
14
|
-
asgi_client_factory: AsgiClientFactory,
|
|
15
9
|
capsys: pytest.CaptureFixture[str],
|
|
16
10
|
) -> None:
|
|
17
11
|
app = create_app()
|
|
18
12
|
|
|
19
|
-
async with
|
|
13
|
+
async with httpx.AsyncClient(
|
|
14
|
+
transport=httpx.ASGITransport(app=app),
|
|
15
|
+
base_url="http://test",
|
|
16
|
+
) as client:
|
|
20
17
|
authz_err_response: httpx.Response = await client.get("/stock?user_id=0")
|
|
21
18
|
out_of_stock_err_response: httpx.Response = await client.get("/stock?user_id=1")
|
|
22
19
|
openapi_response: httpx.Response = await client.get("/openapi.json")
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import pytest
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
|
|
9
|
+
from fastapi_error_map import ErrorAwareRouter, rule
|
|
10
|
+
from fastapi_error_map.translators import ErrorTranslator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CustomError(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ClientErrorWithOptionalDetails:
|
|
19
|
+
error: str
|
|
20
|
+
details: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OptionalDetailsTranslator(ErrorTranslator[ClientErrorWithOptionalDetails]):
|
|
24
|
+
@property
|
|
25
|
+
def error_response_model_cls(self) -> type[ClientErrorWithOptionalDetails]:
|
|
26
|
+
return ClientErrorWithOptionalDetails
|
|
27
|
+
|
|
28
|
+
def from_error(self, err: Exception) -> ClientErrorWithOptionalDetails:
|
|
29
|
+
return ClientErrorWithOptionalDetails(error=str(err), details=None)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.asyncio
|
|
33
|
+
@pytest.mark.parametrize("method", ["get", "post", "put", "patch", "delete"])
|
|
34
|
+
async def test_exclude_none_false_keeps_null_fields(
|
|
35
|
+
method: str,
|
|
36
|
+
app: FastAPI,
|
|
37
|
+
client: httpx.AsyncClient,
|
|
38
|
+
) -> None:
|
|
39
|
+
router = ErrorAwareRouter()
|
|
40
|
+
translator = OptionalDetailsTranslator()
|
|
41
|
+
|
|
42
|
+
async def failing_endpoint() -> None:
|
|
43
|
+
await asyncio.sleep(0)
|
|
44
|
+
raise CustomError("boom")
|
|
45
|
+
|
|
46
|
+
getattr(router, method)(
|
|
47
|
+
"/fail",
|
|
48
|
+
error_map={CustomError: rule(status=418, translator=translator)},
|
|
49
|
+
exclude_none=False,
|
|
50
|
+
)(failing_endpoint)
|
|
51
|
+
app.include_router(router)
|
|
52
|
+
|
|
53
|
+
response: httpx.Response = await getattr(client, method)("/fail")
|
|
54
|
+
|
|
55
|
+
assert response.status_code == 418
|
|
56
|
+
assert response.headers["content-type"].startswith("application/json")
|
|
57
|
+
assert response.json() == {"error": "boom", "details": None}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.asyncio
|
|
61
|
+
@pytest.mark.parametrize("method", ["get", "post", "put", "patch", "delete"])
|
|
62
|
+
async def test_exclude_none_true_drops_null_fields(
|
|
63
|
+
method: str,
|
|
64
|
+
app: FastAPI,
|
|
65
|
+
client: httpx.AsyncClient,
|
|
66
|
+
) -> None:
|
|
67
|
+
router = ErrorAwareRouter()
|
|
68
|
+
translator = OptionalDetailsTranslator()
|
|
69
|
+
|
|
70
|
+
async def failing_endpoint() -> None:
|
|
71
|
+
await asyncio.sleep(0)
|
|
72
|
+
raise CustomError("boom")
|
|
73
|
+
|
|
74
|
+
getattr(router, method)(
|
|
75
|
+
"/fail",
|
|
76
|
+
error_map={CustomError: rule(status=418, translator=translator)},
|
|
77
|
+
exclude_none=True,
|
|
78
|
+
)(failing_endpoint)
|
|
79
|
+
app.include_router(router)
|
|
80
|
+
|
|
81
|
+
response: httpx.Response = await getattr(client, method)("/fail")
|
|
82
|
+
|
|
83
|
+
assert response.status_code == 418
|
|
84
|
+
assert response.headers["content-type"].startswith("application/json")
|
|
85
|
+
assert response.json() == {"error": "boom"}
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from typing import TYPE_CHECKING
|
|
3
2
|
from unittest.mock import Mock
|
|
4
3
|
|
|
4
|
+
import httpx
|
|
5
5
|
import pytest
|
|
6
6
|
from fastapi import FastAPI
|
|
7
7
|
|
|
8
8
|
from fastapi_error_map import ErrorAwareRouter, rule
|
|
9
|
-
from tests.integration.conftest import AsgiClientFactory
|
|
10
|
-
|
|
11
|
-
if TYPE_CHECKING:
|
|
12
|
-
import httpx
|
|
13
9
|
|
|
14
10
|
|
|
15
11
|
class CustomError(Exception):
|
|
@@ -21,7 +17,7 @@ class CustomError(Exception):
|
|
|
21
17
|
async def test_error_aware_router_routes(
|
|
22
18
|
method: str,
|
|
23
19
|
app: FastAPI,
|
|
24
|
-
|
|
20
|
+
client: httpx.AsyncClient,
|
|
25
21
|
) -> None:
|
|
26
22
|
router = ErrorAwareRouter()
|
|
27
23
|
router_path = "/fail"
|
|
@@ -41,9 +37,8 @@ async def test_error_aware_router_routes(
|
|
|
41
37
|
)(failing_endpoint)
|
|
42
38
|
app.include_router(router)
|
|
43
39
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
openapi_response: httpx.Response = await client.get("/openapi.json")
|
|
40
|
+
response: httpx.Response = await getattr(client, method)(router_path)
|
|
41
|
+
openapi_response: httpx.Response = await client.get("/openapi.json")
|
|
47
42
|
|
|
48
43
|
response_data = response.json()
|
|
49
44
|
assert response.status_code == error_status_code
|