turboapi 0.3.24__cp314-cp314-win_amd64.whl → 0.3.29__cp314-cp314-win_amd64.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/_rust.cp314-win_amd64.pyd +0 -0
- turboapi/async_limiter.py +86 -0
- turboapi/async_pool.py +141 -0
- turboapi/middleware.py +292 -14
- turboapi/request_handler.py +21 -22
- turboapi/rust_integration.py +7 -8
- turboapi/security.py +542 -0
- {turboapi-0.3.24.dist-info → turboapi-0.3.29.dist-info}/METADATA +2 -2
- turboapi-0.3.29.dist-info/RECORD +17 -0
- {turboapi-0.3.24.dist-info → turboapi-0.3.29.dist-info}/WHEEL +1 -1
- turboapi/__pycache__/__init__.cpython-312.pyc +0 -0
- turboapi/__pycache__/__init__.cpython-313.pyc +0 -0
- turboapi/__pycache__/app.cpython-312.pyc +0 -0
- turboapi/__pycache__/app.cpython-313.pyc +0 -0
- turboapi/__pycache__/decorators.cpython-312.pyc +0 -0
- turboapi/__pycache__/decorators.cpython-313.pyc +0 -0
- turboapi/__pycache__/main_app.cpython-312.pyc +0 -0
- turboapi/__pycache__/main_app.cpython-313.pyc +0 -0
- turboapi/__pycache__/middleware.cpython-312.pyc +0 -0
- turboapi/__pycache__/middleware.cpython-313.pyc +0 -0
- turboapi/__pycache__/models.cpython-312.pyc +0 -0
- turboapi/__pycache__/models.cpython-313.pyc +0 -0
- turboapi/__pycache__/routing.cpython-312.pyc +0 -0
- turboapi/__pycache__/routing.cpython-313.pyc +0 -0
- turboapi/__pycache__/rust_integration.cpython-312.pyc +0 -0
- turboapi/__pycache__/rust_integration.cpython-313.pyc +0 -0
- turboapi/__pycache__/server_integration.cpython-313.pyc +0 -0
- turboapi/__pycache__/version_check.cpython-312.pyc +0 -0
- turboapi/__pycache__/version_check.cpython-313.pyc +0 -0
- turboapi-0.3.24.dist-info/RECORD +0 -33
|
Binary file
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Async Limiter - Semaphore-based rate limiting for async tasks
|
|
3
|
+
|
|
4
|
+
Prevents event loop overload by limiting concurrent async tasks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from typing import Any, Coroutine
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AsyncLimiter:
|
|
12
|
+
"""Semaphore-based limiter for async tasks
|
|
13
|
+
|
|
14
|
+
Limits the number of concurrent async tasks to prevent event loop overload.
|
|
15
|
+
This is critical for maintaining stable performance under high load.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
max_concurrent: Maximum number of concurrent tasks (default: 512)
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
limiter = AsyncLimiter(max_concurrent=512)
|
|
22
|
+
result = await limiter(some_coroutine())
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, max_concurrent: int = 512):
|
|
26
|
+
self.semaphore = asyncio.Semaphore(max_concurrent)
|
|
27
|
+
self.max_concurrent = max_concurrent
|
|
28
|
+
self._active_tasks = 0
|
|
29
|
+
|
|
30
|
+
async def __call__(self, coro: Coroutine) -> Any:
|
|
31
|
+
"""Execute coroutine with semaphore gating
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
coro: Coroutine to execute
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Result of the coroutine
|
|
38
|
+
"""
|
|
39
|
+
async with self.semaphore:
|
|
40
|
+
self._active_tasks += 1
|
|
41
|
+
try:
|
|
42
|
+
return await coro
|
|
43
|
+
finally:
|
|
44
|
+
self._active_tasks -= 1
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def active_tasks(self) -> int:
|
|
48
|
+
"""Get current number of active tasks"""
|
|
49
|
+
return self._active_tasks
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def available_slots(self) -> int:
|
|
53
|
+
"""Get number of available slots"""
|
|
54
|
+
return self.max_concurrent - self._active_tasks
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Global limiter instance per event loop
|
|
58
|
+
_limiters = {}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_limiter(max_concurrent: int = 512) -> AsyncLimiter:
|
|
62
|
+
"""Get or create limiter for current event loop
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
max_concurrent: Maximum concurrent tasks
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
AsyncLimiter instance for current event loop
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
loop = asyncio.get_running_loop()
|
|
72
|
+
loop_id = id(loop)
|
|
73
|
+
|
|
74
|
+
if loop_id not in _limiters:
|
|
75
|
+
_limiters[loop_id] = AsyncLimiter(max_concurrent)
|
|
76
|
+
|
|
77
|
+
return _limiters[loop_id]
|
|
78
|
+
except RuntimeError:
|
|
79
|
+
# No running loop, create standalone limiter
|
|
80
|
+
return AsyncLimiter(max_concurrent)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def reset_limiters():
|
|
84
|
+
"""Reset all limiters (useful for testing)"""
|
|
85
|
+
global _limiters
|
|
86
|
+
_limiters = {}
|
turboapi/async_pool.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Per-thread asyncio event loop management for Python 3.13+ free-threading.
|
|
3
|
+
|
|
4
|
+
This module provides thread-local event loop management to enable true
|
|
5
|
+
parallel execution of async handlers across multiple threads.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import threading
|
|
10
|
+
from typing import Dict, Optional
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EventLoopPool:
|
|
15
|
+
"""
|
|
16
|
+
Manages per-thread asyncio event loops for parallel async execution.
|
|
17
|
+
|
|
18
|
+
In Python 3.13+ with free-threading, we can run multiple event loops
|
|
19
|
+
in parallel across different threads without GIL contention.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
_loops: Dict[int, asyncio.AbstractEventLoop] = {}
|
|
23
|
+
_lock = threading.Lock()
|
|
24
|
+
_initialized = False
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def initialize(cls, num_threads: Optional[int] = None) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Initialize the event loop pool with the specified number of threads.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
num_threads: Number of threads to create event loops for.
|
|
33
|
+
If None, uses number of CPU cores.
|
|
34
|
+
"""
|
|
35
|
+
if cls._initialized:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
with cls._lock:
|
|
39
|
+
if cls._initialized:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
if num_threads is None:
|
|
43
|
+
import os
|
|
44
|
+
num_threads = os.cpu_count() or 4
|
|
45
|
+
|
|
46
|
+
print(f"🔄 Initializing EventLoopPool with {num_threads} threads")
|
|
47
|
+
cls._initialized = True
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def get_loop_for_thread(cls) -> asyncio.AbstractEventLoop:
|
|
51
|
+
"""
|
|
52
|
+
Get or create an event loop for the current thread.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The event loop for the current thread.
|
|
56
|
+
"""
|
|
57
|
+
thread_id = threading.get_ident()
|
|
58
|
+
|
|
59
|
+
# Fast path: loop already exists
|
|
60
|
+
if thread_id in cls._loops:
|
|
61
|
+
return cls._loops[thread_id]
|
|
62
|
+
|
|
63
|
+
# Slow path: create new loop
|
|
64
|
+
with cls._lock:
|
|
65
|
+
# Double-check after acquiring lock
|
|
66
|
+
if thread_id in cls._loops:
|
|
67
|
+
return cls._loops[thread_id]
|
|
68
|
+
|
|
69
|
+
# Create new event loop for this thread
|
|
70
|
+
loop = asyncio.new_event_loop()
|
|
71
|
+
asyncio.set_event_loop(loop)
|
|
72
|
+
cls._loops[thread_id] = loop
|
|
73
|
+
|
|
74
|
+
print(f"✅ Created event loop for thread {thread_id}")
|
|
75
|
+
return loop
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def get_running_loop(cls) -> Optional[asyncio.AbstractEventLoop]:
|
|
79
|
+
"""
|
|
80
|
+
Get the running event loop for the current thread, if any.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The running event loop, or None if no loop is running.
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
return asyncio.get_running_loop()
|
|
87
|
+
except RuntimeError:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def cleanup(cls) -> None:
|
|
92
|
+
"""Clean up all event loops (call on shutdown)."""
|
|
93
|
+
with cls._lock:
|
|
94
|
+
for thread_id, loop in cls._loops.items():
|
|
95
|
+
if loop.is_running():
|
|
96
|
+
loop.stop()
|
|
97
|
+
loop.close()
|
|
98
|
+
cls._loops.clear()
|
|
99
|
+
cls._initialized = False
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def stats(cls) -> Dict[str, int]:
|
|
103
|
+
"""Get statistics about the event loop pool."""
|
|
104
|
+
with cls._lock:
|
|
105
|
+
return {
|
|
106
|
+
"total_loops": len(cls._loops),
|
|
107
|
+
"active_threads": len([l for l in cls._loops.values() if l.is_running()]),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def ensure_event_loop() -> asyncio.AbstractEventLoop:
|
|
112
|
+
"""
|
|
113
|
+
Ensure an event loop exists for the current thread.
|
|
114
|
+
|
|
115
|
+
This is the primary function to call from Rust to get an event loop.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
The event loop for the current thread.
|
|
119
|
+
"""
|
|
120
|
+
# Try to get running loop first (fast path)
|
|
121
|
+
try:
|
|
122
|
+
return asyncio.get_running_loop()
|
|
123
|
+
except RuntimeError:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
# Get or create thread-local loop
|
|
127
|
+
return EventLoopPool.get_loop_for_thread()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# Python 3.13+ free-threading detection
|
|
131
|
+
def is_free_threading_enabled() -> bool:
|
|
132
|
+
"""Check if Python 3.13+ free-threading is enabled."""
|
|
133
|
+
return hasattr(sys, '_is_gil_enabled') and not sys._is_gil_enabled()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# Initialize on import
|
|
137
|
+
if is_free_threading_enabled():
|
|
138
|
+
print("🚀 Python 3.13+ free-threading detected - enabling parallel event loops!")
|
|
139
|
+
EventLoopPool.initialize()
|
|
140
|
+
else:
|
|
141
|
+
print("⚠️ Free-threading not enabled - async performance may be limited")
|
turboapi/middleware.py
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Middleware system for TurboAPI.
|
|
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
|
|
3
11
|
"""
|
|
4
12
|
|
|
13
|
+
from typing import List, Optional, Callable, Awaitable, Pattern
|
|
14
|
+
import gzip
|
|
15
|
+
import re
|
|
16
|
+
import time
|
|
5
17
|
from .models import Request, Response
|
|
6
18
|
|
|
7
19
|
|
|
@@ -25,40 +37,306 @@ class Middleware:
|
|
|
25
37
|
|
|
26
38
|
|
|
27
39
|
class CORSMiddleware(Middleware):
|
|
28
|
-
"""
|
|
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
|
+
"""
|
|
29
54
|
|
|
30
55
|
def __init__(
|
|
31
56
|
self,
|
|
32
|
-
allow_origins:
|
|
33
|
-
allow_methods:
|
|
34
|
-
allow_headers:
|
|
35
|
-
allow_credentials: bool = False
|
|
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,
|
|
36
64
|
):
|
|
37
65
|
self.allow_origins = allow_origins or ["*"]
|
|
38
|
-
self.allow_methods = allow_methods or ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
|
|
66
|
+
self.allow_methods = allow_methods or ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "HEAD"]
|
|
39
67
|
self.allow_headers = allow_headers or ["*"]
|
|
40
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
|
|
41
78
|
|
|
42
79
|
def after_request(self, request: Request, response: Response) -> Response:
|
|
43
80
|
"""Add CORS headers to response."""
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
|
|
48
97
|
if self.allow_credentials:
|
|
49
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
|
|
50
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
|
+
|
|
51
189
|
return response
|
|
52
190
|
|
|
53
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
|
+
|
|
54
303
|
class LoggingMiddleware(Middleware):
|
|
55
|
-
"""
|
|
304
|
+
"""
|
|
305
|
+
Request logging middleware.
|
|
306
|
+
|
|
307
|
+
Usage:
|
|
308
|
+
app.add_middleware(LoggingMiddleware)
|
|
309
|
+
"""
|
|
56
310
|
|
|
57
311
|
def before_request(self, request: Request) -> None:
|
|
58
312
|
"""Log incoming request."""
|
|
313
|
+
request._start_time = time.time()
|
|
59
314
|
print(f"[REQUEST] {request.method} {request.path}")
|
|
60
315
|
|
|
61
316
|
def after_request(self, request: Request, response: Response) -> Response:
|
|
62
|
-
"""Log response."""
|
|
63
|
-
|
|
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)")
|
|
64
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/request_handler.py
CHANGED
|
@@ -191,38 +191,37 @@ def create_enhanced_handler(original_handler, route_definition):
|
|
|
191
191
|
# Filter kwargs to only pass expected parameters
|
|
192
192
|
filtered_kwargs = {
|
|
193
193
|
k: v for k, v in kwargs.items()
|
|
194
|
+
if k in sig.parameters
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
# Call original handler
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
# Check if result is a coroutine - if so, return it directly for Rust to await
|
|
203
|
-
import inspect
|
|
204
|
-
if inspect.iscoroutine(result):
|
|
205
|
-
# Return coroutine directly - Rust will await it using tokio
|
|
206
|
-
return result
|
|
198
|
+
if inspect.iscoroutinefunction(original_handler):
|
|
199
|
+
# For async handlers (future support)
|
|
200
|
+
result = original_handler(**filtered_kwargs)
|
|
201
|
+
else:
|
|
202
|
+
result = original_handler(**filtered_kwargs)
|
|
207
203
|
|
|
208
|
-
#
|
|
204
|
+
# Normalize response
|
|
209
205
|
content, status_code = ResponseHandler.normalize_response(result)
|
|
210
206
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return json.dumps(content)
|
|
207
|
+
return ResponseHandler.format_json_response(content, status_code)
|
|
208
|
+
|
|
214
209
|
except ValueError as e:
|
|
215
210
|
# Validation or parsing error (400 Bad Request)
|
|
216
|
-
|
|
217
|
-
|
|
211
|
+
return ResponseHandler.format_json_response(
|
|
212
|
+
{"error": "Bad Request", "detail": str(e)},
|
|
213
|
+
400
|
|
214
|
+
)
|
|
218
215
|
except Exception as e:
|
|
219
216
|
# Unexpected error (500 Internal Server Error)
|
|
220
217
|
import traceback
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
218
|
+
return ResponseHandler.format_json_response(
|
|
219
|
+
{
|
|
220
|
+
"error": "Internal Server Error",
|
|
221
|
+
"detail": str(e),
|
|
222
|
+
"traceback": traceback.format_exc()
|
|
223
|
+
},
|
|
224
|
+
500
|
|
225
|
+
)
|
|
227
226
|
|
|
228
227
|
return enhanced_handler
|
turboapi/rust_integration.py
CHANGED
|
@@ -12,7 +12,7 @@ from .request_handler import create_enhanced_handler, ResponseHandler
|
|
|
12
12
|
from .version_check import CHECK_MARK, CROSS_MARK, ROCKET
|
|
13
13
|
|
|
14
14
|
try:
|
|
15
|
-
|
|
15
|
+
import turbonet
|
|
16
16
|
RUST_CORE_AVAILABLE = True
|
|
17
17
|
except ImportError:
|
|
18
18
|
RUST_CORE_AVAILABLE = False
|
|
@@ -175,6 +175,7 @@ class RustIntegratedTurboAPI(TurboAPI):
|
|
|
175
175
|
|
|
176
176
|
# Add query parameters
|
|
177
177
|
call_args.update(query_params)
|
|
178
|
+
|
|
178
179
|
# Always add body and headers for enhanced handler
|
|
179
180
|
call_args['body'] = body if body else b''
|
|
180
181
|
call_args['headers'] = headers
|
|
@@ -186,7 +187,7 @@ class RustIntegratedTurboAPI(TurboAPI):
|
|
|
186
187
|
# {"content": ..., "status_code": ..., "content_type": ...}
|
|
187
188
|
# But Rust expects a plain dict that it will JSON serialize
|
|
188
189
|
# So just return the content directly
|
|
189
|
-
if isinstance(result, dict) and 'content' in result:
|
|
190
|
+
if isinstance(result, dict) and 'content' in result and 'status_code' in result:
|
|
190
191
|
# Return just the content - Rust will handle status codes later
|
|
191
192
|
# For now, just return the content as a dict
|
|
192
193
|
return result['content']
|
|
@@ -205,15 +206,13 @@ class RustIntegratedTurboAPI(TurboAPI):
|
|
|
205
206
|
|
|
206
207
|
return rust_handler # noqa: B023
|
|
207
208
|
|
|
208
|
-
#
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
# Register with Rust server
|
|
209
|
+
# Register the ORIGINAL handler directly with Rust
|
|
210
|
+
# Rust will call it with call0() (no arguments)
|
|
211
|
+
# The original handler doesn't expect any arguments
|
|
213
212
|
self.rust_server.add_route(
|
|
214
213
|
route.method.value,
|
|
215
214
|
route.path,
|
|
216
|
-
|
|
215
|
+
route.handler # Pass original handler, not wrapper!
|
|
217
216
|
)
|
|
218
217
|
|
|
219
218
|
print(f"{CHECK_MARK} Registered {route.method.value} {route.path} with Rust server")
|