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
82 changes: 82 additions & 0 deletions .claude/skills/python-release-workflow/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
name: python-release-workflow
description: Use when releasing nilspodlib from this repo with uv+poe+GitHub Actions+GitHub Releases+PyPI trusted publishing; supports patch/minor/major and requires CI before creating a release.
---

# Python Release Workflow

## Scope
- Repository: `mad-lab-fau/NilsPodLib`
- Tooling: `uv`, `poe`, `gh`
- Release types: `patch`, `minor`, `major`
- Package layout: single package `nilspodlib`
- Docs hosting: Read the Docs, no manual `gh-pages` publish in this repo

## Inputs
- `bump`: `patch` | `minor` | `major`
- `base_ref`: `main`
- Release notes source: latest top section in `CHANGELOG.md`

## Semver Quick Rule
- `patch`: fixes/docs/tooling/compat, no breaking API
- `minor`: backward-compatible features
- `major`: breaking behavior/API

## Flow
1. **Preflight**
- `git status -sb` must be clean or intentionally scoped
- `git log -5 --oneline`

2. **Local verification**
- `uv sync --group dev`
- `uv run poe ci_check`
- `uv run pytest`
- `uv build`

3. **Release prep**
- Finalize release notes in `CHANGELOG.md`
- Bump version: `uv run poe version --bump=<patch|minor|major>`
- Verify touched files:
- `pyproject.toml`
- `src/nilspodlib/__init__.py`
- `uv.lock`
- `CHANGELOG.md`

4. **Commit strategy**
- Commit code/docs/tooling fixes first
- Commit release prep separately: version + changelog + lockfile

5. **Push + CI gate**
- `git push`
- `REL_SHA=$(git rev-parse HEAD)`
- `gh run list --workflow "Test and Lint" --commit "$REL_SHA" --limit 5`
- `gh run watch <run-id>`
- Required: workflow conclusion is `success` for the release SHA

6. **Create GitHub release**
- Tag format: `vX.Y.Z`
- `gh release create vX.Y.Z --target main --title "vX.Y.Z" --notes "<release notes from changelog>"`

7. **Post-release checks**
- Watch the `Upload Python Package` workflow triggered by the release
- Confirm the PyPI publish job succeeded
- Confirm the new version is visible on PyPI

## Hard Gates
- Never create a GitHub release before CI is green for the release SHA.
- Never claim the PyPI release is done without command evidence from GitHub Actions or PyPI.
- Never skip local verification before the version bump.

## Fast Command Set
```bash
uv sync --group dev
uv run poe ci_check
uv run pytest
uv build
uv run poe version --bump=patch
git push
REL_SHA=$(git rev-parse HEAD)
gh run list --workflow "Test and Lint" --commit "$REL_SHA" --limit 5
gh run watch <run-id>
gh release create vX.Y.Z --target main --title "vX.Y.Z" --notes "<changelog section>"
```
3 changes: 1 addition & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@ jobs:
version: "0.6.2"
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: ${{ matrix.python-version }}
python-version: "3.11"
- name: Install the project
run: uv sync --all-extras --dev
- name: Build package
run: |
uv build
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

25 changes: 18 additions & 7 deletions .github/workflows/test-and-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,28 @@ on:
branches: [ main ]

jobs:
test_lint:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv and Python
uses: astral-sh/setup-uv@v5
with:
version: "0.6.2"
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: "3.11"
- name: Install the project
run: uv sync --all-extras --dev
- name: Linting
run: |
uv run poe ci_check

test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4
Expand All @@ -25,11 +41,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install the project
run: uv sync --all-extras --dev
- name: Linting
run: |
uv run poe ci_check
- name: Testing
if: ${{ matrix.python-version == '3.10' }}
run: |
uv run poe test
- name: "Upload coverage to Codecov"
Expand All @@ -38,4 +50,3 @@ jobs:
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

6 changes: 3 additions & 3 deletions .ruff.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
line-length = 120
target-version = "py310"
target-version = "py39"

[lint]
select = [
Expand Down Expand Up @@ -89,10 +89,10 @@ ignore = [
"TC001",
"TC002",
"TC003",
# Local imports are used intentionally to keep optional dependencies lazy.
"PLC0415",
# .pivot_table is preferred to .pivot or .unstack; provides same functionality
"PD010",
# df as varaible name
"PD901",
# melt over stack
"PD013"
]
Expand Down
13 changes: 11 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) (+ the Migration Guide section), and
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Fixed
- Restored Python 3.9 compatibility by postponing annotation evaluation and removing 3.10-only runtime typing usage.
- Added the missing runtime dependency on `pytz`.
- Updated the example scripts to follow the Sphinx-Gallery style so they execute during docs builds and render clearer tables and plots in the documentation.

### Internal Changes
- Split CI so Ruff runs once while tests cover Python 3.9, 3.10, and 3.11.
- Removed stale Black references from the development workflow and aligned Ruff to the Python 3.9 support target.
- Fixed the local release helper to find `uv` outside of `PATH` and corrected the publish workflow Python setup.

## [4.1.1] - 19.05.2025
## Internal Changes
- Migrated from `poetry` to `uv` for dependency management and packaging
Expand Down Expand Up @@ -145,5 +156,3 @@ This means, you need to update it as well.
modify the calibration sessions, so that they work on the factory calibrated instead of the raw data.
However, this is not recommended.
Maybe you are better of just performing a new calibration.


3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
[![codecov](https://codecov.io/gh/mad-lab-fau/NilsPodLib/branch/master/graph/badge.svg?token=2CXLVYMHJF)](https://codecov.io/gh/mad-lab-fau/NilsPodLib)
![Test and Lint](https://github.com/mad-lab-fau/NilsPodLib/workflows/Test%20and%20Lint/badge.svg)
[![Documentation Status](https://readthedocs.org/projects/nilspodlib/badge/?version=latest)](https://nilspodlib.readthedocs.io/en/latest/?badge=latest)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
![PyPI - Downloads](https://img.shields.io/pypi/dm/nilspodlib)

A python package to parse logged NilsPod binary files.
Expand Down Expand Up @@ -46,7 +45,7 @@ To run any of the tools required for the development workflow, use the poe comma
poe

CONFIGURED TASKS
format Format all files with black.
format Format all files with ruff.
lint Lint all files with ruff.
check Check all potential format and linting issues.
test Run Pytest with coverage.
Expand Down
2 changes: 1 addition & 1 deletion _tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import shutil
import subprocess
import sys
from collections.abc import Sequence
from pathlib import Path
from typing import Sequence

HERE = Path(__file__).parent

Expand Down
30 changes: 29 additions & 1 deletion examples/load_sensor_data.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,23 @@

from nilspodlib import Dataset

FILEPATH = Path("../tests/test_data/synced_sample_session/NilsPodX-7FAD_20190430_0933.bin")

def _repo_root() -> Path:
search_roots = [Path.cwd()]
if "__file__" in globals():
search_roots.insert(0, Path(__file__).resolve().parent)

for root in search_roots:
for parent in (root, *root.parents):
if (parent / "pyproject.toml").exists():
return parent
raise FileNotFoundError("Could not locate the repository root from the example path.")


# %%
# Load the dataset
# ----------------
FILEPATH = _repo_root() / "tests/test_data/synced_sample_session/NilsPodX-7FAD_20190430_0933.bin"

# Create a Dataset Object from the bin file
dataset = Dataset.from_bin_file(FILEPATH)
Expand All @@ -23,6 +39,10 @@
print("Stop Date (UTC):", dataset.info.utc_datetime_stop)
print("Enabled Sensors:", dataset.info.enabled_sensors)


# %%
# Work with individual datastreams
# --------------------------------
# You can access the individual sensor data directly from the dataset object using the names provided
# in dataset.info.enabled_sensors
datastream_acc = dataset.acc
Expand All @@ -42,6 +62,10 @@
downsampled_datastream = datastream_acc.downsample(factor=2)
print("The new datastream has a length of", len(downsampled_datastream.data))


# %%
# Apply operations to the full dataset
# ------------------------------------
# However, for many operations it makes more sense to apply them to the Dataset instead of the Datastream.
# This will apply the operations to all Datastream and return a new Dataset object

Expand All @@ -57,6 +81,10 @@

# At this point you would usually apply a calibration to the IMU data (see other examples)


# %%
# Export the data as dataframe
# ----------------------------
# After calibration and initial operations on all datastreams, the easiest way to interface with further processing
# pipelines is a conversion into a pandas DataFrame

Expand Down
26 changes: 25 additions & 1 deletion examples/load_sensor_session.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,23 @@

from nilspodlib import Dataset, Session, SyncedSession

FILEPATH = Path("../tests/test_data/synced_sample_session/")

def _repo_root() -> Path:
search_roots = [Path.cwd()]
if "__file__" in globals():
search_roots.insert(0, Path(__file__).resolve().parent)

for root in search_roots:
for parent in (root, *root.parents):
if (parent / "pyproject.toml").exists():
return parent
raise FileNotFoundError("Could not locate the repository root from the example path.")


# %%
# Create and load a session
# -------------------------
FILEPATH = _repo_root() / "tests/test_data/synced_sample_session"

# A session consists of multiple datasets. By default this is also the way to create one
datasets = [Dataset.from_bin_file(d) for d in FILEPATH.glob("*.bin")]
Expand All @@ -22,6 +38,10 @@
session = Session.from_folder_path(FILEPATH, filter_pattern="*.bin")
print(f"This session has {len(session.datasets)} datasets")


# %%
# Apply operations to all datasets
# --------------------------------
# Like Datasets contain convenience methods to act on all Datastreams, Sessions provide methods that work on all
# datasets

Expand All @@ -35,6 +55,10 @@
print("The samplingrates are:", session.info.sampling_rate_hz)
print("The enabled sensor are:", session.info.enabled_sensors)


# %%
# Work with synchronized sessions
# -------------------------------
# The library differentiates between synchronised and not synchronised session.
# If your session is synchronised your should use a SyncedSession

Expand Down
50 changes: 15 additions & 35 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies = [
"pandas>=1.1.3",
"scipy>=1.5.2",
"imucal>=2.0.0",
"pytz>=2024.1",
"typing-extensions>=4.3.0",
]

Expand All @@ -49,43 +50,22 @@ build-backend = "hatchling.build"

[dependency-groups]
dev = [
"coverage>=6.4.4,<7",
"recommonmark>=0.6.0,<0.7",
"memory_profiler>=0.57.0,<0.58",
"matplotlib>=3.5.3,<4",
"toml>=0.10.2,<0.11",
"pytest>=7.1.2,<8",
"pytest-cov>=3.0.0,<4",
"Sphinx>=5.1.1,<6",
"numpydoc>=1.4.0,<2",
"sphinx-gallery>=0.11.1,<0.12",
"pydata-sphinx-theme>=0.9.0,<0.10",
"isort>=5.10.1,<6",
"poethepoet>=0.16.0,<0.17",
"ruff>=0.11.5,<0.12",
"coverage>=6.4.4",
"recommonmark>=0.6.0",
"memory_profiler>=0.57.0",
"matplotlib>=3.5.3",
"toml>=0.10.2",
"pytest>=7.1.2",
"pytest-cov>=3.0.0",
"Sphinx>=5.1.1",
"numpydoc>=1.4.0",
"sphinx-gallery>=0.11.1",
"pydata-sphinx-theme>=0.9.0",
"isort>=5.10.1",
"poethepoet>=0.16.0",
"ruff>=0.15.1,<0.16",
]

[tool.black]
line-length = 120
target-version = ['py310']
exclude = '''
(
/(
\.eggs # exclude a few common directories in the
| \.git # root of the project
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| docs
| build
| dist
| \.virtual_documents
)/
)
'''

[tool.poe.tasks]
_format = "ruff format ."
_auto_fix = "ruff check . --fix-only --show-fixes --exit-zero"
Expand Down
Loading