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
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Advanced tools that deals with delayed actions.
|
|
3
|
+
|
|
4
|
+
The entry point to the delayed action mechanism is the :class:`PrivateDelayedActionsHub`
|
|
5
|
+
class. One :class:`PrivateDelayedActionsHub` object is created in each
|
|
6
|
+
:class:`~vortex.dataflow.contexts.Context`
|
|
7
|
+
(see :meth:`vortex.dataflow.contexts.Context.delayedactions_hub`). When working
|
|
8
|
+
with delayed actions, you must always use the :class:`PrivateDelayedActionsHub`
|
|
9
|
+
object associated with the current active :class:`~vortex.dataflow.contexts.Context`.
|
|
10
|
+
|
|
11
|
+
Example::
|
|
12
|
+
|
|
13
|
+
# Get the DelayedActionsHub object from the current context
|
|
14
|
+
|
|
15
|
+
>>> from vortex import sessions
|
|
16
|
+
|
|
17
|
+
>>> cur_da_hub = sessions.current().context.delayedactions_hub
|
|
18
|
+
|
|
19
|
+
# Instruct Vortex to sleep (in parallel !)
|
|
20
|
+
|
|
21
|
+
>>> dactions = [cur_da_hub.register(n, kind='sleep') for n in (1, 3, 2)]
|
|
22
|
+
|
|
23
|
+
# How does it look like in the logs for the first action ?
|
|
24
|
+
|
|
25
|
+
>>> cur_da_hub.actionhistory(dactions[0]) # doctest:+ELLIPSIS
|
|
26
|
+
<....DemoSleepDelayedActionHandler object at 0x...> says:
|
|
27
|
+
[...][...] : NEW id=sleeper_action_...: request=1
|
|
28
|
+
[...][...] : UPD id=sleeper_action_...: result=<multiprocessing.pool.ApplyResult object at 0x...> ...
|
|
29
|
+
|
|
30
|
+
# Wait for all the sleepers to wake up... It would be possible to do that
|
|
31
|
+
# explicitly by calling the ``finalise`` method, but ``retrieve`` implicitly
|
|
32
|
+
# calls ``finalise`` so we don't bother !
|
|
33
|
+
|
|
34
|
+
>>> for daction in dactions:
|
|
35
|
+
... action = cur_da_hub.retrieve(daction, bareobject=True)
|
|
36
|
+
... if action.status == d_action_status.done:
|
|
37
|
+
... print('I slept for {0.request:d} seconds and It was good :-)'.format(action))
|
|
38
|
+
... else:
|
|
39
|
+
... print('I failed to get asleep for {0.request:d} seconds :-('.format(action))
|
|
40
|
+
I slept for 1 seconds and It was good :-)
|
|
41
|
+
I slept for 3 seconds and It was good :-)
|
|
42
|
+
I slept for 2 seconds and It was good :-)
|
|
43
|
+
|
|
44
|
+
# And now, what are the log saying ?
|
|
45
|
+
|
|
46
|
+
>>> cur_da_hub.actionhistory(dactions[0]) # doctest:+ELLIPSIS
|
|
47
|
+
<....DemoSleepDelayedActionHandler object at 0x...> says:
|
|
48
|
+
[...][...] : NEW id=sleeper_action_0000000000000001: request=1
|
|
49
|
+
[...][...] : UPD id=sleeper_action_0000000000000001: result=<multiprocessing.pool.ApplyResult object at 0x...> ...
|
|
50
|
+
[...][...] : UPD id=sleeper_action_0000000000000001: status=done (instead of: void)
|
|
51
|
+
[...][...] : USED id=sleeper_action_0000000000000001
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from collections import namedtuple, defaultdict
|
|
56
|
+
import multiprocessing
|
|
57
|
+
import os
|
|
58
|
+
import tempfile
|
|
59
|
+
import time
|
|
60
|
+
|
|
61
|
+
from bronx.fancies import loggers
|
|
62
|
+
from bronx.fancies.dump import lightdump
|
|
63
|
+
from bronx.stdtypes.history import PrivateHistory
|
|
64
|
+
from bronx.patterns import getbytag, observer
|
|
65
|
+
|
|
66
|
+
import footprints
|
|
67
|
+
from footprints import proxy as fpx
|
|
68
|
+
|
|
69
|
+
from vortex.tools.systems import OSExtended
|
|
70
|
+
|
|
71
|
+
#: No automatic export
|
|
72
|
+
__all__ = []
|
|
73
|
+
|
|
74
|
+
logger = loggers.getLogger(__name__)
|
|
75
|
+
|
|
76
|
+
#: Definition of a named tuple DelayedActionStatusTuple
|
|
77
|
+
DelayedActionStatusTuple = namedtuple('DelayedActionStatusTuple',
|
|
78
|
+
['void', 'failed', 'done', 'unclear'])
|
|
79
|
+
|
|
80
|
+
#: Predefined DelayedActionStatus values (void=Not ready yet,
|
|
81
|
+
# failed=processed but KO,
|
|
82
|
+
# done=processed and OK,
|
|
83
|
+
# unclear=processed but cannot tell whether it is KO or OK)
|
|
84
|
+
d_action_status = DelayedActionStatusTuple(void=0, failed=-1, done=1, unclear=-2)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# Module Interface
|
|
88
|
+
def get_hub(**kw):
|
|
89
|
+
"""Return the actual :class:`DelayedActionsHub` object matching the *tag* (or create one)."""
|
|
90
|
+
return DelayedActionsHub(**kw)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class DelayedAction:
|
|
94
|
+
"""Simple object describing one action to be performed."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, obsboard, r_id, request):
|
|
97
|
+
"""
|
|
98
|
+
:param SecludedObserverBoard obsboard: The Observer board that will be used
|
|
99
|
+
to publish the results.
|
|
100
|
+
:param r_id: Any kind of ID that uniquely identifies the delayed action
|
|
101
|
+
:param request: Any kind of data that describes the action to be performed
|
|
102
|
+
"""
|
|
103
|
+
self._obsboard = obsboard
|
|
104
|
+
self._id = r_id
|
|
105
|
+
self._request = request
|
|
106
|
+
self._status = d_action_status.void
|
|
107
|
+
self._result = None
|
|
108
|
+
self._obsboard.notify_new(self, dict())
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def id(self):
|
|
112
|
+
"""The delayed action ID."""
|
|
113
|
+
return self._id
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def request(self):
|
|
117
|
+
"""The data describing the action."""
|
|
118
|
+
return self._request
|
|
119
|
+
|
|
120
|
+
def _set_status(self, value):
|
|
121
|
+
oldres = self.statustext
|
|
122
|
+
self._status = value
|
|
123
|
+
self._obsboard.notify_upd(self, info=dict(changed='status', queryproxy='statustext',
|
|
124
|
+
prev=oldres))
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def status(self):
|
|
128
|
+
"""The delayed action status (see :data:`d_action_status` for possible values)."""
|
|
129
|
+
return self._status
|
|
130
|
+
|
|
131
|
+
def _get_result(self):
|
|
132
|
+
return self._result
|
|
133
|
+
|
|
134
|
+
def _set_result(self, result):
|
|
135
|
+
oldres = self._result
|
|
136
|
+
self._result = result
|
|
137
|
+
self._obsboard.notify_upd(self, info=dict(changed='result', prev=oldres))
|
|
138
|
+
|
|
139
|
+
result = property(_get_result, _set_result,
|
|
140
|
+
doc="Where to find the delayed action result.")
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def statustext(self):
|
|
144
|
+
"""A string that descibres the delayed action status."""
|
|
145
|
+
for k, v in d_action_status._asdict().items():
|
|
146
|
+
if self._status == v:
|
|
147
|
+
return k
|
|
148
|
+
logger.warning('What is this idiotic status (%s) ???', self.status)
|
|
149
|
+
return str(self.status)
|
|
150
|
+
|
|
151
|
+
def mark_as_failed(self):
|
|
152
|
+
"""Change the status to ``failed``."""
|
|
153
|
+
logger.info('Marking early-get %s as failed (request=%s)',
|
|
154
|
+
self.id, self.request)
|
|
155
|
+
self._set_status(d_action_status.failed)
|
|
156
|
+
|
|
157
|
+
def mark_as_done(self):
|
|
158
|
+
"""Change the status to ``done``."""
|
|
159
|
+
logger.debug('Marking early-get %s as done (request=%s)',
|
|
160
|
+
self.id, self.request)
|
|
161
|
+
self._set_status(d_action_status.done)
|
|
162
|
+
|
|
163
|
+
def mark_as_unclear(self):
|
|
164
|
+
"""Change the status to ``unclear``."""
|
|
165
|
+
logger.info('Marking early-get %s as unclear/unconclusive (request=%s)',
|
|
166
|
+
self.id, self.request)
|
|
167
|
+
self._set_status(d_action_status.unclear)
|
|
168
|
+
|
|
169
|
+
def __str__(self):
|
|
170
|
+
return 'id={0._id}: {0.statustext:6s} result={0.result!s}'.format(self)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class AbstractDelayedActionsHandler(footprints.FootprintBase, observer.Observer):
|
|
174
|
+
"""Abstract class that handles a bunch of similar delayed actions."""
|
|
175
|
+
|
|
176
|
+
_abstract = True
|
|
177
|
+
_collector = ('delayedactionshandler',)
|
|
178
|
+
_footprint = dict(
|
|
179
|
+
info = "Abstract class that deal with delayed actions.",
|
|
180
|
+
attr = dict(
|
|
181
|
+
system = dict(
|
|
182
|
+
info = "The current system object",
|
|
183
|
+
type = OSExtended
|
|
184
|
+
),
|
|
185
|
+
observerboard = dict(
|
|
186
|
+
info = 'The observer board where delayed actions updates are published.',
|
|
187
|
+
type = observer.SecludedObserverBoard,
|
|
188
|
+
),
|
|
189
|
+
stagedir = dict(
|
|
190
|
+
info = "The temporary directory (if need be)"
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def __init__(self, *args, **kw):
|
|
196
|
+
super().__init__(*args, **kw)
|
|
197
|
+
self._resultsmap = dict()
|
|
198
|
+
self._history = PrivateHistory(timer=True)
|
|
199
|
+
self.observerboard.register(self)
|
|
200
|
+
self._custom_init()
|
|
201
|
+
|
|
202
|
+
def destroy(self):
|
|
203
|
+
"""Cleanup everything... (useful when multiprocessing is used)"""
|
|
204
|
+
self._resultsmap = None
|
|
205
|
+
self._history = None
|
|
206
|
+
|
|
207
|
+
def _custom_init(self):
|
|
208
|
+
"""This method may be specialised in actual DelayedActionsHandler classes."""
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
def newobsitem(self, item, info): # @UnusedVariable
|
|
212
|
+
"""To get informed when a new :class:`DelayedAction` object is created."""
|
|
213
|
+
if item.id in self._resultsmap:
|
|
214
|
+
self._history.append('NEW ', 'id={0.id!s}: request={0.request!s}'.format(item))
|
|
215
|
+
|
|
216
|
+
def updobsitem(self, item, info):
|
|
217
|
+
"""To get informed when a new :class:`DelayedAction` object is updates."""
|
|
218
|
+
if item.id in self._resultsmap:
|
|
219
|
+
what = info['changed']
|
|
220
|
+
newval = getattr(item, info.get('queryproxy', what))
|
|
221
|
+
oldval = info['prev']
|
|
222
|
+
self._history.append('UPD ',
|
|
223
|
+
'id={0.id!s}: {1:s}={2!s} (instead of: {3!s})'
|
|
224
|
+
.format(item, what, newval, oldval))
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def history(self):
|
|
228
|
+
"""The :class:`PrivateHistory` object where all of this object's activity is logged."""
|
|
229
|
+
return self._history
|
|
230
|
+
|
|
231
|
+
def grephistory(self, r_id):
|
|
232
|
+
"""Return the log lines matching the **r_id** delayed action ID."""
|
|
233
|
+
return self.history.grep('id={!s}'.format(r_id))
|
|
234
|
+
|
|
235
|
+
def showhistory(self, r_id):
|
|
236
|
+
"""Print the log lines matching the **r_id** delayed action ID."""
|
|
237
|
+
return self.history.showgrep('id={!s}'.format(r_id))
|
|
238
|
+
|
|
239
|
+
def __contains__(self, r_id):
|
|
240
|
+
return r_id in self._resultsmap
|
|
241
|
+
|
|
242
|
+
def dispence_resultid(self):
|
|
243
|
+
"""Return a unique ID that will identify a new :class:`DelayedAction` object."""
|
|
244
|
+
raise NotImplementedError()
|
|
245
|
+
|
|
246
|
+
def _create_delayed_action(self, r_id, request):
|
|
247
|
+
"""Create a :class:`DelayedAction` object given **r_id** and **request**."""
|
|
248
|
+
return DelayedAction(self.observerboard, r_id, request)
|
|
249
|
+
|
|
250
|
+
def register(self, request):
|
|
251
|
+
"""Create a new :class:`DelayedAction` object from a user's **request**."""
|
|
252
|
+
r_id = self.dispence_resultid()
|
|
253
|
+
self._resultsmap[r_id] = None # For newobitem to work...
|
|
254
|
+
d_action = self._create_delayed_action(r_id, request)
|
|
255
|
+
self._resultsmap[r_id] = d_action
|
|
256
|
+
self._custom_register(d_action)
|
|
257
|
+
return r_id
|
|
258
|
+
|
|
259
|
+
def _custom_register(self, action):
|
|
260
|
+
"""Any action to be performed each time a new delayed action is registered."""
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def dirty(self):
|
|
265
|
+
"""Is there any of the object's delayed actions that needs finalising ?"""
|
|
266
|
+
return any([a.status == d_action_status.void for a in self._resultsmap.values()])
|
|
267
|
+
|
|
268
|
+
def finalise(self, *r_ids):
|
|
269
|
+
"""Given a **r_ids** list of delayed action IDs, wait upon actions completion."""
|
|
270
|
+
raise NotImplementedError()
|
|
271
|
+
|
|
272
|
+
def retrieve(self, r_id, bareobject=False):
|
|
273
|
+
"""Given a **r_id** delayed action ID, returns the corresponding result.
|
|
274
|
+
|
|
275
|
+
If need be, :meth:`finalise` is called.
|
|
276
|
+
"""
|
|
277
|
+
action = self._resultsmap[r_id]
|
|
278
|
+
try:
|
|
279
|
+
if action.status == d_action_status.void:
|
|
280
|
+
self.finalise(r_id)
|
|
281
|
+
assert action.status != d_action_status.void, 'Finalise does not seem to work.'
|
|
282
|
+
finally:
|
|
283
|
+
del self._resultsmap[r_id]
|
|
284
|
+
self._history.append('USED', 'id={!s}'.format(action.id))
|
|
285
|
+
if bareobject:
|
|
286
|
+
return action
|
|
287
|
+
else:
|
|
288
|
+
if action.status == d_action_status.done:
|
|
289
|
+
return action.result
|
|
290
|
+
else:
|
|
291
|
+
return False
|
|
292
|
+
|
|
293
|
+
def __str__(self):
|
|
294
|
+
return self.describe(fulldump=False)
|
|
295
|
+
|
|
296
|
+
def describe(self, fulldump=False):
|
|
297
|
+
"""Print the object's characteristics and content."""
|
|
298
|
+
res = 'DelayedActionsHandler object of class: {:s}\n'.format(self.__class__)
|
|
299
|
+
for k, v in self.footprint_as_shallow_dict().items():
|
|
300
|
+
res += ' * {:s}: {!s}\n'.format(k, v)
|
|
301
|
+
if fulldump:
|
|
302
|
+
res += '\n * Todo list (i.e still to be processed):\n\n'
|
|
303
|
+
res += '\n'.join(['{:48s}:\n request: {!s}'.format(r_id, a.request)
|
|
304
|
+
for r_id, a in self._resultsmap.items()
|
|
305
|
+
if a.status == d_action_status.void])
|
|
306
|
+
res += '\n * Done (i.e the delayed action succeeded):\n\n'
|
|
307
|
+
res += '\n'.join(['{:48s}:\n request: {!s}'.format(r_id, a.request)
|
|
308
|
+
for r_id, a in self._resultsmap.items()
|
|
309
|
+
if a.status == d_action_status.done])
|
|
310
|
+
res += '\n * Failed (i.e the delayed action failed):\n\n'
|
|
311
|
+
res += '\n'.join(['{:48s}:\n request: {!s}'.format(r_id, a.request)
|
|
312
|
+
for r_id, a in self._resultsmap.items()
|
|
313
|
+
if a.status == d_action_status.failed])
|
|
314
|
+
res += '\n * Unclear (i.e processed but the result is unclear):\n\n'
|
|
315
|
+
res += '\n'.join(['{:48s}:\n request: {!s}'.format(r_id, a.request)
|
|
316
|
+
for r_id, a in self._resultsmap.items()
|
|
317
|
+
if a.status == d_action_status.unclear])
|
|
318
|
+
return res
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class AbstractFileBasedDelayedActionsHandler(AbstractDelayedActionsHandler):
|
|
322
|
+
"""
|
|
323
|
+
A specialised version of :class:`AbstractDelayedActionsHandler` where
|
|
324
|
+
a unique file (created in the ``stagedir``) is associated with each of the
|
|
325
|
+
delayed action.
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
_abstract = True
|
|
329
|
+
|
|
330
|
+
@property
|
|
331
|
+
def resultid_stamp(self):
|
|
332
|
+
"""Some kind of string that identifies the present object."""
|
|
333
|
+
raise NotImplementedError()
|
|
334
|
+
|
|
335
|
+
def dispence_resultid(self):
|
|
336
|
+
"""Return a unique ID that will identify a new :class:`DelayedAction` object."""
|
|
337
|
+
t_temp = tempfile.mkstemp(prefix='{:s}_{:d}'.format(self.resultid_stamp,
|
|
338
|
+
self.system.getpid()),
|
|
339
|
+
dir=self.stagedir)
|
|
340
|
+
os.close(t_temp[0])
|
|
341
|
+
return self.system.path.basename(t_temp[1])
|
|
342
|
+
|
|
343
|
+
def _create_delayed_action(self, r_id, request):
|
|
344
|
+
"""Create a :class:`DelayedAction` object given **r_id** and **request**."""
|
|
345
|
+
d_action = DelayedAction(self.observerboard, r_id, request)
|
|
346
|
+
d_action.result = self.system.path.join(self.stagedir, r_id)
|
|
347
|
+
return d_action
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def demo_sleeper_function(seconds):
|
|
351
|
+
"""Sleep for a while (demo)."""
|
|
352
|
+
time.sleep(seconds)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class DemoSleepDelayedActionHandler(AbstractDelayedActionsHandler):
|
|
356
|
+
"""A Sleeper delayed action handler (Demonstration purposes)."""
|
|
357
|
+
|
|
358
|
+
_footprint = dict(
|
|
359
|
+
info = "Demonstration purposes (sleep for a while).",
|
|
360
|
+
attr = dict(
|
|
361
|
+
kind = dict(
|
|
362
|
+
values = ['sleep', ],
|
|
363
|
+
),
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
def dispence_resultid(self):
|
|
368
|
+
"""Return a unique ID that will identify a new :class:`DelayedAction` object."""
|
|
369
|
+
self._counter += 1
|
|
370
|
+
return 'sleeper_action_{:016d}'.format(self._counter)
|
|
371
|
+
|
|
372
|
+
def _custom_init(self):
|
|
373
|
+
"""Create the multiprocessing pool."""
|
|
374
|
+
self._ppool = multiprocessing.Pool(processes=2)
|
|
375
|
+
self._counter = 0
|
|
376
|
+
|
|
377
|
+
def destroy(self):
|
|
378
|
+
"""Destry the multiprocessing pool before leaving."""
|
|
379
|
+
self._ppool.close()
|
|
380
|
+
self._ppool.terminate()
|
|
381
|
+
self._ppool = None
|
|
382
|
+
super().destroy()
|
|
383
|
+
|
|
384
|
+
def _create_delayed_action(self, r_id, request):
|
|
385
|
+
"""Start the asynchronous processing."""
|
|
386
|
+
daction = DelayedAction(self.observerboard, r_id, request)
|
|
387
|
+
daction.result = self._ppool.apply_async(demo_sleeper_function, (request, ))
|
|
388
|
+
return daction
|
|
389
|
+
|
|
390
|
+
def finalise(self, *r_ids):
|
|
391
|
+
"""Wait until completion."""
|
|
392
|
+
for r_id in r_ids:
|
|
393
|
+
action = self._resultsmap[r_id]
|
|
394
|
+
action.result.wait()
|
|
395
|
+
if action.result.successful():
|
|
396
|
+
action.mark_as_done()
|
|
397
|
+
else:
|
|
398
|
+
action.mark_as_failed()
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class AbstractFtpArchiveDelayedGetHandler(AbstractFileBasedDelayedActionsHandler):
|
|
402
|
+
"""Includes some FTP related methods"""
|
|
403
|
+
|
|
404
|
+
_abstract = True
|
|
405
|
+
_footprint = dict(
|
|
406
|
+
info = "Fetch multiple files using an FTP archive.",
|
|
407
|
+
attr = dict(
|
|
408
|
+
kind = dict(
|
|
409
|
+
values = ['archive', ],
|
|
410
|
+
),
|
|
411
|
+
storage = dict(
|
|
412
|
+
),
|
|
413
|
+
goal = dict(
|
|
414
|
+
values = ['get', ]
|
|
415
|
+
),
|
|
416
|
+
tube = dict(
|
|
417
|
+
values = ['ftp', ],
|
|
418
|
+
),
|
|
419
|
+
raw = dict(
|
|
420
|
+
type = bool,
|
|
421
|
+
optional = True,
|
|
422
|
+
default = False,
|
|
423
|
+
),
|
|
424
|
+
logname = dict(
|
|
425
|
+
optional = True
|
|
426
|
+
)
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
@property
|
|
431
|
+
def resultid_stamp(self):
|
|
432
|
+
bangfmt = '{0.logname:s}@{0.storage:s}' if self.logname else '{0.storage:s}'
|
|
433
|
+
return ('rawftget_' + bangfmt).format(self)
|
|
434
|
+
|
|
435
|
+
def register(self, request):
|
|
436
|
+
"""Create a new :class:`DelayedAction` object from a user's **request**."""
|
|
437
|
+
assert isinstance(request, (tuple, list)) and len(request) == 2, \
|
|
438
|
+
'Request needs to be a two element tuple or list (location, format)'
|
|
439
|
+
# Check for duplicated entries...
|
|
440
|
+
target = request[0]
|
|
441
|
+
for v in self._resultsmap.values():
|
|
442
|
+
if target == v.request[0]:
|
|
443
|
+
return None
|
|
444
|
+
# Ok, let's proceed...
|
|
445
|
+
return super().register(request)
|
|
446
|
+
|
|
447
|
+
@property
|
|
448
|
+
def _ftp_hostinfos(self):
|
|
449
|
+
"""Return the FTP hostname end port number."""
|
|
450
|
+
s_storage = self.storage.split(':', 1)
|
|
451
|
+
hostname = s_storage[0]
|
|
452
|
+
port = None
|
|
453
|
+
if len(s_storage) > 1:
|
|
454
|
+
try:
|
|
455
|
+
port = int(s_storage[1])
|
|
456
|
+
except ValueError:
|
|
457
|
+
logger.error('Invalid port number < %s >. Ignoring it', s_storage[1])
|
|
458
|
+
return hostname, port
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class RawFtpDelayedGetHandler(AbstractFtpArchiveDelayedGetHandler):
|
|
462
|
+
"""
|
|
463
|
+
When FtServ is used, accumulate "GET" requests for several files and fetch
|
|
464
|
+
them during a unique ``ftget`` system call.
|
|
465
|
+
|
|
466
|
+
:note: The *request* needs to be a two-elements tuple where the first element
|
|
467
|
+
is the path to the file that shoudl be fetched and the second element
|
|
468
|
+
the file format.
|
|
469
|
+
:note: The **result** returned by the :meth:`retrieve` method will be the
|
|
470
|
+
path to the temporary file where the resource has been fetched.
|
|
471
|
+
"""
|
|
472
|
+
|
|
473
|
+
_footprint = dict(
|
|
474
|
+
info = "Fetch multiple files using FtServ.",
|
|
475
|
+
attr = dict(
|
|
476
|
+
raw = dict(
|
|
477
|
+
optional = False,
|
|
478
|
+
values = [True, ],
|
|
479
|
+
),
|
|
480
|
+
)
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
def finalise(self, *r_ids): # @UnusedVariable
|
|
484
|
+
"""Given a **r_ids** list of delayed action IDs, wait upon actions completion."""
|
|
485
|
+
todo = defaultdict(list)
|
|
486
|
+
for k, v in self._resultsmap.items():
|
|
487
|
+
if v.status == d_action_status.void:
|
|
488
|
+
a_fmt = (v.request[1]
|
|
489
|
+
if self.system.fmtspecific_mtd('batchrawftget', v.request[1])
|
|
490
|
+
else None)
|
|
491
|
+
todo[a_fmt].append(k)
|
|
492
|
+
rc = True
|
|
493
|
+
if todo:
|
|
494
|
+
for a_fmt, a_todolist in todo.items():
|
|
495
|
+
sources = list()
|
|
496
|
+
destinations = list()
|
|
497
|
+
extras = dict()
|
|
498
|
+
if a_fmt is not None:
|
|
499
|
+
extras['fmt'] = a_fmt
|
|
500
|
+
for k in a_todolist:
|
|
501
|
+
sources.append(self._resultsmap[k].request[0])
|
|
502
|
+
destinations.append(self._resultsmap[k].result)
|
|
503
|
+
try:
|
|
504
|
+
logger.info('Running the ftserv command for format=%s.', str(a_fmt))
|
|
505
|
+
hostname, port = self._ftp_hostinfos
|
|
506
|
+
rc = self.system.batchrawftget(sources, destinations,
|
|
507
|
+
hostname=hostname, logname=self.logname, port=port,
|
|
508
|
+
** extras)
|
|
509
|
+
except OSError:
|
|
510
|
+
rc = [None, ] * len(sources)
|
|
511
|
+
for i, k in enumerate(a_todolist):
|
|
512
|
+
if rc[i] is True:
|
|
513
|
+
self._resultsmap[k].mark_as_done()
|
|
514
|
+
elif rc[i] is False:
|
|
515
|
+
self._resultsmap[k].mark_as_failed()
|
|
516
|
+
else:
|
|
517
|
+
self._resultsmap[k].mark_as_unclear()
|
|
518
|
+
return rc
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
class PrivateDelayedActionsHub:
|
|
522
|
+
"""
|
|
523
|
+
Manages all of the delayed actions request by forwarding them to the appropriate
|
|
524
|
+
:class:`AbstractDelayedActionsHandler` object.
|
|
525
|
+
|
|
526
|
+
If no, :class:`AbstractDelayedActionsHandler` class is able to handle
|
|
527
|
+
the delayed action, just returns ``None`` to inform the caller that the
|
|
528
|
+
requested action can't be performed
|
|
529
|
+
"""
|
|
530
|
+
|
|
531
|
+
def __init__(self, sh, contextrundir):
|
|
532
|
+
"""
|
|
533
|
+
:param vortex.tools.systems.OSExtended sh: The current usable System object
|
|
534
|
+
:param str contextrundir: The current context's run directory where the
|
|
535
|
+
staging area/directory will be created. If ``None``,
|
|
536
|
+
the staging directory is created in the current
|
|
537
|
+
working directory.
|
|
538
|
+
"""
|
|
539
|
+
self._sh = sh
|
|
540
|
+
self._contextrundir = contextrundir
|
|
541
|
+
self._stagedir = None
|
|
542
|
+
self._delayedactionshandlers = set()
|
|
543
|
+
self._obsboard = observer.SecludedObserverBoard()
|
|
544
|
+
self._resultsmap = dict()
|
|
545
|
+
|
|
546
|
+
@property
|
|
547
|
+
def observerboard(self):
|
|
548
|
+
"""The Observer board associated with this Hub;
|
|
549
|
+
|
|
550
|
+
:note: Anyone is free to register to it in order be kept informed when a
|
|
551
|
+
delayed action associated with this Hub is updated.
|
|
552
|
+
"""
|
|
553
|
+
return self._obsboard
|
|
554
|
+
|
|
555
|
+
@property
|
|
556
|
+
def stagedir(self):
|
|
557
|
+
"""This Hub staging area/directory (i.e. where results can be stored)."""
|
|
558
|
+
if self._stagedir is None:
|
|
559
|
+
self._stagedir = tempfile.mkdtemp(prefix='dactions_staging_area_',
|
|
560
|
+
dir=(self._contextrundir
|
|
561
|
+
if self._contextrundir else self._sh.pwd()))
|
|
562
|
+
return self._stagedir
|
|
563
|
+
|
|
564
|
+
def showhistory(self):
|
|
565
|
+
"""
|
|
566
|
+
Print the complete logs of all of the :class:`AbstractDelayedActionsHandler`
|
|
567
|
+
objects leveraged by this Hub.
|
|
568
|
+
"""
|
|
569
|
+
for handler in self._delayedactionshandlers:
|
|
570
|
+
print('{!r} says:\n'.format(handler))
|
|
571
|
+
handler.history.show()
|
|
572
|
+
|
|
573
|
+
def actionhistory(self, r_id):
|
|
574
|
+
"""Print the log lines associated to a given request (identified by its **r_id** ID)."""
|
|
575
|
+
for handler in self._delayedactionshandlers:
|
|
576
|
+
hst = handler.grephistory(r_id)
|
|
577
|
+
if hst:
|
|
578
|
+
print('{!r} says:'.format(handler))
|
|
579
|
+
handler.showhistory(r_id)
|
|
580
|
+
|
|
581
|
+
def register(self, request, **kwargs):
|
|
582
|
+
"""Take into consideration a new delayed action request.
|
|
583
|
+
|
|
584
|
+
:param request: A description of the user's request
|
|
585
|
+
:param dict kwargs: Any argument that will be used to create the
|
|
586
|
+
:class:`AbstractDelayedActionsHandler` object
|
|
587
|
+
"""
|
|
588
|
+
# Prestaging tool descriptions
|
|
589
|
+
myhandler_desc = dict(system=self._sh, observerboard=self._obsboard,
|
|
590
|
+
stagedir=self.stagedir)
|
|
591
|
+
myhandler_desc.update(kwargs)
|
|
592
|
+
myhandler = None
|
|
593
|
+
# Scan pre-existing prestaging tools to find a suitable one
|
|
594
|
+
for ahandler in self._delayedactionshandlers:
|
|
595
|
+
if ahandler.footprint_reusable() and ahandler.footprint_compatible(myhandler_desc):
|
|
596
|
+
logger.debug("Re-usable Actions Handler found: %s", lightdump(myhandler_desc))
|
|
597
|
+
myhandler = ahandler
|
|
598
|
+
break
|
|
599
|
+
# If necessary, create a new one
|
|
600
|
+
if myhandler is None:
|
|
601
|
+
myhandler = fpx.delayedactionshandler(_emptywarning=False, **myhandler_desc)
|
|
602
|
+
if myhandler is not None:
|
|
603
|
+
logger.debug("Fresh prestaging tool created: %s", lightdump(myhandler_desc))
|
|
604
|
+
self._delayedactionshandlers.add(myhandler)
|
|
605
|
+
# Let's role
|
|
606
|
+
if myhandler is None:
|
|
607
|
+
logger.debug("Unable to find a delayed actions handler with: %s", lightdump(myhandler_desc))
|
|
608
|
+
return None
|
|
609
|
+
else:
|
|
610
|
+
resultid = myhandler.register(request)
|
|
611
|
+
if resultid is not None:
|
|
612
|
+
self._resultsmap[resultid] = myhandler
|
|
613
|
+
return resultid
|
|
614
|
+
|
|
615
|
+
@property
|
|
616
|
+
def dirty(self):
|
|
617
|
+
"""Is there any of the hub's delayed actions that needs finalising ?"""
|
|
618
|
+
dirtyflag = False
|
|
619
|
+
for ahandler in self._delayedactionshandlers:
|
|
620
|
+
dirtyflag = dirtyflag or ahandler.dirty
|
|
621
|
+
return dirtyflag
|
|
622
|
+
|
|
623
|
+
def finalise(self, *r_ids):
|
|
624
|
+
"""Given a **r_ids** list of delayed action IDs, wait upon actions completion."""
|
|
625
|
+
todo = defaultdict(set)
|
|
626
|
+
for r_id in r_ids:
|
|
627
|
+
todo[self._resultsmap[r_id]].add(r_id)
|
|
628
|
+
for ahandler, r_ids in todo.items():
|
|
629
|
+
ahandler.finalise(* list(r_ids))
|
|
630
|
+
|
|
631
|
+
def retrieve(self, resultid, bareobject=False):
|
|
632
|
+
"""Given a **resultid** delayed action ID, returns the corresponding result."""
|
|
633
|
+
try:
|
|
634
|
+
res = self._resultsmap[resultid].retrieve(resultid, bareobject=bareobject)
|
|
635
|
+
finally:
|
|
636
|
+
del self._resultsmap[resultid]
|
|
637
|
+
return res
|
|
638
|
+
|
|
639
|
+
def clear(self):
|
|
640
|
+
"""Destroy all of the associated handlers and reset everything."""
|
|
641
|
+
for a_handler in self._delayedactionshandlers:
|
|
642
|
+
a_handler.destroy()
|
|
643
|
+
self._delayedactionshandlers = set()
|
|
644
|
+
self._obsboard = observer.SecludedObserverBoard()
|
|
645
|
+
self._resultsmap = dict()
|
|
646
|
+
self._stagedir = None
|
|
647
|
+
|
|
648
|
+
def __repr__(self):
|
|
649
|
+
return ('{:s} | n_delayedactionshandlers={:d}>'
|
|
650
|
+
.format(super().__repr__().rstrip('>'),
|
|
651
|
+
len(self._delayedactionshandlers)))
|
|
652
|
+
|
|
653
|
+
def __str__(self):
|
|
654
|
+
return (repr(self) + "\n\n" +
|
|
655
|
+
"\n\n".join([ahandler.describe(fulldump=True)
|
|
656
|
+
for ahandler in self._delayedactionshandlers]))
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
class DelayedActionsHub(PrivateDelayedActionsHub, getbytag.GetByTag):
|
|
660
|
+
"""
|
|
661
|
+
A subclass of :class:`PrivateDelayedActionsHub` that uses
|
|
662
|
+
:class:`footprints.util.GetByTag` to remain persistent in memory.
|
|
663
|
+
|
|
664
|
+
Therefore, a *tag* attribute needs to be specified when building/retrieving
|
|
665
|
+
an object of this class.
|
|
666
|
+
"""
|
|
667
|
+
pass
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
if __name__ == '__main__':
|
|
671
|
+
import doctest
|
|
672
|
+
doctest.testmod()
|