Skip to main content

endstation

PyPI Python tests License: BSD-3-Clause

PyPI · Changelog · Issues

Shared scaffolding for desktop scientific applications.

It is not a science library. It covers what surrounds an analysis: guarding optional dependencies, reporting and cancelling slow work, collecting form errors, locating research data kept outside the checkout, running work off the UI thread, embedding a figure, rendering a documentation page, and building a macOS bundle.

At a synchrotron, the endstation is the shared apparatus that every experiment bolts onto (goniometer, detector, motion control), as distinct from the science each experiment does. That is where the name comes from.

Why it exists

Four projects are the same kind of thing: a Python science package with an optional PySide6/matplotlib front end, a macOS .app, research data held outside the checkout, and a test suite that runs headless. Each had solved those problems separately, and the copies had drifted. A layout fix, a worker teardown fix and a stronger toolkit-import test had each landed in one project and not the others. This package is where those pieces live now, so a fix lands once.

The core has no dependencies

dependencies = []

eblfrac declares exactly that, so its fracture engine and GPF writer install anywhere. A shared package that pulled in numpy would take that away from every downstream user, with no error to notice.

endstation, endstation.optional, endstation.progress, endstation.forms and endstation.paths import nothing third-party, and tests/test_core_has_no_dependencies.py fails if that changes. Everything heavier sits behind an extra:

Subpackage Extra Needs
endstation.qt qt PySide6, matplotlib
endstation.docs docs matplotlib, for math only
endstation.testing testing pytest
endstation.tools none macOS sips/iconutil at runtime

requires-python = ">=3.10", because eblfrac declares 3.10 and a shared package should not raise its consumers' floor.

Install

pip install "endstation[qt]"

With uv, the same extras work:

uv pip install "endstation[qt]"    # into the active environment
uv add endstation                  # as a dependency of a uv project

Or from git, pinned to a tag:

pip install "endstation @ git+https://github.com/jamccoy/endstation.git@v0.2.3"

For working on endstation itself:

pip install -e '.[dev]'

Publishing

.github/workflows/publish.yml releases to PyPI when a v* tag is pushed, using Trusted Publishing, so no API token is stored anywhere. The publisher is registered at https://pypi.org/manage/account/publishing/ for jamccoy/endstation, workflow publish.yml, environment pypi.

What is in it

endstation.optional: dependencies that explain themselves

from endstation import require_extra, optional_module

require_extra("PySide6", "gui", still_works="The analysis works without it.")

astropy = optional_module("astropy", "trapezoid", package="als_eff_meas")

require_extra checks with find_spec, so it costs nothing when the dependency is present and is safe to call from a module that must not import a toolkit. optional_module is a lazy proxy: a missing package breaks only the functions that use it, not every function in the module.

The install command in the error message is quoted, because square brackets are glob characters and zsh answers an unquoted pip install .[gui] with no matches found.

endstation.progress: cancellation that stops the work

One callback, progress(done, total). A callback that wants to stop raises Cancelled, and the loops let it propagate.

from endstation.progress import cancelling, throttled

report = cancelling(throttled(on_progress), cancel_event.is_set)

A Python thread cannot be killed, and a NumPy call has no check point, so the progress callback is the only place a caller can interrupt. Without it, cancel only stops the waiting: the CPU keeps going and the result is thrown away.

Reporting is throttled; the cancellation check is not. A Cancel click takes effect at the next unit of work, not the next percent.

endstation.forms: every error at once

A form with three bad fields reports all three, instead of making the user fix them one at a time.

errors = ErrorCollector()
if period <= 0:
    errors.add("period", "must be positive")
if not 0 <= trim < MAX_TRIM:
    errors.add("trim", f"must be between 0 and {MAX_TRIM}")
errors.raise_if_any()

endstation.paths: data kept outside the checkout

scans = find_data("GRATINGLAB_AFM_DIR", "~/Documents/afm_scans",
                  hint="docs/DATA.md")

A missing root is reported by name. FileNotFoundError: [Errno 2] on a path the user never typed tells them nothing; naming the variable and the resolved path makes it a one-line fix.

endstation.testing: a headless test suite with no conftest

It registers through the pytest11 entry point, so no conftest has to name it. It sets QT_QPA_PLATFORM=offscreen, QT_API, PYTEST_QT_API and MPLBACKEND, each with setdefault, so QT_QPA_PLATFORM=cocoa pytest tests/test_gui.py still opens a real window. ENDSTATION_NO_HEADLESS=1 turns it off.

Tests

.venv/bin/python -m pytest -q

The template

The pyproject skeleton, CI workflow, headless conftest and ignore rules repeat across these projects too. They should not be a runtime dependency, so they are a copier template instead:

pipx run copier copy gh:jamccoy/endstation my-project

It asks whether the project has a PySide6 window and whether its tests read data from outside the checkout. A library project gets no gui/ directory, and its CI does not install Qt system libraries it will never use.

The template also carries a few things that are easy to miss:

  • A CI step that constructs a QApplication instead of only importing PySide6. The import succeeds before the platform plugin loads, and the plugin is where a missing system library shows up, so the failure appears next to the apt step that caused it.
  • A check that data-dependent tests were skipped rather than failed, and a minimum number of passing tests, so a suite that quietly shrinks does not go green.
  • No qt_api in [tool.pytest.ini_options]. It is a pytest-qt option, so setting it makes the suite fail without the toolkit; the plugin exports PYTEST_QT_API instead.
  • Local working notes are ignored from the first commit, so a scratch file does not end up in a published repository.

tests/test_template.py renders both variants and checks the output, so a Jinja typo or a ${{ }} the renderer ate fails here instead of in someone's new project.

Metadata

Release files for endstation 0.2.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for endstation 0.2.3
File Size Uploaded Provenance
endstation-0.2.3.tar.gz 68.7 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for endstation 0.2.3
File Interpreter ABI Platform
endstation-0.2.3-py3-none-any.whl Python 3 none any Details

Total release size: 122.1 kB

Release files / endstation-0.2.3.tar.gz

Download URL endstation-0.2.3.tar.gz
Size 68.7 kB
Tags Source
SHA-256 checksum
How to use checksums
8df21230a89cf3af0e930e3b979862c01d43d71c5dbed23ce1dfaf316526d684
BLAKE2b-256 checksum
How to use checksums
96bedf685b0aeb705f0fbcbb1fe3ae849f3955c52f8081a7eadaa5f22deb314b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

Release files / endstation-0.2.3-py3-none-any.whl

Download URL endstation-0.2.3-py3-none-any.whl
Size 53.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
582f8cd3a54a08c8e6704afa84caa3db55b9eb95ab043334b87f7f37c6bbd062
BLAKE2b-256 checksum
How to use checksums
cd1463b80f51f1fc1ab80a05e57679465f336e26f11723e1622b0ae0d37317cd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

Security for endstation 0.2.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for endstation-0.2.3.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page