repoapi.py
changeset 10354 635cfac73d28
parent 10236 ef3059a692cb
child 10356 a009a31fb1ea
equal deleted inserted replaced
10353:d9a1e7939ee6 10354:635cfac73d28
    15 #
    15 #
    16 # You should have received a copy of the GNU Lesser General Public License along
    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/>.
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    18 """Official API to access the content of a repository
    18 """Official API to access the content of a repository
    19 """
    19 """
    20 from logilab.common.deprecation import deprecated
    20 from logilab.common.deprecation import class_deprecated
    21 
    21 
    22 from cubicweb.utils import parse_repo_uri
    22 from cubicweb.utils import parse_repo_uri
    23 from cubicweb import ConnectionError, ProgrammingError, AuthenticationError
    23 from cubicweb import ConnectionError, AuthenticationError
    24 from uuid import uuid4
    24 from cubicweb.server.session import Connection
    25 from contextlib import contextmanager
    25 
    26 from cubicweb.req import RequestSessionBase
       
    27 from functools import wraps
       
    28 
    26 
    29 ### private function for specific method ############################
    27 ### private function for specific method ############################
    30 
    28 
    31 def _get_inmemory_repo(config, vreg=None):
    29 def _get_inmemory_repo(config, vreg=None):
    32     from cubicweb.server.repository import Repository
    30     from cubicweb.server.repository import Repository
    63     raise AuthenticationError if the credential are invalid."""
    61     raise AuthenticationError if the credential are invalid."""
    64     sessionid = repo.connect(login, **kwargs)
    62     sessionid = repo.connect(login, **kwargs)
    65     session = repo._get_session(sessionid)
    63     session = repo._get_session(sessionid)
    66     # XXX the autoclose_session should probably be handle on the session directly
    64     # XXX the autoclose_session should probably be handle on the session directly
    67     # this is something to consider once we have proper server side Connection.
    65     # this is something to consider once we have proper server side Connection.
    68     return ClientConnection(session, autoclose_session=True)
    66     return Connection(session)
    69 
    67 
    70 def anonymous_cnx(repo):
    68 def anonymous_cnx(repo):
    71     """return a ClientConnection for Anonymous user.
    69     """return a ClientConnection for Anonymous user.
    72 
    70 
    73     The ClientConnection is associated to a new Session object that will be
    71     The ClientConnection is associated to a new Session object that will be
    80         raise AuthenticationError('anonymous access is not authorized')
    78         raise AuthenticationError('anonymous access is not authorized')
    81     anon_login, anon_password = anoninfo
    79     anon_login, anon_password = anoninfo
    82     # use vreg's repository cache
    80     # use vreg's repository cache
    83     return connect(repo, anon_login, password=anon_password)
    81     return connect(repo, anon_login, password=anon_password)
    84 
    82 
    85 def _srv_cnx_func(name):
       
    86     """Decorate ClientConnection method blindly forward to Connection
       
    87     THIS TRANSITIONAL PURPOSE
       
    88 
    83 
    89     will be dropped when we have standalone connection"""
    84 class ClientConnection(Connection):
    90     def proxy(clt_cnx, *args, **kwargs):
    85     __metaclass__ = class_deprecated
    91         # the ``with`` dance is transitional. We do not have Standalone
    86     __deprecation_warning__ = '[3.20] %(cls)s is deprecated, use Connection instead'
    92         # Connection yet so we use this trick to unsure the session have the
       
    93         # proper cnx loaded. This can be simplified one we have Standalone
       
    94         # Connection object
       
    95         if not clt_cnx._open:
       
    96             raise ProgrammingError('Closed client connection')
       
    97         return getattr(clt_cnx._cnx, name)(*args, **kwargs)
       
    98     return proxy
       
    99 
       
   100 def _open_only(func):
       
   101     """decorator for ClientConnection method that check it is open"""
       
   102     @wraps(func)
       
   103     def check_open(clt_cnx, *args, **kwargs):
       
   104         if not clt_cnx._open:
       
   105             raise ProgrammingError('Closed client connection')
       
   106         return func(clt_cnx, *args, **kwargs)
       
   107     return check_open
       
   108 
       
   109 
       
   110 class ClientConnection(RequestSessionBase):
       
   111     """A Connection object to be used Client side.
       
   112 
       
   113     This object is aimed to be used client side (so potential communication
       
   114     with the repo through RPC) and aims to offer some compatibility with the
       
   115     cubicweb.dbapi.Connection interface.
       
   116 
       
   117     The autoclose_session parameter informs the connection that this session
       
   118     has been opened explicitly and only for this client connection. The
       
   119     connection will close the session on exit.
       
   120     """
       
   121     # make exceptions available through the connection object
       
   122     ProgrammingError = ProgrammingError
       
   123     # attributes that may be overriden per connection instance
       
   124     anonymous_connection = False # XXX really needed ?
       
   125     is_repo_in_memory = True # BC, always true
       
   126 
       
   127     def __init__(self, session, autoclose_session=False):
       
   128         super(ClientConnection, self).__init__(session.vreg)
       
   129         self._session = session # XXX there is no real reason to keep the
       
   130                                 # session around function still using it should
       
   131                                 # be rewritten and migrated.
       
   132         self._cnx = None
       
   133         self._open = None
       
   134         self._web_request = False
       
   135         #: cache entities built during the connection
       
   136         self._eid_cache = {}
       
   137         self._set_user(session.user)
       
   138         self._autoclose_session = autoclose_session
       
   139 
       
   140     def __enter__(self):
       
   141         assert self._open is None
       
   142         self._open = True
       
   143         self._cnx = self._session.new_cnx()
       
   144         self._cnx.__enter__()
       
   145         self._cnx.ctx_count += 1
       
   146         return self
       
   147 
       
   148     def __exit__(self, exc_type, exc_val, exc_tb):
       
   149         self._open = False
       
   150         self._cnx.ctx_count -= 1
       
   151         self._cnx.__exit__(exc_type, exc_val, exc_tb)
       
   152         self._cnx = None
       
   153         if self._autoclose_session:
       
   154             # we have to call repo.close to ensure the repo properly forgets the
       
   155             # session; calling session.close() is not enough :-(
       
   156             self._session.repo.close(self._session.sessionid)
       
   157 
       
   158 
       
   159     # begin silly BC
       
   160     @property
       
   161     def _closed(self):
       
   162         return not self._open
       
   163 
       
   164     def close(self):
       
   165         if self._open:
       
   166             self.__exit__(None, None, None)
       
   167 
       
   168     def __repr__(self):
       
   169         # XXX we probably want to reference the user of the session here
       
   170         if self._open is None:
       
   171             return '<ClientConnection (not open yet)>'
       
   172         elif not self._open:
       
   173             return '<ClientConnection (closed)>'
       
   174         elif self.anonymous_connection:
       
   175             return '<ClientConnection %s (anonymous)>' % self._cnx.connectionid
       
   176         else:
       
   177             return '<ClientConnection %s>' % self._cnx.connectionid
       
   178     # end silly BC
       
   179 
       
   180     # Main Connection purpose in life #########################################
       
   181 
       
   182     call_service = _srv_cnx_func('call_service')
       
   183 
       
   184     @_open_only
       
   185     def execute(self, *args, **kwargs):
       
   186         # the ``with`` dance is transitional. We do not have Standalone
       
   187         # Connection yet so we use this trick to unsure the session have the
       
   188         # proper cnx loaded. This can be simplified one we have Standalone
       
   189         # Connection object
       
   190         rset = self._cnx.execute(*args, **kwargs)
       
   191         rset.req = self
       
   192         return rset
       
   193 
       
   194     @_open_only
       
   195     def commit(self, *args, **kwargs):
       
   196         try:
       
   197             return self._cnx.commit(*args, **kwargs)
       
   198         finally:
       
   199             self.drop_entity_cache()
       
   200 
       
   201     @_open_only
       
   202     def rollback(self, *args, **kwargs):
       
   203         try:
       
   204             return self._cnx.rollback(*args, **kwargs)
       
   205         finally:
       
   206             self.drop_entity_cache()
       
   207 
       
   208     # security #################################################################
       
   209 
       
   210     allow_all_hooks_but = _srv_cnx_func('allow_all_hooks_but')
       
   211     deny_all_hooks_but = _srv_cnx_func('deny_all_hooks_but')
       
   212     security_enabled = _srv_cnx_func('security_enabled')
       
   213 
       
   214     # direct sql ###############################################################
       
   215 
       
   216     system_sql = _srv_cnx_func('system_sql')
       
   217 
       
   218     # session data methods #####################################################
       
   219 
       
   220     get_shared_data = _srv_cnx_func('get_shared_data')
       
   221     set_shared_data = _srv_cnx_func('set_shared_data')
       
   222 
       
   223     @property
       
   224     def transaction_data(self):
       
   225         return self._cnx.transaction_data
       
   226 
       
   227     # meta-data accessors ######################################################
       
   228 
       
   229     @_open_only
       
   230     def source_defs(self):
       
   231         """Return the definition of sources used by the repository."""
       
   232         return self._session.repo.source_defs()
       
   233 
       
   234     @_open_only
       
   235     def get_schema(self):
       
   236         """Return the schema currently used by the repository."""
       
   237         return self._session.repo.source_defs()
       
   238 
       
   239     @_open_only
       
   240     def get_option_value(self, option):
       
   241         """Return the value for `option` in the configuration."""
       
   242         return self._session.repo.get_option_value(option)
       
   243 
       
   244     entity_metas = _srv_cnx_func('entity_metas')
       
   245     describe = _srv_cnx_func('describe') # XXX deprecated in 3.19
       
   246 
       
   247     # undo support ############################################################
       
   248 
       
   249     @_open_only
       
   250     def undoable_transactions(self, ueid=None, req=None, **actionfilters):
       
   251         """Return a list of undoable transaction objects by the connection's
       
   252         user, ordered by descendant transaction time.
       
   253 
       
   254         Managers may filter according to user (eid) who has done the transaction
       
   255         using the `ueid` argument. Others will only see their own transactions.
       
   256 
       
   257         Additional filtering capabilities is provided by using the following
       
   258         named arguments:
       
   259 
       
   260         * `etype` to get only transactions creating/updating/deleting entities
       
   261           of the given type
       
   262 
       
   263         * `eid` to get only transactions applied to entity of the given eid
       
   264 
       
   265         * `action` to get only transactions doing the given action (action in
       
   266           'C', 'U', 'D', 'A', 'R'). If `etype`, action can only be 'C', 'U' or
       
   267           'D'.
       
   268 
       
   269         * `public`: when additional filtering is provided, their are by default
       
   270           only searched in 'public' actions, unless a `public` argument is given
       
   271           and set to false.
       
   272         """
       
   273         # the ``with`` dance is transitional. We do not have Standalone
       
   274         # Connection yet so we use this trick to unsure the session have the
       
   275         # proper cnx loaded. This can be simplified one we have Standalone
       
   276         # Connection object
       
   277         source = self._cnx.repo.system_source
       
   278         txinfos = source.undoable_transactions(self._cnx, ueid, **actionfilters)
       
   279         for txinfo in txinfos:
       
   280             txinfo.req = req or self  # XXX mostly wrong
       
   281         return txinfos
       
   282 
       
   283     @_open_only
       
   284     def transaction_info(self, txuuid, req=None):
       
   285         """Return transaction object for the given uid.
       
   286 
       
   287         raise `NoSuchTransaction` if not found or if session's user is not
       
   288         allowed (eg not in managers group and the transaction doesn't belong to
       
   289         him).
       
   290         """
       
   291         # the ``with`` dance is transitional. We do not have Standalone
       
   292         # Connection yet so we use this trick to unsure the session have the
       
   293         # proper cnx loaded. This can be simplified one we have Standalone
       
   294         # Connection object
       
   295         txinfo = self._cnx.repo.system_source.tx_info(self._cnx, txuuid)
       
   296         if req:
       
   297             txinfo.req = req
       
   298         else:
       
   299             txinfo.cnx = self
       
   300         return txinfo
       
   301 
       
   302     @_open_only
       
   303     def transaction_actions(self, txuuid, public=True):
       
   304         """Return an ordered list of action effectued during that transaction.
       
   305 
       
   306         If public is true, return only 'public' actions, eg not ones triggered
       
   307         under the cover by hooks, else return all actions.
       
   308 
       
   309         raise `NoSuchTransaction` if the transaction is not found or if
       
   310         session's user is not allowed (eg not in managers group and the
       
   311         transaction doesn't belong to him).
       
   312         """
       
   313         # the ``with`` dance is transitional. We do not have Standalone
       
   314         # Connection yet so we use this trick to unsure the session have the
       
   315         # proper cnx loaded. This can be simplified one we have Standalone
       
   316         # Connection object
       
   317         return self._cnx.repo.system_source.tx_actions(self._cnx, txuuid, public)
       
   318 
       
   319     @_open_only
       
   320     def undo_transaction(self, txuuid):
       
   321         """Undo the given transaction. Return potential restoration errors.
       
   322 
       
   323         raise `NoSuchTransaction` if not found or if session's user is not
       
   324         allowed (eg not in managers group and the transaction doesn't belong to
       
   325         him).
       
   326         """
       
   327         # the ``with`` dance is transitional. We do not have Standalone
       
   328         # Connection yet so we use this trick to unsure the session have the
       
   329         # proper cnx loaded. This can be simplified one we have Standalone
       
   330         # Connection object
       
   331         return self._cnx.repo.system_source.undo_transaction(self._cnx, txuuid)
       
   332 
       
   333     # cache management
       
   334 
       
   335     def entity_cache(self, eid):
       
   336         return self._eid_cache[eid]
       
   337 
       
   338     def set_entity_cache(self, entity):
       
   339         self._eid_cache[entity.eid] = entity
       
   340 
       
   341     def cached_entities(self):
       
   342         return self._eid_cache.values()
       
   343 
       
   344     def drop_entity_cache(self, eid=None):
       
   345         if eid is None:
       
   346             self._eid_cache = {}
       
   347         else:
       
   348             del self._eid_cache[eid]
       
   349 
       
   350     # deprecated stuff
       
   351 
       
   352     @deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
       
   353     def request(self):
       
   354         return self
       
   355 
       
   356     @deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
       
   357     def cursor(self):
       
   358         return self
       
   359 
       
   360     @property
       
   361     @deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
       
   362     def sessionid(self):
       
   363         return self._session.sessionid
       
   364 
       
   365     @property
       
   366     @deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
       
   367     def connection(self):
       
   368         return self
       
   369 
       
   370     @property
       
   371     @deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
       
   372     def _repo(self):
       
   373         return self._session.repo