veloceframework 0.1.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.
- veloce/__init__.py +314 -0
- veloce/_handler_plan.py +405 -0
- veloce/_types.py +26 -0
- veloce/app.py +3316 -0
- veloce/background.py +47 -0
- veloce/blueprints.py +222 -0
- veloce/cli.py +209 -0
- veloce/config.py +318 -0
- veloce/contrib/__init__.py +1 -0
- veloce/contrib/openapi.py +705 -0
- veloce/contrib/staticfiles.py +412 -0
- veloce/contrib/templating.py +260 -0
- veloce/dependency.py +813 -0
- veloce/encoders.py +105 -0
- veloce/exception_handlers.py +43 -0
- veloce/exceptions.py +359 -0
- veloce/helpers.py +640 -0
- veloce/http/__init__.py +28 -0
- veloce/http/cache_control.py +130 -0
- veloce/http/cookies.py +86 -0
- veloce/http/datastructures.py +795 -0
- veloce/http/dates.py +85 -0
- veloce/http/header_set.py +97 -0
- veloce/http/header_utils.py +117 -0
- veloce/http/request.py +1058 -0
- veloce/http/response.py +1285 -0
- veloce/instrumentation.py +46 -0
- veloce/json_provider.py +79 -0
- veloce/markup.py +92 -0
- veloce/middleware/__init__.py +34 -0
- veloce/middleware/base.py +82 -0
- veloce/middleware/compression.py +100 -0
- veloce/middleware/cors.py +184 -0
- veloce/middleware/csrf.py +162 -0
- veloce/middleware/logging.py +86 -0
- veloce/middleware/proxy_fix.py +149 -0
- veloce/middleware/security.py +244 -0
- veloce/middleware/sessions.py +197 -0
- veloce/passwords.py +218 -0
- veloce/py.typed +0 -0
- veloce/routing/__init__.py +17 -0
- veloce/routing/converters.py +189 -0
- veloce/routing/params.py +186 -0
- veloce/routing/router.py +880 -0
- veloce/safe.py +145 -0
- veloce/security/__init__.py +33 -0
- veloce/security/api_key.py +57 -0
- veloce/security/http.py +230 -0
- veloce/security/oauth2.py +186 -0
- veloce/serving/__init__.py +5 -0
- veloce/serving/protocol.py +227 -0
- veloce/sessions.py +188 -0
- veloce/signals.py +181 -0
- veloce/signing.py +196 -0
- veloce/sse.py +130 -0
- veloce/status.py +97 -0
- veloce/testclient.py +1128 -0
- veloce/views.py +142 -0
- veloce/watchdog.py +175 -0
- veloce/websocket.py +703 -0
- veloceframework-0.1.0.dist-info/METADATA +173 -0
- veloceframework-0.1.0.dist-info/RECORD +65 -0
- veloceframework-0.1.0.dist-info/WHEEL +4 -0
- veloceframework-0.1.0.dist-info/entry_points.txt +2 -0
- veloceframework-0.1.0.dist-info/licenses/LICENSE +21 -0
veloce/__init__.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Veloce — Ultra-fast async Python web framework.
|
|
2
|
+
|
|
3
|
+
Veloce is a high-performance asynchronous web framework built on raw asyncio,
|
|
4
|
+
httptools, and orjson. It pairs a small, well-typed API with predictable
|
|
5
|
+
performance under load.
|
|
6
|
+
|
|
7
|
+
Basic usage::
|
|
8
|
+
|
|
9
|
+
from veloce import Veloce, Request
|
|
10
|
+
|
|
11
|
+
app = Veloce()
|
|
12
|
+
|
|
13
|
+
@app.get("/")
|
|
14
|
+
async def index(request: Request):
|
|
15
|
+
return {"message": "Hello, World!"}
|
|
16
|
+
|
|
17
|
+
app.run()
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# Status codes
|
|
21
|
+
from veloce import status
|
|
22
|
+
from veloce.app import Veloce
|
|
23
|
+
|
|
24
|
+
# Background tasks
|
|
25
|
+
from veloce.background import BackgroundTask, BackgroundTasks
|
|
26
|
+
from veloce.blueprints import Blueprint
|
|
27
|
+
|
|
28
|
+
# Static files
|
|
29
|
+
from veloce.contrib.staticfiles import StaticFiles
|
|
30
|
+
|
|
31
|
+
# Dependency injection
|
|
32
|
+
from veloce.dependency import Depends, Security, SecurityScopes
|
|
33
|
+
|
|
34
|
+
# Encoders
|
|
35
|
+
from veloce.encoders import jsonable_encoder
|
|
36
|
+
|
|
37
|
+
# Exceptions
|
|
38
|
+
from veloce.exceptions import (
|
|
39
|
+
BuildError,
|
|
40
|
+
HTTPException,
|
|
41
|
+
RequestValidationError,
|
|
42
|
+
ValidationError,
|
|
43
|
+
WebSocketDisconnect,
|
|
44
|
+
WebSocketException,
|
|
45
|
+
WebSocketRequestValidationError,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Helpers
|
|
49
|
+
from veloce.helpers import (
|
|
50
|
+
abort,
|
|
51
|
+
after_this_request,
|
|
52
|
+
current_app,
|
|
53
|
+
flash,
|
|
54
|
+
g,
|
|
55
|
+
get_flashed_messages,
|
|
56
|
+
has_app_context,
|
|
57
|
+
has_request_context,
|
|
58
|
+
jsonify,
|
|
59
|
+
make_response,
|
|
60
|
+
redirect,
|
|
61
|
+
request,
|
|
62
|
+
send_file,
|
|
63
|
+
send_from_directory,
|
|
64
|
+
send_from_directory_async,
|
|
65
|
+
session,
|
|
66
|
+
stream_with_context,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Data structures
|
|
70
|
+
from veloce.http.datastructures import (
|
|
71
|
+
URL,
|
|
72
|
+
AcceptHeader,
|
|
73
|
+
Authorization,
|
|
74
|
+
FormData,
|
|
75
|
+
Headers,
|
|
76
|
+
RangeSpec,
|
|
77
|
+
UploadFile,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# HTTP
|
|
81
|
+
from veloce.http.request import Request
|
|
82
|
+
from veloce.http.response import (
|
|
83
|
+
FileResponse,
|
|
84
|
+
HTMLResponse,
|
|
85
|
+
JSONResponse,
|
|
86
|
+
ORJSONResponse,
|
|
87
|
+
PlainTextResponse,
|
|
88
|
+
RedirectResponse,
|
|
89
|
+
Response,
|
|
90
|
+
StreamingResponse,
|
|
91
|
+
UJSONResponse,
|
|
92
|
+
)
|
|
93
|
+
from veloce.instrumentation import RequestMetrics
|
|
94
|
+
|
|
95
|
+
# HTML-safe strings
|
|
96
|
+
from veloce.markup import Markup, escape
|
|
97
|
+
|
|
98
|
+
# Middleware
|
|
99
|
+
from veloce.middleware import (
|
|
100
|
+
BaseHTTPMiddleware,
|
|
101
|
+
CORSMiddleware,
|
|
102
|
+
CSRFMiddleware,
|
|
103
|
+
GZipMiddleware,
|
|
104
|
+
HTTPSRedirectMiddleware,
|
|
105
|
+
LoggingMiddleware,
|
|
106
|
+
Middleware,
|
|
107
|
+
ProxyFix,
|
|
108
|
+
RateLimitMiddleware,
|
|
109
|
+
RequestIDMiddleware,
|
|
110
|
+
SecurityHeadersMiddleware,
|
|
111
|
+
ServerSessionMiddleware,
|
|
112
|
+
SessionMiddleware,
|
|
113
|
+
TrustedHostMiddleware,
|
|
114
|
+
WebSocketOriginMiddleware,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Password hashing helpers
|
|
118
|
+
from veloce.passwords import (
|
|
119
|
+
hash_password,
|
|
120
|
+
hash_password_async,
|
|
121
|
+
is_strong_password,
|
|
122
|
+
verify_password,
|
|
123
|
+
verify_password_async,
|
|
124
|
+
)
|
|
125
|
+
from veloce.routing.params import Body, Cookie, File, Form, Header, Path, Query
|
|
126
|
+
|
|
127
|
+
# Routing
|
|
128
|
+
from veloce.routing.router import Router
|
|
129
|
+
|
|
130
|
+
# Filesystem-safety helpers
|
|
131
|
+
from veloce.safe import constant_time_compare, safe_join, secure_filename
|
|
132
|
+
|
|
133
|
+
# Security
|
|
134
|
+
from veloce.security import (
|
|
135
|
+
APIKeyCookie,
|
|
136
|
+
APIKeyHeader,
|
|
137
|
+
APIKeyQuery,
|
|
138
|
+
HTTPBasic,
|
|
139
|
+
HTTPBasicCredentials,
|
|
140
|
+
HTTPBearer,
|
|
141
|
+
HTTPDigest,
|
|
142
|
+
HTTPDigestCredentials,
|
|
143
|
+
OAuth2AuthorizationCodeBearer,
|
|
144
|
+
OAuth2PasswordBearer,
|
|
145
|
+
OAuth2PasswordRequestForm,
|
|
146
|
+
OAuth2PasswordRequestFormStrict,
|
|
147
|
+
OpenIdConnect,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Sessions
|
|
151
|
+
from veloce.sessions import InMemorySessionStore, Session, SessionStore
|
|
152
|
+
|
|
153
|
+
# HMAC-signed value serialiser
|
|
154
|
+
from veloce.signing import BadData, BadSignature, BadTimeSignature, Signer
|
|
155
|
+
|
|
156
|
+
# Server-Sent Events
|
|
157
|
+
from veloce.sse import EventSourceResponse, ServerSentEvent
|
|
158
|
+
|
|
159
|
+
# Testing
|
|
160
|
+
from veloce.testclient import AsyncTestClient, TestClient
|
|
161
|
+
|
|
162
|
+
# Class-based views
|
|
163
|
+
from veloce.views import MethodView, View
|
|
164
|
+
|
|
165
|
+
# Event-loop watchdog
|
|
166
|
+
from veloce.watchdog import EventLoopWatchdog
|
|
167
|
+
|
|
168
|
+
# WebSocket
|
|
169
|
+
from veloce.websocket import WebSocket
|
|
170
|
+
|
|
171
|
+
__version__ = "0.1.0"
|
|
172
|
+
|
|
173
|
+
# some users reach for `APIRouter`; it is the same primitive as
|
|
174
|
+
# Veloce's `Blueprint` (a mountable group of routes + hooks).
|
|
175
|
+
APIRouter = Blueprint
|
|
176
|
+
|
|
177
|
+
__all__ = [
|
|
178
|
+
# Core
|
|
179
|
+
"Veloce",
|
|
180
|
+
"Request",
|
|
181
|
+
"Router",
|
|
182
|
+
"Blueprint",
|
|
183
|
+
"APIRouter",
|
|
184
|
+
# Responses
|
|
185
|
+
"Response",
|
|
186
|
+
"JSONResponse",
|
|
187
|
+
"ORJSONResponse",
|
|
188
|
+
"UJSONResponse",
|
|
189
|
+
"HTMLResponse",
|
|
190
|
+
"PlainTextResponse",
|
|
191
|
+
"RedirectResponse",
|
|
192
|
+
"StreamingResponse",
|
|
193
|
+
"FileResponse",
|
|
194
|
+
"EventSourceResponse",
|
|
195
|
+
# Middleware
|
|
196
|
+
"Middleware",
|
|
197
|
+
"BaseHTTPMiddleware",
|
|
198
|
+
"CORSMiddleware",
|
|
199
|
+
"CSRFMiddleware",
|
|
200
|
+
"GZipMiddleware",
|
|
201
|
+
"TrustedHostMiddleware",
|
|
202
|
+
"RateLimitMiddleware",
|
|
203
|
+
"HTTPSRedirectMiddleware",
|
|
204
|
+
"SecurityHeadersMiddleware",
|
|
205
|
+
"WebSocketOriginMiddleware",
|
|
206
|
+
"LoggingMiddleware",
|
|
207
|
+
"RequestIDMiddleware",
|
|
208
|
+
"SessionMiddleware",
|
|
209
|
+
"ServerSessionMiddleware",
|
|
210
|
+
"ProxyFix",
|
|
211
|
+
# Sessions
|
|
212
|
+
"Session",
|
|
213
|
+
"SessionStore",
|
|
214
|
+
"InMemorySessionStore",
|
|
215
|
+
# WebSocket
|
|
216
|
+
"WebSocket",
|
|
217
|
+
"WebSocketDisconnect",
|
|
218
|
+
"WebSocketException",
|
|
219
|
+
"WebSocketRequestValidationError",
|
|
220
|
+
# DI
|
|
221
|
+
"Depends",
|
|
222
|
+
"Security",
|
|
223
|
+
"SecurityScopes",
|
|
224
|
+
# Exceptions
|
|
225
|
+
"HTTPException",
|
|
226
|
+
"ValidationError",
|
|
227
|
+
"RequestValidationError",
|
|
228
|
+
"BuildError",
|
|
229
|
+
# Background
|
|
230
|
+
"BackgroundTask",
|
|
231
|
+
"BackgroundTasks",
|
|
232
|
+
# Static
|
|
233
|
+
"StaticFiles",
|
|
234
|
+
# Data structures
|
|
235
|
+
"UploadFile",
|
|
236
|
+
"URL",
|
|
237
|
+
"FormData",
|
|
238
|
+
"Headers",
|
|
239
|
+
"Authorization",
|
|
240
|
+
"AcceptHeader",
|
|
241
|
+
"RangeSpec",
|
|
242
|
+
# Security
|
|
243
|
+
"HTTPBasic",
|
|
244
|
+
"HTTPBasicCredentials",
|
|
245
|
+
"HTTPBearer",
|
|
246
|
+
"HTTPDigest",
|
|
247
|
+
"HTTPDigestCredentials",
|
|
248
|
+
"APIKeyHeader",
|
|
249
|
+
"APIKeyQuery",
|
|
250
|
+
"APIKeyCookie",
|
|
251
|
+
"OAuth2PasswordBearer",
|
|
252
|
+
"OAuth2PasswordRequestForm",
|
|
253
|
+
"OAuth2PasswordRequestFormStrict",
|
|
254
|
+
"OAuth2AuthorizationCodeBearer",
|
|
255
|
+
"OpenIdConnect",
|
|
256
|
+
# SSE
|
|
257
|
+
"ServerSentEvent",
|
|
258
|
+
# Testing
|
|
259
|
+
"TestClient",
|
|
260
|
+
"AsyncTestClient",
|
|
261
|
+
# Class-based views
|
|
262
|
+
"View",
|
|
263
|
+
"MethodView",
|
|
264
|
+
# Helpers
|
|
265
|
+
"abort",
|
|
266
|
+
"after_this_request",
|
|
267
|
+
"jsonify",
|
|
268
|
+
"make_response",
|
|
269
|
+
"redirect",
|
|
270
|
+
"send_file",
|
|
271
|
+
"send_from_directory",
|
|
272
|
+
"g",
|
|
273
|
+
"current_app",
|
|
274
|
+
"request",
|
|
275
|
+
"session",
|
|
276
|
+
"flash",
|
|
277
|
+
"get_flashed_messages",
|
|
278
|
+
"has_app_context",
|
|
279
|
+
"has_request_context",
|
|
280
|
+
"stream_with_context",
|
|
281
|
+
# HTML-safe strings
|
|
282
|
+
"Markup",
|
|
283
|
+
"escape",
|
|
284
|
+
# Encoders
|
|
285
|
+
"jsonable_encoder",
|
|
286
|
+
# Observability
|
|
287
|
+
"RequestMetrics",
|
|
288
|
+
"EventLoopWatchdog",
|
|
289
|
+
# Filesystem-safety
|
|
290
|
+
"secure_filename",
|
|
291
|
+
"safe_join",
|
|
292
|
+
"constant_time_compare",
|
|
293
|
+
# Signing
|
|
294
|
+
"Signer",
|
|
295
|
+
"BadSignature",
|
|
296
|
+
"BadTimeSignature",
|
|
297
|
+
"BadData",
|
|
298
|
+
# Passwords
|
|
299
|
+
"hash_password",
|
|
300
|
+
"hash_password_async",
|
|
301
|
+
"verify_password",
|
|
302
|
+
"verify_password_async",
|
|
303
|
+
"is_strong_password",
|
|
304
|
+
# Status
|
|
305
|
+
"status",
|
|
306
|
+
# Parameter classes
|
|
307
|
+
"Query",
|
|
308
|
+
"Path",
|
|
309
|
+
"Body",
|
|
310
|
+
"Form",
|
|
311
|
+
"File",
|
|
312
|
+
"Header",
|
|
313
|
+
"Cookie",
|
|
314
|
+
]
|
veloce/_handler_plan.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""Pre-computed parameter-resolution plan for a route handler.
|
|
2
|
+
|
|
3
|
+
Built once at route registration; consumed per request. Removes the per-request
|
|
4
|
+
cost of `inspect.signature` and `typing.get_type_hints` from the dispatch path.
|
|
5
|
+
|
|
6
|
+
The plan is a list of `_Slot` records — one per handler parameter — each tagged
|
|
7
|
+
with the source the request should be read from. The plan also carries
|
|
8
|
+
pre-planned route-level dependencies and the recursive plans for any
|
|
9
|
+
`Depends` sub-graph reachable from the handler.
|
|
10
|
+
|
|
11
|
+
Reflection happens at registration time only, never on the hot path.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import functools
|
|
17
|
+
import inspect
|
|
18
|
+
import types
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from typing import Any, Union, get_args, get_origin, get_type_hints
|
|
21
|
+
|
|
22
|
+
from pydantic import BaseModel
|
|
23
|
+
|
|
24
|
+
from veloce.background import BackgroundTasks
|
|
25
|
+
from veloce.http.datastructures import UploadFile
|
|
26
|
+
from veloce.http.request import Request
|
|
27
|
+
from veloce.http.response import Response
|
|
28
|
+
from veloce.routing.params import Body, Cookie, File, Form, Header, Path, _ParamBase
|
|
29
|
+
|
|
30
|
+
# `Depends` is imported lazily inside builders to break the dependency.py ↔
|
|
31
|
+
# _handler_plan.py cycle: dependency.py imports the plan API at module load,
|
|
32
|
+
# and we are imported back via that chain.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Slot kinds — bare integers for cheap dispatch in the hot loop.
|
|
36
|
+
K_REQUEST = 0
|
|
37
|
+
K_BG_TASKS = 1
|
|
38
|
+
K_DEPENDS = 2
|
|
39
|
+
K_PARAM_MARKER = 3
|
|
40
|
+
K_PATH = 4
|
|
41
|
+
K_QUERY = 5
|
|
42
|
+
K_QUERY_LIST = 6
|
|
43
|
+
K_BODY_MODEL = 7
|
|
44
|
+
K_UPLOAD_FILE = 8
|
|
45
|
+
K_DEFAULT = 9
|
|
46
|
+
K_NONE = 10
|
|
47
|
+
K_SECURITY_SCOPES = 11
|
|
48
|
+
K_RESPONSE = 12
|
|
49
|
+
K_WEBSOCKET = 13
|
|
50
|
+
|
|
51
|
+
# Marker kinds (for K_PARAM_MARKER slots).
|
|
52
|
+
MK_QUERY = 0
|
|
53
|
+
MK_PATH = 1
|
|
54
|
+
MK_HEADER = 2
|
|
55
|
+
MK_COOKIE = 3
|
|
56
|
+
MK_BODY = 4
|
|
57
|
+
MK_FORM = 5
|
|
58
|
+
MK_FILE = 6
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _unwrap_optional(annotation: Any) -> tuple[bool, Any]:
|
|
62
|
+
"""Detect `Optional[T]` / `Union[T, None]` / `T | None` and unwrap T."""
|
|
63
|
+
origin = get_origin(annotation)
|
|
64
|
+
# `Union[X, None]` has origin typing.Union; `X | None` (PEP 604) has origin
|
|
65
|
+
# types.UnionType — both must be recognised.
|
|
66
|
+
if origin is Union or origin is types.UnionType:
|
|
67
|
+
args = get_args(annotation)
|
|
68
|
+
non_none = [a for a in args if a is not type(None)]
|
|
69
|
+
if len(non_none) == 1 and type(None) in args:
|
|
70
|
+
return True, non_none[0]
|
|
71
|
+
return False, annotation
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _unwrap_list(annotation: Any) -> tuple[bool, Any]:
|
|
75
|
+
"""Detect `list[T]` and unwrap T."""
|
|
76
|
+
origin = get_origin(annotation)
|
|
77
|
+
if origin is list:
|
|
78
|
+
args = get_args(annotation)
|
|
79
|
+
return True, (args[0] if args else str)
|
|
80
|
+
return False, annotation
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _marker_kind(marker: _ParamBase) -> int:
|
|
84
|
+
if isinstance(marker, Path):
|
|
85
|
+
return MK_PATH
|
|
86
|
+
if isinstance(marker, Header):
|
|
87
|
+
return MK_HEADER
|
|
88
|
+
if isinstance(marker, Cookie):
|
|
89
|
+
return MK_COOKIE
|
|
90
|
+
if isinstance(marker, Body):
|
|
91
|
+
return MK_BODY
|
|
92
|
+
if isinstance(marker, Form):
|
|
93
|
+
return MK_FORM
|
|
94
|
+
if isinstance(marker, File):
|
|
95
|
+
return MK_FILE
|
|
96
|
+
# Query is the default and a bare _ParamBase falls here too.
|
|
97
|
+
return MK_QUERY
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class _Slot:
|
|
101
|
+
"""One handler parameter's pre-resolved binding to a request source.
|
|
102
|
+
|
|
103
|
+
All fields packed onto one class — branching on `kind` is cheaper than
|
|
104
|
+
dispatching on object type in the request hot path.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
__slots__ = (
|
|
108
|
+
"kind",
|
|
109
|
+
"name",
|
|
110
|
+
"target_type",
|
|
111
|
+
"default",
|
|
112
|
+
"has_default",
|
|
113
|
+
"is_optional",
|
|
114
|
+
"list_inner",
|
|
115
|
+
"model",
|
|
116
|
+
"marker",
|
|
117
|
+
"marker_kind",
|
|
118
|
+
"lookup_name",
|
|
119
|
+
"sub_plan",
|
|
120
|
+
"use_cache",
|
|
121
|
+
"dep_callable",
|
|
122
|
+
"dep_is_coro",
|
|
123
|
+
"dep_is_gen",
|
|
124
|
+
"dep_is_async_gen",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def __init__(self, kind: int, name: str) -> None:
|
|
128
|
+
self.kind = kind
|
|
129
|
+
self.name = name
|
|
130
|
+
self.target_type: Any = None
|
|
131
|
+
self.default: Any = None
|
|
132
|
+
self.has_default = False
|
|
133
|
+
self.is_optional = False
|
|
134
|
+
self.list_inner: Any = None
|
|
135
|
+
self.model: Any = None
|
|
136
|
+
self.marker: _ParamBase | None = None
|
|
137
|
+
self.marker_kind = MK_QUERY
|
|
138
|
+
self.lookup_name = ""
|
|
139
|
+
self.sub_plan: HandlerPlan | None = None
|
|
140
|
+
self.use_cache = True
|
|
141
|
+
self.dep_callable: Callable | None = None
|
|
142
|
+
self.dep_is_coro = False
|
|
143
|
+
# Yield-style dependencies (the typed-DI "context-manager" pattern): the
|
|
144
|
+
# resolver starts the generator, captures the first yielded value as
|
|
145
|
+
# the dependency result, and runs teardown after the response.
|
|
146
|
+
self.dep_is_gen = False
|
|
147
|
+
self.dep_is_async_gen = False
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class HandlerPlan:
|
|
151
|
+
"""Frozen resolution plan for one handler, plus its dependency graph."""
|
|
152
|
+
|
|
153
|
+
__slots__ = ("handler", "is_coro", "slots", "route_dep_plans")
|
|
154
|
+
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
handler: Callable,
|
|
158
|
+
slots: list[_Slot],
|
|
159
|
+
route_dep_plans: list[_Slot],
|
|
160
|
+
) -> None:
|
|
161
|
+
self.handler = handler
|
|
162
|
+
self.is_coro = inspect.iscoroutinefunction(handler)
|
|
163
|
+
self.slots = slots
|
|
164
|
+
# Each entry is a K_DEPENDS slot; only used for side-effect deps that
|
|
165
|
+
# do not bind to a handler parameter.
|
|
166
|
+
self.route_dep_plans = route_dep_plans
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _build_depends_slot(
|
|
170
|
+
name: str, dep: Any, inferred: Any = None, *, websocket: bool = False
|
|
171
|
+
) -> _Slot:
|
|
172
|
+
"""Build a K_DEPENDS slot, recursively planning the sub-callable.
|
|
173
|
+
|
|
174
|
+
`Depends()` with no explicit dependency falls back to `inferred`
|
|
175
|
+
(the parameter's type annotation) — the shorthand for
|
|
176
|
+
`x: SomeClass = Depends()`. `websocket` is threaded down the chain so
|
|
177
|
+
a dependency of a WebSocket handler can itself receive the
|
|
178
|
+
`WebSocket` connection by annotation or `ws` / `websocket` name.
|
|
179
|
+
"""
|
|
180
|
+
slot = _Slot(K_DEPENDS, name)
|
|
181
|
+
slot.use_cache = dep.use_cache
|
|
182
|
+
callable_ = dep.dependency if dep.dependency is not None else inferred
|
|
183
|
+
slot.dep_callable = callable_
|
|
184
|
+
slot.dep_is_coro = inspect.iscoroutinefunction(callable_)
|
|
185
|
+
slot.dep_is_gen = inspect.isgeneratorfunction(callable_)
|
|
186
|
+
slot.dep_is_async_gen = inspect.isasyncgenfunction(callable_)
|
|
187
|
+
slot.sub_plan = build_plan(callable_, websocket=websocket)
|
|
188
|
+
# Security() scopes flow down the chain so a `SecurityScopes`
|
|
189
|
+
# parameter anywhere below sees the union. Plain `Depends` has no
|
|
190
|
+
# scopes attribute; we read defensively.
|
|
191
|
+
scopes = getattr(dep, "scopes", None)
|
|
192
|
+
if scopes:
|
|
193
|
+
slot.target_type = list(scopes) # repurpose target_type as scope list
|
|
194
|
+
return slot
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def build_plan(handler: Callable, *, websocket: bool = False) -> HandlerPlan:
|
|
198
|
+
"""Inspect `handler` and freeze a resolution plan.
|
|
199
|
+
|
|
200
|
+
Called exactly once per route registration. Safe to call on builtins,
|
|
201
|
+
lambdas, partials, or callable classes — returns an empty plan for
|
|
202
|
+
handlers without inspectable signatures.
|
|
203
|
+
|
|
204
|
+
When `websocket` is set, a parameter typed `WebSocket` (or named `ws`
|
|
205
|
+
/ `websocket`) is bound to a `K_WEBSOCKET` slot instead of being read
|
|
206
|
+
from the request — the same plan machinery then drives WebSocket
|
|
207
|
+
dependency injection, giving it `yield`-teardown and `Security` parity
|
|
208
|
+
with the HTTP path.
|
|
209
|
+
"""
|
|
210
|
+
from veloce.dependency import Depends # local import breaks the import cycle
|
|
211
|
+
|
|
212
|
+
ws_type: Any = None
|
|
213
|
+
if websocket:
|
|
214
|
+
from veloce.websocket import WebSocket
|
|
215
|
+
|
|
216
|
+
ws_type = WebSocket
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
sig = inspect.signature(handler)
|
|
220
|
+
except (TypeError, ValueError):
|
|
221
|
+
return HandlerPlan(handler, [], [])
|
|
222
|
+
|
|
223
|
+
# `inspect.signature` transparently follows the class -> `__init__`,
|
|
224
|
+
# callable-instance -> `__call__`, and `functools.partial` -> wrapped
|
|
225
|
+
# function indirection, but `get_type_hints` does not — on a class it
|
|
226
|
+
# returns the *class-level* annotations, not `__init__`'s. Resolve the
|
|
227
|
+
# same object `signature` did so dependencies keep their parameter types.
|
|
228
|
+
real: Any = handler
|
|
229
|
+
while isinstance(real, functools.partial):
|
|
230
|
+
real = real.func
|
|
231
|
+
if inspect.isclass(real):
|
|
232
|
+
hint_target: Any = real.__init__
|
|
233
|
+
elif not inspect.isfunction(real) and not inspect.ismethod(real) and callable(real):
|
|
234
|
+
hint_target = type(real).__call__
|
|
235
|
+
else:
|
|
236
|
+
hint_target = real
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
# `include_extras=True` keeps PEP 593 `Annotated[T, Depends(...)]`
|
|
240
|
+
# metadata in the result so we can detect dependency markers
|
|
241
|
+
# without forcing users to use the default-value form.
|
|
242
|
+
hints = get_type_hints(hint_target, include_extras=True)
|
|
243
|
+
except Exception:
|
|
244
|
+
# get_type_hints chokes on forward refs / private modules; degrade
|
|
245
|
+
# gracefully — slots that need annotations become K_DEFAULT/K_NONE.
|
|
246
|
+
hints = {}
|
|
247
|
+
|
|
248
|
+
slots: list[_Slot] = []
|
|
249
|
+
|
|
250
|
+
for param_name, param in sig.parameters.items():
|
|
251
|
+
if param_name == "self":
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
annotation = hints.get(param_name)
|
|
255
|
+
default = param.default
|
|
256
|
+
has_default = default is not inspect.Parameter.empty
|
|
257
|
+
|
|
258
|
+
# PEP 593: `Annotated[T, Depends(...)]` or `Annotated[T, Query(...)]`.
|
|
259
|
+
# If the metadata carries a `Depends` (or `_ParamBase` marker) and
|
|
260
|
+
# the user didn't ALSO set it as the default, hoist the marker
|
|
261
|
+
# into `default` and reduce `annotation` to the inner type.
|
|
262
|
+
if get_origin(annotation) is not None and hasattr(annotation, "__metadata__"):
|
|
263
|
+
meta_args = get_args(annotation)
|
|
264
|
+
base_type = meta_args[0] if meta_args else annotation
|
|
265
|
+
metadata = getattr(annotation, "__metadata__", ())
|
|
266
|
+
extracted_marker: Any = None
|
|
267
|
+
for m in metadata:
|
|
268
|
+
if isinstance(m, (Depends, _ParamBase)):
|
|
269
|
+
extracted_marker = m
|
|
270
|
+
break
|
|
271
|
+
if extracted_marker is not None and not isinstance(default, (Depends, _ParamBase)):
|
|
272
|
+
default = extracted_marker
|
|
273
|
+
has_default = True
|
|
274
|
+
annotation = base_type
|
|
275
|
+
|
|
276
|
+
# WebSocket injection (websocket plans only) — by annotation or by
|
|
277
|
+
# the `ws` / `websocket` parameter name. Checked first so it wins
|
|
278
|
+
# over the request/path fallbacks for a WebSocket handler.
|
|
279
|
+
if websocket and (annotation is ws_type or param_name in ("ws", "websocket")):
|
|
280
|
+
slots.append(_Slot(K_WEBSOCKET, param_name))
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
# Request injection — either by name or by annotation.
|
|
284
|
+
if param_name == "request" or annotation is Request:
|
|
285
|
+
slots.append(_Slot(K_REQUEST, param_name))
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
# BackgroundTasks injection by annotation. A WebSocket handshake
|
|
289
|
+
# has no response cycle to attach tasks to, so the parameter is
|
|
290
|
+
# left to its handler default rather than injected.
|
|
291
|
+
if annotation is BackgroundTasks:
|
|
292
|
+
if not websocket:
|
|
293
|
+
slots.append(_Slot(K_BG_TASKS, param_name))
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
# Response injection by annotation. The handler
|
|
297
|
+
# receives a fresh Response it can mutate (status_code, headers,
|
|
298
|
+
# cookies); the dispatcher merges those onto the final response.
|
|
299
|
+
# There is no HTTP Response on a WebSocket route — skip it there.
|
|
300
|
+
if annotation is Response:
|
|
301
|
+
if not websocket:
|
|
302
|
+
slots.append(_Slot(K_RESPONSE, param_name))
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
# SecurityScopes — receives the accumulated Security() chain scopes.
|
|
306
|
+
# Lazy import avoids the dependency.py ↔ _handler_plan.py cycle.
|
|
307
|
+
from veloce.dependency import SecurityScopes
|
|
308
|
+
|
|
309
|
+
if annotation is SecurityScopes:
|
|
310
|
+
slots.append(_Slot(K_SECURITY_SCOPES, param_name))
|
|
311
|
+
continue
|
|
312
|
+
|
|
313
|
+
# Depends() / Security() in default position. A bare `Depends()`
|
|
314
|
+
# with no callable infers the dependency from the annotation
|
|
315
|
+
# (`x: SomeClass = Depends()`).
|
|
316
|
+
if isinstance(default, Depends):
|
|
317
|
+
slots.append(
|
|
318
|
+
_build_depends_slot(param_name, default, inferred=annotation, websocket=websocket)
|
|
319
|
+
)
|
|
320
|
+
continue
|
|
321
|
+
|
|
322
|
+
# Explicit parameter markers (Query/Path/Header/Cookie/Body/Form/File).
|
|
323
|
+
if isinstance(default, _ParamBase):
|
|
324
|
+
marker_kind = _marker_kind(default)
|
|
325
|
+
# Body / Form / File markers read the HTTP request body, which
|
|
326
|
+
# a WebSocket handshake does not have — skip them so the
|
|
327
|
+
# handler default applies instead of crashing at resolve time.
|
|
328
|
+
if websocket and marker_kind in (MK_BODY, MK_FORM, MK_FILE):
|
|
329
|
+
continue
|
|
330
|
+
slot = _Slot(K_PARAM_MARKER, param_name)
|
|
331
|
+
slot.marker = default
|
|
332
|
+
slot.marker_kind = marker_kind
|
|
333
|
+
slot.lookup_name = default.alias or param_name
|
|
334
|
+
# An un-aliased Header param converts `_` → `-`
|
|
335
|
+
# in its name (`x_token` → `x-token`) unless disabled.
|
|
336
|
+
if (
|
|
337
|
+
slot.marker_kind == MK_HEADER
|
|
338
|
+
and not default.alias
|
|
339
|
+
and getattr(default, "convert_underscores", True)
|
|
340
|
+
):
|
|
341
|
+
slot.lookup_name = slot.lookup_name.replace("_", "-")
|
|
342
|
+
is_opt, inner = _unwrap_optional(annotation) if annotation else (False, annotation)
|
|
343
|
+
slot.is_optional = is_opt
|
|
344
|
+
slot.target_type = inner if is_opt else annotation
|
|
345
|
+
slots.append(slot)
|
|
346
|
+
continue
|
|
347
|
+
|
|
348
|
+
is_optional, inner_type = (
|
|
349
|
+
_unwrap_optional(annotation) if annotation else (False, annotation)
|
|
350
|
+
)
|
|
351
|
+
is_list, list_inner = _unwrap_list(inner_type) if inner_type else (False, inner_type)
|
|
352
|
+
|
|
353
|
+
# UploadFile binding (with or without Optional). A WebSocket has no
|
|
354
|
+
# multipart form body, so the parameter is left to its default.
|
|
355
|
+
if annotation is UploadFile or (is_optional and inner_type is UploadFile):
|
|
356
|
+
if not websocket:
|
|
357
|
+
slot = _Slot(K_UPLOAD_FILE, param_name)
|
|
358
|
+
slot.is_optional = is_optional
|
|
359
|
+
slots.append(slot)
|
|
360
|
+
continue
|
|
361
|
+
|
|
362
|
+
# Pydantic model from body. A WebSocket handshake has no request
|
|
363
|
+
# body to validate, so the parameter is left to its default.
|
|
364
|
+
if inner_type and isinstance(inner_type, type) and issubclass(inner_type, BaseModel):
|
|
365
|
+
if not websocket:
|
|
366
|
+
slot = _Slot(K_BODY_MODEL, param_name)
|
|
367
|
+
slot.model = inner_type
|
|
368
|
+
slot.is_optional = is_optional
|
|
369
|
+
slots.append(slot)
|
|
370
|
+
continue
|
|
371
|
+
|
|
372
|
+
# List-typed parameter: read from query as a list.
|
|
373
|
+
if is_list:
|
|
374
|
+
slot = _Slot(K_QUERY_LIST, param_name)
|
|
375
|
+
slot.list_inner = list_inner
|
|
376
|
+
slot.has_default = has_default
|
|
377
|
+
slot.default = default if has_default else None
|
|
378
|
+
slot.is_optional = is_optional
|
|
379
|
+
slots.append(slot)
|
|
380
|
+
continue
|
|
381
|
+
|
|
382
|
+
# Default fallback: path-or-query, decided at resolve time because
|
|
383
|
+
# path_params are scope-local (per match). The slot is K_PATH-or-QUERY
|
|
384
|
+
# ambiguous; we pick K_QUERY and the resolver will prefer path_params
|
|
385
|
+
# when the name is present there. This keeps the plan handler-local
|
|
386
|
+
# (one plan per handler, reusable across overrides).
|
|
387
|
+
slot = _Slot(K_QUERY, param_name)
|
|
388
|
+
slot.target_type = inner_type if inner_type is not None else str
|
|
389
|
+
slot.is_optional = is_optional
|
|
390
|
+
slot.has_default = has_default
|
|
391
|
+
slot.default = default if has_default else None
|
|
392
|
+
slots.append(slot)
|
|
393
|
+
|
|
394
|
+
return HandlerPlan(handler, slots, [])
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def build_route_dep_plans(route_dependencies: list, *, websocket: bool = False) -> list[_Slot]:
|
|
398
|
+
"""Pre-plan a route's `dependencies=[Depends(...), ...]` list."""
|
|
399
|
+
from veloce.dependency import Depends # local import breaks the cycle
|
|
400
|
+
|
|
401
|
+
out: list[_Slot] = []
|
|
402
|
+
for dep in route_dependencies:
|
|
403
|
+
if isinstance(dep, Depends):
|
|
404
|
+
out.append(_build_depends_slot("", dep, websocket=websocket))
|
|
405
|
+
return out
|