Skip to content
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

gmail: replace deprecated oauth2client package with google-auth-oauthlib #743

Merged
merged 6 commits into from
Sep 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions bugwarrior/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,26 @@ bugwarrior - Pull tickets from github, bitbucket, bugzilla, jira, trac, and othe

It currently supports the following remote resources:

- `github <https://github.com>`_ (api v3)
- `gitlab <https://gitlab.com>`_ (api v3)
- `activecollab <https://www.activecollab.com>`_ (2.x and 4.x)
- `bitbucket <https://bitbucket.org>`_
- `pagure <https://pagure.io/>`_
- `bugzilla <https://www.bugzilla.org/>`_
- `Debian BTS <https://bugs.debian.org/>`_
- `trac <https://trac.edgewall.org/>`_
- `gerrit <https://www.gerritcodereview.com/>`_
- `github <https://github.com>`_ (api v3)
- `gitlab <https://gitlab.com>`_ (api v3)
- `gmail <https://www.google.com/gmail/about/>`_
- `jira <https://www.atlassian.com/software/jira/overview>`_
- `megaplan <https://www.megaplan.ru/>`_
- `teamlab <https://www.teamlab.com/>`_
- `pagure <https://pagure.io/>`_
- `phabricator <http://phabricator.org/>`_
- `Pivotal Tracker <https://www.pivotaltracker.com/>`_
- `redmine <https://www.redmine.org/>`_
- `jira <https://www.atlassian.com/software/jira/overview>`_
- `taiga <https://taiga.io>`_
- `gerrit <https://www.gerritcodereview.com/>`_
- `activecollab <https://www.activecollab.com>`_ (2.x and 4.x)
- `phabricator <http://phabricator.org/>`_
- `versionone <http://www.versionone.com/>`_
- `teamlab <https://www.teamlab.com/>`_
- `trac <https://trac.edgewall.org/>`_
- `trello <https://trello.com/>`_
- `versionone <http://www.versionone.com/>`_
- `youtrack <https://www.jetbrains.com/youtrack/>`_
- `Pivotal Tracker <https://www.pivotaltracker.com/>`_

Documentation
-------------
Expand Down
40 changes: 25 additions & 15 deletions bugwarrior/services/gmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
import logging
import multiprocessing
import os
import pickle
import re
import time

import googleapiclient.discovery
import httplib2
import oauth2client.client
import oauth2client.file
import oauth2client.tools
from google.auth.transport.requests import Request
from google_auth_oauthlib.flow import InstalledAppFlow

from bugwarrior.services import IssueService, Issue

Expand Down Expand Up @@ -103,7 +102,7 @@ def get_entry(self):

class GmailService(IssueService):
APPLICATION_NAME = 'Bugwarrior Gmail Service'
SCOPES = 'https://www.googleapis.com/auth/gmail.readonly'
SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']
DEFAULT_CLIENT_SECRET_PATH = '~/.gmail_client_secret.json'

ISSUE_CLASS = GmailIssue
Expand All @@ -122,16 +121,15 @@ def __init__(self, *args, **kw):
credentials_name = clean_filename(self.login_name if self.login_name != 'me' else self.target)
self.credentials_path = os.path.join(
self.config.data.path,
'gmail_credentials_%s.json' % (credentials_name,))
'gmail_credentials_%s.pickle' % (credentials_name,))
self.gmail_api = self.build_api()

def get_config_path(self, varname, default_path=None):
return os.path.expanduser(self.config.get(varname, default_path))

def build_api(self):
credentials = self.get_credentials()
http = credentials.authorize(httplib2.Http())
return googleapiclient.discovery.build('gmail', 'v1', http=http)
return googleapiclient.discovery.build('gmail', 'v1', credentials=credentials, cache_discovery=False)

def get_credentials(self):
"""Gets valid user credentials from storage.
Expand All @@ -144,14 +142,26 @@ def get_credentials(self):
"""
with self.AUTHENTICATION_LOCK:
log.info('Starting authentication for %s', self.target)
store = oauth2client.file.Storage(self.credentials_path)
credentials = store.get()
if not credentials or credentials.invalid:
credentials = None
# The self.credentials_path file stores the user's access and refresh
# tokens as a pickle, and is created automatically when the
# authorization flow completes for the first time.
if os.path.exists(self.credentials_path):
with open(self.credentials_path, 'rb') as token:
credentials = pickle.load(token)

# If there are no (valid) credentials available, let the user log in.
if not credentials or not credentials.valid:
log.info("No valid login. Starting OAUTH flow.")
flow = oauth2client.client.flow_from_clientsecrets(self.client_secret_path, self.SCOPES)
flow.user_agent = self.APPLICATION_NAME
flags = oauth2client.tools.argparser.parse_args([])
credentials = oauth2client.tools.run_flow(flow, store, flags)
if credentials and credentials.expired and credentials.refresh_token:
credentials.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
self.client_secret_path, self.SCOPES)
credentials = flow.run_local_server(port=0)
# Save the credentials for the next run
with open(self.credentials_path, 'wb') as token:
pickle.dump(credentials, token)
log.info('Storing credentials to %r', self.credentials_path)
return credentials

Expand Down
36 changes: 18 additions & 18 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,28 +28,28 @@
include_package_data=True,
zip_safe=False,
install_requires=[
"requests",
"taskw >= 0.8",
"click",
"dogpile.cache>=0.5.3",
"future",
"jinja2>=2.7.2",
"lockfile>=0.9.1",
"python-dateutil",
"pytz",
"requests",
"six>=1.9.0",
"jinja2>=2.7.2",
"dogpile.cache>=0.5.3",
"lockfile>=0.9.1",
"click",
"future",
"taskw>=0.8",
],
extras_require=dict(
keyring=["keyring"],
jira=["jira>=0.22"],
megaplan=["megaplan>=1.4"],
activecollab=["pypandoc", "pyac>=0.1.5"],
bts=["PySimpleSOAP", "python-debianbts>=2.6.1"],
trac=["offtrac"],
bugzilla=["python-bugzilla>=2.0.0"],
gmail=["google-api-python-client", "oauth2client<4.0.0"],
phabricator=["phabricator"],
),
extras_require={
"activecollab": ["pypandoc", "pyac>=0.1.5"],
"bts": ["PySimpleSOAP", "python-debianbts>=2.6.1"],
"bugzilla": ["python-bugzilla>=2.0.0"],
"gmail": ["google-api-python-client", "google-auth-oauthlib"],
"jira": ["jira>=0.22"],
"keyring": ["keyring"],
"megaplan": ["megaplan>=1.4"],
"phabricator": ["phabricator"],
"trac": ["offtrac"],
},
tests_require=[
"Mock",
"nose",
Expand Down
140 changes: 94 additions & 46 deletions tests/test_gmail.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,107 @@
import os.path
from datetime import datetime
import pickle
from copy import copy
from datetime import datetime, timedelta

import mock
from dateutil.tz import tzutc
from google.oauth2.credentials import Credentials
from mock import patch
from six.moves import configparser

import bugwarrior.services.gmail as gmail
from .base import ServiceTest, AbstractServiceTest
from bugwarrior.config import ServiceConfig
from bugwarrior.services.gmail import GmailService

from .base import AbstractServiceTest, ConfigTest, ServiceTest

TEST_CREDENTIAL = {
"token": "itsatokeneveryone",
"refresh_token": "itsarefreshtokeneveryone",
"token_uri": "https://oauth2.googleapis.com/token",
"client_id": "example.apps.googleusercontent.com",
"client_secret": "itsasecrettoeveryone",
"scopes": ["https://www.googleapis.com/auth/gmail.readonly"],
}


class TestGmailService(ConfigTest):

def setUp(self):
super(TestGmailService, self).setUp()
self.config = configparser.RawConfigParser()
self.config.add_section("general")
self.config.add_section("myservice")

mock_data = mock.Mock()
mock_data.path = self.tempdir
self.config.data = mock_data

self.service_config = ServiceConfig(GmailService.CONFIG_PREFIX, self.config, "myservice")

def test_get_credentials_exists_and_valid(self):
mock_api = mock.Mock()
gmail.GmailService.build_api = mock_api
service = GmailService(self.config, "general", "myservice")

expected = Credentials(**copy(TEST_CREDENTIAL))
self.assertEqual(expected.valid, True)
with open(service.credentials_path, "wb") as token:
pickle.dump(expected, token)

self.assertEqual(service.get_credentials().to_json(), expected.to_json())

def test_get_credentials_with_refresh(self):
mock_api = mock.Mock()
gmail.GmailService.build_api = mock_api
service = GmailService(self.config, "general", "myservice")

expired_credential = Credentials(**copy(TEST_CREDENTIAL))
expired_credential.expiry = datetime.now()
self.assertEqual(expired_credential.valid, False)
with open(service.credentials_path, "wb") as token:
pickle.dump(expired_credential, token)

with patch("google.oauth2._client.refresh_grant") as mock_refresh_grant:
access_token = "newaccesstoken"
refresh_token = "newrefreshtoken"
expiry = datetime.now() + timedelta(hours=24)
grant_response = {"id_token": "idtoken"}
mock_refresh_grant.return_value = access_token, refresh_token, expiry, grant_response
refreshed_credential = service.get_credentials()
self.assertEqual(refreshed_credential.valid, True)


TEST_THREAD = {
"messages": [
{
"payload": {
"headers": [
{
"name": "From",
"value": "Foo Bar <[email protected]>"
},
{
"name": "Subject",
"value": "Regarding Bugwarrior"
},
{
"name": "To",
"value": "[email protected]"
},
{
"name": "Message-ID",
"value": "<CMCRSF+6r=x5JtW4wlRYR5qdfRq+iAtSoec5NqrHvRpvVgHbHdg@mail.gmail.com>",
},
],
"parts": [
{
}
]
},
"snippet": "Bugwarrior is great",
"internalDate": 1546722467000,
"threadId": "1234",
"labelIds": [
"IMPORTANT",
"Label_1",
"Label_43",
"CATEGORY_PERSONAL"
"messages": [
{
"payload": {
"headers": [
{"name": "From", "value": "Foo Bar <[email protected]>"},
{"name": "Subject", "value": "Regarding Bugwarrior"},
{"name": "To", "value": "[email protected]"},
{
"name": "Message-ID",
"value": "<CMCRSF+6r=x5JtW4wlRYR5qdfRq+iAtSoec5NqrHvRpvVgHbHdg@mail.gmail.com>",
},
],
"id": "9999"
}
],
"id": "1234"
}
"parts": [{}],
},
"snippet": "Bugwarrior is great",
"internalDate": 1546722467000,
"threadId": "1234",
"labelIds": ["IMPORTANT", "Label_1", "Label_43", "CATEGORY_PERSONAL"],
"id": "9999",
}
],
"id": "1234",
}

TEST_LABELS = [
{'id': 'IMPORTANT', 'name': 'IMPORTANT'},
{'id': 'CATEGORY_PERSONAL', 'name': 'CATEGORY_PERSONAL'},
{'id': 'Label_1', 'name': 'sticky'},
{'id': 'Label_43', 'name': 'postit'},
{"id": "IMPORTANT", "name": "IMPORTANT"},
{"id": "CATEGORY_PERSONAL", "name": "CATEGORY_PERSONAL"},
{"id": "Label_1", "name": "sticky"},
{"id": "Label_43", "name": "postit"},
]


Expand All @@ -76,7 +124,7 @@ def setUp(self):
def test_config_paths(self):
credentials_path = os.path.join(
self.service.config.data.path,
'gmail_credentials_test_example_com.json')
'gmail_credentials_test_example_com.pickle')
self.assertEqual(self.service.credentials_path, credentials_path)

def test_to_taskwarrior(self):
Expand Down