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 diff --git a/Cargo.toml b/Cargo.toml index 759a20c..debdae3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "requestx" -version = "1.0.10" +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 fe53131..a454f5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "requestx" -version = "1.0.10" +version = "1.0.12" description = "Highest-performance Python HTTP client based on Rust Speed" readme = "README.md" license = { text = "MIT" } 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.""" 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.", ));