sillo-framework 0.0.1a1__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.
- sillo/__init__.py +138 -0
- sillo/_internals/__init__.py +0 -0
- sillo/_internals/_middleware.py +546 -0
- sillo/admin/__init__.py +232 -0
- sillo/admin/auth.py +167 -0
- sillo/admin/models.py +153 -0
- sillo/admin/registry.py +302 -0
- sillo/admin/router.py +463 -0
- sillo/admin/routes.py +1734 -0
- sillo/admin/static/logo.png +0 -0
- sillo/admin/templates/base.html +497 -0
- sillo/admin/templates/create.html +76 -0
- sillo/admin/templates/dashboard.html +45 -0
- sillo/admin/templates/delete.html +15 -0
- sillo/admin/templates/detail.html +61 -0
- sillo/admin/templates/list.html +117 -0
- sillo/admin/templates/login.html +54 -0
- sillo/admin/templates/query.html +56 -0
- sillo/admin/templates/update.html +85 -0
- sillo/admin/templating.py +44 -0
- sillo/application.py +2582 -0
- sillo/auth/__init__.py +47 -0
- sillo/auth/apikey/__init__.py +71 -0
- sillo/auth/apikey/backend.py +104 -0
- sillo/auth/apikey/mixins.py +132 -0
- sillo/auth/apikey/models.py +308 -0
- sillo/auth/backend.py +117 -0
- sillo/auth/decorator.py +277 -0
- sillo/auth/exceptions.py +197 -0
- sillo/auth/jwt_auth/__init__.py +87 -0
- sillo/auth/jwt_auth/backend.py +153 -0
- sillo/auth/jwt_auth/mixins.py +237 -0
- sillo/auth/jwt_auth/models.py +266 -0
- sillo/auth/jwt_auth/tokens.py +301 -0
- sillo/auth/middleware.py +158 -0
- sillo/auth/model.py +35 -0
- sillo/auth/session_auth/__init__.py +27 -0
- sillo/auth/session_auth/backend.py +166 -0
- sillo/auth/session_auth/guard.py +285 -0
- sillo/auth/session_auth/mixins.py +164 -0
- sillo/auth/session_auth/models.py +185 -0
- sillo/auth/use_auth.py +276 -0
- sillo/cache/__init__.py +53 -0
- sillo/cache/backends.py +835 -0
- sillo/cache/base.py +644 -0
- sillo/cache/config.py +163 -0
- sillo/cache/decorator.py +355 -0
- sillo/config/__init__.py +25 -0
- sillo/config/core.py +117 -0
- sillo/core/__init__.py +6 -0
- sillo/core/converters.py +572 -0
- sillo/core/dependencies/__init__.py +11 -0
- sillo/core/dependencies/base.py +583 -0
- sillo/core/encoding.py +395 -0
- sillo/core/error/__init__.py +3 -0
- sillo/core/error/handler.py +1606 -0
- sillo/core/helpers/__init__.py +0 -0
- sillo/core/helpers/async_helpers.py +257 -0
- sillo/core/helpers/deprecation.py +208 -0
- sillo/core/http/__init__.py +4 -0
- sillo/core/http/cookies.py +53 -0
- sillo/core/http/request.py +1627 -0
- sillo/core/http/response.py +1740 -0
- sillo/core/routing/__init__.py +12 -0
- sillo/core/routing/_utils.py +66 -0
- sillo/core/routing/base.py +266 -0
- sillo/core/routing/grouping.py +263 -0
- sillo/core/routing/router.py +3390 -0
- sillo/core/routing/websocket.py +309 -0
- sillo/decorator_helper.py +104 -0
- sillo/encoding.py +31 -0
- sillo/events/__init__.py +35 -0
- sillo/events/core.py +867 -0
- sillo/events/emitter.py +663 -0
- sillo/events/enums.py +53 -0
- sillo/events/exceptions.py +106 -0
- sillo/events/mixins.py +109 -0
- sillo/events/transports/__init__.py +183 -0
- sillo/events/transports/base.py +579 -0
- sillo/events/transports/memory.py +77 -0
- sillo/events/transports/persistent.py +288 -0
- sillo/events/transports/record.py +231 -0
- sillo/events/transports/redis.py +242 -0
- sillo/events/types.py +141 -0
- sillo/exception_handler.py +407 -0
- sillo/exceptions.py +242 -0
- sillo/formparser.py +683 -0
- sillo/frontend.py +211 -0
- sillo/graphql/__init__.py +3 -0
- sillo/graphql/handler.py +326 -0
- sillo/handlers/__init__.py +0 -0
- sillo/handlers/not_found.py +162 -0
- sillo/hashing/__init__.py +74 -0
- sillo/hashing/config.py +74 -0
- sillo/hashing/core.py +259 -0
- sillo/hashing/exceptions.py +19 -0
- sillo/hashing/utils.py +166 -0
- sillo/helpers/__init__.py +6 -0
- sillo/helpers/crypto.py +250 -0
- sillo/helpers/files.py +412 -0
- sillo/helpers/hashing.py +189 -0
- sillo/helpers/html.py +253 -0
- sillo/helpers/jwt.py +434 -0
- sillo/helpers/network.py +294 -0
- sillo/helpers/retry.py +289 -0
- sillo/helpers/strings.py +320 -0
- sillo/helpers/text.py +262 -0
- sillo/http/__init__.py +133 -0
- sillo/http/accepts.py +1312 -0
- sillo/http/client/__init__.py +104 -0
- sillo/http/client/caching.py +202 -0
- sillo/http/client/client.py +578 -0
- sillo/http/client/config.py +120 -0
- sillo/http/client/errors.py +101 -0
- sillo/http/client/middleware.py +134 -0
- sillo/http/client/models.py +120 -0
- sillo/http/client/retry.py +85 -0
- sillo/http/client/transport.py +56 -0
- sillo/http/client/utils.py +85 -0
- sillo/http/etag.py +174 -0
- sillo/http/lifecycle/__init__.py +37 -0
- sillo/http/lifecycle/context.py +248 -0
- sillo/http/lifecycle/helpers.py +184 -0
- sillo/http/lifecycle/middleware.py +205 -0
- sillo/http/status.py +138 -0
- sillo/logging.py +187 -0
- sillo/mail/__init__.py +13 -0
- sillo/mail/client.py +404 -0
- sillo/mail/config.py +156 -0
- sillo/mail/models.py +225 -0
- sillo/middleware/__init__.py +5 -0
- sillo/middleware/base.py +168 -0
- sillo/middleware/gzip.py +297 -0
- sillo/middleware/security.py +7 -0
- sillo/middleware/utils.py +112 -0
- sillo/normalize/__init__.py +33 -0
- sillo/normalize/helpers.py +308 -0
- sillo/normalize/middleware.py +298 -0
- sillo/objects/__init__.py +38 -0
- sillo/objects/common.py +209 -0
- sillo/objects/datastructures.py +444 -0
- sillo/objects/http.py +1063 -0
- sillo/objects/routing.py +778 -0
- sillo/openapi/__init__.py +2 -0
- sillo/openapi/_builder.py +787 -0
- sillo/openapi/config.py +173 -0
- sillo/openapi/models.py +767 -0
- sillo/openapi/utils.py +47 -0
- sillo/pagination.py +540 -0
- sillo/parameters.py +39 -0
- sillo/permissions/__init__.py +17 -0
- sillo/permissions/mixins.py +225 -0
- sillo/permissions/models.py +726 -0
- sillo/py.typed +0 -0
- sillo/record/__init__.py +83 -0
- sillo/record/casting.py +247 -0
- sillo/record/collection.py +448 -0
- sillo/record/commands/__init__.py +98 -0
- sillo/record/config.py +156 -0
- sillo/record/events.py +275 -0
- sillo/record/exceptions.py +113 -0
- sillo/record/factories.py +134 -0
- sillo/record/fields.py +176 -0
- sillo/record/helpers.py +321 -0
- sillo/record/logging.py +207 -0
- sillo/record/manager.py +265 -0
- sillo/record/mixins/__init__.py +240 -0
- sillo/record/models.py +417 -0
- sillo/record/pagination.py +107 -0
- sillo/record/pydantic.py +98 -0
- sillo/record/queries.py +156 -0
- sillo/record/scopes.py +135 -0
- sillo/record/transactions.py +115 -0
- sillo/route_builder.py +252 -0
- sillo/security/__init__.py +50 -0
- sillo/security/cors/__init__.py +4 -0
- sillo/security/cors/_middleware.py +332 -0
- sillo/security/cors/config.py +245 -0
- sillo/security/csrf/__init__.py +4 -0
- sillo/security/csrf/_middleware.py +184 -0
- sillo/security/csrf/config.py +230 -0
- sillo/security/ratelimit/__init__.py +105 -0
- sillo/security/ratelimit/_middleware.py +147 -0
- sillo/security/ratelimit/backends/__init__.py +44 -0
- sillo/security/ratelimit/backends/base.py +65 -0
- sillo/security/ratelimit/backends/memory.py +85 -0
- sillo/security/ratelimit/backends/record.py +65 -0
- sillo/security/ratelimit/backends/redis.py +125 -0
- sillo/security/ratelimit/config.py +102 -0
- sillo/security/ratelimit/models.py +102 -0
- sillo/security/ratelimit/strategies/__init__.py +48 -0
- sillo/security/ratelimit/strategies/base.py +69 -0
- sillo/security/ratelimit/strategies/fixed_window.py +75 -0
- sillo/security/ratelimit/strategies/sliding_window.py +74 -0
- sillo/security/ratelimit/strategies/token_bucket.py +76 -0
- sillo/security/shield.py +323 -0
- sillo/session/__init__.py +4 -0
- sillo/session/base.py +160 -0
- sillo/session/config.py +201 -0
- sillo/session/file.py +152 -0
- sillo/session/middleware.py +122 -0
- sillo/session/session_objects.py +392 -0
- sillo/session/signed_cookies.py +114 -0
- sillo/static.py +252 -0
- sillo/templating/__init__.py +150 -0
- sillo/templating/middleware.py +105 -0
- sillo/templating/utils.py +124 -0
- sillo/testclient/__init__.py +17 -0
- sillo/testclient/_internal/__init__.py +0 -0
- sillo/testclient/_internal/inputs.py +54 -0
- sillo/testclient/_internal/transport.py +827 -0
- sillo/testclient/_internal/types.py +15 -0
- sillo/testclient/_internal/utils.py +78 -0
- sillo/testclient/_internal/websockets.py +303 -0
- sillo/testclient/async_client.py +461 -0
- sillo/testclient/base.py +639 -0
- sillo/testclient/exceptions.py +34 -0
- sillo/testclient/helpers.py +148 -0
- sillo/types.py +41 -0
- sillo/users/__init__.py +34 -0
- sillo/users/base.py +749 -0
- sillo/users/managers.py +164 -0
- sillo/users/simple.py +169 -0
- sillo/utils/__init__.py +1 -0
- sillo/utils/concurrency.py +41 -0
- sillo/validation/__init__.py +89 -0
- sillo/validation/compiler.py +521 -0
- sillo/validation/errors.py +121 -0
- sillo/validation/fields.py +666 -0
- sillo/websockets/__init__.py +26 -0
- sillo/websockets/base.py +390 -0
- sillo/websockets/channels.py +338 -0
- sillo/websockets/consumers.py +250 -0
- sillo/websockets/errors.py +80 -0
- sillo/websockets/history.py +146 -0
- sillo/websockets/status.py +66 -0
- sillo/websockets/utils.py +84 -0
- sillo/work/__init__.py +67 -0
- sillo/work/backends.py +586 -0
- sillo/work/background/__init__.py +11 -0
- sillo/work/background/supervisor.py +161 -0
- sillo/work/background/tasks.py +258 -0
- sillo/work/dependency.py +59 -0
- sillo/work/middleware.py +291 -0
- sillo/work/queue/__init__.py +51 -0
- sillo/work/queue/batches.py +219 -0
- sillo/work/queue/connection.py +412 -0
- sillo/work/queue/events.py +236 -0
- sillo/work/queue/failed.py +183 -0
- sillo/work/queue/job.py +334 -0
- sillo/work/queue/listener.py +215 -0
- sillo/work/queue/middleware.py +229 -0
- sillo/work/queue/payloads.py +95 -0
- sillo/work/queue/workers.py +333 -0
- sillo/work/scheduler/__init__.py +34 -0
- sillo/work/scheduler/cron.py +125 -0
- sillo/work/scheduler/jobs.py +167 -0
- sillo/work/scheduler/manager.py +306 -0
- sillo/work/scheduler/middleware.py +117 -0
- sillo/work/scheduler/triggers.py +170 -0
- sillo/work/task.py +670 -0
- sillo/work/types.py +408 -0
- sillo_framework-0.0.1a1.dist-info/METADATA +293 -0
- sillo_framework-0.0.1a1.dist-info/RECORD +266 -0
- sillo_framework-0.0.1a1.dist-info/WHEEL +4 -0
- sillo_framework-0.0.1a1.dist-info/licenses/LICENSE +27 -0
sillo/__init__.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sillo — The Platform for Python Backends
|
|
3
|
+
|
|
4
|
+
A modern ASGI web framework. Async-native. Zero boilerplate validation.
|
|
5
|
+
Built-in DI with pre-flattened execution plans. Everything you need — routing,
|
|
6
|
+
middleware, auth, CORS, CSRF, sessions, caching, GraphQL — ships as first-party.
|
|
7
|
+
|
|
8
|
+
Key Features:
|
|
9
|
+
- ASGI-based, async/await throughout
|
|
10
|
+
- Dependency injection with pre-flattened execution plan (zero recursion at runtime)
|
|
11
|
+
- Pydantic validation on every input and output — no type annotations needed
|
|
12
|
+
- Fluent Responder for building HTTP responses (json, html, file, stream, redirect)
|
|
13
|
+
- Middleware system: CORS, CSRF, sessions, auth, rate limiting, compression
|
|
14
|
+
- Depend(get_request=True) to inject the raw Request into any dependency
|
|
15
|
+
- GraphQL support via Strawberry (sillo.graphql)
|
|
16
|
+
- WebSocket support with type safety
|
|
17
|
+
- Flexible routing with path parameters and type conversion
|
|
18
|
+
- OpenAPI documentation generation
|
|
19
|
+
- Testing utilities with TestClient
|
|
20
|
+
|
|
21
|
+
Quick Start:
|
|
22
|
+
from sillo import silloApp
|
|
23
|
+
from pydantic import BaseModel
|
|
24
|
+
|
|
25
|
+
app = silloApp(title="My API", version="1.0.0")
|
|
26
|
+
|
|
27
|
+
@app.get("/hello/{name}")
|
|
28
|
+
async def hello(request, response, name: str):
|
|
29
|
+
return response.json({"message": f"Hello, {name}!"})
|
|
30
|
+
|
|
31
|
+
Common Patterns:
|
|
32
|
+
|
|
33
|
+
1. Validation (zero annotations — the type lives on the declaration):
|
|
34
|
+
from sillo import Query, Path, Form, File
|
|
35
|
+
|
|
36
|
+
class UserCreate(BaseModel):
|
|
37
|
+
name: str
|
|
38
|
+
email: str
|
|
39
|
+
|
|
40
|
+
@app.post("/teams/{team_id}/users",
|
|
41
|
+
request_model=UserCreate, # JSON body
|
|
42
|
+
response_model=UserOut) # shapes the reply
|
|
43
|
+
async def create_user(request, response, user, # <- the body
|
|
44
|
+
team_id=Path(type=int), # path segment
|
|
45
|
+
notify=Query(False, type=bool),
|
|
46
|
+
db=Depend(get_db)):
|
|
47
|
+
return await save(user, team_id, db)
|
|
48
|
+
|
|
49
|
+
The JSON body is declared once, on the decorator, with request_model=. It
|
|
50
|
+
is injected into the first plain parameter after request/response, and also
|
|
51
|
+
available as request.validated_data. It composes freely with Depend and
|
|
52
|
+
with parameter markers.
|
|
53
|
+
|
|
54
|
+
Every other location has a marker: Query, Header, Cookie, Path, Form, File.
|
|
55
|
+
Constraints go on the marker — Query(1, type=int, ge=1, le=100) — and feed
|
|
56
|
+
both validation and the generated OpenAPI schema, so the published contract
|
|
57
|
+
and the enforced one cannot drift apart.
|
|
58
|
+
|
|
59
|
+
Bad input returns 422 naming the location that failed:
|
|
60
|
+
{"detail": [{"loc": ["query", "page"], "msg": "...", "type": "..."}]}
|
|
61
|
+
|
|
62
|
+
Markers written the old way — Query(1), Header(), Cookie("dark") — keep
|
|
63
|
+
their original behavior. Pass silloApp(strict_validation=True) to validate
|
|
64
|
+
those too, and to get the unified error shape for request_model bodies.
|
|
65
|
+
|
|
66
|
+
2. Dependency Injection:
|
|
67
|
+
from sillo import Depend
|
|
68
|
+
|
|
69
|
+
async def get_db():
|
|
70
|
+
return Database()
|
|
71
|
+
|
|
72
|
+
@app.get("/items")
|
|
73
|
+
async def list_items(request, response, db=Depend(get_db)):
|
|
74
|
+
return response.json(await db.query("SELECT * FROM items"))
|
|
75
|
+
|
|
76
|
+
# Inject Request into any dependency:
|
|
77
|
+
def get_auth(req=Depend(get_request=True)):
|
|
78
|
+
return req.headers.get("Authorization")
|
|
79
|
+
|
|
80
|
+
3. Middleware:
|
|
81
|
+
app.use(CORSMiddleware(config=CorsConfig(allow_origins=["*"])))
|
|
82
|
+
app.use(RateLimitMiddleware(rate=100))
|
|
83
|
+
app.use(SessionMiddleware(config=SessionConfig()))
|
|
84
|
+
|
|
85
|
+
4. Responses:
|
|
86
|
+
return response.json(data, status_code=201)
|
|
87
|
+
return response.html("<h1>Hello</h1>")
|
|
88
|
+
return response.file("downloads/report.pdf")
|
|
89
|
+
return response.stream(async_generator(), content_type="text/plain")
|
|
90
|
+
return response.redirect("/dashboard", status_code=302)
|
|
91
|
+
|
|
92
|
+
5. Exception Handlers:
|
|
93
|
+
async def custom_handler(request, response, exc):
|
|
94
|
+
return response.json({"error": str(exc)}).status(400)
|
|
95
|
+
|
|
96
|
+
app.add_exception_handler(CustomError, custom_handler)
|
|
97
|
+
|
|
98
|
+
6. GraphQL:
|
|
99
|
+
from sillo.graphql import GraphQL
|
|
100
|
+
|
|
101
|
+
GraphQL(app, schema, path="/graphql", graphiql=True)
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
from sillo.core.routing import Route, Router
|
|
105
|
+
|
|
106
|
+
__version__: str = "0.0.1a1"
|
|
107
|
+
|
|
108
|
+
from .application import silloApp
|
|
109
|
+
from .frontend import FrontendApp
|
|
110
|
+
from sillo.core.dependencies import Depend
|
|
111
|
+
from .validation import (
|
|
112
|
+
Cookie,
|
|
113
|
+
File,
|
|
114
|
+
Form,
|
|
115
|
+
Header,
|
|
116
|
+
Path,
|
|
117
|
+
Query,
|
|
118
|
+
RequestValidationError,
|
|
119
|
+
ResponseValidationError,
|
|
120
|
+
UploadFile,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
__all__ = [
|
|
124
|
+
"silloApp",
|
|
125
|
+
"Depend",
|
|
126
|
+
"Router",
|
|
127
|
+
"Route",
|
|
128
|
+
"FrontendApp",
|
|
129
|
+
"Query",
|
|
130
|
+
"Header",
|
|
131
|
+
"Cookie",
|
|
132
|
+
"Path",
|
|
133
|
+
"Form",
|
|
134
|
+
"File",
|
|
135
|
+
"UploadFile",
|
|
136
|
+
"RequestValidationError",
|
|
137
|
+
"ResponseValidationError",
|
|
138
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
from typing import Any, AsyncIterable, Callable, Mapping, MutableMapping
|
|
6
|
+
|
|
7
|
+
import anyio
|
|
8
|
+
|
|
9
|
+
from sillo.core.http.request import ClientDisconnect, Request
|
|
10
|
+
from sillo.core.http.response import (
|
|
11
|
+
BaseResponse,
|
|
12
|
+
)
|
|
13
|
+
from sillo.core.http.response import Responder as Response
|
|
14
|
+
from sillo.types import ASGIApp, Message, MiddlewareType, Receive, Scope, Send
|
|
15
|
+
from sillo.core.helpers.async_helpers import collapse_excgroups
|
|
16
|
+
from sillo.websockets import WebSocket
|
|
17
|
+
|
|
18
|
+
RequestResponseEndpoint = typing.Callable[[Request], typing.Awaitable[Response]]
|
|
19
|
+
|
|
20
|
+
T = typing.TypeVar("T")
|
|
21
|
+
|
|
22
|
+
AsyncContentStream = AsyncIterable[str | bytes | memoryview | MutableMapping[str, Any]]
|
|
23
|
+
|
|
24
|
+
MiddlewareFactory = Callable[..., ASGIApp]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DefineMiddleware:
|
|
28
|
+
"""Container that pairs a middleware factory with its positional and keyword arguments.
|
|
29
|
+
|
|
30
|
+
This class acts as a deferred middleware descriptor. It stores the middleware
|
|
31
|
+
class (or factory callable) together with the arguments that should be passed
|
|
32
|
+
when the middleware is instantiated. The application iterates over a list of
|
|
33
|
+
``DefineMiddleware`` instances to build the middleware stack at startup.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
cls: The middleware factory or class to be instantiated.
|
|
37
|
+
args: Positional arguments forwarded to the middleware constructor.
|
|
38
|
+
kwargs: Keyword arguments forwarded to the middleware constructor.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
cls: MiddlewareFactory,
|
|
44
|
+
*args: Any,
|
|
45
|
+
**kwargs: Any,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Initialise the descriptor with a middleware factory and its arguments.
|
|
48
|
+
|
|
49
|
+
Stores the middleware class together with any positional and keyword
|
|
50
|
+
arguments that should be forwarded when the middleware is later
|
|
51
|
+
instantiated by the application stack builder.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
cls: A callable that produces an ASGI application when invoked with
|
|
55
|
+
the remaining arguments. Typically a middleware class.
|
|
56
|
+
*args: Positional arguments forwarded to ``cls`` at instantiation.
|
|
57
|
+
**kwargs: Keyword arguments forwarded to ``cls`` at instantiation.
|
|
58
|
+
"""
|
|
59
|
+
self.cls = cls
|
|
60
|
+
self.args = args
|
|
61
|
+
self.kwargs = kwargs
|
|
62
|
+
|
|
63
|
+
def __iter__(self) -> Iterator[Any]:
|
|
64
|
+
"""Yield the middleware components as a three-element tuple.
|
|
65
|
+
|
|
66
|
+
Allows the instance to be unpacked into ``(cls, args, kwargs)``, which
|
|
67
|
+
is the format expected by the application's middleware stack builder.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
An iterator over ``(cls, args, kwargs)`` where ``cls`` is the
|
|
71
|
+
middleware factory, ``args`` is a tuple of positional arguments,
|
|
72
|
+
and ``kwargs`` is a dictionary of keyword arguments.
|
|
73
|
+
"""
|
|
74
|
+
as_tuple = (self.cls, self.args, self.kwargs)
|
|
75
|
+
return iter(as_tuple)
|
|
76
|
+
|
|
77
|
+
def __repr__(self) -> str:
|
|
78
|
+
"""Return a developer-friendly string representation of this descriptor.
|
|
79
|
+
|
|
80
|
+
The representation includes the middleware class name, all positional
|
|
81
|
+
arguments, and all keyword arguments so that the descriptor can be
|
|
82
|
+
identified at a glance during debugging or logging.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
A string in the form ``DefineMiddleware(MiddlewareName, arg1, ...,
|
|
86
|
+
key=value, ...)`` suitable for debugging output.
|
|
87
|
+
"""
|
|
88
|
+
class_name = self.__class__.__name__
|
|
89
|
+
args_strings = [f"{value!r}" for value in self.args]
|
|
90
|
+
option_strings = [f"{key}={value!r}" for key, value in self.kwargs.items()]
|
|
91
|
+
name = getattr(self.cls, "__name__", "")
|
|
92
|
+
args_repr = ", ".join([name] + args_strings + option_strings)
|
|
93
|
+
return f"{class_name}({args_repr})"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class _CachedRequest(Request):
|
|
97
|
+
"""A request subclass that caches the body for dispatch-based middleware.
|
|
98
|
+
|
|
99
|
+
If the user calls ``Request.body()`` from their dispatch function we cache
|
|
100
|
+
the entire request body in memory and pass that to downstream middleware,
|
|
101
|
+
but if they call ``Request.stream()`` then all we do is send an empty body
|
|
102
|
+
so that downstream things don't hang forever.
|
|
103
|
+
|
|
104
|
+
The class manages three internal states for the wrapped receive callable:
|
|
105
|
+
disconnected, consumed-but-not-disconnected, and not-yet-consumed. Each
|
|
106
|
+
state determines how subsequent calls to ``wrapped_receive`` behave when
|
|
107
|
+
communicating with the downstream ASGI application.
|
|
108
|
+
|
|
109
|
+
Attributes:
|
|
110
|
+
_wrapped_rcv_disconnected: Whether a disconnect has already been sent
|
|
111
|
+
to the downstream application.
|
|
112
|
+
_wrapped_rcv_consumed: Whether the request body has been fully
|
|
113
|
+
consumed by the dispatch function.
|
|
114
|
+
_wrapped_rc_stream: The async iterator over the raw request body
|
|
115
|
+
chunks, created at initialisation time.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, scope: Scope, receive: Receive):
|
|
119
|
+
"""Initialise the cached request with ASGI scope and receive callable.
|
|
120
|
+
|
|
121
|
+
Sets up internal state tracking flags and creates the initial stream
|
|
122
|
+
iterator from the parent ``Request`` class so that body chunks can be
|
|
123
|
+
lazily consumed by the dispatch middleware.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
scope: The ASGI connection scope dictionary containing metadata
|
|
127
|
+
about the incoming HTTP request such as headers, path, and
|
|
128
|
+
query string.
|
|
129
|
+
receive: An ASGI receive callable that yields messages from the
|
|
130
|
+
client, used to read the request body incrementally.
|
|
131
|
+
"""
|
|
132
|
+
super().__init__(scope, receive)
|
|
133
|
+
self._wrapped_rcv_disconnected = False
|
|
134
|
+
self._wrapped_rcv_consumed = False
|
|
135
|
+
self._wrapped_rc_stream = self.stream()
|
|
136
|
+
|
|
137
|
+
async def wrapped_receive(self) -> Message:
|
|
138
|
+
"""Return the next ASGI message for the downstream application.
|
|
139
|
+
|
|
140
|
+
Manages three internal states to control how request body data is
|
|
141
|
+
forwarded to the downstream ASGI application. When the body has been
|
|
142
|
+
cached via ``body()``, it returns the cached bytes immediately. When
|
|
143
|
+
the stream has been consumed, it returns an empty body. Otherwise it
|
|
144
|
+
reads the next chunk from the underlying stream.
|
|
145
|
+
|
|
146
|
+
The method also handles client disconnection by tracking whether a
|
|
147
|
+
disconnect message has already been forwarded, preventing duplicate
|
|
148
|
+
disconnect signals from being sent to the downstream application.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
An ASGI message dictionary. Either an ``http.request`` message
|
|
152
|
+
containing a body chunk with a ``more_body`` flag, or an
|
|
153
|
+
``http.disconnect`` message when the client has disconnected.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
RuntimeError: If an unexpected message type is received after the
|
|
157
|
+
stream has been consumed and the message is not a disconnect.
|
|
158
|
+
"""
|
|
159
|
+
# wrapped_rcv state 1: disconnected
|
|
160
|
+
if self._wrapped_rcv_disconnected:
|
|
161
|
+
# we've already sent a disconnect to the downstream app
|
|
162
|
+
# we don't need to wait to get another one
|
|
163
|
+
# (although most ASGI servers will just keep sending it)
|
|
164
|
+
return {"type": "http.disconnect"}
|
|
165
|
+
# wrapped_rcv state 1: consumed but not yet disconnected
|
|
166
|
+
if self._wrapped_rcv_consumed:
|
|
167
|
+
# since the downstream app has consumed us all that is left
|
|
168
|
+
# is to send it a disconnect
|
|
169
|
+
if self._is_disconnected:
|
|
170
|
+
# the middleware has already seen the disconnect
|
|
171
|
+
# since we know the client is disconnected no need to wait
|
|
172
|
+
# for the message
|
|
173
|
+
self._wrapped_rcv_disconnected = True
|
|
174
|
+
return {"type": "http.disconnect"}
|
|
175
|
+
# we don't know yet if the client is disconnected or not
|
|
176
|
+
# so we'll wait until we get that message
|
|
177
|
+
msg = await self.receive()
|
|
178
|
+
if msg["type"] != "http.disconnect": # pragma: no cover
|
|
179
|
+
# at this point a disconnect is all that we should be receiving
|
|
180
|
+
# if we get something else, things went wrong somewhere
|
|
181
|
+
raise RuntimeError(f"Unexpected message received: {msg['type']}")
|
|
182
|
+
self._wrapped_rcv_disconnected = True
|
|
183
|
+
return msg
|
|
184
|
+
|
|
185
|
+
# wrapped_rcv state 3: not yet consumed
|
|
186
|
+
if getattr(self, "_body", None) is not None:
|
|
187
|
+
# body() was called, we return it even if the client disconnected
|
|
188
|
+
self._wrapped_rcv_consumed = True
|
|
189
|
+
return {
|
|
190
|
+
"type": "http.request",
|
|
191
|
+
"body": self._body,
|
|
192
|
+
"more_body": False,
|
|
193
|
+
}
|
|
194
|
+
elif self._stream_consumed:
|
|
195
|
+
# stream() was called to completion
|
|
196
|
+
# return an empty body so that downstream apps don't hang
|
|
197
|
+
# waiting for a disconnect
|
|
198
|
+
self._wrapped_rcv_consumed = True
|
|
199
|
+
return {
|
|
200
|
+
"type": "http.request",
|
|
201
|
+
"body": b"",
|
|
202
|
+
"more_body": False,
|
|
203
|
+
}
|
|
204
|
+
else:
|
|
205
|
+
# body() was never called and stream() wasn't consumed
|
|
206
|
+
try:
|
|
207
|
+
stream = self.stream()
|
|
208
|
+
chunk = await stream.__anext__()
|
|
209
|
+
self._wrapped_rcv_consumed = self._stream_consumed
|
|
210
|
+
return {
|
|
211
|
+
"type": "http.request",
|
|
212
|
+
"body": chunk,
|
|
213
|
+
"more_body": not self._stream_consumed,
|
|
214
|
+
}
|
|
215
|
+
except ClientDisconnect:
|
|
216
|
+
self._wrapped_rcv_disconnected = True
|
|
217
|
+
return {"type": "http.disconnect"}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class ASGIRequestResponseBridge:
|
|
221
|
+
"""Bridges dispatch-style middleware into the ASGI application stack.
|
|
222
|
+
|
|
223
|
+
This class wraps an inner ASGI application and a dispatch function that
|
|
224
|
+
follows the request/response middleware pattern. It intercepts HTTP
|
|
225
|
+
requests, creates a cached request object, and uses an in-memory stream
|
|
226
|
+
to shuttle response data between the dispatch function and the inner
|
|
227
|
+
application. Non-HTTP scopes (e.g. websocket, lifespan) are forwarded
|
|
228
|
+
directly to the inner application without interception.
|
|
229
|
+
|
|
230
|
+
Attributes:
|
|
231
|
+
app: The inner ASGI application to delegate to.
|
|
232
|
+
dispatch_func: The dispatch middleware function that receives the
|
|
233
|
+
request, response, and a ``call_next`` callable.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
def __init__(self, app: ASGIApp, dispatch: MiddlewareType) -> None:
|
|
237
|
+
"""Initialise the bridge with an inner app and dispatch function.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
app: The inner ASGI application that will handle the actual
|
|
241
|
+
request processing after the dispatch middleware runs.
|
|
242
|
+
dispatch: A middleware function conforming to the dispatch
|
|
243
|
+
pattern, accepting ``(request, response, call_next)`` and
|
|
244
|
+
returning an awaitable response.
|
|
245
|
+
"""
|
|
246
|
+
self.app = app
|
|
247
|
+
self.dispatch_func = dispatch
|
|
248
|
+
|
|
249
|
+
def __str__(self) -> str:
|
|
250
|
+
"""Return a string representation of the bridge instance.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
A human-readable string showing the inner app and dispatch
|
|
254
|
+
function references for debugging purposes.
|
|
255
|
+
"""
|
|
256
|
+
return f"ASGIRequestResponseBridge({self.app!r}, {self.dispatch_func!r})"
|
|
257
|
+
|
|
258
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
259
|
+
"""Handle an incoming ASGI connection by bridging dispatch middleware.
|
|
260
|
+
|
|
261
|
+
For non-HTTP scopes the call is forwarded directly to the inner app.
|
|
262
|
+
For HTTP scopes, a ``_CachedRequest`` is created along with a
|
|
263
|
+
``Responder`` and an in-memory object stream. The dispatch function is
|
|
264
|
+
invoked with these objects and a ``call_next`` closure that runs the
|
|
265
|
+
inner app in a background task, streaming the response back through
|
|
266
|
+
the memory channel.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
scope: The ASGI connection scope dictionary describing the
|
|
270
|
+
connection type and metadata.
|
|
271
|
+
receive: The ASGI receive callable for reading incoming messages.
|
|
272
|
+
send: The ASGI send callable for transmitting outgoing messages.
|
|
273
|
+
"""
|
|
274
|
+
if scope["type"] != "http":
|
|
275
|
+
await self.app(scope, receive, send)
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
request = _CachedRequest(scope, receive)
|
|
279
|
+
response = Response(request=request)
|
|
280
|
+
wrapped_receive = request.wrapped_receive
|
|
281
|
+
response_sent = anyio.Event()
|
|
282
|
+
|
|
283
|
+
async def call_next(*_):
|
|
284
|
+
"""Invoke the inner application and return its response as a stream.
|
|
285
|
+
|
|
286
|
+
Runs the inner ASGI application inside a background task and
|
|
287
|
+
returns a ``_StreamingResponse`` that replays the response
|
|
288
|
+
headers and body chunks received through the in-memory stream.
|
|
289
|
+
Handles debug info messages, client disconnection, and exception
|
|
290
|
+
propagation from the inner application.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
A ``_StreamingResponse`` instance containing the status code,
|
|
294
|
+
headers, and body stream from the inner application.
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
RuntimeError: If the client disconnects before the inner app
|
|
298
|
+
sends a response, or if the inner app raises no exception
|
|
299
|
+
but the stream ends prematurely.
|
|
300
|
+
Exception: Re-raises any exception caught from the inner
|
|
301
|
+
application during request processing.
|
|
302
|
+
"""
|
|
303
|
+
app_exc: Exception | None = None
|
|
304
|
+
|
|
305
|
+
async def receive_or_disconnect() -> Message:
|
|
306
|
+
"""Return the next message or a disconnect if the response is sent.
|
|
307
|
+
|
|
308
|
+
Races the wrapped receive callable against the ``response_sent``
|
|
309
|
+
event so that the inner application receives a disconnect
|
|
310
|
+
signal once the middleware has finished sending the response.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
An ASGI message dictionary, either from the underlying
|
|
314
|
+
receive callable or a synthetic ``http.disconnect``
|
|
315
|
+
message if the response has already been sent.
|
|
316
|
+
"""
|
|
317
|
+
if response_sent.is_set():
|
|
318
|
+
return {"type": "http.disconnect"}
|
|
319
|
+
async with anyio.create_task_group() as task_group:
|
|
320
|
+
|
|
321
|
+
async def wrap(
|
|
322
|
+
func: typing.Callable[[], typing.Awaitable[T]],
|
|
323
|
+
) -> T:
|
|
324
|
+
"""Await a callable then cancel the sibling task in the group.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
func: A zero-argument async callable to execute.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
The return value of the awaited callable.
|
|
331
|
+
"""
|
|
332
|
+
result = await func()
|
|
333
|
+
task_group.cancel_scope.cancel()
|
|
334
|
+
return result
|
|
335
|
+
|
|
336
|
+
task_group.start_soon(wrap, response_sent.wait)
|
|
337
|
+
message = await wrap(wrapped_receive)
|
|
338
|
+
if response_sent.is_set():
|
|
339
|
+
return {"type": "http.disconnect"}
|
|
340
|
+
return message
|
|
341
|
+
|
|
342
|
+
async def send_no_error(message: Message) -> None:
|
|
343
|
+
"""Send an ASGI message, raising on broken stream resources.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
message: The ASGI message dictionary to send through the
|
|
347
|
+
in-memory stream to the dispatch function.
|
|
348
|
+
|
|
349
|
+
Raises:
|
|
350
|
+
RuntimeError: If the send stream has been closed or
|
|
351
|
+
broken, indicating the response was not properly
|
|
352
|
+
returned by the dispatch function.
|
|
353
|
+
"""
|
|
354
|
+
try:
|
|
355
|
+
await send_stream.send(message)
|
|
356
|
+
except anyio.BrokenResourceError:
|
|
357
|
+
raise RuntimeError("No response returned")
|
|
358
|
+
|
|
359
|
+
async def coro() -> None:
|
|
360
|
+
"""Run the inner ASGI application and capture any exception.
|
|
361
|
+
|
|
362
|
+
Executes the inner application within a context-managed send
|
|
363
|
+
stream, catching and storing any exception so it can be
|
|
364
|
+
re-raised in the dispatch function's task after the body
|
|
365
|
+
stream has been fully consumed.
|
|
366
|
+
"""
|
|
367
|
+
nonlocal app_exc
|
|
368
|
+
with send_stream:
|
|
369
|
+
try:
|
|
370
|
+
await self.app(scope, receive_or_disconnect, send_no_error)
|
|
371
|
+
except Exception as exc:
|
|
372
|
+
app_exc = exc
|
|
373
|
+
|
|
374
|
+
task_group.start_soon(coro)
|
|
375
|
+
try:
|
|
376
|
+
message = await recv_stream.receive()
|
|
377
|
+
info = message.get("info", None)
|
|
378
|
+
if message["type"] == "http.response.debug" and info is not None:
|
|
379
|
+
message = await recv_stream.receive()
|
|
380
|
+
except anyio.EndOfStream:
|
|
381
|
+
if app_exc is not None:
|
|
382
|
+
raise app_exc
|
|
383
|
+
raise RuntimeError("Client disconnected before response was sent")
|
|
384
|
+
assert message["type"] == "http.response.start"
|
|
385
|
+
|
|
386
|
+
async def body_stream() -> typing.AsyncGenerator[bytes, None]:
|
|
387
|
+
"""Yield response body chunks from the in-memory receive stream.
|
|
388
|
+
|
|
389
|
+
Iterates over messages from the receive stream, yielding body
|
|
390
|
+
byte strings until a message with ``more_body`` set to
|
|
391
|
+
``False`` is encountered. Re-raises any exception captured
|
|
392
|
+
from the inner application after the body is fully consumed.
|
|
393
|
+
|
|
394
|
+
Yields:
|
|
395
|
+
Byte strings containing portions of the HTTP response
|
|
396
|
+
body as forwarded by the inner ASGI application.
|
|
397
|
+
|
|
398
|
+
Raises:
|
|
399
|
+
Exception: Re-raises any exception that occurred in the
|
|
400
|
+
inner application during response generation.
|
|
401
|
+
"""
|
|
402
|
+
async for message in recv_stream:
|
|
403
|
+
assert message["type"] == "http.response.body"
|
|
404
|
+
body = message.get("body", b"")
|
|
405
|
+
if body:
|
|
406
|
+
yield body
|
|
407
|
+
if not message.get("more_body", False):
|
|
408
|
+
break
|
|
409
|
+
if app_exc is not None:
|
|
410
|
+
raise app_exc
|
|
411
|
+
|
|
412
|
+
response_object = _StreamingResponse(
|
|
413
|
+
content=body_stream(),
|
|
414
|
+
status_code=message["status"],
|
|
415
|
+
)
|
|
416
|
+
response_object.raw_headers = message["headers"]
|
|
417
|
+
response._response = response_object
|
|
418
|
+
return response_object
|
|
419
|
+
|
|
420
|
+
streams = anyio.create_memory_object_stream()
|
|
421
|
+
send_stream, recv_stream = streams
|
|
422
|
+
with recv_stream, send_stream, collapse_excgroups():
|
|
423
|
+
async with anyio.create_task_group() as task_group:
|
|
424
|
+
returned_response = await self.dispatch_func(
|
|
425
|
+
request, response, call_next
|
|
426
|
+
)
|
|
427
|
+
await returned_response(scope, wrapped_receive, send)
|
|
428
|
+
response_sent.set()
|
|
429
|
+
recv_stream.close()
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class _StreamingResponse(BaseResponse):
|
|
433
|
+
"""A response that streams its body content from an async iterable.
|
|
434
|
+
|
|
435
|
+
Unlike ``BaseResponse`` which holds the full body in memory, this response
|
|
436
|
+
class consumes an async content stream chunk by chunk, sending each chunk
|
|
437
|
+
as a separate ASGI ``http.response.body`` message. This enables efficient
|
|
438
|
+
handling of large responses or responses whose content is generated
|
|
439
|
+
incrementally by the inner application.
|
|
440
|
+
|
|
441
|
+
Optionally supports sending a debug info message before the response
|
|
442
|
+
start, and handles both raw byte chunks and ASGI message dictionaries
|
|
443
|
+
(for features like ``pathsend``) within the content stream.
|
|
444
|
+
|
|
445
|
+
Attributes:
|
|
446
|
+
info: Optional debug information dictionary to send before the
|
|
447
|
+
response start message.
|
|
448
|
+
content_iterator: The async iterable yielding body chunks or ASGI
|
|
449
|
+
message dictionaries.
|
|
450
|
+
status_code: The HTTP status code for the response.
|
|
451
|
+
media_type: Optional media type string for the Content-Type header.
|
|
452
|
+
"""
|
|
453
|
+
|
|
454
|
+
def __init__(
|
|
455
|
+
self,
|
|
456
|
+
content: AsyncContentStream,
|
|
457
|
+
status_code: int = 200,
|
|
458
|
+
headers: Mapping[str, str] | None = None,
|
|
459
|
+
media_type: str | None = None,
|
|
460
|
+
info: Mapping[str, Any] | None = None,
|
|
461
|
+
) -> None:
|
|
462
|
+
"""Initialise a streaming response with an async content source.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
content: An async iterable yielding body chunks as strings, bytes,
|
|
466
|
+
memoryview objects, or ASGI message dictionaries.
|
|
467
|
+
status_code: The HTTP status code to send in the response start
|
|
468
|
+
message. Defaults to 200.
|
|
469
|
+
headers: An optional mapping of HTTP header names to values. If
|
|
470
|
+
``None``, an empty header set is used.
|
|
471
|
+
media_type: An optional media type string for the Content-Type
|
|
472
|
+
response header.
|
|
473
|
+
info: An optional dictionary of debug information to send as an
|
|
474
|
+
``http.response.debug`` message before the response start.
|
|
475
|
+
"""
|
|
476
|
+
self.info = info
|
|
477
|
+
self.content_iterator = content
|
|
478
|
+
self.status_code = status_code
|
|
479
|
+
self.media_type = media_type
|
|
480
|
+
|
|
481
|
+
super().__init__(headers=dict(headers or {}), status_code=status_code)
|
|
482
|
+
|
|
483
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
484
|
+
"""Send the streaming response through the ASGI send callable.
|
|
485
|
+
|
|
486
|
+
Sends the response in three phases: an optional debug info message,
|
|
487
|
+
the response start message with status and headers, and then the body
|
|
488
|
+
chunks from the content iterator. Each body chunk is sent as a
|
|
489
|
+
separate ``http.response.body`` message with ``more_body`` set to
|
|
490
|
+
``True``. A final empty body message with ``more_body`` set to
|
|
491
|
+
``False`` is sent to signal the end of the response, unless the
|
|
492
|
+
content stream contained ASGI message dictionaries.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
scope: The ASGI connection scope dictionary.
|
|
496
|
+
receive: The ASGI receive callable (unused by this response).
|
|
497
|
+
send: The ASGI send callable used to transmit all response
|
|
498
|
+
messages to the client.
|
|
499
|
+
"""
|
|
500
|
+
if self.info is not None:
|
|
501
|
+
await send({"type": "http.response.debug", "info": self.info})
|
|
502
|
+
await send(
|
|
503
|
+
{
|
|
504
|
+
"type": "http.response.start",
|
|
505
|
+
"status": self.status_code,
|
|
506
|
+
"headers": self.raw_headers,
|
|
507
|
+
}
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
should_close_body = True
|
|
511
|
+
async for chunk in self.content_iterator:
|
|
512
|
+
if isinstance(chunk, dict):
|
|
513
|
+
# We got an ASGI message which is not response body (eg: pathsend)
|
|
514
|
+
should_close_body = False
|
|
515
|
+
await send(chunk)
|
|
516
|
+
continue
|
|
517
|
+
await send({"type": "http.response.body", "body": chunk, "more_body": True})
|
|
518
|
+
|
|
519
|
+
if should_close_body:
|
|
520
|
+
await send({"type": "http.response.body", "body": b"", "more_body": False})
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
WebSocketDispatchFunction = typing.Callable[
|
|
524
|
+
["WebSocket", typing.Coroutine[None, None, typing.Any]], typing.Awaitable[None]
|
|
525
|
+
]
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def wrap_middleware(middleware_function: MiddlewareType) -> DefineMiddleware:
|
|
529
|
+
"""Wrap a dispatch-style middleware function into a ``DefineMiddleware`` instance.
|
|
530
|
+
|
|
531
|
+
Creates a ``DefineMiddleware`` descriptor that pairs the
|
|
532
|
+
``ASGIRequestResponseBridge`` class with the given dispatch middleware
|
|
533
|
+
function. This allows the middleware to be added to the application's
|
|
534
|
+
middleware stack in the standard format expected by the framework.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
middleware_function: A dispatch-style middleware callable that accepts
|
|
538
|
+
``(request, response, call_next)`` and returns an awaitable
|
|
539
|
+
response object.
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
A ``DefineMiddleware`` instance wrapping the
|
|
543
|
+
``ASGIRequestResponseBridge`` with the provided dispatch function
|
|
544
|
+
bound as the ``dispatch`` keyword argument.
|
|
545
|
+
"""
|
|
546
|
+
return DefineMiddleware(ASGIRequestResponseBridge, dispatch=middleware_function)
|