diff -r 944d66870c6a -r 4cba5f2cd57b repoapi.py --- a/repoapi.py Tue Jun 25 17:25:47 2013 +0200 +++ b/repoapi.py Tue Jun 25 11:06:57 2013 +0200 @@ -18,7 +18,10 @@ """Official API to access the content of a repository """ from cubicweb.utils import parse_repo_uri -from cubicweb import ConnectionError +from cubicweb import ConnectionError, ProgrammingError +from uuid import uuid4 +from contextlib import contextmanager +from cubicweb.req import RequestSessionBase ### private function for specific method ############################ @@ -71,3 +74,216 @@ else: raise ConnectionError('unknown protocol: `%s`' % protocol) +def _srv_cnx_func(name): + """Decorate ClientConnection method blindly forward to Connection + THIS TRANSITIONAL PURPOSE + + will be dropped when we have standalone connection""" + def proxy(clt_cnx, *args, **kwargs): + # the ``with`` dance is transitional. We do not have Standalone + # Connection yet so we use this trick to unsure the session have the + # proper cnx loaded. This can be simplified one we have Standalone + # Connection object + with clt_cnx._srv_cnx as cnx: + return getattr(cnx, name)(*args, **kwargs) + return proxy + +class ClientConnection(RequestSessionBase): + """A Connection object to be used Client side. + + This object is aimed to be used client side (so potential communication + with the repo through RTC) and aims to offer some compatibility with the + cubicweb.dbapi.Connection interface. + """ + # make exceptions available through the connection object + ProgrammingError = ProgrammingError + # attributes that may be overriden per connection instance + anonymous_connection = False # XXX really needed ? + is_repo_in_memory = True # BC, always true + + def __init__(self, session): + self._session = session + self._cnxid = None + self._open = None + self._web_request = False + self.vreg = session.vreg + self._set_user(session.user) + + def __enter__(self): + assert self._open is None + self._open = True + self._cnxid = '%s-%s' % (self._session.id, uuid4().hex) + self._session.set_cnx(self._cnxid) + self._session._cnx.ctx_count += 1 + + def __exit__(self, exc_type, exc_val, exc_tb): + self._open = False + cnxid = self._cnxid + self._cnxid = None + self._session._cnx.ctx_count -= 1 + self._session.close_cnx(cnxid) + + + # begin silly BC + @property + def _closed(self): + return not self._open + + def close(self): + if self._open: + self.__exit__(None, None, None) + + def __repr__(self): + if self.anonymous_connection: + return '' % self._cnxid + return '' % self._cnxid + # end silly BC + + @property + @contextmanager + def _srv_cnx(self): + """ensure that the session is locked to the right transaction + + TRANSITIONAL PURPOSE, This will be dropped once we use standalone + session object""" + if not self._open: + raise ProgrammingError('Closed connection %s' % self._cnxid) + session = self._session + old_cnx = session._current_cnx_id + try: + session.set_cnx(self._cnxid) + session.set_cnxset() + try: + yield session + finally: + session.free_cnxset() + finally: + if old_cnx is not None: + session.set_cnx(old_cnx) + + # Main Connection purpose in life ######################################### + + call_service = _srv_cnx_func('call_service') + + def execute(self, *args, **kwargs): + # the ``with`` dance is transitional. We do not have Standalone + # Connection yet so we use this trick to unsure the session have the + # proper cnx loaded. This can be simplified one we have Standalone + # Connection object + with self._srv_cnx as cnx: + rset = cnx.execute(*args, **kwargs) + rset.req = self + return rset + + commit = _srv_cnx_func('commit') + rollback = _srv_cnx_func('rollback') + + # session data methods ##################################################### + + get_shared_data = _srv_cnx_func('get_shared_data') + set_shared_data = _srv_cnx_func('set_shared_data') + + # meta-data accessors ###################################################### + + def source_defs(self): + """Return the definition of sources used by the repository.""" + return self._session.repo.source_defs() + + def get_schema(self): + """Return the schema currently used by the repository.""" + return self._session.repo.source_defs() + + def get_option_value(self, option, foreid=None): + """Return the value for `option` in the configuration. If `foreid` is + specified, the actual repository to which this entity belongs is + dereferenced and the option value retrieved from it. + """ + return self._session.repo.get_option_value(option, foreid) + + describe = _srv_cnx_func('describe') + + # undo support ############################################################ + + def undoable_transactions(self, ueid=None, req=None, **actionfilters): + """Return a list of undoable transaction objects by the connection's + user, ordered by descendant transaction time. + + Managers may filter according to user (eid) who has done the transaction + using the `ueid` argument. Others will only see their own transactions. + + Additional filtering capabilities is provided by using the following + named arguments: + + * `etype` to get only transactions creating/updating/deleting entities + of the given type + + * `eid` to get only transactions applied to entity of the given eid + + * `action` to get only transactions doing the given action (action in + 'C', 'U', 'D', 'A', 'R'). If `etype`, action can only be 'C', 'U' or + 'D'. + + * `public`: when additional filtering is provided, their are by default + only searched in 'public' actions, unless a `public` argument is given + and set to false. + """ + # the ``with`` dance is transitional. We do not have Standalone + # Connection yet so we use this trick to unsure the session have the + # proper cnx loaded. This can be simplified one we have Standalone + # Connection object + with self._srv_cnx as cnx: + source = cnx.repo.system_source + txinfos = source.undoable_transactions(cnx, ueid, **actionfilters) + for txinfo in txinfos: + txinfo.req = req or self # XXX mostly wrong + return txinfos + + def transaction_info(self, txuuid, req=None): + """Return transaction object for the given uid. + + raise `NoSuchTransaction` if not found or if session's user is not + allowed (eg not in managers group and the transaction doesn't belong to + him). + """ + # the ``with`` dance is transitional. We do not have Standalone + # Connection yet so we use this trick to unsure the session have the + # proper cnx loaded. This can be simplified one we have Standalone + # Connection object + with self._srv_cnx as cnx: + txinfo = cnx.repo.system_source.tx_info(cnx, txuuid) + if req: + txinfo.req = req + else: + txinfo.cnx = self + return txinfo + + def transaction_actions(self, txuuid, public=True): + """Return an ordered list of action effectued during that transaction. + + If public is true, return only 'public' actions, eg not ones triggered + under the cover by hooks, else return all actions. + + raise `NoSuchTransaction` if the transaction is not found or if + session's user is not allowed (eg not in managers group and the + transaction doesn't belong to him). + """ + # the ``with`` dance is transitional. We do not have Standalone + # Connection yet so we use this trick to unsure the session have the + # proper cnx loaded. This can be simplified one we have Standalone + # Connection object + with self._srv_cnx as cnx: + return cnx.repo.system_source.tx_actions(cnx, txuuid, public) + + def undo_transaction(self, txuuid): + """Undo the given transaction. Return potential restoration errors. + + raise `NoSuchTransaction` if not found or if session's user is not + allowed (eg not in managers group and the transaction doesn't belong to + him). + """ + # the ``with`` dance is transitional. We do not have Standalone + # Connection yet so we use this trick to unsure the session have the + # proper cnx loaded. This can be simplified one we have Standalone + # Connection object + with self._srv_cnx as cnx: + return cnx.repo.system_source.undo_transaction(cnx, txuuid)