psdi-data-conversion 0.0.23__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.
- psdi_data_conversion/__init__.py +11 -0
- psdi_data_conversion/app.py +242 -0
- psdi_data_conversion/bin/linux/atomsk +0 -0
- psdi_data_conversion/bin/linux/c2x +0 -0
- psdi_data_conversion/bin/mac/atomsk +0 -0
- psdi_data_conversion/bin/mac/c2x +0 -0
- psdi_data_conversion/constants.py +185 -0
- psdi_data_conversion/converter.py +459 -0
- psdi_data_conversion/converters/__init__.py +6 -0
- psdi_data_conversion/converters/atomsk.py +32 -0
- psdi_data_conversion/converters/base.py +702 -0
- psdi_data_conversion/converters/c2x.py +32 -0
- psdi_data_conversion/converters/openbabel.py +239 -0
- psdi_data_conversion/database.py +1064 -0
- psdi_data_conversion/dist.py +87 -0
- psdi_data_conversion/file_io.py +216 -0
- psdi_data_conversion/log_utility.py +241 -0
- psdi_data_conversion/main.py +776 -0
- psdi_data_conversion/scripts/atomsk.sh +32 -0
- psdi_data_conversion/scripts/c2x.sh +26 -0
- psdi_data_conversion/security.py +38 -0
- psdi_data_conversion/static/content/accessibility.htm +254 -0
- psdi_data_conversion/static/content/convert.htm +121 -0
- psdi_data_conversion/static/content/convertato.htm +65 -0
- psdi_data_conversion/static/content/convertc2x.htm +65 -0
- psdi_data_conversion/static/content/documentation.htm +94 -0
- psdi_data_conversion/static/content/feedback.htm +53 -0
- psdi_data_conversion/static/content/header-links.html +8 -0
- psdi_data_conversion/static/content/index-versions/header-links.html +8 -0
- psdi_data_conversion/static/content/index-versions/psdi-common-footer.html +99 -0
- psdi_data_conversion/static/content/index-versions/psdi-common-header.html +28 -0
- psdi_data_conversion/static/content/psdi-common-footer.html +99 -0
- psdi_data_conversion/static/content/psdi-common-header.html +28 -0
- psdi_data_conversion/static/content/report.htm +103 -0
- psdi_data_conversion/static/data/data.json +143940 -0
- psdi_data_conversion/static/img/colormode-toggle-dm.svg +3 -0
- psdi_data_conversion/static/img/colormode-toggle-lm.svg +3 -0
- psdi_data_conversion/static/img/psdi-icon-dark.svg +136 -0
- psdi_data_conversion/static/img/psdi-icon-light.svg +208 -0
- psdi_data_conversion/static/img/psdi-logo-darktext.png +0 -0
- psdi_data_conversion/static/img/psdi-logo-lighttext.png +0 -0
- psdi_data_conversion/static/img/social-logo-bluesky-black.svg +4 -0
- psdi_data_conversion/static/img/social-logo-bluesky-white.svg +4 -0
- psdi_data_conversion/static/img/social-logo-instagram-black.svg +1 -0
- psdi_data_conversion/static/img/social-logo-instagram-white.svg +1 -0
- psdi_data_conversion/static/img/social-logo-linkedin-black.png +0 -0
- psdi_data_conversion/static/img/social-logo-linkedin-white.png +0 -0
- psdi_data_conversion/static/img/social-logo-mastodon-black.svg +4 -0
- psdi_data_conversion/static/img/social-logo-mastodon-white.svg +4 -0
- psdi_data_conversion/static/img/social-logo-x-black.svg +3 -0
- psdi_data_conversion/static/img/social-logo-x-white.svg +3 -0
- psdi_data_conversion/static/img/social-logo-youtube-black.png +0 -0
- psdi_data_conversion/static/img/social-logo-youtube-white.png +0 -0
- psdi_data_conversion/static/img/ukri-epsr-logo-darktext.png +0 -0
- psdi_data_conversion/static/img/ukri-epsr-logo-lighttext.png +0 -0
- psdi_data_conversion/static/img/ukri-logo-darktext.png +0 -0
- psdi_data_conversion/static/img/ukri-logo-lighttext.png +0 -0
- psdi_data_conversion/static/javascript/accessibility.js +196 -0
- psdi_data_conversion/static/javascript/common.js +42 -0
- psdi_data_conversion/static/javascript/convert.js +296 -0
- psdi_data_conversion/static/javascript/convert_common.js +252 -0
- psdi_data_conversion/static/javascript/convertato.js +107 -0
- psdi_data_conversion/static/javascript/convertc2x.js +107 -0
- psdi_data_conversion/static/javascript/data.js +176 -0
- psdi_data_conversion/static/javascript/format.js +611 -0
- psdi_data_conversion/static/javascript/load_accessibility.js +89 -0
- psdi_data_conversion/static/javascript/psdi-common.js +177 -0
- psdi_data_conversion/static/javascript/report.js +381 -0
- psdi_data_conversion/static/styles/format.css +147 -0
- psdi_data_conversion/static/styles/psdi-common.css +705 -0
- psdi_data_conversion/templates/index.htm +114 -0
- psdi_data_conversion/testing/__init__.py +5 -0
- psdi_data_conversion/testing/constants.py +12 -0
- psdi_data_conversion/testing/conversion_callbacks.py +394 -0
- psdi_data_conversion/testing/conversion_test_specs.py +208 -0
- psdi_data_conversion/testing/utils.py +522 -0
- psdi_data_conversion-0.0.23.dist-info/METADATA +663 -0
- psdi_data_conversion-0.0.23.dist-info/RECORD +81 -0
- psdi_data_conversion-0.0.23.dist-info/WHEEL +4 -0
- psdi_data_conversion-0.0.23.dist-info/entry_points.txt +2 -0
- psdi_data_conversion-0.0.23.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,776 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
"""@file psdi_data_conversion/main.py
|
4
|
+
|
5
|
+
Created 2025-01-14 by Bryan Gillis.
|
6
|
+
|
7
|
+
Entry-point file for the command-line interface for data conversion.
|
8
|
+
"""
|
9
|
+
|
10
|
+
import logging
|
11
|
+
from argparse import ArgumentParser
|
12
|
+
import os
|
13
|
+
import sys
|
14
|
+
import textwrap
|
15
|
+
|
16
|
+
from psdi_data_conversion import constants as const
|
17
|
+
from psdi_data_conversion.constants import CL_SCRIPT_NAME, CONVERTER_DEFAULT, TERM_WIDTH
|
18
|
+
from psdi_data_conversion.converter import (D_CONVERTER_ARGS, D_SUPPORTED_CONVERTERS, L_REGISTERED_CONVERTERS,
|
19
|
+
L_SUPPORTED_CONVERTERS, run_converter)
|
20
|
+
from psdi_data_conversion.converters.base import (FileConverterAbortException, FileConverterInputException,
|
21
|
+
FileConverterHelpException)
|
22
|
+
from psdi_data_conversion.database import (get_conversion_quality, get_converter_info, get_format_info,
|
23
|
+
get_in_format_args, get_out_format_args, get_possible_converters,
|
24
|
+
get_possible_formats)
|
25
|
+
from psdi_data_conversion.file_io import split_archive_ext
|
26
|
+
from psdi_data_conversion.log_utility import get_log_level_from_str
|
27
|
+
|
28
|
+
|
29
|
+
def print_wrap(s: str, newline=False, err=False, **kwargs):
|
30
|
+
"""Print a string wrapped to the terminal width
|
31
|
+
"""
|
32
|
+
if err:
|
33
|
+
file = sys.stderr
|
34
|
+
else:
|
35
|
+
file = sys.stdout
|
36
|
+
for line in s.split("\n"):
|
37
|
+
print(textwrap.fill(line, width=TERM_WIDTH, **kwargs), file=file)
|
38
|
+
if newline:
|
39
|
+
print("")
|
40
|
+
|
41
|
+
|
42
|
+
class ConvertArgs:
|
43
|
+
"""Class storing arguments for data conversion, processed and determined from the input arguments.
|
44
|
+
"""
|
45
|
+
|
46
|
+
def __init__(self, args):
|
47
|
+
|
48
|
+
# Start by copying over arguments. Some share names with reserved words, so we have to use `getattr` for them
|
49
|
+
|
50
|
+
# Positional arguments
|
51
|
+
self.l_args: list[str] = args.l_args
|
52
|
+
|
53
|
+
# Keyword arguments for standard conversion
|
54
|
+
self.from_format: str | None = getattr(args, "from")
|
55
|
+
self._input_dir: str | None = getattr(args, "in")
|
56
|
+
self.to_format: str | None = args.to
|
57
|
+
self._output_dir: str | None = args.out
|
58
|
+
converter_name = getattr(args, "with")
|
59
|
+
if isinstance(converter_name, str):
|
60
|
+
self.name = converter_name
|
61
|
+
elif converter_name:
|
62
|
+
self.name: str = " ".join(converter_name)
|
63
|
+
else:
|
64
|
+
self.name = None
|
65
|
+
self.delete_input = args.delete_input
|
66
|
+
self.from_flags: str = args.from_flags.replace(r"\-", "-")
|
67
|
+
self.to_flags: str = args.to_flags.replace(r"\-", "-")
|
68
|
+
self.from_options: str = args.from_options.replace(r"\-", "-")
|
69
|
+
self.to_options: str = args.to_options.replace(r"\-", "-")
|
70
|
+
self.no_check: bool = args.nc
|
71
|
+
self.strict: bool = args.strict
|
72
|
+
|
73
|
+
# Keyword arguments for alternative functionality
|
74
|
+
self.list: bool = args.list
|
75
|
+
|
76
|
+
# Logging/stdout arguments
|
77
|
+
self.log_mode: bool = args.log_mode
|
78
|
+
self.quiet = args.quiet
|
79
|
+
self._log_file: str | None = args.log_file
|
80
|
+
|
81
|
+
if not args.log_level:
|
82
|
+
self.log_level = None
|
83
|
+
else:
|
84
|
+
try:
|
85
|
+
self.log_level = get_log_level_from_str(args.log_level)
|
86
|
+
except ValueError as e:
|
87
|
+
# A ValueError indicates an unrecognised logging level, so we reraise this as a help exception to
|
88
|
+
# indicate we want to provide this as feedback to the user so they can correct their command
|
89
|
+
raise FileConverterHelpException(str(e))
|
90
|
+
|
91
|
+
# Special handling for listing converters
|
92
|
+
if self.list:
|
93
|
+
# Force log mode to stdout and turn off quiet
|
94
|
+
self.log_mode = const.LOG_STDOUT
|
95
|
+
self.quiet = False
|
96
|
+
|
97
|
+
# Get the converter name from the arguments if it wasn't provided by -w/--with
|
98
|
+
if not self.name:
|
99
|
+
self.name = " ".join(self.l_args)
|
100
|
+
|
101
|
+
# For this operation, any other arguments can be ignored
|
102
|
+
return
|
103
|
+
|
104
|
+
# If not listing and a converter name wasn't supplied, use the default converter
|
105
|
+
if not self.name:
|
106
|
+
self.name = CONVERTER_DEFAULT
|
107
|
+
|
108
|
+
# Quiet mode is equivalent to logging mode == LOGGING_NONE, so normalize them if either is set
|
109
|
+
if self.quiet:
|
110
|
+
self.log_mode = const.LOG_NONE
|
111
|
+
elif self.log_mode == const.LOG_NONE:
|
112
|
+
self.quiet = True
|
113
|
+
|
114
|
+
# Check validity of input
|
115
|
+
|
116
|
+
if len(self.l_args) == 0:
|
117
|
+
raise FileConverterHelpException("One or more names of files to convert must be provided")
|
118
|
+
|
119
|
+
if self._input_dir is not None and not os.path.isdir(self._input_dir):
|
120
|
+
raise FileConverterHelpException(f"The provided input directory '{self._input_dir}' does not exist as a "
|
121
|
+
"directory")
|
122
|
+
|
123
|
+
if self.to_format is None:
|
124
|
+
msg = textwrap.fill("ERROR Output format (-t or --to) must be provided. For information on supported "
|
125
|
+
"formats and converters, call:\n")
|
126
|
+
msg += f"{CL_SCRIPT_NAME} -l"
|
127
|
+
raise FileConverterHelpException(msg, msg_preformatted=True)
|
128
|
+
|
129
|
+
# If the output directory doesn't exist, silently create it
|
130
|
+
if self._output_dir is not None and not os.path.isdir(self._output_dir):
|
131
|
+
if os.path.exists(self._output_dir):
|
132
|
+
raise FileConverterHelpException(f"Output directory '{self._output_dir}' exists but is not a "
|
133
|
+
"directory")
|
134
|
+
os.makedirs(self._output_dir, exist_ok=True)
|
135
|
+
|
136
|
+
# Check the converter is recognized
|
137
|
+
if self.name not in L_SUPPORTED_CONVERTERS:
|
138
|
+
msg = textwrap.fill(f"ERROR: Converter '{self.name}' not recognised", width=TERM_WIDTH)
|
139
|
+
msg += f"\n\n{get_supported_converters()}"
|
140
|
+
raise FileConverterHelpException(msg, msg_preformatted=True)
|
141
|
+
elif self.name not in L_REGISTERED_CONVERTERS:
|
142
|
+
msg = textwrap.fill(f"ERROR: Converter '{self.name}' is not registered. It may be possible to register "
|
143
|
+
"it by installing an appropriate binary for your platform.", width=TERM_WIDTH)
|
144
|
+
msg += f"\n\n{get_supported_converters()}"
|
145
|
+
raise FileConverterHelpException(msg, msg_preformatted=True)
|
146
|
+
|
147
|
+
# Logging mode is valid
|
148
|
+
if self.log_mode not in const.L_ALLOWED_LOG_MODES:
|
149
|
+
raise FileConverterHelpException(f"Unrecognised logging mode: {self.log_mode}. Allowed "
|
150
|
+
f"modes are: {const.L_ALLOWED_LOG_MODES}")
|
151
|
+
|
152
|
+
# Arguments specific to this converter
|
153
|
+
self.d_converter_args = {}
|
154
|
+
l_converter_args = D_CONVERTER_ARGS[self.name]
|
155
|
+
if not l_converter_args:
|
156
|
+
l_converter_args = []
|
157
|
+
for arg_name, _, get_data in l_converter_args:
|
158
|
+
# Convert the argument name to how it will be represented in the parsed_args object
|
159
|
+
while arg_name.startswith("-"):
|
160
|
+
arg_name = arg_name[1:]
|
161
|
+
arg_name = arg_name.replace("-", "_")
|
162
|
+
self.d_converter_args.update(get_data(getattr(args, arg_name)))
|
163
|
+
|
164
|
+
@property
|
165
|
+
def input_dir(self):
|
166
|
+
"""If the input directory isn't provided, use the current directory.
|
167
|
+
"""
|
168
|
+
if self._input_dir is None:
|
169
|
+
self._input_dir = os.getcwd()
|
170
|
+
return self._input_dir
|
171
|
+
|
172
|
+
@property
|
173
|
+
def output_dir(self):
|
174
|
+
"""If the output directory isn't provided, use the input directory.
|
175
|
+
"""
|
176
|
+
if self._output_dir is None:
|
177
|
+
self._output_dir = self.input_dir
|
178
|
+
return self._output_dir
|
179
|
+
|
180
|
+
@property
|
181
|
+
def log_file(self):
|
182
|
+
"""Determine a name for the log file if one is not provided.
|
183
|
+
"""
|
184
|
+
if self._log_file is None:
|
185
|
+
if self.list:
|
186
|
+
self._log_file = const.DEFAULT_LISTING_LOG_FILE
|
187
|
+
else:
|
188
|
+
first_filename = os.path.join(self.input_dir, self.l_args[0])
|
189
|
+
|
190
|
+
# Find the path to this file
|
191
|
+
if not os.path.isfile(first_filename):
|
192
|
+
if self.from_format:
|
193
|
+
test_filename = first_filename + f".{self.from_format}"
|
194
|
+
if os.path.isfile(test_filename):
|
195
|
+
first_filename = test_filename
|
196
|
+
else:
|
197
|
+
raise FileConverterHelpException(f"Input file {first_filename} cannot be found. Also "
|
198
|
+
f"checked for {test_filename}.")
|
199
|
+
else:
|
200
|
+
raise FileConverterHelpException(f"Input file {first_filename} cannot be found.")
|
201
|
+
|
202
|
+
filename_base = os.path.split(split_archive_ext(first_filename)[0])[1]
|
203
|
+
if self.log_mode == const.LOG_FULL:
|
204
|
+
# For server-style logging, other files will be created and used for logs
|
205
|
+
self._log_file = None
|
206
|
+
else:
|
207
|
+
self._log_file = os.path.join(self.output_dir, filename_base + const.LOG_EXT)
|
208
|
+
return self._log_file
|
209
|
+
|
210
|
+
|
211
|
+
def get_argument_parser():
|
212
|
+
"""Get an argument parser for this script.
|
213
|
+
|
214
|
+
Returns
|
215
|
+
-------
|
216
|
+
parser : ArgumentParser
|
217
|
+
An argument parser set up with the allowed command-line arguments for this script.
|
218
|
+
"""
|
219
|
+
|
220
|
+
parser = ArgumentParser()
|
221
|
+
|
222
|
+
# Positional arguments
|
223
|
+
parser.add_argument("l_args", type=str, nargs="*",
|
224
|
+
help="Normally, file(s) to be converted or zip/tar archives thereof. If an archive or archives "
|
225
|
+
"are provided, the output will be packed into an archive of the same type. Filenames should be "
|
226
|
+
"provided as either relative to the input directory (default current directory) or absolute. "
|
227
|
+
"If the '-l' or '--list' flag is set, instead the name of a converter can be used here to get "
|
228
|
+
"information on it.")
|
229
|
+
|
230
|
+
# Keyword arguments for standard conversion
|
231
|
+
parser.add_argument("-f", "--from", type=str, default=None,
|
232
|
+
help="The input (convert from) file extension (e.g., smi). If not provided, will attempt to "
|
233
|
+
"auto-detect format.")
|
234
|
+
parser.add_argument("-i", "--in", type=str, default=None,
|
235
|
+
help="The directory containing the input file(s), default current directory.")
|
236
|
+
parser.add_argument("-t", "--to", type=str, default=None,
|
237
|
+
help="The output (convert to) file extension (e.g., cmi).")
|
238
|
+
parser.add_argument("-o", "--out", type=str, default=None,
|
239
|
+
help="The directory where output files should be created. If not provided, output files will "
|
240
|
+
"be created in -i/--in directory if that was provided, or else in the directory containing the "
|
241
|
+
"first input file.")
|
242
|
+
parser.add_argument("-w", "--with", type=str, nargs="+",
|
243
|
+
help="The converter to be used (default 'Open Babel').")
|
244
|
+
parser.add_argument("--delete-input", action="store_true",
|
245
|
+
help="If set, input files will be deleted after conversion, default they will be kept")
|
246
|
+
parser.add_argument("--from-flags", type=str, default="",
|
247
|
+
help="Any command-line flags to be provided to the converter for reading in the input file(s). "
|
248
|
+
"For information on the flags accepted by a converter and its required format for them, "
|
249
|
+
"call this script with '-l <converter name>'. If the set of flags includes any spaces, it "
|
250
|
+
"must be quoted, and if hyphens are used, the first preceding hyphen for each flag must "
|
251
|
+
"be backslash-escaped, e.g. '--from-flags \"\\-a \\-bc \\--example\"'")
|
252
|
+
parser.add_argument("--to-flags", type=str, default="",
|
253
|
+
help="Any command-line flags to be provided to the converter for writing the output file(s). "
|
254
|
+
"For information on the flags accepted by a converter and its required format for them, "
|
255
|
+
"call this script with '-l <converter name>'. If the set of flags includes any spaces, it "
|
256
|
+
"must be quoted, and if hyphens are used, the first preceding hyphen for each flag must "
|
257
|
+
"be backslash-escaped, e.g. '--to-flags \"\\-a \\-bc \\--example\"'")
|
258
|
+
parser.add_argument("--from-options", type=str, default="",
|
259
|
+
help="Any command-line options to be provided to the converter for reading in the input "
|
260
|
+
"file(s). For information on the options accepted by a converter and its required format "
|
261
|
+
"for them, call this script with '-l <converter name>'. If the set of options includes "
|
262
|
+
"any spaces, it must be quoted, and the first preceding hyphen for each option must be "
|
263
|
+
"backslash-escaped, e.g. '--from-options \"\\-x xval --opt optval\"'")
|
264
|
+
parser.add_argument("--to-options", type=str, default="",
|
265
|
+
help="Any command-line options to be provided to the converter for writing the output "
|
266
|
+
"file(s). For information on the options accepted by a converter and its required format "
|
267
|
+
"for them, call this script with '-l <converter name>'. If the set of options includes "
|
268
|
+
"any spaces, it must be quoted, and the first preceding hyphen for each option must be "
|
269
|
+
"backslash-escaped, e.g. '--to-options \"\\-x xval --opt optval\"'")
|
270
|
+
parser.add_argument("-s", "--strict", action="store_true",
|
271
|
+
help="If set, will fail if one of the input files has the wrong extension (including those "
|
272
|
+
"contained in archives, but not the archive files themselves). Otherwise, will only print a "
|
273
|
+
"warning in this case.")
|
274
|
+
parser.add_argument("--nc", "--no-check", action="store_true",
|
275
|
+
help="If set, will not perform a pre-check in the database on the validity of a conversion. "
|
276
|
+
"Setting this will result in a less human-friendly error message (or may even falsely indicate "
|
277
|
+
"success) if the conversion is not supported, but will save some execution time. Recommended "
|
278
|
+
"only for automated execution after the user has confirmed a conversion is supported")
|
279
|
+
|
280
|
+
# Keyword arguments specific to converters
|
281
|
+
for converter_name in L_REGISTERED_CONVERTERS:
|
282
|
+
l_converter_args = D_CONVERTER_ARGS[converter_name]
|
283
|
+
if l_converter_args:
|
284
|
+
for arg_name, kwargs, _ in l_converter_args:
|
285
|
+
parser.add_argument(arg_name, **kwargs)
|
286
|
+
|
287
|
+
# Keyword arguments for alternative functionality
|
288
|
+
parser.add_argument("-l", "--list", action="store_true",
|
289
|
+
help="If provided alone, lists all available converters. If the name of a converter is "
|
290
|
+
"provided, gives information on the converter and any command-line flags it accepts.")
|
291
|
+
|
292
|
+
# Logging/stdout arguments
|
293
|
+
parser.add_argument("-g", "--log-file", type=str, default=None,
|
294
|
+
help="The name of the file to log to. This can be provided relative to the current directory "
|
295
|
+
"(e.g. '-g ../logs/log-file.txt') or fully qualified (e.g. /path/to/log-file.txt). "
|
296
|
+
"If not provided, the log file will be named after the =first input file (+'.log') and placed "
|
297
|
+
"in the output directory (specified with -o/--out).\n"
|
298
|
+
"In 'full' logging mode (not recommended with this interface), this will apply only to logs "
|
299
|
+
"from the outermost level of the script if explicitly specified. If not explicitly specified, "
|
300
|
+
"those logs will be sent to stderr.")
|
301
|
+
parser.add_argument("--log-mode", type=str, default=const.LOG_SIMPLE,
|
302
|
+
help="How logs should be stored. Allowed values are: \n"
|
303
|
+
"- 'full' - Multi-file logging, not recommended for the CLI, but allowed for a compatible "
|
304
|
+
"interface with the public web app"
|
305
|
+
"- 'simple' - Logs saved to one file"
|
306
|
+
"- 'stdout' - Output logs and errors only to stdout"
|
307
|
+
"- 'none' - Output only errors to stdout")
|
308
|
+
parser.add_argument("-q", "--quiet", action="store_true",
|
309
|
+
help="If set, all terminal output aside from errors will be suppressed and no log file will be "
|
310
|
+
"generated.")
|
311
|
+
parser.add_argument("--log-level", type=str, default=None,
|
312
|
+
help="The desired level to log at. Allowed values are: 'DEBUG', 'INFO', 'WARNING', 'ERROR, "
|
313
|
+
"'CRITICAL'. Default: 'INFO' for logging to file, 'WARNING' for logging to stdout")
|
314
|
+
|
315
|
+
return parser
|
316
|
+
|
317
|
+
|
318
|
+
def parse_args():
|
319
|
+
"""Parses arguments for this executable.
|
320
|
+
|
321
|
+
Returns
|
322
|
+
-------
|
323
|
+
args : Namespace
|
324
|
+
The parsed arguments.
|
325
|
+
"""
|
326
|
+
|
327
|
+
parser = get_argument_parser()
|
328
|
+
|
329
|
+
args = ConvertArgs(parser.parse_args())
|
330
|
+
|
331
|
+
return args
|
332
|
+
|
333
|
+
|
334
|
+
def detail_converter_use(args: ConvertArgs):
|
335
|
+
"""Prints output providing information on a specific converter, including the flags and options it allows
|
336
|
+
"""
|
337
|
+
|
338
|
+
converter_info = get_converter_info(args.name)
|
339
|
+
converter_class = D_SUPPORTED_CONVERTERS[args.name]
|
340
|
+
|
341
|
+
print_wrap(f"{converter_info.name}: {converter_info.description} ({converter_info.url})", break_long_words=False,
|
342
|
+
break_on_hyphens=False, newline=True)
|
343
|
+
|
344
|
+
if converter_class.info:
|
345
|
+
print_wrap(converter_class.info, break_long_words=False, break_on_hyphens=False, newline=True)
|
346
|
+
|
347
|
+
# If both an input and output format are specified, provide the degree of success for this conversion. Otherwise
|
348
|
+
# list possible input output formats
|
349
|
+
if args.from_format is not None and args.to_format is not None:
|
350
|
+
qual = get_conversion_quality(args.name, args.from_format, args.to_format)
|
351
|
+
if qual is None:
|
352
|
+
print_wrap(f"Conversion from '{args.from_format}' to '{args.to_format}' with {args.name} is not "
|
353
|
+
"supported.", newline=True)
|
354
|
+
else:
|
355
|
+
print_wrap(f"Conversion from '{args.from_format}' to '{args.to_format}' with {args.name} is "
|
356
|
+
f"possible with {qual.qual_str} conversion quality", newline=True)
|
357
|
+
# If there are any potential issues with the conversion, print them out
|
358
|
+
if qual.details:
|
359
|
+
print_wrap("WARNING: Potential data loss or extrapolation issues with this conversion:")
|
360
|
+
for detail_line in qual.details.split("\n"):
|
361
|
+
print_wrap(f"- {detail_line}")
|
362
|
+
print("")
|
363
|
+
else:
|
364
|
+
l_input_formats, l_output_formats = get_possible_formats(args.name)
|
365
|
+
|
366
|
+
# If one format was supplied, check if it's supported
|
367
|
+
for (format_name, l_formats, to_or_from) in ((args.from_format, l_input_formats, "from"),
|
368
|
+
(args.to_format, l_output_formats, "to")):
|
369
|
+
if format_name is None:
|
370
|
+
continue
|
371
|
+
if format_name in l_formats:
|
372
|
+
optional_not: str = ""
|
373
|
+
else:
|
374
|
+
optional_not: str = " not"
|
375
|
+
print_wrap(f"Conversion {to_or_from} {format_name} is {optional_not}supported by {args.name}.\n")
|
376
|
+
|
377
|
+
# List all possible formats, and which can be used for input and which for output
|
378
|
+
s_all_formats: set[str] = set(l_input_formats)
|
379
|
+
s_all_formats.update(l_output_formats)
|
380
|
+
l_all_formats: list[str] = list(s_all_formats)
|
381
|
+
l_all_formats.sort(key=lambda s: s.lower())
|
382
|
+
|
383
|
+
print_wrap(f"File formats supported by {args.name}:", newline=True)
|
384
|
+
max_format_length = max([len(x) for x in l_all_formats])
|
385
|
+
print(" "*(max_format_length+4) + " INPUT OUTPUT")
|
386
|
+
print(" "*(max_format_length+4) + " ----- ------")
|
387
|
+
for file_format in l_all_formats:
|
388
|
+
in_yes_or_no = "yes" if file_format in l_input_formats else "no"
|
389
|
+
out_yes_or_no = "yes" if file_format in l_output_formats else "no"
|
390
|
+
print(f" {file_format:>{max_format_length}}{in_yes_or_no:>8}{out_yes_or_no:>8}")
|
391
|
+
print("")
|
392
|
+
|
393
|
+
if converter_class.allowed_flags is None:
|
394
|
+
print_wrap("Information has not been provided about general flags accepted by this converter.", newline=True)
|
395
|
+
elif len(converter_class.allowed_flags) > 0:
|
396
|
+
print_wrap("Allowed general flags:")
|
397
|
+
for flag, d_data, _ in converter_class.allowed_flags:
|
398
|
+
help = d_data.get("help", "(No information provided)")
|
399
|
+
print(f" {flag}")
|
400
|
+
print_wrap(help, width=TERM_WIDTH, initial_indent=" "*4, subsequent_indent=" "*4)
|
401
|
+
print("")
|
402
|
+
|
403
|
+
if converter_class.allowed_options is None:
|
404
|
+
print_wrap("Information has not been provided about general options accepted by this converter.", newline=True)
|
405
|
+
elif len(converter_class.allowed_options) > 0:
|
406
|
+
print_wrap("Allowed general options:")
|
407
|
+
for option, d_data, _ in converter_class.allowed_options:
|
408
|
+
help = d_data.get("help", "(No information provided)")
|
409
|
+
print(f" {option} <val(s)>")
|
410
|
+
print(textwrap.fill(help, initial_indent=" "*4, subsequent_indent=" "*4))
|
411
|
+
print("")
|
412
|
+
|
413
|
+
# If input/output-format specific flags or options are available for the converter but a format isn't available,
|
414
|
+
# we'll want to take note of that and mention that at the end of the output
|
415
|
+
mention_input_format = False
|
416
|
+
mention_output_format = False
|
417
|
+
|
418
|
+
if args.from_format is not None:
|
419
|
+
from_format = args.from_format
|
420
|
+
in_flags, in_options = get_in_format_args(args.name, from_format)
|
421
|
+
else:
|
422
|
+
in_flags, in_options = [], []
|
423
|
+
from_format = "N/A"
|
424
|
+
if converter_class.has_in_format_flags_or_options:
|
425
|
+
mention_input_format = True
|
426
|
+
|
427
|
+
if args.to_format is not None:
|
428
|
+
to_format = args.to_format
|
429
|
+
out_flags, out_options = get_out_format_args(args.name, to_format)
|
430
|
+
else:
|
431
|
+
out_flags, out_options = [], []
|
432
|
+
to_format = "N/A"
|
433
|
+
if converter_class.has_out_format_flags_or_options:
|
434
|
+
mention_output_format = True
|
435
|
+
|
436
|
+
# Number of character spaces allocated for flags/options when printing them out
|
437
|
+
ARG_LEN = 20
|
438
|
+
|
439
|
+
for l_args, flag_or_option, input_or_output, format_name in ((in_flags, "flag", "input", from_format),
|
440
|
+
(in_options, "option", "input", from_format),
|
441
|
+
(out_flags, "flag", "output", to_format),
|
442
|
+
(out_options, "option", "output", to_format)):
|
443
|
+
if len(l_args) == 0:
|
444
|
+
continue
|
445
|
+
print_wrap(f"Allowed {input_or_output} {flag_or_option}s for format '{format_name}':")
|
446
|
+
for arg_info in l_args:
|
447
|
+
if flag_or_option == "flag":
|
448
|
+
optional_brief = ""
|
449
|
+
else:
|
450
|
+
optional_brief = f" <{arg_info.brief}>"
|
451
|
+
print_wrap(f"{arg_info.flag+optional_brief:>{ARG_LEN}} {arg_info.description}",
|
452
|
+
subsequent_indent=" "*(ARG_LEN+2))
|
453
|
+
if arg_info.info and arg_info.info != "N/A":
|
454
|
+
print_wrap(arg_info.info,
|
455
|
+
initial_indent=" "*(ARG_LEN+2),
|
456
|
+
subsequent_indent=" "*(ARG_LEN+2))
|
457
|
+
print("")
|
458
|
+
|
459
|
+
# Now at the end, bring up input/output-format-specific flags and options
|
460
|
+
if mention_input_format and mention_output_format:
|
461
|
+
print_wrap("For details on input/output flags and options allowed for specific formats, call:\n"
|
462
|
+
f"{CL_SCRIPT_NAME} -l {args.name} -f <input_format> -t <output_format>")
|
463
|
+
elif mention_input_format:
|
464
|
+
print_wrap("For details on input flags and options allowed for a specific format, call:\n"
|
465
|
+
f"{CL_SCRIPT_NAME} -l {args.name} -f <input_format> [-t <output_format>]")
|
466
|
+
elif mention_output_format:
|
467
|
+
print_wrap("For details on output flags and options allowed for a specific format, call:\n"
|
468
|
+
f"{CL_SCRIPT_NAME} -l {args.name} -t <output_format> [-f <input_format>]")
|
469
|
+
|
470
|
+
|
471
|
+
def list_supported_formats(err=False):
|
472
|
+
"""Prints a list of all formats recognised by at least one registered converter
|
473
|
+
"""
|
474
|
+
# Make a list of all formats recognised by at least one registered converter
|
475
|
+
s_all_formats: set[str] = set()
|
476
|
+
s_registered_formats: set[str] = set()
|
477
|
+
for converter_name in L_SUPPORTED_CONVERTERS:
|
478
|
+
l_in_formats, l_out_formats = get_possible_formats(converter_name)
|
479
|
+
s_all_formats.update(l_in_formats)
|
480
|
+
s_all_formats.update(l_out_formats)
|
481
|
+
if converter_name in L_REGISTERED_CONVERTERS:
|
482
|
+
s_registered_formats.update(l_in_formats)
|
483
|
+
s_registered_formats.update(l_out_formats)
|
484
|
+
|
485
|
+
s_unregistered_formats = s_all_formats.difference(s_registered_formats)
|
486
|
+
|
487
|
+
# Convert the sets to lists and alphabetise them
|
488
|
+
l_registered_formats = list(s_registered_formats)
|
489
|
+
l_registered_formats.sort(key=lambda s: s.lower())
|
490
|
+
l_unregistered_formats = list(s_unregistered_formats)
|
491
|
+
l_unregistered_formats.sort(key=lambda s: s.lower())
|
492
|
+
|
493
|
+
# Pad the format strings to all be the same length. To keep columns aligned, all padding is done with non-
|
494
|
+
# breaking spaces (\xa0), and each format is followed by a single normal space
|
495
|
+
longest_format_len = max([len(x) for x in l_registered_formats])
|
496
|
+
l_padded_formats = [f"{x:\xa0<{longest_format_len}} " for x in l_registered_formats]
|
497
|
+
|
498
|
+
print_wrap("Formats supported by registered converters: ", err=err, newline=True)
|
499
|
+
print_wrap("".join(l_padded_formats), err=err, initial_indent=" ", subsequent_indent=" ", newline=True)
|
500
|
+
|
501
|
+
if l_unregistered_formats:
|
502
|
+
longest_unregistered_format_len = max([len(x) for x in l_unregistered_formats])
|
503
|
+
l_padded_unregistered_formats = [f"{x:\xa0<{longest_unregistered_format_len}} "
|
504
|
+
for x in l_unregistered_formats]
|
505
|
+
print_wrap("Formats supported by unregistered converters which are supported by this package: ", err=err,
|
506
|
+
newline=True)
|
507
|
+
print_wrap("".join(l_padded_unregistered_formats), err=err,
|
508
|
+
initial_indent=" ", subsequent_indent=" ", newline=True)
|
509
|
+
|
510
|
+
print_wrap("Note that not all formats are supported with all converters, or both as input and as output.")
|
511
|
+
|
512
|
+
|
513
|
+
def detail_possible_converters(from_format: str, to_format: str):
|
514
|
+
"""Prints details on converters that can perform a conversion from one format to another
|
515
|
+
"""
|
516
|
+
|
517
|
+
# Check that both formats are valid, and print an error if not
|
518
|
+
either_format_failed = False
|
519
|
+
|
520
|
+
try:
|
521
|
+
get_format_info(from_format)
|
522
|
+
except KeyError:
|
523
|
+
either_format_failed = True
|
524
|
+
print_wrap(f"ERROR: Input format '{from_format}' not recognised", newline=True, err=True)
|
525
|
+
|
526
|
+
try:
|
527
|
+
get_format_info(to_format)
|
528
|
+
except KeyError:
|
529
|
+
either_format_failed = True
|
530
|
+
print_wrap(f"ERROR: Output format '{from_format}' not recognised", newline=True, err=True)
|
531
|
+
|
532
|
+
if either_format_failed:
|
533
|
+
# Let the user know about formats which are allowed
|
534
|
+
list_supported_formats(err=True)
|
535
|
+
exit(1)
|
536
|
+
|
537
|
+
l_possible_converters = get_possible_converters(from_format, to_format)
|
538
|
+
|
539
|
+
l_possible_registered_converters = [x for x in l_possible_converters if x in L_REGISTERED_CONVERTERS]
|
540
|
+
l_possible_unregistered_converters = [x for x in l_possible_converters if
|
541
|
+
x in L_SUPPORTED_CONVERTERS and x not in L_REGISTERED_CONVERTERS]
|
542
|
+
|
543
|
+
if len(l_possible_registered_converters)+len(l_possible_unregistered_converters) == 0:
|
544
|
+
print_wrap(f"No converters are available which can perform a conversion from {from_format} to {to_format}")
|
545
|
+
return
|
546
|
+
elif len(l_possible_registered_converters) == 0:
|
547
|
+
print_wrap(f"No registered converters can perform a conversion from {from_format} to {to_format}, however "
|
548
|
+
"the following converters are supported by this package on other platforms and can perform this "
|
549
|
+
"conversion:", newline=True)
|
550
|
+
print("\n ".join(l_possible_unregistered_converters))
|
551
|
+
return
|
552
|
+
|
553
|
+
print_wrap(f"The following registered converters can convert from {from_format} to {to_format}:", newline=True)
|
554
|
+
print("\n ".join(l_possible_registered_converters))
|
555
|
+
if l_possible_unregistered_converters:
|
556
|
+
print("")
|
557
|
+
print_wrap("Additionally, the following converters are supported by this package on other platforms and can "
|
558
|
+
"perform this conversion:", newline=True)
|
559
|
+
print("\n ".join(l_possible_registered_converters))
|
560
|
+
|
561
|
+
print_wrap("For details on input/output flags and options allowed by a converter for this conversion, call:")
|
562
|
+
print(f"{CL_SCRIPT_NAME} -l <converter name> -f {from_format} -t {to_format}")
|
563
|
+
|
564
|
+
|
565
|
+
def get_supported_converters():
|
566
|
+
"""Gets a string containing a list of supported converters
|
567
|
+
"""
|
568
|
+
|
569
|
+
MSG_NOT_REGISTERED = "(supported but not registered)"
|
570
|
+
|
571
|
+
l_converters: list[str] = []
|
572
|
+
any_not_registered = False
|
573
|
+
for converter_name in L_SUPPORTED_CONVERTERS:
|
574
|
+
converter_text = converter_name
|
575
|
+
if converter_name not in L_REGISTERED_CONVERTERS:
|
576
|
+
converter_text += f" {MSG_NOT_REGISTERED}"
|
577
|
+
any_not_registered = True
|
578
|
+
l_converters.append(converter_text)
|
579
|
+
|
580
|
+
output_str = "Available converters: \n\n " + "\n ".join(l_converters)
|
581
|
+
|
582
|
+
if any_not_registered:
|
583
|
+
output_str += (f"\n\nConverters marked as \"{MSG_NOT_REGISTERED}\" are supported by this package, but no "
|
584
|
+
"appropriate binary for your platform was either distributed with this package or "
|
585
|
+
"found on your system")
|
586
|
+
|
587
|
+
return output_str
|
588
|
+
|
589
|
+
|
590
|
+
def list_supported_converters(err=False):
|
591
|
+
"""Prints a list of supported converters for the user
|
592
|
+
"""
|
593
|
+
if err:
|
594
|
+
file = sys.stderr
|
595
|
+
else:
|
596
|
+
file = sys.stdout
|
597
|
+
print(get_supported_converters() + "\n", file=file)
|
598
|
+
|
599
|
+
|
600
|
+
def detail_converters_and_formats(args: ConvertArgs):
|
601
|
+
"""Prints details on available converters and formats for the user.
|
602
|
+
"""
|
603
|
+
if args.name in L_SUPPORTED_CONVERTERS:
|
604
|
+
detail_converter_use(args)
|
605
|
+
if args.name not in L_REGISTERED_CONVERTERS:
|
606
|
+
print_wrap("WARNING: This converter is supported by this package but is not registered. It may be possible "
|
607
|
+
"to register it by installing an appropriate binary on your system.", err=True)
|
608
|
+
return
|
609
|
+
|
610
|
+
elif args.name != "":
|
611
|
+
print_wrap(f"ERROR: Converter '{args.name}' not recognized.", err=True, newline=True)
|
612
|
+
list_supported_converters(err=True)
|
613
|
+
exit(1)
|
614
|
+
elif args.from_format and args.to_format:
|
615
|
+
detail_possible_converters(args.from_format, args.to_format)
|
616
|
+
return
|
617
|
+
|
618
|
+
list_supported_converters()
|
619
|
+
list_supported_formats()
|
620
|
+
|
621
|
+
print("")
|
622
|
+
|
623
|
+
print_wrap("For more details on a converter, call:")
|
624
|
+
print(f"{CL_SCRIPT_NAME} -l <converter name>\n")
|
625
|
+
|
626
|
+
print_wrap("For a list of converters that can perform a desired conversion, call:")
|
627
|
+
print(f"{CL_SCRIPT_NAME} -l -f <input format> -t <output format>\n")
|
628
|
+
|
629
|
+
print_wrap("For a list of options provided by a converter for a desired conversion, call:")
|
630
|
+
print(f"{CL_SCRIPT_NAME} -l <converter name> -f <input format> -t <output format>")
|
631
|
+
|
632
|
+
|
633
|
+
def run_from_args(args: ConvertArgs):
|
634
|
+
"""Workhorse function to perform primary execution of this script, using the provided parsed arguments.
|
635
|
+
|
636
|
+
Parameters
|
637
|
+
----------
|
638
|
+
args : ConvertArgs
|
639
|
+
The parsed arguments for this script.
|
640
|
+
"""
|
641
|
+
|
642
|
+
# Check if we've been asked to list options
|
643
|
+
if args.list:
|
644
|
+
return detail_converters_and_formats(args)
|
645
|
+
|
646
|
+
data = {'success': 'unknown',
|
647
|
+
'from_flags': args.from_flags,
|
648
|
+
'to_flags': args.to_flags,
|
649
|
+
'from_options': args.from_options,
|
650
|
+
'to_options': args.to_options,
|
651
|
+
'from_arg_flags': '',
|
652
|
+
'from_args': '',
|
653
|
+
'to_arg_flags': '',
|
654
|
+
'to_args': '',
|
655
|
+
'upload_file': 'true'}
|
656
|
+
data.update(args.d_converter_args)
|
657
|
+
|
658
|
+
success = True
|
659
|
+
|
660
|
+
for filename in args.l_args:
|
661
|
+
|
662
|
+
# Search for the file in the input directory
|
663
|
+
qualified_filename = os.path.join(args.input_dir, filename)
|
664
|
+
if not os.path.isfile(qualified_filename):
|
665
|
+
# Check if we can add the format to it as an extension to find it
|
666
|
+
ex_extension = f".{args.from_format}"
|
667
|
+
if not qualified_filename.endswith(ex_extension):
|
668
|
+
qualified_filename += ex_extension
|
669
|
+
if not os.path.isfile(qualified_filename):
|
670
|
+
print_wrap(f"ERROR: Cannot find file {filename+ex_extension} in directory {args.input_dir}",
|
671
|
+
err=True)
|
672
|
+
continue
|
673
|
+
else:
|
674
|
+
print_wrap(f"ERROR: Cannot find file {filename} in directory {args.input_dir}", err=True)
|
675
|
+
continue
|
676
|
+
|
677
|
+
if not args.quiet:
|
678
|
+
print_wrap(f"Converting {filename} to {args.to_format}...", newline=True)
|
679
|
+
|
680
|
+
try:
|
681
|
+
conversion_result = run_converter(filename=qualified_filename,
|
682
|
+
to_format=args.to_format,
|
683
|
+
from_format=args.from_format,
|
684
|
+
name=args.name,
|
685
|
+
data=data,
|
686
|
+
use_envvars=False,
|
687
|
+
upload_dir=args.input_dir,
|
688
|
+
download_dir=args.output_dir,
|
689
|
+
no_check=args.no_check,
|
690
|
+
strict=args.strict,
|
691
|
+
log_file=args.log_file,
|
692
|
+
log_mode=args.log_mode,
|
693
|
+
log_level=args.log_level,
|
694
|
+
delete_input=args.delete_input,
|
695
|
+
refresh_local_log=False)
|
696
|
+
except FileConverterHelpException as e:
|
697
|
+
print_wrap(f"ERROR: {e}", err=True)
|
698
|
+
success = False
|
699
|
+
continue
|
700
|
+
except FileConverterAbortException as e:
|
701
|
+
print_wrap(f"ERROR: Attempt to convert file {filename} aborted with status code {e.status_code} and "
|
702
|
+
f"message:\n{e}\n", err=True)
|
703
|
+
success = False
|
704
|
+
continue
|
705
|
+
except FileConverterInputException as e:
|
706
|
+
if "Conversion from" in str(e) and "is not supported" in str(e):
|
707
|
+
print_wrap(f"ERROR: {e}", err=True, newline=True)
|
708
|
+
detail_possible_converters(args.from_format, args.to_format)
|
709
|
+
else:
|
710
|
+
print_wrap(f"ERROR: Attempt to convert file {filename} failed at converter initialization with "
|
711
|
+
f"exception type {type(e)} and message: \n{e}\n", err=True)
|
712
|
+
success = False
|
713
|
+
continue
|
714
|
+
except Exception as e:
|
715
|
+
print_wrap(f"ERROR: Attempt to convert file {filename} failed with exception type {type(e)} and message: " +
|
716
|
+
f"\n{e}\n", err=True)
|
717
|
+
success = False
|
718
|
+
continue
|
719
|
+
|
720
|
+
if not args.quiet:
|
721
|
+
print_wrap("Success! The converted file can be found at:",)
|
722
|
+
print(f" {conversion_result.output_filename}")
|
723
|
+
print_wrap("The log can be found at:")
|
724
|
+
print(f" {conversion_result.log_filename}")
|
725
|
+
|
726
|
+
if not success:
|
727
|
+
exit(1)
|
728
|
+
|
729
|
+
|
730
|
+
def main():
|
731
|
+
"""Standard entry-point function for this script.
|
732
|
+
"""
|
733
|
+
|
734
|
+
# If no inputs were provided, print a message about usage
|
735
|
+
if len(sys.argv) == 1:
|
736
|
+
print_wrap("See the README.md file for information on using this utility and examples of basic usage, or for "
|
737
|
+
"detailed explanation of arguments call:")
|
738
|
+
print(f"{CL_SCRIPT_NAME} -h")
|
739
|
+
exit(1)
|
740
|
+
|
741
|
+
try:
|
742
|
+
args = parse_args()
|
743
|
+
except FileConverterHelpException as e:
|
744
|
+
# If we get a Help exception, it's likely due to user error, so don't bother them with a traceback and simply
|
745
|
+
# print the message to stderr
|
746
|
+
if e.msg_preformatted:
|
747
|
+
print(e, file=sys.stderr)
|
748
|
+
else:
|
749
|
+
print_wrap(f"ERROR: {e}", err=True)
|
750
|
+
exit(1)
|
751
|
+
|
752
|
+
if (args.log_mode == const.LOG_SIMPLE or args.log_mode == const.LOG_FULL) and args.log_file:
|
753
|
+
# Delete any previous local log if it exists
|
754
|
+
try:
|
755
|
+
os.remove(args.log_file)
|
756
|
+
except FileNotFoundError:
|
757
|
+
pass
|
758
|
+
logging.basicConfig(filename=args.log_file, level=args.log_level,
|
759
|
+
format=const.LOG_FORMAT, datefmt=const.TIMESTAMP_FORMAT)
|
760
|
+
else:
|
761
|
+
logging.basicConfig(level=args.log_level, format=const.LOG_FORMAT)
|
762
|
+
|
763
|
+
logging.debug("#")
|
764
|
+
logging.debug("# Beginning execution of script `%s`", __file__)
|
765
|
+
logging.debug("#")
|
766
|
+
|
767
|
+
run_from_args(args)
|
768
|
+
|
769
|
+
logging.debug("#")
|
770
|
+
logging.debug("# Finished execution of script `%s`", __file__)
|
771
|
+
logging.debug("#")
|
772
|
+
|
773
|
+
|
774
|
+
if __name__ == "__main__":
|
775
|
+
|
776
|
+
main()
|