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.
Files changed (146) hide show
  1. vortex/__init__.py +135 -0
  2. vortex/algo/__init__.py +12 -0
  3. vortex/algo/components.py +2136 -0
  4. vortex/algo/mpitools.py +1648 -0
  5. vortex/algo/mpitools_templates/envelope_wrapper_default.tpl +27 -0
  6. vortex/algo/mpitools_templates/envelope_wrapper_mpiauto.tpl +29 -0
  7. vortex/algo/mpitools_templates/wrapstd_wrapper_default.tpl +18 -0
  8. vortex/algo/serversynctools.py +170 -0
  9. vortex/config.py +115 -0
  10. vortex/data/__init__.py +13 -0
  11. vortex/data/abstractstores.py +1572 -0
  12. vortex/data/containers.py +780 -0
  13. vortex/data/contents.py +596 -0
  14. vortex/data/executables.py +284 -0
  15. vortex/data/flow.py +113 -0
  16. vortex/data/geometries.ini +2689 -0
  17. vortex/data/geometries.py +703 -0
  18. vortex/data/handlers.py +1021 -0
  19. vortex/data/outflow.py +67 -0
  20. vortex/data/providers.py +465 -0
  21. vortex/data/resources.py +201 -0
  22. vortex/data/stores.py +1271 -0
  23. vortex/gloves.py +282 -0
  24. vortex/layout/__init__.py +27 -0
  25. vortex/layout/appconf.py +109 -0
  26. vortex/layout/contexts.py +511 -0
  27. vortex/layout/dataflow.py +1069 -0
  28. vortex/layout/jobs.py +1276 -0
  29. vortex/layout/monitor.py +833 -0
  30. vortex/layout/nodes.py +1424 -0
  31. vortex/layout/subjobs.py +464 -0
  32. vortex/nwp/__init__.py +11 -0
  33. vortex/nwp/algo/__init__.py +12 -0
  34. vortex/nwp/algo/assim.py +483 -0
  35. vortex/nwp/algo/clim.py +920 -0
  36. vortex/nwp/algo/coupling.py +609 -0
  37. vortex/nwp/algo/eda.py +632 -0
  38. vortex/nwp/algo/eps.py +613 -0
  39. vortex/nwp/algo/forecasts.py +745 -0
  40. vortex/nwp/algo/fpserver.py +927 -0
  41. vortex/nwp/algo/ifsnaming.py +403 -0
  42. vortex/nwp/algo/ifsroot.py +311 -0
  43. vortex/nwp/algo/monitoring.py +202 -0
  44. vortex/nwp/algo/mpitools.py +554 -0
  45. vortex/nwp/algo/odbtools.py +974 -0
  46. vortex/nwp/algo/oopsroot.py +735 -0
  47. vortex/nwp/algo/oopstests.py +186 -0
  48. vortex/nwp/algo/request.py +579 -0
  49. vortex/nwp/algo/stdpost.py +1285 -0
  50. vortex/nwp/data/__init__.py +12 -0
  51. vortex/nwp/data/assim.py +392 -0
  52. vortex/nwp/data/boundaries.py +261 -0
  53. vortex/nwp/data/climfiles.py +539 -0
  54. vortex/nwp/data/configfiles.py +149 -0
  55. vortex/nwp/data/consts.py +929 -0
  56. vortex/nwp/data/ctpini.py +133 -0
  57. vortex/nwp/data/diagnostics.py +181 -0
  58. vortex/nwp/data/eda.py +148 -0
  59. vortex/nwp/data/eps.py +383 -0
  60. vortex/nwp/data/executables.py +1039 -0
  61. vortex/nwp/data/fields.py +96 -0
  62. vortex/nwp/data/gridfiles.py +308 -0
  63. vortex/nwp/data/logs.py +551 -0
  64. vortex/nwp/data/modelstates.py +334 -0
  65. vortex/nwp/data/monitoring.py +220 -0
  66. vortex/nwp/data/namelists.py +644 -0
  67. vortex/nwp/data/obs.py +748 -0
  68. vortex/nwp/data/oopsexec.py +72 -0
  69. vortex/nwp/data/providers.py +182 -0
  70. vortex/nwp/data/query.py +217 -0
  71. vortex/nwp/data/stores.py +147 -0
  72. vortex/nwp/data/surfex.py +338 -0
  73. vortex/nwp/syntax/__init__.py +9 -0
  74. vortex/nwp/syntax/stdattrs.py +375 -0
  75. vortex/nwp/tools/__init__.py +10 -0
  76. vortex/nwp/tools/addons.py +35 -0
  77. vortex/nwp/tools/agt.py +55 -0
  78. vortex/nwp/tools/bdap.py +48 -0
  79. vortex/nwp/tools/bdcp.py +38 -0
  80. vortex/nwp/tools/bdm.py +21 -0
  81. vortex/nwp/tools/bdmp.py +49 -0
  82. vortex/nwp/tools/conftools.py +1311 -0
  83. vortex/nwp/tools/drhook.py +62 -0
  84. vortex/nwp/tools/grib.py +268 -0
  85. vortex/nwp/tools/gribdiff.py +99 -0
  86. vortex/nwp/tools/ifstools.py +163 -0
  87. vortex/nwp/tools/igastuff.py +249 -0
  88. vortex/nwp/tools/mars.py +56 -0
  89. vortex/nwp/tools/odb.py +548 -0
  90. vortex/nwp/tools/partitioning.py +234 -0
  91. vortex/nwp/tools/satrad.py +56 -0
  92. vortex/nwp/util/__init__.py +6 -0
  93. vortex/nwp/util/async.py +184 -0
  94. vortex/nwp/util/beacon.py +40 -0
  95. vortex/nwp/util/diffpygram.py +359 -0
  96. vortex/nwp/util/ens.py +198 -0
  97. vortex/nwp/util/hooks.py +128 -0
  98. vortex/nwp/util/taskdeco.py +81 -0
  99. vortex/nwp/util/usepygram.py +591 -0
  100. vortex/nwp/util/usetnt.py +87 -0
  101. vortex/proxy.py +6 -0
  102. vortex/sessions.py +341 -0
  103. vortex/syntax/__init__.py +9 -0
  104. vortex/syntax/stdattrs.py +628 -0
  105. vortex/syntax/stddeco.py +176 -0
  106. vortex/toolbox.py +982 -0
  107. vortex/tools/__init__.py +11 -0
  108. vortex/tools/actions.py +457 -0
  109. vortex/tools/addons.py +297 -0
  110. vortex/tools/arm.py +76 -0
  111. vortex/tools/compression.py +322 -0
  112. vortex/tools/date.py +20 -0
  113. vortex/tools/ddhpack.py +10 -0
  114. vortex/tools/delayedactions.py +672 -0
  115. vortex/tools/env.py +513 -0
  116. vortex/tools/folder.py +663 -0
  117. vortex/tools/grib.py +559 -0
  118. vortex/tools/lfi.py +746 -0
  119. vortex/tools/listings.py +354 -0
  120. vortex/tools/names.py +575 -0
  121. vortex/tools/net.py +1790 -0
  122. vortex/tools/odb.py +10 -0
  123. vortex/tools/parallelism.py +336 -0
  124. vortex/tools/prestaging.py +186 -0
  125. vortex/tools/rawfiles.py +10 -0
  126. vortex/tools/schedulers.py +413 -0
  127. vortex/tools/services.py +871 -0
  128. vortex/tools/storage.py +1061 -0
  129. vortex/tools/surfex.py +61 -0
  130. vortex/tools/systems.py +3396 -0
  131. vortex/tools/targets.py +384 -0
  132. vortex/util/__init__.py +9 -0
  133. vortex/util/config.py +1071 -0
  134. vortex/util/empty.py +24 -0
  135. vortex/util/helpers.py +184 -0
  136. vortex/util/introspection.py +63 -0
  137. vortex/util/iosponge.py +76 -0
  138. vortex/util/roles.py +51 -0
  139. vortex/util/storefunctions.py +103 -0
  140. vortex/util/structs.py +26 -0
  141. vortex/util/worker.py +150 -0
  142. vortex_nwp-2.0.0b1.dist-info/LICENSE +517 -0
  143. vortex_nwp-2.0.0b1.dist-info/METADATA +50 -0
  144. vortex_nwp-2.0.0b1.dist-info/RECORD +146 -0
  145. vortex_nwp-2.0.0b1.dist-info/WHEEL +5 -0
  146. 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()