turboapi 0.5.21__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.
- turboapi/request_handler.py +76 -25
- turboapi/turbonet.cp313-win_amd64.pyd +0 -0
- {turboapi-0.5.21.dist-info → turboapi-0.5.22.dist-info}/METADATA +57 -11
- {turboapi-0.5.21.dist-info → turboapi-0.5.22.dist-info}/RECORD +6 -6
- {turboapi-0.5.21.dist-info → turboapi-0.5.22.dist-info}/WHEEL +0 -0
- {turboapi-0.5.21.dist-info → turboapi-0.5.22.dist-info}/licenses/LICENSE +0 -0
turboapi/request_handler.py
CHANGED
|
@@ -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
|
|
360
|
-
|
|
385
|
+
# Binary data - return with content_type
|
|
386
|
+
return body, result.status_code, content_type
|
|
361
387
|
except UnicodeDecodeError:
|
|
362
|
-
# Binary data
|
|
363
|
-
|
|
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
|
|
414
|
+
def format_response(content: Any, status_code: int, content_type: str | None = None) -> dict[str, Any]:
|
|
389
415
|
"""
|
|
390
|
-
Format content as JSON
|
|
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
|
-
#
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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.
|
|
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/
|
|
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="#
|
|
57
|
-
<a href="#
|
|
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
|
-
###
|
|
196
|
+
### Let Turbito Off the Leash
|
|
153
197
|
|
|
154
|
-
|
|
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
|
|
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>
|
|
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=
|
|
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.cp313-win_amd64.pyd,sha256=
|
|
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.
|
|
27
|
-
turboapi-0.5.
|
|
28
|
-
turboapi-0.5.
|
|
29
|
-
turboapi-0.5.
|
|
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,,
|
|
File without changes
|
|
File without changes
|