dbapi.py
branchstable
changeset 8743 27a83746aebd
parent 8702 d47089677d44
child 8744 2091d275fe5c
equal deleted inserted replaced
8742:bd374bd906f3 8743:27a83746aebd
     1 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     1 # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     3 #
     3 #
     4 # This file is part of CubicWeb.
     4 # This file is part of CubicWeb.
     5 #
     5 #
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
    29 from time import time, clock
    29 from time import time, clock
    30 from itertools import count
    30 from itertools import count
    31 from warnings import warn
    31 from warnings import warn
    32 from os.path import join
    32 from os.path import join
    33 from uuid import uuid4
    33 from uuid import uuid4
       
    34 from urlparse import  urlparse
    34 
    35 
    35 from logilab.common.logging_ext import set_log_methods
    36 from logilab.common.logging_ext import set_log_methods
    36 from logilab.common.decorators import monkeypatch
    37 from logilab.common.decorators import monkeypatch
    37 from logilab.common.deprecation import deprecated
    38 from logilab.common.deprecation import deprecated
    38 
    39 
    39 from cubicweb import ETYPE_NAME_MAP, ConnectionError, AuthenticationError,\
    40 from cubicweb import ETYPE_NAME_MAP, ConnectionError, AuthenticationError,\
    40      cwvreg, cwconfig
    41      cwvreg, cwconfig
    41 from cubicweb.req import RequestSessionBase
    42 from cubicweb.req import RequestSessionBase
       
    43 from cubicweb.utils import parse_repo_uri
    42 
    44 
    43 
    45 
    44 _MARKER = object()
    46 _MARKER = object()
    45 
    47 
    46 def _fake_property_value(self, name):
    48 def _fake_property_value(self, name):
    78     etypescls = cwvreg.CWRegistryStore.REGISTRY_FACTORY['etypes']
    80     etypescls = cwvreg.CWRegistryStore.REGISTRY_FACTORY['etypes']
    79     etypescls.etype_class = etypescls.orig_etype_class
    81     etypescls.etype_class = etypescls.orig_etype_class
    80 
    82 
    81 
    83 
    82 class ConnectionProperties(object):
    84 class ConnectionProperties(object):
    83     def __init__(self, cnxtype=None, lang=None, close=True, log=False):
    85     def __init__(self, cnxtype=None, close=True, log=False):
    84         self.cnxtype = cnxtype or 'pyro'
    86         if cnxtype is not None:
    85         self.lang = lang
    87             warn('[3.16] cnxtype argument is deprecated', DeprecationWarning,
       
    88                  stacklevel=2)
       
    89         self.cnxtype = cnxtype
    86         self.log_queries = log
    90         self.log_queries = log
    87         self.close_on_del = close
    91         self.close_on_del = close
    88 
    92 
    89 
    93 
    90 def get_repository(method, database=None, config=None, vreg=None):
    94 def _get_inmemory_repo(config, vreg=None):
    91     """get a proxy object to the CubicWeb repository, using a specific RPC method.
    95     from cubicweb.server.repository import Repository
    92 
    96     from cubicweb.server.utils import TasksManager
    93     Only 'in-memory' and 'pyro' are supported for now. Either vreg or config
    97     return Repository(config, TasksManager(), vreg=vreg)
    94     argument should be given
    98 
       
    99 def get_repository(uri=None, config=None, vreg=None):
       
   100     """get a repository for the given URI or config/vregistry (in case we're
       
   101     loading the repository for a client, eg web server, configuration).
       
   102 
       
   103     The returned repository may be an in-memory repository or a proxy object
       
   104     using a specific RPC method, depending on the given URI (pyro or zmq).
    95     """
   105     """
    96     assert method in ('pyro', 'inmemory', 'zmq')
   106     if uri is None:
    97     assert vreg or config
   107         return _get_inmemory_repo(config, vreg)
    98     if vreg and not config:
   108 
    99         config = vreg.config
   109     protocol, hostport, appid = parse_repo_uri(uri)
   100     if method == 'inmemory':
   110 
   101         # get local access to the repository
   111     if protocol == 'inmemory':
   102         from cubicweb.server.repository import Repository
   112         # me may have been called with a dummy 'inmemory://' uri ...
   103         from cubicweb.server.utils import TasksManager
   113         return _get_inmemory_repo(config, vreg)
   104         return Repository(config, TasksManager(), vreg=vreg)
   114 
   105     elif method == 'zmq':
   115     if protocol == 'pyroloc': # direct connection to the instance
       
   116         from logilab.common.pyro_ext import get_proxy
       
   117         uri = uri.replace('pyroloc', 'PYRO')
       
   118         return get_proxy(uri)
       
   119 
       
   120     if protocol == 'pyro': # connection mediated through the pyro ns
       
   121         from logilab.common.pyro_ext import ns_get_proxy
       
   122         path = appid.strip('/')
       
   123         if not path:
       
   124             raise ConnectionError(
       
   125                 "can't find instance name in %s (expected to be the path component)"
       
   126                 % uri)
       
   127         if '.' in path:
       
   128             nsgroup, nsid = path.rsplit('.', 1)
       
   129         else:
       
   130             nsgroup = 'cubicweb'
       
   131             nsid = path
       
   132         return ns_get_proxy(nsid, defaultnsgroup=nsgroup, nshost=hostport)
       
   133 
       
   134     if protocol.startswith('zmqpickle-'):
   106         from cubicweb.zmqclient import ZMQRepositoryClient
   135         from cubicweb.zmqclient import ZMQRepositoryClient
   107         return ZMQRepositoryClient(database)
   136         return ZMQRepositoryClient(uri)
   108     else: # method == 'pyro'
   137     else:
   109         # resolve the Pyro object
   138         raise ConnectionError('unknown protocol: `%s`' % protocol)
   110         from logilab.common.pyro_ext import ns_get_proxy, get_proxy
   139 
   111         pyroid = database or config['pyro-instance-id'] or config.appid
   140 
   112         try:
   141 def _repo_connect(repo, login, **kwargs):
   113             if config['pyro-ns-host'] == 'NO_PYRONS':
   142     """Constructor to create a new connection to the given CubicWeb repository.
   114                 return get_proxy(pyroid)
       
   115             else:
       
   116                 return ns_get_proxy(pyroid, defaultnsgroup=config['pyro-ns-group'],
       
   117                                     nshost=config['pyro-ns-host'])
       
   118         except Exception, ex:
       
   119             raise ConnectionError(str(ex))
       
   120 
       
   121 def repo_connect(repo, login, **kwargs):
       
   122     """Constructor to create a new connection to the CubicWeb repository.
       
   123 
   143 
   124     Returns a Connection instance.
   144     Returns a Connection instance.
       
   145 
       
   146     Raises AuthenticationError if authentication failed
   125     """
   147     """
   126     if not 'cnxprops' in kwargs:
       
   127         kwargs['cnxprops'] = ConnectionProperties('inmemory')
       
   128     cnxid = repo.connect(unicode(login), **kwargs)
   148     cnxid = repo.connect(unicode(login), **kwargs)
   129     cnx = Connection(repo, cnxid, kwargs['cnxprops'])
   149     cnx = Connection(repo, cnxid, kwargs.get('cnxprops'))
   130     if kwargs['cnxprops'].cnxtype == 'inmemory':
   150     if cnx.is_repo_in_memory:
   131         cnx.vreg = repo.vreg
   151         cnx.vreg = repo.vreg
   132     return cnx
   152     return cnx
   133 
   153 
   134 def connect(database=None, login=None, host=None, group=None,
   154 def connect(database, login=None,
   135             cnxprops=None, setvreg=True, mulcnx=True, initlog=True, **kwargs):
   155             cnxprops=None, setvreg=True, mulcnx=True, initlog=True, **kwargs):
   136     """Constructor for creating a connection to the CubicWeb repository.
   156     """Constructor for creating a connection to the CubicWeb repository.
   137     Returns a :class:`Connection` object.
   157     Returns a :class:`Connection` object.
   138 
   158 
   139     Typical usage::
   159     Typical usage::
   140 
   160 
   141       cnx = connect('myinstance', login='me', password='toto')
   161       cnx = connect('myinstance', login='me', password='toto')
   142 
   162 
   143     Arguments:
   163     `database` may be:
   144 
   164 
   145     :database:
   165     * a simple instance id for in-memory connection
   146       the instance's pyro identifier.
   166 
       
   167     * an uri like scheme://host:port/instanceid where scheme may be one of
       
   168       'pyro', 'inmemory' or 'zmqpickle'
       
   169 
       
   170       * if scheme is 'pyro', <host:port> determine the name server address. If
       
   171         not specified (e.g. 'pyro:///instanceid'), it will be detected through a
       
   172         broadcast query. The instance id is the name of the instance in the name
       
   173         server and may be prefixed by a group (e.g.
       
   174         'pyro:///:cubicweb.instanceid')
       
   175 
       
   176       * if scheme is handled by ZMQ (eg 'tcp'), you should not specify an
       
   177         instance id
       
   178 
       
   179     Other arguments:
   147 
   180 
   148     :login:
   181     :login:
   149       the user login to use to authenticate.
   182       the user login to use to authenticate.
   150 
   183 
   151     :host:
       
   152       - pyro: nameserver host. Will be detected using broadcast query if unspecified
       
   153       - zmq: repository host socket address
       
   154 
       
   155     :group:
       
   156       the instance's pyro nameserver group. You don't have to specify it unless
       
   157       tweaked in instance's configuration.
       
   158 
       
   159     :cnxprops:
   184     :cnxprops:
   160       an optional :class:`ConnectionProperties` instance, allowing to specify
   185       a :class:`ConnectionProperties` instance, allowing to specify
   161       the connection method (eg in memory or pyro). A Pyro connection will be
   186       the connection method (eg in memory or pyro). A Pyro connection will be
   162       established if you don't specify that argument.
   187       established if you don't specify that argument.
   163 
   188 
   164     :setvreg:
   189     :setvreg:
   165       flag telling if a registry should be initialized for the connection.
   190       flag telling if a registry should be initialized for the connection.
   177 
   202 
   178     :kwargs:
   203     :kwargs:
   179       there goes authentication tokens. You usually have to specify a password
   204       there goes authentication tokens. You usually have to specify a password
   180       for the given user, using a named 'password' argument.
   205       for the given user, using a named 'password' argument.
   181     """
   206     """
   182     cnxprops = cnxprops or ConnectionProperties()
   207     if urlparse(database).scheme is None:
   183     method = cnxprops.cnxtype
   208         warn('[3.16] give an qualified URI as database instead of using '
   184     if method == 'pyro':
   209              'host/cnxprops to specify the connection method',
       
   210              DeprecationWarning, stacklevel=2)
       
   211         if cnxprops.cnxtype == 'zmq':
       
   212             database = kwargs.pop('host')
       
   213         elif cnxprops.cnxtype == 'inmemory':
       
   214             database = 'inmemory://' + database
       
   215         else:
       
   216             database = 'pyro://%s/%s.%s' % (kwargs.pop('host', ''),
       
   217                                             kwargs.pop('group', 'cubicweb'),
       
   218                                             database)
       
   219     puri = urlparse(database)
       
   220     method = puri.scheme.lower()
       
   221     if method == 'inmemory':
       
   222         config = cwconfig.instance_configuration(puri.path)
       
   223     else:
   185         config = cwconfig.CubicWebNoAppConfiguration()
   224         config = cwconfig.CubicWebNoAppConfiguration()
   186         if host:
   225     repo = get_repository(database, config=config)
   187             config.global_set_option('pyro-ns-host', host)
       
   188         if group:
       
   189             config.global_set_option('pyro-ns-group', group)
       
   190     elif method == 'zmq':
       
   191         config = cwconfig.CubicWebNoAppConfiguration()
       
   192     else:
       
   193         assert database
       
   194         config = cwconfig.instance_configuration(database)
       
   195     repo = get_repository(method, database, config=config)
       
   196     if method == 'inmemory':
   226     if method == 'inmemory':
   197         vreg = repo.vreg
   227         vreg = repo.vreg
   198     elif setvreg:
   228     elif setvreg:
   199         if mulcnx:
   229         if mulcnx:
   200             multiple_connections_fix()
   230             multiple_connections_fix()
   205                 print 'aliasing', newetype, 'to', oldetype
   235                 print 'aliasing', newetype, 'to', oldetype
   206                 schema._entities[newetype] = schema._entities[oldetype]
   236                 schema._entities[newetype] = schema._entities[oldetype]
   207         vreg.set_schema(schema)
   237         vreg.set_schema(schema)
   208     else:
   238     else:
   209         vreg = None
   239         vreg = None
   210     cnx = repo_connect(repo, login, cnxprops=cnxprops, **kwargs)
   240     cnx = _repo_connect(repo, login, cnxprops=cnxprops, **kwargs)
   211     cnx.vreg = vreg
   241     cnx.vreg = vreg
   212     return cnx
   242     return cnx
   213 
   243 
   214 def in_memory_repo(config):
   244 def in_memory_repo(config):
   215     """Return and in_memory Repository object from a config (or vreg)"""
   245     """Return and in_memory Repository object from a config (or vreg)"""
   217         vreg = config
   247         vreg = config
   218         config = None
   248         config = None
   219     else:
   249     else:
   220         vreg = None
   250         vreg = None
   221     # get local access to the repository
   251     # get local access to the repository
   222     return get_repository('inmemory', config=config, vreg=vreg)
   252     return get_repository('inmemory://', config=config, vreg=vreg)
   223 
       
   224 def in_memory_cnx(repo, login, **kwargs):
       
   225     """Establish a In memory connection to a <repo> for the user with <login>
       
   226 
       
   227     additionel credential might be required"""
       
   228     cnxprops = ConnectionProperties('inmemory')
       
   229     return repo_connect(repo, login, cnxprops=cnxprops, **kwargs)
       
   230 
   253 
   231 def in_memory_repo_cnx(config, login, **kwargs):
   254 def in_memory_repo_cnx(config, login, **kwargs):
   232     """useful method for testing and scripting to get a dbapi.Connection
   255     """useful method for testing and scripting to get a dbapi.Connection
   233     object connected to an in-memory repository instance
   256     object connected to an in-memory repository instance
   234     """
   257     """
   235     # connection to the CubicWeb repository
   258     # connection to the CubicWeb repository
   236     repo = in_memory_repo(config)
   259     repo = in_memory_repo(config)
   237     return repo, in_memory_cnx(repo, login, **kwargs)
   260     return repo, _repo_connect(repo, login, **kwargs)
   238 
   261 
   239 
   262 # XXX web only method, move to webconfig?
   240 def anonymous_session(vreg):
   263 def anonymous_session(vreg):
   241     """return a new anonymous session
   264     """return a new anonymous session
   242 
   265 
   243     raises an AuthenticationError if anonymous usage is not allowed
   266     raises an AuthenticationError if anonymous usage is not allowed
   244     """
   267     """
   245     anoninfo = vreg.config.anonymous_user()
   268     anoninfo = vreg.config.anonymous_user()
   246     if anoninfo is None: # no anonymous user
   269     if anoninfo is None: # no anonymous user
   247         raise AuthenticationError('anonymous access is not authorized')
   270         raise AuthenticationError('anonymous access is not authorized')
   248     anon_login, anon_password = anoninfo
   271     anon_login, anon_password = anoninfo
   249     cnxprops = ConnectionProperties(vreg.config.repo_method)
       
   250     # use vreg's repository cache
   272     # use vreg's repository cache
   251     repo = vreg.config.repository(vreg)
   273     repo = vreg.config.repository(vreg)
   252     anon_cnx = repo_connect(repo, anon_login,
   274     anon_cnx = _repo_connect(repo, anon_login, password=anon_password)
   253                             cnxprops=cnxprops, password=anon_password)
       
   254     anon_cnx.vreg = vreg
   275     anon_cnx.vreg = vreg
   255     return DBAPISession(anon_cnx, anon_login)
   276     return DBAPISession(anon_cnx, anon_login)
   256 
   277 
   257 
   278 
   258 class _NeedAuthAccessMock(object):
   279 class _NeedAuthAccessMock(object):
   280         return not self.cnx or self.cnx.anonymous_connection
   301         return not self.cnx or self.cnx.anonymous_connection
   281 
   302 
   282     def __repr__(self):
   303     def __repr__(self):
   283         return '<DBAPISession %r>' % self.sessionid
   304         return '<DBAPISession %r>' % self.sessionid
   284 
   305 
       
   306 
   285 class DBAPIRequest(RequestSessionBase):
   307 class DBAPIRequest(RequestSessionBase):
       
   308     #: Request language identifier eg: 'en'
       
   309     lang = None
   286 
   310 
   287     def __init__(self, vreg, session=None):
   311     def __init__(self, vreg, session=None):
   288         super(DBAPIRequest, self).__init__(vreg)
   312         super(DBAPIRequest, self).__init__(vreg)
   289         #: 'language' => translation_function() mapping
   313         #: 'language' => translation_function() mapping
   290         try:
   314         try:
   291             # no vreg or config which doesn't handle translations
   315             # no vreg or config which doesn't handle translations
   292             self.translations = vreg.config.translations
   316             self.translations = vreg.config.translations
   293         except AttributeError:
   317         except AttributeError:
   294             self.translations = {}
   318             self.translations = {}
   295         #: Request language identifier eg: 'en'
       
   296         self.lang = None
       
   297         self.set_default_language(vreg)
       
   298         #: cache entities built during the request
   319         #: cache entities built during the request
   299         self._eid_cache = {}
   320         self._eid_cache = {}
   300         if session is not None:
   321         if session is not None:
   301             self.set_session(session)
   322             self.set_session(session)
   302         else:
   323         else:
   303             # these args are initialized after a connection is
   324             # these args are initialized after a connection is
   304             # established
   325             # established
   305             self.session = None
   326             self.session = None
   306             self.cnx = self.user = _NeedAuthAccessMock()
   327             self.cnx = self.user = _NeedAuthAccessMock()
       
   328         self.set_default_language(vreg)
   307 
   329 
   308     def from_controller(self):
   330     def from_controller(self):
   309         return 'view'
   331         return 'view'
   310 
   332 
   311     def get_option_value(self, option, foreid=None):
   333     def get_option_value(self, option, foreid=None):
   318         self.session = session
   340         self.session = session
   319         if session.cnx:
   341         if session.cnx:
   320             self.cnx = session.cnx
   342             self.cnx = session.cnx
   321             self.execute = session.cnx.cursor(self).execute
   343             self.execute = session.cnx.cursor(self).execute
   322             if user is None:
   344             if user is None:
   323                 user = self.cnx.user(self, {'lang': self.lang})
   345                 user = self.cnx.user(self)
   324         if user is not None:
   346         if user is not None:
   325             self.user = user
   347             self.user = user
   326             self.set_entity_cache(user)
   348             self.set_entity_cache(user)
   327 
   349 
   328     def execute(self, *args, **kwargs): # pylint: disable=E0202
   350     def execute(self, *args, **kwargs): # pylint: disable=E0202
   331         """
   353         """
   332         raise AuthenticationError()
   354         raise AuthenticationError()
   333 
   355 
   334     def set_default_language(self, vreg):
   356     def set_default_language(self, vreg):
   335         try:
   357         try:
   336             self.lang = vreg.property_value('ui.language')
   358             lang = vreg.property_value('ui.language')
   337         except Exception: # property may not be registered
   359         except Exception: # property may not be registered
   338             self.lang = 'en'
   360             lang = 'en'
   339         # use req.__ to translate a message without registering it to the catalog
       
   340         try:
   361         try:
   341             gettext, pgettext = self.translations[self.lang]
   362             self.set_language(lang)
   342             self._ = self.__ = gettext
       
   343             self.pgettext = pgettext
       
   344         except KeyError:
   363         except KeyError:
   345             # this occurs usually during test execution
   364             # this occurs usually during test execution
   346             self._ = self.__ = unicode
   365             self._ = self.__ = unicode
   347             self.pgettext = lambda x, y: unicode(y)
   366             self.pgettext = lambda x, y: unicode(y)
   348         self.debug('request default language: %s', self.lang)
       
   349 
   367 
   350     # server-side service call #################################################
   368     # server-side service call #################################################
   351 
   369 
   352     def call_service(self, regid, async=False, **kwargs):
   370     def call_service(self, regid, async=False, **kwargs):
   353         return self.cnx.call_service(regid, async, **kwargs)
   371         return self.cnx.call_service(regid, async, **kwargs)
   538 
   556 
   539     def __init__(self, repo, cnxid, cnxprops=None):
   557     def __init__(self, repo, cnxid, cnxprops=None):
   540         self._repo = repo
   558         self._repo = repo
   541         self.sessionid = cnxid
   559         self.sessionid = cnxid
   542         self._close_on_del = getattr(cnxprops, 'close_on_del', True)
   560         self._close_on_del = getattr(cnxprops, 'close_on_del', True)
   543         self._cnxtype = getattr(cnxprops, 'cnxtype', 'pyro')
       
   544         self._web_request = False
   561         self._web_request = False
   545         if cnxprops and cnxprops.log_queries:
   562         if cnxprops and cnxprops.log_queries:
   546             self.executed_queries = []
   563             self.executed_queries = []
   547             self.cursor_class = LogCursor
   564             self.cursor_class = LogCursor
   548         if self._cnxtype == 'pyro':
   565 
   549             # check client/server compat
   566     @property
   550             if self._repo.get_versions()['cubicweb'] < (3, 8, 6):
   567     def is_repo_in_memory(self):
   551                 self._txid = lambda cursor=None: {}
   568         """return True if this is a local, aka in-memory, connection to the
       
   569         repository
       
   570         """
       
   571         try:
       
   572             from cubicweb.server.repository import Repository
       
   573         except ImportError:
       
   574             # code not available, no way
       
   575             return False
       
   576         return isinstance(self._repo, Repository)
   552 
   577 
   553     def __repr__(self):
   578     def __repr__(self):
   554         if self.anonymous_connection:
   579         if self.anonymous_connection:
   555             return '<Connection %s (anonymous)>' % self.sessionid
   580             return '<Connection %s (anonymous)>' % self.sessionid
   556         return '<Connection %s>' % self.sessionid
   581         return '<Connection %s>' % self.sessionid
   604         # properties initialization
   629         # properties initialization
   605         config.load_available_configs()
   630         config.load_available_configs()
   606         # then init cubes
   631         # then init cubes
   607         config.init_cubes(cubes)
   632         config.init_cubes(cubes)
   608         # then load appobjects into the registry
   633         # then load appobjects into the registry
   609         vpath = config.build_vregistry_path(reversed(config.cubes_path()),
   634         vpath = config.build_appobjects_path(reversed(config.cubes_path()),
   610                                             evobjpath=esubpath,
   635                                              evobjpath=esubpath,
   611                                             tvobjpath=subpath)
   636                                              tvobjpath=subpath)
   612         self.vreg.register_objects(vpath)
   637         self.vreg.register_objects(vpath)
   613 
   638 
   614     def use_web_compatible_requests(self, baseurl, sitetitle=None):
   639     def use_web_compatible_requests(self, baseurl, sitetitle=None):
   615         """monkey patch DBAPIRequest to fake a cw.web.request, so you should
   640         """monkey patch DBAPIRequest to fake a cw.web.request, so you should
   616         able to call html views using rset from a simple dbapi connection.
   641         able to call html views using rset from a simple dbapi connection.
   676         return {'txid': currentThread().getName()}
   701         return {'txid': currentThread().getName()}
   677 
   702 
   678     # session data methods #####################################################
   703     # session data methods #####################################################
   679 
   704 
   680     @check_not_closed
   705     @check_not_closed
   681     def set_session_props(self, **props):
       
   682         """raise `BadConnectionId` if the connection is no more valid"""
       
   683         self._repo.set_session_props(self.sessionid, props)
       
   684 
       
   685     @check_not_closed
       
   686     def get_shared_data(self, key, default=None, pop=False, txdata=False):
   706     def get_shared_data(self, key, default=None, pop=False, txdata=False):
   687         """return value associated to key in the session's data dictionary or
   707         """return value associated to key in the session's data dictionary or
   688         session's transaction's data if `txdata` is true.
   708         session's transaction's data if `txdata` is true.
   689 
   709 
   690         If pop is True, value will be removed from the dictionary.
   710         If pop is True, value will be removed from the dictionary.
   858         allowed (eg not in managers group and the transaction doesn't belong to
   878         allowed (eg not in managers group and the transaction doesn't belong to
   859         him).
   879         him).
   860         """
   880         """
   861         return self._repo.undo_transaction(self.sessionid, txuuid,
   881         return self._repo.undo_transaction(self.sessionid, txuuid,
   862                                            **self._txid())
   882                                            **self._txid())
       
   883 
       
   884 in_memory_cnx = deprecated('[3.16] use _repo_connect instead)')(_repo_connect)