Uh oh!
There was an error while loading. Please reload this page.
Add support for sync-specific or async-specific auth flows - #1217
Conversation
eaf43b3 to
be809fbComparelovelydinosaur
commented
Aug 26, 2020
Great! A bit more discussion about this over at https://github.com/encode/httpx/issues/1176#issuecomment-680763226 |
Okay, I'm getting warm fuzzies about this. 😻 Suggestion that I'm curious to get your feedback on... What do we think to also:
So, something like this... classAuth:
""" Base class for all authentication schemes. To implement a custom authentication scheme, subclass `Auth` and override the `.auth_flow()` method. If the authentication scheme does I/O, such as disk access or network calls, or uses synchronization primitives such as locks, you should override `.async_auth_flow()` to provide an async-friendly implementation that will be used by the `AsyncClient`. Usage of sync I/O within an async codebase would block the event loop, and could cause performance issues. """requires_request_body=Falserequires_response_body=Falsedefauth_flow(self, request: Request) ->typing.Generator[Request, Response, None]:
""" Execute the authentication flow. To dispatch a request, `yield` it: ``` yield request ``` The client will `.send()` the response back into the flow generator. You can access it like so: ``` response = yield request ``` A `return` (or reaching the end of the generator) will result in the client returning the last response obtained from the server. You can dispatch as many requests as is necessary. """# We could *potentially* do something more clever here to return# more helpful errors if `sync_auth_flow`/`async_auth_flow` *has* been overridden,# but the auth flow is being used in the incorrect sync/async context.raiseNotImplementedError("Override 'auth_flow' to implement a custom auth class.")
yieldrequest# pragma: nocover defsync_auth_flow(
self, request: Request
) ->typing.AsyncGenerator[Request, Response]:
""" Execute the authentication flow synchronously. By default, this defers to `.auth_flow()`. You should override this method when the authentication scheme does I/O, such as disk access or network calls, or uses concurrency primitives such as locks. """ifself.requires_request_body:
request.read()
flow=self.auth_flow(request)
request=next(flow)
whileTrue:
response=yieldrequestifself.requires_response_body:
response.read()
try:
request=flow.send(response)
exceptStopIteration:
breakasyncdefasync_auth_flow(
self, request: Request
) ->typing.AsyncGenerator[Request, Response]:
""" Execute the authentication flow asynchronously. By default, this defers to `.auth_flow()`. You should override this method when the authentication scheme does I/O, such as disk access or network calls, or uses concurrency primitives such as locks. """ifself.requires_request_body:
awaitrequest.aread()
flow=self.auth_flow(request)
request=next(flow)
whileTrue:
response=yieldrequestifself.requires_response_body:
awaitresponse.aread()
try:
request=flow.send(response)
exceptStopIteration:
breakHere's why that might be a nice thing to do...
Also...
So I think testing looks something like this?... (Okay, we can't actually create Test for Basic Authauth=httpx.BasicAuth()
request=httpx.Request("GET", "https://www.example.com")
# The initial request should include a basic auth header.flow=auth.sync_auth_flow(request)
request=next(flow)
assertrequest.headers['Authorization'].startswith('Basic')
# No other requests are made.response=httpx.Response(text='Hello, world!', status_code=200)
withpytest.raises(StopIteration):
flow.send(response)Test for Digest Auth with a 200 responseauth=httpx.DigestAuth()
request=httpx.Request("GET", "https://www.example.com")
# The initial request should not include an auth header.flow=auth.sync_auth_flow(request)
request=next(flow)
assert'Authorization'notinrequest.headers# If a 200 response is returned, then no other requests are made.response=httpx.Response(text='Hello, world!', status_code=200)
withpytest.raises(StopIteration):
flow.send(response)Test for Digest Auth with a 401 responseauth=httpx.DigestAuth()
request=httpx.Request("GET", "https://www.example.com")
# The initial request should not include an auth header.flow=auth.sync_auth_flow(request)
request=next(flow)
assert'Authorization'notinrequest.headers# If a 401 response is returned, then a digest auth request is made.headers= {'WWW-Authenticate': 'Digest realm="...", qop="...", nonce="...", opaque="..."'}
response=httpx.Response(text='Auth required', status_code=401, headers=headers)
request=flow.send(response)
assertrequest.headers['Authorization'].startswith('Digest')
# No other requests are made.response=httpx.Response(text='Hello, world!', status_code=200)
withpytest.raises(StopIteration):
flow.send(response)Testing in an async context looks the same except calling What do we think? |
3d43466 to
3432c14CompareUh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
ba5d101 to
1ace944Compareflorimondmanca
commented
Sep 4, 2020
Added docs that were missing to make this PR complete — ready for a new round of reviews! |
Uh oh!
There was an error while loading. Please reload this page.
lovelydinosaur
commented
Sep 9, 2020
Added in auth unit tests for good measure. Anyways, this one is fantastic - very pleased with it! ✨🍰✨ |
Closes #1176, prompted by https://github.com/encode/httpx/issues/1176#issuecomment-680074866
Allow authors or auth classes to provide a sync-specific and/or async-specific implementation of the auth flow, so that they can do I/O, synchronization or concurrency without blocking the event loop:
Doesn't affect the user-side auth API, nor existing auth implementations.
Rendered docs: