fastapi-deferred-init 0.2.10__tar.gz → 0.2.12__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.
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-deferred-init
3
- Version: 0.2.10
3
+ Version: 0.2.12
4
4
  Summary: Faster FastAPI start-up time for Projects with many nested routers
5
5
  Keywords: fastapi,speed,router
6
6
  Author-Email: Jan Vollmer <jan@vllmr.dev>
7
7
  License: MIT
8
8
  Classifier: Framework :: FastAPI
9
9
  Project-URL: Source, https://github.com/jvllmr/fastapi-deferred-init
10
- Requires-Python: >=3.9
11
- Requires-Dist: fastapi>=0.128.5
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: fastapi>=0.132.0
12
12
  Description-Content-Type: text/markdown
13
13
 
14
14
  # fastapi-deferred-init
@@ -10,15 +10,15 @@ authors = [
10
10
  { name = "Jan Vollmer", email = "jan@vllmr.dev" },
11
11
  ]
12
12
  dependencies = [
13
- "fastapi>=0.128.5",
13
+ "fastapi>=0.132.0",
14
14
  ]
15
- requires-python = ">=3.9"
15
+ requires-python = ">=3.10"
16
16
  readme = "README.md"
17
17
  classifiers = [
18
18
  "Framework :: FastAPI",
19
19
  ]
20
20
  dynamic = []
21
- version = "0.2.10"
21
+ version = "0.2.12"
22
22
 
23
23
  [project.license]
24
24
  text = "MIT"
@@ -74,12 +74,12 @@ all = true
74
74
  [dependency-groups]
75
75
  dev = [
76
76
  "pytest>=8.4.2",
77
- "ruff>=0.14.7",
77
+ "ruff>=0.15.2",
78
78
  "pre-commit>=4.3.0",
79
- "mypy>=1.19.0",
79
+ "mypy>=1.19.1",
80
80
  "tuna>=0.5.11",
81
81
  "httpx>=0.28.1",
82
82
  "pytest-benchmark>=5.2.3",
83
- "pip>=25.3",
84
- "inline-snapshot>=0.31.1",
83
+ "pip>=26.0.1",
84
+ "inline-snapshot>=0.32.2",
85
85
  ]
@@ -15,7 +15,10 @@ from starlette.routing import Mount as Mount # noqa
15
15
  from starlette.routing import compile_path, get_name
16
16
  from starlette.types import ASGIApp, Lifespan
17
17
 
18
-
18
+ from fastapi.sse import (
19
+ EventSourceResponse,
20
+ ServerSentEvent,
21
+ )
19
22
  from fastapi import params, routing # type:ignore
20
23
  from fastapi._compat import ModelField, lenient_issubclass
21
24
  from fastapi.datastructures import Default, DefaultPlaceholder
@@ -26,6 +29,7 @@ from fastapi.dependencies.utils import (
26
29
  get_flat_dependant,
27
30
  get_parameterless_sub_dependant,
28
31
  get_typed_return_annotation,
32
+ get_stream_item_type,
29
33
  )
30
34
  from fastapi.responses import JSONResponse, Response
31
35
  from fastapi.types import IncEx
@@ -45,42 +49,56 @@ class DeferringAPIRoute(routing.APIRoute):
45
49
  endpoint: Callable[..., Any],
46
50
  *,
47
51
  response_model: Any = Default(None),
48
- status_code: Optional[int] = None,
49
- tags: Optional[list[Union[str, Enum]]] = None,
50
- dependencies: Optional[Sequence[params.Depends]] = None,
51
- summary: Optional[str] = None,
52
- description: Optional[str] = None,
52
+ status_code: int | None = None,
53
+ tags: list[str | Enum] | None = None,
54
+ dependencies: Sequence[params.Depends] | None = None,
55
+ summary: str | None = None,
56
+ description: str | None = None,
53
57
  response_description: str = "Successful Response",
54
- responses: Optional[dict[Union[int, str], dict[str, Any]]] = None,
55
- deprecated: Optional[bool] = None,
56
- name: Optional[str] = None,
57
- methods: Optional[Union[set[str], list[str]]] = None,
58
- operation_id: Optional[str] = None,
59
- response_model_include: Optional[IncEx] = None,
60
- response_model_exclude: Optional[IncEx] = None,
58
+ responses: dict[int | str, dict[str, Any]] | None = None,
59
+ deprecated: bool | None = None,
60
+ name: str | None = None,
61
+ methods: set[str] | list[str] | None = None,
62
+ operation_id: str | None = None,
63
+ response_model_include: IncEx | None = None,
64
+ response_model_exclude: IncEx | None = None,
61
65
  response_model_by_alias: bool = True,
62
66
  response_model_exclude_unset: bool = False,
63
67
  response_model_exclude_defaults: bool = False,
64
68
  response_model_exclude_none: bool = False,
65
69
  include_in_schema: bool = True,
66
- response_class: Union[type[Response], DefaultPlaceholder] = Default(
67
- JSONResponse
68
- ),
69
- dependency_overrides_provider: Optional[Any] = None,
70
- callbacks: Optional[list[BaseRoute]] = None,
71
- openapi_extra: Optional[dict[str, Any]] = None,
72
- generate_unique_id_function: Union[
73
- Callable[["routing.APIRoute"], str], DefaultPlaceholder
74
- ] = Default(generate_unique_id),
70
+ response_class: type[Response] | DefaultPlaceholder = Default(JSONResponse),
71
+ dependency_overrides_provider: Any | None = None,
72
+ callbacks: list[BaseRoute] | None = None,
73
+ openapi_extra: dict[str, Any] | None = None,
74
+ generate_unique_id_function: Callable[[routing.APIRoute], str]
75
+ | DefaultPlaceholder = Default(generate_unique_id),
76
+ strict_content_type: bool | DefaultPlaceholder = Default(True),
75
77
  ) -> None:
76
78
  self.path = path
77
79
  self.endpoint = endpoint
80
+ self.stream_item_type: Any | None = None
78
81
  if isinstance(response_model, DefaultPlaceholder):
79
82
  return_annotation = get_typed_return_annotation(endpoint)
80
83
  if lenient_issubclass(return_annotation, Response):
81
84
  response_model = None
82
85
  else:
83
- response_model = return_annotation
86
+ stream_item = get_stream_item_type(return_annotation)
87
+ if stream_item is not None:
88
+ # Extract item type for JSONL or SSE streaming when
89
+ # response_class is DefaultPlaceholder (JSONL) or
90
+ # EventSourceResponse (SSE).
91
+ # ServerSentEvent is excluded: it's a transport
92
+ # wrapper, not a data model, so it shouldn't feed
93
+ # into validation or OpenAPI schema generation.
94
+ if (
95
+ isinstance(response_class, DefaultPlaceholder)
96
+ or lenient_issubclass(response_class, EventSourceResponse)
97
+ ) and not lenient_issubclass(stream_item, ServerSentEvent):
98
+ self.stream_item_type = stream_item
99
+ response_model = None
100
+ else:
101
+ response_model = return_annotation
84
102
  self.response_model = response_model
85
103
  self.summary = summary
86
104
  self.response_description = response_description
@@ -98,6 +116,7 @@ class DeferringAPIRoute(routing.APIRoute):
98
116
  self.callbacks = callbacks
99
117
  self.openapi_extra = openapi_extra
100
118
  self.generate_unique_id_function = generate_unique_id_function
119
+ self.strict_content_type = strict_content_type
101
120
  self.tags = tags or []
102
121
  self.responses = responses or {}
103
122
  self.name = get_name(endpoint) if name is None else name
@@ -120,8 +139,11 @@ class DeferringAPIRoute(routing.APIRoute):
120
139
  assert is_body_allowed_for_status_code(status_code), (
121
140
  f"Status code {status_code} must not have a response body"
122
141
  )
142
+
123
143
  self.dependencies = list(dependencies or [])
124
144
  self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
145
+ # if a "form feed" character (page break) is found in the description text,
146
+ # truncate description text to the content preceding the first "form feed"
125
147
  self.description = self.description.split("\f")[0].strip()
126
148
 
127
149
  assert callable(endpoint), "An endpoint must be a callable"
@@ -139,6 +161,20 @@ class DeferringAPIRoute(routing.APIRoute):
139
161
  )
140
162
  return dependant
141
163
 
164
+ @property
165
+ def is_generator(self) -> bool:
166
+ return self.dependant.is_async_gen_callable or self.dependant.is_gen_callable
167
+
168
+ @property
169
+ def is_sse_stream(self) -> bool:
170
+ return self.is_generator and lenient_issubclass(
171
+ self.response_class, EventSourceResponse
172
+ )
173
+
174
+ @property
175
+ def is_json_stream(self) -> bool:
176
+ return self.is_generator and isinstance(self.response_class, DefaultPlaceholder)
177
+
142
178
  @cached_property
143
179
  def _flat_dependant(self):
144
180
  return get_flat_dependant(self.dependant)
@@ -159,6 +195,17 @@ class DeferringAPIRoute(routing.APIRoute):
159
195
  else:
160
196
  return None
161
197
 
198
+ @cached_property
199
+ def stream_item_field(self) -> ModelField | None:
200
+ if not self.stream_item_type:
201
+ return None
202
+ stream_item_name = "StreamItem_" + self.unique_id
203
+ return create_model_field(
204
+ name=stream_item_name,
205
+ type_=self.stream_item_type,
206
+ mode="serialization",
207
+ )
208
+
162
209
  @cached_property
163
210
  def response_fields(self) -> dict[Union[int, str], ModelField]:
164
211
  response_fields: dict[Union[int, str], ModelField] = {}
@@ -212,6 +259,7 @@ class DeferringAPIRouter(routing.APIRouter):
212
259
  generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
213
260
  generate_unique_id
214
261
  ),
262
+ strict_content_type: bool = Default(True),
215
263
  ) -> None:
216
264
  super().__init__(
217
265
  prefix=prefix,
@@ -231,4 +279,5 @@ class DeferringAPIRouter(routing.APIRouter):
231
279
  deprecated=deprecated,
232
280
  include_in_schema=include_in_schema,
233
281
  generate_unique_id_function=generate_unique_id_function,
282
+ strict_content_type=strict_content_type,
234
283
  )
@@ -89,3 +89,20 @@ def test_fastapi_openapi_schema(monkeypatch):
89
89
 
90
90
  fastapi_test_openapi_schema()
91
91
  fastapi_test_openapi_schema_additional()
92
+
93
+
94
+ def test_fastapi_sse(monkeypatch):
95
+ monkeypatch.setattr("fastapi.routing.APIRoute", DeferringAPIRoute)
96
+ monkeypatch.setattr("fastapi.routing.APIRouter", DeferringAPIRouter)
97
+ import_via_file_path(
98
+ "fastapi_clone.tests.test_sse",
99
+ "fastapi/tests/test_sse.py",
100
+ )
101
+
102
+ from fastapi_clone.tests.test_sse import ( # type: ignore
103
+ test_raw_data_sent_without_json_encoding as fastapi_test_raw_data_sent_without_json_encoding,
104
+ client_fixture as fastapi_client_fixture,
105
+ )
106
+
107
+ for client in fastapi_client_fixture.__wrapped__():
108
+ fastapi_test_raw_data_sent_without_json_encoding(client)