turboapi 0.4.12__cp314-cp314t-macosx_10_12_x86_64.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.
- turboapi/__init__.py +24 -0
- turboapi/async_limiter.py +86 -0
- turboapi/async_pool.py +141 -0
- turboapi/decorators.py +69 -0
- turboapi/main_app.py +314 -0
- turboapi/middleware.py +342 -0
- turboapi/models.py +148 -0
- turboapi/request_handler.py +227 -0
- turboapi/routing.py +219 -0
- turboapi/rust_integration.py +335 -0
- turboapi/security.py +542 -0
- turboapi/server_integration.py +436 -0
- turboapi/turbonet.cpython-314t-darwin.so +0 -0
- turboapi/version_check.py +268 -0
- turboapi-0.4.12.dist-info/METADATA +31 -0
- turboapi-0.4.12.dist-info/RECORD +17 -0
- turboapi-0.4.12.dist-info/WHEEL +4 -0
turboapi/middleware.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI-compatible Middleware system for TurboAPI.
|
|
3
|
+
|
|
4
|
+
Includes:
|
|
5
|
+
- CORS (Cross-Origin Resource Sharing)
|
|
6
|
+
- Trusted Host (HTTP Host Header attack prevention)
|
|
7
|
+
- GZip Compression
|
|
8
|
+
- HTTPS Redirect
|
|
9
|
+
- Session Management
|
|
10
|
+
- Custom Middleware Support
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import List, Optional, Callable, Awaitable, Pattern
|
|
14
|
+
import gzip
|
|
15
|
+
import re
|
|
16
|
+
import time
|
|
17
|
+
from .models import Request, Response
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Middleware:
|
|
21
|
+
"""Base middleware class."""
|
|
22
|
+
|
|
23
|
+
def before_request(self, request: Request) -> None:
|
|
24
|
+
"""Called before processing the request."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
def after_request(self, request: Request, response: Response) -> Response:
|
|
28
|
+
"""Called after processing the request."""
|
|
29
|
+
return response
|
|
30
|
+
|
|
31
|
+
def on_error(self, request: Request, error: Exception) -> Response:
|
|
32
|
+
"""Called when an error occurs."""
|
|
33
|
+
return Response(
|
|
34
|
+
content={"error": "Internal Server Error"},
|
|
35
|
+
status_code=500
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CORSMiddleware(Middleware):
|
|
40
|
+
"""
|
|
41
|
+
CORS (Cross-Origin Resource Sharing) middleware.
|
|
42
|
+
|
|
43
|
+
FastAPI-compatible implementation.
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
app.add_middleware(
|
|
47
|
+
CORSMiddleware,
|
|
48
|
+
allow_origins=["http://localhost:8080"],
|
|
49
|
+
allow_credentials=True,
|
|
50
|
+
allow_methods=["*"],
|
|
51
|
+
allow_headers=["*"],
|
|
52
|
+
)
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
allow_origins: List[str] = None,
|
|
58
|
+
allow_methods: List[str] = None,
|
|
59
|
+
allow_headers: List[str] = None,
|
|
60
|
+
allow_credentials: bool = False,
|
|
61
|
+
allow_origin_regex: Optional[str] = None,
|
|
62
|
+
expose_headers: List[str] = None,
|
|
63
|
+
max_age: int = 600,
|
|
64
|
+
):
|
|
65
|
+
self.allow_origins = allow_origins or ["*"]
|
|
66
|
+
self.allow_methods = allow_methods or ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "HEAD"]
|
|
67
|
+
self.allow_headers = allow_headers or ["*"]
|
|
68
|
+
self.allow_credentials = allow_credentials
|
|
69
|
+
self.allow_origin_regex = re.compile(allow_origin_regex) if allow_origin_regex else None
|
|
70
|
+
self.expose_headers = expose_headers or []
|
|
71
|
+
self.max_age = max_age
|
|
72
|
+
|
|
73
|
+
def before_request(self, request: Request) -> None:
|
|
74
|
+
"""Handle preflight OPTIONS requests."""
|
|
75
|
+
if request.method == "OPTIONS":
|
|
76
|
+
# Preflight request
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
def after_request(self, request: Request, response: Response) -> Response:
|
|
80
|
+
"""Add CORS headers to response."""
|
|
81
|
+
origin = request.headers.get("origin", "")
|
|
82
|
+
|
|
83
|
+
# Check if origin is allowed
|
|
84
|
+
if self.allow_origin_regex and self.allow_origin_regex.match(origin):
|
|
85
|
+
response.set_header("Access-Control-Allow-Origin", origin)
|
|
86
|
+
elif "*" in self.allow_origins:
|
|
87
|
+
response.set_header("Access-Control-Allow-Origin", "*")
|
|
88
|
+
elif origin in self.allow_origins:
|
|
89
|
+
response.set_header("Access-Control-Allow-Origin", origin)
|
|
90
|
+
|
|
91
|
+
response.set_header("Access-Control-Allow-Methods", ", ".join(self.allow_methods))
|
|
92
|
+
response.set_header("Access-Control-Allow-Headers", ", ".join(self.allow_headers))
|
|
93
|
+
|
|
94
|
+
if self.expose_headers:
|
|
95
|
+
response.set_header("Access-Control-Expose-Headers", ", ".join(self.expose_headers))
|
|
96
|
+
|
|
97
|
+
if self.allow_credentials:
|
|
98
|
+
response.set_header("Access-Control-Allow-Credentials", "true")
|
|
99
|
+
|
|
100
|
+
response.set_header("Access-Control-Max-Age", str(self.max_age))
|
|
101
|
+
|
|
102
|
+
return response
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TrustedHostMiddleware(Middleware):
|
|
106
|
+
"""
|
|
107
|
+
Trusted Host middleware - prevents HTTP Host Header attacks.
|
|
108
|
+
|
|
109
|
+
FastAPI-compatible implementation.
|
|
110
|
+
|
|
111
|
+
Usage:
|
|
112
|
+
app.add_middleware(
|
|
113
|
+
TrustedHostMiddleware,
|
|
114
|
+
allowed_hosts=["example.com", "*.example.com"]
|
|
115
|
+
)
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
allowed_hosts: List[str] = None,
|
|
121
|
+
www_redirect: bool = True,
|
|
122
|
+
):
|
|
123
|
+
if allowed_hosts is None:
|
|
124
|
+
allowed_hosts = ["*"]
|
|
125
|
+
|
|
126
|
+
self.allowed_hosts = allowed_hosts
|
|
127
|
+
self.www_redirect = www_redirect
|
|
128
|
+
|
|
129
|
+
# Compile regex patterns for wildcard hosts
|
|
130
|
+
self.allowed_host_patterns = []
|
|
131
|
+
for host in allowed_hosts:
|
|
132
|
+
if host == "*":
|
|
133
|
+
self.allowed_host_patterns.append(re.compile(".*"))
|
|
134
|
+
else:
|
|
135
|
+
# Convert wildcard to regex
|
|
136
|
+
pattern = host.replace(".", r"\.").replace("*", ".*")
|
|
137
|
+
self.allowed_host_patterns.append(re.compile(f"^{pattern}$"))
|
|
138
|
+
|
|
139
|
+
def before_request(self, request: Request) -> None:
|
|
140
|
+
"""Validate Host header."""
|
|
141
|
+
host = request.headers.get("host", "").split(":")[0]
|
|
142
|
+
|
|
143
|
+
# Check if host is allowed
|
|
144
|
+
if not any(pattern.match(host) for pattern in self.allowed_host_patterns):
|
|
145
|
+
raise Exception(f"Invalid host header: {host}")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class GZipMiddleware(Middleware):
|
|
149
|
+
"""
|
|
150
|
+
GZip compression middleware.
|
|
151
|
+
|
|
152
|
+
FastAPI-compatible implementation.
|
|
153
|
+
|
|
154
|
+
Usage:
|
|
155
|
+
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def __init__(
|
|
159
|
+
self,
|
|
160
|
+
minimum_size: int = 500,
|
|
161
|
+
compresslevel: int = 9,
|
|
162
|
+
):
|
|
163
|
+
self.minimum_size = minimum_size
|
|
164
|
+
self.compresslevel = compresslevel
|
|
165
|
+
|
|
166
|
+
def after_request(self, request: Request, response: Response) -> Response:
|
|
167
|
+
"""Compress response if client accepts gzip."""
|
|
168
|
+
accept_encoding = request.headers.get("accept-encoding", "")
|
|
169
|
+
|
|
170
|
+
if "gzip" not in accept_encoding.lower():
|
|
171
|
+
return response
|
|
172
|
+
|
|
173
|
+
# Check if response is large enough to compress
|
|
174
|
+
if hasattr(response, 'content'):
|
|
175
|
+
content = response.content
|
|
176
|
+
if isinstance(content, str):
|
|
177
|
+
content = content.encode('utf-8')
|
|
178
|
+
|
|
179
|
+
if len(content) < self.minimum_size:
|
|
180
|
+
return response
|
|
181
|
+
|
|
182
|
+
# Compress content
|
|
183
|
+
compressed = gzip.compress(content, compresslevel=self.compresslevel)
|
|
184
|
+
response.content = compressed
|
|
185
|
+
response.set_header("Content-Encoding", "gzip")
|
|
186
|
+
response.set_header("Content-Length", str(len(compressed)))
|
|
187
|
+
response.set_header("Vary", "Accept-Encoding")
|
|
188
|
+
|
|
189
|
+
return response
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class HTTPSRedirectMiddleware(Middleware):
|
|
193
|
+
"""
|
|
194
|
+
HTTPS redirect middleware - redirects HTTP to HTTPS.
|
|
195
|
+
|
|
196
|
+
FastAPI-compatible implementation.
|
|
197
|
+
|
|
198
|
+
Usage:
|
|
199
|
+
app.add_middleware(HTTPSRedirectMiddleware)
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def before_request(self, request: Request) -> None:
|
|
203
|
+
"""Redirect HTTP to HTTPS."""
|
|
204
|
+
# Check if request is HTTP
|
|
205
|
+
scheme = request.headers.get("x-forwarded-proto", "http")
|
|
206
|
+
if scheme == "http":
|
|
207
|
+
# Redirect to HTTPS
|
|
208
|
+
https_url = f"https://{request.headers.get('host', '')}{request.path}"
|
|
209
|
+
if request.query_string:
|
|
210
|
+
https_url += f"?{request.query_string}"
|
|
211
|
+
|
|
212
|
+
raise HTTPSRedirect(https_url)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class HTTPSRedirect(Exception):
|
|
216
|
+
"""Exception to trigger HTTPS redirect."""
|
|
217
|
+
def __init__(self, url: str):
|
|
218
|
+
self.url = url
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class SessionMiddleware(Middleware):
|
|
222
|
+
"""
|
|
223
|
+
Session management middleware.
|
|
224
|
+
|
|
225
|
+
Usage:
|
|
226
|
+
app.add_middleware(
|
|
227
|
+
SessionMiddleware,
|
|
228
|
+
secret_key="your-secret-key-here",
|
|
229
|
+
session_cookie="session"
|
|
230
|
+
)
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
def __init__(
|
|
234
|
+
self,
|
|
235
|
+
secret_key: str,
|
|
236
|
+
session_cookie: str = "session",
|
|
237
|
+
max_age: int = 14 * 24 * 60 * 60, # 14 days
|
|
238
|
+
same_site: str = "lax",
|
|
239
|
+
https_only: bool = False,
|
|
240
|
+
):
|
|
241
|
+
self.secret_key = secret_key
|
|
242
|
+
self.session_cookie = session_cookie
|
|
243
|
+
self.max_age = max_age
|
|
244
|
+
self.same_site = same_site
|
|
245
|
+
self.https_only = https_only
|
|
246
|
+
|
|
247
|
+
def before_request(self, request: Request) -> None:
|
|
248
|
+
"""Load session from cookie."""
|
|
249
|
+
# TODO: Implement session loading
|
|
250
|
+
request.session = {}
|
|
251
|
+
|
|
252
|
+
def after_request(self, request: Request, response: Response) -> Response:
|
|
253
|
+
"""Save session to cookie."""
|
|
254
|
+
# TODO: Implement session saving
|
|
255
|
+
return response
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class RateLimitMiddleware(Middleware):
|
|
259
|
+
"""
|
|
260
|
+
Rate limiting middleware.
|
|
261
|
+
|
|
262
|
+
Usage:
|
|
263
|
+
app.add_middleware(
|
|
264
|
+
RateLimitMiddleware,
|
|
265
|
+
requests_per_minute=60
|
|
266
|
+
)
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
def __init__(
|
|
270
|
+
self,
|
|
271
|
+
requests_per_minute: int = 60,
|
|
272
|
+
burst: int = 10,
|
|
273
|
+
):
|
|
274
|
+
self.requests_per_minute = requests_per_minute
|
|
275
|
+
self.burst = burst
|
|
276
|
+
self.requests = {} # IP -> [(timestamp, count)]
|
|
277
|
+
|
|
278
|
+
def before_request(self, request: Request) -> None:
|
|
279
|
+
"""Check rate limit."""
|
|
280
|
+
client_ip = request.headers.get("x-forwarded-for", "unknown").split(",")[0].strip()
|
|
281
|
+
now = time.time()
|
|
282
|
+
|
|
283
|
+
# Clean old requests
|
|
284
|
+
if client_ip in self.requests:
|
|
285
|
+
self.requests[client_ip] = [
|
|
286
|
+
(ts, count) for ts, count in self.requests[client_ip]
|
|
287
|
+
if now - ts < 60
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
# Count requests in last minute
|
|
291
|
+
if client_ip not in self.requests:
|
|
292
|
+
self.requests[client_ip] = []
|
|
293
|
+
|
|
294
|
+
request_count = sum(count for _, count in self.requests[client_ip])
|
|
295
|
+
|
|
296
|
+
if request_count >= self.requests_per_minute:
|
|
297
|
+
raise Exception("Rate limit exceeded")
|
|
298
|
+
|
|
299
|
+
# Add this request
|
|
300
|
+
self.requests[client_ip].append((now, 1))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class LoggingMiddleware(Middleware):
|
|
304
|
+
"""
|
|
305
|
+
Request logging middleware.
|
|
306
|
+
|
|
307
|
+
Usage:
|
|
308
|
+
app.add_middleware(LoggingMiddleware)
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
def before_request(self, request: Request) -> None:
|
|
312
|
+
"""Log incoming request."""
|
|
313
|
+
request._start_time = time.time()
|
|
314
|
+
print(f"[REQUEST] {request.method} {request.path}")
|
|
315
|
+
|
|
316
|
+
def after_request(self, request: Request, response: Response) -> Response:
|
|
317
|
+
"""Log response with timing."""
|
|
318
|
+
duration = time.time() - getattr(request, '_start_time', time.time())
|
|
319
|
+
print(f"[RESPONSE] {request.method} {request.path} -> {response.status_code} ({duration*1000:.2f}ms)")
|
|
320
|
+
return response
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class CustomMiddleware(Middleware):
|
|
324
|
+
"""
|
|
325
|
+
Custom middleware wrapper for function-based middleware.
|
|
326
|
+
|
|
327
|
+
Usage:
|
|
328
|
+
@app.middleware("http")
|
|
329
|
+
async def add_process_time_header(request, call_next):
|
|
330
|
+
start_time = time.time()
|
|
331
|
+
response = await call_next(request)
|
|
332
|
+
process_time = time.time() - start_time
|
|
333
|
+
response.headers["X-Process-Time"] = str(process_time)
|
|
334
|
+
return response
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
def __init__(self, func: Callable):
|
|
338
|
+
self.func = func
|
|
339
|
+
|
|
340
|
+
async def __call__(self, request: Request, call_next: Callable) -> Response:
|
|
341
|
+
"""Execute custom middleware function."""
|
|
342
|
+
return await self.func(request, call_next)
|
turboapi/models.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Request and Response models for TurboAPI with Satya integration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from satya import Field, Model
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TurboRequest(Model):
|
|
12
|
+
"""High-performance HTTP Request model powered by Satya."""
|
|
13
|
+
|
|
14
|
+
method: str = Field(description="HTTP method")
|
|
15
|
+
path: str = Field(description="Request path")
|
|
16
|
+
query_string: str = Field(default="", description="Query string")
|
|
17
|
+
headers: dict[str, str] = Field(default={}, description="HTTP headers")
|
|
18
|
+
path_params: dict[str, str] = Field(default={}, description="Path parameters")
|
|
19
|
+
query_params: dict[str, str] = Field(default={}, description="Query parameters")
|
|
20
|
+
body: bytes | None = Field(default=None, description="Request body")
|
|
21
|
+
|
|
22
|
+
def get_header(self, name: str, default: str | None = None) -> str | None:
|
|
23
|
+
"""Get header value (case-insensitive)."""
|
|
24
|
+
name_lower = name.lower()
|
|
25
|
+
for key, value in self.headers.items():
|
|
26
|
+
if key.lower() == name_lower:
|
|
27
|
+
return value
|
|
28
|
+
return default
|
|
29
|
+
|
|
30
|
+
def json(self) -> Any:
|
|
31
|
+
"""Parse request body as JSON using Satya's fast parsing."""
|
|
32
|
+
if not self.body:
|
|
33
|
+
return None
|
|
34
|
+
# Use Satya's streaming JSON parsing for performance
|
|
35
|
+
return json.loads(self.body.decode('utf-8'))
|
|
36
|
+
|
|
37
|
+
def validate_json(self, model_class: type) -> Any:
|
|
38
|
+
"""Validate JSON body against a Satya model."""
|
|
39
|
+
if not self.body:
|
|
40
|
+
return None
|
|
41
|
+
return model_class.model_validate_json_bytes(self.body, streaming=True)
|
|
42
|
+
|
|
43
|
+
def text(self) -> str:
|
|
44
|
+
"""Get request body as text."""
|
|
45
|
+
return self.body.decode('utf-8') if self.body else ""
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def content_type(self) -> str | None:
|
|
49
|
+
"""Get Content-Type header."""
|
|
50
|
+
return self.get_header('content-type')
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def content_length(self) -> int:
|
|
54
|
+
"""Get Content-Length."""
|
|
55
|
+
length_str = self.get_header('content-length')
|
|
56
|
+
return int(length_str) if length_str else len(self.body or b"")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Backward compatibility alias
|
|
60
|
+
Request = TurboRequest
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TurboResponse(Model):
|
|
64
|
+
"""High-performance HTTP Response model powered by Satya."""
|
|
65
|
+
|
|
66
|
+
status_code: int = Field(ge=100, le=599, default=200, description="HTTP status code")
|
|
67
|
+
headers: dict[str, str] = Field(default={}, description="HTTP headers")
|
|
68
|
+
content: Any = Field(default="", description="Response content")
|
|
69
|
+
|
|
70
|
+
def __init__(self, **data):
|
|
71
|
+
# Handle content serialization before validation
|
|
72
|
+
if 'content' in data:
|
|
73
|
+
content = data['content']
|
|
74
|
+
if isinstance(content, dict):
|
|
75
|
+
# Serialize dict to JSON
|
|
76
|
+
data['content'] = json.dumps(content)
|
|
77
|
+
if 'headers' not in data:
|
|
78
|
+
data['headers'] = {}
|
|
79
|
+
data['headers']['content-type'] = 'application/json'
|
|
80
|
+
elif isinstance(content, (str, int, float, bool)):
|
|
81
|
+
# Keep as-is, will be converted to string
|
|
82
|
+
pass
|
|
83
|
+
elif isinstance(content, bytes):
|
|
84
|
+
# Convert bytes to string for storage
|
|
85
|
+
data['content'] = content.decode('utf-8')
|
|
86
|
+
else:
|
|
87
|
+
# Convert other types to string
|
|
88
|
+
data['content'] = str(content)
|
|
89
|
+
|
|
90
|
+
super().__init__(**data)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def body(self) -> bytes:
|
|
94
|
+
"""Get response body as bytes."""
|
|
95
|
+
if isinstance(self.content, str):
|
|
96
|
+
return self.content.encode('utf-8')
|
|
97
|
+
elif isinstance(self.content, bytes):
|
|
98
|
+
return self.content
|
|
99
|
+
else:
|
|
100
|
+
return str(self.content).encode('utf-8')
|
|
101
|
+
|
|
102
|
+
def set_header(self, name: str, value: str) -> None:
|
|
103
|
+
"""Set a response header."""
|
|
104
|
+
self.headers[name] = value
|
|
105
|
+
|
|
106
|
+
def get_header(self, name: str, default: str | None = None) -> str | None:
|
|
107
|
+
"""Get a response header."""
|
|
108
|
+
return self.headers.get(name, default)
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def json(cls, data: Any, status_code: int = 200, headers: dict[str, str] | None = None):
|
|
112
|
+
"""Create a JSON response with automatic serialization."""
|
|
113
|
+
response_headers = headers or {}
|
|
114
|
+
response_headers['content-type'] = 'application/json'
|
|
115
|
+
|
|
116
|
+
return cls(
|
|
117
|
+
content=data, # Will be auto-serialized to JSON
|
|
118
|
+
status_code=status_code,
|
|
119
|
+
headers=response_headers
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def text(cls, content: str, status_code: int = 200, headers: dict[str, str] | None = None):
|
|
124
|
+
"""Create a text response."""
|
|
125
|
+
response_headers = headers or {}
|
|
126
|
+
response_headers['content-type'] = 'text/plain; charset=utf-8'
|
|
127
|
+
|
|
128
|
+
return cls(
|
|
129
|
+
content=content,
|
|
130
|
+
status_code=status_code,
|
|
131
|
+
headers=response_headers
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def html(cls, content: str, status_code: int = 200, headers: dict[str, str] | None = None):
|
|
136
|
+
"""Create an HTML response."""
|
|
137
|
+
response_headers = headers or {}
|
|
138
|
+
response_headers['content-type'] = 'text/html; charset=utf-8'
|
|
139
|
+
|
|
140
|
+
return cls(
|
|
141
|
+
content=content,
|
|
142
|
+
status_code=status_code,
|
|
143
|
+
headers=response_headers
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Backward compatibility alias
|
|
148
|
+
Response = TurboResponse
|