dbapi.py
changeset 5223 6abd6e3599f4
parent 5174 78438ad513ca
child 5244 5467674ad101
--- a/dbapi.py	Mon Apr 12 14:41:01 2010 +0200
+++ b/dbapi.py	Tue Apr 13 12:19:24 2010 +0200
@@ -20,7 +20,8 @@
 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
 
 
@@ -156,9 +157,25 @@
     return repo, cnx
 
 
+class DBAPISession(object):
+    def __init__(self, cnx, login=None, authinfo=None):
+        self.cnx = cnx
+        self.data = {}
+        self.login = login
+        self.authinfo = authinfo
+
+    @property
+    def anonymous_session(self):
+        return self.cnx is None or self.cnx.anonymous_connection
+
+    @property
+    def sessionid(self):
+        return self.cnx.sessionid
+
+
 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
@@ -168,12 +185,12 @@
         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 = self.cnx = self._user = None
 
     def base_url(self):
         return self.vreg.config['base-url']
@@ -181,14 +198,22 @@
     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.session = session
+        if session.cnx is not None:
+            self.cnx = session.cnx
+            self.execute = session.cnx.cursor(self).execute
         self.set_user(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:
             self.lang = vreg.property_value('ui.language')
@@ -205,14 +230,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 <eid>"""
-        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):
@@ -232,26 +249,10 @@
 
     # 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"""
+    def get_shared_data(self, key, default=None, pop=False):
+        """return value associated to `key` in shared 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)
 
     def set_shared_data(self, key, value, querydata=False):
@@ -266,10 +267,18 @@
 
     # server session compat layer #############################################
 
+    def describe(self, eid):
+        """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
+        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
@@ -283,11 +292,25 @@
         if user:
             self.set_entity_cache(user)
 
-    def execute(self, rql, args=None, eid_key=None, build_descr=True):
-        if eid_key is not None:
-            warn('[3.8] eid_key is deprecated, you can safely remove this argument',
-                 DeprecationWarning, stacklevel=2)
-        return self.cursor.execute(rql, args, build_descr=build_descr)
+    @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
+
+    @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)
+
+    @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'))
 
@@ -302,68 +325,104 @@
     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)
+        rset = self._repo.execute(self._sessid, rql, args, 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:
@@ -381,29 +440,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"""
@@ -477,8 +514,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.
@@ -662,220 +697,3 @@
         him).
         """
         return self._repo.undo_transaction(self.sessionid, txuuid)
-
-
-# cursor object ###############################################################
-
-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 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 return 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, 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)
-        self._res = rset = self._repo.execute(self._sessid, rql,
-                                              args, 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
-