move cli script to reduce core package dependencies

This commit is contained in:
Thomas (Tom) C. Gorordo 2026-05-25 00:15:49 -07:00
parent 2138a0ea6b
commit 8b39991bcd
Signed by: tgorordo
GPG key ID: 0CBED22BB0D94490
11 changed files with 251 additions and 141 deletions

148
README.md
View file

@ -1,5 +1,147 @@
# Smithy
*A simple smith set solver for ranked-choice ballots.*
*A simple Smith set solver for ranked-choice ballots.*
The Smith set is the minimal set of election candidates which can beat all others pairwise
(by simple majority ranking preference) - if there is a single winner in the set they are
guaranteed the standard Condorcet i.e. Majority winner (they beat all others pairwise).
`smithy` currently identifies the set by brute-force search which is combinatoric complexity
in the worst case (TODO: better [algorithm](https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm))
but appears approximately $O(n^2)$ on-average in the number of candidates for typical/random ballots,
and linear in the number of ballots.
## Usage
`smithy` can be used a few different ways: as a CLI command, via a web form upload,
or imported as a python package (TODO: a small [GUI](https://doc.qt.io/qtforpython-6/)).
### CLI Command
`smithy` provides a [`click`](https://click.palletsprojects.com/en/stable/)
CLI command defined in `src/cmd.py` which can be invoked a couple of ways:
- The [GitHub Releases Page](https://github.com/tgorordo/smithy/releases) provides standalone
CLI executables bundled using [PyInstaller](https://pyinstaller.org/en/stable/index.html)
for linux and windows on `x86_64` and linux on `aarch64`. Downloading the appropriate
executable should give you access to the `smithycmd` CLI command.
- Alternatively, if you are using `uv` (see [Development](#Development)), you can invoke
this in your shell as `uv run src/smithycmd.py [...]`.
In either case, the command expects the same argument structure:
TODO
While plain output is the default, so that the command can easily be used in a unix-pipe
or `stdio` workflows, it can also pretty-print its output for your reading pleasure
using [`rich`](https://rich.readthedocs.io/en/stable/introduction.html) if you pass it the
`--pretty` (or `-p`) flag.
### UOregon CGI Application
[**This** `pages.uoregon.edu` page](https://pages.uoregon.edu/tgorordo/files/smithy/src/cgi/form.html)
hosts a [CGI form](https://service.uoregon.edu/TDClient/2030/Portal/KB/ArticleDet?ID=43069)
which accepts a `.csv` or `.xls`(`x`) upload of a ballot-box and responds with the resulting
Smith set. The application can typically handle ~100s of candidates and ~10ks of votes
before running into hosting resource limitations and timing out (pathological cases, involving
many or large cycles, or especially a large cyclic tie, may perform worse and are not extensively tested).
### Python Package
The `smithy` python package primarily provides the function `smith_set(ballot_box_df)`
(defined in `src/smithy/rcv.py`), which returns the smith set given a
[polars](https://docs.pola.rs/api/python/stable/reference/index.html)
dataframe `ballot_box_df` whose columns are candidates and whose rows are RCV ballots.
e.g. `smithy` can be imported into a [marimo](https://docs.marimo.io/#highlights) notebook
as seen in `test/test_nb.py` - if you're using `uv` you can launch marimo via
`uv run marimo --edit` and navigate to that example notebook
(`just marimo` is an available shorthand if you are using the `just` taskrunner --
see [Development](#Development)). Cloning `smithy` then working in scratch notebooks within the repo
(e.g. makeing your own `nbs/` directory)
is probably the easiest way to use it if you need to do some of cleanup of the tabular
data before invocation - just do your cleanup in a notebook using
[polars](https://docs.pola.rs/py-polars/html/reference/) then pass a tidy dataframe to `smith_set(df)`.
Of course, you can also import into any python script or package for a more involved project
as well:
- If you're using `uv` adding `smithy` should be as simple as
```bash
uv add git+https://github.com/tgorordo/smithy.git
```
- Alternatively, if you're using the more default `pip` you can make it available by
```bash
git clone https://github.com/tgorordo/smithy.git # or submodule add somewhere appropriate in your project
cd smithy
pip install .
```
(if you're new to pip-venvs,
[this brief guide might be helpful](https://pages.uoregon.edu/tgorordo/courses/uoph410-510a_Image-Analysis/setup.html)).
## Development
Current development tooling is based on the [`uv`](https://docs.astral.sh/uv/) Python package and
project manager. A [`justfile`](https://github.com/casey/just) lists some common useful
task commands. A simple [`shell.nix`](https://nix.dev/tutorials/first-steps/declarative-shell.html)
can be used to load these tools into a local environment, and you might find
[`direnv`](https://github.com/direnv/direnv) a convenient pairing to `nix` - but just having a
system-provided `uv` available is more than enough to use and develop `smithy`.
### Formatting, Checking, Testing and Locking
Source can be formatted using [`ruff`](https://docs.astral.sh/ruff/) by running
```bash
uv run ruff format src test
```
(or `just format`)
which should be done before any `git` commit.
This is not really taken full advantage of at the moment, but type hints can be checked
by running
```bash
uv run pyright src
```
(or `just check`).
A test suite can be run as
```bash
uv run pytest -vvv --tb=short --log-cli-level=INFO
```
(or `just test`).
Dependencies can be locked by running
```bash
uv lock
uv pip compile pyproject.toml -o requirements.txt --group dev
```
(or `just lock`).
### Bundling, Releases, Deployment and Cleanup
Bundling with [PyInstaller](https://pyinstaller.org/en/stable/index.html) can be done by
```bash
uv run pyinstaller --clean -F src/cmd.py --name smithycmd
```
which will output an executable `smithycmd` for the CLI in `dist/`, which can be uploaded
to the latest [GitHub Releases Page](https://github.com/tgorordo/smithy/releases).
Deploying the main CGI page at
[`pages.uoregon.edu/tgorordo/files/smithy/src/cgi`](https://pages.uoregon.edu/tgorordo/files/smithy/src/cgi)
requires shell or SFTP access to [my `pages.uoregon.edu`](https://pages.uoregon.edu/tgorordo),
which you won't get unless you're [me](https://github.com/tgorordo).
If you want to deploy it to your own `pages.uoregon.edu` page, then it should be enough to
ensure you have [CGI set up](https://service.uoregon.edu/TDClient/2030/Portal/KB/ArticleDet?ID=43069)
then SFTP into `sftp.uoregon.edu` and `put -r smithy` (or git clone when `ssh`ed into
`shell.uoregon.edu`) wherever you'd like in your `public_html`, ensure `smithy.cgi` has
`chmod 755` permissions,
[standalone install `uv` for your user](https://docs.astral.sh/uv/getting-started/installation/)
then run `uv sync` from somewhere inside the `smithy` directory. Navigating to the `smithy.cgi`
file in `pages.uoregon.edu/YOUR_USER/.../cgi/smithy.cgi` should give you a working form.
If you're using `just` you can cleanup working files by running `just clean` and can fully
wipe your `uv` generated `.venv` with `just wipe` (if you want manual versions of those,
check the `justfile` definition). It's probably best to do this before any commit as well,
though probably not required if you're careful or have `.gitignore` updated appropriately
for any changes you made.
The Smith set is the minimal set of candidates which can beat all others pairwise - if there is a single winner
in the set they are guaranteed the standard Condorcet i.e. Majority winner (they beat all others pairwise).