diff -r 8167de96c523 -r 7c84e3f370de dbapi.py --- a/dbapi.py Wed Apr 28 10:06:01 2010 +0200 +++ b/dbapi.py Wed Apr 28 12:21:48 2010 +0200 @@ -27,12 +27,14 @@ from logging import getLogger from time import time, clock from itertools import count +from warnings import warn from logilab.common.logging_ext import set_log_methods from logilab.common.decorators import monkeypatch from logilab.common.deprecation import deprecated -from cubicweb import ETYPE_NAME_MAP, ConnectionError, cwvreg, cwconfig +from cubicweb import ETYPE_NAME_MAP, ConnectionError, AuthenticationError,\ + cwvreg, cwconfig from cubicweb.req import RequestSessionBase @@ -206,10 +208,34 @@ cnx = repo_connect(repo, login, cnxprops=cnxprops, **kwargs) return repo, cnx +class _NeedAuthAccessMock(object): + def __getattribute__(self, attr): + raise AuthenticationError() + def __nonzero__(self): + return False + +class DBAPISession(object): + def __init__(self, cnx, login=None, authinfo=None): + self.cnx = cnx + self.data = {} + self.login = login + self.authinfo = authinfo + # dbapi session identifier is the same as the first connection + # identifier, but may later differ in case of auto-reconnection as done + # by the web authentication manager (in cw.web.views.authentication) + if cnx is not None: + self.sessionid = cnx.sessionid + else: + self.sessionid = None + + @property + def anonymous_session(self): + return not self.cnx or self.cnx.anonymous_connection + class DBAPIRequest(RequestSessionBase): - def __init__(self, vreg, cnx=None): + def __init__(self, vreg, session=None): super(DBAPIRequest, self).__init__(vreg) try: # no vreg or config which doesn't handle translations @@ -219,12 +245,13 @@ self.set_default_language(vreg) # cache entities built during the request self._eid_cache = {} - # these args are initialized after a connection is - # established - self.cnx = None # connection associated to the request - self._user = None # request's user, set at authentication - if cnx is not None: - self.set_connection(cnx) + if session is not None: + self.set_session(session) + else: + # these args are initialized after a connection is + # established + self.session = None + self.cnx = self.user = _NeedAuthAccessMock() def base_url(self): return self.vreg.config['base-url'] @@ -232,13 +259,25 @@ def from_controller(self): return 'view' - def set_connection(self, cnx, user=None): + def set_session(self, session, user=None): """method called by the session handler when the user is authenticated or an anonymous connection is open """ - self.cnx = cnx - self.cursor = cnx.cursor(self) - self.set_user(user) + self.session = session + if session.cnx: + self.cnx = session.cnx + self.execute = session.cnx.cursor(self).execute + if user is None: + user = self.cnx.user(self, {'lang': self.lang}) + if user is not None: + self.user = user + self.set_entity_cache(user) + + def execute(self, *args, **kwargs): + """overriden when session is set. By default raise authentication error + so authentication is requested. + """ + raise AuthenticationError() def set_default_language(self, vreg): try: @@ -256,14 +295,6 @@ self.pgettext = lambda x, y: y self.debug('request default language: %s', self.lang) - def describe(self, eid): - """return a tuple (type, sourceuri, extid) for the entity with id """ - return self.cnx.describe(eid) - - def source_defs(self): - """return the definition of sources used by the repository.""" - return self.cnx.source_defs() - # entities cache management ############################################### def entity_cache(self, eid): @@ -283,24 +314,6 @@ # low level session data management ####################################### - def session_data(self): - """return a dictionnary containing session data""" - return self.cnx.session_data() - - def get_session_data(self, key, default=None, pop=False): - """return value associated to `key` in session data""" - if self.cnx is None: - return default # before the connection has been established - return self.cnx.get_session_data(key, default, pop) - - def set_session_data(self, key, value): - """set value associated to `key` in session data""" - return self.cnx.set_session_data(key, value) - - def del_session_data(self, key): - """remove value associated to `key` in session data""" - return self.cnx.del_session_data(key) - def get_shared_data(self, key, default=None, pop=False): """return value associated to `key` in shared data""" return self.cnx.get_shared_data(key, default, pop) @@ -317,26 +330,39 @@ # server session compat layer ############################################# + def describe(self, eid): + """return a tuple (type, sourceuri, extid) for the entity with id """ + return self.cnx.describe(eid) + + def source_defs(self): + """return the definition of sources used by the repository.""" + return self.cnx.source_defs() + def hijack_user(self, user): """return a fake request/session using specified user""" req = DBAPIRequest(self.vreg) - req.set_connection(self.cnx, user) + req.set_session(self.session, user) return req - @property - def user(self): - if self._user is None and self.cnx: - self.set_user(self.cnx.user(self, {'lang': self.lang})) - return self._user + @deprecated('[3.8] use direct access to req.session.data dictionary') + def session_data(self): + """return a dictionnary containing session data""" + return self.session.data - def set_user(self, user): - self._user = user - if user: - self.set_entity_cache(user) + @deprecated('[3.8] use direct access to req.session.data dictionary') + def get_session_data(self, key, default=None, pop=False): + if pop: + return self.session.data.pop(key, default) + return self.session.data.get(key, default) - def execute(self, *args, **kwargs): - """Session interface compatibility""" - return self.cursor.execute(*args, **kwargs) + @deprecated('[3.8] use direct access to req.session.data dictionary') + def set_session_data(self, key, value): + self.session.data[key] = value + + @deprecated('[3.8] use direct access to req.session.data dictionary') + def del_session_data(self, key): + self.session.data.pop(key, None) + set_log_methods(DBAPIRequest, getLogger('cubicweb.dbapi')) @@ -351,68 +377,105 @@ etc. """ -# module level objects ######################################################## + +# cursor / connection objects ################################################## + +class Cursor(object): + """These objects represent a database cursor, which is used to manage the + context of a fetch operation. Cursors created from the same connection are + not isolated, i.e., any changes done to the database by a cursor are + immediately visible by the other cursors. Cursors created from different + connections are isolated. + """ + + def __init__(self, connection, repo, req=None): + """This read-only attribute return a reference to the Connection + object on which the cursor was created. + """ + self.connection = connection + """optionnal issuing request instance""" + self.req = req + self._repo = repo + self._sessid = connection.sessionid + + def close(self): + """no effect""" + pass + + def execute(self, rql, args=None, eid_key=None, build_descr=True): + """execute a rql query, return resulting rows and their description in + a :class:`~cubicweb.rset.ResultSet` object + + * `rql` should be an Unicode string or a plain ASCII string, containing + the rql query + + * `args` the optional args dictionary associated to the query, with key + matching named substitution in `rql` + + * `build_descr` is a boolean flag indicating if the description should + be built on select queries (if false, the description will be en empty + list) + + on INSERT queries, there will be one row for each inserted entity, + containing its eid + + on SET queries, XXX describe + + DELETE queries returns no result. + + .. Note:: + to maximize the rql parsing/analyzing cache performance, you should + always use substitute arguments in queries, i.e. avoid query such as:: + + execute('Any X WHERE X eid 123') + + use:: + + execute('Any X WHERE X eid %(x)s', {'x': 123}) + """ + if eid_key is not None: + warn('[3.8] eid_key is deprecated, you can safely remove this argument', + DeprecationWarning, stacklevel=2) + # XXX use named argument for build_descr in case repo is < 3.8 + rset = self._repo.execute(self._sessid, rql, args, build_descr=build_descr) + rset.req = self.req + return rset -apilevel = '2.0' - -"""Integer constant stating the level of thread safety the interface supports. -Possible values are: - - 0 Threads may not share the module. - 1 Threads may share the module, but not connections. - 2 Threads may share the module and connections. - 3 Threads may share the module, connections and - cursors. - -Sharing in the above context means that two threads may use a resource without -wrapping it using a mutex semaphore to implement resource locking. Note that -you cannot always make external resources thread safe by managing access using -a mutex: the resource may rely on global variables or other external sources -that are beyond your control. -""" -threadsafety = 1 +class LogCursor(Cursor): + """override the standard cursor to log executed queries""" -"""String constant stating the type of parameter marker formatting expected by -the interface. Possible values are : + def execute(self, operation, parameters=None, eid_key=None, build_descr=True): + """override the standard cursor to log executed queries""" + if eid_key is not None: + warn('[3.8] eid_key is deprecated, you can safely remove this argument', + DeprecationWarning, stacklevel=2) + tstart, cstart = time(), clock() + rset = Cursor.execute(self, operation, parameters, build_descr=build_descr) + self.connection.executed_queries.append((operation, parameters, + time() - tstart, clock() - cstart)) + return rset - 'qmark' Question mark style, - e.g. '...WHERE name=?' - 'numeric' Numeric, positional style, - e.g. '...WHERE name=:1' - 'named' Named style, - e.g. '...WHERE name=:name' - 'format' ANSI C printf format codes, - e.g. '...WHERE name=%s' - 'pyformat' Python extended format codes, - e.g. '...WHERE name=%(name)s' -""" -paramstyle = 'pyformat' - - -# connection object ########################################################### class Connection(object): """DB-API 2.0 compatible Connection object for CubicWeb """ # make exceptions available through the connection object ProgrammingError = ProgrammingError + # attributes that may be overriden per connection instance + anonymous_connection = False + cursor_class = Cursor + vreg = None + _closed = None def __init__(self, repo, cnxid, cnxprops=None): self._repo = repo self.sessionid = cnxid self._close_on_del = getattr(cnxprops, 'close_on_del', True) self._cnxtype = getattr(cnxprops, 'cnxtype', 'pyro') - self._closed = None if cnxprops and cnxprops.log_queries: self.executed_queries = [] self.cursor_class = LogCursor - else: - self.cursor_class = Cursor - self.anonymous_connection = False - self.vreg = None - # session's data - self.data = {} def __repr__(self): if self.anonymous_connection: @@ -430,29 +493,7 @@ return False #propagate the exception def request(self): - return DBAPIRequest(self.vreg, self) - - def session_data(self): - """return a dictionnary containing session data""" - return self.data - - def get_session_data(self, key, default=None, pop=False): - """return value associated to `key` in session data""" - if pop: - return self.data.pop(key, default) - else: - return self.data.get(key, default) - - def set_session_data(self, key, value): - """set value associated to `key` in session data""" - self.data[key] = value - - def del_session_data(self, key): - """remove value associated to `key` in session data""" - try: - del self.data[key] - except KeyError: - pass + return DBAPIRequest(self.vreg, DBAPISession(self)) def check(self): """raise `BadConnectionId` if the connection is no more valid""" @@ -526,8 +567,6 @@ if self._repo.config.instance_hooks: hm.register_hooks(config.load_hooks(self.vreg)) - load_vobjects = deprecated()(load_appobjects) - def use_web_compatible_requests(self, baseurl, sitetitle=None): """monkey patch DBAPIRequest to fake a cw.web.request, so you should able to call html views using rset from a simple dbapi connection. @@ -574,9 +613,13 @@ if req is None: req = self.request() rset = req.eid_rset(eid, 'CWUser') - user = self.vreg['etypes'].etype_class('CWUser')(req, rset, row=0, - groups=groups, - properties=properties) + if self.vreg is not None and 'etypes' in self.vreg: + user = self.vreg['etypes'].etype_class('CWUser')(req, rset, row=0, + groups=groups, + properties=properties) + else: + from cubicweb.entity import Entity + user = Entity(req, rset, row=0) user['login'] = login # cache login return user @@ -711,207 +754,3 @@ him). """ return self._repo.undo_transaction(self.sessionid, txuuid) - - -# cursor object ############################################################### - -class Cursor(object): - """This represents a database cursor, which is used to manage the - context of a fetch operation. Cursors created from the same connection are - not isolated, i.e., any changes done to the database by a cursor are - immediately visible by the other cursors. Cursors created from different - connections can or can not be isolated, depending on how the transaction - support is implemented (see also the connection's rollback() and commit() - methods.) - """ - - def __init__(self, connection, repo, req=None): - # This read-only attribute returns a reference to the Connection - # object on which the cursor was created. - self.connection = connection - # optionnal issuing request instance - self.req = req - - # This read/write attribute specifies the number of rows to fetch at a - # time with fetchmany(). It defaults to 1 meaning to fetch a single row - # at a time. - # Implementations must observe this value with respect to the fetchmany() - # method, but are free to interact with the database a single row at a - # time. It may also be used in the implementation of executemany(). - self.arraysize = 1 - - self._repo = repo - self._sessid = connection.sessionid - self._res = None - self._closed = None - self._index = 0 - - def close(self): - """Close the cursor now (rather than whenever __del__ is called). The - cursor will be unusable from this point forward; an Error (or subclass) - exception will be raised if any operation is attempted with the cursor. - """ - self._closed = True - - - def execute(self, operation, parameters=None, eid_key=None, build_descr=True): - """Prepare and execute a database operation (query or command). - Parameters may be provided as sequence or mapping and will be bound to - variables in the operation. Variables are specified in a - database-specific notation (see the module's paramstyle attribute for - details). - - A reference to the operation will be retained by the cursor. If the - same operation object is passed in again, then the cursor can optimize - its behavior. This is most effective for algorithms where the same - operation is used, but different parameters are bound to it (many - times). - - For maximum efficiency when reusing an operation, it is best to use the - setinputsizes() method to specify the parameter types and sizes ahead - of time. It is legal for a parameter to not match the predefined - information; the implementation should compensate, possibly with a loss - of efficiency. - - The parameters may also be specified as list of tuples to e.g. insert - multiple rows in a single operation, but this kind of usage is - depreciated: executemany() should be used instead. - - Return values are not defined by the DB-API, but this here it returns a - ResultSet object. - """ - self._res = rset = self._repo.execute(self._sessid, operation, - parameters, eid_key, build_descr) - rset.req = self.req - self._index = 0 - return rset - - - def executemany(self, operation, seq_of_parameters): - """Prepare a database operation (query or command) and then execute it - against all parameter sequences or mappings found in the sequence - seq_of_parameters. - - Modules are free to implement this method using multiple calls to the - execute() method or by using array operations to have the database - process the sequence as a whole in one call. - - Use of this method for an operation which produces one or more result - sets constitutes undefined behavior, and the implementation is - permitted (but not required) to raise an exception when it detects that - a result set has been created by an invocation of the operation. - - The same comments as for execute() also apply accordingly to this - method. - - Return values are not defined. - """ - for parameters in seq_of_parameters: - self.execute(operation, parameters) - if self._res.rows is not None: - self._res = None - raise ProgrammingError('Operation returned a result set') - - - def fetchone(self): - """Fetch the next row of a query result set, returning a single - sequence, or None when no more data is available. - - An Error (or subclass) exception is raised if the previous call to - execute*() did not produce any result set or no call was issued yet. - """ - if self._res is None: - raise ProgrammingError('No result set') - row = self._res.rows[self._index] - self._index += 1 - return row - - - def fetchmany(self, size=None): - """Fetch the next set of rows of a query result, returning a sequence - of sequences (e.g. a list of tuples). An empty sequence is returned - when no more rows are available. - - The number of rows to fetch per call is specified by the parameter. If - it is not given, the cursor's arraysize determines the number of rows - to be fetched. The method should try to fetch as many rows as indicated - by the size parameter. If this is not possible due to the specified - number of rows not being available, fewer rows may be returned. - - An Error (or subclass) exception is raised if the previous call to - execute*() did not produce any result set or no call was issued yet. - - Note there are performance considerations involved with the size - parameter. For optimal performance, it is usually best to use the - arraysize attribute. If the size parameter is used, then it is best - for it to retain the same value from one fetchmany() call to the next. - """ - if self._res is None: - raise ProgrammingError('No result set') - if size is None: - size = self.arraysize - rows = self._res.rows[self._index:self._index + size] - self._index += size - return rows - - - def fetchall(self): - """Fetch all (remaining) rows of a query result, returning them as a - sequence of sequences (e.g. a list of tuples). Note that the cursor's - arraysize attribute can affect the performance of this operation. - - An Error (or subclass) exception is raised if the previous call to - execute*() did not produce any result set or no call was issued yet. - """ - if self._res is None: - raise ProgrammingError('No result set') - if not self._res.rows: - return [] - rows = self._res.rows[self._index:] - self._index = len(self._res) - return rows - - - def setinputsizes(self, sizes): - """This can be used before a call to execute*() to predefine memory - areas for the operation's parameters. - - sizes is specified as a sequence -- one item for each input parameter. - The item should be a Type Object that corresponds to the input that - will be used, or it should be an integer specifying the maximum length - of a string parameter. If the item is None, then no predefined memory - area will be reserved for that column (this is useful to avoid - predefined areas for large inputs). - - This method would be used before the execute*() method is invoked. - - Implementations are free to have this method do nothing and users are - free to not use it. - """ - pass - - - def setoutputsize(self, size, column=None): - """Set a column buffer size for fetches of large columns (e.g. LONGs, - BLOBs, etc.). The column is specified as an index into the result - sequence. Not specifying the column will set the default size for all - large columns in the cursor. - - This method would be used before the execute*() method is invoked. - - Implementations are free to have this method do nothing and users are - free to not use it. - """ - pass - - -class LogCursor(Cursor): - """override the standard cursor to log executed queries""" - - def execute(self, operation, parameters=None, eid_key=None, build_descr=True): - """override the standard cursor to log executed queries""" - tstart, cstart = time(), clock() - rset = Cursor.execute(self, operation, parameters, eid_key, build_descr) - self.connection.executed_queries.append((operation, parameters, - time() - tstart, clock() - cstart)) - return rset