Skip to content

Commit

Permalink
Add a launcher script for CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
jackrosenthal committed Feb 12, 2024
1 parent 631784a commit b76b216
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 17 deletions.
64 changes: 47 additions & 17 deletions algobowl/templates/cli_setup.xhtml
Original file line number Diff line number Diff line change
Expand Up @@ -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 &amp;&amp; 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>
Expand Down
4 changes: 4 additions & 0 deletions algobowl/templates/master.xhtml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
116 changes: 116 additions & 0 deletions cli_launcher.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit b76b216

Please sign in to comment.