dbapi.py
changeset 10331 6f25c7e4f19b
parent 10330 6c12264b3f18
child 10333 569324f890d7
equal deleted inserted replaced
10330:6c12264b3f18 10331:6f25c7e4f19b
     1 # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """DB-API 2.0 compliant module
       
    19 
       
    20 Take a look at http://www.python.org/peps/pep-0249.html
       
    21 
       
    22 (most parts of this document are reported here in docstrings)
       
    23 """
       
    24 
       
    25 __docformat__ = "restructuredtext en"
       
    26 
       
    27 from threading import currentThread
       
    28 from logging import getLogger
       
    29 from time import time, clock
       
    30 from itertools import count
       
    31 from warnings import warn
       
    32 from os.path import join
       
    33 from uuid import uuid4
       
    34 from urlparse import  urlparse
       
    35 
       
    36 from logilab.common.logging_ext import set_log_methods
       
    37 from logilab.common.decorators import monkeypatch, cachedproperty
       
    38 from logilab.common.deprecation import deprecated
       
    39 
       
    40 from cubicweb import (ETYPE_NAME_MAP, AuthenticationError, ProgrammingError,
       
    41                       cwvreg, cwconfig)
       
    42 from cubicweb.repoapi import get_repository
       
    43 from cubicweb.req import RequestSessionBase
       
    44 
       
    45 
       
    46 _MARKER = object()
       
    47 
       
    48 def _fake_property_value(self, name):
       
    49     try:
       
    50         return super(DBAPIRequest, self).property_value(name)
       
    51     except KeyError:
       
    52         return ''
       
    53 
       
    54 def fake(*args, **kwargs):
       
    55     return None
       
    56 
       
    57 def multiple_connections_fix():
       
    58     """some monkey patching necessary when an application has to deal with
       
    59     several connections to different repositories. It tries to hide buggy class
       
    60     attributes since classes are not designed to be shared among multiple
       
    61     registries.
       
    62     """
       
    63     defaultcls = cwvreg.CWRegistryStore.REGISTRY_FACTORY[None]
       
    64 
       
    65     etypescls = cwvreg.CWRegistryStore.REGISTRY_FACTORY['etypes']
       
    66     orig_etype_class = etypescls.orig_etype_class = etypescls.etype_class
       
    67     @monkeypatch(defaultcls)
       
    68     def etype_class(self, etype):
       
    69         """return an entity class for the given entity type.
       
    70         Try to find out a specific class for this kind of entity or
       
    71         default to a dump of the class registered for 'Any'
       
    72         """
       
    73         usercls = orig_etype_class(self, etype)
       
    74         if etype == 'Any':
       
    75             return usercls
       
    76         usercls.e_schema = self.schema.eschema(etype)
       
    77         return usercls
       
    78 
       
    79 def multiple_connections_unfix():
       
    80     etypescls = cwvreg.CWRegistryStore.REGISTRY_FACTORY['etypes']
       
    81     etypescls.etype_class = etypescls.orig_etype_class
       
    82 
       
    83 
       
    84 class ConnectionProperties(object):
       
    85     def __init__(self, cnxtype=None, close=True, log=False):
       
    86         if cnxtype is not None:
       
    87             warn('[3.16] cnxtype argument is deprecated', DeprecationWarning,
       
    88                  stacklevel=2)
       
    89         self.cnxtype = cnxtype
       
    90         self.log_queries = log
       
    91         self.close_on_del = close
       
    92 
       
    93 
       
    94 @deprecated('[3.19] the dbapi is deprecated. Have a look at the new repoapi.')
       
    95 def _repo_connect(repo, login, **kwargs):
       
    96     """Constructor to create a new connection to the given CubicWeb repository.
       
    97 
       
    98     Returns a Connection instance.
       
    99 
       
   100     Raises AuthenticationError if authentication failed
       
   101     """
       
   102     cnxid = repo.connect(unicode(login), **kwargs)
       
   103     cnx = Connection(repo, cnxid, kwargs.get('cnxprops'))
       
   104     if cnx.is_repo_in_memory:
       
   105         cnx.vreg = repo.vreg
       
   106     return cnx
       
   107 
       
   108 def connect(database, login=None,
       
   109             cnxprops=None, setvreg=True, mulcnx=True, initlog=True, **kwargs):
       
   110     """Constructor for creating a connection to the CubicWeb repository.
       
   111     Returns a :class:`Connection` object.
       
   112 
       
   113     Typical usage::
       
   114 
       
   115       cnx = connect('myinstance', login='me', password='toto')
       
   116 
       
   117     `database` may be:
       
   118 
       
   119     * a simple instance id for in-memory connection
       
   120 
       
   121     * a uri like scheme://host:port/instanceid where scheme must be
       
   122       'inmemory'
       
   123 
       
   124     Other arguments:
       
   125 
       
   126     :login:
       
   127       the user login to use to authenticate.
       
   128 
       
   129     :cnxprops:
       
   130       a :class:`ConnectionProperties` instance, allowing to specify
       
   131       the connection method (eg in memory).
       
   132 
       
   133     :setvreg:
       
   134       flag telling if a registry should be initialized for the connection.
       
   135       Don't change this unless you know what you're doing.
       
   136 
       
   137     :mulcnx:
       
   138       Will disappear at some point. Try to deal with connections to differents
       
   139       instances in the same process unless specified otherwise by setting this
       
   140       flag to False. Don't change this unless you know what you're doing.
       
   141 
       
   142     :initlog:
       
   143       flag telling if logging should be initialized. You usually don't want
       
   144       logging initialization when establishing the connection from a process
       
   145       where it's already initialized.
       
   146 
       
   147     :kwargs:
       
   148       there goes authentication tokens. You usually have to specify a password
       
   149       for the given user, using a named 'password' argument.
       
   150 
       
   151     """
       
   152     if not urlparse(database).scheme:
       
   153         warn('[3.16] give an qualified URI as database instead of using '
       
   154              'host/cnxprops to specify the connection method',
       
   155              DeprecationWarning, stacklevel=2)
       
   156     puri = urlparse(database)
       
   157     method = puri.scheme.lower()
       
   158     assert method == 'inmemory'
       
   159     config = cwconfig.instance_configuration(puri.netloc)
       
   160     repo = get_repository(database, config=config)
       
   161     vreg = repo.vreg
       
   162     cnx = _repo_connect(repo, login, cnxprops=cnxprops, **kwargs)
       
   163     cnx.vreg = vreg
       
   164     return cnx
       
   165 
       
   166 def in_memory_repo(config):
       
   167     """Return and in_memory Repository object from a config (or vreg)"""
       
   168     if isinstance(config, cwvreg.CWRegistryStore):
       
   169         vreg = config
       
   170         config = None
       
   171     else:
       
   172         vreg = None
       
   173     # get local access to the repository
       
   174     return get_repository('inmemory://', config=config, vreg=vreg)
       
   175 
       
   176 def in_memory_repo_cnx(config, login, **kwargs):
       
   177     """useful method for testing and scripting to get a dbapi.Connection
       
   178     object connected to an in-memory repository instance
       
   179     """
       
   180     # connection to the CubicWeb repository
       
   181     repo = in_memory_repo(config)
       
   182     return repo, _repo_connect(repo, login, **kwargs)
       
   183 
       
   184 # XXX web only method, move to webconfig?
       
   185 def anonymous_session(vreg):
       
   186     """return a new anonymous session
       
   187 
       
   188     raises an AuthenticationError if anonymous usage is not allowed
       
   189     """
       
   190     anoninfo = vreg.config.anonymous_user()
       
   191     if anoninfo[0] is None: # no anonymous user
       
   192         raise AuthenticationError('anonymous access is not authorized')
       
   193     anon_login, anon_password = anoninfo
       
   194     # use vreg's repository cache
       
   195     repo = vreg.config.repository(vreg)
       
   196     anon_cnx = _repo_connect(repo, anon_login, password=anon_password)
       
   197     anon_cnx.vreg = vreg
       
   198     return DBAPISession(anon_cnx, anon_login)
       
   199 
       
   200 
       
   201 class _NeedAuthAccessMock(object):
       
   202     def __getattribute__(self, attr):
       
   203         raise AuthenticationError()
       
   204     def __nonzero__(self):
       
   205         return False
       
   206 
       
   207 class DBAPISession(object):
       
   208     def __init__(self, cnx, login=None):
       
   209         self.cnx = cnx
       
   210         self.data = {}
       
   211         self.login = login
       
   212         # dbapi session identifier is the same as the first connection
       
   213         # identifier, but may later differ in case of auto-reconnection as done
       
   214         # by the web authentication manager (in cw.web.views.authentication)
       
   215         if cnx is not None:
       
   216             self.sessionid = cnx.sessionid
       
   217         else:
       
   218             self.sessionid = uuid4().hex
       
   219 
       
   220     @property
       
   221     def anonymous_session(self):
       
   222         return not self.cnx or self.cnx.anonymous_connection
       
   223 
       
   224     def __repr__(self):
       
   225         return '<DBAPISession %r>' % self.sessionid
       
   226 
       
   227 
       
   228 class DBAPIRequest(RequestSessionBase):
       
   229     #: Request language identifier eg: 'en'
       
   230     lang = None
       
   231 
       
   232     def __init__(self, vreg, session=None):
       
   233         super(DBAPIRequest, self).__init__(vreg)
       
   234         #: 'language' => translation_function() mapping
       
   235         try:
       
   236             # no vreg or config which doesn't handle translations
       
   237             self.translations = vreg.config.translations
       
   238         except AttributeError:
       
   239             self.translations = {}
       
   240         #: cache entities built during the request
       
   241         self._eid_cache = {}
       
   242         if session is not None:
       
   243             self.set_session(session)
       
   244         else:
       
   245             # these args are initialized after a connection is
       
   246             # established
       
   247             self.session = DBAPISession(None)
       
   248             self.cnx = self.user = _NeedAuthAccessMock()
       
   249         self.set_default_language(vreg)
       
   250 
       
   251     def get_option_value(self, option, foreid=None):
       
   252         if foreid is not None:
       
   253             warn('[3.19] foreid argument is deprecated', DeprecationWarning,
       
   254                  stacklevel=2)
       
   255         return self.cnx.get_option_value(option)
       
   256 
       
   257     def set_session(self, session):
       
   258         """method called by the session handler when the user is authenticated
       
   259         or an anonymous connection is open
       
   260         """
       
   261         self.session = session
       
   262         if session.cnx:
       
   263             self.cnx = session.cnx
       
   264             self.execute = session.cnx.cursor(self).execute
       
   265             self.user = self.cnx.user(self)
       
   266             self.set_entity_cache(self.user)
       
   267 
       
   268     def execute(self, *args, **kwargs): # pylint: disable=E0202
       
   269         """overriden when session is set. By default raise authentication error
       
   270         so authentication is requested.
       
   271         """
       
   272         raise AuthenticationError()
       
   273 
       
   274     def set_default_language(self, vreg):
       
   275         try:
       
   276             lang = vreg.property_value('ui.language')
       
   277         except Exception: # property may not be registered
       
   278             lang = 'en'
       
   279         try:
       
   280             self.set_language(lang)
       
   281         except KeyError:
       
   282             # this occurs usually during test execution
       
   283             self._ = self.__ = unicode
       
   284             self.pgettext = lambda x, y: unicode(y)
       
   285 
       
   286     # server-side service call #################################################
       
   287 
       
   288     def call_service(self, regid, **kwargs):
       
   289         return self.cnx.call_service(regid, **kwargs)
       
   290 
       
   291     # entities cache management ###############################################
       
   292 
       
   293     def entity_cache(self, eid):
       
   294         return self._eid_cache[eid]
       
   295 
       
   296     def set_entity_cache(self, entity):
       
   297         self._eid_cache[entity.eid] = entity
       
   298 
       
   299     def cached_entities(self):
       
   300         return self._eid_cache.values()
       
   301 
       
   302     def drop_entity_cache(self, eid=None):
       
   303         if eid is None:
       
   304             self._eid_cache = {}
       
   305         else:
       
   306             del self._eid_cache[eid]
       
   307 
       
   308     # low level session data management #######################################
       
   309 
       
   310     @deprecated('[3.19] use session or transaction data')
       
   311     def get_shared_data(self, key, default=None, pop=False, txdata=False):
       
   312         """see :meth:`Connection.get_shared_data`"""
       
   313         return self.cnx.get_shared_data(key, default, pop, txdata)
       
   314 
       
   315     @deprecated('[3.19] use session or transaction data')
       
   316     def set_shared_data(self, key, value, txdata=False, querydata=None):
       
   317         """see :meth:`Connection.set_shared_data`"""
       
   318         if querydata is not None:
       
   319             txdata = querydata
       
   320             warn('[3.10] querydata argument has been renamed to txdata',
       
   321                  DeprecationWarning, stacklevel=2)
       
   322         return self.cnx.set_shared_data(key, value, txdata)
       
   323 
       
   324     # server session compat layer #############################################
       
   325 
       
   326     def entity_metas(self, eid):
       
   327         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
       
   328         return self.cnx.entity_metas(eid)
       
   329 
       
   330     def source_defs(self):
       
   331         """return the definition of sources used by the repository."""
       
   332         return self.cnx.source_defs()
       
   333 
       
   334     @deprecated('[3.19] use .entity_metas(eid) instead')
       
   335     def describe(self, eid, asdict=False):
       
   336         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
       
   337         return self.cnx.describe(eid, asdict)
       
   338 
       
   339     # these are overridden by set_log_methods below
       
   340     # only defining here to prevent pylint from complaining
       
   341     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
       
   342 
       
   343 set_log_methods(DBAPIRequest, getLogger('cubicweb.dbapi'))
       
   344 
       
   345 
       
   346 
       
   347 # cursor / connection objects ##################################################
       
   348 
       
   349 class Cursor(object):
       
   350     """These objects represent a database cursor, which is used to manage the
       
   351     context of a fetch operation. Cursors created from the same connection are
       
   352     not isolated, i.e., any changes done to the database by a cursor are
       
   353     immediately visible by the other cursors. Cursors created from different
       
   354     connections are isolated.
       
   355     """
       
   356 
       
   357     def __init__(self, connection, repo, req=None):
       
   358         """This read-only attribute return a reference to the Connection
       
   359         object on which the cursor was created.
       
   360         """
       
   361         self.connection = connection
       
   362         """optionnal issuing request instance"""
       
   363         self.req = req
       
   364         self._repo = repo
       
   365         self._sessid = connection.sessionid
       
   366 
       
   367     def close(self):
       
   368         """no effect"""
       
   369         pass
       
   370 
       
   371     def _txid(self):
       
   372         return self.connection._txid(self)
       
   373 
       
   374     def execute(self, rql, args=None, build_descr=True):
       
   375         """execute a rql query, return resulting rows and their description in
       
   376         a :class:`~cubicweb.rset.ResultSet` object
       
   377 
       
   378         * `rql` should be a Unicode string or a plain ASCII string, containing
       
   379           the rql query
       
   380 
       
   381         * `args` the optional args dictionary associated to the query, with key
       
   382           matching named substitution in `rql`
       
   383 
       
   384         * `build_descr` is a boolean flag indicating if the description should
       
   385           be built on select queries (if false, the description will be en empty
       
   386           list)
       
   387 
       
   388         on INSERT queries, there will be one row for each inserted entity,
       
   389         containing its eid
       
   390 
       
   391         on SET queries, XXX describe
       
   392 
       
   393         DELETE queries returns no result.
       
   394 
       
   395         .. Note::
       
   396           to maximize the rql parsing/analyzing cache performance, you should
       
   397           always use substitute arguments in queries, i.e. avoid query such as::
       
   398 
       
   399             execute('Any X WHERE X eid 123')
       
   400 
       
   401           use::
       
   402 
       
   403             execute('Any X WHERE X eid %(x)s', {'x': 123})
       
   404         """
       
   405         rset = self._repo.execute(self._sessid, rql, args,
       
   406                                   build_descr=build_descr, **self._txid())
       
   407         rset.req = self.req
       
   408         return rset
       
   409 
       
   410 
       
   411 class LogCursor(Cursor):
       
   412     """override the standard cursor to log executed queries"""
       
   413 
       
   414     def execute(self, operation, parameters=None, build_descr=True):
       
   415         """override the standard cursor to log executed queries"""
       
   416         tstart, cstart = time(), clock()
       
   417         rset = Cursor.execute(self, operation, parameters, build_descr=build_descr)
       
   418         self.connection.executed_queries.append((operation, parameters,
       
   419                                                  time() - tstart, clock() - cstart))
       
   420         return rset
       
   421 
       
   422 def check_not_closed(func):
       
   423     def decorator(self, *args, **kwargs):
       
   424         if self._closed is not None:
       
   425             raise ProgrammingError('Closed connection %s' % self.sessionid)
       
   426         return func(self, *args, **kwargs)
       
   427     return decorator
       
   428 
       
   429 class Connection(object):
       
   430     """DB-API 2.0 compatible Connection object for CubicWeb
       
   431     """
       
   432     # make exceptions available through the connection object
       
   433     ProgrammingError = ProgrammingError
       
   434     # attributes that may be overriden per connection instance
       
   435     cursor_class = Cursor
       
   436     vreg = None
       
   437     _closed = None
       
   438 
       
   439     def __init__(self, repo, cnxid, cnxprops=None):
       
   440         self._repo = repo
       
   441         self.sessionid = cnxid
       
   442         self._close_on_del = getattr(cnxprops, 'close_on_del', True)
       
   443         self._web_request = False
       
   444         if cnxprops and cnxprops.log_queries:
       
   445             self.executed_queries = []
       
   446             self.cursor_class = LogCursor
       
   447 
       
   448     @property
       
   449     def is_repo_in_memory(self):
       
   450         """return True if this is a local, aka in-memory, connection to the
       
   451         repository
       
   452         """
       
   453         try:
       
   454             from cubicweb.server.repository import Repository
       
   455         except ImportError:
       
   456             # code not available, no way
       
   457             return False
       
   458         return isinstance(self._repo, Repository)
       
   459 
       
   460     @property # could be a cached property but we want to prevent assigment to
       
   461               # catch potential programming error.
       
   462     def anonymous_connection(self):
       
   463         login = self._repo.user_info(self.sessionid)[1]
       
   464         anon_login = self.vreg.config.get('anonymous-user')
       
   465         return login == anon_login
       
   466 
       
   467     def __repr__(self):
       
   468         if self.anonymous_connection:
       
   469             return '<Connection %s (anonymous)>' % self.sessionid
       
   470         return '<Connection %s>' % self.sessionid
       
   471 
       
   472     def __enter__(self):
       
   473         return self.cursor()
       
   474 
       
   475     def __exit__(self, exc_type, exc_val, exc_tb):
       
   476         if exc_type is None:
       
   477             self.commit()
       
   478         else:
       
   479             self.rollback()
       
   480             return False #propagate the exception
       
   481 
       
   482     def __del__(self):
       
   483         """close the remote connection if necessary"""
       
   484         if self._closed is None and self._close_on_del:
       
   485             try:
       
   486                 self.close()
       
   487             except Exception:
       
   488                 pass
       
   489 
       
   490     # server-side service call #################################################
       
   491 
       
   492     @check_not_closed
       
   493     def call_service(self, regid, **kwargs):
       
   494         return self._repo.call_service(self.sessionid, regid, **kwargs)
       
   495 
       
   496     # connection initialization methods ########################################
       
   497 
       
   498     def load_appobjects(self, cubes=_MARKER, subpath=None, expand=True):
       
   499         config = self.vreg.config
       
   500         if cubes is _MARKER:
       
   501             cubes = self._repo.get_cubes()
       
   502         elif cubes is None:
       
   503             cubes = ()
       
   504         else:
       
   505             if not isinstance(cubes, (list, tuple)):
       
   506                 cubes = (cubes,)
       
   507             if expand:
       
   508                 cubes = config.expand_cubes(cubes)
       
   509         if subpath is None:
       
   510             subpath = esubpath = ('entities', 'views')
       
   511         else:
       
   512             esubpath = subpath
       
   513         if 'views' in subpath:
       
   514             esubpath = list(subpath)
       
   515             esubpath.remove('views')
       
   516             esubpath.append(join('web', 'views'))
       
   517         # first load available configs, necessary for proper persistent
       
   518         # properties initialization
       
   519         config.load_available_configs()
       
   520         # then init cubes
       
   521         config.init_cubes(cubes)
       
   522         # then load appobjects into the registry
       
   523         vpath = config.build_appobjects_path(reversed(config.cubes_path()),
       
   524                                              evobjpath=esubpath,
       
   525                                              tvobjpath=subpath)
       
   526         self.vreg.register_objects(vpath)
       
   527 
       
   528     def use_web_compatible_requests(self, baseurl, sitetitle=None):
       
   529         """monkey patch DBAPIRequest to fake a cw.web.request, so you should
       
   530         able to call html views using rset from a simple dbapi connection.
       
   531 
       
   532         You should call `load_appobjects` at some point to register those views.
       
   533         """
       
   534         DBAPIRequest.property_value = _fake_property_value
       
   535         DBAPIRequest.next_tabindex = count().next
       
   536         DBAPIRequest.relative_path = fake
       
   537         DBAPIRequest.url = fake
       
   538         DBAPIRequest.get_page_data = fake
       
   539         DBAPIRequest.set_page_data = fake
       
   540         # XXX could ask the repo for it's base-url configuration
       
   541         self.vreg.config.set_option('base-url', baseurl)
       
   542         self.vreg.config.uiprops = {}
       
   543         self.vreg.config.datadir_url = baseurl + '/data'
       
   544         # XXX why is this needed? if really needed, could be fetched by a query
       
   545         if sitetitle is not None:
       
   546             self.vreg['propertydefs']['ui.site-title'] = {'default': sitetitle}
       
   547         self._web_request = True
       
   548 
       
   549     def request(self):
       
   550         if self._web_request:
       
   551             from cubicweb.web.request import DBAPICubicWebRequestBase
       
   552             req = DBAPICubicWebRequestBase(self.vreg, False)
       
   553             req.get_header = lambda x, default=None: default
       
   554             req.set_session = lambda session: DBAPIRequest.set_session(
       
   555                 req, session)
       
   556             req.relative_path = lambda includeparams=True: ''
       
   557         else:
       
   558             req = DBAPIRequest(self.vreg)
       
   559         req.set_session(DBAPISession(self))
       
   560         return req
       
   561 
       
   562     @check_not_closed
       
   563     def user(self, req=None, props=None):
       
   564         """return the User object associated to this connection"""
       
   565         # cnx validity is checked by the call to .user_info
       
   566         eid, login, groups, properties = self._repo.user_info(self.sessionid,
       
   567                                                               props)
       
   568         if req is None:
       
   569             req = self.request()
       
   570         rset = req.eid_rset(eid, 'CWUser')
       
   571         if self.vreg is not None and 'etypes' in self.vreg:
       
   572             user = self.vreg['etypes'].etype_class('CWUser')(
       
   573                 req, rset, row=0, groups=groups, properties=properties)
       
   574         else:
       
   575             from cubicweb.entity import Entity
       
   576             user = Entity(req, rset, row=0)
       
   577         user.cw_attr_cache['login'] = login # cache login
       
   578         return user
       
   579 
       
   580     @check_not_closed
       
   581     def check(self):
       
   582         """raise `BadConnectionId` if the connection is no more valid, else
       
   583         return its latest activity timestamp.
       
   584         """
       
   585         return self._repo.check_session(self.sessionid)
       
   586 
       
   587     def _txid(self, cursor=None): # pylint: disable=E0202
       
   588         # XXX could now handle various isolation level!
       
   589         # return a dict as bw compat trick
       
   590         return {'txid': currentThread().getName()}
       
   591 
       
   592     # session data methods #####################################################
       
   593 
       
   594     @check_not_closed
       
   595     def get_shared_data(self, key, default=None, pop=False, txdata=False):
       
   596         """return value associated to key in the session's data dictionary or
       
   597         session's transaction's data if `txdata` is true.
       
   598 
       
   599         If pop is True, value will be removed from the dictionary.
       
   600 
       
   601         If key isn't defined in the dictionary, value specified by the
       
   602         `default` argument will be returned.
       
   603         """
       
   604         return self._repo.get_shared_data(self.sessionid, key, default, pop, txdata)
       
   605 
       
   606     @check_not_closed
       
   607     def set_shared_data(self, key, value, txdata=False):
       
   608         """set value associated to `key` in shared data
       
   609 
       
   610         if `txdata` is true, the value will be added to the repository
       
   611         session's query data which are cleared on commit/rollback of the current
       
   612         transaction.
       
   613         """
       
   614         return self._repo.set_shared_data(self.sessionid, key, value, txdata)
       
   615 
       
   616     # meta-data accessors ######################################################
       
   617 
       
   618     @check_not_closed
       
   619     def source_defs(self):
       
   620         """Return the definition of sources used by the repository."""
       
   621         return self._repo.source_defs()
       
   622 
       
   623     @check_not_closed
       
   624     def get_schema(self):
       
   625         """Return the schema currently used by the repository."""
       
   626         return self._repo.get_schema()
       
   627 
       
   628     @check_not_closed
       
   629     def get_option_value(self, option, foreid=None):
       
   630         """Return the value for `option` in the configuration.
       
   631 
       
   632         `foreid` argument is deprecated and now useless (as of 3.19).
       
   633         """
       
   634         if foreid is not None:
       
   635             warn('[3.19] foreid argument is deprecated', DeprecationWarning,
       
   636                  stacklevel=2)
       
   637         return self._repo.get_option_value(option)
       
   638 
       
   639 
       
   640     @check_not_closed
       
   641     def entity_metas(self, eid):
       
   642         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
       
   643         try:
       
   644             return self._repo.entity_metas(self.sessionid, eid, **self._txid())
       
   645         except AttributeError:
       
   646             # talking to pre 3.19 repository
       
   647             metas = self._repo.describe(self.sessionid, eid, **self._txid())
       
   648             if len(metas) == 3: # even older backward compat
       
   649                 metas = list(metas)
       
   650                 metas.append(metas[1])
       
   651             return dict(zip(('type', 'source', 'extid', 'asource'), metas))
       
   652 
       
   653 
       
   654     @deprecated('[3.19] use .entity_metas(eid) instead')
       
   655     @check_not_closed
       
   656     def describe(self, eid, asdict=False):
       
   657         try:
       
   658             metas = self._repo.entity_metas(self.sessionid, eid, **self._txid())
       
   659         except AttributeError:
       
   660             metas = self._repo.describe(self.sessionid, eid, **self._txid())
       
   661             # talking to pre 3.19 repository
       
   662             if len(metas) == 3: # even older backward compat
       
   663                 metas = list(metas)
       
   664                 metas.append(metas[1])
       
   665             if asdict:
       
   666                 return dict(zip(('type', 'source', 'extid', 'asource'), metas))
       
   667             return metas[:-1]
       
   668         if asdict:
       
   669             metas['asource'] = meta['source'] # XXX pre 3.19 client compat
       
   670             return metas
       
   671         return metas['type'], metas['source'], metas['extid']
       
   672 
       
   673 
       
   674     # db-api like interface ####################################################
       
   675 
       
   676     @check_not_closed
       
   677     def commit(self):
       
   678         """Commit pending transaction for this connection to the repository.
       
   679 
       
   680         may raises `Unauthorized` or `ValidationError` if we attempted to do
       
   681         something we're not allowed to for security or integrity reason.
       
   682 
       
   683         If the transaction is undoable, a transaction id will be returned.
       
   684         """
       
   685         return self._repo.commit(self.sessionid, **self._txid())
       
   686 
       
   687     @check_not_closed
       
   688     def rollback(self):
       
   689         """This method is optional since not all databases provide transaction
       
   690         support.
       
   691 
       
   692         In case a database does provide transactions this method causes the the
       
   693         database to roll back to the start of any pending transaction.  Closing
       
   694         a connection without committing the changes first will cause an implicit
       
   695         rollback to be performed.
       
   696         """
       
   697         self._repo.rollback(self.sessionid, **self._txid())
       
   698 
       
   699     @check_not_closed
       
   700     def cursor(self, req=None):
       
   701         """Return a new Cursor Object using the connection.
       
   702         """
       
   703         if req is None:
       
   704             req = self.request()
       
   705         return self.cursor_class(self, self._repo, req=req)
       
   706 
       
   707     @check_not_closed
       
   708     def close(self):
       
   709         """Close the connection now (rather than whenever __del__ is called).
       
   710 
       
   711         The connection will be unusable from this point forward; an Error (or
       
   712         subclass) exception will be raised if any operation is attempted with
       
   713         the connection. The same applies to all cursor objects trying to use the
       
   714         connection.  Note that closing a connection without committing the
       
   715         changes first will cause an implicit rollback to be performed.
       
   716         """
       
   717         self._repo.close(self.sessionid, **self._txid())
       
   718         del self._repo # necessary for proper garbage collection
       
   719         self._closed = 1
       
   720 
       
   721     # undo support ############################################################
       
   722 
       
   723     @check_not_closed
       
   724     def undoable_transactions(self, ueid=None, req=None, **actionfilters):
       
   725         """Return a list of undoable transaction objects by the connection's
       
   726         user, ordered by descendant transaction time.
       
   727 
       
   728         Managers may filter according to user (eid) who has done the transaction
       
   729         using the `ueid` argument. Others will only see their own transactions.
       
   730 
       
   731         Additional filtering capabilities is provided by using the following
       
   732         named arguments:
       
   733 
       
   734         * `etype` to get only transactions creating/updating/deleting entities
       
   735           of the given type
       
   736 
       
   737         * `eid` to get only transactions applied to entity of the given eid
       
   738 
       
   739         * `action` to get only transactions doing the given action (action in
       
   740           'C', 'U', 'D', 'A', 'R'). If `etype`, action can only be 'C', 'U' or
       
   741           'D'.
       
   742 
       
   743         * `public`: when additional filtering is provided, their are by default
       
   744           only searched in 'public' actions, unless a `public` argument is given
       
   745           and set to false.
       
   746         """
       
   747         actionfilters.update(self._txid())
       
   748         txinfos = self._repo.undoable_transactions(self.sessionid, ueid,
       
   749                                                    **actionfilters)
       
   750         if req is None:
       
   751             req = self.request()
       
   752         for txinfo in txinfos:
       
   753             txinfo.req = req
       
   754         return txinfos
       
   755 
       
   756     @check_not_closed
       
   757     def transaction_info(self, txuuid, req=None):
       
   758         """Return transaction object for the given uid.
       
   759 
       
   760         raise `NoSuchTransaction` if not found or if session's user is not
       
   761         allowed (eg not in managers group and the transaction doesn't belong to
       
   762         him).
       
   763         """
       
   764         txinfo = self._repo.transaction_info(self.sessionid, txuuid,
       
   765                                              **self._txid())
       
   766         if req is None:
       
   767             req = self.request()
       
   768         txinfo.req = req
       
   769         return txinfo
       
   770 
       
   771     @check_not_closed
       
   772     def transaction_actions(self, txuuid, public=True):
       
   773         """Return an ordered list of action effectued during that transaction.
       
   774 
       
   775         If public is true, return only 'public' actions, eg not ones triggered
       
   776         under the cover by hooks, else return all actions.
       
   777 
       
   778         raise `NoSuchTransaction` if the transaction is not found or if
       
   779         session's user is not allowed (eg not in managers group and the
       
   780         transaction doesn't belong to him).
       
   781         """
       
   782         return self._repo.transaction_actions(self.sessionid, txuuid, public,
       
   783                                               **self._txid())
       
   784 
       
   785     @check_not_closed
       
   786     def undo_transaction(self, txuuid):
       
   787         """Undo the given transaction. Return potential restoration errors.
       
   788 
       
   789         raise `NoSuchTransaction` if not found or if session's user is not
       
   790         allowed (eg not in managers group and the transaction doesn't belong to
       
   791         him).
       
   792         """
       
   793         return self._repo.undo_transaction(self.sessionid, txuuid,
       
   794                                            **self._txid())
       
   795 
       
   796 in_memory_cnx = deprecated('[3.16] use _repo_connect instead)')(_repo_connect)