Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
144 changes: 144 additions & 0 deletions .github/workflows/build-python-bindings.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Build `libfec_parser` (crates/fec-py) wheels for the five supported platforms + an sdist.
#
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a local
# artifacts job within `cargo-dist` (registered in dist-workspace.toml as
# `local-artifacts-jobs = [..., "./build-python-bindings"]`), hence the `workflow_call` +
# `plan` input shape. `plan` is unused here — the version comes from crates/fec-py/Cargo.toml,
# which is the same workspace version the tag names (D7).
#
# cargo-dist does NOT attach custom-job artifacts to the GitHub Release; the companion
# .github/workflows/publish-python-bindings.yml does that with `gh release upload`.
#
# Platforms are Q10 (plans/python/00-decisions.md): manylinux_2_17 x86_64 + aarch64,
# macOS x86_64 + arm64, Windows x64, plus an sdist. No musllinux, no armv7, no QEMU, and
# no free-threaded builds (D8) — one `cp311-abi3` wheel per platform, no interpreter matrix.
name: "Build libfec_parser wheels"

on:
workflow_call:
inputs:
plan:
required: true
type: string

jobs:
linux:
name: linux-${{ matrix.target }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- { runner: ubuntu-22.04, target: x86_64 }
# Native arm64 runner, so `manylinux: auto` picks quay.io/pypa/manylinux2014_aarch64
# rather than a cross image, and there is no QEMU in the loop.
- { runner: ubuntu-22.04-arm, target: aarch64 }
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- name: Build wheel
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
with:
working-directory: crates/fec-py
target: ${{ matrix.target }}
# `auto` == the manylinux2014 container (glibc 2.17). This is the fix for the CLI
# wheel's mistake: building on a bare ubuntu-22.04 host tags the wheel
# manylinux_2_34, which will not install on older glibc.
manylinux: auto
# `--compatibility pypi` makes a mis-tagged wheel fail the *build*. We never upload
# to PyPI (D1), but it is the cheapest correctness check for the manylinux tag.
args: --release --out dist --compatibility pypi
maturin-version: "1.15.0"
- name: Smoke test the wheel
shell: bash
run: |
uv venv .venv-smoke --python cpython-3.11
uv pip install --python .venv-smoke crates/fec-py/dist/*.whl
uv run --python .venv-smoke --no-project python -c "from libfec_parser.parser import Filing; print(Filing('crates/fec-py/tests/fixtures/1921705.fec'))"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: wheels_libfec_parser-linux-${{ matrix.target }}
path: crates/fec-py/dist

macos:
name: macos-${{ matrix.target }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
# `macos-15-intel` is the current x86_64 macOS label (macos-13 is retired); it is
# what `maturin generate-ci github` 1.15 emits — see
# plans/python/probes/maturin-1.15-generate-ci.yml.
- { runner: macos-15-intel, target: x86_64 }
- { runner: macos-latest, target: aarch64 }
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- name: Build wheel
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
with:
working-directory: crates/fec-py
target: ${{ matrix.target }}
# No `manylinux:` key here. `--compatibility pypi` is kept: unlike the
# `manylinux*`/`musllinux*` values, `pypi` "applies on all platforms"
# (`maturin build --help`, 1.15.0), so it also rejects a macOS tag PyPI would.
args: --release --out dist --compatibility pypi
maturin-version: "1.15.0"
- name: Smoke test the wheel
shell: bash
run: |
uv venv .venv-smoke --python cpython-3.11
uv pip install --python .venv-smoke crates/fec-py/dist/*.whl
uv run --python .venv-smoke --no-project python -c "from libfec_parser.parser import Filing; print(Filing('crates/fec-py/tests/fixtures/1921705.fec'))"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: wheels_libfec_parser-macos-${{ matrix.target }}
path: crates/fec-py/dist

windows:
name: windows-${{ matrix.target }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- { runner: windows-latest, target: x64 }
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- name: Build wheel
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
with:
working-directory: crates/fec-py
target: ${{ matrix.target }}
args: --release --out dist --compatibility pypi
maturin-version: "1.15.0"
- name: Smoke test the wheel
# `shell: bash` (git-bash) so the `dist/*.whl` glob expands here the same way it
# does on the other runners; PowerShell would pass the literal string to uv.
shell: bash
run: |
uv venv .venv-smoke --python cpython-3.11
uv pip install --python .venv-smoke crates/fec-py/dist/*.whl
uv run --python .venv-smoke --no-project python -c "from libfec_parser.parser import Filing; print(Filing('crates/fec-py/tests/fixtures/1921705.fec'))"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: wheels_libfec_parser-windows-${{ matrix.target }}
path: crates/fec-py/dist

sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Build sdist
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
with:
working-directory: crates/fec-py
command: sdist
args: --out dist
maturin-version: "1.15.0"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: wheels_libfec_parser-sdist
path: crates/fec-py/dist
58 changes: 58 additions & 0 deletions .github/workflows/publish-python-bindings.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Attach the `libfec_parser` wheels built by build-python-bindings.yml to the GitHub Release.
#
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a publish
# job within `cargo-dist` (registered in dist-workspace.toml as
# `publish-jobs = [..., "./publish-python-bindings"]`), hence the `workflow_call` + `plan`
# input shape. Publish jobs run after `host`, so the Release already exists by now.
#
# Why this job exists at all: cargo-dist only puts `artifacts-*` (its own dist-manifest
# artifacts) on the Release page — verified on 0.0.31, whose assets contain no wheels even
# though build-pypi.yml runs as a local-artifacts job. Custom-job artifacts have to be
# uploaded by hand, which is what this does.
#
# Nothing here goes to PyPI (plans/python/00-decisions.md D1).
name: "Attach libfec_parser wheels to the GitHub Release"

on:
workflow_call:
inputs:
plan:
required: true
type: string

jobs:
attach:
runs-on: ubuntu-latest
permissions:
# `gh release upload` needs this. cargo-dist gives custom publish jobs
# `id-token: write` + `packages: write` by default, which *replaces* the
# workflow-level `contents: write` in release.yml, so dist-workspace.toml carries a
# `[dist.github-custom-job-permissions]` override granting `contents = "write"` to
# this job. Keep the two in sync.
contents: write
steps:
- name: Download wheels
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
# Deliberately `wheels_libfec_parser-*`, NOT `wheels_libfec-*`.
# download-artifact matches `pattern` with minimatch glob semantics, where `*`
# matches any run of characters but the literal `-` in `wheels_libfec-*` still has
# to be present: `wheels_libfec_parser-linux-x86_64` has `_` at that position, so
# publish-pypi.yml's `pattern: wheels_libfec-*` does not match these artifacts and
# the bindings can never be pushed to PyPI by that job (D1).
pattern: wheels_libfec_parser-*
path: wheels
merge-multiple: true
- name: Upload wheels to the GitHub Release
env:
GH_TOKEN: ${{ github.token }}
# `announcement_tag` is the key cargo-dist 0.30.3 puts the release tag under in
# the `dist plan` / `dist host --steps=create` JSON (verified locally:
# `dist plan --output-format=json | jq .announcement_tag` -> "v0.0.32").
TAG: ${{ fromJson(inputs.plan).announcement_tag }}
run: |
set -euo pipefail
ls -l wheels
# Fail loudly rather than silently "succeeding" with nothing attached.
test -n "$(ls -A wheels)"
gh release upload "$TAG" wheels/* --clobber --repo "$GITHUB_REPOSITORY"
29 changes: 27 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,22 @@ jobs:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit

custom-build-python-bindings:
needs:
- plan
if: ${{ needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' }}
uses: ./.github/workflows/build-python-bindings.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit

# Build and package all the platform-agnostic(ish) things
build-global-artifacts:
needs:
- plan
- build-local-artifacts
- custom-build-pypi
- custom-build-python-bindings
runs-on: "ubuntu-latest"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down Expand Up @@ -227,9 +237,10 @@ jobs:
- plan
- build-local-artifacts
- custom-build-pypi
- custom-build-python-bindings
- build-global-artifacts
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') && (needs.custom-build-pypi.result == 'skipped' || needs.custom-build-pypi.result == 'success') }}
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') && (needs.custom-build-pypi.result == 'skipped' || needs.custom-build-pypi.result == 'success') && (needs.custom-build-python-bindings.result == 'skipped' || needs.custom-build-python-bindings.result == 'success') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: "ubuntu-latest"
Expand Down Expand Up @@ -303,15 +314,29 @@ jobs:
"id-token": "write"
"packages": "write"

custom-publish-python-bindings:
needs:
- plan
- host
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
uses: ./.github/workflows/publish-python-bindings.yml
with:
plan: ${{ needs.plan.outputs.val }}
secrets: inherit
# publish jobs get escalated permissions
permissions:
"contents": "write"

announce:
needs:
- plan
- host
- custom-publish-pypi
- custom-publish-python-bindings
# use "always() && ..." to allow us to wait for all publish jobs while
# still allowing individual publish jobs to skip themselves (for prereleases).
# "host" however must run to completion, no skipping allowed!
if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') }}
if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') && (needs.custom-publish-python-bindings.result == 'skipped' || needs.custom-publish-python-bindings.result == 'success') }}
runs-on: "ubuntu-latest"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
71 changes: 58 additions & 13 deletions .github/workflows/test-python.yml
Original file line number Diff line number Diff line change
@@ -1,29 +1,74 @@
name: "test-python"
on:
push:
branches:
- main
branches: [main]
pull_request:
permissions:
contents: read
concurrency:
group: test-python-${{ github.ref }}
cancel-in-progress: true
jobs:
test-python:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, macos-15-intel, windows-latest]
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ["3.11", "3.14"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: rustup toolchain install stable --profile minimal
- uses: Swatinem/rust-cache@v2
- uses: astral-sh/setup-uv@v6
- run: uv tool install maturin
- name: Build Python wheels
run: make build
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: ". -> target"
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with:
python-version: ${{ matrix.python }}
- name: Build wheel
shell: bash
run: uvx maturin@1.15.0 build --release --out dist
working-directory: crates/fec-py
- name: Install wheel + test deps
shell: bash
working-directory: crates/fec-py
run: |
# "cpython-${{ matrix.python }}" (not bare "${{ matrix.python }}"): uv can
# otherwise resolve "3.14" to the free-threaded (3.14t) build when one happens
# to already be installed on the runner. We only build/ship the GIL build (D8),
# and free-threaded wheels install fine but crash on import (see ticket 01), so
# the assertion below fails loudly instead of leaving a confusing pytest crash.
uv venv .venv --python cpython-${{ matrix.python }}
uv run --python .venv --no-project python -c "import sysconfig, sys; sys.exit('picked the free-threaded interpreter for .venv; expected the GIL build' if sysconfig.get_config_var('Py_GIL_DISABLED') else 0)"
uv pip install --python .venv dist/*.whl pytest
- name: pytest
shell: bash
working-directory: crates/fec-py
run: uv run --python .venv --no-project pytest tests -v -rs
- name: stubtest
# One job is enough: the stubs are platform- and version-independent. This
# runs against the *installed wheel*, so it also proves the wheel ships
# py.typed and the .pyi files.
if: matrix.os == 'ubuntu-latest' && matrix.python == '3.11'
shell: bash
working-directory: crates/fec-py
run: |
uv pip install --python .venv mypy
uv run --python .venv --no-project python -m mypy.stubtest libfec_parser --allowlist stubtest-allowlist.txt
uv run --python .venv --no-project mypy --check-untyped-defs tests
- name: Execute quickstart notebook
if: matrix.os == 'ubuntu-latest' && matrix.python == '3.11'
shell: bash
working-directory: crates/fec-py
run: |
uv pip install --python .venv pandas nbconvert ipykernel
uv run --python .venv --no-project jupyter nbconvert --to notebook --execute --ExecutePreprocessor.timeout=120 --output /tmp/quickstart-out.ipynb examples/quickstart.ipynb
- name: Smoke test - construct a Filing (guards against the pyo3 0.22 3.14 segfault)
shell: bash
working-directory: crates/fec-py
- name: Upload wheels
uses: actions/upload-artifact@v4
run: uv run --python .venv --no-project python -c "from libfec_parser.parser import Filing; print(Filing('tests/fixtures/1921705.fec'))"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: matrix.python == '3.11'
with:
name: wheels-${{ matrix.os }}
name: wheel-${{ matrix.os }}
path: crates/fec-py/dist/*.whl
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ june/
*.fec.part
*.fec.gz
*.fec.zst
# committed test fixtures for the Python bindings (see crates/fec-py/tests/README.md)
!crates/fec-py/tests/fixtures/*.fec
*.db.gz
*.csv
*.zip
Expand Down
Loading
Loading