This guide takes you from a fresh checkout to a published oxilite release on PyPI. The package lives in
bindings/python. For its API, see python.md.
It covers five things:
- Setting up a development environment.
- Building and testing the package.
- Building release wheels locally.
- The one-time PyPI setup.
- Releasing, either automatically from a tag or by hand.
| Piece | Where | Role |
|---|---|---|
Native module oxilite._native |
bindings/python/src/lib.rs (Rust crate oxilite-python, PyO3) |
Wraps oxilite::blocking::Store, exchanging JSON with Python |
Python package oxilite |
bindings/python/python/oxilite/ |
pyoxigraph's API: terms, formats, results, Store, typed extension results |
| Type stubs | python/oxilite/_native.pyi, py.typed |
Strict type checking for users |
| Build configuration | bindings/python/pyproject.toml |
maturin build backend; the version comes from the Cargo workspace |
| Tests | bindings/python/tests/ |
pyoxigraph's test suite (ported verbatim) and oxilite's own tests |
| CI | .github/workflows/ci.yml, job python |
Builds, tests and type-checks on Linux and macOS for every push |
| Release | .github/workflows/python-wheels.yml |
Builds every wheel and the sdist; publishes on v* tags |
The wheels use CPython's stable ABI (abi3, Python 3.9 and later), so each platform needs one wheel,
whatever the Python version. SQLite is compiled into the wheel, and the HTTP client used for JSON-LD
contexts uses rustls, so a wheel needs no system library.
You need three things:
- Rust: the stable toolchain from rustup. The workspace needs rustc 1.88 or later.
- Python: 3.9 or later. Use a recent one for development, such as 3.12.
- A C compiler, for the bundled SQLite: Xcode command-line tools on macOS,
build-essentialon Debian or Ubuntu, or the Visual Studio Build Tools on Windows.
git clone https://github.com/Volland/oxilite.git
cd oxilite/bindings/python
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install maturin pytest mypyTwo
rustcs on one machine. If anotherrustcis also installed (for example Homebrew's), put rustup's first on yourPATHwithexport PATH="$HOME/.cargo/bin:$PATH". Otherwise the build can fail withrustc … is not supported.
-
Build the package into the virtual environment in editable mode:
maturin develop # debug build: fast to compile maturin develop --release # optimized build, for benchmarks
After changing Python files, you need not rebuild. After changing
src/lib.rs, runmaturin developagain. -
Run the tests:
python -m pytest
Expect about 130 passing tests and three expected failures (
xfail). Those three are the pyoxigraph tests listed aspy:entries intestsuite/allowlist.toml. The allow-list is strict in both directions:- an unlisted failure fails the run
- a listed test that starts passing also fails the run, until you remove its entry
To run the system-SQLite test on macOS, point it at the library:
OXILITE_SQLITE_LIBRARY=/usr/lib/libsqlite3.dylib python -m pytest
The run includes
tests/test_examples.py, which runs every script inexamples/python/, the tutorial'sexamples/python-tour/tour.py, and the code blocks of the package README (the page PyPI shows), so an API change that breaks an example fails the tests. -
Type-check:
mypy --strict python/oxilite
-
Lint the Rust side from the repository root:
cargo fmt -p oxilite-python cargo clippy -p oxilite-python --all-targets
CI runs the same steps in the python job of .github/workflows/ci.yml.
cd bindings/python
maturin build --release --out dist # the wheel for this machine: dist/oxilite-X.Y.Z-cp39-abi3-<platform>.whl
maturin sdist --out dist # the source distribution: dist/oxilite-X.Y.Z.tar.gzCheck the wheel in a clean environment, preferably on the oldest supported Python:
python3.9 -m venv /tmp/oxilite-check
/tmp/oxilite-check/bin/pip install dist/oxilite-*.whl
/tmp/oxilite-check/bin/python -c "import oxilite; s = oxilite.Store(); s.update('INSERT DATA { <urn:a> <urn:b> <urn:c> }'); print(len(s), oxilite.__version__)"Wheels for other platforms are built by the release workflow. maturin can cross-compile (--target,
--zig), but the workflow is the tested path.
The release workflow publishes with PyPI trusted publishing: PyPI trusts this repository's workflow, and no API token is stored anywhere.
-
Create the accounts. You need an account on pypi.org with two-factor authentication. For rehearsals, create one on test.pypi.org too.
-
Register the trusted publisher. The project does not exist yet, so add a pending publisher. On PyPI, go to Your account → Publishing → Add a new pending publisher and fill in:
Field Value PyPI Project Name oxiliteOwner VollandRepository name oxiliteWorkflow name python-wheels.ymlEnvironment name pypi -
Create the environment. In the GitHub repository, go to Settings → Environments → New environment and create
pypi. Under Deployment branches and tags, choose Selected branches and tags and allow the tag patternv*and the branchmain, so only release tags and manual runs onmaincan publish. As an extra safety catch, add yourself as a required reviewer: each publish then waits for your approval in the Actions tab.The same with the GitHub CLI:
gh api -X PUT repos/Volland/oxilite/environments/pypi \ --input - <<< '{"deployment_branch_policy": {"protected_branches": false, "custom_branch_policies": true}}' gh api -X POST repos/Volland/oxilite/environments/pypi/deployment-branch-policies -f name='v*' -f type=tag gh api -X POST repos/Volland/oxilite/environments/pypi/deployment-branch-policies -f name=main -f type=branch
After the first successful upload, the pending publisher becomes a normal publisher of the oxilite
project.
A release is tagged in the usual way:
- Bump the workspace version in the root
Cargo.toml, along with the internal dependency versions and the npm packages. - Commit "Release X.Y.Z".
- Create an annotated tag
vX.Y.Zand push it.
The package version is dynamic in pyproject.toml: maturin reads it from the Cargo workspace, so there
is no Python version to bump.
Pushing the tag starts python-wheels.yml:
-
wheelsbuilds six wheels:manylinuxx86_64 and aarch64musllinuxx86_64- macOS x86_64 and arm64
- Windows x64
It installs the Linux x86_64, macOS arm64 and Windows wheels and runs a smoke test with each.
-
sdistbuilds the source distribution. -
publishdownloads every artifact and uploads it to PyPI through trusted publishing. If you added a required reviewer, approve the deployment in the Actions tab.
Check the result at https://pypi.org/project/oxilite/, then run pip install oxilite==X.Y.Z in a clean
environment.
A tag cannot be rebuilt with a fix, and moving a pushed tag is bad practice. If a wheel fails on the tag
(so nothing was published), fix it on main, then run the workflow by hand from main (Actions → Python
wheels → Run workflow, branch main) with Publish checked, or:
gh workflow run python-wheels.yml --ref main -f publish=trueThe version still comes from the Cargo workspace, so this publishes the tagged version with the fix. Do it
only while main has not moved on to the next version's changes in the Python package or its crates.
Run the workflow by hand (Actions → Python wheels → Run workflow) with Publish unchecked. It builds and uploads every wheel as a workflow artifact without publishing. To rehearse the upload itself:
-
Download the artifacts.
-
Upload them to TestPyPI with twine and a TestPyPI token:
pip install twine twine upload --repository testpypi dist/* pip install --index-url https://test.pypi.org/simple/ oxilite
This path is for emergencies, or for a platform the workflow does not build.
-
Create an API token under Account settings → API tokens on PyPI, scoped to the
oxiliteproject. -
Run:
cd bindings/python export MATURIN_PYPI_TOKEN=pypi-… # never commit it maturin publish # builds this platform's wheel and the sdist, then uploads both
Or upload wheels you built elsewhere:
twine upload dist/*
PyPI refuses a second upload of the same file name. A version cannot be replaced, only yanked, so a fix means a new version.
| Symptom | Cause and fix |
|---|---|
rustc 1.87.0 is not supported |
An older rustc is first on PATH. Use rustup's (export PATH="$HOME/.cargo/bin:$PATH") |
maturin develop says it needs a virtualenv |
Activate the venv first, or use maturin build and pip install the wheel |
ImportError: … _native after pulling |
The native module is stale. Run maturin develop again |
| A pyoxigraph test fails in the port | Fix the behaviour, or, if the difference is intended, add a py: entry with a reason and a decision to testsuite/allowlist.toml |
readme path … does not exist during maturin sdist |
Every workspace crate needs the README its manifest names. The sdist includes the workspace |
The publish job waits, then fails with Branch "…" is not allowed to deploy to pypi |
The run is not on a v* tag or main. Run it from main |
The publish job fails with invalid-publisher |
The trusted publisher on PyPI does not match. Check the owner, repository, workflow file name and environment (pypi) |
File already exists on upload |
That version is already on PyPI. Bump the version |
ARM assembler must define __ARM_ARCH (ring) in the aarch64 wheel |
manylinux2014's aarch64 cross compiler is too old for ring. Build that wheel with manylinux: "2_28" (glibc 2.28 or later) |