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