dbapi.py
changeset 8669 62213a34726e
parent 8596 bd4f5052a532
child 8670 f02139297beb
equal deleted inserted replaced
8668:4fea61c636b2 8669:62213a34726e
     1 # copyright 2003-2012 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 
    79     etypescls.etype_class = etypescls.orig_etype_class
    80     etypescls.etype_class = etypescls.orig_etype_class
    80 
    81 
    81 
    82 
    82 class ConnectionProperties(object):
    83 class ConnectionProperties(object):
    83     def __init__(self, cnxtype=None, close=True, log=False):
    84     def __init__(self, cnxtype=None, close=True, log=False):
    84         self.cnxtype = cnxtype or 'pyro'
    85         if cnxtype is not None:
       
    86             warn('[3.16] cnxtype argument is deprecated', DeprecationWarning,
       
    87                  stacklevel=2)
       
    88         self.cnxtype = cnxtype
    85         self.log_queries = log
    89         self.log_queries = log
    86         self.close_on_del = close
    90         self.close_on_del = close
    87 
    91 
    88 
    92 
    89 def get_repository(method, database=None, config=None, vreg=None):
    93 def get_repository(uri=None, config=None, vreg=None):
    90     """get a proxy object to the CubicWeb repository, using a specific RPC method.
    94     """get a repository for the given URI or config/vregistry (in case we're
    91 
    95     loading the repository for a client, eg web server, configuration).
    92     Only 'in-memory' and 'pyro' are supported for now. Either vreg or config
    96 
    93     argument should be given
    97     The returned repository may be an in-memory repository or a proxy object
       
    98     using a specific RPC method, depending on the given URI (pyro or zmq).
    94     """
    99     """
    95     assert method in ('pyro', 'inmemory', 'zmq')
   100     if uri is None:
    96     assert vreg or config
   101         uri = config['repository-uri'] or config.appid
    97     if vreg and not config:
   102     puri = urlparse(uri)
    98         config = vreg.config
   103     method = puri.scheme.lower() or 'inmemory'
    99     if method == 'inmemory':
   104     if method == 'inmemory':
   100         # get local access to the repository
   105         # get local access to the repository
   101         from cubicweb.server.repository import Repository
   106         from cubicweb.server.repository import Repository
   102         from cubicweb.server.utils import TasksManager
   107         from cubicweb.server.utils import TasksManager
   103         return Repository(config, TasksManager(), vreg=vreg)
   108         return Repository(config, TasksManager(), vreg=vreg)
   104     elif method == 'zmq':
   109     elif method in ('pyro', 'pyroloc'):
   105         from cubicweb.zmqclient import ZMQRepositoryClient
       
   106         return ZMQRepositoryClient(database)
       
   107     else: # method == 'pyro'
       
   108         # resolve the Pyro object
   110         # resolve the Pyro object
   109         from logilab.common.pyro_ext import ns_get_proxy, get_proxy
   111         from logilab.common.pyro_ext import ns_get_proxy, get_proxy
   110         pyroid = database or config['pyro-instance-id'] or config.appid
       
   111         try:
   112         try:
   112             if config['pyro-ns-host'] == 'NO_PYRONS':
   113             if puri.scheme == 'pyroloc':
   113                 return get_proxy(pyroid)
   114                 return get_proxy(uri)
       
   115             path = puri.path.rstrip('/')
       
   116             if not path:
       
   117                 raise ConnectionError(
       
   118                     "can't find instance name in %s (expected to be the path component)"
       
   119                     % uri)
       
   120             if '.' in path:
       
   121                 nsgroup, nsid = path.rsplit('.', 1)
   114             else:
   122             else:
   115                 return ns_get_proxy(pyroid, defaultnsgroup=config['pyro-ns-group'],
   123                 nsgroup = 'cubicweb'
   116                                     nshost=config['pyro-ns-host'])
   124                 nsid = path
       
   125             return ns_get_proxy(nsid, defaultnsgroup=nsgroup, nshost=puri.netloc)
   117         except Exception, ex:
   126         except Exception, ex:
   118             raise ConnectionError(str(ex))
   127             raise ConnectionError(str(ex))
       
   128     elif method == 'tcp': # use zmq (see zmq documentation)
       
   129         from cubicweb.zmqclient import ZMQRepositoryClient
       
   130         return ZMQRepositoryClient(uri)
       
   131     else:
       
   132         raise ConnectionError('unknown protocol: `%s`' % method)
       
   133 
   119 
   134 
   120 def repo_connect(repo, login, **kwargs):
   135 def repo_connect(repo, login, **kwargs):
   121     """Constructor to create a new connection to the CubicWeb repository.
   136     """Constructor to create a new connection to the given CubicWeb repository.
   122 
   137 
   123     Returns a Connection instance.
   138     Returns a Connection instance.
       
   139 
       
   140     Raises AuthenticationError if authentication failed
   124     """
   141     """
   125     if not 'cnxprops' in kwargs:
       
   126         kwargs['cnxprops'] = ConnectionProperties('inmemory')
       
   127     cnxid = repo.connect(unicode(login), **kwargs)
   142     cnxid = repo.connect(unicode(login), **kwargs)
   128     cnx = Connection(repo, cnxid, kwargs['cnxprops'])
   143     cnx = Connection(repo, cnxid, kwargs.get('cnxprops'))
   129     if kwargs['cnxprops'].cnxtype == 'inmemory':
   144     if cnx.is_repo_in_memory:
   130         cnx.vreg = repo.vreg
   145         cnx.vreg = repo.vreg
   131     return cnx
   146     return cnx
   132 
   147 
   133 def connect(database=None, login=None, host=None, group=None,
   148 def connect(database, login=None,
   134             cnxprops=None, setvreg=True, mulcnx=True, initlog=True, **kwargs):
   149             cnxprops=None, setvreg=True, mulcnx=True, initlog=True, **kwargs):
   135     """Constructor for creating a connection to the CubicWeb repository.
   150     """Constructor for creating a connection to the CubicWeb repository.
   136     Returns a :class:`Connection` object.
   151     Returns a :class:`Connection` object.
   137 
   152 
   138     Typical usage::
   153     Typical usage::
   139 
   154 
   140       cnx = connect('myinstance', login='me', password='toto')
   155       cnx = connect('myinstance', login='me', password='toto')
   141 
   156 
   142     Arguments:
   157     `database` may be:
   143 
   158 
   144     :database:
   159     * a simple instance id for in-memory connection
   145       the instance's pyro identifier.
   160 
       
   161     * an uri like scheme://host:port/instanceid where scheme may be one of
       
   162       'pyro', 'pyroloc', 'inmemory' or a schema supported by ZMQ
       
   163 
       
   164       * if scheme is 'pyro', <host:port> determine the name server address. If
       
   165         not specified (e.g. 'pyro:///instanceid'), it will be detected through a
       
   166         broadcast query. The instance id is the name of the instance in the name
       
   167         server and may be prefixed by a group (e.g.
       
   168         'pyro:///:cubicweb.instanceid')
       
   169 
       
   170       * if scheme is 'pyroloc', it's expected to be a bare pyro location URI
       
   171 
       
   172       * if scheme is handled by ZMQ (eg 'tcp'), you should not specify an
       
   173         instance id
       
   174 
       
   175     Other arguments:
   146 
   176 
   147     :login:
   177     :login:
   148       the user login to use to authenticate.
   178       the user login to use to authenticate.
   149 
   179 
   150     :host:
       
   151       - pyro: nameserver host. Will be detected using broadcast query if unspecified
       
   152       - zmq: repository host socket address
       
   153 
       
   154     :group:
       
   155       the instance's pyro nameserver group. You don't have to specify it unless
       
   156       tweaked in instance's configuration.
       
   157 
       
   158     :cnxprops:
   180     :cnxprops:
   159       an optional :class:`ConnectionProperties` instance, allowing to specify
   181       a :class:`ConnectionProperties` instance, allowing to specify
   160       the connection method (eg in memory or pyro). A Pyro connection will be
   182       the connection method (eg in memory or pyro). A Pyro connection will be
   161       established if you don't specify that argument.
   183       established if you don't specify that argument.
   162 
   184 
   163     :setvreg:
   185     :setvreg:
   164       flag telling if a registry should be initialized for the connection.
   186       flag telling if a registry should be initialized for the connection.
   176 
   198 
   177     :kwargs:
   199     :kwargs:
   178       there goes authentication tokens. You usually have to specify a password
   200       there goes authentication tokens. You usually have to specify a password
   179       for the given user, using a named 'password' argument.
   201       for the given user, using a named 'password' argument.
   180     """
   202     """
   181     cnxprops = cnxprops or ConnectionProperties()
   203     if urlparse(database).scheme is None:
   182     method = cnxprops.cnxtype
   204         warn('[3.16] give an qualified URI as database instead of using '
   183     if method == 'pyro':
   205              'host/cnxprops to specify the connection method',
       
   206              DeprecationWarning, stacklevel=2)
       
   207         if cnxprops.cnxtype == 'zmq':
       
   208             database = kwargs.pop('host')
       
   209         elif cnxprops.cnxtype == 'inmemory':
       
   210             database = 'inmemory://' + database
       
   211         else:
       
   212             database = 'pyro://%s/%s.%s' % (kwargs.pop('host', ''),
       
   213                                             kwargs.pop('group', 'cubicweb'),
       
   214                                             database)
       
   215     puri = urlparse(database)
       
   216     method = puri.scheme.lower()
       
   217     if method == 'inmemory':
       
   218         config = cwconfig.instance_configuration(puuri.path)
       
   219     else:
   184         config = cwconfig.CubicWebNoAppConfiguration()
   220         config = cwconfig.CubicWebNoAppConfiguration()
   185         if host:
   221     repo = get_repository(database, config=config)
   186             config.global_set_option('pyro-ns-host', host)
       
   187         if group:
       
   188             config.global_set_option('pyro-ns-group', group)
       
   189     elif method == 'zmq':
       
   190         config = cwconfig.CubicWebNoAppConfiguration()
       
   191     else:
       
   192         assert database
       
   193         config = cwconfig.instance_configuration(database)
       
   194     repo = get_repository(method, database, config=config)
       
   195     if method == 'inmemory':
   222     if method == 'inmemory':
   196         vreg = repo.vreg
   223         vreg = repo.vreg
   197     elif setvreg:
   224     elif setvreg:
   198         if mulcnx:
   225         if mulcnx:
   199             multiple_connections_fix()
   226             multiple_connections_fix()
   216         vreg = config
   243         vreg = config
   217         config = None
   244         config = None
   218     else:
   245     else:
   219         vreg = None
   246         vreg = None
   220     # get local access to the repository
   247     # get local access to the repository
   221     return get_repository('inmemory', config=config, vreg=vreg)
   248     return get_repository('inmemory://', config=config, vreg=vreg)
   222 
       
   223 def in_memory_cnx(repo, login, **kwargs):
       
   224     """Establish a In memory connection to a <repo> for the user with <login>
       
   225 
       
   226     additionel credential might be required"""
       
   227     cnxprops = ConnectionProperties('inmemory')
       
   228     return repo_connect(repo, login, cnxprops=cnxprops, **kwargs)
       
   229 
   249 
   230 def in_memory_repo_cnx(config, login, **kwargs):
   250 def in_memory_repo_cnx(config, login, **kwargs):
   231     """useful method for testing and scripting to get a dbapi.Connection
   251     """useful method for testing and scripting to get a dbapi.Connection
   232     object connected to an in-memory repository instance
   252     object connected to an in-memory repository instance
   233     """
   253     """
   234     # connection to the CubicWeb repository
   254     # connection to the CubicWeb repository
   235     repo = in_memory_repo(config)
   255     repo = in_memory_repo(config)
   236     return repo, in_memory_cnx(repo, login, **kwargs)
   256     return repo, repo_connect(repo, login, **kwargs)
   237 
   257 
   238 
   258 # XXX web only method, move to webconfig?
   239 def anonymous_session(vreg):
   259 def anonymous_session(vreg):
   240     """return a new anonymous session
   260     """return a new anonymous session
   241 
   261 
   242     raises an AuthenticationError if anonymous usage is not allowed
   262     raises an AuthenticationError if anonymous usage is not allowed
   243     """
   263     """
   244     anoninfo = vreg.config.anonymous_user()
   264     anoninfo = vreg.config.anonymous_user()
   245     if anoninfo is None: # no anonymous user
   265     if anoninfo is None: # no anonymous user
   246         raise AuthenticationError('anonymous access is not authorized')
   266         raise AuthenticationError('anonymous access is not authorized')
   247     anon_login, anon_password = anoninfo
   267     anon_login, anon_password = anoninfo
   248     cnxprops = ConnectionProperties(vreg.config.repo_method)
       
   249     # use vreg's repository cache
   268     # use vreg's repository cache
   250     repo = vreg.config.repository(vreg)
   269     repo = vreg.config.repository(vreg)
   251     anon_cnx = repo_connect(repo, anon_login,
   270     anon_cnx = repo_connect(repo, anon_login, password=anon_password)
   252                             cnxprops=cnxprops, password=anon_password)
       
   253     anon_cnx.vreg = vreg
   271     anon_cnx.vreg = vreg
   254     return DBAPISession(anon_cnx, anon_login)
   272     return DBAPISession(anon_cnx, anon_login)
   255 
   273 
   256 
   274 
   257 class _NeedAuthAccessMock(object):
   275 class _NeedAuthAccessMock(object):
   278     def anonymous_session(self):
   296     def anonymous_session(self):
   279         return not self.cnx or self.cnx.anonymous_connection
   297         return not self.cnx or self.cnx.anonymous_connection
   280 
   298 
   281     def __repr__(self):
   299     def __repr__(self):
   282         return '<DBAPISession %r>' % self.sessionid
   300         return '<DBAPISession %r>' % self.sessionid
       
   301 
   283 
   302 
   284 class DBAPIRequest(RequestSessionBase):
   303 class DBAPIRequest(RequestSessionBase):
   285     #: Request language identifier eg: 'en'
   304     #: Request language identifier eg: 'en'
   286     lang = None
   305     lang = None
   287 
   306 
   533 
   552 
   534     def __init__(self, repo, cnxid, cnxprops=None):
   553     def __init__(self, repo, cnxid, cnxprops=None):
   535         self._repo = repo
   554         self._repo = repo
   536         self.sessionid = cnxid
   555         self.sessionid = cnxid
   537         self._close_on_del = getattr(cnxprops, 'close_on_del', True)
   556         self._close_on_del = getattr(cnxprops, 'close_on_del', True)
   538         self._cnxtype = getattr(cnxprops, 'cnxtype', 'pyro')
       
   539         self._web_request = False
   557         self._web_request = False
   540         if cnxprops and cnxprops.log_queries:
   558         if cnxprops and cnxprops.log_queries:
   541             self.executed_queries = []
   559             self.executed_queries = []
   542             self.cursor_class = LogCursor
   560             self.cursor_class = LogCursor
   543         if self._cnxtype == 'pyro':
   561 
   544             # check client/server compat
   562     @property
   545             if self._repo.get_versions()['cubicweb'] < (3, 8, 6):
   563     def is_repo_in_memory(self):
   546                 self._txid = lambda cursor=None: {}
   564         """return True if this is a local, aka in-memory, connection to the
       
   565         repository
       
   566         """
       
   567         try:
       
   568             from cubicweb.server.repository import Repository
       
   569         except ImportError:
       
   570             # code not available, no way
       
   571             return False
       
   572         return isinstance(self._repo, Repository)
   547 
   573 
   548     def __repr__(self):
   574     def __repr__(self):
   549         if self.anonymous_connection:
   575         if self.anonymous_connection:
   550             return '<Connection %s (anonymous)>' % self.sessionid
   576             return '<Connection %s (anonymous)>' % self.sessionid
   551         return '<Connection %s>' % self.sessionid
   577         return '<Connection %s>' % self.sessionid
   848         allowed (eg not in managers group and the transaction doesn't belong to
   874         allowed (eg not in managers group and the transaction doesn't belong to
   849         him).
   875         him).
   850         """
   876         """
   851         return self._repo.undo_transaction(self.sessionid, txuuid,
   877         return self._repo.undo_transaction(self.sessionid, txuuid,
   852                                            **self._txid())
   878                                            **self._txid())
       
   879 
       
   880 in_memory_cnx = deprecated('[3.16] use repo_connect instead)')(repo_connect)