| #!/usr/bin/env python |
| # |
| # Copyright 2007 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. |
| # |
| |
| |
| |
| |
| """Django database backend for rdbms. |
| |
| This acts as a simple wrapper around the MySQLdb database backend to utilize an |
| alternate settings.py configuration. When used in an application running on |
| Google App Engine, this backend will use the GAE Apiproxy as a communications |
| driver. When used with dev_appserver, or from outside the context of an App |
| Engine app, this backend will instead use a driver that communicates over the |
| Google API for SQL Service. |
| |
| Communicating over Google API requires valid OAuth 2.0 credentials. Before |
| the backend can be used with this transport on dev_appserver, users should |
| first run the Django 'syncdb' management command (or any other of the commands |
| that interact with the database), and follow the instructions to obtain an |
| OAuth2 token and persist it to disk for subsequent use. |
| |
| If you should need to manually force the selection of a particular driver |
| module, you can do so by specifying it in the OPTIONS section of the database |
| configuration in settings.py. For example: |
| |
| DATABASES = { |
| 'default': { |
| 'ENGINE': 'google.storage.speckle.python.django.backend', |
| 'INSTANCE': 'example.com:project:instance', |
| 'NAME': 'mydb', |
| 'USER': 'myusername', |
| 'PASSWORD': 'mypassword', |
| 'OPTIONS': { |
| 'driver': 'google.storage.speckle.python.api.rdbms_googleapi', |
| } |
| } |
| } |
| """ |
| |
| |
| |
| |
| |
| import logging |
| import os |
| import sys |
| import time |
| |
| from django.core import exceptions |
| from django.db import backends |
| from django.db.backends import signals |
| from django.utils import safestring |
| |
| from google.appengine.api import apiproxy_stub_map |
| from google.storage.speckle.python.api import rdbms |
| from google.storage.speckle.python.django.backend import client |
| |
| DEV_SERVER_SOFTWARE = 'Development' |
| PROD_SERVER_SOFTWARE = 'Google App Engine' |
| PING_INTERVAL_SECS = 60 |
| |
| |
| |
| |
| |
| |
| modules_to_swap = ( |
| 'MySQLdb', |
| 'MySQLdb.constants', |
| 'MySQLdb.constants.CLIENT', |
| 'MySQLdb.constants.FIELD_TYPE', |
| 'MySQLdb.constants.FLAG', |
| 'MySQLdb.converters', |
| ) |
| |
| |
| old_modules = [(name, sys.modules.pop(name)) for name in modules_to_swap |
| if name in sys.modules] |
| |
| sys.modules['MySQLdb'] = rdbms |
| |
| from django.db.backends.mysql import base |
| |
| |
| for module_name, module in old_modules: |
| sys.modules[module_name] = module |
| |
| |
| |
| |
| |
| |
| _SETTINGS_CONNECT_ARGS = ( |
| ('HOST', 'dsn', False), |
| ('INSTANCE', 'instance', True), |
| ('NAME', 'database', True), |
| |
| ('USER', 'user', False), |
| ('PASSWORD', 'password', False), |
| |
| |
| ('OAUTH2_SECRET', 'oauth2_refresh_token', False), |
| ('driver', 'driver_name', False), |
| ('oauth_storage', 'oauth_storage', False), |
| ) |
| |
| |
| def _GetDriver(driver_name=None): |
| """Imports the driver module specified by the given module name. |
| |
| If no name is given, this will attempt to automatically determine an |
| appropriate driver to use based on the current environment. When running on |
| a production App Engine instance, the ApiProxy driver will be used, otherwise, |
| the Google API driver will be used. This conveniently allows the backend to |
| be used with the same configuration on production, and with command line tools |
| like manage.py syncdb. |
| |
| Args: |
| driver_name: The name of the driver module to import. |
| |
| Returns: |
| The imported driver module, or None if a suitable driver can not be found. |
| """ |
| if not driver_name: |
| base_pkg_path = 'google.storage.speckle.python.api.' |
| if apiproxy_stub_map.apiproxy.GetStub('rdbms'): |
| driver_name = base_pkg_path + 'rdbms_apiproxy' |
| else: |
| driver_name = base_pkg_path + 'rdbms_googleapi' |
| __import__(driver_name) |
| return sys.modules[driver_name] |
| |
| |
| def Connect(driver_name=None, oauth2_refresh_token=None, **kwargs): |
| """Gets an appropriate connection driver, and connects with it. |
| |
| Args: |
| driver_name: The name of the driver module to use. |
| oauth2_refresh_token: The OAuth2 refresh token used to aquire an access |
| token for authenticating requests made by the Google API driver; defaults |
| to the value provided by the GOOGLE_SQL_OAUTH2_REFRESH_TOKEN environment |
| variable, if present. |
| kwargs: Additional keyword arguments to pass to the driver's connect |
| function. |
| |
| Returns: |
| An rdbms.Connection subclass instance. |
| |
| Raises: |
| exceptions.ImproperlyConfigured: Valid OAuth 2.0 credentials could not be |
| found in storage and no oauth2_refresh_token was given. |
| """ |
| driver = _GetDriver(driver_name) |
| server_software = os.getenv('SERVER_SOFTWARE', '').split('/')[0] |
| if (server_software in (DEV_SERVER_SOFTWARE, PROD_SERVER_SOFTWARE) and |
| driver.__name__.endswith('rdbms_googleapi')): |
| if server_software == PROD_SERVER_SOFTWARE: |
| logging.warning( |
| 'Using the Google API driver is not recommended when running on ' |
| 'production App Engine. You should instead use the GAE API Proxy ' |
| 'driver (google.storage.speckle.python.api.rdbms_apiproxy).') |
| |
| |
| import oauth2client.client |
| from google.storage.speckle.python.api import rdbms_googleapi |
| from google.storage.speckle.python.django.backend import oauth2storage |
| |
| |
| storage = kwargs.setdefault('oauth_storage', oauth2storage.storage) |
| credentials = storage.get() |
| if credentials is None or credentials.invalid: |
| if not oauth2_refresh_token: |
| oauth2_refresh_token = os.getenv('GOOGLE_SQL_OAUTH2_REFRESH_TOKEN') |
| if not oauth2_refresh_token: |
| raise exceptions.ImproperlyConfigured( |
| 'No valid OAuth 2.0 credentials. Before using the Google SQL ' |
| 'Service backend on dev_appserver, you must first run "manage.py ' |
| 'syncdb" and proceed through the given instructions to fetch an ' |
| 'OAuth 2.0 token.') |
| credentials = oauth2client.client.OAuth2Credentials( |
| None, rdbms_googleapi.CLIENT_ID, rdbms_googleapi.CLIENT_SECRET, |
| oauth2_refresh_token, None, |
| 'https://accounts.google.com/o/oauth2/token', |
| rdbms_googleapi.USER_AGENT) |
| credentials.set_store(storage) |
| storage.put(credentials) |
| return driver.connect(**kwargs) |
| |
| |
| class DatabaseOperations(base.DatabaseOperations): |
| """DatabaseOperations for use with rdbms.""" |
| |
| |
| def last_executed_query(self, cursor, sql, params): |
| """Returns the query last executed by the given cursor. |
| |
| Placeholders found in the given sql string will be replaced with actual |
| values from the params list. |
| |
| Args: |
| cursor: The database Cursor. |
| sql: The raw query containing placeholders. |
| params: The sequence of parameters. |
| |
| Returns: |
| The string representing the query last executed by the cursor. |
| """ |
| |
| |
| |
| |
| return backends.BaseDatabaseOperations.last_executed_query( |
| self, cursor, sql, params) |
| |
| |
| class DatabaseWrapper(base.DatabaseWrapper): |
| """Django DatabaseWrapper for use with rdbms. |
| |
| Overrides many pieces of the MySQL DatabaseWrapper for compatibility with |
| the rdbms API. |
| """ |
| vendor = 'rdbms' |
| |
| def __init__(self, *args, **kwargs): |
| super(DatabaseWrapper, self).__init__(*args, **kwargs) |
| self._last_ping_time = 0 |
| self.client = client.DatabaseClient(self) |
| try: |
| self.ops = DatabaseOperations() |
| except TypeError: |
| |
| self.ops = DatabaseOperations(self) |
| |
| def _valid_connection(self): |
| """Disable ping on every operation.""" |
| if self.connection is not None: |
| if time.time() - self._last_ping_time < PING_INTERVAL_SECS: |
| |
| |
| |
| return True |
| else: |
| self._last_ping_time = time.time() |
| |
| return super(DatabaseWrapper, self)._valid_connection() |
| else: |
| return False |
| |
| def _cursor(self): |
| if not self._valid_connection(): |
| kwargs = {'conv': base.django_conversions, 'dsn': None} |
| settings_dict = self.settings_dict |
| settings_dict.update(settings_dict.get('OPTIONS', {})) |
| for settings_key, kwarg, required in _SETTINGS_CONNECT_ARGS: |
| value = settings_dict.get(settings_key) |
| if value: |
| kwargs[kwarg] = value |
| elif required: |
| raise exceptions.ImproperlyConfigured( |
| "You must specify a '%s' for database '%s'" % |
| (settings_key, self.alias)) |
| self.connection = Connect(**kwargs) |
| encoders = {safestring.SafeUnicode: self.connection.encoders[unicode], |
| safestring.SafeString: self.connection.encoders[str]} |
| self.connection.encoders.update(encoders) |
| signals.connection_created.send(sender=self.__class__, connection=self) |
| cursor = base.CursorWrapper(self.connection.cursor()) |
| return cursor |