1 .. -*- coding: utf-8 -*- |
1 .. -*- coding: utf-8 -*- |
2 |
|
3 .. _hooks: |
2 .. _hooks: |
4 |
3 |
5 Hooks and Operations |
4 Hooks and Operations |
6 ==================== |
5 ==================== |
7 |
6 |
8 Generalities |
7 .. autodocstring:: cubicweb.server.hook |
9 ------------ |
8 |
10 |
9 Example using dataflow hooks |
11 Paraphrasing the `emacs`_ documentation, let us say that hooks are an |
10 ---------------------------- |
12 important mechanism for customizing an application. A hook is |
11 |
13 basically a list of functions to be called on some well-defined |
12 We will use a very simple example to show hooks usage. Let us start with the |
14 occasion (this is called `running the hook`). |
13 following schema. |
15 |
|
16 .. _`emacs`: http://www.gnu.org/software/emacs/manual/html_node/emacs/Hooks.html |
|
17 |
|
18 In CubicWeb, hooks are subclasses of the Hook class in |
|
19 `server/hook.py`, implementing their own `call` method, and selected |
|
20 over a set of pre-defined `events` (and possibly more conditions, |
|
21 hooks being selectable AppObjects like views and components). |
|
22 |
|
23 There are two families of events: data events and server events. In a |
|
24 typical application, most of the Hooks are defined over data |
|
25 events. |
|
26 |
|
27 The purpose of data hooks is to complement the data model as defined |
|
28 in the schema.py, which is static by nature, with dynamic or value |
|
29 driven behaviours. It is functionally equivalent to a `database |
|
30 trigger`_, except that database triggers definition languages are not |
|
31 standardized, hence not portable (for instance, PL/SQL works with |
|
32 Oracle and PostgreSQL but not SqlServer nor Sqlite). |
|
33 |
|
34 .. _`database trigger`: http://en.wikipedia.org/wiki/Database_trigger |
|
35 |
|
36 Data hooks can serve the following purposes: |
|
37 |
|
38 * enforcing constraints that the static schema cannot express |
|
39 (spanning several entities/relations, exotic value ranges and |
|
40 cardinalities, etc.) |
|
41 |
|
42 * implement computed attributes |
|
43 |
|
44 Operations are Hook-like objects that may be created by Hooks and |
|
45 scheduled to happen just before (or after) the `commit` event. Hooks |
|
46 being fired immediately on data operations, it is sometime necessary |
|
47 to delay the actual work down to a time where all other Hooks have |
|
48 run, for instance a validation check which needs that all relations be |
|
49 already set on an entity. Also while the order of execution of Hooks |
|
50 is data dependant (and thus hard to predict), it is possible to force |
|
51 an order on Operations. |
|
52 |
|
53 Operations also may be used to process various side effects associated |
|
54 with a transaction such as filesystem udpates, mail notifications, |
|
55 etc. |
|
56 |
|
57 Operations are subclasses of the Operation class in `server/hook.py`, |
|
58 implementing `precommit_event` and other standard methods (wholly |
|
59 described in :ref:`operations_api`). |
|
60 |
|
61 Events |
|
62 ------ |
|
63 |
|
64 Hooks are mostly defined and used to handle `dataflow`_ operations. It |
|
65 means as data gets in (entities added, updated, relations set or |
|
66 unset), specific events are issued and the Hooks matching these events |
|
67 are called. |
|
68 |
|
69 .. _`dataflow`: http://en.wikipedia.org/wiki/Dataflow |
|
70 |
|
71 Below comes a list of the dataflow events related to entities operations: |
|
72 |
|
73 * before_add_entity |
|
74 |
|
75 * before_update_entity |
|
76 |
|
77 * before_delete_entity |
|
78 |
|
79 * after_add_entity |
|
80 |
|
81 * after_update_entity |
|
82 |
|
83 * after_delete_entity |
|
84 |
|
85 These define ENTTIES HOOKS. RELATIONS HOOKS are defined |
|
86 over the following events: |
|
87 |
|
88 * after_add_relation |
|
89 |
|
90 * after_delete_relation |
|
91 |
|
92 * before_add_relation |
|
93 |
|
94 * before_delete_relation |
|
95 |
|
96 This is an occasion to remind us that relations support the add/delete |
|
97 operation, but no update. |
|
98 |
|
99 Non data events also exist. These are called SYSTEM HOOKS. |
|
100 |
|
101 * server_startup |
|
102 |
|
103 * server_shutdown |
|
104 |
|
105 * server_maintenance |
|
106 |
|
107 * server_backup |
|
108 |
|
109 * server_restore |
|
110 |
|
111 * session_open |
|
112 |
|
113 * session_close |
|
114 |
|
115 |
|
116 Using dataflow Hooks |
|
117 -------------------- |
|
118 |
|
119 Dataflow hooks either automate data operations or maintain the |
|
120 consistency of the data model. In the later case, we must use a |
|
121 specific exception named ValidationError |
|
122 |
|
123 Validation Errors |
|
124 ~~~~~~~~~~~~~~~~~ |
|
125 |
|
126 When a condition is not met in a Hook/Operation, it must raise a |
|
127 `ValidationError`. Raising anything but a (subclass of) |
|
128 ValidationError is a programming error. Raising a ValidationError |
|
129 entails aborting the current transaction. |
|
130 |
|
131 The ValidationError exception is used to convey enough information up |
|
132 to the user interface. Hence its constructor is different from the |
|
133 default Exception constructor. It accepts, positionally: |
|
134 |
|
135 * an entity eid, |
|
136 |
|
137 * a dict whose keys represent attribute (or relation) names and values |
|
138 an end-user facing message (hence properly translated) relating the |
|
139 problem. |
|
140 |
|
141 An entity hook |
|
142 ~~~~~~~~~~~~~~ |
|
143 |
|
144 We will use a very simple example to show hooks usage. Let us start |
|
145 with the following schema. |
|
146 |
14 |
147 .. sourcecode:: python |
15 .. sourcecode:: python |
148 |
16 |
149 class Person(EntityType): |
17 class Person(EntityType): |
150 age = Int(required=True) |
18 age = Int(required=True) |
151 |
19 |
152 We would like to add a range constraint over a person's age. Let's |
20 We would like to add a range constraint over a person's age. Let's write an hook |
153 write an hook. It shall be placed into mycube/hooks.py. If this file |
21 (supposing yams can not handle this nativly, which is wrong). It shall be placed |
154 were to grow too much, we can easily have a mycube/hooks/... package |
22 into `mycube/hooks.py`. If this file were to grow too much, we can easily have a |
155 containing hooks in various modules. |
23 `mycube/hooks/... package` containing hooks in various modules. |
156 |
24 |
157 .. sourcecode:: python |
25 .. sourcecode:: python |
158 |
26 |
159 from cubicweb import ValidationError |
27 from cubicweb import ValidationError |
160 from cubicweb.selectors import implements |
28 from cubicweb.selectors import implements |
164 __regid__ = 'person_age_range' |
32 __regid__ = 'person_age_range' |
165 events = ('before_add_entity', 'before_update_entity') |
33 events = ('before_add_entity', 'before_update_entity') |
166 __select__ = Hook.__select__ & implements('Person') |
34 __select__ = Hook.__select__ & implements('Person') |
167 |
35 |
168 def __call__(self): |
36 def __call__(self): |
169 if 0 >= self.entity.age <= 120: |
37 if 'age' in self.entity.cw_edited: |
170 return |
38 if 0 >= self.entity.age <= 120: |
171 msg = self._cw._('age must be between 0 and 120') |
39 return |
172 raise ValidationError(self.entity.eid, {'age': msg}) |
40 msg = self._cw._('age must be between 0 and 120') |
173 |
41 raise ValidationError(self.entity.eid, {'age': msg}) |
174 Hooks being AppObjects like views, they have a __regid__ and a |
42 |
175 __select__ class attribute. The base __select__ is augmented with an |
43 In our example the base `__select__` is augmented with an `implements` selector |
176 `implements` selector matching the desired entity type. The `events` |
44 matching the desired entity type. |
177 tuple is used by the Hook.__select__ base selector to dispatch the |
45 |
178 hook on the right events. In an entity hook, it is possible to |
46 The `events` tuple is used specify that our hook should be called before the |
179 dispatch on any entity event (e.g. 'before_add_entity', |
47 entity is added or updated. |
180 'before_update_entity') at once if needed. |
48 |
181 |
49 Then in the hook's `__call__` method, we: |
182 Like all appobjects, hooks have the `self._cw` attribute which |
50 |
183 represents the current session. In entity hooks, a `self.entity` |
51 * check if the 'age' attribute is edited |
184 attribute is also present. |
52 * if so, check the value is in the range |
185 |
53 * if not, raise a validation error properly |
186 |
54 |
187 A relation hook |
55 Now Let's augment our schema with new `Company` entity type with some relation to |
188 ~~~~~~~~~~~~~~~ |
56 `Person` (in 'mycube/schema.py'). |
189 |
|
190 Let us add another entity type with a relation to person (in |
|
191 mycube/schema.py). |
|
192 |
57 |
193 .. sourcecode:: python |
58 .. sourcecode:: python |
194 |
59 |
195 class Company(EntityType): |
60 class Company(EntityType): |
196 name = String(required=True) |
61 name = String(required=True) |
197 boss = SubjectRelation('Person', cardinality='1*') |
62 boss = SubjectRelation('Person', cardinality='1*') |
198 |
63 subsidiary_of = SubjectRelation('Company', cardinality='*?') |
199 We would like to constrain the company's bosses to have a minimum |
64 |
200 (legal) age. Let's write an hook for this, which will be fired when |
65 |
201 the `boss` relation is established. |
66 We would like to constrain the company's bosses to have a minimum (legal) |
|
67 age. Let's write an hook for this, which will be fired when the `boss` relation |
|
68 is established (still supposing we could not specify that kind of thing in the |
|
69 schema). |
202 |
70 |
203 .. sourcecode:: python |
71 .. sourcecode:: python |
204 |
72 |
205 class CompanyBossLegalAge(Hook): |
73 class CompanyBossLegalAge(Hook): |
206 __regid__ = 'company_boss_legal_age' |
74 __regid__ = 'company_boss_legal_age' |
|
75 __select__ = Hook.__select__ & match_rtype('boss') |
207 events = ('before_add_relation',) |
76 events = ('before_add_relation',) |
208 __select__ = Hook.__select__ & match_rtype('boss') |
|
209 |
77 |
210 def __call__(self): |
78 def __call__(self): |
211 boss = self._cw.entity_from_eid(self.eidto) |
79 boss = self._cw.entity_from_eid(self.eidto) |
212 if boss.age < 18: |
80 if boss.age < 18: |
213 msg = self._cw._('the minimum age for a boss is 18') |
81 msg = self._cw._('the minimum age for a boss is 18') |
214 raise ValidationError(self.eidfrom, {'boss': msg}) |
82 raise ValidationError(self.eidfrom, {'boss': msg}) |
215 |
83 |
216 We use the `match_rtype` selector to select the proper relation type. |
84 .. Note:: |
217 |
85 |
218 The essential difference with respect to an entity hook is that there |
86 We use the :class:`~cubicweb.server.hook.match_rtype` selector to select the |
219 is no self.entity, but `self.eidfrom` and `self.eidto` hook attributes |
87 proper relation type. |
220 which represent the subject and object eid of the relation. |
88 |
221 |
89 The essential difference with respect to an entity hook is that there is no |
222 |
90 self.entity, but `self.eidfrom` and `self.eidto` hook attributes which |
223 Using Operations |
91 represent the subject and object **eid** of the relation. |
224 ---------------- |
92 |
225 |
93 Suppose we want to check that there is no cycle by the `subsidiary_of` |
226 Let's augment our example with a new `subsidiary_of` relation on Company. |
94 relation. This is best achieved in an operation since all relations are likely to |
227 |
95 be set at commit time. |
228 .. sourcecode:: python |
|
229 |
|
230 class Company(EntityType): |
|
231 name = String(required=True) |
|
232 boss = SubjectRelation('Person', cardinality='1*') |
|
233 subsidiary_of = SubjectRelation('Company', cardinality='*?') |
|
234 |
|
235 Base example |
|
236 ~~~~~~~~~~~~ |
|
237 |
|
238 We would like to check that there is no cycle by the `subsidiary_of` |
|
239 relation. This is best achieved in an Operation since all relations |
|
240 are likely to be set at commit time. |
|
241 |
96 |
242 .. sourcecode:: python |
97 .. sourcecode:: python |
243 |
98 |
244 def check_cycle(self, session, eid, rtype, role='subject'): |
99 def check_cycle(self, session, eid, rtype, role='subject'): |
245 parents = set([eid]) |
100 parents = set([eid]) |
249 if parent.eid in parents: |
104 if parent.eid in parents: |
250 msg = session._('detected %s cycle' % rtype) |
105 msg = session._('detected %s cycle' % rtype) |
251 raise ValidationError(eid, {rtype: msg}) |
106 raise ValidationError(eid, {rtype: msg}) |
252 parents.add(parent.eid) |
107 parents.add(parent.eid) |
253 |
108 |
|
109 |
254 class CheckSubsidiaryCycleOp(Operation): |
110 class CheckSubsidiaryCycleOp(Operation): |
255 |
111 |
256 def precommit_event(self): |
112 def precommit_event(self): |
257 check_cycle(self.session, self.eidto, 'subsidiary_of') |
113 check_cycle(self.session, self.eidto, 'subsidiary_of') |
258 |
114 |
259 |
115 |
260 class CheckSubsidiaryCycleHook(Hook): |
116 class CheckSubsidiaryCycleHook(Hook): |
261 __regid__ = 'check_no_subsidiary_cycle' |
117 __regid__ = 'check_no_subsidiary_cycle' |
|
118 __select__ = Hook.__select__ & match_rtype('subsidiary_of') |
262 events = ('after_add_relation',) |
119 events = ('after_add_relation',) |
263 __select__ = Hook.__select__ & match_rtype('subsidiary_of') |
|
264 |
120 |
265 def __call__(self): |
121 def __call__(self): |
266 CheckSubsidiaryCycleOp(self._cw, eidto=self.eidto) |
122 CheckSubsidiaryCycleOp(self._cw, eidto=self.eidto) |
267 |
123 |
268 The operation is instantiated in the Hook.__call__ method. |
124 |
269 |
125 Like in hooks, :exc:`~cubicweb.ValidationError` can be raised in operations. Other |
270 An operation always takes a session object as first argument |
126 exceptions are usually programming errors. |
271 (accessible as `.session` from the operation instance), and optionally |
127 |
272 all keyword arguments needed by the operation. These keyword arguments |
128 In the above example, our hook will instantiate an operation each time the hook |
273 will be accessible as attributes from the operation instance. |
129 is called, i.e. each time the `subsidiary_of` relation is set. There is an |
274 |
130 alternative method to schedule an operation from a hook, using the |
275 Like in Hooks, ValidationError can be raised in Operations. Other |
131 :func:`set_operation` function. |
276 exceptions are programming errors. |
|
277 |
|
278 Notice how our hook will instantiate an operation each time the Hook |
|
279 is called, i.e. each time the `subsidiary_of` relation is set. |
|
280 |
|
281 Using set_operation |
|
282 ~~~~~~~~~~~~~~~~~~~ |
|
283 |
|
284 There is an alternative method to schedule an Operation from a Hook, |
|
285 using the `set_operation` function. |
|
286 |
132 |
287 .. sourcecode:: python |
133 .. sourcecode:: python |
288 |
134 |
289 from cubicweb.server.hook import set_operation |
135 from cubicweb.server.hook import set_operation |
290 |
136 |
293 events = ('after_add_relation',) |
139 events = ('after_add_relation',) |
294 __select__ = Hook.__select__ & match_rtype('subsidiary_of') |
140 __select__ = Hook.__select__ & match_rtype('subsidiary_of') |
295 |
141 |
296 def __call__(self): |
142 def __call__(self): |
297 set_operation(self._cw, 'subsidiary_cycle_detection', self.eidto, |
143 set_operation(self._cw, 'subsidiary_cycle_detection', self.eidto, |
298 CheckSubsidiaryCycleOp, rtype=self.rtype) |
144 CheckSubsidiaryCycleOp) |
299 |
145 |
300 class CheckSubsidiaryCycleOp(Operation): |
146 class CheckSubsidiaryCycleOp(Operation): |
301 |
147 |
302 def precommit_event(self): |
148 def precommit_event(self): |
303 for eid in self._cw.transaction_data['subsidiary_cycle_detection']: |
149 for eid in self._cw.transaction_data.pop('subsidiary_cycle_detection'): |
304 check_cycle(self.session, eid, self.rtype) |
150 check_cycle(self.session, eid, 'subsidiary_of') |
305 |
151 |
306 Here, we call set_operation with a session object, a specially forged |
152 |
307 key, a value that is the actual payload of an individual operation (in |
153 Here, we call :func:`set_operation` so that we will simply accumulate eids of |
308 our case, the object of the subsidiary_of relation) , the class of the |
154 entities to check at the end in a single CheckSubsidiaryCycleOp operation. Value |
309 Operation, and more optional parameters to give to the operation (here |
155 are stored in a set associated to the 'subsidiary_cycle_detection' transaction |
310 the rtype which do not vary accross operations). |
156 data key. The set initialization and operation creation are handled nicely by |
311 |
157 :func:set_operation. |
312 The body of the operation must then iterate over the values that have |
158 |
313 been mapped in the transaction_data dictionary to the forged key. |
159 A more realistic example can be found in the advanced tutorial chapter |
314 |
160 :ref:`adv_tuto_security_propagation`. |
315 This mechanism is especially useful on two occasions (not shown in our |
161 |
316 example): |
162 |
317 |
163 Hooks writing tips |
318 * massive data import (reduced memory consumption within a large |
164 ------------------ |
319 transaction) |
165 |
320 |
166 Reminder |
321 * when several hooks need to instantiate the same operation (e.g. an |
167 ~~~~~~~~ |
322 entity and a relation hook). |
168 |
323 |
169 Never, ever use the `entity.foo = 42` notation to update an entity. It will not |
324 .. note:: |
170 work.To updating an entity attribute or relation, uses :meth:`set_attributes` and |
325 |
171 :meth:`set_relations` methods. |
326 A more realistic example can be found in the advanced tutorial |
172 |
327 chapter :ref:`adv_tuto_security_propagation`. |
|
328 |
|
329 .. _operations_api: |
|
330 |
|
331 Operation: a small API overview |
|
332 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
333 |
|
334 .. autoclass:: cubicweb.server.hook.Operation |
|
335 .. autoclass:: cubicweb.server.hook.LateOperation |
|
336 .. autofunction:: cubicweb.server.hook.set_operation |
|
337 |
|
338 Hooks writing rules |
|
339 ------------------- |
|
340 |
|
341 Remainder |
|
342 ~~~~~~~~~ |
|
343 |
|
344 Never, ever use the `entity.foo = 42` notation to update an entity. It |
|
345 will not work. |
|
346 |
173 |
347 How to choose between a before and an after event ? |
174 How to choose between a before and an after event ? |
348 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
175 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
349 |
176 |
350 Before hooks give you access to the old attribute (or relation) |
177 'before_*' hooks give you access to the old attribute (or relation) |
351 values. By definition the database is not yet updated in a before |
178 values. You can also hi-jack actually edited stuff in the case of entity |
352 hook. |
179 modification. Needing one of this will definitly guide your choice. |
353 |
180 |
354 To access old and new values in an before_update_entity hook, one can |
181 Else the question is: should I need to do things before or after the actual |
355 use the `server.hook.entity_oldnewvalue` function which returns a |
182 modification. If the answer is "it doesn't matter", use an 'after' event. |
356 tuple of the old and new values. This function takes an entity and an |
183 |
357 attribute name as parameters. |
184 |
358 |
185 Validation Errors |
359 In a 'before_add|update_entity' hook the self.entity contains the new |
186 ~~~~~~~~~~~~~~~~~ |
360 values. One is allowed to further modify them before database |
187 |
361 operations, using the dictionary notation. |
188 When a hook is responsible to maintain the consistency of the data model detect |
362 |
189 an error, it must use a specific exception named |
363 .. sourcecode:: python |
190 :exc:`~cubicweb.ValidationError`. Raising anything but a (subclass of) |
364 |
191 :exc:`~cubicweb.ValidationError` is a programming error. Raising a it entails |
365 self.entity['age'] = 42 |
192 aborting the current transaction. |
366 |
193 |
367 This is because using self.entity.set_attributes(age=42) will |
194 This exception is used to convey enough information up to the user |
368 immediately update the database (which does not make sense in a |
195 interface. Hence its constructor is different from the default Exception |
369 pre-database hook), and will trigger any existing |
196 constructor. It accepts, positionally: |
370 before_add|update_entity hook, thus leading to infinite hook loops or |
197 |
371 such awkward situations. |
198 * an entity eid, |
372 |
199 |
373 Beyond these specific cases, updating an entity attribute or relation |
200 * a dict whose keys represent attribute (or relation) names and values |
374 must *always* be done using `set_attributes` and `set_relations` |
201 an end-user facing message (hence properly translated) relating the |
375 methods. |
202 problem. |
376 |
203 |
377 (Of course, ValidationError will always abort the current transaction, |
204 |
378 whetever the event). |
205 Checking for object created/deleted in the current transaction |
|
206 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
207 |
|
208 In hooks, you can use the |
|
209 :meth:`~cubicweb.server.session.Session.added_in_transaction` or |
|
210 :meth:`~cubicweb.server.session.Session.deleted_in_transaction` of the session |
|
211 object to check if an eid has been created or deleted during the hook's |
|
212 transaction. |
|
213 |
|
214 This is useful to enable or disable some stuff if some entity is being added or |
|
215 deleted. |
|
216 |
|
217 .. sourcecode:: python |
|
218 |
|
219 if self._cw.deleted_in_transaction(self.eidto): |
|
220 return |
|
221 |
379 |
222 |
380 Peculiarities of inlined relations |
223 Peculiarities of inlined relations |
381 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
224 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
382 |
225 |
383 Some relations are defined in the schema as `inlined` (see |
226 Relations which are defined in the schema as `inlined` (see :ref:`RelationType` |
384 :ref:`RelationType` for details). In this case, they are inserted in |
227 for details) are inserted in the database at the same time as entity attributes. |
385 the database at the same time as entity attributes. |
228 This may have some side effect, for instance when creating entity and setting an |
386 |
229 inlined relation in the same rql query, when 'before_add_relation' for that |
387 Hence in the case of before_add_relation, such relations already exist |
230 relation will be run, the relation will already exist in the database (it's |
388 in the database. |
231 usually not the case). |
389 |
|
390 Edited attributes |
|
391 ~~~~~~~~~~~~~~~~~ |
|
392 |
|
393 On udpates, it is possible to ask the `entity.edited_attributes` |
|
394 variable whether one attribute has been updated. |
|
395 |
|
396 .. sourcecode:: python |
|
397 |
|
398 if 'age' not in entity.edited_attribute: |
|
399 return |
|
400 |
|
401 Deleted in transaction |
|
402 ~~~~~~~~~~~~~~~~~~~~~~ |
|
403 |
|
404 The session object has a deleted_in_transaction method, which can help |
|
405 writing deletion Hooks. |
|
406 |
|
407 .. sourcecode:: python |
|
408 |
|
409 if self._cw.deleted_in_transaction(self.eidto): |
|
410 return |
|
411 |
|
412 Given this predicate, we can avoid scheduling an operation. |
|
413 |
|
414 Disabling hooks |
|
415 ~~~~~~~~~~~~~~~ |
|
416 |
|
417 It is sometimes convenient to disable some hooks. For instance to |
|
418 avoid infinite Hook loops. One uses the `hooks_control` context |
|
419 manager. |
|
420 |
|
421 This can be controlled more finely through the `category` Hook class |
|
422 attribute, which is a string. |
|
423 |
|
424 .. sourcecode:: python |
|
425 |
|
426 with hooks_control(self.session, self.session.HOOKS_ALLOW_ALL, <category>): |
|
427 # ... do stuff |
|
428 |
|
429 .. autoclass:: cubicweb.server.session.hooks_control |
|
430 |
|
431 The existing categories are: ``email``, ``syncsession``, |
|
432 ``syncschema``, ``bookmark``, ``security``, ``worfklow``, |
|
433 ``metadata``, ``notification``, ``integrity``, ``activeintegrity``. |
|
434 |
|
435 Nothing precludes one to invent new categories and use the |
|
436 hooks_control context manager to filter them (in or out). |
|