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.
- vortex/__init__.py +159 -0
- vortex/algo/__init__.py +13 -0
- vortex/algo/components.py +2462 -0
- vortex/algo/mpitools.py +1953 -0
- vortex/algo/mpitools_templates/__init__.py +1 -0
- vortex/algo/mpitools_templates/envelope_wrapper_default.tpl +27 -0
- vortex/algo/mpitools_templates/envelope_wrapper_mpiauto.tpl +29 -0
- vortex/algo/mpitools_templates/wrapstd_wrapper_default.tpl +18 -0
- vortex/algo/serversynctools.py +171 -0
- vortex/config.py +112 -0
- vortex/data/__init__.py +19 -0
- vortex/data/abstractstores.py +1510 -0
- vortex/data/containers.py +835 -0
- vortex/data/contents.py +622 -0
- vortex/data/executables.py +275 -0
- vortex/data/flow.py +119 -0
- vortex/data/geometries.ini +2689 -0
- vortex/data/geometries.py +799 -0
- vortex/data/handlers.py +1230 -0
- vortex/data/outflow.py +67 -0
- vortex/data/providers.py +487 -0
- vortex/data/resources.py +207 -0
- vortex/data/stores.py +1390 -0
- vortex/data/sync_templates/__init__.py +0 -0
- vortex/gloves.py +309 -0
- vortex/layout/__init__.py +20 -0
- vortex/layout/contexts.py +577 -0
- vortex/layout/dataflow.py +1220 -0
- vortex/layout/monitor.py +969 -0
- vortex/nwp/__init__.py +14 -0
- vortex/nwp/algo/__init__.py +21 -0
- vortex/nwp/algo/assim.py +537 -0
- vortex/nwp/algo/clim.py +1086 -0
- vortex/nwp/algo/coupling.py +831 -0
- vortex/nwp/algo/eda.py +840 -0
- vortex/nwp/algo/eps.py +785 -0
- vortex/nwp/algo/forecasts.py +886 -0
- vortex/nwp/algo/fpserver.py +1303 -0
- vortex/nwp/algo/ifsnaming.py +463 -0
- vortex/nwp/algo/ifsroot.py +404 -0
- vortex/nwp/algo/monitoring.py +263 -0
- vortex/nwp/algo/mpitools.py +694 -0
- vortex/nwp/algo/odbtools.py +1258 -0
- vortex/nwp/algo/oopsroot.py +916 -0
- vortex/nwp/algo/oopstests.py +220 -0
- vortex/nwp/algo/request.py +660 -0
- vortex/nwp/algo/stdpost.py +1641 -0
- vortex/nwp/data/__init__.py +30 -0
- vortex/nwp/data/assim.py +380 -0
- vortex/nwp/data/boundaries.py +314 -0
- vortex/nwp/data/climfiles.py +521 -0
- vortex/nwp/data/configfiles.py +153 -0
- vortex/nwp/data/consts.py +954 -0
- vortex/nwp/data/ctpini.py +149 -0
- vortex/nwp/data/diagnostics.py +209 -0
- vortex/nwp/data/eda.py +147 -0
- vortex/nwp/data/eps.py +432 -0
- vortex/nwp/data/executables.py +1045 -0
- vortex/nwp/data/fields.py +111 -0
- vortex/nwp/data/gridfiles.py +380 -0
- vortex/nwp/data/logs.py +584 -0
- vortex/nwp/data/modelstates.py +363 -0
- vortex/nwp/data/monitoring.py +193 -0
- vortex/nwp/data/namelists.py +696 -0
- vortex/nwp/data/obs.py +840 -0
- vortex/nwp/data/oopsexec.py +74 -0
- vortex/nwp/data/providers.py +207 -0
- vortex/nwp/data/query.py +206 -0
- vortex/nwp/data/stores.py +160 -0
- vortex/nwp/data/surfex.py +337 -0
- vortex/nwp/syntax/__init__.py +9 -0
- vortex/nwp/syntax/stdattrs.py +437 -0
- vortex/nwp/tools/__init__.py +10 -0
- vortex/nwp/tools/addons.py +40 -0
- vortex/nwp/tools/agt.py +67 -0
- vortex/nwp/tools/bdap.py +59 -0
- vortex/nwp/tools/bdcp.py +41 -0
- vortex/nwp/tools/bdm.py +24 -0
- vortex/nwp/tools/bdmp.py +54 -0
- vortex/nwp/tools/conftools.py +1661 -0
- vortex/nwp/tools/drhook.py +66 -0
- vortex/nwp/tools/grib.py +294 -0
- vortex/nwp/tools/gribdiff.py +104 -0
- vortex/nwp/tools/ifstools.py +203 -0
- vortex/nwp/tools/igastuff.py +273 -0
- vortex/nwp/tools/mars.py +68 -0
- vortex/nwp/tools/odb.py +657 -0
- vortex/nwp/tools/partitioning.py +258 -0
- vortex/nwp/tools/satrad.py +71 -0
- vortex/nwp/util/__init__.py +6 -0
- vortex/nwp/util/async.py +212 -0
- vortex/nwp/util/beacon.py +40 -0
- vortex/nwp/util/diffpygram.py +447 -0
- vortex/nwp/util/ens.py +279 -0
- vortex/nwp/util/hooks.py +139 -0
- vortex/nwp/util/taskdeco.py +85 -0
- vortex/nwp/util/usepygram.py +697 -0
- vortex/nwp/util/usetnt.py +101 -0
- vortex/proxy.py +6 -0
- vortex/sessions.py +374 -0
- vortex/syntax/__init__.py +9 -0
- vortex/syntax/stdattrs.py +867 -0
- vortex/syntax/stddeco.py +185 -0
- vortex/toolbox.py +1117 -0
- vortex/tools/__init__.py +20 -0
- vortex/tools/actions.py +523 -0
- vortex/tools/addons.py +316 -0
- vortex/tools/arm.py +96 -0
- vortex/tools/compression.py +325 -0
- vortex/tools/date.py +27 -0
- vortex/tools/ddhpack.py +10 -0
- vortex/tools/delayedactions.py +782 -0
- vortex/tools/env.py +541 -0
- vortex/tools/folder.py +834 -0
- vortex/tools/grib.py +738 -0
- vortex/tools/lfi.py +953 -0
- vortex/tools/listings.py +423 -0
- vortex/tools/names.py +637 -0
- vortex/tools/net.py +2124 -0
- vortex/tools/odb.py +10 -0
- vortex/tools/parallelism.py +368 -0
- vortex/tools/prestaging.py +210 -0
- vortex/tools/rawfiles.py +10 -0
- vortex/tools/schedulers.py +480 -0
- vortex/tools/services.py +940 -0
- vortex/tools/storage.py +996 -0
- vortex/tools/surfex.py +61 -0
- vortex/tools/systems.py +3976 -0
- vortex/tools/targets.py +440 -0
- vortex/util/__init__.py +9 -0
- vortex/util/config.py +1122 -0
- vortex/util/empty.py +24 -0
- vortex/util/helpers.py +216 -0
- vortex/util/introspection.py +69 -0
- vortex/util/iosponge.py +80 -0
- vortex/util/roles.py +49 -0
- vortex/util/storefunctions.py +129 -0
- vortex/util/structs.py +26 -0
- vortex/util/worker.py +162 -0
- vortex_nwp-2.0.0.dist-info/METADATA +67 -0
- vortex_nwp-2.0.0.dist-info/RECORD +144 -0
- vortex_nwp-2.0.0.dist-info/WHEEL +5 -0
- vortex_nwp-2.0.0.dist-info/licenses/LICENSE +517 -0
- 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
|