1 """Convenient access to an SQLObject or SQLAlchemy managed database."""
2
3 import sys
4 import time
5 import logging
6
7 import cherrypy
8 from cherrypy.filters.basefilter import BaseFilter
9
10 try:
11 import sqlalchemy
12 from sqlalchemy.orm import create_session as orm_create_session
13 except ImportError:
14 sqlalchemy = None
15
16 try:
17 import sqlobject
18 from sqlobject.dbconnection import ConnectionHub, Transaction, TheURIOpener
19 from sqlobject.util.threadinglocal import local as threading_local
20 except ImportError:
21 sqlobject = None
22
23 import dispatch
24 from turbogears import config
25 from turbogears.util import remove_keys
26 from turbogears.genericfunctions import MultiorderGenericFunction
27
28 log = logging.getLogger("turbogears.database")
29
30 _engine = None
31
32
33 if sqlalchemy:
35 """Retrieve the engine based on the current configuration."""
36 global _engine
37 if not _engine:
38 alch_args = dict()
39 for k, v in config.config.configMap["global"].items():
40 if "sqlalchemy" in k:
41 alch_args[k.split(".")[-1]] = v
42 dburi = alch_args.pop('dburi')
43 if not dburi:
44 raise KeyError("No sqlalchemy database config found!")
45 _engine = sqlalchemy.create_engine(dburi, **alch_args)
46 if not metadata.is_bound():
47 metadata.bind = _engine
48 return _engine
49
51 """Create a session that uses the engine from thread-local metadata."""
52 if not metadata.is_bound():
53 get_engine()
54 return orm_create_session()
55
56 metadata = sqlalchemy.MetaData()
57 try:
58 from sqlalchemy.orm import scoped_session
59 session = scoped_session(create_session)
60 mapper = session.mapper
61 except ImportError:
62 from sqlalchemy.ext.sessioncontext import SessionContext
69 session = Objectstore()
70 context = session.context
71 Query = sqlalchemy.Query
72 from sqlalchemy.orm import mapper as orm_mapper
73 - def mapper(cls, *args, **kwargs):
74 validate = kwargs.pop('validate', False)
75 if not hasattr(getattr(cls, '__init__'), 'im_func'):
76 def __init__(self, **kwargs):
77 for key, value in kwargs.items():
78 if validate and key not in self.mapper.props:
79 raise KeyError(
80 "Property does not exist: '%s'" % key)
81 setattr(self, key, value)
82 cls.__init__ = __init__
83 m = orm_mapper(cls, extension=context.mapper_extension,
84 *args, **kwargs)
85 class query_property(object):
86 def __get__(self, instance, cls):
87 return Query(cls, session=context.current)
88 cls.query = query_property()
89 return m
90
91 try:
92 from sqlalchemy.ext import activemapper
93 activemapper.metadata, activemapper.objectstore = metadata, session
94 except ImportError:
95 pass
96 try:
97 import elixir
98 elixir.metadata, elixir.session = metadata, session
99 except ImportError:
100 pass
101
102 else:
105
108
109 metadata = session = mapper = None
110
111 bind_meta_data = bind_metadata = get_engine
112
113 try:
114 set
115 except NameError:
116 from sets import Set as set
117
118 hub_registry = set()
119
120 _hubs = dict()
121
122
123 if sqlobject:
125 """Convert a MySQL TIMESTAMP to a floating point number representing
126 the seconds since the Un*x Epoch. It uses custom code the input seems
127 to be the new (MySQL 4.1+) timestamp format, otherwise code from the
128 MySQLdb module is used."""
129 if raw[4] == '-':
130 return time.mktime(time.strptime(raw, '%Y-%m-%d %H:%M:%S'))
131 else:
132 import MySQLdb.converters
133 return MySQLdb.converters.mysql_timestamp_converter(raw)
134
135
137 """Connects to the database once per thread. The AutoConnectHub also
138 provides convenient methods for managing transactions."""
139 uri = None
140 params = {}
141
142 - def __init__(self, uri=None, supports_transactions=True):
149
151 """Return True only if version of MySQLdb <= 1.0."""
152 import MySQLdb
153 module_version = MySQLdb.version_info[0:2]
154 major = module_version[0]
155 minor = module_version[1]
156
157 return (major < 1 or (major == 1 and minor < 2))
158
160 """Enable a workaround for an incompatible timestamp format change
161 in MySQL 4.1 when using an old version of MySQLdb. See trac ticket
162 #1235 - http://trac.turbogears.org/ticket/1235 for details."""
163
164 import MySQLdb
165 import MySQLdb.converters
166 if self._is_interesting_version():
167 conversions = MySQLdb.converters.conversions.copy()
168 conversions[MySQLdb.constants.FIELD_TYPE.TIMESTAMP] = \
169 _mysql_timestamp_converter
170
171
172
173 connection.kw["conv"] = conversions
174
176 try:
177 conn = self.threadingLocal.connection
178 return self.begin(conn)
179 except AttributeError:
180 if self.uri:
181 conn = sqlobject.connectionForURI(self.uri)
182
183
184
185 if self.uri.startswith("sqlite"):
186 TheURIOpener.cachedURIs = {}
187 elif self.uri.startswith("mysql") and \
188 config.get("turbogears.enable_mysql41_timestamp_workaround", False):
189 self._enable_timestamp_workaround(conn)
190 self.threadingLocal.connection = conn
191 return self.begin(conn)
192 raise AttributeError(
193 "No connection has been defined for this thread "
194 "or process")
195
197 """Used for testing purposes. This drops all of the connections
198 that are being held."""
199 self.threadingLocal = threading_local()
200
201 - def begin(self, conn=None):
202 """Start a transaction."""
203 if not self.supports_transactions:
204 return conn
205 if not conn:
206 conn = self.getConnection()
207 if isinstance(conn, Transaction):
208 if conn._obsolete:
209 conn.begin()
210 return conn
211 self.threadingLocal.old_conn = conn
212 trans = conn.transaction()
213 self.threadingLocal.connection = trans
214 return trans
215
217 """Commit the current transaction."""
218 if not self.supports_transactions:
219 return
220 try:
221 conn = self.threadingLocal.connection
222 except AttributeError:
223 return
224 if isinstance(conn, Transaction):
225 self.threadingLocal.connection.commit()
226