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/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"]
vega/web/routing.py ADDED
@@ -0,0 +1,347 @@
1
+ """Routing utilities and decorators for Vega Web Framework"""
2
+
3
+ import inspect
4
+ from typing import Any, Callable, Dict, List, Optional, Type, get_type_hints
5
+ from functools import wraps
6
+
7
+ from starlette.routing import Route as StarletteRoute, Mount
8
+ from starlette.requests import Request as StarletteRequest
9
+
10
+ from .exceptions import HTTPException
11
+ from .request import Request
12
+ from .response import JSONResponse, Response, create_response
13
+ from .route_middleware import MiddlewareChain
14
+
15
+
16
+ class Route:
17
+ """
18
+ Represents a single route in the application.
19
+
20
+ Args:
21
+ path: URL path pattern (e.g., "/users/{user_id}")
22
+ endpoint: Handler function
23
+ methods: HTTP methods (e.g., ["GET", "POST"])
24
+ name: Optional route name
25
+ include_in_schema: Whether to include in OpenAPI schema
26
+ tags: Tags for documentation
27
+ summary: Short description
28
+ description: Longer description
29
+ response_model: Expected response model type
30
+ status_code: Default status code
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ path: str,
36
+ endpoint: Callable,
37
+ methods: List[str],
38
+ name: Optional[str] = None,
39
+ include_in_schema: bool = True,
40
+ tags: Optional[List[str]] = None,
41
+ summary: Optional[str] = None,
42
+ description: Optional[str] = None,
43
+ response_model: Optional[Type] = None,
44
+ status_code: int = 200,
45
+ ):
46
+ self.path = path
47
+ self.endpoint = endpoint
48
+ self.methods = methods
49
+ self.name = name or endpoint.__name__
50
+ self.include_in_schema = include_in_schema
51
+ self.tags = tags or []
52
+ self.summary = summary
53
+ self.description = description or inspect.getdoc(endpoint)
54
+ self.response_model = response_model
55
+ self.status_code = status_code
56
+
57
+ # Extract middleware from endpoint if decorated with @middleware
58
+ self.middlewares = getattr(endpoint, '_route_middlewares', [])
59
+
60
+ async def __call__(self, request: StarletteRequest) -> Response:
61
+ """Execute the route handler"""
62
+ return await self.endpoint(request)
63
+
64
+ def to_starlette_route(self) -> StarletteRoute:
65
+ """Convert to Starlette Route object"""
66
+ async def wrapped_endpoint(request: StarletteRequest) -> Response:
67
+ """Wrapper that handles request/response conversion and exceptions"""
68
+ try:
69
+ # Use request directly - Request is already a subclass of StarletteRequest
70
+ vega_request = request
71
+
72
+ # Get function signature to determine how to call it
73
+ sig = inspect.signature(self.endpoint)
74
+ params = sig.parameters
75
+
76
+ # Prepare kwargs for function call
77
+ kwargs = {}
78
+
79
+ # Extract path parameters
80
+ path_params = request.path_params
81
+
82
+ # Check if function expects Request object
83
+ has_request_param = any(
84
+ param.annotation == Request or param.name == "request"
85
+ for param in params.values()
86
+ )
87
+
88
+ if has_request_param:
89
+ kwargs["request"] = vega_request
90
+
91
+ # Add path parameters
92
+ for param_name, param_value in path_params.items():
93
+ if param_name in params:
94
+ kwargs[param_name] = param_value
95
+
96
+ # Execute middleware chain if present
97
+ if self.middlewares:
98
+ middleware_chain = MiddlewareChain(self.middlewares)
99
+ # Remove request from kwargs since it's passed separately to middleware
100
+ handler_kwargs = {k: v for k, v in kwargs.items() if k != "request"}
101
+ return await middleware_chain.execute(
102
+ vega_request,
103
+ self.endpoint,
104
+ **handler_kwargs
105
+ )
106
+
107
+ # No middleware, call endpoint directly
108
+ if inspect.iscoroutinefunction(self.endpoint):
109
+ result = await self.endpoint(**kwargs)
110
+ else:
111
+ result = self.endpoint(**kwargs)
112
+
113
+ # Handle different return types
114
+ if isinstance(result, (Response, JSONResponse)):
115
+ return result
116
+ elif isinstance(result, dict):
117
+ return JSONResponse(content=result, status_code=self.status_code)
118
+ elif isinstance(result, (list, tuple)):
119
+ return JSONResponse(content=result, status_code=self.status_code)
120
+ elif isinstance(result, str):
121
+ return Response(content=result, status_code=self.status_code)
122
+ elif result is None:
123
+ return Response(content=b"", status_code=self.status_code)
124
+ else:
125
+ # Try to serialize as JSON
126
+ return JSONResponse(content=result, status_code=self.status_code)
127
+
128
+ except HTTPException as exc:
129
+ # Handle HTTP exceptions
130
+ return JSONResponse(
131
+ content={"detail": exc.detail},
132
+ status_code=exc.status_code,
133
+ headers=exc.headers,
134
+ )
135
+ except Exception as exc:
136
+ # Handle unexpected exceptions
137
+ return JSONResponse(
138
+ content={"detail": str(exc)},
139
+ status_code=500,
140
+ )
141
+
142
+ return StarletteRoute(
143
+ path=self.path,
144
+ endpoint=wrapped_endpoint,
145
+ methods=self.methods,
146
+ name=self.name,
147
+ )
148
+
149
+
150
+ def route(
151
+ path: str,
152
+ methods: List[str],
153
+ *,
154
+ name: Optional[str] = None,
155
+ include_in_schema: bool = True,
156
+ tags: Optional[List[str]] = None,
157
+ summary: Optional[str] = None,
158
+ description: Optional[str] = None,
159
+ response_model: Optional[Type] = None,
160
+ status_code: int = 200,
161
+ ) -> Callable:
162
+ """
163
+ Generic route decorator.
164
+
165
+ Args:
166
+ path: URL path pattern
167
+ methods: List of HTTP methods
168
+ name: Optional route name
169
+ include_in_schema: Whether to include in API docs
170
+ tags: Tags for documentation
171
+ summary: Short description
172
+ description: Longer description
173
+ response_model: Expected response type
174
+ status_code: Default HTTP status code
175
+
176
+ Example:
177
+ @route("/users", methods=["GET", "POST"])
178
+ async def users_handler():
179
+ return {"users": []}
180
+ """
181
+
182
+ def decorator(func: Callable) -> Callable:
183
+ func._route_info = {
184
+ "path": path,
185
+ "methods": methods,
186
+ "name": name,
187
+ "include_in_schema": include_in_schema,
188
+ "tags": tags,
189
+ "summary": summary,
190
+ "description": description,
191
+ "response_model": response_model,
192
+ "status_code": status_code,
193
+ }
194
+ return func
195
+
196
+ return decorator
197
+
198
+
199
+ def get(
200
+ path: str,
201
+ *,
202
+ name: Optional[str] = None,
203
+ include_in_schema: bool = True,
204
+ tags: Optional[List[str]] = None,
205
+ summary: Optional[str] = None,
206
+ description: Optional[str] = None,
207
+ response_model: Optional[Type] = None,
208
+ status_code: int = 200,
209
+ ) -> Callable:
210
+ """
211
+ GET request decorator.
212
+
213
+ Example:
214
+ @get("/users/{user_id}")
215
+ async def get_user(user_id: str):
216
+ return {"id": user_id}
217
+ """
218
+ return route(
219
+ path,
220
+ methods=["GET"],
221
+ name=name,
222
+ include_in_schema=include_in_schema,
223
+ tags=tags,
224
+ summary=summary,
225
+ description=description,
226
+ response_model=response_model,
227
+ status_code=status_code,
228
+ )
229
+
230
+
231
+ def post(
232
+ path: str,
233
+ *,
234
+ name: Optional[str] = None,
235
+ include_in_schema: bool = True,
236
+ tags: Optional[List[str]] = None,
237
+ summary: Optional[str] = None,
238
+ description: Optional[str] = None,
239
+ response_model: Optional[Type] = None,
240
+ status_code: int = 201,
241
+ ) -> Callable:
242
+ """
243
+ POST request decorator.
244
+
245
+ Example:
246
+ @post("/users")
247
+ async def create_user(request: Request):
248
+ data = await request.json()
249
+ return {"id": "new_user", **data}
250
+ """
251
+ return route(
252
+ path,
253
+ methods=["POST"],
254
+ name=name,
255
+ include_in_schema=include_in_schema,
256
+ tags=tags,
257
+ summary=summary,
258
+ description=description,
259
+ response_model=response_model,
260
+ status_code=status_code,
261
+ )
262
+
263
+
264
+ def put(
265
+ path: str,
266
+ *,
267
+ name: Optional[str] = None,
268
+ include_in_schema: bool = True,
269
+ tags: Optional[List[str]] = None,
270
+ summary: Optional[str] = None,
271
+ description: Optional[str] = None,
272
+ response_model: Optional[Type] = None,
273
+ status_code: int = 200,
274
+ ) -> Callable:
275
+ """PUT request decorator."""
276
+ return route(
277
+ path,
278
+ methods=["PUT"],
279
+ name=name,
280
+ include_in_schema=include_in_schema,
281
+ tags=tags,
282
+ summary=summary,
283
+ description=description,
284
+ response_model=response_model,
285
+ status_code=status_code,
286
+ )
287
+
288
+
289
+ def patch(
290
+ path: str,
291
+ *,
292
+ name: Optional[str] = None,
293
+ include_in_schema: bool = True,
294
+ tags: Optional[List[str]] = None,
295
+ summary: Optional[str] = None,
296
+ description: Optional[str] = None,
297
+ response_model: Optional[Type] = None,
298
+ status_code: int = 200,
299
+ ) -> Callable:
300
+ """PATCH request decorator."""
301
+ return route(
302
+ path,
303
+ methods=["PATCH"],
304
+ name=name,
305
+ include_in_schema=include_in_schema,
306
+ tags=tags,
307
+ summary=summary,
308
+ description=description,
309
+ response_model=response_model,
310
+ status_code=status_code,
311
+ )
312
+
313
+
314
+ def delete(
315
+ path: str,
316
+ *,
317
+ name: Optional[str] = None,
318
+ include_in_schema: bool = True,
319
+ tags: Optional[List[str]] = None,
320
+ summary: Optional[str] = None,
321
+ description: Optional[str] = None,
322
+ response_model: Optional[Type] = None,
323
+ status_code: int = 204,
324
+ ) -> Callable:
325
+ """DELETE request decorator."""
326
+ return route(
327
+ path,
328
+ methods=["DELETE"],
329
+ name=name,
330
+ include_in_schema=include_in_schema,
331
+ tags=tags,
332
+ summary=summary,
333
+ description=description,
334
+ response_model=response_model,
335
+ status_code=status_code,
336
+ )
337
+
338
+
339
+ __all__ = [
340
+ "Route",
341
+ "route",
342
+ "get",
343
+ "post",
344
+ "put",
345
+ "patch",
346
+ "delete",
347
+ ]