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,551 @@
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(
30
+ values = ['flow_logs']
31
+ ),
32
+ nativefmt = dict(
33
+ values = ['filespack', ],
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('The "{!s}" class is not compatible with the FlowLogsStack class.'
51
+ .format(cls))
52
+
53
+ def stackedstorage_resource(self):
54
+ """Use the FlowLogsStack resource for stacked storage."""
55
+ return FlowLogsStack(kind='flow_logs', date=self.date, cutoff=self.cutoff), False
56
+
57
+ cls.stackedstorage_resource = stackedstorage_resource
58
+ return cls
59
+
60
+
61
+ @use_flow_logs_stack
62
+ @namebuilding_insert('src', lambda s: [s.binary, '-'.join(s.task.split('/')[s.task_start:s.task_stop])])
63
+ @namebuilding_insert('compute', lambda s: s.part)
64
+ @namebuilding_delete('fmt')
65
+ class Listing(FlowResource):
66
+ """Miscellaneous application output from a task processing."""
67
+ _footprint = [
68
+ dict(
69
+ info = 'Listing',
70
+ attr = dict(
71
+ task = dict(
72
+ optional = True,
73
+ default = 'anonymous'
74
+ ),
75
+ task_start = dict(
76
+ optional = True,
77
+ type = int,
78
+ default = -1,
79
+ ),
80
+ task_stop = dict(
81
+ optional = True,
82
+ type = int,
83
+ default = None,
84
+ ),
85
+ kind = dict(
86
+ values = ['listing']
87
+ ),
88
+ part = dict(
89
+ optional = True,
90
+ default = 'all',
91
+ ),
92
+ binary = dict(
93
+ optional = True,
94
+ default = '[model]',
95
+ ),
96
+ clscontents = dict(
97
+ default = FormatAdapter,
98
+ ),
99
+ ),
100
+ )
101
+ ]
102
+
103
+ @property
104
+ def realkind(self):
105
+ return 'listing'
106
+
107
+ def olive_basename(self):
108
+ """Fake basename for getting olive listings"""
109
+ if hasattr(self, '_listingpath'):
110
+ return self._listingpath
111
+ else:
112
+ return "NOT_IMPLEMENTED"
113
+
114
+ def archive_basename(self):
115
+ return "listing." + self.part
116
+
117
+
118
+ class ParallelListing(Listing):
119
+ """Multi output for parallel MPI and/or OpenMP processing."""
120
+ _footprint = [
121
+ dict(
122
+ attr = dict(
123
+ kind = dict(
124
+ values = ['listing', 'plisting', 'mlisting'],
125
+ remap = dict(
126
+ listing = 'plisting',
127
+ mlisting = 'plisting',
128
+ )
129
+ ),
130
+ mpi = dict(
131
+ optional = True,
132
+ default = None,
133
+ type = FmtInt,
134
+ args = dict(fmt = '03'),
135
+ ),
136
+ openmp = dict(
137
+ optional = True,
138
+ default = None,
139
+ type = FmtInt,
140
+ args = dict(fmt = '02'),
141
+ ),
142
+ seta = dict(
143
+ optional = True,
144
+ default = None,
145
+ type = FmtInt,
146
+ args = dict(fmt = '03'),
147
+ ),
148
+ setb = dict(
149
+ optional = True,
150
+ default = None,
151
+ type = FmtInt,
152
+ args = dict(fmt = '02'),
153
+ ),
154
+ )
155
+ )
156
+ ]
157
+
158
+ def namebuilding_info(self):
159
+ """From base information of ``listing`` add mpi and openmp values."""
160
+ info = super().namebuilding_info()
161
+ if self.mpi and self.openmp:
162
+ info['compute'] = [{'mpi': self.mpi}, {'openmp': self.openmp}]
163
+ if self.seta and self.setb:
164
+ info['compute'] = [{'seta': self.seta}, {'setb': self.setb}]
165
+ return info
166
+
167
+
168
+ @namebuilding_insert('src', lambda s: [s.binary, s.task.split('/').pop()])
169
+ @namebuilding_insert('compute', lambda s: s.part)
170
+ @namebuilding_delete('fmt')
171
+ class StaticListing(Resource):
172
+ """Miscelanous application output from a task processing, out-of-flow."""
173
+ _footprint = [
174
+ dict(
175
+ info = 'Listing',
176
+ attr = dict(
177
+ task = dict(
178
+ optional = True,
179
+ default = 'anonymous'
180
+ ),
181
+ kind = dict(
182
+ values = ['staticlisting']
183
+ ),
184
+ part = dict(
185
+ optional = True,
186
+ default = 'all',
187
+ ),
188
+ binary = dict(
189
+ optional = True,
190
+ default = '[model]',
191
+ ),
192
+ clscontents = dict(
193
+ default = FormatAdapter,
194
+ ),
195
+ ),
196
+ )
197
+ ]
198
+
199
+ @property
200
+ def realkind(self):
201
+ return 'staticlisting'
202
+
203
+
204
+ @namebuilding_insert('compute', lambda s: None if s.mpi is None else [{'mpi': s.mpi}, ],
205
+ none_discard=True)
206
+ class DrHookListing(Listing):
207
+ """Output produced by DrHook"""
208
+ _footprint = [
209
+ dict(
210
+ attr = dict(
211
+ kind = dict(
212
+ values = ['drhook', ],
213
+ ),
214
+ mpi = dict(
215
+ optional = True,
216
+ type = FmtInt,
217
+ args = dict(fmt = '03'),
218
+ ),
219
+ )
220
+ )
221
+ ]
222
+
223
+ @property
224
+ def realkind(self):
225
+ return 'drhookprof'
226
+
227
+
228
+ @use_flow_logs_stack
229
+ class Beacon(FlowResource):
230
+ """Output indicating the end of a model run."""
231
+ _footprint = [
232
+ dict(
233
+ info = 'Beacon',
234
+ attr = dict(
235
+ kind = dict(
236
+ values = ['beacon']
237
+ ),
238
+ clscontents = dict(
239
+ default = JsonDictContent,
240
+ ),
241
+ nativefmt = dict(
242
+ default = 'json',
243
+ ),
244
+ )
245
+ )
246
+ ]
247
+
248
+ @property
249
+ def realkind(self):
250
+ return 'beacon'
251
+
252
+
253
+ @use_flow_logs_stack
254
+ @namebuilding_insert('src', lambda s: s.task.split('/').pop())
255
+ @namebuilding_insert('compute', lambda s: s.scope)
256
+ class TaskInfo(FlowResource):
257
+ """Task informations."""
258
+ _footprint = [
259
+ dict(
260
+ info = 'Task informations',
261
+ attr = dict(
262
+ task = dict(
263
+ optional = True,
264
+ default = 'anonymous'
265
+ ),
266
+ kind = dict(
267
+ values = ['taskinfo']
268
+ ),
269
+ scope = dict(
270
+ optional = True,
271
+ default = 'void',
272
+ ),
273
+ clscontents = dict(
274
+ default = JsonDictContent,
275
+ ),
276
+ nativefmt = dict(
277
+ default = 'json',
278
+ ),
279
+ )
280
+ )
281
+ ]
282
+
283
+ @property
284
+ def realkind(self):
285
+ return 'taskinfo'
286
+
287
+
288
+ @namebuilding_insert('src', lambda s: s.task.split('/').pop())
289
+ @namebuilding_insert('compute', lambda s: s.scope)
290
+ @namebuilding_delete('fmt')
291
+ class StaticTaskInfo(Resource):
292
+ """Task informations."""
293
+ _footprint = [
294
+ dict(
295
+ info = 'Task informations',
296
+ attr = dict(
297
+ task = dict(
298
+ optional = True,
299
+ default = 'anonymous'
300
+ ),
301
+ kind = dict(
302
+ values = ['statictaskinfo']
303
+ ),
304
+ scope = dict(
305
+ optional = True,
306
+ default = 'void',
307
+ ),
308
+ clscontents = dict(
309
+ default = JsonDictContent,
310
+ ),
311
+ nativefmt = dict(
312
+ default = 'json',
313
+ ),
314
+ )
315
+ )
316
+ ]
317
+
318
+ @property
319
+ def realkind(self):
320
+ return 'statictaskinfo'
321
+
322
+
323
+ class SectionsSlice(collections.abc.Sequence):
324
+ """Hold a list of dictionaries representing Sections."""
325
+
326
+ _INDEX_PREFIX = 'sslice'
327
+ _INDEX_ATTR = 'sliceindex'
328
+
329
+ def __init__(self, sequence):
330
+ self._data = sequence
331
+
332
+ def __getitem__(self, i):
333
+ if isinstance(i, str) and i.startswith(self._INDEX_PREFIX):
334
+ i = int(i[len(self._INDEX_PREFIX):])
335
+ return self._data[i]
336
+
337
+ def __eq__(self, other):
338
+ return self.to_list() == other.to_list()
339
+
340
+ def __len__(self):
341
+ return len(self._data)
342
+
343
+ def __iter__(self):
344
+ return iter(self._data)
345
+
346
+ def to_list(self):
347
+ """Returns a list object with the exact same content."""
348
+ return list(self._data)
349
+
350
+ @staticmethod
351
+ def _sloppy_lookup(item, k):
352
+ """Look for a key *k* in the *item* dictionary and returns it.
353
+
354
+ :note: A special treatment is made for the 'role' key (the role factory is used
355
+ and the 'alternate' attribute may also be looked for).
356
+
357
+ :note: A special case is made for the attribute 'kind' of the section which can be
358
+ accessed via the 'section_kind' attribute (the attribute 'kind' is used for the resource attribute).
359
+
360
+ :note: if *k* is not found at the top level of the dictionary, the
361
+ 'resource', 'provider' and 'container' parts of the 'rh'sub-dictionary
362
+ are also looked for.
363
+ """
364
+ if k == 'role':
365
+ return item[k] or item['alternate']
366
+ elif k == 'kind' and k in item.get('rh', dict()).get('resource', dict()):
367
+ return item['rh']['resource'][k]
368
+ elif k == 'section_kind' and 'kind' in item:
369
+ return item['kind']
370
+ elif k in item:
371
+ return item[k]
372
+ elif k in item.get('rh', dict()).get('resource', dict()):
373
+ return item['rh']['resource'][k]
374
+ elif k in item.get('rh', dict()).get('provider', dict()):
375
+ return item['rh']['provider'][k]
376
+ elif k in item.get('rh', dict()).get('container', dict()):
377
+ return item['rh']['container'][k]
378
+ else:
379
+ raise KeyError("'{:s}' wasn't found in the designated dictionary"
380
+ .format(k))
381
+
382
+ @staticmethod
383
+ def _sloppy_compare(json_v, v):
384
+ """Try a very very permissive check."""
385
+ if callable(v):
386
+ try:
387
+ return v(json_v)
388
+ except (ValueError, TypeError):
389
+ return False
390
+ else:
391
+ try:
392
+ return type(v)(json_v) == v
393
+ except (ValueError, TypeError):
394
+ try:
395
+ return json_v == v
396
+ except (ValueError, TypeError):
397
+ return False
398
+
399
+ def _sloppy_ckeck(self, item, k, v, extras):
400
+ """Perform a _sloppy_lookup and check the result against *v*."""
401
+ if k in ('role', 'alternate'):
402
+ v = setrole(v)
403
+ try:
404
+ if k == 'baseterm':
405
+ found = self._sloppy_lookup(item, 'term')
406
+ foundbis = self._sloppy_lookup(item, 'date')
407
+ else:
408
+ found = self._sloppy_lookup(item, k)
409
+ except KeyError:
410
+ return False
411
+ if not isinstance(v, (list, tuple, set)):
412
+ v = [v, ]
413
+ if k == 'baseterm' and extras.get('basedate', None):
414
+ delta = (Date(extras['basedate']) - Date(foundbis))
415
+ found = Time(found) - delta
416
+ return any([self._sloppy_compare(found, a_v) for a_v in v])
417
+
418
+ def filter(self, **kwargs):
419
+ """Create a new :class:`SectionsSlice` object that will be filtered using *kwargs*.
420
+
421
+ :example: To retrieve sections with ``role=='Guess'`` and ``rh.provider.member==1``::
422
+
423
+ >>> self.filter(role='Guess', member=1)
424
+ """
425
+ extras = dict()
426
+ extras['basedate'] = kwargs.pop('basedate', None)
427
+ newslice = [s for s in self
428
+ if all([self._sloppy_ckeck(s, k, v, extras)
429
+ for k, v in kwargs.items()])]
430
+ return self.__class__(newslice)
431
+
432
+ def uniquefilter(self, **kwargs):
433
+ """Like :meth:`filter` but checks that only one element matches."""
434
+ newslice = self.filter(**kwargs)
435
+ if len(newslice) == 0:
436
+ raise ValueError("No section was found")
437
+ elif len(newslice) > 1:
438
+ raise ValueError("Multiple sections were found")
439
+ else:
440
+ return newslice
441
+
442
+ @property
443
+ def indexes(self):
444
+ """Returns an index list of all the element contained if the present object."""
445
+ return [self._INDEX_PREFIX + '{:d}'.format(i) for i in range(len(self))]
446
+
447
+ def __deepcopy__(self, memo):
448
+ newslice = self.__class__(self._data)
449
+ memo[id(self)] = newslice
450
+ return newslice
451
+
452
+ def __getattr__(self, attr):
453
+ """Provides an easy access to content's data with footprint's mechanisms.*
454
+
455
+ If the present :class:`SectionsSlice` only contains one element, a
456
+ :meth:`_sloppy_lookup` is performed on this unique element and returned.
457
+ For exemple ``self.vapp`` will be equivalent to
458
+ ``self[0]['rh']['provider']['vapp']``.
459
+
460
+ If the present :class:`SectionsSlice` contains several elements, it's more
461
+ complex : a callback function is returned. Such a callback can be used
462
+ in conjunction with footprint's replacement mechanism. Provided that a
463
+ ``{idx_attr:s}`` attribute exists in the footprint description and
464
+ can be used as an index in the present object (such a list of indexes can
465
+ be generated using the :meth:`indexes` property), the corresponding element
466
+ will be searched using :meth:`_sloppy_lookup`.
467
+ """.format(idx_attr=self._INDEX_ATTR)
468
+ if attr.startswith('__'):
469
+ raise AttributeError(attr)
470
+ if len(self) == 1:
471
+ try:
472
+ return self._sloppy_lookup(self[0], attr)
473
+ except KeyError:
474
+ raise AttributeError("'{:s}' wasn't found in the unique dictionary"
475
+ .format(attr))
476
+ elif len(self) == 0:
477
+ raise AttributeError("The current SectionsSlice is empty. No attribute lookup allowed !")
478
+ else:
479
+ def _attr_lookup(g, x):
480
+ if len(self) > 1 and (self._INDEX_ATTR in g or self._INDEX_ATTR in x):
481
+ idx = g.get(self._INDEX_ATTR, x.get(self._INDEX_ATTR))
482
+ try:
483
+ return self._sloppy_lookup(self[idx], attr)
484
+ except KeyError:
485
+ raise AttributeError("'{:s}' wasn't found in the {!s}-th dictionary"
486
+ .format(attr, idx))
487
+ else:
488
+ raise AttributeError("A '{:s}' attribute must be there !".format(self._INDEX_ATTR))
489
+
490
+ return _attr_lookup
491
+
492
+
493
+ class SectionsJsonListContent(DataContent):
494
+ """Load/Dump a JSON file that contains a list of Sections.
495
+
496
+ The conents of the JSON file is then stored in a query-able
497
+ :class:`SectionsSlice` object.
498
+ """
499
+
500
+ def slurp(self, container):
501
+ """Get data from the ``container``."""
502
+ t = sessions.current()
503
+ with container.preferred_decoding(byte=False):
504
+ container.rewind()
505
+ self._data = SectionsSlice(t.sh.json_load(container.iotarget()))
506
+ self._size = len(self._data)
507
+
508
+ def rewrite(self, container):
509
+ """Write the data in the specified container."""
510
+ t = sessions.current()
511
+ container.close()
512
+ with container.iod_context():
513
+ with container.preferred_decoding(byte=False):
514
+ with container.preferred_write():
515
+ iod = container.iodesc()
516
+ t.sh.json_dump(self.data.to_list(), iod, indent=4)
517
+ container.updfill(True)
518
+
519
+
520
+ @use_flow_logs_stack
521
+ @namebuilding_insert('src', lambda s: s.task.split('/').pop())
522
+ class SectionsList(FlowResource):
523
+ """Class to handle a resource that contains a list of Sections in JSON format.
524
+
525
+ Such a resource can be generated using the :class:`FunctionStore` with the
526
+ :func:`vortex.util.storefunctions.dumpinputs` function.
527
+ """
528
+
529
+ _footprint = dict(
530
+ info = 'A Sections List',
531
+ attr = dict(
532
+ kind=dict(
533
+ values=['sectionslist', ],
534
+ ),
535
+ task = dict(
536
+ optional = True,
537
+ default = 'anonymous'
538
+ ),
539
+ clscontents = dict(
540
+ default = SectionsJsonListContent,
541
+ ),
542
+ nativefmt = dict(
543
+ values = ['json', ],
544
+ default = 'json',
545
+ )
546
+ )
547
+ )
548
+
549
+ @property
550
+ def realkind(self):
551
+ return "sectionslist"