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/cli/commands/add.py +9 -10
- vega/cli/commands/init.py +9 -8
- vega/cli/main.py +4 -4
- vega/cli/scaffolds/__init__.py +6 -2
- vega/cli/scaffolds/vega_web.py +109 -0
- vega/cli/templates/__init__.py +34 -8
- vega/cli/templates/components.py +29 -13
- vega/cli/templates/web/app.py.j2 +5 -5
- vega/cli/templates/web/health_route.py.j2 +2 -2
- vega/cli/templates/web/router.py.j2 +2 -2
- vega/cli/templates/web/routes_init_autodiscovery.py.j2 +2 -2
- vega/cli/templates/web/users_route.py.j2 +2 -2
- vega/discovery/routes.py +13 -13
- vega/web/__init__.py +100 -0
- vega/web/application.py +234 -0
- vega/web/builtin_middlewares.py +288 -0
- vega/web/exceptions.py +151 -0
- vega/web/middleware.py +185 -0
- vega/web/request.py +120 -0
- vega/web/response.py +220 -0
- vega/web/route_middleware.py +266 -0
- vega/web/router.py +350 -0
- vega/web/routing.py +347 -0
- {vega_framework-0.1.34.dist-info → vega_framework-0.2.0.dist-info}/METADATA +10 -9
- {vega_framework-0.1.34.dist-info → vega_framework-0.2.0.dist-info}/RECORD +28 -17
- {vega_framework-0.1.34.dist-info → vega_framework-0.2.0.dist-info}/WHEEL +0 -0
- {vega_framework-0.1.34.dist-info → vega_framework-0.2.0.dist-info}/entry_points.txt +0 -0
- {vega_framework-0.1.34.dist-info → vega_framework-0.2.0.dist-info}/licenses/LICENSE +0 -0
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
|
+
]
|