fastapi-deferred-init 0.3.2__tar.gz → 0.3.4__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_deferred_init-0.3.2 → fastapi_deferred_init-0.3.4}/PKG-INFO +4 -4
- {fastapi_deferred_init-0.3.2 → fastapi_deferred_init-0.3.4}/pyproject.toml +30 -18
- {fastapi_deferred_init-0.3.2 → fastapi_deferred_init-0.3.4}/src/fastapi_deferred_init/patch.py +7 -1
- {fastapi_deferred_init-0.3.2 → fastapi_deferred_init-0.3.4}/src/fastapi_deferred_init/routing.py +166 -46
- {fastapi_deferred_init-0.3.2 → fastapi_deferred_init-0.3.4}/tests/data/template.py +4 -4
- {fastapi_deferred_init-0.3.2 → fastapi_deferred_init-0.3.4}/tests/helpers.py +27 -0
- {fastapi_deferred_init-0.3.2 → fastapi_deferred_init-0.3.4}/tests/test_lib.py +8 -17
- fastapi_deferred_init-0.3.4/tests/test_realworld.py +314 -0
- {fastapi_deferred_init-0.3.2 → fastapi_deferred_init-0.3.4}/LICENSE +0 -0
- {fastapi_deferred_init-0.3.2 → fastapi_deferred_init-0.3.4}/README.md +0 -0
- {fastapi_deferred_init-0.3.2 → fastapi_deferred_init-0.3.4}/src/fastapi_deferred_init/__init__.py +0 -0
- {fastapi_deferred_init-0.3.2 → fastapi_deferred_init-0.3.4}/src/fastapi_deferred_init/py.typed +0 -0
- {fastapi_deferred_init-0.3.2 → fastapi_deferred_init-0.3.4}/tests/__init__.py +0 -0
- {fastapi_deferred_init-0.3.2 → fastapi_deferred_init-0.3.4}/tests/conftest.py +0 -0
- {fastapi_deferred_init-0.3.2 → fastapi_deferred_init-0.3.4}/tests/data/__init__.py +0 -0
- {fastapi_deferred_init-0.3.2 → fastapi_deferred_init-0.3.4}/tests/data/gen_code_ast.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fastapi-deferred-init
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Faster FastAPI start-up time for Projects with many nested routers
|
|
5
5
|
Keywords: fastapi,speed,router,startup,optimization
|
|
6
6
|
Author-Email: Jan Vollmer <jan@vllmr.dev>
|
|
@@ -9,16 +9,16 @@ Classifier: Environment :: Plugins
|
|
|
9
9
|
Classifier: Framework :: FastAPI
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
17
17
|
Classifier: Typing :: Typed
|
|
18
18
|
Project-URL: Source, https://github.com/jvllmr/fastapi-deferred-init
|
|
19
19
|
Project-URL: Changelog, https://github.com/jvllmr/fastapi-deferred-init/releases
|
|
20
|
-
Requires-Python: >=3.
|
|
21
|
-
Requires-Dist: fastapi>=0.
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: fastapi>=0.162.0
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
|
|
24
24
|
# fastapi-deferred-init
|
|
@@ -12,24 +12,24 @@ authors = [
|
|
|
12
12
|
{ name = "Jan Vollmer", email = "jan@vllmr.dev" },
|
|
13
13
|
]
|
|
14
14
|
dependencies = [
|
|
15
|
-
"fastapi>=0.
|
|
15
|
+
"fastapi>=0.162.0",
|
|
16
16
|
]
|
|
17
|
-
requires-python = ">=3.
|
|
17
|
+
requires-python = ">=3.11"
|
|
18
18
|
readme = "README.md"
|
|
19
19
|
classifiers = [
|
|
20
20
|
"Environment :: Plugins",
|
|
21
21
|
"Framework :: FastAPI",
|
|
22
22
|
"Intended Audience :: Developers",
|
|
23
23
|
"License :: OSI Approved :: MIT License",
|
|
24
|
-
"Programming Language :: Python :: 3.10",
|
|
25
24
|
"Programming Language :: Python :: 3.11",
|
|
26
25
|
"Programming Language :: Python :: 3.12",
|
|
27
26
|
"Programming Language :: Python :: 3.13",
|
|
28
27
|
"Programming Language :: Python :: 3.14",
|
|
28
|
+
"Programming Language :: Python :: 3.15",
|
|
29
29
|
"Typing :: Typed",
|
|
30
30
|
]
|
|
31
31
|
dynamic = []
|
|
32
|
-
version = "0.3.
|
|
32
|
+
version = "0.3.4"
|
|
33
33
|
|
|
34
34
|
[project.license]
|
|
35
35
|
text = "MIT"
|
|
@@ -45,9 +45,6 @@ requires = [
|
|
|
45
45
|
build-backend = "pdm.backend"
|
|
46
46
|
|
|
47
47
|
[tool.pdm]
|
|
48
|
-
ignore_package_warnings = [
|
|
49
|
-
"pre-commit",
|
|
50
|
-
]
|
|
51
48
|
plugins = [
|
|
52
49
|
"sync-pre-commit-lock",
|
|
53
50
|
]
|
|
@@ -69,7 +66,7 @@ FULL_TEST = "true"
|
|
|
69
66
|
|
|
70
67
|
[tool.pdm.scripts.post_install]
|
|
71
68
|
composite = [
|
|
72
|
-
"
|
|
69
|
+
"prek install",
|
|
73
70
|
"pdm install --plugins --frozen-lock",
|
|
74
71
|
"pip install ./fastapi",
|
|
75
72
|
]
|
|
@@ -80,18 +77,33 @@ source = "scm"
|
|
|
80
77
|
[tool.isort]
|
|
81
78
|
profile = "black"
|
|
82
79
|
|
|
83
|
-
[tool.
|
|
84
|
-
|
|
80
|
+
[tool.ruff]
|
|
81
|
+
target-version = "py311"
|
|
82
|
+
|
|
83
|
+
[tool.ruff.lint]
|
|
84
|
+
extend-select = [
|
|
85
|
+
"F",
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
[tool.ty.environment]
|
|
89
|
+
python-version = "3.11"
|
|
90
|
+
python = ".venv"
|
|
91
|
+
|
|
92
|
+
[tool.ty.src]
|
|
93
|
+
include = [
|
|
94
|
+
"src",
|
|
95
|
+
"tests",
|
|
96
|
+
]
|
|
85
97
|
|
|
86
98
|
[dependency-groups]
|
|
87
99
|
dev = [
|
|
88
|
-
"pytest>=9.1.1",
|
|
89
|
-
"ruff>=0.
|
|
90
|
-
"pre-commit>=4.3.0",
|
|
91
|
-
"mypy>=2.1.0",
|
|
100
|
+
"pytest>=9.1.1,<9.2",
|
|
101
|
+
"ruff>=0.17.1",
|
|
92
102
|
"tuna>=0.5.15",
|
|
93
|
-
"pytest-benchmark>=5.
|
|
94
|
-
"pip>=26.1
|
|
95
|
-
"inline-snapshot>=0.
|
|
96
|
-
"httpx2>=2.
|
|
103
|
+
"pytest-benchmark>=5.3.0",
|
|
104
|
+
"pip>=26.2.1",
|
|
105
|
+
"inline-snapshot>=0.36.1",
|
|
106
|
+
"httpx2>=2.14.0",
|
|
107
|
+
"prek>=0.5.9",
|
|
108
|
+
"ty>=0.0.95",
|
|
97
109
|
]
|
{fastapi_deferred_init-0.3.2 → fastapi_deferred_init-0.3.4}/src/fastapi_deferred_init/patch.py
RENAMED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
from .routing import
|
|
1
|
+
from .routing import (
|
|
2
|
+
DeferringAPIRoute,
|
|
3
|
+
DeferringAPIRouter,
|
|
4
|
+
_populate_api_route_state,
|
|
5
|
+
apply_include_patch,
|
|
6
|
+
)
|
|
2
7
|
|
|
3
8
|
|
|
4
9
|
def apply_patch() -> None:
|
|
@@ -8,6 +13,7 @@ def apply_patch() -> None:
|
|
|
8
13
|
routing._populate_api_route_state = _populate_api_route_state # ty:ignore[invalid-assignment]
|
|
9
14
|
routing.APIRoute = DeferringAPIRoute # ty:ignore[invalid-assignment]
|
|
10
15
|
routing.APIRouter = DeferringAPIRouter # ty:ignore[invalid-assignment]
|
|
16
|
+
apply_include_patch(routing)
|
|
11
17
|
|
|
12
18
|
import fastapi
|
|
13
19
|
|
{fastapi_deferred_init-0.3.2 → fastapi_deferred_init-0.3.4}/src/fastapi_deferred_init/routing.py
RENAMED
|
@@ -1,36 +1,37 @@
|
|
|
1
|
-
from fastapi.dependencies.models import Dependant
|
|
2
1
|
import inspect
|
|
2
|
+
from collections.abc import Callable, Sequence
|
|
3
3
|
from enum import Enum, IntEnum
|
|
4
4
|
from functools import cached_property
|
|
5
|
-
from typing import Any,
|
|
6
|
-
from collections.abc import Sequence
|
|
7
|
-
|
|
5
|
+
from typing import Any, cast
|
|
8
6
|
|
|
9
|
-
from fastapi.sse import (
|
|
10
|
-
EventSourceResponse,
|
|
11
|
-
ServerSentEvent,
|
|
12
|
-
)
|
|
13
|
-
from fastapi.routing import _is_async_gen_callable, _is_gen_callable
|
|
14
|
-
from fastapi import params, routing # type: ignore[attr-defined]
|
|
15
7
|
from fastapi._compat import ModelField, lenient_issubclass
|
|
16
8
|
from fastapi.datastructures import Default, DefaultPlaceholder
|
|
9
|
+
from fastapi.dependencies.models import Dependant
|
|
17
10
|
from fastapi.dependencies.utils import (
|
|
18
|
-
_should_embed_body_fields,
|
|
19
11
|
_get_body_field,
|
|
12
|
+
_get_flat_body_params,
|
|
13
|
+
_should_embed_body_fields,
|
|
20
14
|
get_dependant,
|
|
21
15
|
get_parameterless_sub_dependant,
|
|
22
|
-
get_typed_return_annotation,
|
|
23
16
|
get_stream_item_type,
|
|
24
|
-
|
|
17
|
+
get_typed_return_annotation,
|
|
25
18
|
)
|
|
26
19
|
from fastapi.responses import JSONResponse, Response
|
|
20
|
+
from fastapi.routing import _is_async_gen_callable, _is_gen_callable
|
|
21
|
+
from fastapi.sse import (
|
|
22
|
+
EventSourceResponse,
|
|
23
|
+
ServerSentEvent,
|
|
24
|
+
)
|
|
27
25
|
from fastapi.types import IncEx
|
|
28
26
|
from fastapi.utils import (
|
|
29
27
|
create_model_field,
|
|
30
28
|
generate_unique_id,
|
|
29
|
+
get_value_or_default,
|
|
31
30
|
is_body_allowed_for_status_code,
|
|
32
31
|
)
|
|
33
32
|
|
|
33
|
+
from fastapi import params, routing # type: ignore[attr-defined]
|
|
34
|
+
|
|
34
35
|
|
|
35
36
|
def _add_cache_attribute(
|
|
36
37
|
instance: routing._APIRouteLike, name: str, func: Callable[[Any], Any]
|
|
@@ -54,7 +55,7 @@ def _populate_api_route_state(
|
|
|
54
55
|
path: str,
|
|
55
56
|
endpoint: Callable[..., Any],
|
|
56
57
|
*,
|
|
57
|
-
response_model: Any = Default(None),
|
|
58
|
+
response_model: Any = Default(None), # noqa: B008
|
|
58
59
|
status_code: int | None = None,
|
|
59
60
|
tags: list[str | Enum] | None = None,
|
|
60
61
|
dependencies: Sequence[params.Depends] | None = None,
|
|
@@ -73,40 +74,20 @@ def _populate_api_route_state(
|
|
|
73
74
|
response_model_exclude_defaults: bool = False,
|
|
74
75
|
response_model_exclude_none: bool = False,
|
|
75
76
|
include_in_schema: bool = True,
|
|
76
|
-
response_class: type[Response] | DefaultPlaceholder = Default(JSONResponse),
|
|
77
|
+
response_class: type[Response] | DefaultPlaceholder = Default(JSONResponse), # noqa: B008
|
|
77
78
|
dependency_overrides_provider: Any | None = None,
|
|
78
79
|
callbacks: list[routing.BaseRoute] | None = None,
|
|
79
80
|
openapi_extra: dict[str, Any] | None = None,
|
|
80
|
-
generate_unique_id_function: Callable[[Any], str] | DefaultPlaceholder = Default(
|
|
81
|
+
generate_unique_id_function: Callable[[Any], str] | DefaultPlaceholder = Default( # noqa: B008
|
|
81
82
|
generate_unique_id
|
|
82
83
|
),
|
|
83
|
-
strict_content_type: bool | DefaultPlaceholder = Default(True),
|
|
84
|
+
strict_content_type: bool | DefaultPlaceholder = Default(True), # noqa: B008
|
|
85
|
+
stream_item_type: Any | None = None,
|
|
84
86
|
) -> None:
|
|
85
87
|
route.path = path
|
|
86
88
|
route.endpoint = endpoint
|
|
87
|
-
route.stream_item_type =
|
|
88
|
-
|
|
89
|
-
return_annotation = get_typed_return_annotation(endpoint)
|
|
90
|
-
if lenient_issubclass(return_annotation, Response):
|
|
91
|
-
response_model = None
|
|
92
|
-
else:
|
|
93
|
-
stream_item = get_stream_item_type(return_annotation)
|
|
94
|
-
if stream_item is not None:
|
|
95
|
-
# Extract item type for JSONL or SSE streaming when
|
|
96
|
-
# response_class is DefaultPlaceholder (JSONL) or
|
|
97
|
-
# EventSourceResponse (SSE).
|
|
98
|
-
# ServerSentEvent is excluded: it's a transport
|
|
99
|
-
# wrapper, not a data model, so it shouldn't feed
|
|
100
|
-
# into validation or OpenAPI schema generation.
|
|
101
|
-
if (
|
|
102
|
-
isinstance(response_class, DefaultPlaceholder)
|
|
103
|
-
or lenient_issubclass(response_class, EventSourceResponse)
|
|
104
|
-
) and not lenient_issubclass(stream_item, ServerSentEvent):
|
|
105
|
-
route.stream_item_type = stream_item
|
|
106
|
-
response_model = None
|
|
107
|
-
else:
|
|
108
|
-
response_model = return_annotation
|
|
109
|
-
route.response_model = response_model
|
|
89
|
+
route.stream_item_type = stream_item_type
|
|
90
|
+
|
|
110
91
|
route.summary = summary
|
|
111
92
|
route.response_description = response_description
|
|
112
93
|
route.deprecated = deprecated
|
|
@@ -144,13 +125,18 @@ def _populate_api_route_state(
|
|
|
144
125
|
if isinstance(status_code, IntEnum):
|
|
145
126
|
status_code = int(status_code)
|
|
146
127
|
route.status_code = status_code
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
128
|
+
|
|
129
|
+
# Store the raw response_model / stream_item_type parameters so the
|
|
130
|
+
# deferred cached_properties can access them per-instance (not via
|
|
131
|
+
# closure, which would be shared across all instances of the class).
|
|
132
|
+
route._raw_response_model = response_model # ty: ignore[unresolved-attribute]
|
|
133
|
+
route._raw_stream_item_type = stream_item_type # ty: ignore[unresolved-attribute]
|
|
151
134
|
|
|
152
135
|
def _response_field(self):
|
|
153
136
|
if self.response_model:
|
|
137
|
+
assert is_body_allowed_for_status_code(self.status_code), (
|
|
138
|
+
f"Status code {self.status_code} must not have a response body"
|
|
139
|
+
)
|
|
154
140
|
response_name = "Response_" + self.unique_id
|
|
155
141
|
return create_model_field(
|
|
156
142
|
name=response_name,
|
|
@@ -180,8 +166,8 @@ def _populate_api_route_state(
|
|
|
180
166
|
# truncate description text to the content preceding the first "form feed"
|
|
181
167
|
route.description = route.description.split("\f")[0].strip()
|
|
182
168
|
|
|
183
|
-
def _response_fields(self) -> dict[
|
|
184
|
-
response_fields: dict[
|
|
169
|
+
def _response_fields(self) -> dict[int | str, ModelField]:
|
|
170
|
+
response_fields: dict[int | str, ModelField] = {}
|
|
185
171
|
for additional_status_code, response in self.responses.items():
|
|
186
172
|
assert isinstance(response, dict), "An additional response must be a dict"
|
|
187
173
|
model = response.get("model")
|
|
@@ -254,6 +240,50 @@ def _populate_api_route_state(
|
|
|
254
240
|
|
|
255
241
|
_add_cache_attribute(route, "is_json_stream", _is_json_stream)
|
|
256
242
|
|
|
243
|
+
def _resolved_response_model_stream_item(self):
|
|
244
|
+
resolved_model = self._raw_response_model
|
|
245
|
+
resolved_item_type = self._raw_stream_item_type
|
|
246
|
+
if isinstance(resolved_model, DefaultPlaceholder):
|
|
247
|
+
return_annotation = get_typed_return_annotation(self.endpoint)
|
|
248
|
+
if lenient_issubclass(return_annotation, Response):
|
|
249
|
+
resolved_model = None
|
|
250
|
+
else:
|
|
251
|
+
stream_item = get_stream_item_type(return_annotation)
|
|
252
|
+
if stream_item is not None and self.is_generator:
|
|
253
|
+
# Extract item type for JSONL or SSE streaming for
|
|
254
|
+
# generator endpoints when response_class is
|
|
255
|
+
# DefaultPlaceholder (JSONL) or EventSourceResponse (SSE).
|
|
256
|
+
# ServerSentEvent is excluded: it's a transport
|
|
257
|
+
# wrapper, not a data model, so it shouldn't feed
|
|
258
|
+
# into validation or OpenAPI schema generation.
|
|
259
|
+
if (
|
|
260
|
+
isinstance(self.response_class, DefaultPlaceholder)
|
|
261
|
+
or lenient_issubclass(self.response_class, EventSourceResponse)
|
|
262
|
+
) and not lenient_issubclass(stream_item, ServerSentEvent):
|
|
263
|
+
resolved_item_type = stream_item
|
|
264
|
+
resolved_model = None
|
|
265
|
+
else:
|
|
266
|
+
resolved_model = return_annotation
|
|
267
|
+
|
|
268
|
+
return resolved_model, resolved_item_type
|
|
269
|
+
|
|
270
|
+
_add_cache_attribute(
|
|
271
|
+
route,
|
|
272
|
+
"_resolved_response_model_stream_item",
|
|
273
|
+
_resolved_response_model_stream_item,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def _response_model(self):
|
|
277
|
+
|
|
278
|
+
return self._resolved_response_model_stream_item[0]
|
|
279
|
+
|
|
280
|
+
_add_cache_attribute(route, "response_model", _response_model)
|
|
281
|
+
|
|
282
|
+
def _stream_item_type(self):
|
|
283
|
+
return self._resolved_response_model_stream_item[1]
|
|
284
|
+
|
|
285
|
+
_add_cache_attribute(route, "stream_item_type", _stream_item_type)
|
|
286
|
+
|
|
257
287
|
|
|
258
288
|
class DeferringAPIRoute(routing.APIRoute):
|
|
259
289
|
def __init__(
|
|
@@ -273,3 +303,93 @@ class DeferringAPIRouter(routing.APIRouter):
|
|
|
273
303
|
if "route_class" not in kwargs:
|
|
274
304
|
kwargs["route_class"] = DeferringAPIRoute
|
|
275
305
|
super().__init__(*args, **kwargs)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _from_api_route_deferred(
|
|
309
|
+
cls: type, *, original_route: Any, include_context: Any
|
|
310
|
+
) -> Any:
|
|
311
|
+
"""Deferring replacement for fastapi>=0.162's `_EffectiveRouteContext.from_api_route`.
|
|
312
|
+
|
|
313
|
+
Upstream eagerly builds the ASGI app for every route of an included router
|
|
314
|
+
(`context.app = request_response(original_route.get_route_handler())`),
|
|
315
|
+
which forces the deferred dependant/body_field/response_model to be
|
|
316
|
+
computed at include time. Defer `app` to first access instead, like
|
|
317
|
+
DeferringAPIRoute does for direct routes.
|
|
318
|
+
"""
|
|
319
|
+
route = cast(routing._APIRouteLike, original_route)
|
|
320
|
+
context = cls(original_route=original_route)
|
|
321
|
+
_populate_api_route_state(
|
|
322
|
+
cast(routing._APIRouteLike, context),
|
|
323
|
+
include_context.path_for(original_route),
|
|
324
|
+
route.endpoint,
|
|
325
|
+
response_model=route.response_model,
|
|
326
|
+
status_code=route.status_code,
|
|
327
|
+
tags=[*include_context.tags, *route.tags],
|
|
328
|
+
dependencies=[*include_context.dependencies, *route.dependencies],
|
|
329
|
+
summary=route.summary,
|
|
330
|
+
description=route.description,
|
|
331
|
+
response_description=route.response_description,
|
|
332
|
+
responses={**include_context.responses, **route.responses},
|
|
333
|
+
deprecated=route.deprecated or include_context.deprecated,
|
|
334
|
+
methods=route.methods,
|
|
335
|
+
operation_id=route.operation_id,
|
|
336
|
+
response_model_include=route.response_model_include,
|
|
337
|
+
response_model_exclude=route.response_model_exclude,
|
|
338
|
+
response_model_by_alias=route.response_model_by_alias,
|
|
339
|
+
response_model_exclude_unset=route.response_model_exclude_unset,
|
|
340
|
+
response_model_exclude_defaults=route.response_model_exclude_defaults,
|
|
341
|
+
response_model_exclude_none=route.response_model_exclude_none,
|
|
342
|
+
include_in_schema=route.include_in_schema and include_context.include_in_schema,
|
|
343
|
+
response_class=get_value_or_default(
|
|
344
|
+
route.response_class,
|
|
345
|
+
include_context.included_router.default_response_class,
|
|
346
|
+
include_context.default_response_class,
|
|
347
|
+
),
|
|
348
|
+
name=route.name,
|
|
349
|
+
dependency_overrides_provider=include_context.dependency_overrides_provider,
|
|
350
|
+
callbacks=[*include_context.callbacks, *(route.callbacks or [])],
|
|
351
|
+
openapi_extra=route.openapi_extra,
|
|
352
|
+
generate_unique_id_function=get_value_or_default(
|
|
353
|
+
route.generate_unique_id_function,
|
|
354
|
+
include_context.included_router.generate_unique_id_function,
|
|
355
|
+
include_context.generate_unique_id_function,
|
|
356
|
+
),
|
|
357
|
+
strict_content_type=get_value_or_default(
|
|
358
|
+
route.strict_content_type,
|
|
359
|
+
include_context.included_router.strict_content_type,
|
|
360
|
+
include_context.strict_content_type,
|
|
361
|
+
),
|
|
362
|
+
stream_item_type=route.stream_item_type,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
def _app(self: Any) -> Any:
|
|
366
|
+
# Build the handler lazily with the effective context active, mirroring
|
|
367
|
+
# upstream's ContextVar usage so integrations wrapping dependant.call
|
|
368
|
+
# while constructing the handler still see the context.
|
|
369
|
+
context_var = getattr(routing, "_effective_route_context_var", None)
|
|
370
|
+
if context_var is not None:
|
|
371
|
+
token = context_var.set(self)
|
|
372
|
+
try:
|
|
373
|
+
handler = self.original_route.get_route_handler()
|
|
374
|
+
finally:
|
|
375
|
+
context_var.reset(token)
|
|
376
|
+
else:
|
|
377
|
+
handler = self.original_route.get_route_handler()
|
|
378
|
+
return routing.request_response(handler)
|
|
379
|
+
|
|
380
|
+
_add_cache_attribute(context, "app", _app)
|
|
381
|
+
return context
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def apply_include_patch(routing_module: Any) -> bool:
|
|
385
|
+
"""Patch fastapi>=0.162's include machinery to defer app construction.
|
|
386
|
+
|
|
387
|
+
Returns True when the patch was applied, i.e. the running fastapi version
|
|
388
|
+
has `_EffectiveRouteContext` (older versions re-run APIRoute.__init__ on
|
|
389
|
+
include, which the route-class patch already covers).
|
|
390
|
+
"""
|
|
391
|
+
effective_route_context = getattr(routing_module, "_EffectiveRouteContext", None)
|
|
392
|
+
if effective_route_context is None:
|
|
393
|
+
return False
|
|
394
|
+
effective_route_context.from_api_route = classmethod(_from_api_route_deferred)
|
|
395
|
+
return True
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from fastapi import APIRouter, Depends
|
|
2
2
|
|
|
3
|
-
dependency0 = lambda: 1
|
|
3
|
+
dependency0 = lambda: 1
|
|
4
4
|
|
|
5
|
-
dependency = lambda: 0
|
|
5
|
+
dependency = lambda: 0
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def dependency1(sub_dependant: int = Depends(dependency0)):
|
|
@@ -13,7 +13,7 @@ router1 = APIRouter(prefix="/prefix1")
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@router1.get("/get1")
|
|
16
|
-
def get1(dependency=Depends(dependency)):
|
|
16
|
+
def get1(dependency=Depends(dependency)): # noqa: B008
|
|
17
17
|
return {"1": 1}
|
|
18
18
|
|
|
19
19
|
|
|
@@ -21,7 +21,7 @@ router2 = APIRouter(prefix="/prefix2")
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
@router2.get("/get2")
|
|
24
|
-
def get2(dependency=Depends(dependency)):
|
|
24
|
+
def get2(dependency=Depends(dependency)): # noqa: B008
|
|
25
25
|
return {"2": 2}
|
|
26
26
|
|
|
27
27
|
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import importlib
|
|
4
|
+
import importlib.util
|
|
4
5
|
import sys
|
|
5
6
|
from contextlib import contextmanager
|
|
6
7
|
from cProfile import Profile
|
|
7
8
|
from pstats import SortKey, Stats
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from fastapi_deferred_init.routing import (
|
|
12
|
+
DeferringAPIRoute,
|
|
13
|
+
DeferringAPIRouter,
|
|
14
|
+
_from_api_route_deferred,
|
|
15
|
+
_populate_api_route_state,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
import pytest
|
|
8
20
|
|
|
9
21
|
|
|
10
22
|
@contextmanager
|
|
@@ -26,6 +38,21 @@ def load_code():
|
|
|
26
38
|
def import_via_file_path(module_name: str, file_path: str):
|
|
27
39
|
# https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
|
|
28
40
|
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
|
41
|
+
assert spec is not None and spec.loader is not None
|
|
29
42
|
module = importlib.util.module_from_spec(spec)
|
|
30
43
|
sys.modules[module_name] = module
|
|
31
44
|
spec.loader.exec_module(module)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def apply_monkeypatch(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
48
|
+
monkeypatch.setattr(
|
|
49
|
+
"fastapi.routing._populate_api_route_state", _populate_api_route_state
|
|
50
|
+
)
|
|
51
|
+
monkeypatch.setattr("fastapi.routing.APIRoute", DeferringAPIRoute)
|
|
52
|
+
monkeypatch.setattr("fastapi.routing.APIRouter", DeferringAPIRouter)
|
|
53
|
+
monkeypatch.setattr("fastapi.APIRouter", DeferringAPIRouter)
|
|
54
|
+
monkeypatch.setattr(
|
|
55
|
+
"fastapi.routing._EffectiveRouteContext.from_api_route",
|
|
56
|
+
classmethod(_from_api_route_deferred),
|
|
57
|
+
raising=False,
|
|
58
|
+
)
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
|
+
from fastapi.testclient import TestClient
|
|
4
5
|
from pydantic import BaseModel
|
|
5
6
|
|
|
6
|
-
from fastapi import FastAPI,
|
|
7
|
-
from fastapi import routing
|
|
8
|
-
from fastapi.testclient import TestClient
|
|
7
|
+
from fastapi import APIRouter, FastAPI, routing
|
|
9
8
|
from fastapi_deferred_init.routing import (
|
|
10
|
-
_populate_api_route_state,
|
|
11
9
|
DeferringAPIRoute,
|
|
12
10
|
DeferringAPIRouter,
|
|
13
11
|
)
|
|
14
12
|
|
|
15
13
|
from .data.gen_code_ast import create_code
|
|
16
|
-
from .helpers import import_via_file_path, load_code
|
|
14
|
+
from .helpers import apply_monkeypatch, import_via_file_path, load_code
|
|
17
15
|
|
|
18
16
|
|
|
19
17
|
def skip_test(use_lib: bool):
|
|
@@ -21,15 +19,6 @@ def skip_test(use_lib: bool):
|
|
|
21
19
|
pytest.skip()
|
|
22
20
|
|
|
23
21
|
|
|
24
|
-
def apply_monkeypatch(monkeypatch):
|
|
25
|
-
monkeypatch.setattr(
|
|
26
|
-
"fastapi.routing._populate_api_route_state", _populate_api_route_state
|
|
27
|
-
)
|
|
28
|
-
monkeypatch.setattr("fastapi.routing.APIRoute", DeferringAPIRoute)
|
|
29
|
-
monkeypatch.setattr("fastapi.routing.APIRouter", DeferringAPIRouter)
|
|
30
|
-
monkeypatch.setattr("fastapi.APIRouter", DeferringAPIRouter)
|
|
31
|
-
|
|
32
|
-
|
|
33
22
|
@pytest.mark.parametrize(["use_lib"], [(True,), (False,)])
|
|
34
23
|
def test_basic(use_lib: bool, benchmark, monkeypatch):
|
|
35
24
|
skip_test(use_lib=use_lib)
|
|
@@ -107,10 +96,10 @@ def test_fastapi_openapi_schema(monkeypatch):
|
|
|
107
96
|
"fastapi/tests/test_openapi_examples.py",
|
|
108
97
|
)
|
|
109
98
|
|
|
110
|
-
from fastapi_clone.tests.test_additional_properties import ( #
|
|
99
|
+
from fastapi_clone.tests.test_additional_properties import ( # type: ignore
|
|
111
100
|
test_openapi_schema as fastapi_test_openapi_schema_additional,
|
|
112
101
|
)
|
|
113
|
-
from fastapi_clone.tests.test_openapi_examples import ( #
|
|
102
|
+
from fastapi_clone.tests.test_openapi_examples import ( # type: ignore
|
|
114
103
|
test_openapi_schema as fastapi_test_openapi_schema,
|
|
115
104
|
)
|
|
116
105
|
|
|
@@ -125,9 +114,11 @@ def test_fastapi_sse(monkeypatch):
|
|
|
125
114
|
"fastapi/tests/test_sse.py",
|
|
126
115
|
)
|
|
127
116
|
|
|
117
|
+
from fastapi_clone.tests.test_sse import ( # ty: ignore[unresolved-import]
|
|
118
|
+
client_fixture as fastapi_client_fixture,
|
|
119
|
+
)
|
|
128
120
|
from fastapi_clone.tests.test_sse import ( # type: ignore
|
|
129
121
|
test_raw_data_sent_without_json_encoding as fastapi_test_raw_data_sent_without_json_encoding,
|
|
130
|
-
client_fixture as fastapi_client_fixture,
|
|
131
122
|
)
|
|
132
123
|
|
|
133
124
|
for client in fastapi_client_fixture.__wrapped__():
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Real-world usage tests for fastapi-deferred-init.
|
|
2
|
+
|
|
3
|
+
These tests mimic common patterns found in production FastAPI apps
|
|
4
|
+
that go beyond what the existing tests cover.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import typing as t
|
|
8
|
+
|
|
9
|
+
from fastapi.responses import JSONResponse, PlainTextResponse
|
|
10
|
+
from fastapi.testclient import TestClient
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from fastapi import Depends, FastAPI, Header, HTTPException, Query, routing, status
|
|
14
|
+
|
|
15
|
+
from .helpers import apply_monkeypatch
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class User(BaseModel):
|
|
19
|
+
id: int
|
|
20
|
+
name: str
|
|
21
|
+
email: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Item(BaseModel):
|
|
25
|
+
id: int
|
|
26
|
+
title: str
|
|
27
|
+
owner_id: int
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _make_app():
|
|
31
|
+
"""Build a realistic app. Route creation order matters: router routes
|
|
32
|
+
are created before app-level routes because decorators run top-to-bottom."""
|
|
33
|
+
|
|
34
|
+
def get_current_user() -> str:
|
|
35
|
+
return "fake-token"
|
|
36
|
+
|
|
37
|
+
def get_db() -> str:
|
|
38
|
+
return "fake-db-conn"
|
|
39
|
+
|
|
40
|
+
# --- sub-router: items (created FIRST, so its closures get installed on the class) ---
|
|
41
|
+
items_router = routing.APIRouter(prefix="/items", tags=["items"])
|
|
42
|
+
|
|
43
|
+
@items_router.get("/{item_id}", response_model=Item)
|
|
44
|
+
def read_item(item_id: int, db: t.Annotated[str, Depends(get_db)]):
|
|
45
|
+
return Item(id=item_id, title=f"Item {item_id}", owner_id=1)
|
|
46
|
+
|
|
47
|
+
@items_router.post("", response_model=Item, status_code=status.HTTP_201_CREATED)
|
|
48
|
+
def create_item(item: Item, user: t.Annotated[str, Depends(get_current_user)]):
|
|
49
|
+
return item
|
|
50
|
+
|
|
51
|
+
# --- sub-router: users ---
|
|
52
|
+
users_router = routing.APIRouter(prefix="/users", tags=["users"])
|
|
53
|
+
|
|
54
|
+
@users_router.get("/{user_id}", response_model=User)
|
|
55
|
+
def read_user(user_id: int):
|
|
56
|
+
if user_id == 999:
|
|
57
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
58
|
+
return User(
|
|
59
|
+
id=user_id, name=f"User {user_id}", email=f"user{user_id}@example.com"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# --- v1 router ---
|
|
63
|
+
v1_router = routing.APIRouter(prefix="/v1")
|
|
64
|
+
v1_router.include_router(items_router)
|
|
65
|
+
v1_router.include_router(users_router, prefix="/u", tags=["v1-users"])
|
|
66
|
+
|
|
67
|
+
# --- app (created LAST) ---
|
|
68
|
+
app = FastAPI(title="Real World App")
|
|
69
|
+
|
|
70
|
+
@app.get("/", response_class=PlainTextResponse)
|
|
71
|
+
def root():
|
|
72
|
+
return "Hello World"
|
|
73
|
+
|
|
74
|
+
@app.get("/health")
|
|
75
|
+
def health():
|
|
76
|
+
return {"status": "ok"}
|
|
77
|
+
|
|
78
|
+
app.include_router(v1_router)
|
|
79
|
+
return app
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_nested_routers_with_response_model_and_dependencies(monkeypatch):
|
|
83
|
+
apply_monkeypatch(monkeypatch)
|
|
84
|
+
app = _make_app()
|
|
85
|
+
client = TestClient(app)
|
|
86
|
+
|
|
87
|
+
# Direct app routes
|
|
88
|
+
resp = client.get("/")
|
|
89
|
+
assert resp.status_code == 200, resp.text
|
|
90
|
+
assert resp.text == "Hello World"
|
|
91
|
+
|
|
92
|
+
resp = client.get("/health")
|
|
93
|
+
assert resp.status_code == 200
|
|
94
|
+
assert resp.json() == {"status": "ok"}
|
|
95
|
+
|
|
96
|
+
# Nested router routes
|
|
97
|
+
resp = client.get("/v1/items/42")
|
|
98
|
+
assert resp.status_code == 200, resp.text
|
|
99
|
+
assert resp.json() == {"id": 42, "title": "Item 42", "owner_id": 1}
|
|
100
|
+
|
|
101
|
+
resp = client.post("/v1/items", json={"id": 1, "title": "Test", "owner_id": 2})
|
|
102
|
+
assert resp.status_code == 201, resp.text
|
|
103
|
+
assert resp.json() == {"id": 1, "title": "Test", "owner_id": 2}
|
|
104
|
+
|
|
105
|
+
resp = client.get("/v1/u/users/5")
|
|
106
|
+
assert resp.status_code == 200, resp.text
|
|
107
|
+
assert resp.json() == {"id": 5, "name": "User 5", "email": "user5@example.com"}
|
|
108
|
+
|
|
109
|
+
resp = client.get("/v1/u/users/999")
|
|
110
|
+
assert resp.status_code == 404, resp.text
|
|
111
|
+
|
|
112
|
+
# OpenAPI schema should be generatable
|
|
113
|
+
resp = client.get("/openapi.json")
|
|
114
|
+
assert resp.status_code == 200, resp.text
|
|
115
|
+
schema = resp.json()
|
|
116
|
+
assert "/v1/items/{item_id}" in schema["paths"]
|
|
117
|
+
assert "/v1/u/users/{user_id}" in schema["paths"]
|
|
118
|
+
assert "/" in schema["paths"]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_response_model_not_leaked_between_routes(monkeypatch):
|
|
122
|
+
"""Regression test: response_model from one route must not leak to another.
|
|
123
|
+
|
|
124
|
+
The deferred cached_property closures were installed once per class,
|
|
125
|
+
capturing the first route's parameters for all subsequent routes.
|
|
126
|
+
"""
|
|
127
|
+
apply_monkeypatch(monkeypatch)
|
|
128
|
+
|
|
129
|
+
# First route with response_model — its closures get installed on the class
|
|
130
|
+
router_a = routing.APIRouter()
|
|
131
|
+
|
|
132
|
+
@router_a.get("/a", response_model=Item)
|
|
133
|
+
def get_a():
|
|
134
|
+
return Item(id=1, title="A", owner_id=0)
|
|
135
|
+
|
|
136
|
+
# Second route WITHOUT response_model — should NOT inherit Item
|
|
137
|
+
router_b = routing.APIRouter()
|
|
138
|
+
|
|
139
|
+
@router_b.get("/b")
|
|
140
|
+
def get_b():
|
|
141
|
+
return {"plain": "dict"}
|
|
142
|
+
|
|
143
|
+
app = FastAPI()
|
|
144
|
+
app.include_router(router_a)
|
|
145
|
+
app.include_router(router_b)
|
|
146
|
+
|
|
147
|
+
# Check the original routes' response_model
|
|
148
|
+
for r in router_b.routes:
|
|
149
|
+
assert r.response_model is None, ( # ty: ignore[unresolved-attribute]
|
|
150
|
+
f"Route {r.path} incorrectly has response_model={r.response_model} " # ty: ignore[unresolved-attribute]
|
|
151
|
+
f"(leaked from another route)"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
client = TestClient(app)
|
|
155
|
+
resp = client.get("/b")
|
|
156
|
+
assert resp.status_code == 200, resp.text
|
|
157
|
+
assert resp.json() == {"plain": "dict"}
|
|
158
|
+
|
|
159
|
+
# Also verify /a still works
|
|
160
|
+
resp = client.get("/a")
|
|
161
|
+
assert resp.status_code == 200, resp.text
|
|
162
|
+
assert resp.json() == {"id": 1, "title": "A", "owner_id": 0}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_custom_response_class_not_leaked(monkeypatch):
|
|
166
|
+
"""Custom response_class on one route must not leak to others."""
|
|
167
|
+
apply_monkeypatch(monkeypatch)
|
|
168
|
+
|
|
169
|
+
router = routing.APIRouter()
|
|
170
|
+
|
|
171
|
+
@router.get("/html", response_class=PlainTextResponse)
|
|
172
|
+
def html():
|
|
173
|
+
return "<h1>hi</h1>"
|
|
174
|
+
|
|
175
|
+
@router.get("/json")
|
|
176
|
+
def json_route():
|
|
177
|
+
return {"ok": True}
|
|
178
|
+
|
|
179
|
+
app = FastAPI()
|
|
180
|
+
app.include_router(router)
|
|
181
|
+
|
|
182
|
+
client = TestClient(app)
|
|
183
|
+
resp = client.get("/html")
|
|
184
|
+
assert resp.status_code == 200
|
|
185
|
+
assert "text/plain" in resp.headers["content-type"]
|
|
186
|
+
|
|
187
|
+
resp = client.get("/json")
|
|
188
|
+
assert resp.status_code == 200
|
|
189
|
+
assert "application/json" in resp.headers["content-type"]
|
|
190
|
+
assert resp.json() == {"ok": True}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_dependency_overrides(monkeypatch):
|
|
194
|
+
apply_monkeypatch(monkeypatch)
|
|
195
|
+
|
|
196
|
+
def original_dep():
|
|
197
|
+
return "original"
|
|
198
|
+
|
|
199
|
+
def override_dep():
|
|
200
|
+
return "overridden"
|
|
201
|
+
|
|
202
|
+
router = routing.APIRouter()
|
|
203
|
+
|
|
204
|
+
@router.get("/dep")
|
|
205
|
+
def get_dep(val: t.Annotated[str, Depends(original_dep)]):
|
|
206
|
+
return {"val": val}
|
|
207
|
+
|
|
208
|
+
app = FastAPI()
|
|
209
|
+
app.include_router(router)
|
|
210
|
+
app.dependency_overrides[original_dep] = override_dep
|
|
211
|
+
|
|
212
|
+
client = TestClient(app)
|
|
213
|
+
resp = client.get("/dep")
|
|
214
|
+
assert resp.status_code == 200, resp.text
|
|
215
|
+
assert resp.json() == {"val": "overridden"}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_multiple_includes_same_router(monkeypatch):
|
|
219
|
+
apply_monkeypatch(monkeypatch)
|
|
220
|
+
|
|
221
|
+
router = routing.APIRouter()
|
|
222
|
+
|
|
223
|
+
@router.get("/ping")
|
|
224
|
+
def ping():
|
|
225
|
+
return {"pong": True}
|
|
226
|
+
|
|
227
|
+
app = FastAPI()
|
|
228
|
+
app.include_router(router, prefix="/api/v1")
|
|
229
|
+
app.include_router(router, prefix="/api/v2")
|
|
230
|
+
|
|
231
|
+
client = TestClient(app)
|
|
232
|
+
for prefix in ("/api/v1", "/api/v2"):
|
|
233
|
+
resp = client.get(f"{prefix}/ping")
|
|
234
|
+
assert resp.status_code == 200, f"{prefix}: {resp.text}"
|
|
235
|
+
assert resp.json() == {"pong": True}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_path_and_query_params(monkeypatch):
|
|
239
|
+
apply_monkeypatch(monkeypatch)
|
|
240
|
+
|
|
241
|
+
router = routing.APIRouter(prefix="/search")
|
|
242
|
+
|
|
243
|
+
@router.get("/{category}")
|
|
244
|
+
def search(
|
|
245
|
+
category: str,
|
|
246
|
+
q: str = Query(default=""),
|
|
247
|
+
limit: int = Query(default=10, le=100),
|
|
248
|
+
x_token: str = Header(default=""),
|
|
249
|
+
):
|
|
250
|
+
return {"category": category, "q": q, "limit": limit, "token": x_token}
|
|
251
|
+
|
|
252
|
+
app = FastAPI()
|
|
253
|
+
app.include_router(router)
|
|
254
|
+
|
|
255
|
+
client = TestClient(app)
|
|
256
|
+
resp = client.get("/search/books?q=fastapi&limit=5", headers={"X-Token": "abc"})
|
|
257
|
+
assert resp.status_code == 200, resp.text
|
|
258
|
+
assert resp.json() == {
|
|
259
|
+
"category": "books",
|
|
260
|
+
"q": "fastapi",
|
|
261
|
+
"limit": 5,
|
|
262
|
+
"token": "abc",
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_head_returns_405_like_upstream(monkeypatch):
|
|
267
|
+
"""HEAD on a GET-only route returns 405 — same as unpatched FastAPI."""
|
|
268
|
+
apply_monkeypatch(monkeypatch)
|
|
269
|
+
|
|
270
|
+
router = routing.APIRouter()
|
|
271
|
+
|
|
272
|
+
@router.get("/auto")
|
|
273
|
+
def auto():
|
|
274
|
+
return {"ok": True}
|
|
275
|
+
|
|
276
|
+
app = FastAPI()
|
|
277
|
+
app.include_router(router)
|
|
278
|
+
|
|
279
|
+
client = TestClient(app)
|
|
280
|
+
resp = client.head("/auto")
|
|
281
|
+
assert resp.status_code == 405, resp.text
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def test_exception_handlers(monkeypatch):
|
|
285
|
+
apply_monkeypatch(monkeypatch)
|
|
286
|
+
|
|
287
|
+
router = routing.APIRouter()
|
|
288
|
+
|
|
289
|
+
class CustomError(Exception):
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
@router.get("/custom-error")
|
|
293
|
+
def raise_custom():
|
|
294
|
+
raise CustomError("something went wrong")
|
|
295
|
+
|
|
296
|
+
@router.get("/validation-error")
|
|
297
|
+
def cause_validation(q: int):
|
|
298
|
+
return {"q": q}
|
|
299
|
+
|
|
300
|
+
app = FastAPI()
|
|
301
|
+
|
|
302
|
+
@app.exception_handler(CustomError)
|
|
303
|
+
def custom_handler(request, exc):
|
|
304
|
+
return JSONResponse(status_code=418, content={"error": str(exc)})
|
|
305
|
+
|
|
306
|
+
app.include_router(router)
|
|
307
|
+
|
|
308
|
+
client = TestClient(app)
|
|
309
|
+
resp = client.get("/custom-error")
|
|
310
|
+
assert resp.status_code == 418, resp.text
|
|
311
|
+
assert resp.json() == {"error": "something went wrong"}
|
|
312
|
+
|
|
313
|
+
resp = client.get("/validation-error?q=notanint")
|
|
314
|
+
assert resp.status_code == 422, resp.text
|
|
File without changes
|
|
File without changes
|
{fastapi_deferred_init-0.3.2 → fastapi_deferred_init-0.3.4}/src/fastapi_deferred_init/__init__.py
RENAMED
|
File without changes
|
{fastapi_deferred_init-0.3.2 → fastapi_deferred_init-0.3.4}/src/fastapi_deferred_init/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|