turboapi 0.3.23__cp314-cp314-win_amd64.whl → 0.3.28__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.
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
- """CORS 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
+ """
29
54
 
30
55
  def __init__(
31
56
  self,
32
- allow_origins: list = None,
33
- allow_methods: list = None,
34
- allow_headers: list = None,
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
- response.set_header("Access-Control-Allow-Origin", ",".join(self.allow_origins))
45
- response.set_header("Access-Control-Allow-Methods", ",".join(self.allow_methods))
46
- response.set_header("Access-Control-Allow-Headers", ",".join(self.allow_headers))
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
- """Request logging middleware."""
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
- print(f"[RESPONSE] {request.method} {request.path} -> {response.status_code}")
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)
@@ -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
- # v0.3.21: Async handlers are now supported via Rust's tokio runtime!
198
- # The Rust layer (server.rs) will detect coroutines and await them properly
199
- # using pyo3-async-runtimes, giving us native async performance
200
- result = original_handler(**filtered_kwargs)
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
- # Sync result - normalize and return as JSON string
204
+ # Normalize response
209
205
  content, status_code = ResponseHandler.normalize_response(result)
210
206
 
211
- # Return JSON string directly for Rust to use
212
- import json
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
- import json
217
- return json.dumps({"error": "Bad Request", "detail": str(e)})
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
- import json
222
- return json.dumps({
223
- "error": "Internal Server Error",
224
- "detail": str(e),
225
- "traceback": traceback.format_exc()
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
@@ -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
- from turboapi import _rust as turbonet
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
- # Create and register the handler
209
- handler_func = create_rust_handler(enhanced_handler, route)
210
- rust_handler = handler_func
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
- rust_handler
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.23
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.3.7
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=tZ3VAZ5HuUzziFCJ2lDsDJnJO-xy4omAQIa7TJCFCZk,96
3
+ turboapi/__init__.py,sha256=r9Fphtu9ruHFUhSpBMAGxY5en2wvcnsE1nMp2DDRM6w,692
4
+ turboapi/_rust.cp314-win_amd64.pyd,sha256=MTsJQVRiTHwFalC_BI6K6l9dIqre6nYJiix7Lk3drBE,3523072
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: maturin (1.9.5)
2
+ Generator: maturin (1.9.6)
3
3
  Root-Is-Purelib: false
4
4
  Tag: cp314-cp314-win_amd64
Binary file
Binary file
Binary file
Binary file
@@ -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=EiVjE0x0W7bvcwYW2YLFujJOVfmyJVkIkPLDNmxDbbI,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.cp314-win_amd64.pyd,sha256=rZvjeHLthKAGBev1_AO1IWZwktRPub-UMD0CzVjyrYY,3444736
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,,