vortex-nwp 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. vortex/__init__.py +159 -0
  2. vortex/algo/__init__.py +13 -0
  3. vortex/algo/components.py +2462 -0
  4. vortex/algo/mpitools.py +1953 -0
  5. vortex/algo/mpitools_templates/__init__.py +1 -0
  6. vortex/algo/mpitools_templates/envelope_wrapper_default.tpl +27 -0
  7. vortex/algo/mpitools_templates/envelope_wrapper_mpiauto.tpl +29 -0
  8. vortex/algo/mpitools_templates/wrapstd_wrapper_default.tpl +18 -0
  9. vortex/algo/serversynctools.py +171 -0
  10. vortex/config.py +112 -0
  11. vortex/data/__init__.py +19 -0
  12. vortex/data/abstractstores.py +1510 -0
  13. vortex/data/containers.py +835 -0
  14. vortex/data/contents.py +622 -0
  15. vortex/data/executables.py +275 -0
  16. vortex/data/flow.py +119 -0
  17. vortex/data/geometries.ini +2689 -0
  18. vortex/data/geometries.py +799 -0
  19. vortex/data/handlers.py +1230 -0
  20. vortex/data/outflow.py +67 -0
  21. vortex/data/providers.py +487 -0
  22. vortex/data/resources.py +207 -0
  23. vortex/data/stores.py +1390 -0
  24. vortex/data/sync_templates/__init__.py +0 -0
  25. vortex/gloves.py +309 -0
  26. vortex/layout/__init__.py +20 -0
  27. vortex/layout/contexts.py +577 -0
  28. vortex/layout/dataflow.py +1220 -0
  29. vortex/layout/monitor.py +969 -0
  30. vortex/nwp/__init__.py +14 -0
  31. vortex/nwp/algo/__init__.py +21 -0
  32. vortex/nwp/algo/assim.py +537 -0
  33. vortex/nwp/algo/clim.py +1086 -0
  34. vortex/nwp/algo/coupling.py +831 -0
  35. vortex/nwp/algo/eda.py +840 -0
  36. vortex/nwp/algo/eps.py +785 -0
  37. vortex/nwp/algo/forecasts.py +886 -0
  38. vortex/nwp/algo/fpserver.py +1303 -0
  39. vortex/nwp/algo/ifsnaming.py +463 -0
  40. vortex/nwp/algo/ifsroot.py +404 -0
  41. vortex/nwp/algo/monitoring.py +263 -0
  42. vortex/nwp/algo/mpitools.py +694 -0
  43. vortex/nwp/algo/odbtools.py +1258 -0
  44. vortex/nwp/algo/oopsroot.py +916 -0
  45. vortex/nwp/algo/oopstests.py +220 -0
  46. vortex/nwp/algo/request.py +660 -0
  47. vortex/nwp/algo/stdpost.py +1641 -0
  48. vortex/nwp/data/__init__.py +30 -0
  49. vortex/nwp/data/assim.py +380 -0
  50. vortex/nwp/data/boundaries.py +314 -0
  51. vortex/nwp/data/climfiles.py +521 -0
  52. vortex/nwp/data/configfiles.py +153 -0
  53. vortex/nwp/data/consts.py +954 -0
  54. vortex/nwp/data/ctpini.py +149 -0
  55. vortex/nwp/data/diagnostics.py +209 -0
  56. vortex/nwp/data/eda.py +147 -0
  57. vortex/nwp/data/eps.py +432 -0
  58. vortex/nwp/data/executables.py +1045 -0
  59. vortex/nwp/data/fields.py +111 -0
  60. vortex/nwp/data/gridfiles.py +380 -0
  61. vortex/nwp/data/logs.py +584 -0
  62. vortex/nwp/data/modelstates.py +363 -0
  63. vortex/nwp/data/monitoring.py +193 -0
  64. vortex/nwp/data/namelists.py +696 -0
  65. vortex/nwp/data/obs.py +840 -0
  66. vortex/nwp/data/oopsexec.py +74 -0
  67. vortex/nwp/data/providers.py +207 -0
  68. vortex/nwp/data/query.py +206 -0
  69. vortex/nwp/data/stores.py +160 -0
  70. vortex/nwp/data/surfex.py +337 -0
  71. vortex/nwp/syntax/__init__.py +9 -0
  72. vortex/nwp/syntax/stdattrs.py +437 -0
  73. vortex/nwp/tools/__init__.py +10 -0
  74. vortex/nwp/tools/addons.py +40 -0
  75. vortex/nwp/tools/agt.py +67 -0
  76. vortex/nwp/tools/bdap.py +59 -0
  77. vortex/nwp/tools/bdcp.py +41 -0
  78. vortex/nwp/tools/bdm.py +24 -0
  79. vortex/nwp/tools/bdmp.py +54 -0
  80. vortex/nwp/tools/conftools.py +1661 -0
  81. vortex/nwp/tools/drhook.py +66 -0
  82. vortex/nwp/tools/grib.py +294 -0
  83. vortex/nwp/tools/gribdiff.py +104 -0
  84. vortex/nwp/tools/ifstools.py +203 -0
  85. vortex/nwp/tools/igastuff.py +273 -0
  86. vortex/nwp/tools/mars.py +68 -0
  87. vortex/nwp/tools/odb.py +657 -0
  88. vortex/nwp/tools/partitioning.py +258 -0
  89. vortex/nwp/tools/satrad.py +71 -0
  90. vortex/nwp/util/__init__.py +6 -0
  91. vortex/nwp/util/async.py +212 -0
  92. vortex/nwp/util/beacon.py +40 -0
  93. vortex/nwp/util/diffpygram.py +447 -0
  94. vortex/nwp/util/ens.py +279 -0
  95. vortex/nwp/util/hooks.py +139 -0
  96. vortex/nwp/util/taskdeco.py +85 -0
  97. vortex/nwp/util/usepygram.py +697 -0
  98. vortex/nwp/util/usetnt.py +101 -0
  99. vortex/proxy.py +6 -0
  100. vortex/sessions.py +374 -0
  101. vortex/syntax/__init__.py +9 -0
  102. vortex/syntax/stdattrs.py +867 -0
  103. vortex/syntax/stddeco.py +185 -0
  104. vortex/toolbox.py +1117 -0
  105. vortex/tools/__init__.py +20 -0
  106. vortex/tools/actions.py +523 -0
  107. vortex/tools/addons.py +316 -0
  108. vortex/tools/arm.py +96 -0
  109. vortex/tools/compression.py +325 -0
  110. vortex/tools/date.py +27 -0
  111. vortex/tools/ddhpack.py +10 -0
  112. vortex/tools/delayedactions.py +782 -0
  113. vortex/tools/env.py +541 -0
  114. vortex/tools/folder.py +834 -0
  115. vortex/tools/grib.py +738 -0
  116. vortex/tools/lfi.py +953 -0
  117. vortex/tools/listings.py +423 -0
  118. vortex/tools/names.py +637 -0
  119. vortex/tools/net.py +2124 -0
  120. vortex/tools/odb.py +10 -0
  121. vortex/tools/parallelism.py +368 -0
  122. vortex/tools/prestaging.py +210 -0
  123. vortex/tools/rawfiles.py +10 -0
  124. vortex/tools/schedulers.py +480 -0
  125. vortex/tools/services.py +940 -0
  126. vortex/tools/storage.py +996 -0
  127. vortex/tools/surfex.py +61 -0
  128. vortex/tools/systems.py +3976 -0
  129. vortex/tools/targets.py +440 -0
  130. vortex/util/__init__.py +9 -0
  131. vortex/util/config.py +1122 -0
  132. vortex/util/empty.py +24 -0
  133. vortex/util/helpers.py +216 -0
  134. vortex/util/introspection.py +69 -0
  135. vortex/util/iosponge.py +80 -0
  136. vortex/util/roles.py +49 -0
  137. vortex/util/storefunctions.py +129 -0
  138. vortex/util/structs.py +26 -0
  139. vortex/util/worker.py +162 -0
  140. vortex_nwp-2.0.0.dist-info/METADATA +67 -0
  141. vortex_nwp-2.0.0.dist-info/RECORD +144 -0
  142. vortex_nwp-2.0.0.dist-info/WHEEL +5 -0
  143. vortex_nwp-2.0.0.dist-info/licenses/LICENSE +517 -0
  144. vortex_nwp-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,584 @@
1
+ """
2
+ TODO: Module documentation.
3
+ """
4
+
5
+ import collections.abc
6
+
7
+ from bronx.stdtypes.date import Date, Time
8
+ from vortex import sessions
9
+ from vortex.data.contents import DataContent, JsonDictContent, FormatAdapter
10
+ from vortex.data.flow import FlowResource
11
+ from vortex.data.resources import Resource
12
+ from vortex.syntax.stdattrs import FmtInt, date_deco, cutoff_deco
13
+ from vortex.syntax.stddeco import namebuilding_delete, namebuilding_insert
14
+ from vortex.util.roles import setrole
15
+
16
+ #: No automatic export
17
+ __all__ = []
18
+
19
+
20
+ class FlowLogsStack(Resource):
21
+ """Stack of miscellaneous log files"""
22
+
23
+ _footprint = [
24
+ date_deco,
25
+ cutoff_deco,
26
+ dict(
27
+ info="Stack of miscellaneous log files.",
28
+ attr=dict(
29
+ kind=dict(values=["flow_logs"]),
30
+ nativefmt=dict(
31
+ values=[
32
+ "filespack",
33
+ ],
34
+ default="filespack",
35
+ ),
36
+ ),
37
+ ),
38
+ ]
39
+
40
+ @property
41
+ def realkind(self):
42
+ return "flow_logs"
43
+
44
+
45
+ def use_flow_logs_stack(cls):
46
+ """Setup the decorated class to work with the FlowLogsStack resource."""
47
+ fpattrs = set(cls.footprint_retrieve().attr.keys())
48
+ fpcheck = all([k in fpattrs for k in ("date", "cutoff")])
49
+ if not fpcheck:
50
+ raise ImportError(
51
+ 'The "{!s}" class is not compatible with the FlowLogsStack class.'.format(
52
+ cls
53
+ )
54
+ )
55
+
56
+ def stackedstorage_resource(self):
57
+ """Use the FlowLogsStack resource for stacked storage."""
58
+ return FlowLogsStack(
59
+ kind="flow_logs", date=self.date, cutoff=self.cutoff
60
+ ), False
61
+
62
+ cls.stackedstorage_resource = stackedstorage_resource
63
+ return cls
64
+
65
+
66
+ @use_flow_logs_stack
67
+ @namebuilding_insert(
68
+ "src",
69
+ lambda s: [
70
+ s.binary,
71
+ "-".join(s.task.split("/")[s.task_start : s.task_stop]),
72
+ ],
73
+ )
74
+ @namebuilding_insert("compute", lambda s: s.part)
75
+ @namebuilding_delete("fmt")
76
+ class Listing(FlowResource):
77
+ """Miscellaneous application output from a task processing."""
78
+
79
+ _footprint = [
80
+ dict(
81
+ info="Listing",
82
+ attr=dict(
83
+ task=dict(optional=True, default="anonymous"),
84
+ task_start=dict(
85
+ optional=True,
86
+ type=int,
87
+ default=-1,
88
+ ),
89
+ task_stop=dict(
90
+ optional=True,
91
+ type=int,
92
+ default=None,
93
+ ),
94
+ kind=dict(values=["listing"]),
95
+ part=dict(
96
+ optional=True,
97
+ default="all",
98
+ ),
99
+ binary=dict(
100
+ optional=True,
101
+ default="[model]",
102
+ ),
103
+ clscontents=dict(
104
+ default=FormatAdapter,
105
+ ),
106
+ ),
107
+ )
108
+ ]
109
+
110
+ @property
111
+ def realkind(self):
112
+ return "listing"
113
+
114
+ def olive_basename(self):
115
+ """Fake basename for getting olive listings"""
116
+ if hasattr(self, "_listingpath"):
117
+ return self._listingpath
118
+ else:
119
+ return "NOT_IMPLEMENTED"
120
+
121
+ def archive_basename(self):
122
+ return "listing." + self.part
123
+
124
+
125
+ class ParallelListing(Listing):
126
+ """Multi output for parallel MPI and/or OpenMP processing."""
127
+
128
+ _footprint = [
129
+ dict(
130
+ attr=dict(
131
+ kind=dict(
132
+ values=["listing", "plisting", "mlisting"],
133
+ remap=dict(
134
+ listing="plisting",
135
+ mlisting="plisting",
136
+ ),
137
+ ),
138
+ mpi=dict(
139
+ optional=True,
140
+ default=None,
141
+ type=FmtInt,
142
+ args=dict(fmt="03"),
143
+ ),
144
+ openmp=dict(
145
+ optional=True,
146
+ default=None,
147
+ type=FmtInt,
148
+ args=dict(fmt="02"),
149
+ ),
150
+ seta=dict(
151
+ optional=True,
152
+ default=None,
153
+ type=FmtInt,
154
+ args=dict(fmt="03"),
155
+ ),
156
+ setb=dict(
157
+ optional=True,
158
+ default=None,
159
+ type=FmtInt,
160
+ args=dict(fmt="02"),
161
+ ),
162
+ )
163
+ )
164
+ ]
165
+
166
+ def namebuilding_info(self):
167
+ """From base information of ``listing`` add mpi and openmp values."""
168
+ info = super().namebuilding_info()
169
+ if self.mpi and self.openmp:
170
+ info["compute"] = [{"mpi": self.mpi}, {"openmp": self.openmp}]
171
+ if self.seta and self.setb:
172
+ info["compute"] = [{"seta": self.seta}, {"setb": self.setb}]
173
+ return info
174
+
175
+
176
+ @namebuilding_insert("src", lambda s: [s.binary, s.task.split("/").pop()])
177
+ @namebuilding_insert("compute", lambda s: s.part)
178
+ @namebuilding_delete("fmt")
179
+ class StaticListing(Resource):
180
+ """Miscelanous application output from a task processing, out-of-flow."""
181
+
182
+ _footprint = [
183
+ dict(
184
+ info="Listing",
185
+ attr=dict(
186
+ task=dict(optional=True, default="anonymous"),
187
+ kind=dict(values=["staticlisting"]),
188
+ part=dict(
189
+ optional=True,
190
+ default="all",
191
+ ),
192
+ binary=dict(
193
+ optional=True,
194
+ default="[model]",
195
+ ),
196
+ clscontents=dict(
197
+ default=FormatAdapter,
198
+ ),
199
+ ),
200
+ )
201
+ ]
202
+
203
+ @property
204
+ def realkind(self):
205
+ return "staticlisting"
206
+
207
+
208
+ @namebuilding_insert(
209
+ "compute",
210
+ lambda s: None
211
+ if s.mpi is None
212
+ else [
213
+ {"mpi": s.mpi},
214
+ ],
215
+ none_discard=True,
216
+ )
217
+ class DrHookListing(Listing):
218
+ """Output produced by DrHook"""
219
+
220
+ _footprint = [
221
+ dict(
222
+ attr=dict(
223
+ kind=dict(
224
+ values=[
225
+ "drhook",
226
+ ],
227
+ ),
228
+ mpi=dict(
229
+ optional=True,
230
+ type=FmtInt,
231
+ args=dict(fmt="03"),
232
+ ),
233
+ )
234
+ )
235
+ ]
236
+
237
+ @property
238
+ def realkind(self):
239
+ return "drhookprof"
240
+
241
+
242
+ @use_flow_logs_stack
243
+ class Beacon(FlowResource):
244
+ """Output indicating the end of a model run."""
245
+
246
+ _footprint = [
247
+ dict(
248
+ info="Beacon",
249
+ attr=dict(
250
+ kind=dict(values=["beacon"]),
251
+ clscontents=dict(
252
+ default=JsonDictContent,
253
+ ),
254
+ nativefmt=dict(
255
+ default="json",
256
+ ),
257
+ ),
258
+ )
259
+ ]
260
+
261
+ @property
262
+ def realkind(self):
263
+ return "beacon"
264
+
265
+
266
+ @use_flow_logs_stack
267
+ @namebuilding_insert("src", lambda s: s.task.split("/").pop())
268
+ @namebuilding_insert("compute", lambda s: s.scope)
269
+ class TaskInfo(FlowResource):
270
+ """Task informations."""
271
+
272
+ _footprint = [
273
+ dict(
274
+ info="Task informations",
275
+ attr=dict(
276
+ task=dict(optional=True, default="anonymous"),
277
+ kind=dict(values=["taskinfo"]),
278
+ scope=dict(
279
+ optional=True,
280
+ default="void",
281
+ ),
282
+ clscontents=dict(
283
+ default=JsonDictContent,
284
+ ),
285
+ nativefmt=dict(
286
+ default="json",
287
+ ),
288
+ ),
289
+ )
290
+ ]
291
+
292
+ @property
293
+ def realkind(self):
294
+ return "taskinfo"
295
+
296
+
297
+ @namebuilding_insert("src", lambda s: s.task.split("/").pop())
298
+ @namebuilding_insert("compute", lambda s: s.scope)
299
+ @namebuilding_delete("fmt")
300
+ class StaticTaskInfo(Resource):
301
+ """Task informations."""
302
+
303
+ _footprint = [
304
+ dict(
305
+ info="Task informations",
306
+ attr=dict(
307
+ task=dict(optional=True, default="anonymous"),
308
+ kind=dict(values=["statictaskinfo"]),
309
+ scope=dict(
310
+ optional=True,
311
+ default="void",
312
+ ),
313
+ clscontents=dict(
314
+ default=JsonDictContent,
315
+ ),
316
+ nativefmt=dict(
317
+ default="json",
318
+ ),
319
+ ),
320
+ )
321
+ ]
322
+
323
+ @property
324
+ def realkind(self):
325
+ return "statictaskinfo"
326
+
327
+
328
+ class SectionsSlice(collections.abc.Sequence):
329
+ """Hold a list of dictionaries representing Sections."""
330
+
331
+ _INDEX_PREFIX = "sslice"
332
+ _INDEX_ATTR = "sliceindex"
333
+
334
+ def __init__(self, sequence):
335
+ self._data = sequence
336
+
337
+ def __getitem__(self, i):
338
+ if isinstance(i, str) and i.startswith(self._INDEX_PREFIX):
339
+ i = int(i[len(self._INDEX_PREFIX) :])
340
+ return self._data[i]
341
+
342
+ def __eq__(self, other):
343
+ return self.to_list() == other.to_list()
344
+
345
+ def __len__(self):
346
+ return len(self._data)
347
+
348
+ def __iter__(self):
349
+ return iter(self._data)
350
+
351
+ def to_list(self):
352
+ """Returns a list object with the exact same content."""
353
+ return list(self._data)
354
+
355
+ @staticmethod
356
+ def _sloppy_lookup(item, k):
357
+ """Look for a key *k* in the *item* dictionary and returns it.
358
+
359
+ :note: A special treatment is made for the 'role' key (the role factory is used
360
+ and the 'alternate' attribute may also be looked for).
361
+
362
+ :note: A special case is made for the attribute 'kind' of the section which can be
363
+ accessed via the 'section_kind' attribute (the attribute 'kind' is used for the resource attribute).
364
+
365
+ :note: if *k* is not found at the top level of the dictionary, the
366
+ 'resource', 'provider' and 'container' parts of the 'rh'sub-dictionary
367
+ are also looked for.
368
+ """
369
+ if k == "role":
370
+ return item[k] or item["alternate"]
371
+ elif k == "kind" and k in item.get("rh", dict()).get(
372
+ "resource", dict()
373
+ ):
374
+ return item["rh"]["resource"][k]
375
+ elif k == "section_kind" and "kind" in item:
376
+ return item["kind"]
377
+ elif k in item:
378
+ return item[k]
379
+ elif k in item.get("rh", dict()).get("resource", dict()):
380
+ return item["rh"]["resource"][k]
381
+ elif k in item.get("rh", dict()).get("provider", dict()):
382
+ return item["rh"]["provider"][k]
383
+ elif k in item.get("rh", dict()).get("container", dict()):
384
+ return item["rh"]["container"][k]
385
+ else:
386
+ raise KeyError(
387
+ "'{:s}' wasn't found in the designated dictionary".format(k)
388
+ )
389
+
390
+ @staticmethod
391
+ def _sloppy_compare(json_v, v):
392
+ """Try a very very permissive check."""
393
+ if callable(v):
394
+ try:
395
+ return v(json_v)
396
+ except (ValueError, TypeError):
397
+ return False
398
+ else:
399
+ try:
400
+ return type(v)(json_v) == v
401
+ except (ValueError, TypeError):
402
+ try:
403
+ return json_v == v
404
+ except (ValueError, TypeError):
405
+ return False
406
+
407
+ def _sloppy_ckeck(self, item, k, v, extras):
408
+ """Perform a _sloppy_lookup and check the result against *v*."""
409
+ if k in ("role", "alternate"):
410
+ v = setrole(v)
411
+ try:
412
+ if k == "baseterm":
413
+ found = self._sloppy_lookup(item, "term")
414
+ foundbis = self._sloppy_lookup(item, "date")
415
+ else:
416
+ found = self._sloppy_lookup(item, k)
417
+ except KeyError:
418
+ return False
419
+ if not isinstance(v, (list, tuple, set)):
420
+ v = [
421
+ v,
422
+ ]
423
+ if k == "baseterm" and extras.get("basedate", None):
424
+ delta = Date(extras["basedate"]) - Date(foundbis)
425
+ found = Time(found) - delta
426
+ return any([self._sloppy_compare(found, a_v) for a_v in v])
427
+
428
+ def filter(self, **kwargs):
429
+ """Create a new :class:`SectionsSlice` object that will be filtered using *kwargs*.
430
+
431
+ :example: To retrieve sections with ``role=='Guess'`` and ``rh.provider.member==1``::
432
+
433
+ >>> self.filter(role='Guess', member=1)
434
+ """
435
+ extras = dict()
436
+ extras["basedate"] = kwargs.pop("basedate", None)
437
+ newslice = [
438
+ s
439
+ for s in self
440
+ if all(
441
+ [
442
+ self._sloppy_ckeck(s, k, v, extras)
443
+ for k, v in kwargs.items()
444
+ ]
445
+ )
446
+ ]
447
+ return self.__class__(newslice)
448
+
449
+ def uniquefilter(self, **kwargs):
450
+ """Like :meth:`filter` but checks that only one element matches."""
451
+ newslice = self.filter(**kwargs)
452
+ if len(newslice) == 0:
453
+ raise ValueError("No section was found")
454
+ elif len(newslice) > 1:
455
+ raise ValueError("Multiple sections were found")
456
+ else:
457
+ return newslice
458
+
459
+ @property
460
+ def indexes(self):
461
+ """Returns an index list of all the element contained if the present object."""
462
+ return [
463
+ self._INDEX_PREFIX + "{:d}".format(i) for i in range(len(self))
464
+ ]
465
+
466
+ def __deepcopy__(self, memo):
467
+ newslice = self.__class__(self._data)
468
+ memo[id(self)] = newslice
469
+ return newslice
470
+
471
+ def __getattr__(self, attr):
472
+ """Provides an easy access to content's data with footprint's mechanisms.*
473
+
474
+ If the present :class:`SectionsSlice` only contains one element, a
475
+ :meth:`_sloppy_lookup` is performed on this unique element and returned.
476
+ For exemple ``self.vapp`` will be equivalent to
477
+ ``self[0]['rh']['provider']['vapp']``.
478
+
479
+ If the present :class:`SectionsSlice` contains several elements, it's more
480
+ complex : a callback function is returned. Such a callback can be used
481
+ in conjunction with footprint's replacement mechanism. Provided that a
482
+ ``{idx_attr:s}`` attribute exists in the footprint description and
483
+ can be used as an index in the present object (such a list of indexes can
484
+ be generated using the :meth:`indexes` property), the corresponding element
485
+ will be searched using :meth:`_sloppy_lookup`.
486
+ """.format(idx_attr=self._INDEX_ATTR)
487
+ if attr.startswith("__"):
488
+ raise AttributeError(attr)
489
+ if len(self) == 1:
490
+ try:
491
+ return self._sloppy_lookup(self[0], attr)
492
+ except KeyError:
493
+ raise AttributeError(
494
+ "'{:s}' wasn't found in the unique dictionary".format(attr)
495
+ )
496
+ elif len(self) == 0:
497
+ raise AttributeError(
498
+ "The current SectionsSlice is empty. No attribute lookup allowed !"
499
+ )
500
+ else:
501
+
502
+ def _attr_lookup(g, x):
503
+ if len(self) > 1 and (
504
+ self._INDEX_ATTR in g or self._INDEX_ATTR in x
505
+ ):
506
+ idx = g.get(self._INDEX_ATTR, x.get(self._INDEX_ATTR))
507
+ try:
508
+ return self._sloppy_lookup(self[idx], attr)
509
+ except KeyError:
510
+ raise AttributeError(
511
+ "'{:s}' wasn't found in the {!s}-th dictionary".format(
512
+ attr, idx
513
+ )
514
+ )
515
+ else:
516
+ raise AttributeError(
517
+ "A '{:s}' attribute must be there !".format(
518
+ self._INDEX_ATTR
519
+ )
520
+ )
521
+
522
+ return _attr_lookup
523
+
524
+
525
+ class SectionsJsonListContent(DataContent):
526
+ """Load/Dump a JSON file that contains a list of Sections.
527
+
528
+ The conents of the JSON file is then stored in a query-able
529
+ :class:`SectionsSlice` object.
530
+ """
531
+
532
+ def slurp(self, container):
533
+ """Get data from the ``container``."""
534
+ t = sessions.current()
535
+ with container.preferred_decoding(byte=False):
536
+ container.rewind()
537
+ self._data = SectionsSlice(t.sh.json_load(container.iotarget()))
538
+ self._size = len(self._data)
539
+
540
+ def rewrite(self, container):
541
+ """Write the data in the specified container."""
542
+ t = sessions.current()
543
+ container.close()
544
+ with container.iod_context():
545
+ with container.preferred_decoding(byte=False):
546
+ with container.preferred_write():
547
+ iod = container.iodesc()
548
+ t.sh.json_dump(self.data.to_list(), iod, indent=4)
549
+ container.updfill(True)
550
+
551
+
552
+ @use_flow_logs_stack
553
+ @namebuilding_insert("src", lambda s: s.task.split("/").pop())
554
+ class SectionsList(FlowResource):
555
+ """Class to handle a resource that contains a list of Sections in JSON format.
556
+
557
+ Such a resource can be generated using the :class:`FunctionStore` with the
558
+ :func:`vortex.util.storefunctions.dumpinputs` function.
559
+ """
560
+
561
+ _footprint = dict(
562
+ info="A Sections List",
563
+ attr=dict(
564
+ kind=dict(
565
+ values=[
566
+ "sectionslist",
567
+ ],
568
+ ),
569
+ task=dict(optional=True, default="anonymous"),
570
+ clscontents=dict(
571
+ default=SectionsJsonListContent,
572
+ ),
573
+ nativefmt=dict(
574
+ values=[
575
+ "json",
576
+ ],
577
+ default="json",
578
+ ),
579
+ ),
580
+ )
581
+
582
+ @property
583
+ def realkind(self):
584
+ return "sectionslist"