-
-
Notifications
You must be signed in to change notification settings - Fork 190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for httpx as backend #1085
base: master
Are you sure you want to change the base?
Conversation
aiobotocore/httpsession.py
Outdated
|
||
# previously data was wrapped in _IOBaseWrapper | ||
# github.com/aio-libs/aiohttp/issues/1907 | ||
# I haven't researched whether that's relevant with httpx. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ya silly decision of aiohttp, they took over the stream. Most likely httpx does the right thing. I think to get around the sync/async thing we can just make a stream wrapper that hides the relevant methods...I think I did this somewhere...will try to remember
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would the current tests catch if httpx didn't do the right thing?
I started wondering whether The way that |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1085 +/- ##
==========================================
- Coverage 90.76% 89.93% -0.83%
==========================================
Files 68 69 +1
Lines 6542 6819 +277
==========================================
+ Hits 5938 6133 +195
- Misses 604 686 +82
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:
|
Whooooo, all tests are passing!!!! current TODOs:
codecov is very sad, but most of that is due to me duplicating code that wasn't covered to start with, or extending tests that aren't run in CI. I'll try to make it very slightly less sad, but making it completely unsad is very much out of scope for this PR. Likewise RTD is failing ... and I think that's unrelated to the PR? |
Add no-httpx run to CI on 3.12 Tests can now run without httpx installed. Exclude `if TYPE_CHECKING` blocks from coverage. various code cleanup
…ive errors on connector args not compatible with httpx. Remove proxy code and raise NotImplementedError. fix/add tests
@thejcannon @aneeshusa if you wanna do a review pass |
Hey @thehesiod what's the feeling on this? It is turning out to be a messier and more disruptive change than initially thought in #749. I can pull out some of the changes to a separate PR to make this a bit smaller at least |
hey sorry been down with a cold, will look asap. I don't mind big PRs |
if httpx and isinstance(aio_session, httpx.AsyncClient): | ||
async with aio_session.stream("GET", presigned_url) as resp: | ||
data = await resp.aread() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what do you think of making an adapter class so no test changes are necessary. We can expose the raw stream via a new property if needed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no test changes is not going to be possible - see #1085 (comment). But I could try to make one that implements a bunch of the basic functionality which would minimize test changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, translating between await resp.read()
and await resp.aread()
with an adapter class is trivial.
But having an adapter class turn calls to resp['Body'].close()
into await resp['Body'].aclose
...
I guess it's possible in theory to hook into the currently running async framework and run .aclose()
from a sync .close()
in an adapter class... but that feels like a very bad idea. Especially as we're looking to support anyio and structured concurrency.
I suppose I could write a wrapper class that .. only translates read
->aread
, and gives specific errors for the other ones? It could maybe help transitioning code currently written, but I think perhaps more appropriate is to if/when dropping aiohttp we make the adapter class raise DeprecationError
if calling .read()
or .close()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the issue is you didn't wrap the body in the StreamingBody, that will solve these issues. For the close
I think aiohttp did a bad decision, it should have been an async method. I think we should have a sync close method on the StreamingBody that creates a task around aclose and returns that task. That way if you call it sync it will be fine because eventually it will get closed, and if you want you can await it to wait for it to actually get closed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should have a sync close method on the StreamingBody that creates a task around aclose and returns that task. That way if you call it sync it will be fine because eventually it will get closed, and if you want you can await it to wait for it to actually get closed.
this will not play well with structured concurrency, and even with asyncio I think it's gonna be hard to guarantee it actually executes. But I think most reasonable people will be using the context-manager form anyway, so it's probably fine to have close
return an informative error (with httpx) + add aclose
to Streamingbody
tests/test_basic_s3.py
Outdated
if current_http_backend == 'httpx': | ||
assert key == 'key./name' | ||
else: | ||
assert key == 'key./././name' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, ya this needs to be fixed. We can't have the response changing. This should match whatever botocore does, if current way is incorrect this is fine, otherwise needs to be fixed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current behaviour is indeed how botocore handles it:
https://github.com/boto/botocore/blob/970d577087d404b0927cc27dc57178e01a3371cd/tests/integration/test_s3.py#L599-L606
so I'll have to do some digging
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
digging success!... but looks like it needs changes in upstream httpx to get fixed.
When we call self._session.build_request
in
aiobotocore/aiobotocore/httpsession.py
Lines 451 to 456 in b19bc09
httpx_request = self._session.build_request( | |
method=request.method, | |
url=url, | |
headers=headers, | |
content=content, | |
) |
httpx turns the string url into a httpx.URL
object, which explicitly normalizes the path https://github.com/encode/httpx/blob/392dbe45f086d0877bd288c5d68abf860653b680/httpx/_urlparse.py#L387
We can manually create a httpx.URL
object to be passed into httpx.build_request
, but there's no parameter that control normalization, so at that point we'd have to create a subclass of httpx.URL
or something to customize the behaviour.
I can open an issue for it in the httpx repo, but I'll first try to figure out why botocore seems to explicitly not want to normalize the path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay so the reason these slashes aren't normalized is because having slashes (and periods) in a key name is allowed
But I think if we can replace the slashes in the key name with %2F
somewhere along the chain then I think it'll be handled correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the current percent encoding happens deep within the guts of botocore, where they explicitly mark /
as safe for some reason: https://github.com/boto/botocore/blob/970d577087d404b0927cc27dc57178e01a3371cd/botocore/serialize.py#L520
Why? no clue. Can it be marked unsafe? I tried and ran unit tests and some functional tests in botocore/, and some tests did start to fail - though unclear if they're bad errors or just defensive asserts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has been requested in httpx since forever: encode/httpx#1805
let's see if me offering to write a PR can give it some traction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ya main thing is downstream consumers may be doing string matching and could break logic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
appreciate the digging!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
turns out httpx does have a way to do it now! encode/httpx#1805 (reply in thread)
thank you for the quick update! |
aiobotocore/endpoint.py
Outdated
if httpx and isinstance(http_response.raw, httpx.Response): | ||
response_dict['body'] = http_response.raw |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should keep the StreamingBody
as this is our own version so we can adapt as necessary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went into detail on this in #1085 (comment)
But if we want to get rid of e.g. aread
vs read
differences maybe it could make sense to create a HttpxStreamingBody
that tries to strike a middle ground
@@ -286,6 +287,10 @@ async def test_sso_token_provider_refresh(test_case): | |||
cache_key = "d033e22ae348aeb5660fc2140aec35850c4da997" | |||
token_cache = {} | |||
|
|||
# deepcopy the test case so the test can be parametrized against the same | |||
# test case w/ aiohttp & httpx | |||
test_case = deepcopy(test_case) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh man, parametrize
should be returning a frozendict. That's crazy of botocore changing global state in a test, we inherited this code from botocore: https://github.com/boto/botocore/blob/develop/tests/unit/test_tokens.py#L26
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah it sounds like botocore should get a PR
@pytest.fixture | ||
def skip_httpx(current_http_backend: str) -> None: | ||
if current_http_backend == 'httpx': | ||
pytest.skip('proxy support not implemented for httpx') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
better put this in conftest.py
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why did I ever make it a fixture in the first place?? 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, because it fails in the setup of test_fail_proxy_request
so I can't skip in the test body. It's specifically only used by test_fail_proxy_request
so I think it should be in this file, but I'll add a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we could insert a bunch of logic into the s3_client
fixture or somewhere, but I think the logic for skipping should be next to the test it skips. If it was used for skipping several other tests as well I'd agree though
tests/test_basic_s3.py
Outdated
# TODO: think about better api and make behavior like in aiohttp | ||
resp['Body'].close() | ||
if httpx and isinstance(resp['Body'], httpx.Response): | ||
data = await resp['Body'].aread() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
given body is our own StreamResponse wrapper, i prefer we expose a more sane read
method to match botocore, their whole thing of read
vs aread
is very strange design decision. This will also reduce the diff
ok did a first pass, after StreamingBody fix I can do another pass for exceptions |
awesome work btw! |
I'm not sure why I'm getting spurious errors in Also the 3.13 fail is.. idk? codecov fail? |
I notice two things:
|
👍
This PR effectively doubles the number of tests, and the 3.13 in particular triples it with the no-httpx run, so it's not a big surprise that it's hitting timeout limits. Is it fine to just increase the 5-minute limit, or should I make no-httpx a separate CI run or something? (running full pre-commit in every CI job (taking 1 minute) is also kinda overkill, but that's not for this PR) |
Could we leverage the GitHub Action matrix for that purpose, i.e. add a "no-httpx" dimension or something? That would leverage parallelism and scale better IMO. |
ugh, giving up for today. github actions is dark magic, I'll go play around in a separate branch/fork and figure out wth is going on. But @thehesiod can still review the code, CI should be all green as soon as I get it to run again :) |
gah, I think I figured it out. Because you use
and likewise with the codecov upload on 3.11; since you're not actually using But I can work around it for now if you prefer it the way it is |
A minor problem with this is that you only upload code coverage from your 3.11 run, which means the code for specifically handling httpx not being installed does not get coverage. But this mostly just impacts stuff like this try:
import httpx
except ImportError: # pragma: no cover # tested in a different matrix entry
http = None |
First step of #749 as described in #749 (comment)
I was tasked with implementing this, but it's been a bit of a struggle not being very familiar with aiohttp, httpx or aiobotocore - and there being ~zero in-line types. But I think I've fixed enough of the major problems that it's probably useful to share my progress.
There's a bunch of random types added. I can split those off into a separate PR or remove if requested. Likewise for
from __future__ import annotations
.TODO:
aiobotocore/aiobotocore/httpsession.py
Lines 478 to 534 in b19bc09
but AFAICT you can only configure proxies per-client in httpx. So need to move the logic for it, and cannot usebotocore.httpsession.ProxyConfiguration.proxy_[url,headers]_for(request.url)
BOTO_EXPERIMENTAL__ADD_PROXY_HOST_HEADERseems not possible to do when configuring proxies per-client?No longer TODOs after changing the scope to implement httpx alongside aiohttp:
test_patches
previously cared about aiohttp. That can probably be retired?tests.mock_server.AIOServer
?NotImplementedError
:use_dns_cache
: did not find any mentions of dns caches on a quick skim of httpx docsforce_close
: same. Can maybe find out more by digging into docs on what this option does in aiohttp.resolver
: this is anaiohttp.abc.AbstractResolver
which is obviously a no-go.yarl.URL(url, encoding=True)
. httpx does not support yarl. I don't know what this achieved (maybe the non-normalization??), so skipping it for now.Some extra tests would probably also be good, but not super critical when we're just implementing httpx alongside aiohttp.