turboapi 0.4.12__cp314-cp314t-macosx_10_12_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- turboapi/__init__.py +24 -0
- turboapi/async_limiter.py +86 -0
- turboapi/async_pool.py +141 -0
- turboapi/decorators.py +69 -0
- turboapi/main_app.py +314 -0
- turboapi/middleware.py +342 -0
- turboapi/models.py +148 -0
- turboapi/request_handler.py +227 -0
- turboapi/routing.py +219 -0
- turboapi/rust_integration.py +335 -0
- turboapi/security.py +542 -0
- turboapi/server_integration.py +436 -0
- turboapi/turbonet.cpython-314t-darwin.so +0 -0
- turboapi/version_check.py +268 -0
- turboapi-0.4.12.dist-info/METADATA +31 -0
- turboapi-0.4.12.dist-info/RECORD +17 -0
- turboapi-0.4.12.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enhanced Request Handler with Satya Integration
|
|
3
|
+
Provides FastAPI-compatible automatic JSON body parsing and validation
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import inspect
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any, get_args, get_origin
|
|
9
|
+
|
|
10
|
+
from satya import Model
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RequestBodyParser:
|
|
14
|
+
"""Parse and validate request bodies using Satya models."""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def parse_json_body(body: bytes, handler_signature: inspect.Signature) -> dict[str, Any]:
|
|
18
|
+
"""
|
|
19
|
+
Parse JSON body and extract parameters for handler.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
body: Raw request body bytes
|
|
23
|
+
handler_signature: Signature of the handler function
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Dictionary of parsed parameters ready for handler
|
|
27
|
+
"""
|
|
28
|
+
if not body:
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
json_data = json.loads(body.decode('utf-8'))
|
|
33
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
34
|
+
raise ValueError(f"Invalid JSON body: {e}")
|
|
35
|
+
|
|
36
|
+
parsed_params = {}
|
|
37
|
+
|
|
38
|
+
# Check each parameter in the handler signature
|
|
39
|
+
for param_name, param in handler_signature.parameters.items():
|
|
40
|
+
if param.annotation == inspect.Parameter.empty:
|
|
41
|
+
# No type annotation, try to match by name
|
|
42
|
+
if param_name in json_data:
|
|
43
|
+
parsed_params[param_name] = json_data[param_name]
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
# Check if parameter is a Satya Model
|
|
47
|
+
try:
|
|
48
|
+
is_satya_model = inspect.isclass(param.annotation) and issubclass(param.annotation, Model)
|
|
49
|
+
except Exception:
|
|
50
|
+
is_satya_model = False
|
|
51
|
+
|
|
52
|
+
if is_satya_model:
|
|
53
|
+
# Validate entire JSON body against Satya model
|
|
54
|
+
try:
|
|
55
|
+
validated_model = param.annotation.model_validate(json_data)
|
|
56
|
+
parsed_params[param_name] = validated_model
|
|
57
|
+
except Exception as e:
|
|
58
|
+
raise ValueError(f"Validation error for {param_name}: {e}")
|
|
59
|
+
|
|
60
|
+
# Check if parameter name exists in JSON data
|
|
61
|
+
elif param_name in json_data:
|
|
62
|
+
value = json_data[param_name]
|
|
63
|
+
|
|
64
|
+
# Type conversion for basic types
|
|
65
|
+
if param.annotation in (int, float, str, bool):
|
|
66
|
+
try:
|
|
67
|
+
if param.annotation is bool and isinstance(value, str):
|
|
68
|
+
parsed_params[param_name] = value.lower() in ('true', '1', 'yes', 'on')
|
|
69
|
+
else:
|
|
70
|
+
parsed_params[param_name] = param.annotation(value)
|
|
71
|
+
except (ValueError, TypeError) as e:
|
|
72
|
+
raise ValueError(f"Invalid type for {param_name}: {e}")
|
|
73
|
+
else:
|
|
74
|
+
# Use value as-is for other types (lists, dicts, etc.)
|
|
75
|
+
parsed_params[param_name] = value
|
|
76
|
+
|
|
77
|
+
# Handle default values
|
|
78
|
+
elif param.default != inspect.Parameter.empty:
|
|
79
|
+
parsed_params[param_name] = param.default
|
|
80
|
+
|
|
81
|
+
return parsed_params
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ResponseHandler:
|
|
85
|
+
"""Handle different response formats including FastAPI-style tuples."""
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def normalize_response(result: Any) -> tuple[Any, int]:
|
|
89
|
+
"""
|
|
90
|
+
Normalize handler response to (content, status_code) format.
|
|
91
|
+
|
|
92
|
+
Supports:
|
|
93
|
+
- return {"data": "value"} -> ({"data": "value"}, 200)
|
|
94
|
+
- return {"error": "msg"}, 404 -> ({"error": "msg"}, 404)
|
|
95
|
+
- return "text" -> ("text", 200)
|
|
96
|
+
- return satya_model -> (model.model_dump(), 200)
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
result: Raw result from handler
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Tuple of (content, status_code)
|
|
103
|
+
"""
|
|
104
|
+
# Handle tuple returns: (content, status_code)
|
|
105
|
+
if isinstance(result, tuple):
|
|
106
|
+
if len(result) == 2:
|
|
107
|
+
content, status_code = result
|
|
108
|
+
return content, status_code
|
|
109
|
+
else:
|
|
110
|
+
# Invalid tuple format, treat as regular response
|
|
111
|
+
return result, 200
|
|
112
|
+
|
|
113
|
+
# Handle Satya models
|
|
114
|
+
if isinstance(result, Model):
|
|
115
|
+
return result.model_dump(), 200
|
|
116
|
+
|
|
117
|
+
# Handle dict with status_code key (internal format)
|
|
118
|
+
if isinstance(result, dict) and "status_code" in result:
|
|
119
|
+
status = result.pop("status_code")
|
|
120
|
+
return result, status
|
|
121
|
+
|
|
122
|
+
# Default: treat as 200 OK response
|
|
123
|
+
return result, 200
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def format_json_response(content: Any, status_code: int) -> dict[str, Any]:
|
|
127
|
+
"""
|
|
128
|
+
Format content as JSON response.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
content: Response content
|
|
132
|
+
status_code: HTTP status code
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Dictionary with properly formatted response
|
|
136
|
+
"""
|
|
137
|
+
# Handle Satya models
|
|
138
|
+
if isinstance(content, Model):
|
|
139
|
+
content = content.model_dump()
|
|
140
|
+
|
|
141
|
+
# Ensure content is JSON-serializable
|
|
142
|
+
if not isinstance(content, (dict, list, str, int, float, bool, type(None))):
|
|
143
|
+
content = str(content)
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
"content": content,
|
|
147
|
+
"status_code": status_code,
|
|
148
|
+
"content_type": "application/json"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def create_enhanced_handler(original_handler, route_definition):
|
|
153
|
+
"""
|
|
154
|
+
Create an enhanced handler with automatic body parsing and response normalization.
|
|
155
|
+
|
|
156
|
+
This wrapper:
|
|
157
|
+
1. Parses JSON body automatically using Satya validation
|
|
158
|
+
2. Normalizes responses (supports tuple returns)
|
|
159
|
+
3. Provides better error messages
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
original_handler: The original Python handler function
|
|
163
|
+
route_definition: RouteDefinition with metadata
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Enhanced handler function
|
|
167
|
+
"""
|
|
168
|
+
sig = inspect.signature(original_handler)
|
|
169
|
+
|
|
170
|
+
def enhanced_handler(**kwargs):
|
|
171
|
+
"""Enhanced handler with automatic body parsing."""
|
|
172
|
+
try:
|
|
173
|
+
# If there's a body in kwargs, parse it
|
|
174
|
+
if "body" in kwargs:
|
|
175
|
+
body_data = kwargs["body"]
|
|
176
|
+
|
|
177
|
+
if body_data: # Only parse if body is not empty
|
|
178
|
+
parsed_body = RequestBodyParser.parse_json_body(
|
|
179
|
+
body_data,
|
|
180
|
+
sig
|
|
181
|
+
)
|
|
182
|
+
# Merge parsed body params into kwargs
|
|
183
|
+
kwargs.update(parsed_body)
|
|
184
|
+
|
|
185
|
+
# Remove the raw body to avoid passing it to handler
|
|
186
|
+
kwargs.pop("body", None)
|
|
187
|
+
|
|
188
|
+
# Remove headers if present
|
|
189
|
+
kwargs.pop("headers", None)
|
|
190
|
+
|
|
191
|
+
# Filter kwargs to only pass expected parameters
|
|
192
|
+
filtered_kwargs = {
|
|
193
|
+
k: v for k, v in kwargs.items()
|
|
194
|
+
if k in sig.parameters
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# Call original handler
|
|
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)
|
|
203
|
+
|
|
204
|
+
# Normalize response
|
|
205
|
+
content, status_code = ResponseHandler.normalize_response(result)
|
|
206
|
+
|
|
207
|
+
return ResponseHandler.format_json_response(content, status_code)
|
|
208
|
+
|
|
209
|
+
except ValueError as e:
|
|
210
|
+
# Validation or parsing error (400 Bad Request)
|
|
211
|
+
return ResponseHandler.format_json_response(
|
|
212
|
+
{"error": "Bad Request", "detail": str(e)},
|
|
213
|
+
400
|
|
214
|
+
)
|
|
215
|
+
except Exception as e:
|
|
216
|
+
# Unexpected error (500 Internal Server Error)
|
|
217
|
+
import traceback
|
|
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
|
+
)
|
|
226
|
+
|
|
227
|
+
return enhanced_handler
|
turboapi/routing.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TurboAPI Route Registration System
|
|
3
|
+
FastAPI-compatible decorators with revolutionary performance
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import inspect
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .version_check import CHECK_MARK
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HTTPMethod(Enum):
|
|
17
|
+
"""HTTP methods supported by TurboAPI."""
|
|
18
|
+
GET = "GET"
|
|
19
|
+
POST = "POST"
|
|
20
|
+
PUT = "PUT"
|
|
21
|
+
DELETE = "DELETE"
|
|
22
|
+
PATCH = "PATCH"
|
|
23
|
+
HEAD = "HEAD"
|
|
24
|
+
OPTIONS = "OPTIONS"
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class PathParameter:
|
|
28
|
+
"""Path parameter definition."""
|
|
29
|
+
name: str
|
|
30
|
+
type: type
|
|
31
|
+
default: Any = None
|
|
32
|
+
required: bool = True
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class RouteDefinition:
|
|
36
|
+
"""Complete route definition."""
|
|
37
|
+
path: str
|
|
38
|
+
method: HTTPMethod
|
|
39
|
+
handler: Callable
|
|
40
|
+
path_params: list[PathParameter]
|
|
41
|
+
query_params: dict[str, type]
|
|
42
|
+
request_model: type | None = None
|
|
43
|
+
response_model: type | None = None
|
|
44
|
+
tags: list[str] = None
|
|
45
|
+
summary: str | None = None
|
|
46
|
+
description: str | None = None
|
|
47
|
+
|
|
48
|
+
class RouteRegistry:
|
|
49
|
+
"""Registry for all routes in the application."""
|
|
50
|
+
|
|
51
|
+
def __init__(self):
|
|
52
|
+
self.routes: list[RouteDefinition] = []
|
|
53
|
+
self.path_patterns: dict[str, re.Pattern] = {}
|
|
54
|
+
|
|
55
|
+
def register_route(self, route: RouteDefinition) -> None:
|
|
56
|
+
"""Register a new route."""
|
|
57
|
+
self.routes.append(route)
|
|
58
|
+
|
|
59
|
+
# Compile path pattern for fast matching
|
|
60
|
+
pattern = self._compile_path_pattern(route.path)
|
|
61
|
+
self.path_patterns[route.path] = pattern
|
|
62
|
+
|
|
63
|
+
print(f"{CHECK_MARK} Registered route: {route.method.value} {route.path}")
|
|
64
|
+
|
|
65
|
+
def _compile_path_pattern(self, path: str) -> re.Pattern:
|
|
66
|
+
"""Compile path with parameters into regex pattern."""
|
|
67
|
+
# Convert FastAPI-style {param} to regex groups
|
|
68
|
+
pattern = path
|
|
69
|
+
|
|
70
|
+
# Find all path parameters
|
|
71
|
+
param_matches = re.findall(r'\{([^}]+)\}', path)
|
|
72
|
+
|
|
73
|
+
for param in param_matches:
|
|
74
|
+
# Replace {param} with named regex group
|
|
75
|
+
pattern = pattern.replace(f'{{{param}}}', f'(?P<{param}>[^/]+)')
|
|
76
|
+
|
|
77
|
+
# Ensure exact match
|
|
78
|
+
pattern = f'^{pattern}$'
|
|
79
|
+
|
|
80
|
+
return re.compile(pattern)
|
|
81
|
+
|
|
82
|
+
def match_route(self, method: str, path: str) -> tuple | None:
|
|
83
|
+
"""Match incoming request to registered route."""
|
|
84
|
+
for route in self.routes:
|
|
85
|
+
if route.method.value != method:
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
pattern = self.path_patterns.get(route.path)
|
|
89
|
+
if not pattern:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
match = pattern.match(path)
|
|
93
|
+
if match:
|
|
94
|
+
# Extract path parameters
|
|
95
|
+
path_params = match.groupdict()
|
|
96
|
+
return route, path_params
|
|
97
|
+
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def get_routes(self) -> list[RouteDefinition]:
|
|
101
|
+
"""Get all registered routes."""
|
|
102
|
+
return self.routes.copy()
|
|
103
|
+
|
|
104
|
+
class Router:
|
|
105
|
+
"""FastAPI-compatible router with decorators."""
|
|
106
|
+
|
|
107
|
+
def __init__(self, prefix: str = "", tags: list[str] = None):
|
|
108
|
+
self.prefix = prefix
|
|
109
|
+
self.tags = tags or []
|
|
110
|
+
self.registry = RouteRegistry()
|
|
111
|
+
|
|
112
|
+
def _create_route_decorator(self, method: HTTPMethod):
|
|
113
|
+
"""Create a route decorator for the given HTTP method."""
|
|
114
|
+
def decorator(
|
|
115
|
+
path: str,
|
|
116
|
+
*,
|
|
117
|
+
response_model: type | None = None,
|
|
118
|
+
tags: list[str] = None,
|
|
119
|
+
summary: str | None = None,
|
|
120
|
+
description: str | None = None,
|
|
121
|
+
**kwargs
|
|
122
|
+
):
|
|
123
|
+
def wrapper(func: Callable) -> Callable:
|
|
124
|
+
# Analyze function signature
|
|
125
|
+
sig = inspect.signature(func)
|
|
126
|
+
path_params = []
|
|
127
|
+
query_params = {}
|
|
128
|
+
request_model = None
|
|
129
|
+
|
|
130
|
+
for param_name, param in sig.parameters.items():
|
|
131
|
+
if param_name in path:
|
|
132
|
+
# Path parameter
|
|
133
|
+
path_param = PathParameter(
|
|
134
|
+
name=param_name,
|
|
135
|
+
type=param.annotation if param.annotation != inspect.Parameter.empty else str,
|
|
136
|
+
default=param.default if param.default != inspect.Parameter.empty else None,
|
|
137
|
+
required=param.default == inspect.Parameter.empty
|
|
138
|
+
)
|
|
139
|
+
path_params.append(path_param)
|
|
140
|
+
elif param.annotation != inspect.Parameter.empty:
|
|
141
|
+
# Check if it's a request model (class type)
|
|
142
|
+
if inspect.isclass(param.annotation):
|
|
143
|
+
request_model = param.annotation
|
|
144
|
+
else:
|
|
145
|
+
# Query parameter
|
|
146
|
+
query_params[param_name] = param.annotation
|
|
147
|
+
|
|
148
|
+
# Create route definition
|
|
149
|
+
full_path = self.prefix + path
|
|
150
|
+
route = RouteDefinition(
|
|
151
|
+
path=full_path,
|
|
152
|
+
method=method,
|
|
153
|
+
handler=func,
|
|
154
|
+
path_params=path_params,
|
|
155
|
+
query_params=query_params,
|
|
156
|
+
request_model=request_model,
|
|
157
|
+
response_model=response_model,
|
|
158
|
+
tags=(tags or []) + self.tags,
|
|
159
|
+
summary=summary,
|
|
160
|
+
description=description
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Register the route
|
|
164
|
+
self.registry.register_route(route)
|
|
165
|
+
|
|
166
|
+
# Return the original function (for direct calling)
|
|
167
|
+
return func
|
|
168
|
+
|
|
169
|
+
return wrapper
|
|
170
|
+
return decorator
|
|
171
|
+
|
|
172
|
+
def get(self, path: str, **kwargs):
|
|
173
|
+
"""GET route decorator."""
|
|
174
|
+
return self._create_route_decorator(HTTPMethod.GET)(path, **kwargs)
|
|
175
|
+
|
|
176
|
+
def post(self, path: str, **kwargs):
|
|
177
|
+
"""POST route decorator."""
|
|
178
|
+
return self._create_route_decorator(HTTPMethod.POST)(path, **kwargs)
|
|
179
|
+
|
|
180
|
+
def put(self, path: str, **kwargs):
|
|
181
|
+
"""PUT route decorator."""
|
|
182
|
+
return self._create_route_decorator(HTTPMethod.PUT)(path, **kwargs)
|
|
183
|
+
|
|
184
|
+
def delete(self, path: str, **kwargs):
|
|
185
|
+
"""DELETE route decorator."""
|
|
186
|
+
return self._create_route_decorator(HTTPMethod.DELETE)(path, **kwargs)
|
|
187
|
+
|
|
188
|
+
def patch(self, path: str, **kwargs):
|
|
189
|
+
"""PATCH route decorator."""
|
|
190
|
+
return self._create_route_decorator(HTTPMethod.PATCH)(path, **kwargs)
|
|
191
|
+
|
|
192
|
+
def head(self, path: str, **kwargs):
|
|
193
|
+
"""HEAD route decorator."""
|
|
194
|
+
return self._create_route_decorator(HTTPMethod.HEAD)(path, **kwargs)
|
|
195
|
+
|
|
196
|
+
def options(self, path: str, **kwargs):
|
|
197
|
+
"""OPTIONS route decorator."""
|
|
198
|
+
return self._create_route_decorator(HTTPMethod.OPTIONS)(path, **kwargs)
|
|
199
|
+
|
|
200
|
+
def include_router(self, router: 'Router', prefix: str = "", tags: list[str] = None):
|
|
201
|
+
"""Include another router's routes."""
|
|
202
|
+
for route in router.registry.get_routes():
|
|
203
|
+
# Create new route with updated prefix and tags
|
|
204
|
+
new_route = RouteDefinition(
|
|
205
|
+
path=prefix + route.path,
|
|
206
|
+
method=route.method,
|
|
207
|
+
handler=route.handler,
|
|
208
|
+
path_params=route.path_params,
|
|
209
|
+
query_params=route.query_params,
|
|
210
|
+
request_model=route.request_model,
|
|
211
|
+
response_model=route.response_model,
|
|
212
|
+
tags=(tags or []) + (route.tags or []),
|
|
213
|
+
summary=route.summary,
|
|
214
|
+
description=route.description
|
|
215
|
+
)
|
|
216
|
+
self.registry.register_route(new_route)
|
|
217
|
+
|
|
218
|
+
# Global router instance for the main app
|
|
219
|
+
APIRouter = Router
|