Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c5deae6
docs: add plan for migrating from eel to Qt
foobacca Aug 3, 2026
a536926
test: add the test harness, fixtures and CI workflow
foobacca Aug 3, 2026
88a5aff
test: pin down the current eel app's behaviour before porting it
foobacca Aug 3, 2026
f5de19a
refactor: extract a UI-agnostic session layer out of script.py
foobacca Aug 3, 2026
26982f6
feat: add the Qt widgets for both tabs
foobacca Aug 3, 2026
fa04c50
feat: run the slow work off the GUI thread
foobacca Aug 3, 2026
8af8e2b
feat: wire the Qt app together and test it end to end
foobacca Aug 3, 2026
b51d06f
refactor: delete eel
foobacca Aug 3, 2026
57f3a14
build: package the Qt app with a checked-in PyInstaller spec
foobacca Aug 3, 2026
700fbec
ignore the ignore/ folder
foobacca Aug 3, 2026
ec20476
docs: research the sortition-algorithms 0.12 bump in detail
foobacca Aug 4, 2026
17abc1a
build: bump sortition-algorithms to 0.12.10
foobacca Aug 4, 2026
86ccda1
test: key the live-log tests off the algorithm line, not the trial line
foobacca Aug 4, 2026
5fe9858
build: keep python-mip and cbcbox out of the packaged app
foobacca Aug 4, 2026
d39095d
feat: refuse settings this build cannot honour, at load time
foobacca Aug 4, 2026
33a7162
fix: actually refuse the test panel button for several selections
foobacca Aug 4, 2026
84e1aab
test: cover the header-whitespace fix through the gsheet session
foobacca Aug 4, 2026
9b1229d
feat: show real progress during a selection, not just a busy bar
foobacca Aug 4, 2026
184bee9
docs: record how the 0.12 bump actually went
foobacca Aug 4, 2026
11b5d5f
test: wait for the task runner before the progress test ends
foobacca Aug 4, 2026
32cdb88
fix: bundle the OSQP backend so cvxpy stops logging an error on start up
foobacca Sep 16, 2026
c542c80
ci: replace retired macos-13 runner with macos-15-intel
foobacca Sep 16, 2026
6e5fd90
ci: restore the release jobs dropped in the spec rewrite
foobacca Sep 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 40 additions & 22 deletions .github/workflows/build-executables.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
# 1. Build Windows Executable
#
build-windows-exe:
name: "script.exe creation using python 3.11 on Windows"
name: "strat-select.exe creation using python 3.11 on Windows"
runs-on: "windows-latest"
steps:
- uses: actions/checkout@v6
Expand All @@ -31,9 +31,13 @@ jobs:
uv sync
- name: "Build windows package"
run: |
uv run python -m eel script.py web -n strat-select-win.exe --additional-hooks-dir=./pyinstallerhooks/ --onefile
uv run pyinstaller strat-select.spec
mkdir -p target/release
mv dist/strat-select-win.exe target/release
# a packaged build fails in ways a development one never does - most often by
# not finding the Qt platform plugin. So run the thing we just built.
- name: "Smoke test the executable"
run: ./target/release/strat-select-win.exe --self-test
# copy script exe to build artifacts so we can test it
- name: Upload executable to artifacts
uses: actions/upload-artifact@v7
Expand All @@ -45,12 +49,13 @@ jobs:

# 2. Build MacOS Executable
#
# Note that this produces an ARM based build, so won't work on older
# intel based Macs.
# Note that this produces an x86_64 build, so it needs Rosetta on ARM based Macs.
build-macos-executable:
name: "script.app creation using python 3.11 on MacOS"
# we use 13 to build x86_64 apps
runs-on: "macos-13"
name: "strat-select.app creation using python 3.11 on MacOS"
# The `-intel` images are the x86_64 runners. macos-26-intel is the other
# option - we stay on 15 so the bundle links against the older SDK and keeps
# working on the older macOS versions that Intel Macs tend to be stuck on.
runs-on: "macos-15-intel"
steps:
- uses: actions/checkout@v6
- name: Install uv
Expand All @@ -66,17 +71,19 @@ jobs:
uv run python -m site
uv sync
# notes:
# --onedir rather than --onefile is required for app bundle
# --target-arch=x86_64 is required for python-mip 1.15.0 - revisit once 1.16.0 is properly released
# See https://groups.google.com/g/python-mip/c/0R2NC_URD6M
# the spec builds onedir on macOS, which an app bundle requires, and sets
# target_arch=x86_64 to match this runner. See strat-select.spec for what
# moving to an arm64 build would take.
- name: "Build MacOS package"
run: |
uv run python -m eel script.py web -n strat-select-macos --additional-hooks-dir=./pyinstallerhooks/ --onedir --windowed --target-arch=x86_64
uv run pyinstaller strat-select.spec
mkdir -p target/release
ls -l dist/strat-select-macos.app
mv dist/strat-select-macos target/release
cd dist && zip -r ../target/release/strat-select-macos.app.zip strat-select-macos.app
cd dist && zip -r ../target/release/strat-select-macos.app.zip strat-select-macos.app
ls -l ../target/release/
- name: "Smoke test the executable"
run: ./target/release/strat-select-macos/strat-select-macos --self-test
# copy script app to build artifacts so we can test it
- name: Upload executable to artifacts
uses: actions/upload-artifact@v7
Expand All @@ -93,6 +100,12 @@ jobs:
runs-on: "ubuntu-24.04"
steps:
- uses: actions/checkout@v6
- name: "Install Qt system libraries"
run: |
sudo apt-get update
sudo apt-get install -y xvfb libegl1 libgl1 libdbus-1-3 \
libxkbcommon-x11-0 libxcb-cursor0 libxcb-icccm4 \
libxcb-keysyms1 libxcb-shape0
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
Expand All @@ -107,9 +120,13 @@ jobs:
uv sync
- name: "Build Linux package"
run: |
uv run python -m eel script.py web -n strat-select-linux --additional-hooks-dir=./pyinstallerhooks/ --onefile
uv run pyinstaller strat-select.spec
mkdir -p target/release
mv dist/strat-select-linux target/release
# under xvfb rather than the offscreen platform, so this exercises the xcb
# plugin - the one that actually goes missing from a packaged build
- name: "Smoke test the executable"
run: xvfb-run --auto-servernum ./target/release/strat-select-linux --self-test
# copy script app to build artifacts so we can test it
- name: Upload executable to artifacts
uses: actions/upload-artifact@v7
Expand All @@ -122,18 +139,18 @@ jobs:
# 4. Release Windows Executable (tag pushes only)
#
release-windows-exe:
name: "script.exe release for Windows"
name: "strat-select.exe release for Windows"
needs: build-windows-exe
if: startsWith(github.ref, 'refs/tags/')
runs-on: "windows-latest"
# this job only moves a file, so it runs on the cheapest runner
runs-on: "ubuntu-24.04"
permissions:
contents: write
steps:
- name: Download a single artifact
uses: actions/download-artifact@v8
with:
name: strat-select-win.exe
# copy nicks-script.exe to the release
# https://github.com/marketplace/actions/upload-files-to-a-github-release
- name: upload executable to release
uses: svenstaro/upload-release-action@v2
Expand All @@ -147,24 +164,26 @@ jobs:
# 5. Release MacOS Executable (tag pushes only)
#
release-macos-executable:
name: "script.app release for MacOS"
name: "strat-select.app release for MacOS"
needs: build-macos-executable
if: startsWith(github.ref, 'refs/tags/')
runs-on: "macos-latest"
# this job only moves a file, so it runs on the cheapest runner
runs-on: "ubuntu-24.04"
permissions:
contents: write
steps:
- name: Download a single artifact
uses: actions/download-artifact@v8
with:
name: strat-select-macos
# copy script.app to the release
# The artifact holds the unpacked onedir build alongside the zip. A release
# asset has to be a single file, so we attach the zipped app bundle.
# https://github.com/marketplace/actions/upload-files-to-a-github-release
- name: upload executable to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: strat-select-macos
file: strat-select-macos.app.zip
tag: ${{ github.ref }}
overwrite: true
body: "script release text"
Expand All @@ -175,6 +194,7 @@ jobs:
name: "executable release for Linux"
needs: build-linux-executable
if: startsWith(github.ref, 'refs/tags/')
# this job only moves a file, so it runs on the cheapest runner
runs-on: "ubuntu-24.04"
permissions:
contents: write
Expand All @@ -183,8 +203,6 @@ jobs:
uses: actions/download-artifact@v8
with:
name: strat-select-linux
# copy script.app to the release
#
# https://github.com/marketplace/actions/upload-files-to-a-github-release
- name: upload executable to release
uses: svenstaro/upload-release-action@v2
Expand Down
63 changes: 63 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Run Tests

permissions:
contents: read

on:
push:
pull_request:

jobs:
test:
name: "pytest on python ${{ matrix.python-version }} / ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: ["ubuntu-24.04", "macos-latest", "windows-latest"]
python-version: ["3.11", "3.12"]
env:
# the tests never open a window - see tests/conftest.py
QT_QPA_PLATFORM: offscreen
steps:
- uses: actions/checkout@v6
# PySide6 needs these even for the offscreen platform plugin
- name: "Install Qt system libraries"
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libegl1 libgl1 libdbus-1-3 libxkbcommon0
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
python-version: "${{ matrix.python-version }}"
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: "Install dependencies"
run: |
uv run python -VV
uv sync --locked
- name: "Run the tests"
run: uv run pytest -v

check:
name: "lint and type check"
runs-on: "ubuntu-24.04"
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
python-version: "3.12"
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: "Install dependencies"
run: uv sync --locked
- name: "Check formatting and lint"
run: |
uv run ruff format --check .
uv run ruff check .
- name: "Type check"
run: uv run mypy .
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Stuff I want to look at but not commit
ignore/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down Expand Up @@ -30,8 +33,10 @@ MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
# Ours is checked in, since it is the build recipe.
*.manifest
*.spec
!strat-select.spec

# Installer logs
pip-log.txt
Expand Down
4 changes: 3 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ repos:
- id: detect-private-key

# Run the Ruff linter.
# keep this in step with the ruff dev dependency, or `just check` and this hook
# disagree about the same file
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.1
rev: v0.15.12
hooks:
# Linter
- id: ruff
Expand Down
43 changes: 30 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Strat App
=========

[![Test Status](https://github.com/sortitionfoundation/stratification-app/workflows/tests/badge.svg?branch=master)](https://github.com/sortitionfoundation/stratification-app/actions?workflow=tests)
[![Test Status](https://github.com/sortitionfoundation/stratification-app/actions/workflows/run-tests.yml/badge.svg?branch=main)](https://github.com/sortitionfoundation/stratification-app/actions/workflows/run-tests.yml)

A simple GUI for stratification for sortition/citizens' assemblies.

Expand All @@ -22,40 +22,57 @@ Other relevant papers:
Development
-----------

The app is built using [eel](https://github.com/ChrisKnott/Eel) - a framework that allows the GUI to be defined in HTML and CSS, but then some basic JavaScript can call Python and I can do the heavy lifting in Python.
The GUI is built with [PySide6](https://doc.qt.io/qtforpython-6/) - native Qt widgets, in
Python. There is no HTML, no JavaScript and no embedded browser.

### Install for development

First you need to have the following installed:

- git
- python 3.11 or 3.12
- a recent version of Chrome or Chromium
- `uv` - see <https://docs.astral.sh/uv/>
- `just` - see <https://just.systems/> (optional, but the recipes below assume it)

### Running in development

When you first set up a development version, you need to clone this repo, open a terminal in the root of the repo and run:

``` sh
uv run python script.py
just run
```

At this point you should have a window pop up and be able to interact with it, either via uploading .csv files or else by reading directly from a google sheet.
At this point you should have a window pop up and be able to interact with it, either by
choosing .csv files or else by reading directly from a google sheet.

As you update the repo or want to re-run, the above command is all you need.

### Key files
### Tests and checks

The python command *python script.py* requires only:
``` sh
just test # unit, integration and end to end
just check # ruff format, ruff lint, mypy
```

The tests run headless - `tests/conftest.py` sets Qt's offscreen platform - so they need no
display, and none of them touch the network or a real Google Spreadsheet.

- script.py
- stratification.py
- all of the files in the folder *web*
### Layout

```
strat_app/
__main__.py # QApplication and the window
settings_holder.py # reads sf_stratification_settings.toml
sessions/ # all the logic, importing neither Qt nor any GUI library
qt/ # the widgets, which are the only thing that imports PySide6
main.py # the entry point PyInstaller builds from
```

To use `uv` (as we suggest above) to control dependencies, you need *pyproject.toml*
The split is the point: `sessions/` talks to the GUI through the protocols in
`sessions/view.py`, so all of it can be tested without a window. `qt/` implements those
protocols and does nothing else.

To create exectuables using pyinstaller (as we describe below), you need `hook-mip.py` to deal with [this error](https://github.com/coin-or/python-mip/issues/198).
To create executables using pyinstaller (as we describe below), you need `hook-mip.py` to deal with [this error](https://github.com/coin-or/python-mip/issues/198).

Executables
-----------
Expand All @@ -68,7 +85,7 @@ If you want to make an executable yourself, use [PyInstaller](https://pyinstalle

``` sh
git pull
uv run python -m eel script.py web --additional-hooks-dir=. --onefile --noconsole
uv run pyinstaller strat-select.spec
```

The resulting executable will work on any computer running the same operating system as yours, i.e. Windows, Mac OS or Linux. So if you run the above command on Linux, you can give the file to someone else running Linux. If the person who wants the app is running Windows, you need to run the above command on Windows.
4 changes: 3 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ The app will let you search for the Spreadsheet by name - it should have a uniqu

You can select what the names of the input tabs are - the defaults are "Categories" and "Respondents".

It will create new tabs, with the default names (if they don't already exist) of "X" and "Y". **TODO:** fix this line.
It will create new tabs for the output, named "Original Selected - output - N" and
"Remaining - output - N". `N` is the lowest number for which neither tab already exists, so
running a selection twice does not overwrite the first run's output.

## Config Files

Expand Down
21 changes: 21 additions & 0 deletions fixtures/categories_no_flex.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
category,name,min,max
gender,Female,11,12
gender,Male,10,11
gender,Non-binary or other,0,1
age_bracket,0-15,0,0
age_bracket,16-29,5,7
age_bracket,30-44,5,7
age_bracket,45-59,5,6
age_bracket,60+,5,6
edu_level,No qualifications,5,7
edu_level,Level 1,5,7
edu_level,Level 2 or 3,5,6
edu_level,Level 4 and above,5,6
geo_bucket,Central Scotland,3,5
geo_bucket,Glasgow,3,5
geo_bucket,Highlands and Islands,2,5
geo_bucket,Lothian,3,5
geo_bucket,Mid Scotland and Fife,3,5
geo_bucket,North East Scotland,2,5
geo_bucket,South Scotland,3,5
geo_bucket,West Scotland,3,5
Loading