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
vortex/tools/grib.py ADDED
@@ -0,0 +1,738 @@
1
+ """
2
+ Module needed to interact with GRIB files.
3
+
4
+ It provides shell addons to deal with:
5
+
6
+ * Splitted GRIB files (as produced by the Arpege/IFS IO server)
7
+ * The ability to compare GRIB files
8
+
9
+ It also provdes an AlgoComponent's Mixin to properly setup the environment
10
+ when using the grib_api or ecCodes libraries.
11
+ """
12
+
13
+ from pathlib import Path
14
+ from urllib import parse as urlparse
15
+
16
+ import re
17
+
18
+ from bronx.fancies import loggers
19
+ import footprints
20
+
21
+ from . import addons
22
+ from vortex.config import get_from_config_w_default
23
+ from vortex.algo.components import (
24
+ AlgoComponentDecoMixin,
25
+ algo_component_deco_mixin_autodoc,
26
+ )
27
+ from vortex.tools.net import DEFAULT_FTP_PORT
28
+
29
+ #: No automatic export
30
+ __all__ = []
31
+
32
+ logger = loggers.getLogger(__name__)
33
+
34
+
35
+ def use_in_shell(sh, **kw):
36
+ """Extend current shell with the LFI interface defined by optional arguments."""
37
+ kw["shell"] = sh
38
+ return footprints.proxy.addon(**kw)
39
+
40
+
41
+ class GRIB_Tool(addons.FtrawEnableAddon):
42
+ """
43
+ Handle multipart-GRIB files properly.
44
+ """
45
+
46
+ _footprint = dict(
47
+ info="Default GRIB system interface",
48
+ attr=dict(
49
+ kind=dict(
50
+ values=["grib"],
51
+ ),
52
+ ),
53
+ )
54
+
55
+ def _std_grib_index_get(self, source):
56
+ with open(source) as fd:
57
+ gribparts = fd.read().splitlines()
58
+ return [urlparse.urlparse(url).path for url in gribparts]
59
+
60
+ xgrib_index_get = _std_grib_index_get
61
+
62
+ def _std_grib_index_write(self, destination, gribpaths):
63
+ gribparts = [
64
+ str(urlparse.urlunparse(("file", "", path, "", "", "")))
65
+ for path in gribpaths
66
+ ]
67
+ tmpfile = self.sh.safe_fileaddsuffix(destination)
68
+ with open(tmpfile, "w") as fd:
69
+ fd.write("\n".join(gribparts))
70
+ return self.sh.move(tmpfile, destination)
71
+
72
+ def is_xgrib(self, source):
73
+ """Check if the given ``source`` is a multipart-GRIB file."""
74
+ rc = False
75
+ if source and isinstance(source, str) and self.sh.path.exists(source):
76
+ with open(source, "rb") as fd:
77
+ rc = fd.read(7) == b"file://"
78
+ return rc
79
+
80
+ def _backend_cp(
81
+ self, source, destination, smartcp_threshold=0, intent="in"
82
+ ):
83
+ return self.sh.cp(
84
+ source,
85
+ destination,
86
+ smartcp_threshold=smartcp_threshold,
87
+ intent=intent,
88
+ smartcp=True,
89
+ )
90
+
91
+ def _backend_rm(self, *args):
92
+ return self.sh.rm(*args)
93
+
94
+ def _backend_mv(self, source, destination):
95
+ return self.sh.mv(source, destination)
96
+
97
+ def _std_remove(self, *args):
98
+ """Remove (possibly) multi GRIB files."""
99
+ rc = True
100
+ for pname in args:
101
+ for objpath in self.sh.glob(pname):
102
+ if self.is_xgrib(objpath):
103
+ with self.sh.mute_stderr():
104
+ idx = self._std_grib_index_get(objpath)
105
+ target_dirs = set()
106
+ for a_mpart in idx:
107
+ target_dirs.add(self.sh.path.dirname(a_mpart))
108
+ rc = rc and self._backend_rm(a_mpart)
109
+ for a_dir in target_dirs:
110
+ # Only if the directory is empty
111
+ if not self.sh.listdir(a_dir):
112
+ rc = rc and self._backend_rm(a_dir)
113
+ rc = rc and self._backend_rm(objpath)
114
+ else:
115
+ rc = rc and self._backend_rm(objpath)
116
+ return rc
117
+
118
+ grib_rm = grib_remove = _std_remove
119
+
120
+ def _std_copy(
121
+ self,
122
+ source,
123
+ destination,
124
+ smartcp_threshold=0,
125
+ intent="in",
126
+ pack=False,
127
+ silent=False,
128
+ ):
129
+ """Extended copy for (possibly) multi GRIB file."""
130
+ # Might be multipart
131
+ if self.is_xgrib(source):
132
+ rc = True
133
+ if isinstance(destination, str) and not pack:
134
+ with self.sh.mute_stderr():
135
+ idx = self._std_grib_index_get(source)
136
+ destdir = self.sh.path.abspath(
137
+ self.sh.path.expanduser(destination) + ".d"
138
+ )
139
+ rc = rc and self.sh.mkdir(destdir)
140
+ target_idx = list()
141
+ for i, a_mpart in enumerate(idx):
142
+ target_idx.append(
143
+ self.sh.path.join(
144
+ destdir, "GRIB_mpart{:06d}".format(i)
145
+ )
146
+ )
147
+ rc = rc and self._backend_cp(
148
+ a_mpart,
149
+ target_idx[-1],
150
+ smartcp_threshold=smartcp_threshold,
151
+ intent=intent,
152
+ )
153
+ rc = rc and self._std_grib_index_write(
154
+ destination, target_idx
155
+ )
156
+ if intent == "in":
157
+ self.sh.chmod(destination, 0o444)
158
+ else:
159
+ rc = rc and self.xgrib_pack(source, destination)
160
+ else:
161
+ # Usual file or file descriptor
162
+ rc = self._backend_cp(
163
+ source,
164
+ destination,
165
+ smartcp_threshold=smartcp_threshold,
166
+ intent=intent,
167
+ )
168
+ return rc
169
+
170
+ grib_cp = grib_copy = _std_copy
171
+
172
+ def _std_move(self, source, destination):
173
+ """Extended mv for (possibly) multi GRIB file."""
174
+ # Might be multipart
175
+ if self.is_xgrib(source):
176
+ intent = "inout" if self.sh.access(source, self.sh.W_OK) else "in"
177
+ rc = self._std_copy(source, destination, intent=intent)
178
+ rc = rc and self._std_remove(source)
179
+ else:
180
+ rc = self._backend_mv(source, destination)
181
+ return rc
182
+
183
+ grib_mv = grib_move = _std_move
184
+
185
+ def _pack_stream(self, source, stdout=True):
186
+ cmd = [
187
+ "cat",
188
+ ]
189
+ cmd.extend(self._std_grib_index_get(source))
190
+ return self.sh.popen(cmd, stdout=stdout, bufsize=8192)
191
+
192
+ def _packed_size(self, source):
193
+ total = 0
194
+ for filepath in self._std_grib_index_get(source):
195
+ size = self.sh.size(filepath)
196
+ if size == -1:
197
+ return None
198
+ total += size
199
+ return total
200
+
201
+ def xgrib_pack(self, source, destination, intent="in"):
202
+ """Manually pack a multi GRIB."""
203
+ if isinstance(destination, str):
204
+ tmpfile = self.sh.safe_fileaddsuffix(destination)
205
+ with open(tmpfile, "wb") as fd:
206
+ p = self._pack_stream(source, stdout=fd)
207
+ self.sh.pclose(p)
208
+ if intent == "in":
209
+ self.sh.chmod(tmpfile, 0o444)
210
+ return self.sh.move(tmpfile, destination)
211
+ else:
212
+ p = self._pack_stream(source, stdout=destination)
213
+ self.sh.pclose(p)
214
+ return True
215
+
216
+ def _std_forcepack(self, source, destination=None):
217
+ """Returned a path to a packed data."""
218
+ if self.is_xgrib(source):
219
+ destination = (
220
+ destination
221
+ if destination
222
+ else self.sh.safe_fileaddsuffix(source)
223
+ )
224
+ if not self.sh.path.exists(destination):
225
+ if self.xgrib_pack(source, destination):
226
+ return destination
227
+ else:
228
+ raise OSError("XGrib packing failed")
229
+ else:
230
+ return destination
231
+ else:
232
+ return source
233
+
234
+ grib_forcepack = _std_forcepack
235
+
236
+ def _std_ftput(
237
+ self,
238
+ source,
239
+ destination,
240
+ hostname=None,
241
+ logname=None,
242
+ port=DEFAULT_FTP_PORT,
243
+ cpipeline=None,
244
+ sync=False,
245
+ ):
246
+ """On the fly packing and ftp."""
247
+ if self.is_xgrib(source):
248
+ if cpipeline is not None:
249
+ raise OSError("It's not allowed to compress xgrib files.")
250
+ hostname = self.sh.fix_fthostname(hostname)
251
+ ftp = self.sh.ftp(hostname, logname, port=port)
252
+ if ftp:
253
+ packed_size = self._packed_size(source)
254
+ p = self._pack_stream(source)
255
+ rc = ftp.put(
256
+ p.stdout, destination, size=packed_size, exact=True
257
+ )
258
+ self.sh.pclose(p)
259
+ ftp.close()
260
+ else:
261
+ rc = False
262
+ return rc
263
+ else:
264
+ return self.sh.ftput(
265
+ source,
266
+ destination,
267
+ hostname=hostname,
268
+ logname=logname,
269
+ port=port,
270
+ cpipeline=cpipeline,
271
+ sync=sync,
272
+ )
273
+
274
+ def _std_rawftput(
275
+ self,
276
+ source,
277
+ destination,
278
+ hostname=None,
279
+ logname=None,
280
+ port=None,
281
+ cpipeline=None,
282
+ sync=False,
283
+ ):
284
+ """Use ftserv as much as possible."""
285
+ if self.is_xgrib(source):
286
+ if cpipeline is not None:
287
+ raise OSError("It's not allowed to compress xgrib files.")
288
+ if self.sh.ftraw and self.rawftshell is not None:
289
+ # Copy the GRIB pieces individually
290
+ pieces = self.xgrib_index_get(source)
291
+ newsources = [
292
+ str(self.sh.copy2ftspool(piece)) for piece in pieces
293
+ ]
294
+ request = newsources[0] + ".request"
295
+ with open(request, "w") as request_fh:
296
+ request_fh.writelines("\n".join(newsources))
297
+ self.sh.readonly(request)
298
+ rc = self.sh.ftserv_put(
299
+ request,
300
+ destination,
301
+ hostname=hostname,
302
+ logname=logname,
303
+ port=port,
304
+ specialshell=self.rawftshell,
305
+ sync=sync,
306
+ )
307
+ self.sh.rm(request)
308
+ return rc
309
+ else:
310
+ if port is None:
311
+ port = DEFAULT_FTP_PORT
312
+ return self._std_ftput(
313
+ source,
314
+ destination,
315
+ hostname=hostname,
316
+ logname=logname,
317
+ port=port,
318
+ sync=sync,
319
+ )
320
+ else:
321
+ return self.sh.rawftput(
322
+ source,
323
+ destination,
324
+ hostname=hostname,
325
+ logname=logname,
326
+ port=port,
327
+ cpipeline=cpipeline,
328
+ sync=sync,
329
+ )
330
+
331
+ grib_ftput = _std_ftput
332
+ grib_rawftput = _std_rawftput
333
+
334
+ def _std_scpput(
335
+ self, source, destination, hostname, logname=None, cpipeline=None
336
+ ):
337
+ """On the fly packing and scp."""
338
+ if self.is_xgrib(source):
339
+ if cpipeline is not None:
340
+ raise OSError("It's not allowed to compress xgrib files.")
341
+ logname = self.sh.fix_ftuser(
342
+ hostname, logname, fatal=False, defaults_to_user=False
343
+ )
344
+ ssh = self.sh.ssh(hostname, logname)
345
+ permissions = ssh.get_permissions(source)
346
+ # remove the .d companion directory (scp_stream removes the destination)
347
+ # go on on failure : the .d lingers on, but the grib will be self-contained
348
+ ssh.remove(destination + ".d")
349
+ p = self._pack_stream(source)
350
+ rc = ssh.scpput_stream(
351
+ p.stdout, destination, permissions=permissions
352
+ )
353
+ self.sh.pclose(p)
354
+ return rc
355
+ else:
356
+ return self.sh.scpput(
357
+ source,
358
+ destination,
359
+ hostname,
360
+ logname=logname,
361
+ cpipeline=cpipeline,
362
+ )
363
+
364
+ grib_scpput = _std_scpput
365
+
366
+ @addons.require_external_addon("ecfs")
367
+ def grib_ecfsput(self, source, target, cpipeline=None, options=None):
368
+ """Put a grib resource using ECfs.
369
+
370
+ :param source: source file
371
+ :param target: target file
372
+ :param cpipeline: compression pipeline used, if provided
373
+ :param options: list of options to be used
374
+ :return: return code and additional attributes used
375
+ """
376
+ if self.is_xgrib(source):
377
+ if cpipeline is not None:
378
+ raise OSError("It's not allowed to compress xgrib files.")
379
+ psource = self.sh.safe_fileaddsuffix(source)
380
+ try:
381
+ rc = self.xgrib_pack(source=source, destination=psource)
382
+ dict_args = dict()
383
+ if rc:
384
+ rc, dict_args = self.sh.ecfsput(
385
+ source=psource, target=target, options=options
386
+ )
387
+ finally:
388
+ self.sh.rm(psource)
389
+ return rc, dict_args
390
+ else:
391
+ return self.sh.ecfsput(
392
+ source=source,
393
+ target=target,
394
+ options=options,
395
+ cpipeline=cpipeline,
396
+ )
397
+
398
+ @addons.require_external_addon("ectrans")
399
+ def grib_ectransput(
400
+ self,
401
+ source,
402
+ target,
403
+ gateway=None,
404
+ remote=None,
405
+ cpipeline=None,
406
+ sync=False,
407
+ ):
408
+ """Put a grib resource using ECtrans.
409
+
410
+ :param source: source file
411
+ :param target: target file
412
+ :param gateway: gateway used by ECtrans
413
+ :param remote: remote used by ECtrans
414
+ :param cpipeline: compression pipeline used, if provided
415
+ :param bool sync: If False, allow asynchronous transfers
416
+ :return: return code and additional attributes used
417
+ """
418
+ if self.is_xgrib(source):
419
+ if cpipeline is not None:
420
+ raise OSError("It's not allowed to compress xgrib files.")
421
+ psource = self.sh.safe_fileaddsuffix(source)
422
+ try:
423
+ rc = self.xgrib_pack(source=source, destination=psource)
424
+ dict_args = dict()
425
+ if rc:
426
+ rc, dict_args = self.sh.raw_ectransput(
427
+ source=psource,
428
+ target=target,
429
+ gateway=gateway,
430
+ remote=remote,
431
+ sync=sync,
432
+ )
433
+ finally:
434
+ self.sh.rm(psource)
435
+ return rc, dict_args
436
+ else:
437
+ return self.sh.ectransput(
438
+ source=source,
439
+ target=target,
440
+ gateway=gateway,
441
+ remote=remote,
442
+ cpipeline=cpipeline,
443
+ sync=sync,
444
+ )
445
+
446
+
447
+ @algo_component_deco_mixin_autodoc
448
+ class EcGribDecoMixin(AlgoComponentDecoMixin):
449
+ """Extend Algo Components with EcCodes/GribApi features."
450
+
451
+ This mixin class is intended to be used with AlgoComponent classes. It will
452
+ automatically set up the ecCodes/GribApi environment variable given the
453
+ path to the EcCodes/GribApi library (which is found by performing a ``ldd``
454
+ on the AlgoComponent's target binary).
455
+ """
456
+
457
+ _ECGRIB_SETUP_COMPAT = True
458
+ _ECGRIB_SETUP_FATAL = True
459
+
460
+ def _ecgrib_libs_detext(self, rh):
461
+ """Run ldd and tries to find ecCodes or grib_api libraries locations."""
462
+ eccodes_lib = None
463
+ gribapi_lib = None
464
+ if rh is not None:
465
+ if not isinstance(rh, (list, tuple)):
466
+ rh = [
467
+ rh,
468
+ ]
469
+ for a_rh in rh:
470
+ libs = self.system.ldd(a_rh.container.localpath())
471
+ a_eccodes_lib = None
472
+ a_gribapi_lib = None
473
+ for lib, path in libs.items():
474
+ if re.match(
475
+ r"^libeccodes(?:-[.0-9]+)?\.so(?:\.[.0-9]+)?$", lib
476
+ ):
477
+ a_eccodes_lib = path
478
+ if re.match(
479
+ r"^libgrib_api(?:-[.0-9]+)?\.so(?:\.[.0-9]+)?$", lib
480
+ ):
481
+ a_gribapi_lib = path
482
+ if a_eccodes_lib:
483
+ self.algoassert(
484
+ eccodes_lib is None or (eccodes_lib == a_eccodes_lib),
485
+ "ecCodes library inconsistency (rh: {!s})".format(
486
+ a_rh
487
+ ),
488
+ )
489
+ eccodes_lib = a_eccodes_lib
490
+ if a_gribapi_lib:
491
+ self.algoassert(
492
+ gribapi_lib is None or (gribapi_lib == a_gribapi_lib),
493
+ "grib_api library inconsistency (rh: {!s})".format(
494
+ a_rh
495
+ ),
496
+ )
497
+ gribapi_lib = a_gribapi_lib
498
+ return eccodes_lib, gribapi_lib
499
+
500
+ def _ecgrib_additional_config(self, a_role, a_var):
501
+ """Add axtra definitions/samples to the library path."""
502
+ for gdef in self.context.sequence.effective_inputs(role=a_role):
503
+ local_path = gdef.rh.container.localpath()
504
+ new_path = (
505
+ local_path
506
+ if self.system.path.isdir(local_path)
507
+ else self.system.path.dirname(local_path)
508
+ )
509
+ # NB: Grib-API doesn't understand relative paths...
510
+ new_path = self.system.path.abspath(new_path)
511
+ self.env.setgenericpath(a_var, new_path, pos=0)
512
+
513
+ def _gribapi_envsetup(self, gribapi_lib):
514
+ """Setup environment variables for grib_api."""
515
+ defvar = "GRIB_DEFINITION_PATH"
516
+ samplevar = "GRIB_SAMPLES_PATH"
517
+ if gribapi_lib is not None:
518
+ gribapi_root = self.system.path.dirname(gribapi_lib)
519
+ gribapi_root = self.system.path.split(gribapi_root)[0]
520
+ gribapi_share = self.system.path.join(
521
+ gribapi_root, "share", "grib_api"
522
+ )
523
+ if defvar not in self.env:
524
+ # This one is for compatibility with old versions of the gribapi !
525
+ self.env.setgenericpath(
526
+ defvar,
527
+ self.system.path.join(
528
+ gribapi_root, "share", "definitions"
529
+ ),
530
+ )
531
+ # This should be the lastest one:
532
+ self.env.setgenericpath(
533
+ defvar, self.system.path.join(gribapi_share, "definitions")
534
+ )
535
+ if samplevar not in self.env:
536
+ # This one is for compatibility with old versions of the gribapi !
537
+ self.env.setgenericpath(
538
+ samplevar,
539
+ self.system.path.join(
540
+ gribapi_root, "ifs_samples", "grib1"
541
+ ),
542
+ )
543
+ # This should be the lastest one:
544
+ self.env.setgenericpath(
545
+ samplevar,
546
+ self.system.path.join(
547
+ gribapi_share, "ifs_samples", "grib1"
548
+ ),
549
+ )
550
+ else:
551
+ # Use the default GRIB-API config if the ldd approach fails
552
+ self.export("gribapi")
553
+ return defvar, samplevar
554
+
555
+ def gribapi_setup(self, rh, opts):
556
+ """Setup the grib_api related stuff."""
557
+ _, gribapi_lib = self._ecgrib_libs_detext(rh)
558
+ defvar, samplevar = self._gribapi_envsetup(gribapi_lib)
559
+ self._ecgrib_additional_config("AdditionalGribAPIDefinitions", defvar)
560
+ self._ecgrib_additional_config("AdditionalGribAPISamples", samplevar)
561
+ # Recap
562
+ for a_var in (defvar, samplevar):
563
+ logger.info(
564
+ "After gribapi_setup %s = %s", a_var, self.env.getvar(a_var)
565
+ )
566
+
567
+ def _eccodes_envsetup(
568
+ self,
569
+ eccodes_lib,
570
+ envvar="ECCODES_DEFINITIONS_PATH",
571
+ tgt_path="definitions",
572
+ ):
573
+ """Export envirionment variables required by ECCODES
574
+
575
+ Value is
576
+
577
+ /path/to/eccodes-X.Y.Z/share/eccodes/<target_path>
578
+
579
+ eccodes_lib: Absolute path to the eccodes so file
580
+ envvar: Name of the environment variable to export
581
+ tgt_path: Name of the eccodes install subdirectory to appear
582
+ in the value
583
+ """
584
+ if envvar in self.env:
585
+ return envvar
586
+ if envvar.replace("ECCODES", "GRIB") in self.env:
587
+ logger.warning(
588
+ (
589
+ "%s is left unconfigured because the old grib_api's"
590
+ "variable is defined. ",
591
+ "Please remove that!",
592
+ ),
593
+ envvar,
594
+ )
595
+ return envvar.replace("ECCODES", "GRIB")
596
+ eccodes_root = Path(eccodes_lib).parent.parent
597
+ self.env.setgenericpath(
598
+ envvar,
599
+ str(eccodes_root / "share" / "eccodes" / tgt_path),
600
+ )
601
+ return envvar
602
+
603
+ def eccodes_setup(self, rh, opts, compat=False, fatal=True):
604
+ """Setup the grib_api related stuff.
605
+
606
+ If **compat** is ``True`` and ecCodes is not found, the old grib_api
607
+ will be set-up. Otherwise, it will just return (if **fatal** is ``False``)
608
+ or raise an exception (if **fatal** is ``True``).
609
+ """
610
+ # Detect the library's path and setup appropriate variables
611
+ eccodes_lib, gribapi_lib = self._ecgrib_libs_detext(rh)
612
+ if eccodes_lib is not None:
613
+ defvar = self._eccodes_envsetup(
614
+ eccodes_lib,
615
+ envvar="ECCODES_DEFINITIONS_PATH",
616
+ tgt_path="definitions",
617
+ )
618
+ subdir = Path("ifs_samples") / (
619
+ "grib1" if rh.resource.cycle < "cy49" else "grib1_mlgrib2"
620
+ )
621
+ samplevar = self._eccodes_envsetup(
622
+ eccodes_lib,
623
+ envvar="ECCODES_SAMPLES_PATH",
624
+ tgt_path=subdir,
625
+ )
626
+ elif compat:
627
+ defvar, samplevar = self._gribapi_envsetup(gribapi_lib)
628
+ else:
629
+ if fatal:
630
+ raise RuntimeError(
631
+ "No suitable configuration found for ecCodes."
632
+ )
633
+ else:
634
+ logger.error("ecCodes was not found !")
635
+ return
636
+ # Then, inspect the context to look for customised search paths
637
+ self._ecgrib_additional_config(
638
+ ("AdditionalGribAPIDefinitions", "AdditionalEcCodesDefinitions"),
639
+ defvar,
640
+ )
641
+ self._ecgrib_additional_config(
642
+ ("AdditionalGribAPISamples", "AdditionalEcCodesSamples"), samplevar
643
+ )
644
+ # Recap
645
+ for a_var in (defvar, samplevar):
646
+ logger.info(
647
+ "After eccodes_setup (compat=%s) : %s = %s",
648
+ str(compat),
649
+ a_var,
650
+ self.env.getvar(a_var),
651
+ )
652
+
653
+ def _ecgrib_mixin_setup(self, rh, opts):
654
+ self.eccodes_setup(
655
+ rh,
656
+ opts,
657
+ compat=self._ECGRIB_SETUP_COMPAT,
658
+ fatal=self._ECGRIB_SETUP_FATAL,
659
+ )
660
+
661
+ _MIXIN_PREPARE_HOOKS = (_ecgrib_mixin_setup,)
662
+
663
+
664
+ class GRIBAPI_Tool(addons.Addon):
665
+ """
666
+ Interface to gribapi commands (designed as a shell Addon).
667
+ """
668
+
669
+ _footprint = dict(
670
+ info="Default GRIBAPI system interface",
671
+ attr=dict(
672
+ kind=dict(
673
+ values=["gribapi"],
674
+ ),
675
+ ),
676
+ )
677
+
678
+ def __init__(self, *args, **kw):
679
+ """Addon initialisation."""
680
+ super().__init__(*args, **kw)
681
+ # Additionaly, check for the GRIB_API_ROOTDIR key in the config file
682
+ if self.path is None and self.cfginfo is not None:
683
+ addon_rootdir = get_from_config_w_default(
684
+ section=self.cfginfo,
685
+ key="grib_api_rootdir",
686
+ default=None,
687
+ )
688
+ if addon_rootdir is not None:
689
+ self.path = addon_rootdir
690
+
691
+ def _spawn_wrap(self, cmd, **kw):
692
+ """Internal method calling standard shell spawn."""
693
+ cmd[0] = "bin" + self.sh.path.sep + cmd[0]
694
+ return super()._spawn_wrap(cmd, **kw)
695
+
696
+ def _actual_diff(self, grib1, grib2, skipkeys, **kw):
697
+ """Run the actual GRIBAPI command."""
698
+ cmd = ["grib_compare", "-r", "-b", ",".join(skipkeys), grib1, grib2]
699
+ kw["fatal"] = False
700
+ kw["output"] = False
701
+ return self._spawn_wrap(cmd, **kw)
702
+
703
+ def grib_diff(
704
+ self, grib1, grib2, skipkeys=("generatingProcessIdentifier",), **kw
705
+ ):
706
+ """
707
+ Difference between two GRIB files (using the GRIB-API)
708
+
709
+ :param grib1: first file to compare
710
+ :param grib2: second file to compare
711
+ :param skipkeys: List of GRIB keys that will be ignored
712
+
713
+ GRIB messages may not be in the same order in both files.
714
+
715
+ If *grib1* or *grib2* are multipart files, they will be concatenated
716
+ prior to the comparison.
717
+ """
718
+
719
+ # Are multipart GRIB suported ?
720
+ xgrib_support = "grib" in self.sh.loaded_addons()
721
+ grib1_ori = grib1
722
+ grib2_ori = grib2
723
+ if xgrib_support:
724
+ if self.sh.is_xgrib(grib1):
725
+ grib1 = self.sh.safe_fileaddsuffix(grib1_ori) + "_diffcat"
726
+ self.sh.xgrib_pack(grib1_ori, grib1)
727
+ if self.sh.is_xgrib(grib2):
728
+ grib2 = self.sh.safe_fileaddsuffix(grib2_ori) + "_diffcat"
729
+ self.sh.xgrib_pack(grib2_ori, grib2)
730
+
731
+ rc = self._actual_diff(grib1, grib2, skipkeys, **kw)
732
+
733
+ if xgrib_support and grib1 != grib1_ori:
734
+ self.sh.grib_rm(grib1)
735
+ if xgrib_support and grib2 != grib2_ori:
736
+ self.sh.grib_rm(grib2)
737
+
738
+ return rc