vega-framework 0.1.34__py3-none-any.whl → 0.2.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.
@@ -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
+ ]
vega/web/router.py ADDED
@@ -0,0 +1,350 @@
1
+ """Router class for Vega Web Framework"""
2
+
3
+ from typing import Any, Callable, List, Optional, Sequence, Type
4
+
5
+ from starlette.routing import Mount
6
+
7
+ from .routing import Route, route as create_route
8
+
9
+
10
+ class Router:
11
+ """
12
+ HTTP Router for organizing endpoints.
13
+
14
+ Similar to FastAPI's APIRouter, this class allows you to group related
15
+ endpoints together with common configuration like prefix, tags, etc.
16
+
17
+ Args:
18
+ prefix: URL prefix for all routes (e.g., "/api/v1")
19
+ tags: Default tags for all routes
20
+ dependencies: Shared dependencies (future feature)
21
+ responses: Common response models (future feature)
22
+
23
+ Example:
24
+ router = Router(prefix="/users", tags=["users"])
25
+
26
+ @router.get("/{user_id}")
27
+ async def get_user(user_id: str):
28
+ return {"id": user_id, "name": "John"}
29
+
30
+ @router.post("")
31
+ async def create_user(request: Request):
32
+ data = await request.json()
33
+ return {"id": "new_id", **data}
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ prefix: str = "",
39
+ tags: Optional[List[str]] = None,
40
+ dependencies: Optional[Sequence[Any]] = None,
41
+ responses: Optional[dict] = None,
42
+ ):
43
+ self.prefix = prefix
44
+ self.tags = tags or []
45
+ self.dependencies = dependencies or []
46
+ self.responses = responses or {}
47
+ self.routes: List[Route] = []
48
+ self.child_routers: List[tuple[Router, str, Optional[List[str]]]] = []
49
+
50
+ def add_route(
51
+ self,
52
+ path: str,
53
+ endpoint: Callable,
54
+ methods: List[str],
55
+ *,
56
+ name: Optional[str] = None,
57
+ include_in_schema: bool = True,
58
+ tags: Optional[List[str]] = None,
59
+ summary: Optional[str] = None,
60
+ description: Optional[str] = None,
61
+ response_model: Optional[Type] = None,
62
+ status_code: int = 200,
63
+ ) -> None:
64
+ """
65
+ Add a route to the router.
66
+
67
+ Args:
68
+ path: URL path pattern
69
+ endpoint: Handler function
70
+ methods: HTTP methods
71
+ name: Optional route name
72
+ include_in_schema: Include in API docs
73
+ tags: Route tags
74
+ summary: Short description
75
+ description: Longer description
76
+ response_model: Expected response type
77
+ status_code: Default status code
78
+ """
79
+ # Merge router tags with route-specific tags
80
+ route_tags = (tags or []) + self.tags
81
+
82
+ route_obj = Route(
83
+ path=path,
84
+ endpoint=endpoint,
85
+ methods=methods,
86
+ name=name,
87
+ include_in_schema=include_in_schema,
88
+ tags=route_tags,
89
+ summary=summary,
90
+ description=description,
91
+ response_model=response_model,
92
+ status_code=status_code,
93
+ )
94
+ self.routes.append(route_obj)
95
+
96
+ def route(
97
+ self,
98
+ path: str,
99
+ methods: List[str],
100
+ *,
101
+ name: Optional[str] = None,
102
+ include_in_schema: bool = True,
103
+ tags: Optional[List[str]] = None,
104
+ summary: Optional[str] = None,
105
+ description: Optional[str] = None,
106
+ response_model: Optional[Type] = None,
107
+ status_code: int = 200,
108
+ ) -> Callable:
109
+ """
110
+ Decorator to add a route.
111
+
112
+ Example:
113
+ @router.route("/items", methods=["GET", "POST"])
114
+ async def items():
115
+ return {"items": []}
116
+ """
117
+
118
+ def decorator(func: Callable) -> Callable:
119
+ self.add_route(
120
+ path=path,
121
+ endpoint=func,
122
+ methods=methods,
123
+ name=name,
124
+ include_in_schema=include_in_schema,
125
+ tags=tags,
126
+ summary=summary,
127
+ description=description,
128
+ response_model=response_model,
129
+ status_code=status_code,
130
+ )
131
+ return func
132
+
133
+ return decorator
134
+
135
+ def get(
136
+ self,
137
+ path: str,
138
+ *,
139
+ name: Optional[str] = None,
140
+ include_in_schema: bool = True,
141
+ tags: Optional[List[str]] = None,
142
+ summary: Optional[str] = None,
143
+ description: Optional[str] = None,
144
+ response_model: Optional[Type] = None,
145
+ status_code: int = 200,
146
+ ) -> Callable:
147
+ """
148
+ Decorator for GET requests.
149
+
150
+ Example:
151
+ @router.get("/items/{item_id}")
152
+ async def get_item(item_id: str):
153
+ return {"id": item_id}
154
+ """
155
+ return self.route(
156
+ path,
157
+ methods=["GET"],
158
+ name=name,
159
+ include_in_schema=include_in_schema,
160
+ tags=tags,
161
+ summary=summary,
162
+ description=description,
163
+ response_model=response_model,
164
+ status_code=status_code,
165
+ )
166
+
167
+ def post(
168
+ self,
169
+ path: str,
170
+ *,
171
+ name: Optional[str] = None,
172
+ include_in_schema: bool = True,
173
+ tags: Optional[List[str]] = None,
174
+ summary: Optional[str] = None,
175
+ description: Optional[str] = None,
176
+ response_model: Optional[Type] = None,
177
+ status_code: int = 201,
178
+ ) -> Callable:
179
+ """
180
+ Decorator for POST requests.
181
+
182
+ Example:
183
+ @router.post("/items")
184
+ async def create_item(request: Request):
185
+ data = await request.json()
186
+ return {"id": "new", **data}
187
+ """
188
+ return self.route(
189
+ path,
190
+ methods=["POST"],
191
+ name=name,
192
+ include_in_schema=include_in_schema,
193
+ tags=tags,
194
+ summary=summary,
195
+ description=description,
196
+ response_model=response_model,
197
+ status_code=status_code,
198
+ )
199
+
200
+ def put(
201
+ self,
202
+ path: str,
203
+ *,
204
+ name: Optional[str] = None,
205
+ include_in_schema: bool = True,
206
+ tags: Optional[List[str]] = None,
207
+ summary: Optional[str] = None,
208
+ description: Optional[str] = None,
209
+ response_model: Optional[Type] = None,
210
+ status_code: int = 200,
211
+ ) -> Callable:
212
+ """Decorator for PUT requests."""
213
+ return self.route(
214
+ path,
215
+ methods=["PUT"],
216
+ name=name,
217
+ include_in_schema=include_in_schema,
218
+ tags=tags,
219
+ summary=summary,
220
+ description=description,
221
+ response_model=response_model,
222
+ status_code=status_code,
223
+ )
224
+
225
+ def patch(
226
+ self,
227
+ path: str,
228
+ *,
229
+ name: Optional[str] = None,
230
+ include_in_schema: bool = True,
231
+ tags: Optional[List[str]] = None,
232
+ summary: Optional[str] = None,
233
+ description: Optional[str] = None,
234
+ response_model: Optional[Type] = None,
235
+ status_code: int = 200,
236
+ ) -> Callable:
237
+ """Decorator for PATCH requests."""
238
+ return self.route(
239
+ path,
240
+ methods=["PATCH"],
241
+ name=name,
242
+ include_in_schema=include_in_schema,
243
+ tags=tags,
244
+ summary=summary,
245
+ description=description,
246
+ response_model=response_model,
247
+ status_code=status_code,
248
+ )
249
+
250
+ def delete(
251
+ self,
252
+ path: str,
253
+ *,
254
+ name: Optional[str] = None,
255
+ include_in_schema: bool = True,
256
+ tags: Optional[List[str]] = None,
257
+ summary: Optional[str] = None,
258
+ description: Optional[str] = None,
259
+ response_model: Optional[Type] = None,
260
+ status_code: int = 204,
261
+ ) -> Callable:
262
+ """Decorator for DELETE requests."""
263
+ return self.route(
264
+ path,
265
+ methods=["DELETE"],
266
+ name=name,
267
+ include_in_schema=include_in_schema,
268
+ tags=tags,
269
+ summary=summary,
270
+ description=description,
271
+ response_model=response_model,
272
+ status_code=status_code,
273
+ )
274
+
275
+ def include_router(
276
+ self,
277
+ router: "Router",
278
+ prefix: str = "",
279
+ tags: Optional[List[str]] = None,
280
+ ) -> None:
281
+ """
282
+ Include another router's routes in this router.
283
+
284
+ Args:
285
+ router: Router to include
286
+ prefix: Additional prefix for included routes
287
+ tags: Additional tags for included routes
288
+
289
+ Example:
290
+ users_router = Router()
291
+ @users_router.get("/{user_id}")
292
+ async def get_user(user_id: str):
293
+ return {"id": user_id}
294
+
295
+ main_router = Router()
296
+ main_router.include_router(users_router, prefix="/users", tags=["users"])
297
+ """
298
+ self.child_routers.append((router, prefix, tags))
299
+
300
+ def get_routes(self) -> List[Route]:
301
+ """
302
+ Get all routes including child routers.
303
+
304
+ Returns:
305
+ List of Route objects with prefixes applied
306
+ """
307
+ routes = []
308
+
309
+ # Add direct routes with prefix
310
+ for route in self.routes:
311
+ # Create a copy with the prefix applied
312
+ prefixed_route = Route(
313
+ path=self.prefix + route.path,
314
+ endpoint=route.endpoint,
315
+ methods=route.methods,
316
+ name=route.name,
317
+ include_in_schema=route.include_in_schema,
318
+ tags=route.tags,
319
+ summary=route.summary,
320
+ description=route.description,
321
+ response_model=route.response_model,
322
+ status_code=route.status_code,
323
+ )
324
+ routes.append(prefixed_route)
325
+
326
+ # Add child router routes
327
+ for child_router, child_prefix, child_tags in self.child_routers:
328
+ for route in child_router.get_routes():
329
+ # Apply additional prefix and tags
330
+ combined_prefix = self.prefix + child_prefix
331
+ combined_tags = route.tags + (child_tags or [])
332
+
333
+ prefixed_route = Route(
334
+ path=combined_prefix + route.path,
335
+ endpoint=route.endpoint,
336
+ methods=route.methods,
337
+ name=route.name,
338
+ include_in_schema=route.include_in_schema,
339
+ tags=combined_tags,
340
+ summary=route.summary,
341
+ description=route.description,
342
+ response_model=route.response_model,
343
+ status_code=route.status_code,
344
+ )
345
+ routes.append(prefixed_route)
346
+
347
+ return routes
348
+
349
+
350
+ __all__ = ["Router"]