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