brrag 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brrag-0.0.2/.github/workflows/check.yml +65 -0
- brrag-0.0.2/.github/workflows/release.yml +223 -0
- brrag-0.0.2/.gitignore +33 -0
- brrag-0.0.2/LICENSE +43 -0
- brrag-0.0.2/MLDRRoadFactors2025.xlsm +0 -0
- brrag-0.0.2/Makefile +89 -0
- brrag-0.0.2/PKG-INFO +200 -0
- brrag-0.0.2/README.md +171 -0
- brrag-0.0.2/assets/brrag-0.0.2-excel.zip +0 -0
- brrag-0.0.2/brrag/__init__.py +42 -0
- brrag-0.0.2/brrag/cli.py +161 -0
- brrag-0.0.2/brrag/core.py +273 -0
- brrag-0.0.2/brrag/data/mldr_road.json +17951 -0
- brrag-0.0.2/brrag/ui.py +72 -0
- brrag-0.0.2/brrag.egg-info/PKG-INFO +200 -0
- brrag-0.0.2/brrag.egg-info/SOURCES.txt +69 -0
- brrag-0.0.2/brrag.egg-info/dependency_links.txt +1 -0
- brrag-0.0.2/brrag.egg-info/entry_points.txt +2 -0
- brrag-0.0.2/brrag.egg-info/scm_file_list.json +66 -0
- brrag-0.0.2/brrag.egg-info/scm_version.json +8 -0
- brrag-0.0.2/brrag.egg-info/top_level.txt +1 -0
- brrag-0.0.2/data/mldr_road.json +17951 -0
- brrag-0.0.2/data/source_sha256.txt +4 -0
- brrag-0.0.2/docs/01-concepts.md +145 -0
- brrag-0.0.2/docs/02-tables-2025.md +168 -0
- brrag-0.0.2/docs/03-algorithm.md +237 -0
- brrag-0.0.2/docs/04-methodology.md +248 -0
- brrag-0.0.2/docs/05-provenance.md +155 -0
- brrag-0.0.2/docs/06-edge-cases.md +88 -0
- brrag-0.0.2/docs/07-implementation.md +168 -0
- brrag-0.0.2/docs/08-validation.md +132 -0
- brrag-0.0.2/docs/09-history.md +90 -0
- brrag-0.0.2/docs/10-excel-addin.md +90 -0
- brrag-0.0.2/docs/11-releasing.md +155 -0
- brrag-0.0.2/docs/glossary.md +96 -0
- brrag-0.0.2/docs/index.md +43 -0
- brrag-0.0.2/docs/references.md +71 -0
- brrag-0.0.2/page.html +829 -0
- brrag-0.0.2/pyproject.toml +55 -0
- brrag-0.0.2/requirements-tools.txt +11 -0
- brrag-0.0.2/setup.cfg +4 -0
- brrag-0.0.2/tools/extract_tables.py +73 -0
- brrag-0.0.2/tools/js_oracle.js +133 -0
- brrag-0.0.2/tools/render_wiki.py +341 -0
- brrag-0.0.2/tools/reproduce_curve.py +150 -0
- brrag-0.0.2/tools/sync_package_data.py +52 -0
- brrag-0.0.2/tools/verify.py +251 -0
- brrag-0.0.2/tools/wheel_smoke.py +97 -0
- brrag-0.0.2/xll/Makefile +124 -0
- brrag-0.0.2/xll/README.md +239 -0
- brrag-0.0.2/xll/docs/ad-hoc-tables.md +183 -0
- brrag-0.0.2/xll/docs/framework-patches.md +357 -0
- brrag-0.0.2/xll/docs/functions.md +249 -0
- brrag-0.0.2/xll/docs/tables-and-caching.md +116 -0
- brrag-0.0.2/xll/examples/age-grading-sample.xlsx +0 -0
- brrag-0.0.2/xll/examples/build_sample_workbook.py +1810 -0
- brrag-0.0.2/xll/examples/tables/bellville-vets.csv +3 -0
- brrag-0.0.2/xll/excel4_compat.cpp +217 -0
- brrag-0.0.2/xll/patches/xll-mingw-portability.patch +161 -0
- brrag-0.0.2/xll/road_running_age_grading.def +33 -0
- brrag-0.0.2/xll/src/ag_api.cpp +418 -0
- brrag-0.0.2/xll/src/ag_api.h +96 -0
- brrag-0.0.2/xll/src/ag_core.cpp +1017 -0
- brrag-0.0.2/xll/src/ag_core.h +237 -0
- brrag-0.0.2/xll/src/ag_tables_generated.h +2167 -0
- brrag-0.0.2/xll/src/road_running_age_grading.cpp +407 -0
- brrag-0.0.2/xll/test/expected.h +487 -0
- brrag-0.0.2/xll/test/test_main.cpp +575 -0
- brrag-0.0.2/xll/tools/apply-xll-patches.sh +45 -0
- brrag-0.0.2/xll/tools/gen_expectations.py +100 -0
- brrag-0.0.2/xll/tools/gen_tables.py +196 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: check
|
|
2
|
+
|
|
3
|
+
# Proves three things on every push:
|
|
4
|
+
# 1. the committed data/mldr_road.json really is what the published calculator
|
|
5
|
+
# page says (re-extracted and diffed);
|
|
6
|
+
# 2. the published artefacts have not changed (SHA-256);
|
|
7
|
+
# 3. the implementation still reproduces the calculator and the workbooks.
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
branches: [main]
|
|
12
|
+
pull_request:
|
|
13
|
+
schedule:
|
|
14
|
+
# monthly: tells us when the upstream tables or calculator change
|
|
15
|
+
- cron: "0 6 1 * *"
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
check:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: "3.12"
|
|
27
|
+
|
|
28
|
+
- name: Install tool dependencies
|
|
29
|
+
run: |
|
|
30
|
+
python -m pip install --quiet --upgrade pip
|
|
31
|
+
python -m pip install --quiet -r requirements-tools.txt
|
|
32
|
+
|
|
33
|
+
- name: Fetch sources (calculator page, workbook, upstream tables repo)
|
|
34
|
+
run: make fetch
|
|
35
|
+
|
|
36
|
+
- name: Re-extract the factor tables and require a clean diff
|
|
37
|
+
run: |
|
|
38
|
+
make data
|
|
39
|
+
if ! git diff --exit-code -- data/mldr_road.json; then
|
|
40
|
+
echo "::error file=data/mldr_road.json::Published tables changed since data/ was extracted"
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
- name: Validate (exact grid, JS oracle, standards cross-check)
|
|
45
|
+
run: make verify
|
|
46
|
+
|
|
47
|
+
- name: Reproduce the fitted curves from their parameters
|
|
48
|
+
run: make curve
|
|
49
|
+
|
|
50
|
+
- name: Check documentation links
|
|
51
|
+
run: make check-links
|
|
52
|
+
|
|
53
|
+
# The Excel add-in's portable core: table lookup, vector handling, both
|
|
54
|
+
# ad hoc table layouts, caching. Building the .xll itself needs mingw-w64
|
|
55
|
+
# and the mingw-patched xll library, so that stays a local step.
|
|
56
|
+
- name: Excel add-in — generated files up to date
|
|
57
|
+
run: |
|
|
58
|
+
python3 xll/tools/gen_tables.py --check
|
|
59
|
+
python3 xll/tools/gen_expectations.py --check
|
|
60
|
+
|
|
61
|
+
- name: Excel add-in — core test suite
|
|
62
|
+
run: make xll-test
|
|
63
|
+
|
|
64
|
+
- name: Excel add-in — sample workbook up to date
|
|
65
|
+
run: python3 xll/examples/build_sample_workbook.py --check
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# A tag starting with v is the whole release procedure. The version is not in
|
|
4
|
+
# any file: setuptools-scm derives it from the tag, so `git tag v0.0.2` publishes
|
|
5
|
+
# 0.0.2 and an untagged commit cannot ship by accident.
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
tags: ["v*"]
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
inputs:
|
|
12
|
+
note:
|
|
13
|
+
description: "Optional note. A dispatch builds and tests but publishes nothing (no tag)."
|
|
14
|
+
required: false
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
concurrency:
|
|
20
|
+
group: release-${{ github.ref }}
|
|
21
|
+
cancel-in-progress: false
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
build:
|
|
25
|
+
name: build and test the distribution
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
outputs:
|
|
28
|
+
version: ${{ steps.version.outputs.version }}
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
with:
|
|
32
|
+
# setuptools-scm needs the tags, so a shallow clone is not enough.
|
|
33
|
+
fetch-depth: 0
|
|
34
|
+
|
|
35
|
+
- uses: actions/setup-python@v5
|
|
36
|
+
with:
|
|
37
|
+
python-version: "3.12"
|
|
38
|
+
|
|
39
|
+
- name: Install build tools
|
|
40
|
+
run: python -m pip install --upgrade build
|
|
41
|
+
|
|
42
|
+
- name: Build sdist and wheel
|
|
43
|
+
run: python -m build
|
|
44
|
+
|
|
45
|
+
- name: The version must be the tag
|
|
46
|
+
id: version
|
|
47
|
+
run: |
|
|
48
|
+
set -euo pipefail
|
|
49
|
+
built=$(ls dist/*.whl | sed -E 's|^dist/brrag-([^-]+)-.*$|\1|')
|
|
50
|
+
echo "built $built"
|
|
51
|
+
echo "version=$built" >> "$GITHUB_OUTPUT"
|
|
52
|
+
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
|
|
53
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
54
|
+
if [ "$built" != "$tag" ]; then
|
|
55
|
+
echo "::error::tag is $GITHUB_REF_NAME but the build is $built - bump or move the tag"
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
58
|
+
else
|
|
59
|
+
echo "::notice::not a tag, so nothing will be published (version $built)"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
- name: The wheel must carry its own tables
|
|
63
|
+
run: |
|
|
64
|
+
set -euo pipefail
|
|
65
|
+
python -m venv /tmp/wheeltest
|
|
66
|
+
/tmp/wheeltest/bin/pip install --quiet --disable-pip-version-check dist/*.whl
|
|
67
|
+
# Run from outside the checkout: brrag must not fall back to ../data.
|
|
68
|
+
cd /tmp
|
|
69
|
+
/tmp/wheeltest/bin/python "$GITHUB_WORKSPACE/tools/wheel_smoke.py" "${{ steps.version.outputs.version }}"
|
|
70
|
+
|
|
71
|
+
- name: Install the mingw cross-compiler
|
|
72
|
+
run: |
|
|
73
|
+
set -euo pipefail
|
|
74
|
+
sudo apt-get update -qq
|
|
75
|
+
sudo apt-get install -y -qq --no-install-recommends g++-mingw-w64-x86-64
|
|
76
|
+
|
|
77
|
+
- name: Check out the xll framework this build is pinned to
|
|
78
|
+
run: |
|
|
79
|
+
set -euo pipefail
|
|
80
|
+
# Pinned to the commit xll/patches/xll-mingw-portability.patch applies to:
|
|
81
|
+
# a moving upstream would silently change the .xll we ship.
|
|
82
|
+
XLL_DIR="$HOME/code/xll"
|
|
83
|
+
mkdir -p "$XLL_DIR"
|
|
84
|
+
git -C "$XLL_DIR" init -q
|
|
85
|
+
git -C "$XLL_DIR" remote add origin https://github.com/xlladdins/xll
|
|
86
|
+
git -C "$XLL_DIR" fetch -q --depth 1 origin 481178bced1022f0b36b582388b79a6eabe1a11f
|
|
87
|
+
git -C "$XLL_DIR" checkout -q FETCH_HEAD
|
|
88
|
+
git -C "$XLL_DIR" rev-parse --short HEAD
|
|
89
|
+
./xll/tools/apply-xll-patches.sh
|
|
90
|
+
|
|
91
|
+
- name: Build the add-in and audit its exports
|
|
92
|
+
run: |
|
|
93
|
+
set -euo pipefail
|
|
94
|
+
make -C xll shim
|
|
95
|
+
make -C xll xll verify
|
|
96
|
+
# The link is timestamp-free, so with THIS compiler the binary is
|
|
97
|
+
# reproducible byte for byte. Record the version, because a different
|
|
98
|
+
# mingw produces a different binary from identical sources.
|
|
99
|
+
x86_64-w64-mingw32-g++ --version | head -1
|
|
100
|
+
sha256sum xll/road_running_age_grading.xll
|
|
101
|
+
x86_64-w64-mingw32-objdump -f xll/road_running_age_grading.xll | head -3
|
|
102
|
+
strings xll/road_running_age_grading.xll | grep -c '^RRAG\.' | xargs -I{} test {} -eq 12
|
|
103
|
+
|
|
104
|
+
- name: Package the Excel bundle
|
|
105
|
+
run: |
|
|
106
|
+
set -euo pipefail
|
|
107
|
+
python -m pip install --quiet "openpyxl>=3.1"
|
|
108
|
+
# --check rebuilds it and fails if the committed copy is stale, so the
|
|
109
|
+
# asset is known to match this commit rather than merely to exist.
|
|
110
|
+
python xll/examples/build_sample_workbook.py --check
|
|
111
|
+
version="${{ steps.version.outputs.version }}"
|
|
112
|
+
mkdir -p assets
|
|
113
|
+
python - "$version" <<'PY'
|
|
114
|
+
import sys, zipfile
|
|
115
|
+
version = sys.argv[1]
|
|
116
|
+
target = f"assets/brrag-{version}-excel.zip"
|
|
117
|
+
with zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z:
|
|
118
|
+
z.write("xll/road_running_age_grading.xll", "road_running_age_grading.xll")
|
|
119
|
+
z.write("xll/examples/age-grading-sample.xlsx", "age-grading-sample.xlsx")
|
|
120
|
+
with zipfile.ZipFile(target) as z:
|
|
121
|
+
names = z.namelist()
|
|
122
|
+
for i in z.infolist():
|
|
123
|
+
print(f" {i.filename:32} {i.file_size:>9,} -> {i.compress_size:>8,}")
|
|
124
|
+
assert names == ["road_running_age_grading.xll", "age-grading-sample.xlsx"], names
|
|
125
|
+
PY
|
|
126
|
+
ls -l dist/ assets/
|
|
127
|
+
|
|
128
|
+
# Two artifacts on purpose: `dist` is exactly what twine may upload, and
|
|
129
|
+
# gh-action-pypi-publish rejects anything else in it (a stray .zip is read
|
|
130
|
+
# as an sdist: "Too many top-level members in sdist archive").
|
|
131
|
+
- name: Upload the distribution
|
|
132
|
+
uses: actions/upload-artifact@v4
|
|
133
|
+
with:
|
|
134
|
+
name: dist
|
|
135
|
+
path: dist/
|
|
136
|
+
if-no-files-found: error
|
|
137
|
+
|
|
138
|
+
- name: Upload the Excel bundle
|
|
139
|
+
uses: actions/upload-artifact@v4
|
|
140
|
+
with:
|
|
141
|
+
name: assets
|
|
142
|
+
path: assets/
|
|
143
|
+
if-no-files-found: error
|
|
144
|
+
|
|
145
|
+
github-release:
|
|
146
|
+
name: publish the GitHub release
|
|
147
|
+
needs: build
|
|
148
|
+
if: github.ref_type == 'tag'
|
|
149
|
+
runs-on: ubuntu-latest
|
|
150
|
+
permissions:
|
|
151
|
+
contents: write
|
|
152
|
+
steps:
|
|
153
|
+
- uses: actions/download-artifact@v4
|
|
154
|
+
with:
|
|
155
|
+
name: dist
|
|
156
|
+
path: dist
|
|
157
|
+
|
|
158
|
+
- uses: actions/download-artifact@v4
|
|
159
|
+
with:
|
|
160
|
+
name: assets
|
|
161
|
+
path: assets
|
|
162
|
+
|
|
163
|
+
- name: Checksums
|
|
164
|
+
run: |
|
|
165
|
+
set -euo pipefail
|
|
166
|
+
# Named explicitly: `sha256sum *` would include the file being written.
|
|
167
|
+
sha256sum dist/* assets/* > SHA256SUMS
|
|
168
|
+
cat SHA256SUMS
|
|
169
|
+
|
|
170
|
+
- name: Create the release
|
|
171
|
+
env:
|
|
172
|
+
GH_TOKEN: ${{ github.token }}
|
|
173
|
+
GH_REPO: ${{ github.repository }}
|
|
174
|
+
run: |
|
|
175
|
+
set -euo pipefail
|
|
176
|
+
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
|
|
177
|
+
echo "::error::a release for $GITHUB_REF_NAME already exists - bump the version instead of reusing a tag"
|
|
178
|
+
exit 1
|
|
179
|
+
fi
|
|
180
|
+
gh release create "$GITHUB_REF_NAME" dist/* assets/* SHA256SUMS \
|
|
181
|
+
--title "$GITHUB_REF_NAME" \
|
|
182
|
+
--generate-notes \
|
|
183
|
+
--verify-tag
|
|
184
|
+
gh release view "$GITHUB_REF_NAME" --json url,assets \
|
|
185
|
+
--jq '.url, (.assets[] | " " + .name + " " + (.size|tostring) + " B")'
|
|
186
|
+
|
|
187
|
+
pypi:
|
|
188
|
+
name: publish to PyPI
|
|
189
|
+
needs: build
|
|
190
|
+
# Trusted publishing: no token, no secret - PyPI verifies this workflow's
|
|
191
|
+
# OIDC identity instead. Set the repository variable PYPI_ENABLED=true once
|
|
192
|
+
# the pending publisher exists on pypi.org; see docs/11-releasing.md.
|
|
193
|
+
if: github.ref_type == 'tag' && vars.PYPI_ENABLED == 'true'
|
|
194
|
+
runs-on: ubuntu-latest
|
|
195
|
+
environment:
|
|
196
|
+
name: pypi
|
|
197
|
+
url: https://pypi.org/p/brrag
|
|
198
|
+
permissions:
|
|
199
|
+
id-token: write
|
|
200
|
+
steps:
|
|
201
|
+
- uses: actions/download-artifact@v4
|
|
202
|
+
with:
|
|
203
|
+
name: dist
|
|
204
|
+
path: dist
|
|
205
|
+
|
|
206
|
+
- name: Publish
|
|
207
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
208
|
+
|
|
209
|
+
- name: Prove it landed
|
|
210
|
+
run: |
|
|
211
|
+
set -euo pipefail
|
|
212
|
+
version="${{ needs.build.outputs.version }}"
|
|
213
|
+
for _ in $(seq 1 12); do
|
|
214
|
+
if curl -fsS "https://pypi.org/pypi/brrag/${version}/json" >/dev/null; then
|
|
215
|
+
echo "brrag ${version} is on PyPI"
|
|
216
|
+
curl -fsS "https://pypi.org/pypi/brrag/${version}/json" \
|
|
217
|
+
| python -c 'import json,sys; d=json.load(sys.stdin); print("\n".join(" "+u["filename"] for u in d["urls"]))'
|
|
218
|
+
exit 0
|
|
219
|
+
fi
|
|
220
|
+
sleep 5
|
|
221
|
+
done
|
|
222
|
+
echo "::error::PyPI does not list brrag ${version} yet"
|
|
223
|
+
exit 1
|
brrag-0.0.2/.gitignore
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# virtualenvs and caches
|
|
2
|
+
.venv/
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.pyc
|
|
5
|
+
|
|
6
|
+
# unzipped copies of source artefacts (regenerate with `make unpack`)
|
|
7
|
+
raw/
|
|
8
|
+
|
|
9
|
+
# generated documentation HTML (regenerate with `make wiki`)
|
|
10
|
+
wiki/
|
|
11
|
+
|
|
12
|
+
# upstream clone for the standards cross-check (regenerate with `make fetch`)
|
|
13
|
+
repo/
|
|
14
|
+
|
|
15
|
+
# extracted text of upstream PDFs
|
|
16
|
+
notes/
|
|
17
|
+
|
|
18
|
+
# editable-install metadata
|
|
19
|
+
*.egg-info/
|
|
20
|
+
|
|
21
|
+
# build outputs (`make dist`); releases carry them, git does not
|
|
22
|
+
dist/
|
|
23
|
+
build/
|
|
24
|
+
|
|
25
|
+
# --- Excel add-in build artefacts (xll/) -----------------------------------
|
|
26
|
+
xll/*.o
|
|
27
|
+
xll/*.xll
|
|
28
|
+
xll/road_running_age_grading_test
|
|
29
|
+
xll_test_tmp/
|
|
30
|
+
# case-fixing header symlinks, created by `make -C xll shim`
|
|
31
|
+
xll/shim/
|
|
32
|
+
# The generated sources ARE committed on purpose (a Windows/MSVC user must not
|
|
33
|
+
# need Python): xll/src/ag_tables_generated.h and xll/test/expected.h.
|
brrag-0.0.2/LICENSE
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Francois Botha
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
SCOPE
|
|
26
|
+
|
|
27
|
+
This licence covers the code in this repository: the `brrag/` package, the
|
|
28
|
+
`tools/` scripts and the prose in `docs/` and `README.md`.
|
|
29
|
+
|
|
30
|
+
It does NOT cover the age-grading tables or the source artefacts. Those are the
|
|
31
|
+
work of others:
|
|
32
|
+
|
|
33
|
+
* age-grade factors and age standards — compiled by Alan Jones
|
|
34
|
+
(alanjones@grests.com) with single-age bests by Tom Bernhard, approved by
|
|
35
|
+
the Masters Long Distance Running Council of USA Track & Field.
|
|
36
|
+
Upstream: https://github.com/AlanLyttonJones/Age-Grade-Tables
|
|
37
|
+
* the calculator page (`page.html`) and the factors workbook
|
|
38
|
+
(`MLDRRoadFactors2025.xlsm`) — Howard Grubb,
|
|
39
|
+
https://www.howardgrubb.co.uk/athletics/
|
|
40
|
+
|
|
41
|
+
They are included here unmodified, for reference and reproducibility, with
|
|
42
|
+
SHA-256 hashes recorded in `data/source_sha256.txt`. Cite the upstream sources,
|
|
43
|
+
not this repository, when publishing graded results.
|
|
Binary file
|
brrag-0.0.2/Makefile
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Age-grading docs + reference implementation.
|
|
2
|
+
# `make check` is what CI (or a future table revision) should run.
|
|
3
|
+
|
|
4
|
+
PY ?= $(shell [ -x .venv/bin/python ] && echo .venv/bin/python || echo python3)
|
|
5
|
+
UPSTREAM := https://github.com/AlanLyttonJones/Age-Grade-Tables.git
|
|
6
|
+
|
|
7
|
+
.PHONY: help all setup fetch unpack data pkgdata dist verify curve wiki check check-links xll-test sample clean
|
|
8
|
+
|
|
9
|
+
help:
|
|
10
|
+
@echo "setup create .venv and install tool dependencies"
|
|
11
|
+
@echo "fetch download the source artefacts (calculator page, xlsm, upstream clone)"
|
|
12
|
+
@echo "unpack unzip the xlsm (sheet XML + vbaProject.bin) into raw/"
|
|
13
|
+
@echo "data re-extract factor tables from page.html -> data/mldr_road.json"
|
|
14
|
+
@echo "pkgdata copy data/mldr_road.json into the package, so a wheel ships its tables"
|
|
15
|
+
@echo "dist build the sdist and wheel into dist/"
|
|
16
|
+
@echo "verify run checks A/B/C (exact grid, JS oracle, standards cross-check)"
|
|
17
|
+
@echo "curve reproduce factor curves from the fitted parameters"
|
|
18
|
+
@echo "wiki render docs/*.md -> wiki/*.html"
|
|
19
|
+
@echo "check verify + curve + wiki link check <-- the CI target"
|
|
20
|
+
@echo "check-links verify every docs/ cross-page link and anchor"
|
|
21
|
+
@echo "xll-test verify the Excel add-in core (needs g++ only)"
|
|
22
|
+
@echo "sample regenerate the sample workbook (needs openpyxl)"
|
|
23
|
+
@echo "all fetch + unpack + data + verify + curve + wiki"
|
|
24
|
+
|
|
25
|
+
all: fetch unpack data verify curve wiki
|
|
26
|
+
@echo OK
|
|
27
|
+
|
|
28
|
+
setup:
|
|
29
|
+
uv venv --python 3.12 .venv
|
|
30
|
+
uv pip install --python .venv/bin/python -r requirements-tools.txt
|
|
31
|
+
|
|
32
|
+
fetch:
|
|
33
|
+
@echo "-> calculator page"
|
|
34
|
+
curl -sS -o page.html https://www.howardgrubb.co.uk/athletics/mldrroad25.html
|
|
35
|
+
@echo "-> factors workbook"
|
|
36
|
+
curl -sS -o MLDRRoadFactors2025.xlsm https://www.howardgrubb.co.uk/athletics/data/MLDRRoadFactors2025.xlsm
|
|
37
|
+
@echo "-> upstream tables repository (needed by check C)"
|
|
38
|
+
test -d repo || git clone --depth 1 $(UPSTREAM) repo
|
|
39
|
+
@echo "-> verifying hashes"; sha256sum -c data/source_sha256.txt
|
|
40
|
+
|
|
41
|
+
unpack:
|
|
42
|
+
rm -rf raw && mkdir -p raw && cd raw && unzip -o ../MLDRRoadFactors2025.xlsm >/dev/null
|
|
43
|
+
@echo "raw/ holds the sheet XML and xl/vbaProject.bin; dump the macros with:"
|
|
44
|
+
@echo " .venv/bin/olevba raw/xl/vbaProject.bin"
|
|
45
|
+
|
|
46
|
+
data:
|
|
47
|
+
$(PY) tools/extract_tables.py page.html data/mldr_road.json
|
|
48
|
+
$(PY) tools/sync_package_data.py
|
|
49
|
+
|
|
50
|
+
pkgdata:
|
|
51
|
+
$(PY) tools/sync_package_data.py
|
|
52
|
+
|
|
53
|
+
# setuptools-scm takes the version from the git tag, so this is only meaningful
|
|
54
|
+
# (and only publishable) on a tagged commit.
|
|
55
|
+
dist:
|
|
56
|
+
$(PY) -m build
|
|
57
|
+
|
|
58
|
+
verify:
|
|
59
|
+
$(PY) tools/verify.py --all
|
|
60
|
+
|
|
61
|
+
curve:
|
|
62
|
+
$(PY) tools/reproduce_curve.py
|
|
63
|
+
|
|
64
|
+
wiki:
|
|
65
|
+
$(PY) tools/render_wiki.py
|
|
66
|
+
|
|
67
|
+
check-links:
|
|
68
|
+
$(PY) tools/render_wiki.py --check
|
|
69
|
+
|
|
70
|
+
# The add-in's portable core and test suite. Building the .xll itself needs
|
|
71
|
+
# mingw-w64 and the patched xll library; see xll/README.md.
|
|
72
|
+
xll-test:
|
|
73
|
+
$(MAKE) -C xll test
|
|
74
|
+
|
|
75
|
+
sample:
|
|
76
|
+
$(PY) xll/examples/build_sample_workbook.py
|
|
77
|
+
|
|
78
|
+
check:
|
|
79
|
+
$(PY) tools/sync_package_data.py --check && \
|
|
80
|
+
$(PY) tools/verify.py --all && \
|
|
81
|
+
$(PY) tools/reproduce_curve.py && \
|
|
82
|
+
$(PY) tools/render_wiki.py --check && \
|
|
83
|
+
$(PY) xll/tools/gen_tables.py --check && \
|
|
84
|
+
$(PY) xll/tools/gen_expectations.py --check && \
|
|
85
|
+
$(MAKE) -C xll test && \
|
|
86
|
+
$(PY) xll/examples/build_sample_workbook.py --check
|
|
87
|
+
|
|
88
|
+
clean:
|
|
89
|
+
rm -rf wiki raw
|
brrag-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: brrag
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: BRRAG - Botha's Road Running Age Grading on the USATF MLDR 2025 tables
|
|
5
|
+
Author-email: Francois Botha <igitur@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/igitur/road-running-age-grading
|
|
8
|
+
Project-URL: Documentation, https://github.com/igitur/road-running-age-grading/blob/main/docs/index.md
|
|
9
|
+
Project-URL: Source, https://github.com/igitur/road-running-age-grading
|
|
10
|
+
Project-URL: Changelog, https://github.com/igitur/road-running-age-grading/releases
|
|
11
|
+
Project-URL: Published calculator, https://www.howardgrubb.co.uk/athletics/mldrroad25.html
|
|
12
|
+
Keywords: running,age-grading,usatf,mldr,road-running,athletics
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Games/Entertainment
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# BRRAG — Botha's Road Running Age Grading
|
|
31
|
+
|
|
32
|
+
Age grading on the USATF MLDR 2025 road tables. The Python package, the
|
|
33
|
+
CLI and the PyPI distribution are called `brrag`; the repository keeps its
|
|
34
|
+
name and URL.
|
|
35
|
+
|
|
36
|
+
[](https://github.com/igitur/road-running-age-grading/actions/workflows/check.yml)
|
|
37
|
+
|
|
38
|
+
Reference documentation and a verified implementation of **road-running age
|
|
39
|
+
grading** on the USATF Masters Long Distance Running (MLDR) 2025 tables.
|
|
40
|
+
|
|
41
|
+
You give it a sex, an age, a distance and a time. It tells you the **age-graded
|
|
42
|
+
time** and the **age grade percentage** — one number that lets a 12-year-old's
|
|
43
|
+
5 km and a 72-year-old's marathon be compared on the same scale.
|
|
44
|
+
|
|
45
|
+
```console
|
|
46
|
+
$ brrag 40:00 --age 40 --sex M --distance 10k
|
|
47
|
+
factor 0.9636
|
|
48
|
+
age-graded 38:32.6
|
|
49
|
+
age grade 68.49%
|
|
50
|
+
|
|
51
|
+
$ brrag 32:00 --age 65 --sex M --distance 10k
|
|
52
|
+
factor 0.7761
|
|
53
|
+
age-graded 24:50.1
|
|
54
|
+
age grade 106.30% # above 100% = better than the age standard
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
```console
|
|
60
|
+
$ pip install brrag
|
|
61
|
+
$ brrag 40:00 --age 40 --sex M --distance 10k
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Stdlib-only, no dependencies, and the wheel carries its own tables so it works
|
|
65
|
+
offline. Versions come from the git tag; see
|
|
66
|
+
[docs/11-releasing.md](docs/11-releasing.md).
|
|
67
|
+
|
|
68
|
+
## Where to start
|
|
69
|
+
|
|
70
|
+
| If you want to… | Read |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| Understand the idea and the numbers | [docs/01-concepts.md](docs/01-concepts.md) |
|
|
73
|
+
| See what the 2025 tables contain | [docs/02-tables-2025.md](docs/02-tables-2025.md) |
|
|
74
|
+
| **Grade a time (the algorithm)** | [docs/03-algorithm.md](docs/03-algorithm.md) |
|
|
75
|
+
| Know how the tables were built | [docs/04-methodology.md](docs/04-methodology.md) |
|
|
76
|
+
| Trust or audit the data | [docs/05-provenance.md](docs/05-provenance.md) |
|
|
77
|
+
| Handle weird inputs | [docs/06-edge-cases.md](docs/06-edge-cases.md) |
|
|
78
|
+
| Write your own implementation | [docs/07-implementation.md](docs/07-implementation.md) |
|
|
79
|
+
| Check our claims | [docs/08-validation.md](docs/08-validation.md) |
|
|
80
|
+
| Know the revision history / what 2026 needs | [docs/09-history.md](docs/09-history.md) |
|
|
81
|
+
| **Grade a whole club in Excel** | [docs/10-excel-addin.md](docs/10-excel-addin.md) |
|
|
82
|
+
| Release or publish a version | [docs/11-releasing.md](docs/11-releasing.md) |
|
|
83
|
+
|
|
84
|
+
Full index: [docs/index.md](docs/index.md).
|
|
85
|
+
|
|
86
|
+
## Quick use (Python)
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from brrag import table, format_time
|
|
90
|
+
|
|
91
|
+
t = table(2025, "M") # or table(2025, "F")
|
|
92
|
+
g = t.grade("M", 40, 40 * 60, dist_km=10) # 40:00 for 10 km at age 40
|
|
93
|
+
print(g.factor, g.percent) # 0.9636 68.49
|
|
94
|
+
print(format_time(g.age_graded_s)) # 38:32.6
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
No dependencies outside the standard library for the library itself
|
|
98
|
+
(`openpyxl` is only needed by the table-extraction and verification tools).
|
|
99
|
+
|
|
100
|
+
## In Excel
|
|
101
|
+
|
|
102
|
+
`xll/road_running_age_grading.xll` is a native add-in for grading a whole squad at once:
|
|
103
|
+
|
|
104
|
+
```excel
|
|
105
|
+
=RRAG.GRADE(B2:B181, C2:C181, D2:D181, E2:E181) 180 age grades, spilled
|
|
106
|
+
=RRAG.BLOCK(B2:B181, C2:C181, D2:D181, E2:E181) factor, standards, graded time, percent
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Four editions are compiled in (2025/2020/2015/2010), the fifth argument selects
|
|
110
|
+
one or names a custom `<name>.csv` table, and a range passed there is used as an
|
|
111
|
+
ad-hoc table. Build and install: [xll/README.md](xll/README.md); design notes:
|
|
112
|
+
[docs/10-excel-addin.md](docs/10-excel-addin.md).
|
|
113
|
+
|
|
114
|
+
## Getting started
|
|
115
|
+
|
|
116
|
+
```console
|
|
117
|
+
$ git clone https://github.com/igitur/road-running-age-grading.git
|
|
118
|
+
$ cd road-running-age-grading
|
|
119
|
+
$ make setup # .venv + openpyxl/oletools/pypdf
|
|
120
|
+
$ make check # the full validation suite
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The repo ships `data/mldr_road.json`, so the library and CLI work immediately
|
|
124
|
+
with nothing but Python 3.10+. `make fetch` downloads the two source artefacts
|
|
125
|
+
and clones the upstream tables repository when you want checks B and C to run
|
|
126
|
+
(after a fresh clone they are already present, since the artefacts are committed).
|
|
127
|
+
|
|
128
|
+
## Layout
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
data/mldr_road.json 2025 (+2020/2015/2010) factors, extracted and hashed
|
|
132
|
+
data/source_sha256.txt hashes of every source artefact we read
|
|
133
|
+
brrag/ reference implementation (core.py, ui.py, cli.py)
|
|
134
|
+
tools/extract_tables.py pull factor tables out of the calculator page -> JSON
|
|
135
|
+
tools/js_oracle.js run the *published calculator's own JS* under Node
|
|
136
|
+
tools/verify.py three-way validation harness (A/B/C)
|
|
137
|
+
tools/reproduce_curve.py rebuild each factor curve from its 10 fitted parameters
|
|
138
|
+
tools/render_wiki.py docs/*.md -> wiki/*.html (no JS, no CDN)
|
|
139
|
+
xll/ native Excel add-in: columns in, age grades out
|
|
140
|
+
docs/ the documentation set (Markdown; source of truth)
|
|
141
|
+
page.html archived copy of the calculator page we read
|
|
142
|
+
MLDRRoadFactors2025.xlsm archived copy of the official factors workbook (VBA)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Generated or fetched, not committed: `wiki/` (`make wiki`), `raw/` (`make unpack`),
|
|
146
|
+
`repo/` and `notes/` (`make fetch` + `pypdf`).
|
|
147
|
+
|
|
148
|
+
`make help` lists the tasks; `make all` re-downloads the sources, re-extracts the
|
|
149
|
+
data, validates it, reproduces the curves and re-renders the wiki.
|
|
150
|
+
|
|
151
|
+
## Verify it
|
|
152
|
+
|
|
153
|
+
```console
|
|
154
|
+
$ make check
|
|
155
|
+
A. exact-event grid: 4224 lookups, 0 mismatches
|
|
156
|
+
B. JS oracle: 1412 requests, 0 mismatches, worst percent delta 0.000000 pp
|
|
157
|
+
C. standards file: 4224 cells, 0 mismatches, 18 known anomalies
|
|
158
|
+
RESULT: PASS
|
|
159
|
+
worst absolute error across the curves that match exactly: 0.0001
|
|
160
|
+
links: 0 problem(s)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Details and what each check catches: [docs/08-validation.md](docs/08-validation.md).
|
|
164
|
+
|
|
165
|
+
## Reading it as a wiki
|
|
166
|
+
|
|
167
|
+
The Markdown in `docs/` is the source of truth — it versions with the data, diffs
|
|
168
|
+
cleanly, and renders on GitHub. If you would rather click through it, render the
|
|
169
|
+
HTML copy and serve it:
|
|
170
|
+
|
|
171
|
+
```console
|
|
172
|
+
$ .venv/bin/python tools/render_wiki.py # writes wiki/*.html
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Twelve self-contained pages, a sticky sidebar, no JavaScript, no CDN, no build
|
|
176
|
+
step beyond that one command — which is why it drops straight into the existing
|
|
177
|
+
Caddy static-site setup if you want it online.
|
|
178
|
+
|
|
179
|
+
## Status of the tables
|
|
180
|
+
|
|
181
|
+
The 2025 road tables were approved by the USATF MLDR Council on **2025-01-10**;
|
|
182
|
+
the published workbooks are dated **2025-07-27**. The tables are re-fitted
|
|
183
|
+
roughly every five years (2010 → 2015 → 2020 → 2025), so treat 2025 as current
|
|
184
|
+
and expect a 2030 revision. Everything in this repo is pinned to the published
|
|
185
|
+
2025 artefacts by SHA-256 — see [docs/05-provenance.md](docs/05-provenance.md).
|
|
186
|
+
|
|
187
|
+
One caveat worth knowing up front: the *working* spreadsheets shipped in the
|
|
188
|
+
upstream repo (`maleRoad2025.xlsx`, dated 2025-07-25) do **not** reproduce the
|
|
189
|
+
published male 5 km / 10 km curves. The published factor workbook, the
|
|
190
|
+
calculator and the `…Std2025.xlsx` standards files all agree with each other;
|
|
191
|
+
the male working file is one revision behind. [docs/05-provenance.md](docs/05-provenance.md)
|
|
192
|
+
has the numbers.
|
|
193
|
+
|
|
194
|
+
## Licence / attribution
|
|
195
|
+
|
|
196
|
+
Tables compiled by **Alan Jones** (alanjones@grests.com) with single-age bests
|
|
197
|
+
by **Tom Bernhard**; approved by the USATF MLDR Council. Calculator by
|
|
198
|
+
**Howard Grubb** (howardgrubb.co.uk). Upstream repository:
|
|
199
|
+
<https://github.com/AlanLyttonJones/Age-Grade-Tables>. Cite the upstream tables,
|
|
200
|
+
not this repo, when publishing results.
|