ncarnate 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.
ncarnate/__init__.py ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ '''
5
+
6
+ ncarnate: reincarnate legacy scientific data files as recompressed netCDF4.
7
+
8
+ Copyright (c) 2020-2026 Erick Edward Shepherd. MIT License — see the
9
+ top-level LICENSE file.
10
+
11
+ '''
12
+
13
+ # Local application imports.
14
+ from ncarnate.constants import __author__
15
+ from ncarnate.constants import __version__
16
+ from ncarnate.core import recompress
17
+ from ncarnate.errors import NcarnateError
18
+ from ncarnate.errors import UnsupportedFormatError
19
+ from ncarnate.errors import UnsupportedTypeError
20
+ from ncarnate.errors import VerificationError
21
+ from ncarnate.formats import FileFormat
22
+ from ncarnate.formats import detect_format
23
+
24
+ __all__ = [
25
+ "recompress",
26
+ "detect_format",
27
+ "FileFormat",
28
+ "NcarnateError",
29
+ "UnsupportedFormatError",
30
+ "UnsupportedTypeError",
31
+ "VerificationError",
32
+ "__author__",
33
+ "__version__",
34
+ ]
ncarnate/__main__.py ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ '''
5
+
6
+ Allows the package to be run as a module: ``python -m ncarnate``.
7
+
8
+ Copyright (c) 2020-2026 Erick Edward Shepherd. MIT License — see the
9
+ top-level LICENSE file.
10
+
11
+ '''
12
+
13
+ # Standard library imports.
14
+ import sys
15
+
16
+ # Local application imports.
17
+ from ncarnate.cli import main
18
+
19
+ if __name__ == "__main__":
20
+
21
+ sys.exit(main())
ncarnate/cli.py ADDED
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ '''
5
+
6
+ The command line interface for the ncarnate package, which allows users to
7
+ alter the compression of a supported netCDF or HDF file.
8
+
9
+ Copyright (c) 2020-2026 Erick Edward Shepherd. MIT License — see the
10
+ top-level LICENSE file.
11
+
12
+ '''
13
+
14
+ # Standard library imports.
15
+ import argparse
16
+ import logging
17
+ import os
18
+
19
+ # Third party imports.
20
+ from tqdm import tqdm
21
+
22
+ # Local application imports.
23
+ from ncarnate.constants import PACKAGE_NAME
24
+ from ncarnate.constants import SUPPORTED_EXTENSIONS
25
+ from ncarnate.constants import __version__
26
+ from ncarnate.core import recompress
27
+ from ncarnate.errors import NcarnateError
28
+
29
+
30
+ def _has_supported_extension(path : str) -> bool:
31
+
32
+ extension = os.path.splitext(path)[1].lower().lstrip(".")
33
+
34
+ return extension in SUPPORTED_EXTENSIONS
35
+
36
+
37
+ def _files_to_paths(root : str, files : list[str]) -> list[str]:
38
+
39
+ paths = [os.path.join(root, file) for file in files]
40
+
41
+ return paths
42
+
43
+
44
+ def _get_files(paths : list[str], recursive : bool) -> list[str]:
45
+
46
+ '''
47
+
48
+ Expands the given paths into the list of files to process. Directories
49
+ are scanned (recursively with ``recursive``) for supported extensions;
50
+ explicitly named files must exist and carry a supported extension.
51
+
52
+ '''
53
+
54
+ paths = [os.path.abspath(path) for path in paths]
55
+ files = []
56
+
57
+ for path in paths:
58
+
59
+ if os.path.isdir(path):
60
+
61
+ if recursive:
62
+
63
+ for root, subdirectories, subfiles in os.walk(path):
64
+
65
+ subfiles = _files_to_paths(root, subfiles)
66
+ valid_files = filter(_has_supported_extension, subfiles)
67
+
68
+ files += sorted(valid_files)
69
+
70
+ else:
71
+
72
+ subfiles = _files_to_paths(path, os.listdir(path))
73
+ subfiles = filter(os.path.isfile, subfiles)
74
+ valid_files = filter(_has_supported_extension, subfiles)
75
+
76
+ files += sorted(valid_files)
77
+
78
+ elif os.path.isfile(path):
79
+
80
+ if not _has_supported_extension(path):
81
+
82
+ raise NcarnateError(
83
+ f"Unsupported file extension: {path} (supported: "
84
+ f"{', '.join(sorted(SUPPORTED_EXTENSIONS))})"
85
+ )
86
+
87
+ files += [path]
88
+
89
+ else:
90
+
91
+ raise NcarnateError(f"No such file or directory: {path}")
92
+
93
+ # De-duplicate while preserving order: overlapping arguments (the same
94
+ # file twice, a directory plus a file inside it, or overlapping trees
95
+ # under -r) would otherwise recompress the same path more than once.
96
+ # Paths are already absolute, so equal files compare equal.
97
+ return list(dict.fromkeys(files))
98
+
99
+
100
+ def _build_argument_parser() -> argparse.ArgumentParser:
101
+
102
+ parser = argparse.ArgumentParser(
103
+ prog = PACKAGE_NAME,
104
+ description = "Losslessly rewrites netCDF4/HDF5 files with "
105
+ "different compression settings."
106
+ )
107
+
108
+ parser.add_argument(
109
+ "path",
110
+ type = str,
111
+ nargs = "+",
112
+ help = "The path(s) to the file(s) to alter."
113
+ )
114
+
115
+ parser.add_argument(
116
+ "--complevel",
117
+ type = int,
118
+ default = 7,
119
+ choices = list(range(10)),
120
+ help = "The desired gzip deflate compression level."
121
+ )
122
+
123
+ group = parser.add_mutually_exclusive_group(required = False)
124
+
125
+ group.add_argument(
126
+ "--zlib",
127
+ dest = "zlib",
128
+ action = "store_true",
129
+ help = "Enables zlib gzip compression."
130
+ )
131
+
132
+ group.add_argument(
133
+ "--no-zlib",
134
+ dest = "zlib",
135
+ action = "store_false",
136
+ help = "Disables zlib gzip compression."
137
+ )
138
+
139
+ parser.set_defaults(zlib = True)
140
+
141
+ group = parser.add_mutually_exclusive_group(required = False)
142
+
143
+ group.add_argument(
144
+ "--shuffle",
145
+ dest = "shuffle",
146
+ action = "store_true",
147
+ help = "Enables the HDF5 shuffle filter."
148
+ )
149
+
150
+ group.add_argument(
151
+ "--no-shuffle",
152
+ dest = "shuffle",
153
+ action = "store_false",
154
+ help = "Disables the HDF5 shuffle filter."
155
+ )
156
+
157
+ parser.set_defaults(shuffle = True)
158
+
159
+ group = parser.add_mutually_exclusive_group(required = False)
160
+
161
+ group.add_argument(
162
+ "--overwrite",
163
+ dest = "overwrite",
164
+ action = "store_true",
165
+ help = "Replaces each source file with its recompressed copy "
166
+ "(only after the copy verifies lossless)."
167
+ )
168
+
169
+ group.add_argument(
170
+ "--no-overwrite",
171
+ dest = "overwrite",
172
+ action = "store_false",
173
+ help = "Keeps the source file; writes the recompressed copy "
174
+ "alongside it with a '_recompressed' suffix."
175
+ )
176
+
177
+ parser.set_defaults(overwrite = True)
178
+
179
+ parser.add_argument(
180
+ "--no-geolocation",
181
+ dest = "geolocation",
182
+ action = "store_false",
183
+ default = True,
184
+ help = "Converts HDF-EOS2 files SDS-only, skipping CF "
185
+ "geolocation reconstruction (the escape hatch for "
186
+ "unsupported projections/layouts)."
187
+ )
188
+
189
+ parser.add_argument(
190
+ "-r",
191
+ "--recursive",
192
+ dest = "recursive",
193
+ action = "store_true",
194
+ default = False,
195
+ help = "Acts recursively on the given director(y/ies)."
196
+ )
197
+
198
+ parser.add_argument(
199
+ "-V",
200
+ "--version",
201
+ action = "version",
202
+ version = f"{PACKAGE_NAME} {__version__}",
203
+ help = "Prints the current package version."
204
+ )
205
+
206
+ return parser
207
+
208
+
209
+ def _configure_logging() -> logging.Logger:
210
+
211
+ logger = logging.getLogger(PACKAGE_NAME)
212
+
213
+ # Idempotent: repeated main() calls in one process (e.g. under tests)
214
+ # must not stack duplicate handlers.
215
+ if not logger.handlers:
216
+
217
+ handler = logging.StreamHandler()
218
+ formatter = logging.Formatter("%(levelname)s: %(message)s")
219
+
220
+ handler.setFormatter(formatter)
221
+ logger.addHandler(handler)
222
+
223
+ logger.setLevel(logging.WARNING)
224
+
225
+ return logger
226
+
227
+
228
+ def main() -> int:
229
+
230
+ parser = _build_argument_parser()
231
+ args = parser.parse_args()
232
+ logger = _configure_logging()
233
+
234
+ try:
235
+
236
+ files = _get_files(args.path, args.recursive)
237
+
238
+ except NcarnateError as error:
239
+
240
+ logger.error(str(error))
241
+
242
+ return 2
243
+
244
+ if not files:
245
+
246
+ logger.error("No supported input files found.")
247
+
248
+ return 2
249
+
250
+ failures = 0
251
+
252
+ for file in tqdm(files, desc = "Files recompressed"):
253
+
254
+ try:
255
+
256
+ recompress(
257
+ file,
258
+ zlib = args.zlib,
259
+ shuffle = args.shuffle,
260
+ complevel = args.complevel,
261
+ overwrite = args.overwrite,
262
+ geolocation = args.geolocation
263
+ )
264
+
265
+ except (NcarnateError, OSError) as error:
266
+
267
+ logger.error("%s", error)
268
+
269
+ failures += 1
270
+
271
+ except Exception:
272
+
273
+ logger.exception(
274
+ "Unexpected error while recompressing the given file: %s",
275
+ file
276
+ )
277
+
278
+ failures += 1
279
+
280
+ if failures:
281
+
282
+ logger.error(
283
+ "%d of %d file(s) failed to recompress.", failures, len(files)
284
+ )
285
+
286
+ return 1
287
+
288
+ return 0
ncarnate/constants.py ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ '''
5
+
6
+ Defines constant values shared across the ncarnate package.
7
+
8
+ Copyright (c) 2020-2026 Erick Edward Shepherd. MIT License — see the
9
+ top-level LICENSE file.
10
+
11
+ '''
12
+
13
+ # Constant definitions.
14
+ PACKAGE_NAME = "ncarnate"
15
+ SUPPORTED_EXTENSIONS = ["nc", "nc4", "hdf", "hdf5", "h5", "he5"]
16
+
17
+ # Module dunder definitions.
18
+ __author__ = "Erick Edward Shepherd"
19
+ __version__ = "2.0.0"