Module psdi_data_conversion.main
@file psdi_data_conversion/main.py
Created 2025-01-14 by Bryan Gillis.
Entry-point file for the command-line interface for data conversion.
Functions
def detail_converter_use(args: ConvertArgs)
-
Expand source code
def detail_converter_use(args: ConvertArgs): """Prints output providing information on a specific converter, including the flags and options it allows """ converter_info = get_converter_info(args.name) converter_class = get_supported_converter_class(args.name) converter_name = converter_class.name print_wrap(f"{converter_name}: {converter_info.description} ({converter_info.url})", break_long_words=False, break_on_hyphens=False, newline=True) if converter_class.info: print_wrap(converter_class.info, break_long_words=False, break_on_hyphens=False, newline=True) # If both an input and output format are specified, provide the degree of success for this conversion. Otherwise # list possible input/output formats if args.from_format is not None and args.to_format is not None: qual = get_conversion_quality(args.name, args.from_format, args.to_format) if qual is None: print_wrap(f"Conversion from '{args.from_format}' to '{args.to_format}' with {converter_name} is not " "supported.", newline=True) else: print_wrap(f"Conversion from '{args.from_format}' to '{args.to_format}' with {converter_name} is " f"possible with {qual.qual_str} conversion quality", newline=True) # If there are any potential issues with the conversion, print them out if qual.details: print_wrap("WARNING: Potential data loss or extrapolation issues with this conversion:") for detail_line in qual.details.split("\n"): print_wrap(f"- {detail_line}") print("") else: l_input_formats, l_output_formats = get_possible_formats(args.name) # If one format was supplied, check if it's supported for (format_name, l_formats, to_or_from) in ((args.from_format, l_input_formats, "from"), (args.to_format, l_output_formats, "to")): if format_name is None: continue if format_name in l_formats: optional_not: str = "" else: optional_not: str = "not " print_wrap(f"Conversion {to_or_from} {format_name} is {optional_not}supported by {converter_name}.\n") # List all possible formats, and which can be used for input and which for output s_all_formats: set[FormatInfo] = set(l_input_formats) s_all_formats.update(l_output_formats) l_all_formats: list[FormatInfo] = list(s_all_formats) l_all_formats.sort(key=lambda x: x.disambiguated_name.lower()) print_wrap(f"File formats supported by {converter_name}:", newline=True) max_format_length = max([len(x.disambiguated_name) for x in l_all_formats]) print(" "*(max_format_length+4) + " INPUT OUTPUT") print(" "*(max_format_length+4) + " ----- ------") for file_format in l_all_formats: in_yes_or_no = "yes" if file_format in l_input_formats else "no" out_yes_or_no = "yes" if file_format in l_output_formats else "no" print(f" {file_format.disambiguated_name:>{max_format_length}}{in_yes_or_no:>8}{out_yes_or_no:>8}") print("") if converter_class.allowed_flags is None: print_wrap("Information has not been provided about general flags accepted by this converter.", newline=True) elif len(converter_class.allowed_flags) > 0: print_wrap("Allowed general flags:") for flag, d_data, _ in converter_class.allowed_flags: help = d_data.get("help", "(No information provided)") print(f" {flag}") print_wrap(help, width=TERM_WIDTH, initial_indent=" "*4, subsequent_indent=" "*4) print("") if converter_class.allowed_options is None: print_wrap("Information has not been provided about general options accepted by this converter.", newline=True) elif len(converter_class.allowed_options) > 0: print_wrap("Allowed general options:") for option, d_data, _ in converter_class.allowed_options: help = d_data.get("help", "(No information provided)") print(f" {option} <val(s)>") print(textwrap.fill(help, initial_indent=" "*4, subsequent_indent=" "*4)) print("") # If input/output-format specific flags or options are available for the converter but a format isn't available, # we'll want to take note of that and mention that at the end of the output mention_input_format = False mention_output_format = False if args.from_format is not None: from_format = args.from_format in_flags, in_options = get_in_format_args(args.name, from_format) else: in_flags, in_options = [], [] from_format = "N/A" if converter_class.has_in_format_flags_or_options: mention_input_format = True if args.to_format is not None: to_format = args.to_format out_flags, out_options = get_out_format_args(args.name, to_format) else: out_flags, out_options = [], [] to_format = "N/A" if converter_class.has_out_format_flags_or_options: mention_output_format = True # Number of character spaces allocated for flags/options when printing them out ARG_LEN = 20 for l_args, flag_or_option, input_or_output, format_name in ((in_flags, "flag", "input", from_format), (in_options, "option", "input", from_format), (out_flags, "flag", "output", to_format), (out_options, "option", "output", to_format)): if len(l_args) == 0: continue print_wrap(f"Allowed {input_or_output} {flag_or_option}s for format '{format_name}':") for arg_info in l_args: if flag_or_option == "flag": optional_brief = "" else: optional_brief = f" <{arg_info.brief}>" print_wrap(f"{arg_info.flag+optional_brief:>{ARG_LEN}} {arg_info.description}", subsequent_indent=" "*(ARG_LEN+2)) if arg_info.info and arg_info.info != "N/A": print_wrap(arg_info.info, initial_indent=" "*(ARG_LEN+2), subsequent_indent=" "*(ARG_LEN+2)) print("") # Now at the end, bring up input/output-format-specific flags and options if mention_input_format and mention_output_format: print_wrap("For details on input/output flags and options allowed for specific formats, call:\n" f"{CL_SCRIPT_NAME} -l {converter_name} -f <input_format> -t <output_format>") elif mention_input_format: print_wrap("For details on input flags and options allowed for a specific format, call:\n" f"{CL_SCRIPT_NAME} -l {converter_name} -f <input_format> [-t <output_format>]") elif mention_output_format: print_wrap("For details on output flags and options allowed for a specific format, call:\n" f"{CL_SCRIPT_NAME} -l {converter_name} -t <output_format> [-f <input_format>]")
Prints output providing information on a specific converter, including the flags and options it allows
def detail_converters_and_formats(args: ConvertArgs)
-
Expand source code
def detail_converters_and_formats(args: ConvertArgs): """Prints details on available converters and formats for the user. """ if args.name in L_SUPPORTED_CONVERTERS: detail_converter_use(args) if args.name not in L_REGISTERED_CONVERTERS: print_wrap("WARNING: This converter is supported by this package but is not registered. It may be possible " "to register it by installing an appropriate binary on your system.", err=True) return elif args.name != "": print_wrap(f"ERROR: Converter '{args.name}' not recognized.", err=True, newline=True) list_supported_converters(err=True) exit(1) elif args.from_format and args.to_format: detail_formats_and_possible_converters(args.from_format, args.to_format) return elif args.from_format: detail_format(args.from_format) return elif args.to_format: detail_format(args.to_format) return list_supported_converters() list_supported_formats() print("") print_wrap("For more details on a converter, call:") print(f"{CL_SCRIPT_NAME} -l <converter name>\n") print_wrap("For more details on a format, call:") print(f"{CL_SCRIPT_NAME} -l -f <format>\n") print_wrap("For a list of converters that can perform a desired conversion, call:") print(f"{CL_SCRIPT_NAME} -l -f <input format> -t <output format>\n") print_wrap("For a list of options provided by a converter for a desired conversion, call:") print(f"{CL_SCRIPT_NAME} -l <converter name> -f <input format> -t <output format>")
Prints details on available converters and formats for the user.
def detail_format(format_name: str)
-
Expand source code
def detail_format(format_name: str): """Prints details on a format """ l_format_info = get_format_info(format_name, which="all") if len(l_format_info) == 0: print_wrap(f"ERROR: Format '{format_name}' not recognised", err=True, newline=True) list_supported_formats(err=True) exit(1) if len(l_format_info) > 1: print_wrap(f"WARNING: Format '{format_name}' is ambiguous and could refer to multiple formats. It may be " "necessary to explicitly specify which you want to use when calling this script, e.g. with " f"'-f {format_name}-0' - see the disambiguated names in the list below:", newline=True) first = True for format_info in l_format_info: # Add linebreak before each after the first if first: first = False else: print() # Print the format's basic details print_wrap(f"{format_info.id}: {format_info.disambiguated_name} ({format_info.note})") # Print whether or not it supports each possible property for attr, label in FormatInfo.D_PROPERTY_ATTRS.items(): support_str = label if getattr(format_info, attr): support_str += " supported" elif getattr(format_info, attr) is False: support_str += " not supported" else: support_str += " unknown whether or not to be supported" print_wrap(f"- {support_str}")
Prints details on a format
def detail_formats_and_possible_converters(from_format: str, to_format: str)
-
Expand source code
def detail_formats_and_possible_converters(from_format: str, to_format: str): """Prints details on converters that can perform a conversion from one format to another """ # Check that both formats are valid, and print an error if not either_format_failed = False try: get_format_info(from_format, which=0) except KeyError: either_format_failed = True print_wrap(f"ERROR: Input format '{from_format}' not recognised", newline=True, err=True) try: get_format_info(to_format, which=0) except KeyError: either_format_failed = True print_wrap(f"ERROR: Output format '{from_format}' not recognised", newline=True, err=True) if either_format_failed: # Let the user know about formats which are allowed list_supported_formats(err=True) exit(1) # Provide details on both the input and output formats detail_format(from_format) print() detail_format(to_format) l_possible_conversions = get_possible_conversions(from_format, to_format) # Check if no direct conversions are possible, and if formats are specified uniquely, recommend a chained conversion if len(l_possible_conversions) == 0: print() print_wrap(f"No direct conversions are possible from {from_format} to {to_format}") print() l_from_formats = get_format_info(from_format, which="all") l_to_formats = get_format_info(to_format, which="all") if len(l_from_formats) == 1 and len(l_to_formats) == 1: for only in "registered", "supported", "all": pathway = get_conversion_pathway(l_from_formats[0], l_to_formats[0], only=only) if pathway is None: continue if only == "registered" or only == "supported": converter_type_needed = only else: converter_type_needed = "unsupported" print_wrap(f"A chained conversion is possible from {from_format} to {to_format} using " f"{converter_type_needed} converters:") for i, step in enumerate(pathway): print_wrap(f"{i+1}) Convert from {step[1].name} to {step[2].name} with {step[0].pretty_name}") print() print_wrap("Chained conversion is not yet supported by this utility, but will be added soon") break else: print_wrap(f"No chained conversions are possible from {from_format} to {to_format}.") else: print_wrap("To see possible chained conversions, specify each format uniquely using the ID or " "disambiguated name (e.g. \"xxx-0\") listed above)") # Get a list of all different formats which share the provided name, cutting out duplicates l_from_formats = list(set([x[1] for x in l_possible_conversions])) l_from_formats.sort(key=lambda x: x.disambiguated_name) l_to_formats = list(set([x[2] for x in l_possible_conversions])) l_to_formats.sort(key=lambda x: x.disambiguated_name) # Loop over all possible combinations of formats for possible_from_format, possible_to_format in product(l_from_formats, l_to_formats): print() from_name = possible_from_format.disambiguated_name to_name = possible_to_format.disambiguated_name l_conversions_matching_formats = [x for x in l_possible_conversions if x[1] == possible_from_format and x[2] == possible_to_format] l_possible_registered_converters = [x[0].pretty_name for x in l_conversions_matching_formats if x[0].name in L_REGISTERED_CONVERTERS] l_possible_unregistered_converters = [x[0].pretty_name for x in l_conversions_matching_formats if x[0].name in L_SUPPORTED_CONVERTERS and x[0].name not in L_REGISTERED_CONVERTERS] if len(l_possible_registered_converters)+len(l_possible_unregistered_converters) == 0: print_wrap(f"No converters are available which can perform a conversion from {from_name} to " f"{to_name}") continue elif len(l_possible_registered_converters) == 0: print_wrap(f"No registered converters can perform a conversion from {from_name} to " f"{to_name}, however the following converters are supported by this package on other " "platforms and can perform this conversion:", newline=True) print("\n ".join(l_possible_unregistered_converters)) continue print_wrap(f"The following registered converters can convert from {from_name} to " f"{to_name}:", newline=True) print(" " + "\n ".join(l_possible_registered_converters) + "\n") if l_possible_unregistered_converters: print("") print_wrap("Additionally, the following converters are supported by this package on other platforms and " "can perform this conversion:", newline=True) print(" " + "\n ".join(l_possible_unregistered_converters) + "\n") print_wrap("For details on input/output flags and options allowed by a converter for this conversion, call:") print(f"{CL_SCRIPT_NAME} -l <converter name> -f {from_name} -t {to_name}")
Prints details on converters that can perform a conversion from one format to another
def get_argument_parser()
-
Expand source code
def get_argument_parser(): """Get an argument parser for this script. Returns ------- parser : ArgumentParser An argument parser set up with the allowed command-line arguments for this script. """ parser = ArgumentParser() # Positional arguments parser.add_argument("l_args", type=str, nargs="*", help="Normally, file(s) to be converted or zip/tar archives thereof. If an archive or archives " "are provided, the output will be packed into an archive of the same type. Filenames should be " "provided as either relative to the input directory (default current directory) or absolute. " "If the '-l' or '--list' flag is set, instead the name of a converter can be used here to get " "information on it.") # Keyword arguments for standard conversion parser.add_argument("-f", "--from", type=str, default=None, help="The input (convert from) file extension (e.g., smi). If not provided, will attempt to " "auto-detect format.") parser.add_argument("-i", "--in", type=str, default=None, help="The directory containing the input file(s), default current directory.") parser.add_argument("-t", "--to", type=str, default=None, help="The output (convert to) file extension (e.g., cmi).") parser.add_argument("-o", "--out", type=str, default=None, help="The directory where output files should be created. If not provided, output files will " "be created in -i/--in directory if that was provided, or else in the directory containing the " "first input file.") parser.add_argument("-w", "--with", type=str, nargs="+", help="The converter to be used (default 'Open Babel').") parser.add_argument("--delete-input", action="store_true", help="If set, input files will be deleted after conversion, default they will be kept") parser.add_argument("--from-flags", type=str, default="", help="Any command-line flags to be provided to the converter for reading in the input file(s). " "For information on the flags accepted by a converter and its required format for them, " "call this script with '-l <converter name>'. If the set of flags includes any spaces, it " "must be quoted, and if hyphens are used, the first preceding hyphen for each flag must " "be backslash-escaped, e.g. '--from-flags \"\\-a \\-bc \\--example\"'") parser.add_argument("--to-flags", type=str, default="", help="Any command-line flags to be provided to the converter for writing the output file(s). " "For information on the flags accepted by a converter and its required format for them, " "call this script with '-l <converter name>'. If the set of flags includes any spaces, it " "must be quoted, and if hyphens are used, the first preceding hyphen for each flag must " "be backslash-escaped, e.g. '--to-flags \"\\-a \\-bc \\--example\"'") parser.add_argument("--from-options", type=str, default="", help="Any command-line options to be provided to the converter for reading in the input " "file(s). For information on the options accepted by a converter and its required format " "for them, call this script with '-l <converter name>'. If the set of options includes " "any spaces, it must be quoted, and the first preceding hyphen for each option must be " "backslash-escaped, e.g. '--from-options \"\\-x xval --opt optval\"'") parser.add_argument("--to-options", type=str, default="", help="Any command-line options to be provided to the converter for writing the output " "file(s). For information on the options accepted by a converter and its required format " "for them, call this script with '-l <converter name>'. If the set of options includes " "any spaces, it must be quoted, and the first preceding hyphen for each option must be " "backslash-escaped, e.g. '--to-options \"\\-x xval --opt optval\"'") parser.add_argument("-s", "--strict", action="store_true", help="If set, will fail if one of the input files has the wrong extension (including those " "contained in archives, but not the archive files themselves). Otherwise, will only print a " "warning in this case.") parser.add_argument("--nc", "--no-check", action="store_true", help="If set, will not perform a pre-check in the database on the validity of a conversion. " "Setting this will result in a less human-friendly error message (or may even falsely indicate " "success) if the conversion is not supported, but will save some execution time. Recommended " "only for automated execution after the user has confirmed a conversion is supported") # Keyword arguments specific to converters for converter_name in L_REGISTERED_CONVERTERS: l_converter_args = D_CONVERTER_ARGS[converter_name] if l_converter_args: for arg_name, kwargs, _ in l_converter_args: parser.add_argument(arg_name, **kwargs) # Keyword arguments for alternative functionality parser.add_argument("-l", "--list", action="store_true", help="If provided alone, lists all available converters. If the name of a converter is " "provided, gives information on the converter and any command-line flags it accepts.") # Logging/stdout arguments parser.add_argument("-g", "--log-file", type=str, default=None, help="The name of the file to log to. This can be provided relative to the current directory " "(e.g. '-g ../logs/log-file.txt') or fully qualified (e.g. /path/to/log-file.txt). " "If not provided, the log file will be named after the =first input file (+'.log') and placed " "in the output directory (specified with -o/--out).\n" "In 'full' logging mode (not recommended with this interface), this will apply only to logs " "from the outermost level of the script if explicitly specified. If not explicitly specified, " "those logs will be sent to stderr.") parser.add_argument("--log-mode", type=str, default=const.LOG_SIMPLE, help="How logs should be stored. Allowed values are: \n" "- 'full' - Multi-file logging, not recommended for the CLI, but allowed for a compatible " "interface with the public web app" "- 'simple' - Logs saved to one file" "- 'stdout' - Output logs and errors only to stdout" "- 'none' - Output only errors to stdout") parser.add_argument("-q", "--quiet", action="store_true", help="If set, all terminal output aside from errors will be suppressed and no log file will be " "generated.") parser.add_argument("--log-level", type=str, default=None, help="The desired level to log at. Allowed values are: 'DEBUG', 'INFO', 'WARNING', 'ERROR, " "'CRITICAL'. Default: 'INFO' for logging to file, 'WARNING' for logging to stdout") return parser
Get an argument parser for this script.
Returns
parser
:ArgumentParser
- An argument parser set up with the allowed command-line arguments for this script.
def get_supported_converters()
-
Expand source code
def get_supported_converters(): """Gets a string containing a list of supported converters """ MSG_NOT_REGISTERED = "(supported but not registered)" l_converters: list[str] = [] any_not_registered = False for converter_name in L_SUPPORTED_CONVERTERS: converter_text = get_supported_converter_class(converter_name).name if converter_name not in L_REGISTERED_CONVERTERS: converter_text += f" {MSG_NOT_REGISTERED}" any_not_registered = True l_converters.append(converter_text) output_str = "Available converters: \n\n " + "\n ".join(l_converters) if any_not_registered: output_str += (f"\n\nConverters marked as \"{MSG_NOT_REGISTERED}\" are supported by this package, but no " "appropriate binary for your platform was either distributed with this package or " "found on your system") return output_str
Gets a string containing a list of supported converters
def list_supported_converters(err=False)
-
Expand source code
def list_supported_converters(err=False): """Prints a list of supported converters for the user """ if err: file = sys.stderr else: file = sys.stdout print(get_supported_converters() + "\n", file=file)
Prints a list of supported converters for the user
def list_supported_formats(err=False)
-
Expand source code
def list_supported_formats(err=False): """Prints a list of all formats recognised by at least one registered converter """ # Make a list of all formats recognised by at least one registered converter s_all_formats: set[FormatInfo] = set() s_registered_formats: set[FormatInfo] = set() for converter_name in L_SUPPORTED_CONVERTERS: l_in_formats, l_out_formats = get_possible_formats(converter_name) # To make sure we don't see any unexpected duplicates in the set due to cached/uncached values, get the # disambiguated name of each format first [x.disambiguated_name for x in l_in_formats] [x.disambiguated_name for x in l_out_formats] s_all_formats.update(l_in_formats) s_all_formats.update(l_out_formats) if converter_name in L_REGISTERED_CONVERTERS: s_registered_formats.update(l_in_formats) s_registered_formats.update(l_out_formats) s_unregistered_formats = s_all_formats.difference(s_registered_formats) # Convert the sets to lists and alphabetise them l_registered_formats = list(s_registered_formats) l_registered_formats.sort(key=lambda x: x.disambiguated_name.lower()) l_unregistered_formats = list(s_unregistered_formats) l_unregistered_formats.sort(key=lambda x: x.disambiguated_name.lower()) # Pad the format strings to all be the same length. To keep columns aligned, all padding is done with non- # breaking spaces (\xa0), and each format is followed by a single normal space longest_format_len = max([len(x.disambiguated_name) for x in l_registered_formats]) l_padded_formats = [f"{x.disambiguated_name:\xa0<{longest_format_len}} " for x in l_registered_formats] print_wrap("Formats supported by registered converters: ", err=err, newline=True) print_wrap("".join(l_padded_formats), err=err, initial_indent=" ", subsequent_indent=" ", newline=True) if l_unregistered_formats: longest_unregistered_format_len = max([len(x) for x in l_unregistered_formats]) l_padded_unregistered_formats = [f"{x:\xa0<{longest_unregistered_format_len}} " for x in l_unregistered_formats] print_wrap("Formats supported by unregistered converters which are supported by this package: ", err=err, newline=True) print_wrap("".join(l_padded_unregistered_formats), err=err, initial_indent=" ", subsequent_indent=" ", newline=True) print_wrap("Note that not all formats are supported with all converters, or both as input and as output.") if err: print("") print_wrap("For more details on a format, call:") print(f"{CL_SCRIPT_NAME} -l -f <format>")
Prints a list of all formats recognised by at least one registered converter
def main()
-
Expand source code
def main(): """Standard entry-point function for this script. """ # If no inputs were provided, print a message about usage if len(sys.argv) == 1: print_wrap("See the README.md file for information on using this utility and examples of basic usage, or for " "detailed explanation of arguments call:") print(f"{CL_SCRIPT_NAME} -h") exit(1) try: args = parse_args() except FileConverterInputException as e: if not e.help: raise # If we get an exception with the help flag set, it's likely due to user error, so don't bother them with a # traceback and simply print the message to stderr if e.msg_preformatted: print(e, file=sys.stderr) else: print_wrap(f"ERROR: {e}", err=True) exit(1) if (args.log_mode == const.LOG_SIMPLE or args.log_mode == const.LOG_FULL) and args.log_file: # Delete any previous local log if it exists try: os.remove(args.log_file) except FileNotFoundError: pass logging.basicConfig(filename=args.log_file, level=args.log_level, format=const.LOG_FORMAT, datefmt=const.TIMESTAMP_FORMAT) else: logging.basicConfig(level=args.log_level, format=const.LOG_FORMAT) logging.debug("#") logging.debug("# Beginning execution of script `%s`", __file__) logging.debug("#") run_from_args(args) logging.debug("#") logging.debug("# Finished execution of script `%s`", __file__) logging.debug("#")
Standard entry-point function for this script.
def parse_args()
-
Expand source code
def parse_args(): """Parses arguments for this executable. Returns ------- args : Namespace The parsed arguments. """ parser = get_argument_parser() args = ConvertArgs(parser.parse_args()) return args
Parses arguments for this executable.
Returns
args
:Namespace
- The parsed arguments.
def print_wrap(s: str, newline=False, err=False, **kwargs)
-
Expand source code
def print_wrap(s: str, newline=False, err=False, **kwargs): """Print a string wrapped to the terminal width """ if err: file = sys.stderr else: file = sys.stdout for line in s.split("\n"): print(textwrap.fill(line, width=TERM_WIDTH, **kwargs), file=file) if newline: print("")
Print a string wrapped to the terminal width
def run_from_args(args: ConvertArgs)
-
Expand source code
def run_from_args(args: ConvertArgs): """Workhorse function to perform primary execution of this script, using the provided parsed arguments. Parameters ---------- args : ConvertArgs The parsed arguments for this script. """ # Check if we've been asked to list options if args.list: return detail_converters_and_formats(args) data = {'success': 'unknown', 'from_flags': args.from_flags, 'to_flags': args.to_flags, 'from_options': args.from_options, 'to_options': args.to_options, 'from_arg_flags': '', 'from_args': '', 'to_arg_flags': '', 'to_args': '', 'upload_file': 'true'} data.update(args.d_converter_args) success = True for filename in args.l_args: # Search for the file in the input directory qualified_filename = os.path.join(args.input_dir, filename) if not os.path.isfile(qualified_filename): # Check if we can add the format to it as an extension to find it ex_extension = f".{args.from_format}" if not qualified_filename.endswith(ex_extension): qualified_filename += ex_extension if not os.path.isfile(qualified_filename): print_wrap(f"ERROR: Cannot find file {filename+ex_extension} in directory {args.input_dir}", err=True) continue else: print_wrap(f"ERROR: Cannot find file {filename} in directory {args.input_dir}", err=True) continue if not args.quiet: print_wrap(f"Converting {filename} to {args.to_format}...", newline=True) try: conversion_result = run_converter(filename=qualified_filename, to_format=args.to_format, from_format=args.from_format, name=args.name, data=data, use_envvars=False, input_dir=args.input_dir, output_dir=args.output_dir, no_check=args.no_check, strict=args.strict, log_file=args.log_file, log_mode=args.log_mode, log_level=args.log_level, delete_input=args.delete_input, refresh_local_log=False) except FileConverterAbortException as e: if not e.logged: print_wrap(f"ERROR: Attempt to convert file {filename} aborted with status code {e.status_code} and " f"message:\n{e}\n", err=True) e.logged = True success = False continue except FileConverterException as e: if e.help and not e.logged: print_wrap(f"ERROR: {e}", err=True) e.logged = True elif "Conversion from" in str(e) and "is not supported" in str(e): if not e.logged: print_wrap(f"ERROR: {e}", err=True, newline=True) detail_formats_and_possible_converters(args.from_format, args.to_format) elif e.help and not e.logged: if e.msg_preformatted: print(e, file=sys.stderr) else: print_wrap(f"ERROR: {e}", err=True) elif not e.logged: print_wrap(f"ERROR: Attempt to convert file {filename} failed at converter initialization with " f"exception type {type(e)} and message: \n{e}\n", err=True) e.logged = True success = False continue except Exception as e: if not hasattr(e, "logged") or e.logged is False: print_wrap(f"ERROR: Attempt to convert file {filename} failed with exception type {type(e)} and " f"message: \n{e}\n", err=True) e.logged = True success = False continue if not args.quiet: print_wrap("Success! The converted file can be found at:",) print(f" {conversion_result.output_filename}") print_wrap("The log can be found at:") print(f" {conversion_result.log_filename}") if not success: exit(1)
Workhorse function to perform primary execution of this script, using the provided parsed arguments.
Parameters
args
:ConvertArgs
- The parsed arguments for this script.
Classes
class ConvertArgs (args)
-
Expand source code
class ConvertArgs: """Class storing arguments for data conversion, processed and determined from the input arguments. """ def __init__(self, args): # Start by copying over arguments. Some share names with reserved words, so we have to use `getattr` for them # Positional arguments self.l_args: list[str] = args.l_args # Keyword arguments for standard conversion self.from_format: str | None = getattr(args, "from") self._input_dir: str | None = getattr(args, "in") self.to_format: str | None = args.to self._output_dir: str | None = args.out converter_name = getattr(args, "with") if isinstance(converter_name, str): self.name = regularize_name(converter_name) elif converter_name: self.name = regularize_name(" ".join(converter_name)) else: self.name = None self.delete_input = args.delete_input self.from_flags: str = args.from_flags.replace(r"\-", "-") self.to_flags: str = args.to_flags.replace(r"\-", "-") self.from_options: str = args.from_options.replace(r"\-", "-") self.to_options: str = args.to_options.replace(r"\-", "-") self.no_check: bool = args.nc self.strict: bool = args.strict # Keyword arguments for alternative functionality self.list: bool = args.list # Logging/stdout arguments self.log_mode: bool = args.log_mode self.quiet = args.quiet self._log_file: str | None = args.log_file if not args.log_level: self.log_level = None else: try: self.log_level = get_log_level_from_str(args.log_level) except ValueError as e: # A ValueError indicates an unrecognised logging level, so we reraise this with the help flag to # indicate we want to provide this as feedback to the user so they can correct their command raise FileConverterInputException(str(e), help=True) # If formats were provided as ints, convert them to the int type now try: if self.from_format: self.from_format = int(self.from_format) except ValueError: pass try: if self.to_format: self.to_format = int(self.to_format) except ValueError: pass # Special handling for listing converters if self.list: # Force log mode to stdout and turn off quiet self.log_mode = const.LOG_STDOUT self.quiet = False # Get the converter name from the arguments if it wasn't provided by -w/--with if not self.name: self.name = regularize_name(" ".join(self.l_args)) # For this operation, any other arguments can be ignored return # If not listing and a converter name wasn't supplied, use the default converter if not self.name: self.name = regularize_name(CONVERTER_DEFAULT) # Quiet mode is equivalent to logging mode == LOGGING_NONE, so normalize them if either is set if self.quiet: self.log_mode = const.LOG_NONE elif self.log_mode == const.LOG_NONE: self.quiet = True # Check validity of input if len(self.l_args) == 0: raise FileConverterInputException("One or more names of files to convert must be provided", help=True) if self._input_dir is not None and not os.path.isdir(self._input_dir): raise FileConverterInputException(f"The provided input directory '{self._input_dir}' does not exist as a " "directory", help=True) if self.to_format is None: msg = textwrap.fill("ERROR Output format (-t or --to) must be provided. For information on supported " "formats and converters, call:\n") msg += f"{CL_SCRIPT_NAME} -l" raise FileConverterInputException(msg, msg_preformatted=True, help=True) # If the output directory doesn't exist, silently create it if self._output_dir is not None and not os.path.isdir(self._output_dir): if os.path.exists(self._output_dir): raise FileConverterInputException(f"Output directory '{self._output_dir}' exists but is not a " "directory", help=True) os.makedirs(self._output_dir, exist_ok=True) # Check the converter is recognized if not converter_is_supported(self.name): msg = textwrap.fill(f"ERROR: Converter '{self.name}' not recognised", width=TERM_WIDTH) msg += f"\n\n{get_supported_converters()}" raise FileConverterInputException(msg, help=True, msg_preformatted=True) elif not converter_is_registered(self.name): converter_name = get_supported_converter_class(self.name).name msg = textwrap.fill(f"ERROR: Converter '{converter_name}' is not registered. It may be possible to " "register it by installing an appropriate binary for your platform.", width=TERM_WIDTH) msg += f"\n\n{get_supported_converters()}" raise FileConverterInputException(msg, help=True, msg_preformatted=True) # Logging mode is valid if self.log_mode not in const.L_ALLOWED_LOG_MODES: raise FileConverterInputException(f"Unrecognised logging mode: {self.log_mode}. Allowed " f"modes are: {const.L_ALLOWED_LOG_MODES}", help=True) # Arguments specific to this converter self.d_converter_args = {} l_converter_args = D_CONVERTER_ARGS[self.name] if not l_converter_args: l_converter_args = [] for arg_name, _, get_data in l_converter_args: # Convert the argument name to how it will be represented in the parsed_args object while arg_name.startswith("-"): arg_name = arg_name[1:] arg_name = arg_name.replace("-", "_") self.d_converter_args.update(get_data(getattr(args, arg_name))) @property def input_dir(self): """If the input directory isn't provided, use the current directory. """ if self._input_dir is None: self._input_dir = os.getcwd() return self._input_dir @property def output_dir(self): """If the output directory isn't provided, use the input directory. """ if self._output_dir is None: self._output_dir = self.input_dir return self._output_dir @property def log_file(self): """Determine a name for the log file if one is not provided. """ if self._log_file is None: if self.list: self._log_file = const.DEFAULT_LISTING_LOG_FILE else: first_filename = os.path.join(self.input_dir, self.l_args[0]) # Find the path to this file if not os.path.isfile(first_filename): if self.from_format: test_filename = first_filename + f".{self.from_format}" if os.path.isfile(test_filename): first_filename = test_filename else: raise FileConverterInputException(f"Input file {first_filename} cannot be found. Also " f"checked for {test_filename}.", help=True) else: raise FileConverterInputException(f"Input file {first_filename} cannot be found.", help=True) filename_base = os.path.split(split_archive_ext(first_filename)[0])[1] if self.log_mode == const.LOG_FULL: # For server-style logging, other files will be created and used for logs self._log_file = None else: self._log_file = os.path.join(self.output_dir, filename_base + const.LOG_EXT) return self._log_file
Class storing arguments for data conversion, processed and determined from the input arguments.
Instance variables
prop input_dir
-
Expand source code
@property def input_dir(self): """If the input directory isn't provided, use the current directory. """ if self._input_dir is None: self._input_dir = os.getcwd() return self._input_dir
If the input directory isn't provided, use the current directory.
prop log_file
-
Expand source code
@property def log_file(self): """Determine a name for the log file if one is not provided. """ if self._log_file is None: if self.list: self._log_file = const.DEFAULT_LISTING_LOG_FILE else: first_filename = os.path.join(self.input_dir, self.l_args[0]) # Find the path to this file if not os.path.isfile(first_filename): if self.from_format: test_filename = first_filename + f".{self.from_format}" if os.path.isfile(test_filename): first_filename = test_filename else: raise FileConverterInputException(f"Input file {first_filename} cannot be found. Also " f"checked for {test_filename}.", help=True) else: raise FileConverterInputException(f"Input file {first_filename} cannot be found.", help=True) filename_base = os.path.split(split_archive_ext(first_filename)[0])[1] if self.log_mode == const.LOG_FULL: # For server-style logging, other files will be created and used for logs self._log_file = None else: self._log_file = os.path.join(self.output_dir, filename_base + const.LOG_EXT) return self._log_file
Determine a name for the log file if one is not provided.
prop output_dir
-
Expand source code
@property def output_dir(self): """If the output directory isn't provided, use the input directory. """ if self._output_dir is None: self._output_dir = self.input_dir return self._output_dir
If the output directory isn't provided, use the input directory.