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