turboapi 0.5.2__tar.gz → 0.5.21__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.
- {turboapi-0.5.2 → turboapi-0.5.21}/.gitignore +9 -4
- {turboapi-0.5.2 → turboapi-0.5.21}/Cargo.lock +1 -1
- {turboapi-0.5.2 → turboapi-0.5.21}/Cargo.toml +1 -1
- {turboapi-0.5.2 → turboapi-0.5.21}/PKG-INFO +5 -5
- {turboapi-0.5.2 → turboapi-0.5.21}/benchmarks/run_all.sh +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/pyproject.toml +1 -1
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/request_handler.py +23 -6
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/rust_integration.py +4 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/src/server.rs +9 -3
- {turboapi-0.5.2 → turboapi-0.5.21}/src/simd_json.rs +48 -32
- {turboapi-0.5.2 → turboapi-0.5.21}/src/simd_parse.rs +27 -12
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_async_handlers.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_async_simple.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_comprehensive_v0_4_15.py +0 -0
- turboapi-0.5.21/tests/test_issue_fixes.py +508 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_performance_regression.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_post_body_parsing.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_query_and_headers.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_request_parsing.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_wrk_regression.py +0 -0
- turboapi-0.5.21/uv.lock +667 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/.github/scripts/check_performance_regression.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/.github/scripts/compare_benchmarks.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/.github/workflows/README.md +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/.github/workflows/benchmark.yml +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/.github/workflows/build-and-release.yml +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/.github/workflows/ci.yml +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/.github/workflows/release.yml +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/CHANGELOG.md +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/LICENSE +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/Makefile +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/README.md +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/assets/architecture.png +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/assets/benchmark_latency.png +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/assets/benchmark_speedup.png +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/assets/benchmark_throughput.png +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/benches/async_comparison_bench.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/benches/performance_bench.rs +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/benches/python_benchmark.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/benchmarks/README.md +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/benchmarks/bench_json.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/benchmarks/bench_memory.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/benchmarks/bench_throughput.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/benchmarks/bench_validation.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/benchmarks/generate_charts.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/benchmarks/run_benchmarks.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/docs/ARCHITECTURE.md +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/docs/ASYNC_HANDLERS.md +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/docs/BENCHMARKS.md +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/docs/HTTP2.md +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/docs/PERFORMANCE_TUNING.md +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/docs/README.md +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/docs/TLS_SETUP.md +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/docs/WEBSOCKET.md +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/examples/authentication_demo.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/examples/multi_route_app.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/MANIFEST.in +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/setup.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/__init__.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/async_limiter.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/async_pool.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/background.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/datastructures.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/decorators.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/encoders.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/exceptions.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/main_app.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/middleware.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/models.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/openapi.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/responses.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/routing.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/security.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/server_integration.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/staticfiles.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/status.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/templating.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/testclient.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/version_check.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/websockets.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/src/http2.rs +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/src/lib.rs +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/src/micro_bench.rs +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/src/middleware.rs +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/src/python_worker.rs +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/src/request.rs +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/src/response.rs +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/src/router.rs +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/src/threadpool.rs +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/src/tls.rs +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/src/validation.rs +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/src/websocket.rs +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/src/zerocopy.rs +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/README.md +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/benchmark_comparison.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/comparison_before_after.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/fastapi_equivalent.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/quick_body_test.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/quick_test.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/test.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_comprehensive_parity.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_fastapi_compatibility.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_fastapi_parity.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_satya_0_4_0_compatibility.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_security_features.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/wrk_benchmark.py +0 -0
- {turboapi-0.5.2 → turboapi-0.5.21}/tests/wrk_comparison.py +0 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# Rust build artifacts
|
|
2
2
|
target/
|
|
3
|
-
Cargo.lock
|
|
4
3
|
|
|
5
4
|
# Conductor workspace
|
|
6
5
|
.context/
|
|
@@ -14,6 +13,8 @@ __pycache__/
|
|
|
14
13
|
build/
|
|
15
14
|
dist/
|
|
16
15
|
.eggs/
|
|
16
|
+
*.egg
|
|
17
|
+
pip-wheel-metadata/
|
|
17
18
|
|
|
18
19
|
# Virtual environments
|
|
19
20
|
.env
|
|
@@ -30,24 +31,28 @@ turbo-freethreaded/
|
|
|
30
31
|
.idea/
|
|
31
32
|
*.swp
|
|
32
33
|
*.swo
|
|
34
|
+
*~
|
|
33
35
|
|
|
34
36
|
# macOS
|
|
35
37
|
.DS_Store
|
|
38
|
+
.AppleDouble
|
|
39
|
+
.LSOverride
|
|
36
40
|
|
|
37
41
|
# Benchmark and test outputs
|
|
38
42
|
benchmark_graphs/
|
|
39
|
-
*.json
|
|
40
43
|
archive/
|
|
44
|
+
*.bench.json
|
|
45
|
+
benchmark_results*.json
|
|
41
46
|
|
|
42
47
|
# Temporary files
|
|
43
48
|
*.tmp
|
|
44
49
|
*.temp
|
|
45
50
|
*.log
|
|
51
|
+
*.bak
|
|
46
52
|
|
|
47
53
|
# Documentation build
|
|
48
54
|
docs/_build/
|
|
55
|
+
site/
|
|
49
56
|
|
|
50
57
|
# PyO3/maturin build artifacts
|
|
51
58
|
*.whl
|
|
52
|
-
target/wheels/
|
|
53
|
-
benchmark_graphs/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "turbonet"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.21"
|
|
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.
|
|
3
|
+
Version: 0.5.21
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -12,14 +12,14 @@ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
|
12
12
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
13
13
|
Classifier: Framework :: FastAPI
|
|
14
14
|
Requires-Dist: dhi>=1.1.15
|
|
15
|
-
Requires-Dist: matplotlib>=3.5.0 ; extra == 'benchmark'
|
|
16
|
-
Requires-Dist: requests>=2.25.0 ; extra == 'benchmark'
|
|
17
15
|
Requires-Dist: pytest>=7.0.0 ; extra == 'dev'
|
|
18
16
|
Requires-Dist: pytest-asyncio>=0.21.0 ; extra == 'dev'
|
|
19
17
|
Requires-Dist: ruff>=0.1.0 ; extra == 'dev'
|
|
20
18
|
Requires-Dist: mypy>=1.0.0 ; extra == 'dev'
|
|
21
|
-
|
|
19
|
+
Requires-Dist: matplotlib>=3.5.0 ; extra == 'benchmark'
|
|
20
|
+
Requires-Dist: requests>=2.25.0 ; extra == 'benchmark'
|
|
22
21
|
Provides-Extra: dev
|
|
22
|
+
Provides-Extra: benchmark
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
Summary: FastAPI-compatible web framework with Rust HTTP core - 2-3x faster with Python 3.13 free-threading
|
|
25
25
|
Keywords: web,framework,http,server,rust,performance,fastapi,async
|
|
@@ -28,9 +28,9 @@ Author-email: Rach Pradhan <rach@turboapi.dev>
|
|
|
28
28
|
License: MIT
|
|
29
29
|
Requires-Python: >=3.13
|
|
30
30
|
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
31
|
-
Project-URL: Documentation, https://github.com/justrach/turboAPI#readme
|
|
32
31
|
Project-URL: Homepage, https://github.com/justrach/turboAPI
|
|
33
32
|
Project-URL: Repository, https://github.com/justrach/turboAPI
|
|
33
|
+
Project-URL: Documentation, https://github.com/justrach/turboAPI#readme
|
|
34
34
|
|
|
35
35
|
<p align="center">
|
|
36
36
|
<img src="assets/architecture.png" alt="TurboAPI Architecture" width="600"/>
|
|
File without changes
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "turboapi"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.21"
|
|
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"
|
|
@@ -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
|
|
|
@@ -348,9 +351,16 @@ class ResponseHandler:
|
|
|
348
351
|
try:
|
|
349
352
|
import json
|
|
350
353
|
body = json.loads(body.decode('utf-8'))
|
|
351
|
-
except
|
|
352
|
-
#
|
|
353
|
-
|
|
354
|
+
except json.JSONDecodeError:
|
|
355
|
+
# Not JSON, try as plain text
|
|
356
|
+
try:
|
|
357
|
+
body = body.decode('utf-8')
|
|
358
|
+
except UnicodeDecodeError:
|
|
359
|
+
# Binary data (audio, image, etc.) - keep as bytes
|
|
360
|
+
pass
|
|
361
|
+
except UnicodeDecodeError:
|
|
362
|
+
# Binary data (audio, image, etc.) - keep as bytes
|
|
363
|
+
pass
|
|
354
364
|
return body, result.status_code
|
|
355
365
|
|
|
356
366
|
# Handle tuple returns: (content, status_code)
|
|
@@ -394,6 +404,13 @@ class ResponseHandler:
|
|
|
394
404
|
def make_serializable(obj):
|
|
395
405
|
if isinstance(obj, Model):
|
|
396
406
|
return obj.model_dump()
|
|
407
|
+
elif isinstance(obj, bytes):
|
|
408
|
+
# Binary data - try to decode as UTF-8, otherwise base64 encode
|
|
409
|
+
try:
|
|
410
|
+
return obj.decode('utf-8')
|
|
411
|
+
except UnicodeDecodeError:
|
|
412
|
+
import base64
|
|
413
|
+
return base64.b64encode(obj).decode('ascii')
|
|
397
414
|
elif isinstance(obj, dict):
|
|
398
415
|
return {k: make_serializable(v) for k, v in obj.items()}
|
|
399
416
|
elif isinstance(obj, (list, tuple)):
|
|
@@ -491,7 +508,7 @@ def create_enhanced_handler(original_handler, route_definition):
|
|
|
491
508
|
|
|
492
509
|
# Call original async handler and await it
|
|
493
510
|
result = await original_handler(**filtered_kwargs)
|
|
494
|
-
|
|
511
|
+
|
|
495
512
|
# Normalize response
|
|
496
513
|
content, status_code = ResponseHandler.normalize_response(result)
|
|
497
514
|
|
|
@@ -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
|
|
@@ -855,13 +855,19 @@ async fn call_python_handler_enhanced_async(
|
|
|
855
855
|
.map_err(|e| format!("Semaphore error: {}", e))?;
|
|
856
856
|
|
|
857
857
|
// Build kwargs and call async handler
|
|
858
|
-
|
|
858
|
+
// Make a defensive copy to own the data before the async boundary
|
|
859
|
+
let body_vec: Vec<u8> = body_bytes.to_vec();
|
|
860
|
+
|
|
861
|
+
// Use Python::attach (like sync handlers) instead of with_gil for better free-threading support
|
|
862
|
+
let future = Python::attach(|py| {
|
|
859
863
|
use pyo3::types::PyDict;
|
|
860
864
|
let kwargs = PyDict::new(py);
|
|
861
865
|
|
|
862
|
-
// Add body as bytes
|
|
866
|
+
// Add body as bytes - use PyBytes to copy into Python-managed memory
|
|
867
|
+
// (as_slice() would reference Rust memory that may be freed after closure ends)
|
|
868
|
+
let body_py = pyo3::types::PyBytes::new(py, body_vec.as_slice());
|
|
863
869
|
kwargs
|
|
864
|
-
.set_item("body",
|
|
870
|
+
.set_item("body", body_py)
|
|
865
871
|
.map_err(|e| format!("Body set error: {}", e))?;
|
|
866
872
|
|
|
867
873
|
// Add headers dict
|
|
@@ -88,48 +88,64 @@ fn write_value(py: Python, obj: &Bound<'_, PyAny>, buf: &mut Vec<u8>) -> PyResul
|
|
|
88
88
|
|
|
89
89
|
// Fallback: try to convert to a serializable Python representation
|
|
90
90
|
|
|
91
|
-
// Check
|
|
92
|
-
//
|
|
93
|
-
if let Ok(
|
|
94
|
-
if
|
|
95
|
-
|
|
91
|
+
// Check if it has a model_dump() method (dhi/Pydantic models AND Response objects)
|
|
92
|
+
// Response classes in turboapi have model_dump() that returns the decoded content
|
|
93
|
+
if let Ok(dump_method) = obj.getattr("model_dump") {
|
|
94
|
+
if dump_method.is_callable() {
|
|
95
|
+
if let Ok(dumped) = dump_method.call0() {
|
|
96
|
+
return write_value(py, &dumped, buf);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check for Response objects (JSONResponse, HTMLResponse, FileResponse, etc.)
|
|
102
|
+
// These have 'body' and 'status_code' attributes
|
|
103
|
+
if obj.hasattr("body")? && obj.hasattr("status_code")? {
|
|
104
|
+
// Try to get body as bytes first
|
|
105
|
+
if let Ok(body_attr) = obj.getattr("body") {
|
|
106
|
+
// Try extracting as bytes (Vec<u8>)
|
|
96
107
|
if let Ok(body_bytes) = body_attr.extract::<Vec<u8>>() {
|
|
97
108
|
// Try to parse body as JSON first
|
|
98
109
|
if let Ok(json_str) = String::from_utf8(body_bytes.clone()) {
|
|
99
|
-
// If it
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
||
|
|
110
|
+
// If it looks like valid JSON, use it directly
|
|
111
|
+
let trimmed = json_str.trim();
|
|
112
|
+
if trimmed.starts_with('{')
|
|
113
|
+
|| trimmed.starts_with('[')
|
|
114
|
+
|| trimmed.starts_with('"')
|
|
115
|
+
|| trimmed == "null"
|
|
116
|
+
|| trimmed == "true"
|
|
117
|
+
|| trimmed == "false"
|
|
118
|
+
|| trimmed.parse::<f64>().is_ok()
|
|
103
119
|
{
|
|
104
120
|
buf.extend_from_slice(json_str.as_bytes());
|
|
105
121
|
return Ok(());
|
|
106
122
|
}
|
|
107
|
-
// Otherwise treat as string
|
|
108
|
-
buf
|
|
109
|
-
for byte in json_str.bytes() {
|
|
110
|
-
match byte {
|
|
111
|
-
b'"' => buf.extend_from_slice(b"\\\""),
|
|
112
|
-
b'\\' => buf.extend_from_slice(b"\\\\"),
|
|
113
|
-
b'\n' => buf.extend_from_slice(b"\\n"),
|
|
114
|
-
b'\r' => buf.extend_from_slice(b"\\r"),
|
|
115
|
-
b'\t' => buf.extend_from_slice(b"\\t"),
|
|
116
|
-
b if b < 32 => {
|
|
117
|
-
buf.extend_from_slice(format!("\\u{:04x}", b).as_bytes());
|
|
118
|
-
}
|
|
119
|
-
_ => buf.push(byte),
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
buf.push(b'"');
|
|
123
|
+
// Otherwise treat as string content
|
|
124
|
+
write_str_escaped(&json_str, buf);
|
|
123
125
|
return Ok(());
|
|
124
126
|
}
|
|
127
|
+
// Binary content - encode as base64 or return error
|
|
128
|
+
// For now, just return the length as a placeholder
|
|
129
|
+
buf.extend_from_slice(b"\"<binary content>\"");
|
|
130
|
+
return Ok(());
|
|
131
|
+
}
|
|
132
|
+
// Try extracting as string
|
|
133
|
+
if let Ok(body_str) = body_attr.extract::<String>() {
|
|
134
|
+
let trimmed = body_str.trim();
|
|
135
|
+
if trimmed.starts_with('{')
|
|
136
|
+
|| trimmed.starts_with('[')
|
|
137
|
+
|| trimmed.starts_with('"')
|
|
138
|
+
|| trimmed == "null"
|
|
139
|
+
|| trimmed == "true"
|
|
140
|
+
|| trimmed == "false"
|
|
141
|
+
|| trimmed.parse::<f64>().is_ok()
|
|
142
|
+
{
|
|
143
|
+
buf.extend_from_slice(body_str.as_bytes());
|
|
144
|
+
return Ok(());
|
|
145
|
+
}
|
|
146
|
+
write_str_escaped(&body_str, buf);
|
|
147
|
+
return Ok(());
|
|
125
148
|
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Check if it has a model_dump() method (dhi/Pydantic models)
|
|
130
|
-
if let Ok(dump_method) = obj.getattr("model_dump") {
|
|
131
|
-
if let Ok(dumped) = dump_method.call0() {
|
|
132
|
-
return write_value(py, &dumped, buf);
|
|
133
149
|
}
|
|
134
150
|
}
|
|
135
151
|
|
|
@@ -350,31 +350,46 @@ fn set_simd_object_into_dict<'py>(
|
|
|
350
350
|
|
|
351
351
|
/// Parse JSON body using simd-json and return as a Python dict.
|
|
352
352
|
/// This is used for model validation where we need the full dict.
|
|
353
|
+
/// Falls back to Python's json.loads if simd-json fails.
|
|
353
354
|
#[inline]
|
|
354
355
|
pub fn parse_json_to_pydict<'py>(py: Python<'py>, body: &[u8]) -> PyResult<Bound<'py, PyDict>> {
|
|
355
356
|
if body.is_empty() {
|
|
356
357
|
return Ok(PyDict::new(py));
|
|
357
358
|
}
|
|
358
359
|
|
|
359
|
-
//
|
|
360
|
+
// Try simd-json first for fast parsing
|
|
360
361
|
let mut body_copy = body.to_vec();
|
|
361
|
-
let
|
|
362
|
-
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("JSON parse error: {}", e)))?;
|
|
362
|
+
let simd_result = simd_json::to_borrowed_value(&mut body_copy);
|
|
363
363
|
|
|
364
|
-
let
|
|
364
|
+
if let Ok(parsed) = simd_result {
|
|
365
|
+
let dict = PyDict::new(py);
|
|
365
366
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
367
|
+
// Only handle object (dict) bodies
|
|
368
|
+
if let simd_json::BorrowedValue::Object(map) = parsed {
|
|
369
|
+
for (key, value) in map.iter() {
|
|
370
|
+
set_simd_value_into_dict(py, key.as_ref(), value, &dict)?;
|
|
371
|
+
}
|
|
372
|
+
return Ok(dict);
|
|
373
|
+
} else {
|
|
374
|
+
return Err(pyo3::exceptions::PyValueError::new_err(
|
|
375
|
+
"Expected JSON object",
|
|
376
|
+
));
|
|
370
377
|
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// simd-json failed, fall back to Python's json.loads
|
|
381
|
+
let json_module = py.import("json")?;
|
|
382
|
+
let body_str = String::from_utf8_lossy(body);
|
|
383
|
+
let result = json_module.call_method1("loads", (body_str.as_ref(),))?;
|
|
384
|
+
|
|
385
|
+
// Check if result is a dict
|
|
386
|
+
if let Ok(dict) = result.downcast::<PyDict>() {
|
|
387
|
+
Ok(dict.clone())
|
|
371
388
|
} else {
|
|
372
|
-
|
|
389
|
+
Err(pyo3::exceptions::PyValueError::new_err(
|
|
373
390
|
"Expected JSON object",
|
|
374
|
-
))
|
|
391
|
+
))
|
|
375
392
|
}
|
|
376
|
-
|
|
377
|
-
Ok(dict)
|
|
378
393
|
}
|
|
379
394
|
|
|
380
395
|
/// Set a single simd-json value into a PyDict at the given key.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|