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.
Files changed (107) hide show
  1. {turboapi-0.5.2 → turboapi-0.5.21}/.gitignore +9 -4
  2. {turboapi-0.5.2 → turboapi-0.5.21}/Cargo.lock +1 -1
  3. {turboapi-0.5.2 → turboapi-0.5.21}/Cargo.toml +1 -1
  4. {turboapi-0.5.2 → turboapi-0.5.21}/PKG-INFO +5 -5
  5. {turboapi-0.5.2 → turboapi-0.5.21}/benchmarks/run_all.sh +0 -0
  6. {turboapi-0.5.2 → turboapi-0.5.21}/pyproject.toml +1 -1
  7. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/request_handler.py +23 -6
  8. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/rust_integration.py +4 -0
  9. {turboapi-0.5.2 → turboapi-0.5.21}/src/server.rs +9 -3
  10. {turboapi-0.5.2 → turboapi-0.5.21}/src/simd_json.rs +48 -32
  11. {turboapi-0.5.2 → turboapi-0.5.21}/src/simd_parse.rs +27 -12
  12. {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_async_handlers.py +0 -0
  13. {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_async_simple.py +0 -0
  14. {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_comprehensive_v0_4_15.py +0 -0
  15. turboapi-0.5.21/tests/test_issue_fixes.py +508 -0
  16. {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_performance_regression.py +0 -0
  17. {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_post_body_parsing.py +0 -0
  18. {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_query_and_headers.py +0 -0
  19. {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_request_parsing.py +0 -0
  20. {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_wrk_regression.py +0 -0
  21. turboapi-0.5.21/uv.lock +667 -0
  22. {turboapi-0.5.2 → turboapi-0.5.21}/.github/scripts/check_performance_regression.py +0 -0
  23. {turboapi-0.5.2 → turboapi-0.5.21}/.github/scripts/compare_benchmarks.py +0 -0
  24. {turboapi-0.5.2 → turboapi-0.5.21}/.github/workflows/README.md +0 -0
  25. {turboapi-0.5.2 → turboapi-0.5.21}/.github/workflows/benchmark.yml +0 -0
  26. {turboapi-0.5.2 → turboapi-0.5.21}/.github/workflows/build-and-release.yml +0 -0
  27. {turboapi-0.5.2 → turboapi-0.5.21}/.github/workflows/ci.yml +0 -0
  28. {turboapi-0.5.2 → turboapi-0.5.21}/.github/workflows/release.yml +0 -0
  29. {turboapi-0.5.2 → turboapi-0.5.21}/CHANGELOG.md +0 -0
  30. {turboapi-0.5.2 → turboapi-0.5.21}/LICENSE +0 -0
  31. {turboapi-0.5.2 → turboapi-0.5.21}/Makefile +0 -0
  32. {turboapi-0.5.2 → turboapi-0.5.21}/README.md +0 -0
  33. {turboapi-0.5.2 → turboapi-0.5.21}/assets/architecture.png +0 -0
  34. {turboapi-0.5.2 → turboapi-0.5.21}/assets/benchmark_latency.png +0 -0
  35. {turboapi-0.5.2 → turboapi-0.5.21}/assets/benchmark_speedup.png +0 -0
  36. {turboapi-0.5.2 → turboapi-0.5.21}/assets/benchmark_throughput.png +0 -0
  37. {turboapi-0.5.2 → turboapi-0.5.21}/benches/async_comparison_bench.py +0 -0
  38. {turboapi-0.5.2 → turboapi-0.5.21}/benches/performance_bench.rs +0 -0
  39. {turboapi-0.5.2 → turboapi-0.5.21}/benches/python_benchmark.py +0 -0
  40. {turboapi-0.5.2 → turboapi-0.5.21}/benchmarks/README.md +0 -0
  41. {turboapi-0.5.2 → turboapi-0.5.21}/benchmarks/bench_json.py +0 -0
  42. {turboapi-0.5.2 → turboapi-0.5.21}/benchmarks/bench_memory.py +0 -0
  43. {turboapi-0.5.2 → turboapi-0.5.21}/benchmarks/bench_throughput.py +0 -0
  44. {turboapi-0.5.2 → turboapi-0.5.21}/benchmarks/bench_validation.py +0 -0
  45. {turboapi-0.5.2 → turboapi-0.5.21}/benchmarks/generate_charts.py +0 -0
  46. {turboapi-0.5.2 → turboapi-0.5.21}/benchmarks/run_benchmarks.py +0 -0
  47. {turboapi-0.5.2 → turboapi-0.5.21}/docs/ARCHITECTURE.md +0 -0
  48. {turboapi-0.5.2 → turboapi-0.5.21}/docs/ASYNC_HANDLERS.md +0 -0
  49. {turboapi-0.5.2 → turboapi-0.5.21}/docs/BENCHMARKS.md +0 -0
  50. {turboapi-0.5.2 → turboapi-0.5.21}/docs/HTTP2.md +0 -0
  51. {turboapi-0.5.2 → turboapi-0.5.21}/docs/PERFORMANCE_TUNING.md +0 -0
  52. {turboapi-0.5.2 → turboapi-0.5.21}/docs/README.md +0 -0
  53. {turboapi-0.5.2 → turboapi-0.5.21}/docs/TLS_SETUP.md +0 -0
  54. {turboapi-0.5.2 → turboapi-0.5.21}/docs/WEBSOCKET.md +0 -0
  55. {turboapi-0.5.2 → turboapi-0.5.21}/examples/authentication_demo.py +0 -0
  56. {turboapi-0.5.2 → turboapi-0.5.21}/examples/multi_route_app.py +0 -0
  57. {turboapi-0.5.2 → turboapi-0.5.21}/python/MANIFEST.in +0 -0
  58. {turboapi-0.5.2 → turboapi-0.5.21}/python/setup.py +0 -0
  59. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/__init__.py +0 -0
  60. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/async_limiter.py +0 -0
  61. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/async_pool.py +0 -0
  62. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/background.py +0 -0
  63. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/datastructures.py +0 -0
  64. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/decorators.py +0 -0
  65. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/encoders.py +0 -0
  66. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/exceptions.py +0 -0
  67. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/main_app.py +0 -0
  68. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/middleware.py +0 -0
  69. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/models.py +0 -0
  70. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/openapi.py +0 -0
  71. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/responses.py +0 -0
  72. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/routing.py +0 -0
  73. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/security.py +0 -0
  74. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/server_integration.py +0 -0
  75. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/staticfiles.py +0 -0
  76. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/status.py +0 -0
  77. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/templating.py +0 -0
  78. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/testclient.py +0 -0
  79. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/version_check.py +0 -0
  80. {turboapi-0.5.2 → turboapi-0.5.21}/python/turboapi/websockets.py +0 -0
  81. {turboapi-0.5.2 → turboapi-0.5.21}/src/http2.rs +0 -0
  82. {turboapi-0.5.2 → turboapi-0.5.21}/src/lib.rs +0 -0
  83. {turboapi-0.5.2 → turboapi-0.5.21}/src/micro_bench.rs +0 -0
  84. {turboapi-0.5.2 → turboapi-0.5.21}/src/middleware.rs +0 -0
  85. {turboapi-0.5.2 → turboapi-0.5.21}/src/python_worker.rs +0 -0
  86. {turboapi-0.5.2 → turboapi-0.5.21}/src/request.rs +0 -0
  87. {turboapi-0.5.2 → turboapi-0.5.21}/src/response.rs +0 -0
  88. {turboapi-0.5.2 → turboapi-0.5.21}/src/router.rs +0 -0
  89. {turboapi-0.5.2 → turboapi-0.5.21}/src/threadpool.rs +0 -0
  90. {turboapi-0.5.2 → turboapi-0.5.21}/src/tls.rs +0 -0
  91. {turboapi-0.5.2 → turboapi-0.5.21}/src/validation.rs +0 -0
  92. {turboapi-0.5.2 → turboapi-0.5.21}/src/websocket.rs +0 -0
  93. {turboapi-0.5.2 → turboapi-0.5.21}/src/zerocopy.rs +0 -0
  94. {turboapi-0.5.2 → turboapi-0.5.21}/tests/README.md +0 -0
  95. {turboapi-0.5.2 → turboapi-0.5.21}/tests/benchmark_comparison.py +0 -0
  96. {turboapi-0.5.2 → turboapi-0.5.21}/tests/comparison_before_after.py +0 -0
  97. {turboapi-0.5.2 → turboapi-0.5.21}/tests/fastapi_equivalent.py +0 -0
  98. {turboapi-0.5.2 → turboapi-0.5.21}/tests/quick_body_test.py +0 -0
  99. {turboapi-0.5.2 → turboapi-0.5.21}/tests/quick_test.py +0 -0
  100. {turboapi-0.5.2 → turboapi-0.5.21}/tests/test.py +0 -0
  101. {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_comprehensive_parity.py +0 -0
  102. {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_fastapi_compatibility.py +0 -0
  103. {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_fastapi_parity.py +0 -0
  104. {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_satya_0_4_0_compatibility.py +0 -0
  105. {turboapi-0.5.2 → turboapi-0.5.21}/tests/test_security_features.py +0 -0
  106. {turboapi-0.5.2 → turboapi-0.5.21}/tests/wrk_benchmark.py +0 -0
  107. {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/
@@ -1709,7 +1709,7 @@ dependencies = [
1709
1709
 
1710
1710
  [[package]]
1711
1711
  name = "turbonet"
1712
- version = "0.5.2"
1712
+ version = "0.5.21"
1713
1713
  dependencies = [
1714
1714
  "anyhow",
1715
1715
  "bytes",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "turbonet"
3
- version = "0.5.2"
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.2
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
- Provides-Extra: benchmark
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"/>
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "turboapi"
7
- version = "0.5.2"
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
- json_data = json.loads(body.decode('utf-8'))
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 (json.JSONDecodeError, UnicodeDecodeError):
352
- # Keep as string for HTML/Text responses
353
- body = body.decode('utf-8')
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
- let future = Python::with_gil(|py| {
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", body_bytes.as_ref())
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 for Response objects (JSONResponse, HTMLResponse, etc.)
92
- // These have a 'body' attribute that contains the serialized content
93
- if let Ok(body_attr) = obj.getattr("body") {
94
- if let Ok(status_attr) = obj.getattr("status_code") {
95
- // This is a Response object - extract and serialize the body content
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's valid JSON, use it directly
100
- if json_str.starts_with('{')
101
- || json_str.starts_with('[')
102
- || json_str.starts_with('"')
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.push(b'"');
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
- // Use simd-json for fast parsing
360
+ // Try simd-json first for fast parsing
360
361
  let mut body_copy = body.to_vec();
361
- let parsed = simd_json::to_borrowed_value(&mut body_copy)
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 dict = PyDict::new(py);
364
+ if let Ok(parsed) = simd_result {
365
+ let dict = PyDict::new(py);
365
366
 
366
- // Only handle object (dict) bodies
367
- if let simd_json::BorrowedValue::Object(map) = parsed {
368
- for (key, value) in map.iter() {
369
- set_simd_value_into_dict(py, key.as_ref(), value, &dict)?;
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
- return Err(pyo3::exceptions::PyValueError::new_err(
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.