turboapi 0.5.21__cp314-cp314-win_amd64.whl → 0.5.22__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.
@@ -319,6 +319,25 @@ class RequestBodyParser:
319
319
  return parsed_params
320
320
 
321
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
+
322
341
  class ResponseHandler:
323
342
  """Handle different response formats including FastAPI-style tuples."""
324
343
 
@@ -339,13 +358,20 @@ class ResponseHandler:
339
358
  result: Raw result from handler
340
359
 
341
360
  Returns:
342
- Tuple of (content, status_code)
361
+ Tuple of (content, status_code) or (content, status_code, content_type)
343
362
  """
344
363
  # Handle Response objects (JSONResponse, HTMLResponse, etc.)
345
364
  from turboapi.responses import Response
346
365
  if isinstance(result, Response):
347
366
  # Extract content from Response object
348
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
+
349
375
  if isinstance(body, bytes):
350
376
  # Try to decode as JSON for JSONResponse
351
377
  try:
@@ -356,12 +382,12 @@ class ResponseHandler:
356
382
  try:
357
383
  body = body.decode('utf-8')
358
384
  except UnicodeDecodeError:
359
- # Binary data (audio, image, etc.) - keep as bytes
360
- pass
385
+ # Binary data - return with content_type
386
+ return body, result.status_code, content_type
361
387
  except UnicodeDecodeError:
362
- # Binary data (audio, image, etc.) - keep as bytes
363
- pass
364
- return body, result.status_code
388
+ # Binary data - return with content_type
389
+ return body, result.status_code, content_type
390
+ return body, result.status_code, content_type
365
391
 
366
392
  # Handle tuple returns: (content, status_code)
367
393
  if isinstance(result, tuple):
@@ -385,27 +411,37 @@ class ResponseHandler:
385
411
  return result, 200
386
412
 
387
413
  @staticmethod
388
- 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]:
389
415
  """
390
- Format content as JSON response.
391
-
416
+ Format content as response. Handles both JSON and binary responses.
417
+
392
418
  Args:
393
- content: Response content
419
+ content: Response content (can be dict, str, bytes, etc.)
394
420
  status_code: HTTP status code
395
-
421
+ content_type: Optional content type (for binary responses)
422
+
396
423
  Returns:
397
424
  Dictionary with properly formatted response
398
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
+
399
435
  # Handle Satya models
400
436
  if isinstance(content, Model):
401
437
  content = content.model_dump()
402
-
438
+
403
439
  # Recursively convert any nested Satya models in dicts/lists
404
440
  def make_serializable(obj):
405
441
  if isinstance(obj, Model):
406
442
  return obj.model_dump()
407
443
  elif isinstance(obj, bytes):
408
- # Binary data - try to decode as UTF-8, otherwise base64 encode
444
+ # Non-binary bytes - try to decode as UTF-8, otherwise base64 encode
409
445
  try:
410
446
  return obj.decode('utf-8')
411
447
  except UnicodeDecodeError:
@@ -420,15 +456,20 @@ class ResponseHandler:
420
456
  else:
421
457
  # Try to convert to string for unknown types
422
458
  return str(obj)
423
-
459
+
424
460
  content = make_serializable(content)
425
-
461
+
426
462
  return {
427
463
  "content": content,
428
464
  "status_code": status_code,
429
- "content_type": "application/json"
465
+ "content_type": content_type or "application/json"
430
466
  }
431
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
+
432
473
 
433
474
  def create_enhanced_handler(original_handler, route_definition):
434
475
  """
@@ -509,10 +550,15 @@ def create_enhanced_handler(original_handler, route_definition):
509
550
  # Call original async handler and await it
510
551
  result = await original_handler(**filtered_kwargs)
511
552
 
512
- # Normalize response
513
- content, status_code = ResponseHandler.normalize_response(result)
514
-
515
- return ResponseHandler.format_json_response(content, status_code)
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)
516
562
 
517
563
  except ValueError as e:
518
564
  # Validation or parsing error (400 Bad Request)
@@ -592,11 +638,16 @@ def create_enhanced_handler(original_handler, route_definition):
592
638
 
593
639
  # Call original sync handler
594
640
  result = original_handler(**filtered_kwargs)
595
-
596
- # Normalize response
597
- content, status_code = ResponseHandler.normalize_response(result)
598
-
599
- 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)
600
651
 
601
652
  except ValueError as e:
602
653
  # Validation or parsing error (400 Bad Request)
Binary file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: turboapi
3
- Version: 0.5.21
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,7 +10,7 @@ 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=9fY-OJ4Jgfy99zBJ7sA0JYb2syQIoiuL7j2rCmRZQI4,25212
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
16
  turboapi\rust_integration.py,sha256=TfP4zOqE4NOUlVr_Lheu6apQ5cZVx2JWJ0kvGHF-nf8,15918
@@ -20,10 +20,10 @@ 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.cp314-win_amd64.pyd,sha256=wT-jXZ-A4Cz4hgyCFayW9xe4ZnX9A24oQKMnCeU3638,5139456
23
+ turboapi\turbonet.cp314-win_amd64.pyd,sha256=sLYcnvK-8BGc1ZO98xN3hfdKrcHPE31rFOHYbTXUQF4,5167616
24
24
  turboapi\version_check.py,sha256=z3O1vIJsWmG_DO271ayYWSwaDfgpFnfJzYRYyowKYMc,9625
25
25
  turboapi\websockets.py,sha256=FV3tcCXKutgnkkmyJCdTVgGB5eNBLHT3MBPmXAx9Rzk,4356
26
- turboapi-0.5.21.dist-info\METADATA,sha256=fhcunMLbHUGX-x7cZyeHM6Bh7szKPgH1NGssZ8rZsWQ,18055
27
- turboapi-0.5.21.dist-info\WHEEL,sha256=TASrtxyeL-Pi7odwPBMCgR1YebCHdBFZvgqiADG_4b0,97
28
- turboapi-0.5.21.dist-info\licenses\LICENSE,sha256=BcaHZdCsG_B-lpQab7W63JiSB8wlewkjk9i1MXss26A,1088
29
- turboapi-0.5.21.dist-info\RECORD,,
26
+ turboapi-0.5.22.dist-info\METADATA,sha256=xHSztJU9mw1yio4v4MN23XDBbCqeewD_xRFw5I25W30,19814
27
+ turboapi-0.5.22.dist-info\WHEEL,sha256=TASrtxyeL-Pi7odwPBMCgR1YebCHdBFZvgqiADG_4b0,97
28
+ turboapi-0.5.22.dist-info\licenses\LICENSE,sha256=BcaHZdCsG_B-lpQab7W63JiSB8wlewkjk9i1MXss26A,1088
29
+ turboapi-0.5.22.dist-info\RECORD,,