Search code examples
djangogoogle-apigoogle-oauthdjango-allauthgoogle-python-api

Chronic timeout issue with Google's API via python?


I am working on a little web app to help me manage my gmail. I have set it up through Google's API with the following function using the OAuth token I received through django-allauth.

import google.oauth2.credentials
from .choices import GMAIL
from allauth.socialaccount.models import SocialToken, SocialAccount
from apiclient.discovery import build


def get_credentials(user):
    account = SocialAccount.objects.get(user=user.id)
    token = SocialToken.objects.get(app=GMAIL, account=account).token
    credentials = google.oauth2.credentials.Credentials(token)
    service = build('gmail', 'v1', credentials=credentials)
    return service

This seems to work sometimes, but unfortunately, it isn't very reliable. It times out frequently at the build() function, only succeeding about a third of the time. I am wondering what could cause this behavior and if there is a more reliable way to access the API?

I found the following AuthorizedSession class from these docs:

from google.auth.transport.requests import AuthorizedSession

authed_session = AuthorizedSession(credentials)

response = authed_session.request(
    'GET', 'https://www.googleapis.com/storage/v1/b')

But I don't know how to turn it into the kind of object that works with Google's API:

def get_labels(user):
    service = get_credentials(user)
    results = service.users().labels().list(userId='me').execute()
    labels = results.get('labels', [])
    return labels

Unfortunately, Google's docs recommend using a deprecated package that I was hoping to avoid.

This is my first time really using an OAuth-enforced API. Does anyone have any advice?

EDIT: I posted after trying from my Macbook. I tried it on my Windows machine as well, where it works more consistently, but it takes about 20 seconds each time just to do build(). I feel like I am doing something wrong.


Solution

  • This is working much better this morning after I finally added the refresh token into the mix. Perhaps it's absence was causing some issues on the other end. I will continue testing, but it everything is working well right now:

    Here is my full solution:

    import google.oauth2.credentials
    from google.auth.transport.requests import Request
    
    from apiclient import errors, discovery
    
    from myproject.settings import GMAIL_CLIENT_API_KEY, GMAIL_CLIENT_API_SECRET
    
    
    def get_credentials(user):
        token_set = user.socialaccount_set.first().socialtoken_set.first()
        token = token_set.token
        refresh_token = token_set.token_secret
        credentials = google.oauth2.credentials.Credentials(
            token,
            refresh_token=refresh_token,
            client_id=GMAIL_CLIENT_API_KEY,
            client_secret=GMAIL_CLIENT_API_SECRET,
            token_uri= google.oauth2.credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT,
        )
    
        if credentials.expired:
            request = Request()
            credentials.refresh(request)
    
        service = discovery.build('gmail', 'v1', credentials=credentials)
        return service
    

    As you can see, I added my API keys into my settings file and referenced them here. I happened to stumble upon the token_uri while nosing around in the source files yesterday. The refresh_token was the piece that took the longest to find.

    The good news is that django-allauth will save the refresh token to SocialToken model under the token_secret column. However, it will only do this if the following is in your settings:

    SOCIALACCOUNT_PROVIDERS = {
        'google': {
            'SCOPE': [
                'profile',
                'email',
                'https://www.googleapis.com/auth/gmail.labels',
                'https://www.googleapis.com/auth/gmail.modify'
            ],
            'AUTH_PARAMS': {
                'access_type': 'offline',
            }
        }
    }
    

    Specifically, the access_type in AUTH_PARAMS must be set to 'offline' according to the docs.

    Now, this will still give you trouble if you connected your account before you implemented this change, so you'll also need to revoke access to your app through your Google permissions to obtain a new refresh token. More about that can be found in this question.

    I'm not sure if this is the proper/intended way of doing this, but until Google updates their docs for Django, this approach should work at least for testing purposes.