0001"""Depreacted: pylons.database will be removed from a future version of
0002Pylons. SQLAlchemy 0.3.x users are recommended to migrate to SAContext
0003(http://cheeseshop.python.org/pypi/SAContext) for similar functionality.
0004
0005Provides convenient access to SQLObject-managed and/or SQLAlchemy-managed
0006databases.
0007
0008This module enables easy use of an SQLObject database by providing an
0009auto-connect hub that will utilize the db uri string given in the Paste conf
0010file called ``sqlobject.dburi``.
0011
0012A SQLAlchemy ``SessionContext`` is also available: it provides both thread and
0013process safe ``Session`` objects via the ``session_context.current`` property.
0014"""
0015import logging
0016import thread
0017import warnings
0018
0019from paste.deploy.converters import asbool
0020
0021import pylons
0022
0023__all__ = ["AutoConnectHub", "PackageHub"]
0024
0025log = logging.getLogger(__name__)
0026
0027warnings.warn(pylons.legacy.pylons_database_warning, DeprecationWarning, 2)
0028
0029try:
0030 import sqlalchemy
0031 from sqlalchemy.ext import sessioncontext
0032
0033 BOOL_OPTIONS = set([
0034 "convert_unicode",
0035 "echo",
0036 "echo_pool",
0037 "threaded",
0038 "use_ansi",
0039 "use_oids",
0040 ])
0041
0042 INT_OPTIONS = set([
0043 "max_overflow",
0044 "pool_size",
0045 "pool_recycle",
0046 "pool_timeout",
0047 ])
0048
0049 def app_scope():
0050 """Return the id keying the current database session's scope.
0051
0052 The session is particular to the current Pylons application -- this
0053 returns an id generated from the current thread and the current Pylons
0054 application's Globals object at pylons.g (if one is registered).
0055 """
0056 try:
0057 app_scope_id = str(id(pylons.config._current_obj()))
0058 except TypeError:
0059 app_scope_id = ''
0060 log.debug("Returning %s as the database session scope id",
0061 app_scope_id)
0062 return '%s|%i' % (app_scope_id, thread.get_ident())
0063
0064 def create_engine(uri=None, echo=None, **kwargs):
0065 """Return a SQLAlchemy db engine. Uses the configuration values from
0066 ``get_engine_conf``.
0067
0068 Engines are cached in the ``get_engines`` dict.
0069 """
0070 conf = get_engine_conf()
0071 conf.update(kwargs)
0072
0073 # replace 'dburi' with 'uri' for consistency
0074 if 'dburi' in conf:
0075 if not 'uri' in conf:
0076 conf['uri'] = conf['dburi']
0077 del conf['dburi']
0078
0079 # override config with passed-in values
0080 conf['uri'] = uri or conf.get('uri')
0081 conf['echo'] = asbool(echo) or conf.get('echo')
0082
0083 uri = conf.pop('uri')
0084 assert uri
0085
0086 # call create_engine or fetch engine from cache
0087
0088 ## use a sorted list of tuples since order isn't guaranteed
0089 ## in the dict
0090 conf_key = str(sorted(conf.items(), key=lambda x: x[0]))
0091
0092 engine_key = '%s|%s' % (uri, conf_key)
0093 db_engines = pylons.config['pylons.db_engines']
0094 if engine_key in db_engines:
0095 engine = db_engines[engine_key]
0096 else:
0097 engine = db_engines[engine_key] = sqlalchemy.create_engine(uri, **conf)
0099
0100 log.debug("Created engine using uri: %s with engine arguments %s", uri, conf)
0101 return engine
0102
0103 def get_engine_conf():
0104 """Returns a dict of SQLAlchemy engine configuration values
0105 from the Pylons config file values ``sqlalchemy.*``"""
0106 result = {}
0107 for k,v in pylons.config.iteritems():
0108 if not k.startswith('sqlalchemy.'):
0109 continue
0110 k = k[11:]
0111 if k in BOOL_OPTIONS:
0112 result[k] = asbool(v)
0113 elif k in INT_OPTIONS:
0114 try:
0115 result[k] = int(v)
0116 except ValueError:
0117 reason = 'config sqlalchemy.%s is not an integer: %s'
0118 raise ValueError(reason % (k,v))
0119 else:
0120 result[k] = v
0121 return result
0122
0123 def make_session(uri=None, echo=None, session_kwargs=None, **kwargs):
0124 """Returns a SQLAlchemy session for the specified database uri from
0125 the the engine cache (returned from ``get_engines``)``. Uses the
0126 configuration values from ``get_engine_conf`` for uri and echo when
0127 None are specified.
0128
0129 ``session_kwargs`` are passed to SQLAlchemy's ``create_session``
0130 function as keyword arguments.
0131
0132 If the uri's engine does not exist, it will be created and added to
0133 the engine cache.
0134 """
0135 if session_kwargs is None:
0136 session_kwargs = {}
0137 engine = create_engine(uri, echo=echo, **kwargs)
0138 log.debug("Created engine for session context")
0139 return sqlalchemy.create_session(bind_to=engine, **session_kwargs)
0140
0141 session_context = sessioncontext.SessionContext(make_session,
0142 scopefunc=app_scope)
0143
0144 __all__.extend(['app_scope', 'create_engine', 'get_engine_conf',
0145 'make_session', 'session_context'])
0146
0147except ImportError:
0148 pass
0149
0150
0151# Provide support for sqlobject
0152try:
0153 import sqlobject
0154 from sqlobject.dbconnection import ConnectionHub, Transaction, TheURIOpener
0155except:
0156 ConnectionHub = object
0157
0158
0159class AutoConnectHub(ConnectionHub):
0160 """Connects to the database once per thread.
0161
0162 The AutoConnectHub also provides convenient methods for managing
0163 transactions.
0164 """
0165 uri = None
0166 params = {}
0167
0168 def __init__(self, uri=None, pool_connections=True):
0169 if not uri:
0170 uri = pylons.config.get('sqlobject.dburi')
0171 self.uri = uri
0172 self.pool_connections = pool_connections
0173 ConnectionHub.__init__(self)
0174
0175 def getConnection(self):
0176 try:
0177 conn = self.threadingLocal.connection
0178 return conn
0179 except AttributeError:
0180 if self.uri:
0181 conn = sqlobject.connectionForURI(self.uri)
0182 # the following line effectively turns off the DBAPI connection
0183 # cache. We're already holding on to a connection per thread,
0184 # and the cache causes problems with sqlite.
0185 if self.uri.startswith("sqlite"):
0186 TheURIOpener.cachedURIs = {}
0187 self.threadingLocal.connection = conn
0188 if not self.pool_connections:
0189 # This disables pooling
0190 conn._pool = None
0191 return conn
0192 try:
0193 return self.processConnection
0194 except AttributeError:
0195 raise AttributeError(
0196 "No connection has been defined for this thread "
0197 "or process")
0198
0199 def begin(self):
0200 """Starts a transaction."""
0201 conn = self.getConnection()
0202 if isinstance(conn, Transaction):
0203 if conn._obsolete:
0204 conn.begin()
0205 return
0206 self.threadingLocal.old_conn = conn
0207 self.threadingLocal.connection = conn.transaction()
0208
0209 def commit(self):
0210 """Commits the current transaction."""
0211 conn = self.threadingLocal.connection
0212 if isinstance(conn, Transaction):
0213 self.threadingLocal.connection.commit()
0214
0215 def rollback(self):
0216 """Rolls back the current transaction."""
0217 conn = self.threadingLocal.connection
0218 if isinstance(conn, Transaction) and not conn._obsolete:
0219 self.threadingLocal.connection.rollback()
0220
0221 def end(self):
0222 """Ends the transaction, returning to a standard connection."""
0223 conn = self.threadingLocal.connection
0224 if not isinstance(conn, Transaction):
0225 return
0226 if not conn._obsolete:
0227 conn.rollback()
0228 self.threadingLocal.connection = self.threadingLocal.old_conn
0229 del self.threadingLocal.old_conn
0230 self.threadingLocal.connection.cache.clear()
0231
0232
0233# This dictionary stores the AutoConnectHubs used for each
0234# connection URI
0235_hubs = dict()
0236
0237
0238class UnconfiguredConnectionError(KeyError):
0239 """
0240 Raised when no configuration is available to set up a connection.
0241 """
0242
0243
0244class PackageHub(object):
0245 """Transparently proxies to an AutoConnectHub for the URI
0246 that is appropriate for this package. A package URI is
0247 configured via "packagename.dburi" in the Paste ini file
0248 settings. If there is no package DB URI configured, the
0249 default (provided by "sqlobject.dburi") is used.
0250
0251 The hub is not instantiated until an attempt is made to
0252 use the database.
0253
0254 If pool_connections=False, then a new database connection
0255 will be opened for every request. This will avoid
0256 problems with database connections that periodically die.
0257 """
0258 def __init__(self, packagename, dburi=None, pool_connections=True):
0259 self.packagename = packagename
0260 self.hub = None
0261 self.dburi = dburi
0262 self.pool_connections = pool_connections
0263
0264 def __get__(self, obj, type):
0265 if not self.hub:
0266 try:
0267 self.set_hub()
0268 except UnconfiguredConnectionError, e:
0269 raise AttributeError(str(e))
0270 return self.hub.__get__(obj, type)
0271
0272 def __set__(self, obj, type):
0273 if not self.hub:
0274 self.set_hub()
0275 return self.hub.__set__(obj, type)
0276
0277 def __getattr__(self, name):
0278 if not self.hub:
0279 self.set_hub()
0280 return getattr(self.hub, name)
0281
0282 def set_hub(self):
0283 dburi = self.dburi
0284 if not dburi:
0285 try:
0286 dburi = pylons.config.get("%s.dburi" % self.packagename)
0288 except TypeError, e:
0289 # No configuration is registered
0290 raise UnconfiguredConnectionError(str(e))
0291 if not dburi:
0292 dburi = pylons.config.get("sqlobject.dburi")
0293 if not dburi:
0294 raise UnconfiguredConnectionError(
0295 "No database configuration found!")
0296 hub = _hubs.get(dburi)
0297 if not hub:
0298 hub = AutoConnectHub(
0299 dburi, pool_connections=self.pool_connections)
0300 _hubs[dburi] = hub
0301 self.hub = hub