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/folder.py ADDED
@@ -0,0 +1,834 @@
1
+ """
2
+ Various shell addons that handle formats relying on a folder structure.
3
+
4
+ In any kind of cache directories, the folder structure is kept as is. When
5
+ data are sent using FTP or SSH, a tar file is created on the fly.
6
+ """
7
+
8
+ import contextlib
9
+ import ftplib
10
+ import tempfile
11
+
12
+ from bronx.fancies import loggers
13
+ from vortex.tools.net import DEFAULT_FTP_PORT
14
+ from vortex.util.iosponge import IoSponge
15
+ from . import addons
16
+
17
+ #: No automatic export
18
+ __all__ = []
19
+
20
+ logger = loggers.getLogger(__name__)
21
+
22
+ _folder_exposed_methods = {
23
+ "cp",
24
+ "mv",
25
+ "forcepack",
26
+ "forceunpack",
27
+ "anyft_remote_rewrite",
28
+ "ftget",
29
+ "rawftget",
30
+ "batchrawftget",
31
+ "ftput",
32
+ "rawftput",
33
+ "scpget",
34
+ "scpput",
35
+ "ecfsget",
36
+ "ecfsput",
37
+ "ectransget",
38
+ "ectransput",
39
+ }
40
+
41
+
42
+ def folderize(cls):
43
+ """Create the necessary methods in a class that inherits from :class:`FolderShell`."""
44
+ addon_kind = cls.footprint_retrieve().get_values("kind")
45
+ if len(addon_kind) != 1:
46
+ raise SyntaxError(
47
+ "Authorised values for a given Addon's kind must be unique"
48
+ )
49
+ addon_kind = addon_kind[0]
50
+ for basic_mtdname in _folder_exposed_methods:
51
+ expected_mtdname = "{:s}_{:s}".format(addon_kind, basic_mtdname)
52
+ if not hasattr(cls, expected_mtdname):
53
+ parent_mtd = getattr(cls, "_folder_{:s}".format(basic_mtdname))
54
+ setattr(cls, expected_mtdname, parent_mtd)
55
+ return cls
56
+
57
+
58
+ class FolderShell(addons.FtrawEnableAddon):
59
+ """
60
+ This abstract class defines methods to manipulate folders.
61
+ """
62
+
63
+ _COMPRESSED = "gz"
64
+
65
+ _abstract = True
66
+ _footprint = dict(
67
+ info="Tools for manipulating folders",
68
+ attr=dict(
69
+ kind=dict(
70
+ values=["folder"],
71
+ ),
72
+ tmpname=dict(
73
+ optional=True,
74
+ default="folder_tmpunpack",
75
+ ),
76
+ pipeget=dict(
77
+ type=bool,
78
+ optional=True,
79
+ default=False,
80
+ ),
81
+ supportedfmt=dict(
82
+ optional=True,
83
+ default="[kind]",
84
+ ),
85
+ ),
86
+ )
87
+
88
+ def _folder_cp(
89
+ self,
90
+ source,
91
+ destination,
92
+ smartcp_threshold=0,
93
+ intent="in",
94
+ silent=False,
95
+ ):
96
+ """Extended copy for a folder repository."""
97
+ rc, source, destination = self._folder_tarfix_out(source, destination)
98
+ rc = rc and self.sh.cp(
99
+ source,
100
+ destination,
101
+ smartcp_threshold=smartcp_threshold,
102
+ intent=intent,
103
+ )
104
+ if rc:
105
+ rc, source, destination = self._folder_tarfix_in(
106
+ source, destination
107
+ )
108
+ if rc and intent == "inout":
109
+ self.sh.stderr("chmod", 0o644, destination)
110
+ with self.sh.mute_stderr():
111
+ for infile in self.sh.ffind(destination):
112
+ self.sh.chmod(infile, 0o644)
113
+ return rc
114
+
115
+ def _folder_mv(self, source, destination):
116
+ """Shortcut to :meth:`move` method (file or directory)."""
117
+ if not isinstance(source, str) or not isinstance(destination, str):
118
+ rc = self.sh.hybridcp(source, destination)
119
+ if isinstance(source, str):
120
+ rc = rc and self.sh.remove(source)
121
+ else:
122
+ rc, source, destination = self._folder_tarfix_out(
123
+ source, destination
124
+ )
125
+ rc = rc and self.sh.move(source, destination)
126
+ if rc:
127
+ rc, source, destination = self._folder_tarfix_in(
128
+ source, destination
129
+ )
130
+ return rc
131
+
132
+ def _folder_forcepack(self, source, destination=None):
133
+ """Returned a path to a packed data."""
134
+ if not self.sh.is_tarname(source):
135
+ destination = (
136
+ destination
137
+ if destination
138
+ else "{:s}.{:s}".format(
139
+ self.sh.safe_fileaddsuffix(source),
140
+ self._folder_tarfix_extension,
141
+ )
142
+ )
143
+ if not self.sh.path.exists(destination):
144
+ absdestination = self.sh.path.abspath(destination)
145
+ with self.sh.cdcontext(self.sh.path.dirname(source)):
146
+ self.sh.tar(absdestination, self.sh.path.basename(source))
147
+ return destination
148
+ else:
149
+ return source
150
+
151
+ def _folder_forceunpack(self, source):
152
+ """Unpack the data "inplace"."""
153
+ fakesource = "{:s}.{:s}".format(
154
+ self.sh.safe_fileaddsuffix(source), self._folder_tarfix_extension
155
+ )
156
+ rc, _, _ = self._folder_tarfix_in(fakesource, source)
157
+ return rc
158
+
159
+ def _folder_pack_stream(self, source, stdout=True):
160
+ source_name = self.sh.path.basename(source)
161
+ source_dirname = self.sh.path.dirname(source)
162
+ compression_map = {"gz": "z", "bz2": "j"}
163
+ compression_opt = compression_map.get(self._COMPRESSED, "")
164
+ cmd = [
165
+ "tar",
166
+ "--directory",
167
+ source_dirname,
168
+ "-c" + compression_opt,
169
+ source_name,
170
+ ]
171
+ return self.sh.popen(cmd, stdout=stdout, bufsize=8192)
172
+
173
+ def _folder_unpack_stream(self, stdin=True, options="xvf"):
174
+ return self.sh.popen(
175
+ # the z option is omitted consequently it also works if the file is not compressed
176
+ ["tar", options, "-"],
177
+ stdin=stdin,
178
+ bufsize=8192,
179
+ )
180
+
181
+ def _packed_size(self, source):
182
+ """Size of the final file, must be exact or be an overestimation.
183
+
184
+ A file 1 byte bigger than this estimation might be rejected,
185
+ hence the conservative options:
186
+ - tar adds 1% with a minimum of 1 Mbytes
187
+ - compression gain is 0%
188
+ """
189
+ dir_size = self.sh.treesize(source)
190
+ tar_mini = 1024 * 1024 # 1 Mbytes
191
+ tar_loss = 1 # 1%
192
+ zip_gain = 0
193
+ tar_size = dir_size + max(tar_mini, (dir_size * tar_loss) // 100)
194
+ return (tar_size * (100 - zip_gain)) // 100
195
+
196
+ def _folder_preftget(self, source, destination):
197
+ """Prepare source and destination"""
198
+ destination = self.sh.path.abspath(
199
+ self.sh.path.expanduser(destination)
200
+ )
201
+ self.sh.rm(destination)
202
+ return source, destination
203
+
204
+ def _folder_postftget(self, destination):
205
+ """Move the untared stuff to the destination and clean-up things."""
206
+ try:
207
+ unpacked = self.sh.glob("*")
208
+ if unpacked:
209
+ if len(unpacked) == 1 and self.sh.path.isdir(
210
+ self.sh.path.join(unpacked[-1])
211
+ ):
212
+ # This is the most usual case... (ODB, DDH packs produced by Vortex)
213
+ self.sh.wpermtree(unpacked[-1], force=True)
214
+ if self.sh.path.isdir(unpacked[-1]):
215
+ with self.sh.secure_directory_move(destination):
216
+ self.sh.mv(unpacked[-1], destination)
217
+ else:
218
+ self.sh.mv(unpacked[-1], destination)
219
+ else:
220
+ # Old-style DDH packs (produced by Olive)
221
+ with self.sh.secure_directory_move(destination):
222
+ self.sh.mkdir(destination)
223
+ for item in unpacked:
224
+ self.sh.wpermtree(item, force=True)
225
+ self.sh.mv(
226
+ item, self.sh.path.join(destination, item)
227
+ )
228
+ else:
229
+ logger.error("Nothing to unpack")
230
+ except Exception as trouble:
231
+ logger.critical("Unable to proceed folder post-ftget step")
232
+ raise trouble
233
+
234
+ @contextlib.contextmanager
235
+ def _folder_postftget_context(self, destination):
236
+ """Move the untared stuff to the destination and clean-up things."""
237
+ with self.sh.temporary_dir_cdcontext(
238
+ prefix="folder_", dir=self.sh.getcwd()
239
+ ):
240
+ try:
241
+ yield
242
+ finally:
243
+ self._folder_postftget(destination)
244
+
245
+ @contextlib.contextmanager
246
+ def _folder_ftget_file_extract(self, source):
247
+ ext_name = self.sh.tarname_splitext(source)[1]
248
+ target = self.tmpname + ext_name
249
+ try:
250
+ yield target
251
+ if self.sh.path.exists(target):
252
+ self.sh.untar(target, autocompress=False)
253
+ finally:
254
+ self.sh.rm(target)
255
+
256
+ @contextlib.contextmanager
257
+ def _folder_ftget_pipe_extract(self):
258
+ p = self._folder_unpack_stream()
259
+ yield p
260
+ self.sh.pclose(p)
261
+
262
+ @contextlib.contextmanager
263
+ def _folder_ftput_file_compress(self, source):
264
+ c_source = (
265
+ self.sh.safe_fileaddsuffix(source)
266
+ + "."
267
+ + self._folder_tarfix_extension
268
+ )
269
+ try:
270
+ self.sh.tar(c_source, source)
271
+ yield c_source
272
+ finally:
273
+ self.sh.rm(c_source)
274
+
275
+ @contextlib.contextmanager
276
+ def _folder_ftput_pipe_compress(self, source):
277
+ p = self._folder_pack_stream(source)
278
+ yield p
279
+ self.sh.pclose(p)
280
+
281
+ def _folder_anyft_remote_rewrite(self, remote):
282
+ """Add the folder suffix before using file transfert protocols."""
283
+ if not self.sh.is_tarname(self.sh.path.basename(remote)):
284
+ return "{:s}.{:s}".format(remote, self._folder_tarfix_extension)
285
+ else:
286
+ return remote
287
+
288
+ def _folder_ftget(
289
+ self,
290
+ source,
291
+ destination,
292
+ hostname=None,
293
+ logname=None,
294
+ port=DEFAULT_FTP_PORT,
295
+ cpipeline=None,
296
+ ):
297
+ """Proceed direct ftp get on the specified target."""
298
+ if cpipeline is not None:
299
+ raise OSError("It's not allowed to compress folder like data.")
300
+ hostname = self.sh.fix_fthostname(hostname)
301
+ with self.sh.ftpcontext(hostname, logname, port=port) as ftp:
302
+ if ftp:
303
+ source, destination = self._folder_preftget(
304
+ source, destination
305
+ )
306
+ with self._folder_postftget_context(destination):
307
+ try:
308
+ if self.pipeget:
309
+ with self._folder_ftget_pipe_extract() as p:
310
+ rc = ftp.get(source, p.stdin)
311
+ else:
312
+ with self._folder_ftget_file_extract(
313
+ source
314
+ ) as tmp_target:
315
+ rc = ftp.get(source, tmp_target)
316
+ except ftplib.all_errors as e:
317
+ logger.warning("An FTP error occurred: %s", str(e))
318
+ rc = False
319
+ return rc
320
+ else:
321
+ return False
322
+
323
+ def _folder_rawftget(
324
+ self,
325
+ source,
326
+ destination,
327
+ hostname=None,
328
+ logname=None,
329
+ port=None,
330
+ cpipeline=None,
331
+ ):
332
+ """Use ftserv as much as possible."""
333
+ if cpipeline is not None:
334
+ raise OSError("It's not allowed to compress folder like data.")
335
+ if self.sh.ftraw:
336
+ source, destination = self._folder_preftget(source, destination)
337
+ with self._folder_postftget_context(destination):
338
+ with self._folder_ftget_file_extract(source) as tmp_target:
339
+ rc = self.sh.ftserv_get(
340
+ source,
341
+ tmp_target,
342
+ hostname=hostname,
343
+ logname=logname,
344
+ port=port,
345
+ )
346
+ return rc
347
+ else:
348
+ if port is None:
349
+ port = DEFAULT_FTP_PORT
350
+ return self._folder_ftget(
351
+ source, destination, hostname, logname, port=port
352
+ )
353
+
354
+ def _folder_batchrawftget(
355
+ self,
356
+ source,
357
+ destination,
358
+ hostname=None,
359
+ logname=None,
360
+ port=None,
361
+ cpipeline=None,
362
+ ):
363
+ """Use ftserv to fetch several folder-like resources."""
364
+ if cpipeline is not None:
365
+ raise OSError("It's not allowed to compress folder like data.")
366
+ if self.sh.ftraw:
367
+ actualsources = list()
368
+ actualdestinations = list()
369
+ tmpdestinations = list()
370
+ try:
371
+ for s, d in zip(source, destination):
372
+ actual_s, actual_d = self._folder_preftget(s, d)
373
+ actualsources.append(actual_s)
374
+ actualdestinations.append(actual_d)
375
+ d_dirname = self.sh.path.dirname(actual_d)
376
+ self.sh.mkdir(d_dirname)
377
+ d_tmpdir = tempfile.mkdtemp(
378
+ prefix="folder_", dir=d_dirname
379
+ )
380
+ d_extname = self.sh.tarname_splitext(actual_s)[1]
381
+ tmpdestinations.append(
382
+ self.sh.path.join(d_tmpdir, self.tmpname + d_extname)
383
+ )
384
+
385
+ rc = self.sh.ftserv_batchget(
386
+ actualsources,
387
+ tmpdestinations,
388
+ hostname,
389
+ logname,
390
+ port=port,
391
+ )
392
+
393
+ for i, (d, t) in enumerate(
394
+ zip(actualdestinations, tmpdestinations)
395
+ ):
396
+ if rc[i]:
397
+ with self.sh.cdcontext(self.sh.path.dirname(t)):
398
+ try:
399
+ try:
400
+ rc[i] = rc[i] and bool(
401
+ self.sh.untar(
402
+ self.sh.path.basename(t),
403
+ autocompress=False,
404
+ )
405
+ )
406
+ finally:
407
+ self.sh.rm(t)
408
+ finally:
409
+ self._folder_postftget(d)
410
+ finally:
411
+ for t in tmpdestinations:
412
+ self.sh.rm(self.sh.path.dirname(t))
413
+ return rc
414
+ else:
415
+ raise RuntimeError("You are not supposed to land here !")
416
+
417
+ def _folder_ftput(
418
+ self,
419
+ source,
420
+ destination,
421
+ hostname=None,
422
+ logname=None,
423
+ port=DEFAULT_FTP_PORT,
424
+ cpipeline=None,
425
+ sync=False,
426
+ ):
427
+ """Proceed direct ftp put on the specified target."""
428
+ if cpipeline is not None:
429
+ raise OSError("It's not allowed to compress folder like data.")
430
+ hostname = self.sh.fix_fthostname(hostname)
431
+ source = self.sh.path.abspath(source)
432
+ with self.sh.ftpcontext(hostname, logname, port=port) as ftp:
433
+ if ftp:
434
+ packed_size = self._packed_size(source)
435
+ with self._folder_ftput_pipe_compress(source) as p:
436
+ sponge = IoSponge(p.stdout, guessed_size=packed_size)
437
+ try:
438
+ rc = ftp.put(
439
+ sponge, destination, size=sponge.size, exact=False
440
+ )
441
+ except ftplib.all_errors as e:
442
+ logger.warning("An FTP error occurred: %s", str(e))
443
+ rc = False
444
+ return rc
445
+ else:
446
+ return False
447
+
448
+ def _folder_rawftput(
449
+ self,
450
+ source,
451
+ destination,
452
+ hostname=None,
453
+ logname=None,
454
+ port=None,
455
+ cpipeline=None,
456
+ sync=False,
457
+ ):
458
+ """Use ftserv as much as possible."""
459
+ if cpipeline is not None:
460
+ raise OSError("It's not allowed to compress folder like data.")
461
+ if self.sh.ftraw and self.rawftshell is not None:
462
+ newsource = self.sh.copy2ftspool(
463
+ source, nest=True, fmt=self.supportedfmt
464
+ )
465
+ request = self.sh.path.dirname(newsource) + ".request"
466
+ with open(request, "w") as request_fh:
467
+ request_fh.write(str(self.sh.path.dirname(newsource)))
468
+ self.sh.readonly(request)
469
+ rc = self.sh.ftserv_put(
470
+ request,
471
+ destination,
472
+ hostname=hostname,
473
+ logname=logname,
474
+ port=port,
475
+ specialshell=self.rawftshell,
476
+ sync=sync,
477
+ )
478
+ self.sh.rm(request)
479
+ return rc
480
+ else:
481
+ if port is None:
482
+ port = DEFAULT_FTP_PORT
483
+ return self._folder_ftput(
484
+ source, destination, hostname, logname, port=port, sync=sync
485
+ )
486
+
487
+ def _folder_scpget(
488
+ self, source, destination, hostname, logname=None, cpipeline=None
489
+ ):
490
+ """Retrieve a folder using scp."""
491
+ if cpipeline is not None:
492
+ raise OSError("It's not allowed to compress folder like data.")
493
+ logname = self.sh.fix_ftuser(
494
+ hostname, logname, fatal=False, defaults_to_user=False
495
+ )
496
+ ssh = self.sh.ssh(hostname, logname)
497
+ rc = False
498
+ source, destination = self._folder_preftget(source, destination)
499
+ with self._folder_postftget_context(destination):
500
+ with self._folder_ftget_pipe_extract() as p:
501
+ rc = ssh.scpget_stream(source, p.stdin)
502
+ return rc
503
+
504
+ def _folder_scpput(
505
+ self, source, destination, hostname, logname=None, cpipeline=None
506
+ ):
507
+ """Upload a folder using scp."""
508
+ if cpipeline is not None:
509
+ raise OSError("It's not allowed to compress folder like data.")
510
+ source = self.sh.path.abspath(source)
511
+ logname = self.sh.fix_ftuser(
512
+ hostname, logname, fatal=False, defaults_to_user=False
513
+ )
514
+ ssh = self.sh.ssh(hostname, logname)
515
+ with self._folder_ftput_pipe_compress(source) as p:
516
+ rc = ssh.scpput_stream(p.stdout, destination)
517
+ return rc
518
+
519
+ @addons.require_external_addon("ecfs")
520
+ def _folder_ecfsget(self, source, target, cpipeline=None, options=None):
521
+ """Get a folder resource using ECfs.
522
+
523
+ :param source: source file
524
+ :param target: target file
525
+ :param cpipeline: compression pipeline to be used, if provided
526
+ :param options: list of options to be used
527
+ :return: return code and additional attributes used
528
+ """
529
+ # The folder must not be compressed
530
+ if cpipeline is not None:
531
+ raise OSError("It's not allowed to compress folder like data.")
532
+ source, target = self._folder_preftget(source, target)
533
+ with self._folder_postftget_context(target):
534
+ with self._folder_ftget_file_extract(source) as tmp_target:
535
+ rc = self.sh.ecfsget(
536
+ source=source, target=tmp_target, options=options
537
+ )
538
+ return rc
539
+
540
+ @addons.require_external_addon("ecfs")
541
+ def _folder_ecfsput(self, source, target, cpipeline=None, options=None):
542
+ """Put a folder resource using ECfs.
543
+
544
+ :param source: source file
545
+ :param target: target file
546
+ :param cpipeline: compression pipeline to be used, if provided
547
+ :param options: list of options to be used
548
+ :return: return code and additional attributes used
549
+ """
550
+ if cpipeline is not None:
551
+ raise OSError("It's not allowed to compress folder like data.")
552
+ source = self.sh.path.abspath(source)
553
+ with self._folder_ftput_file_compress(source) as c_source:
554
+ rc = self.sh.ecfsput(
555
+ source=c_source, target=target, options=options
556
+ )
557
+ return rc
558
+
559
+ @addons.require_external_addon("ectrans")
560
+ def _folder_ectransget(
561
+ self, source, target, gateway=None, remote=None, cpipeline=None
562
+ ):
563
+ """Get a folder resource using ECtrans.
564
+
565
+ :param source: source file
566
+ :param target: target file
567
+ :param gateway: gateway used by ECtrans
568
+ :param remote: remote used by ECtrans
569
+ :param cpipeline: compression pipeline to be used, if provided
570
+ :return: return code and additional attributes used
571
+ """
572
+ # The folder must not be compressed
573
+ if cpipeline is not None:
574
+ raise OSError("It's not allowed to compress folder like data.")
575
+ source, target = self._folder_preftget(source, target)
576
+ with self._folder_postftget_context(target):
577
+ with self._folder_ftget_file_extract(source) as tmp_target:
578
+ rc = self.sh.raw_ectransget(
579
+ source=source,
580
+ target=tmp_target,
581
+ gateway=gateway,
582
+ remote=remote,
583
+ )
584
+ return rc
585
+
586
+ @addons.require_external_addon("ectrans")
587
+ def _folder_ectransput(
588
+ self,
589
+ source,
590
+ target,
591
+ gateway=None,
592
+ remote=None,
593
+ cpipeline=None,
594
+ sync=False,
595
+ ):
596
+ """Put a folder resource using ECtrans.
597
+
598
+ :param source: source file
599
+ :param target: target file
600
+ :param gateway: gateway used by ECtrans
601
+ :param remote: remote used by ECtrans
602
+ :param cpipeline: compression pipeline to be used, if provided
603
+ :param bool sync: If False, allow asynchronous transfers.
604
+ :return: return code and additional attributes used
605
+ """
606
+ if cpipeline is not None:
607
+ raise OSError("It's not allowed to compress folder like data.")
608
+ source = self.sh.path.abspath(source)
609
+ with self._folder_ftput_file_compress(source) as c_source:
610
+ rc = self.sh.raw_ectransput(
611
+ source=c_source,
612
+ target=target,
613
+ gateway=gateway,
614
+ remote=remote,
615
+ sync=sync,
616
+ )
617
+ return rc
618
+
619
+ @property
620
+ def _folder_tarfix_extension(self):
621
+ """Return the extension of tar file associated with this extension."""
622
+ if self._COMPRESSED:
623
+ if self._COMPRESSED == "gz":
624
+ return "tgz"
625
+ elif self._COMPRESSED == "bz2":
626
+ return "tar.bz2"
627
+ else:
628
+ raise ValueError(
629
+ "Unsupported compression type: {:s}".format(
630
+ self._COMPRESSED
631
+ )
632
+ )
633
+ else:
634
+ return "tar"
635
+
636
+ def _folder_tarfix_in(self, source, destination):
637
+ """Automatically untar **source** if **source** is a tarfile and **destination** is not.
638
+
639
+ This is called after a copy was blindly done: a ``source='foo.tgz'`` might have
640
+ been copied to ``destination='bar'``, which must be untarred here.
641
+ """
642
+ ok = True
643
+ sh = self.sh
644
+ if sh.is_tarname(source) and not sh.is_tarname(destination):
645
+ logger.info(
646
+ "tarfix_in: untar from get <%s> to <%s>", source, destination
647
+ )
648
+ (destdir, destfile) = sh.path.split(sh.path.abspath(destination))
649
+ tar_ext = sh.tarname_splitext(source)[1]
650
+ desttar = sh.path.abspath(destination + tar_ext)
651
+ sh.remove(desttar)
652
+ ok = ok and sh.move(destination, desttar)
653
+ with sh.temporary_dir_cdcontext(prefix="untar_", dir=destdir):
654
+ ok = ok and sh.untar(desttar, output=False)
655
+ unpacked = sh.glob("*")
656
+ ok = (
657
+ ok and len(unpacked) == 1
658
+ ) # Only one element allowed in this kind of tarfiles
659
+ ok = ok and sh.move(
660
+ unpacked[0], sh.path.join(destdir, destfile)
661
+ )
662
+ ok = ok and sh.remove(desttar)
663
+ return (ok, source, destination)
664
+
665
+ def _folder_tarfix_out(self, source, destination):
666
+ """Automatically tar **source** if **destination** is a tarfile and **source** is not.
667
+
668
+ This is called after a copy was blindly done: a directory might have been copied
669
+ to ``destination='foo.tgz'`` or ``destination='foo.tar.bz2'``.
670
+ The tar and compression implied by the name must be addressed here.
671
+ """
672
+ ok = True
673
+ sh = self.sh
674
+ if sh.is_tarname(destination) and not sh.is_tarname(source):
675
+ logger.info(
676
+ "tarfix_out: tar before put <%s> to <%s>", source, destination
677
+ )
678
+ tar_ext = sh.tarname_splitext(destination)[1]
679
+ sourcetar = sh.path.abspath(source + tar_ext)
680
+ source_rel = sh.path.basename(source)
681
+ (sourcedir, sourcefile) = sh.path.split(sourcetar)
682
+ with sh.cdcontext(sourcedir):
683
+ ok = ok and sh.remove(sourcefile)
684
+ ok = ok and sh.tar(sourcefile, source_rel, output=False)
685
+ return (ok, sourcetar, destination)
686
+ else:
687
+ return (ok, source, destination)
688
+
689
+
690
+ @folderize
691
+ class OdbShell(FolderShell):
692
+ """
693
+ Default interface to ODB commands.
694
+ These commands extend the shell.
695
+ """
696
+
697
+ _footprint = dict(
698
+ info="Default ODB system interface",
699
+ attr=dict(
700
+ kind=dict(
701
+ values=["odb"],
702
+ ),
703
+ ),
704
+ )
705
+
706
+
707
+ @folderize
708
+ class DdhPackShell(FolderShell):
709
+ """
710
+ Default interface to DDHpack commands.
711
+ These commands extend the shell.
712
+ """
713
+
714
+ _footprint = dict(
715
+ info="Default DDHpack system interface",
716
+ attr=dict(
717
+ kind=dict(
718
+ values=["ddhpack"],
719
+ ),
720
+ ),
721
+ )
722
+
723
+
724
+ @folderize
725
+ class RawFilesShell(FolderShell):
726
+ """
727
+ Default interface to rawfiles commands.
728
+ These commands extend the shell.
729
+ """
730
+
731
+ _footprint = dict(
732
+ info="Default (g)RRRRawfiles system interface",
733
+ attr=dict(
734
+ kind=dict(
735
+ values=["rawfiles"],
736
+ ),
737
+ ),
738
+ )
739
+
740
+
741
+ @folderize
742
+ class ObsLocationPackShell(FolderShell):
743
+ """
744
+ Default interface to Obs Location packs commands.
745
+ These commands extend the shell.
746
+ """
747
+
748
+ _footprint = dict(
749
+ info="Default Obs Location packs system interface",
750
+ attr=dict(
751
+ kind=dict(
752
+ values=["obslocationpack"],
753
+ ),
754
+ ),
755
+ )
756
+
757
+
758
+ @folderize
759
+ class ObsFirePackShell(FolderShell):
760
+ """
761
+ Default interface to Obs Fire packs commands.
762
+ These commands extend the shell.
763
+ """
764
+
765
+ _footprint = dict(
766
+ info="Default Obs Location packs system interface",
767
+ attr=dict(
768
+ kind=dict(
769
+ values=["obsfirepack"],
770
+ ),
771
+ ),
772
+ )
773
+
774
+
775
+ @folderize
776
+ class WavesBCShell(FolderShell):
777
+ """
778
+ Default interface to waves bounding conditions commands.
779
+ These commands extend the shell.
780
+ """
781
+
782
+ _footprint = dict(
783
+ info="Default waves BC system interface",
784
+ attr=dict(
785
+ kind=dict(
786
+ values=["wbcpack"],
787
+ ),
788
+ ),
789
+ )
790
+
791
+
792
+ @folderize
793
+ class FilesPackShell(FolderShell):
794
+ """
795
+ Default interface to files packs commands.
796
+ These commands extend the shell.
797
+ """
798
+
799
+ _footprint = dict(
800
+ info="Default Files packs system interface",
801
+ attr=dict(
802
+ kind=dict(
803
+ values=["filespack"],
804
+ ),
805
+ ),
806
+ )
807
+
808
+
809
+ available_foldershells = [
810
+ e.footprint_values("kind")[0]
811
+ for e in locals().values()
812
+ if (
813
+ isinstance(e, type)
814
+ and issubclass(e, FolderShell)
815
+ and not e.footprint_abstract()
816
+ )
817
+ ]
818
+
819
+
820
+ class FolderShellsGroup(addons.AddonGroup):
821
+ """The whole bunch of folder shells."""
822
+
823
+ _footprint = dict(
824
+ info="The whole bunch of folder shells",
825
+ attr=dict(
826
+ kind=dict(
827
+ values=[
828
+ "allfolders",
829
+ ],
830
+ ),
831
+ ),
832
+ )
833
+
834
+ _addonslist = available_foldershells