fastapi-error-map 0.9.8__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.8 → 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.8 → fastapi_error_map-0.9.9}/.github/workflows/test-compatibility.yaml +5 -3
- {fastapi_error_map-0.9.8 → 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.8 → fastapi_error_map-0.9.9}/PKG-INFO +14 -5
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/README.md +13 -4
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/fastapi_error_map/error_handling.py +8 -2
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/fastapi_error_map/routing.py +16 -0
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/fastapi_error_map/rules.py +8 -3
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/noxfile.py +1 -1
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/pyproject.toml +5 -2
- fastapi_error_map-0.9.9/tests/integration/conftest.py +19 -0
- {fastapi_error_map-0.9.8 → 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.8 → fastapi_error_map-0.9.9}/tests/integration/test_routing.py +4 -9
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/tests/integration/test_threadpool.py +15 -18
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/tests/unit/test_error_handling.py +5 -0
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/tests/unit/test_rules.py +67 -0
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/uv.lock +33 -7
- fastapi_error_map-0.9.8/Makefile +0 -23
- fastapi_error_map-0.9.8/tests/integration/conftest.py +0 -25
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/.gitignore +0 -0
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/LICENSE +0 -0
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/docs/example-openapi.png +0 -0
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/examples/__init__.py +0 -0
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/examples/errors.py +0 -0
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/examples/main.py +0 -0
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/fastapi_error_map/__init__.py +0 -0
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/fastapi_error_map/openapi.py +0 -0
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/fastapi_error_map/py.typed +0 -0
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/fastapi_error_map/translator_policy.py +0 -0
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/fastapi_error_map/translators.py +0 -0
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/tests/__init__.py +0 -0
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/tests/integration/__init__.py +0 -0
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/tests/unit/__init__.py +0 -0
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/tests/unit/error_stubs.py +0 -0
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/tests/unit/test_openapi.py +0 -0
- {fastapi_error_map-0.9.8 → fastapi_error_map-0.9.9}/tests/unit/test_translators.py +0 -0
- {fastapi_error_map-0.9.8 → 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.8 → 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
|
|
@@ -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
|
|
@@ -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" }
|
|
@@ -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
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from typing import TYPE_CHECKING
|
|
3
2
|
|
|
3
|
+
import httpx
|
|
4
4
|
import pytest
|
|
5
|
-
from fastapi import FastAPI
|
|
5
|
+
from fastapi import APIRouter, FastAPI
|
|
6
6
|
|
|
7
7
|
from fastapi_error_map import ErrorAwareRouter, rule
|
|
8
|
-
from tests.integration.conftest import AsgiClientFactory
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
import httpx
|
|
12
8
|
|
|
13
9
|
|
|
14
10
|
def has_running_loop_in_this_thread() -> bool:
|
|
@@ -20,16 +16,19 @@ def has_running_loop_in_this_thread() -> bool:
|
|
|
20
16
|
|
|
21
17
|
|
|
22
18
|
@pytest.mark.asyncio
|
|
23
|
-
async def
|
|
19
|
+
async def test_fastapi_api_router_sync_handler_runs_in_threadpool(
|
|
24
20
|
app: FastAPI,
|
|
25
|
-
|
|
21
|
+
client: httpx.AsyncClient,
|
|
26
22
|
) -> None:
|
|
27
|
-
|
|
23
|
+
router = APIRouter()
|
|
24
|
+
|
|
25
|
+
@router.get("/")
|
|
28
26
|
def index():
|
|
29
27
|
return {"in_loop": has_running_loop_in_this_thread()}
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
app.include_router(router)
|
|
30
|
+
|
|
31
|
+
index_response: httpx.Response = await client.get("/")
|
|
33
32
|
|
|
34
33
|
assert index_response.status_code == 200
|
|
35
34
|
assert index_response.json() == {"in_loop": False}
|
|
@@ -38,18 +37,17 @@ async def test_fastapi_sync_handler_runs_in_threadpool(
|
|
|
38
37
|
@pytest.mark.asyncio
|
|
39
38
|
async def test_error_aware_router_sync_handler_runs_in_threadpool(
|
|
40
39
|
app: FastAPI,
|
|
41
|
-
|
|
40
|
+
client: httpx.AsyncClient,
|
|
42
41
|
) -> None:
|
|
43
42
|
router = ErrorAwareRouter()
|
|
44
43
|
|
|
45
|
-
@
|
|
44
|
+
@router.get("/", error_map={ValueError: 400})
|
|
46
45
|
def index():
|
|
47
46
|
return {"in_loop": has_running_loop_in_this_thread()}
|
|
48
47
|
|
|
49
48
|
app.include_router(router)
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
index_response: httpx.Response = await client.get("/")
|
|
50
|
+
index_response: httpx.Response = await client.get("/")
|
|
53
51
|
|
|
54
52
|
assert index_response.status_code == 200
|
|
55
53
|
assert index_response.json() == {"in_loop": False}
|
|
@@ -58,7 +56,7 @@ async def test_error_aware_router_sync_handler_runs_in_threadpool(
|
|
|
58
56
|
@pytest.mark.asyncio
|
|
59
57
|
async def test_error_aware_router_sync_on_error_runs_in_threadpool(
|
|
60
58
|
app: FastAPI,
|
|
61
|
-
|
|
59
|
+
client: httpx.AsyncClient,
|
|
62
60
|
) -> None:
|
|
63
61
|
router = ErrorAwareRouter()
|
|
64
62
|
seen: dict[str, bool] = {}
|
|
@@ -72,8 +70,7 @@ async def test_error_aware_router_sync_on_error_runs_in_threadpool(
|
|
|
72
70
|
|
|
73
71
|
app.include_router(router)
|
|
74
72
|
|
|
75
|
-
|
|
76
|
-
index_response: httpx.Response = await client.get("/err")
|
|
73
|
+
index_response: httpx.Response = await client.get("/err")
|
|
77
74
|
|
|
78
75
|
assert index_response.status_code == 400
|
|
79
76
|
assert seen["in_loop"] is False
|
|
@@ -145,6 +145,7 @@ async def test_handles_sync_on_error_from_map() -> None:
|
|
|
145
145
|
default_client_error_translator=DefaultClientErrorTranslator(),
|
|
146
146
|
default_server_error_translator=DefaultServerErrorTranslator(),
|
|
147
147
|
default_on_error=None,
|
|
148
|
+
exclude_none=False,
|
|
148
149
|
)
|
|
149
150
|
|
|
150
151
|
assert sut.status_code == 422
|
|
@@ -163,6 +164,7 @@ async def test_handles_default_on_error_from_defaults() -> None:
|
|
|
163
164
|
default_client_error_translator=DefaultClientErrorTranslator(),
|
|
164
165
|
default_server_error_translator=DefaultServerErrorTranslator(),
|
|
165
166
|
default_on_error=mock_on_error,
|
|
167
|
+
exclude_none=False,
|
|
166
168
|
)
|
|
167
169
|
|
|
168
170
|
assert sut.status_code == 422
|
|
@@ -181,6 +183,7 @@ async def test_handles_async_on_error() -> None:
|
|
|
181
183
|
default_client_error_translator=DefaultClientErrorTranslator(),
|
|
182
184
|
default_server_error_translator=DefaultServerErrorTranslator(),
|
|
183
185
|
default_on_error=None,
|
|
186
|
+
exclude_none=False,
|
|
184
187
|
)
|
|
185
188
|
|
|
186
189
|
assert sut.status_code == 422
|
|
@@ -200,6 +203,7 @@ async def test_handles_async_on_error_partial() -> None:
|
|
|
200
203
|
default_client_error_translator=DefaultClientErrorTranslator(),
|
|
201
204
|
default_server_error_translator=DefaultServerErrorTranslator(),
|
|
202
205
|
default_on_error=None,
|
|
206
|
+
exclude_none=False,
|
|
203
207
|
)
|
|
204
208
|
|
|
205
209
|
assert sut.status_code == 422
|
|
@@ -221,6 +225,7 @@ async def test_handles_sync_on_error_returning_awaitable() -> None:
|
|
|
221
225
|
default_client_error_translator=DefaultClientErrorTranslator(),
|
|
222
226
|
default_server_error_translator=DefaultServerErrorTranslator(),
|
|
223
227
|
default_on_error=None,
|
|
228
|
+
exclude_none=False,
|
|
224
229
|
)
|
|
225
230
|
|
|
226
231
|
assert sut.status_code == 422
|
|
@@ -14,6 +14,22 @@ from tests.unit.translator_stubs import (
|
|
|
14
14
|
ErrorMap = dict[type[Exception], Union[int, Rule]]
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
class ParentError(Exception):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ChildError(ParentError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OtherParentError(Exception):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MultipleInheritanceError(ChildError, OtherParentError):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
17
33
|
@pytest.mark.parametrize(
|
|
18
34
|
"rule",
|
|
19
35
|
[
|
|
@@ -105,3 +121,54 @@ def test_unmapped_error_raises_runtime_error() -> None:
|
|
|
105
121
|
default_server_error_translator=DummyServerErrorTranslator(),
|
|
106
122
|
default_on_error=None,
|
|
107
123
|
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_resolves_child_over_parent() -> None:
|
|
127
|
+
sut = resolve_rule_for_error(
|
|
128
|
+
error=ChildError(),
|
|
129
|
+
error_map={
|
|
130
|
+
ParentError: 400,
|
|
131
|
+
ChildError: 409,
|
|
132
|
+
},
|
|
133
|
+
default_client_error_translator=DummyClientErrorTranslator(),
|
|
134
|
+
default_server_error_translator=DummyServerErrorTranslator(),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
assert sut.status == 409
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_falls_back_to_parent_when_child_missing() -> None:
|
|
141
|
+
sut = resolve_rule_for_error(
|
|
142
|
+
error=ChildError(),
|
|
143
|
+
error_map={
|
|
144
|
+
ParentError: 400,
|
|
145
|
+
},
|
|
146
|
+
default_client_error_translator=DummyClientErrorTranslator(),
|
|
147
|
+
default_server_error_translator=DummyServerErrorTranslator(),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
assert sut.status == 400
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_multiple_inheritance_prefers_first_match_in_mro() -> None:
|
|
154
|
+
sut = resolve_rule_for_error(
|
|
155
|
+
error=MultipleInheritanceError(),
|
|
156
|
+
error_map={
|
|
157
|
+
OtherParentError: 402,
|
|
158
|
+
ChildError: 409,
|
|
159
|
+
},
|
|
160
|
+
default_client_error_translator=DummyClientErrorTranslator(),
|
|
161
|
+
default_server_error_translator=DummyServerErrorTranslator(),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
assert sut.status == 409
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_does_not_resolve_base_exception_subclasses() -> None:
|
|
168
|
+
with pytest.raises(RuntimeError):
|
|
169
|
+
resolve_rule_for_error(
|
|
170
|
+
error=KeyboardInterrupt(), # type: ignore
|
|
171
|
+
error_map={},
|
|
172
|
+
default_client_error_translator=DummyClientErrorTranslator(),
|
|
173
|
+
default_server_error_translator=DummyServerErrorTranslator(),
|
|
174
|
+
)
|
|
@@ -405,7 +405,7 @@ wheels = [
|
|
|
405
405
|
|
|
406
406
|
[[package]]
|
|
407
407
|
name = "fastapi-error-map"
|
|
408
|
-
version = "0.9.
|
|
408
|
+
version = "0.9.9"
|
|
409
409
|
source = { editable = "." }
|
|
410
410
|
dependencies = [
|
|
411
411
|
{ name = "fastapi" },
|
|
@@ -464,7 +464,7 @@ all = [
|
|
|
464
464
|
{ name = "hatch", specifier = "==1.14.1" },
|
|
465
465
|
{ name = "httpx", specifier = "==0.28.1" },
|
|
466
466
|
{ name = "mypy", specifier = "==1.8.0" },
|
|
467
|
-
{ name = "nox", specifier = "==2025.
|
|
467
|
+
{ name = "nox", specifier = "==2025.11.12" },
|
|
468
468
|
{ name = "pre-commit", specifier = "==4.2.0" },
|
|
469
469
|
{ name = "pytest", specifier = "==8.4.1" },
|
|
470
470
|
{ name = "pytest-asyncio", specifier = "==1.1.0" },
|
|
@@ -476,7 +476,7 @@ dev = [
|
|
|
476
476
|
{ name = "coverage", specifier = "==7.10.1" },
|
|
477
477
|
{ name = "httpx", specifier = "==0.28.1" },
|
|
478
478
|
{ name = "mypy", specifier = "==1.8.0" },
|
|
479
|
-
{ name = "nox", specifier = "==2025.
|
|
479
|
+
{ name = "nox", specifier = "==2025.11.12" },
|
|
480
480
|
{ name = "pre-commit", specifier = "==4.2.0" },
|
|
481
481
|
{ name = "pytest", specifier = "==8.4.1" },
|
|
482
482
|
{ name = "pytest-asyncio", specifier = "==1.1.0" },
|
|
@@ -488,7 +488,7 @@ publish = [{ name = "hatch", specifier = "==1.14.1" }]
|
|
|
488
488
|
test = [
|
|
489
489
|
{ name = "coverage", specifier = "==7.10.1" },
|
|
490
490
|
{ name = "httpx", specifier = "==0.28.1" },
|
|
491
|
-
{ name = "nox", specifier = "==2025.
|
|
491
|
+
{ name = "nox", specifier = "==2025.11.12" },
|
|
492
492
|
{ name = "pytest", specifier = "==8.4.1" },
|
|
493
493
|
{ name = "pytest-asyncio", specifier = "==1.1.0" },
|
|
494
494
|
{ name = "pytest-cov", specifier = "==6.2.1" },
|
|
@@ -600,6 +600,30 @@ wheels = [
|
|
|
600
600
|
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
|
601
601
|
]
|
|
602
602
|
|
|
603
|
+
[[package]]
|
|
604
|
+
name = "humanize"
|
|
605
|
+
version = "4.13.0"
|
|
606
|
+
source = { registry = "https://pypi.org/simple" }
|
|
607
|
+
resolution-markers = [
|
|
608
|
+
"python_full_version < '3.10'",
|
|
609
|
+
]
|
|
610
|
+
sdist = { url = "https://files.pythonhosted.org/packages/98/1d/3062fcc89ee05a715c0b9bfe6490c00c576314f27ffee3a704122c6fd259/humanize-4.13.0.tar.gz", hash = "sha256:78f79e68f76f0b04d711c4e55d32bebef5be387148862cb1ef83d2b58e7935a0", size = 81884, upload-time = "2025-08-25T09:39:20.04Z" }
|
|
611
|
+
wheels = [
|
|
612
|
+
{ url = "https://files.pythonhosted.org/packages/1e/c7/316e7ca04d26695ef0635dc81683d628350810eb8e9b2299fc08ba49f366/humanize-4.13.0-py3-none-any.whl", hash = "sha256:b810820b31891813b1673e8fec7f1ed3312061eab2f26e3fa192c393d11ed25f", size = 128869, upload-time = "2025-08-25T09:39:18.54Z" },
|
|
613
|
+
]
|
|
614
|
+
|
|
615
|
+
[[package]]
|
|
616
|
+
name = "humanize"
|
|
617
|
+
version = "4.15.0"
|
|
618
|
+
source = { registry = "https://pypi.org/simple" }
|
|
619
|
+
resolution-markers = [
|
|
620
|
+
"python_full_version >= '3.10'",
|
|
621
|
+
]
|
|
622
|
+
sdist = { url = "https://files.pythonhosted.org/packages/ba/66/a3921783d54be8a6870ac4ccffcd15c4dc0dd7fcce51c6d63b8c63935276/humanize-4.15.0.tar.gz", hash = "sha256:1dd098483eb1c7ee8e32eb2e99ad1910baefa4b75c3aff3a82f4d78688993b10", size = 83599, upload-time = "2025-12-20T20:16:13.19Z" }
|
|
623
|
+
wheels = [
|
|
624
|
+
{ url = "https://files.pythonhosted.org/packages/c5/7b/bca5613a0c3b542420cf92bd5e5fb8ebd5435ce1011a091f66bb7693285e/humanize-4.15.0-py3-none-any.whl", hash = "sha256:b1186eb9f5a9749cd9cb8565aee77919dd7c8d076161cf44d70e59e3301e1769", size = 132203, upload-time = "2025-12-20T20:16:11.67Z" },
|
|
625
|
+
]
|
|
626
|
+
|
|
603
627
|
[[package]]
|
|
604
628
|
name = "hyperlink"
|
|
605
629
|
version = "21.0.0"
|
|
@@ -832,20 +856,22 @@ wheels = [
|
|
|
832
856
|
|
|
833
857
|
[[package]]
|
|
834
858
|
name = "nox"
|
|
835
|
-
version = "2025.
|
|
859
|
+
version = "2025.11.12"
|
|
836
860
|
source = { registry = "https://pypi.org/simple" }
|
|
837
861
|
dependencies = [
|
|
838
862
|
{ name = "argcomplete" },
|
|
839
863
|
{ name = "attrs" },
|
|
840
864
|
{ name = "colorlog" },
|
|
841
865
|
{ name = "dependency-groups" },
|
|
866
|
+
{ name = "humanize", version = "4.13.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
|
867
|
+
{ name = "humanize", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
|
842
868
|
{ name = "packaging" },
|
|
843
869
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
|
844
870
|
{ name = "virtualenv" },
|
|
845
871
|
]
|
|
846
|
-
sdist = { url = "https://files.pythonhosted.org/packages/b4/
|
|
872
|
+
sdist = { url = "https://files.pythonhosted.org/packages/b4/a8/e169497599266d176832e2232c08557ffba97eef87bf8a18f9f918e0c6aa/nox-2025.11.12.tar.gz", hash = "sha256:3d317f9e61f49d6bde39cf2f59695bb4e1722960457eee3ae19dacfe03c07259", size = 4030561, upload-time = "2025-11-12T18:39:03.319Z" }
|
|
847
873
|
wheels = [
|
|
848
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
874
|
+
{ url = "https://files.pythonhosted.org/packages/b9/34/434c594e0125a16b05a7bedaea33e63c90abbfbe47e5729a735a8a8a90ea/nox-2025.11.12-py3-none-any.whl", hash = "sha256:707171f9f63bc685da9d00edd8c2ceec8405b8e38b5fb4e46114a860070ef0ff", size = 74447, upload-time = "2025-11-12T18:39:01.575Z" },
|
|
849
875
|
]
|
|
850
876
|
|
|
851
877
|
[[package]]
|
fastapi_error_map-0.9.8/Makefile
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# Code quality
|
|
2
|
-
.PHONY: code.format code.lint code.test code.cov code.cov.html code.check
|
|
3
|
-
code.format:
|
|
4
|
-
ruff format
|
|
5
|
-
|
|
6
|
-
code.lint: code.format
|
|
7
|
-
ruff check --exit-non-zero-on-fix
|
|
8
|
-
mypy
|
|
9
|
-
|
|
10
|
-
code.test:
|
|
11
|
-
pytest -v
|
|
12
|
-
|
|
13
|
-
code.cov:
|
|
14
|
-
coverage run -m pytest
|
|
15
|
-
coverage combine
|
|
16
|
-
coverage report
|
|
17
|
-
|
|
18
|
-
code.cov.html:
|
|
19
|
-
coverage run -m pytest
|
|
20
|
-
coverage combine
|
|
21
|
-
coverage html
|
|
22
|
-
|
|
23
|
-
code.check: code.lint code.test
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
from collections.abc import AsyncIterator
|
|
2
|
-
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
|
3
|
-
from typing import Callable
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
from fastapi import FastAPI
|
|
7
|
-
from httpx import ASGITransport, AsyncClient
|
|
8
|
-
|
|
9
|
-
AsgiClientFactory = Callable[[FastAPI], AbstractAsyncContextManager[AsyncClient]]
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@pytest.fixture
|
|
13
|
-
def app() -> FastAPI:
|
|
14
|
-
return FastAPI()
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@pytest.fixture
|
|
18
|
-
def asgi_client_factory() -> AsgiClientFactory:
|
|
19
|
-
@asynccontextmanager
|
|
20
|
-
async def _make(app: FastAPI) -> AsyncIterator[AsyncClient]:
|
|
21
|
-
transport = ASGITransport(app=app)
|
|
22
|
-
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
23
|
-
yield client
|
|
24
|
-
|
|
25
|
-
return _make
|
|
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
|