endstation
endstation is a library of shared components for desktop scientific applications written in Python. It provides the parts of such an application that surround the analysis itself: guards for optional dependencies, progress reporting and cancellation for slow work, collection of form errors, location of research data kept outside the checkout, background workers for the user interface, embedded figures, rendered documentation pages, and macOS application bundles.
The name is taken from synchrotron beamlines, where the endstation is the shared apparatus to which each experiment is attached, such as the goniometer, detector and motion control, as distinct from the science each experiment carries out.
Why it exists
Four projects share the same structure: a Python science package with an optional
PySide6 and matplotlib front end, a macOS .app, research data held outside the
checkout, and a test suite that runs without a display. Each had solved those problems
separately, and the copies had diverged; a layout fix, a fix to worker shutdown and a
stronger test of the toolkit import had each been made in one project and not in the
others. This package now holds those components, so that a fix made once applies to
every project.
The core has no dependencies
dependencies = []
One of the projects that depend on endstation, eblfrac, a fracturing tool for
electron-beam lithography patterns, declares exactly that, so its fracture engine and
GPF writer can be installed anywhere. A shared package that required numpy would remove
that property from every project that depends on it, and nothing would report the
change.
endstation, endstation.optional, endstation.progress, endstation.forms and
endstation.paths import no third-party packages, and
tests/test_core_has_no_dependencies.py fails if that changes. Everything that needs
more is installed through 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 |
The package declares requires-python = ">=3.10", because eblfrac declares 3.10 and
a shared package should not raise the minimum Python version of the projects that use
it.
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.5"
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 needs to be stored. The publisher is registered at
https://pypi.org/manage/account/publishing/ for jamccoy/endstation, workflow
publish.yml, environment pypi.
Contents
endstation.optional: optional dependencies
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 for the dependency with find_spec, so it costs nothing when the
dependency is present and can be called from a module that must not import a toolkit.
When the dependency is missing, the error names the extra that provides it and the
command that installs it. optional_module returns a lazy proxy, so a missing package
affects only the functions that use it and not the rest of the module.
The install command in the error message is quoted, because square brackets are glob
characters and zsh rejects an unquoted pip install .[gui] with no matches found.
endstation.progress: progress reporting and cancellation
Progress is reported through a single callback, progress(done, total). A callback
that needs to stop the work raises Cancelled, and the loops let the exception
propagate.
from endstation.progress import cancelling, throttled
report = cancelling(throttled(on_progress), cancel_event.is_set)
A Python thread cannot be terminated from outside, and a NumPy call has no point at which it can be interrupted, so the progress callback is the only place a caller can stop the computation. Without it, a Cancel button would stop only the waiting, while the computation continued and its result was discarded.
Progress reports are throttled, but the cancellation check runs at every call, so a Cancel click takes effect at the next unit of work.
endstation.forms: form validation
A form with several invalid fields reports all of them together, so that the user does not have to correct 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 data directory is reported with the name of the environment variable and the
path it resolved to. A bare FileNotFoundError: [Errno 2] on a path the user never
entered gives no indication of what to change.
endstation.testing: headless test suites
The plugin is registered through the pytest11 entry point, so no conftest needs to
load 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. Setting ENDSTATION_NO_HEADLESS=1 disables it.
Tests
.venv/bin/python -m pytest -q
The template
The same pyproject skeleton, CI workflow, headless conftest and ignore rules are also repeated across these projects. They should not be a runtime dependency, so they are provided as a copier template instead:
pipx run copier copy gh:jamccoy/endstation my-project
The template asks whether the project has a PySide6 window and whether its tests read
data from outside the checkout. A library project receives no gui/ directory, and its
CI does not install Qt system libraries it does not need.
The template also includes several details that are easy to overlook:
- A CI step that constructs a
QApplicationinstead of only importing PySide6. The import succeeds before the platform plugin loads, and a missing system library only shows up when the plugin loads, so constructing the application makes the failure appear next to the installation step that caused it. - A check that data-dependent tests were skipped rather than failed, and a minimum number of passing tests, so that a suite which loses tests without notice fails CI.
- No
qt_apisetting in[tool.pytest.ini_options]. It is a pytest-qt option, so setting it makes the suite fail on a machine without the toolkit; the plugin setsPYTEST_QT_APIinstead. - Local working notes are ignored from the first commit, so a scratch file is not published by accident.
tests/test_template.py renders both variants of the template and checks the output,
so a Jinja error, or a ${{ }} expression consumed by the renderer, fails in this
repository rather than in a newly generated project.
Metadata
Release files for endstation 0.2.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| endstation-0.2.5.tar.gz | 70.2 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| endstation-0.2.5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 124.5 kB
Release files / endstation-0.2.5.tar.gz
| Download URL | endstation-0.2.5.tar.gz |
|---|---|
| Size | 70.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
829c3dd5c07e088b41e784ac766eeaf2134db69e638edfc67fea792e982f1100
|
|
BLAKE2b-256 checksum How to use checksums |
6158dc176068f88941a9831d23b10004d21b3ee336a73f98dcd3c439a56f2197
|
| 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 23, 2026.
Transparency logRelease files / endstation-0.2.5-py3-none-any.whl
| Download URL | endstation-0.2.5-py3-none-any.whl |
|---|---|
| Size | 54.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fe91df0b83bd90f4a2465d8d8143a76ede3304808be957b22ef9668df0895d29
|
|
BLAKE2b-256 checksum How to use checksums |
c585225516e97817ff15519893b22cfc7893011f6edf8b95d311f3a17e91138c
|
| 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 23, 2026.
Transparency logSecurity for endstation 0.2.5
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.
Attestation for endstation-0.2.5.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 23, 2026.
Transparency logA 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.