jpetrucciani / hubspot3

python3.6+ hubspot client based on hapipy, but modified to use the newer endpoints and non-legacy python
MIT License
147 stars 73 forks source link

Add support for custom storage of oauth2 tokens #78

Closed sangaline closed 4 years ago

sangaline commented 4 years ago

This implements support for using an external storage mechanism for oauth2 tokens to prevent contention issues when using multiple clients in different processes. The proposal was written out in #72, but I made some slight changes that felt natural when writing the code. This implementation replaces BaseClient.access_token and BaseClient.refresh_token with properties that defer to BaseClient.__access_token and BaseClient.__refresh_token when no external storage is configured. This behavior should be fully backwards compatible with how the library worked before this PR.

There are two new options that you can specify when initializing the base client that will change this default behavior: oauth2_token_getter: Optional[Callable[[Literal['access_token', 'refresh_token'], str], str]] = None and oauth2_token_setter: Optional[Callable[[Literal['access_token', 'refresh_token'], str, str], None]] = None. They both take the token type as the first argument and the client ID as the second argument. The setter has one additional argument that is the value of the token. These methods both default to None, but are used instead of deferring to BaseClient.__access_token and BaseClient.__refresh_token when the are provided. This allows using whichever shared storage mechanism you see fit to manage the tokens. If no tokens are retrieved from the getter method, then the properties will fall back to the internal variables. Even if the access_token is expired, this allows the client to get new tokens when the external storage is expired or otherwise unavailable.

Here's an example of how you can use the new functionality to store your oauth2 tokens in redis:

import aioredis
from hubspot3 import Hubspot3

redis_client = await aioredis.create_redis_pool(url, db=db, encoding='utf-8', timeout=10)

def oauth2_token_getter(token_type: str, client_id: str) -> str:
    loop = asyncio.get_event_loop()
    key = f'hubspot-oauth2-tokens:{token_type}:{client_id}'
    return loop.run_until_complete(redis_client.get(key))

def oauth2_token_setter(token_type: str, client_id: str, token: str) -> None:
    loop = asyncio.get_event_loop()
    key = f'hubspot-oauth2-tokens:{token_type}:{client_id}'
    # Token expiration is six hours, so match that when we store the tokens.
    # See: https://developers.hubspot.com/docs/methods/oauth2/refresh-access-token
    expire_in_seconds = 6 * 60 * 60
    loop.run_until_complete(redis_client.set(key, token, expire=expire_in_seconds))

# This client will share oauth2 credentials with other clients configured in the same way.
hubspot3_client = Hubspot3(
    access_token=access_token,
    client_id=client_id,
    client_secret=client_secret,
    refresh_token=refresh_token,
    oauth2_token_getter=oauth2_token_getter,
    oauth2_token_setter=oauth2_token_setter,
)

Closes #72

sangaline commented 4 years ago

Note that these new init arguments and attributes broke the pylint checking. I added exceptions to the too-many-arguments and too-many-instance-attributes rules in BaseClient to get tests to pass, not sure if you want to handle this in a different way.

jpetrucciani commented 4 years ago

Awesome! I'm out at AWS re:Invent right now, but I'll try to get around to reviewing and merging ASAP!

jpetrucciani commented 4 years ago

This is now live on pip as version 3.2.40

Thanks again for your contributions! 😄