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,1220 @@
1
+ """
2
+ This modules defines the low level physical layout for data handling.
3
+ """
4
+
5
+ from collections import namedtuple, defaultdict
6
+ import collections.abc
7
+ import json
8
+ import pprint
9
+ import re
10
+ import traceback
11
+ import weakref
12
+
13
+ from bronx.fancies import loggers
14
+ from bronx.patterns import observer
15
+ from bronx.syntax import mktuple
16
+ from bronx.syntax.pretty import EncodedPrettyPrinter
17
+ import footprints
18
+
19
+ from vortex.util.roles import setrole
20
+
21
+ #: No automatic export.
22
+ __all__ = []
23
+
24
+ logger = loggers.getLogger(__name__)
25
+
26
+ _RHANDLERS_OBSBOARD = "Resources-Handlers"
27
+
28
+
29
+ class SectionFatalError(Exception):
30
+ """Exception when fatal mode is activated."""
31
+
32
+ pass
33
+
34
+
35
+ #: Definition of a named tuple INTENT
36
+ IntentTuple = namedtuple("IntentTuple", ["IN", "OUT", "INOUT"])
37
+
38
+ #: Predefined INTENT values IN, OUT and INOUT.
39
+ intent = IntentTuple(IN="in", OUT="out", INOUT="inout")
40
+
41
+ #: Definition of a named tuple IXO sequence
42
+ IXOTuple = namedtuple("IXOTuple", ["INPUT", "OUTPUT", "EXEC"])
43
+
44
+ #: Predefined IXO sequence values INPUT, OUTPUT and EXEC.
45
+ ixo = IXOTuple(INPUT=1, OUTPUT=2, EXEC=3)
46
+
47
+ #: Arguments specific to a section (to be striped away from a resource handler description)
48
+ section_args = ["role", "alternate", "intent", "fatal", "coherentgroup"]
49
+
50
+
51
+ def stripargs_section(**kw):
52
+ """
53
+ Utility function to separate the named arguments in two parts: the one that
54
+ describe section options and any other ones. Return a tuple with
55
+ ( section_options, other_options ).
56
+ """
57
+ opts = dict()
58
+ for opt in [x for x in section_args if x in kw]:
59
+ opts[opt] = kw.pop(opt)
60
+ return (opts, kw)
61
+
62
+
63
+ class _ReplaceSectionArgs:
64
+ """
65
+ Trigger the footprint's replacement mechanism on some of the section arguments.
66
+ """
67
+
68
+ _REPL_TODO = ("coherentgroup",)
69
+
70
+ def __init__(self):
71
+ self._fptmp = footprints.Footprint(
72
+ attr={k: dict(optional=True) for k in self._REPL_TODO}
73
+ )
74
+
75
+ def __call__(self, rh, opts):
76
+ if any(
77
+ {
78
+ footprints.replattr.search(opts[k])
79
+ for k in self._REPL_TODO
80
+ if k in opts
81
+ }
82
+ ):
83
+ # The "description"
84
+ desc = opts.copy()
85
+ if rh is not None:
86
+ desc.update(rh.options)
87
+ desc["container"] = rh.container
88
+ desc["provider"] = rh.provider
89
+ desc["resource"] = rh.resource
90
+ # Resolve
91
+ resolved, _, _ = self._fptmp.resolve(desc, fatal=False, fast=False)
92
+ # ok, let's use the resolved values
93
+ for k in self._REPL_TODO:
94
+ if resolved[k] is not None:
95
+ opts[k] = resolved[k]
96
+
97
+
98
+ _default_replace_section_args = _ReplaceSectionArgs()
99
+
100
+
101
+ class Section:
102
+ """Low level unit to handle a resource."""
103
+
104
+ def __init__(self, **kw):
105
+ logger.debug("Section initialisation %s", self)
106
+ self.kind = ixo.INPUT
107
+ self.intent = intent.INOUT
108
+ self.fatal = True
109
+ # Fetch the ResourceHandler
110
+ self._rh = kw.pop("rh", None)
111
+ # We realy need a ResourceHandler...
112
+ if self.rh is None:
113
+ raise AttributeError("A proper rh attribute have to be provided")
114
+ # Call the footprint's replacement mechanism if needed
115
+ _default_replace_section_args(self._rh, kw)
116
+ # Process the remaining options
117
+ self._role = setrole(kw.pop("role", "anonymous"))
118
+ self._alternate = setrole(kw.pop("alternate", None))
119
+ self._coherentgroups = kw.pop("coherentgroup", None)
120
+ self._coherentgroups = set(
121
+ self._coherentgroups.split(",") if self._coherentgroups else []
122
+ )
123
+ self._coherentgroups_opened = {g: True for g in self._coherentgroups}
124
+ self.stages = [
125
+ kw.pop("stage", "load"),
126
+ ]
127
+ self.__dict__.update(kw)
128
+ # If alternate is specified role have to be removed
129
+ if self._alternate:
130
+ self._role = None
131
+
132
+ @property
133
+ def role(self):
134
+ return self._role
135
+
136
+ @property
137
+ def alternate(self):
138
+ return self._alternate
139
+
140
+ @property
141
+ def coherentgroups(self):
142
+ """The list of belonging coherent groups."""
143
+ return self._coherentgroups
144
+
145
+ @property
146
+ def any_coherentgroup_opened(self):
147
+ """Is, at least, one belonging coherent group opened ?"""
148
+ return not self.coherentgroups or any(
149
+ self._coherentgroups_opened.values()
150
+ )
151
+
152
+ def coherent_group_close(self, group):
153
+ """Close the coherent group (get and put will fail from now and on)."""
154
+ if group in self._coherentgroups_opened:
155
+ self._coherentgroups_opened[group] = False
156
+ # Another group's resource failed, re-checking and possibly deleting myself !
157
+ if (
158
+ self.stage in ("expected", "get")
159
+ and not self.any_coherentgroup_opened
160
+ ):
161
+ logger.info(
162
+ "Clearing %s because of the coherent group failure.",
163
+ str(self.rh.container),
164
+ )
165
+ self.rh.clear()
166
+
167
+ def check_groupstatus(self, info):
168
+ """Given the updstage's info dict, check that a coherent group still holds"""
169
+ return info.get("stage") != "void"
170
+
171
+ @property
172
+ def rh(self):
173
+ return self._rh
174
+
175
+ @property
176
+ def stage(self):
177
+ """The last stage of the current section."""
178
+ return self.stages[-1]
179
+
180
+ def _updignore(self, info):
181
+ """Fake function for undefined information driven updates."""
182
+ logger.warning("Unable to update %s with info %s", self, info)
183
+
184
+ def _updstage_void(self, info):
185
+ """Upgrade current section to 'checked' level."""
186
+ if info.get("stage") == "void" and self.kind in (ixo.INPUT, ixo.EXEC):
187
+ self.stages.append("void")
188
+
189
+ def _updstage_checked(self, info):
190
+ """Upgrade current section to 'checked' level."""
191
+ if info.get("stage") == "checked" and self.kind in (
192
+ ixo.INPUT,
193
+ ixo.EXEC,
194
+ ):
195
+ self.stages.append("checked")
196
+
197
+ def _updstage_get(self, info):
198
+ """Upgrade current section to 'get' level."""
199
+ if info.get("stage") == "get" and self.kind in (ixo.INPUT, ixo.EXEC):
200
+ self.stages.append("get")
201
+
202
+ def _updstage_expected(self, info):
203
+ """Upgrade current section to 'expected' level."""
204
+ if info.get("stage") == "expected" and self.kind in (
205
+ ixo.INPUT,
206
+ ixo.EXEC,
207
+ ):
208
+ self.stages.append("expected")
209
+
210
+ def _updstage_put(self, info):
211
+ """Upgrade current section to 'put' level."""
212
+ if info.get("stage") == "put" and self.kind == ixo.OUTPUT:
213
+ self.stages.append("put")
214
+
215
+ def _updstage_ghost(self, info):
216
+ """Upgrade current section to 'ghost' level."""
217
+ if info.get("stage") == "ghost" and self.kind == ixo.OUTPUT:
218
+ self.stages.append("ghost")
219
+
220
+ def updstage(self, info):
221
+ """Upgrade current section level according to information given in dict ``info``."""
222
+ updmethod = getattr(
223
+ self, "_updstage_" + info.get("stage"), self._updignore
224
+ )
225
+ updmethod(info)
226
+
227
+ def _stronglocate(self, **kw):
228
+ """A locate call that can not fail..."""
229
+ try:
230
+ loc = self.rh.locate(**kw)
231
+ except Exception:
232
+ loc = "???"
233
+ return loc
234
+
235
+ def _fatal_wrap(self, sectiontype, callback, **kw):
236
+ """Launch **callback** and process the returncode/exceptions according to **fatal**."""
237
+ action = {"input": "get", "output": "put"}[sectiontype]
238
+ rc = False
239
+ try:
240
+ rc = callback(**kw)
241
+ except Exception as e:
242
+ logger.error(
243
+ "Something wrong (%s section): %s. %s",
244
+ sectiontype,
245
+ str(e),
246
+ traceback.format_exc(),
247
+ )
248
+ logger.error("Resource %s", self._stronglocate())
249
+ if not rc and self.fatal:
250
+ logger.critical(
251
+ "Fatal error with action %s on %s",
252
+ action,
253
+ self._stronglocate(),
254
+ )
255
+ raise SectionFatalError(
256
+ "Could not {:s} resource {!s}".format(action, rc)
257
+ )
258
+ return rc
259
+
260
+ def _just_fail(self, sectiontype, **kw): # @UnusedVariable
261
+ """Check if a resource exists but fails anyway."""
262
+ action = {"input": "get", "output": "put"}[sectiontype]
263
+ rc = False
264
+ if self.fatal:
265
+ logger.critical(
266
+ "Fatal error with action %s on %s",
267
+ action,
268
+ self._stronglocate(),
269
+ )
270
+ raise SectionFatalError(
271
+ "Could not {:s} resource {!s}".format(action, rc)
272
+ )
273
+ return rc
274
+
275
+ def get(self, **kw):
276
+ """Shortcut to resource handler :meth:`~vortex.data.handlers.get`."""
277
+ if self.kind == ixo.INPUT or self.kind == ixo.EXEC:
278
+ if self.any_coherentgroup_opened:
279
+ kw["intent"] = self.intent
280
+ if self.alternate:
281
+ kw["alternate"] = self.alternate
282
+ rc = self._fatal_wrap("input", self.rh.get, **kw)
283
+ else:
284
+ logger.info("The coherent group is closed... doing nothing.")
285
+ rc = self._just_fail("input")
286
+ else:
287
+ rc = False
288
+ logger.error("Try to get from an output section")
289
+ return rc
290
+
291
+ def finaliseget(self):
292
+ """Shortcut to resource handler :meth:`~vortex.data.handlers.finaliseget`."""
293
+ if self.kind == ixo.INPUT or self.kind == ixo.EXEC:
294
+ if self.any_coherentgroup_opened:
295
+ rc = self._fatal_wrap("input", self.rh.finaliseget)
296
+ else:
297
+ logger.info("The coherent group is closed... doing nothing.")
298
+ rc = self._just_fail("input")
299
+ else:
300
+ rc = False
301
+ logger.error("Try to get from an output section")
302
+ return rc
303
+
304
+ def earlyget(self, **kw):
305
+ """Shortcut to resource handler :meth:`~vortex.data.handlers.earlyget`."""
306
+ rc = False
307
+ if self.kind == ixo.INPUT or self.kind == ixo.EXEC:
308
+ if self.any_coherentgroup_opened:
309
+ kw["intent"] = self.intent
310
+ if self.alternate:
311
+ kw["alternate"] = self.alternate
312
+ rc = self.rh.earlyget(**kw)
313
+ else:
314
+ rc = None
315
+ return rc
316
+
317
+ def put(self, **kw):
318
+ """Shortcut to resource handler :meth:`~vortex.data.handlers.put`."""
319
+ if self.kind == ixo.OUTPUT:
320
+ if self.any_coherentgroup_opened:
321
+ kw["intent"] = self.intent
322
+ rc = self._fatal_wrap("output", self.rh.put, **kw)
323
+ else:
324
+ logger.info("The coherent group is closed... failing !.")
325
+ rc = False
326
+ if self.fatal:
327
+ logger.critical(
328
+ "Fatal error with action put on %s",
329
+ self._stronglocate(),
330
+ )
331
+ raise SectionFatalError(
332
+ "Could not get resource {!s}".format(rc)
333
+ )
334
+ else:
335
+ rc = False
336
+ logger.error("Try to put from an input section.")
337
+ return rc
338
+
339
+ def show(self, **kw):
340
+ """Nice dump of the section attributes and contents."""
341
+ for k, v in sorted(vars(self).items()):
342
+ if k != "rh":
343
+ print(" ", k.ljust(16), ":", v)
344
+ self.rh.quickview(indent=1)
345
+
346
+ def as_dict(self):
347
+ """Export the section in a dictionary"""
348
+ outdict = dict()
349
+ for k, v in sorted(vars(self).items()):
350
+ if k == "_rh":
351
+ outdict["rh"] = v.as_dict()
352
+ elif k == "_coherentgroups":
353
+ outdict["coherentgroup"] = ",".join(sorted(v))
354
+ elif k == "_coherentgroups_opened":
355
+ continue
356
+ elif k.startswith("_"):
357
+ outdict[k[1:]] = v
358
+ else:
359
+ outdict[k] = v
360
+ # Add the latest stage
361
+ outdict["stage"] = self.stage
362
+ return outdict
363
+
364
+
365
+ class Sequence(observer.Observer):
366
+ """
367
+ Logical sequence of sections such as inputs or outputs sections.
368
+ Instances are iterable and callable.
369
+ """
370
+
371
+ def __init__(self, *args, **kw):
372
+ logger.debug("Sequence initialisation %s", self)
373
+ self.sections = list()
374
+ # This hash table will be used to speedup the searches...
375
+ # If one uses the remove method, a WeakSet is not usefull. However,
376
+ # nothing will prevent the user from trashing the sections list...
377
+ # consequently a WealSet is safer !
378
+ self._sections_hash = defaultdict(weakref.WeakSet)
379
+ self._coherentgroups = defaultdict(weakref.WeakSet)
380
+ self._coherentgroups_openings = defaultdict(lambda: True)
381
+ observer.get(tag=_RHANDLERS_OBSBOARD).register(self)
382
+
383
+ def __del__(self):
384
+ observer.get(tag=_RHANDLERS_OBSBOARD).unregister(self)
385
+
386
+ def __iter__(self):
387
+ yield from self.sections
388
+
389
+ def __call__(self):
390
+ return self.sections[:]
391
+
392
+ def free_resources(self):
393
+ """Free contents and io descriptors on every sections."""
394
+ for section in self.sections:
395
+ section.rh.reset_contents()
396
+ if section.rh.container is not None:
397
+ section.rh.container.close()
398
+
399
+ def clear(self):
400
+ """Clear the internal list of sections."""
401
+ self.sections = list()
402
+ self._sections_hash.clear()
403
+
404
+ def add(self, candidate):
405
+ """
406
+ Push the ``candidate`` to the internal list of sections
407
+ as long as it is a :class:`Section` object.
408
+ """
409
+ if isinstance(candidate, Section):
410
+ self.sections.append(candidate)
411
+ self._sections_hash[candidate.rh.simplified_hashkey].add(candidate)
412
+ for cgroup in candidate.coherentgroups:
413
+ self._coherentgroups[cgroup].add(candidate)
414
+ if not self._coherentgroups_openings[cgroup]:
415
+ candidate.coherent_group_close(cgroup)
416
+ else:
417
+ logger.warning(
418
+ "Try to add a non-section object %s in sequence %s",
419
+ candidate,
420
+ self,
421
+ )
422
+
423
+ def remove(self, candidate):
424
+ """
425
+ Remove the ``candidate`` from the internal list of sections
426
+ as long as it is a :class:`Section` object.
427
+ """
428
+ if isinstance(candidate, Section):
429
+ self.sections.remove(candidate)
430
+ self._sections_hash[candidate.rh.simplified_hashkey].discard(
431
+ candidate
432
+ )
433
+ for cgroup in candidate.coherentgroups:
434
+ self._coherentgroups[cgroup].discard(candidate)
435
+ else:
436
+ logger.warning(
437
+ "Try to remove a non-section object %s in sequence %s",
438
+ candidate,
439
+ self,
440
+ )
441
+
442
+ def section(self, **kw):
443
+ """Section factory wrapping a given ``rh`` (Resource Handler)."""
444
+ rhset = kw.get("rh", list())
445
+ if not isinstance(rhset, list):
446
+ rhset = [
447
+ rhset,
448
+ ]
449
+ ralter = kw.get("alternate", kw.get("role", "anonymous"))
450
+ newsections = list()
451
+ for rh in rhset:
452
+ kw["rh"] = rh
453
+ this_section = Section(**kw)
454
+ self.add(this_section)
455
+ newsections.append(this_section)
456
+ kw["alternate"] = ralter
457
+ if "role" in kw:
458
+ del kw["role"]
459
+ return newsections
460
+
461
+ def input(self, **kw):
462
+ """Create a section with default kind equal to ``ixo.INPUT``."""
463
+ if "kind" in kw:
464
+ del kw["kind"]
465
+ kw.setdefault("intent", intent.IN)
466
+ return self.section(kind=ixo.INPUT, **kw)
467
+
468
+ def output(self, **kw):
469
+ """Create a section with default kind equal to ``ixo.OUTPUT`` and intent equal to ``intent.OUT``."""
470
+ if "kind" in kw:
471
+ del kw["kind"]
472
+ kw.setdefault("intent", intent.OUT)
473
+ return self.section(kind=ixo.OUTPUT, **kw)
474
+
475
+ def executable(self, **kw):
476
+ """Create a section with default kind equal to to ``ixo.EXEC``."""
477
+ if "kind" in kw:
478
+ del kw["kind"]
479
+ kw.setdefault("intent", intent.IN)
480
+ return self.section(kind=ixo.EXEC, **kw)
481
+
482
+ @staticmethod
483
+ def _fuzzy_match(stuff, allowed):
484
+ """Check if ``stuff`` is in ``allowed``. ``allowed`` may contain regex."""
485
+ if isinstance(allowed, str) or not isinstance(
486
+ allowed, collections.abc.Iterable
487
+ ):
488
+ allowed = [
489
+ allowed,
490
+ ]
491
+ for pattern in allowed:
492
+ if (isinstance(pattern, re.Pattern) and pattern.search(stuff)) or (
493
+ pattern == stuff
494
+ ):
495
+ return True
496
+ return False
497
+
498
+ def _section_list_filter(self, sections, **kw):
499
+ if not kw:
500
+ return list(sections)
501
+ inrole = list()
502
+ inkind = list()
503
+ with_alternates = not kw.get("no_alternates", False)
504
+ if "role" in kw and kw["role"] is not None:
505
+ selectrole = mktuple(kw["role"])
506
+ inrole = [
507
+ x
508
+ for x in sections
509
+ if (
510
+ (
511
+ x.role is not None
512
+ and self._fuzzy_match(x.role, selectrole)
513
+ )
514
+ or (
515
+ with_alternates
516
+ and x.alternate is not None
517
+ and self._fuzzy_match(x.alternate, selectrole)
518
+ )
519
+ )
520
+ ]
521
+ if not inrole and "kind" in kw:
522
+ selectkind = mktuple(kw["kind"])
523
+ inkind = [
524
+ x
525
+ for x in sections
526
+ if self._fuzzy_match(x.rh.resource.realkind, selectkind)
527
+ ]
528
+ return inrole or inkind
529
+
530
+ def inputs(self):
531
+ """Return a list of current sequence sections with ``ixo.INPUT`` or ``ixo.EXEC`` kind."""
532
+ for s in self.sections:
533
+ if s.kind == ixo.INPUT or s.kind == ixo.EXEC:
534
+ yield s
535
+
536
+ def rinputs(self):
537
+ """The reversed list of input sections."""
538
+ for s in reversed(self.sections):
539
+ if s.kind == ixo.INPUT or s.kind == ixo.EXEC:
540
+ yield s
541
+
542
+ def inputs_report(self):
543
+ """Return a SequenceInputsReport object built using the current sequence."""
544
+ return SequenceInputsReport(self.inputs())
545
+
546
+ def effective_inputs(self, **kw):
547
+ """
548
+ Similar to :meth:`filtered_inputs` but only walk through the inputs of
549
+ that reached the 'get' or 'expected' stage.
550
+ """
551
+ return [
552
+ x
553
+ for x in self._section_list_filter(list(self.inputs()), **kw)
554
+ if (x.stage == "get" or x.stage == "expected")
555
+ and x.rh.container.exists()
556
+ ]
557
+
558
+ def filtered_inputs(self, **kw):
559
+ """Walk through the inputs of the current sequence.
560
+
561
+ If a ``role`` or ``kind`` (or both) is provided as named argument,
562
+ it operates as a filter on the inputs list. If both keys are available
563
+ the ``role`` applies first, and then the ``kind`` in case of empty match.
564
+
565
+ The ``role`` or ``kind`` named arguments are lists that may contain
566
+ strings and/or compiled regular expressions. Regular expressions are c
567
+ hacked against the input's attributes using the 'search' function
568
+ (i.e. ^ should be explicitly added if one wants to match the beginning
569
+ of the string).
570
+ """
571
+ return self._section_list_filter(list(self.inputs()), **kw)
572
+
573
+ def is_somehow_viable(self, section):
574
+ """Tells wether *section* is ok or has a viable alternate."""
575
+ if section.role is None:
576
+ raise ValueError(
577
+ "An alternate section was given ; this is incorrect..."
578
+ )
579
+ if (
580
+ section.stage in ("get", "expected")
581
+ and section.rh.container.exists()
582
+ ):
583
+ return section
584
+ else:
585
+ for isec in self.inputs():
586
+ if (
587
+ isec.alternate == section.role
588
+ and isec.stage in ("get", "expected")
589
+ and isec.rh.container.localpath()
590
+ == section.rh.container.localpath()
591
+ and isec.rh.container.exists()
592
+ ):
593
+ return isec
594
+ return None
595
+
596
+ def executables(self):
597
+ """Return a list of current sequence sections with ``ixo.EXEC`` kind."""
598
+ return [x for x in self.sections if x.kind == ixo.EXEC]
599
+
600
+ def outputs(self):
601
+ """Return a list of current sequence sections with ``ixo.OUTPUT`` kind."""
602
+ for s in self.sections:
603
+ if s.kind == ixo.OUTPUT:
604
+ yield s
605
+
606
+ def effective_outputs(self, **kw):
607
+ """
608
+ Walk through the outputs of the current sequence whatever the stage value is.
609
+ If a ``role`` or ``kind`` (or both) is provided as named argument,
610
+ it operates as a filter on the inputs list. If both keys are available
611
+ the ``role`` applies first, and then the ``kind`` in case of empty match.
612
+ """
613
+ return self._section_list_filter(list(self.outputs()), **kw)
614
+
615
+ def coherentgroup_iter(self, cgroup):
616
+ """Iterate over sections belonging to a given coherentgroup."""
617
+ c_sections = self._coherentgroups[cgroup]
618
+ yield from c_sections
619
+
620
+ def section_updstage(self, a_section, info):
621
+ """
622
+ Update the section's stage but also check other sections from the same
623
+ coherent group.
624
+ """
625
+ a_section.updstage(info)
626
+
627
+ def _s_group_check(s):
628
+ return s.check_groupstatus(info)
629
+
630
+ for cgroup in a_section.coherentgroups:
631
+ if self._coherentgroups_openings[cgroup]:
632
+ if not all(
633
+ map(_s_group_check, self.coherentgroup_iter(cgroup))
634
+ ):
635
+ for c_section in self.coherentgroup_iter(cgroup):
636
+ c_section.coherent_group_close(cgroup)
637
+ self._coherentgroups_openings[cgroup] = False
638
+
639
+ def updobsitem(self, item, info):
640
+ """
641
+ Resources-Handlers observing facility.
642
+ Track hashkey alteration for the resource handler ``item``.
643
+ """
644
+ if info["observerboard"] == _RHANDLERS_OBSBOARD and "oldhash" in info:
645
+ logger.debug("Notified %s upd item %s", self, item)
646
+ oldhash = info["oldhash"]
647
+ # First remove the oldhash
648
+ if oldhash in self._sections_hash:
649
+ for section in [
650
+ s for s in self._sections_hash[oldhash] if s.rh is item
651
+ ]:
652
+ self._sections_hash[oldhash].discard(section)
653
+ # Then add the new hash: This is relatively slow so that it should not be used much...
654
+ for section in [s for s in self.sections if s.rh is item]:
655
+ self._sections_hash[section.rh.simplified_hashkey].add(section)
656
+
657
+ def fastsearch(self, skeleton):
658
+ """
659
+ Uses the sections hash table to significantly speed-up searches.
660
+
661
+ The fastsearch method returns a list of possible candidates (given the
662
+ skeleton). It is of the user responsibility to check each of the
663
+ returned sections to verify if it exactly matches or not.
664
+ """
665
+ try:
666
+ hkey = skeleton.simplified_hashkey
667
+ trydict = False
668
+ except AttributeError:
669
+ trydict = True
670
+ if not trydict:
671
+ return self._sections_hash[hkey]
672
+ elif trydict and isinstance(skeleton, dict):
673
+ # We assume it is a resource handler dictionary
674
+ try:
675
+ hkey = (
676
+ skeleton["resource"].get("kind", None),
677
+ skeleton["container"].get("filename", None),
678
+ )
679
+ except KeyError:
680
+ logger.critical(
681
+ "This is probably not a ResourceHandler dictionary."
682
+ )
683
+ raise
684
+ return self._sections_hash[hkey]
685
+ raise ValueError(
686
+ "Cannot process a {!s} type skeleton".format(type(skeleton))
687
+ )
688
+
689
+
690
+ #: Class of a list of statuses
691
+ InputsReportStatusTupple = namedtuple(
692
+ "InputsReportStatusTupple",
693
+ ("PRESENT", "EXPECTED", "CHECKED", "MISSING", "UNUSED"),
694
+ )
695
+
696
+
697
+ #: Possible statuses used in :class:`SequenceInputsReport` objects
698
+ InputsReportStatus = InputsReportStatusTupple(
699
+ PRESENT="present",
700
+ EXPECTED="expected",
701
+ CHECKED="checked",
702
+ MISSING="missing",
703
+ UNUSED="unused",
704
+ )
705
+
706
+
707
+ class SequenceInputsReport:
708
+ """Summarize data about inputs (missing resources, alternates, ...)."""
709
+
710
+ _TranslateStage = dict(
711
+ get=InputsReportStatus.PRESENT,
712
+ expected=InputsReportStatus.EXPECTED,
713
+ checked=InputsReportStatus.CHECKED,
714
+ void=InputsReportStatus.MISSING,
715
+ load=InputsReportStatus.UNUSED,
716
+ )
717
+
718
+ def __init__(self, inputs):
719
+ self._local_map = defaultdict(lambda: defaultdict(list))
720
+ for insec in inputs:
721
+ local = insec.rh.container.localpath()
722
+ # Determine if the current section is an alternate or not...
723
+ kind = "alternate" if insec.alternate is not None else "nominal"
724
+ self._local_map[local][kind].append(insec)
725
+
726
+ def _local_status(self, local):
727
+ """Find out the local resource status (see InputsReportStatus).
728
+
729
+ It returns a tuple that contains:
730
+
731
+ * The local resource status (see InputsReportStatus)
732
+ * The resource handler that was actually used to get the resource
733
+ * The resource handler that should have been used in the nominal case
734
+ """
735
+ desc = self._local_map[local]
736
+ # First, check the nominal resource
737
+ if len(desc["nominal"]) > 0:
738
+ nominal = desc["nominal"][-1]
739
+ status = self._TranslateStage[nominal.stage]
740
+ true_rh = nominal.rh
741
+ else:
742
+ logger.warning(
743
+ "No nominal section for < %s >. This should not happened !",
744
+ local,
745
+ )
746
+ nominal = None
747
+ status = None
748
+ true_rh = None
749
+ # Look for alternates:
750
+ if status not in (
751
+ InputsReportStatus.PRESENT,
752
+ InputsReportStatus.EXPECTED,
753
+ ):
754
+ for alter in desc["alternate"]:
755
+ alter_status = self._TranslateStage[alter.stage]
756
+ if alter_status in (
757
+ InputsReportStatus.PRESENT,
758
+ InputsReportStatus.EXPECTED,
759
+ ):
760
+ status = alter_status
761
+ true_rh = alter.rh
762
+ break
763
+ return status, true_rh, (nominal.rh if nominal else None)
764
+
765
+ def synthetic_report(self, detailed=False, only=None):
766
+ """Returns a string that describes each local resource with its status.
767
+
768
+ :param bool detailed: when alternates are used, tell which resource handler
769
+ is actually used and which one should have been used
770
+ in the nominal case.
771
+ :param list[str] only: Output only the listed statuses (statuses are defined in
772
+ :data:`InputsReportStatus`). By default (*None*), output
773
+ everything. Note that "alternates" are always shown.
774
+ """
775
+ if only is None:
776
+ # The default is to display everything
777
+ only = list(InputsReportStatus)
778
+ else:
779
+ # Convert a single string to a list
780
+ if isinstance(only, str):
781
+ only = [
782
+ only,
783
+ ]
784
+ # Check that the provided statuses exist
785
+ if not all([f in InputsReportStatus for f in only]):
786
+ return "* The only attribute is wrong ! ({!s})".format(only)
787
+
788
+ outstr = ""
789
+ for local in sorted(self._local_map):
790
+ # For each and every local file, check alternates and find out the status
791
+ status, true_rh, nominal_rh = self._local_status(local)
792
+ extrainfo = ""
793
+ # Detect alternates
794
+ is_alternate = status != InputsReportStatus.MISSING and (
795
+ true_rh is not nominal_rh
796
+ )
797
+ if is_alternate:
798
+ extrainfo = "(ALTERNATE USED)"
799
+ # Alternates are always printed. Otherwise rely on **only**
800
+ if is_alternate or status in only:
801
+ outstr += "* {:8s} {:16s} : {:s}\n".format(
802
+ status, extrainfo, local
803
+ )
804
+ if detailed and extrainfo != "":
805
+ outstr += " * The following resource is used:\n"
806
+ outstr += true_rh.idcard(indent=4) + "\n"
807
+ if nominal_rh is not None:
808
+ outstr += " * Instead of:\n"
809
+ outstr += nominal_rh.idcard(indent=4) + "\n"
810
+
811
+ return outstr
812
+
813
+ def print_report(self, detailed=False, only=None):
814
+ """Print a list of each local resource with its status.
815
+
816
+ :param bool detailed: when alternates are used, tell which resource handler
817
+ is actually used and which one should have been used
818
+ in the nominal case.
819
+ :param list[str] only: Output only the listed statuses (statuses are defined in
820
+ :data:`InputsReportStatus`). By default (*None*), output
821
+ everything. Note that "alternates" are always shown.
822
+ """
823
+ print(self.synthetic_report(detailed=detailed, only=only))
824
+
825
+ def active_alternates(self):
826
+ """List the local resource for which an alternative resource has been used.
827
+
828
+ It returns a dictionary that associates the local resource name with
829
+ a tuple that contains:
830
+
831
+ * The resource handler that was actually used to get the resource
832
+ * The resource handler that should have been used in the nominal case
833
+ """
834
+ outstack = dict()
835
+ for local in self._local_map:
836
+ status, true_rh, nominal_rh = self._local_status(local)
837
+ if status != InputsReportStatus.MISSING and (
838
+ true_rh is not nominal_rh
839
+ ):
840
+ outstack[local] = (true_rh, nominal_rh)
841
+ return outstack
842
+
843
+ def missing_resources(self):
844
+ """List the missing local resources."""
845
+ outstack = dict()
846
+ for local in self._local_map:
847
+ (
848
+ status,
849
+ true_rh, # @UnusedVariable
850
+ nominal_rh,
851
+ ) = self._local_status(local)
852
+ if status == InputsReportStatus.MISSING:
853
+ outstack[local] = nominal_rh
854
+ return outstack
855
+
856
+
857
+ def _fast_clean_uri(store, remote):
858
+ """Clean a URI so that it can be compared with a JSON load version."""
859
+ qsl = remote["query"].copy()
860
+ qsl.update(
861
+ {
862
+ "storearg_{:s}".format(k): v
863
+ for k, v in store.tracking_extraargs.items()
864
+ }
865
+ )
866
+ return {
867
+ "scheme": str(store.scheme),
868
+ "netloc": str(store.netloc),
869
+ "path": str(remote["path"]),
870
+ "params": str(remote["params"]),
871
+ "query": qsl,
872
+ "fragment": str(remote["fragment"]),
873
+ }
874
+
875
+
876
+ class LocalTrackerEntry:
877
+ """Holds the data for a given local container.
878
+
879
+ It includes data for two kinds of "actions": get/put. For each "action",
880
+ Involved resource handlers, hook functions calls and get/put from low level
881
+ stores are tracked.
882
+ """
883
+
884
+ _actions = (
885
+ "get",
886
+ "put",
887
+ )
888
+ _internals = ("rhdict", "hook", "uri")
889
+
890
+ def __init__(self, master_tracker=None):
891
+ """
892
+
893
+ :param master_tracker: The LocalTracker this entry belongs to.
894
+ """
895
+ self._data = dict()
896
+ self._master_tracker = master_tracker
897
+ for internal in self._internals:
898
+ self._data[internal] = {act: list() for act in self._actions}
899
+
900
+ @classmethod
901
+ def _check_action(cls, action):
902
+ return action in cls._actions
903
+
904
+ @staticmethod
905
+ def _jsonize(stuff):
906
+ """Make 'stuff' comparable to the result of a json.load."""
907
+ return json.loads(json.dumps(stuff))
908
+
909
+ def _clean_rhdict(self, rhdict):
910
+ if "options" in rhdict:
911
+ del rhdict["options"]
912
+ return self._jsonize(rhdict)
913
+
914
+ def update_rh(self, rh, info):
915
+ """Update the entry based on data received from the observer board.
916
+
917
+ This method is to be called with data originated from the
918
+ Resources-Handlers observer board (when updates are notified).
919
+
920
+ :param rh: :class:`~vortex.data.handlers.Handler` object that sends the update.
921
+ :param info: Info dictionary sent by the :class:`~vortex.data.handlers.Handler` object
922
+ """
923
+ stage = info["stage"]
924
+ if self._check_action(stage):
925
+ if "hook" in info:
926
+ self._data["hook"][stage].append(self._jsonize(info["hook"]))
927
+ elif not info.get("insitu", False):
928
+ # We are using as_dict since this may be written to a JSON file
929
+ self._data["rhdict"][stage].append(
930
+ self._clean_rhdict(rh.as_dict())
931
+ )
932
+
933
+ def _update_store(self, info, uri):
934
+ """Update the entry based on data received from the observer board.
935
+
936
+ This method is to be called with data originated from the
937
+ Stores-Activity observer board (when updates are notified).
938
+
939
+ :param info: Info dictionary sent by the :class:`~vortex.data.stores.Store` object
940
+ :param uri: A cleaned (i.e. compatible with JSON) representation of the URI
941
+ """
942
+ action = info["action"]
943
+ # Only known action and successfull attempts
944
+ if self._check_action(action) and info["status"]:
945
+ self._data["uri"][action].append(uri)
946
+ if self._master_tracker is not None:
947
+ self._master_tracker.uri_map_append(self, action, uri)
948
+
949
+ def dump_as_dict(self):
950
+ """Export the entry as a dictionary."""
951
+ return self._data
952
+
953
+ def load_from_dict(self, dumpeddict):
954
+ """Restore the entry from a previous export.
955
+
956
+ :param dumpeddict: Dictionary that will be loaded (usually generated by
957
+ the :meth:`dump_as_dict` method)
958
+ """
959
+ self._data = dumpeddict
960
+ for action in self._actions:
961
+ for uri in self._data["uri"][action]:
962
+ self._master_tracker.uri_map_append(self, action, uri)
963
+
964
+ def append(self, anotherentry):
965
+ """Append the content of another LocalTrackerEntry object into this one."""
966
+ for internal in self._internals:
967
+ for act in self._actions:
968
+ self._data[internal][act].extend(
969
+ anotherentry._data[internal][act]
970
+ )
971
+
972
+ def latest_rhdict(self, action):
973
+ """Return the dictionary that represents the latest :class:`~vortex.data.handlers.Handler` object involved.
974
+
975
+ :param action: Action that is considered.
976
+ """
977
+ if self._check_action(action) and self._data["rhdict"][action]:
978
+ return self._data["rhdict"][action][-1]
979
+ else:
980
+ return dict()
981
+
982
+ def match_rh(self, action, rh, verbose=False):
983
+ """Check if an :class:`~vortex.data.handlers.Handler` object matches the one stored internally.
984
+
985
+ :param action: Action that is considered
986
+ :param rh: :class:`~vortex.data.handlers.Handler` object that will be checked
987
+ """
988
+ if self._check_action(action):
989
+ cleaned = self._clean_rhdict(rh.as_dict())
990
+ latest = self.latest_rhdict(action)
991
+ res = latest == cleaned
992
+ if verbose and not res:
993
+ for key, item in latest.items():
994
+ newitem = cleaned.get(key, None)
995
+ if newitem != item:
996
+ logger.error("Expected %s:", key)
997
+ logger.error(pprint.pformat(item))
998
+ logger.error("Got:")
999
+ logger.error(pprint.pformat(newitem))
1000
+ return res
1001
+ else:
1002
+ return False
1003
+
1004
+ def _check_uri_remote_delete(self, uri):
1005
+ """Called when a :class:`~vortex.data.stores.Store` object notifies a delete.
1006
+
1007
+ The URIs stored for the "put" action are checked against the delete
1008
+ request. If a match is found, the URI is deleted.
1009
+
1010
+ :param uri: A cleaned (i.e. compatible with JSON) representation of the URI
1011
+ """
1012
+ while uri in self._data["uri"]["put"]:
1013
+ self._data["uri"]["put"].remove(uri)
1014
+ if self._master_tracker is not None:
1015
+ self._master_tracker.uri_map_remove(self, "put", uri)
1016
+
1017
+ def _redundant_stuff(self, internal, action, stuff):
1018
+ if self._check_action(action):
1019
+ return stuff in self._data[internal][action]
1020
+ else:
1021
+ return False
1022
+
1023
+ def redundant_hook(self, action, hookname):
1024
+ """Check of a hook function has already been applied.
1025
+
1026
+ :param action: Action that is considered.
1027
+ :param hookname: Name of the Hook function that will be checked.
1028
+ """
1029
+ return self._redundant_stuff("hook", action, self._jsonize(hookname))
1030
+
1031
+ def redundant_uri(self, action, store, remote):
1032
+ """Check if an URI has already been processed.
1033
+
1034
+ :param action: Action that is considered.
1035
+ :param store: :class:`~vortex.data.stores.Store` object that will be checked.
1036
+ :param remote: Remote path that will be checked.
1037
+ """
1038
+ return self._redundant_stuff(
1039
+ "uri", action, _fast_clean_uri(store, remote)
1040
+ )
1041
+
1042
+ def _grep_stuff(self, internal, action, skeleton=dict()):
1043
+ stack = []
1044
+ for element in self._data[internal][action]:
1045
+ if isinstance(element, collections.abc.Mapping):
1046
+ succeed = True
1047
+ for key, val in skeleton.items():
1048
+ succeed = succeed and (
1049
+ (key in element) and (element[key] == val)
1050
+ )
1051
+ if succeed:
1052
+ stack.append(element)
1053
+ return stack
1054
+
1055
+ def __str__(self):
1056
+ out = ""
1057
+ for action in self._actions:
1058
+ for internal in self._internals:
1059
+ if len(self._data[internal][action]) > 0:
1060
+ out += "+ {:4s} / {}\n{}\n".format(
1061
+ action.upper(),
1062
+ internal,
1063
+ EncodedPrettyPrinter().pformat(
1064
+ self._data[internal][action]
1065
+ ),
1066
+ )
1067
+ return out
1068
+
1069
+
1070
+ class LocalTracker(defaultdict):
1071
+ """Dictionary like structure that gathers data on the various local containers.
1072
+
1073
+ For each local container (identified by the result of its iotarget method), a
1074
+ dictionary entry is created. Its value is a :class:`~vortex.layout.dataflow.LocalTrackerEntry`
1075
+ object.
1076
+ """
1077
+
1078
+ _default_json_filename = "local-tracker-state.json"
1079
+
1080
+ def __init__(self):
1081
+ super().__init__()
1082
+ # This hash table will be used to speedup searches
1083
+ self._uri_map = defaultdict(lambda: defaultdict(weakref.WeakSet))
1084
+
1085
+ def __missing__(self, key):
1086
+ self[key] = LocalTrackerEntry(master_tracker=self)
1087
+ return self[key]
1088
+
1089
+ def _hashable_uri(self, uri):
1090
+ """Produces a version of the URI that is hashable."""
1091
+ listuri = list()
1092
+ for k in sorted(uri.keys()):
1093
+ listuri.append(k)
1094
+ if isinstance(uri[k], dict):
1095
+ listuri.append(self._hashable_uri(uri[k]))
1096
+ elif isinstance(uri[k], list):
1097
+ listuri.append(tuple(uri[k]))
1098
+ else:
1099
+ listuri.append(uri[k])
1100
+ return tuple(listuri)
1101
+
1102
+ def uri_map_remove(self, entry, action, uri):
1103
+ """Delete an entry in the URI hash table."""
1104
+ self._uri_map[action][self._hashable_uri(uri)].discard(entry)
1105
+
1106
+ def uri_map_append(self, entry, action, uri):
1107
+ """Add a new entry in the URI hash table."""
1108
+ self._uri_map[action][self._hashable_uri(uri)].add(entry)
1109
+
1110
+ def update_rh(self, rh, info):
1111
+ """Update the object based on data received from the observer board.
1112
+
1113
+ This method is to be called with data originated from the
1114
+ Resources-Handlers observer board (when updates are notified).
1115
+
1116
+ :param rh: :class:`~vortex.data.handlers.Handler` object that sends the update.
1117
+ :param info: Info dictionary sent by the :class:`~vortex.data.handlers.Handler` object
1118
+ """
1119
+ lpath = rh.container.iotarget()
1120
+ if isinstance(lpath, str):
1121
+ if info.get("clear", False):
1122
+ self.pop(lpath, None)
1123
+ else:
1124
+ self[lpath].update_rh(rh, info)
1125
+ else:
1126
+ logger.debug(
1127
+ "The iotarget is not a str: skipped in %s", self.__class__
1128
+ )
1129
+
1130
+ def update_store(self, store, info):
1131
+ """Update the object based on data received from the observer board.
1132
+
1133
+ This method is to be called with data originated from the
1134
+ Stores-Activity observer board (when updates are notified).
1135
+
1136
+ :param store: :class:`~vortex.data.stores.Store` object that sends the update.
1137
+ :param info: Info dictionary sent by the :class:`~vortex.data.stores.Store` object
1138
+ """
1139
+ lpath = info.get("local", None)
1140
+ if lpath is None:
1141
+ # Check for file deleted on the remote side
1142
+ if info["action"] == "del" and info["status"]:
1143
+ clean_uri = _fast_clean_uri(store, info["remote"])
1144
+ huri = self._hashable_uri(clean_uri)
1145
+ for atracker in list(self._uri_map["put"][huri]):
1146
+ atracker._check_uri_remote_delete(clean_uri)
1147
+ else:
1148
+ if isinstance(lpath, str):
1149
+ clean_uri = _fast_clean_uri(store, info["remote"])
1150
+ self[lpath]._update_store(info, clean_uri)
1151
+ else:
1152
+ logger.debug(
1153
+ "The iotarget isn't a str: It will be skipped in %s",
1154
+ self.__class__,
1155
+ )
1156
+
1157
+ def is_tracked_input(self, local):
1158
+ """Check if the given `local` container is listed as an input and associated with
1159
+ a valid :class:`~vortex.data.handlers.Handler`.
1160
+
1161
+ :param local: Local name of the input that will be checked
1162
+ """
1163
+ return (
1164
+ isinstance(local, str)
1165
+ and (local in self)
1166
+ and (self[local].latest_rhdict("get"))
1167
+ )
1168
+
1169
+ def _grep_stuff(self, internal, action, skeleton=dict()):
1170
+ stack = []
1171
+ for entry in self.values():
1172
+ stack.extend(entry._grep_stuff(internal, action, skeleton))
1173
+ return stack
1174
+
1175
+ def grep_uri(self, action, skeleton=dict()):
1176
+ """Returns all the URIs that contains the same key/values than `skeleton`.
1177
+
1178
+ :param action: Action that is considered.
1179
+ :param skeleton: Dictionary that will be used as a search pattern
1180
+ """
1181
+ return self._grep_stuff("uri", action, skeleton)
1182
+
1183
+ def json_dump(self, filename=_default_json_filename):
1184
+ """Dump the object to a JSON file.
1185
+
1186
+ :param filename: Path to the JSON file.
1187
+ """
1188
+ outdict = {loc: entry.dump_as_dict() for loc, entry in self.items()}
1189
+ with open(filename, "w", encoding="utf-8") as fpout:
1190
+ json.dump(outdict, fpout, indent=2, sort_keys=True)
1191
+
1192
+ def json_load(self, filename=_default_json_filename):
1193
+ """Restore the object using a JSON file.
1194
+
1195
+ :param filename: Path to the JSON file.
1196
+ """
1197
+ with open(filename, encoding="utf-8") as fpin:
1198
+ indict = json.load(fpin)
1199
+ # Start from scratch
1200
+ self.clear()
1201
+ for loc, adict in indict.items():
1202
+ self[loc].load_from_dict(adict)
1203
+
1204
+ def append(self, othertracker):
1205
+ """Append the content of another LocalTracker object into this one."""
1206
+ for loc, entry in othertracker.items():
1207
+ self[loc].append(entry)
1208
+
1209
+ def datastore_inplace_overwrite(self, other):
1210
+ """Used by a DataStore object to refill a LocalTracker."""
1211
+ self.clear()
1212
+ self.append(other)
1213
+
1214
+ def __str__(self):
1215
+ out = ""
1216
+ for loc, entry in self.items():
1217
+ entryout = str(entry)
1218
+ if entryout:
1219
+ out += "========== {} ==========\n{}".format(loc, entryout)
1220
+ return out