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

Refactor common code from google.oauth2.flow to google.oauth2.oauthlib #106

Merged
merged 3 commits into from
Feb 8, 2017
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
7 changes: 7 additions & 0 deletions docs/reference/google.oauth2.oauthlib.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
google.oauth2.oauthlib module
=============================

.. automodule:: google.oauth2.oauthlib

This comment was marked as spam.

This comment was marked as spam.

:members:
:inherited-members:
:show-inheritance:
1 change: 1 addition & 0 deletions docs/reference/google.oauth2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ Submodules
google.oauth2.credentials
google.oauth2.flow
google.oauth2.id_token
google.oauth2.oauthlib
google.oauth2.service_account

72 changes: 38 additions & 34 deletions google/oauth2/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,9 @@

import json

import requests_oauthlib

import google.auth.transport.requests
import google.oauth2.credentials

_REQUIRED_CONFIG_KEYS = frozenset(('auth_uri', 'token_uri', 'client_id'))
import google.oauth2.oauthlib


class Flow(object):
Expand All @@ -82,8 +79,32 @@ class Flow(object):
https://console.developers.google.com/apis/credentials
"""

def __init__(self, client_config, scopes, **kwargs):
def __init__(self, oauth2session, client_type, client_config):
"""
Args:
oauth2session (requests_oauthlib.OAuth2Session):
The OAuth 2.0 session from ``requests-oauthlib``.
client_type (str): The client type, either ``web`` or
``installed``.
client_config (Mapping[str, Any]): The client
configuration in the Google `client secrets`_ format.

.. _client secrets:
https://developers.google.com/api-client-library/python/guide
/aaa_client_secrets
"""
self.client_type = client_type
"""str: The client type, either ``'web'`` or ``'installed'``"""
self.client_config = client_config[client_type]
"""Mapping[str, Any]: The OAuth 2.0 client configuration."""
self.oauth2session = oauth2session
"""requests_oauthlib.OAuth2Session: The OAuth 2.0 session."""

@classmethod
def from_client_config(cls, client_config, scopes, **kwargs):
"""Creates a :class:`requests_oauthlib.OAuth2Session` from client
configuration loaded from a Google-format client secrets file.

Args:
client_config (Mapping[str, Any]): The client
configuration in the Google `client secrets`_ format.
Expand All @@ -92,6 +113,9 @@ def __init__(self, client_config, scopes, **kwargs):
kwargs: Any additional parameters passed to
:class:`requests_oauthlib.OAuth2Session`

Returns:
Flow: The constructed Flow instance.

Raises:
ValueError: If the client configuration is not in the correct
format.
Expand All @@ -100,29 +124,19 @@ def __init__(self, client_config, scopes, **kwargs):
https://developers.google.com/api-client-library/python/guide
/aaa_client_secrets
"""
self.client_config = None
"""Mapping[str, Any]: The OAuth 2.0 client configuration."""
self.client_type = None
"""str: The client type, either ``'web'`` or ``'installed'``"""

if 'web' in client_config:
self.client_config = client_config['web']
self.client_type = 'web'
client_type = 'web'
elif 'installed' in client_config:
self.client_config = client_config['installed']
self.client_type = 'installed'
client_type = 'installed'
else:
raise ValueError(
'Client secrets must be for a web or installed app.')

if not _REQUIRED_CONFIG_KEYS.issubset(self.client_config.keys()):
raise ValueError('Client secrets is not in the correct format.')
session, client_config = (
google.oauth2.oauthlib.session_from_client_config(
client_config, scopes, **kwargs))

self.oauth2session = requests_oauthlib.OAuth2Session(
client_id=self.client_config['client_id'],
scope=scopes,
**kwargs)
"""requests_oauthlib.OAuth2Session: The OAuth 2.0 session."""
return cls(session, client_type, client_config)

@classmethod
def from_client_secrets_file(cls, client_secrets_file, scopes, **kwargs):
Expand All @@ -142,7 +156,7 @@ def from_client_secrets_file(cls, client_secrets_file, scopes, **kwargs):
with open(client_secrets_file, 'r') as json_file:
client_config = json.load(json_file)

return cls(client_config, scopes=scopes, **kwargs)
return cls.from_client_config(client_config, scopes=scopes, **kwargs)

@property
def redirect_uri(self):
Expand Down Expand Up @@ -226,18 +240,8 @@ def credentials(self):
Raises:
ValueError: If there is no access token in the session.
"""
if not self.oauth2session.token:
raise ValueError(
'There is no access token for this session, did you call '
'fetch_token?')

return google.oauth2.credentials.Credentials(
self.oauth2session.token['access_token'],
refresh_token=self.oauth2session.token['refresh_token'],
token_uri=self.client_config['token_uri'],
client_id=self.client_config['client_id'],
client_secret=self.client_config['client_secret'],
scopes=self.oauth2session.scope)
return google.oauth2.oauthlib.credentials_from_session(
self.oauth2session, self.client_config)

def authorized_session(self):
"""Returns a :class:`requests.Session` authorized with credentials.
Expand Down
142 changes: 142 additions & 0 deletions google/oauth2/oauthlib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Copyright 2017 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Integration with oauthlib

.. warning::
This module is experimental and is subject to change signficantly
within major version releases.

This module provides helpers for integrating with `requests-oauthlib`_.
Typically, you'll want to use the higher-level helpers in
:mod:`google.oauth2.flow`.

.. _requests-oauthlib: http://requests-oauthlib.readthedocs.io/en/stable/
"""

import json

import requests_oauthlib

import google.oauth2.credentials

_REQUIRED_CONFIG_KEYS = frozenset(('auth_uri', 'token_uri', 'client_id'))


def session_from_client_config(client_config, scopes, **kwargs):
"""Creates a :class:`requests_oauthlib.OAuth2Session` from client
configuration loaded from a Google-format client secrets file.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.


Args:
client_config (Mapping[str, Any]): The client
configuration in the Google `client secrets`_ format.
scopes (Sequence[str]): The list of scopes to request during the
flow.
kwargs: Any additional parameters passed to
:class:`requests_oauthlib.OAuth2Session`

Raises:
ValueError: If the client configuration is not in the correct
format.

Returns:
Tuple[requests_oauthlib.OAuth2Session, Mapping[str, Any]]: The new
oauthlib session and the validated client configuration.

.. _client secrets:
https://developers.google.com/api-client-library/python/guide
/aaa_client_secrets
"""

if 'web' in client_config:
config = client_config['web']
elif 'installed' in client_config:
config = client_config['installed']
else:
raise ValueError(
'Client secrets must be for a web or installed app.')

if not _REQUIRED_CONFIG_KEYS.issubset(config.keys()):
raise ValueError('Client secrets is not in the correct format.')

session = requests_oauthlib.OAuth2Session(
client_id=config['client_id'],
scope=scopes,
**kwargs)

return session, client_config


def session_from_client_secrets_file(client_secrets_file, scopes, **kwargs):
"""Creates a :class:`requests_oauthlib.OAuth2Session` instance from a
Google-format client secrets file.

Args:
client_secrets_file (str): The path to the `client secrets`_ .json
file.
scopes (Sequence[str]): The list of scopes to request during the
flow.
kwargs: Any additional parameters passed to
:class:`requests_oauthlib.OAuth2Session`

Returns:
Tuple[requests_oauthlib.OAuth2Session, Mapping[str, Any]]: The new
oauthlib session and the validated client configuration.

.. _client secrets:
https://developers.google.com/api-client-library/python/guide
/aaa_client_secrets
"""
with open(client_secrets_file, 'r') as json_file:
client_config = json.load(json_file)

return session_from_client_config(client_config, scopes, **kwargs)


def credentials_from_session(session, client_config=None):
"""Creates :class:`google.oauth2.credentials.Credentials` from a
:class:`requests_oauthlib.OAuth2Session`.

:meth:`fetch_token` must be called on the session before before calling
this. This uses the session's auth token and the provided client
configuration to create :class:`google.oauth2.credentials.Credentials`.
This allows you to use the credentials from the session with Google
API client libraries.

Args:
session (requests_oauthlib.OAuth2Session): The OAuth 2.0 session.
client_config (Mapping[str, Any]): The subset of the client
configuration to use. For example, if you have a web client
you would pass in `client_config['web']`.

Returns:
google.oauth2.credentials.Credentials: The constructed credentials.

Raises:
ValueError: If there is no access token in the session.
"""
client_config = client_config if client_config is not None else {}

if not session.token:
raise ValueError(
'There is no access token for this session, did you call '
'fetch_token?')

return google.oauth2.credentials.Credentials(
session.token['access_token'],
refresh_token=session.token.get('refresh_token'),
token_uri=client_config.get('token_uri'),
client_id=client_config.get('client_id'),
client_secret=client_config.get('client_secret'),
scopes=session.scope)
43 changes: 14 additions & 29 deletions tests/oauth2/test_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,44 +27,34 @@
CLIENT_SECRETS_INFO = json.load(fh)


def test_constructor_web():
instance = flow.Flow(CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes)
def test_from_client_secrets_file():
instance = flow.Flow.from_client_secrets_file(
CLIENT_SECRETS_FILE, scopes=mock.sentinel.scopes)
assert instance.client_config == CLIENT_SECRETS_INFO['web']
assert (instance.oauth2session.client_id ==
CLIENT_SECRETS_INFO['web']['client_id'])
assert instance.oauth2session.scope == mock.sentinel.scopes


def test_constructor_installed():
info = {'installed': CLIENT_SECRETS_INFO['web']}
instance = flow.Flow(info, scopes=mock.sentinel.scopes)
assert instance.client_config == info['installed']
assert instance.oauth2session.client_id == info['installed']['client_id']
def test_from_client_config_installed():
client_config = {'installed': CLIENT_SECRETS_INFO['web']}
instance = flow.Flow.from_client_config(
client_config, scopes=mock.sentinel.scopes)
assert instance.client_config == client_config['installed']
assert (instance.oauth2session.client_id ==
client_config['installed']['client_id'])
assert instance.oauth2session.scope == mock.sentinel.scopes


def test_constructor_bad_format():
with pytest.raises(ValueError):
flow.Flow({}, scopes=[])


def test_constructor_missing_keys():
def test_from_client_config_bad_format():
with pytest.raises(ValueError):
flow.Flow({'web': {}}, scopes=[])


def test_from_client_secrets_file():
instance = flow.Flow.from_client_secrets_file(
CLIENT_SECRETS_FILE, scopes=mock.sentinel.scopes)
assert instance.client_config == CLIENT_SECRETS_INFO['web']
assert (instance.oauth2session.client_id ==
CLIENT_SECRETS_INFO['web']['client_id'])
assert instance.oauth2session.scope == mock.sentinel.scopes
flow.Flow.from_client_config({}, scopes=mock.sentinel.scopes)


@pytest.fixture
def instance():
yield flow.Flow(CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes)
yield flow.Flow.from_client_config(
CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes)


def test_redirect_uri(instance):
Expand Down Expand Up @@ -123,11 +113,6 @@ def test_credentials(instance):
assert credentials._token_uri == CLIENT_SECRETS_INFO['web']['token_uri']


def test_bad_credentials(instance):
with pytest.raises(ValueError):
assert instance.credentials


def test_authorized_session(instance):
instance.oauth2session.token = {
'access_token': mock.sentinel.access_token,
Expand Down
Loading