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,548 @@
1
+ """
2
+ Common interest classes to help setup the ODB software environment.
3
+ """
4
+
5
+ import re
6
+
7
+ from bronx.fancies import loggers
8
+ from bronx.stdtypes import date as bdate
9
+ import footprints
10
+
11
+ from vortex.algo.components import AlgoComponentDecoMixin, AlgoComponentError, algo_component_deco_mixin_autodoc
12
+ from vortex.layout.dataflow import intent
13
+
14
+ from ..syntax.stdattrs import ArpIfsSimplifiedCycle
15
+
16
+ #: No automatic export
17
+ __all__ = []
18
+
19
+ logger = loggers.getLogger(__name__)
20
+
21
+
22
+ class TimeSlots:
23
+ """Handling of assimilation time slots."""
24
+
25
+ def __init__(self, nslot=7, start='-PT3H', window='PT6H', chunk=None, center=True):
26
+ if isinstance(nslot, str):
27
+ info = [x.strip() for x in nslot.split('/')]
28
+ nslot = info[0]
29
+ if len(info) > 1:
30
+ start = info[1]
31
+ if len(info) > 2:
32
+ window = info[2]
33
+ if len(info) > 3:
34
+ if re.match('^regular', info[3]):
35
+ center = False
36
+ else:
37
+ chunk = info[3]
38
+ self.nslot = int(nslot)
39
+ self.center = center if self.nslot > 1 else False
40
+ self.start = bdate.Period(start)
41
+ self.window = bdate.Period(window)
42
+ if chunk is None:
43
+ cslot = self.nslot - 1 if self.center else self.nslot
44
+ chunk = 'PT' + str((self.window.length // max(1, cslot)) // 60) + 'M'
45
+ self.chunk = self.window if self.nslot < 2 else bdate.Period(chunk)
46
+
47
+ def __eq__(self, other):
48
+ if isinstance(other, str):
49
+ try:
50
+ other = TimeSlots(other)
51
+ except ValueError:
52
+ pass
53
+ return (isinstance(other, TimeSlots) and
54
+ self.nslot == other.nslot and self.center == other.center and
55
+ self.start == other.start and self.window == other.window and
56
+ self.chunk == other.chunk)
57
+
58
+ def __str__(self):
59
+ chunky = self.chunk.isoformat() if self.center else 'regular'
60
+ return '{0.nslot:d}/{1:s}/{2:s}/{3:s}'.format(self,
61
+ self.start.isoformat(),
62
+ self.window.isoformat(),
63
+ chunky)
64
+
65
+ def __repr__(self, *args, **kwargs):
66
+ return super().__repr__()[:-1] + ' | {!s}>'.format(self)
67
+
68
+ def as_slots(self):
69
+ """Return a list of slots in seconds."""
70
+ if self.center:
71
+ slots = [self.chunk.length, ] * self.nslot
72
+ nb = self.window.length // self.chunk.length
73
+ if nb != self.nslot:
74
+ slots[0] = slots[-1] = self.chunk.length // 2
75
+ else:
76
+ islot = self.window.length // self.nslot
77
+ slots = [islot, ] * self.nslot
78
+ return slots
79
+
80
+ def as_centers_fromstart(self):
81
+ """Return time slots centers as a list of Period objects."""
82
+ slots = self.as_slots()
83
+ fromstart = []
84
+ acc = 0
85
+ for i in range(len(slots)):
86
+ fromstart.append(acc + slots[i] / 2)
87
+ acc += slots[i]
88
+ if self.center and (self.window.length // self.chunk.length != self.nslot):
89
+ fromstart[0] = 0
90
+ fromstart[-1] = self.window.length
91
+ return [bdate.Period(seconds=t) for t in fromstart]
92
+
93
+ def as_bounds(self, date):
94
+ """Return time slots as a list of compact date values."""
95
+ date = bdate.Date(date)
96
+ boundlist = [date + self.start, ]
97
+ for x in self.as_slots():
98
+ boundlist.append(boundlist[-1] + x)
99
+ boundlist = [x.compact() for x in boundlist]
100
+ return boundlist
101
+
102
+ @property
103
+ def leftmargin(self):
104
+ """Return length in minutes from left margin of the window."""
105
+ return int(self.start.total_seconds()) // 60
106
+
107
+ @property
108
+ def rightmargin(self):
109
+ """Return length in minutes from rigth margin of the window."""
110
+ return int((self.start + self.window).total_seconds()) // 60
111
+
112
+ def as_environment(self):
113
+ """Return a dictionary of ready-to-export variables that describe the timeslots."""
114
+ thelen = self.chunk.length // 60 if self.center and self.nslot > 1 else 0
115
+ return dict(BATOR_WINDOW_LEN=self.window.length // 60,
116
+ BATOR_WINDOW_SHIFT=int(self.start.total_seconds()) // 60,
117
+ BATOR_SLOT_LEN=thelen, BATOR_CENTER_LEN=thelen)
118
+
119
+ def as_file(self, date, filename):
120
+ """Fill the specified ``filename`` wih the current list of time slots at this ``date``."""
121
+ with open(filename, 'w') as fd:
122
+ for x in self.as_bounds(date):
123
+ fd.write(str(x) + '\n')
124
+ nbx = fd.tell()
125
+ return nbx
126
+
127
+
128
+ class OdbDriver:
129
+ """A dedicated class for handling some ODB settings."""
130
+
131
+ def __init__(self, cycle, sh=None, env=None):
132
+ """
133
+ A quite challenging initialisation since cycle, sh, env and target
134
+ should be provided...
135
+ """
136
+ self.cycle = cycle
137
+ self.sh = sh
138
+ if self.sh is None:
139
+ logger.critical('%s created with a proper shell access [%s]', self.__class__, self)
140
+ self.env = env
141
+ if self.env is None:
142
+ logger.critical('%s created with a proper environment access [%s]', self.__class__, self)
143
+
144
+ def setup(self, date, npool=1, nslot=1, iomethod=1, layout='ecma'):
145
+ """Setup given environment with default ODB env variables."""
146
+
147
+ logger.info('ODB: generic setup called.'),
148
+ self.env.update(
149
+ ODB_CMA=layout.upper(),
150
+ ODB_IO_METHOD=iomethod,
151
+ )
152
+
153
+ self.env.default(
154
+ ODB_DEBUG=0,
155
+ ODB_CTX_DEBUG=0,
156
+ ODB_REPRODUCIBLE_SEQNO=4,
157
+ ODB_STATIC_LINKING=1,
158
+ ODB_ANALYSIS_DATE=date.ymd,
159
+ ODB_ANALYSIS_TIME=date.hm + '00',
160
+ TO_ODB_ECMWF=0,
161
+ TO_ODB_SWAPOUT=0,
162
+ )
163
+
164
+ if iomethod == 4:
165
+ self.env.default(
166
+ ODB_IO_GRPSIZE=npool,
167
+ ODB_IO_FILESIZE=128,
168
+ )
169
+
170
+ if self.sh.path.exists('IOASSIGN'):
171
+ self.env.default(
172
+ IOASSIGN=self.sh.path.abspath('IOASSIGN'),
173
+ )
174
+
175
+ def force_overwrite_method(self):
176
+ """Force ODB_OVERWRITE_METHOD if necessary."""
177
+ if not int(self.env.get('ODB_OVERWRITE_METHOD', 0)):
178
+ logger.info('ODB: Some input ODB databases are read-only. Setting ODB_OVERWRITE_METHOD to 1.')
179
+ self.env.ODB_OVERWRITE_METHOD = 1
180
+
181
+ def _process_layout_dbpath(self, layout, dbpath=None):
182
+ """Normalise **layout** and **dbpath**."""
183
+ layout = layout.upper()
184
+ thispwd = self.sh.path.abspath(self.sh.getcwd())
185
+ if dbpath is None:
186
+ dbpath = self.sh.path.join(thispwd, layout)
187
+ return layout, dbpath, thispwd
188
+
189
+ def fix_db_path(self, layout, dbpath=None, env=None):
190
+ """Setup the path to the **layout** database."""
191
+ if env is None:
192
+ env = self.env
193
+ layout, dbpath, _ = self._process_layout_dbpath(layout, dbpath)
194
+ logger.info('ODB: Fix %s path: %s', layout, dbpath)
195
+ env['ODB_SRCPATH_{:s}'.format(layout)] = dbpath
196
+ env['ODB_DATAPATH_{:s}'.format(layout)] = dbpath
197
+
198
+ @property
199
+ def _default_iocreate_path(self):
200
+ """The location to the default create_ioassign utility."""
201
+ return self.sh.path.join(
202
+ from_config(section="nwp-tools", key="odb"),
203
+ get_from_config_w_default(
204
+ section="nwp-tools",
205
+ key="iocreate_cmd",
206
+ default="create_ioassign"
207
+ ),
208
+ )
209
+
210
+ @property
211
+ def _default_iomerge_path(self):
212
+ """The location to the default merge_ioassign utility."""
213
+ return self.sh.path.join(
214
+ from_config(section="nwp-tools", key="odb"),
215
+ get_from_config_w_default(
216
+ section="nwp-tools",
217
+ key="iomerge_cmd",
218
+ default="merge_ioassign"
219
+ ),
220
+ )
221
+
222
+ def ioassign_create(self, ioassign='ioassign.x', npool=1, layout='ecma',
223
+ dbpath=None, iocreate_path=None):
224
+ """Build IO-Assign table."""
225
+ layout, dbpath, _ = self._process_layout_dbpath(layout, dbpath)
226
+ if iocreate_path is None:
227
+ iocreate_path = self._default_iocreate_path
228
+ ioassign = self.sh.path.abspath(ioassign)
229
+ self.sh.xperm(ioassign, force=True)
230
+ self.sh.mkdir(dbpath)
231
+ with self.env.clone() as lenv:
232
+ lenv['ODB_IOASSIGN_BINARY'] = ioassign
233
+ self.fix_db_path(layout, dbpath, env=lenv)
234
+ self.sh.spawn([iocreate_path,
235
+ '-d' + dbpath,
236
+ '-l' + layout,
237
+ '-n' + str(npool)],
238
+ output=False)
239
+ return dbpath
240
+
241
+ def ioassign_merge(self, ioassign='ioassign.x', layout='ecma', odbnames=None,
242
+ dbpath=None, iomerge_path=None, iocreate_path=None):
243
+ """Build IO-Assign table."""
244
+ layout, dbpath, thispwd = self._process_layout_dbpath(layout, dbpath)
245
+ if iomerge_path is None:
246
+ iomerge_path = self._default_iomerge_path
247
+ if iocreate_path is None:
248
+ iocreate_path = self._default_iocreate_path
249
+ iocmd = [iomerge_path]
250
+ ioassign = self.sh.path.abspath(ioassign)
251
+ self.sh.xperm(ioassign, force=True)
252
+ with self.sh.cdcontext(dbpath, create=True):
253
+ iocmd.extend(['-d', thispwd])
254
+ for dbname in odbnames:
255
+ iocmd.extend(['-t', dbname])
256
+ with self.env.clone() as lenv:
257
+ lenv['ODB_IOASSIGN_BINARY'] = ioassign
258
+ if 'ODB_IOCREATE_COMMAND' not in lenv:
259
+ lenv['ODB_IOCREATE_COMMAND'] = iocreate_path
260
+ self.fix_db_path(layout, dbpath, env=lenv)
261
+ self.sh.spawn(iocmd, output=False)
262
+ return dbpath
263
+
264
+ def _ioassign_process(self, dbpaths, wmode):
265
+ with open('IOASSIGN', wmode) as fhgather:
266
+ for dbpath in dbpaths:
267
+ with open(self.sh.path.join(dbpath, 'IOASSIGN')) as fhlay:
268
+ for line in fhlay:
269
+ fhgather.write(line)
270
+
271
+ def ioassign_gather(self, *dbpaths):
272
+ """Gather IOASSIGN data from **dbpaths** databases and create a global IOASSIGN file."""
273
+ logger.info('ODB: creating a global IOASSIGN file from: %s',
274
+ ','.join([self.sh.path.basename(db) for db in dbpaths]))
275
+ self._ioassign_process(dbpaths, 'w')
276
+
277
+ def ioassign_append(self, *dbpaths):
278
+ """Append IOASSIGN data from **dbpaths** databases into the global IOASSIGN file."""
279
+ logger.info('ODB: extending the IOASSIGN file with: %s',
280
+ ','.join([self.sh.path.basename(db) for db in dbpaths]))
281
+ self._ioassign_process(dbpaths, 'a')
282
+
283
+ def shuffle_setup(self, slots, mergedirect=False, ccmadirect=False):
284
+ """Setup environment variables to control ODB shuffle behaviour.
285
+
286
+ :param bool mergedirect: Run the shuffle procedure on the input database.
287
+ :param bool ccmadirect: Create a CCMA database at the end of the run.
288
+ """
289
+ logger.info('ODB: shuffle_setup: mergedirect=%s, ccmadirect=%s',
290
+ str(mergedirect), str(ccmadirect))
291
+ if mergedirect or ccmadirect:
292
+ self.env.update(
293
+ ODB_CCMA_TSLOTS=slots.nslot,
294
+ )
295
+ self.env.default(
296
+ ODB_CCMA_LEFT_MARGIN=slots.leftmargin,
297
+ ODB_CCMA_RIGHT_MARGIN=slots.rightmargin,
298
+ )
299
+ if mergedirect:
300
+ self.env.default(ODB_MERGEODB_DIRECT=1)
301
+ if ccmadirect:
302
+ self.env.update(ODB_CCMA_CREATE_DIRECT=1)
303
+
304
+ def create_poolmask(self, layout, dbpath=None):
305
+ """Request the poolmask file creation."""
306
+ layout, dbpath, _ = self._process_layout_dbpath(layout, dbpath)
307
+ logger.info('ODB: requesting poolmask file for: %s (layout=%s).', dbpath, layout)
308
+ self.env.update(
309
+ ODB_CCMA_CREATE_POOLMASK=1,
310
+ ODB_CCMA_POOLMASK_FILE=self.sh.path.join(dbpath, layout + '.poolmask'),
311
+ )
312
+
313
+ def change_layout(self, layout, layout_new, dbpath=None):
314
+ """Make the appropriate renaming of files in ECMA to CCMA."""
315
+ layout, dbpath, _ = self._process_layout_dbpath(layout, dbpath)
316
+ layout_new = layout_new.upper()
317
+ logger.info('ODB: changing layout (%s -> %s) for %s.', layout, layout_new, dbpath)
318
+ to_cleanup = set()
319
+ for f in self.sh.ls(dbpath):
320
+ if self.sh.path.islink(self.sh.path.join(dbpath, f)):
321
+ fullpath = self.sh.path.join(dbpath, f)
322
+ target = self.sh.readlink(fullpath)
323
+ self.sh.unlink(fullpath)
324
+ self.sh.symlink(target.replace(layout, layout_new),
325
+ fullpath.replace(layout, layout_new))
326
+ continue
327
+ if f in [n.format(layout) for n in ('{:s}.dd', '{:s}.flags')]:
328
+ self.sh.mv(self.sh.path.join(dbpath, f),
329
+ self.sh.path.join(dbpath, f.replace(layout, layout_new)))
330
+ if f in [n.format(layout) for n in ('{:s}.iomap', '{:s}.sch',
331
+ '{:s}.IOASSIGN', 'IOASSIGN.{:s}', 'IOASSIGN')]:
332
+ tmp_target = self.sh.path.join(dbpath, f + '.tmp_new')
333
+ with open(self.sh.path.join(dbpath, f)) as inodb:
334
+ with open(tmp_target, 'w') as outodb:
335
+ for line in inodb:
336
+ outodb.write(line.replace(layout, layout_new))
337
+ self.sh.mv(tmp_target,
338
+ self.sh.path.join(dbpath, f.replace(layout, layout_new)))
339
+ if layout in f:
340
+ to_cleanup.add(self.sh.path.join(dbpath, f))
341
+ for f_name in to_cleanup:
342
+ self.sh.rm(f_name)
343
+
344
+
345
+ #: Footprint's attributes needed to ODB to setup properly
346
+ odbmix_attributes = footprints.Footprint(
347
+ info="Abstract ODB footprints' attributes.",
348
+ attr=dict(
349
+ npool=dict(
350
+ info='The number of pool(s) in the ODB database.',
351
+ type=int,
352
+ optional=True,
353
+ default=1,
354
+ ),
355
+ iomethod=dict(
356
+ info='The io_method of the ODB database.',
357
+ type=int,
358
+ optional=True,
359
+ default=1,
360
+ doc_zorder=-50,
361
+ ),
362
+ slots=dict(
363
+ info='The timeslots of the assimilation window.',
364
+ type=TimeSlots,
365
+ optional=True,
366
+ default=TimeSlots(7, chunk='PT1H'),
367
+ ),
368
+ virtualdb=dict(
369
+ info='The type of the virtual ODB database.',
370
+ optional=True,
371
+ default='ecma',
372
+ access='rwx',
373
+ doc_visibility=footprints.doc.visibility.ADVANCED,
374
+ ),
375
+ date=dict(
376
+ info='The current run date.',
377
+ optional=True,
378
+ access='rwx',
379
+ type=bdate.Date,
380
+ doc_zorder=-50,
381
+ ),
382
+ ioassign=dict(
383
+ info='The path to the ioassign binary (needed for merge/create actions',
384
+ optional=True,
385
+ default='ioassign.x',
386
+ )
387
+ ))
388
+
389
+
390
+ @algo_component_deco_mixin_autodoc
391
+ class OdbComponentDecoMixin(AlgoComponentDecoMixin):
392
+ """Handle ODB settings in AlgoComponents.
393
+
394
+ This mixin class is intended to be used with AlgoComponent classes. It will
395
+ automatically add footprints' arguments related to ODB
396
+ (see :data:`odbmix_attributes`), set up generic ODB environment variables
397
+ (:meth:`_odbobj_setup`) and provides a :attr:`odb` property that gives
398
+ access to a properly initialised :class:`OdbDriver` object that can be used
399
+ directly in AlgoComponents.
400
+
401
+ In addition it provides directly some utility methods that can be called
402
+ manually if needed.
403
+ """
404
+
405
+ _MIXIN_EXTRA_FOOTPRINTS = [odbmix_attributes, ]
406
+
407
+ def _odbobj_init(self, rh, opts): # @UnusedVariable
408
+ """Setup the OdbDriver object."""
409
+ cycle = ArpIfsSimplifiedCycle('cy01')
410
+ if rh and hasattr(rh.resource, 'cycle'):
411
+ cycle = rh.resource.cycle
412
+ self._odb = OdbDriver(
413
+ cycle=cycle,
414
+ sh=self.system,
415
+ env=self.env,
416
+ )
417
+ if self.system.path.exists(self.ioassign):
418
+ self._x_ioassign = self.system.path.abspath(self.ioassign)
419
+ else:
420
+ # Legacy...
421
+ self._x_ioassign = self.ioassign
422
+
423
+ def _odbobj_setup(self, rh, opts): # @UnusedVariable
424
+ """Setup the ODB object."""
425
+ self.odb.setup(
426
+ layout=self.virtualdb,
427
+ date=self.date,
428
+ npool=self.npool,
429
+ nslot=self.slots.nslot,
430
+ iomethod=self.iomethod,
431
+ )
432
+
433
+ _MIXIN_PREPARE_PREHOOKS = (_odbobj_init,)
434
+ _MIXIN_PREPARE_HOOKS = (_odbobj_setup,)
435
+
436
+ @property
437
+ def odb(self):
438
+ """Access to a properly initialised :class:`OdbDriver` object."""
439
+ if not hasattr(self, '_odb'):
440
+ raise RuntimeError('Uninitialised *odb* object.')
441
+ return self._odb
442
+
443
+ def lookupodb(self, fatal=True):
444
+ """Return a list of effective input resources which are odb observations."""
445
+ allodb = [
446
+ x for x in self.context.sequence.effective_inputs(kind='observations')
447
+ if x.rh.container.actualfmt == 'odb'
448
+ ]
449
+ allodb.sort(key=lambda s: s.rh.resource.part)
450
+ if not allodb and fatal:
451
+ logger.critical('Missing ODB input data for %s', self.fullname())
452
+ raise ValueError('Missing ODB input data')
453
+ return allodb
454
+
455
+ def odb_date_and_layout_from_sections(self, odbsections):
456
+ """
457
+ Look into the **odsections** section list in order to find the current
458
+ run date and ODB database layout.
459
+ """
460
+ alllayouts = {s.rh.resource.layout for s in odbsections}
461
+ alldates = {s.rh.resource.date for s in odbsections}
462
+ if len(alllayouts) != 1:
463
+ raise AlgoComponentError("Inconsistent ODB layouts")
464
+ if len(alldates) != 1:
465
+ raise AlgoComponentError("Inconsistent ODB dates")
466
+ self.virtualdb = alllayouts.pop()
467
+ self.date = alldates.pop()
468
+ logger.info('ODB: Detected from ODB database(s). self.date=%s, self.virtualdb=%s.',
469
+ self.date.stdvortex, self.virtualdb)
470
+
471
+ def _odb_find_ioassign_script(self, purpose):
472
+ """Look for ioassign script of *purpose" attribute, return path."""
473
+ scripts = [x.rh.container.abspath
474
+ for x in self.context.sequence.effective_inputs(kind='ioassign_script')
475
+ if x.rh.resource.purpose == purpose]
476
+ if len(scripts) > 1:
477
+ raise AlgoComponentError("More than one purpose={} ioassign_script found in resources.")
478
+ elif len(scripts) == 1:
479
+ self.system.xperm(scripts[0], force=True)
480
+ return scripts[0]
481
+ else:
482
+ return None
483
+
484
+ def odb_merge_if_needed(self, odbsections, subdir='.'):
485
+ """
486
+ If multiple ODB databases are listed in the **odsections** section list,
487
+ start an ODB merge.
488
+
489
+ :return: The path to the ODB database (the virtual database if a merge is
490
+ performed).
491
+ """
492
+ if len(odbsections) > 1 or self.virtualdb.lower() == 'ecma':
493
+ logger.info('ODB: merge for: %s.', self.virtualdb)
494
+ iomerge_p = self._odb_find_ioassign_script('merge')
495
+ iocreate_p = self._odb_find_ioassign_script('create')
496
+ with self.system.cdcontext(subdir):
497
+ virtualdb_path = self.odb.ioassign_merge(
498
+ layout=self.virtualdb,
499
+ ioassign=self._x_ioassign,
500
+ odbnames=[x.rh.resource.part for x in odbsections],
501
+ iomerge_path=iomerge_p,
502
+ iocreate_path=iocreate_p,
503
+ )
504
+ else:
505
+ virtualdb_path = self.system.path.abspath(odbsections[0].rh.container.localpath())
506
+ return virtualdb_path
507
+
508
+ def odb_create_db(self, layout, dbpath=None):
509
+ """Create a new empty ODB database.
510
+
511
+ :param str layout: The new database layout
512
+ :param str dbpath: The path to the new database (current_dir/layout if omitted)
513
+ :return: The path to the new ODB database.
514
+ """
515
+ dbout = self.odb.ioassign_create(
516
+ layout=layout,
517
+ npool=self.npool,
518
+ ioassign=self._x_ioassign,
519
+ dbpath=dbpath,
520
+ iocreate_path=self._odb_find_ioassign_script('create')
521
+ )
522
+ logger.info('ODB: database created: %s (layout=%s).', dbout, layout)
523
+ return dbout
524
+
525
+ def odb_handle_raw_dbs(self):
526
+ """Look for extras ODB raw databases and fix the environment accordingly."""
527
+ odbraw = [
528
+ x.rh for x in self.context.sequence.effective_inputs(kind='odbraw')
529
+ if x.rh.container.actualfmt == 'odb'
530
+ ]
531
+ if not odbraw:
532
+ logger.error('No ODB raw databases found.')
533
+ else:
534
+ for rraw in odbraw:
535
+ rawpath = rraw.container.localpath()
536
+ self.odb.fix_db_path(rraw.resource.layout, rawpath)
537
+ for badlink in [bl
538
+ for bl in self.system.glob(self.system.path.join(rawpath, '*.h'))
539
+ if self.system.path.islink(bl) and not self.system.path.exists(bl)]:
540
+ self.system.unlink(badlink)
541
+ self.odb.ioassign_append(* [rraw.container.localpath() for rraw in odbraw])
542
+ return odbraw
543
+
544
+ def odb_rw_or_overwrite_method(self, *dbsections):
545
+ """Are the input databases fetch with intent=inout ?"""
546
+ needs_work = [s for s in dbsections if s.intent == intent.IN]
547
+ if needs_work:
548
+ self.odb.force_overwrite_method()