vortex-nwp 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. vortex/__init__.py +159 -0
  2. vortex/algo/__init__.py +13 -0
  3. vortex/algo/components.py +2462 -0
  4. vortex/algo/mpitools.py +1953 -0
  5. vortex/algo/mpitools_templates/__init__.py +1 -0
  6. vortex/algo/mpitools_templates/envelope_wrapper_default.tpl +27 -0
  7. vortex/algo/mpitools_templates/envelope_wrapper_mpiauto.tpl +29 -0
  8. vortex/algo/mpitools_templates/wrapstd_wrapper_default.tpl +18 -0
  9. vortex/algo/serversynctools.py +171 -0
  10. vortex/config.py +112 -0
  11. vortex/data/__init__.py +19 -0
  12. vortex/data/abstractstores.py +1510 -0
  13. vortex/data/containers.py +835 -0
  14. vortex/data/contents.py +622 -0
  15. vortex/data/executables.py +275 -0
  16. vortex/data/flow.py +119 -0
  17. vortex/data/geometries.ini +2689 -0
  18. vortex/data/geometries.py +799 -0
  19. vortex/data/handlers.py +1230 -0
  20. vortex/data/outflow.py +67 -0
  21. vortex/data/providers.py +487 -0
  22. vortex/data/resources.py +207 -0
  23. vortex/data/stores.py +1390 -0
  24. vortex/data/sync_templates/__init__.py +0 -0
  25. vortex/gloves.py +309 -0
  26. vortex/layout/__init__.py +20 -0
  27. vortex/layout/contexts.py +577 -0
  28. vortex/layout/dataflow.py +1220 -0
  29. vortex/layout/monitor.py +969 -0
  30. vortex/nwp/__init__.py +14 -0
  31. vortex/nwp/algo/__init__.py +21 -0
  32. vortex/nwp/algo/assim.py +537 -0
  33. vortex/nwp/algo/clim.py +1086 -0
  34. vortex/nwp/algo/coupling.py +831 -0
  35. vortex/nwp/algo/eda.py +840 -0
  36. vortex/nwp/algo/eps.py +785 -0
  37. vortex/nwp/algo/forecasts.py +886 -0
  38. vortex/nwp/algo/fpserver.py +1303 -0
  39. vortex/nwp/algo/ifsnaming.py +463 -0
  40. vortex/nwp/algo/ifsroot.py +404 -0
  41. vortex/nwp/algo/monitoring.py +263 -0
  42. vortex/nwp/algo/mpitools.py +694 -0
  43. vortex/nwp/algo/odbtools.py +1258 -0
  44. vortex/nwp/algo/oopsroot.py +916 -0
  45. vortex/nwp/algo/oopstests.py +220 -0
  46. vortex/nwp/algo/request.py +660 -0
  47. vortex/nwp/algo/stdpost.py +1641 -0
  48. vortex/nwp/data/__init__.py +30 -0
  49. vortex/nwp/data/assim.py +380 -0
  50. vortex/nwp/data/boundaries.py +314 -0
  51. vortex/nwp/data/climfiles.py +521 -0
  52. vortex/nwp/data/configfiles.py +153 -0
  53. vortex/nwp/data/consts.py +954 -0
  54. vortex/nwp/data/ctpini.py +149 -0
  55. vortex/nwp/data/diagnostics.py +209 -0
  56. vortex/nwp/data/eda.py +147 -0
  57. vortex/nwp/data/eps.py +432 -0
  58. vortex/nwp/data/executables.py +1045 -0
  59. vortex/nwp/data/fields.py +111 -0
  60. vortex/nwp/data/gridfiles.py +380 -0
  61. vortex/nwp/data/logs.py +584 -0
  62. vortex/nwp/data/modelstates.py +363 -0
  63. vortex/nwp/data/monitoring.py +193 -0
  64. vortex/nwp/data/namelists.py +696 -0
  65. vortex/nwp/data/obs.py +840 -0
  66. vortex/nwp/data/oopsexec.py +74 -0
  67. vortex/nwp/data/providers.py +207 -0
  68. vortex/nwp/data/query.py +206 -0
  69. vortex/nwp/data/stores.py +160 -0
  70. vortex/nwp/data/surfex.py +337 -0
  71. vortex/nwp/syntax/__init__.py +9 -0
  72. vortex/nwp/syntax/stdattrs.py +437 -0
  73. vortex/nwp/tools/__init__.py +10 -0
  74. vortex/nwp/tools/addons.py +40 -0
  75. vortex/nwp/tools/agt.py +67 -0
  76. vortex/nwp/tools/bdap.py +59 -0
  77. vortex/nwp/tools/bdcp.py +41 -0
  78. vortex/nwp/tools/bdm.py +24 -0
  79. vortex/nwp/tools/bdmp.py +54 -0
  80. vortex/nwp/tools/conftools.py +1661 -0
  81. vortex/nwp/tools/drhook.py +66 -0
  82. vortex/nwp/tools/grib.py +294 -0
  83. vortex/nwp/tools/gribdiff.py +104 -0
  84. vortex/nwp/tools/ifstools.py +203 -0
  85. vortex/nwp/tools/igastuff.py +273 -0
  86. vortex/nwp/tools/mars.py +68 -0
  87. vortex/nwp/tools/odb.py +657 -0
  88. vortex/nwp/tools/partitioning.py +258 -0
  89. vortex/nwp/tools/satrad.py +71 -0
  90. vortex/nwp/util/__init__.py +6 -0
  91. vortex/nwp/util/async.py +212 -0
  92. vortex/nwp/util/beacon.py +40 -0
  93. vortex/nwp/util/diffpygram.py +447 -0
  94. vortex/nwp/util/ens.py +279 -0
  95. vortex/nwp/util/hooks.py +139 -0
  96. vortex/nwp/util/taskdeco.py +85 -0
  97. vortex/nwp/util/usepygram.py +697 -0
  98. vortex/nwp/util/usetnt.py +101 -0
  99. vortex/proxy.py +6 -0
  100. vortex/sessions.py +374 -0
  101. vortex/syntax/__init__.py +9 -0
  102. vortex/syntax/stdattrs.py +867 -0
  103. vortex/syntax/stddeco.py +185 -0
  104. vortex/toolbox.py +1117 -0
  105. vortex/tools/__init__.py +20 -0
  106. vortex/tools/actions.py +523 -0
  107. vortex/tools/addons.py +316 -0
  108. vortex/tools/arm.py +96 -0
  109. vortex/tools/compression.py +325 -0
  110. vortex/tools/date.py +27 -0
  111. vortex/tools/ddhpack.py +10 -0
  112. vortex/tools/delayedactions.py +782 -0
  113. vortex/tools/env.py +541 -0
  114. vortex/tools/folder.py +834 -0
  115. vortex/tools/grib.py +738 -0
  116. vortex/tools/lfi.py +953 -0
  117. vortex/tools/listings.py +423 -0
  118. vortex/tools/names.py +637 -0
  119. vortex/tools/net.py +2124 -0
  120. vortex/tools/odb.py +10 -0
  121. vortex/tools/parallelism.py +368 -0
  122. vortex/tools/prestaging.py +210 -0
  123. vortex/tools/rawfiles.py +10 -0
  124. vortex/tools/schedulers.py +480 -0
  125. vortex/tools/services.py +940 -0
  126. vortex/tools/storage.py +996 -0
  127. vortex/tools/surfex.py +61 -0
  128. vortex/tools/systems.py +3976 -0
  129. vortex/tools/targets.py +440 -0
  130. vortex/util/__init__.py +9 -0
  131. vortex/util/config.py +1122 -0
  132. vortex/util/empty.py +24 -0
  133. vortex/util/helpers.py +216 -0
  134. vortex/util/introspection.py +69 -0
  135. vortex/util/iosponge.py +80 -0
  136. vortex/util/roles.py +49 -0
  137. vortex/util/storefunctions.py +129 -0
  138. vortex/util/structs.py +26 -0
  139. vortex/util/worker.py +162 -0
  140. vortex_nwp-2.0.0.dist-info/METADATA +67 -0
  141. vortex_nwp-2.0.0.dist-info/RECORD +144 -0
  142. vortex_nwp-2.0.0.dist-info/WHEEL +5 -0
  143. vortex_nwp-2.0.0.dist-info/licenses/LICENSE +517 -0
  144. vortex_nwp-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,20 @@
1
+ """
2
+ This is a pure package containing several modules that could be used
3
+ as standalone tools.
4
+ """
5
+
6
+ from . import storage as storage
7
+ from . import schedulers as schedulers
8
+ from . import services as services
9
+ from . import systems as systems
10
+ from . import targets as targets
11
+ from . import date as date
12
+ from . import env as env
13
+ from . import names as names
14
+
15
+ #: No automatic export
16
+ __all__ = []
17
+
18
+ __tocinfoline__ = (
19
+ "VORTEX generic tools (system interfaces, format handling, ...)"
20
+ )
@@ -0,0 +1,523 @@
1
+ """
2
+ Module managing the sending of messages.
3
+ Default action classes must provide four methods: on, off, status, execute.
4
+ The on, off and status functions must return a boolean value reflecting the
5
+ status of the action. As far as the execute function is concerned,
6
+ it must deal with the data (given to realize the action) and the action
7
+ to be processed: e.g. mail, routing, alarm.
8
+ """
9
+
10
+ import bronx.stdtypes.catalog
11
+ import footprints
12
+ from bronx.fancies import loggers
13
+ from bronx.fancies.display import dict_as_str
14
+ from vortex import sessions
15
+
16
+ #: Export nothing
17
+ __all__ = []
18
+
19
+ logger = loggers.getLogger(__name__)
20
+
21
+
22
+ class Action:
23
+ """
24
+ An ``Action`` object is intended to produce a dedicated service through a simple command
25
+ which internally refers to the :meth:`execute` method.
26
+ Such an action could be activated or not, and is basically driven by permissions settings.
27
+ """
28
+
29
+ def __init__(
30
+ self, kind="foo", service=None, active=False, permanent=False
31
+ ):
32
+ if service is None:
33
+ service = "send" + kind
34
+ self._service = service
35
+ self._kind = kind
36
+ self._active = active
37
+ self._permanent = permanent
38
+ self._frozen = None
39
+
40
+ @property
41
+ def kind(self):
42
+ """Kind name of this action."""
43
+ return self._kind
44
+
45
+ @property
46
+ def service(self):
47
+ """Standard service associated to this action."""
48
+ return self._service
49
+
50
+ @property
51
+ def active(self):
52
+ """Current status of the action as a boolean property."""
53
+ return self._active
54
+
55
+ def permanent(self, update=None):
56
+ """Return or update the permanent status of this action."""
57
+ if update is not None:
58
+ self._permanent = bool(update)
59
+ if not self._permanent:
60
+ self._frozen = None
61
+ return self._permanent
62
+
63
+ def clear_service(self):
64
+ """Clear the possibly defined permanent service."""
65
+ self._frozen = None
66
+
67
+ def status(self, update=None):
68
+ """Return or update current active status."""
69
+ if update is not None:
70
+ self._active = bool(update)
71
+ return self._active
72
+
73
+ def on(self):
74
+ """Switch on this action."""
75
+ self._active = True
76
+ return self._active
77
+
78
+ def off(self):
79
+ """Switch off this action."""
80
+ self._active = False
81
+ return self._active
82
+
83
+ def info(self):
84
+ """Informative string (may serve debugging purposes)."""
85
+ return "{} Action {} (kind={})".format(
86
+ "ON " if self.status() else "OFF",
87
+ self.__class__.__name__,
88
+ self.kind,
89
+ )
90
+
91
+ def service_kind(self, **kw):
92
+ """Actual service kind name to be used for footprint evaluation."""
93
+ return self.service
94
+
95
+ def service_info(self, **kw):
96
+ """On the fly remapping of the expected footprint."""
97
+ info = dict(kw)
98
+ info.setdefault("kind", self.service_kind(**kw))
99
+ return info
100
+
101
+ def get_actual_service(self, **kw):
102
+ """Return the service instance determined by the actual description."""
103
+ info = self.service_info(**kw)
104
+ if self.permanent():
105
+ if self._frozen is None:
106
+ self._frozen = footprints.proxy.services.default(**info)
107
+ a_service = self._frozen
108
+ else:
109
+ a_service = footprints.proxy.service(**info)
110
+ return a_service
111
+
112
+ def get_active_service(self, **kw):
113
+ """Return the actual service according to active status and user authorizations."""
114
+ a_service = None
115
+ if self.active:
116
+ a_service = self.get_actual_service(**kw)
117
+ if a_service is None:
118
+ logger.warning(
119
+ "Could not find any service for action %s", self.kind
120
+ )
121
+ else:
122
+ logger.warning("Action %s is not active", self.kind)
123
+ return a_service
124
+
125
+ def execute(self, *args, **kw):
126
+ """Generic method to perform the action through a service."""
127
+ rc = None
128
+ service = self.get_active_service(**kw)
129
+ if service:
130
+ rc = service(*args)
131
+ return rc
132
+
133
+
134
+ class TunableAction(Action):
135
+ """An Action that may be tuned
136
+
137
+ - may have it's own section in the target configuration files
138
+ - accepts the syntax `ad.action_tune(key=value)` (which has priority)
139
+ """
140
+
141
+ def __init__(self, configuration=None, **kwargs):
142
+ super().__init__(**kwargs)
143
+ self._tuning = dict()
144
+ self._conf_section = configuration
145
+ self._conf_dict = None
146
+
147
+ @property
148
+ def _shtarget(self):
149
+ """Warning: this may be a `vortex.syntax.stdattrs.DelayedInit` object
150
+ during Vortex initialization and may not have a `sections()` method
151
+ nor a `config` property.
152
+ """
153
+ return sessions.current().sh.default_target
154
+
155
+ @property
156
+ def _conf_items(self):
157
+ """Check and return the configuration: a section in the target-xxx.ini file.
158
+
159
+ If the configuration is None, an attempt is made to use the Action's kind.
160
+ Don't use before Vortex initialization is done (see `_shtarget`).
161
+ """
162
+ if self._conf_dict is None:
163
+ if self._conf_section is None:
164
+ if self.kind in self._shtarget.sections():
165
+ self._conf_section = self.kind
166
+ else:
167
+ if self._conf_section not in self._shtarget.sections():
168
+ raise KeyError(
169
+ 'No section "{}" in "{}"'.format(
170
+ self._conf_section, self._shtarget.config.file
171
+ )
172
+ )
173
+ if self._conf_section is None:
174
+ self._conf_dict = dict()
175
+ else:
176
+ self._conf_dict = self._shtarget.items(self._conf_section)
177
+ return self._conf_dict
178
+
179
+ def service_info(self, **kw):
180
+ for k, v in self._get_config_dict().items():
181
+ kw.setdefault(k, v)
182
+ return super().service_info(**kw)
183
+
184
+ def tune(self, section=None, **kw):
185
+ """Add options to override the .ini file configuration.
186
+
187
+ ``section`` is a specific section name, or ``None`` for all.
188
+ """
189
+ if section is None or section == self._conf_section:
190
+ self._tuning.update(kw)
191
+
192
+ def _get_config_dict(self):
193
+ final_dict = dict()
194
+ final_dict.update(self._conf_items)
195
+ final_dict.update(self._tuning)
196
+ return final_dict
197
+
198
+ def info(self):
199
+ """Informative string (may serve debugging purposes)."""
200
+ s = super().info() + " - tunable\n"
201
+ mix = dict()
202
+ mix.update(self._conf_items)
203
+ mix.update(self._tuning)
204
+ prt = dict()
205
+ for k, v in mix.items():
206
+ if k in self._tuning:
207
+ prt["++ " + k] = "{} (was: {})".format(
208
+ v,
209
+ str(self._conf_items[k])
210
+ if k in self._conf_items
211
+ else "<not set>",
212
+ )
213
+ else:
214
+ prt[" " + k] = v
215
+ if self._conf_section is not None:
216
+ s += " " * 4 + "configuration: " + self._conf_section + "\n"
217
+ s += dict_as_str(prt, prefix=4)
218
+ return s.strip()
219
+
220
+ def getx(self, key, *args, **kw):
221
+ """Shortcut to access the configuration overridden by the tuning."""
222
+ if key in self._tuning:
223
+ return self._tuning[key]
224
+
225
+ if self._conf_section is not None:
226
+ return self._shtarget.getx(
227
+ key=self._conf_section + ":" + key, *args, **kw
228
+ )
229
+
230
+ if "default" in kw:
231
+ return kw["default"]
232
+
233
+ raise KeyError(
234
+ 'The "{:s}" entry was not found in any configuration'.format(key)
235
+ )
236
+
237
+
238
+ class SendMail(Action):
239
+ """
240
+ Class responsible for sending emails.
241
+ """
242
+
243
+ def __init__(
244
+ self, kind="mail", service="sendmail", active=True, quoteprintable=True
245
+ ):
246
+ super().__init__(kind=kind, active=active, service=service)
247
+ if quoteprintable:
248
+ from email import charset
249
+
250
+ charset.add_charset("utf-8", charset.QP, charset.QP, "utf-8")
251
+
252
+
253
+ class TemplatedMail(TunableAction):
254
+ """Abstract class to end email from a given template.
255
+
256
+ Do not use directly !
257
+ """
258
+
259
+ def __init__(
260
+ self,
261
+ catalog,
262
+ kind="templatedmail",
263
+ service="templatedmail",
264
+ active=True,
265
+ inputs_charset=None,
266
+ ):
267
+ super().__init__(
268
+ configuration=None, kind=kind, active=active, service=service
269
+ )
270
+ self.inputs_charset = inputs_charset
271
+
272
+ def service_info(self, **kw):
273
+ """Kindly propose the permanent directory and catalog to the final service"""
274
+ kw.setdefault("catalog", self.catalog)
275
+ kw.setdefault("inputs_charset", self.inputs_charset)
276
+ return super().service_info(**kw)
277
+
278
+ def execute(self, *args, **kw):
279
+ """
280
+ Perform the action through a service. Extraneous arguments (not included
281
+ in the footprint) are collected and explicitely transmitted to the service
282
+ in a dictionary.
283
+ """
284
+ rc = None
285
+ service = self.get_active_service(**kw)
286
+ if service:
287
+ options = {
288
+ k: v
289
+ for k, v in kw.items()
290
+ if k not in service.footprint_attributes
291
+ }
292
+ rc = service(options)
293
+ return rc
294
+
295
+
296
+ class Report(TunableAction):
297
+ """
298
+ Class responsible for sending reports.
299
+ """
300
+
301
+ def __init__(self, kind="report", service="sendreport", active=True):
302
+ super().__init__(kind=kind, active=active, service=service)
303
+
304
+
305
+ class SSH(Action):
306
+ """
307
+ Class responsible for sending commands to an SSH proxy.
308
+ """
309
+
310
+ def __init__(self, kind="ssh", service="ssh", active=True):
311
+ super().__init__(kind=kind, active=active, service=service)
312
+
313
+
314
+ class AskJeeves(TunableAction):
315
+ """
316
+ Class responsible for posting requests to Jeeves daemon.
317
+ """
318
+
319
+ def __init__(self, kind="jeeves", service="askjeeves", active=True):
320
+ super().__init__(
321
+ configuration=None, kind=kind, active=active, service=service
322
+ )
323
+
324
+ def execute(self, *args, **kw):
325
+ """Generic method to perform the action through a service."""
326
+ rc = None
327
+ if "kind" in kw:
328
+ kw["fwd_kind"] = kw.pop("kind")
329
+ service = self.get_active_service(**kw)
330
+ if service:
331
+ talk = {
332
+ k: v
333
+ for k, v in kw.items()
334
+ if k not in service.footprint_attributes
335
+ }
336
+ rc = service(talk)
337
+ return rc
338
+
339
+
340
+ class Prompt(Action):
341
+ """
342
+ Fake action that could be used for any real action.
343
+ """
344
+
345
+ def __init__(self, kind="prompt", service="prompt", active=True):
346
+ super().__init__(kind=kind, active=active, service=service)
347
+
348
+ def execute(self, *args, **kw):
349
+ """Do nothing but prompt the actual arguments."""
350
+ # kind could be unintentionally given, force it back
351
+ kw["kind"] = self.kind
352
+ service = self.get_active_service(**kw)
353
+ rc = False
354
+ if service:
355
+ options = {
356
+ k: v
357
+ for k, v in kw.items()
358
+ if k not in service.footprint_attributes
359
+ }
360
+ rc = service(options)
361
+ return rc
362
+
363
+ def foo(self, *args, **kw):
364
+ """Yet an other foo method."""
365
+ print("#FOO", self.kind, "/ args:", args, "/ kw:", kw)
366
+ return True
367
+
368
+
369
+ class FlowSchedulerGateway(Action):
370
+ """
371
+ Send a child command to any ECMWF's workfow scheduler.
372
+ """
373
+
374
+ _KNOWN_CMD = dict(
375
+ sms=[
376
+ "abort",
377
+ "complete",
378
+ "event",
379
+ "init",
380
+ "label",
381
+ "meter",
382
+ "msg",
383
+ "variable",
384
+ "fix",
385
+ ],
386
+ ecflow=["abort", "complete", "event", "init", "label", "meter", "msg"],
387
+ )
388
+
389
+ def __init__(self, kind="flow", service=None, active=True, permanent=True):
390
+ """
391
+ The `service` attribute must be specified (it can be either sms or ecflow).
392
+ """
393
+ if service is None:
394
+ raise ValueError("The service name must be provided")
395
+ super().__init__(
396
+ kind=kind, active=active, service=service, permanent=permanent
397
+ )
398
+
399
+ def gateway(self, *args, **kw):
400
+ """Ask the Scheduler to run any (but known) command."""
401
+ rc = None
402
+ service = self.get_active_service(**kw)
403
+ if service and self._schedcmd is not None:
404
+ kwbis = {k: v for k, v in kw.items() if k in ("critical",)}
405
+ rc = getattr(service, self._schedcmd)(*args, **kwbis)
406
+ self._schedcmd = None
407
+ return rc
408
+
409
+ def __getattr__(self, attr):
410
+ if attr.startswith("_"):
411
+ raise AttributeError
412
+ if attr in (
413
+ [
414
+ "conf",
415
+ "info",
416
+ "clear",
417
+ "mute",
418
+ "play",
419
+ "path",
420
+ ]
421
+ + self._KNOWN_CMD[self.service]
422
+ ):
423
+ self._schedcmd = attr
424
+ return self.gateway
425
+ else:
426
+ self._schedcmd = None
427
+ return None
428
+
429
+
430
+ class SmsGateway(FlowSchedulerGateway):
431
+ """Send a child command to an SMS server."""
432
+
433
+ def __init__(self, kind="sms", service="sms", active=True, permanent=True):
434
+ super().__init__(
435
+ kind=kind, active=active, service=service, permanent=permanent
436
+ )
437
+
438
+
439
+ class EcflowGateway(FlowSchedulerGateway):
440
+ """Send a child command to an Ecflow server."""
441
+
442
+ def __init__(
443
+ self, kind="ecflow", service="ecflow", active=True, permanent=True
444
+ ):
445
+ super().__init__(
446
+ kind=kind, active=active, service=service, permanent=permanent
447
+ )
448
+
449
+
450
+ class SpooledActions:
451
+ """
452
+ Delayed action to be processed.
453
+ """
454
+
455
+ def __init__(self, kind=None, method=None, actions=None):
456
+ """Store effective action and method to be processed."""
457
+ self._kind = kind
458
+ self._method = method
459
+ self._actions = actions
460
+
461
+ @property
462
+ def kind(self):
463
+ return self._kind
464
+
465
+ @property
466
+ def method(self):
467
+ return self._method
468
+
469
+ @property
470
+ def actions(self):
471
+ return self._actions[:]
472
+
473
+ def __call__(self, *args, **kw):
474
+ return self.process(*args, **kw)
475
+
476
+ def process(self, *args, **kw):
477
+ """Process the actual method for all action candidates of a given kind."""
478
+ rc = list()
479
+ for item in self.actions:
480
+ xx = getattr(item, self.method, None)
481
+ if xx is not None:
482
+ rc.append(xx(*args, **kw))
483
+ else:
484
+ rc.append(None)
485
+ return rc
486
+
487
+
488
+ class Dispatcher(bronx.stdtypes.catalog.Catalog):
489
+ """
490
+ Central office for dispatching actions.
491
+ """
492
+
493
+ def __init__(self, **kw):
494
+ logger.debug("Action dispatcher init %s", self)
495
+ super().__init__(**kw)
496
+
497
+ @property
498
+ def actions(self):
499
+ """A set of kind names of actual actions registered in that Dispatcher."""
500
+ return {x.kind for x in self.items()}
501
+
502
+ def candidates(self, kind):
503
+ """Return a selection of the dispatcher's items with the specified ``kind``."""
504
+ return [x for x in self.items() if x.kind == kind]
505
+
506
+ def discard_kind(self, kind):
507
+ """A shortcut to discard from the dispatcher any item with the specified ``kind``."""
508
+ for item in self:
509
+ if item.kind == kind:
510
+ self.discard(item)
511
+
512
+ def __getattr__(self, attr):
513
+ if attr.startswith("_"):
514
+ raise AttributeError
515
+ a_kind, u_sep, a_method = attr.partition("_")
516
+ if not a_method:
517
+ a_method = "execute"
518
+ return SpooledActions(a_kind, a_method, self.candidates(a_kind))
519
+
520
+
521
+ #: Default action dispatcher... containing an anonymous SendMail action
522
+ actiond = Dispatcher()
523
+ actiond.add(SendMail(), Report(), AskJeeves(), SSH(), Prompt())