From b76b216d21890e3084ed030c678cf31075e76ba9 Mon Sep 17 00:00:00 2001 From: Jack Rosenthal Date: Sun, 11 Feb 2024 23:05:08 -0700 Subject: [PATCH] Add a launcher script for CLI --- algobowl/templates/cli_setup.xhtml | 64 +++++++++++----- algobowl/templates/master.xhtml | 4 + cli_launcher.py | 116 +++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 17 deletions(-) create mode 100755 cli_launcher.py 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 @@

Command Line Interface (Beta)

- AlgoBOWL currently has an experimental 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.

-

Prerequisites

+

Installation

- - -

Installing the CLI

+

+ Download the + launcher script + and mark it executable. For example: +

- Run pip install --user algobowl. - Ensure ~/.local/bin is in your PATH. + curl https://raw.githubusercontent.com/jackrosenthal/algobowl/main/cli_launcher.py -o algobowl && chmod +x algobowl

-

Authentication

+

+ You can either put it in a location referenced by your PATH + environment variable and run it as algobowl, or it's designed + so you can check it in to your group's Git repo as well (and run it as + ./algobowl). +

- First, set the default server for your commands by running - algobowl config set-default-server ${tg.request.application_url}. + The launcher is designed to depend on nothing but Python 3.8+, so you + don't need to worry about installing any dependencies.

+

Authentication

+

- Then, run algobowl auth login and follow the instructions. + Run algobowl auth login and follow the instructions.

Usage

- Run algobowl --help. + Run algobowl --help for help. Here are some commands you may + be interested in: +

+ +

Input Upload

+ + + +

Output Upload

+ + + +

+ Note: if you use filenames containing the group ID, (e.g., + output_group123.txt), the CLI can infer the group ID from the + filename, and passing --to-group-id is not required.

Logged-in Clients

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 @@ My Group + + + Command Line Interface + 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()