From 618f94dfc4d13d45c783e99bc2aa11dd674a705c Mon Sep 17 00:00:00 2001 From: Qunfei Wu Date: Fri, 6 Feb 2026 20:10:26 +0100 Subject: [PATCH 1/6] fix the cd issue --- .github/workflows/cd.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 183f220..0baa008 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -61,13 +61,11 @@ jobs: run: | CARGO_VER=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') PYPROJECT_VER=$(grep '^version = ' pyproject.toml | head -1 | sed 's/version = "\(.*\)"/\1/') - INIT_VER=$(grep '__version__ = ' python/requestx/__init__.py | sed 's/__version__ = "\(.*\)"/\1/') echo "Cargo.toml: $CARGO_VER" echo "pyproject.toml: $PYPROJECT_VER" - echo "__init__.py: $INIT_VER" - if [ "$CARGO_VER" != "$PYPROJECT_VER" ] || [ "$CARGO_VER" != "$INIT_VER" ]; then + if [ "$CARGO_VER" != "$PYPROJECT_VER" ]; then echo "::error::Version mismatch! All versions must be identical." exit 1 fi From 47b95099c033c5b4901fbb5fc753ce62044f5f54 Mon Sep 17 00:00:00 2001 From: Qunfei Wu Date: Fri, 6 Feb 2026 20:40:01 +0100 Subject: [PATCH 2/6] fix the issue for test --- python/requestx/_response.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/python/requestx/_response.py b/python/requestx/_response.py index 706fe32..a807bf2 100644 --- a/python/requestx/_response.py +++ b/python/requestx/_response.py @@ -725,17 +725,29 @@ async def aiter_raw(self, chunk_size=None): self._raw_content = all_content self._stream_content = None else: - # No async stream, yield from content - content = self.content - if chunk_size is None: - if content: - self._num_bytes_downloaded += len(content) - yield content - else: - for i in range(0, len(content), chunk_size): - chunk = content[i : i + chunk_size] + # If this is a streaming response, delegate to Rust's aiter_bytes + if self._stream_not_read or self._is_stream: + self._stream_not_read = False # Clear flag + rust_iter = self._response.aiter_bytes(chunk_size) + all_content = b"" + async for chunk in rust_iter: + all_content += chunk self._num_bytes_downloaded += len(chunk) yield chunk + # Store content after iteration + self._raw_content = all_content + else: + # No async stream, yield from content + content = self.content + if chunk_size is None: + if content: + self._num_bytes_downloaded += len(content) + yield content + else: + for i in range(0, len(content), chunk_size): + chunk = content[i : i + chunk_size] + self._num_bytes_downloaded += len(chunk) + yield chunk async def aiter_bytes(self, chunk_size=None): """Async iterate over the response body as bytes chunks.""" From 6eb4f7c64adb32422bd3fd166518a66ff0ae7a24 Mon Sep 17 00:00:00 2001 From: Qunfei Wu Date: Fri, 6 Feb 2026 20:59:59 +0100 Subject: [PATCH 3/6] chore: bump version to 1.0.11 --- Cargo.toml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 759a20c..dd21ea8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "requestx" -version = "1.0.10" +version = "1.0.11" edition = "2021" description = "High-performance Python HTTP client based on reqwest" license = "MIT" diff --git a/pyproject.toml b/pyproject.toml index fe53131..f8b8a2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "requestx" -version = "1.0.10" +version = "1.0.11" description = "Highest-performance Python HTTP client based on Rust Speed" readme = "README.md" license = { text = "MIT" } From 54f0a251021217b4d1d410b4aae115c3b0d5df72 Mon Sep 17 00:00:00 2001 From: Qunfei Wu Date: Fri, 6 Feb 2026 21:57:23 +0100 Subject: [PATCH 4/6] fix the response rs --- src/response.rs | 16 ++++++--- tests_performance/test_stream.py | 62 ++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 tests_performance/test_stream.py diff --git a/src/response.rs b/src/response.rs index a975e65..2b14392 100644 --- a/src/response.rs +++ b/src/response.rs @@ -859,7 +859,9 @@ impl Response { return Err(pyo3::exceptions::PyRuntimeError::new_err("Attempted to call an async iterator method on a sync stream.")); } - if self.is_stream_consumed && self.stream.is_none() { + // Allow iteration if we have content (even if stream was previously consumed) + // Only block if we have no content AND stream was consumed + if self.is_stream_consumed && self.content.is_empty() && self.stream.is_none() { return Err(crate::exceptions::StreamConsumed::new_err( "Attempted to read or stream content, but the content has already been streamed.", )); @@ -900,7 +902,9 @@ impl Response { return Err(pyo3::exceptions::PyRuntimeError::new_err("Attempted to call an async iterator method on a sync stream.")); } - if self.is_stream_consumed && self.stream.is_none() { + // Allow iteration if we have content (even if stream was previously consumed) + // Only block if we have no content AND stream was consumed + if self.is_stream_consumed && self.content.is_empty() && self.stream.is_none() { return Err(crate::exceptions::StreamConsumed::new_err( "Attempted to read or stream content, but the content has already been streamed.", )); @@ -941,7 +945,9 @@ impl Response { return Err(pyo3::exceptions::PyRuntimeError::new_err("Attempted to call an async iterator method on a sync stream.")); } - if self.is_stream_consumed && self.stream.is_none() { + // Allow iteration if we have content (even if stream was previously consumed) + // Only block if we have no content AND stream was consumed + if self.is_stream_consumed && self.content.is_empty() && self.stream.is_none() { return Err(crate::exceptions::StreamConsumed::new_err( "Attempted to read or stream content, but the content has already been streamed.", )); @@ -962,7 +968,9 @@ impl Response { return Err(pyo3::exceptions::PyRuntimeError::new_err("Attempted to call an async iterator method on a sync stream.")); } - if self.is_stream_consumed && self.stream.is_none() { + // Allow iteration if we have content (even if stream was previously consumed) + // Only block if we have no content AND stream was consumed + if self.is_stream_consumed && self.content.is_empty() && self.stream.is_none() { return Err(crate::exceptions::StreamConsumed::new_err( "Attempted to read or stream content, but the content has already been streamed.", )); diff --git a/tests_performance/test_stream.py b/tests_performance/test_stream.py new file mode 100644 index 0000000..e1e87b6 --- /dev/null +++ b/tests_performance/test_stream.py @@ -0,0 +1,62 @@ +import unittest + +from http_benchmark.clients.aiohttp_adapter import AiohttpAdapter +from http_benchmark.clients.requestx_adapter import RequestXAdapter +from http_benchmark.models.http_request import HTTPRequest + + +class TestAiohttpStreaming(unittest.IsolatedAsyncioTestCase): + """Test async streaming functionality for AiohttpAdapter.""" + + async def asyncSetUp(self): + self.adapter = AiohttpAdapter() + await self.adapter.__aenter__() + self.request = HTTPRequest( + method="GET", + url="https://httpbin.org/stream/2", + stream=True, + timeout=30, + ) + + async def asyncTearDown(self): + await self.adapter.__aexit__(None, None, None) + + async def test_aiohttp_stream_async_request(self): + """Test async streaming request with aiohttp adapter.""" + result = await self.adapter.make_request_stream_async(self.request) + + self.assertTrue(result["success"]) + self.assertEqual(result["status_code"], 200) + self.assertTrue(result["streamed"]) + self.assertIn("chunk_count", result) + self.assertGreater(result["chunk_count"], 0) + # httpbin.org/stream/2 returns newline-delimited JSON + self.assertIn("id", result["content"]) + +class TestRequestXAsyncStreaming(unittest.IsolatedAsyncioTestCase): + """Test async streaming functionality for RequestXAdapter.""" + + async def asyncSetUp(self): + self.adapter = RequestXAdapter() + await self.adapter.__aenter__() + self.request = HTTPRequest( + method="GET", + url="https://httpbin.org/stream/2", + stream=True, + timeout=30, + ) + + async def asyncTearDown(self): + await self.adapter.__aexit__(None, None, None) + + async def test_requestx_stream_async_request(self): + """Test async streaming request with requestx adapter.""" + result = await self.adapter.make_request_stream_async(self.request) + + self.assertTrue(result["success"]) + self.assertEqual(result["status_code"], 200) + self.assertTrue(result["streamed"]) + self.assertIn("chunk_count", result) + self.assertGreater(result["chunk_count"], 0) + # httpbin.org/stream/2 returns newline-delimited JSON + self.assertIn("id", result["content"]) \ No newline at end of file From c0703a8294d22ddf2abd2f87a05566f53317c238 Mon Sep 17 00:00:00 2001 From: Qunfei Wu Date: Fri, 6 Feb 2026 22:00:18 +0100 Subject: [PATCH 5/6] clean the status --- tests_performance/test_stream.py | 62 -------------------------------- 1 file changed, 62 deletions(-) delete mode 100644 tests_performance/test_stream.py diff --git a/tests_performance/test_stream.py b/tests_performance/test_stream.py deleted file mode 100644 index e1e87b6..0000000 --- a/tests_performance/test_stream.py +++ /dev/null @@ -1,62 +0,0 @@ -import unittest - -from http_benchmark.clients.aiohttp_adapter import AiohttpAdapter -from http_benchmark.clients.requestx_adapter import RequestXAdapter -from http_benchmark.models.http_request import HTTPRequest - - -class TestAiohttpStreaming(unittest.IsolatedAsyncioTestCase): - """Test async streaming functionality for AiohttpAdapter.""" - - async def asyncSetUp(self): - self.adapter = AiohttpAdapter() - await self.adapter.__aenter__() - self.request = HTTPRequest( - method="GET", - url="https://httpbin.org/stream/2", - stream=True, - timeout=30, - ) - - async def asyncTearDown(self): - await self.adapter.__aexit__(None, None, None) - - async def test_aiohttp_stream_async_request(self): - """Test async streaming request with aiohttp adapter.""" - result = await self.adapter.make_request_stream_async(self.request) - - self.assertTrue(result["success"]) - self.assertEqual(result["status_code"], 200) - self.assertTrue(result["streamed"]) - self.assertIn("chunk_count", result) - self.assertGreater(result["chunk_count"], 0) - # httpbin.org/stream/2 returns newline-delimited JSON - self.assertIn("id", result["content"]) - -class TestRequestXAsyncStreaming(unittest.IsolatedAsyncioTestCase): - """Test async streaming functionality for RequestXAdapter.""" - - async def asyncSetUp(self): - self.adapter = RequestXAdapter() - await self.adapter.__aenter__() - self.request = HTTPRequest( - method="GET", - url="https://httpbin.org/stream/2", - stream=True, - timeout=30, - ) - - async def asyncTearDown(self): - await self.adapter.__aexit__(None, None, None) - - async def test_requestx_stream_async_request(self): - """Test async streaming request with requestx adapter.""" - result = await self.adapter.make_request_stream_async(self.request) - - self.assertTrue(result["success"]) - self.assertEqual(result["status_code"], 200) - self.assertTrue(result["streamed"]) - self.assertIn("chunk_count", result) - self.assertGreater(result["chunk_count"], 0) - # httpbin.org/stream/2 returns newline-delimited JSON - self.assertIn("id", result["content"]) \ No newline at end of file From fb91147994005c4d86da49fae6bfe83c7b13e8c1 Mon Sep 17 00:00:00 2001 From: Qunfei Wu Date: Fri, 6 Feb 2026 22:02:32 +0100 Subject: [PATCH 6/6] chore: bump version to 1.0.12 --- Cargo.toml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dd21ea8..debdae3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "requestx" -version = "1.0.11" +version = "1.0.12" edition = "2021" description = "High-performance Python HTTP client based on reqwest" license = "MIT" diff --git a/pyproject.toml b/pyproject.toml index f8b8a2e..a454f5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "requestx" -version = "1.0.11" +version = "1.0.12" description = "Highest-performance Python HTTP client based on Rust Speed" readme = "README.md" license = { text = "MIT" }