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,480 @@
1
+ """
2
+ Interface to SMS commands.
3
+ """
4
+
5
+ import contextlib
6
+ import functools
7
+
8
+ from bronx.fancies import loggers
9
+ import footprints
10
+
11
+ from vortex import config
12
+ from .services import Service
13
+
14
+ __all__ = []
15
+
16
+ logger = loggers.getLogger(__name__)
17
+
18
+
19
+ class Scheduler(Service):
20
+ """Abstract class for scheduling systems."""
21
+
22
+ _abstract = True
23
+ _footprint = dict(
24
+ info="Scheduling service class",
25
+ attr=dict(
26
+ muteset=dict(
27
+ optional=True,
28
+ default=footprints.FPSet(),
29
+ type=footprints.FPSet,
30
+ )
31
+ ),
32
+ )
33
+
34
+ def __init__(self, *args, **kw):
35
+ logger.debug("Scheduler init %s", self.__class__)
36
+ super().__init__(*args, **kw)
37
+
38
+ @property
39
+ def env(self):
40
+ """Return the current active environment."""
41
+ return self.sh.env
42
+
43
+ def cmd_rename(self, cmd):
44
+ """Remap command name. Default is lowercase command name."""
45
+ return cmd.lower()
46
+
47
+ def mute(self, cmd):
48
+ """Switch off the given command."""
49
+ self.muteset.add(self.cmd_rename(cmd))
50
+
51
+ def play(self, cmd):
52
+ """Switch on the given command."""
53
+ self.muteset.discard(self.cmd_rename(cmd))
54
+
55
+ def clear(self):
56
+ """Clear set of mute commands."""
57
+ self.muteset.clear()
58
+
59
+
60
+ class EcmwfLikeScheduler(Scheduler):
61
+ """Abstract class for any ECMWF scheduling systems (SMS, Ecflow)."""
62
+
63
+ _abstract = True
64
+ _footprint = dict(
65
+ attr=dict(
66
+ env_pattern=dict(
67
+ info="Scheduler configuration variables start with...",
68
+ ),
69
+ non_critical_timeout=dict(
70
+ info="Timeout in seconds for non-critical commands",
71
+ type=int,
72
+ default=5,
73
+ optional=True,
74
+ ),
75
+ )
76
+ )
77
+
78
+ _KNOWN_CMD = ()
79
+
80
+ def __init__(self, *args, **kw):
81
+ logger.debug("Scheduler init %s", self.__class__)
82
+ super(Scheduler, self).__init__(*args, **kw)
83
+ self._inside_child_session = False
84
+
85
+ def conf(self, kwenv):
86
+ """Possibly export the provided variables and return a dictionary of positioned variables."""
87
+ if kwenv:
88
+ for schedvar in [
89
+ x.upper()
90
+ for x in kwenv.keys()
91
+ if x.upper().startswith(self.env_pattern)
92
+ ]:
93
+ self.env[schedvar] = str(kwenv[schedvar])
94
+ subenv = dict()
95
+ for schedvar in [
96
+ x for x in self.env.keys() if x.startswith(self.env_pattern)
97
+ ]:
98
+ subenv[schedvar] = self.env.get(schedvar)
99
+ return subenv
100
+
101
+ def info(self):
102
+ """Dump current defined variables."""
103
+ for schedvar, schedvalue in self.conf(dict()).items():
104
+ print('{:s}="{!s}"'.format(schedvar, schedvalue))
105
+
106
+ def __call__(self, *args):
107
+ """By default call the :meth:`info` method."""
108
+ return self.info()
109
+
110
+ @contextlib.contextmanager
111
+ def child_session_setup(self):
112
+ """This may be customised in order to setup session related stuff."""
113
+ yield True
114
+
115
+ @contextlib.contextmanager
116
+ def child_session(self):
117
+ """Prepare the environment and possibly setup the session.
118
+
119
+ It will only clone the environment and call child_session_setup
120
+ once (even if child is called again from within the first child call).
121
+ """
122
+ if not self._inside_child_session:
123
+ self._inside_child_session = True
124
+ try:
125
+ with self.env.clone():
126
+ with self.child_session_setup() as setup_rc:
127
+ yield setup_rc
128
+ finally:
129
+ self._inside_child_session = False
130
+ else:
131
+ yield True
132
+
133
+ def setup_default(self, *args):
134
+ """Fake method for any missing callback, ie: setup_init, setup_abort, etc."""
135
+ return True
136
+
137
+ def close_default(self, *args):
138
+ """Fake method for any missing callback, ie: close_init, close_abort, etc."""
139
+ return True
140
+
141
+ @contextlib.contextmanager
142
+ def wrap_actual_child_command(self, kwoptions):
143
+ """Last minute wrap before binary child command."""
144
+ yield True
145
+
146
+ def child(self, cmd, *options, **kwoptions):
147
+ """Miscellaneous sms/ecflow child sub-command."""
148
+ rc = None
149
+ cmd = self.cmd_rename(cmd)
150
+ if cmd in self.muteset:
151
+ logger.warning("%s mute command [%s]", self.kind, cmd)
152
+ else:
153
+ with self.child_session() as session_rc:
154
+ if session_rc:
155
+ if getattr(self, "setup_" + cmd, self.setup_default)(
156
+ *options
157
+ ):
158
+ wrapp_rc = False
159
+ try:
160
+ with self.wrap_actual_child_command(
161
+ kwoptions
162
+ ) as wrapp_rc:
163
+ if wrapp_rc:
164
+ rc = self._actual_child(
165
+ cmd, options, **kwoptions
166
+ )
167
+ else:
168
+ logger.warning(
169
+ "Actual [%s %s] command wrap failed",
170
+ self.kind,
171
+ cmd,
172
+ )
173
+ finally:
174
+ if wrapp_rc:
175
+ getattr(
176
+ self, "close_" + cmd, self.close_default
177
+ )(*options)
178
+ else:
179
+ logger.warning(
180
+ "Actual [%s %s] command skipped due to setup action",
181
+ self.kind,
182
+ cmd,
183
+ )
184
+ else:
185
+ logger.warning(
186
+ "Actual [%s %s] command skipped session setup failure",
187
+ self.kind,
188
+ cmd,
189
+ )
190
+ return rc
191
+
192
+ def _actual_child(self, cmd, options, critical=True):
193
+ """The actual child command implementation."""
194
+ raise NotImplementedError("This an abstract method.")
195
+
196
+ def __getattr__(self, name):
197
+ """Deal with any known commands."""
198
+ if name in self._KNOWN_CMD:
199
+ return functools.partial(self.child, name)
200
+ else:
201
+ raise AttributeError(name)
202
+
203
+
204
+ class SMS(EcmwfLikeScheduler):
205
+ """
206
+ Client interface to SMS scheduling and monitoring system.
207
+ """
208
+
209
+ _footprint = dict(
210
+ info="SMS client service",
211
+ attr=dict(
212
+ kind=dict(
213
+ values=["sms"],
214
+ ),
215
+ rootdir=dict(
216
+ optional=True,
217
+ default=None,
218
+ alias=("install",),
219
+ ),
220
+ env_pattern=dict(
221
+ default="SMS",
222
+ optional=True,
223
+ ),
224
+ ),
225
+ )
226
+
227
+ _KNOWN_CMD = (
228
+ "abort",
229
+ "complete",
230
+ "event",
231
+ "init",
232
+ "label",
233
+ "meter",
234
+ "msg",
235
+ "variable",
236
+ "fix",
237
+ )
238
+
239
+ def __init__(self, *args, **kw):
240
+ logger.debug("SMS scheduler client init %s", self)
241
+ super().__init__(*args, **kw)
242
+ self._actual_rootdir = self.rootdir
243
+ if self._actual_rootdir is None:
244
+ self._actual_rootdir = (
245
+ self.env.SMS_INSTALL_ROOT
246
+ or config.from_config(section="sms", key="rootdir")
247
+ )
248
+ if self.sh.path.exists(self.cmdpath("init")):
249
+ self.env.setbinpath(self._actual_rootdir)
250
+ else:
251
+ logger.warning(
252
+ "No SMS client found at init time [rootdir:%s]>",
253
+ self._actual_rootdir,
254
+ )
255
+
256
+ def cmd_rename(self, cmd):
257
+ """Remap command name. Strip any sms prefix."""
258
+ cmd = super().cmd_rename(cmd)
259
+ while cmd.startswith("sms"):
260
+ cmd = cmd[3:]
261
+ return cmd
262
+
263
+ def cmdpath(self, cmd):
264
+ """Return a complete binary path to cmd."""
265
+ cmd = "sms" + self.cmd_rename(cmd)
266
+ if self._actual_rootdir:
267
+ return self.sh.path.join(self._actual_rootdir, cmd)
268
+ else:
269
+ return cmd
270
+
271
+ def path(self):
272
+ """Return actual binary path to SMS commands."""
273
+ return self._actual_rootdir
274
+
275
+ @contextlib.contextmanager
276
+ def child_session_setup(self):
277
+ """Setup the path to the SMS client."""
278
+ with super().child_session_setup() as setup_rc:
279
+ self.env.SMSACTUALPATH = self._actual_rootdir
280
+ yield setup_rc
281
+
282
+ @contextlib.contextmanager
283
+ def wrap_actual_child_command(self, kwoptions):
284
+ """Last minute wrap before binary child command."""
285
+ with super().wrap_actual_child_command(kwoptions) as wrapp_rc:
286
+ upd_env = dict()
287
+ if not kwoptions.get("critical", True):
288
+ upd_env["SMSDENIED"] = 1
289
+ if self.non_critical_timeout:
290
+ upd_env["SMSTIMEOUT"] = self.non_critical_timeout
291
+ if upd_env:
292
+ with self.env.delta_context(**upd_env):
293
+ yield wrapp_rc
294
+ else:
295
+ yield wrapp_rc
296
+
297
+ def _actual_child(self, cmd, options, critical=True):
298
+ """Miscellaneous smschild subcommand."""
299
+ args = [self.cmdpath(cmd)]
300
+ args.extend(options)
301
+ return self.sh.spawn(args, output=False, fatal=critical)
302
+
303
+
304
+ class SMSColor(SMS):
305
+ """
306
+ Default SMS service with some extra colorful features.
307
+ """
308
+
309
+ _footprint = dict(
310
+ info="SMS color client service",
311
+ attr=dict(
312
+ kind=dict(
313
+ values=["smscolor"],
314
+ ),
315
+ ),
316
+ )
317
+
318
+ @contextlib.contextmanager
319
+ def wrap_actual_child_command(self, kwoptions):
320
+ """Last minute wrap before binary child command."""
321
+ with super().wrap_actual_child_command(kwoptions) as wrapp_rc:
322
+ print("SMS COLOR")
323
+ yield wrapp_rc
324
+
325
+
326
+ class EcFlow(EcmwfLikeScheduler):
327
+ """
328
+ Client interface to the ecFlow scheduling and monitoring system.
329
+ """
330
+
331
+ _footprint = dict(
332
+ info="SMS client service",
333
+ attr=dict(
334
+ kind=dict(
335
+ values=["ecflow"],
336
+ ),
337
+ clientpath=dict(
338
+ info=(
339
+ "Path to the ecFlow client binary (if omitted, "
340
+ + "it's read in the configuration file)"
341
+ ),
342
+ optional=True,
343
+ default=None,
344
+ ),
345
+ env_pattern=dict(
346
+ default="ECF_",
347
+ optional=True,
348
+ ),
349
+ ),
350
+ )
351
+
352
+ _KNOWN_CMD = (
353
+ "abort",
354
+ "complete",
355
+ "event",
356
+ "init",
357
+ "label",
358
+ "meter",
359
+ "msg",
360
+ "alter",
361
+ )
362
+
363
+ def __init__(self, *args, **kw):
364
+ logger.debug("EcFlow scheduler client init %s", self)
365
+ super().__init__(*args, **kw)
366
+ self._actual_clientpath = self.clientpath
367
+
368
+ def path(self):
369
+ """Return the actual binary path to the EcFlow client."""
370
+ if self._actual_clientpath is None:
371
+ thistarget = self.sh.default_target
372
+ guesspath = self.env.ECF_CLIENT_PATH or thistarget.get(
373
+ "ecflow:clientpath"
374
+ )
375
+ ecfversion = self.env.get("ECF_VERSION", "default")
376
+ guesspath = guesspath.format(version=ecfversion)
377
+ if guesspath is None:
378
+ logger.warning(
379
+ "ecFlow service could not guess the install location [%s]",
380
+ str(guesspath),
381
+ )
382
+ else:
383
+ self._actual_clientpath = guesspath
384
+ if not self.sh.path.exists(self._actual_clientpath):
385
+ logger.warning(
386
+ "No ecFlow client found at init time [path:%s]>",
387
+ self._actual_clientpath,
388
+ )
389
+ return self._actual_clientpath
390
+
391
+ @contextlib.contextmanager
392
+ def child_session_setup(self):
393
+ """Setup a SSH tunnel if necessary."""
394
+ with super().child_session_setup() as setup_rc:
395
+ if setup_rc and not self.sh.default_target.isnetworknode:
396
+ tunnel = None
397
+ # wait and retries from config
398
+ thistarget = self.sh.default_target
399
+ sshwait = float(thistarget.get("ecflow:sshproxy_wait", 6))
400
+ sshretries = float(
401
+ thistarget.get("ecflow:sshproxy_retries", 2)
402
+ )
403
+ sshretrydelay = float(
404
+ thistarget.get("ecflow:sshproxy_retrydelay", 1)
405
+ )
406
+ # Build up an SSH tunnel to convey the EcFlow command
407
+ ecconf = self.conf(dict())
408
+ echost = ecconf.get("{:s}HOST".format(self.env_pattern), None)
409
+ ecport = ecconf.get("{:s}PORT".format(self.env_pattern), None)
410
+ if not (echost and ecport):
411
+ setup_rc = False
412
+ else:
413
+ sshobj = self.sh.ssh(
414
+ "network",
415
+ virtualnode=True,
416
+ mandatory_hostcheck=False,
417
+ maxtries=sshretries,
418
+ triesdelay=sshretrydelay,
419
+ )
420
+ tunnel = sshobj.tunnel(
421
+ echost, int(ecport), maxwait=sshwait
422
+ )
423
+ if not tunnel:
424
+ setup_rc = False
425
+ else:
426
+ newvars = {
427
+ "{:s}HOST".format(self.env_pattern): "localhost",
428
+ "{:s}PORT".format(
429
+ self.env_pattern
430
+ ): tunnel.entranceport,
431
+ }
432
+ self.env.update(**newvars)
433
+ try:
434
+ yield setup_rc
435
+ finally:
436
+ # Close the SSH tunnel regardless of the exit status
437
+ if tunnel:
438
+ tunnel.close()
439
+ else:
440
+ yield setup_rc
441
+
442
+ @contextlib.contextmanager
443
+ def wrap_actual_child_command(self, kwoptions):
444
+ """Last minute wrap before binary child command."""
445
+ with super().wrap_actual_child_command(kwoptions) as wrapp_rc:
446
+ upd_env = dict()
447
+ if not kwoptions.get("critical", True):
448
+ upd_env["{:s}DENIED".format(self.env_pattern)] = 1
449
+ if self.non_critical_timeout:
450
+ upd_env["{:s}TIMEOUT".format(self.env_pattern)] = (
451
+ self.non_critical_timeout
452
+ )
453
+ if upd_env:
454
+ with self.env.delta_context(**upd_env):
455
+ yield wrapp_rc
456
+ else:
457
+ yield wrapp_rc
458
+
459
+ def _actual_child(self, cmd, options, critical=True):
460
+ """Miscellaneous ecFlow sub-command."""
461
+ args = [
462
+ self.path(),
463
+ ]
464
+ if options:
465
+ args.append("--{:s}={!s}".format(cmd, options[0]))
466
+ if len(options) > 1:
467
+ args.extend(options[1:])
468
+ else:
469
+ args.append("--{:s}".format(cmd))
470
+ args = [str(a) for a in args]
471
+ logger.info("Issuing the ecFlow command: %s", " ".join(args[1:]))
472
+ return self.sh.spawn(args, output=False, fatal=critical)
473
+
474
+ def abort(self, *opts):
475
+ """Gateway to :meth:`child` abort method."""
476
+ actual_opts = list(opts)
477
+ if not actual_opts:
478
+ # For backward compatibility with SMS
479
+ actual_opts.append("No abort reason provided")
480
+ return self.child("abort", *actual_opts)