faststream_fastapi 1.0.0__py3-none-any.whl
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.
- faststream_fastapi/__about__.py +5 -0
- faststream_fastapi/__init__.py +11 -0
- faststream_fastapi/_internal/__init__.py +0 -0
- faststream_fastapi/_internal/asyncapi_router.py +115 -0
- faststream_fastapi/_internal/background_middleware.py +22 -0
- faststream_fastapi/_internal/config.py +23 -0
- faststream_fastapi/_internal/fastapi_compat.py +152 -0
- faststream_fastapi/_internal/fs_re_exports/__init__.py +0 -0
- faststream_fastapi/_internal/fs_re_exports/_compat.py +6 -0
- faststream_fastapi/_internal/fs_re_exports/application.py +3 -0
- faststream_fastapi/_internal/fs_re_exports/broker.py +4 -0
- faststream_fastapi/_internal/fs_re_exports/constants.py +3 -0
- faststream_fastapi/_internal/fs_re_exports/context.py +8 -0
- faststream_fastapi/_internal/fs_re_exports/di.py +3 -0
- faststream_fastapi/_internal/fs_re_exports/logger.py +3 -0
- faststream_fastapi/_internal/get_dependant.py +182 -0
- faststream_fastapi/_internal/logger.py +11 -0
- faststream_fastapi/_internal/replace_context.py +119 -0
- faststream_fastapi/_internal/wrap_callable_to_fastapi_compatible.py +180 -0
- faststream_fastapi/asyncapi_config.py +71 -0
- faststream_fastapi/context.py +31 -0
- faststream_fastapi/faststream_api.py +165 -0
- faststream_fastapi/stream_message.py +25 -0
- faststream_fastapi-1.0.0.dist-info/METADATA +151 -0
- faststream_fastapi-1.0.0.dist-info/RECORD +27 -0
- faststream_fastapi-1.0.0.dist-info/WHEEL +4 -0
- faststream_fastapi-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from faststream_fastapi.asyncapi_config import AsyncAPIConfig
|
|
2
|
+
from faststream_fastapi.context import Context
|
|
3
|
+
from faststream_fastapi.faststream_api import FastStreamAPI
|
|
4
|
+
from faststream_fastapi.stream_message import StreamMessage
|
|
5
|
+
|
|
6
|
+
__all__ = (
|
|
7
|
+
"AsyncAPIConfig",
|
|
8
|
+
"Context",
|
|
9
|
+
"FastStreamAPI",
|
|
10
|
+
"StreamMessage",
|
|
11
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Request, Response
|
|
6
|
+
from fastapi.responses import HTMLResponse, JSONResponse
|
|
7
|
+
from faststream.asgi.factories.asyncapi.try_it_out import TryItOutProcessor
|
|
8
|
+
from faststream.specification.asyncapi.site import get_asyncapi_html
|
|
9
|
+
from faststream.specification.base import SpecificationFactory
|
|
10
|
+
|
|
11
|
+
from faststream_fastapi._internal.fs_re_exports.broker import BrokerUsecase
|
|
12
|
+
from faststream_fastapi.asyncapi_config import AsyncAPIConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AsyncAPIRouter(APIRouter):
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
brokers: Sequence[BrokerUsecase[Any, Any]],
|
|
19
|
+
config: AsyncAPIConfig,
|
|
20
|
+
schema: SpecificationFactory,
|
|
21
|
+
) -> None:
|
|
22
|
+
super().__init__(include_in_schema=config.include_in_schema)
|
|
23
|
+
|
|
24
|
+
self._config = config
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
try_processor = TryItOutProcessor(*brokers)
|
|
28
|
+
except ValueError:
|
|
29
|
+
try_processor = None
|
|
30
|
+
|
|
31
|
+
self.get(self._config.path)(self.make_serve_asyncapi_schema(schema))
|
|
32
|
+
|
|
33
|
+
if self._config.asyncapi_json_path is not None:
|
|
34
|
+
self.get(self._config.asyncapi_json_path)(self.make_download_app_json_schema(schema))
|
|
35
|
+
|
|
36
|
+
if self._config.asyncapi_yaml_path is not None:
|
|
37
|
+
self.get(self._config.asyncapi_yaml_path)(self.make_download_app_yaml_schema(schema))
|
|
38
|
+
|
|
39
|
+
if try_processor is not None and self._config.try_it_out_path is not None:
|
|
40
|
+
self.post(self._config.try_it_out_path, include_in_schema=False)(
|
|
41
|
+
self.make_try_asyncapi_schema(try_processor),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def make_download_app_json_schema(
|
|
45
|
+
self,
|
|
46
|
+
schema: SpecificationFactory,
|
|
47
|
+
) -> Callable[[], Awaitable[Response]]:
|
|
48
|
+
async def download_app_json_schema() -> Response:
|
|
49
|
+
return Response(
|
|
50
|
+
content=json.dumps(
|
|
51
|
+
schema.to_specification().to_jsonable(),
|
|
52
|
+
indent=2,
|
|
53
|
+
),
|
|
54
|
+
headers={"Content-Type": "application/json"},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return download_app_json_schema
|
|
58
|
+
|
|
59
|
+
def make_download_app_yaml_schema(
|
|
60
|
+
self,
|
|
61
|
+
schema: SpecificationFactory,
|
|
62
|
+
) -> Callable[[], Awaitable[Response]]:
|
|
63
|
+
async def download_app_yaml_schema() -> Response:
|
|
64
|
+
return Response(
|
|
65
|
+
content=schema.to_specification().to_yaml(),
|
|
66
|
+
headers={
|
|
67
|
+
"Content-Type": "application/octet-stream",
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return download_app_yaml_schema
|
|
72
|
+
|
|
73
|
+
def make_serve_asyncapi_schema(
|
|
74
|
+
self,
|
|
75
|
+
schema: SpecificationFactory,
|
|
76
|
+
) -> Callable[[], Awaitable[Response]]:
|
|
77
|
+
async def serve_asyncapi_schema() -> Response:
|
|
78
|
+
return HTMLResponse(
|
|
79
|
+
content=get_asyncapi_html(
|
|
80
|
+
schema.to_specification(),
|
|
81
|
+
sidebar=self._config.sidebar,
|
|
82
|
+
info=self._config.info,
|
|
83
|
+
servers=self._config.servers,
|
|
84
|
+
operations=self._config.operations,
|
|
85
|
+
messages=self._config.messages,
|
|
86
|
+
schemas=self._config.schemas,
|
|
87
|
+
errors=self._config.errors,
|
|
88
|
+
expand_message_examples=self._config.expand_message_examples,
|
|
89
|
+
asyncapi_js_url=self._config.asyncapi_js_url,
|
|
90
|
+
asyncapi_css_url=self._config.asyncapi_css_url,
|
|
91
|
+
try_it_out_plugin_url=self._config.try_it_out_plugin_url,
|
|
92
|
+
try_it_out_path=self._config.try_it_out_path,
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return serve_asyncapi_schema
|
|
97
|
+
|
|
98
|
+
def make_try_asyncapi_schema(
|
|
99
|
+
self,
|
|
100
|
+
try_processor: TryItOutProcessor,
|
|
101
|
+
) -> Callable[[Request], Awaitable[Response]]:
|
|
102
|
+
async def try_asyncapi_schema(request: Request) -> Response:
|
|
103
|
+
try:
|
|
104
|
+
body = await request.json()
|
|
105
|
+
except Exception as e: # noqa: BLE001
|
|
106
|
+
return JSONResponse({"details": f"Invalid JSON: {e}"}, 400)
|
|
107
|
+
|
|
108
|
+
result = await try_processor.process(body)
|
|
109
|
+
return Response(
|
|
110
|
+
content=result.body,
|
|
111
|
+
status_code=result.status_code,
|
|
112
|
+
media_type="application/json",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return try_asyncapi_schema
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from types import TracebackType
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
from fastapi.background import BackgroundTasks
|
|
5
|
+
from faststream.middlewares import BaseMiddleware
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _BackgroundMiddleware(BaseMiddleware):
|
|
9
|
+
async def __aexit__(
|
|
10
|
+
self,
|
|
11
|
+
exc_type: type[BaseException] | None = None,
|
|
12
|
+
exc_val: BaseException | None = None,
|
|
13
|
+
exc_tb: TracebackType | None = None,
|
|
14
|
+
) -> bool | None:
|
|
15
|
+
background = cast(
|
|
16
|
+
"BackgroundTasks | None",
|
|
17
|
+
getattr(self.context.get_local("message"), "background", None),
|
|
18
|
+
)
|
|
19
|
+
if exc_type is None and background is not None:
|
|
20
|
+
await background()
|
|
21
|
+
|
|
22
|
+
return await super().after_processed(exc_type, exc_val, exc_tb)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from fastapi import FastAPI
|
|
5
|
+
from fastapi.datastructures import State
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConfigAsgiStateError(ValueError):
|
|
9
|
+
def __str__(self) -> str:
|
|
10
|
+
return "ASGI state already is set"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Config:
|
|
15
|
+
application: FastAPI
|
|
16
|
+
dependency_overrides_provider: Any | None
|
|
17
|
+
asgi_state: State | None = None
|
|
18
|
+
|
|
19
|
+
def set_asgi_state(self, asgi_state: State) -> None:
|
|
20
|
+
if self.asgi_state is not None:
|
|
21
|
+
raise ConfigAsgiStateError # pragma: no cover
|
|
22
|
+
|
|
23
|
+
self.asgi_state = asgi_state
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from fastapi import __version__ as FASTAPI_VERSION # noqa: N812
|
|
5
|
+
from fastapi.dependencies.models import Dependant
|
|
6
|
+
from fastapi.dependencies.utils import solve_dependencies
|
|
7
|
+
from fastapi.requests import Request
|
|
8
|
+
from starlette.background import BackgroundTasks
|
|
9
|
+
from typing_extensions import Never
|
|
10
|
+
|
|
11
|
+
major, minor, patch, *_ = FASTAPI_VERSION.split(".")
|
|
12
|
+
|
|
13
|
+
_FASTAPI_MAJOR, _FASTAPI_MINOR = int(major), int(minor)
|
|
14
|
+
|
|
15
|
+
FASTAPI_V2 = _FASTAPI_MAJOR > 0 or _FASTAPI_MINOR > 100
|
|
16
|
+
FASTAPI_V106 = _FASTAPI_MAJOR > 0 or _FASTAPI_MINOR >= 106
|
|
17
|
+
FASTAPI_V121 = _FASTAPI_MAJOR > 0 or _FASTAPI_MINOR >= 121
|
|
18
|
+
FASTAPI_V128 = _FASTAPI_MAJOR > 0 or _FASTAPI_MINOR >= 128
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
_FASTAPI_PATCH = int(patch)
|
|
22
|
+
except ValueError:
|
|
23
|
+
FASTAPI_v102_3 = True
|
|
24
|
+
FASTAPI_v102_4 = True
|
|
25
|
+
else:
|
|
26
|
+
FASTAPI_v102_3 = (
|
|
27
|
+
_FASTAPI_MAJOR > 0
|
|
28
|
+
or _FASTAPI_MINOR > 112
|
|
29
|
+
or (_FASTAPI_MINOR == 112 and _FASTAPI_PATCH > 2)
|
|
30
|
+
)
|
|
31
|
+
FASTAPI_v102_4 = (
|
|
32
|
+
_FASTAPI_MAJOR > 0
|
|
33
|
+
or _FASTAPI_MINOR > 112
|
|
34
|
+
or (_FASTAPI_MINOR == 112 and _FASTAPI_PATCH > 3)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = (
|
|
38
|
+
"RequestValidationError",
|
|
39
|
+
"create_response_field",
|
|
40
|
+
"raise_fastapi_validation_error",
|
|
41
|
+
"solve_faststream_dependency",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class SolvedDependency:
|
|
47
|
+
values: dict[str, Any]
|
|
48
|
+
errors: list[Any]
|
|
49
|
+
background_tasks: BackgroundTasks | None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if FASTAPI_V128:
|
|
53
|
+
from fastapi.exceptions import RequestValidationError
|
|
54
|
+
|
|
55
|
+
def raise_fastapi_validation_error(
|
|
56
|
+
errors: list[Any],
|
|
57
|
+
body: dict[str, Any],
|
|
58
|
+
) -> Never:
|
|
59
|
+
raise RequestValidationError(errors, body=body)
|
|
60
|
+
|
|
61
|
+
elif FASTAPI_V2:
|
|
62
|
+
from fastapi._compat import _normalize_errors # type: ignore[attr-defined]
|
|
63
|
+
from fastapi.exceptions import RequestValidationError
|
|
64
|
+
|
|
65
|
+
def raise_fastapi_validation_error(
|
|
66
|
+
errors: list[Any],
|
|
67
|
+
body: dict[str, Any],
|
|
68
|
+
) -> Never:
|
|
69
|
+
raise RequestValidationError(_normalize_errors(errors), body=body)
|
|
70
|
+
|
|
71
|
+
else:
|
|
72
|
+
from pydantic import ( # type: ignore[assignment]
|
|
73
|
+
ValidationError as RequestValidationError,
|
|
74
|
+
)
|
|
75
|
+
from pydantic import (
|
|
76
|
+
create_model,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
ROUTER_VALIDATION_ERROR_MODEL = create_model("StreamRoute")
|
|
80
|
+
|
|
81
|
+
def raise_fastapi_validation_error(
|
|
82
|
+
errors: list[Any],
|
|
83
|
+
body: dict[str, Any],
|
|
84
|
+
) -> Never:
|
|
85
|
+
raise RequestValidationError(errors, ROUTER_VALIDATION_ERROR_MODEL) # type: ignore[misc]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if FASTAPI_v102_3:
|
|
89
|
+
from fastapi.utils import (
|
|
90
|
+
create_model_field as create_response_field,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
extra = {"embed_body_fields": False} if FASTAPI_v102_4 else {}
|
|
94
|
+
|
|
95
|
+
async def solve_faststream_dependency(
|
|
96
|
+
request: Request,
|
|
97
|
+
dependant: Dependant,
|
|
98
|
+
dependency_overrides_provider: Any | None,
|
|
99
|
+
**kwargs: Any,
|
|
100
|
+
) -> SolvedDependency:
|
|
101
|
+
solved_result = await solve_dependencies(
|
|
102
|
+
request=request,
|
|
103
|
+
body=request._body,
|
|
104
|
+
dependant=dependant,
|
|
105
|
+
dependency_overrides_provider=dependency_overrides_provider,
|
|
106
|
+
**extra, # type: ignore[arg-type]
|
|
107
|
+
**kwargs,
|
|
108
|
+
)
|
|
109
|
+
values, errors, background = (
|
|
110
|
+
solved_result.values,
|
|
111
|
+
solved_result.errors,
|
|
112
|
+
solved_result.background_tasks,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return SolvedDependency(
|
|
116
|
+
values=values,
|
|
117
|
+
errors=errors,
|
|
118
|
+
background_tasks=background,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
else:
|
|
122
|
+
from fastapi.utils import ( # type: ignore[attr-defined,no-redef]
|
|
123
|
+
create_response_field,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
async def solve_faststream_dependency(
|
|
127
|
+
request: Request,
|
|
128
|
+
dependant: Dependant,
|
|
129
|
+
dependency_overrides_provider: Any | None,
|
|
130
|
+
**kwargs: Any,
|
|
131
|
+
) -> SolvedDependency:
|
|
132
|
+
solved_result = await solve_dependencies(
|
|
133
|
+
request=request,
|
|
134
|
+
body=request._body,
|
|
135
|
+
dependant=dependant,
|
|
136
|
+
dependency_overrides_provider=dependency_overrides_provider,
|
|
137
|
+
**kwargs,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
(
|
|
141
|
+
values,
|
|
142
|
+
errors,
|
|
143
|
+
background,
|
|
144
|
+
_response,
|
|
145
|
+
_dependency_cache,
|
|
146
|
+
) = solved_result # type: ignore[misc]
|
|
147
|
+
|
|
148
|
+
return SolvedDependency(
|
|
149
|
+
values=values, # type: ignore[has-type]
|
|
150
|
+
errors=errors, # type: ignore[has-type]
|
|
151
|
+
background_tasks=background, # type: ignore[has-type]
|
|
152
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from collections.abc import Callable, Iterable
|
|
3
|
+
from typing import Annotated, Any, Final, cast, get_args, get_origin
|
|
4
|
+
|
|
5
|
+
from fast_depends.library.serializer import OptionItem
|
|
6
|
+
from fast_depends.utils import get_typed_annotation
|
|
7
|
+
from fastapi.dependencies.models import Dependant
|
|
8
|
+
from fastapi.dependencies.utils import (
|
|
9
|
+
get_dependant,
|
|
10
|
+
get_parameterless_sub_dependant,
|
|
11
|
+
get_typed_signature,
|
|
12
|
+
)
|
|
13
|
+
from fastapi.params import Depends
|
|
14
|
+
from pydantic import Field, create_model
|
|
15
|
+
|
|
16
|
+
from faststream_fastapi._internal.fs_re_exports._compat import PYDANTIC_V2, PydanticUndefined
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_fastapi_dependant(
|
|
20
|
+
orig_call: Callable[..., Any],
|
|
21
|
+
dependencies: Iterable[Depends],
|
|
22
|
+
) -> Dependant:
|
|
23
|
+
dependent = get_fastapi_native_dependant(orig_call=orig_call, dependencies=dependencies)
|
|
24
|
+
return _patch_fastapi_dependent(dependent)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_fastapi_native_dependant(
|
|
28
|
+
orig_call: Callable[..., Any],
|
|
29
|
+
dependencies: Iterable[Depends],
|
|
30
|
+
) -> Dependant:
|
|
31
|
+
dependent = get_dependant(
|
|
32
|
+
path="",
|
|
33
|
+
call=orig_call,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
for depends in list(dependencies)[::-1]:
|
|
37
|
+
dependent.dependencies.insert(
|
|
38
|
+
0,
|
|
39
|
+
get_parameterless_sub_dependant(depends=depends, path=""),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return dependent
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _patch_fastapi_dependent(dependant: Dependant) -> Dependant:
|
|
46
|
+
params = dependant.query_params + dependant.body_params
|
|
47
|
+
|
|
48
|
+
for d in dependant.dependencies:
|
|
49
|
+
params.extend(d.query_params + d.body_params)
|
|
50
|
+
|
|
51
|
+
params_unique = {}
|
|
52
|
+
|
|
53
|
+
call = dependant.call
|
|
54
|
+
if is_faststream_decorated(call):
|
|
55
|
+
call = getattr(call, "__wrapped__", call)
|
|
56
|
+
globalns = getattr(call, "__globals__", {})
|
|
57
|
+
|
|
58
|
+
for p in params:
|
|
59
|
+
if p.name not in params_unique:
|
|
60
|
+
info: Any = p.field_info if PYDANTIC_V2 else p
|
|
61
|
+
|
|
62
|
+
field_data = {
|
|
63
|
+
"default": ... if info.default is PydanticUndefined else info.default,
|
|
64
|
+
"default_factory": info.default_factory,
|
|
65
|
+
"alias": info.alias,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if PYDANTIC_V2:
|
|
69
|
+
from pydantic.fields import FieldInfo # noqa: PLC0415
|
|
70
|
+
|
|
71
|
+
info = cast("FieldInfo", info)
|
|
72
|
+
|
|
73
|
+
field_data.update(
|
|
74
|
+
{
|
|
75
|
+
"title": info.title,
|
|
76
|
+
"alias_priority": info.alias_priority,
|
|
77
|
+
"validation_alias": info.validation_alias,
|
|
78
|
+
"serialization_alias": info.serialization_alias,
|
|
79
|
+
"description": info.description,
|
|
80
|
+
"discriminator": info.discriminator,
|
|
81
|
+
"examples": info.examples,
|
|
82
|
+
"exclude": info.exclude,
|
|
83
|
+
"json_schema_extra": info.json_schema_extra,
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
f = next(
|
|
88
|
+
filter(
|
|
89
|
+
lambda x: isinstance(x, FieldInfo),
|
|
90
|
+
p.field_info.metadata or (),
|
|
91
|
+
),
|
|
92
|
+
Field(**field_data), # type: ignore[pydantic-field,unused-ignore]
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
else:
|
|
96
|
+
from pydantic.fields import ( # type: ignore[attr-defined] # noqa: PLC0415 # pragma: no cover
|
|
97
|
+
ModelField,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
info = cast("ModelField", info)
|
|
101
|
+
|
|
102
|
+
field_data.update(
|
|
103
|
+
{
|
|
104
|
+
"title": info.field_info.title,
|
|
105
|
+
"description": info.field_info.description,
|
|
106
|
+
"discriminator": info.field_info.discriminator,
|
|
107
|
+
"exclude": info.field_info.exclude,
|
|
108
|
+
"gt": info.field_info.gt,
|
|
109
|
+
"ge": info.field_info.ge,
|
|
110
|
+
"lt": info.field_info.lt,
|
|
111
|
+
"le": info.field_info.le,
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
f = Field(**field_data) # type: ignore[pydantic-field,unused-ignore]
|
|
115
|
+
|
|
116
|
+
params_unique[p.name] = (
|
|
117
|
+
get_typed_annotation(info.annotation, globalns, {}),
|
|
118
|
+
f,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
dependant.model = create_model( # type: ignore[attr-defined]
|
|
122
|
+
getattr(call, "__name__", type(call).__name__),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
dependant.custom_fields = {} # type: ignore[attr-defined]
|
|
126
|
+
dependant.flat_params = [ # type: ignore[attr-defined]
|
|
127
|
+
OptionItem(field_name=name, field_type=type_, default_value=default)
|
|
128
|
+
for name, (type_, default) in params_unique.items()
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
return dependant
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def has_forbidden_types( # noqa: C901
|
|
135
|
+
orig_call: Callable[..., Any],
|
|
136
|
+
forbidden_types: tuple[Any, ...],
|
|
137
|
+
) -> set[Any]:
|
|
138
|
+
endpoint_signature = get_typed_signature(orig_call)
|
|
139
|
+
signature_params = endpoint_signature.parameters
|
|
140
|
+
|
|
141
|
+
founded_types = set()
|
|
142
|
+
|
|
143
|
+
for param in signature_params.values():
|
|
144
|
+
ann = param.annotation
|
|
145
|
+
|
|
146
|
+
founded_buffer = set()
|
|
147
|
+
has_fastapi_depends = False
|
|
148
|
+
if ann is not inspect.Signature.empty and get_origin(ann) is Annotated:
|
|
149
|
+
annotated_args = get_args(ann)
|
|
150
|
+
|
|
151
|
+
for arg in annotated_args[1:]:
|
|
152
|
+
if isinstance(arg, Depends):
|
|
153
|
+
has_fastapi_depends = True
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
for t in forbidden_types:
|
|
157
|
+
if isinstance(arg, t):
|
|
158
|
+
founded_buffer.add(t)
|
|
159
|
+
|
|
160
|
+
if isinstance(param.default, Depends):
|
|
161
|
+
has_fastapi_depends = True
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
for t in forbidden_types:
|
|
165
|
+
if isinstance(param.default, t):
|
|
166
|
+
founded_buffer.add(t)
|
|
167
|
+
|
|
168
|
+
if not has_fastapi_depends:
|
|
169
|
+
founded_types |= founded_buffer
|
|
170
|
+
|
|
171
|
+
return founded_types
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
FASTSTREAM_FASTAPI_PLUGIN_DECORATOR_MARKER: Final = "__faststream_consumer__"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def is_faststream_decorated(func: object) -> bool:
|
|
178
|
+
return getattr(func, FASTSTREAM_FASTAPI_PLUGIN_DECORATOR_MARKER, False)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def mark_faststream_decorated(func: object) -> None:
|
|
182
|
+
setattr(func, FASTSTREAM_FASTAPI_PLUGIN_DECORATOR_MARKER, True)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from faststream_fastapi._internal.fs_re_exports.logger import get_logger
|
|
5
|
+
|
|
6
|
+
logger = get_logger(
|
|
7
|
+
name="faststream_fastapi",
|
|
8
|
+
log_level=logging.INFO,
|
|
9
|
+
stream=sys.stderr,
|
|
10
|
+
fmt="%(asctime)s %(levelname)8s - %(message)s",
|
|
11
|
+
)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
from collections.abc import Callable, Mapping
|
|
4
|
+
from typing import (
|
|
5
|
+
Annotated,
|
|
6
|
+
Any,
|
|
7
|
+
ParamSpec,
|
|
8
|
+
TypeVar,
|
|
9
|
+
cast,
|
|
10
|
+
get_args,
|
|
11
|
+
get_origin,
|
|
12
|
+
get_type_hints,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from fastapi.dependencies.utils import get_typed_signature
|
|
16
|
+
from faststream._internal.context import Context as ContextCls
|
|
17
|
+
|
|
18
|
+
from faststream_fastapi import Context as PluginContext
|
|
19
|
+
|
|
20
|
+
P_HandlerParams = ParamSpec("P_HandlerParams")
|
|
21
|
+
T_HandlerReturn = TypeVar("T_HandlerReturn")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def replace_context(
|
|
25
|
+
func: Callable[P_HandlerParams, T_HandlerReturn],
|
|
26
|
+
) -> Callable[P_HandlerParams, T_HandlerReturn]:
|
|
27
|
+
if inspect.iscoroutinefunction(func):
|
|
28
|
+
|
|
29
|
+
@functools.wraps(func)
|
|
30
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
31
|
+
return await func(*args, **kwargs)
|
|
32
|
+
else:
|
|
33
|
+
|
|
34
|
+
@functools.wraps(func)
|
|
35
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
36
|
+
return func(*args, **kwargs)
|
|
37
|
+
|
|
38
|
+
endpoint_signature = get_typed_signature(func)
|
|
39
|
+
new_params = get_new_parameters(endpoint_signature.parameters)
|
|
40
|
+
new_annotations = get_new_annotations(get_type_hints(func, include_extras=True))
|
|
41
|
+
|
|
42
|
+
wrapper.__name__ = func.__name__
|
|
43
|
+
wrapper.__qualname__ = func.__qualname__
|
|
44
|
+
wrapper.__doc__ = func.__doc__
|
|
45
|
+
wrapper.__module__ = func.__module__
|
|
46
|
+
wrapper.__annotations__ = new_annotations
|
|
47
|
+
wrapper.__signature__ = inspect.Signature( # type: ignore[attr-defined]
|
|
48
|
+
parameters=new_params,
|
|
49
|
+
return_annotation=endpoint_signature.return_annotation,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return cast(Callable[P_HandlerParams, T_HandlerReturn], wrapper)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_new_parameters(
|
|
56
|
+
signature_params: Mapping[str, inspect.Parameter],
|
|
57
|
+
) -> list[inspect.Parameter]:
|
|
58
|
+
new_params = []
|
|
59
|
+
|
|
60
|
+
for param in signature_params.values():
|
|
61
|
+
if (
|
|
62
|
+
param.annotation is not inspect.Signature.empty
|
|
63
|
+
and get_origin(param.annotation) is Annotated
|
|
64
|
+
):
|
|
65
|
+
new_params.append(
|
|
66
|
+
inspect.Parameter(
|
|
67
|
+
name=param.name,
|
|
68
|
+
default=param.default,
|
|
69
|
+
kind=param.kind,
|
|
70
|
+
annotation=replace_annotated(param.annotation, param.name),
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
elif isinstance(param.default, ContextCls):
|
|
74
|
+
new_param = inspect.Parameter(
|
|
75
|
+
name=param.name,
|
|
76
|
+
default=create_plugin_context(param.default, param.name),
|
|
77
|
+
kind=param.kind,
|
|
78
|
+
annotation=param.annotation,
|
|
79
|
+
)
|
|
80
|
+
new_params.append(new_param)
|
|
81
|
+
else:
|
|
82
|
+
new_params.append(param)
|
|
83
|
+
|
|
84
|
+
return new_params
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_new_annotations(type_hints: Mapping[str, Any]) -> dict[str, Any]:
|
|
88
|
+
new_annotations = {}
|
|
89
|
+
|
|
90
|
+
for param_name, type_hint in type_hints.items():
|
|
91
|
+
if get_origin(type_hint) is Annotated:
|
|
92
|
+
new_annotations[param_name] = replace_annotated(type_hint, param_name)
|
|
93
|
+
else:
|
|
94
|
+
new_annotations[param_name] = type_hint
|
|
95
|
+
|
|
96
|
+
return new_annotations
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def replace_annotated(type_hint: Any, parameter_name: str) -> Any:
|
|
100
|
+
annotated_args = get_args(type_hint)
|
|
101
|
+
|
|
102
|
+
new_annotated_args = [annotated_args[0]]
|
|
103
|
+
for arg in annotated_args[1:]:
|
|
104
|
+
if isinstance(arg, ContextCls):
|
|
105
|
+
new_annotated_args.append(create_plugin_context(arg, parameter_name))
|
|
106
|
+
else:
|
|
107
|
+
new_annotated_args.append(arg)
|
|
108
|
+
|
|
109
|
+
return Annotated[tuple(new_annotated_args)]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def create_plugin_context(fs_context: ContextCls, parameter_name: str) -> Any:
|
|
113
|
+
fs_context.set_param_name(parameter_name)
|
|
114
|
+
|
|
115
|
+
return PluginContext(
|
|
116
|
+
name=fs_context.name or fs_context.param_name,
|
|
117
|
+
default=fs_context.default,
|
|
118
|
+
initial=fs_context.initial,
|
|
119
|
+
)
|