diff --git a/algobowl/templates/cli_setup.xhtml b/algobowl/templates/cli_setup.xhtml index 054dd43..34d564a 100644 --- a/algobowl/templates/cli_setup.xhtml +++ b/algobowl/templates/cli_setup.xhtml @@ -7,40 +7,70 @@ <h1>Command Line Interface (Beta)</h1> <p> - AlgoBOWL currently has an <strong>experimental</strong> command - line you can use to interface with it. It is not currently to - feature-parity with the web interface. + AlgoBOWL has a command-line interface for power-users to interact with + the site from the command-line or from scripts. The CLI is currently in + beta, and commands may be subject to change in the future. Not all + functionality that's currently available on the site is also implemented + in the CLI. </p> - <h2>Prerequisites</h2> + <h2>Installation</h2> - <ul> - <li>Python 3.7+</li> - <li><tt>pip</tt> Package Manager</li> - </ul> - - <h2>Installing the CLI</h2> + <p> + Download the + <a href="https://raw.githubusercontent.com/jackrosenthal/algobowl/main/cli_launcher.py">launcher script</a> + and mark it executable. For example: + </p> <p> - Run <tt>pip install --user algobowl</tt>. - Ensure <tt>~/.local/bin</tt> is in your <tt>PATH</tt>. + <tt>curl https://raw.githubusercontent.com/jackrosenthal/algobowl/main/cli_launcher.py -o algobowl && chmod +x algobowl</tt> </p> - <h2>Authentication</h2> + <p> + You can either put it in a location referenced by your <tt>PATH</tt> + environment variable and run it as <tt>algobowl</tt>, or it's designed + so you can check it in to your group's Git repo as well (and run it as + <tt>./algobowl</tt>). + </p> <p> - First, set the default server for your commands by running - <tt>algobowl config set-default-server ${tg.request.application_url}</tt>. + The launcher is designed to depend on nothing but Python 3.8+, so you + don't need to worry about installing any dependencies. </p> + <h3>Authentication</h3> + <p> - Then, run <tt>algobowl auth login</tt> and follow the instructions. + Run <tt>algobowl auth login</tt> and follow the instructions. </p> <h2>Usage</h2> <p> - Run <tt>algobowl --help</tt>. + Run <tt>algobowl --help</tt> for help. Here are some commands you may + be interested in: + </p> + + <h3>Input Upload</h3> + + <ul> + <li><tt>algobowl group input upload FILENAME</tt>: Upload your group's input.</li> + <li><tt>algobowl group input download OUTPUT_FILE</tt>: Download your group's input.</li> + <li><tt>algobowl group set-team-name TEAM_NAME</tt>: Set your team name.</li> + </ul> + + <h3>Output Upload</h3> + + <ul> + <li><tt>algobowl group output --to-group-id GROUP_ID upload FILENAME</tt>: Upload an output.</li> + <li><tt>algobowl group output --to-group-id GROUP_ID download OUTPUT_FILE</tt>: Download one of your submitted outputs.</li> + <li><tt>algobowl group output list</tt>: List output files you'll need to provide.</li> + </ul> + + <p> + Note: if you use filenames containing the group ID, (e.g., + <tt>output_group123.txt</tt>), the CLI can infer the group ID from the + filename, and passing <tt>--to-group-id</tt> is not required. </p> <h2>Logged-in Clients</h2> diff --git a/algobowl/templates/master.xhtml b/algobowl/templates/master.xhtml index f5d871a..efb027e 100644 --- a/algobowl/templates/master.xhtml +++ b/algobowl/templates/master.xhtml @@ -84,6 +84,10 @@ <i class="fas fa-users fa-fw"></i> My Group </a> + <a class="dropdown-item" href="${tg.url('/pref/cli')}"> + <i class="fas fa-terminal fa-fw"></i> + Command Line Interface + </a> <a class="dropdown-item" py:if="request.identity['user'].admin" href="${tg.url('/admin')}"> <i class="fas fa-user-secret fa-fw"></i> diff --git a/cli_launcher.py b/cli_launcher.py new file mode 100755 index 0000000..add7e3a --- /dev/null +++ b/cli_launcher.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 + +"""Launcher for AlgoBOWL CLI. + +This is a small little launcher script for the AlgoBOWL CLI. It launches the +CLI creating a Python virtual environment and installing it from GitHub. The +launcher will update the CLI on startup every 24 hours. By design, this has no +dependencies other than Python 3.8+ and is a single file. You should be able to +"chmod +x" this script and put it in your PATH, or put it in your team's Git +repo for everyone on your team to use. + +This launcher can be downloaded from: +https://raw.githubusercontent.com/jackrosenthal/algobowl/main/cli_launcher.py + +All command line arguments are passed as-is to the real AlgoBOWL CLI. It can +minimally be configured via environment variables: + +- ALGOBOWL_VENV: Path to the virtual environment to use (by default, create one + in the XDG cache directory). +- ALGOBOWL_FORCE_UPDATE: Set to 1 to force the launcher to re-build the virtual + environment. +- ALGOBOWL_NO_UPDATE: Set to 1 to force the launcher to not update the virtual + environment. +""" + +import datetime +import os +import subprocess +import sys +import venv +from pathlib import Path + +assert sys.version_info >= (3, 8), "AlgoBOWL CLI requires Python 3.8+" + + +def quiet_run(argv) -> None: + """Run a command, staying quiet unless there's an error.""" + try: + subprocess.run( + argv, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) + except subprocess.CalledProcessError as e: + print(f"Command failed ({argv})!", file=sys.stderr) + sys.stderr.write(subprocess.stdout) + raise + + +def get_cache_dir() -> Path: + """Get the XDG-specified cache directory.""" + xdg_cache_home = os.environ.get("XDG_CACHE_HOME") + if xdg_cache_home: + return Path(xdg_cache_home) + return Path.home() / ".cache" + + +def get_venv_dir() -> Path: + """Get the path to the virtual environment to use.""" + venv_dir = os.environ.get("ALGOBOWL_VENV") + if venv_dir: + return Path(venv_dir) + + return ( + get_cache_dir() + / "algobowl" + / f"venv-{sys.version_info.major}.{sys.version_info.minor}" + ) + + +def venv_cmd(executable: str) -> Path: + """Get the path to a command in the virtual environment.""" + return get_venv_dir() / "bin" / executable + + +def build_venv() -> None: + """Build the virtual environment.""" + venv.EnvBuilder( + system_site_packages=False, + clear=bool(os.environ.get("ALGOBOWL_FORCE_UPDATE")), + symlinks=True, + with_pip=True, + ).create(get_venv_dir()) + quiet_run([venv_cmd("pip"), "install", "--upgrade", "pip"]) + url = os.environ.get( + "ALGOBOWL_URL", + "https://github.com/jackrosenthal/algobowl/archive/refs/heads/main.zip", + ) + quiet_run([venv_cmd("pip"), "install", "--upgrade", url]) + + +def update_venv() -> None: + """Build the virtual environment if necessary.""" + if os.environ.get("ALGOBOWL_NO_UPDATE"): + return + update_file = get_venv_dir() / "UPDATE" + force_update = os.environ.get("ALGOBOWL_FORCE_UPDATE") + now = datetime.datetime.now() + if not force_update and update_file.exists(): + last_update = datetime.datetime.fromisoformat( + update_file.read_text(encoding="ascii") + ) + required_update = last_update + datetime.timedelta(hours=24) + if required_update > now: + return + build_venv() + update_file.write_text(now.isoformat(), encoding="ascii") + + +def main(): + """The main function.""" + update_venv() + algobowl = venv_cmd("algobowl") + os.execv(venv_cmd("algobowl"), sys.argv) + + +if __name__ == "__main__": + main()