#!/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
[docs]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
[docs]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
[docs]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
[docs]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
[docs]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()