Updated CW tutorial.
* Summarized the list of steps to create a new cube.
* Added a subsection on writing entities.
* Added a subsection on modifying the schema and updating the corresponding instance.
"""Hooks managementThis module defined the `Hook` class and registry and a set of abstract classesfor operations.Hooks are called before / after any individual update of entities / relationsin the repository and on special events such as server startup or shutdown.Operations may be registered by hooks during a transaction, which will befired when the pool is commited or rollbacked.Entity hooks (eg before_add_entity, after_add_entity, before_update_entity,after_update_entity, before_delete_entity, after_delete_entity) all have an`entity` attributeRelation (eg before_add_relation, after_add_relation, before_delete_relation,after_delete_relation) all have `eidfrom`, `rtype`, `eidto` attributes.Server start/stop hooks (eg server_startup, server_shutdown) have a `repo`attribute, but *their `_cw` attribute is None*.Backup/restore hooks (eg server_backup, server_restore) have a `repo` and a`timestamp` attributes, but *their `_cw` attribute is None*.Session hooks (eg session_open, session_close) have no special attribute.:organization: Logilab:copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses"""__docformat__="restructuredtext en"fromwarningsimportwarnfromloggingimportgetLoggerfromitertoolsimportchainfromlogilab.common.decoratorsimportclasspropertyfromlogilab.common.deprecationimportdeprecatedfromlogilab.common.logging_extimportset_log_methodsfromcubicweb.cwvregimportCWRegistry,VRegistryfromcubicweb.selectorsimport(objectify_selector,lltrace,match_search_state,implements)fromcubicweb.appobjectimportAppObjectENTITIES_HOOKS=set(('before_add_entity','after_add_entity','before_update_entity','after_update_entity','before_delete_entity','after_delete_entity'))RELATIONS_HOOKS=set(('before_add_relation','after_add_relation','before_delete_relation','after_delete_relation'))SYSTEM_HOOKS=set(('server_backup','server_restore','server_startup','server_shutdown','session_open','session_close'))ALL_HOOKS=ENTITIES_HOOKS|RELATIONS_HOOKS|SYSTEM_HOOKSclassHooksRegistry(CWRegistry):defregister(self,obj,**kwargs):try:iter(obj.events)exceptAttributeError:raiseexcept:raiseException('bad .events attribute %s on %s.%s'%(obj.events,obj.__module__,obj.__name__))foreventinobj.events:ifeventnotinALL_HOOKS:raiseException('bad event %s on %s.%s'%(event,obj.__module__,obj.__name__))super(HooksRegistry,self).register(obj,**kwargs)defcall_hooks(self,event,req=None,**kwargs):kwargs['event']=eventforhookinsorted(self.possible_objects(req,**kwargs),key=lambdax:x.order):ifhook.enabled:hook()else:warn('[3.6] %s: enabled is deprecated'%hook.__class__)VRegistry.REGISTRY_FACTORY['hooks']=HooksRegistrydefentity_oldnewvalue(entity,attr):"""returns the couple (old attr value, new attr value) NOTE: will only work in a before_update_entity hook """# get new value and remove from local dict to force a db query to# fetch old valuenewvalue=entity.pop(attr,None)oldvalue=getattr(entity,attr)ifnewvalueisnotNone:entity[attr]=newvaluereturnoldvalue,newvalue# some hook specific selectors #################################################@objectify_selector@lltracedefmatch_event(cls,req,**kwargs):ifkwargs.get('event')incls.events:return1return0@objectify_selector@lltracedefenabled_category(cls,req,**kwargs):ifreqisNone:# server startup / shutdown eventconfig=kwargs['repo'].configelse:config=req.vreg.configreturnconfig.is_hook_activated(cls)@objectify_selector@lltracedefregular_session(cls,req,**kwargs):ifreqisNoneorreq.is_super_session:return0return1classrechain(object):def__init__(self,*iterators):self.iterators=iteratorsdef__iter__(self):returniter(chain(*self.iterators))classmatch_rtype(match_search_state):"""accept if parameters specified as initializer arguments are specified in named arguments given to the selector :param *expected: parameters (eg `basestring`) which are expected to be found in named arguments (kwargs) """def__init__(self,*expected,**more):self.expected=expectedself.frometypes=more.pop('frometypes',None)self.toetypes=more.pop('toetypes',None)@lltracedef__call__(self,cls,req,*args,**kwargs):ifkwargs.get('rtype')notinself.expected:return0ifself.frometypesisnotNoneand \req.describe(kwargs['eidfrom'])[0]notinself.frometypes:return0ifself.toetypesisnotNoneand \req.describe(kwargs['eidto'])[0]notinself.toetypes:return0return1classmatch_rtype_sets(match_search_state):"""accept if parameters specified as initializer arguments are specified in named arguments given to the selector """def__init__(self,*expected):self.expected=expected@lltracedef__call__(self,cls,req,*args,**kwargs):forrel_setinself.expected:ifkwargs.get('rtype')inrel_set:return1return0# base class for hook ##########################################################classHook(AppObject):__registry__='hooks'__select__=match_event()&enabled_category()# set this in derivated classesevents=Nonecategory=Noneorder=0# XXX deprecatedenabled=True@classpropertydef__regid__(cls):warn('[3.6] %s.%s: please specify an id for your hook'%(cls.__module__,cls.__name__),DeprecationWarning)returnstr(id(cls))@classmethoddef__registered__(cls,vreg):super(Hook,cls).__registered__(vreg)ifgetattr(cls,'accepts',None):warn('[3.6] %s.%s: accepts is deprecated, define proper __select__'%(cls.__module__,cls.__name__),DeprecationWarning)rtypes=[]forertypeincls.accepts:ifertype.islower():rtypes.append(ertype)else:cls.__select__=cls.__select__&implements(ertype)ifrtypes:cls.__select__=cls.__select__&match_rtype(*rtypes)returnclsknown_args=set(('entity','rtype','eidfrom','eidto','repo','timestamp'))def__init__(self,req,event,**kwargs):forarginself.known_args:ifarginkwargs:setattr(self,arg,kwargs.pop(arg))super(Hook,self).__init__(req,**kwargs)self.event=eventdef__call__(self):ifhasattr(self,'call'):cls=self.__class__warn('[3.6] %s.%s: call is deprecated, implements __call__'%(cls.__module__,cls.__name__),DeprecationWarning)ifself.event.endswith('_relation'):self.call(self._cw,self.eidfrom,self.rtype,self.eidto)elif'delete'inself.event:self.call(self._cw,self.entity.eid)elifself.event.startswith('server_'):self.call(self.repo)elifself.event.startswith('session_'):self.call(self._cw)else:self.call(self._cw,self.entity)set_log_methods(Hook,getLogger('cubicweb.hook'))# base classes for relation propagation ########################################classPropagateSubjectRelationHook(Hook):"""propagate permissions and nosy list when new entity are added"""events=('after_add_relation',)# to set in concrete classmain_rtype=Nonesubject_relations=Noneobject_relations=Nonedef__call__(self):foreidin(self.eidfrom,self.eidto):etype=self._cw.describe(eid)[0]ifnotself._cw.vreg.schema.eschema(etype).has_subject_relation(self.main_rtype):returnifself.rtypeinself.subject_relations:meid,seid=self.eidfrom,self.eidtoelse:assertself.rtypeinself.object_relationsmeid,seid=self.eidto,self.eidfromself._cw.unsafe_execute('SET E %s P WHERE X %s P, X eid %%(x)s, E eid %%(e)s, NOT E %s P'\%(self.main_rtype,self.main_rtype,self.main_rtype),{'x':meid,'e':seid},('x','e'))classPropagateSubjectRelationAddHook(Hook):"""propagate on existing entities when a permission or nosy list is added"""events=('after_add_relation',)# to set in concrete classmain_rtype=Nonesubject_relations=Noneobject_relations=Nonedef__call__(self):eschema=self._cw.vreg.schema.eschema(self._cw.describe(self.eidfrom)[0])execute=self._cw.unsafe_executeforrelinself.subject_relations:ifrelineschema.subjrels:execute('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, ''X %s R, NOT R %s P'%(self.rtype,rel,self.rtype),{'x':self.eidfrom,'p':self.eidto},'x')forrelinself.object_relations:ifrelineschema.objrels:execute('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, ''R %s X, NOT R %s P'%(self.rtype,rel,self.rtype),{'x':self.eidfrom,'p':self.eidto},'x')classPropagateSubjectRelationDelHook(Hook):"""propagate on existing entities when a permission is deleted"""events=('after_delete_relation',)# to set in concrete classmain_rtype=Nonesubject_relations=Noneobject_relations=Nonedef__call__(self):eschema=self._cw.vreg.schema.eschema(self._cw.describe(self.eidfrom)[0])execute=self._cw.unsafe_executeforrelinself.subject_relations:ifrelineschema.subjrels:execute('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, ''X %s R'%(self.rtype,rel),{'x':self.eidfrom,'p':self.eidto},'x')forrelinself.object_relations:ifrelineschema.objrels:execute('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, ''R %s X'%(self.rtype,rel),{'x':self.eidfrom,'p':self.eidto},'x')# abstract classes for operation ###############################################classOperation(object):"""an operation is triggered on connections pool events related to commit / rollback transations. Possible events are: precommit: the pool is preparing to commit. You shouldn't do anything things which has to be reverted if the commit fail at this point, but you can freely do any heavy computation or raise an exception if the commit can't go. You can add some new operation during this phase but their precommit event won't be triggered commit: the pool is preparing to commit. You should avoid to do to expensive stuff or something that may cause an exception in this event revertcommit: if an operation failed while commited, this event is triggered for all operations which had their commit event already to let them revert things (including the operation which made fail the commit) rollback: the transaction has been either rollbacked either * intentionaly * a precommit event failed, all operations are rollbacked * a commit event failed, all operations which are not been triggered for commit are rollbacked order of operations may be important, and is controlled according to: * operation's class """def__init__(self,session,**kwargs):self.session=sessionself.__dict__.update(kwargs)self.register(session)# execution informationself.processed=None# 'precommit', 'commit'self.failed=Falsedefregister(self,session):session.add_operation(self,self.insert_index())definsert_index(self):"""return the index of the lastest instance which is not a LateOperation instance """# faster by inspecting operation in reverse order for heavy transactionsi=Nonefori,opinenumerate(reversed(self.session.pending_operations)):ifisinstance(op,(LateOperation,SingleLastOperation)):continuereturn-iorNoneifiisNone:returnNonereturn-(i+1)defhandle_event(self,event):"""delegate event handling to the opertaion"""getattr(self,event)()defprecommit_event(self):"""the observed connections pool is preparing a commit"""defrevertprecommit_event(self):"""an error went when pre-commiting this operation or a later one should revert pre-commit's changes but take care, they may have not been all considered if it's this operation which failed """defcommit_event(self):"""the observed connections pool is commiting"""defrevertcommit_event(self):"""an error went when commiting this operation or a later one should revert commit's changes but take care, they may have not been all considered if it's this operation which failed """defrollback_event(self):"""the observed connections pool has been rollbacked do nothing by default, the operation will just be removed from the pool operation list """defpostcommit_event(self):"""the observed connections pool has committed"""@property@deprecated('[3.6] use self.session.user')defuser(self):returnself.session.user@property@deprecated('[3.6] use self.session.repo')defrepo(self):returnself.session.repo@property@deprecated('[3.6] use self.session.vreg.schema')defschema(self):returnself.session.repo.schema@property@deprecated('[3.6] use self.session.vreg.config')defconfig(self):returnself.session.repo.configset_log_methods(Operation,getLogger('cubicweb.session'))classLateOperation(Operation):"""special operation which should be called after all possible (ie non late) operations """definsert_index(self):"""return the index of the lastest instance which is not a SingleLastOperation instance """# faster by inspecting operation in reverse order for heavy transactionsi=Nonefori,opinenumerate(reversed(self.session.pending_operations)):ifisinstance(op,SingleLastOperation):continuereturn-iorNoneifiisNone:returnNonereturn-(i+1)classSingleOperation(Operation):"""special operation which should be called once"""defregister(self,session):"""override register to handle cases where this operation has already been added """operations=session.pending_operationsindex=self.equivalent_index(operations)ifindexisnotNone:equivalent=operations.pop(index)else:equivalent=Nonesession.add_operation(self,self.insert_index())returnequivalentdefequivalent_index(self,operations):"""return the index of the equivalent operation if any"""fori,opinenumerate(reversed(operations)):ifop.__class__isself.__class__:return-(i+1)returnNoneclassSingleLastOperation(SingleOperation):"""special operation which should be called once and after all other operations """definsert_index(self):returnNoneclassSendMailOp(SingleLastOperation):def__init__(self,session,msg=None,recipients=None,**kwargs):# may not specify msg yet, as# `cubicweb.sobjects.supervision.SupervisionMailOp`ifmsgisnotNone:assertrecipientsself.to_send=[(msg,recipients)]else:assertrecipientsisNoneself.to_send=[]super(SendMailOp,self).__init__(session,**kwargs)defregister(self,session):previous=super(SendMailOp,self).register(session)ifprevious:self.to_send=previous.to_send+self.to_senddefcommit_event(self):self.session.repo.threaded_task(self.sendmails)defsendmails(self):self.session.vreg.config.sendmails(self.to_send)classRQLPrecommitOperation(Operation):defprecommit_event(self):execute=self.session.unsafe_executeforrqlinself.rqls:execute(*rql)