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,1021 @@
1
+ """
2
+ :class:`Handler` class is a cornerstone in any Vortex script. :class:`Handler`
3
+ objects are in charge of manipulating data between the working directory and
4
+ the various caches or archives".
5
+ """
6
+
7
+
8
+ import functools
9
+ import re
10
+ import sys
11
+
12
+ import bronx.fancies.dump
13
+ from bronx.fancies import loggers
14
+ from bronx.patterns import observer
15
+ from bronx.stdtypes.history import History
16
+
17
+ import footprints
18
+
19
+ from vortex import sessions
20
+
21
+ from vortex.tools import net
22
+ from vortex.util import config
23
+ from vortex.layout import contexts, dataflow
24
+ from vortex.data import containers, resources, providers
25
+
26
+ #: No automatic export
27
+ __all__ = []
28
+
29
+ logger = loggers.getLogger(__name__)
30
+
31
+ OBSERVER_TAG = 'Resources-Handlers'
32
+
33
+
34
+ class HandlerError(RuntimeError):
35
+ """Exception in case of missing resource during the wait mechanism."""
36
+ pass
37
+
38
+
39
+ def observer_board(obsname=None):
40
+ """Proxy to :func:`footprints.observers.get`."""
41
+ if obsname is None:
42
+ obsname = OBSERVER_TAG
43
+ return observer.get(tag=obsname)
44
+
45
+
46
+ class IdCardAttrDumper(bronx.fancies.dump.TxtDumper):
47
+ """Dump a text representation of almost any footprint object..."""
48
+
49
+ indent_size = 2
50
+ max_depth = 2
51
+
52
+ def __init__(self):
53
+ self._indent_first = 4
54
+
55
+ def _get_indent_first(self):
56
+ return self._indent_first
57
+
58
+ def _set_indent_first(self, val):
59
+ self._indent_first = val
60
+
61
+ indent_first = property(_get_indent_first, _set_indent_first)
62
+
63
+ def dump_fpattrs(self, fpobj, level=0):
64
+ """Dump the attributes of a footprint based object."""
65
+ if level + 1 > self.max_depth:
66
+ return "{}{{...}}{}".format(
67
+ self._indent(level, self.break_before_dict_begin),
68
+ self._indent(level, self.break_after_dict_end)
69
+ )
70
+ else:
71
+ items = ["{}{} = {}{},".format(self._indent(level + 1, self.break_before_dict_key),
72
+ str(k),
73
+ self._indent(level + 2, self.break_before_dict_value),
74
+ self._recursive_dump(v, level + 1))
75
+ for k, v in sorted(fpobj.footprint_as_shallow_dict().items())]
76
+ return ' '.join(items)
77
+
78
+ def dump_default(self, obj, level=0, nextline=True):
79
+ """Generic dump function. Concise view for GetByTag objects."""
80
+ if level + 1 > self.max_depth:
81
+ return " <%s...>" % type(obj).__class__
82
+ else:
83
+ if hasattr(obj, 'tag'):
84
+ return "{:s} obj: tag={:s}".format(type(obj).__name__, obj.tag)
85
+ else:
86
+ parent_dump = super(bronx.fancies.dump.TxtDumper, self).dump_default(obj, level,
87
+ nextline and
88
+ self.break_default)
89
+ return "{:s} obj: {!s}".format(type(obj).__name__, parent_dump)
90
+
91
+
92
+ class Handler:
93
+ """
94
+ The resource handler object gathers a provider, a resource and a container
95
+ for any specific resource.
96
+
97
+ Other parameters given at construct time are stored as options.
98
+ """
99
+
100
+ def __init__(self, rd, **kw):
101
+ if 'glove' in rd:
102
+ del rd['glove']
103
+ self._resource = rd.pop('resource', None)
104
+ self._provider = rd.pop('provider', None)
105
+ self._container = rd.pop('container', None)
106
+ self._empty = rd.pop('empty', False)
107
+ self._contents = None
108
+ self._uridata = None
109
+ self._options = rd.copy()
110
+ self._observer = observer_board(obsname=kw.pop('observer', None))
111
+ self._options.update(kw)
112
+ self._mdcheck = self._options.pop('metadatacheck', False)
113
+ self._mddelta = self._options.pop('metadatadelta', dict())
114
+ self._ghost = self._options.pop('ghost', False)
115
+ hook_names = [x for x in self._options.keys() if x.startswith('hook_')]
116
+ self._hooks = {x[5:]: self._options.pop(x) for x in hook_names}
117
+ self._delayhooks = self._options.pop('delayhooks', False)
118
+
119
+ self._history = History(tag='data-handler')
120
+ self._history.append(self.__class__.__name__, 'init', True)
121
+ self._stage = ['load']
122
+ self._observer.notify_new(self, dict(stage='load'))
123
+ self._localpr_cache = None # To cache the promise dictionary
124
+ self._latest_earlyget_id = None
125
+ self._latest_earlyget_opts = None
126
+ logger.debug('New resource handler %s', self.__dict__)
127
+
128
+ def __str__(self):
129
+ return str(self.__dict__)
130
+
131
+ def _get_resource(self):
132
+ """Getter for ``resource`` property."""
133
+ return self._resource
134
+
135
+ def _set_resource(self, value):
136
+ """Setter for ``resource`` property."""
137
+ if isinstance(value, resources.Resource):
138
+ oldhash = self.simplified_hashkey
139
+ self._resource = value
140
+ self._notifyhash(oldhash)
141
+ self.reset_contents()
142
+ else:
143
+ raise ValueError('This value is not a plain Resource <{!s}>'.format(value))
144
+
145
+ resource = property(_get_resource, _set_resource)
146
+
147
+ def _get_provider(self):
148
+ """Getter for ``provider`` property."""
149
+ return self._provider
150
+
151
+ def _set_provider(self, value):
152
+ """Setter for ``provider`` property."""
153
+ if isinstance(value, providers.Provider):
154
+ oldhash = self.simplified_hashkey
155
+ self._provider = value
156
+ self._notifyhash(oldhash)
157
+ self.reset_contents()
158
+ else:
159
+ raise ValueError('This value is not a plain Provider <{!s}>'.format(value))
160
+
161
+ provider = property(_get_provider, _set_provider)
162
+
163
+ def _get_container(self):
164
+ """Getter for ``container`` property."""
165
+ return self._container
166
+
167
+ def _set_container(self, value):
168
+ """Setter for ``container`` property."""
169
+ if isinstance(value, containers.Container):
170
+ oldhash = self.simplified_hashkey
171
+ self._container = value
172
+ self._notifyhash(oldhash)
173
+ else:
174
+ raise ValueError('This value is not a plain Container <{!s}>'.format(value))
175
+
176
+ container = property(_get_container, _set_container)
177
+
178
+ @property
179
+ def history(self):
180
+ return self._history
181
+
182
+ @property
183
+ def observer(self):
184
+ """Footprint observer devoted to resource handlers tracking."""
185
+ return self._observer
186
+
187
+ def observers(self):
188
+ """Remote objects observing the current resource handler... and maybe others."""
189
+ return self._observer.observers()
190
+
191
+ def observed(self):
192
+ """Other objects observed by the observers of the current resource handler."""
193
+ return [x for x in self._observer.observed() if x is not self]
194
+
195
+ @property
196
+ def complete(self):
197
+ """Returns whether all the internal components are defined."""
198
+ return bool(self.resource and self.provider and self.container)
199
+
200
+ @property
201
+ def stage(self):
202
+ """Return current resource handler stage (load, get, put)."""
203
+ return self._stage[-1]
204
+
205
+ @property
206
+ def simplified_hashkey(self):
207
+ """Returns a tuple that can be used as a hashkey to quickly identify the handler."""
208
+ if self.complete:
209
+ rkind = getattr(self.resource, 'kind', None)
210
+ rfile = getattr(self.container, 'filename', None)
211
+ return (rkind, rfile)
212
+ else:
213
+ return ('incomplete', )
214
+
215
+ @property
216
+ def _cur_session(self):
217
+ """Return the current active session."""
218
+ return sessions.current()
219
+
220
+ @property
221
+ def _cur_context(self):
222
+ """Return the current active context."""
223
+ return contexts.current()
224
+
225
+ def external_stage_update(self, newstage):
226
+ """This method must not be used directly by users!
227
+
228
+ Update the stage upon request (e.g. the file has been fetched by another process).
229
+ """
230
+ self._stage.append(newstage)
231
+ if newstage in ('get', ):
232
+ self.container.updfill(True)
233
+
234
+ def _updstage(self, newstage, insitu=False):
235
+ """Notify the new stage to any observing system."""
236
+ self._stage.append(newstage)
237
+ self._observer.notify_upd(self, dict(stage=newstage, insitu=insitu))
238
+
239
+ def _notifyhook(self, stage, hookname):
240
+ """Notify that a hook function has been executed."""
241
+ self._observer.notify_upd(self, dict(stage=stage, hook=hookname))
242
+
243
+ def _notifyclear(self):
244
+ """Notify that the hashkey has changed."""
245
+ self._observer.notify_upd(self, dict(clear=True, ))
246
+
247
+ def _notifyhash(self, oldhash):
248
+ """Notify that the hashkey has changed."""
249
+ self._observer.notify_upd(self, dict(oldhash=oldhash, ))
250
+
251
+ def is_expected(self):
252
+ """Return a boolean value according to the last stage value (expected or not)."""
253
+ return self.stage.startswith('expect')
254
+
255
+ @property
256
+ def contents(self):
257
+ """
258
+ Returns an valid data layout object as long as the current handler
259
+ is complete and the container filled.
260
+ """
261
+ if self._empty:
262
+ self.container.write('')
263
+ self._empty = False
264
+ if self.complete:
265
+ if self.container.filled or self.stage == 'put':
266
+ if self._contents is None:
267
+ self._contents = self.resource.contents_handler(datafmt=self.container.actualfmt)
268
+ with self.container.iod_context():
269
+ self._contents.slurp(self.container)
270
+ return self._contents
271
+ else:
272
+ logger.warning('Contents requested on an empty container [%s]', self.container)
273
+ else:
274
+ logger.warning('Contents requested for an uncomplete handler [%s]', self.container)
275
+ return None
276
+
277
+ def reset_contents(self):
278
+ """Delete actual internal reference to data contents manager."""
279
+ self._contents = None
280
+
281
+ @property
282
+ def ghost(self):
283
+ return self._ghost
284
+
285
+ @property
286
+ def hooks(self):
287
+ return self._hooks
288
+
289
+ @property
290
+ def options(self):
291
+ return self._options
292
+
293
+ @property
294
+ def delayhooks(self):
295
+ return self._delayhooks
296
+
297
+ def mkopts(self, *dicos, **kw):
298
+ """Returns options associated to that handler and a system reference."""
299
+ opts = dict(
300
+ intent=dataflow.intent.IN,
301
+ fmt=self.container.actualfmt,
302
+ )
303
+ opts.update(self.options)
304
+ for d in dicos:
305
+ opts.update(d)
306
+ opts.update(kw)
307
+ return opts
308
+
309
+ def location(self, fatal=True):
310
+ """Returns the URL as defined by the internal provider and resource."""
311
+ self._lasturl = None
312
+ if self.provider and self.resource:
313
+ try:
314
+ self._lasturl = self.provider.uri(self.resource)
315
+ except Exception as e:
316
+ if fatal:
317
+ raise
318
+ else:
319
+ return 'OOPS: {!s} (but fatal is False)'.format(e)
320
+ return self._lasturl
321
+ else:
322
+ logger.warning('Resource handler %s could not build location', self)
323
+ return None
324
+
325
+ def idcard(self, indent=2):
326
+ """
327
+ Returns a multilines documentation string with a summary
328
+ of the valuable information contained by this handler.
329
+ """
330
+ tab = ' ' * indent
331
+ card = "\n".join((
332
+ '{0}Handler {1!r}',
333
+ '{0}{0}Complete : {2}',
334
+ '{0}{0}Options : {3}',
335
+ '{0}{0}Location : {4}'
336
+ )).format(tab,
337
+ self, self.complete, self.options, self.location())
338
+ if self.hooks:
339
+ card += '\n{0}{0}Hooks : {1}'.format(tab, ','.join(list(self.hooks.keys())))
340
+ d = IdCardAttrDumper(tag='idcarddumper')
341
+ d.reset()
342
+ d.indent_first = 2 * len(tab)
343
+ for subobj in ('resource', 'provider', 'container'):
344
+ obj = getattr(self, subobj, None)
345
+ if obj:
346
+ thisdoc = '{0}{0}{1:s} {2!r}'.format(tab,
347
+ subobj.capitalize(), obj)
348
+ thisdoc += d.dump_fpattrs(obj)
349
+ else:
350
+ thisdoc = '{0}{0}{1:s} undefined'.format(tab, subobj.capitalize())
351
+ card = card + "\n" + thisdoc
352
+ return card
353
+
354
+ def quickview(self, nb=0, indent=0):
355
+ """Standard glance to objects."""
356
+ tab = ' ' * indent
357
+ print('{}{:02d}. {:s}'.format(tab, nb, repr(self)))
358
+ print('{} Complete : {!s}'.format(tab, self.complete))
359
+ for subobj in ('container', 'provider', 'resource'):
360
+ obj = getattr(self, subobj, None)
361
+ if obj:
362
+ print('{} {:10s}: {!s}'.format(tab, subobj.capitalize(), obj))
363
+
364
+ def wide_key_lookup(self, key, exports=False, fatal=True):
365
+ """Return the *key* attribute if it exists in the provider or resource.
366
+
367
+ If *exports* is True, the footprint_export() or the export_dict() function
368
+ is called upon the return value.
369
+ """
370
+ try:
371
+ if key == 'safeblock':
372
+ # In olive experiments, the block may contain an indication of
373
+ # the member's number. Usually we do not want to get that...
374
+ a_value = getattr(self.provider, 'block')
375
+ a_value = re.sub(r'(member|fc)_?\d+/', '', a_value)
376
+ else:
377
+ a_value = getattr(self.provider, key)
378
+ except AttributeError:
379
+ try:
380
+ a_value = getattr(self.resource, key)
381
+ except AttributeError:
382
+ if fatal:
383
+ raise AttributeError('The {:s} attribute could not be found in {!r}'.format(key, self))
384
+ else:
385
+ a_value = None
386
+ if exports:
387
+ if hasattr(a_value, 'footprint_export'):
388
+ a_value = a_value.footprint_export()
389
+ elif hasattr(a_value, 'export_dict'):
390
+ a_value = a_value.export_dict()
391
+ return a_value
392
+
393
+ def as_dict(self):
394
+ """Produce a raw json-compatible dictionary."""
395
+ rhd = dict(options=dict())
396
+ for k, v in self.options.items():
397
+ try:
398
+ v = v.export_dict()
399
+ except (AttributeError, TypeError):
400
+ pass
401
+ rhd['options'][k] = v
402
+ for subobj in ('resource', 'provider', 'container'):
403
+ obj = getattr(self, subobj, None)
404
+ if obj is not None:
405
+ rhd[subobj] = obj.footprint_export()
406
+ return rhd
407
+
408
+ @property
409
+ def lasturl(self):
410
+ """The last actual URL value evaluated."""
411
+ return self._lasturl
412
+
413
+ @property
414
+ def uridata(self):
415
+ """Actual extra URI values after store definition."""
416
+ return self._uridata
417
+
418
+ @property
419
+ def store(self):
420
+ if self.resource and self.provider:
421
+ self._uridata = net.uriparse(self.location())
422
+ stopts = {k: v for k, v in self.options.items() if k.startswith('stor')}
423
+ return footprints.proxy.store(
424
+ scheme=self._uridata.pop('scheme'),
425
+ netloc=self._uridata.pop('netloc'),
426
+ **stopts
427
+ )
428
+ else:
429
+ return None
430
+
431
+ def check(self, **extras):
432
+ """Returns a stat-like information to the remote resource."""
433
+ rst = None
434
+ if self.resource and self.provider:
435
+ store = self.store
436
+ if store:
437
+ logger.debug('Check resource %s at %s from %s', self, self.lasturl, store)
438
+ rst = store.check(
439
+ self.uridata,
440
+ self.mkopts(extras)
441
+ )
442
+ if rst and self._mdcheck:
443
+ logger.info('metadatacheck is on: we are forcing a real get()...')
444
+ # We are using a temporary fake container
445
+ mycontainer = footprints.proxy.container(shouldfly=True,
446
+ actualfmt=self.container.actualfmt)
447
+ try:
448
+ tmp_options = self.mkopts(extras)
449
+ tmp_options['obs_notify'] = False
450
+ rst = store.get(
451
+ self.uridata,
452
+ mycontainer.iotarget(),
453
+ tmp_options
454
+ )
455
+ if rst:
456
+ if store.delayed:
457
+ logger.warning("The resource is expected... let's say that's fine.")
458
+ else:
459
+ # Create the contents manually and drop it when we are done.
460
+ contents = self.resource.contents_handler(datafmt=mycontainer.actualfmt)
461
+ contents.slurp(mycontainer)
462
+ rst = contents.metadata_check(self.resource, delta=self._mddelta)
463
+ finally:
464
+ # Delete the temporary container
465
+ mycontainer.clear()
466
+ self.history.append(store.fullname(), 'check', rst)
467
+ if rst and self.stage == 'load':
468
+ # Indicate that the resource was checked
469
+ self._updstage('checked')
470
+ if not rst:
471
+ # Always signal failures
472
+ self._updstage('void')
473
+ else:
474
+ logger.error('Could not find any store to check %s', self.lasturl)
475
+ else:
476
+ logger.error('Could not check a rh without defined resource and provider %s', self)
477
+ return rst
478
+
479
+ def locate(self, **extras):
480
+ """Try to figure out what would be the physical location of the resource."""
481
+ rst = None
482
+ if self.resource and self.provider:
483
+ store = self.store
484
+ if store:
485
+ logger.debug('Locate resource %s at %s from %s', self, self.lasturl, store)
486
+ rst = store.locate(
487
+ self.uridata,
488
+ self.mkopts(extras)
489
+ )
490
+ self.history.append(store.fullname(), 'locate', rst)
491
+ else:
492
+ logger.error('Could not find any store to locate %s', self.lasturl)
493
+ else:
494
+ logger.error('Could not locate an incomplete rh %s', self)
495
+ return rst
496
+
497
+ def prestage(self, **extras):
498
+ """Request the pre-staging of the remote resource."""
499
+ rst = None
500
+ if self.resource and self.provider:
501
+ store = self.store
502
+ if store:
503
+ logger.debug('Prestage resource %s at %s from %s', self, self.lasturl, store)
504
+ rst = store.prestage(
505
+ self.uridata,
506
+ self.mkopts(extras)
507
+ )
508
+ self.history.append(store.fullname(), 'prestage', rst)
509
+ else:
510
+ logger.error('Could not find any store to prestage %s', self.lasturl)
511
+ else:
512
+ logger.error('Could not prestage an incomplete rh %s', self)
513
+ return rst
514
+
515
+ def _generic_apply_hooks(self, action, **extras):
516
+ """Apply the hooks after a get request (or verify that they were done)."""
517
+ if self.hooks:
518
+ mytracker = extras.get('mytracker', None)
519
+ if mytracker is None:
520
+ iotarget = self.container.iotarget()
521
+ mytracker = self._cur_context.localtracker[iotarget]
522
+ for hook_name in sorted(self.hooks.keys()):
523
+ if mytracker.redundant_hook(action, hook_name):
524
+ logger.info('Hook already executed <hook_name:%s>', hook_name)
525
+ else:
526
+ logger.info('Executing Hook <hook_name:%s>', hook_name)
527
+ hook_func, hook_args = self.hooks[hook_name]
528
+ hook_func(self._cur_session, self, *hook_args)
529
+ self._notifyhook(action, hook_name)
530
+
531
+ def apply_get_hooks(self, **extras):
532
+ """Apply the hooks after a get request (or verify that they were done)."""
533
+ self._generic_apply_hooks(action='get', **extras)
534
+
535
+ def apply_put_hooks(self, **extras):
536
+ """Apply the hooks before a put request (or verify that they were done)."""
537
+ self._generic_apply_hooks(action='put', **extras)
538
+
539
+ def _postproc_get(self, store, rst, extras):
540
+ self.container.updfill(rst)
541
+ # Check metadata if sensible
542
+ if self._mdcheck and rst and not store.delayed:
543
+ rst = self.contents.metadata_check(self.resource,
544
+ delta=self._mddelta)
545
+ if not rst:
546
+ logger.info("We are now cleaning up the container and data contents.")
547
+ self.reset_contents()
548
+ self.clear()
549
+ # For the record...
550
+ self.history.append(store.fullname(), 'get', rst)
551
+ if rst:
552
+ # This is an expected resource
553
+ if store.delayed:
554
+ self._updstage('expected')
555
+ logger.info('Resource <%s> is expected', self.container.iotarget())
556
+ # This is a "real" resource
557
+ else:
558
+ self._updstage('get')
559
+ if self.hooks:
560
+ if not self.delayhooks:
561
+ self.apply_get_hooks(**extras)
562
+ else:
563
+ logger.info("(get-)Hooks were delayed")
564
+ else:
565
+ # Always signal failures
566
+ self._updstage('void')
567
+ return rst
568
+
569
+ def _actual_get(self, **extras):
570
+ """Internal method in charge of getting the resource.
571
+
572
+ If requested, it will check the metadata of the resource and apply the
573
+ hook functions.
574
+ """
575
+ rst = False
576
+ store = self.store
577
+ if store:
578
+ logger.debug('Get resource %s at %s from %s', self, self.lasturl, store)
579
+ st_options = self.mkopts(dict(rhandler=self.as_dict()), extras)
580
+ # Actual get
581
+ try:
582
+ rst = store.get(
583
+ self.uridata,
584
+ self.container.iotarget(),
585
+ st_options,
586
+ )
587
+ except Exception:
588
+ rst = False
589
+ raise
590
+ finally:
591
+ rst = self._postproc_get(store, rst, extras)
592
+ else:
593
+ logger.error('Could not find any store to get %s', self.lasturl)
594
+
595
+ # Reset the promise dictionary cache
596
+ self._localpr_cache = None # To cache the promise dictionary
597
+
598
+ return rst
599
+
600
+ def _actual_earlyget(self, **extras):
601
+ """Internal method in charge of requesting an earlyget on the resource.
602
+
603
+ :return: ``None`` if earlyget is unavailable (depending on the store's kind
604
+ and resource it can be perfectly fine). ``True`` if the resource was
605
+ actually fetched (no need to call :meth:`finaliseget`). Some kind of
606
+ non-null identifier that will be used to call :meth:`finaliseget`.
607
+ """
608
+ try:
609
+ store = self.store
610
+ except Exception as e:
611
+ logger.error("The Resource handler was unable to create a store object (%s).",
612
+ str(e))
613
+ store = None
614
+ if store:
615
+ logger.debug('Early-Get resource %s at %s from %s', self, self.lasturl, store)
616
+ st_options = self.mkopts(dict(rhandler=self.as_dict()), extras)
617
+ # Actual earlyget
618
+ try:
619
+ return store.earlyget(
620
+ self.uridata,
621
+ self.container.iotarget(),
622
+ st_options,
623
+ )
624
+ except Exception as e:
625
+ logger.error("The store's earlyget method did not return (%s): it should never append!",
626
+ str(e))
627
+ return None
628
+ else:
629
+ logger.error('Could not find any store to get %s', self.lasturl)
630
+ return None
631
+
632
+ def _get_proxy(self, callback, alternate=False, **extras):
633
+ """
634
+ Process the **insitu** and **alternate** option and launch the **callback**
635
+ callable if sensible.
636
+ """
637
+ rst = False
638
+ if self.complete:
639
+ if self.options.get('insitu', False): # This a second pass (or third, forth, ...)
640
+ cur_tracker = self._cur_context.localtracker
641
+ cur_seq = self._cur_context.sequence
642
+ iotarget = self.container.iotarget()
643
+ # The localpath is here and listed in the tracker
644
+ if self.container.exists() and cur_tracker.is_tracked_input(iotarget):
645
+ # Am I consistent with the ResourceHandler recorded in the tracker ?
646
+ if cur_tracker[iotarget].match_rh('get', self):
647
+ rst = True
648
+ # There is the tricky usecase where we are dealing with an alternate
649
+ # that was already dealt with (yes, sometimes the nominal case and
650
+ # the alternate is the same !)
651
+ if not (alternate and iotarget in [s.rh.container.iotarget()
652
+ for s in cur_seq.effective_inputs()]):
653
+ self.container.updfill(True)
654
+ self._updstage('get', insitu=True)
655
+ logger.info(
656
+ 'The <%s> resource is already here and matches the RH description :-)',
657
+ self.container.iotarget())
658
+ else:
659
+ # This may happen if fatal=False and the local file was fetched
660
+ # by an alternate
661
+ if alternate:
662
+ if not self.container.is_virtual():
663
+ lpath = self.container.localpath()
664
+ for isec in self._cur_context.sequence.rinputs():
665
+ if (isec.stage in ('get' or 'expected') and
666
+ not isec.rh.container.is_virtual() and
667
+ isec.rh.container.localpath() == lpath):
668
+ rst = True
669
+ break
670
+ if rst:
671
+ logger.info("Alternate is on and the local file exists.")
672
+ else:
673
+ logger.info("Alternate is on but the local file is not yet matched.")
674
+ self._updstage('void', insitu=True)
675
+ else:
676
+ logger.info("Alternate is on. The local file exists. The container is virtual.")
677
+ rst = True
678
+ else:
679
+ logger.info("The resource is already here but doesn't match the RH description :-(")
680
+ cur_tracker[iotarget].match_rh('get', self, verbose=True)
681
+ self._updstage('void', insitu=True)
682
+ # Bloody hell, the localpath doesn't exist
683
+ else:
684
+ rst = callback(**extras) # This might be an expected resource...
685
+ if rst:
686
+ logger.info("The resource was successfully fetched :-)")
687
+ else:
688
+ logger.info("Could not get the resource :-(")
689
+ else:
690
+ if alternate and self.container.exists():
691
+ logger.info('Alternate <%s> exists', alternate)
692
+ rst = True
693
+ else:
694
+ if self.container.exists():
695
+ logger.warning('The resource is already here: that should not happen at this stage !')
696
+ rst = callback(**extras)
697
+ else:
698
+ logger.error('Could not get an incomplete rh %s', self)
699
+ return rst
700
+
701
+ def get(self, alternate=False, **extras):
702
+ """Method to retrieve the resource through the provider and feed the current container.
703
+
704
+ The behaviour of this method depends on the **insitu** and **alternate** options:
705
+
706
+ * When **insitu** is True, the :class:`~vortex.layout.dataflow.LocalTracker`
707
+ object associated with the active context is checked to determine
708
+ whether the resource has already been fetched or not. If not, another
709
+ try is made (but without using any non-cache store).
710
+ * When **insitu** is False, an attempt to get the resource is systematically
711
+ made except if **alternate** is defined and the local container already
712
+ exists.
713
+ """
714
+ return self._get_proxy(self._actual_get, alternate=alternate, **extras)
715
+
716
+ def earlyget(self, alternate=False, **extras):
717
+ """The earlyget feature is somehow a declaration of intent.
718
+
719
+ It records in the current context that, at some point in the future, we will
720
+ retrieve the present resource. It can be useful for some kind of stores
721
+ (and useless to others). For example, when using a store that targets a mass
722
+ archive system, this information can be used to ask for several files at
723
+ once, which accelerates the overall process and optimises the tape's drivers
724
+ usage. On the other hand, for a cache based store, it does not make much sense
725
+ since the data is readily available on disk.
726
+
727
+ Return values can be:
728
+
729
+ * ``None`` if earlyget is unavailable (depending on the store's kind
730
+ and resource it can be perfectly fine).
731
+ * Some kind of non-null identifier that will be used later on to actually
732
+ retrieve the resource. It is returned to the user as a diagnostic but is
733
+ also stored internally within the :class:`Handler` object.
734
+ * ``True`` if the resource has actually been retrieved through the provider
735
+ and fed into the current container.
736
+
737
+ In any case, the :meth:`finaliseget` method should be called later on
738
+ to actually retrieve the resource and feed the container. When ``True``
739
+ is returned by the :meth:`earlyget` method, the :meth:`finaliseget` call
740
+ can be made although it is useless.
741
+
742
+ Like with the :meth:`get` method, the behaviour of this method depends
743
+ on the **insitu** and **alternate** options:
744
+
745
+ * When **insitu** is True, the :class:`~vortex.layout.dataflow.LocalTracker`
746
+ object associated with the active context is checked to determine
747
+ whether the resource has already been fetched or not. If not, another
748
+ try is made (but without using any non-cache store).
749
+ * When **insitu** is False, an attempt to get the resource is systematically
750
+ made except if **alternate** is defined and the local container already
751
+ exists.
752
+ """
753
+ r_opts = extras.copy()
754
+ self._latest_earlyget_opts = r_opts
755
+ self._latest_earlyget_opts['alternate'] = alternate
756
+ self._latest_earlyget_id = self._get_proxy(self._actual_earlyget, alternate=alternate, **extras)
757
+ return self._latest_earlyget_id
758
+
759
+ def finaliseget(self):
760
+ """
761
+ When the :meth:`earlyget` method had previously been called, the
762
+ :meth:`finaliseget` can be called to finalise the ``get`` sequence.
763
+
764
+ When :meth:`finaliseget` returns, if the return code is non-zero, the resource
765
+ has been retrieved and fed into the container.
766
+
767
+ :raises HandlerError: if :meth:`earlyget` is not called prior to this
768
+ method.
769
+ """
770
+ if self._latest_earlyget_id is None and self._latest_earlyget_opts is None:
771
+ raise HandlerError('earlyget was not called yet. Calling finaliseget is not Allowed !')
772
+ try:
773
+ if self._latest_earlyget_id is True:
774
+ # Nothing to be done...
775
+ return True
776
+ elif self._latest_earlyget_id is None:
777
+ # Delayed get not available... do the usual get !
778
+ e_opts = self._latest_earlyget_opts.copy()
779
+ e_opts['insitu'] = False
780
+ return self._get_proxy(self._actual_get, **e_opts)
781
+ else:
782
+ alternate = self._latest_earlyget_opts.get('alternate', False)
783
+ if alternate and self.container.exists():
784
+ # The container may have been filled be another finaliseget
785
+ logger.info('Alternate <%s> exists', alternate)
786
+ rst = True
787
+ else:
788
+ rst = False
789
+ store = self.store
790
+ if store:
791
+ logger.debug('Finalise-Get resource %s at %s from %s', self, self.lasturl, store)
792
+ st_options = self.mkopts(dict(rhandler=self.as_dict()), self._latest_earlyget_opts)
793
+ # Actual get
794
+ rst = store.finaliseget(
795
+ self._latest_earlyget_id,
796
+ self.uridata,
797
+ self.container.iotarget(),
798
+ st_options,
799
+ )
800
+ if rst is None:
801
+ # Delayed get failed... attempt the usual get
802
+ logger.warning('Delayed get result was unclear ! Reverting to the usual get.')
803
+ e_opts = self._latest_earlyget_opts.copy()
804
+ e_opts['insitu'] = False
805
+ return self._get_proxy(self._actual_get, **e_opts)
806
+ else:
807
+ rst = self._postproc_get(store, rst, self._latest_earlyget_opts)
808
+ else:
809
+ logger.error('Could not find any store to get %s', self.lasturl)
810
+
811
+ # Reset the promise dictionary cache
812
+ self._localpr_cache = None # To cache the promise dictionary
813
+
814
+ return rst
815
+ finally:
816
+ self._latest_earlyget_id = None
817
+ self._latest_earlyget_opts = None
818
+
819
+ def insitu_quickget(self, alternate=False, **extras):
820
+ """This method attempts a straightforward insitu get.
821
+
822
+ It is designed to minimise the amount of outputs when everything goes smoothly.
823
+ """
824
+ rst = False
825
+ if self.complete:
826
+ if self.options.get('insitu', False): # This a second pass (or third, forth, ...)
827
+ cur_tracker = self._cur_context.localtracker
828
+ cur_seq = self._cur_context.sequence
829
+ iotarget = self.container.iotarget()
830
+ # The localpath is here and listed in the tracker
831
+ if (self.container.exists() and
832
+ cur_tracker.is_tracked_input(iotarget)):
833
+ if cur_tracker[iotarget].match_rh('get', self):
834
+ rst = True
835
+ # There is the tricky usecase where we are dealing with an alternate
836
+ # that was already dealt with (yes, sometimes the nominal case and
837
+ # the alternate is the same !)
838
+ if not (alternate and iotarget in [s.rh.container.iotarget()
839
+ for s in cur_seq.effective_inputs()]):
840
+ self.container.updfill(True)
841
+ self._updstage('get', insitu=True)
842
+ elif alternate:
843
+ # Alternate is on and the local file exists: check if
844
+ # the file has already been fetch previously in the sequence
845
+ if iotarget in [s.rh.container.iotarget()
846
+ for s in cur_seq.effective_inputs()]:
847
+ rst = True
848
+ else:
849
+ logger.error('This method should not be called with insitu=False (rh %s)', self)
850
+ return rst
851
+
852
+ def put(self, **extras):
853
+ """Method to store data from the current container through the provider.
854
+
855
+ Hook functions may be applied before the put in the designated store. We
856
+ will ensure that a given hook function (identified by its name) is not
857
+ applied more than once to the local container.
858
+
859
+ Conversely, the low-level stores are made aware of the previous successful
860
+ put. That way, a local container is not put twice to the same destination.
861
+ """
862
+ rst = False
863
+ if self.complete:
864
+ store = self.store
865
+ if store:
866
+ iotarget = self.container.iotarget()
867
+ logger.debug('Put resource %s as io %s at store %s', self, iotarget, store)
868
+ if iotarget is not None and (self.container.exists() or self.provider.expected):
869
+ mytracker = self._cur_context.localtracker[iotarget]
870
+ # Execute the hooks only if the local file exists
871
+ if self.container.exists():
872
+ self.container.updfill(True)
873
+ if self.hooks:
874
+ if not self.delayhooks:
875
+ self.apply_put_hooks(mytracker=mytracker, **extras)
876
+ else:
877
+ logger.info("(put-)Hooks were delayed")
878
+ # Add a filter function to remove duplicated PUTs to the same uri
879
+ extras_ext = dict(extras)
880
+ extras_ext['urifilter'] = functools.partial(mytracker.redundant_uri, 'put')
881
+ # Actual put
882
+ logger.debug('Put resource %s at %s from %s', self, self.lasturl, store)
883
+ rst = store.put(
884
+ iotarget,
885
+ self.uridata,
886
+ self.mkopts(dict(rhandler=self.as_dict()), extras_ext)
887
+ )
888
+ # For the record...
889
+ self.history.append(store.fullname(), 'put', rst)
890
+ self._updstage('put')
891
+ elif self.ghost:
892
+ self.history.append(store.fullname(), 'put', False)
893
+ self._updstage('ghost')
894
+ rst = True
895
+ else:
896
+ logger.error('Could not find any source to put [%s]', iotarget)
897
+ else:
898
+ logger.error('Could not find any store to put [%s]', self.lasturl)
899
+ else:
900
+ logger.error('Could not put an incomplete rh [%s]', self)
901
+ return rst
902
+
903
+ def delete(self, **extras):
904
+ """Delete the remote resource from store."""
905
+ rst = None
906
+ if self.resource and self.provider:
907
+ store = self.store
908
+ if store:
909
+ logger.debug('Delete resource %s at %s from %s', self, self.lasturl, store)
910
+ rst = store.delete(
911
+ self.uridata,
912
+ self.mkopts(dict(rhandler=self.as_dict()), extras)
913
+ )
914
+ self.history.append(store.fullname(), 'delete', rst)
915
+ else:
916
+ logger.error('Could not find any store to delete %s', self.lasturl)
917
+ else:
918
+ logger.error('Could not delete a rh without defined resource and provider %s', self)
919
+ return rst
920
+
921
+ def clear(self):
922
+ """Clear the local container contents."""
923
+ rst = False
924
+ if self.container:
925
+ logger.debug('Remove resource container %s', self.container)
926
+ rst = self.container.clear()
927
+ self.history.append(self.container.actualpath(), 'clear', rst)
928
+ self._notifyclear()
929
+ stage_clear_mapping = dict(expected='checked', get='checked')
930
+ if self.stage in stage_clear_mapping:
931
+ self._updstage(stage_clear_mapping[self.stage])
932
+ return rst
933
+
934
+ def mkgetpr(self, pr_getter=None, tplfile=None, tplskip='@sync-skip.tpl',
935
+ tplfetch='@sync-fetch.tpl', py_exec=sys.executable, py_opts=''):
936
+ """Build a getter for the expected resource."""
937
+ if tplfile is None:
938
+ tplfile = tplfetch if self.is_expected() else tplskip
939
+ if pr_getter is None:
940
+ pr_getter = self.container.localpath() + '.getpr'
941
+ t = self._cur_session
942
+ tpl = config.load_template(t, tplfile)
943
+ with open(pr_getter, 'w', encoding='utf-8') as fd:
944
+ fd.write(tpl.substitute(
945
+ python=py_exec,
946
+ pyopts=py_opts,
947
+ promise=self.container.localpath(),
948
+ ))
949
+ t.sh.chmod(pr_getter, 0o555)
950
+ return pr_getter
951
+
952
+ @property
953
+ def _localpr_json(self):
954
+ if self.is_expected():
955
+ if self._localpr_cache is None:
956
+ self._localpr_cache = self._cur_session.sh.json_load(self.container.localpath())
957
+ return self._localpr_cache
958
+ else:
959
+ return None
960
+
961
+ def is_grabable(self, check_exists=False):
962
+ """Return if an expected resource is available or not.
963
+
964
+ Note: If it returns True, the user still needs to :meth:`get` the resource.
965
+ """
966
+ rc = True
967
+ if self.is_expected():
968
+ pr = self._localpr_json
969
+ itself = pr.get('itself')
970
+ rc = not self._cur_session.sh.path.exists(itself)
971
+ if rc and check_exists:
972
+ remote = pr.get('locate').split(';')[0]
973
+ rc = self._cur_session.sh.path.exists(remote)
974
+ return rc
975
+
976
+ def wait(self, sleep=10, timeout=300, fatal=False):
977
+ """Wait for an expected resource or return immediately."""
978
+ rc = True
979
+ local = self.container.localpath()
980
+ if self.is_expected():
981
+ nb = 0
982
+ sh = self._cur_session.sh
983
+ pr = self._localpr_json
984
+ itself = pr.get('itself')
985
+ nbtries = int(timeout / sleep)
986
+ logger.info('Waiting %d x %d s. for expected resource <%s>', nbtries, sleep, local)
987
+ while sh.path.exists(itself):
988
+ sh.sleep(sleep)
989
+ nb += 1
990
+ if nb > nbtries:
991
+ logger.error('Could not wait anymore <%d>', nb)
992
+ rc = False
993
+ if fatal:
994
+ logger.critical('Missing expected resource is fatal <%s>', local)
995
+ raise HandlerError('Expected resource missing')
996
+ break
997
+ else:
998
+ remote = pr.get('locate').split(';')[0]
999
+ if sh.path.exists(remote):
1000
+ logger.info('Keeping promise for remote resource <%s>', remote)
1001
+ else:
1002
+ logger.warning('Empty promise for remote resource <%s>', remote)
1003
+ rc = False
1004
+ else:
1005
+ logger.info('Resource <%s> not expected', local)
1006
+ return rc
1007
+
1008
+ def save(self):
1009
+ """Rewrite data if contents have been updated."""
1010
+ rst = False
1011
+ if self.contents:
1012
+ rst = self.contents.rewrite(self.container)
1013
+ if not self.container.is_virtual():
1014
+ self.container.close()
1015
+ else:
1016
+ logger.warning('Try to save undefined contents %s', self)
1017
+ return rst
1018
+
1019
+ def strlast(self):
1020
+ """String formatted log of the last action."""
1021
+ return ' '.join([str(x) for x in self.history.last])