fastapi-deferred-init 0.2.11__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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-deferred-init
3
- Version: 0.2.11
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>
@@ -18,7 +18,7 @@ classifiers = [
18
18
  "Framework :: FastAPI",
19
19
  ]
20
20
  dynamic = []
21
- version = "0.2.11"
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.15.0",
77
+ "ruff>=0.15.2",
78
78
  "pre-commit>=4.3.0",
79
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
83
  "pip>=26.0.1",
84
- "inline-snapshot>=0.31.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,43 +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),
75
- strict_content_type: bool = Default(True),
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),
76
77
  ) -> None:
77
78
  self.path = path
78
79
  self.endpoint = endpoint
80
+ self.stream_item_type: Any | None = None
79
81
  if isinstance(response_model, DefaultPlaceholder):
80
82
  return_annotation = get_typed_return_annotation(endpoint)
81
83
  if lenient_issubclass(return_annotation, Response):
82
84
  response_model = None
83
85
  else:
84
- 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
85
102
  self.response_model = response_model
86
103
  self.summary = summary
87
104
  self.response_description = response_description
@@ -122,8 +139,11 @@ class DeferringAPIRoute(routing.APIRoute):
122
139
  assert is_body_allowed_for_status_code(status_code), (
123
140
  f"Status code {status_code} must not have a response body"
124
141
  )
142
+
125
143
  self.dependencies = list(dependencies or [])
126
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"
127
147
  self.description = self.description.split("\f")[0].strip()
128
148
 
129
149
  assert callable(endpoint), "An endpoint must be a callable"
@@ -141,6 +161,20 @@ class DeferringAPIRoute(routing.APIRoute):
141
161
  )
142
162
  return dependant
143
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
+
144
178
  @cached_property
145
179
  def _flat_dependant(self):
146
180
  return get_flat_dependant(self.dependant)
@@ -161,6 +195,17 @@ class DeferringAPIRoute(routing.APIRoute):
161
195
  else:
162
196
  return None
163
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
+
164
209
  @cached_property
165
210
  def response_fields(self) -> dict[Union[int, str], ModelField]:
166
211
  response_fields: dict[Union[int, str], ModelField] = {}
@@ -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)