Skip to content
Merged
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
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ jobs:
key: ubuntu-cargo-hdf5-${{ hashFiles('**/Cargo.lock') }}

- name: Install HDF5
run: sudo apt-get install -y libhdf5-dev
run: |
sudo apt-get update
sudo apt-get install -y libhdf5-dev

- name: Clippy (hdf5 feature)
run: cargo clippy -p evt3-core -p evt3-cli --features hdf5 -- -D warnings
Expand All @@ -83,7 +85,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
python-version: ['3.9', '3.10', '3.11', '3.12']
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v4

Expand Down
15 changes: 8 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,12 @@ jobs:
path: ${{ matrix.name }}

build-wheels:
name: Build Python wheels - ${{ matrix.os }}
name: Build Python wheels - ${{ matrix.os }} - py${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v4

Expand All @@ -82,19 +83,19 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install maturin
run: uv tool install maturin
python-version: ${{ matrix.python-version }}

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Build wheels
working-directory: evt3-python
run: maturin build --release --out dist
run: uv tool run maturin build --release --out dist --interpreter python

- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.os }}
name: wheels-${{ matrix.os }}-py${{ matrix.python-version }}
path: evt3-python/dist/*.whl

release:
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ Or with pip:
pip install evt3
```

Published wheels target CPython 3.9 through 3.13.

> **Note:** The pip package supports `.raw` files only. HDF5 (`.h5`/`.hdf5`)
> requires building from source — see [HDF5 Inputs](#hdf5-inputs) below.

Expand All @@ -87,7 +89,7 @@ HDF5_DIR="$(brew --prefix hdf5)" cargo build --release -p evt3-cli --features hd
# Optional: Install to PATH
cp target/release/evt3 ~/.local/bin/

# Build Python package (requires uv + Rust)
# Build Python package (requires Python 3.9+, uv + Rust)
cd evt3-python
uv venv
uv pip install maturin
Expand Down
31 changes: 31 additions & 0 deletions docs/adr/0002-python-version-support-and-wheel-builds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 0002: Support CPython 3.9-3.13 With Per-Interpreter Wheels

## Status

Accepted

## Context

The package metadata still advertised Python 3.8 support even though Python 3.8
reached end of life in October 2024. At the same time, the release workflow
only built Python wheels with a single Python 3.11 interpreter on each OS, so
PyPI users on other supported versions had to build from source. Adding Python
3.13 support also requires newer PyO3 and `numpy` crate releases than the
project was using.

## Decision

- Set the supported Python range to CPython 3.9 through 3.13.
- Upgrade the Python bindings to `pyo3 = 0.23` and `numpy = 0.23`.
- Migrate the bindings to the current PyO3 Bound API required by that upgrade.
- Run Python CI coverage on 3.9, 3.10, 3.11, 3.12, and 3.13.
- Build release wheels per operating system and interpreter with
`maturin build --interpreter python`.

## Consequences

- Python 3.8 users must stay on an older release or build from an older branch.
- Release CI now runs more wheel jobs because each OS builds one wheel per
supported interpreter.
- PyPI users on supported CPython versions get prebuilt wheels instead of a
single 3.11-only release artifact set.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# ADR Index

- [0001: HDF5 File Support Through `decode_file`](./0001-hdf5-file-support.md)
- [0002: Support CPython 3.9-3.13 With Per-Interpreter Wheels](./0002-python-version-support-and-wheel-builds.md)
1 change: 1 addition & 0 deletions docs/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

- [Byte-Stream Decoding](./byte-stream-decoding.md): Incremental decoding of raw EVT3 byte streams via `Evt3Decoder::decode_bytes` and `finish_stream`.
- [HDF5 File Support](./hdf5-file-support.md): Optional `.h5`/`.hdf5` decoding through the existing `Evt3Decoder::decode_file` API.
- [Python Version Support And Wheel Builds](./python-version-support.md): CPython 3.9-3.13 support with per-interpreter wheels for release builds.
34 changes: 34 additions & 0 deletions docs/features/python-version-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Python Version Support And Wheel Builds

## Summary

The Python package now targets CPython 3.9 through 3.13. The bindings were
updated to PyO3 and `numpy` crate releases that support Python 3.13, and the
release workflow now builds wheels for every supported interpreter on Linux,
macOS, and Windows instead of only producing Python 3.11 wheels.

## User Impact

- `pip install evt3` now has published wheel coverage for CPython 3.9, 3.10,
3.11, 3.12, and 3.13.
- Python 3.8 is no longer supported; package metadata now requires Python 3.9+.
- Local source builds continue to use `maturin develop` / `maturin build` and
still require Rust.
- HDF5 support remains source-only because it depends on the optional native
HDF5 toolchain and runtime plugin setup.

## Implementation Notes

- `evt3-python/Cargo.toml` now uses `pyo3 = 0.23` and `numpy = 0.23`.
- `evt3-python/src/lib.rs` was migrated to the current PyO3 Bound API used by
those releases.
- `.github/workflows/ci.yml` runs the Python synthetic test suite on Python 3.9
through 3.13.
- `.github/workflows/release.yml` builds one wheel per supported interpreter and
operating system with `maturin build --interpreter python`.

## Verification

- `cargo check -p evt3-python`
- `cargo fmt --all`
- Python build and test verification commands are recorded in `tasks/todo.md`
4 changes: 2 additions & 2 deletions evt3-python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ hdf5 = ["evt3-core/hdf5"]

[dependencies]
evt3-core = { path = "../evt3-core" }
pyo3 = { version = "0.20", features = ["extension-module"] }
numpy = "0.20"
pyo3 = { version = "0.23", features = ["extension-module"] }
numpy = "0.23"
4 changes: 3 additions & 1 deletion evt3-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ High-performance EVT 3.0 decoder for Prophesee event cameras with zero-copy nump

## Installation

Supported Python versions: CPython 3.9 through 3.13.

```bash
# From source (requires Rust toolchain)
cd evt3-python
pip install maturin
maturin develop

# Or build a wheel
maturin build --release
maturin build --release --interpreter python
pip install target/wheels/evt3-*.whl
```

Expand Down
4 changes: 2 additions & 2 deletions evt3-python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description = "High-performance EVT 3.0 decoder for Prophesee event cameras"
readme = "README.md"
license = { text = "MIT" }
authors = [{ name = "Uthmann", email = "mika.uthmann@uni-bielefeld.de" }]
requires-python = ">=3.8"
requires-python = ">=3.9"
keywords = ["event-camera", "evt3", "prophesee", "decoder", "neuromorphic", "dvs"]
classifiers = [
"Development Status :: 4 - Beta",
Expand All @@ -18,11 +18,11 @@ classifiers = [
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Scientific/Engineering",
"Topic :: Scientific/Engineering :: Image Processing",
"Topic :: Multimedia :: Video",
Expand Down
26 changes: 13 additions & 13 deletions evt3-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use evt3_core::{CdEvent, Evt3Decoder, TriggerEvent};
use numpy::{IntoPyArray, PyArray1};
use pyo3::exceptions::PyIOError;
use pyo3::prelude::*;
use pyo3::types::PyDict;
use pyo3::types::{PyDict, PyModule};
use std::path::PathBuf;

/// Container for decoded CD events with zero-copy numpy access.
Expand Down Expand Up @@ -52,39 +52,39 @@ impl Events {
/// This creates a view into the Rust-allocated memory without copying.
/// The array is valid as long as this Events object is alive.
#[getter]
fn x<'py>(&self, py: Python<'py>) -> &'py PyArray1<u16> {
fn x<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<u16>> {
self.x.clone().into_pyarray(py)
}

/// Returns the Y coordinates as a numpy array.
#[getter]
fn y<'py>(&self, py: Python<'py>) -> &'py PyArray1<u16> {
fn y<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<u16>> {
self.y.clone().into_pyarray(py)
}

/// Returns the polarities as a numpy array.
///
/// Values: 0 = OFF (decrease in brightness), 1 = ON (increase)
#[getter]
fn polarity<'py>(&self, py: Python<'py>) -> &'py PyArray1<u8> {
fn polarity<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<u8>> {
self.polarity.clone().into_pyarray(py)
}

/// Alias for polarity (shorter name).
#[getter]
fn p<'py>(&self, py: Python<'py>) -> &'py PyArray1<u8> {
fn p<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<u8>> {
self.polarity.clone().into_pyarray(py)
}

/// Returns the timestamps as a numpy array (in microseconds).
#[getter]
fn timestamp<'py>(&self, py: Python<'py>) -> &'py PyArray1<u64> {
fn timestamp<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<u64>> {
self.timestamp.clone().into_pyarray(py)
}

/// Alias for timestamp (shorter name).
#[getter]
fn t<'py>(&self, py: Python<'py>) -> &'py PyArray1<u64> {
fn t<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<u64>> {
self.timestamp.clone().into_pyarray(py)
}

Expand All @@ -109,13 +109,13 @@ impl Events {
/// Returns all arrays as a dictionary.
///
/// This is useful for creating a pandas DataFrame or structured array.
fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<PyObject> {
fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Py<PyDict>> {
let dict = PyDict::new(py);
dict.set_item("x", self.x.clone().into_pyarray(py))?;
dict.set_item("y", self.y.clone().into_pyarray(py))?;
dict.set_item("polarity", self.polarity.clone().into_pyarray(py))?;
dict.set_item("timestamp", self.timestamp.clone().into_pyarray(py))?;
Ok(dict.into())
Ok(dict.unbind())
}
}

Expand Down Expand Up @@ -166,19 +166,19 @@ impl TriggerEvents {

/// Returns the trigger values as a numpy array.
#[getter]
fn value<'py>(&self, py: Python<'py>) -> &'py PyArray1<u8> {
fn value<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<u8>> {
self.value.clone().into_pyarray(py)
}

/// Returns the trigger channel IDs as a numpy array.
#[getter]
fn id<'py>(&self, py: Python<'py>) -> &'py PyArray1<u8> {
fn id<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<u8>> {
self.id.clone().into_pyarray(py)
}

/// Returns the timestamps as a numpy array.
#[getter]
fn timestamp<'py>(&self, py: Python<'py>) -> &'py PyArray1<u64> {
fn timestamp<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<u64>> {
self.timestamp.clone().into_pyarray(py)
}
}
Expand Down Expand Up @@ -319,7 +319,7 @@ fn decode_bytes(

/// EVT 3.0 decoder module for Python.
#[pymodule]
fn _evt3(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
fn _evt3(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(decode_file, m)?)?;
m.add_function(wrap_pyfunction!(decode_file_with_triggers, m)?)?;
m.add_function(wrap_pyfunction!(decode_bytes, m)?)?;
Expand Down
Loading