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/exceptions.py ADDED
@@ -0,0 +1,151 @@
1
+ """HTTP exceptions and status codes for Vega Web Framework"""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+
6
+ class HTTPException(Exception):
7
+ """
8
+ HTTP exception that can be raised to return an HTTP error response.
9
+
10
+ Compatible with FastAPI's HTTPException API for easy migration.
11
+
12
+ Args:
13
+ status_code: HTTP status code
14
+ detail: Error message or detail object
15
+ headers: Optional HTTP headers to include in the response
16
+
17
+ Example:
18
+ raise HTTPException(status_code=404, detail="User not found")
19
+ raise HTTPException(
20
+ status_code=401,
21
+ detail="Not authenticated",
22
+ headers={"WWW-Authenticate": "Bearer"}
23
+ )
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ status_code: int,
29
+ detail: Any = None,
30
+ headers: Optional[Dict[str, str]] = None
31
+ ) -> None:
32
+ self.status_code = status_code
33
+ self.detail = detail
34
+ self.headers = headers
35
+ super().__init__(detail)
36
+
37
+ def __repr__(self) -> str:
38
+ return f"{self.__class__.__name__}(status_code={self.status_code}, detail={self.detail})"
39
+
40
+
41
+ class ValidationError(HTTPException):
42
+ """Raised when request validation fails (422)"""
43
+
44
+ def __init__(self, detail: Any = "Validation Error", headers: Optional[Dict[str, str]] = None):
45
+ super().__init__(status_code=422, detail=detail, headers=headers)
46
+
47
+
48
+ class NotFoundError(HTTPException):
49
+ """Raised when resource is not found (404)"""
50
+
51
+ def __init__(self, detail: Any = "Not Found", headers: Optional[Dict[str, str]] = None):
52
+ super().__init__(status_code=404, detail=detail, headers=headers)
53
+
54
+
55
+ class UnauthorizedError(HTTPException):
56
+ """Raised when authentication is required (401)"""
57
+
58
+ def __init__(self, detail: Any = "Unauthorized", headers: Optional[Dict[str, str]] = None):
59
+ super().__init__(status_code=401, detail=detail, headers=headers)
60
+
61
+
62
+ class ForbiddenError(HTTPException):
63
+ """Raised when access is forbidden (403)"""
64
+
65
+ def __init__(self, detail: Any = "Forbidden", headers: Optional[Dict[str, str]] = None):
66
+ super().__init__(status_code=403, detail=detail, headers=headers)
67
+
68
+
69
+ class BadRequestError(HTTPException):
70
+ """Raised for bad requests (400)"""
71
+
72
+ def __init__(self, detail: Any = "Bad Request", headers: Optional[Dict[str, str]] = None):
73
+ super().__init__(status_code=400, detail=detail, headers=headers)
74
+
75
+
76
+ # HTTP Status codes - compatible with FastAPI's status module
77
+ class status:
78
+ """HTTP status codes (compatible with fastapi.status)"""
79
+
80
+ # 1xx Informational
81
+ HTTP_100_CONTINUE = 100
82
+ HTTP_101_SWITCHING_PROTOCOLS = 101
83
+ HTTP_102_PROCESSING = 102
84
+ HTTP_103_EARLY_HINTS = 103
85
+
86
+ # 2xx Success
87
+ HTTP_200_OK = 200
88
+ HTTP_201_CREATED = 201
89
+ HTTP_202_ACCEPTED = 202
90
+ HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203
91
+ HTTP_204_NO_CONTENT = 204
92
+ HTTP_205_RESET_CONTENT = 205
93
+ HTTP_206_PARTIAL_CONTENT = 206
94
+ HTTP_207_MULTI_STATUS = 207
95
+ HTTP_208_ALREADY_REPORTED = 208
96
+ HTTP_226_IM_USED = 226
97
+
98
+ # 3xx Redirection
99
+ HTTP_300_MULTIPLE_CHOICES = 300
100
+ HTTP_301_MOVED_PERMANENTLY = 301
101
+ HTTP_302_FOUND = 302
102
+ HTTP_303_SEE_OTHER = 303
103
+ HTTP_304_NOT_MODIFIED = 304
104
+ HTTP_305_USE_PROXY = 305
105
+ HTTP_306_RESERVED = 306
106
+ HTTP_307_TEMPORARY_REDIRECT = 307
107
+ HTTP_308_PERMANENT_REDIRECT = 308
108
+
109
+ # 4xx Client Error
110
+ HTTP_400_BAD_REQUEST = 400
111
+ HTTP_401_UNAUTHORIZED = 401
112
+ HTTP_402_PAYMENT_REQUIRED = 402
113
+ HTTP_403_FORBIDDEN = 403
114
+ HTTP_404_NOT_FOUND = 404
115
+ HTTP_405_METHOD_NOT_ALLOWED = 405
116
+ HTTP_406_NOT_ACCEPTABLE = 406
117
+ HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407
118
+ HTTP_408_REQUEST_TIMEOUT = 408
119
+ HTTP_409_CONFLICT = 409
120
+ HTTP_410_GONE = 410
121
+ HTTP_411_LENGTH_REQUIRED = 411
122
+ HTTP_412_PRECONDITION_FAILED = 412
123
+ HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413
124
+ HTTP_414_REQUEST_URI_TOO_LONG = 414
125
+ HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415
126
+ HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416
127
+ HTTP_417_EXPECTATION_FAILED = 417
128
+ HTTP_418_IM_A_TEAPOT = 418
129
+ HTTP_421_MISDIRECTED_REQUEST = 421
130
+ HTTP_422_UNPROCESSABLE_ENTITY = 422
131
+ HTTP_423_LOCKED = 423
132
+ HTTP_424_FAILED_DEPENDENCY = 424
133
+ HTTP_425_TOO_EARLY = 425
134
+ HTTP_426_UPGRADE_REQUIRED = 426
135
+ HTTP_428_PRECONDITION_REQUIRED = 428
136
+ HTTP_429_TOO_MANY_REQUESTS = 429
137
+ HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
138
+ HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS = 451
139
+
140
+ # 5xx Server Error
141
+ HTTP_500_INTERNAL_SERVER_ERROR = 500
142
+ HTTP_501_NOT_IMPLEMENTED = 501
143
+ HTTP_502_BAD_GATEWAY = 502
144
+ HTTP_503_SERVICE_UNAVAILABLE = 503
145
+ HTTP_504_GATEWAY_TIMEOUT = 504
146
+ HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
147
+ HTTP_506_VARIANT_ALSO_NEGOTIATES = 506
148
+ HTTP_507_INSUFFICIENT_STORAGE = 507
149
+ HTTP_508_LOOP_DETECTED = 508
150
+ HTTP_510_NOT_EXTENDED = 510
151
+ HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511
vega/web/middleware.py ADDED
@@ -0,0 +1,185 @@
1
+ """Middleware utilities for Vega Web Framework"""
2
+
3
+ from typing import Callable
4
+ from starlette.middleware.base import BaseHTTPMiddleware
5
+ from starlette.requests import Request
6
+ from starlette.responses import Response
7
+
8
+
9
+ class VegaMiddleware(BaseHTTPMiddleware):
10
+ """
11
+ Base middleware class for Vega applications.
12
+
13
+ Extend this class to create custom middleware.
14
+
15
+ Example:
16
+ class LoggingMiddleware(VegaMiddleware):
17
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
18
+ print(f"Request: {request.method} {request.url.path}")
19
+ response = await call_next(request)
20
+ print(f"Response: {response.status_code}")
21
+ return response
22
+
23
+ app.add_middleware(LoggingMiddleware)
24
+ """
25
+
26
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
27
+ """
28
+ Process the request.
29
+
30
+ Args:
31
+ request: Incoming request
32
+ call_next: Function to call next middleware or endpoint
33
+
34
+ Returns:
35
+ Response object
36
+ """
37
+ return await call_next(request)
38
+
39
+
40
+ class CORSMiddleware:
41
+ """
42
+ CORS (Cross-Origin Resource Sharing) middleware.
43
+
44
+ This is a re-export of Starlette's CORSMiddleware for convenience.
45
+
46
+ Example:
47
+ from vega.web import VegaApp
48
+ from vega.web.middleware import CORSMiddleware
49
+
50
+ app = VegaApp()
51
+ app.add_middleware(
52
+ CORSMiddleware,
53
+ allow_origins=["*"],
54
+ allow_credentials=True,
55
+ allow_methods=["*"],
56
+ allow_headers=["*"],
57
+ )
58
+ """
59
+
60
+ # This will be imported from Starlette
61
+ from starlette.middleware.cors import CORSMiddleware as _CORSMiddleware
62
+
63
+ def __new__(cls, *args, **kwargs):
64
+ return cls._CORSMiddleware(*args, **kwargs)
65
+
66
+
67
+ class TrustedHostMiddleware:
68
+ """
69
+ Middleware to validate the Host header.
70
+
71
+ This is a re-export of Starlette's TrustedHostMiddleware.
72
+
73
+ Example:
74
+ app.add_middleware(
75
+ TrustedHostMiddleware,
76
+ allowed_hosts=["example.com", "*.example.com"]
77
+ )
78
+ """
79
+
80
+ from starlette.middleware.trustedhost import TrustedHostMiddleware as _TrustedHostMiddleware
81
+
82
+ def __new__(cls, *args, **kwargs):
83
+ return cls._TrustedHostMiddleware(*args, **kwargs)
84
+
85
+
86
+ class GZipMiddleware:
87
+ """
88
+ Middleware to compress responses using GZip.
89
+
90
+ This is a re-export of Starlette's GZipMiddleware.
91
+
92
+ Example:
93
+ app.add_middleware(GZipMiddleware, minimum_size=1000)
94
+ """
95
+
96
+ from starlette.middleware.gzip import GZipMiddleware as _GZipMiddleware
97
+
98
+ def __new__(cls, *args, **kwargs):
99
+ return cls._GZipMiddleware(*args, **kwargs)
100
+
101
+
102
+ class RateLimitMiddleware(VegaMiddleware):
103
+ """
104
+ Simple rate limiting middleware (example implementation).
105
+
106
+ Args:
107
+ requests_per_minute: Maximum requests per minute per IP
108
+
109
+ Example:
110
+ app.add_middleware(RateLimitMiddleware, requests_per_minute=60)
111
+ """
112
+
113
+ def __init__(self, app, requests_per_minute: int = 60):
114
+ super().__init__(app)
115
+ self.requests_per_minute = requests_per_minute
116
+ self.requests: dict = {} # IP -> [timestamps]
117
+
118
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
119
+ """Check rate limit before processing request"""
120
+ from datetime import datetime, timedelta
121
+
122
+ client_ip = request.client.host if request.client else "unknown"
123
+ now = datetime.now()
124
+
125
+ # Clean old entries
126
+ if client_ip in self.requests:
127
+ cutoff = now - timedelta(minutes=1)
128
+ self.requests[client_ip] = [
129
+ ts for ts in self.requests[client_ip] if ts > cutoff
130
+ ]
131
+
132
+ # Check rate limit
133
+ if client_ip in self.requests:
134
+ if len(self.requests[client_ip]) >= self.requests_per_minute:
135
+ from ..response import JSONResponse
136
+ return JSONResponse(
137
+ content={"detail": "Rate limit exceeded"},
138
+ status_code=429,
139
+ )
140
+
141
+ # Record request
142
+ if client_ip not in self.requests:
143
+ self.requests[client_ip] = []
144
+ self.requests[client_ip].append(now)
145
+
146
+ return await call_next(request)
147
+
148
+
149
+ class RequestLoggingMiddleware(VegaMiddleware):
150
+ """
151
+ Middleware to log all requests.
152
+
153
+ Example:
154
+ app.add_middleware(RequestLoggingMiddleware)
155
+ """
156
+
157
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
158
+ """Log request and response"""
159
+ import logging
160
+ import time
161
+
162
+ logger = logging.getLogger("vega.web")
163
+
164
+ start_time = time.time()
165
+ logger.info(f"→ {request.method} {request.url.path}")
166
+
167
+ response = await call_next(request)
168
+
169
+ process_time = time.time() - start_time
170
+ logger.info(
171
+ f"← {request.method} {request.url.path} "
172
+ f"[{response.status_code}] {process_time:.3f}s"
173
+ )
174
+
175
+ return response
176
+
177
+
178
+ __all__ = [
179
+ "VegaMiddleware",
180
+ "CORSMiddleware",
181
+ "TrustedHostMiddleware",
182
+ "GZipMiddleware",
183
+ "RateLimitMiddleware",
184
+ "RequestLoggingMiddleware",
185
+ ]
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
+ ]