1 """Convenient access to an SQLObject or SQLAlchemy managed database."""
2
3 __all__ = ['AutoConnectHub', 'bind_metadata', 'create_session',
4 'create_session_mapper', 'commit_all', 'end_all',
5 'DatabaseError', 'DatabaseConfigurationError',
6 'EndTransactions', 'get_engine', 'get_metadata', 'mapper',
7 'metadata', 'PackageHub', 'rollback_all', 'session',
8 'session_mapper', 'set_db_uri', 'so_columns', 'so_joins', 'so_to_dict']
9
10 import sys
11 import time
12 import logging
13
14 import cherrypy
15 from cherrypy import request
16
17 try:
18 import sqlalchemy
19 import sqlalchemy.orm
20 from sqlalchemy import MetaData
21 try:
22 from sqlalchemy.exc import ArgumentError, OperationalError
23 except ImportError:
24 from sqlalchemy.exceptions import ArgumentError, OperationalError
25 except ImportError:
26 sqlalchemy = None
27
28 try:
29 import sqlobject
30 from sqlobject.dbconnection import ConnectionHub, Transaction, TheURIOpener
31 from sqlobject.util.threadinglocal import local as threading_local
32 except ImportError:
33 sqlobject = None
34
35 from peak.rules import abstract, when, NoApplicableMethods
36
37 from turbogears import config
38 from turbogears.util import remove_keys
39
40 log = logging.getLogger('turbogears.database')
44 """TurboGears Database Error."""
45
48 """TurboGears Database Configuration Error."""
49
50
51
52 if sqlalchemy:
58
66
95
97 """Create a session that uses the engine from thread-local metadata.
98
99 The session by default does not begin a transaction, and requires that
100 flush() be called explicitly in order to persist results to the database.
101
102 """
103 if not metadata.is_bound():
104 bind_metadata()
105 return sqlalchemy.orm.create_session()
106
107 session = sqlalchemy.orm.scoped_session(create_session)
108
109 if not hasattr(session, 'add'):
110 session.add = session.save_or_update
123 def mapper(cls, *args, **kw):
124 set_kwargs_on_init = kw.pop('set_kwargs_on_init', True)
125 validate = kw.pop('validate', False)
126
127
128 autoadd = kw.pop('autoadd', kw.pop('save_on_init', True))
129
130 if set_kwargs_on_init and (getattr(cls,
131 '__init__', object.__init__) is object.__init__
132 or getattr(cls.__init__, '_session_mapper', False)):
133 def __init__(self, **kwargs):
134 for key, value in kwargs.items():
135 if validate:
136 if not hasattr(self, key):
137 raise TypeError(
138 "Invalid __init__ argument: '%s'" % key)
139 setattr(self, key, value)
140 if autoadd:
141 session.add(self)
142 __init__._session_mapper = True
143 cls.__init__ = __init__
144 cls.query = scoped_session.query_property()
145 return sqlalchemy.orm.mapper(cls, *args, **kw)
146 return mapper
147 session_mapper = create_session_mapper()
148 if hasattr(session, 'mapper'):
149
150 mapper = session.mapper
151 else:
152 mapper = session_mapper
153
154 _metadatas = {}
155 _metadatas[None] = MetaData()
156 metadata = _metadatas[None]
157
158 try:
159 import elixir
160 elixir.metadata, elixir.session = metadata, session
161 except ImportError:
162 pass
163
164 else:
173 session = metadata = mapper = None
174
175 bind_meta_data = bind_metadata
176
177 hub_registry = set()
178
179 _hubs = dict()
180
181
182 if sqlobject:
184 """Timestamp-converter for MySQL.
185
186 Convert a MySQL TIMESTAMP to a floating point number representing
187 the seconds since the Un*x Epoch. It uses custom code the input seems
188 to be the new (MySQL 4.1+) timestamp format, otherwise code from the
189 MySQLdb module is used.
190
191 """
192 if raw[4] == '-':
193 return time.mktime(time.strptime(raw, '%Y-%m-%d %H:%M:%S'))
194 else:
195 import MySQLdb.converters
196 return MySQLdb.converters.mysql_timestamp_converter(raw)
197
200 """Connect to the database once per thread.
201
202 The AutoConnectHub also provides convenient methods for managing
203 transactions.
204
205 """
206 uri = None
207 params = {}
208
209 - def __init__(self, uri=None, supports_transactions=True):
210 if not uri:
211 uri = config.get('sqlobject.dburi')
212 self.uri = uri
213 self.supports_transactions = supports_transactions
214 hub_registry.add(self)
215 ConnectionHub.__init__(self)
216
218 """Return True only if version of MySQLdb <= 1.0."""
219 import MySQLdb
220 module_version = MySQLdb.version_info[0:2]
221 major = module_version[0]
222 minor = module_version[1]
223
224 return major < 1 or (major == 1 and minor < 2)
225
227 """Enable timestamp-workaround for MySQL.
228
229 Enable a workaround for an incompatible timestamp format change
230 in MySQL 4.1 when using an old version of MySQLdb. See trac ticket
231 #1235 - http://trac.turbogears.org/ticket/1235 for details.
232
233 """
234
235 import MySQLdb
236 import MySQLdb.converters
237 if self._is_interesting_version():
238 conversions = MySQLdb.converters.conversions.copy()
239 conversions[MySQLdb.constants.FIELD_TYPE.TIMESTAMP] = \
240 _mysql_timestamp_converter
241
242
243
244 connection.kw['conv'] = conversions
245
247 try:
248 conn = self.threadingLocal.connection
249 return self.begin(conn)
250 except AttributeError:
251 uri = self.uri
252 if uri:
253 conn = sqlobject.connectionForURI(uri)
254
255
256
257 if uri.startswith('sqlite'):
258 TheURIOpener.cachedURIs = {}
259 elif uri.startswith('mysql') and config.get('turbogears.'
260 'enable_mysql41_timestamp_workaround', False):
261 self._enable_timestamp_workaround(conn)
262 self.threadingLocal.connection = conn
263 return self.begin(conn)
264 raise AttributeError("No connection has been defined"
265 " for this thread or process")
266
268 """Used for testing purposes.
269
270 This drops all of the connections that are being held.
271
272 """
273 self.threadingLocal = threading_local()
274
275 - def begin(self, conn=None):
276 """Start a transaction."""
277 if not self.supports_transactions:
278 return conn
279 if not conn:
280 conn = self.getConnection()
281 if isinstance(conn, Transaction):
282 if conn._obsolete:
283 conn.begin()
284 return conn
285 self.threadingLocal.old_conn = conn
286 trans = conn.transaction()
287 self.threadingLocal.connection = trans
288 return trans
289
291 """Commit the current transaction."""
292 if not self.supports_transactions:
293 return
294 try:
295 conn = self.threadingLocal.connection
296 except AttributeError:
297 return
298 if isinstance(conn, Transaction):
299 self.threadingLocal.connection.commit()
300
302 """Rollback the current transaction."""
303 if not self.supports_transactions:
304 return
305 try:
306 conn = self.threadingLocal.connection
307 except AttributeError:
308 return
309 if isinstance(conn, Transaction) and not conn._obsolete:
310 self.threadingLocal.connection.rollback()
311
313 """End the transaction, returning to a standard connection."""
314 if not self.supports_transactions:
315 return
316 try:
317 conn = self.threadingLocal.connection
318 except AttributeError:
319 return
320 if not isinstance(conn, Transaction):
321 return
322 if not conn._obsolete:
323 conn.rollback()
324 self.threadingLocal.connection = self.threadingLocal.old_conn
325 del self.threadingLocal.old_conn
326 self.threadingLocal.connection.expireAll()
327
329 """A package specific database hub.
330
331 Transparently proxies to an AutoConnectHub for the URI
332 that is appropriate for this package. A package URI is
333 configured via "packagename.dburi" in the TurboGears config
334 settings. If there is no package DB URI configured, the
335 default (provided by "sqlobject.dburi") is used.
336
337 The hub is not instantiated until an attempt is made to
338 use the database.
339
340 """
342 self.packagename = packagename
343 self.hub = None
344
346 if self.hub:
347 return self.hub.__get__(obj, type)
348 else:
349 return self
350
352 if not self.hub:
353 self.set_hub()
354 return self.hub.__set__(obj, type)
355
357 if not self.hub:
358 self.set_hub()
359 try:
360 return getattr(self.hub, name)
361 except AttributeError:
362 return getattr(self.hub.getConnection(), name)
363
365 dburi = config.get('%s.dburi' % self.packagename, None)
366 if not dburi:
367 dburi = config.get('sqlobject.dburi', None)
368 if not dburi:
369 raise DatabaseConfigurationError(
370 "No sqlobject database configuration found!")
371 if dburi.startswith('notrans_'):
372 dburi = dburi[8:]
373 trans = False
374 else:
375 trans = True
376 hub = _hubs.get(dburi, None)
377 if not hub:
378 hub = AutoConnectHub(dburi, supports_transactions=trans)
379 _hubs[dburi] = hub
380 self.hub = hub
381 else:
384
387
390 """Set the database URI.
391
392 Sets the database URI to use either globally or for a specific package.
393 Note that once the database is accessed, calling it will have no effect.
394
395 @param dburi: database URI to use
396 @param package: package name this applies to, or None to set the default.
397
398 """
399 if package:
400 config.update({'%s.dburi' % package: dburi})
401 else:
402 config.update({'sqlobject.dburi': dburi})
403
406 """Commit the transactions in all registered hubs (for this thread)."""
407 for hub in hub_registry:
408 hub.commit()
409
412 """Rollback the transactions in all registered hubs (for this thread)."""
413 for hub in hub_registry:
414 hub.rollback()
415
418 """End the transactions in all registered hubs (for this thread)."""
419 for hub in hub_registry:
420 hub.end()
421
426
431
432
433 _using_sa = False
437
438
439
440 @when(run_with_transaction, "not _use_sa(args)")
441 -def so_rwt(func, *args, **kw):
442 log.debug("Starting SQLObject transaction")
443 try:
444 try:
445 retval = func(*args, **kw)
446 commit_all()
447 return retval
448 except cherrypy.HTTPRedirect:
449 commit_all()
450 raise
451 except cherrypy.InternalRedirect:
452 commit_all()
453 raise
454 except:
455
456
457 rollback_all()
458 raise
459 finally:
460 end_all()
461
469
472
473 from turbogears.errorhandling import dispatch_error
474
475 real_func, accept, allow_json, controller = args[:4]
476 args = args[4:]
477 exc_type, exc_value, exc_trace = sys.exc_info()
478 remove_keys(kw, ('tg_source', 'tg_errors', 'tg_exceptions'))
479 try:
480 output = dispatch_error(
481 controller, real_func, None, exception, *args, **kw)
482 except NoApplicableMethods:
483 raise exc_type, exc_value, exc_trace
484 else:
485 del exc_trace
486 return output
487
488
489
490 @when(run_with_transaction, "_use_sa(args)")
491 -def sa_rwt(func, *args, **kw):
492 log.debug("Starting SA transaction")
493 request.sa_transaction = session.begin()
494 try:
495 try:
496 retval = func(*args, **kw)
497 except (cherrypy.HTTPRedirect, cherrypy.InternalRedirect):
498
499 if sa_transaction_active():
500 log.debug('Redirect in active transaction - will commit now')
501 session.commit()
502 else:
503 log.debug('Redirect in inactive transaction')
504 raise
505 except:
506
507 if sa_transaction_active():
508 log.debug('Error in active transaction - will rollback now')
509 session.rollback()
510 else:
511 log.debug('Error in inactive transaction')
512 raise
513
514 if sa_transaction_active():
515 log.debug('Transaction is still active - will commit now')
516 session.commit()
517 else:
518 log.debug('Transaction is already inactive')
519 finally:
520 log.debug('Ending SA transaction')
521 session.close()
522 return retval
523
528 log.debug("Restarting SA transaction")
529 if sa_transaction_active():
530 log.debug('Transaction is still active - will rollback now')
531 session.rollback()
532 else:
533 log.debug('Transaction is already inactive')
534 session.close()
535 request.sa_transaction = session.begin()
536
539 """Check whether SA transaction is still active."""
540 try:
541 return session.is_active
542 except AttributeError:
543 try:
544 return session().is_active
545 except (TypeError, AttributeError):
546 try:
547 transaction = request.sa_transaction
548 return transaction and transaction.is_active
549 except AttributeError:
550 return False
551
554 """Convert SQLObject to a dictionary based on columns."""
555 d = {}
556 if sqlobj is None:
557 return d
558 for name in sqlobj.sqlmeta.columns.keys():
559 d[name] = getattr(sqlobj, name)
560 d['id'] = sqlobj.id
561 if sqlobj._inheritable:
562 d.update(so_to_dict(sqlobj._parent))
563 d.pop('childName')
564 return d
565
568 """Return a dict with all columns from a SQLObject.
569
570 This includes the columns from InheritableSO's bases.
571
572 """
573 if columns is None:
574 columns = {}
575 columns.update(filter(lambda i: i[0] != 'childName',
576 sqlclass.sqlmeta.columns.items()))
577 if sqlclass._inheritable:
578 so_columns(sqlclass.__base__, columns)
579 return columns
580
583 """Return a list with all joins from a SQLObject.
584
585 The list includes the columns from InheritableSO's bases.
586
587 """
588 if joins is None:
589 joins = []
590 joins.extend(sqlclass.sqlmeta.joins)
591 if sqlclass._inheritable:
592 so_joins(sqlclass.__base__, joins)
593 return joins
594
597 if _use_sa():
598 try:
599 session.expunge_all()
600 except AttributeError:
601 session.clear()
602 else:
603 end_all()
604