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,180 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from collections.abc import Awaitable, Callable, Iterable
|
|
3
|
+
from contextlib import AsyncExitStack
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from itertools import dropwhile
|
|
6
|
+
from typing import Any, ParamSpec, TypeVar
|
|
7
|
+
|
|
8
|
+
from fast_depends.dependencies import Dependant
|
|
9
|
+
from fastapi.dependencies.models import Dependant as FastAPIDependant
|
|
10
|
+
from fastapi.params import Depends
|
|
11
|
+
from fastapi.routing import run_endpoint_function, serialize_response
|
|
12
|
+
from faststream.exceptions import SetupError
|
|
13
|
+
from faststream.message import StreamMessage as NativeMessage
|
|
14
|
+
from faststream.response import Response, ensure_response
|
|
15
|
+
|
|
16
|
+
from faststream_fastapi._internal.config import Config
|
|
17
|
+
from faststream_fastapi._internal.fastapi_compat import (
|
|
18
|
+
FASTAPI_V106,
|
|
19
|
+
FASTAPI_V121,
|
|
20
|
+
raise_fastapi_validation_error,
|
|
21
|
+
solve_faststream_dependency,
|
|
22
|
+
)
|
|
23
|
+
from faststream_fastapi._internal.fs_re_exports.context import ContextRepo
|
|
24
|
+
from faststream_fastapi._internal.get_dependant import (
|
|
25
|
+
get_fastapi_native_dependant,
|
|
26
|
+
has_forbidden_types,
|
|
27
|
+
is_faststream_decorated,
|
|
28
|
+
mark_faststream_decorated,
|
|
29
|
+
)
|
|
30
|
+
from faststream_fastapi._internal.replace_context import replace_context
|
|
31
|
+
from faststream_fastapi.stream_message import StreamMessage
|
|
32
|
+
|
|
33
|
+
P_HandlerParams = ParamSpec("P_HandlerParams")
|
|
34
|
+
T_HandlerReturn = TypeVar("T_HandlerReturn")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def wrap_callable_to_fastapi_compatible(
|
|
38
|
+
user_callable: Callable[P_HandlerParams, T_HandlerReturn],
|
|
39
|
+
*,
|
|
40
|
+
config: Config,
|
|
41
|
+
context: ContextRepo,
|
|
42
|
+
dependencies: Iterable[Depends],
|
|
43
|
+
) -> Callable[[NativeMessage[Any]], Awaitable[Any]]:
|
|
44
|
+
user_callable = replace_context(user_callable)
|
|
45
|
+
|
|
46
|
+
if has_forbidden_types(user_callable, (Dependant,)):
|
|
47
|
+
msg = (
|
|
48
|
+
f"Incorrect `faststream.Depends` usage at `{user_callable.__name__}`. "
|
|
49
|
+
"For FastAPI integration use `fastapi.Depends` instead."
|
|
50
|
+
)
|
|
51
|
+
raise SetupError(msg)
|
|
52
|
+
|
|
53
|
+
if is_faststream_decorated(user_callable):
|
|
54
|
+
return user_callable # type: ignore[return-value]
|
|
55
|
+
|
|
56
|
+
parsed_callable = build_faststream_to_fastapi_parser(
|
|
57
|
+
dependent=get_fastapi_native_dependant(user_callable, dependencies),
|
|
58
|
+
config=config,
|
|
59
|
+
context=context,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
mark_faststream_decorated(parsed_callable)
|
|
63
|
+
return wraps(user_callable)(parsed_callable)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def build_faststream_to_fastapi_parser(
|
|
67
|
+
*,
|
|
68
|
+
dependent: FastAPIDependant,
|
|
69
|
+
config: Config,
|
|
70
|
+
context: ContextRepo,
|
|
71
|
+
) -> Callable[[NativeMessage[Any]], Awaitable[Any]]:
|
|
72
|
+
if dependent.call is None: # pragma: no cover
|
|
73
|
+
raise ValueError("dependent.call is None") # noqa: TRY003
|
|
74
|
+
|
|
75
|
+
consume = make_fastapi_execution(
|
|
76
|
+
dependent=dependent,
|
|
77
|
+
config=config,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
dependencies_names = tuple(i.name for i in dependent.dependencies)
|
|
81
|
+
|
|
82
|
+
first_arg = next(
|
|
83
|
+
dropwhile(
|
|
84
|
+
lambda i: i in dependencies_names,
|
|
85
|
+
inspect.signature(dependent.call).parameters,
|
|
86
|
+
),
|
|
87
|
+
None,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
async def parsed_consumer(message: NativeMessage[Any]) -> Any:
|
|
91
|
+
body = await message.decode()
|
|
92
|
+
|
|
93
|
+
fastapi_body: dict[str, Any] | list[Any]
|
|
94
|
+
stream_message: StreamMessage
|
|
95
|
+
if first_arg is not None:
|
|
96
|
+
if isinstance(body, dict):
|
|
97
|
+
path = fastapi_body = body or {}
|
|
98
|
+
elif isinstance(body, list):
|
|
99
|
+
fastapi_body, path = body, {}
|
|
100
|
+
else:
|
|
101
|
+
path = fastapi_body = {first_arg: body}
|
|
102
|
+
|
|
103
|
+
stream_message = StreamMessage(
|
|
104
|
+
body=fastapi_body,
|
|
105
|
+
headers={"context__": context, **message.headers},
|
|
106
|
+
path={**path, **message.path},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
else:
|
|
110
|
+
stream_message = StreamMessage(
|
|
111
|
+
body={},
|
|
112
|
+
headers={"context__": context},
|
|
113
|
+
path={},
|
|
114
|
+
)
|
|
115
|
+
return await consume(stream_message, message)
|
|
116
|
+
|
|
117
|
+
return parsed_consumer
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def make_fastapi_execution(
|
|
121
|
+
*,
|
|
122
|
+
dependent: FastAPIDependant,
|
|
123
|
+
config: Config,
|
|
124
|
+
) -> Callable[
|
|
125
|
+
[StreamMessage, NativeMessage[Any]],
|
|
126
|
+
Awaitable[Response],
|
|
127
|
+
]:
|
|
128
|
+
is_coroutine = inspect.iscoroutinefunction(dependent.call)
|
|
129
|
+
|
|
130
|
+
async def app(
|
|
131
|
+
request: StreamMessage,
|
|
132
|
+
raw_message: NativeMessage[Any], # to support BackgroundTasks by middleware
|
|
133
|
+
) -> Response:
|
|
134
|
+
async with AsyncExitStack() as stack:
|
|
135
|
+
kwargs = {}
|
|
136
|
+
if FASTAPI_V121:
|
|
137
|
+
request.scope["fastapi_inner_astack"] = stack
|
|
138
|
+
function_stack = AsyncExitStack()
|
|
139
|
+
await stack.enter_async_context(function_stack)
|
|
140
|
+
request.scope["fastapi_function_astack"] = function_stack
|
|
141
|
+
|
|
142
|
+
if FASTAPI_V106:
|
|
143
|
+
kwargs = {"async_exit_stack": stack}
|
|
144
|
+
|
|
145
|
+
else: # pragma: no cover
|
|
146
|
+
request.scope["fastapi_astack"] = stack
|
|
147
|
+
|
|
148
|
+
request.scope["app"] = config.application
|
|
149
|
+
request.scope["state"] = config.asgi_state
|
|
150
|
+
|
|
151
|
+
solved_result = await solve_faststream_dependency(
|
|
152
|
+
request=request,
|
|
153
|
+
dependant=dependent,
|
|
154
|
+
dependency_overrides_provider=config.dependency_overrides_provider,
|
|
155
|
+
**kwargs,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
raw_message.background = solved_result.background_tasks # type: ignore[attr-defined]
|
|
159
|
+
|
|
160
|
+
if solved_result.errors:
|
|
161
|
+
raise_fastapi_validation_error(solved_result.errors, request._body) # type: ignore[arg-type]
|
|
162
|
+
|
|
163
|
+
function_result = await run_endpoint_function(
|
|
164
|
+
dependant=dependent,
|
|
165
|
+
values=solved_result.values,
|
|
166
|
+
is_coroutine=is_coroutine,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
response = ensure_response(function_result)
|
|
170
|
+
|
|
171
|
+
response.body = await serialize_response(
|
|
172
|
+
response_content=response.body,
|
|
173
|
+
is_coroutine=is_coroutine,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return response
|
|
177
|
+
|
|
178
|
+
raise AssertionError("unreachable")
|
|
179
|
+
|
|
180
|
+
return app
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from faststream.specification.asyncapi.site import (
|
|
5
|
+
ASYNCAPI_CSS_DEFAULT_URL,
|
|
6
|
+
ASYNCAPI_JS_DEFAULT_URL,
|
|
7
|
+
ASYNCAPI_TRY_IT_PLUGIN_URL,
|
|
8
|
+
)
|
|
9
|
+
from faststream.specification.schema import Tag, TagDict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _Empty: ...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_EMPTY = _Empty()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AsyncAPIConfig:
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
path: str,
|
|
22
|
+
*,
|
|
23
|
+
description: str | None = None,
|
|
24
|
+
tags: Sequence[Tag | TagDict | dict[str, Any]] | None = None,
|
|
25
|
+
unique_id: str | None = None,
|
|
26
|
+
include_in_schema: bool = False,
|
|
27
|
+
asyncapi_json_path: str | None | _Empty = _EMPTY,
|
|
28
|
+
asyncapi_yaml_path: str | None | _Empty = _EMPTY,
|
|
29
|
+
try_it_out_path: str | None | _Empty = _EMPTY,
|
|
30
|
+
sidebar: bool = True,
|
|
31
|
+
info: bool = True,
|
|
32
|
+
servers: bool = True,
|
|
33
|
+
operations: bool = True,
|
|
34
|
+
messages: bool = True,
|
|
35
|
+
schemas: bool = True,
|
|
36
|
+
errors: bool = True,
|
|
37
|
+
expand_message_examples: bool = True,
|
|
38
|
+
asyncapi_js_url: str = ASYNCAPI_JS_DEFAULT_URL,
|
|
39
|
+
asyncapi_css_url: str = ASYNCAPI_CSS_DEFAULT_URL,
|
|
40
|
+
try_it_out_plugin_url: str = ASYNCAPI_TRY_IT_PLUGIN_URL,
|
|
41
|
+
) -> None:
|
|
42
|
+
self.path = path
|
|
43
|
+
|
|
44
|
+
if isinstance(asyncapi_json_path, _Empty):
|
|
45
|
+
asyncapi_json_path = path.rstrip("/") + ".json"
|
|
46
|
+
self.asyncapi_json_path = asyncapi_json_path
|
|
47
|
+
|
|
48
|
+
if isinstance(asyncapi_yaml_path, _Empty):
|
|
49
|
+
asyncapi_yaml_path = path.rstrip("/") + ".yaml"
|
|
50
|
+
self.asyncapi_yaml_path = asyncapi_yaml_path
|
|
51
|
+
|
|
52
|
+
if isinstance(try_it_out_path, _Empty):
|
|
53
|
+
try_it_out_path = path.rstrip("/") + "/try"
|
|
54
|
+
self.try_it_out_path = try_it_out_path
|
|
55
|
+
|
|
56
|
+
self.description = description
|
|
57
|
+
self.tags = tags
|
|
58
|
+
self.unique_id = unique_id
|
|
59
|
+
self.include_in_schema = include_in_schema
|
|
60
|
+
|
|
61
|
+
self.sidebar = sidebar
|
|
62
|
+
self.info = info
|
|
63
|
+
self.servers = servers
|
|
64
|
+
self.operations = operations
|
|
65
|
+
self.messages = messages
|
|
66
|
+
self.schemas = schemas
|
|
67
|
+
self.errors = errors
|
|
68
|
+
self.expand_message_examples = expand_message_examples
|
|
69
|
+
self.asyncapi_js_url = asyncapi_js_url
|
|
70
|
+
self.asyncapi_css_url = asyncapi_css_url
|
|
71
|
+
self.try_it_out_plugin_url = try_it_out_plugin_url
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Annotated, Any
|
|
3
|
+
|
|
4
|
+
from fastapi import params
|
|
5
|
+
|
|
6
|
+
from faststream_fastapi._internal.fs_re_exports.constants import EMPTY
|
|
7
|
+
from faststream_fastapi._internal.fs_re_exports.context import (
|
|
8
|
+
resolve_context_by_name,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def Context( # noqa: N802
|
|
13
|
+
name: str = "",
|
|
14
|
+
*,
|
|
15
|
+
default: Any = EMPTY,
|
|
16
|
+
initial: Callable[..., Any] | None = None,
|
|
17
|
+
) -> Any:
|
|
18
|
+
def solve_context(
|
|
19
|
+
context: Annotated[
|
|
20
|
+
Any,
|
|
21
|
+
params.Header(alias="context__", include_in_schema=False),
|
|
22
|
+
],
|
|
23
|
+
) -> Any:
|
|
24
|
+
return resolve_context_by_name(
|
|
25
|
+
name=name,
|
|
26
|
+
default=default,
|
|
27
|
+
initial=initial,
|
|
28
|
+
context=context,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return params.Depends(solve_context, use_cache=True)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI
|
|
7
|
+
from fastapi.params import Depends
|
|
8
|
+
from faststream.message import StreamMessage
|
|
9
|
+
from faststream.specification.base import SpecificationFactory
|
|
10
|
+
from starlette.types import Receive, Scope, Send
|
|
11
|
+
|
|
12
|
+
from faststream_fastapi._internal.asyncapi_router import AsyncAPIRouter
|
|
13
|
+
from faststream_fastapi._internal.background_middleware import _BackgroundMiddleware
|
|
14
|
+
from faststream_fastapi._internal.config import Config
|
|
15
|
+
from faststream_fastapi._internal.fs_re_exports.application import StartAbleApplication
|
|
16
|
+
from faststream_fastapi._internal.fs_re_exports.broker import BrokerUsecase
|
|
17
|
+
from faststream_fastapi._internal.fs_re_exports.context import ContextRepo
|
|
18
|
+
from faststream_fastapi._internal.fs_re_exports.di import FastDependsConfig
|
|
19
|
+
from faststream_fastapi._internal.get_dependant import get_fastapi_dependant
|
|
20
|
+
from faststream_fastapi._internal.wrap_callable_to_fastapi_compatible import (
|
|
21
|
+
wrap_callable_to_fastapi_compatible,
|
|
22
|
+
)
|
|
23
|
+
from faststream_fastapi.asyncapi_config import AsyncAPIConfig
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _subscriber_compatibility_wrapper(
|
|
27
|
+
config: Config,
|
|
28
|
+
context: ContextRepo,
|
|
29
|
+
dependencies: Iterable[Depends],
|
|
30
|
+
) -> Callable[[Callable[..., Any]], Callable[["StreamMessage[Any]"], Awaitable[Any]]]:
|
|
31
|
+
def subscriber_compatibility_wrapper(
|
|
32
|
+
endpoint: Callable[..., Any],
|
|
33
|
+
) -> Callable[["StreamMessage[Any]"], Awaitable[Any]]:
|
|
34
|
+
return wrap_callable_to_fastapi_compatible(
|
|
35
|
+
user_callable=endpoint,
|
|
36
|
+
config=config,
|
|
37
|
+
context=context,
|
|
38
|
+
dependencies=dependencies,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return subscriber_compatibility_wrapper
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class FastStreamAPI:
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
*brokers: BrokerUsecase[Any, Any],
|
|
48
|
+
application: FastAPI,
|
|
49
|
+
context: ContextRepo | None = None,
|
|
50
|
+
# AsyncAPI
|
|
51
|
+
specification: SpecificationFactory | None = None,
|
|
52
|
+
asyncapi_path: str | AsyncAPIConfig | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
self._application = application
|
|
55
|
+
|
|
56
|
+
self._brokers = brokers
|
|
57
|
+
|
|
58
|
+
self._startable_application = StartAbleApplication(
|
|
59
|
+
*brokers,
|
|
60
|
+
specification=specification,
|
|
61
|
+
config=FastDependsConfig(
|
|
62
|
+
get_dependent=get_fastapi_dependant,
|
|
63
|
+
context=context or ContextRepo(),
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
self._config = Config(
|
|
68
|
+
application=application,
|
|
69
|
+
dependency_overrides_provider=self._application,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if asyncapi_path is not None:
|
|
73
|
+
if isinstance(asyncapi_path, str):
|
|
74
|
+
asyncapi_config = AsyncAPIConfig(asyncapi_path)
|
|
75
|
+
else:
|
|
76
|
+
asyncapi_config = asyncapi_path
|
|
77
|
+
|
|
78
|
+
else:
|
|
79
|
+
asyncapi_config = None
|
|
80
|
+
|
|
81
|
+
self._asyncapi_config = asyncapi_config
|
|
82
|
+
|
|
83
|
+
for broker in self._brokers:
|
|
84
|
+
broker.config.add_middleware(_BackgroundMiddleware)
|
|
85
|
+
|
|
86
|
+
for subscriber in broker.subscribers:
|
|
87
|
+
dependencies = (
|
|
88
|
+
*broker.config.broker_dependencies,
|
|
89
|
+
*subscriber._call_options.dependencies,
|
|
90
|
+
)
|
|
91
|
+
subscriber._call_decorators = (
|
|
92
|
+
_subscriber_compatibility_wrapper(
|
|
93
|
+
config=self._config,
|
|
94
|
+
context=self._startable_application.context,
|
|
95
|
+
dependencies=dependencies, # type: ignore[arg-type]
|
|
96
|
+
),
|
|
97
|
+
*subscriber._call_decorators,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# For FastStream docs gen
|
|
101
|
+
@property
|
|
102
|
+
def schema(self) -> SpecificationFactory:
|
|
103
|
+
return self._startable_application.schema
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def context(self) -> ContextRepo:
|
|
107
|
+
return self._startable_application.context
|
|
108
|
+
|
|
109
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
110
|
+
if scope["type"] == "lifespan":
|
|
111
|
+
await self.lifespan(scope, receive, send)
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
return await self._application(scope, receive, send)
|
|
115
|
+
|
|
116
|
+
async def lifespan(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
117
|
+
started = False
|
|
118
|
+
app: Any = scope.get("app")
|
|
119
|
+
await receive()
|
|
120
|
+
try:
|
|
121
|
+
async with (
|
|
122
|
+
self._lifespan_context(app),
|
|
123
|
+
self._application.router.lifespan_context(app) as maybe_state,
|
|
124
|
+
):
|
|
125
|
+
if maybe_state is not None:
|
|
126
|
+
if "state" not in scope:
|
|
127
|
+
msg = 'The server does not support "state" in the lifespan scope.'
|
|
128
|
+
raise RuntimeError(msg) # noqa: TRY301
|
|
129
|
+
|
|
130
|
+
scope["state"].update(maybe_state)
|
|
131
|
+
|
|
132
|
+
await send({"type": "lifespan.startup.complete"})
|
|
133
|
+
started = True
|
|
134
|
+
await receive()
|
|
135
|
+
except BaseException:
|
|
136
|
+
exc_text = traceback.format_exc()
|
|
137
|
+
if started:
|
|
138
|
+
await send({"type": "lifespan.shutdown.failed", "message": exc_text})
|
|
139
|
+
else:
|
|
140
|
+
await send({"type": "lifespan.startup.failed", "message": exc_text})
|
|
141
|
+
raise
|
|
142
|
+
else:
|
|
143
|
+
await send({"type": "lifespan.shutdown.complete"})
|
|
144
|
+
|
|
145
|
+
@asynccontextmanager
|
|
146
|
+
async def _lifespan_context(self, application: Any) -> AsyncIterator[None]:
|
|
147
|
+
if self._asyncapi_config is not None:
|
|
148
|
+
asyncapi_router = AsyncAPIRouter(
|
|
149
|
+
brokers=self._brokers,
|
|
150
|
+
config=self._asyncapi_config,
|
|
151
|
+
schema=self._startable_application.schema,
|
|
152
|
+
)
|
|
153
|
+
self._application.include_router(asyncapi_router)
|
|
154
|
+
|
|
155
|
+
started_brokers: list[BrokerUsecase[Any, Any]] = []
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
for broker in self._brokers:
|
|
159
|
+
await broker.start()
|
|
160
|
+
started_brokers.append(broker)
|
|
161
|
+
|
|
162
|
+
yield None
|
|
163
|
+
finally:
|
|
164
|
+
for started_broker in started_brokers:
|
|
165
|
+
await started_broker.stop()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from fastapi import Request
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StreamMessage(Request):
|
|
7
|
+
scope: dict[str, Any]
|
|
8
|
+
_cookies: dict[str, Any]
|
|
9
|
+
_headers: dict[str, Any] # type: ignore[assignment]
|
|
10
|
+
_body: dict[str, Any] | list[Any] # type: ignore[assignment]
|
|
11
|
+
_query_params: dict[str, Any] # type: ignore[assignment]
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
*,
|
|
16
|
+
body: dict[str, Any] | list[Any],
|
|
17
|
+
headers: dict[str, Any],
|
|
18
|
+
path: dict[str, Any],
|
|
19
|
+
) -> None:
|
|
20
|
+
self._headers = headers
|
|
21
|
+
self._body = body
|
|
22
|
+
self._query_params = path
|
|
23
|
+
|
|
24
|
+
self.scope = {"path_params": self._query_params}
|
|
25
|
+
self._cookies = {}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: faststream_fastapi
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: FastAPI integration for FastStream
|
|
5
|
+
Keywords: rabbitmq,kafka,nats,redis,mqtt,asyncapi,framework,message brokers,fastapi
|
|
6
|
+
Author: Ivan Kirpichnikov
|
|
7
|
+
Author-email: Ivan Kirpichnikov <mmssvvvv570@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
24
|
+
Classifier: Topic :: Software Development
|
|
25
|
+
Classifier: Topic :: System :: Networking
|
|
26
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Classifier: Intended Audience :: Developers
|
|
29
|
+
Classifier: Intended Audience :: Information Technology
|
|
30
|
+
Classifier: Intended Audience :: System Administrators
|
|
31
|
+
Classifier: Environment :: Web Environment
|
|
32
|
+
Classifier: Framework :: AsyncIO
|
|
33
|
+
Classifier: Framework :: Pydantic
|
|
34
|
+
Classifier: Framework :: Pydantic :: 1
|
|
35
|
+
Classifier: Framework :: Pydantic :: 2
|
|
36
|
+
Requires-Dist: fastapi<1.0.0
|
|
37
|
+
Requires-Dist: faststream>=0.7.0
|
|
38
|
+
Requires-Python: >=3.10
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# FastAPI Plugin for FastStream
|
|
42
|
+
|
|
43
|
+
A plugin that allows you to use **Depends** and **FastAPI** other objects in the **FastStream**
|
|
44
|
+
|
|
45
|
+
# Features
|
|
46
|
+
|
|
47
|
+
### Use FastAPI Dependency Injection
|
|
48
|
+
In **FastStream** handlers, it will be possible to use the familiar DI from **FastAPI**
|
|
49
|
+
```py
|
|
50
|
+
from fastapi import Path, Body, Header, Depends
|
|
51
|
+
|
|
52
|
+
class BodyModel(BaseModel):
|
|
53
|
+
field: int
|
|
54
|
+
|
|
55
|
+
@broker.subscriber("subject.{num}")
|
|
56
|
+
async def subscriber_handler(
|
|
57
|
+
num: Annotated[int, Path()],
|
|
58
|
+
body: Annotated[BodyModel, Body()],
|
|
59
|
+
x_user_id: Annotated[int, Header()],
|
|
60
|
+
my_dep: Annotated[int, Depends(int)],
|
|
61
|
+
) -> None:
|
|
62
|
+
...
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Сan use FastAPI dependency overrides
|
|
66
|
+
```py
|
|
67
|
+
fastapi = FastAPI()
|
|
68
|
+
fastapi.dependency_overrides[Dep] = lambda: "Dep"
|
|
69
|
+
|
|
70
|
+
@broker.subscriber("subject")
|
|
71
|
+
async def subscriber(dep: Annotated[str, De[]]) -> None:
|
|
72
|
+
assert dep == "Dep"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Using the FastStream Context
|
|
76
|
+
```py
|
|
77
|
+
from faststream import Context
|
|
78
|
+
|
|
79
|
+
@broker.subscriber("subject")
|
|
80
|
+
async def subscriber_handler(context_data: Annotated[int, Context("data")]) -> Response: ...
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Use FastAPI Request and Response
|
|
84
|
+
```py
|
|
85
|
+
from fastapi import Request, Response
|
|
86
|
+
from fastapi.responses import JSONResponse
|
|
87
|
+
|
|
88
|
+
@broker.subscriber("subject")
|
|
89
|
+
async def subscriber_handler(request: Request) -> Response:
|
|
90
|
+
return JSONResponse({"data": 1})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Minimalistic plugin connection to your application
|
|
94
|
+
|
|
95
|
+
All you need to do is wrap the **FastAPI** with the **FastStreamAPI** object from the **plugin**
|
|
96
|
+
|
|
97
|
+
```py
|
|
98
|
+
application = FastStreamAPI(
|
|
99
|
+
NatsBroker(),
|
|
100
|
+
application=FastAPI(),
|
|
101
|
+
)
|
|
102
|
+
uvicorn.run(application)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Managing a lifespan state
|
|
106
|
+
Now the **lifespan state** is also available in **FastStream handlers**
|
|
107
|
+
|
|
108
|
+
```py
|
|
109
|
+
@asynccontextmanager
|
|
110
|
+
async def lifespan(app: FastAPI):
|
|
111
|
+
yield {"lifespan_data": "LIFESPAN DATA"}
|
|
112
|
+
|
|
113
|
+
@broker.subscriber("subject")
|
|
114
|
+
async def subscriber(request: Request) -> None:
|
|
115
|
+
assert request.state.lifespan_data == "LIFESPAN DATA"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### The ability to configure AsyncAPI
|
|
119
|
+
|
|
120
|
+
The ability to configure **AsyncAPI** using **SpecificationFactory** from **FastStream** and **AsyncAPIConfig** from the **plugin**
|
|
121
|
+
|
|
122
|
+
```py
|
|
123
|
+
FastStreamAPI(
|
|
124
|
+
...,
|
|
125
|
+
specification=AsyncAPI(
|
|
126
|
+
title="My app",
|
|
127
|
+
version="1.0.0",
|
|
128
|
+
description="...",
|
|
129
|
+
...,
|
|
130
|
+
),
|
|
131
|
+
asyncapi_path="/fs_docs",
|
|
132
|
+
# or
|
|
133
|
+
asyncapi_path=AsyncAPIRouter(
|
|
134
|
+
"/fs_docs",
|
|
135
|
+
description="...",
|
|
136
|
+
...
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Backgrounds tasks
|
|
142
|
+
|
|
143
|
+
You can use **BackgroundTasks** from **FastAPI** in **FastStream handlers**
|
|
144
|
+
|
|
145
|
+
```py
|
|
146
|
+
@broker.subscriber("subject")
|
|
147
|
+
async def handler1(
|
|
148
|
+
tasks: BackgroundTasks,
|
|
149
|
+
) -> None:
|
|
150
|
+
tasks.add_task(...)
|
|
151
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
faststream_fastapi/__about__.py,sha256=oYISj9X_HBZogY0KMHgJ8v3EPboAd_XANLfWjFBtwMc,136
|
|
2
|
+
faststream_fastapi/__init__.py,sha256=Trs7HiP8zCim_vz7lyzN-7jDbLFNL2Vmes0lKmHsjGI,323
|
|
3
|
+
faststream_fastapi/_internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
faststream_fastapi/_internal/asyncapi_router.py,sha256=BIwHuAby1mhTQuoTRMWY2X2oa7_GGORB329Lj6ouXtc,4299
|
|
5
|
+
faststream_fastapi/_internal/background_middleware.py,sha256=3IvdDQ1ZBIzgIzeFamc2B4Z2YJMEAzdI7M96Ndvl7y0,717
|
|
6
|
+
faststream_fastapi/_internal/config.py,sha256=YmFNnpk6FpOTM_JGJgSbSE-MbT-pMh5SxBD-1yKEOvc,571
|
|
7
|
+
faststream_fastapi/_internal/fastapi_compat.py,sha256=0ChnQV7FbB2OsEWPwb74rDqwfeFMjXf8FN7qEkYjqnk,4362
|
|
8
|
+
faststream_fastapi/_internal/fs_re_exports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
faststream_fastapi/_internal/fs_re_exports/_compat.py,sha256=h7xdEqvop1FfFzKB4kZBsHtRSdNNXIRQNt7BYJQPHms,131
|
|
10
|
+
faststream_fastapi/_internal/fs_re_exports/application.py,sha256=ocziVIpGpB0IgOw_kimZAVdCF0OzBdTLCts3e5QlpOE,103
|
|
11
|
+
faststream_fastapi/_internal/fs_re_exports/broker.py,sha256=6mZ08_R_Z9e3qbDSYx_UdB8mQY1M_jJlU9ZrPT7EXW4,159
|
|
12
|
+
faststream_fastapi/_internal/fs_re_exports/constants.py,sha256=mJ8kiBNoF2VstW6sZ4LZY44Bwx1AUKJhb5mbd5DJMEE,71
|
|
13
|
+
faststream_fastapi/_internal/fs_re_exports/context.py,sha256=ep53p8_UYAgUVxFw7QAvqiZjbNfQ1QMLahriUraB_ZU,215
|
|
14
|
+
faststream_fastapi/_internal/fs_re_exports/di.py,sha256=x38rHqtwmrOduz7W31_Hke_7vXlKIGVtAQh-ej52w_0,88
|
|
15
|
+
faststream_fastapi/_internal/fs_re_exports/logger.py,sha256=SMxqu9YwIwLBa9hvzSFM-qYW_B48AsdhP1uL3LV1vEo,86
|
|
16
|
+
faststream_fastapi/_internal/get_dependant.py,sha256=ObQvRmywHTNM7c7zQM6VAtirCBtJhxH2f7-6DMxfcSk,5997
|
|
17
|
+
faststream_fastapi/_internal/logger.py,sha256=qaa8lXlQxtrp9hzDZHOijTBH0cKiV-2ZpUEbnjjdrUc,258
|
|
18
|
+
faststream_fastapi/_internal/replace_context.py,sha256=nCLVn8kTV9SuGS_whbyzXYi-pq96x8Vft5cll9vCpMo,3697
|
|
19
|
+
faststream_fastapi/_internal/wrap_callable_to_fastapi_compatible.py,sha256=LTotExCxEeqLEI7KAx82JSyzmz1PoXV8fpbvVqGPkfQ,5889
|
|
20
|
+
faststream_fastapi/asyncapi_config.py,sha256=2D5Jc8ByUztmMn12a2fcNWtvfHZPeIhffbW6bhUO9lM,2294
|
|
21
|
+
faststream_fastapi/context.py,sha256=bcE0DB8lQMPtCO3eTP3bNu1i_YL8uIOViwmk8IMiSGs,788
|
|
22
|
+
faststream_fastapi/faststream_api.py,sha256=RLXcwA5hqyp6Q90siNg_LGpXs3r_kYqnwtaqzH2Ptmw,6021
|
|
23
|
+
faststream_fastapi/stream_message.py,sha256=1k2vzUGL76wSJjbpPwL03LTBctRtD0syBlLqUuK5MpQ,665
|
|
24
|
+
faststream_fastapi-1.0.0.dist-info/licenses/LICENSE,sha256=ZDtF8IsAs002GXg_yHpuS_RLkXDOOjKFuGFSTYUiTY4,1074
|
|
25
|
+
faststream_fastapi-1.0.0.dist-info/WHEEL,sha256=8ZlpUMJ7mlDirmlHRhDirEx_nPnARrwDjeE92mlk68E,81
|
|
26
|
+
faststream_fastapi-1.0.0.dist-info/METADATA,sha256=UrIblBJ44uDZuCgiuNtzCkTFSTTlg6L6al3nNvmqNCM,4354
|
|
27
|
+
faststream_fastapi-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivan Kirpichnikov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|