turboapi 0.5.21__tar.gz → 0.5.22__tar.gz

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.
Files changed (110) hide show
  1. {turboapi-0.5.21 → turboapi-0.5.22}/Cargo.lock +1 -1
  2. {turboapi-0.5.21 → turboapi-0.5.22}/Cargo.toml +1 -1
  3. {turboapi-0.5.21 → turboapi-0.5.22}/PKG-INFO +57 -11
  4. {turboapi-0.5.21 → turboapi-0.5.22}/README.md +56 -10
  5. turboapi-0.5.22/assets/turbito.png +0 -0
  6. {turboapi-0.5.21 → turboapi-0.5.22}/pyproject.toml +1 -1
  7. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/request_handler.py +76 -25
  8. {turboapi-0.5.21 → turboapi-0.5.22}/src/server.rs +196 -23
  9. turboapi-0.5.22/tests/fixtures/test_audio.wav +0 -0
  10. turboapi-0.5.22/tests/test_binary_responses.py +383 -0
  11. {turboapi-0.5.21 → turboapi-0.5.22}/tests/test_issue_fixes.py +8 -4
  12. {turboapi-0.5.21 → turboapi-0.5.22}/uv.lock +1 -1
  13. {turboapi-0.5.21 → turboapi-0.5.22}/.github/scripts/check_performance_regression.py +0 -0
  14. {turboapi-0.5.21 → turboapi-0.5.22}/.github/scripts/compare_benchmarks.py +0 -0
  15. {turboapi-0.5.21 → turboapi-0.5.22}/.github/workflows/README.md +0 -0
  16. {turboapi-0.5.21 → turboapi-0.5.22}/.github/workflows/benchmark.yml +0 -0
  17. {turboapi-0.5.21 → turboapi-0.5.22}/.github/workflows/build-and-release.yml +0 -0
  18. {turboapi-0.5.21 → turboapi-0.5.22}/.github/workflows/ci.yml +0 -0
  19. {turboapi-0.5.21 → turboapi-0.5.22}/.github/workflows/release.yml +0 -0
  20. {turboapi-0.5.21 → turboapi-0.5.22}/.gitignore +0 -0
  21. {turboapi-0.5.21 → turboapi-0.5.22}/CHANGELOG.md +0 -0
  22. {turboapi-0.5.21 → turboapi-0.5.22}/LICENSE +0 -0
  23. {turboapi-0.5.21 → turboapi-0.5.22}/Makefile +0 -0
  24. {turboapi-0.5.21 → turboapi-0.5.22}/assets/architecture.png +0 -0
  25. {turboapi-0.5.21 → turboapi-0.5.22}/assets/benchmark_latency.png +0 -0
  26. {turboapi-0.5.21 → turboapi-0.5.22}/assets/benchmark_speedup.png +0 -0
  27. {turboapi-0.5.21 → turboapi-0.5.22}/assets/benchmark_throughput.png +0 -0
  28. {turboapi-0.5.21 → turboapi-0.5.22}/benches/async_comparison_bench.py +0 -0
  29. {turboapi-0.5.21 → turboapi-0.5.22}/benches/performance_bench.rs +0 -0
  30. {turboapi-0.5.21 → turboapi-0.5.22}/benches/python_benchmark.py +0 -0
  31. {turboapi-0.5.21 → turboapi-0.5.22}/benchmarks/README.md +0 -0
  32. {turboapi-0.5.21 → turboapi-0.5.22}/benchmarks/bench_json.py +0 -0
  33. {turboapi-0.5.21 → turboapi-0.5.22}/benchmarks/bench_memory.py +0 -0
  34. {turboapi-0.5.21 → turboapi-0.5.22}/benchmarks/bench_throughput.py +0 -0
  35. {turboapi-0.5.21 → turboapi-0.5.22}/benchmarks/bench_validation.py +0 -0
  36. {turboapi-0.5.21 → turboapi-0.5.22}/benchmarks/generate_charts.py +0 -0
  37. {turboapi-0.5.21 → turboapi-0.5.22}/benchmarks/run_all.sh +0 -0
  38. {turboapi-0.5.21 → turboapi-0.5.22}/benchmarks/run_benchmarks.py +0 -0
  39. {turboapi-0.5.21 → turboapi-0.5.22}/docs/ARCHITECTURE.md +0 -0
  40. {turboapi-0.5.21 → turboapi-0.5.22}/docs/ASYNC_HANDLERS.md +0 -0
  41. {turboapi-0.5.21 → turboapi-0.5.22}/docs/BENCHMARKS.md +0 -0
  42. {turboapi-0.5.21 → turboapi-0.5.22}/docs/HTTP2.md +0 -0
  43. {turboapi-0.5.21 → turboapi-0.5.22}/docs/PERFORMANCE_TUNING.md +0 -0
  44. {turboapi-0.5.21 → turboapi-0.5.22}/docs/README.md +0 -0
  45. {turboapi-0.5.21 → turboapi-0.5.22}/docs/TLS_SETUP.md +0 -0
  46. {turboapi-0.5.21 → turboapi-0.5.22}/docs/WEBSOCKET.md +0 -0
  47. {turboapi-0.5.21 → turboapi-0.5.22}/examples/authentication_demo.py +0 -0
  48. {turboapi-0.5.21 → turboapi-0.5.22}/examples/multi_route_app.py +0 -0
  49. {turboapi-0.5.21 → turboapi-0.5.22}/python/MANIFEST.in +0 -0
  50. {turboapi-0.5.21 → turboapi-0.5.22}/python/setup.py +0 -0
  51. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/__init__.py +0 -0
  52. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/async_limiter.py +0 -0
  53. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/async_pool.py +0 -0
  54. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/background.py +0 -0
  55. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/datastructures.py +0 -0
  56. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/decorators.py +0 -0
  57. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/encoders.py +0 -0
  58. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/exceptions.py +0 -0
  59. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/main_app.py +0 -0
  60. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/middleware.py +0 -0
  61. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/models.py +0 -0
  62. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/openapi.py +0 -0
  63. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/responses.py +0 -0
  64. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/routing.py +0 -0
  65. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/rust_integration.py +0 -0
  66. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/security.py +0 -0
  67. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/server_integration.py +0 -0
  68. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/staticfiles.py +0 -0
  69. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/status.py +0 -0
  70. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/templating.py +0 -0
  71. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/testclient.py +0 -0
  72. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/version_check.py +0 -0
  73. {turboapi-0.5.21 → turboapi-0.5.22}/python/turboapi/websockets.py +0 -0
  74. {turboapi-0.5.21 → turboapi-0.5.22}/src/http2.rs +0 -0
  75. {turboapi-0.5.21 → turboapi-0.5.22}/src/lib.rs +0 -0
  76. {turboapi-0.5.21 → turboapi-0.5.22}/src/micro_bench.rs +0 -0
  77. {turboapi-0.5.21 → turboapi-0.5.22}/src/middleware.rs +0 -0
  78. {turboapi-0.5.21 → turboapi-0.5.22}/src/python_worker.rs +0 -0
  79. {turboapi-0.5.21 → turboapi-0.5.22}/src/request.rs +0 -0
  80. {turboapi-0.5.21 → turboapi-0.5.22}/src/response.rs +0 -0
  81. {turboapi-0.5.21 → turboapi-0.5.22}/src/router.rs +0 -0
  82. {turboapi-0.5.21 → turboapi-0.5.22}/src/simd_json.rs +0 -0
  83. {turboapi-0.5.21 → turboapi-0.5.22}/src/simd_parse.rs +0 -0
  84. {turboapi-0.5.21 → turboapi-0.5.22}/src/threadpool.rs +0 -0
  85. {turboapi-0.5.21 → turboapi-0.5.22}/src/tls.rs +0 -0
  86. {turboapi-0.5.21 → turboapi-0.5.22}/src/validation.rs +0 -0
  87. {turboapi-0.5.21 → turboapi-0.5.22}/src/websocket.rs +0 -0
  88. {turboapi-0.5.21 → turboapi-0.5.22}/src/zerocopy.rs +0 -0
  89. {turboapi-0.5.21 → turboapi-0.5.22}/tests/README.md +0 -0
  90. {turboapi-0.5.21 → turboapi-0.5.22}/tests/benchmark_comparison.py +0 -0
  91. {turboapi-0.5.21 → turboapi-0.5.22}/tests/comparison_before_after.py +0 -0
  92. {turboapi-0.5.21 → turboapi-0.5.22}/tests/fastapi_equivalent.py +0 -0
  93. {turboapi-0.5.21 → turboapi-0.5.22}/tests/quick_body_test.py +0 -0
  94. {turboapi-0.5.21 → turboapi-0.5.22}/tests/quick_test.py +0 -0
  95. {turboapi-0.5.21 → turboapi-0.5.22}/tests/test.py +0 -0
  96. {turboapi-0.5.21 → turboapi-0.5.22}/tests/test_async_handlers.py +0 -0
  97. {turboapi-0.5.21 → turboapi-0.5.22}/tests/test_async_simple.py +0 -0
  98. {turboapi-0.5.21 → turboapi-0.5.22}/tests/test_comprehensive_parity.py +0 -0
  99. {turboapi-0.5.21 → turboapi-0.5.22}/tests/test_comprehensive_v0_4_15.py +0 -0
  100. {turboapi-0.5.21 → turboapi-0.5.22}/tests/test_fastapi_compatibility.py +0 -0
  101. {turboapi-0.5.21 → turboapi-0.5.22}/tests/test_fastapi_parity.py +0 -0
  102. {turboapi-0.5.21 → turboapi-0.5.22}/tests/test_performance_regression.py +0 -0
  103. {turboapi-0.5.21 → turboapi-0.5.22}/tests/test_post_body_parsing.py +0 -0
  104. {turboapi-0.5.21 → turboapi-0.5.22}/tests/test_query_and_headers.py +0 -0
  105. {turboapi-0.5.21 → turboapi-0.5.22}/tests/test_request_parsing.py +0 -0
  106. {turboapi-0.5.21 → turboapi-0.5.22}/tests/test_satya_0_4_0_compatibility.py +0 -0
  107. {turboapi-0.5.21 → turboapi-0.5.22}/tests/test_security_features.py +0 -0
  108. {turboapi-0.5.21 → turboapi-0.5.22}/tests/test_wrk_regression.py +0 -0
  109. {turboapi-0.5.21 → turboapi-0.5.22}/tests/wrk_benchmark.py +0 -0
  110. {turboapi-0.5.21 → turboapi-0.5.22}/tests/wrk_comparison.py +0 -0
@@ -1709,7 +1709,7 @@ dependencies = [
1709
1709
 
1710
1710
  [[package]]
1711
1711
  name = "turbonet"
1712
- version = "0.5.21"
1712
+ version = "0.5.22"
1713
1713
  dependencies = [
1714
1714
  "anyhow",
1715
1715
  "bytes",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "turbonet"
3
- version = "0.5.21"
3
+ version = "0.5.22"
4
4
  edition = "2021"
5
5
  authors = ["Rach Pradhan <rach@turboapi.dev>"]
6
6
  description = "High-performance Python web framework core - Rust-powered HTTP server with Python 3.14 free-threading support, FastAPI-compatible security and middleware"
@@ -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: Repository, https://github.com/justrach/turboAPI
33
33
  Project-URL: Documentation, https://github.com/justrach/turboAPI#readme
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: Documentation, https://github.com/justrach/turboAPI#readme
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: Documentation, https://github.com/justrach/turboAPI#readme
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">
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="assets/architecture.png" alt="TurboAPI Architecture" width="600"/>
2
+ <img src="assets/turbito.png" alt="Turbito - TurboAPI Mascot" width="260"/>
3
3
  </p>
4
4
 
5
5
  <h1 align="center">TurboAPI</h1>
@@ -8,6 +8,10 @@
8
8
  <strong>The FastAPI you know. The speed you deserve.</strong>
9
9
  </p>
10
10
 
11
+ <p align="center">
12
+ <em>Meet Turbito — the tiny Rust-powered engine that makes your FastAPI fly.</em>
13
+ </p>
14
+
11
15
  <p align="center">
12
16
  <a href="https://pypi.org/project/turboapi/"><img src="https://img.shields.io/pypi/v/turboapi.svg" alt="PyPI version"></a>
13
17
  <a href="https://github.com/justrach/turboAPI/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
@@ -18,10 +22,10 @@
18
22
  <p align="center">
19
23
  <a href="#the-problem">The Problem</a> •
20
24
  <a href="#the-solution">The Solution</a> •
25
+ <a href="#meet-turbito">Meet Turbito</a> •
21
26
  <a href="#quick-start">Quick Start</a> •
22
- <a href="#benchmarks">Benchmarks</a> •
23
- <a href="#async-support">Async Support</a>
24
- <a href="#migration-guide">Migration Guide</a>
27
+ <a href="#whats-new">What's New</a> •
28
+ <a href="#benchmarks">Benchmarks</a>
25
29
  </p>
26
30
 
27
31
  ---
@@ -65,6 +69,46 @@ The result? Your existing FastAPI code runs faster without changing a single lin
65
69
 
66
70
  ---
67
71
 
72
+ ## Meet Turbito
73
+
74
+ <p align="center">
75
+ <img src="assets/turbito.png" alt="Turbito" width="180"/>
76
+ </p>
77
+
78
+ Turbito is the little engine inside TurboAPI.
79
+
80
+ While you write normal FastAPI code, Turbito is:
81
+ - Parsing HTTP in Rust (Hyper/Tokio)
82
+ - Serializing JSON with SIMD acceleration
83
+ - Scheduling async work with Tokio's work-stealing scheduler
84
+ - Dodging the GIL like a speed demon
85
+
86
+ You never see Turbito. You just feel the speed.
87
+
88
+ ---
89
+
90
+ ## What's New
91
+
92
+ ### v0.5.21 — Free-Threading Stability Release
93
+
94
+ This release fixes critical issues when running with **free-threaded Python (3.13t)** and **Metal/MLX GPU frameworks**:
95
+
96
+ | Fix | Description |
97
+ |-----|-------------|
98
+ | **Memory Corruption** | Fixed race condition where request body bytes were corrupted when using async handlers with MLX models loaded |
99
+ | **Response Serialization** | Response objects now properly serialize their content instead of string representation |
100
+ | **Async BaseModel** | Async handlers with BaseModel parameters now correctly receive the validated model instance |
101
+ | **JSON Parsing** | Added Python fallback for edge cases where simd-json is too strict |
102
+
103
+ **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.
104
+
105
+ ```bash
106
+ # Upgrade to get the fixes
107
+ pip install --upgrade turboapi
108
+ ```
109
+
110
+ ---
111
+
68
112
  ## Quick Start
69
113
 
70
114
  ### Installation
@@ -115,15 +159,15 @@ app.run()
115
159
 
116
160
  Async handlers are automatically detected and routed through Tokio's work-stealing scheduler for optimal concurrency.
117
161
 
118
- ### For Maximum Performance
162
+ ### Let Turbito Off the Leash
119
163
 
120
- Run with Python's free-threading mode:
164
+ For maximum performance, run with Python's free-threading mode:
121
165
 
122
166
  ```bash
123
167
  PYTHON_GIL=0 python app.py
124
168
  ```
125
169
 
126
- This unlocks the full power of TurboAPI's Rust core by removing the GIL bottleneck.
170
+ This unlocks Turbito's full power by removing the GIL bottleneck. True parallelism, finally.
127
171
 
128
172
  ---
129
173
 
@@ -306,6 +350,7 @@ Everything you use in FastAPI works in TurboAPI:
306
350
  | GZip middleware | ✅ | Configurable |
307
351
  | Background tasks | ✅ | Async-compatible |
308
352
  | WebSocket | ✅ | HTTP upgrade support |
353
+ | HTTP/2 | ✅ | With server push |
309
354
  | APIRouter | ✅ | Prefixes and tags |
310
355
  | HTTPException | ✅ | With custom headers |
311
356
  | Custom responses | ✅ | JSON, HTML, Redirect, etc. |
@@ -386,7 +431,7 @@ app.add_middleware(GZipMiddleware, minimum_size=1000)
386
431
 
387
432
  ## Architecture
388
433
 
389
- TurboAPI's secret is a hybrid architecture:
434
+ TurboAPI's secret is a hybrid architecture where Python meets Rust:
390
435
 
391
436
  ```
392
437
  ┌──────────────────────────────────────────────────────────┐
@@ -460,10 +505,10 @@ python tests/benchmark_comparison.py
460
505
  - [x] Handler classification for optimized fast paths
461
506
  - [x] **Async handler optimization (Tokio + pyo3-async-runtimes)**
462
507
  - [x] **WebSocket HTTP upgrade support**
508
+ - [x] **HTTP/2 with server push**
463
509
 
464
510
  ### In Progress 🚧
465
511
 
466
- - [ ] HTTP/2 with server push
467
512
  - [ ] OpenAPI/Swagger auto-generation
468
513
 
469
514
  ### Planned 📋
@@ -491,7 +536,8 @@ MIT License. Use it, modify it, ship it.
491
536
  ---
492
537
 
493
538
  <p align="center">
494
- <strong>Stop waiting for Python to be fast. Make it fast.</strong>
539
+ <strong>Built for developers who love FastAPI.</strong><br/>
540
+ <em>Powered by Turbito ⚡</em>
495
541
  </p>
496
542
 
497
543
  <p align="center">
Binary file
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "turboapi"
7
- version = "0.5.21"
7
+ version = "0.5.22"
8
8
  description = "FastAPI-compatible web framework with Rust HTTP core - 2-3x faster with Python 3.13 free-threading"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -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)