Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ca4a49e
Explicit lower bounds for all package versions in pyproject.toml
marcoheisig Jul 6, 2026
23f537c
Add Github actions for CI and PyPI release
marcoheisig Jul 6, 2026
0b6d122
Use imageio's version of ffmpeg for better portability
marcoheisig Jul 6, 2026
fa54018
Remove Python 3.11 and bump pyarrow and tifffile minimum versions
marcoheisig Jul 6, 2026
e58a41c
Bump pyarrow and scipy minimum versions
marcoheisig Jul 7, 2026
261b5e0
Bump versions of all Github workflows to avoid Node 20 dependency
marcoheisig Jul 7, 2026
f37bdde
Pin the version of setup-uv as advertised in their documentation.
marcoheisig Jul 7, 2026
34af1ec
Enable JAX's persistent compilation
marcoheisig Jul 7, 2026
a121f73
Raise minimum JAX version and timeout limits to avoid CI timeouts
marcoheisig Jul 7, 2026
3c16c3e
Bump JAX version further to fix CI
marcoheisig Jul 7, 2026
6db9715
Bump minimum JAX version further, hoping to fix the CI pipeline
marcoheisig Jul 7, 2026
821895a
Add safety net to debug CI issues
marcoheisig Jul 7, 2026
aaab470
More version-fiddling in hopes of fixing CI
marcoheisig Jul 7, 2026
41662a2
Fix pytest-timeout to allow debugging the actual issues
marcoheisig Jul 7, 2026
16f3fff
Bump minimum Dask version in hopes to fix CI
marcoheisig Jul 7, 2026
75d3673
Bump fsspec, numpy, pyarrow, and xarray minimum versions
marcoheisig Jul 8, 2026
d3c05a0
Investigate a deadlock on CI by changing thread count to 1
marcoheisig Jul 8, 2026
d724eef
Wrap up the CI integration
marcoheisig Jul 8, 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
78 changes: 78 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: CI

on:
push:
branches: [main]
pull_request:

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
# Run the full pre-commit suite (ruff, pyright, validate-pyproject, uv-lock,
# and the local pytest hook) exactly as developers do locally. Uses the
# committed uv.lock via --frozen, so any lockfile drift fails loudly.
pre-commit:
runs-on: ubuntu-latest
timeout-minutes: 8
env:
# Uniform cache path across OSes so the actions/cache `path` below
# matches what conftest.py writes (it honors this env var via
# expanduser). Local dev uses the platform-canonical default instead.
JAX_COMPILATION_CACHE_DIR: ~/.cache/jax-compilation-cache
steps:
- uses: actions/checkout@main
- uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3
with:
enable-cache: true
python-version: "3.12"
- name: Cache JAX compilations
uses: actions/cache@v5
with:
path: ~/.cache/jax-compilation-cache
key: jax-cc-precommit-py3.12-${{ hashFiles('uv.lock') }}
restore-keys: |
jax-cc-precommit-py3.12-
- run: uv run --frozen pre-commit run --all-files --show-diff-on-failure

# Portability matrix: prove the declared floors and the latest versions both
# work across the supported Python versions and operating systems. Each cell
# re-resolves dependencies directly from pyproject.toml (--no-project bypasses
# uv.lock), so the lowest-direct cell exercises the real lower bounds.
test:
runs-on: ${{ matrix.os }}
timeout-minutes: 8
env:
JAX_COMPILATION_CACHE_DIR: ~/.cache/jax-compilation-cache
# Self-diagnosing safety net for hangs: dump all thread stacks on
# SIGABRT/crash, and log every JAX JIT compilation with its timing so a
# stuck job shows exactly which compile/test was in flight.
PYTHONFAULTHANDLER: "1"
JAX_LOG_COMPILES: "1"
# Disable BLAS threads for now because they have shown to cause deadlocks
# in our test suite.
OMP_NUM_THREADS: "1"
OPENBLAS_NUM_THREADS: "1"
MKL_NUM_THREADS: "1"
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python: ["3.12", "3.13"]
resolution: [highest, lowest-direct]
steps:
- uses: actions/checkout@main
- uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3
with:
enable-cache: true
python-version: ${{ matrix.python }}
- name: Cache JAX compilations
uses: actions/cache@v5
with:
path: ~/.cache/jax-compilation-cache
key: jax-cc-${{ runner.os }}-py${{ matrix.python }}-${{ matrix.resolution }}-${{ hashFiles('uv.lock') }}
restore-keys: |
jax-cc-${{ runner.os }}-py${{ matrix.python }}-${{ matrix.resolution }}-
jax-cc-${{ runner.os }}-py${{ matrix.python }}-
- run: uv run --no-project --with-editable . --with "pytest>=8.0" --with "pytest-timeout>=2.4" --python ${{ matrix.python }} --resolution ${{ matrix.resolution }} pytest --timeout=180 --timeout-method=thread -v --durations=20
60 changes: 60 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Release

on:
push:
tags:
- "v*"

# Never cancel an in-flight release; only one release per tag at a time.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read

jobs:
# Build the sdist and wheel from the tagged commit. setuptools-scm derives
# the version from git, so a full history checkout is required.
build:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@main
with:
fetch-depth: 0
- uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3
with:
enable-cache: true
python-version: "3.12"
- run: uv build -o dist
- uses: actions/upload-artifact@v6
with:
name: dist
path: dist/

# Publish to PyPI via Trusted Publishing (OIDC, no stored tokens) and create
# a GitHub Release with the built artifacts attached. Requires a one-time
# Trusted Publisher registration on pypi.org pointing at this workflow file
# (release.yml), the `pypi` environment, and this repository.
publish:
needs: build
runs-on: ubuntu-latest
timeout-minutes: 5
environment: pypi
permissions:
id-token: write # PyPI Trusted Publishing (OIDC)
contents: write # create GitHub Release
steps:
- uses: actions/download-artifact@v6
with:
name: dist
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
files: dist/*
name: ${{ github.ref_name }}
generate_release_notes: true
50 changes: 26 additions & 24 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,37 @@ repos:
hooks:
- id: uv-lock

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.20
hooks:
- id: ruff-check
args: [ --fix ]
- id: ruff-format

# - repo: https://github.com/numpy/numpydoc
# rev: v1.10.0
# hooks:
# - id: numpydoc-validation

- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.25
hooks:
- id: validate-pyproject
additional_dependencies: ["validate-pyproject-schema-store[all]"]

- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.411
hooks:
- id: pyright

- repo: local
hooks:
- id: pytest
name: pytest
entry: uv run ./.venv/bin/pytest
language: system
types: [python]
pass_filenames: false
always_run: true
- id: ruff-check
name: ruff check
entry: uv run ruff check --fix --exit-non-zero-on-fix
language: system
types: [python]
require_serial: true
- id: ruff-format
name: ruff format
entry: uv run ruff format
language: system
types: [python]
require_serial: true
- id: pyright
name: pyright
entry: uv run pyright
language: system
types: [python]
pass_filenames: false
always_run: true
- id: pytest
name: pytest
entry: uv run pytest
language: system
types: [python]
pass_filenames: false
always_run: true
78 changes: 38 additions & 40 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,32 @@ description = "A collection of Python scripts for iSCAT microscopy data analysis
authors = [{ name = "Marco Heisig", email = "marco.heisig@mpl.mpg.de" }]
readme = "README.md"
license = "GPL-3.0-or-later"
requires-python = ">=3.12,<3.13"
requires-python = ">=3.12"
dependencies = [
"bioio",
"bioio-dv",
"bioio-imageio>=1.2",
"bioio-lif",
"bioio-nd2",
"bioio>=3.4",
"bioio-dv>=1.2",
"bioio-imageio>=1.3",
"bioio-lif>=1.3",
"bioio-nd2>=2.0",
"bioio-ome-tiff>=1.4",
"bioio-ome-zarr>=2.2",
"bioio-tifffile",
"bioio-tiff-glob",
"dask",
"fsspec[fuse]",
"imageio[ffmpeg]",
"imgrvt",
"jax[cpu]",
"jaxtyping",
"numpy",
"ome-types",
"bioio-ome-zarr>=3.5",
"bioio-tifffile>=1.3",
"bioio-tiff-glob>=1.2",
"dask>=2026.1",
"fsspec>=2025.3",
"imageio[ffmpeg]>=2.37",
"imgrvt>=1.0",
"jax>=0.10.2",
"jaxtyping>=0.3",
"numpy>=2.3",
"ome-types>=0.6",
"polars>=1.0",
"pyarrow",
"scikit-image",
"scipy",
"tqdm",
"xarray",
"pyarrow>=22.0",
"scikit-image>=0.23",
"scipy>=1.17",
"tifffile>=2026.5",
"tqdm>=4.66",
"xarray>=2026.1",
]
keywords = ["tools", "utilities", "python", "microscopy"]

Expand All @@ -39,25 +40,21 @@ Issues = "https://github.com/SandoghdarLab/toolsandogh/issues"

[dependency-groups]
dev = [
"build",
"ipython",
"ipywidgets",
"ruff",
"pre-commit",
"pyright[nodejs]",
"ty",
"pytest",
"python-lsp-server",
"numpydoc",
"setuptools",
"setuptools-scm",
"coverage",
"jupyter",
"jupyter_rfb",
"build>=1.2",
"ruff>=0.15",
"pre-commit>=3.7",
"pyright[nodejs]>=1.1.400",
"ty>=0.0.50",
"pytest>=8.0",
"pytest-timeout>=2.4",
"python-lsp-server>=1.10",
"numpydoc>=1.7",
"setuptools>=80",
"setuptools-scm>=8",
"coverage>=5.0",
"rendercanvas>=2.2",
"sidecar",
"twine",
"bokeh",
"twine>=5.0",
"bokeh>=3.4",
]

[build-system]
Expand All @@ -84,6 +81,7 @@ select = [
"RET", # flake8-return
"YTT", # Truthiness
"TRY", # Try block errors
"NPY201" # Use Numpy 2.0
]
ignore = [
"C901", # Function too complex
Expand Down
21 changes: 7 additions & 14 deletions src/toolsandogh/_store_video.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import os
import pathlib
import shutil
import subprocess
import tempfile
import urllib.parse

import imageio_ffmpeg
import numpy as np
import numpy.typing as npt
import xarray as xr
Expand All @@ -14,14 +14,10 @@

from ._canonicalize_video import canonicalize_video


def _check_ffmpeg_installed() -> None:
"""Ensure ffmpeg is installed and accessible."""
if shutil.which("ffmpeg") is None:
raise RuntimeError(
"ffmpeg is not installed or not found in PATH. "
"Please install ffmpeg to use video encoding features."
)
# ffmpeg binary shipped by the `imageio-ffmpeg` wheel. Using this (rather than
# shelling out to a bare `ffmpeg` on PATH) means users get a working ffmpeg on
# every supported platform without a separate system install.
_FFMPEG_EXE = imageio_ffmpeg.get_ffmpeg_exe()


def store_video(video: npt.ArrayLike, path: str | os.PathLike, **kwargs) -> None:
Expand Down Expand Up @@ -117,9 +113,6 @@ def store_video_as_mp4(video: xr.DataArray, path: str | os.PathLike, fps: int =
fps : int
The number of frames per second of the resulting mp4 video.
"""
# Check for ffmpeg availability
_check_ffmpeg_installed()

# Stack T, C, and Z into a single Frame dimension
data = video.stack(F=("T", "C", "Z")).transpose("F", "Y", "X")

Expand Down Expand Up @@ -158,7 +151,7 @@ def store_video_as_mp4(video: xr.DataArray, path: str | os.PathLike, fps: int =

# Run ffmpeg to concatenate
concat_cmd = [
"ffmpeg",
_FFMPEG_EXE,
"-y",
"-f",
"concat",
Expand Down Expand Up @@ -219,7 +212,7 @@ def chunk_to_mp4(chunk: np.ndarray, path: str | os.PathLike, fps: int = 30) -> N
pix_fmt = "gray8"

ffmpeg_cmd = [
"ffmpeg",
_FFMPEG_EXE,
"-y",
"-f",
"rawvideo",
Expand Down
Loading
Loading