rse.client — The Research Software Encyclopedia 11 documentation

# Source code for rse.client

    #!/usr/bin/env python

    """

    Copyright (C) 2020 Vanessa Sochat.

    This Source Code Form is subject to the terms of the
    Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
    with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

    """

    from rse.logger import RSE_LOG_LEVEL, RSE_LOG_LEVELS
    from rse.defaults import RSE_CONFIG_FILE
    import rse
    import argparse
    import sys
    import logging
    import os

    def get_parser():
        parser = argparse.ArgumentParser(
            description="Research software engineering software inspector."
        )

        parser.add_argument(
            "--version",
            dest="version",
            help="suppress additional output.",
            default=False,
            action="store_true",
        )

        parser.add_argument(
            "--log_level",
            dest="log_level",
            choices=RSE_LOG_LEVELS,
            default=RSE_LOG_LEVEL,
            help="Customize logging level for rse inspector.",
        )

        # Configuration file
        parser.add_argument(
            "--config_file",
            dest="config_file",
            default=RSE_CONFIG_FILE,
            help="Path to rse.ini configuration file.",
        )

        description = "actions for rse"
        subparsers = parser.add_subparsers(
            help="rse actions",
            title="actions",
            description=description,
            dest="command",
        )

        # print version and exit
        subparsers.add_parser("version", help="show software version")

        # Annotate criteria or taxonomy
        annotate = subparsers.add_parser(
            "annotate", help="Annotate a database with criteria or taxonomy"
        )
        annotate.add_argument(
            "type",
            help="Type to annotate (taxonomy or criteria)",
            nargs=1,
            choices=["taxonomy", "criteria"],
        )
        annotate.add_argument(
            "-u",
            "--username",
            dest="username",
            default=None,
            help="GitHub username (must be provided if not available with git config)",
        )
        annotate.add_argument(
            "-r",
            "--repo",
            dest="repo",
            default=None,
            help="Specify a particular repository name to annotate.",
        )
        annotate.add_argument(
            "-f",
            "--file",
            dest="file",
            default=None,
            help="Read annotation from file (e.g., markdown list, GitHub issue template).",
        )
        annotate.add_argument(
            "--all",
            "-a",
            dest="all_repos",
            help="Annotate all repos, even those that have already been seen (defaults to show only those unseen).",
            default=False,
            action="store_true",
        )

        # Generate a key for the interface
        generate = subparsers.add_parser(
            "generate-key",
            help="generate a key for rse start, should be exported to RSE_SERVER_KEY.",
        )

        # Init
        init = subparsers.add_parser(
            "init", help="Add an rse.ini to the present working directory."
        )
        init.add_argument(
            "path",
            help="Path to generate rse.ini file",
            nargs="?",
            default=".",
        )

        # Config
        config = subparsers.add_parser(
            "config", help="Update an rse.ini configuration file."
        )

        # Clear
        clear = subparsers.add_parser("clear", help="Remove software from the database.")
        clear.add_argument("target", nargs="?")
        clear.add_argument(
            "--force",
            dest="force",
            help="Don't ask for confirmation for delete (for headless).",
            default=False,
            action="store_true",
        )

        # Exists
        exists = subparsers.add_parser(
            "exists", help="Determine if an entry exists in the database."
        )

        # Export
        export = subparsers.add_parser(
            "export", help="Export repository names, metadata, or static files."
        )
        export.add_argument(
            "--type",
            dest="export_type",
            help="Type to export (defaults to repos.txt list)",
            default="repos-txt",
            choices=["repos-txt", "static-web"],
        )
        export.add_argument(
            "--force",
            dest="force",
            help="Don't ask for confirmation to overwrite existing file(s).",
            default=False,
            action="store_true",
        )
        export.add_argument(
            "path",
            help="Fileame to export repos to (default repos.txt)",
            default="repos.txt",
        )

        # Metrics
        summary = subparsers.add_parser(
            "summary", help="View summary metrics for all repositories."
        )
        summary.add_argument(
            "--type",
            dest="metric_type",
            help="Metric type to view.",
            default="summary",
            choices=["criteria", "taxonomy", "users"],
        )
        summary.add_argument(
            "repo", help="Filter down to one repository.", default=None, nargs="?"
        )

        analyze = subparsers.add_parser(
            "analyze", help="View metrics for a specific repository."
        )
        analyze.add_argument(
            "repo",
            help="Software repository to show",
            default=None,
        )
        analyze.add_argument(
            "--ct",
            "--cthresh",
            dest="cthresh",
            help="Criteria threshold (between 0 and 1)",
            default=0.5,
            type=percentage_type_asint,
        )
        analyze.add_argument(
            "--tt",
            "--tthresh",
            dest="tthresh",
            help="Minimum taxonomy votes to count category in response",
            default=1,
            type=positive_int_type,
        )

        # Update
        update = subparsers.add_parser(
            "update", help="Update one or more software entries."
        )

        update.add_argument(
            "-p",
            "--path",
            dest="path",
            default=None,
            help="Path to single folder or set of folders to update.",
        )
        update.add_argument(
            "--force",
            dest="force",
            help="If a repository is not present, add it.",
            default=False,
            action="store_true",
        )
        update.add_argument(
            "--rewrite",
            dest="rewrite",
            help="If data exists, don't update but rewrite.",
            default=False,
            action="store_true",
        )

        # List topics (global) or matching pattern
        topics = subparsers.add_parser(
            "topics", help="List software topics (GitHub support only)"
        )
        topics.add_argument(
            "--pattern",
            help="filter topics to a particular pattern",
            type=str,
            default=None,
        )
        topics.add_argument(
            "--search",
            help="find repositories based on a list of topics",
            type=str,
            nargs="*",
        )

        # List repos and print to terminal
        ls = subparsers.add_parser("ls", help="List software")
        ls.add_argument(
            "parser", help="list one or more parsers or specific software.", nargs="*"
        )

        # Search for software
        search = subparsers.add_parser(
            "search",
            help="Search for a piece of research software",
        )
        search.add_argument("query", nargs="*")
        search.add_argument("--taxonomy", nargs="*")
        search.add_argument("--criteria", nargs="*")

        # Scrape for new repos
        scrape = subparsers.add_parser(
            "scrape",
            help="Add new software repositories from a resource.",
        )
        scrape.add_argument("scraper_name", nargs=1)
        scrape.add_argument("query", nargs="?")
        scrape.add_argument(
            "--dry-run",
            dest="dry_run",
            help="Dump scrape output to the terminal, but don't create new repos.",
            default=False,
            action="store_true",
        )
        scrape.add_argument(
            "--delay",
            dest="delay",
            help="Number of seconds (float) to delay, default 0.",
            type=float_type,
            default=0.0,
        )

        # Shell
        subparsers.add_parser(
            "shell", help="start an interactive shell for an encyclopedia"
        )

        # Start the rse dashboard
        start = subparsers.add_parser(
            "start", help="start an interface to browse software (requires Flask)"
        )
        for command in [start, export]:
            command.add_argument(
                "--port",
                dest="port",
                default=5000,
                type=int,
                help="select port to run dashboard on (defaults to 5000)",
            )
            command.add_argument(
                "--host",
                dest="host",
                default="127.0.0.1",
                type=str,
                help="the hostname to run for the server (defaults to 127.0.0.1)",
            )
            command.add_argument(
                "--debug",
                dest="debug",
                help="run server in debug mode (defaults to False)",
                default=False,
                action="store_true",
            )
            command.add_argument(
                "--disable-annotate",
                dest="disable_annotate",
                help="disable annotation button (defaults to False)",
                default=False,
                action="store_true",
            )

        # Label a repository with metadata, e.g., add a DOI.
        label = subparsers.add_parser(
            "label", help="Add a metadata value for an existing repository"
        )
        label.add_argument("values", nargs=3)
        label.add_argument(
            "--force",
            dest="force",
            help="Overwrite existing label, if it exists.",
            default=False,
            action="store_true",
        )

        # Print complete metadata for a specific piece of software
        get = subparsers.add_parser("get", help="Show metadata for software")
        add = subparsers.add_parser("add", help="Add a repository to the database.")

        # Most commands allow specification of a database backend
        for command in [
            add,
            analyze,
            annotate,
            clear,
            config,
            exists,
            export,
            get,
            init,
            label,
            ls,
            search,
            scrape,
            start,
            summary,
            topics,
            update,
        ]:
            command.add_argument(
                "--database",
                dest="database",
                choices=["filesystem", "sqlite"],
                default=None,
                help="database backend to use, to override configuration.",
            )

        for command in [exists, get]:
            command.add_argument("uid", help="uri within software namespace.", nargs="?")

        for command in [update, add]:
            command.add_argument("uid", help="uri within software namespace.", nargs="?")
            command.add_argument(
                "--file",
                dest="file",
                default=None,
                help="single line delimited file of repositories.",
            )

        return parser

    def percentage_type_asint(arg):
        """ensure that an input is between 0 and 1"""
        try:
            number = float(arg)
        except ValueError:
            raise argparse.ArgumentTypeError("Must be a floating point number")
        if number < 0 or number > 1:
            raise argparse.ArgumentTypeError("Argument must be between 0 and 1")
        return number

    def float_type(arg):
        """ensure that an input is greater than 0 and a float"""
        try:
            number = float(arg)
        except ValueError:
            raise argparse.ArgumentTypeError("Must be a floating point number")
        if number < 0:
            raise argparse.ArgumentTypeError("Argument must be greater than 0.")
        return number

    def positive_int_type(arg):
        """ensure user is providing a positive integer"""
        value = int(arg)
        if value < 0:
            raise argparse.ArgumentTypeError(
                "%s is an invalid positive integer value" % arg
            )
        return value

    def main():
        """main entrypoint for rse"""

        parser = get_parser()

        def help(return_code=0):
            """print help, including the software version and active client
            and exit with return code.
            """
            version = rse.__version__

            print("\nResearch Software Engineering software inspector v%s" % version)
            parser.print_help()
            sys.exit(return_code)

        # If the user didn't provide any arguments, show the full help
        if len(sys.argv) == 1:
            help()

        # If an error occurs while parsing the arguments, the interpreter will exit with value 2
        args, extra = parser.parse_known_args()

        # Set the logging level
        os.putenv("RSE_LOG_LEVEL", args.log_level)
        RSE_LOG_LEVEL = args.log_level
        logging.basicConfig(level=getattr(logging, args.log_level))
        bot = logging.getLogger("rse.client")
        bot.setLevel(getattr(logging, args.log_level))

        # Show the version and exit
        if args.command == "version" or args.version:
            print(rse.__version__)
            sys.exit(0)

        # Does the user want a shell?
        if args.command == "analyze":
            from .metrics import analyze as main
        if args.command == "annotate":
            from .annotate import main
        if args.command == "add":
            from .add import main
        if args.command == "clear":
            from .clear import main
        if args.command == "config":
            from .config import main
        if args.command == "exists":
            from .exists import main
        if args.command == "export":
            from .export import main
        if args.command == "generate-key":
            from .generate import main
        if args.command == "label":
            from .label import main
        if args.command == "update":
            from .update import main
        if args.command == "get":
            from .get import main
        if args.command == "init":
            from .init import main
        if args.command == "ls":
            from .listing import main
        if args.command == "summary":
            from .metrics import summary as main
        if args.command == "search":
            from .search import main
        if args.command == "scrape":
            from .scrape import main
        if args.command == "shell":
            from .shell import main
        if args.command == "start":
            from .start import main
        if args.command == "topics":
            from .topics import main

        # Pass on to the correct parser
        main(args=args, extra=extra)

    if __name__ == "__main__":
        main()