turboapi 0.5.2__cp313-cp313-win_amd64.whl → 0.5.22__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -229,9 +229,12 @@ class RequestBodyParser:
229
229
  """
230
230
  if not body:
231
231
  return {}
232
-
232
+
233
233
  try:
234
- json_data = json.loads(body.decode('utf-8'))
234
+ # CRITICAL: Make a defensive copy immediately using bytearray to force real copy
235
+ # Free-threaded Python with Metal/MLX can have concurrent memory access issues
236
+ body_copy = bytes(bytearray(body))
237
+ json_data = json.loads(body_copy.decode('utf-8'))
235
238
  except (json.JSONDecodeError, UnicodeDecodeError) as e:
236
239
  raise ValueError(f"Invalid JSON body: {e}")
237
240
 
@@ -316,6 +319,25 @@ class RequestBodyParser:
316
319
  return parsed_params
317
320
 
318
321
 
322
+ def _is_binary_content_type(content_type: str) -> bool:
323
+ """Check if the content type indicates binary data."""
324
+ if not content_type:
325
+ return False
326
+ ct_lower = content_type.lower()
327
+ # Binary content types that should not be JSON serialized
328
+ binary_prefixes = (
329
+ 'audio/',
330
+ 'video/',
331
+ 'image/',
332
+ 'application/octet-stream',
333
+ 'application/pdf',
334
+ 'application/zip',
335
+ 'application/gzip',
336
+ 'application/x-tar',
337
+ )
338
+ return ct_lower.startswith(binary_prefixes)
339
+
340
+
319
341
  class ResponseHandler:
320
342
  """Handle different response formats including FastAPI-style tuples."""
321
343
 
@@ -336,22 +358,36 @@ class ResponseHandler:
336
358
  result: Raw result from handler
337
359
 
338
360
  Returns:
339
- Tuple of (content, status_code)
361
+ Tuple of (content, status_code) or (content, status_code, content_type)
340
362
  """
341
363
  # Handle Response objects (JSONResponse, HTMLResponse, etc.)
342
364
  from turboapi.responses import Response
343
365
  if isinstance(result, Response):
344
366
  # Extract content from Response object
345
367
  body = result.body
368
+ content_type = result.media_type
369
+
370
+ # For binary content types, return raw bytes
371
+ if content_type and _is_binary_content_type(content_type):
372
+ # Return raw bytes with content_type for binary responses
373
+ return body, result.status_code, content_type
374
+
346
375
  if isinstance(body, bytes):
347
376
  # Try to decode as JSON for JSONResponse
348
377
  try:
349
378
  import json
350
379
  body = json.loads(body.decode('utf-8'))
351
- except (json.JSONDecodeError, UnicodeDecodeError):
352
- # Keep as string for HTML/Text responses
353
- body = body.decode('utf-8')
354
- return body, result.status_code
380
+ except json.JSONDecodeError:
381
+ # Not JSON, try as plain text
382
+ try:
383
+ body = body.decode('utf-8')
384
+ except UnicodeDecodeError:
385
+ # Binary data - return with content_type
386
+ return body, result.status_code, content_type
387
+ except UnicodeDecodeError:
388
+ # Binary data - return with content_type
389
+ return body, result.status_code, content_type
390
+ return body, result.status_code, content_type
355
391
 
356
392
  # Handle tuple returns: (content, status_code)
357
393
  if isinstance(result, tuple):
@@ -375,25 +411,42 @@ class ResponseHandler:
375
411
  return result, 200
376
412
 
377
413
  @staticmethod
378
- def format_json_response(content: Any, status_code: int) -> dict[str, Any]:
414
+ def format_response(content: Any, status_code: int, content_type: str | None = None) -> dict[str, Any]:
379
415
  """
380
- Format content as JSON response.
381
-
416
+ Format content as response. Handles both JSON and binary responses.
417
+
382
418
  Args:
383
- content: Response content
419
+ content: Response content (can be dict, str, bytes, etc.)
384
420
  status_code: HTTP status code
385
-
421
+ content_type: Optional content type (for binary responses)
422
+
386
423
  Returns:
387
424
  Dictionary with properly formatted response
388
425
  """
426
+ # For binary content (bytes with binary content_type), return directly
427
+ if isinstance(content, bytes) and content_type and _is_binary_content_type(content_type):
428
+ # Return bytes directly - Rust will handle as raw binary
429
+ return {
430
+ "content": content, # Keep as bytes for Rust to extract
431
+ "status_code": status_code,
432
+ "content_type": content_type
433
+ }
434
+
389
435
  # Handle Satya models
390
436
  if isinstance(content, Model):
391
437
  content = content.model_dump()
392
-
438
+
393
439
  # Recursively convert any nested Satya models in dicts/lists
394
440
  def make_serializable(obj):
395
441
  if isinstance(obj, Model):
396
442
  return obj.model_dump()
443
+ elif isinstance(obj, bytes):
444
+ # Non-binary bytes - try to decode as UTF-8, otherwise base64 encode
445
+ try:
446
+ return obj.decode('utf-8')
447
+ except UnicodeDecodeError:
448
+ import base64
449
+ return base64.b64encode(obj).decode('ascii')
397
450
  elif isinstance(obj, dict):
398
451
  return {k: make_serializable(v) for k, v in obj.items()}
399
452
  elif isinstance(obj, (list, tuple)):
@@ -403,15 +456,20 @@ class ResponseHandler:
403
456
  else:
404
457
  # Try to convert to string for unknown types
405
458
  return str(obj)
406
-
459
+
407
460
  content = make_serializable(content)
408
-
461
+
409
462
  return {
410
463
  "content": content,
411
464
  "status_code": status_code,
412
- "content_type": "application/json"
465
+ "content_type": content_type or "application/json"
413
466
  }
414
467
 
468
+ @staticmethod
469
+ def format_json_response(content: Any, status_code: int, content_type: str | None = None) -> dict[str, Any]:
470
+ """Alias for format_response for backwards compatibility."""
471
+ return ResponseHandler.format_response(content, status_code, content_type)
472
+
415
473
 
416
474
  def create_enhanced_handler(original_handler, route_definition):
417
475
  """
@@ -491,11 +549,16 @@ def create_enhanced_handler(original_handler, route_definition):
491
549
 
492
550
  # Call original async handler and await it
493
551
  result = await original_handler(**filtered_kwargs)
494
-
495
- # Normalize response
496
- content, status_code = ResponseHandler.normalize_response(result)
497
-
498
- return ResponseHandler.format_json_response(content, status_code)
552
+
553
+ # Normalize response - may return (content, status) or (content, status, content_type)
554
+ normalized = ResponseHandler.normalize_response(result)
555
+ if len(normalized) == 3:
556
+ content, status_code, content_type = normalized
557
+ else:
558
+ content, status_code = normalized
559
+ content_type = None
560
+
561
+ return ResponseHandler.format_json_response(content, status_code, content_type)
499
562
 
500
563
  except ValueError as e:
501
564
  # Validation or parsing error (400 Bad Request)
@@ -575,11 +638,16 @@ def create_enhanced_handler(original_handler, route_definition):
575
638
 
576
639
  # Call original sync handler
577
640
  result = original_handler(**filtered_kwargs)
578
-
579
- # Normalize response
580
- content, status_code = ResponseHandler.normalize_response(result)
581
-
582
- return ResponseHandler.format_json_response(content, status_code)
641
+
642
+ # Normalize response - may return (content, status) or (content, status, content_type)
643
+ normalized = ResponseHandler.normalize_response(result)
644
+ if len(normalized) == 3:
645
+ content, status_code, content_type = normalized
646
+ else:
647
+ content, status_code = normalized
648
+ content_type = None
649
+
650
+ return ResponseHandler.format_json_response(content, status_code, content_type)
583
651
 
584
652
  except ValueError as e:
585
653
  # Validation or parsing error (400 Bad Request)
@@ -43,6 +43,10 @@ def classify_handler(handler, route) -> tuple[str, dict[str, str], dict]:
43
43
  if BaseModel is not None and inspect.isclass(annotation) and issubclass(annotation, BaseModel):
44
44
  # Found a model parameter - use fast model path (sync only for now)
45
45
  model_info = {"param_name": param_name, "model_class": annotation}
46
+ # For async handlers, model parsing needs the enhanced path
47
+ # since Rust-side model parsing only supports sync handlers
48
+ if is_async:
49
+ needs_body = True
46
50
  continue # Don't add to param_types
47
51
  except TypeError:
48
52
  pass
Binary file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: turboapi
3
- Version: 0.5.2
3
+ Version: 0.5.22
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -33,7 +33,7 @@ Project-URL: Homepage, https://github.com/justrach/turboAPI
33
33
  Project-URL: Repository, https://github.com/justrach/turboAPI
34
34
 
35
35
  <p align="center">
36
- <img src="assets/architecture.png" alt="TurboAPI Architecture" width="600"/>
36
+ <img src="assets/turbito.png" alt="Turbito - TurboAPI Mascot" width="260"/>
37
37
  </p>
38
38
 
39
39
  <h1 align="center">TurboAPI</h1>
@@ -42,6 +42,10 @@ Project-URL: Repository, https://github.com/justrach/turboAPI
42
42
  <strong>The FastAPI you know. The speed you deserve.</strong>
43
43
  </p>
44
44
 
45
+ <p align="center">
46
+ <em>Meet Turbito — the tiny Rust-powered engine that makes your FastAPI fly.</em>
47
+ </p>
48
+
45
49
  <p align="center">
46
50
  <a href="https://pypi.org/project/turboapi/"><img src="https://img.shields.io/pypi/v/turboapi.svg" alt="PyPI version"></a>
47
51
  <a href="https://github.com/justrach/turboAPI/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
@@ -52,10 +56,10 @@ Project-URL: Repository, https://github.com/justrach/turboAPI
52
56
  <p align="center">
53
57
  <a href="#the-problem">The Problem</a> •
54
58
  <a href="#the-solution">The Solution</a> •
59
+ <a href="#meet-turbito">Meet Turbito</a> •
55
60
  <a href="#quick-start">Quick Start</a> •
56
- <a href="#benchmarks">Benchmarks</a> •
57
- <a href="#async-support">Async Support</a>
58
- <a href="#migration-guide">Migration Guide</a>
61
+ <a href="#whats-new">What's New</a> •
62
+ <a href="#benchmarks">Benchmarks</a>
59
63
  </p>
60
64
 
61
65
  ---
@@ -99,6 +103,46 @@ The result? Your existing FastAPI code runs faster without changing a single lin
99
103
 
100
104
  ---
101
105
 
106
+ ## Meet Turbito
107
+
108
+ <p align="center">
109
+ <img src="assets/turbito.png" alt="Turbito" width="180"/>
110
+ </p>
111
+
112
+ Turbito is the little engine inside TurboAPI.
113
+
114
+ While you write normal FastAPI code, Turbito is:
115
+ - Parsing HTTP in Rust (Hyper/Tokio)
116
+ - Serializing JSON with SIMD acceleration
117
+ - Scheduling async work with Tokio's work-stealing scheduler
118
+ - Dodging the GIL like a speed demon
119
+
120
+ You never see Turbito. You just feel the speed.
121
+
122
+ ---
123
+
124
+ ## What's New
125
+
126
+ ### v0.5.21 — Free-Threading Stability Release
127
+
128
+ This release fixes critical issues when running with **free-threaded Python (3.13t)** and **Metal/MLX GPU frameworks**:
129
+
130
+ | Fix | Description |
131
+ |-----|-------------|
132
+ | **Memory Corruption** | Fixed race condition where request body bytes were corrupted when using async handlers with MLX models loaded |
133
+ | **Response Serialization** | Response objects now properly serialize their content instead of string representation |
134
+ | **Async BaseModel** | Async handlers with BaseModel parameters now correctly receive the validated model instance |
135
+ | **JSON Parsing** | Added Python fallback for edge cases where simd-json is too strict |
136
+
137
+ **Technical Deep Dive:** When running free-threaded Python with Metal GPU frameworks, memory can be accessed concurrently by the CPU and GPU. We now use defensive copying (`PyBytes::new()` in Rust, `bytes(bytearray())` in Python) to ensure request data is isolated before processing.
138
+
139
+ ```bash
140
+ # Upgrade to get the fixes
141
+ pip install --upgrade turboapi
142
+ ```
143
+
144
+ ---
145
+
102
146
  ## Quick Start
103
147
 
104
148
  ### Installation
@@ -149,15 +193,15 @@ app.run()
149
193
 
150
194
  Async handlers are automatically detected and routed through Tokio's work-stealing scheduler for optimal concurrency.
151
195
 
152
- ### For Maximum Performance
196
+ ### Let Turbito Off the Leash
153
197
 
154
- Run with Python's free-threading mode:
198
+ For maximum performance, run with Python's free-threading mode:
155
199
 
156
200
  ```bash
157
201
  PYTHON_GIL=0 python app.py
158
202
  ```
159
203
 
160
- This unlocks the full power of TurboAPI's Rust core by removing the GIL bottleneck.
204
+ This unlocks Turbito's full power by removing the GIL bottleneck. True parallelism, finally.
161
205
 
162
206
  ---
163
207
 
@@ -340,6 +384,7 @@ Everything you use in FastAPI works in TurboAPI:
340
384
  | GZip middleware | ✅ | Configurable |
341
385
  | Background tasks | ✅ | Async-compatible |
342
386
  | WebSocket | ✅ | HTTP upgrade support |
387
+ | HTTP/2 | ✅ | With server push |
343
388
  | APIRouter | ✅ | Prefixes and tags |
344
389
  | HTTPException | ✅ | With custom headers |
345
390
  | Custom responses | ✅ | JSON, HTML, Redirect, etc. |
@@ -420,7 +465,7 @@ app.add_middleware(GZipMiddleware, minimum_size=1000)
420
465
 
421
466
  ## Architecture
422
467
 
423
- TurboAPI's secret is a hybrid architecture:
468
+ TurboAPI's secret is a hybrid architecture where Python meets Rust:
424
469
 
425
470
  ```
426
471
  ┌──────────────────────────────────────────────────────────┐
@@ -494,10 +539,10 @@ python tests/benchmark_comparison.py
494
539
  - [x] Handler classification for optimized fast paths
495
540
  - [x] **Async handler optimization (Tokio + pyo3-async-runtimes)**
496
541
  - [x] **WebSocket HTTP upgrade support**
542
+ - [x] **HTTP/2 with server push**
497
543
 
498
544
  ### In Progress 🚧
499
545
 
500
- - [ ] HTTP/2 with server push
501
546
  - [ ] OpenAPI/Swagger auto-generation
502
547
 
503
548
  ### Planned 📋
@@ -525,7 +570,8 @@ MIT License. Use it, modify it, ship it.
525
570
  ---
526
571
 
527
572
  <p align="center">
528
- <strong>Stop waiting for Python to be fast. Make it fast.</strong>
573
+ <strong>Built for developers who love FastAPI.</strong><br/>
574
+ <em>Powered by Turbito ⚡</em>
529
575
  </p>
530
576
 
531
577
  <p align="center">
@@ -10,20 +10,20 @@ turboapi\main_app.py,sha256=27MWIQaJKfNEUnkjSbGR76E0GNlJk-P4ZqokUAH1j4I,13780
10
10
  turboapi\middleware.py,sha256=xmuzCm07OIdEtMI_PJcrE5GQg7l0PfQTLEbIbZnuJSE,11077
11
11
  turboapi\models.py,sha256=LxWzSy3GXl9BvI8j6hsX4XuQRrspFyLfg4lwi-kwuPE,4606
12
12
  turboapi\openapi.py,sha256=sn5KpV_-50OJZv5NM8pWpLrjmb0abFg9k2sSDMZz5Fs,7517
13
- turboapi\request_handler.py,sha256=4MSekNk2upQVAdySjIK18NV5RfSCcdqUcwIUtaZJVCE,24370
13
+ turboapi\request_handler.py,sha256=TZas04Vf8jKoNi_4TJ_-McuB6YWiIYfOdOtekKRe-U8,27643
14
14
  turboapi\responses.py,sha256=Ry64gbK993s0LUf3jgdBAAAMAnR_6d-o8EDPCAv3vqQ,6261
15
15
  turboapi\routing.py,sha256=mZdDBQqKOfIvPJrLplKrC3sEqqic79qPEMFl0Iw0ugM,7821
16
- turboapi\rust_integration.py,sha256=pn5q_AvVQw27lwxHko5y7-iE51GtL1c_YT5XrrfKD0g,15695
16
+ turboapi\rust_integration.py,sha256=TfP4zOqE4NOUlVr_Lheu6apQ5cZVx2JWJ0kvGHF-nf8,15918
17
17
  turboapi\security.py,sha256=KBzyDkF8CN9-hOEUoRRBTqG0Zc1OtwI7aFInDJYusLM,18064
18
18
  turboapi\server_integration.py,sha256=drUhhTasWgQfyhFiAaHKd987N3mnE0qkMab1ylmqd4c,18340
19
19
  turboapi\staticfiles.py,sha256=w_y4cw0ncYjdO8kZN_LZz5rQ5Iz8OE7TiWBtgAeBZGo,2952
20
20
  turboapi\status.py,sha256=MT9i17mLjksTnX3d5Vr7Wt1M6GVj5SsQ78p1XktRWvQ,3153
21
21
  turboapi\templating.py,sha256=0UKtBLA-MQNutwFOV8sy7gIOLy94dsAQnpkIb4GrBnw,2160
22
22
  turboapi\testclient.py,sha256=g4gm-Nj84JjMPk90Y1TDjZ7sKXzn_Mcf0XrEQvHyU-k,10990
23
- turboapi\turbonet.cp313-win_amd64.pyd,sha256=oqQGb6MefZ2cWcQFSEuxCfsKBueal9-Rju38SxH7Me0,5146624
23
+ turboapi\turbonet.cp313-win_amd64.pyd,sha256=UhBl_GDjmp8igWn4M33W962BxzJBEncOuk7eZWMH95Y,5170688
24
24
  turboapi\version_check.py,sha256=z3O1vIJsWmG_DO271ayYWSwaDfgpFnfJzYRYyowKYMc,9625
25
25
  turboapi\websockets.py,sha256=FV3tcCXKutgnkkmyJCdTVgGB5eNBLHT3MBPmXAx9Rzk,4356
26
- turboapi-0.5.2.dist-info\METADATA,sha256=_G7zPH9sdzWImSQgpuhpJJDEFDTg-XHQUrpqcFmHejA,18054
27
- turboapi-0.5.2.dist-info\WHEEL,sha256=n_BmF69IyGtioVWE9c3M_zsEfe6-xMZy1v5HCL_6qE0,97
28
- turboapi-0.5.2.dist-info\licenses\LICENSE,sha256=BcaHZdCsG_B-lpQab7W63JiSB8wlewkjk9i1MXss26A,1088
29
- turboapi-0.5.2.dist-info\RECORD,,
26
+ turboapi-0.5.22.dist-info\METADATA,sha256=xHSztJU9mw1yio4v4MN23XDBbCqeewD_xRFw5I25W30,19814
27
+ turboapi-0.5.22.dist-info\WHEEL,sha256=n_BmF69IyGtioVWE9c3M_zsEfe6-xMZy1v5HCL_6qE0,97
28
+ turboapi-0.5.22.dist-info\licenses\LICENSE,sha256=BcaHZdCsG_B-lpQab7W63JiSB8wlewkjk9i1MXss26A,1088
29
+ turboapi-0.5.22.dist-info\RECORD,,