Define explicit StreamingBody API, add httpx read(amt) support#1539
Define explicit StreamingBody API, add httpx read(amt) support#1539
Conversation
Remove wrapt.ObjectProxy from StreamingBody and HttpxStreamingBody to stop leaking backend-specific internals. Both classes now provide the same well-defined async streaming API (read, readinto, readlines, iter_lines, iter_chunks, tell, readable, close) with a .raw_stream property for advanced access. Key changes: - StreamingBody: plain class with _raw_stream instead of __wrapped__ - HttpxStreamingBody: buffered read(amt) via aiter_bytes(), full API parity - _ChecksumMixin: eliminates duplicated checksum validation code - endpoint.py: passes content-length to HttpxStreamingBody - 20 new tests covering HttpxStreamingBody thoroughly Closes #1365 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
|
Important Installation incomplete: to start using Gemini Code Assist, please ask the organization owner(s) to visit the Gemini Code Assist Admin Console and sign the Terms of Services. |
Merge origin/main into feature branch. Add note about default branch to CLAUDE.md. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Add tests for tell() and readlines() on StreamingBody (aiohttp). Remove @pytest.mark.moto/@pytest.mark.asyncio from httpx unit tests since they are not moto integration tests. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1539 +/- ##
==========================================
+ Coverage 93.86% 94.11% +0.25%
==========================================
Files 77 77
Lines 8218 8434 +216
==========================================
+ Hits 7714 7938 +224
+ Misses 504 496 -8
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Tests and user code call .aclose() on response bodies. With wrapt.ObjectProxy removed, this no longer proxies through to the underlying stream. Add explicit aclose() methods. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
|
I'm all for dropping |
|
@jakob-keller great point, let me point the agent at the context, this was a simple v1 agent draft |
|
@claude can you review the pr cmts and related issue to see if there's a better design that follows our repo design patterns |
Design analysis: aligning with repo patternsLooking at the PR diff, #1365 discussion, and the repo's The deviation @jakob-keller identifiedEvery other aiobotocore override follows Pattern 1 from
Examples: The PR's Recommended design
Subclass upstream directly. class AioStreamingBody(botocore.response.StreamingBody):
def __init__(self, raw_stream: aiohttp.ClientResponse, content_length):
# Don't call super().__init__() — it expects urllib3
# Call IOBase.__init__() directly
IOBase.__init__(self)
self._raw_stream = raw_stream # same attribute name as botocore
self._content_length = content_length # same attribute name as botocore
self._amount_read = 0 # same attribute name as botocore
async def read(self, amt=None): ... # overrides botocore sync read()
async def readinto(self, b): ...
async def readlines(self): ...
def __aiter__(self): ...
async def __anext__(self): ...
async def __aenter__(self): ...
async def __aexit__(self): ...
async def iter_lines(self, ...): ...
async def iter_chunks(self, ...): ...
async def close(self): ...
# tell(), _verify_content_length(), _DEFAULT_CHUNK_SIZE — inherit unchangedThis preserves
Subclass class AioHttpxStreamingBody(AioStreamingBody):
def __init__(self, raw_stream: httpx.Response, content_length=None):
IOBase.__init__(self)
self._raw_stream = raw_stream
self._content_length = content_length
self._amount_read = 0
self._buffer = b''
self._stream_iter = None
self._stream_exhausted = False
async def read(self, amt=None): ... # buffered read logic from PR
async def readinto(self, b): ...
async def close(self): ... # httpx uses aclose()
async def __aenter__(self): return self
async def __aexit__(self, *a): await self.close()
# Everything else inherits from AioStreamingBodyWhat to keep from the PR vs. what to change
On jakkdl's objection to subclassingThe concern was: "most methods change from sync to async, leaving only a few with the same signature". This is true of every The one real concern is SummaryThe PR's logic is sound; the buffered httpx 🤖 Generated with Claude Code |
|
I'll have my local claude do the refactor to save some money once i get some sleep ;) |
…amingBody Addresses @jakob-keller's PR review: align with the repo's standard pattern of subclassing the botocore upstream with `Aio` prefix, instead of standalone classes that duplicate logic. - AioStreamingBody(botocore.response.StreamingBody) — aiohttp backend. Async overrides for read/readinto/readlines/iter_*; sync API explicitly blocked (raises TypeError) so __iter__/__enter__ can't silently yield coroutines on this async-only class. set_socket_timeout raises NotImplementedError (urllib3-specific). - AioHttpxStreamingBody(AioStreamingBody) — httpx backend. Inherits all iteration, line-splitting, tell, content-length validation, and the __aenter__/__aexit__ contract; overrides only the I/O layer (buffered read(amt), readinto, async close). - _ChecksumMixin unchanged; checksum bodies renamed to AioStreamingChecksumBody / AioHttpxStreamingChecksumBody. - Old names (StreamingBody, HttpxStreamingBody, StreamingChecksumBody, HttpxStreamingChecksumBody) preserved as module-level aliases so external callers and isinstance checks continue to work. - AioStreamingBody.__aenter__ now returns self (was: raw aiohttp response), matching the httpx backend and mirroring the pattern used elsewhere in the repo. Use `body.raw_stream` for the underlying response object. Closes the design discussion from #1365.
|
@jakob-keller restructured the design to follow the repo's
Old class names ( One deliberate behavior change worth flagging: Re jakkdl's earlier objection in #1365 about subclassing being low-value: the win turned out to be sharing the iteration/validation layer across both backends (which we couldn't do without a common base), not method-signature reuse. Most methods do go sync→async, but that just means the base provides a stable type identity ( |
| try: | ||
| async for chunk in self._stream_iter: | ||
| chunks.append(chunk) | ||
| except StopAsyncIteration: |
Summary
Restructure aiobotocore's
StreamingBodyclasses to subclassbotocore.response.StreamingBody, following the repo's establishedAio*override pattern. Addresses @jakob-keller's review feedback that the original PR diverged from this convention.Class hierarchy
botocore.response.StreamingBodyAioStreamingBody— aiohttp backend, async I/O overridesAioHttpxStreamingBody— httpx backend, inherits all iteration/validation logic, only overrides the I/O layer (bufferedread(amt),readinto, asyncclose)AioStreamingChecksumBodyandAioHttpxStreamingChecksumBodymix in_ChecksumMixinover the corresponding base.What this delivers
wrapt.ObjectProxyso the API no longer leaks backend internals; users access the raw stream via.raw_stream.read(amt),readinto(),readlines(),__aiter__/__anext__,iter_lines(),iter_chunks(),tell(),readable(),close(), content-length validation. Previouslyread(amt)raisedValueErrorand most other methods didn't exist.HttpxStreamingChecksumBody.readintowhich calledcontent.read()on anhttpx.Response(would have failed at runtime).AioHttpxStreamingBodyinherit__aiter__,__anext__,iter_chunks,iter_lines,readlines,tell,_verify_content_lengthfromAioStreamingBody. Only the buffer machinery and httpx I/O layer live in the subclass.__iter__,__next__,__enter__,__exit__,set_socket_timeout) with explicit errors so users don't accidentally get coroutines fromfor chunk in bodyor call urllib3-shaped APIs on an aiohttp/httpx stream.StreamingBody = AioStreamingBody,HttpxStreamingBody = AioHttpxStreamingBody, same for the checksum classes. External callers and the existingisinstancechecks throughout the codebase continue to work unchanged.Behavior change
AioStreamingBody.__aenter__now returnsself(previously returned the raw aiohttpClientResponse). This matches the httpx side and is the standard pattern. Usebody.raw_streamfor the underlying response object. Noted in CHANGES.rst.Test plan
StreamingBodyunit tests passStreamingBodyunit tests passtest_httpchecksum.py,test_response.pyinbotocore_tests/) passtest_patches.py) pass — botocore class hash unchangedtest_configprovider::test_defaults_mode,test_eventstreams::test_kinesis_stream_json_parser) — unrelated to this PRCloses #1365
🤖 Generated with Claude Code