vega-framework 0.1.35__py3-none-any.whl → 0.2.1__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.
- vega/cli/commands/add.py +9 -10
- vega/cli/commands/generate.py +15 -15
- vega/cli/commands/init.py +9 -8
- vega/cli/commands/web.py +8 -7
- vega/cli/main.py +4 -4
- vega/cli/scaffolds/__init__.py +6 -2
- vega/cli/scaffolds/vega_web.py +109 -0
- vega/cli/templates/__init__.py +34 -8
- vega/cli/templates/components.py +29 -13
- vega/cli/templates/project/ARCHITECTURE.md.j2 +13 -13
- vega/cli/templates/project/README.md.j2 +5 -5
- vega/cli/templates/web/app.py.j2 +5 -5
- vega/cli/templates/web/health_route.py.j2 +2 -2
- vega/cli/templates/web/main.py.j2 +2 -3
- vega/cli/templates/web/middleware.py.j2 +3 -3
- vega/cli/templates/web/router.py.j2 +2 -2
- vega/cli/templates/web/routes_init.py.j2 +3 -3
- vega/cli/templates/web/routes_init_autodiscovery.py.j2 +2 -2
- vega/cli/templates/web/users_route.py.j2 +2 -2
- vega/discovery/routes.py +13 -13
- vega/web/__init__.py +100 -0
- vega/web/application.py +234 -0
- vega/web/builtin_middlewares.py +288 -0
- vega/web/exceptions.py +151 -0
- vega/web/middleware.py +185 -0
- vega/web/request.py +120 -0
- vega/web/response.py +220 -0
- vega/web/route_middleware.py +266 -0
- vega/web/router.py +350 -0
- vega/web/routing.py +347 -0
- {vega_framework-0.1.35.dist-info → vega_framework-0.2.1.dist-info}/METADATA +10 -9
- {vega_framework-0.1.35.dist-info → vega_framework-0.2.1.dist-info}/RECORD +35 -24
- {vega_framework-0.1.35.dist-info → vega_framework-0.2.1.dist-info}/WHEEL +0 -0
- {vega_framework-0.1.35.dist-info → vega_framework-0.2.1.dist-info}/entry_points.txt +0 -0
- {vega_framework-0.1.35.dist-info → vega_framework-0.2.1.dist-info}/licenses/LICENSE +0 -0
vega/web/request.py
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
"""Request wrapper for Vega Web Framework"""
|
2
|
+
|
3
|
+
from typing import Any, Dict, Optional
|
4
|
+
from starlette.requests import Request as StarletteRequest
|
5
|
+
|
6
|
+
|
7
|
+
class Request(StarletteRequest):
|
8
|
+
"""
|
9
|
+
HTTP Request wrapper built on Starlette.
|
10
|
+
|
11
|
+
This class extends Starlette's Request to provide a familiar interface
|
12
|
+
for developers coming from FastAPI while maintaining full compatibility
|
13
|
+
with Starlette's ecosystem.
|
14
|
+
|
15
|
+
Attributes:
|
16
|
+
method: HTTP method (GET, POST, etc.)
|
17
|
+
url: Request URL
|
18
|
+
headers: HTTP headers
|
19
|
+
query_params: Query string parameters
|
20
|
+
path_params: Path parameters from URL
|
21
|
+
cookies: Request cookies
|
22
|
+
client: Client address info
|
23
|
+
|
24
|
+
Example:
|
25
|
+
@router.get("/users/{user_id}")
|
26
|
+
async def get_user(request: Request, user_id: str):
|
27
|
+
# Access path parameters
|
28
|
+
assert user_id == request.path_params["user_id"]
|
29
|
+
|
30
|
+
# Access query parameters
|
31
|
+
limit = request.query_params.get("limit", "10")
|
32
|
+
|
33
|
+
# Parse JSON body
|
34
|
+
body = await request.json()
|
35
|
+
|
36
|
+
return {"user_id": user_id, "limit": limit}
|
37
|
+
"""
|
38
|
+
|
39
|
+
async def json(self) -> Any:
|
40
|
+
"""
|
41
|
+
Parse request body as JSON.
|
42
|
+
|
43
|
+
Returns:
|
44
|
+
Parsed JSON data
|
45
|
+
|
46
|
+
Raises:
|
47
|
+
ValueError: If body is not valid JSON
|
48
|
+
"""
|
49
|
+
return await super().json()
|
50
|
+
|
51
|
+
async def form(self) -> Dict[str, Any]:
|
52
|
+
"""
|
53
|
+
Parse request body as form data.
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
Form data as dictionary
|
57
|
+
"""
|
58
|
+
form_data = await super().form()
|
59
|
+
return dict(form_data)
|
60
|
+
|
61
|
+
async def body(self) -> bytes:
|
62
|
+
"""
|
63
|
+
Get raw request body as bytes.
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
Raw body bytes
|
67
|
+
"""
|
68
|
+
return await super().body()
|
69
|
+
|
70
|
+
@property
|
71
|
+
def path_params(self) -> Dict[str, Any]:
|
72
|
+
"""
|
73
|
+
Get path parameters extracted from URL.
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
Dictionary of path parameters
|
77
|
+
"""
|
78
|
+
return self.scope.get("path_params", {})
|
79
|
+
|
80
|
+
def get_header(self, name: str, default: Optional[str] = None) -> Optional[str]:
|
81
|
+
"""
|
82
|
+
Get a specific header value.
|
83
|
+
|
84
|
+
Args:
|
85
|
+
name: Header name (case-insensitive)
|
86
|
+
default: Default value if header is not present
|
87
|
+
|
88
|
+
Returns:
|
89
|
+
Header value or default
|
90
|
+
"""
|
91
|
+
return self.headers.get(name.lower(), default)
|
92
|
+
|
93
|
+
def get_query_param(self, name: str, default: Optional[str] = None) -> Optional[str]:
|
94
|
+
"""
|
95
|
+
Get a specific query parameter value.
|
96
|
+
|
97
|
+
Args:
|
98
|
+
name: Parameter name
|
99
|
+
default: Default value if parameter is not present
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
Parameter value or default
|
103
|
+
"""
|
104
|
+
return self.query_params.get(name, default)
|
105
|
+
|
106
|
+
def get_cookie(self, name: str, default: Optional[str] = None) -> Optional[str]:
|
107
|
+
"""
|
108
|
+
Get a specific cookie value.
|
109
|
+
|
110
|
+
Args:
|
111
|
+
name: Cookie name
|
112
|
+
default: Default value if cookie is not present
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
Cookie value or default
|
116
|
+
"""
|
117
|
+
return self.cookies.get(name, default)
|
118
|
+
|
119
|
+
|
120
|
+
__all__ = ["Request"]
|
vega/web/response.py
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
"""Response classes for Vega Web Framework"""
|
2
|
+
|
3
|
+
import json
|
4
|
+
from typing import Any, Dict, Optional, Union
|
5
|
+
|
6
|
+
from starlette.responses import (
|
7
|
+
Response as StarletteResponse,
|
8
|
+
JSONResponse as StarletteJSONResponse,
|
9
|
+
HTMLResponse as StarletteHTMLResponse,
|
10
|
+
PlainTextResponse as StarlettePlainTextResponse,
|
11
|
+
RedirectResponse as StarletteRedirectResponse,
|
12
|
+
StreamingResponse as StarletteStreamingResponse,
|
13
|
+
FileResponse as StarletteFileResponse,
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
class Response(StarletteResponse):
|
18
|
+
"""
|
19
|
+
Base HTTP Response class.
|
20
|
+
|
21
|
+
A thin wrapper around Starlette's Response for API consistency.
|
22
|
+
|
23
|
+
Args:
|
24
|
+
content: Response body content
|
25
|
+
status_code: HTTP status code (default: 200)
|
26
|
+
headers: Optional HTTP headers
|
27
|
+
media_type: Content-Type header value
|
28
|
+
|
29
|
+
Example:
|
30
|
+
return Response(content="Success", status_code=200)
|
31
|
+
return Response(content=b"binary data", media_type="application/octet-stream")
|
32
|
+
"""
|
33
|
+
|
34
|
+
pass
|
35
|
+
|
36
|
+
|
37
|
+
class JSONResponse(StarletteJSONResponse):
|
38
|
+
"""
|
39
|
+
JSON HTTP Response.
|
40
|
+
|
41
|
+
Automatically serializes Python objects to JSON and sets appropriate headers.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
content: Object to serialize as JSON
|
45
|
+
status_code: HTTP status code (default: 200)
|
46
|
+
headers: Optional HTTP headers
|
47
|
+
|
48
|
+
Example:
|
49
|
+
return JSONResponse({"status": "ok", "data": [1, 2, 3]})
|
50
|
+
return JSONResponse({"error": "Not found"}, status_code=404)
|
51
|
+
"""
|
52
|
+
|
53
|
+
def render(self, content: Any) -> bytes:
|
54
|
+
"""Render content as JSON bytes"""
|
55
|
+
return json.dumps(
|
56
|
+
content,
|
57
|
+
ensure_ascii=False,
|
58
|
+
allow_nan=False,
|
59
|
+
indent=None,
|
60
|
+
separators=(",", ":"),
|
61
|
+
).encode("utf-8")
|
62
|
+
|
63
|
+
|
64
|
+
class HTMLResponse(StarletteHTMLResponse):
|
65
|
+
"""
|
66
|
+
HTML HTTP Response.
|
67
|
+
|
68
|
+
Args:
|
69
|
+
content: HTML content as string
|
70
|
+
status_code: HTTP status code (default: 200)
|
71
|
+
headers: Optional HTTP headers
|
72
|
+
|
73
|
+
Example:
|
74
|
+
return HTMLResponse("<h1>Hello World</h1>")
|
75
|
+
"""
|
76
|
+
|
77
|
+
pass
|
78
|
+
|
79
|
+
|
80
|
+
class PlainTextResponse(StarlettePlainTextResponse):
|
81
|
+
"""
|
82
|
+
Plain text HTTP Response.
|
83
|
+
|
84
|
+
Args:
|
85
|
+
content: Text content
|
86
|
+
status_code: HTTP status code (default: 200)
|
87
|
+
headers: Optional HTTP headers
|
88
|
+
|
89
|
+
Example:
|
90
|
+
return PlainTextResponse("Hello, World!")
|
91
|
+
"""
|
92
|
+
|
93
|
+
pass
|
94
|
+
|
95
|
+
|
96
|
+
class RedirectResponse(StarletteRedirectResponse):
|
97
|
+
"""
|
98
|
+
HTTP Redirect Response.
|
99
|
+
|
100
|
+
Args:
|
101
|
+
url: URL to redirect to
|
102
|
+
status_code: HTTP status code (default: 307)
|
103
|
+
headers: Optional HTTP headers
|
104
|
+
|
105
|
+
Example:
|
106
|
+
return RedirectResponse(url="/new-location")
|
107
|
+
return RedirectResponse(url="/login", status_code=302)
|
108
|
+
"""
|
109
|
+
|
110
|
+
pass
|
111
|
+
|
112
|
+
|
113
|
+
class StreamingResponse(StarletteStreamingResponse):
|
114
|
+
"""
|
115
|
+
Streaming HTTP Response.
|
116
|
+
|
117
|
+
Useful for large files or real-time data.
|
118
|
+
|
119
|
+
Args:
|
120
|
+
content: Async generator or iterator
|
121
|
+
status_code: HTTP status code (default: 200)
|
122
|
+
headers: Optional HTTP headers
|
123
|
+
media_type: Content-Type header value
|
124
|
+
|
125
|
+
Example:
|
126
|
+
async def generate():
|
127
|
+
for i in range(10):
|
128
|
+
yield f"data: {i}\\n\\n"
|
129
|
+
await asyncio.sleep(1)
|
130
|
+
|
131
|
+
return StreamingResponse(generate(), media_type="text/event-stream")
|
132
|
+
"""
|
133
|
+
|
134
|
+
pass
|
135
|
+
|
136
|
+
|
137
|
+
class FileResponse(StarletteFileResponse):
|
138
|
+
"""
|
139
|
+
File HTTP Response.
|
140
|
+
|
141
|
+
Efficiently serves files from disk.
|
142
|
+
|
143
|
+
Args:
|
144
|
+
path: Path to file
|
145
|
+
status_code: HTTP status code (default: 200)
|
146
|
+
headers: Optional HTTP headers
|
147
|
+
media_type: Content-Type (auto-detected if not provided)
|
148
|
+
filename: If set, includes Content-Disposition header
|
149
|
+
|
150
|
+
Example:
|
151
|
+
return FileResponse("report.pdf", media_type="application/pdf")
|
152
|
+
return FileResponse("image.jpg", filename="download.jpg")
|
153
|
+
"""
|
154
|
+
|
155
|
+
pass
|
156
|
+
|
157
|
+
|
158
|
+
def create_response(
|
159
|
+
content: Any = None,
|
160
|
+
status_code: int = 200,
|
161
|
+
headers: Optional[Dict[str, str]] = None,
|
162
|
+
media_type: Optional[str] = None,
|
163
|
+
) -> Union[Response, JSONResponse]:
|
164
|
+
"""
|
165
|
+
Create an appropriate response based on content type.
|
166
|
+
|
167
|
+
Automatically chooses between Response and JSONResponse based on content.
|
168
|
+
|
169
|
+
Args:
|
170
|
+
content: Response content
|
171
|
+
status_code: HTTP status code
|
172
|
+
headers: Optional HTTP headers
|
173
|
+
media_type: Content-Type header value
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
Response object
|
177
|
+
|
178
|
+
Example:
|
179
|
+
return create_response({"status": "ok"}) # Returns JSONResponse
|
180
|
+
return create_response("text content") # Returns Response
|
181
|
+
"""
|
182
|
+
if content is None:
|
183
|
+
return Response(content=b"", status_code=status_code, headers=headers)
|
184
|
+
|
185
|
+
# If it's a dict, list, or other JSON-serializable type
|
186
|
+
if isinstance(content, (dict, list)):
|
187
|
+
return JSONResponse(content=content, status_code=status_code, headers=headers)
|
188
|
+
|
189
|
+
# If it's a string
|
190
|
+
if isinstance(content, str):
|
191
|
+
return Response(
|
192
|
+
content=content,
|
193
|
+
status_code=status_code,
|
194
|
+
headers=headers,
|
195
|
+
media_type=media_type or "text/plain",
|
196
|
+
)
|
197
|
+
|
198
|
+
# If it's bytes
|
199
|
+
if isinstance(content, bytes):
|
200
|
+
return Response(
|
201
|
+
content=content,
|
202
|
+
status_code=status_code,
|
203
|
+
headers=headers,
|
204
|
+
media_type=media_type or "application/octet-stream",
|
205
|
+
)
|
206
|
+
|
207
|
+
# Default to JSON for other types
|
208
|
+
return JSONResponse(content=content, status_code=status_code, headers=headers)
|
209
|
+
|
210
|
+
|
211
|
+
__all__ = [
|
212
|
+
"Response",
|
213
|
+
"JSONResponse",
|
214
|
+
"HTMLResponse",
|
215
|
+
"PlainTextResponse",
|
216
|
+
"RedirectResponse",
|
217
|
+
"StreamingResponse",
|
218
|
+
"FileResponse",
|
219
|
+
"create_response",
|
220
|
+
]
|
@@ -0,0 +1,266 @@
|
|
1
|
+
"""Route-level middleware system for Vega Web Framework"""
|
2
|
+
|
3
|
+
from typing import Callable, List, Optional, Union, Any
|
4
|
+
from enum import Enum
|
5
|
+
from functools import wraps
|
6
|
+
import inspect
|
7
|
+
|
8
|
+
from .request import Request
|
9
|
+
from .response import Response, JSONResponse
|
10
|
+
|
11
|
+
|
12
|
+
class MiddlewarePhase(str, Enum):
|
13
|
+
"""When the middleware should execute"""
|
14
|
+
BEFORE = "before" # Execute before the handler
|
15
|
+
AFTER = "after" # Execute after the handler
|
16
|
+
BOTH = "both" # Execute both before and after
|
17
|
+
|
18
|
+
|
19
|
+
class RouteMiddleware:
|
20
|
+
"""
|
21
|
+
Base class for route-level middleware.
|
22
|
+
|
23
|
+
Route middleware can execute before or after the handler function,
|
24
|
+
allowing for request preprocessing, response postprocessing, or both.
|
25
|
+
|
26
|
+
Attributes:
|
27
|
+
phase: When to execute (BEFORE, AFTER, or BOTH)
|
28
|
+
|
29
|
+
Example:
|
30
|
+
class AuthMiddleware(RouteMiddleware):
|
31
|
+
def __init__(self):
|
32
|
+
super().__init__(phase=MiddlewarePhase.BEFORE)
|
33
|
+
|
34
|
+
async def before(self, request: Request) -> Optional[Response]:
|
35
|
+
token = request.headers.get("Authorization")
|
36
|
+
if not token:
|
37
|
+
return JSONResponse(
|
38
|
+
{"detail": "Missing authorization"},
|
39
|
+
status_code=401
|
40
|
+
)
|
41
|
+
# Continue to handler
|
42
|
+
return None
|
43
|
+
|
44
|
+
@router.get("/protected")
|
45
|
+
@middleware(AuthMiddleware())
|
46
|
+
async def protected_route():
|
47
|
+
return {"message": "Protected data"}
|
48
|
+
"""
|
49
|
+
|
50
|
+
def __init__(self, phase: MiddlewarePhase = MiddlewarePhase.BEFORE):
|
51
|
+
self.phase = phase
|
52
|
+
|
53
|
+
async def before(self, request: Request) -> Optional[Response]:
|
54
|
+
"""
|
55
|
+
Execute before the handler.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
request: The incoming request
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
Optional[Response]: Return a Response to short-circuit,
|
62
|
+
or None to continue to the handler
|
63
|
+
"""
|
64
|
+
return None
|
65
|
+
|
66
|
+
async def after(
|
67
|
+
self,
|
68
|
+
request: Request,
|
69
|
+
response: Response
|
70
|
+
) -> Response:
|
71
|
+
"""
|
72
|
+
Execute after the handler.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
request: The incoming request
|
76
|
+
response: The response from the handler
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
Response: Modified or original response
|
80
|
+
"""
|
81
|
+
return response
|
82
|
+
|
83
|
+
async def process(
|
84
|
+
self,
|
85
|
+
request: Request,
|
86
|
+
handler: Callable,
|
87
|
+
**kwargs
|
88
|
+
) -> Response:
|
89
|
+
"""
|
90
|
+
Process the middleware chain.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
request: The incoming request
|
94
|
+
handler: The route handler function
|
95
|
+
**kwargs: Handler keyword arguments
|
96
|
+
|
97
|
+
Returns:
|
98
|
+
Response object
|
99
|
+
"""
|
100
|
+
# Execute before phase
|
101
|
+
if self.phase in (MiddlewarePhase.BEFORE, MiddlewarePhase.BOTH):
|
102
|
+
before_response = await self.before(request)
|
103
|
+
if before_response is not None:
|
104
|
+
# Short-circuit: return response without calling handler
|
105
|
+
return before_response
|
106
|
+
|
107
|
+
# Call the handler
|
108
|
+
if inspect.iscoroutinefunction(handler):
|
109
|
+
result = await handler(**kwargs)
|
110
|
+
else:
|
111
|
+
result = handler(**kwargs)
|
112
|
+
|
113
|
+
# Convert result to Response if needed
|
114
|
+
if isinstance(result, (Response, JSONResponse)):
|
115
|
+
response = result
|
116
|
+
elif isinstance(result, dict):
|
117
|
+
response = JSONResponse(content=result)
|
118
|
+
elif isinstance(result, (list, tuple)):
|
119
|
+
response = JSONResponse(content=result)
|
120
|
+
elif isinstance(result, str):
|
121
|
+
response = Response(content=result)
|
122
|
+
elif result is None:
|
123
|
+
response = Response(content=b"")
|
124
|
+
else:
|
125
|
+
response = JSONResponse(content=result)
|
126
|
+
|
127
|
+
# Execute after phase
|
128
|
+
if self.phase in (MiddlewarePhase.AFTER, MiddlewarePhase.BOTH):
|
129
|
+
response = await self.after(request, response)
|
130
|
+
|
131
|
+
return response
|
132
|
+
|
133
|
+
|
134
|
+
class MiddlewareChain:
|
135
|
+
"""
|
136
|
+
Manages a chain of middleware for a route.
|
137
|
+
|
138
|
+
Example:
|
139
|
+
chain = MiddlewareChain([AuthMiddleware(), LoggingMiddleware()])
|
140
|
+
response = await chain.execute(request, handler, user_id="123")
|
141
|
+
"""
|
142
|
+
|
143
|
+
def __init__(self, middlewares: List[RouteMiddleware]):
|
144
|
+
self.middlewares = middlewares
|
145
|
+
|
146
|
+
async def execute(
|
147
|
+
self,
|
148
|
+
request: Request,
|
149
|
+
handler: Callable,
|
150
|
+
**kwargs
|
151
|
+
) -> Response:
|
152
|
+
"""
|
153
|
+
Execute the middleware chain.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
request: The incoming request
|
157
|
+
handler: The route handler
|
158
|
+
**kwargs: Handler arguments
|
159
|
+
|
160
|
+
Returns:
|
161
|
+
Response object
|
162
|
+
"""
|
163
|
+
if not self.middlewares:
|
164
|
+
# No middleware, call handler directly
|
165
|
+
if inspect.iscoroutinefunction(handler):
|
166
|
+
result = await handler(**kwargs)
|
167
|
+
else:
|
168
|
+
result = handler(**kwargs)
|
169
|
+
|
170
|
+
# Convert to response
|
171
|
+
if isinstance(result, (Response, JSONResponse)):
|
172
|
+
return result
|
173
|
+
elif isinstance(result, dict):
|
174
|
+
return JSONResponse(content=result)
|
175
|
+
elif isinstance(result, (list, tuple)):
|
176
|
+
return JSONResponse(content=result)
|
177
|
+
elif isinstance(result, str):
|
178
|
+
return Response(content=result)
|
179
|
+
elif result is None:
|
180
|
+
return Response(content=b"")
|
181
|
+
else:
|
182
|
+
return JSONResponse(content=result)
|
183
|
+
|
184
|
+
# Execute BEFORE middleware
|
185
|
+
for mw in self.middlewares:
|
186
|
+
if mw.phase in (MiddlewarePhase.BEFORE, MiddlewarePhase.BOTH):
|
187
|
+
before_response = await mw.before(request)
|
188
|
+
if before_response is not None:
|
189
|
+
# Short-circuit
|
190
|
+
return before_response
|
191
|
+
|
192
|
+
# Call handler - check if it needs request parameter
|
193
|
+
sig = inspect.signature(handler)
|
194
|
+
handler_params = sig.parameters
|
195
|
+
|
196
|
+
# Add request to kwargs if handler expects it
|
197
|
+
if "request" in handler_params or any(
|
198
|
+
p.annotation.__name__ == "Request" if hasattr(p.annotation, "__name__") else False
|
199
|
+
for p in handler_params.values()
|
200
|
+
):
|
201
|
+
kwargs["request"] = request
|
202
|
+
|
203
|
+
if inspect.iscoroutinefunction(handler):
|
204
|
+
result = await handler(**kwargs)
|
205
|
+
else:
|
206
|
+
result = handler(**kwargs)
|
207
|
+
|
208
|
+
# Convert to response
|
209
|
+
if isinstance(result, (Response, JSONResponse)):
|
210
|
+
response = result
|
211
|
+
elif isinstance(result, dict):
|
212
|
+
response = JSONResponse(content=result)
|
213
|
+
elif isinstance(result, (list, tuple)):
|
214
|
+
response = JSONResponse(content=result)
|
215
|
+
elif isinstance(result, str):
|
216
|
+
response = Response(content=result)
|
217
|
+
elif result is None:
|
218
|
+
response = Response(content=b"")
|
219
|
+
else:
|
220
|
+
response = JSONResponse(content=result)
|
221
|
+
|
222
|
+
# Execute AFTER middleware (in reverse order)
|
223
|
+
for mw in reversed(self.middlewares):
|
224
|
+
if mw.phase in (MiddlewarePhase.AFTER, MiddlewarePhase.BOTH):
|
225
|
+
response = await mw.after(request, response)
|
226
|
+
|
227
|
+
return response
|
228
|
+
|
229
|
+
|
230
|
+
def middleware(*middlewares: RouteMiddleware) -> Callable:
|
231
|
+
"""
|
232
|
+
Decorator to attach middleware to a route handler.
|
233
|
+
|
234
|
+
Args:
|
235
|
+
*middlewares: One or more RouteMiddleware instances
|
236
|
+
|
237
|
+
Example:
|
238
|
+
@router.get("/users/{user_id}")
|
239
|
+
@middleware(AuthMiddleware(), LoggingMiddleware())
|
240
|
+
async def get_user(user_id: str):
|
241
|
+
return {"id": user_id}
|
242
|
+
|
243
|
+
# Or single middleware
|
244
|
+
@router.post("/admin/action")
|
245
|
+
@middleware(AdminOnlyMiddleware())
|
246
|
+
async def admin_action():
|
247
|
+
return {"status": "done"}
|
248
|
+
"""
|
249
|
+
middleware_list = list(middlewares)
|
250
|
+
|
251
|
+
def decorator(func: Callable) -> Callable:
|
252
|
+
# Store middleware list on the function
|
253
|
+
if not hasattr(func, '_route_middlewares'):
|
254
|
+
func._route_middlewares = []
|
255
|
+
func._route_middlewares.extend(middleware_list)
|
256
|
+
return func
|
257
|
+
|
258
|
+
return decorator
|
259
|
+
|
260
|
+
|
261
|
+
__all__ = [
|
262
|
+
"RouteMiddleware",
|
263
|
+
"MiddlewarePhase",
|
264
|
+
"MiddlewareChain",
|
265
|
+
"middleware",
|
266
|
+
]
|