turboapi 0.4.12__cp314-cp314-win_amd64.whl → 0.4.15__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.
@@ -1,15 +1,107 @@
1
1
  """
2
2
  Enhanced Request Handler with Satya Integration
3
3
  Provides FastAPI-compatible automatic JSON body parsing and validation
4
+ Supports query parameters, path parameters, headers, and request body
4
5
  """
5
6
 
6
7
  import inspect
7
8
  import json
9
+ import urllib.parse
8
10
  from typing import Any, get_args, get_origin
9
11
 
10
12
  from satya import Model
11
13
 
12
14
 
15
+ class QueryParamParser:
16
+ """Parse query parameters from query string."""
17
+
18
+ @staticmethod
19
+ def parse_query_params(query_string: str) -> dict[str, Any]:
20
+ """
21
+ Parse query string into dict of parameters.
22
+ Supports multiple values for same key (returns list).
23
+
24
+ Args:
25
+ query_string: URL query string (e.g., "q=test&limit=10")
26
+
27
+ Returns:
28
+ Dictionary of parsed query parameters
29
+ """
30
+ if not query_string:
31
+ return {}
32
+
33
+ params = {}
34
+ parsed = urllib.parse.parse_qs(query_string, keep_blank_values=True)
35
+
36
+ for key, values in parsed.items():
37
+ # If only one value, return as string; otherwise return as list
38
+ if len(values) == 1:
39
+ params[key] = values[0]
40
+ else:
41
+ params[key] = values
42
+
43
+ return params
44
+
45
+
46
+ class PathParamParser:
47
+ """Parse path parameters from URL path."""
48
+
49
+ @staticmethod
50
+ def extract_path_params(route_pattern: str, actual_path: str) -> dict[str, str]:
51
+ """
52
+ Extract path parameters from actual path using route pattern.
53
+
54
+ Args:
55
+ route_pattern: Route pattern with {param} placeholders (e.g., "/users/{user_id}")
56
+ actual_path: Actual request path (e.g., "/users/123")
57
+
58
+ Returns:
59
+ Dictionary of extracted path parameters
60
+ """
61
+ import re
62
+
63
+ # Convert route pattern to regex
64
+ # Replace {param} with named capture groups
65
+ pattern = re.sub(r'\{(\w+)\}', r'(?P<\1>[^/]+)', route_pattern)
66
+ pattern = f'^{pattern}$'
67
+
68
+ match = re.match(pattern, actual_path)
69
+ if match:
70
+ return match.groupdict()
71
+
72
+ return {}
73
+
74
+
75
+ class HeaderParser:
76
+ """Parse and extract headers from request."""
77
+
78
+ @staticmethod
79
+ def parse_headers(headers_dict: dict[str, str], handler_signature: inspect.Signature) -> dict[str, Any]:
80
+ """
81
+ Parse headers and extract parameters needed by handler.
82
+
83
+ Args:
84
+ headers_dict: Dictionary of request headers
85
+ handler_signature: Signature of the handler function
86
+
87
+ Returns:
88
+ Dictionary of parsed header parameters
89
+ """
90
+ parsed_headers = {}
91
+
92
+ # Check each parameter in handler signature
93
+ for param_name, param in handler_signature.parameters.items():
94
+ # Check if parameter name matches a header (case-insensitive)
95
+ header_key = param_name.replace('_', '-').lower()
96
+
97
+ for header_name, header_value in headers_dict.items():
98
+ if header_name.lower() == header_key:
99
+ parsed_headers[param_name] = header_value
100
+ break
101
+
102
+ return parsed_headers
103
+
104
+
13
105
  class RequestBodyParser:
14
106
  """Parse and validate request bodies using Satya models."""
15
107
 
@@ -18,6 +110,11 @@ class RequestBodyParser:
18
110
  """
19
111
  Parse JSON body and extract parameters for handler.
20
112
 
113
+ Supports multiple patterns:
114
+ 1. Single parameter (dict/list/Model) - receives entire body
115
+ 2. Multiple parameters - extracts fields from JSON
116
+ 3. Satya Model - validates entire body
117
+
21
118
  Args:
22
119
  body: Raw request body bytes
23
120
  handler_signature: Signature of the handler function
@@ -34,9 +131,42 @@ class RequestBodyParser:
34
131
  raise ValueError(f"Invalid JSON body: {e}")
35
132
 
36
133
  parsed_params = {}
134
+ params_list = list(handler_signature.parameters.items())
37
135
 
38
- # Check each parameter in the handler signature
39
- for param_name, param in handler_signature.parameters.items():
136
+ # PATTERN 1: Single parameter that should receive entire body
137
+ # Examples: handler(data: dict), handler(items: list), handler(request: Model)
138
+ if len(params_list) == 1:
139
+ param_name, param = params_list[0]
140
+
141
+ # Check if parameter is a Satya Model
142
+ try:
143
+ is_satya_model = inspect.isclass(param.annotation) and issubclass(param.annotation, Model)
144
+ except Exception:
145
+ is_satya_model = False
146
+
147
+ if is_satya_model:
148
+ # Validate entire JSON body against Satya model
149
+ try:
150
+ validated_model = param.annotation.model_validate(json_data)
151
+ parsed_params[param_name] = validated_model
152
+ return parsed_params
153
+ except Exception as e:
154
+ raise ValueError(f"Validation error for {param_name}: {e}")
155
+
156
+ # If annotated as dict or list, pass entire body
157
+ elif param.annotation in (dict, list) or param.annotation == inspect.Parameter.empty:
158
+ parsed_params[param_name] = json_data
159
+ return parsed_params
160
+
161
+ # Check for typing.Dict, typing.List, etc.
162
+ origin = get_origin(param.annotation)
163
+ if origin in (dict, list):
164
+ parsed_params[param_name] = json_data
165
+ return parsed_params
166
+
167
+ # PATTERN 2: Multiple parameters - extract individual fields
168
+ # Example: handler(name: str, age: int, email: str)
169
+ for param_name, param in params_list:
40
170
  if param.annotation == inspect.Parameter.empty:
41
171
  # No type annotation, try to match by name
42
172
  if param_name in json_data:
@@ -138,9 +268,21 @@ class ResponseHandler:
138
268
  if isinstance(content, Model):
139
269
  content = content.model_dump()
140
270
 
141
- # Ensure content is JSON-serializable
142
- if not isinstance(content, (dict, list, str, int, float, bool, type(None))):
143
- content = str(content)
271
+ # Recursively convert any nested Satya models in dicts/lists
272
+ def make_serializable(obj):
273
+ if isinstance(obj, Model):
274
+ return obj.model_dump()
275
+ elif isinstance(obj, dict):
276
+ return {k: make_serializable(v) for k, v in obj.items()}
277
+ elif isinstance(obj, (list, tuple)):
278
+ return [make_serializable(item) for item in obj]
279
+ elif isinstance(obj, (str, int, float, bool, type(None))):
280
+ return obj
281
+ else:
282
+ # Try to convert to string for unknown types
283
+ return str(obj)
284
+
285
+ content = make_serializable(content)
144
286
 
145
287
  return {
146
288
  "content": content,
@@ -157,71 +299,164 @@ def create_enhanced_handler(original_handler, route_definition):
157
299
  1. Parses JSON body automatically using Satya validation
158
300
  2. Normalizes responses (supports tuple returns)
159
301
  3. Provides better error messages
302
+ 4. Properly handles both sync and async handlers
160
303
 
161
304
  Args:
162
305
  original_handler: The original Python handler function
163
306
  route_definition: RouteDefinition with metadata
164
307
 
165
308
  Returns:
166
- Enhanced handler function
309
+ Enhanced handler function (async if original is async, sync otherwise)
167
310
  """
168
311
  sig = inspect.signature(original_handler)
312
+ is_async = inspect.iscoroutinefunction(original_handler)
169
313
 
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
- )
314
+ if is_async:
315
+ # Create async enhanced handler for async original handlers
316
+ async def enhanced_handler(**kwargs):
317
+ """Enhanced handler with automatic parsing of body, query params, path params, and headers."""
318
+ try:
319
+ parsed_params = {}
320
+
321
+ # 1. Parse query parameters
322
+ if "query_string" in kwargs:
323
+ query_string = kwargs.get("query_string", "")
324
+ if query_string:
325
+ query_params = QueryParamParser.parse_query_params(query_string)
326
+ parsed_params.update(query_params)
327
+
328
+ # 2. Parse path parameters (if route pattern is available)
329
+ if "path" in kwargs and hasattr(route_definition, 'path'):
330
+ actual_path = kwargs.get("path", "")
331
+ route_pattern = route_definition.path
332
+ if actual_path and route_pattern:
333
+ path_params = PathParamParser.extract_path_params(route_pattern, actual_path)
334
+ parsed_params.update(path_params)
335
+
336
+ # 3. Parse headers
337
+ if "headers" in kwargs:
338
+ headers_dict = kwargs.get("headers", {})
339
+ if headers_dict:
340
+ header_params = HeaderParser.parse_headers(headers_dict, sig)
341
+ parsed_params.update(header_params)
342
+
343
+ # 4. Parse request body (JSON)
344
+ if "body" in kwargs:
345
+ body_data = kwargs["body"]
346
+
347
+ if body_data: # Only parse if body is not empty
348
+ parsed_body = RequestBodyParser.parse_json_body(
349
+ body_data,
350
+ sig
351
+ )
352
+ # Merge parsed body params (body params take precedence)
353
+ parsed_params.update(parsed_body)
354
+
355
+ # Filter to only pass expected parameters
356
+ filtered_kwargs = {
357
+ k: v for k, v in parsed_params.items()
358
+ if k in sig.parameters
359
+ }
360
+
361
+ # Call original async handler and await it
362
+ result = await original_handler(**filtered_kwargs)
363
+
364
+ # Normalize response
365
+ content, status_code = ResponseHandler.normalize_response(result)
366
+
367
+ return ResponseHandler.format_json_response(content, status_code)
368
+
369
+ except ValueError as e:
370
+ # Validation or parsing error (400 Bad Request)
371
+ return ResponseHandler.format_json_response(
372
+ {"error": "Bad Request", "detail": str(e)},
373
+ 400
374
+ )
375
+ except Exception as e:
376
+ # Unexpected error (500 Internal Server Error)
377
+ import traceback
378
+ return ResponseHandler.format_json_response(
379
+ {
380
+ "error": "Internal Server Error",
381
+ "detail": str(e),
382
+ "traceback": traceback.format_exc()
383
+ },
384
+ 500
385
+ )
386
+
387
+ return enhanced_handler
226
388
 
227
- return enhanced_handler
389
+ else:
390
+ # Create sync enhanced handler for sync original handlers
391
+ def enhanced_handler(**kwargs):
392
+ """Enhanced handler with automatic parsing of body, query params, path params, and headers."""
393
+ try:
394
+ parsed_params = {}
395
+
396
+ # 1. Parse query parameters
397
+ if "query_string" in kwargs:
398
+ query_string = kwargs.get("query_string", "")
399
+ if query_string:
400
+ query_params = QueryParamParser.parse_query_params(query_string)
401
+ parsed_params.update(query_params)
402
+
403
+ # 2. Parse path parameters (if route pattern is available)
404
+ if "path" in kwargs and hasattr(route_definition, 'path'):
405
+ actual_path = kwargs.get("path", "")
406
+ route_pattern = route_definition.path
407
+ if actual_path and route_pattern:
408
+ path_params = PathParamParser.extract_path_params(route_pattern, actual_path)
409
+ parsed_params.update(path_params)
410
+
411
+ # 3. Parse headers
412
+ if "headers" in kwargs:
413
+ headers_dict = kwargs.get("headers", {})
414
+ if headers_dict:
415
+ header_params = HeaderParser.parse_headers(headers_dict, sig)
416
+ parsed_params.update(header_params)
417
+
418
+ # 4. Parse request body (JSON)
419
+ if "body" in kwargs:
420
+ body_data = kwargs["body"]
421
+
422
+ if body_data: # Only parse if body is not empty
423
+ parsed_body = RequestBodyParser.parse_json_body(
424
+ body_data,
425
+ sig
426
+ )
427
+ # Merge parsed body params (body params take precedence)
428
+ parsed_params.update(parsed_body)
429
+
430
+ # Filter to only pass expected parameters
431
+ filtered_kwargs = {
432
+ k: v for k, v in parsed_params.items()
433
+ if k in sig.parameters
434
+ }
435
+
436
+ # Call original sync handler
437
+ result = original_handler(**filtered_kwargs)
438
+
439
+ # Normalize response
440
+ content, status_code = ResponseHandler.normalize_response(result)
441
+
442
+ return ResponseHandler.format_json_response(content, status_code)
443
+
444
+ except ValueError as e:
445
+ # Validation or parsing error (400 Bad Request)
446
+ return ResponseHandler.format_json_response(
447
+ {"error": "Bad Request", "detail": str(e)},
448
+ 400
449
+ )
450
+ except Exception as e:
451
+ # Unexpected error (500 Internal Server Error)
452
+ import traceback
453
+ return ResponseHandler.format_json_response(
454
+ {
455
+ "error": "Internal Server Error",
456
+ "detail": str(e),
457
+ "traceback": traceback.format_exc()
458
+ },
459
+ 500
460
+ )
461
+
462
+ return enhanced_handler
@@ -128,91 +128,14 @@ class RustIntegratedTurboAPI(TurboAPI):
128
128
  # Create enhanced handler with automatic body parsing
129
129
  enhanced_handler = create_enhanced_handler(route.handler, route)
130
130
 
131
- # Create Rust-compatible handler wrapper
132
- def create_rust_handler(python_handler, route_def):
133
- def rust_handler(rust_request):
134
- """Rust-callable handler that calls Python function with automatic body parsing."""
135
- try:
136
- # Extract request data from Rust
137
- path = rust_request.path
138
- query_string = rust_request.query_string
139
-
140
- # Get headers - try method call first, then attribute
141
- try:
142
- headers = rust_request.get_headers() if callable(getattr(rust_request, 'get_headers', None)) else {}
143
- except:
144
- headers = getattr(rust_request, 'headers', {})
145
-
146
- # Get body - Rust sets it as 'body' attribute (bytes)
147
- body = getattr(rust_request, 'body', b'')
148
-
149
- # Also try get_body if it's set
150
- if not body:
151
- get_body_attr = getattr(rust_request, 'get_body', None)
152
- if get_body_attr is not None:
153
- if callable(get_body_attr):
154
- body = get_body_attr()
155
- else:
156
- body = get_body_attr
157
-
158
- # Parse query parameters
159
- query_params = {}
160
- if query_string:
161
- # Simple query string parsing
162
- for param in query_string.split('&'):
163
- if '=' in param:
164
- key, value = param.split('=', 1)
165
- query_params[key] = value
166
-
167
- # Parse path parameters
168
- path_params = self._extract_path_params(route_def.path, path)
169
-
170
- # Prepare arguments for enhanced handler
171
- call_args = {}
172
-
173
- # Add path parameters
174
- call_args.update(path_params)
175
-
176
- # Add query parameters
177
- call_args.update(query_params)
178
-
179
- # Always add body and headers for enhanced handler
180
- call_args['body'] = body if body else b''
181
- call_args['headers'] = headers
182
-
183
- # Call enhanced handler (handles parsing, validation, response normalization)
184
- result = python_handler(**call_args)
185
-
186
- # Enhanced handler returns normalized format
187
- # {"content": ..., "status_code": ..., "content_type": ...}
188
- # But Rust expects a plain dict that it will JSON serialize
189
- # So just return the content directly
190
- if isinstance(result, dict) and 'content' in result and 'status_code' in result:
191
- # Return just the content - Rust will handle status codes later
192
- # For now, just return the content as a dict
193
- return result['content']
194
-
195
- # Fallback for plain dict responses
196
- return result
197
-
198
- except Exception as e:
199
- # Return 500 error as plain dict (Rust will serialize it)
200
- import traceback
201
- return {
202
- "error": "Internal Server Error",
203
- "detail": str(e),
204
- "traceback": traceback.format_exc()
205
- }
206
-
207
- return rust_handler # noqa: B023
208
-
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
131
+ # For now, just register the original handler
132
+ # TODO: Implement request data passing from Rust to enable enhanced handler
133
+ # The Rust server currently calls handlers with call0() (no arguments)
134
+ # We need to modify the Rust server to pass request data
212
135
  self.rust_server.add_route(
213
136
  route.method.value,
214
137
  route.path,
215
- route.handler # Pass original handler, not wrapper!
138
+ enhanced_handler # Register enhanced handler directly
216
139
  )
217
140
 
218
141
  print(f"{CHECK_MARK} Registered {route.method.value} {route.path} with Rust server")
Binary file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: turboapi
3
- Version: 0.4.12
3
+ Version: 0.4.15
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -1,5 +1,5 @@
1
- turboapi-0.4.12.dist-info/METADATA,sha256=IBt2GTnjmkVpSOn-PzMpMzXtwdB4jcqesqC2Bvj64_E,1482
2
- turboapi-0.4.12.dist-info/WHEEL,sha256=tZ3VAZ5HuUzziFCJ2lDsDJnJO-xy4omAQIa7TJCFCZk,96
1
+ turboapi-0.4.15.dist-info/METADATA,sha256=4_skH-GqtS3RKpgwaT13pcN7nNKWCH_6TK84i0A6gQw,1482
2
+ turboapi-0.4.15.dist-info/WHEEL,sha256=tZ3VAZ5HuUzziFCJ2lDsDJnJO-xy4omAQIa7TJCFCZk,96
3
3
  turboapi/__init__.py,sha256=r9Fphtu9ruHFUhSpBMAGxY5en2wvcnsE1nMp2DDRM6w,692
4
4
  turboapi/async_limiter.py,sha256=x2qkloPbg2YelDNUXKya2BwBTq5zVxDHxuaQspIgYBg,2416
5
5
  turboapi/async_pool.py,sha256=UVm0A-0jIN4V43jY8a5XEU_L0SSyWGMV2bs5FiQGr2M,4489
@@ -7,11 +7,11 @@ turboapi/decorators.py,sha256=jjJrIXZ3y_yJ231ar24hS09OCDtTqmYA7arpIOcr2kk,1788
7
7
  turboapi/main_app.py,sha256=_rH5xUahFvyqk8Y9O4rs7v5m1q4_AbkBHitg04KL6O4,11678
8
8
  turboapi/middleware.py,sha256=iqtklH5_GMICuAmmxMBfaFSNZkR8wHSNbwhNscGe-pA,11200
9
9
  turboapi/models.py,sha256=VCU68f9MGtDdFb4crsx2e0SHghICg8zjU8OumfdpZLQ,5363
10
- turboapi/request_handler.py,sha256=KrN9d3r7bO8LUU68X6cXTtl3a2dCoRqmdWrjDW2V2qQ,8413
10
+ turboapi/request_handler.py,sha256=ymAuJushPEyUUMpyolTp70cn3mvjSF3Et5hPtdMoZYk,18642
11
11
  turboapi/routing.py,sha256=iCbty56a2J9qnCtxIHQtYf66ZoKVxgISxwCxYvGmgEs,7746
12
- turboapi/rust_integration.py,sha256=AsdB14odDYHFcMNlKeef0Dh8uqE0lfEqcC4MjSld0tM,14930
12
+ turboapi/rust_integration.py,sha256=d1xqC8cX8kehDlRuDQhVbMPw-cQysmI2ZR-j29JD8GA,10676
13
13
  turboapi/security.py,sha256=-XgwBhiqQZdfU7oKLHi-3xN_UwlKiQxpfSQ6kTA0ko8,17230
14
14
  turboapi/server_integration.py,sha256=drUhhTasWgQfyhFiAaHKd987N3mnE0qkMab1ylmqd4c,18340
15
- turboapi/turbonet.cp314-win_amd64.pyd,sha256=a4_Rd0h6Mvi4S2m0bw9-8q1fcagh7ptwb1Z5nxdvn00,3664384
15
+ turboapi/turbonet.cp314-win_amd64.pyd,sha256=GF_oeKq8Lm8dU_LJpqvq51Jo5n_uWzuPdp5GB8f-DrY,3681280
16
16
  turboapi/version_check.py,sha256=z3O1vIJsWmG_DO271ayYWSwaDfgpFnfJzYRYyowKYMc,9625
17
- turboapi-0.4.12.dist-info/RECORD,,
17
+ turboapi-0.4.15.dist-info/RECORD,,