server/hook.py
changeset 9608 e4d9a489ec3f
parent 9469 032825bbacab
child 10006 8391bf718485
equal deleted inserted replaced
9607:6942622fd5dc 9608:e4d9a489ec3f
   201 
   201 
   202 It is sometimes convenient to explicitly enable or disable some hooks. For
   202 It is sometimes convenient to explicitly enable or disable some hooks. For
   203 instance if you want to disable some integrity checking hook. This can be
   203 instance if you want to disable some integrity checking hook. This can be
   204 controlled more finely through the `category` class attribute, which is a string
   204 controlled more finely through the `category` class attribute, which is a string
   205 giving a category name.  One can then uses the
   205 giving a category name.  One can then uses the
   206 :meth:`~cubicweb.server.session.Session.deny_all_hooks_but` and
   206 :meth:`~cubicweb.server.session.Connection.deny_all_hooks_but` and
   207 :meth:`~cubicweb.server.session.Session.allow_all_hooks_but` context managers to
   207 :meth:`~cubicweb.server.session.Connection.allow_all_hooks_but` context managers to
   208 explicitly enable or disable some categories.
   208 explicitly enable or disable some categories.
   209 
   209 
   210 The existing categories are:
   210 The existing categories are:
   211 
   211 
   212 * ``security``, security checking hooks
   212 * ``security``, security checking hooks
   293 
   293 
   294     def register(self, obj, **kwargs):
   294     def register(self, obj, **kwargs):
   295         obj.check_events()
   295         obj.check_events()
   296         super(HooksRegistry, self).register(obj, **kwargs)
   296         super(HooksRegistry, self).register(obj, **kwargs)
   297 
   297 
   298     def call_hooks(self, event, session=None, **kwargs):
   298     def call_hooks(self, event, cnx=None, **kwargs):
   299         """call `event` hooks for an entity or a list of entities (passed
   299         """call `event` hooks for an entity or a list of entities (passed
   300         respectively as the `entity` or ``entities`` keyword argument).
   300         respectively as the `entity` or ``entities`` keyword argument).
   301         """
   301         """
   302         kwargs['event'] = event
   302         kwargs['event'] = event
   303         if session is None: # True for events such as server_start
   303         if cnx is None: # True for events such as server_start
   304             for hook in sorted(self.possible_objects(session, **kwargs),
   304             for hook in sorted(self.possible_objects(cnx, **kwargs),
   305                                key=lambda x: x.order):
   305                                key=lambda x: x.order):
   306                 hook()
   306                 hook()
   307         else:
   307         else:
   308             if 'entities' in kwargs:
   308             if 'entities' in kwargs:
   309                 assert 'entity' not in kwargs, \
   309                 assert 'entity' not in kwargs, \
   316                 entities = []
   316                 entities = []
   317                 eids_from_to = kwargs.pop('eids_from_to')
   317                 eids_from_to = kwargs.pop('eids_from_to')
   318             else:
   318             else:
   319                 entities = []
   319                 entities = []
   320                 eids_from_to = []
   320                 eids_from_to = []
   321             pruned = self.get_pruned_hooks(session, event,
   321             pruned = self.get_pruned_hooks(cnx, event,
   322                                            entities, eids_from_to, kwargs)
   322                                            entities, eids_from_to, kwargs)
   323             # by default, hooks are executed with security turned off
   323             # by default, hooks are executed with security turned off
   324             with session.security_enabled(read=False):
   324             with cnx.security_enabled(read=False):
   325                 for _kwargs in _iter_kwargs(entities, eids_from_to, kwargs):
   325                 for _kwargs in _iter_kwargs(entities, eids_from_to, kwargs):
   326                     hooks = sorted(self.filtered_possible_objects(pruned, session, **_kwargs),
   326                     hooks = sorted(self.filtered_possible_objects(pruned, cnx, **_kwargs),
   327                                    key=lambda x: x.order)
   327                                    key=lambda x: x.order)
   328                     debug = server.DEBUG & server.DBG_HOOKS
   328                     debug = server.DEBUG & server.DBG_HOOKS
   329                     with session.security_enabled(write=False):
   329                     with cnx.security_enabled(write=False):
   330                         for hook in hooks:
   330                         for hook in hooks:
   331                             if debug:
   331                             if debug:
   332                                 print event, _kwargs, hook
   332                                 print event, _kwargs, hook
   333                             hook()
   333                             hook()
   334 
   334 
   335     def get_pruned_hooks(self, session, event, entities, eids_from_to, kwargs):
   335     def get_pruned_hooks(self, cnx, event, entities, eids_from_to, kwargs):
   336         """return a set of hooks that should not be considered by filtered_possible objects
   336         """return a set of hooks that should not be considered by filtered_possible objects
   337 
   337 
   338         the idea is to make a first pass over all the hooks in the
   338         the idea is to make a first pass over all the hooks in the
   339         registry and to mark put some of them in a pruned list. The
   339         registry and to mark put some of them in a pruned list. The
   340         pruned hooks are the one which:
   340         pruned hooks are the one which:
   341 
   341 
   342         * are disabled at the session level
   342         * are disabled at the connection level
   343 
   343 
   344         * have a selector containing a :class:`match_rtype` or an
   344         * have a selector containing a :class:`match_rtype` or an
   345           :class:`is_instance` predicate which does not match the rtype / etype
   345           :class:`is_instance` predicate which does not match the rtype / etype
   346           of the relations / entities for which we are calling the hooks. This
   346           of the relations / entities for which we are calling the hooks. This
   347           works because the repository calls the hooks grouped by rtype or by
   347           works because the repository calls the hooks grouped by rtype or by
   360             look_for_selector = match_rtype
   360             look_for_selector = match_rtype
   361             etype = None
   361             etype = None
   362         else: # nothing to prune, how did we get there ???
   362         else: # nothing to prune, how did we get there ???
   363             return set()
   363             return set()
   364         cache_key = (event, kwargs.get('rtype'), etype)
   364         cache_key = (event, kwargs.get('rtype'), etype)
   365         pruned = session.pruned_hooks_cache.get(cache_key)
   365         pruned = cnx.pruned_hooks_cache.get(cache_key)
   366         if pruned is not None:
   366         if pruned is not None:
   367             return pruned
   367             return pruned
   368         pruned = set()
   368         pruned = set()
   369         session.pruned_hooks_cache[cache_key] = pruned
   369         cnx.pruned_hooks_cache[cache_key] = pruned
   370         if look_for_selector is not None:
   370         if look_for_selector is not None:
   371             for id, hooks in self.iteritems():
   371             for id, hooks in self.iteritems():
   372                 for hook in hooks:
   372                 for hook in hooks:
   373                     enabled_cat, main_filter = hook.filterable_selectors()
   373                     enabled_cat, main_filter = hook.filterable_selectors()
   374                     if enabled_cat is not None:
   374                     if enabled_cat is not None:
   375                         if not enabled_cat(hook, session):
   375                         if not enabled_cat(hook, cnx):
   376                             pruned.add(hook)
   376                             pruned.add(hook)
   377                             continue
   377                             continue
   378                     if main_filter is not None:
   378                     if main_filter is not None:
   379                         if isinstance(main_filter, match_rtype) and \
   379                         if isinstance(main_filter, match_rtype) and \
   380                            (main_filter.frometypes is not None  or \
   380                            (main_filter.frometypes is not None  or \
   381                             main_filter.toetypes is not None):
   381                             main_filter.toetypes is not None):
   382                             continue
   382                             continue
   383                         first_kwargs = _iter_kwargs(entities, eids_from_to, kwargs).next()
   383                         first_kwargs = _iter_kwargs(entities, eids_from_to, kwargs).next()
   384                         if not main_filter(hook, session, **first_kwargs):
   384                         if not main_filter(hook, cnx, **first_kwargs):
   385                             pruned.add(hook)
   385                             pruned.add(hook)
   386         return pruned
   386         return pruned
   387 
   387 
   388 
   388 
   389     def filtered_possible_objects(self, pruned, *args, **kwargs):
   389     def filtered_possible_objects(self, pruned, *args, **kwargs):
   402 
   402 
   403 class HooksManager(object):
   403 class HooksManager(object):
   404     def __init__(self, vreg):
   404     def __init__(self, vreg):
   405         self.vreg = vreg
   405         self.vreg = vreg
   406 
   406 
   407     def call_hooks(self, event, session=None, **kwargs):
   407     def call_hooks(self, event, cnx=None, **kwargs):
   408         try:
   408         try:
   409             registry = self.vreg['%s_hooks' % event]
   409             registry = self.vreg['%s_hooks' % event]
   410         except RegistryNotFound:
   410         except RegistryNotFound:
   411             return # no hooks for this event
   411             return # no hooks for this event
   412         registry.call_hooks(event, session, **kwargs)
   412         registry.call_hooks(event, cnx, **kwargs)
   413 
   413 
   414 
   414 
   415 for event in ALL_HOOKS:
   415 for event in ALL_HOOKS:
   416     CWRegistryStore.REGISTRY_FACTORY['%s_hooks' % event] = HooksRegistry
   416     CWRegistryStore.REGISTRY_FACTORY['%s_hooks' % event] = HooksRegistry
   417 
   417 
   505 class Hook(AppObject):
   505 class Hook(AppObject):
   506     """Base class for hook.
   506     """Base class for hook.
   507 
   507 
   508     Hooks being appobjects like views, they have a `__regid__` and a `__select__`
   508     Hooks being appobjects like views, they have a `__regid__` and a `__select__`
   509     class attribute. Like all appobjects, hooks have the `self._cw` attribute which
   509     class attribute. Like all appobjects, hooks have the `self._cw` attribute which
   510     represents the current session. In entity hooks, a `self.entity` attribute is
   510     represents the current connection. In entity hooks, a `self.entity` attribute is
   511     also present.
   511     also present.
   512 
   512 
   513     The `events` tuple is used by the base class selector to dispatch the hook
   513     The `events` tuple is used by the base class selector to dispatch the hook
   514     on the right events. It is possible to dispatch on multiple events at once
   514     on the right events. It is possible to dispatch on multiple events at once
   515     if needed (though take care as hook attribute may vary as described above).
   515     if needed (though take care as hook attribute may vary as described above).
   683 
   683 
   684 class Operation(object):
   684 class Operation(object):
   685     """Base class for operations.
   685     """Base class for operations.
   686 
   686 
   687     Operation may be instantiated in the hooks' `__call__` method. It always
   687     Operation may be instantiated in the hooks' `__call__` method. It always
   688     takes a session object as first argument (accessible as `.session` from the
   688     takes a connection object as first argument (accessible as `.cnx` from the
   689     operation instance), and optionally all keyword arguments needed by the
   689     operation instance), and optionally all keyword arguments needed by the
   690     operation. These keyword arguments will be accessible as attributes from the
   690     operation. These keyword arguments will be accessible as attributes from the
   691     operation instance.
   691     operation instance.
   692 
   692 
   693     An operation is triggered on connections set events related to commit /
   693     An operation is triggered on connections set events related to commit /
   718 
   718 
   719     * `postcommit`:
   719     * `postcommit`:
   720 
   720 
   721       the transaction is over. All the ORM entities accessed by the earlier
   721       the transaction is over. All the ORM entities accessed by the earlier
   722       transaction are invalid. If you need to work on the database, you need to
   722       transaction are invalid. If you need to work on the database, you need to
   723       start a new transaction, for instance using a new internal session, which
   723       start a new transaction, for instance using a new internal connection,
   724       you will need to commit (and close!).
   724       which you will need to commit.
   725 
   725 
   726     For an operation to support an event, one has to implement the `<event
   726     For an operation to support an event, one has to implement the `<event
   727     name>_event` method with no arguments.
   727     name>_event` method with no arguments.
   728 
   728 
   729     The order of operations may be important, and is controlled according to
   729     The order of operations may be important, and is controlled according to
   730     the insert_index's method output (whose implementation vary according to the
   730     the insert_index's method output (whose implementation vary according to the
   731     base hook class used).
   731     base hook class used).
   732     """
   732     """
   733 
   733 
   734     def __init__(self, session, **kwargs):
   734     def __init__(self, cnx, **kwargs):
   735         self.session = session
   735         self.cnx = cnx
   736         self.__dict__.update(kwargs)
   736         self.__dict__.update(kwargs)
   737         self.register(session)
   737         self.register(cnx)
   738         # execution information
   738         # execution information
   739         self.processed = None # 'precommit', 'commit'
   739         self.processed = None # 'precommit', 'commit'
   740         self.failed = False
   740         self.failed = False
   741 
   741 
   742     def register(self, session):
   742     @property
   743         session.add_operation(self, self.insert_index())
   743     @deprecated('[3.19] Operation.session is deprecated, use Operation.cnx instead')
       
   744     def session(self):
       
   745         return self.cnx
       
   746 
       
   747     def register(self, cnx):
       
   748         cnx.add_operation(self, self.insert_index())
   744 
   749 
   745     def insert_index(self):
   750     def insert_index(self):
   746         """return the index of  the lastest instance which is not a
   751         """return the index of the latest instance which is not a
   747         LateOperation instance
   752         LateOperation instance
   748         """
   753         """
   749         # faster by inspecting operation in reverse order for heavy transactions
   754         # faster by inspecting operation in reverse order for heavy transactions
   750         i = None
   755         i = None
   751         for i, op in enumerate(reversed(self.session.pending_operations)):
   756         for i, op in enumerate(reversed(self.cnx.pending_operations)):
   752             if isinstance(op, (LateOperation, SingleLastOperation)):
   757             if isinstance(op, (LateOperation, SingleLastOperation)):
   753                 continue
   758                 continue
   754             return -i or None
   759             return -i or None
   755         if i is None:
   760         if i is None:
   756             return None
   761             return None
   847     @classproperty
   852     @classproperty
   848     def data_key(cls):
   853     def data_key(cls):
   849         return ('cw.dataops', cls.__name__)
   854         return ('cw.dataops', cls.__name__)
   850 
   855 
   851     @classmethod
   856     @classmethod
   852     def get_instance(cls, session, **kwargs):
   857     def get_instance(cls, cnx, **kwargs):
   853         # no need to lock: transaction_data already comes from thread's local storage
   858         # no need to lock: transaction_data already comes from thread's local storage
   854         try:
   859         try:
   855             return session.transaction_data[cls.data_key]
   860             return cnx.transaction_data[cls.data_key]
   856         except KeyError:
   861         except KeyError:
   857             op = session.transaction_data[cls.data_key] = cls(session, **kwargs)
   862             op = cnx.transaction_data[cls.data_key] = cls(cnx, **kwargs)
   858             return op
   863             return op
   859 
   864 
   860     def __init__(self, *args, **kwargs):
   865     def __init__(self, *args, **kwargs):
   861         super(DataOperationMixIn, self).__init__(*args, **kwargs)
   866         super(DataOperationMixIn, self).__init__(*args, **kwargs)
   862         self._container = self._build_container()
   867         self._container = self._build_container()
   890     def get_data(self):
   895     def get_data(self):
   891         assert not self._processed, """Trying to get data from a closed operation.
   896         assert not self._processed, """Trying to get data from a closed operation.
   892 Iterating over operation data closed it and should be reserved to precommit /
   897 Iterating over operation data closed it and should be reserved to precommit /
   893 postcommit method of the operation."""
   898 postcommit method of the operation."""
   894         self._processed = True
   899         self._processed = True
   895         op = self.session.transaction_data.pop(self.data_key)
   900         op = self.cnx.transaction_data.pop(self.data_key)
   896         assert op is self, "Bad handling of operation data, found %s instead of %s for key %s" % (
   901         assert op is self, "Bad handling of operation data, found %s instead of %s for key %s" % (
   897             op, self, self.data_key)
   902             op, self, self.data_key)
   898         return self._container
   903         return self._container
   899 
   904 
   900 
   905 
   901 @deprecated('[3.10] use opcls.get_instance(session, **opkwargs).add_data(value)')
   906 @deprecated('[3.10] use opcls.get_instance(cnx, **opkwargs).add_data(value)')
   902 def set_operation(session, datakey, value, opcls, containercls=set, **opkwargs):
   907 def set_operation(cnx, datakey, value, opcls, containercls=set, **opkwargs):
   903     """Function to ease applying a single operation on a set of data, avoiding
   908     """Function to ease applying a single operation on a set of data, avoiding
   904     to create as many as operation as they are individual modification. You
   909     to create as many as operation as they are individual modification. You
   905     should try to use this instead of creating on operation for each `value`,
   910     should try to use this instead of creating on operation for each `value`,
   906     since handling operations becomes coslty on massive data import.
   911     since handling operations becomes coslty on massive data import.
   907 
   912 
   908     Arguments are:
   913     Arguments are:
   909 
   914 
   910     * the `session` object
   915     * `cnx`, the current connection
   911 
   916 
   912     * `datakey`, a specially forged key that will be used as key in
   917     * `datakey`, a specially forged key that will be used as key in
   913       session.transaction_data
   918       cnx.transaction_data
   914 
   919 
   915     * `value` that is the actual payload of an individual operation
   920     * `value` that is the actual payload of an individual operation
   916 
   921 
   917     * `opcls`, the class of the operation. An instance is created on the first
   922     * `opcls`, the class of the operation. An instance is created on the first
   918       call for the given key, and then subsequent calls will simply add the
   923       call for the given key, and then subsequent calls will simply add the
   938     .. Note::
   943     .. Note::
   939        **poping** the key from `transaction_data` is not an option, else you may
   944        **poping** the key from `transaction_data` is not an option, else you may
   940        get unexpected data loss in some case of nested hooks.
   945        get unexpected data loss in some case of nested hooks.
   941     """
   946     """
   942     try:
   947     try:
   943         # Search for session.transaction_data[`datakey`] (expected to be a set):
   948         # Search for cnx.transaction_data[`datakey`] (expected to be a set):
   944         # if found, simply append `value`
   949         # if found, simply append `value`
   945         _container_add(session.transaction_data[datakey], value)
   950         _container_add(cnx.transaction_data[datakey], value)
   946     except KeyError:
   951     except KeyError:
   947         # else, initialize it to containercls([`value`]) and instantiate the given
   952         # else, initialize it to containercls([`value`]) and instantiate the given
   948         # `opcls` operation class with additional keyword arguments
   953         # `opcls` operation class with additional keyword arguments
   949         opcls(session, **opkwargs)
   954         opcls(cnx, **opkwargs)
   950         session.transaction_data[datakey] = containercls()
   955         cnx.transaction_data[datakey] = containercls()
   951         _container_add(session.transaction_data[datakey], value)
   956         _container_add(cnx.transaction_data[datakey], value)
   952 
   957 
   953 
   958 
   954 class LateOperation(Operation):
   959 class LateOperation(Operation):
   955     """special operation which should be called after all possible (ie non late)
   960     """special operation which should be called after all possible (ie non late)
   956     operations
   961     operations
   959         """return the index of  the lastest instance which is not a
   964         """return the index of  the lastest instance which is not a
   960         SingleLastOperation instance
   965         SingleLastOperation instance
   961         """
   966         """
   962         # faster by inspecting operation in reverse order for heavy transactions
   967         # faster by inspecting operation in reverse order for heavy transactions
   963         i = None
   968         i = None
   964         for i, op in enumerate(reversed(self.session.pending_operations)):
   969         for i, op in enumerate(reversed(self.cnx.pending_operations)):
   965             if isinstance(op, SingleLastOperation):
   970             if isinstance(op, SingleLastOperation):
   966                 continue
   971                 continue
   967             return -i or None
   972             return -i or None
   968         if i is None:
   973         if i is None:
   969             return None
   974             return None
   974 class SingleLastOperation(Operation):
   979 class SingleLastOperation(Operation):
   975     """special operation which should be called once and after all other
   980     """special operation which should be called once and after all other
   976     operations
   981     operations
   977     """
   982     """
   978 
   983 
   979     def register(self, session):
   984     def register(self, cnx):
   980         """override register to handle cases where this operation has already
   985         """override register to handle cases where this operation has already
   981         been added
   986         been added
   982         """
   987         """
   983         operations = session.pending_operations
   988         operations = cnx.pending_operations
   984         index = self.equivalent_index(operations)
   989         index = self.equivalent_index(operations)
   985         if index is not None:
   990         if index is not None:
   986             equivalent = operations.pop(index)
   991             equivalent = operations.pop(index)
   987         else:
   992         else:
   988             equivalent = None
   993             equivalent = None
   989         session.add_operation(self, self.insert_index())
   994         cnx.add_operation(self, self.insert_index())
   990         return equivalent
   995         return equivalent
   991 
   996 
   992     def equivalent_index(self, operations):
   997     def equivalent_index(self, operations):
   993         """return the index of the equivalent operation if any"""
   998         """return the index of the equivalent operation if any"""
   994         for i, op in enumerate(reversed(operations)):
   999         for i, op in enumerate(reversed(operations)):
   999     def insert_index(self):
  1004     def insert_index(self):
  1000         return None
  1005         return None
  1001 
  1006 
  1002 
  1007 
  1003 class SendMailOp(SingleLastOperation):
  1008 class SendMailOp(SingleLastOperation):
  1004     def __init__(self, session, msg=None, recipients=None, **kwargs):
  1009     def __init__(self, cnx, msg=None, recipients=None, **kwargs):
  1005         # may not specify msg yet, as
  1010         # may not specify msg yet, as
  1006         # `cubicweb.sobjects.supervision.SupervisionMailOp`
  1011         # `cubicweb.sobjects.supervision.SupervisionMailOp`
  1007         if msg is not None:
  1012         if msg is not None:
  1008             assert recipients
  1013             assert recipients
  1009             self.to_send = [(msg, recipients)]
  1014             self.to_send = [(msg, recipients)]
  1010         else:
  1015         else:
  1011             assert recipients is None
  1016             assert recipients is None
  1012             self.to_send = []
  1017             self.to_send = []
  1013         super(SendMailOp, self).__init__(session, **kwargs)
  1018         super(SendMailOp, self).__init__(cnx, **kwargs)
  1014 
  1019 
  1015     def register(self, session):
  1020     def register(self, cnx):
  1016         previous = super(SendMailOp, self).register(session)
  1021         previous = super(SendMailOp, self).register(cnx)
  1017         if previous:
  1022         if previous:
  1018             self.to_send = previous.to_send + self.to_send
  1023             self.to_send = previous.to_send + self.to_send
  1019 
  1024 
  1020     def postcommit_event(self):
  1025     def postcommit_event(self):
  1021         self.session.repo.threaded_task(self.sendmails)
  1026         self.cnx.repo.threaded_task(self.sendmails)
  1022 
  1027 
  1023     def sendmails(self):
  1028     def sendmails(self):
  1024         self.session.vreg.config.sendmails(self.to_send)
  1029         self.cnx.vreg.config.sendmails(self.to_send)
  1025 
  1030 
  1026 
  1031 
  1027 class RQLPrecommitOperation(Operation):
  1032 class RQLPrecommitOperation(Operation):
  1028     # to be defined in concrete classes
  1033     # to be defined in concrete classes
  1029     rqls = None
  1034     rqls = None
  1030 
  1035 
  1031     def precommit_event(self):
  1036     def precommit_event(self):
  1032         execute = self.session.execute
  1037         execute = self.cnx.execute
  1033         for rql in self.rqls:
  1038         for rql in self.rqls:
  1034             execute(*rql)
  1039             execute(*rql)
  1035 
  1040 
  1036 
  1041 
  1037 class CleanupNewEidsCacheOp(DataOperationMixIn, SingleLastOperation):
  1042 class CleanupNewEidsCacheOp(DataOperationMixIn, SingleLastOperation):
  1049     def rollback_event(self):
  1054     def rollback_event(self):
  1050         """the observed connections set has been rolled back,
  1055         """the observed connections set has been rolled back,
  1051         remove inserted eid from repository type/source cache
  1056         remove inserted eid from repository type/source cache
  1052         """
  1057         """
  1053         try:
  1058         try:
  1054             self.session.repo.clear_caches(self.get_data())
  1059             self.cnx.repo.clear_caches(self.get_data())
  1055         except KeyError:
  1060         except KeyError:
  1056             pass
  1061             pass
  1057 
  1062 
  1058 class CleanupDeletedEidsCacheOp(DataOperationMixIn, SingleLastOperation):
  1063 class CleanupDeletedEidsCacheOp(DataOperationMixIn, SingleLastOperation):
  1059     """on commit of delete query, we have to remove from repository's
  1064     """on commit of delete query, we have to remove from repository's
  1064         """the observed connections set has been rolled back,
  1069         """the observed connections set has been rolled back,
  1065         remove inserted eid from repository type/source cache
  1070         remove inserted eid from repository type/source cache
  1066         """
  1071         """
  1067         try:
  1072         try:
  1068             eids = self.get_data()
  1073             eids = self.get_data()
  1069             self.session.repo.clear_caches(eids)
  1074             self.cnx.repo.clear_caches(eids)
  1070             self.session.repo.app_instances_bus.publish(['delete'] + list(str(eid) for eid in eids))
  1075             self.cnx.repo.app_instances_bus.publish(['delete'] + list(str(eid) for eid in eids))
  1071         except KeyError:
  1076         except KeyError:
  1072             pass
  1077             pass