vortex-nwp 2.0.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. vortex/__init__.py +135 -0
  2. vortex/algo/__init__.py +12 -0
  3. vortex/algo/components.py +2136 -0
  4. vortex/algo/mpitools.py +1648 -0
  5. vortex/algo/mpitools_templates/envelope_wrapper_default.tpl +27 -0
  6. vortex/algo/mpitools_templates/envelope_wrapper_mpiauto.tpl +29 -0
  7. vortex/algo/mpitools_templates/wrapstd_wrapper_default.tpl +18 -0
  8. vortex/algo/serversynctools.py +170 -0
  9. vortex/config.py +115 -0
  10. vortex/data/__init__.py +13 -0
  11. vortex/data/abstractstores.py +1572 -0
  12. vortex/data/containers.py +780 -0
  13. vortex/data/contents.py +596 -0
  14. vortex/data/executables.py +284 -0
  15. vortex/data/flow.py +113 -0
  16. vortex/data/geometries.ini +2689 -0
  17. vortex/data/geometries.py +703 -0
  18. vortex/data/handlers.py +1021 -0
  19. vortex/data/outflow.py +67 -0
  20. vortex/data/providers.py +465 -0
  21. vortex/data/resources.py +201 -0
  22. vortex/data/stores.py +1271 -0
  23. vortex/gloves.py +282 -0
  24. vortex/layout/__init__.py +27 -0
  25. vortex/layout/appconf.py +109 -0
  26. vortex/layout/contexts.py +511 -0
  27. vortex/layout/dataflow.py +1069 -0
  28. vortex/layout/jobs.py +1276 -0
  29. vortex/layout/monitor.py +833 -0
  30. vortex/layout/nodes.py +1424 -0
  31. vortex/layout/subjobs.py +464 -0
  32. vortex/nwp/__init__.py +11 -0
  33. vortex/nwp/algo/__init__.py +12 -0
  34. vortex/nwp/algo/assim.py +483 -0
  35. vortex/nwp/algo/clim.py +920 -0
  36. vortex/nwp/algo/coupling.py +609 -0
  37. vortex/nwp/algo/eda.py +632 -0
  38. vortex/nwp/algo/eps.py +613 -0
  39. vortex/nwp/algo/forecasts.py +745 -0
  40. vortex/nwp/algo/fpserver.py +927 -0
  41. vortex/nwp/algo/ifsnaming.py +403 -0
  42. vortex/nwp/algo/ifsroot.py +311 -0
  43. vortex/nwp/algo/monitoring.py +202 -0
  44. vortex/nwp/algo/mpitools.py +554 -0
  45. vortex/nwp/algo/odbtools.py +974 -0
  46. vortex/nwp/algo/oopsroot.py +735 -0
  47. vortex/nwp/algo/oopstests.py +186 -0
  48. vortex/nwp/algo/request.py +579 -0
  49. vortex/nwp/algo/stdpost.py +1285 -0
  50. vortex/nwp/data/__init__.py +12 -0
  51. vortex/nwp/data/assim.py +392 -0
  52. vortex/nwp/data/boundaries.py +261 -0
  53. vortex/nwp/data/climfiles.py +539 -0
  54. vortex/nwp/data/configfiles.py +149 -0
  55. vortex/nwp/data/consts.py +929 -0
  56. vortex/nwp/data/ctpini.py +133 -0
  57. vortex/nwp/data/diagnostics.py +181 -0
  58. vortex/nwp/data/eda.py +148 -0
  59. vortex/nwp/data/eps.py +383 -0
  60. vortex/nwp/data/executables.py +1039 -0
  61. vortex/nwp/data/fields.py +96 -0
  62. vortex/nwp/data/gridfiles.py +308 -0
  63. vortex/nwp/data/logs.py +551 -0
  64. vortex/nwp/data/modelstates.py +334 -0
  65. vortex/nwp/data/monitoring.py +220 -0
  66. vortex/nwp/data/namelists.py +644 -0
  67. vortex/nwp/data/obs.py +748 -0
  68. vortex/nwp/data/oopsexec.py +72 -0
  69. vortex/nwp/data/providers.py +182 -0
  70. vortex/nwp/data/query.py +217 -0
  71. vortex/nwp/data/stores.py +147 -0
  72. vortex/nwp/data/surfex.py +338 -0
  73. vortex/nwp/syntax/__init__.py +9 -0
  74. vortex/nwp/syntax/stdattrs.py +375 -0
  75. vortex/nwp/tools/__init__.py +10 -0
  76. vortex/nwp/tools/addons.py +35 -0
  77. vortex/nwp/tools/agt.py +55 -0
  78. vortex/nwp/tools/bdap.py +48 -0
  79. vortex/nwp/tools/bdcp.py +38 -0
  80. vortex/nwp/tools/bdm.py +21 -0
  81. vortex/nwp/tools/bdmp.py +49 -0
  82. vortex/nwp/tools/conftools.py +1311 -0
  83. vortex/nwp/tools/drhook.py +62 -0
  84. vortex/nwp/tools/grib.py +268 -0
  85. vortex/nwp/tools/gribdiff.py +99 -0
  86. vortex/nwp/tools/ifstools.py +163 -0
  87. vortex/nwp/tools/igastuff.py +249 -0
  88. vortex/nwp/tools/mars.py +56 -0
  89. vortex/nwp/tools/odb.py +548 -0
  90. vortex/nwp/tools/partitioning.py +234 -0
  91. vortex/nwp/tools/satrad.py +56 -0
  92. vortex/nwp/util/__init__.py +6 -0
  93. vortex/nwp/util/async.py +184 -0
  94. vortex/nwp/util/beacon.py +40 -0
  95. vortex/nwp/util/diffpygram.py +359 -0
  96. vortex/nwp/util/ens.py +198 -0
  97. vortex/nwp/util/hooks.py +128 -0
  98. vortex/nwp/util/taskdeco.py +81 -0
  99. vortex/nwp/util/usepygram.py +591 -0
  100. vortex/nwp/util/usetnt.py +87 -0
  101. vortex/proxy.py +6 -0
  102. vortex/sessions.py +341 -0
  103. vortex/syntax/__init__.py +9 -0
  104. vortex/syntax/stdattrs.py +628 -0
  105. vortex/syntax/stddeco.py +176 -0
  106. vortex/toolbox.py +982 -0
  107. vortex/tools/__init__.py +11 -0
  108. vortex/tools/actions.py +457 -0
  109. vortex/tools/addons.py +297 -0
  110. vortex/tools/arm.py +76 -0
  111. vortex/tools/compression.py +322 -0
  112. vortex/tools/date.py +20 -0
  113. vortex/tools/ddhpack.py +10 -0
  114. vortex/tools/delayedactions.py +672 -0
  115. vortex/tools/env.py +513 -0
  116. vortex/tools/folder.py +663 -0
  117. vortex/tools/grib.py +559 -0
  118. vortex/tools/lfi.py +746 -0
  119. vortex/tools/listings.py +354 -0
  120. vortex/tools/names.py +575 -0
  121. vortex/tools/net.py +1790 -0
  122. vortex/tools/odb.py +10 -0
  123. vortex/tools/parallelism.py +336 -0
  124. vortex/tools/prestaging.py +186 -0
  125. vortex/tools/rawfiles.py +10 -0
  126. vortex/tools/schedulers.py +413 -0
  127. vortex/tools/services.py +871 -0
  128. vortex/tools/storage.py +1061 -0
  129. vortex/tools/surfex.py +61 -0
  130. vortex/tools/systems.py +3396 -0
  131. vortex/tools/targets.py +384 -0
  132. vortex/util/__init__.py +9 -0
  133. vortex/util/config.py +1071 -0
  134. vortex/util/empty.py +24 -0
  135. vortex/util/helpers.py +184 -0
  136. vortex/util/introspection.py +63 -0
  137. vortex/util/iosponge.py +76 -0
  138. vortex/util/roles.py +51 -0
  139. vortex/util/storefunctions.py +103 -0
  140. vortex/util/structs.py +26 -0
  141. vortex/util/worker.py +150 -0
  142. vortex_nwp-2.0.0b1.dist-info/LICENSE +517 -0
  143. vortex_nwp-2.0.0b1.dist-info/METADATA +50 -0
  144. vortex_nwp-2.0.0b1.dist-info/RECORD +146 -0
  145. vortex_nwp-2.0.0b1.dist-info/WHEEL +5 -0
  146. vortex_nwp-2.0.0b1.dist-info/top_level.txt +1 -0
vortex/tools/addons.py ADDED
@@ -0,0 +1,297 @@
1
+ """
2
+ Abstract classes for System addons.
3
+ """
4
+
5
+ from collections import defaultdict
6
+
7
+ from bronx.fancies import loggers
8
+ from bronx.syntax.decorators import nicedeco
9
+ import footprints
10
+
11
+ from vortex.config import get_from_config_w_default
12
+ from vortex.layout import contexts
13
+ from vortex.tools.env import Environment
14
+ from vortex.tools.systems import OSExtended
15
+
16
+ logger = loggers.getLogger(__name__)
17
+
18
+ #: No automatic export
19
+ __all__ = []
20
+
21
+
22
+ class Addon(footprints.FootprintBase):
23
+ """Root class for any :class:`Addon` system subclasses."""
24
+
25
+ _abstract = True
26
+ _collector = ('addon',)
27
+ _footprint = dict(
28
+ info = 'Default add-on',
29
+ attr = dict(
30
+ kind = dict(),
31
+ sh = dict(
32
+ type = OSExtended,
33
+ alias = ('shell',),
34
+ access = 'rwx-weak',
35
+ ),
36
+ env = dict(
37
+ type = Environment,
38
+ optional = True,
39
+ default = None,
40
+ access = 'rwx',
41
+ doc_visibility = footprints.doc.visibility.ADVANCED
42
+ ),
43
+ cfginfo = dict(
44
+ optional = True,
45
+ default = '[kind]',
46
+ doc_visibility = footprints.doc.visibility.ADVANCED
47
+ ),
48
+ cmd = dict(
49
+ optional = True,
50
+ default = None,
51
+ access = 'rwx',
52
+ ),
53
+ path = dict(
54
+ optional = True,
55
+ default = None,
56
+ access = 'rwx',
57
+ ),
58
+ cycle = dict(
59
+ optional = True,
60
+ default = None,
61
+ access = 'rwx',
62
+ ),
63
+ toolkind = dict(
64
+ optional = True,
65
+ default = None
66
+ ),
67
+ )
68
+ )
69
+
70
+ def __init__(self, *args, **kw):
71
+ """Abstract Addon initialisation."""
72
+ logger.debug('Abstract Addon init %s', self.__class__)
73
+ super().__init__(*args, **kw)
74
+ self.sh.extend(self)
75
+ self._context_cache = defaultdict(dict)
76
+ self._cmd_xperms_cache = set()
77
+ if self.env is None:
78
+ self.env = Environment(active=False, clear=True)
79
+ clsenv = self.__class__.__dict__
80
+ for k in [x for x in clsenv.keys() if x.isupper()]:
81
+ self.env[k] = clsenv[k]
82
+ if self.path is None:
83
+ self.path = get_from_config_w_default(
84
+ section="nwp-tools", key=self.kind, default=None,
85
+ )
86
+
87
+ @classmethod
88
+ def in_shell(cls, shell):
89
+ """Grep any active instance of that class in the specified shell."""
90
+ lx = [x for x in shell.search if isinstance(x, cls)]
91
+ return lx[0] if lx else None
92
+
93
+ def _query_context(self):
94
+ """Return the path and cmd for the current context.
95
+
96
+ Results are cached so that the context's localtracker is explored only once.
97
+
98
+ .. note:: We use the localtracker instead of the sequence because, in
99
+ multistep jobs, the localtracker is preserved between steps. It's
100
+ less elegant but it plays nice with MTOOL.
101
+ """
102
+ ctxtag = contexts.Context.tag_focus()
103
+ if ctxtag not in self._context_cache and self.toolkind is not None:
104
+ ltrack = contexts.current().localtracker
105
+ # NB: 'str' is important because local might be in unicode...
106
+ candidates = [str(self.sh.path.realpath(local))
107
+ for local, entry in ltrack.items()
108
+ if (entry.latest_rhdict('get').get('resource', dict()).get('kind', '') ==
109
+ self.toolkind)]
110
+ if candidates:
111
+ realpath = candidates.pop()
112
+ self._context_cache[ctxtag] = dict(path=self.sh.path.dirname(realpath),
113
+ cmd=self.sh.path.basename(realpath))
114
+ return self._context_cache[ctxtag]
115
+
116
+ @property
117
+ def actual_path(self):
118
+ """The path that should be used in the current context."""
119
+ infos = self._query_context()
120
+ ctxpath = infos.get('path', None)
121
+ return self.path if ctxpath is None else ctxpath
122
+
123
+ @property
124
+ def actual_cmd(self):
125
+ """The cmd that should be used in the current context."""
126
+ infos = self._query_context()
127
+ ctxcmd = infos.get('cmd', None)
128
+ return self.cmd if ctxcmd is None else ctxcmd
129
+
130
+ def _spawn_commons(self, cmd, **kw):
131
+ """Internal method setting local environment and calling standard shell spawn."""
132
+
133
+ # Is there a need for an interpreter ?
134
+ if 'interpreter' in kw:
135
+ cmd.insert(0, kw.pop('interpreter'))
136
+ else:
137
+ # The first element of the command line needs to be executable
138
+ if cmd[0] not in self._cmd_xperms_cache:
139
+ self._cmd_xperms_cache.add(cmd[0])
140
+ self.sh.xperm(cmd[0], force=True)
141
+
142
+ # Overwrite global module env values with specific ones
143
+ with self.sh.env.clone() as localenv:
144
+
145
+ localenv.verbose(True, self.sh)
146
+ localenv.update(self.env)
147
+
148
+ # Check if a pipe is requested
149
+ inpipe = kw.pop('inpipe', False)
150
+
151
+ # Ask the attached shell to run the addon command
152
+ if inpipe:
153
+ kw.setdefault('stdout', True)
154
+ rc = self.sh.popen(cmd, **kw)
155
+ else:
156
+ rc = self.sh.spawn(cmd, **kw)
157
+
158
+ return rc
159
+
160
+ def _spawn(self, cmd, **kw):
161
+ """Internal method setting local environment and calling standard shell spawn."""
162
+
163
+ # Insert the actual tool command as first argument
164
+ cmd.insert(0, self.actual_cmd)
165
+ if self.actual_path is not None:
166
+ cmd[0] = self.actual_path + '/' + cmd[0]
167
+
168
+ return self._spawn_commons(cmd, **kw)
169
+
170
+ def _spawn_wrap(self, cmd, **kw):
171
+ """Internal method setting local environment and calling standard shell spawn."""
172
+
173
+ # Insert the tool path before the first argument
174
+ if self.actual_path is not None:
175
+ cmd[0] = self.actual_path + '/' + cmd[0]
176
+
177
+ return self._spawn_commons(cmd, **kw)
178
+
179
+
180
+ class FtrawEnableAddon(Addon):
181
+ """Root class for any :class:`Addon` system subclasses that needs to override rawftput."""
182
+
183
+ _abstract = True
184
+ _footprint = dict(
185
+ info = 'Default add-on with rawftput support.',
186
+ attr = dict(
187
+ rawftshell = dict(
188
+ info = "Path to ftserv's concatenation shell",
189
+ optional = True,
190
+ default = None,
191
+ access = 'rwx',
192
+ doc_visibility = footprints.doc.visibility.GURU,
193
+ ),
194
+ )
195
+ )
196
+
197
+ def __init__(self, *args, **kw):
198
+ """Abstract Addon initialisation."""
199
+ logger.debug('Abstract Addon init %s', self.__class__)
200
+ super().__init__(*args, **kw)
201
+ # If needed, look in the config file for the rawftshell
202
+ if self.rawftshell is None:
203
+ self.rawftshell = get_from_config_w_default(
204
+ section="rawftshell", key=self.kind, default=None,
205
+ )
206
+
207
+
208
+ class AddonGroup(footprints.FootprintBase):
209
+ """Root class for any :class:`AddonGroup` system subclasses.
210
+
211
+ An AddonGroup is not really an Addon... it just loads a bunch of other
212
+ Addons or AddonGroups into the current shell.
213
+ """
214
+
215
+ _abstract = True
216
+ _collector = ('addon',)
217
+ _footprint = dict(
218
+ info = 'Default add-on group',
219
+ attr = dict(
220
+ kind = dict(),
221
+ sh = dict(
222
+ type = OSExtended,
223
+ alias = ('shell',),
224
+ ),
225
+ env = dict(
226
+ type = Environment,
227
+ optional = True,
228
+ default = None,
229
+ doc_visibility = footprints.doc.visibility.ADVANCED,
230
+ ),
231
+ cycle = dict(
232
+ optional = True,
233
+ default = None,
234
+ ),
235
+ verboseload = dict(
236
+ optional = True,
237
+ default = True,
238
+ type = bool,
239
+ ),
240
+ )
241
+ )
242
+
243
+ _addonslist = None
244
+
245
+ def __init__(self, *args, **kw):
246
+ """Abstract Addon initialisation."""
247
+ logger.debug('Abstract Addon init %s', self.__class__)
248
+ super().__init__(*args, **kw)
249
+ self._addons_load()
250
+
251
+ def _addons_load(self):
252
+ if self._addonslist is None:
253
+ raise RuntimeError("the _addonslist classe variable must be overriden.")
254
+ self._load_addons_from_list(self._addonslist)
255
+
256
+ def _load_addons_from_list(self, addons):
257
+ if self.verboseload:
258
+ logger.info("Loading the %s Addons group.", self.kind)
259
+ for addon in addons:
260
+ _shadd = footprints.proxy.addon(kind=addon, sh=self.sh, env=self.env,
261
+ cycle=self.cycle, verboseload=self.verboseload)
262
+ if self.verboseload:
263
+ logger.info("%s Addon is: %s", addon, repr(_shadd))
264
+
265
+
266
+ def require_external_addon(*addons):
267
+ """
268
+ A method decorator usable in addons, that will check if addons listed in
269
+ **addons** are properly loaded in the parent System object.
270
+
271
+ If not, a :class:`RuntimeError` exception will be raised.
272
+ """
273
+ @nicedeco
274
+ def r_addon_decorator(method):
275
+
276
+ def decorated(self, *kargs, **kwargs):
277
+ # Create a cache in self... ugly but efficient !
278
+ if not hasattr(self, '_require_external_addon_check_cache'):
279
+ setattr(self, '_require_external_addon_check_cache', set())
280
+ ko_addons = set()
281
+ loaded_addons = None
282
+ for addon in addons:
283
+ if addon in self._require_external_addon_check_cache:
284
+ continue
285
+ if loaded_addons is None:
286
+ loaded_addons = self.sh.loaded_addons()
287
+ if addon in loaded_addons:
288
+ self._require_external_addon_check_cache.add(addon)
289
+ else:
290
+ ko_addons.add(addon)
291
+ if ko_addons:
292
+ raise RuntimeError('The following addons are needed to use the {:s} method: {:s}'
293
+ .format(method.__name__, ', '.join(ko_addons)))
294
+ return method(self, *kargs, **kwargs)
295
+
296
+ return decorated
297
+ return r_addon_decorator
vortex/tools/arm.py ADDED
@@ -0,0 +1,76 @@
1
+ """
2
+ Module needed to work with ARM tools such as Forge.
3
+ """
4
+
5
+ from bronx.fancies import loggers
6
+
7
+ from vortex.util.config import load_template
8
+
9
+ #: No automatic export
10
+ __all__ = ['ArmForgeTool']
11
+
12
+ logger = loggers.getLogger(__name__)
13
+
14
+
15
+ class ArmForgeTool:
16
+ """Work with the ARM tools such as DDT & MAP."""
17
+
18
+ def __init__(self, ticket):
19
+ """
20
+ :param ticket: The current Vortex' session ticket.
21
+ """
22
+ self._t = ticket
23
+ self._sh = self._t.sh
24
+ self._config = self._sh.default_target.items('armtools')
25
+ self._ddtpath = self._sh.env.get('VORTEX_ARM_DDT_PATH', None)
26
+ self._mappath = self._sh.env.get('VORTEX_ARM_MAP_PATH', None)
27
+ self._forgedir = self._sh.env.get('VORTEX_ARM_FORGE_DIR',
28
+ self.config.get('forgedir', None))
29
+ self._forgeversion = self._sh.env.get('VORTEX_ARM_FORGE_VERSION',
30
+ self.config.get('forgeversion', 999999))
31
+ self._forgeversion = int(self._forgeversion)
32
+ if self._forgedir and self._ddtpath is None:
33
+ self._ddtpath = self._sh.path.join(self._forgedir, 'bin', 'ddt')
34
+ if self._forgedir and self._mappath is None:
35
+ self._mappath = self._sh.path.join(self._forgedir, 'bin', 'map')
36
+
37
+ @property
38
+ def config(self):
39
+ """The configuration dictionary."""
40
+ return self._config
41
+
42
+ @property
43
+ def ddtpath(self):
44
+ """The path to the DDT debuger executable."""
45
+ if self._ddtpath is None:
46
+ raise RuntimeError('DDT requested but the DDT path is not configured.')
47
+ return self._ddtpath
48
+
49
+ @property
50
+ def mappath(self):
51
+ """The path to the MAP profiler executable."""
52
+ if self._mappath is None:
53
+ raise RuntimeError('MAP requested but the MAP path is not configured.')
54
+ return self._mappath
55
+
56
+ def _dump_forge_session(self, sources=(), workdir=None):
57
+ """Create the ARM Forge's session file to list source directories."""
58
+ targetfile = 'armforge-vortex-session-file.ddt'
59
+ if workdir:
60
+ targetfile = self._sh.path.join(workdir, targetfile)
61
+ tpl = load_template(self._t, '@armforge-session-conf.tpl',
62
+ encoding='utf-8', version=self._forgeversion)
63
+ sconf = tpl.substitute(sourcedirs='\n'.join([' <directory>{:s}</directory>'.format(d)
64
+ for d in sources]))
65
+ with open(targetfile, 'w') as fhs:
66
+ fhs.write(sconf)
67
+ return targetfile
68
+
69
+ def ddt_prefix_cmd(self, sources=(), workdir=None):
70
+ """Generate the prefix command required to start DDT."""
71
+ if sources:
72
+ return [self.ddtpath,
73
+ '--session={:s}'.format(self._dump_forge_session(sources, workdir=workdir)),
74
+ '--connect']
75
+ else:
76
+ return [self.ddtpath, '--connect']
@@ -0,0 +1,322 @@
1
+ """
2
+ Stream/File compression tools.
3
+
4
+ The user interface for such tools is the :class:`CompressionPipeline`.
5
+ """
6
+
7
+ from contextlib import contextmanager
8
+ import io
9
+ import functools
10
+ import operator
11
+
12
+ import footprints
13
+ from bronx.fancies import loggers
14
+ from vortex.util.iosponge import IoSponge
15
+
16
+
17
+ #: No automatic export
18
+ __all__ = []
19
+
20
+ logger = loggers.getLogger(__name__)
21
+
22
+
23
+ class CompressionPipeline:
24
+ """Main interface to data compression algorithms."""
25
+
26
+ def __init__(self, system, compression=''):
27
+ """
28
+ :param System system: The system object that will be used to carry out
29
+ the task.
30
+ :param str compression: The description of the compression tools that
31
+ will be used in this compression pipeline.
32
+ (e.g. 'gzip' will just compress/uncompress using the gzip software,
33
+ 'gzip|bzip2' will use the gzip software piped to the bzip2 software
34
+ (which is useless), 'gzip&complevel=5' will use the gzip software
35
+ with a compression factor of 5.)
36
+
37
+ :note: See the subclasses of :class:`CompressionUnit` for a description
38
+ of available compression tools (and their options).
39
+ """
40
+ self._units = list()
41
+ self._sh = system
42
+ self.description_string = compression
43
+ for c in [c for c in compression.split('|') if c]:
44
+ c_raw = c.split('&')
45
+ ckind = c_raw.pop(0)
46
+ cargs = dict([arg.split('=', 1) for arg in c_raw])
47
+ self.add_compression_unit(ckind, **cargs)
48
+
49
+ def add_compression_unit(self, unit, **kwargs):
50
+ """Add a new compression tool to the compression pipeline.
51
+
52
+ :param str unit: The kind of the compression tool (see :class:`CompressionUnit`
53
+ subclases
54
+ :param kwargs: Options that will be used during the compression tool
55
+ initialisation
56
+ """
57
+ c_unit = footprints.proxy.compression_unit(kind=unit, **kwargs)
58
+ if c_unit is None:
59
+ raise ValueError("The {:s} compression unit could not be found.".
60
+ format(unit))
61
+ self._units.append(c_unit)
62
+
63
+ @property
64
+ def units(self):
65
+ """The list of compression tools forming the compression pipeline."""
66
+ return self._units
67
+
68
+ @property
69
+ def _rawftp_shell(self):
70
+ """The name of the corresponding rawftp specialshell (if relevant)."""
71
+ if len(self.units) == 1:
72
+ return self.units[0].rawftp_shell(self._sh)
73
+ else:
74
+ return None
75
+
76
+ @property
77
+ def suffix(self):
78
+ """The suffix usualy associated with this compression pipeline."""
79
+ s = '.'.join([s.suffix for s in self.units])
80
+ return '.' + s if s else ''
81
+
82
+ @property
83
+ def compression_factor(self):
84
+ """The minimal compression factor expected with such a compression pipeline."""
85
+ return functools.reduce(operator.mul, [s.cfactor for s in self.units], 1.)
86
+
87
+ @staticmethod
88
+ def _inputstream_size(stream):
89
+ """Find out the size of a seekable input stream."""
90
+ estimated_size = 0
91
+ try:
92
+ stream.seek(0, io.SEEK_END)
93
+ estimated_size = stream.tell()
94
+ stream.seek(0)
95
+ except AttributeError:
96
+ logger.warning('Could not rewind <source:%s>', str(stream))
97
+ except OSError:
98
+ logger.debug('Seek trouble <source:%s>', str(stream))
99
+ return estimated_size
100
+
101
+ @contextmanager
102
+ def _openstream(self, local, mode='rb'):
103
+ """If *local* is not an opened file, open it..."""
104
+ if isinstance(local, str):
105
+ localfh = open(local, mode)
106
+ yield localfh
107
+ localfh.close()
108
+ elif isinstance(local, io.IOBase):
109
+ yield local
110
+ else:
111
+ raise ValueError("Unknown type for {!s}".format(local))
112
+
113
+ def _genericstream_close(self, processes):
114
+ """Close a list of Popen objects (and look for the returncode)."""
115
+ for i, p in enumerate(processes):
116
+ if not self._sh.pclose(p):
117
+ logger.error("Abnormal return code for one of the processes (#%d)", i)
118
+
119
+ @contextmanager
120
+ def compress2stream(self, local, iosponge=False):
121
+ """Compress *local* into a pipe or an :class:`IoSponge` object.
122
+
123
+ *local* can be an opened file-like object or a filename.
124
+
125
+ This method creates a context manager. Example::
126
+
127
+ source='myfile'
128
+ cp = CompressionPipeline(systemobj, 'gzip')
129
+ ftp = systemobj.ftp('hendrix.meteo.fr')
130
+ with cp.compress2stream(source) as csource:
131
+ ftp.put(csource, 'remote_compressedfile')
132
+
133
+ When leaving the context, the gzip process that compresses the data will
134
+ be properly closed
135
+ """
136
+ with self._openstream(local) as stream:
137
+ estimated_size = self._inputstream_size(stream) * self.compression_factor
138
+ processes = list()
139
+ lstream = stream
140
+ for unit in self.units:
141
+ p = unit.compress(self._sh, lstream)
142
+ lstream = p.stdout
143
+ processes.append(p)
144
+ if iosponge:
145
+ yield IoSponge(lstream, guessed_size=estimated_size)
146
+ else:
147
+ yield lstream
148
+ self._genericstream_close(processes)
149
+
150
+ def _xcopyfileobj(self, in_fh, out_fh):
151
+ try:
152
+ self._sh.copyfileobj(in_fh, out_fh)
153
+ except OSError:
154
+ return False
155
+ else:
156
+ return True
157
+
158
+ def compress2file(self, local, destination):
159
+ """Compress *local* into a file (named *destination*)
160
+
161
+ *local* can be an opened file-like object or a filename.
162
+ *destination* is a filename.
163
+ """
164
+ with open(destination, 'wb') as fhout:
165
+ with self.compress2stream(local) as fhcompressed:
166
+ return self._xcopyfileobj(fhcompressed, fhout)
167
+
168
+ def compress2rawftp(self, local):
169
+ """
170
+ Return the name of the rawftp's specialshell that can be used to
171
+ compress the *local* data. It might return None.
172
+ """
173
+ return self._rawftp_shell
174
+
175
+ @contextmanager
176
+ def stream2uncompress(self, destination):
177
+ """Uncompress piped data to *destination*.
178
+
179
+ *destination* can be an opened file-like object or a filename.
180
+
181
+ This method creates a context manager. Example::
182
+
183
+ destination='mydownloadedfile'
184
+ cp = CompressionPipeline(systemobj, 'gzip')
185
+ ftp = systemobj.ftp('hendrix.meteo.fr')
186
+ with cp.stream2uncompress(destination) as cdestination:
187
+ ftp.get('remote_compressedfile', cdestination)
188
+
189
+ When leaving the context, the gunzip process that uncompresses the data
190
+ will be properly closed.
191
+ """
192
+ with self._openstream(destination, 'wb') as dstream:
193
+ processes = list()
194
+ instream = True
195
+ nunits = len(self.units)
196
+ for i, unit in enumerate(reversed(self.units)):
197
+ outstream = dstream if i == nunits - 1 else True
198
+ p = unit.uncompress(self._sh, instream, outstream)
199
+ instream = p.stdout
200
+ processes.append(p)
201
+ yield processes[0].stdin
202
+ self._genericstream_close(processes)
203
+
204
+ def file2uncompress(self, local, destination):
205
+ """Uncompress *local* into *destination*.
206
+
207
+ *local* is a filename.
208
+ *destination* can be an opened file-like object or a filename.
209
+ """
210
+ with self.stream2uncompress(destination) as fhuncompressed:
211
+ with open(local, 'rb') as fhcompressed:
212
+ return self._xcopyfileobj(fhcompressed, fhuncompressed)
213
+
214
+
215
+ class CompressionUnit(footprints.FootprintBase):
216
+ """Defines compress/uncompress methods for a given compression tool."""
217
+
218
+ _abstract = True
219
+ _collector = ('compression_unit',)
220
+ _footprint = dict(
221
+ info = 'Abstract Compression Unit',
222
+ attr = dict(
223
+ kind = dict(
224
+ info = "The name of the compression tool.",
225
+ ),
226
+ suffix = dict(
227
+ info = "The usual file extension for this compression tool.",
228
+ optional = True,
229
+ ),
230
+ cfactor = dict(
231
+ info = "The usual compression factor for this compression tool.",
232
+ type = float,
233
+ default = 1.,
234
+ optional =True,
235
+ ),
236
+ ),
237
+ )
238
+
239
+ def rawftp_shell(self, sh):
240
+ """The rawftp's speciall shell that may carry out a comparable compression."""
241
+ return None
242
+
243
+ def _run_in_pipe(self, sh, cmd, stream, outstream=True):
244
+ """Run *cmd* with the piped input *stream*."""
245
+ p = sh.popen(cmd, stdin=stream, stdout=outstream, bufsize=8192)
246
+ return p
247
+
248
+ def compress(self, sh, stream):
249
+ """Compress the input *stream*. Returns a Popen object."""
250
+ raise NotImplementedError()
251
+
252
+ def uncompress(self, sh, stream, outstream=True):
253
+ """Uncompress the input *stream*. Returns a Popen object."""
254
+ raise NotImplementedError()
255
+
256
+
257
+ class GzipCompressionUnit(CompressionUnit):
258
+
259
+ _footprint = dict(
260
+ info = 'Compress/Uncompress a stream using gzip',
261
+ attr = dict(
262
+ kind = dict(
263
+ values = ['gzip', 'gz']
264
+ ),
265
+ suffix = dict(
266
+ default = 'gz',
267
+ ),
268
+ complevel = dict(
269
+ info = "The gzip algorithm compression level (see 'man gzip')",
270
+ type = int,
271
+ values = range(1, 10),
272
+ default = 6,
273
+ optional = True
274
+ ),
275
+ cfactor = dict(
276
+ default = 0.9
277
+ ),
278
+ ),
279
+ )
280
+
281
+ def compress(self, sh, stream):
282
+ """Compress the input *stream*. Returns a Popen object."""
283
+ return self._run_in_pipe(sh, ['gzip', '--stdout', '-{!s}'.format(self.complevel)],
284
+ stream)
285
+
286
+ def uncompress(self, sh, stream, outstream=True):
287
+ """Uncompress the input *stream*. Returns a Popen object."""
288
+ return self._run_in_pipe(sh, ['gunzip', '--stdout'], stream, outstream)
289
+
290
+
291
+ class Bzip2CompressionUnit(CompressionUnit):
292
+
293
+ _footprint = dict(
294
+ info = 'Compress/Uncompress a stream using bzip2',
295
+ attr = dict(
296
+ kind = dict(
297
+ values = ['bzip2', 'bz2']
298
+ ),
299
+ suffix = dict(
300
+ default = 'bz2',
301
+ ),
302
+ complevel = dict(
303
+ info = "The bzip2 algorithm compression level (see 'man bzip2')",
304
+ type = int,
305
+ values = range(1, 10),
306
+ default = 9,
307
+ optional = True
308
+ ),
309
+ cfactor = dict(
310
+ default = 0.85,
311
+ ),
312
+ ),
313
+ )
314
+
315
+ def compress(self, sh, stream):
316
+ """Compress the input *stream*. Returns a Popen object."""
317
+ return self._run_in_pipe(sh, ['bzip2', '--stdout', '-{!s}'.format(self.complevel)],
318
+ stream)
319
+
320
+ def uncompress(self, sh, stream, outstream=True):
321
+ """Uncompress the input *stream*. Returns a Popen object."""
322
+ return self._run_in_pipe(sh, ['bunzip2', '--stdout'], stream, outstream)