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.
- turboapi/request_handler.py +94 -26
- turboapi/rust_integration.py +4 -0
- turboapi/turbonet.cp313-win_amd64.pyd +0 -0
- {turboapi-0.5.2.dist-info → turboapi-0.5.22.dist-info}/METADATA +57 -11
- {turboapi-0.5.2.dist-info → turboapi-0.5.22.dist-info}/RECORD +7 -7
- {turboapi-0.5.2.dist-info → turboapi-0.5.22.dist-info}/WHEEL +0 -0
- {turboapi-0.5.2.dist-info → turboapi-0.5.22.dist-info}/licenses/LICENSE +0 -0
turboapi/request_handler.py
CHANGED
|
@@ -229,9 +229,12 @@ class RequestBodyParser:
|
|
|
229
229
|
"""
|
|
230
230
|
if not body:
|
|
231
231
|
return {}
|
|
232
|
-
|
|
232
|
+
|
|
233
233
|
try:
|
|
234
|
-
|
|
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
|
|
352
|
-
#
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
414
|
+
def format_response(content: Any, status_code: int, content_type: str | None = None) -> dict[str, Any]:
|
|
379
415
|
"""
|
|
380
|
-
Format content as JSON
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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)
|
turboapi/rust_integration.py
CHANGED
|
@@ -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.
|
|
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,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=
|
|
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=
|
|
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=
|
|
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
|