fix what a 1.0 would have welded into PyPI metadata

An adversarial review of the release, and four of its findings were things
PyPI never lets you replace once uploaded.

  - rust-version said 1.74. clap has needed 1.85 since 4.6, and cargo
    hard-errors rather than warning, so the sdist advertised a floor it could
    not build on. docs/releasing.md sends macOS and Windows users to that
    sdist.
  - The wheel was classified Development Status :: 4 - Beta on a release the
    changelog calls stable, with none of the Python version classifiers PyPI
    indexes on.
  - Six README links were relative. PyPI resolves them against the project
    page, so every one 404'd there, as they do on the live 0.2.0 page now.
  -  in that order packs the wheel into the
    sdist. The tarball on disk was 431KB with an x86-64 binary inside it.

Also: twine upload gains --skip-existing, so a retry after a partial upload
finishes rather than dying on a version that cannot be reused; twine check
gains the renderer that makes it parse the description at all, without which
it returns None and reports PASSED; and the README no longer claims a
twenty-year sweep the test did not run, because 366 * 21 / 4 is five years.
The test now runs the twenty-one it was named for.

The crates.io name belongs to an unrelated project, which the README now says
rather than leaving a reader to find out from cargo.
This commit is contained in:
funkpower
2026-09-18 13:41:41 -04:00
parent 785754295b
commit 7970f785c8
9 changed files with 64 additions and 27 deletions
+9 -3
View File
@@ -39,7 +39,10 @@ jobs:
run: |
apt-get update && apt-get install -y --no-install-recommends python3 python3-venv
python3 -m venv /tmp/venv
/tmp/venv/bin/pip install --quiet 'maturin[zig]' twine
# readme-renderer[md] is what makes `twine check` actually parse the
# long description. Without it the render returns None and the
# check reports PASSED regardless, even with --strict.
/tmp/venv/bin/pip install --quiet 'maturin[zig]' twine 'readme-renderer[md]'
# --zig cross-compiles against an older glibc than this image ships, so
# the wheel is not silently pinned to bookworm's.
for target in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do
@@ -60,5 +63,8 @@ jobs:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: |
/tmp/venv/bin/twine check dist/*
/tmp/venv/bin/twine upload --non-interactive dist/*
/tmp/venv/bin/twine check --strict dist/*
# --skip-existing so a retry after a partial upload finishes the job
# instead of dying on the first file. A version cannot be reused,
# so a half-published release would otherwise burn it.
/tmp/venv/bin/twine upload --non-interactive --skip-existing dist/*
+2
View File
@@ -3,3 +3,5 @@
.venv/
*.egg-info/
/dist
__pycache__/
*.py[cod]
+10
View File
@@ -49,6 +49,16 @@ Everything below changes what a day reports against pre-1.0 versions.
- `--set-default` remembers the tradition and the discipline alongside the
jurisdiction.
### Packaging
- Requires Rust 1.85, which is what the dependency tree has needed since clap
4.6. `rust-version` had said 1.74, so the source distribution claimed to
build on toolchains where cargo refuses outright.
- Classified as production/stable, with the Python version and platform
classifiers PyPI indexes on, and Changelog, Documentation and Issues links.
- README links are absolute, because PyPI does not resolve relative ones and
every one of them 404'd on the project page.
### Changed
- Rewritten in Rust. It ships as one static binary with the menologion compiled
+1 -1
View File
@@ -2,7 +2,7 @@
name = "typikon"
version = "1.0.0"
edition = "2021"
rust-version = "1.74"
rust-version = "1.85"
description = "The Orthodox liturgical day: feasts, seasons and the fast, computed rather than fetched"
license = "GPL-2.0-only"
repository = "https://git.pumpin.online/funkpower/typikon"
+19 -15
View File
@@ -45,6 +45,10 @@ One static binary, no runtime to install first.
$ cargo install --git https://git.pumpin.online/funkpower/typikon
```
Rust 1.85 or newer. The name `typikon` on crates.io belongs to an unrelated
project, so `cargo install typikon` and a `typikon = "1"` dependency will not
get you this; install and depend on it by git URL.
Or build it from a checkout:
```console
@@ -63,7 +67,9 @@ $ pip install typikon
```
The menologion is compiled into that too, so it carries no data files and needs
no network either.
no network either. Wheels are built for Linux on x86-64 and aarch64; everywhere
else pip falls back to the source distribution, which needs a Rust toolchain of
1.85 or newer on the installing machine.
## Which calendar?
@@ -176,7 +182,7 @@ will want.
Ten of the fourteen Byzantine-rite Eastern Catholic churches have no entry of
their own. Each keeps one of the three reckonings, and
[docs/calendars.md](docs/calendars.md) sets out which questions decide it.
[docs/calendars.md](https://git.pumpin.online/funkpower/typikon/src/branch/main/docs/calendars.md) sets out which questions decide it.
## The fast
@@ -425,22 +431,18 @@ It lands in `~/.local/share/typikon/` and is overlaid automatically.
## Further reading
- [docs/calendars.md](docs/calendars.md) sets out which churches keep which
- [docs/calendars.md](https://git.pumpin.online/funkpower/typikon/src/branch/main/docs/calendars.md) sets out which churches keep which
reckoning, how the split came about, what is modelled here and what is not.
- [docs/versioning.md](docs/versioning.md) says what counts as the public
- [docs/versioning.md](https://git.pumpin.online/funkpower/typikon/src/branch/main/docs/versioning.md) says what counts as the public
surface, and how a correction to the calendar itself is announced.
- [CHANGELOG.md](CHANGELOG.md) carries those announcements.
- [CHANGELOG.md](https://git.pumpin.online/funkpower/typikon/src/branch/main/CHANGELOG.md) carries those announcements.
## Accuracy
`typikon` is pinned against published calendars: the paschalion for six years
by both reckonings and its invariants across two centuries, 146,000 calendar
round-trips from 1800 to 2200, the fast on every day that is not open to
interpretation, and every jurisdiction over twenty years.
It was ported from a Python implementation, and the port was checked by
running both over 199,446 day-records, 26 jurisdictions across 21 years, and
requiring every field of every record to match.
`typikon` is pinned against published calendars: the paschalion for eleven
named years and its invariants across the two centuries from 1900, 146,000
calendar round-trips from 1800 to 2200, the fast on every day that is not open
to interpretation, and every jurisdiction over the twenty-one years from 2020.
It is a calendar, not an authority. If it disagrees with the one your parish
prints, your parish is right. Please open an issue.
@@ -455,10 +457,12 @@ widgets for Hyprland (Quickshell), waybar and eww, all reading the JSON above.
## Releasing
`main` is always releasable; a `vX.Y.Z` tag on it builds the wheels and publishes
to PyPI. [docs/releasing.md](docs/releasing.md) has the workflow, and what has to
to PyPI. [docs/releasing.md](https://git.pumpin.online/funkpower/typikon/src/branch/main/docs/releasing.md) has the workflow, and what has to
be set up before the first one.
## Licence
GPL-2.0-only. See [LICENSE](LICENSE). The menologion is under it too;
GPL-2.0-only. The source for any published release is the source distribution
beside it on PyPI, and `Cargo.lock` pins every dependency to an exact version.
See [LICENSE](https://git.pumpin.online/funkpower/typikon/src/branch/main/LICENSE). The menologion is under it too;
`data/LICENSE` says so and notes what the fetched sets are not.
+2 -1
View File
@@ -110,7 +110,8 @@ developers, a wall for everyone else.
## Checking a release without publishing
```console
$ maturin build --release --out dist && maturin sdist --out dist
$ rm -rf dist # a wheel left in dist/ gets packed into the sdist
$ maturin sdist --out dist && maturin build --release --out dist
$ twine check dist/*
$ python -m venv /tmp/v && /tmp/v/bin/pip install --no-binary :all: dist/*.tar.gz
$ /tmp/v/bin/python -c 'import typikon, datetime; print(typikon.today())'
+14 -2
View File
@@ -11,10 +11,19 @@ license-files = ["LICENSE"]
requires-python = ">=3.9"
keywords = ["orthodox", "liturgical", "calendar", "byzantine", "pascha", "fasting"]
classifiers = [
"Development Status :: 4 - Beta",
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Developers",
"Programming Language :: Rust",
"Intended Audience :: End Users/Desktop",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Rust",
"Topic :: Religion",
"Topic :: Software Development :: Libraries",
"Typing :: Typed",
@@ -25,6 +34,9 @@ dynamic = ["version"]
[project.urls]
Homepage = "https://git.pumpin.online/funkpower/typikon"
Repository = "https://git.pumpin.online/funkpower/typikon"
Changelog = "https://git.pumpin.online/funkpower/typikon/src/branch/main/CHANGELOG.md"
Documentation = "https://git.pumpin.online/funkpower/typikon/src/branch/main/docs/calendars.md"
Issues = "https://git.pumpin.online/funkpower/typikon/issues"
[tool.maturin]
bindings = "pyo3"
+6 -4
View File
@@ -4,15 +4,17 @@ Everything derives from the paschalion and the fixed calendar, so this needs no
network, cannot go stale, and answers for any year you ask about.
>>> import typikon, datetime
>>> day = typikon.describe(datetime.date(2026, 4, 12))
>>> day = typikon.describe(datetime.date(2026, 4, 12), jurisdiction='new')
>>> day.feast
'The Radiant and Life-giving Resurrection of our Lord — Holy Pascha'
>>> day.fast, day.days_to_pascha
('none', 0)
Called without a jurisdiction, every function uses the one `typikon
--set-default` wrote, so the library and the command line agree about the same
date. Pass ``jurisdiction=`` to override it; ``jurisdictions()`` lists the keys.
The jurisdiction is given here because 12 April 2026 is Pascha on the Julian
paschalion and an ordinary Sunday on the Gregorian one. Called without one,
every function uses whatever `typikon --set-default` wrote, so the library and
the command line agree about the same date. Pass ``jurisdiction=`` to override
it; ``jurisdictions()`` lists the keys.
"""
from ._typikon import (
+1 -1
View File
@@ -371,7 +371,7 @@ fn ics_is_well_formed() {
#[test]
fn every_jurisdiction_holds_together_for_twenty_years() {
for jur in JURISDICTIONS.iter().chain(CALENDARS.iter()) {
for day in week(d("2020-01-01"), 366 * 21 / 4, jur) {
for day in week(d("2020-01-01"), 366 * 21, jur) {
assert!(!day.commemorations.is_empty(), "{} {}", jur.key, day.date);
assert!(!day.title().is_empty(), "{} {}", jur.key, day.date);
// `binds` may never be stricter than the level it sits under.