diff --git a/pyproject.toml b/pyproject.toml index 52e6123b0..b374cf177 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ viztracer = "^0.16.0" [tool.poetry.scripts] -dsp-tools = "dsp_tools.cli:main" # definition of the CLI entry point +dsp-tools = "dsp_tools.cli.entry_point:main" # definition of the CLI entry point [tool.poetry-exec-plugin.commands] diff --git a/src/dsp_tools/cli.py b/src/dsp_tools/cli.py deleted file mode 100644 index 1e539677d..000000000 --- a/src/dsp_tools/cli.py +++ /dev/null @@ -1,629 +0,0 @@ -""" -The code in this file handles the arguments passed by the user from the command line and calls the requested actions. -""" -import argparse -import datetime -import subprocess -import sys -from importlib.metadata import version - -import regex - -from dsp_tools.commands.excel2json.lists import excel2lists, validate_lists_section_with_schema -from dsp_tools.commands.excel2json.project import excel2json -from dsp_tools.commands.excel2json.properties import excel2properties -from dsp_tools.commands.excel2json.resources import excel2resources -from dsp_tools.commands.excel2xml.excel2xml_cli import excel2xml -from dsp_tools.commands.fast_xmlupload.process_files import process_files -from dsp_tools.commands.fast_xmlupload.upload_files import upload_files -from dsp_tools.commands.fast_xmlupload.upload_xml import fast_xmlupload -from dsp_tools.commands.id2iri import id2iri -from dsp_tools.commands.project.create.project_create import create_project -from dsp_tools.commands.project.create.project_create_lists import create_lists -from dsp_tools.commands.project.create.project_validate import validate_project -from dsp_tools.commands.project.get import get_project -from dsp_tools.commands.rosetta import upload_rosetta -from dsp_tools.commands.start_stack import StackConfiguration, StackHandler -from dsp_tools.commands.template import generate_template_repo -from dsp_tools.commands.xmlupload.upload_config import DiagnosticsConfig, UploadConfig -from dsp_tools.commands.xmlupload.xmlupload import xmlupload -from dsp_tools.models.exceptions import BaseError, InternalError, UserError -from dsp_tools.utils.create_logger import get_logger -from dsp_tools.utils.shared import validate_xml_against_schema - -logger = get_logger(__name__) - - -def _make_parser( - default_dsp_api_url: str, - root_user_email: str, - root_user_pw: str, -) -> argparse.ArgumentParser: - """ - Create a parser for the command line arguments. - - Args: - default_dsp_api_url: URL of the DSP server (default value for localhost) - root_user_email: username (e-mail) used for authentication with the DSP-API (default value for localhost) - root_user_pw: password used for authentication with the DSP-API (default value for localhost) - - Returns: - parser - """ - # help texts - username_text = "username (e-mail) used for authentication with the DSP-API" - password_text = "password used for authentication with the DSP-API" - dsp_server_text = "URL of the DSP server" - verbose_text = "print more information about the progress to the console" - - # make a parser - parser = argparse.ArgumentParser( - description=f"DSP-TOOLS (version {version('dsp-tools')}, © {datetime.datetime.now().year} by DaSCH)" - ) - subparsers = parser.add_subparsers( - title="Subcommands", description="Valid subcommands are", help="sub-command help" - ) - - # create - parser_create = subparsers.add_parser( - name="create", - help="Create a project defined in a JSON project file on a DSP server. " - "A project can consist of lists, groups, users, and ontologies (data models).", - ) - parser_create.set_defaults(action="create") - parser_create.add_argument("-s", "--server", default=default_dsp_api_url, help=dsp_server_text) - parser_create.add_argument("-u", "--user", default=root_user_email, help=username_text) - parser_create.add_argument("-p", "--password", default=root_user_pw, help=password_text) - parser_create.add_argument( - "-V", - "--validate-only", - action="store_true", - help="validate the JSON file without creating it on the DSP server", - ) - parser_create.add_argument( - "-l", - "--lists-only", - action="store_true", - help="create only the lists (prerequisite: the project exists on the server)", - ) - parser_create.add_argument("-v", "--verbose", action="store_true", help=verbose_text) - parser_create.add_argument("-d", "--dump", action="store_true", help="write every request to DSP-API into a file") - parser_create.add_argument("project_definition", help="path to the JSON project file") - - # get - parser_get = subparsers.add_parser( - name="get", - help="Retrieve a project with its data model(s) from a DSP server and write it into a JSON file", - ) - parser_get.set_defaults(action="get") - parser_get.add_argument("-s", "--server", default=default_dsp_api_url, help=dsp_server_text) - parser_get.add_argument("-u", "--user", default=root_user_email, help=username_text) - parser_get.add_argument("-p", "--password", default=root_user_pw, help=password_text) - parser_get.add_argument("-P", "--project", help="shortcode, shortname or IRI of the project", required=True) - parser_get.add_argument("-v", "--verbose", action="store_true", help=verbose_text) - parser_get.add_argument("-d", "--dump", action="store_true", help="write every request to DSP-API into a file") - parser_get.add_argument("project_definition", help="path to the file the project should be written to") - - # xmlupload - parser_upload = subparsers.add_parser(name="xmlupload", help="Upload data defined in an XML file to a DSP server") - parser_upload.set_defaults(action="xmlupload") - parser_upload.add_argument( - "-s", "--server", default=default_dsp_api_url, help="URL of the DSP server where DSP-TOOLS sends the data to" - ) - parser_upload.add_argument("-u", "--user", default=root_user_email, help=username_text) - parser_upload.add_argument("-p", "--password", default=root_user_pw, help=password_text) - parser_upload.add_argument( - "-i", "--imgdir", default=".", help="folder from where the paths in the tags are evaluated" - ) - parser_upload.add_argument( - "-V", "--validate-only", action="store_true", help="validate the XML file without uploading it" - ) - parser_upload.add_argument("-v", "--verbose", action="store_true", help=verbose_text) - parser_upload.add_argument( - "-d", "--dump", action="store_true", help="write every request to DSP-API/SIPI into a file" - ) - parser_upload.add_argument("xmlfile", help="path to the XML file containing the data") - - # process-files - parser_process_files = subparsers.add_parser( - name="process-files", - help="For internal use only: process all files referenced in an XML file", - ) - parser_process_files.set_defaults(action="process-files") - parser_process_files.add_argument( - "--input-dir", help="path to the input directory where the files should be read from" - ) - parser_process_files.add_argument( - "--output-dir", help="path to the output directory where the processed/transformed files should be written to" - ) - parser_process_files.add_argument("--nthreads", type=int, default=None, help="number of threads to use") - parser_process_files.add_argument("xml_file", help="path to XML file containing the data") - - # upload-files - parser_upload_files = subparsers.add_parser( - name="upload-files", - help="For internal use only: upload already processed files", - ) - parser_upload_files.set_defaults(action="upload-files") - parser_upload_files.add_argument("-d", "--processed-dir", help="path to the directory with the processed files") - parser_upload_files.add_argument("-n", "--nthreads", type=int, default=4, help="number of threads to use") - parser_upload_files.add_argument("-s", "--server", default=default_dsp_api_url, help=dsp_server_text) - parser_upload_files.add_argument("-u", "--user", default=root_user_email, help=username_text) - parser_upload_files.add_argument("-p", "--password", default=root_user_pw, help=password_text) - - # fast-xmlupload - parser_fast_xmlupload = subparsers.add_parser( - name="fast-xmlupload", - help="For internal use only: create resources with already uploaded files", - ) - parser_fast_xmlupload.set_defaults(action="fast-xmlupload") - parser_fast_xmlupload.add_argument("-s", "--server", default=default_dsp_api_url, help=dsp_server_text) - parser_fast_xmlupload.add_argument("-u", "--user", default=root_user_email, help=username_text) - parser_fast_xmlupload.add_argument("-p", "--password", default=root_user_pw, help=password_text) - parser_fast_xmlupload.add_argument("xml_file", help="path to XML file containing the data") - - # excel2json - parser_excel2json = subparsers.add_parser( - name="excel2json", - help="Create an entire JSON project file from a folder containing the required Excel files", - ) - parser_excel2json.set_defaults(action="excel2json") - parser_excel2json.add_argument("excelfolder", help="path to the folder containing the Excel files") - parser_excel2json.add_argument("project_definition", help="path to the output JSON file") - - # excel2lists - parser_excel_lists = subparsers.add_parser( - name="excel2lists", - help="Create the 'lists' section of a JSON project file from one or multiple Excel files", - ) - parser_excel_lists.set_defaults(action="excel2lists") - parser_excel_lists.add_argument("-v", "--verbose", action="store_true", help=verbose_text) - parser_excel_lists.add_argument("excelfolder", help="path to the folder containing the Excel file(s)") - parser_excel_lists.add_argument("lists_section", help="path to the output JSON file containing the 'lists' section") - - # excel2resources - parser_excel_resources = subparsers.add_parser( - name="excel2resources", - help="Create the 'resources' section of a JSON project file from one or multiple Excel files", - ) - parser_excel_resources.set_defaults(action="excel2resources") - parser_excel_resources.add_argument("excelfile", help="path to the Excel file containing the resources") - parser_excel_resources.add_argument( - "resources_section", help="path to the output JSON file containing the 'resources' section" - ) - - # excel2properties - parser_excel_properties = subparsers.add_parser( - name="excel2properties", - help="Create the 'properties' section of a JSON project file from one or multiple Excel files", - ) - parser_excel_properties.set_defaults(action="excel2properties") - parser_excel_properties.add_argument("excelfile", help="path to the Excel file containing the properties") - parser_excel_properties.add_argument( - "properties_section", help="path to the output JSON file containing the 'properties' section" - ) - - # excel2xml - parser_excel2xml = subparsers.add_parser( - name="excel2xml", - help="Create an XML file from an Excel/CSV file that is already structured according to the DSP specifications", - ) - parser_excel2xml.set_defaults(action="excel2xml") - parser_excel2xml.add_argument("data_source", help="path to the CSV or XLS(X) file containing the data") - parser_excel2xml.add_argument("project_shortcode", help="shortcode of the project that this data belongs to") - parser_excel2xml.add_argument("ontology_name", help="name of the ontology that this data belongs to") - - # id2iri - parser_id2iri = subparsers.add_parser( - name="id2iri", - help="Replace internal IDs of an XML file (resptr tags or salsah-links) by IRIs provided in a mapping file.", - ) - parser_id2iri.set_defaults(action="id2iri") - parser_id2iri.add_argument( - "-r", "--remove-resources", action="store_true", help="remove resources if their ID is in the mapping" - ) - parser_id2iri.add_argument("xmlfile", help="path to the XML file containing the data to be replaced") - parser_id2iri.add_argument("mapping", help="path to the JSON file containing the mapping of IDs to IRIs") - - # startup DSP stack - parser_stackup = subparsers.add_parser(name="start-stack", help="Run a local instance of DSP-API and DSP-APP") - parser_stackup.set_defaults(action="start-stack") - parser_stackup.add_argument( - "--max_file_size", - type=int, - help="max. multimedia file size allowed by SIPI, in MB (default: 250, max: 100'000)", - ) - parser_stackup.add_argument("--prune", action="store_true", help="execute 'docker system prune' without asking") - parser_stackup.add_argument( - "--no-prune", action="store_true", help="don't execute 'docker system prune' (and don't ask)" - ) - parser_stackup.add_argument( - "--latest", - action="store_true", - help="use the latest dev version of DSP-API, from the main branch of the GitHub repository", - ) - - # shutdown DSP-API - parser_stackdown = subparsers.add_parser( - name="stop-stack", help="Shut down the local instance of DSP-API and DSP-APP, and delete all data in it" - ) - parser_stackdown.set_defaults(action="stop-stack") - - # create template repo with minimal JSON and XML files - parser_template = subparsers.add_parser( - name="template", help="Create a template repository with a minimal JSON and XML file" - ) - parser_template.set_defaults(action="template") - - # clone rosetta - parser_rosetta = subparsers.add_parser( - name="rosetta", help="Clone the most up to data rosetta repository, create the data model and upload the data" - ) - parser_rosetta.set_defaults(action="rosetta") - - return parser - - -def _parse_arguments( - user_args: list[str], - parser: argparse.ArgumentParser, -) -> argparse.Namespace: - """ - Parse the user-provided CLI arguments. - If no action is provided, - print the help text and exit with error code 1. - - Args: - user_args: user-provided CLI arguments - parser: parser used to parse the arguments - - Returns: - parsed arguments - """ - args = parser.parse_args(user_args) - if not hasattr(args, "action"): - parser.print_help(sys.stderr) - sys.exit(1) - return args - - -def _get_version() -> str: - result = subprocess.run("pip freeze | grep dsp-tools", check=False, shell=True, capture_output=True) - _detail_version = result.stdout.decode("utf-8") - # _detail_version has one of the following formats: - # - 'dsp-tools==5.0.3\n' - # - 'dsp-tools @ git+https://github.com/dasch-swiss/dsp-tools.git@1f95f8d1b79bd5170a652c0d04e7ada417d76734\n' - # - '-e git+ssh://git@github.com/dasch-swiss/dsp-tools.git@af9a35692b542676f2aa0a802ca7fc3b35f5713d#egg=dsp_tools\n' - # - '' - if version_number := regex.search(r"\d+\.\d+\.\d+", _detail_version): - return version_number.group(0) - if regex.search(r"github.com", _detail_version): - return _detail_version.replace("\n", "") - return version("dsp-tools") - - -def _log_cli_arguments(parsed_args: argparse.Namespace) -> None: - """ - Log the CLI arguments passed by the user from the command line. - - Args: - parsed_args: parsed arguments - """ - metadata_lines = [ - f"DSP-TOOLS: Called the action '{parsed_args.action}' from the command line", - f"DSP-TOOLS version: {_get_version()}", - f"Location of this installation: {__file__}", - "CLI arguments:", - ] - metadata_lines = [f"*** {line}" for line in metadata_lines] - - parameter_lines = [] - parameters_to_log = {key: value for key, value in vars(parsed_args).items() if key != "action"} - longest_key_length = max((len(key) for key in parameters_to_log), default=0) - for key, value in parameters_to_log.items(): - if key == "password": - parameter_lines.append(f"{key:<{longest_key_length}} = {'*' * len(value)}") - else: - parameter_lines.append(f"{key:<{longest_key_length}} = {value}") - parameter_lines = parameter_lines or ["(no parameters)"] - parameter_lines = [f"*** {line}" for line in parameter_lines] - - asterisk_count = max(len(line) for line in metadata_lines + parameter_lines) - logger.info("*" * asterisk_count) - for line in metadata_lines: - logger.info(line) - for line in parameter_lines: - logger.info(line) - logger.info("*" * asterisk_count) - - -def _get_canonical_server_and_sipi_url( - server: str, - default_dsp_api_url: str, - default_sipi_url: str, -) -> tuple[str, str]: - """ - Based on the DSP server URL passed by the user, - transform it to its canonical form, - and derive the SIPI URL from it. - - If the DSP server URL points to port 3333 on localhost, - the SIPI URL will point to port 1024 on localhost. - - If the DSP server URL points to a remote server ending in "dasch.swiss", - modify it (if necessary) to point to the "api" subdomain of that server, - and add a new "sipi_url" argument pointing to the "iiif" subdomain of that server. - - Args: - server: DSP server URL passed by the user - default_dsp_api_url: default DSP server on localhost - default_sipi_url: default SIPI server on localhost - - Raises: - UserError: if the DSP server URL passed by the user is invalid - - Returns: - canonical DSP URL and SIPI URL - """ - localhost_match = regex.search(r"(0\.0\.0\.0|localhost):3333", server) - remote_url_match = regex.search(r"^(?:https?:\/\/)?(?:admin\.|api\.|iiif\.|app\.)?((?:.+\.)?dasch)\.swiss", server) - - if localhost_match: - server = default_dsp_api_url - sipi_url = default_sipi_url - elif remote_url_match: - server = f"https://api.{remote_url_match.group(1)}.swiss" - sipi_url = f"https://iiif.{remote_url_match.group(1)}.swiss" - else: - logger.error(f"Invalid DSP server URL '{server}'") - raise UserError(f"ERROR: Invalid DSP server URL '{server}'") - - logger.info(f"Using DSP server '{server}' and SIPI server '{sipi_url}'") - print(f"Using DSP server '{server}' and SIPI server '{sipi_url}'") - - return server, sipi_url - - -def _derive_sipi_url( - parsed_arguments: argparse.Namespace, - default_dsp_api_url: str, - default_sipi_url: str, -) -> argparse.Namespace: - """ - Modify the parsed arguments so that the DSP and SIPI URLs are correct. - Based on the DSP server URL passed by the user, - transform it to its canonical form, - and derive the SIPI URL from it. - - Args: - parsed_arguments: CLI arguments passed by the user, parsed by argparse - default_dsp_api_url: default DSP server on localhost - default_sipi_url: default SIPI server on localhost - - Raises: - UserError: if the DSP server URL passed by the user is invalid - - Returns: - the modified arguments - """ - if not hasattr(parsed_arguments, "server"): - # some CLI actions (like excel2json, excel2xml, start-stack, ...) don't have a server at all - return parsed_arguments - - server, sipi_url = _get_canonical_server_and_sipi_url( - server=parsed_arguments.server, - default_dsp_api_url=default_dsp_api_url, - default_sipi_url=default_sipi_url, - ) - parsed_arguments.server = server - parsed_arguments.sipi_url = sipi_url - - return parsed_arguments - - -def _call_requested_action(args: argparse.Namespace) -> bool: - """ - Call the appropriate method of DSP-TOOLS. - - Args: - args: parsed CLI arguments - - Raises: - BaseError from the called methods - UserError from the called methods - unexpected errors from the called methods and underlying libraries - - Returns: - success status - """ - if args.action == "create": - if args.lists_only: - if args.validate_only: - success = validate_lists_section_with_schema(args.project_definition) - print("'Lists' section of the JSON project file is syntactically correct and passed validation.") - else: - _, success = create_lists( - project_file_as_path_or_parsed=args.project_definition, - server=args.server, - user=args.user, - password=args.password, - dump=args.dump, - ) - else: - if args.validate_only: - success = validate_project(args.project_definition) - print("JSON project file is syntactically correct and passed validation.") - else: - success = create_project( - project_file_as_path_or_parsed=args.project_definition, - server=args.server, - user_mail=args.user, - password=args.password, - verbose=args.verbose, - dump=args.dump, - ) - elif args.action == "get": - success = get_project( - project_identifier=args.project, - outfile_path=args.project_definition, - server=args.server, - user=args.user, - password=args.password, - verbose=args.verbose, - dump=args.dump, - ) - elif args.action == "xmlupload": - if args.validate_only: - success = validate_xml_against_schema(args.xmlfile) - else: - success = xmlupload( - input_file=args.xmlfile, - server=args.server, - user=args.user, - password=args.password, - imgdir=args.imgdir, - sipi=args.sipi_url, - config=UploadConfig(diagnostics=DiagnosticsConfig(verbose=args.verbose, dump=args.dump)), - ) - elif args.action == "process-files": - success = process_files( - input_dir=args.input_dir, - output_dir=args.output_dir, - xml_file=args.xml_file, - nthreads=args.nthreads, - ) - elif args.action == "upload-files": - success = upload_files( - dir_with_processed_files=args.processed_dir, - nthreads=args.nthreads, - user=args.user, - password=args.password, - dsp_url=args.server, - sipi_url=args.sipi_url, - ) - elif args.action == "fast-xmlupload": - success = fast_xmlupload( - xml_file=args.xml_file, - user=args.user, - password=args.password, - dsp_url=args.server, - sipi_url=args.sipi_url, - ) - elif args.action == "excel2json": - success = excel2json( - data_model_files=args.excelfolder, - path_to_output_file=args.project_definition, - ) - elif args.action == "excel2lists": - _, success = excel2lists( - excelfolder=args.excelfolder, - path_to_output_file=args.lists_section, - verbose=args.verbose, - ) - elif args.action == "excel2resources": - _, success = excel2resources( - excelfile=args.excelfile, - path_to_output_file=args.resources_section, - ) - elif args.action == "excel2properties": - _, success = excel2properties( - excelfile=args.excelfile, - path_to_output_file=args.properties_section, - ) - elif args.action == "id2iri": - success = id2iri( - xml_file=args.xmlfile, - json_file=args.mapping, - remove_resource_if_id_in_mapping=args.remove_resources, - ) - elif args.action == "excel2xml": - success, _ = excel2xml( - datafile=args.data_source, - shortcode=args.project_shortcode, - default_ontology=args.ontology_name, - ) - elif args.action == "start-stack": - stack_handler = StackHandler( - StackConfiguration( - max_file_size=args.max_file_size, - enforce_docker_system_prune=args.prune, - suppress_docker_system_prune=args.no_prune, - latest_dev_version=args.latest, - ) - ) - success = stack_handler.start_stack() - elif args.action == "stop-stack": - stack_handler = StackHandler(StackConfiguration()) - success = stack_handler.stop_stack() - elif args.action == "template": - success = generate_template_repo() - elif args.action == "rosetta": - success = upload_rosetta() - else: - success = False - print(f"ERROR: Unknown action '{args.action}'") - logger.error(f"Unknown action '{args.action}'") - - return success - - -def main() -> None: - """ - Main entry point of the program as referenced in pyproject.toml - """ - run(sys.argv[1:]) - - -def run(args: list[str]) -> None: - """ - Main function of the CLI. - - Args: - args: a list of arguments passed by the user from the command line, - excluding the leading "dsp-tools" command. - - Raises: - UserError: if user input was wrong - InputError: if user input was wrong - InternalError: if the user cannot fix it - RetryError: if the problem may disappear when trying again later - """ - default_dsp_api_url = "http://0.0.0.0:3333" - default_sipi_url = "http://0.0.0.0:1024" - root_user_email = "root@example.com" - root_user_pw = "test" - - parser = _make_parser( - default_dsp_api_url=default_dsp_api_url, - root_user_email=root_user_email, - root_user_pw=root_user_pw, - ) - parsed_arguments = _parse_arguments( - user_args=args, - parser=parser, - ) - _log_cli_arguments(parsed_arguments) - - try: - parsed_arguments = _derive_sipi_url( - parsed_arguments=parsed_arguments, - default_dsp_api_url=default_dsp_api_url, - default_sipi_url=default_sipi_url, - ) - success = _call_requested_action(parsed_arguments) - except BaseError as err: - logger.error(err) - print("\nThe process was terminated because of an Error:") - print(err.message) - sys.exit(1) - except Exception as err: - logger.error(err) - raise InternalError from None - - if not success: - logger.error("Terminate without success") - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/src/dsp_tools/cli/__init__.py b/src/dsp_tools/cli/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/dsp_tools/cli/call_action.py b/src/dsp_tools/cli/call_action.py new file mode 100644 index 000000000..f741f3a2d --- /dev/null +++ b/src/dsp_tools/cli/call_action.py @@ -0,0 +1,230 @@ +import argparse + +from dsp_tools.commands.excel2json.lists import excel2lists, validate_lists_section_with_schema +from dsp_tools.commands.excel2json.project import excel2json +from dsp_tools.commands.excel2json.properties import excel2properties +from dsp_tools.commands.excel2json.resources import excel2resources +from dsp_tools.commands.excel2xml.excel2xml_cli import excel2xml +from dsp_tools.commands.fast_xmlupload.process_files import process_files +from dsp_tools.commands.fast_xmlupload.upload_files import upload_files +from dsp_tools.commands.fast_xmlupload.upload_xml import fast_xmlupload +from dsp_tools.commands.id2iri import id2iri +from dsp_tools.commands.project.create.project_create import create_project +from dsp_tools.commands.project.create.project_create_lists import create_lists +from dsp_tools.commands.project.create.project_validate import validate_project +from dsp_tools.commands.project.get import get_project +from dsp_tools.commands.rosetta import upload_rosetta +from dsp_tools.commands.start_stack import StackConfiguration, StackHandler +from dsp_tools.commands.template import generate_template_repo +from dsp_tools.commands.xmlupload.upload_config import DiagnosticsConfig, UploadConfig +from dsp_tools.commands.xmlupload.xmlupload import xmlupload +from dsp_tools.utils.create_logger import get_logger +from dsp_tools.utils.shared import validate_xml_against_schema + +logger = get_logger(__name__) + + +def call_requested_action(args: argparse.Namespace) -> bool: + """ + Call the appropriate method of DSP-TOOLS. + + Args: + args: parsed CLI arguments + + Raises: + BaseError from the called methods + UserError from the called methods + unexpected errors from the called methods and underlying libraries + + Returns: + success status + """ + match args.action: + case "create": + return _call_create(args) + case "xmlupload": + return _call_xmlupload(args) + case "excel2json": + return _call_excel2json(args) + case "excel2lists": + return _call_excel2lists(args) + case "excel2resources": + return _call_excel2resources(args) + case "excel2properties": + return _call_excel2properties(args) + case "id2iri": + return _call_id2iri(args) + case "excel2xml": + return _call_excel2xml(args) + case "start-stack": + return _call_start_stack(args) + case "stop-stack": + return _call_stop_stack() + case "get": + return _call_get(args) + case "process-files": + return _call_process_files(args) + case "upload-files": + return _call_upload_files(args) + case "fast-xmlupload": + return _call_fast_xmlupload(args) + case "template": + return generate_template_repo() + case "rosetta": + return upload_rosetta() + case _: + print(f"ERROR: Unknown action '{args.action}'") + logger.error(f"Unknown action '{args.action}'") + return False + + +def _call_stop_stack() -> bool: + stack_handler = StackHandler(StackConfiguration()) + return stack_handler.stop_stack() + + +def _call_start_stack(args: argparse.Namespace) -> bool: + stack_handler = StackHandler( + StackConfiguration( + max_file_size=args.max_file_size, + enforce_docker_system_prune=args.prune, + suppress_docker_system_prune=args.no_prune, + latest_dev_version=args.latest, + ) + ) + return stack_handler.start_stack() + + +def _call_excel2xml(args: argparse.Namespace) -> bool: + success, _ = excel2xml( + datafile=args.data_source, + shortcode=args.project_shortcode, + default_ontology=args.ontology_name, + ) + return success + + +def _call_id2iri(args: argparse.Namespace) -> bool: + return id2iri( + xml_file=args.xmlfile, + json_file=args.mapping, + remove_resource_if_id_in_mapping=args.remove_resources, + ) + + +def _call_excel2properties(args: argparse.Namespace) -> bool: + _, success = excel2properties( + excelfile=args.excelfile, + path_to_output_file=args.properties_section, + ) + return success + + +def _call_excel2resources(args: argparse.Namespace) -> bool: + _, success = excel2resources( + excelfile=args.excelfile, + path_to_output_file=args.resources_section, + ) + return success + + +def _call_excel2lists(args: argparse.Namespace) -> bool: + _, success = excel2lists( + excelfolder=args.excelfolder, + path_to_output_file=args.lists_section, + verbose=args.verbose, + ) + return success + + +def _call_excel2json(args: argparse.Namespace) -> bool: + return excel2json( + data_model_files=args.excelfolder, + path_to_output_file=args.project_definition, + ) + + +def _call_fast_xmlupload(args: argparse.Namespace) -> bool: + return fast_xmlupload( + xml_file=args.xml_file, + user=args.user, + password=args.password, + dsp_url=args.server, + sipi_url=args.sipi_url, + ) + + +def _call_upload_files(args: argparse.Namespace) -> bool: + return upload_files( + dir_with_processed_files=args.processed_dir, + nthreads=args.nthreads, + user=args.user, + password=args.password, + dsp_url=args.server, + sipi_url=args.sipi_url, + ) + + +def _call_process_files(args: argparse.Namespace) -> bool: + return process_files( + input_dir=args.input_dir, + output_dir=args.output_dir, + xml_file=args.xml_file, + nthreads=args.nthreads, + ) + + +def _call_xmlupload(args: argparse.Namespace) -> bool: + if args.validate_only: + return validate_xml_against_schema(args.xmlfile) + else: + return xmlupload( + input_file=args.xmlfile, + server=args.server, + user=args.user, + password=args.password, + imgdir=args.imgdir, + sipi=args.sipi_url, + config=UploadConfig(diagnostics=DiagnosticsConfig(verbose=args.verbose, dump=args.dump)), + ) + + +def _call_get(args: argparse.Namespace) -> bool: + return get_project( + project_identifier=args.project, + outfile_path=args.project_definition, + server=args.server, + user=args.user, + password=args.password, + verbose=args.verbose, + dump=args.dump, + ) + + +def _call_create(args: argparse.Namespace) -> bool: + success = False + match args.lists_only, args.validate_only: + case True, True: + success = validate_lists_section_with_schema(args.project_definition) + print("'Lists' section of the JSON project file is syntactically correct and passed validation.") + case True, False: + _, success = create_lists( + project_file_as_path_or_parsed=args.project_definition, + server=args.server, + user=args.user, + password=args.password, + dump=args.dump, + ) + case False, True: + success = validate_project(args.project_definition) + print("JSON project file is syntactically correct and passed validation.") + case False, False: + success = create_project( + project_file_as_path_or_parsed=args.project_definition, + server=args.server, + user_mail=args.user, + password=args.password, + verbose=args.verbose, + dump=args.dump, + ) + return success diff --git a/src/dsp_tools/cli/create_parsers.py b/src/dsp_tools/cli/create_parsers.py new file mode 100644 index 000000000..29ef0c624 --- /dev/null +++ b/src/dsp_tools/cli/create_parsers.py @@ -0,0 +1,303 @@ +from __future__ import annotations + +import datetime +from argparse import ArgumentParser, _SubParsersAction +from importlib.metadata import version + +# help texts +username_text = "username (e-mail) used for authentication with the DSP-API" +password_text = "password used for authentication with the DSP-API" +dsp_server_text = "URL of the DSP server" +verbose_text = "print more information about the progress to the console" + + +def make_parser( + default_dsp_api_url: str, + root_user_email: str, + root_user_pw: str, +) -> ArgumentParser: + """ + Create a parser for the command line arguments. + + Args: + default_dsp_api_url: URL of the DSP server (default value for localhost) + root_user_email: username (e-mail) used for authentication with the DSP-API (default value for localhost) + root_user_pw: password used for authentication with the DSP-API (default value for localhost) + + Returns: + parser + """ + + parser = ArgumentParser( + description=f"DSP-TOOLS (version {version('dsp-tools')}, © {datetime.datetime.now().year} by DaSCH)" + ) + subparsers = parser.add_subparsers( + title="Subcommands", description="Valid subcommands are", help="sub-command help" + ) + + _add_start_stack(subparsers) + + _add_stop_stack(subparsers) + + _add_create(subparsers, default_dsp_api_url, root_user_email, root_user_pw) + + _add_get(subparsers, default_dsp_api_url, root_user_email, root_user_pw) + + _add_xmlupload(subparsers, default_dsp_api_url, root_user_email, root_user_pw) + + _add_fast_xmlupload(subparsers, default_dsp_api_url, root_user_email, root_user_pw) + + _add_process_files(subparsers) + + _add_upload_files(subparsers, default_dsp_api_url, root_user_email, root_user_pw) + + _add_excel2json(subparsers) + + _add_excel2lists(subparsers) + + _add_excel2resources(subparsers) + + _add_excel2properties(subparsers) + + _add_excel2xml(subparsers) + + _add_id2iri(subparsers) + + _add_create_template(subparsers) + + _add_rosetta(subparsers) + + return parser + + +def _add_rosetta(subparsers: _SubParsersAction[ArgumentParser]) -> None: + subparser = subparsers.add_parser( + name="rosetta", help="Clone the most up to data rosetta repository, create the data model and upload the data" + ) + subparser.set_defaults(action="rosetta") + + +def _add_create_template(subparsers: _SubParsersAction[ArgumentParser]) -> None: + # create template repo with minimal JSON and XML files + subparser = subparsers.add_parser( + name="template", help="Create a template repository with a minimal JSON and XML file" + ) + subparser.set_defaults(action="template") + + +def _add_stop_stack(subparsers: _SubParsersAction[ArgumentParser]) -> None: + subparser = subparsers.add_parser( + name="stop-stack", help="Shut down the local instance of DSP-API and DSP-APP, and delete all data in it" + ) + subparser.set_defaults(action="stop-stack") + + +def _add_start_stack(subparsers: _SubParsersAction[ArgumentParser]) -> None: + subparser = subparsers.add_parser(name="start-stack", help="Run a local instance of DSP-API and DSP-APP") + subparser.set_defaults(action="start-stack") + subparser.add_argument( + "--max_file_size", + type=int, + help="max. multimedia file size allowed by SIPI, in MB (default: 250, max: 100'000)", + ) + subparser.add_argument("--prune", action="store_true", help="execute 'docker system prune' without asking") + subparser.add_argument( + "--no-prune", action="store_true", help="don't execute 'docker system prune' (and don't ask)" + ) + subparser.add_argument( + "--latest", + action="store_true", + help="use the latest dev version of DSP-API, from the main branch of the GitHub repository", + ) + + +def _add_id2iri(subparsers: _SubParsersAction[ArgumentParser]) -> None: + subparser = subparsers.add_parser( + name="id2iri", + help="Replace internal IDs of an XML file (resptr tags or salsah-links) by IRIs provided in a mapping file.", + ) + subparser.set_defaults(action="id2iri") + subparser.add_argument( + "-r", "--remove-resources", action="store_true", help="remove resources if their ID is in the mapping" + ) + subparser.add_argument("xmlfile", help="path to the XML file containing the data to be replaced") + subparser.add_argument("mapping", help="path to the JSON file containing the mapping of IDs to IRIs") + + +def _add_excel2xml(subparsers: _SubParsersAction[ArgumentParser]) -> None: + subparser = subparsers.add_parser( + name="excel2xml", + help="Create an XML file from an Excel/CSV file that is already structured according to the DSP specifications", + ) + subparser.set_defaults(action="excel2xml") + subparser.add_argument("data_source", help="path to the CSV or XLS(X) file containing the data") + subparser.add_argument("project_shortcode", help="shortcode of the project that this data belongs to") + subparser.add_argument("ontology_name", help="name of the ontology that this data belongs to") + + +def _add_excel2properties(subparsers: _SubParsersAction[ArgumentParser]) -> None: + subparser = subparsers.add_parser( + name="excel2properties", + help="Create the 'properties' section of a JSON project file from one or multiple Excel files", + ) + subparser.set_defaults(action="excel2properties") + subparser.add_argument("excelfile", help="path to the Excel file containing the properties") + subparser.add_argument( + "properties_section", help="path to the output JSON file containing the 'properties' section" + ) + + +def _add_excel2resources(subparsers: _SubParsersAction[ArgumentParser]) -> None: + subparser = subparsers.add_parser( + name="excel2resources", + help="Create the 'resources' section of a JSON project file from one or multiple Excel files", + ) + subparser.set_defaults(action="excel2resources") + subparser.add_argument("excelfile", help="path to the Excel file containing the resources") + subparser.add_argument("resources_section", help="path to the output JSON file containing the 'resources' section") + + +def _add_excel2lists(subparsers: _SubParsersAction[ArgumentParser]) -> None: + subparser = subparsers.add_parser( + name="excel2lists", + help="Create the 'lists' section of a JSON project file from one or multiple Excel files", + ) + subparser.set_defaults(action="excel2lists") + subparser.add_argument("-v", "--verbose", action="store_true", help=verbose_text) + subparser.add_argument("excelfolder", help="path to the folder containing the Excel file(s)") + subparser.add_argument("lists_section", help="path to the output JSON file containing the 'lists' section") + + +def _add_excel2json(subparsers: _SubParsersAction[ArgumentParser]) -> None: + subparser = subparsers.add_parser( + name="excel2json", + help="Create an entire JSON project file from a folder containing the required Excel files", + ) + subparser.set_defaults(action="excel2json") + subparser.add_argument("excelfolder", help="path to the folder containing the Excel files") + subparser.add_argument("project_definition", help="path to the output JSON file") + + +def _add_fast_xmlupload( + subparsers: _SubParsersAction[ArgumentParser], + default_dsp_api_url: str, + root_user_email: str, + root_user_pw: str, +) -> None: + subparser = subparsers.add_parser( + name="fast-xmlupload", + help="For internal use only: create resources with already uploaded files", + ) + subparser.set_defaults(action="fast-xmlupload") + subparser.add_argument("-s", "--server", default=default_dsp_api_url, help=dsp_server_text) + subparser.add_argument("-u", "--user", default=root_user_email, help=username_text) + subparser.add_argument("-p", "--password", default=root_user_pw, help=password_text) + subparser.add_argument("xml_file", help="path to XML file containing the data") + + +def _add_upload_files( + subparsers: _SubParsersAction[ArgumentParser], + default_dsp_api_url: str, + root_user_email: str, + root_user_pw: str, +) -> None: + subparser = subparsers.add_parser( + name="upload-files", + help="For internal use only: upload already processed files", + ) + subparser.set_defaults(action="upload-files") + subparser.add_argument("-d", "--processed-dir", help="path to the directory with the processed files") + subparser.add_argument("-n", "--nthreads", type=int, default=4, help="number of threads to use") + subparser.add_argument("-s", "--server", default=default_dsp_api_url, help=dsp_server_text) + subparser.add_argument("-u", "--user", default=root_user_email, help=username_text) + subparser.add_argument("-p", "--password", default=root_user_pw, help=password_text) + + +def _add_process_files(subparsers: _SubParsersAction[ArgumentParser]) -> None: + subparser = subparsers.add_parser( + name="process-files", + help="For internal use only: process all files referenced in an XML file", + ) + subparser.set_defaults(action="process-files") + subparser.add_argument("--input-dir", help="path to the input directory where the files should be read from") + subparser.add_argument( + "--output-dir", help="path to the output directory where the processed/transformed files should be written to" + ) + subparser.add_argument("--nthreads", type=int, default=None, help="number of threads to use") + subparser.add_argument("xml_file", help="path to XML file containing the data") + + +def _add_xmlupload( + subparsers: _SubParsersAction[ArgumentParser], + default_dsp_api_url: str, + root_user_email: str, + root_user_pw: str, +) -> None: + subparser = subparsers.add_parser(name="xmlupload", help="Upload data defined in an XML file to a DSP server") + subparser.set_defaults(action="xmlupload") + subparser.add_argument( + "-s", "--server", default=default_dsp_api_url, help="URL of the DSP server where DSP-TOOLS sends the data to" + ) + subparser.add_argument("-u", "--user", default=root_user_email, help=username_text) + subparser.add_argument("-p", "--password", default=root_user_pw, help=password_text) + subparser.add_argument( + "-i", "--imgdir", default=".", help="folder from where the paths in the tags are evaluated" + ) + subparser.add_argument( + "-V", "--validate-only", action="store_true", help="validate the XML file without uploading it" + ) + subparser.add_argument("-v", "--verbose", action="store_true", help=verbose_text) + subparser.add_argument("-d", "--dump", action="store_true", help="write every request to DSP-API/SIPI into a file") + subparser.add_argument("xmlfile", help="path to the XML file containing the data") + + +def _add_get( + subparsers: _SubParsersAction[ArgumentParser], + default_dsp_api_url: str, + root_user_email: str, + root_user_pw: str, +) -> None: + subparser = subparsers.add_parser( + name="get", + help="Retrieve a project with its data model(s) from a DSP server and write it into a JSON file", + ) + subparser.set_defaults(action="get") + subparser.add_argument("-s", "--server", default=default_dsp_api_url, help=dsp_server_text) + subparser.add_argument("-u", "--user", default=root_user_email, help=username_text) + subparser.add_argument("-p", "--password", default=root_user_pw, help=password_text) + subparser.add_argument("-P", "--project", help="shortcode, shortname or IRI of the project", required=True) + subparser.add_argument("-v", "--verbose", action="store_true", help=verbose_text) + subparser.add_argument("-d", "--dump", action="store_true", help="write every request to DSP-API into a file") + subparser.add_argument("project_definition", help="path to the file the project should be written to") + + +def _add_create( + subparsers: _SubParsersAction[ArgumentParser], + default_dsp_api_url: str, + root_user_email: str, + root_user_pw: str, +) -> None: + subparser = subparsers.add_parser( + name="create", + help="Create a project defined in a JSON project file on a DSP server. " + "A project can consist of lists, groups, users, and ontologies (data models).", + ) + subparser.set_defaults(action="create") + subparser.add_argument("-s", "--server", default=default_dsp_api_url, help=dsp_server_text) + subparser.add_argument("-u", "--user", default=root_user_email, help=username_text) + subparser.add_argument("-p", "--password", default=root_user_pw, help=password_text) + subparser.add_argument( + "-V", + "--validate-only", + action="store_true", + help="validate the JSON file without creating it on the DSP server", + ) + subparser.add_argument( + "-l", + "--lists-only", + action="store_true", + help="create only the lists (prerequisite: the project exists on the server)", + ) + subparser.add_argument("-v", "--verbose", action="store_true", help=verbose_text) + subparser.add_argument("-d", "--dump", action="store_true", help="write every request to DSP-API into a file") + subparser.add_argument("project_definition", help="path to the JSON project file") diff --git a/src/dsp_tools/cli/entry_point.py b/src/dsp_tools/cli/entry_point.py new file mode 100644 index 000000000..ad4781ec4 --- /dev/null +++ b/src/dsp_tools/cli/entry_point.py @@ -0,0 +1,235 @@ +""" +The code in this file handles the arguments passed by the user from the command line and calls the requested actions. +""" +import argparse +import subprocess +import sys +from importlib.metadata import version + +import regex + +from dsp_tools.cli.call_action import call_requested_action +from dsp_tools.cli.create_parsers import make_parser +from dsp_tools.models.exceptions import BaseError, InternalError, UserError +from dsp_tools.utils.create_logger import get_logger + +logger = get_logger(__name__) + + +def main() -> None: + """ + Main entry point of the program as referenced in pyproject.toml + """ + run(sys.argv[1:]) + + +def run(args: list[str]) -> None: + """ + Main function of the CLI. + + Args: + args: a list of arguments passed by the user from the command line, + excluding the leading "dsp-tools" command. + + Raises: + UserError: if user input was wrong + InputError: if user input was wrong + InternalError: if the user cannot fix it + RetryError: if the problem may disappear when trying again later + """ + default_dsp_api_url = "http://0.0.0.0:3333" + default_sipi_url = "http://0.0.0.0:1024" + root_user_email = "root@example.com" + root_user_pw = "test" + + parser = make_parser( + default_dsp_api_url=default_dsp_api_url, + root_user_email=root_user_email, + root_user_pw=root_user_pw, + ) + parsed_arguments = _parse_arguments( + user_args=args, + parser=parser, + ) + _log_cli_arguments(parsed_arguments) + + try: + parsed_arguments = _derive_sipi_url( + parsed_arguments=parsed_arguments, + default_dsp_api_url=default_dsp_api_url, + default_sipi_url=default_sipi_url, + ) + success = call_requested_action(parsed_arguments) + except BaseError as err: + logger.error(err) + print("\nThe process was terminated because of an Error:") + print(err.message) + sys.exit(1) + except Exception as err: + logger.error(err) + raise InternalError from None + + if not success: + logger.error("Terminate without success") + sys.exit(1) + + +def _parse_arguments( + user_args: list[str], + parser: argparse.ArgumentParser, +) -> argparse.Namespace: + """ + Parse the user-provided CLI arguments. + If no action is provided, + print the help text and exit with error code 1. + + Args: + user_args: user-provided CLI arguments + parser: parser used to parse the arguments + + Returns: + parsed arguments + """ + args = parser.parse_args(user_args) + if not hasattr(args, "action"): + parser.print_help(sys.stderr) + sys.exit(1) + return args + + +def _get_version() -> str: + result = subprocess.run("pip freeze | grep dsp-tools", check=False, shell=True, capture_output=True) + _detail_version = result.stdout.decode("utf-8") + # _detail_version has one of the following formats: + # - 'dsp-tools==5.0.3\n' + # - 'dsp-tools @ git+https://github.com/dasch-swiss/dsp-tools.git@1f95f8d1b79bd5170a652c0d04e7ada417d76734\n' + # - '-e git+ssh://git@github.com/dasch-swiss/dsp-tools.git@af9a35692b542676f2aa0a802ca7fc3b35f5713d#egg=dsp_tools\n' + # - '' + if version_number := regex.search(r"\d+\.\d+\.\d+", _detail_version): + return version_number.group(0) + if regex.search(r"github.com", _detail_version): + return _detail_version.replace("\n", "") + return version("dsp-tools") + + +def _log_cli_arguments(parsed_args: argparse.Namespace) -> None: + """ + Log the CLI arguments passed by the user from the command line. + + Args: + parsed_args: parsed arguments + """ + metadata_lines = [ + f"DSP-TOOLS: Called the action '{parsed_args.action}' from the command line", + f"DSP-TOOLS version: {_get_version()}", + f"Location of this installation: {__file__}", + "CLI arguments:", + ] + metadata_lines = [f"*** {line}" for line in metadata_lines] + + parameter_lines = [] + parameters_to_log = {key: value for key, value in vars(parsed_args).items() if key != "action"} + longest_key_length = max((len(key) for key in parameters_to_log), default=0) + for key, value in parameters_to_log.items(): + if key == "password": + parameter_lines.append(f"{key:<{longest_key_length}} = {'*' * len(value)}") + else: + parameter_lines.append(f"{key:<{longest_key_length}} = {value}") + parameter_lines = parameter_lines or ["(no parameters)"] + parameter_lines = [f"*** {line}" for line in parameter_lines] + + asterisk_count = max(len(line) for line in metadata_lines + parameter_lines) + logger.info("*" * asterisk_count) + for line in metadata_lines: + logger.info(line) + for line in parameter_lines: + logger.info(line) + logger.info("*" * asterisk_count) + + +def _get_canonical_server_and_sipi_url( + server: str, + default_dsp_api_url: str, + default_sipi_url: str, +) -> tuple[str, str]: + """ + Based on the DSP server URL passed by the user, + transform it to its canonical form, + and derive the SIPI URL from it. + + If the DSP server URL points to port 3333 on localhost, + the SIPI URL will point to port 1024 on localhost. + + If the DSP server URL points to a remote server ending in "dasch.swiss", + modify it (if necessary) to point to the "api" subdomain of that server, + and add a new "sipi_url" argument pointing to the "iiif" subdomain of that server. + + Args: + server: DSP server URL passed by the user + default_dsp_api_url: default DSP server on localhost + default_sipi_url: default SIPI server on localhost + + Raises: + UserError: if the DSP server URL passed by the user is invalid + + Returns: + canonical DSP URL and SIPI URL + """ + localhost_match = regex.search(r"(0\.0\.0\.0|localhost):3333", server) + remote_url_match = regex.search(r"^(?:https?:\/\/)?(?:admin\.|api\.|iiif\.|app\.)?((?:.+\.)?dasch)\.swiss", server) + + if localhost_match: + server = default_dsp_api_url + sipi_url = default_sipi_url + elif remote_url_match: + server = f"https://api.{remote_url_match.group(1)}.swiss" + sipi_url = f"https://iiif.{remote_url_match.group(1)}.swiss" + else: + logger.error(f"Invalid DSP server URL '{server}'") + raise UserError(f"ERROR: Invalid DSP server URL '{server}'") + + logger.info(f"Using DSP server '{server}' and SIPI server '{sipi_url}'") + print(f"Using DSP server '{server}' and SIPI server '{sipi_url}'") + + return server, sipi_url + + +def _derive_sipi_url( + parsed_arguments: argparse.Namespace, + default_dsp_api_url: str, + default_sipi_url: str, +) -> argparse.Namespace: + """ + Modify the parsed arguments so that the DSP and SIPI URLs are correct. + Based on the DSP server URL passed by the user, + transform it to its canonical form, + and derive the SIPI URL from it. + + Args: + parsed_arguments: CLI arguments passed by the user, parsed by argparse + default_dsp_api_url: default DSP server on localhost + default_sipi_url: default SIPI server on localhost + + Raises: + UserError: if the DSP server URL passed by the user is invalid + + Returns: + the modified arguments + """ + if not hasattr(parsed_arguments, "server"): + # some CLI actions (like excel2json, excel2xml, start-stack, ...) don't have a server at all + return parsed_arguments + + server, sipi_url = _get_canonical_server_and_sipi_url( + server=parsed_arguments.server, + default_dsp_api_url=default_dsp_api_url, + default_sipi_url=default_sipi_url, + ) + parsed_arguments.server = server + parsed_arguments.sipi_url = sipi_url + + return parsed_arguments + + +if __name__ == "__main__": + main() diff --git a/test/unittests/test_cli.py b/test/unittests/test_cli.py index 3a3c2fc96..f4be68ac0 100644 --- a/test/unittests/test_cli.py +++ b/test/unittests/test_cli.py @@ -5,7 +5,7 @@ import pytest -from dsp_tools.cli import _derive_sipi_url, _get_canonical_server_and_sipi_url +from dsp_tools.cli.entry_point import _derive_sipi_url, _get_canonical_server_and_sipi_url from dsp_tools.models.exceptions import UserError diff --git a/test/unittests/test_cli_with_mock.py b/test/unittests/test_cli_with_mock.py index 521cffa24..ab2389f22 100644 --- a/test/unittests/test_cli_with_mock.py +++ b/test/unittests/test_cli_with_mock.py @@ -2,7 +2,7 @@ import pytest -from dsp_tools import cli +from dsp_tools.cli import entry_point from dsp_tools.commands.xmlupload.upload_config import UploadConfig @@ -10,26 +10,26 @@ def test_invalid_arguments() -> None: """Test the 'dsp-tools' command with invalid arguments""" args = "invalid".split() with pytest.raises(SystemExit) as ex: - cli.run(args) + entry_point.run(args) assert ex.value.code == 2 -@patch("dsp_tools.cli.validate_lists_section_with_schema") +@patch("dsp_tools.cli.call_action.validate_lists_section_with_schema") def test_lists_validate(validate_lists: Mock) -> None: """Test the 'dsp-tools create --lists-only --validate-only' command""" file = "filename.json" args = f"create --lists-only --validate-only {file}".split() - cli.run(args) + entry_point.run(args) validate_lists.assert_called_once_with(file) -@patch("dsp_tools.cli.create_lists") +@patch("dsp_tools.cli.call_action.create_lists") def test_lists_create(create_lists: Mock) -> None: """Test the 'dsp-tools create --lists-only' command""" create_lists.return_value = ({}, True) file = "filename.json" args = f"create --lists-only {file}".split() - cli.run(args) + entry_point.run(args) create_lists.assert_called_once_with( project_file_as_path_or_parsed=file, server="http://0.0.0.0:3333", @@ -39,21 +39,21 @@ def test_lists_create(create_lists: Mock) -> None: ) -@patch("dsp_tools.cli.validate_project") +@patch("dsp_tools.cli.call_action.validate_project") def test_project_validate(validate_project: Mock) -> None: """Test the 'dsp-tools create --validate-only' command""" file = "filename.json" args = f"create --validate-only {file}".split() - cli.run(args) + entry_point.run(args) validate_project.assert_called_once_with(file) -@patch("dsp_tools.cli.create_project") +@patch("dsp_tools.cli.call_action.create_project") def test_project_create(create_project: Mock) -> None: """Test the 'dsp-tools create' command""" file = "filename.json" args = f"create {file}".split() - cli.run(args) + entry_point.run(args) create_project.assert_called_once_with( project_file_as_path_or_parsed=file, server="http://0.0.0.0:3333", @@ -64,13 +64,13 @@ def test_project_create(create_project: Mock) -> None: ) -@patch("dsp_tools.cli.get_project") +@patch("dsp_tools.cli.call_action.get_project") def test_project_get(get_project: Mock) -> None: """Test the 'dsp-tools get --project' command""" file = "filename.json" project = "shortname" args = f"get --project {project} {file}".split() - cli.run(args) + entry_point.run(args) get_project.assert_called_once_with( project_identifier=project, outfile_path=file, @@ -82,21 +82,21 @@ def test_project_get(get_project: Mock) -> None: ) -@patch("dsp_tools.cli.validate_xml_against_schema") +@patch("dsp_tools.cli.call_action.validate_xml_against_schema") def test_xmlupload_validate(validate_xml: Mock) -> None: """Test the 'dsp-tools xmlupload --validate-only' command""" file = "filename.xml" args = f"xmlupload --validate-only {file}".split() - cli.run(args) + entry_point.run(args) validate_xml.assert_called_once_with(file) -@patch("dsp_tools.cli.xmlupload") +@patch("dsp_tools.cli.call_action.xmlupload") def test_xmlupload(xmlupload: Mock) -> None: """Test the 'dsp-tools xmlupload' command""" file = "filename.xml" args = f"xmlupload {file}".split() - cli.run(args) + entry_point.run(args) xmlupload.assert_called_once_with( input_file=file, server="http://0.0.0.0:3333", @@ -108,7 +108,7 @@ def test_xmlupload(xmlupload: Mock) -> None: ) -@patch("dsp_tools.cli.process_files") +@patch("dsp_tools.cli.call_action.process_files") def test_process_files(process_files: Mock) -> None: """Test the 'dsp-tools process-files' command""" input_dir = "input" @@ -116,7 +116,7 @@ def test_process_files(process_files: Mock) -> None: nthreads = 12 file = "filename.xml" args = f"process-files --input-dir {input_dir} --output-dir {output_dir} --nthreads {nthreads} {file}".split() - cli.run(args) + entry_point.run(args) process_files.assert_called_once_with( input_dir=input_dir, output_dir=output_dir, @@ -125,13 +125,13 @@ def test_process_files(process_files: Mock) -> None: ) -@patch("dsp_tools.cli.upload_files") +@patch("dsp_tools.cli.call_action.upload_files") def test_upload_files(upload_files: Mock) -> None: """Test the 'dsp-tools upload-files' command""" processed_dir = "processed" nthreads = 12 args = f"upload-files --processed-dir {processed_dir} --nthreads {nthreads}".split() - cli.run(args) + entry_point.run(args) upload_files.assert_called_once_with( dir_with_processed_files=processed_dir, nthreads=nthreads, @@ -142,12 +142,12 @@ def test_upload_files(upload_files: Mock) -> None: ) -@patch("dsp_tools.cli.fast_xmlupload") +@patch("dsp_tools.cli.call_action.fast_xmlupload") def test_fast_xmlupload(fast_xmlupload: Mock) -> None: """Test the 'dsp-tools fast-xmlupload' command""" file = "filename.xml" args = f"fast-xmlupload {file}".split() - cli.run(args) + entry_point.run(args) fast_xmlupload.assert_called_once_with( xml_file=file, user="root@example.com", @@ -157,27 +157,27 @@ def test_fast_xmlupload(fast_xmlupload: Mock) -> None: ) -@patch("dsp_tools.cli.excel2json") +@patch("dsp_tools.cli.call_action.excel2json") def test_excel2json(excel2json: Mock) -> None: """Test the 'dsp-tools excel2json' command""" folder = "folder" out_file = "filename.json" args = f"excel2json {folder} {out_file}".split() - cli.run(args) + entry_point.run(args) excel2json.assert_called_once_with( data_model_files=folder, path_to_output_file=out_file, ) -@patch("dsp_tools.cli.excel2lists") +@patch("dsp_tools.cli.call_action.excel2lists") def test_excel2lists(excel2lists: Mock) -> None: """Test the 'dsp-tools excel2lists' command""" excel2lists.return_value = ([], True) file = "filename.xlsx" out_file = "filename.json" args = f"excel2lists {file} {out_file}".split() - cli.run(args) + entry_point.run(args) excel2lists.assert_called_once_with( excelfolder=file, path_to_output_file=out_file, @@ -185,41 +185,41 @@ def test_excel2lists(excel2lists: Mock) -> None: ) -@patch("dsp_tools.cli.excel2resources") +@patch("dsp_tools.cli.call_action.excel2resources") def test_excel2resources(excel2resources: Mock) -> None: """Test the 'dsp-tools excel2resources' command""" excel2resources.return_value = ([], True) file = "filename.xlsx" out_file = "filename.json" args = f"excel2resources {file} {out_file}".split() - cli.run(args) + entry_point.run(args) excel2resources.assert_called_once_with( excelfile=file, path_to_output_file=out_file, ) -@patch("dsp_tools.cli.excel2properties") +@patch("dsp_tools.cli.call_action.excel2properties") def test_excel2properties(excel2properties: Mock) -> None: """Test the 'dsp-tools excel2properties' command""" excel2properties.return_value = ([], True) file = "filename.xlsx" out_file = "filename.json" args = f"excel2properties {file} {out_file}".split() - cli.run(args) + entry_point.run(args) excel2properties.assert_called_once_with( excelfile=file, path_to_output_file=out_file, ) -@patch("dsp_tools.cli.id2iri") +@patch("dsp_tools.cli.call_action.id2iri") def test_id2iri(id2iri: Mock) -> None: """Test the 'dsp-tools id2iri' command""" xml_file = "filename.xml" json_file = "filename.json" args = f"id2iri {xml_file} {json_file}".split() - cli.run(args) + entry_point.run(args) id2iri.assert_called_once_with( xml_file=xml_file, json_file=json_file, @@ -227,14 +227,14 @@ def test_id2iri(id2iri: Mock) -> None: ) -@patch("dsp_tools.cli.excel2xml", return_value=("foo", "bar")) +@patch("dsp_tools.cli.call_action.excel2xml", return_value=("foo", "bar")) def test_excel2xml(excel2xml: Mock) -> None: """Test the 'dsp-tools excel2xml' command""" excel_file = "filename.xlsx" shortcode = "1234" onto = "someonto" args = f"excel2xml {excel_file} {shortcode} {onto}".split() - cli.run(args) + entry_point.run(args) excel2xml.assert_called_once_with( datafile=excel_file, shortcode=shortcode, @@ -246,7 +246,7 @@ def test_excel2xml(excel2xml: Mock) -> None: def test_start_stack(start_stack: Mock) -> None: """Test the 'dsp-tools start-stack' command""" args = "start-stack".split() - cli.run(args) + entry_point.run(args) start_stack.assert_called_once_with() @@ -254,21 +254,21 @@ def test_start_stack(start_stack: Mock) -> None: def test_stop_stack(stop_stack: Mock) -> None: """Test the 'dsp-tools stop-stack' command""" args = "stop-stack".split() - cli.run(args) + entry_point.run(args) stop_stack.assert_called_once_with() -@patch("dsp_tools.cli.generate_template_repo") +@patch("dsp_tools.cli.call_action.generate_template_repo") def test_template(generate_template_repo: Mock) -> None: """Test the 'dsp-tools template' command""" args = "template".split() - cli.run(args) + entry_point.run(args) generate_template_repo.assert_called_once_with() -@patch("dsp_tools.cli.upload_rosetta") +@patch("dsp_tools.cli.call_action.upload_rosetta") def test_rosetta(upload_rosetta: Mock) -> None: """Test the 'dsp-tools rosetta' command""" args = "rosetta".split() - cli.run(args) + entry_point.run(args) upload_rosetta.assert_called_once_with()