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.
Files changed (65) hide show
  1. veloce/__init__.py +314 -0
  2. veloce/_handler_plan.py +405 -0
  3. veloce/_types.py +26 -0
  4. veloce/app.py +3316 -0
  5. veloce/background.py +47 -0
  6. veloce/blueprints.py +222 -0
  7. veloce/cli.py +209 -0
  8. veloce/config.py +318 -0
  9. veloce/contrib/__init__.py +1 -0
  10. veloce/contrib/openapi.py +705 -0
  11. veloce/contrib/staticfiles.py +412 -0
  12. veloce/contrib/templating.py +260 -0
  13. veloce/dependency.py +813 -0
  14. veloce/encoders.py +105 -0
  15. veloce/exception_handlers.py +43 -0
  16. veloce/exceptions.py +359 -0
  17. veloce/helpers.py +640 -0
  18. veloce/http/__init__.py +28 -0
  19. veloce/http/cache_control.py +130 -0
  20. veloce/http/cookies.py +86 -0
  21. veloce/http/datastructures.py +795 -0
  22. veloce/http/dates.py +85 -0
  23. veloce/http/header_set.py +97 -0
  24. veloce/http/header_utils.py +117 -0
  25. veloce/http/request.py +1058 -0
  26. veloce/http/response.py +1285 -0
  27. veloce/instrumentation.py +46 -0
  28. veloce/json_provider.py +79 -0
  29. veloce/markup.py +92 -0
  30. veloce/middleware/__init__.py +34 -0
  31. veloce/middleware/base.py +82 -0
  32. veloce/middleware/compression.py +100 -0
  33. veloce/middleware/cors.py +184 -0
  34. veloce/middleware/csrf.py +162 -0
  35. veloce/middleware/logging.py +86 -0
  36. veloce/middleware/proxy_fix.py +149 -0
  37. veloce/middleware/security.py +244 -0
  38. veloce/middleware/sessions.py +197 -0
  39. veloce/passwords.py +218 -0
  40. veloce/py.typed +0 -0
  41. veloce/routing/__init__.py +17 -0
  42. veloce/routing/converters.py +189 -0
  43. veloce/routing/params.py +186 -0
  44. veloce/routing/router.py +880 -0
  45. veloce/safe.py +145 -0
  46. veloce/security/__init__.py +33 -0
  47. veloce/security/api_key.py +57 -0
  48. veloce/security/http.py +230 -0
  49. veloce/security/oauth2.py +186 -0
  50. veloce/serving/__init__.py +5 -0
  51. veloce/serving/protocol.py +227 -0
  52. veloce/sessions.py +188 -0
  53. veloce/signals.py +181 -0
  54. veloce/signing.py +196 -0
  55. veloce/sse.py +130 -0
  56. veloce/status.py +97 -0
  57. veloce/testclient.py +1128 -0
  58. veloce/views.py +142 -0
  59. veloce/watchdog.py +175 -0
  60. veloce/websocket.py +703 -0
  61. veloceframework-0.1.0.dist-info/METADATA +173 -0
  62. veloceframework-0.1.0.dist-info/RECORD +65 -0
  63. veloceframework-0.1.0.dist-info/WHEEL +4 -0
  64. veloceframework-0.1.0.dist-info/entry_points.txt +2 -0
  65. 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
+ ]
@@ -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