turboapi 0.3.23__cp313-cp313-win_amd64.whl → 0.3.28__cp313-cp313-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.cp313-win_amd64.pyd +0 -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.23.dist-info → turboapi-0.3.28.dist-info}/METADATA +2 -2
- turboapi-0.3.28.dist-info/RECORD +16 -0
- {turboapi-0.3.23.dist-info → turboapi-0.3.28.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.23.dist-info/RECORD +0 -33
|
Binary file
|
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")
|
turboapi/security.py
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI-compatible Security and Authentication for TurboAPI.
|
|
3
|
+
|
|
4
|
+
Includes:
|
|
5
|
+
- OAuth2 (Password Bearer, Authorization Code)
|
|
6
|
+
- HTTP Basic Authentication
|
|
7
|
+
- HTTP Bearer Authentication
|
|
8
|
+
- API Key Authentication (Header, Query, Cookie)
|
|
9
|
+
- Security scopes and dependencies
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Optional, List, Dict, Any, Callable
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
import secrets
|
|
15
|
+
import base64
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ============================================================================
|
|
19
|
+
# Base Security Classes
|
|
20
|
+
# ============================================================================
|
|
21
|
+
|
|
22
|
+
class SecurityBase:
|
|
23
|
+
"""Base class for all security schemes."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, *, scheme_name: Optional[str] = None, auto_error: bool = True):
|
|
26
|
+
self.scheme_name = scheme_name
|
|
27
|
+
self.auto_error = auto_error
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ============================================================================
|
|
31
|
+
# OAuth2 Authentication
|
|
32
|
+
# ============================================================================
|
|
33
|
+
|
|
34
|
+
class OAuth2PasswordBearer(SecurityBase):
|
|
35
|
+
"""
|
|
36
|
+
OAuth2 password bearer token authentication.
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
|
40
|
+
|
|
41
|
+
@app.get("/users/me")
|
|
42
|
+
async def get_user(token: str = Depends(oauth2_scheme)):
|
|
43
|
+
return {"token": token}
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
tokenUrl: str,
|
|
49
|
+
scheme_name: Optional[str] = None,
|
|
50
|
+
scopes: Optional[Dict[str, str]] = None,
|
|
51
|
+
description: Optional[str] = None,
|
|
52
|
+
auto_error: bool = True,
|
|
53
|
+
):
|
|
54
|
+
super().__init__(scheme_name=scheme_name, auto_error=auto_error)
|
|
55
|
+
self.tokenUrl = tokenUrl
|
|
56
|
+
self.scopes = scopes or {}
|
|
57
|
+
self.description = description
|
|
58
|
+
self.model = {
|
|
59
|
+
"type": "oauth2",
|
|
60
|
+
"flows": {
|
|
61
|
+
"password": {
|
|
62
|
+
"tokenUrl": tokenUrl,
|
|
63
|
+
"scopes": self.scopes,
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
def __call__(self, authorization: Optional[str] = None) -> Optional[str]:
|
|
69
|
+
"""Extract token from Authorization header."""
|
|
70
|
+
if not authorization:
|
|
71
|
+
if self.auto_error:
|
|
72
|
+
raise HTTPException(
|
|
73
|
+
status_code=401,
|
|
74
|
+
detail="Not authenticated",
|
|
75
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
76
|
+
)
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
scheme, _, token = authorization.partition(" ")
|
|
80
|
+
if scheme.lower() != "bearer":
|
|
81
|
+
if self.auto_error:
|
|
82
|
+
raise HTTPException(
|
|
83
|
+
status_code=401,
|
|
84
|
+
detail="Invalid authentication credentials",
|
|
85
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
86
|
+
)
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
return token
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class OAuth2PasswordRequestForm:
|
|
94
|
+
"""
|
|
95
|
+
OAuth2 password request form data.
|
|
96
|
+
|
|
97
|
+
Automatically parses form data for OAuth2 password flow.
|
|
98
|
+
"""
|
|
99
|
+
username: str
|
|
100
|
+
password: str
|
|
101
|
+
scope: str = ""
|
|
102
|
+
grant_type: Optional[str] = "password"
|
|
103
|
+
client_id: Optional[str] = None
|
|
104
|
+
client_secret: Optional[str] = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class OAuth2AuthorizationCodeBearer(SecurityBase):
|
|
108
|
+
"""
|
|
109
|
+
OAuth2 authorization code flow with bearer token.
|
|
110
|
+
|
|
111
|
+
Usage:
|
|
112
|
+
oauth2_scheme = OAuth2AuthorizationCodeBearer(
|
|
113
|
+
authorizationUrl="https://example.com/oauth/authorize",
|
|
114
|
+
tokenUrl="https://example.com/oauth/token"
|
|
115
|
+
)
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
authorizationUrl: str,
|
|
121
|
+
tokenUrl: str,
|
|
122
|
+
refreshUrl: Optional[str] = None,
|
|
123
|
+
scheme_name: Optional[str] = None,
|
|
124
|
+
scopes: Optional[Dict[str, str]] = None,
|
|
125
|
+
description: Optional[str] = None,
|
|
126
|
+
auto_error: bool = True,
|
|
127
|
+
):
|
|
128
|
+
super().__init__(scheme_name=scheme_name, auto_error=auto_error)
|
|
129
|
+
self.authorizationUrl = authorizationUrl
|
|
130
|
+
self.tokenUrl = tokenUrl
|
|
131
|
+
self.refreshUrl = refreshUrl
|
|
132
|
+
self.scopes = scopes or {}
|
|
133
|
+
self.description = description
|
|
134
|
+
self.model = {
|
|
135
|
+
"type": "oauth2",
|
|
136
|
+
"flows": {
|
|
137
|
+
"authorizationCode": {
|
|
138
|
+
"authorizationUrl": authorizationUrl,
|
|
139
|
+
"tokenUrl": tokenUrl,
|
|
140
|
+
"refreshUrl": refreshUrl,
|
|
141
|
+
"scopes": self.scopes,
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def __call__(self, authorization: Optional[str] = None) -> Optional[str]:
|
|
147
|
+
"""Extract token from Authorization header."""
|
|
148
|
+
if not authorization:
|
|
149
|
+
if self.auto_error:
|
|
150
|
+
raise HTTPException(
|
|
151
|
+
status_code=401,
|
|
152
|
+
detail="Not authenticated",
|
|
153
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
154
|
+
)
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
scheme, _, token = authorization.partition(" ")
|
|
158
|
+
if scheme.lower() != "bearer":
|
|
159
|
+
if self.auto_error:
|
|
160
|
+
raise HTTPException(
|
|
161
|
+
status_code=401,
|
|
162
|
+
detail="Invalid authentication credentials",
|
|
163
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
164
|
+
)
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
return token
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ============================================================================
|
|
171
|
+
# HTTP Basic Authentication
|
|
172
|
+
# ============================================================================
|
|
173
|
+
|
|
174
|
+
@dataclass
|
|
175
|
+
class HTTPBasicCredentials:
|
|
176
|
+
"""HTTP Basic authentication credentials."""
|
|
177
|
+
username: str
|
|
178
|
+
password: str
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class HTTPBasic(SecurityBase):
|
|
182
|
+
"""
|
|
183
|
+
HTTP Basic authentication.
|
|
184
|
+
|
|
185
|
+
Usage:
|
|
186
|
+
security = HTTPBasic()
|
|
187
|
+
|
|
188
|
+
@app.get("/users/me")
|
|
189
|
+
def get_user(credentials: HTTPBasicCredentials = Depends(security)):
|
|
190
|
+
return {"username": credentials.username}
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
def __init__(
|
|
194
|
+
self,
|
|
195
|
+
*,
|
|
196
|
+
scheme_name: Optional[str] = None,
|
|
197
|
+
realm: Optional[str] = None,
|
|
198
|
+
auto_error: bool = True,
|
|
199
|
+
):
|
|
200
|
+
super().__init__(scheme_name=scheme_name, auto_error=auto_error)
|
|
201
|
+
self.realm = realm
|
|
202
|
+
self.model = {"type": "http", "scheme": "basic"}
|
|
203
|
+
|
|
204
|
+
def __call__(self, authorization: Optional[str] = None) -> Optional[HTTPBasicCredentials]:
|
|
205
|
+
"""Extract and decode Basic auth credentials."""
|
|
206
|
+
if not authorization:
|
|
207
|
+
if self.auto_error:
|
|
208
|
+
raise HTTPException(
|
|
209
|
+
status_code=401,
|
|
210
|
+
detail="Not authenticated",
|
|
211
|
+
headers={"WWW-Authenticate": f'Basic realm="{self.realm}"' if self.realm else "Basic"},
|
|
212
|
+
)
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
scheme, _, credentials = authorization.partition(" ")
|
|
216
|
+
if scheme.lower() != "basic":
|
|
217
|
+
if self.auto_error:
|
|
218
|
+
raise HTTPException(
|
|
219
|
+
status_code=401,
|
|
220
|
+
detail="Invalid authentication credentials",
|
|
221
|
+
headers={"WWW-Authenticate": f'Basic realm="{self.realm}"' if self.realm else "Basic"},
|
|
222
|
+
)
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
decoded = base64.b64decode(credentials).decode("utf-8")
|
|
227
|
+
username, _, password = decoded.partition(":")
|
|
228
|
+
return HTTPBasicCredentials(username=username, password=password)
|
|
229
|
+
except Exception:
|
|
230
|
+
if self.auto_error:
|
|
231
|
+
raise HTTPException(
|
|
232
|
+
status_code=401,
|
|
233
|
+
detail="Invalid authentication credentials",
|
|
234
|
+
headers={"WWW-Authenticate": f'Basic realm="{self.realm}"' if self.realm else "Basic"},
|
|
235
|
+
)
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ============================================================================
|
|
240
|
+
# HTTP Bearer Authentication
|
|
241
|
+
# ============================================================================
|
|
242
|
+
|
|
243
|
+
@dataclass
|
|
244
|
+
class HTTPAuthorizationCredentials:
|
|
245
|
+
"""HTTP authorization credentials."""
|
|
246
|
+
scheme: str
|
|
247
|
+
credentials: str
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class HTTPBearer(SecurityBase):
|
|
251
|
+
"""
|
|
252
|
+
HTTP Bearer token authentication.
|
|
253
|
+
|
|
254
|
+
Usage:
|
|
255
|
+
security = HTTPBearer()
|
|
256
|
+
|
|
257
|
+
@app.get("/users/me")
|
|
258
|
+
def get_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
|
259
|
+
return {"token": credentials.credentials}
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
def __init__(
|
|
263
|
+
self,
|
|
264
|
+
*,
|
|
265
|
+
scheme_name: Optional[str] = None,
|
|
266
|
+
auto_error: bool = True,
|
|
267
|
+
):
|
|
268
|
+
super().__init__(scheme_name=scheme_name, auto_error=auto_error)
|
|
269
|
+
self.model = {"type": "http", "scheme": "bearer"}
|
|
270
|
+
|
|
271
|
+
def __call__(self, authorization: Optional[str] = None) -> Optional[HTTPAuthorizationCredentials]:
|
|
272
|
+
"""Extract Bearer token."""
|
|
273
|
+
if not authorization:
|
|
274
|
+
if self.auto_error:
|
|
275
|
+
raise HTTPException(
|
|
276
|
+
status_code=401,
|
|
277
|
+
detail="Not authenticated",
|
|
278
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
279
|
+
)
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
scheme, _, credentials = authorization.partition(" ")
|
|
283
|
+
if scheme.lower() != "bearer":
|
|
284
|
+
if self.auto_error:
|
|
285
|
+
raise HTTPException(
|
|
286
|
+
status_code=401,
|
|
287
|
+
detail="Invalid authentication credentials",
|
|
288
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
289
|
+
)
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class HTTPDigest(SecurityBase):
|
|
296
|
+
"""
|
|
297
|
+
HTTP Digest authentication.
|
|
298
|
+
|
|
299
|
+
Usage:
|
|
300
|
+
security = HTTPDigest()
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
def __init__(
|
|
304
|
+
self,
|
|
305
|
+
*,
|
|
306
|
+
scheme_name: Optional[str] = None,
|
|
307
|
+
auto_error: bool = True,
|
|
308
|
+
):
|
|
309
|
+
super().__init__(scheme_name=scheme_name, auto_error=auto_error)
|
|
310
|
+
self.model = {"type": "http", "scheme": "digest"}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ============================================================================
|
|
314
|
+
# API Key Authentication
|
|
315
|
+
# ============================================================================
|
|
316
|
+
|
|
317
|
+
class APIKeyBase(SecurityBase):
|
|
318
|
+
"""Base class for API key authentication."""
|
|
319
|
+
|
|
320
|
+
def __init__(
|
|
321
|
+
self,
|
|
322
|
+
*,
|
|
323
|
+
name: str,
|
|
324
|
+
scheme_name: Optional[str] = None,
|
|
325
|
+
description: Optional[str] = None,
|
|
326
|
+
auto_error: bool = True,
|
|
327
|
+
):
|
|
328
|
+
super().__init__(scheme_name=scheme_name, auto_error=auto_error)
|
|
329
|
+
self.name = name
|
|
330
|
+
self.description = description
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class APIKeyQuery(APIKeyBase):
|
|
334
|
+
"""
|
|
335
|
+
API Key authentication via query parameter.
|
|
336
|
+
|
|
337
|
+
Usage:
|
|
338
|
+
api_key = APIKeyQuery(name="api_key")
|
|
339
|
+
|
|
340
|
+
@app.get("/items")
|
|
341
|
+
def get_items(key: str = Depends(api_key)):
|
|
342
|
+
return {"api_key": key}
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
def __init__(
|
|
346
|
+
self,
|
|
347
|
+
*,
|
|
348
|
+
name: str,
|
|
349
|
+
scheme_name: Optional[str] = None,
|
|
350
|
+
description: Optional[str] = None,
|
|
351
|
+
auto_error: bool = True,
|
|
352
|
+
):
|
|
353
|
+
super().__init__(
|
|
354
|
+
name=name,
|
|
355
|
+
scheme_name=scheme_name,
|
|
356
|
+
description=description,
|
|
357
|
+
auto_error=auto_error,
|
|
358
|
+
)
|
|
359
|
+
self.model = {"type": "apiKey", "in": "query", "name": name}
|
|
360
|
+
|
|
361
|
+
def __call__(self, query_params: Optional[Dict[str, str]] = None) -> Optional[str]:
|
|
362
|
+
"""Extract API key from query parameters."""
|
|
363
|
+
if not query_params or self.name not in query_params:
|
|
364
|
+
if self.auto_error:
|
|
365
|
+
raise HTTPException(
|
|
366
|
+
status_code=403,
|
|
367
|
+
detail="Not authenticated",
|
|
368
|
+
)
|
|
369
|
+
return None
|
|
370
|
+
return query_params[self.name]
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class APIKeyHeader(APIKeyBase):
|
|
374
|
+
"""
|
|
375
|
+
API Key authentication via HTTP header.
|
|
376
|
+
|
|
377
|
+
Usage:
|
|
378
|
+
api_key = APIKeyHeader(name="X-API-Key")
|
|
379
|
+
|
|
380
|
+
@app.get("/items")
|
|
381
|
+
def get_items(key: str = Depends(api_key)):
|
|
382
|
+
return {"api_key": key}
|
|
383
|
+
"""
|
|
384
|
+
|
|
385
|
+
def __init__(
|
|
386
|
+
self,
|
|
387
|
+
*,
|
|
388
|
+
name: str,
|
|
389
|
+
scheme_name: Optional[str] = None,
|
|
390
|
+
description: Optional[str] = None,
|
|
391
|
+
auto_error: bool = True,
|
|
392
|
+
):
|
|
393
|
+
super().__init__(
|
|
394
|
+
name=name,
|
|
395
|
+
scheme_name=scheme_name,
|
|
396
|
+
description=description,
|
|
397
|
+
auto_error=auto_error,
|
|
398
|
+
)
|
|
399
|
+
self.model = {"type": "apiKey", "in": "header", "name": name}
|
|
400
|
+
|
|
401
|
+
def __call__(self, headers: Optional[Dict[str, str]] = None) -> Optional[str]:
|
|
402
|
+
"""Extract API key from headers."""
|
|
403
|
+
if not headers or self.name.lower() not in {k.lower(): v for k, v in headers.items()}:
|
|
404
|
+
if self.auto_error:
|
|
405
|
+
raise HTTPException(
|
|
406
|
+
status_code=403,
|
|
407
|
+
detail="Not authenticated",
|
|
408
|
+
)
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
# Case-insensitive header lookup
|
|
412
|
+
for key, value in headers.items():
|
|
413
|
+
if key.lower() == self.name.lower():
|
|
414
|
+
return value
|
|
415
|
+
return None
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class APIKeyCookie(APIKeyBase):
|
|
419
|
+
"""
|
|
420
|
+
API Key authentication via HTTP cookie.
|
|
421
|
+
|
|
422
|
+
Usage:
|
|
423
|
+
api_key = APIKeyCookie(name="session")
|
|
424
|
+
|
|
425
|
+
@app.get("/items")
|
|
426
|
+
def get_items(key: str = Depends(api_key)):
|
|
427
|
+
return {"session": key}
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
def __init__(
|
|
431
|
+
self,
|
|
432
|
+
*,
|
|
433
|
+
name: str,
|
|
434
|
+
scheme_name: Optional[str] = None,
|
|
435
|
+
description: Optional[str] = None,
|
|
436
|
+
auto_error: bool = True,
|
|
437
|
+
):
|
|
438
|
+
super().__init__(
|
|
439
|
+
name=name,
|
|
440
|
+
scheme_name=scheme_name,
|
|
441
|
+
description=description,
|
|
442
|
+
auto_error=auto_error,
|
|
443
|
+
)
|
|
444
|
+
self.model = {"type": "apiKey", "in": "cookie", "name": name}
|
|
445
|
+
|
|
446
|
+
def __call__(self, cookies: Optional[Dict[str, str]] = None) -> Optional[str]:
|
|
447
|
+
"""Extract API key from cookies."""
|
|
448
|
+
if not cookies or self.name not in cookies:
|
|
449
|
+
if self.auto_error:
|
|
450
|
+
raise HTTPException(
|
|
451
|
+
status_code=403,
|
|
452
|
+
detail="Not authenticated",
|
|
453
|
+
)
|
|
454
|
+
return None
|
|
455
|
+
return cookies[self.name]
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
# ============================================================================
|
|
459
|
+
# Security Scopes
|
|
460
|
+
# ============================================================================
|
|
461
|
+
|
|
462
|
+
class SecurityScopes:
|
|
463
|
+
"""
|
|
464
|
+
Security scopes for OAuth2 and other scope-based auth.
|
|
465
|
+
|
|
466
|
+
Usage:
|
|
467
|
+
def get_current_user(
|
|
468
|
+
security_scopes: SecurityScopes,
|
|
469
|
+
token: str = Depends(oauth2_scheme)
|
|
470
|
+
):
|
|
471
|
+
if security_scopes.scopes:
|
|
472
|
+
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
|
|
473
|
+
else:
|
|
474
|
+
authenticate_value = "Bearer"
|
|
475
|
+
# Validate token and scopes...
|
|
476
|
+
"""
|
|
477
|
+
|
|
478
|
+
def __init__(self, scopes: Optional[List[str]] = None):
|
|
479
|
+
self.scopes = scopes or []
|
|
480
|
+
self.scope_str = " ".join(self.scopes)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
# ============================================================================
|
|
484
|
+
# Helper Functions
|
|
485
|
+
# ============================================================================
|
|
486
|
+
|
|
487
|
+
class HTTPException(Exception):
|
|
488
|
+
"""HTTP exception for authentication errors."""
|
|
489
|
+
|
|
490
|
+
def __init__(
|
|
491
|
+
self,
|
|
492
|
+
status_code: int,
|
|
493
|
+
detail: Any = None,
|
|
494
|
+
headers: Optional[Dict[str, str]] = None,
|
|
495
|
+
):
|
|
496
|
+
self.status_code = status_code
|
|
497
|
+
self.detail = detail
|
|
498
|
+
self.headers = headers
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|
502
|
+
"""
|
|
503
|
+
Verify a password against a hash.
|
|
504
|
+
|
|
505
|
+
Note: This is a placeholder. Use a proper password hashing library like:
|
|
506
|
+
- passlib with bcrypt
|
|
507
|
+
- argon2-cffi
|
|
508
|
+
"""
|
|
509
|
+
# TODO: Implement with proper password hashing
|
|
510
|
+
return secrets.compare_digest(plain_password, hashed_password)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def get_password_hash(password: str) -> str:
|
|
514
|
+
"""
|
|
515
|
+
Hash a password.
|
|
516
|
+
|
|
517
|
+
Note: This is a placeholder. Use a proper password hashing library.
|
|
518
|
+
"""
|
|
519
|
+
# TODO: Implement with proper password hashing
|
|
520
|
+
return password # INSECURE - just for demo!
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
# ============================================================================
|
|
524
|
+
# Dependency Injection Helper
|
|
525
|
+
# ============================================================================
|
|
526
|
+
|
|
527
|
+
class Depends:
|
|
528
|
+
"""
|
|
529
|
+
Dependency injection marker (compatible with FastAPI).
|
|
530
|
+
|
|
531
|
+
Usage:
|
|
532
|
+
def get_current_user(token: str = Depends(oauth2_scheme)):
|
|
533
|
+
return decode_token(token)
|
|
534
|
+
|
|
535
|
+
@app.get("/users/me")
|
|
536
|
+
def read_users_me(user = Depends(get_current_user)):
|
|
537
|
+
return user
|
|
538
|
+
"""
|
|
539
|
+
|
|
540
|
+
def __init__(self, dependency: Optional[Callable] = None, *, use_cache: bool = True):
|
|
541
|
+
self.dependency = dependency
|
|
542
|
+
self.use_cache = use_cache
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: turboapi
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.28
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
10
10
|
Classifier: Programming Language :: Rust
|
|
11
11
|
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
12
12
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
13
|
-
Requires-Dist: satya>=0.
|
|
13
|
+
Requires-Dist: satya>=0.4.0
|
|
14
14
|
Requires-Dist: pytest>=7.0.0 ; extra == 'dev'
|
|
15
15
|
Requires-Dist: pytest-asyncio>=0.21.0 ; extra == 'dev'
|
|
16
16
|
Requires-Dist: ruff==0.13.2 ; extra == 'dev'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
turboapi-0.3.28.dist-info/METADATA,sha256=T1NpF_kL7xM6dTysG2jKQPVyJ85m0q2ffvCDpwCMnKs,1458
|
|
2
|
+
turboapi-0.3.28.dist-info/WHEEL,sha256=TJQY77QRLvXq32tEs9ATmwKO6NAOtuKOAw50eyiSmWU,96
|
|
3
|
+
turboapi/__init__.py,sha256=r9Fphtu9ruHFUhSpBMAGxY5en2wvcnsE1nMp2DDRM6w,692
|
|
4
|
+
turboapi/_rust.cp313-win_amd64.pyd,sha256=qJsomUpv-aKgCBiSCpg1Z7xxB3O6lnhDV813m32pK_0,3554304
|
|
5
|
+
turboapi/async_pool.py,sha256=UVm0A-0jIN4V43jY8a5XEU_L0SSyWGMV2bs5FiQGr2M,4489
|
|
6
|
+
turboapi/decorators.py,sha256=jjJrIXZ3y_yJ231ar24hS09OCDtTqmYA7arpIOcr2kk,1788
|
|
7
|
+
turboapi/main_app.py,sha256=mR-x-RPJn96Jtg0a313hU_2UsLQNV_xNXRtpFYWAr30,9188
|
|
8
|
+
turboapi/middleware.py,sha256=iqtklH5_GMICuAmmxMBfaFSNZkR8wHSNbwhNscGe-pA,11200
|
|
9
|
+
turboapi/models.py,sha256=VCU68f9MGtDdFb4crsx2e0SHghICg8zjU8OumfdpZLQ,5363
|
|
10
|
+
turboapi/request_handler.py,sha256=KrN9d3r7bO8LUU68X6cXTtl3a2dCoRqmdWrjDW2V2qQ,8413
|
|
11
|
+
turboapi/routing.py,sha256=iCbty56a2J9qnCtxIHQtYf66ZoKVxgISxwCxYvGmgEs,7746
|
|
12
|
+
turboapi/rust_integration.py,sha256=ycA_i8kxC2Upbu7PAqC2EdjsqRw5AVkYwLvx9aTWBWc,14916
|
|
13
|
+
turboapi/security.py,sha256=-XgwBhiqQZdfU7oKLHi-3xN_UwlKiQxpfSQ6kTA0ko8,17230
|
|
14
|
+
turboapi/server_integration.py,sha256=drUhhTasWgQfyhFiAaHKd987N3mnE0qkMab1ylmqd4c,18340
|
|
15
|
+
turboapi/version_check.py,sha256=z3O1vIJsWmG_DO271ayYWSwaDfgpFnfJzYRYyowKYMc,9625
|
|
16
|
+
turboapi-0.3.28.dist-info/RECORD,,
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
turboapi-0.3.23.dist-info/RECORD
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
turboapi-0.3.23.dist-info/METADATA,sha256=VpVhmCyvdoNHS9gk-eEOkUHBrew-Phst2zTKRamNGH4,1458
|
|
2
|
-
turboapi-0.3.23.dist-info/WHEEL,sha256=tA3jeK73gMv2hhaFxfSe9XAOJHRQ2Ol5rfMGxqHyYEk,96
|
|
3
|
-
turboapi/__init__.py,sha256=r9Fphtu9ruHFUhSpBMAGxY5en2wvcnsE1nMp2DDRM6w,692
|
|
4
|
-
turboapi/__pycache__/__init__.cpython-312.pyc,sha256=WGxT-5DbR5Q-z4xzZmEJQt71dI-ib_lgOs0KB8wJqX8,728
|
|
5
|
-
turboapi/__pycache__/__init__.cpython-313.pyc,sha256=WvwBnRja8h3Oj0Q1NMdIBm_M3fAp25eUD7dM9h0lbeM,675
|
|
6
|
-
turboapi/__pycache__/app.cpython-312.pyc,sha256=GUj7vWFw_NIVJM9rDs76CYY69gtVs8YLkIsGHhjpARM,6465
|
|
7
|
-
turboapi/__pycache__/app.cpython-313.pyc,sha256=oK3djVB2jzdcl8GLsseeMSgy7dPU_fTc_vJG90IO-Gs,5892
|
|
8
|
-
turboapi/__pycache__/decorators.cpython-312.pyc,sha256=s3Y5oY1vaUv8cq0L6YW_GaJLbnBKNfzKhwnnlhn5LPA,2859
|
|
9
|
-
turboapi/__pycache__/decorators.cpython-313.pyc,sha256=xmwwVbH9nJAjEGft1KU1s0_jbyNkwM9j0gQx25NRE4o,2893
|
|
10
|
-
turboapi/__pycache__/main_app.cpython-312.pyc,sha256=80Bgcksf_vl1FGeY981Xh33zzSBQtTfhV6ToLS6l_Ew,11819
|
|
11
|
-
turboapi/__pycache__/main_app.cpython-313.pyc,sha256=LdprlqOyUg3-AIlbNozi0RzQUhqTgeiwqtjv1e6NjuM,12153
|
|
12
|
-
turboapi/__pycache__/middleware.cpython-312.pyc,sha256=4jNKPjYfH1DQaS1810uxmUa_ibJ4rHyCHWWrYRSrpSc,3748
|
|
13
|
-
turboapi/__pycache__/middleware.cpython-313.pyc,sha256=t5ERZ_VIIW7dsnsyCG4P01CYX5XSL3F3cIjPQLMiTng,3925
|
|
14
|
-
turboapi/__pycache__/models.cpython-312.pyc,sha256=D23MQCkMlpP5xPOhMx0kat0UD2LSxxfWZX-Onz6n17E,7223
|
|
15
|
-
turboapi/__pycache__/models.cpython-313.pyc,sha256=yCVUcn71_yTxLaJHtbUXwYifd8nA3nq_9KZt8y_vV6s,7356
|
|
16
|
-
turboapi/__pycache__/routing.cpython-312.pyc,sha256=iYGmnOogsL8XbkGE2bP-sc5egpX8SMkBHbeZYu6Rif0,10264
|
|
17
|
-
turboapi/__pycache__/routing.cpython-313.pyc,sha256=BQW1xl8UXO-pQ1u5FaoLcbQ4qW_V2dUHolHzzZ-kOjM,10554
|
|
18
|
-
turboapi/__pycache__/rust_integration.cpython-312.pyc,sha256=5h-bsxlOcLcWdyYk6Ql6lh4Ql14DqY_lhr88hbBxBTY,15910
|
|
19
|
-
turboapi/__pycache__/rust_integration.cpython-313.pyc,sha256=4vpl3q5yBszAV9y44XxRHcrK-KlbhgkLwzs-Rc3vNYc,15290
|
|
20
|
-
turboapi/__pycache__/server_integration.cpython-313.pyc,sha256=wSqZ72Sh6iyWbIJsdfk8s9oDL9L3DJPJWnz41mDGxCY,20089
|
|
21
|
-
turboapi/__pycache__/version_check.cpython-312.pyc,sha256=xfdKv0_EU8gs3Ot1cDrQFJuuZYpEknwNEYzoTJzWk9g,9180
|
|
22
|
-
turboapi/__pycache__/version_check.cpython-313.pyc,sha256=xgmdzdAt31IIw15NZxhc6JZjDCpw2Kw8DXrWUhNvHqw,10364
|
|
23
|
-
turboapi/_rust.cp313-win_amd64.pyd,sha256=V6i3g1I6zquV8Rfk9SUsmOpuho7Y8WYBCkwfg7VZAhA,3465216
|
|
24
|
-
turboapi/decorators.py,sha256=jjJrIXZ3y_yJ231ar24hS09OCDtTqmYA7arpIOcr2kk,1788
|
|
25
|
-
turboapi/main_app.py,sha256=mR-x-RPJn96Jtg0a313hU_2UsLQNV_xNXRtpFYWAr30,9188
|
|
26
|
-
turboapi/middleware.py,sha256=kDRGblEWopPqROT2O4P4HhB87tGw73qfpLm1svjNs6U,2183
|
|
27
|
-
turboapi/models.py,sha256=VCU68f9MGtDdFb4crsx2e0SHghICg8zjU8OumfdpZLQ,5363
|
|
28
|
-
turboapi/request_handler.py,sha256=lFKXPTjOAFdgKmuQhLV-mwVIZQU6wg-FkSYkASCC_tc,8643
|
|
29
|
-
turboapi/routing.py,sha256=iCbty56a2J9qnCtxIHQtYf66ZoKVxgISxwCxYvGmgEs,7746
|
|
30
|
-
turboapi/rust_integration.py,sha256=6fFhucDHku4r4mKX5dedz6MmPWTx2MxIq9M7H1xiyvo,14859
|
|
31
|
-
turboapi/server_integration.py,sha256=drUhhTasWgQfyhFiAaHKd987N3mnE0qkMab1ylmqd4c,18340
|
|
32
|
-
turboapi/version_check.py,sha256=z3O1vIJsWmG_DO271ayYWSwaDfgpFnfJzYRYyowKYMc,9625
|
|
33
|
-
turboapi-0.3.23.dist-info/RECORD,,
|