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.
Files changed (35) hide show
  1. vega/cli/commands/add.py +9 -10
  2. vega/cli/commands/generate.py +15 -15
  3. vega/cli/commands/init.py +9 -8
  4. vega/cli/commands/web.py +8 -7
  5. vega/cli/main.py +4 -4
  6. vega/cli/scaffolds/__init__.py +6 -2
  7. vega/cli/scaffolds/vega_web.py +109 -0
  8. vega/cli/templates/__init__.py +34 -8
  9. vega/cli/templates/components.py +29 -13
  10. vega/cli/templates/project/ARCHITECTURE.md.j2 +13 -13
  11. vega/cli/templates/project/README.md.j2 +5 -5
  12. vega/cli/templates/web/app.py.j2 +5 -5
  13. vega/cli/templates/web/health_route.py.j2 +2 -2
  14. vega/cli/templates/web/main.py.j2 +2 -3
  15. vega/cli/templates/web/middleware.py.j2 +3 -3
  16. vega/cli/templates/web/router.py.j2 +2 -2
  17. vega/cli/templates/web/routes_init.py.j2 +3 -3
  18. vega/cli/templates/web/routes_init_autodiscovery.py.j2 +2 -2
  19. vega/cli/templates/web/users_route.py.j2 +2 -2
  20. vega/discovery/routes.py +13 -13
  21. vega/web/__init__.py +100 -0
  22. vega/web/application.py +234 -0
  23. vega/web/builtin_middlewares.py +288 -0
  24. vega/web/exceptions.py +151 -0
  25. vega/web/middleware.py +185 -0
  26. vega/web/request.py +120 -0
  27. vega/web/response.py +220 -0
  28. vega/web/route_middleware.py +266 -0
  29. vega/web/router.py +350 -0
  30. vega/web/routing.py +347 -0
  31. {vega_framework-0.1.35.dist-info → vega_framework-0.2.1.dist-info}/METADATA +10 -9
  32. {vega_framework-0.1.35.dist-info → vega_framework-0.2.1.dist-info}/RECORD +35 -24
  33. {vega_framework-0.1.35.dist-info → vega_framework-0.2.1.dist-info}/WHEEL +0 -0
  34. {vega_framework-0.1.35.dist-info → vega_framework-0.2.1.dist-info}/entry_points.txt +0 -0
  35. {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
+ ]