Skip to content
This repository has been archived by the owner on Nov 5, 2019. It is now read-only.

Commit

Permalink
Add SQLAlchemy storage
Browse files Browse the repository at this point in the history
  • Loading branch information
miedzinski committed Jun 28, 2016
1 parent c82816c commit c31ff3d
Show file tree
Hide file tree
Showing 5 changed files with 302 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/source/oauth2client.contrib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Submodules
oauth2client.contrib.keyring_storage
oauth2client.contrib.locked_file
oauth2client.contrib.multistore_file
oauth2client.contrib.sqlalchemy
oauth2client.contrib.xsrfutil

Module contents
Expand Down
7 changes: 7 additions & 0 deletions docs/source/oauth2client.contrib.sqlalchemy.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
oauth2client.contrib.sqlalchemy module
======================================

.. automodule:: oauth2client.contrib.sqlalchemy
:members:
:undoc-members:
:show-inheritance:
173 changes: 173 additions & 0 deletions oauth2client/contrib/sqlalchemy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Copyright 2016 Google Inc. All rights reserved.
#
# 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.

"""OAuth 2.0 utilities for SQLAlchemy.
Utilities for using OAuth 2.0 in conjunction with a SQLAlchemy.
Configuration
=============
In order to use this storage, you'll need to create table
with :class:`oauth2client.contrib.sql_alchemy.CredentialsType` column.
It's recommended to either put this column on some sort of user info
table or put the column in a table with a belongs-to relationship to
a user info table.
Here's an example of a simple table with a :class:`CredentialsType`
column that's related to a user table by the `user_id` key.
.. code-block:: python
from oauth2client.contrib.sql_alchemy import CredentialsType
from sqlalchemy import Column, ForeignKey, Integer
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class Credentials(Base):
__tablename__ = 'credentials'
user_id = Column(Integer, ForeignKey('user.id'))
credentials = Column(CredentialsType)
class User(Base):
id = Column(Integer, primary_key=True)
# bunch of other columns
credentials = relationship('Credentials')
Usage
=====
With tables ready, you are now able to store credentials in database.
We will reuse tables defined above.
.. code-block:: python
from oauth2client.client import OAuth2Credentials
from oauth2client.contrib.sql_alchemy import Storage
from sqlalchemy.orm import Session
session = Session()
user = session.query(User).first()
storage = Storage(
session=session,
model_class=Credentials,
# This is the key column used to identify
# the row that stores the credentials.
key_name='user_id',
key_value=user.id,
property_name='credentials',
)
# Store
credentials = OAuth2Credentials(...)
storage.put(credentials)
# Retrieve
credentials = storage.get()
# Delete
storage.delete()
"""

from __future__ import absolute_import

from oauth2client.client import Storage as BaseStorage
from sqlalchemy.types import PickleType


class CredentialsType(PickleType):
"""Type representing credentials.
Alias for :class:`sqlalchemy.types.PickleType`.
"""


class Storage(BaseStorage):
"""Store and retrieve a single credential to and from SQLAlchemy.
This helper presumes the Credentials
have been stored as a Credentials column
on a db model class.
"""

def __init__(self, session, model_class, key_name,
key_value, property_name):
"""Constructor for Storage.
Args:
session: An instance of :class:`sqlalchemy.orm.Session`.
model_class: SQLAlchemy declarative mapping.
key_name: string, key name for the entity that has the credentials
key_value: key value for the entity that has the credentials
property_name: A string indicating which property on the
``model_class`` to store the credentials.
This property must be a
:class:`CredentialsType` column.
"""
super(Storage, self).__init__()

self.session = session
self.model_class = model_class
self.key_name = key_name
self.key_value = key_value
self.property_name = property_name

def locked_get(self):
"""Retrieve stored credential.
Returns:
A :class:`oauth2client.Credentials` instance or `None`.
"""
credential = None

session = self.session
query = {self.key_name: self.key_value}

entity = session.query(self.model_class).filter_by(**query).first()
if entity:
credential = getattr(entity, self.property_name)
if credential and hasattr(credential, 'set_store'):
credential.set_store(self)

return credential

def locked_put(self, credentials):
"""Write a credentials to the SQLAlchemy datastore.
Args:
credentials: :class:`oauth2client.Credentials`
"""
session = self.session
query = {self.key_name: self.key_value}

entity = session.query(self.model_class).filter_by(**query).first()
if not entity:
entity = self.model_class(**query)

setattr(entity, self.property_name, credentials)
session.add(entity)

def locked_delete(self):
"""Delete credentials from the SQLAlchemy datastore."""

session = self.session
query = {self.key_name: self.key_value}
session.query(self.model_class).filter_by(**query).delete()
120 changes: 120 additions & 0 deletions tests/contrib/test_sqlalchemy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Copyright 2016 Google Inc. All rights reserved.
#
# 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.

import datetime
import unittest

from sqlalchemy import Column, create_engine, Integer
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

from oauth2client import GOOGLE_TOKEN_URI
from oauth2client.client import OAuth2Credentials
from oauth2client.contrib.sqlalchemy import CredentialsType, Storage

Base = declarative_base()


class DummyModel(Base):
__tablename__ = 'dummy'

id = Column(Integer, primary_key=True)
key = Column(Integer) # we will query against this, because of ROWID
credentials = Column(CredentialsType)


class TestSQLAlchemyStorage(unittest.TestCase):
engine = create_engine('sqlite://')

@classmethod
def setUpClass(cls):
Base.metadata.create_all(cls.engine)
cls.session = sessionmaker(bind=cls.engine)

def setUp(self):
self.credentials = OAuth2Credentials(
access_token='token',
client_id='client_id',
client_secret='client_secret',
refresh_token='refresh_token',
token_expiry=datetime.datetime.utcnow(),
token_uri=GOOGLE_TOKEN_URI,
user_agent='DummyAgent',
)

def tearDown(self):
session = self.session()
session.query(DummyModel).filter_by(key=1).delete()
session.commit()

def compare_credentials(self, result):
self.assertEqual(result.access_token, self.credentials.access_token)
self.assertEqual(result.client_id, self.credentials.client_id)
self.assertEqual(result.client_secret, self.credentials.client_secret)
self.assertEqual(result.refresh_token, self.credentials.refresh_token)
self.assertEqual(result.token_expiry, self.credentials.token_expiry)
self.assertEqual(result.token_uri, self.credentials.token_uri)
self.assertEqual(result.user_agent, self.credentials.user_agent)

def test_get(self):
session = self.session()
session.add(DummyModel(
key=1,
credentials=self.credentials,
))
session.commit()

ret = Storage(
session=session,
model_class=DummyModel,
key_name='key',
key_value=1,
property_name='credentials',
).get()

self.compare_credentials(ret)

def test_put(self):
session = self.session()
Storage(
session=session,
model_class=DummyModel,
key_name='key',
key_value=1,
property_name='credentials',
).put(self.credentials)
session.commit()

ret = session.query(DummyModel).filter_by(key=1).first()
self.compare_credentials(ret.credentials)

def test_delete(self):
session = self.session()
session.add(DummyModel(
key=1,
credentials=self.credentials,
))
session.commit()

q = session.query(DummyModel).filter_by(key=1)
self.assertTrue(q.first() is not None)
Storage(
session=session,
model_class=DummyModel,
key_name='key',
key_value=1,
property_name='credentials',
).delete()
session.commit()
self.assertTrue(q.first() is None)
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ basedeps = mock>=1.3.0
nose
flask
unittest2
sqlalchemy
deps = {[testenv]basedeps}
django
keyring
Expand Down

0 comments on commit c31ff3d

Please sign in to comment.