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