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.
@@ -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