This repository has been archived by the owner on Nov 5, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 430
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c82816c
commit e2884d7
Showing
5 changed files
with
288 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
oauth2client.contrib.sql_alchemy module | ||
======================================= | ||
|
||
.. automodule:: oauth2client.contrib.sql_alchemy | ||
:members: | ||
:undoc-members: | ||
:show-inheritance: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
# 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 SQLAlchemy. | ||
Heavily inspired by equivalent Django module. | ||
Configuration | ||
============= | ||
In order to use this storage, you'll need to create table | ||
with :class:`oauth2client.contrib.sql_alchemy.CredentialsType` column. | ||
It is best used as relationship to some kind of user table. | ||
.. 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. | ||
.. 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, | ||
key_name='user_id', | ||
key_value=user.id, | ||
property_name='credentials', | ||
) | ||
# save | ||
credentials = OAuth2Credentials(...) # usually obtained different way | ||
storage.locked_put(credentials) | ||
# retrieve | ||
credentials = storage.locked_get() | ||
# delete | ||
storage.locked_delete() | ||
""" | ||
|
||
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 FlowType(PickleType): | ||
"""Type representing OAuth2 flow. | ||
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. | ||
""" | ||
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: | ||
oauth2client.Credentials | ||
""" | ||
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: Credentials, the credentials to store. | ||
""" | ||
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
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.sql_alchemy import CredentialsType, FlowType, 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) | ||
flow = Column(FlowType) | ||
|
||
|
||
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(id=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_locked_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', | ||
).locked_get() | ||
|
||
self.compare_credentials(ret) | ||
|
||
def test_locked_put(self): | ||
session = self.session() | ||
Storage( | ||
session=session, | ||
model_class=DummyModel, | ||
key_name='key', | ||
key_value=1, | ||
property_name='credentials', | ||
).locked_put(self.credentials) | ||
session.commit() | ||
|
||
ret = session.query(DummyModel).filter_by(key=1).first() | ||
self.compare_credentials(ret.credentials) | ||
|
||
def test_locked_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', | ||
).locked_delete() | ||
session.commit() | ||
self.assertTrue(q.first() is None) | ||
|
||
|
||
if __name__ == '__main__': # pragma: NO COVER | ||
unittest.main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ basedeps = mock>=1.3.0 | |
nose | ||
flask | ||
unittest2 | ||
sqlalchemy | ||
deps = {[testenv]basedeps} | ||
django | ||
keyring | ||
|