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
60 changes: 57 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: CI test
name: CI

on:
push:
Expand All @@ -11,7 +11,61 @@ on:
- ".github/**"
pull_request:
branches: [main]
workflow_call:

permissions:
contents: read

jobs:
ci:
uses: eye2Gene/e2g-workflows/.github/workflows/python-ci.yml@main
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
- run: uv sync --locked
- run: uv run just ci
- name: Type check (advisory during annotation migration)
continue-on-error: true
run: uv run just type-check

license-check:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
- run: uv sync --locked
- run: uv run just license-check

test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.13"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
python-version: ${{ matrix.python-version }}
- run: uv sync --locked
- run: uv run just test
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
if: always()
with:
name: coverage-${{ matrix.python-version }}
path: coverage.xml

build:
runs-on: ubuntu-latest
needs: [quality, test]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
- run: uv sync --locked
- run: uv run just build
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: dist
path: dist/
if-no-files-found: error
72 changes: 41 additions & 31 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,56 @@ name: Release

on:
push:
branches: [main]
tags:
- "v*"

permissions:
contents: write
id-token: write # needed for PyPI trusted publishing
contents: read

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
validate-tag:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Check tag matches package version
run: |
project_version=$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
test "v${project_version}" = "${GITHUB_REF_NAME}"

ci:
uses: eye2Gene/e2g-pypkg/.github/workflows/python-ci.yml@main
needs: [validate-tag]
uses: ./.github/workflows/ci.yml

release:
publish:
runs-on: ubuntu-latest
needs: [ci]
concurrency:
group: release
cancel-in-progress: false
environment:
name: pypi
url: https://pypi.org/project/pyescan/
permissions:
id-token: write
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- uses: astral-sh/setup-uv@main
name: dist
path: dist
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1

- name: Python Semantic Release
id: release
uses: python-semantic-release/python-semantic-release@v9
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

- name: Build package
if: steps.release.outputs.released == 'true'
run: uv run just build

- name: Publish to PyPI
if: steps.release.outputs.released == 'true'
uses: pypa/gh-action-pypi-publish@release/v1

- name: Upload to GitHub Release assets
if: steps.release.outputs.released == 'true'
uses: python-semantic-release/publish-action@v9
github-release:
runs-on: ubuntu-latest
needs: [publish]
permissions:
contents: write
steps:
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
name: dist
path: dist
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: gh release create "${GITHUB_REF_NAME}" dist/* --verify-tag --generate-notes
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 Moorfields Ophthalmic Reading Centre & Clinical AI Lab
Copyright (c) 2023 William Woof

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ uv pip install pyescan # or: pip install pyescan
# With optional extras
uv pip install "pyescan[registration]" # image registration (scipy)
uv pip install "pyescan[metrics]" # metrics with OpenCV and scikit-image
uv pip install "pyescan[projection]" # OCT/enface projection with scikit-image
uv pip install "pyescan[jupyter]" # Jupyter notebook support
uv pip install "pyescan[all]" # everything
```
Expand All @@ -32,6 +33,30 @@ uv sync --all-extras # installs core + all optional extras + dev tools

This creates a `.venv`, installs pyescan in editable mode, and pins all dependencies to the exact versions in `uv.lock`.

## Releasing

Releases are built and published by GitHub Actions when a version tag is pushed.
PyPI authentication uses a Trusted Publisher, so no API token is stored in the
repository.

1. Update the version and commit it:

```bash
uv version 0.1.0
git add pyproject.toml uv.lock
git commit -m "chore: release 0.1.0"
```

2. Tag that exact commit and push the tag:

```bash
git tag -a v0.1.0 -m "PyeScan 0.1.0"
git push origin v0.1.0
```

The release workflow runs quality checks and tests, builds and smoke-tests both
distribution formats, publishes them to PyPI, and creates a GitHub release.

## Project structure

```txt
Expand Down
12 changes: 9 additions & 3 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,10 @@ qa:
-uv run ty check .
uv audit

# Check formatting, linting and type checking (no fixes, for CI)
# Check formatting, linting and dependencies (no fixes, for CI)
ci:
uv run ruff format --check .
uv run ruff check .
uv run ty check .
uv audit

# Check dependency licenses
Expand Down Expand Up @@ -62,7 +61,14 @@ pdb *ARGS:
build:
rm -rf build
rm -rf dist
uv build
uv build --no-sources
uv run twine check dist/*
just smoke-dist

# Test the wheel and sdist in environments containing only declared dependencies
smoke-dist:
uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py
uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py

# remove all build, test, coverage and Python artifacts
clean:
Expand Down
36 changes: 24 additions & 12 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
[project]
name = "pyescan"
name = "PyeScan"
version = "0.0.2"
description = "A Python library for working with retinal scans exported by Private Eye / Crystal Eye"
readme = "README.md"
requires-python = ">=3.12,<4.0"
license = "MIT"
authors = [{ name = "William Woof", email = "awwoof@hotmail.com" }]
maintainers = [{ name = "William Woof", email = "awwoof@hotmail.com" }]
license-files = ["LICENSE"]
authors = [{ name = "William Woof", email = "William.woof@eye2gene.com" }]
maintainers = [{ name = "William Woof", email = "William.woof@eye2gene.com" }]
keywords = ["fundus", "medical imaging", "ophthalmology", "OCT", "retina"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Libraries",
"Typing :: Typed",
]

dependencies = [
"Pillow>=12",
"matplotlib",
Expand All @@ -26,22 +30,30 @@ dependencies = [
"typer",
]

[project.urls]
Homepage = "https://github.com/eye2gene/PyeScan"
Repository = "https://github.com/eye2gene/PyeScan"
Issues = "https://github.com/eye2gene/PyeScan/issues"

[project.scripts]
pyescan = "pyescan.tools.cli:app"

[project.optional-dependencies]
all = [
"pyescan[jupyter,registration,metrics]",
"pyescan[jupyter,metrics,projection,registration]",
]
jupyter = [
"ipython",
"ipywidgets",
]
metrics = [
"opencv-python",
"opencv-python-headless",
"scikit-image",
"scipy",
]
projection = [
"scikit-image",
]
registration = [
"retinalysis-registration",
"scipy",
Expand All @@ -63,17 +75,19 @@ test = [
"pytest>=9",
"pytest-cov",
"pytest-xdist",
"scikit-image",
"ty",
]
tools = ["cruft", "rust-just"]
tools = ["cruft", "rust-just", "twine"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.coverage.report]
omit = ["__main__.py"]
fail_under = 50 # minimum coverage required, but aim for 100% though
# Preserve the current baseline while the legacy registration modules gain tests.
fail_under = 40
exclude_also = [
"if TYPE_CHECKING:",
"if typing.TYPE_CHECKING:",
Expand Down Expand Up @@ -185,11 +199,6 @@ ignore = [
[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.semantic_release]
version_toml = ["pyproject.toml:project.version"]
branch = "main"
commit_message = "chore(release): {version}"

[tool.ty]
# All rules are enabled as "error" by default; no need to specify unless overriding.

Expand All @@ -205,7 +214,10 @@ include = ["src"]

[tool.uv]
exclude-newer = "7 days"
# Security fixes should not be delayed by the general dependency cooldown.
exclude-newer-package = { gitpython = false }
package = true
required-version = ">=0.12.3,<0.13"
default-groups = ["dev"]
cache-keys = [
{ file = "pyproject.toml" },
Expand Down
9 changes: 8 additions & 1 deletion src/pyescan/core/scan_enface.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from IPython.display import display
from numpy.typing import NDArray

from .scan import SingleImageScan
Expand Down Expand Up @@ -28,6 +27,14 @@ def _build_display_widget(self):
return enface_display_widget(enface_image, width=320, height=320)

def _ipython_display_(self):
try:
from IPython.display import display
except ModuleNotFoundError as exc:
raise ModuleNotFoundError(
"Notebook display requires the 'jupyter' extra; "
"install it with `pip install 'pyescan[jupyter]'`."
) from exc

display(self._build_display_widget())


Expand Down
Loading
Loading