vortex-nwp 2.0.0b1__py3-none-any.whl

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