From 7c93b6bc6362cbbdaef804be3f8eb3a2ec0617b2 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 21 Aug 2026 14:14:50 -0500 Subject: [PATCH 1/4] ci: add a PyPI release workflow, and modernize the license declaration This repo had no workflows at all, so nothing was built, checked, or published automatically -- and it is not on PyPI, while its README tells readers to pip install it. The workflow builds on every push and pull request via build-and-inspect-python-package (pinned to v3.0.1, whose Twine 7 understands the packaging metadata a PEP 639 license produces; the v2 line's Twine 6 does not), and publishes only on a published GitHub release. Publishing uses PyPI trusted publishing rather than a stored token: PyPI verifies the workflow's OIDC identity, so no long-lived secret exists to leak. It also attaches a build-provenance attestation. This requires a pending publisher to be configured on PyPI for the project name before the first release -- the workflow cannot create that, and the first release will fail without it. The license declaration is modernized in the same change, deliberately, because a release writes metadata to PyPI permanently and a version cannot be re-uploaded. The deprecated license table becomes a PEP 639 SPDX expression with license-files, and the redundant "License :: OSI Approved" classifier is removed -- current backends reject carrying both. Verified by building locally: the wheel now declares License-Expression: Apache-2.0 with the LICENSE captured, and passes twine check --strict. --- .github/workflows/cd.yml | 60 ++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 6 ++-- 2 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/cd.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..e9b9f61 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,60 @@ +name: CD + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + release: + types: + - published + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_COLOR: 3 + +jobs: + dist: + name: Distribution build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + # v3 ships Twine 7, which understands packaging metadata 2.4/2.5 — what a + # PEP 639 SPDX license declaration produces. v2 bundles Twine 6 and fails + # `twine check --strict`. v3 no longer force-tags majors, so pin in full. + - uses: hynek/build-and-inspect-python-package@v3.0.1 + + publish: + needs: [dist] + name: Publish to PyPI + environment: pypi + permissions: + # Trusted publishing: PyPI verifies this workflow's OIDC identity, so no + # API token is stored anywhere. Requires a pending publisher configured on + # PyPI for this project before the first release. + id-token: write + attestations: write + contents: read + runs-on: ubuntu-latest + if: github.event_name == 'release' && github.event.action == 'published' + + steps: + - uses: actions/download-artifact@v8 + with: + name: Packages + path: dist + + - name: Generate artifact attestation for sdist and wheel + uses: actions/attest-build-provenance@v4 + with: + subject-path: "dist/*" + + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/pyproject.toml b/pyproject.toml index d998733..6007bed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["hatchling"] +requires = ["hatchling>=1.27"] build-backend = "hatchling.build" [project] @@ -7,7 +7,8 @@ name = "dj-figpack-codecs" version = "0.2.0" description = "DataJoint codec for storing figpack visualizations in schema-addressed OAS" readme = "README.md" -license = { file = "LICENSE" } +license = "Apache-2.0" +license-files = ["LICENSE"] requires-python = ">=3.10" authors = [ { name = "DataJoint", email = "info@datajoint.com" } @@ -16,7 +17,6 @@ keywords = ["datajoint", "figpack", "visualization", "neuroscience"] classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Science/Research", - "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", From ab52f41b541fbf146bf5f7ffcfa6c1aa516ff260 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 21 Aug 2026 14:26:42 -0500 Subject: [PATCH 2/4] Declare the Python range datajoint actually supports Two problems, both making the package claim something untrue. The classifiers stopped at 3.12. datajoint 2.3.2 classifies 3.10 through 3.14, so this package was telling PyPI it did not support the two most recent Pythons, including the current stable release. There was no upper bound. datajoint pins <3.15; this package did not, so pip would happily install it on 3.15 into an environment where its only real dependency cannot be installed at all. requires-python is now >=3.10,<3.15, matching datajoint exactly rather than approximating it -- the floor and the ceiling both come from the dependency that determines them. Verified by building: Requires-Python: <3.15,>=3.10, classifiers through 3.14, and twine check --strict passes. --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6007bed..a31cd5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "DataJoint codec for storing figpack visualizations in schema-addr readme = "README.md" license = "Apache-2.0" license-files = ["LICENSE"] -requires-python = ">=3.10" +requires-python = ">=3.10,<3.15" authors = [ { name = "DataJoint", email = "info@datajoint.com" } ] @@ -21,6 +21,8 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering", ] From baaa8cef418f3c7d11d723f3cb336148c7090eb0 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 21 Aug 2026 14:30:35 -0500 Subject: [PATCH 3/4] Drop the upper bound on requires-python; keep the classifiers The cap I added is unnecessary and would cause the failure mode it was meant to prevent. Removing it. My justification for it was wrong. I claimed that without a cap, pip on 3.15 would install this package into an environment where datajoint cannot be installed. It will not: this package pins datajoint>=2.0, and a dependency's requires-python is honored during resolution, so pip fails there with an error that names datajoint -- the accurate diagnosis. Verified by resolving into a 3.9 environment: `datajoint>=2.0` fails cleanly with "datajoint>=2.0.0 cannot be used", while bare `datajoint` silently resolves to 0.14.9, an ancient release predating 2.0. That silent downgrade is the argument against caps. requires-python is a resolution input, so a capped release is skipped rather than reported: on a Python the cap excludes, pip installs an older release of this package instead of saying why. And a published cap cannot be relaxed -- when 3.15 ships and works, every capped version still refuses it, and support requires a new release rather than nothing at all. The classifiers stay at 3.10 through 3.14, which is the range actually tested and claimed. Classifiers are documentation and carry no resolution behavior, so they can say what is verified without constraining what is possible. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a31cd5f..6a56762 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "DataJoint codec for storing figpack visualizations in schema-addr readme = "README.md" license = "Apache-2.0" license-files = ["LICENSE"] -requires-python = ">=3.10,<3.15" +requires-python = ">=3.10" authors = [ { name = "DataJoint", email = "info@datajoint.com" } ] From 000e582c6381683325a41a395fb7a682022d7b34 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 21 Aug 2026 14:33:57 -0500 Subject: [PATCH 4/4] Derive the version from the git tag, mirroring dj-zarr-codecs The version was declared statically in pyproject.toml, and in this package's __init__.py as well, so two files had to agree with each other and with whatever tag a release carried. Nothing enforced that, and a release where they disagree is the kind of bug that is only visible after publishing. Now hatchling plus hatch-vcs, matching dj-zarr-codecs: the git tag is the single source of truth, the build hook writes _version.py, and the package imports it. A release is a tag, and no pull request touches a version number -- which is what we want anyway. Consequence worth stating plainly: the previously declared version is gone. Until a tag exists the build produces a development version derived from the commit distance, and the first tag is what fixes the number. That tag is a decision, not a carry-over. Also adds a .gitignore, which this repo did not have, covering the generated _version.py and the usual build artifacts. --- .gitignore | 33 ++++++++----------------------- pyproject.toml | 13 ++++++++++-- src/dj_figpack_codecs/__init__.py | 3 ++- 3 files changed, 21 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 7e81c99..a6617e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,13 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class +# Generated by the hatch-vcs build hook — the git tag is the source of truth +src/**/_version.py -# Distribution / packaging -dist/ +# Build artifacts build/ +dist/ *.egg-info/ -*.egg - -# Virtual environments +__pycache__/ +*.py[cod] .venv/ -venv/ -ENV/ - -# IDE -.idea/ -.vscode/ -*.swp -*.swo - -# Testing .pytest_cache/ -.coverage -htmlcov/ - -# OS -.DS_Store -Thumbs.db +.ruff_cache/ +.mypy_cache/ diff --git a/pyproject.toml b/pyproject.toml index 6a56762..ce46961 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,15 @@ [build-system] -requires = ["hatchling>=1.27"] +requires = ["hatchling>=1.27", "hatch-vcs"] build-backend = "hatchling.build" [project] name = "dj-figpack-codecs" -version = "0.2.0" description = "DataJoint codec for storing figpack visualizations in schema-addressed OAS" readme = "README.md" license = "Apache-2.0" license-files = ["LICENSE"] requires-python = ">=3.10" +dynamic = ["version"] authors = [ { name = "DataJoint", email = "info@datajoint.com" } ] @@ -67,3 +67,12 @@ addopts = "-v" [tool.black] line-length = 100 target-version = ["py310"] + +# The git tag is the single source of truth for the version — mirrors dj-zarr-codecs. +# The build hook writes _version.py, which the package imports, so nothing declares a +# version twice and nothing needs bumping in a pull request. +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build.hooks.vcs] +version-file = "src/dj_figpack_codecs/_version.py" diff --git a/src/dj_figpack_codecs/__init__.py b/src/dj_figpack_codecs/__init__.py index 2f332c5..587e2c8 100644 --- a/src/dj_figpack_codecs/__init__.py +++ b/src/dj_figpack_codecs/__init__.py @@ -41,5 +41,6 @@ def make(self, key): from .codec import FigpackCodec from .ref import FigpackRef +from ._version import version as __version__ -__all__ = ["FigpackCodec", "FigpackRef", "__version__"] +__all__ = ["__version__", "FigpackCodec", "FigpackRef", "__version__"]