-
-
Notifications
You must be signed in to change notification settings - Fork 263
Removing AuthenticatedClient in favor of Client.with_authorization_header and Generics #1287
-
Hi, I've been exploring this package and am very excited about the output it generates. Especially the extensibility due to the jinja2 templating is great! Thank you for maintaining it. 🙇
After generating a client I noticed that there's a lot of duplication between AuthenticatedClient and Client. From what I can tell the usage of AuthenticatedClient is:
- To pre-load Client with authentication headers to authenticate requests.
- To have good type hinting in endpoint_module's, showing when an endpoint required authentication.
Setting authentication headers
Currently we solve for this usecase:
client = Client() # will make unauthenticated requests auth_client = AuthenticatedClient(token='my-token') # will make authenticated requests
However, when the token needs to change (f.e. b/c different users use different tokens) then we'd have to completely change the client. Leading to the httpx.ClientSession being recreated and causing overhead.
auth_client1 = AuthenticatedClient(token='user1-token') # will make authenticated requests for user1 auth_client2 = AuthenticatedClient(token='user2-token') # will make authenticated requests for user2
It might be nicer to instead have methods on Client to set and del authorization headers so that the Client can be longlived and the authentication can vary per request.
client = Client() client.with_authorization_token(token='user1-token') client.request() # will make auth requests for user1 client.with_authorization_token(token='user2-token')
The added benefit here is that we can solve for #823 by adding with_authorization_basic as well. Without duplicating Client more.
Type hinting
To make clear when an endpoint requires authentication headers set we can add a phantom type parameter.
from base64 import b64encode from typing import Dict, Generic, TypeVar, cast from attrs import define, evolve, field class Unauthenticated: ... class Authenticated: ... _State = TypeVar("_State") @define class _Client(Generic[_State]): _headers: Dict[str, str] = field(factory=dict, kw_only=True) def with_headers(self, headers: Dict[str, str]) -> "Client": return evolve(self, _headers={**self._headers, **headers}) def with_authorization_token( self: "Client", token: str, prefix: str = "Bearer", ) -> "AuthClient": return cast("AuthClient", self.with_headers({"Authorization": f"{prefix} {token}"})) def with_authorization_basic( self: "Client", username: str, password: str, prefix: str = "Basic", ) -> "AuthClient": encoded_credentials = b64encode(f"{username}:{password}".encode()).decode() return cast("AuthClient", self.with_headers({"Authorization": f"{prefix} {encoded_credentials}"})) def remove_headers(self: "AuthClient") -> "Client": return evolve(self, _headers={}) Client = _Client[Unauthenticated] AuthClient = _Client[Authenticated] def my_func(client: AuthClient) -> None: # only accepts authenticated clients print("got headers:", client._headers) # --- usage demos --- c0 = Client() # Unauthenticated my_func(c0) # static type error c1 = c0.with_authorization_token("my-token") # Authenticated my_func(c1) # ok c2 = c1.remove_headers() # back to Unauthenticated my_func(c2) # static type error c1 = c0.with_authorization_basic("my-username", "my-password") # Authenticated my_func(c1) # ok
Let me know what you think. I'm happy to suggest the PR 🙇 and thanks again!
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 2 comments
-
Oh, and this would be a breaking change as AuthenticatedClient would not take token as input anymore.
Beta Was this translation helpful? Give feedback.
All reactions
-
Beta Was this translation helpful? Give feedback.