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.
vega/web/__init__.py ADDED
@@ -0,0 +1,100 @@
1
+ """
2
+ Vega Web Framework
3
+
4
+ A lightweight web framework built on Starlette, providing a FastAPI-like
5
+ developer experience while being deeply integrated with Vega's architecture.
6
+
7
+ Example:
8
+ from vega.web import Router, VegaApp, HTTPException, status
9
+
10
+ router = Router()
11
+
12
+ @router.get("/users/{user_id}")
13
+ async def get_user(user_id: str):
14
+ if user_id == "invalid":
15
+ raise HTTPException(status_code=404, detail="User not found")
16
+ return {"id": user_id, "name": "John"}
17
+
18
+ app = VegaApp(title="My API", version="1.0.0")
19
+ app.include_router(router, prefix="/api")
20
+
21
+ # Run with: uvicorn main:app --reload
22
+ """
23
+
24
+ __version__ = "0.1.0"
25
+
26
+ # Core application
27
+ from .application import VegaApp
28
+
29
+ # Routing
30
+ from .router import Router
31
+
32
+ # Request/Response
33
+ from .request import Request
34
+ from .response import (
35
+ Response,
36
+ JSONResponse,
37
+ HTMLResponse,
38
+ PlainTextResponse,
39
+ RedirectResponse,
40
+ StreamingResponse,
41
+ FileResponse,
42
+ )
43
+
44
+ # Exceptions
45
+ from .exceptions import (
46
+ HTTPException,
47
+ ValidationError,
48
+ NotFoundError,
49
+ UnauthorizedError,
50
+ ForbiddenError,
51
+ BadRequestError,
52
+ status,
53
+ )
54
+
55
+ # Middleware (optional, can be imported explicitly)
56
+ from .middleware import (
57
+ VegaMiddleware,
58
+ CORSMiddleware,
59
+ RequestLoggingMiddleware,
60
+ )
61
+
62
+ # Route middleware
63
+ from .route_middleware import (
64
+ RouteMiddleware,
65
+ MiddlewarePhase,
66
+ middleware,
67
+ )
68
+
69
+ __all__ = [
70
+ # Version
71
+ "__version__",
72
+ # Core
73
+ "VegaApp",
74
+ "Router",
75
+ # Request/Response
76
+ "Request",
77
+ "Response",
78
+ "JSONResponse",
79
+ "HTMLResponse",
80
+ "PlainTextResponse",
81
+ "RedirectResponse",
82
+ "StreamingResponse",
83
+ "FileResponse",
84
+ # Exceptions
85
+ "HTTPException",
86
+ "ValidationError",
87
+ "NotFoundError",
88
+ "UnauthorizedError",
89
+ "ForbiddenError",
90
+ "BadRequestError",
91
+ "status",
92
+ # Middleware
93
+ "VegaMiddleware",
94
+ "CORSMiddleware",
95
+ "RequestLoggingMiddleware",
96
+ # Route Middleware
97
+ "RouteMiddleware",
98
+ "MiddlewarePhase",
99
+ "middleware",
100
+ ]
@@ -0,0 +1,234 @@
1
+ """Main application class for Vega Web Framework"""
2
+
3
+ from typing import Any, Callable, Dict, List, Optional, Sequence
4
+
5
+ from starlette.applications import Starlette
6
+ from starlette.middleware import Middleware
7
+ from starlette.middleware.cors import CORSMiddleware
8
+ from starlette.routing import Mount, Route as StarletteRoute
9
+ from starlette.types import ASGIApp
10
+
11
+ from .router import Router
12
+ from .exceptions import HTTPException
13
+ from .response import JSONResponse
14
+
15
+
16
+ class VegaApp:
17
+ """
18
+ Main application class for Vega Web Framework.
19
+
20
+ This is the core ASGI application that handles all HTTP requests.
21
+ It's built on Starlette but provides a FastAPI-like API for familiarity.
22
+
23
+ Args:
24
+ title: Application title
25
+ description: Application description
26
+ version: Application version
27
+ debug: Enable debug mode
28
+ middleware: List of middleware to apply
29
+ on_startup: Functions to run on startup
30
+ on_shutdown: Functions to run on shutdown
31
+
32
+ Example:
33
+ app = VegaApp(
34
+ title="My API",
35
+ version="1.0.0",
36
+ debug=True
37
+ )
38
+
39
+ @app.get("/")
40
+ async def root():
41
+ return {"message": "Hello World"}
42
+
43
+ # Or with routers
44
+ router = Router(prefix="/api")
45
+ @router.get("/users")
46
+ async def get_users():
47
+ return {"users": []}
48
+
49
+ app.include_router(router)
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ *,
55
+ title: str = "Vega API",
56
+ description: str = "",
57
+ version: str = "0.1.0",
58
+ debug: bool = False,
59
+ middleware: Optional[Sequence[Middleware]] = None,
60
+ on_startup: Optional[Sequence[Callable]] = None,
61
+ on_shutdown: Optional[Sequence[Callable]] = None,
62
+ ):
63
+ self.title = title
64
+ self.description = description
65
+ self.version = version
66
+ self.debug = debug
67
+
68
+ # Internal router for top-level routes
69
+ self._router = Router()
70
+
71
+ # Middleware stack
72
+ self._middleware = list(middleware) if middleware else []
73
+
74
+ # Lifecycle handlers
75
+ self._on_startup = list(on_startup) if on_startup else []
76
+ self._on_shutdown = list(on_shutdown) if on_shutdown else []
77
+
78
+ # Starlette app (created lazily)
79
+ self._starlette_app: Optional[Starlette] = None
80
+
81
+ def add_middleware(
82
+ self,
83
+ middleware_class: type,
84
+ **options: Any,
85
+ ) -> None:
86
+ """
87
+ Add middleware to the application.
88
+
89
+ Args:
90
+ middleware_class: Middleware class
91
+ **options: Middleware configuration options
92
+
93
+ Example:
94
+ from starlette.middleware.cors import CORSMiddleware
95
+
96
+ app.add_middleware(
97
+ CORSMiddleware,
98
+ allow_origins=["*"],
99
+ allow_methods=["*"],
100
+ )
101
+ """
102
+ self._middleware.append(Middleware(middleware_class, **options))
103
+ # Invalidate cached Starlette app
104
+ self._starlette_app = None
105
+
106
+ def on_event(self, event_type: str) -> Callable:
107
+ """
108
+ Register lifecycle event handler.
109
+
110
+ Args:
111
+ event_type: Either "startup" or "shutdown"
112
+
113
+ Example:
114
+ @app.on_event("startup")
115
+ async def startup():
116
+ print("Starting up!")
117
+
118
+ @app.on_event("shutdown")
119
+ async def shutdown():
120
+ print("Shutting down!")
121
+ """
122
+
123
+ def decorator(func: Callable) -> Callable:
124
+ if event_type == "startup":
125
+ self._on_startup.append(func)
126
+ elif event_type == "shutdown":
127
+ self._on_shutdown.append(func)
128
+ else:
129
+ raise ValueError(f"Invalid event type: {event_type}")
130
+ # Invalidate cached Starlette app
131
+ self._starlette_app = None
132
+ return func
133
+
134
+ return decorator
135
+
136
+ def include_router(
137
+ self,
138
+ router: Router,
139
+ prefix: str = "",
140
+ tags: Optional[List[str]] = None,
141
+ ) -> None:
142
+ """
143
+ Include a router in the application.
144
+
145
+ Args:
146
+ router: Router to include
147
+ prefix: URL prefix for all routes
148
+ tags: Tags to add to all routes
149
+
150
+ Example:
151
+ users_router = Router()
152
+ @users_router.get("/{user_id}")
153
+ async def get_user(user_id: str):
154
+ return {"id": user_id}
155
+
156
+ app.include_router(users_router, prefix="/users", tags=["users"])
157
+ """
158
+ self._router.include_router(router, prefix=prefix, tags=tags)
159
+ # Invalidate cached Starlette app
160
+ self._starlette_app = None
161
+
162
+ def get(self, path: str, **kwargs: Any) -> Callable:
163
+ """
164
+ Decorator for GET requests.
165
+
166
+ Example:
167
+ @app.get("/items/{item_id}")
168
+ async def get_item(item_id: str):
169
+ return {"id": item_id}
170
+ """
171
+ return self._router.get(path, **kwargs)
172
+
173
+ def post(self, path: str, **kwargs: Any) -> Callable:
174
+ """Decorator for POST requests."""
175
+ return self._router.post(path, **kwargs)
176
+
177
+ def put(self, path: str, **kwargs: Any) -> Callable:
178
+ """Decorator for PUT requests."""
179
+ return self._router.put(path, **kwargs)
180
+
181
+ def patch(self, path: str, **kwargs: Any) -> Callable:
182
+ """Decorator for PATCH requests."""
183
+ return self._router.patch(path, **kwargs)
184
+
185
+ def delete(self, path: str, **kwargs: Any) -> Callable:
186
+ """Decorator for DELETE requests."""
187
+ return self._router.delete(path, **kwargs)
188
+
189
+ def route(self, path: str, methods: List[str], **kwargs: Any) -> Callable:
190
+ """
191
+ Decorator for custom methods.
192
+
193
+ Example:
194
+ @app.route("/items", methods=["GET", "POST"])
195
+ async def items():
196
+ return {"items": []}
197
+ """
198
+ return self._router.route(path, methods, **kwargs)
199
+
200
+ def _build_starlette_app(self) -> Starlette:
201
+ """Build the Starlette application from routes and middleware."""
202
+ # Convert Vega routes to Starlette routes
203
+ starlette_routes = [
204
+ route.to_starlette_route() for route in self._router.get_routes()
205
+ ]
206
+
207
+ # Create Starlette app
208
+ app = Starlette(
209
+ debug=self.debug,
210
+ routes=starlette_routes,
211
+ middleware=self._middleware,
212
+ on_startup=self._on_startup,
213
+ on_shutdown=self._on_shutdown,
214
+ )
215
+
216
+ return app
217
+
218
+ def _get_app(self) -> Starlette:
219
+ """Get or create the Starlette app."""
220
+ if self._starlette_app is None:
221
+ self._starlette_app = self._build_starlette_app()
222
+ return self._starlette_app
223
+
224
+ async def __call__(self, scope: dict, receive: Callable, send: Callable) -> None:
225
+ """
226
+ ASGI application callable.
227
+
228
+ This makes VegaApp a valid ASGI application.
229
+ """
230
+ app = self._get_app()
231
+ await app(scope, receive, send)
232
+
233
+
234
+ __all__ = ["VegaApp"]
@@ -0,0 +1,288 @@
1
+ """Built-in route middleware implementations for Vega Web Framework"""
2
+
3
+ import time
4
+ import logging
5
+ from typing import Optional
6
+
7
+ from .route_middleware import RouteMiddleware, MiddlewarePhase
8
+ from .request import Request
9
+ from .response import Response, JSONResponse
10
+ from .exceptions import HTTPException, status
11
+
12
+
13
+ class AuthMiddleware(RouteMiddleware):
14
+ """
15
+ Authentication middleware - validates Authorization header.
16
+
17
+ Executes BEFORE the handler.
18
+
19
+ Args:
20
+ header_name: Name of the header to check (default: "Authorization")
21
+ scheme: Expected auth scheme (default: "Bearer")
22
+
23
+ Example:
24
+ @router.get("/protected")
25
+ @middleware(AuthMiddleware())
26
+ async def protected_route():
27
+ return {"data": "secret"}
28
+ """
29
+
30
+ def __init__(self, header_name: str = "Authorization", scheme: str = "Bearer"):
31
+ super().__init__(phase=MiddlewarePhase.BEFORE)
32
+ self.header_name = header_name
33
+ self.scheme = scheme
34
+
35
+ async def before(self, request: Request) -> Optional[Response]:
36
+ """Check for valid authentication token"""
37
+ auth_header = request.headers.get(self.header_name.lower())
38
+
39
+ if not auth_header:
40
+ return JSONResponse(
41
+ content={"detail": "Missing authentication credentials"},
42
+ status_code=status.HTTP_401_UNAUTHORIZED,
43
+ headers={"WWW-Authenticate": f"{self.scheme}"},
44
+ )
45
+
46
+ # Parse scheme and token
47
+ parts = auth_header.split()
48
+ if len(parts) != 2 or parts[0].lower() != self.scheme.lower():
49
+ return JSONResponse(
50
+ content={"detail": f"Invalid authentication scheme. Expected: {self.scheme}"},
51
+ status_code=status.HTTP_401_UNAUTHORIZED,
52
+ headers={"WWW-Authenticate": f"{self.scheme}"},
53
+ )
54
+
55
+ token = parts[1]
56
+
57
+ # TODO: Validate token here (this is a simple example)
58
+ if token == "invalid":
59
+ return JSONResponse(
60
+ content={"detail": "Invalid or expired token"},
61
+ status_code=status.HTTP_401_UNAUTHORIZED,
62
+ )
63
+
64
+ # Store user info in request state for handler to use
65
+ request.state.user_id = "user_from_token"
66
+ request.state.token = token
67
+
68
+ return None # Continue to handler
69
+
70
+
71
+ class TimingMiddleware(RouteMiddleware):
72
+ """
73
+ Request timing middleware - measures execution time.
74
+
75
+ Executes BOTH before and after the handler.
76
+
77
+ Example:
78
+ @router.get("/slow-operation")
79
+ @middleware(TimingMiddleware())
80
+ async def slow_operation():
81
+ await asyncio.sleep(1)
82
+ return {"status": "done"}
83
+ """
84
+
85
+ def __init__(self):
86
+ super().__init__(phase=MiddlewarePhase.BOTH)
87
+ self.logger = logging.getLogger("vega.web.timing")
88
+
89
+ async def before(self, request: Request) -> Optional[Response]:
90
+ """Record start time"""
91
+ request.state.start_time = time.time()
92
+ return None
93
+
94
+ async def after(self, request: Request, response: Response) -> Response:
95
+ """Calculate and log execution time"""
96
+ if hasattr(request.state, "start_time"):
97
+ duration = time.time() - request.state.start_time
98
+ self.logger.info(
99
+ f"{request.method} {request.url.path} completed in {duration:.3f}s"
100
+ )
101
+
102
+ # Add timing header to response
103
+ if hasattr(response, "headers"):
104
+ response.headers["X-Process-Time"] = f"{duration:.3f}"
105
+
106
+ return response
107
+
108
+
109
+ class CacheControlMiddleware(RouteMiddleware):
110
+ """
111
+ Cache control middleware - adds cache headers to response.
112
+
113
+ Executes AFTER the handler.
114
+
115
+ Args:
116
+ max_age: Cache max age in seconds
117
+ public: Whether cache is public (default: True)
118
+
119
+ Example:
120
+ @router.get("/static-data")
121
+ @middleware(CacheControlMiddleware(max_age=3600))
122
+ async def get_static_data():
123
+ return {"data": "rarely changes"}
124
+ """
125
+
126
+ def __init__(self, max_age: int = 300, public: bool = True):
127
+ super().__init__(phase=MiddlewarePhase.AFTER)
128
+ self.max_age = max_age
129
+ self.public = public
130
+
131
+ async def after(self, request: Request, response: Response) -> Response:
132
+ """Add cache control headers"""
133
+ cache_type = "public" if self.public else "private"
134
+ cache_value = f"{cache_type}, max-age={self.max_age}"
135
+
136
+ if hasattr(response, "headers"):
137
+ response.headers["Cache-Control"] = cache_value
138
+
139
+ return response
140
+
141
+
142
+ class CORSMiddleware(RouteMiddleware):
143
+ """
144
+ CORS middleware for specific routes.
145
+
146
+ Executes AFTER the handler to add CORS headers.
147
+
148
+ Args:
149
+ allow_origins: List of allowed origins or "*"
150
+ allow_methods: List of allowed methods
151
+ allow_headers: List of allowed headers
152
+
153
+ Example:
154
+ @router.get("/public-api/data")
155
+ @middleware(CORSMiddleware(allow_origins=["*"]))
156
+ async def public_data():
157
+ return {"data": "public"}
158
+ """
159
+
160
+ def __init__(
161
+ self,
162
+ allow_origins: list = None,
163
+ allow_methods: list = None,
164
+ allow_headers: list = None,
165
+ ):
166
+ super().__init__(phase=MiddlewarePhase.AFTER)
167
+ self.allow_origins = allow_origins or ["*"]
168
+ self.allow_methods = allow_methods or ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
169
+ self.allow_headers = allow_headers or ["*"]
170
+
171
+ async def after(self, request: Request, response: Response) -> Response:
172
+ """Add CORS headers to response"""
173
+ if hasattr(response, "headers"):
174
+ origin = request.headers.get("origin", "*")
175
+
176
+ if "*" in self.allow_origins or origin in self.allow_origins:
177
+ response.headers["Access-Control-Allow-Origin"] = origin
178
+ response.headers["Access-Control-Allow-Methods"] = ", ".join(
179
+ self.allow_methods
180
+ )
181
+ response.headers["Access-Control-Allow-Headers"] = ", ".join(
182
+ self.allow_headers
183
+ )
184
+
185
+ return response
186
+
187
+
188
+ class RateLimitMiddleware(RouteMiddleware):
189
+ """
190
+ Simple rate limiting middleware.
191
+
192
+ Executes BEFORE the handler.
193
+
194
+ Args:
195
+ max_requests: Maximum requests allowed
196
+ window_seconds: Time window in seconds
197
+
198
+ Example:
199
+ @router.post("/expensive-operation")
200
+ @middleware(RateLimitMiddleware(max_requests=10, window_seconds=60))
201
+ async def expensive_op():
202
+ return {"status": "processing"}
203
+ """
204
+
205
+ def __init__(self, max_requests: int = 100, window_seconds: int = 60):
206
+ super().__init__(phase=MiddlewarePhase.BEFORE)
207
+ self.max_requests = max_requests
208
+ self.window_seconds = window_seconds
209
+ self.requests = {} # IP -> [timestamps]
210
+
211
+ async def before(self, request: Request) -> Optional[Response]:
212
+ """Check rate limit"""
213
+ from datetime import datetime, timedelta
214
+
215
+ client_ip = request.client.host if request.client else "unknown"
216
+ now = datetime.now()
217
+
218
+ # Clean old entries
219
+ if client_ip in self.requests:
220
+ cutoff = now - timedelta(seconds=self.window_seconds)
221
+ self.requests[client_ip] = [
222
+ ts for ts in self.requests[client_ip] if ts > cutoff
223
+ ]
224
+
225
+ # Check limit
226
+ if client_ip in self.requests:
227
+ if len(self.requests[client_ip]) >= self.max_requests:
228
+ return JSONResponse(
229
+ content={
230
+ "detail": f"Rate limit exceeded. Max {self.max_requests} requests per {self.window_seconds}s"
231
+ },
232
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
233
+ headers={
234
+ "Retry-After": str(self.window_seconds),
235
+ "X-RateLimit-Limit": str(self.max_requests),
236
+ "X-RateLimit-Remaining": "0",
237
+ },
238
+ )
239
+
240
+ # Record request
241
+ if client_ip not in self.requests:
242
+ self.requests[client_ip] = []
243
+ self.requests[client_ip].append(now)
244
+
245
+ return None
246
+
247
+
248
+ class LoggingMiddleware(RouteMiddleware):
249
+ """
250
+ Request/Response logging middleware.
251
+
252
+ Executes BOTH before and after.
253
+
254
+ Example:
255
+ @router.post("/important-action")
256
+ @middleware(LoggingMiddleware())
257
+ async def important_action():
258
+ return {"status": "done"}
259
+ """
260
+
261
+ def __init__(self, logger_name: str = "vega.web.routes"):
262
+ super().__init__(phase=MiddlewarePhase.BOTH)
263
+ self.logger = logging.getLogger(logger_name)
264
+
265
+ async def before(self, request: Request) -> Optional[Response]:
266
+ """Log incoming request"""
267
+ self.logger.info(
268
+ f"→ {request.method} {request.url.path} "
269
+ f"from {request.client.host if request.client else 'unknown'}"
270
+ )
271
+ return None
272
+
273
+ async def after(self, request: Request, response: Response) -> Response:
274
+ """Log response"""
275
+ self.logger.info(
276
+ f"← {request.method} {request.url.path} [{response.status_code}]"
277
+ )
278
+ return response
279
+
280
+
281
+ __all__ = [
282
+ "AuthMiddleware",
283
+ "TimingMiddleware",
284
+ "CacheControlMiddleware",
285
+ "CORSMiddleware",
286
+ "RateLimitMiddleware",
287
+ "LoggingMiddleware",
288
+ ]