answer from Python too

The engine is the same; only the front door is new. pyo3 behind an optional
feature, maturin for the wheel, and the bindings mirror DayJson rather than Day
because DayJson is already the shape front ends read and the one that has to stay
still. to_dict() therefore gives exactly what --json gives, and a test holds the
two to that.

That test earned its place immediately. The bindings were calling
jurisdictions::resolve(None), which answers with a built-in default that knows
nothing about the machine, so Python was reckoning by the New Calendar while the
command line on the same box reckoned by ROCOR — every field downstream of the
calendar differed, including the fast. They now take the same two steps main.rs
takes, and agree field for field on Pascha, the Nativity and Theophany.

The wheel is abi3, so one build serves 3.9 upward rather than one per
interpreter. That costs the datetime C API, which the limited API does not
expose, so a date is read through its attributes instead — which incidentally
makes datetime.datetime work as well as datetime.date. Constructing dates is
unaffected, pyo3 calls Python's own constructor for that.

panic = "abort" comes out of the release profile. An extension module has to be
able to unwind a panic into a Python exception; aborting would take the
interpreter down with it. The CLI keeps the same behaviour it always had, a
panic there just unwinds now.

Mixed layout so the wheel can carry py.typed and a stub file, which a bare
extension module has nowhere to put. mypy --strict is clean against the stubs
and catches a deliberate error, so the Typing :: Typed classifier is not a claim
about nothing.
This commit is contained in:
funkpower
2026-09-18 11:28:08 -04:00
parent 227abac6f6
commit 5d7e02cffb
11 changed files with 763 additions and 3 deletions
+36
View File
@@ -0,0 +1,36 @@
"""The Orthodox liturgical day: feasts, seasons and the fast, computed rather than fetched.
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.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.
"""
from ._typikon import (
Day,
__version__,
describe,
jurisdictions,
pascha,
today,
week,
)
__all__ = [
"Day",
"__version__",
"describe",
"jurisdictions",
"pascha",
"today",
"week",
]
+64
View File
@@ -0,0 +1,64 @@
import datetime
from typing import Any, Final
__version__: Final[str]
class Day:
"""One day, fully reckoned. Immutable."""
@property
def date(self) -> datetime.date: ...
@property
def iso(self) -> str: ...
@property
def weekday(self) -> str: ...
@property
def jurisdiction(self) -> str: ...
@property
def calendar(self) -> str: ...
@property
def church_date(self) -> str:
"""The ecclesiastical date, as "25 December"."""
@property
def season(self) -> str | None: ...
@property
def cycle(self) -> str | None: ...
@property
def feast(self) -> str | None: ...
@property
def rank(self) -> str | None: ...
@property
def title(self) -> str:
"""The feast if there is one, else the principal commemoration."""
@property
def commemorations(self) -> list[str]: ...
@property
def fast(self) -> str: ...
@property
def fast_label(self) -> str: ...
@property
def emoji(self) -> str: ...
@property
def reason(self) -> str: ...
@property
def obligation(self) -> str | None:
"""``"obligatory"``, ``"customary"``, or ``None``."""
@property
def binds(self) -> str: ...
@property
def pascha(self) -> datetime.date: ...
@property
def days_to_pascha(self) -> int: ...
@property
def julian_offset(self) -> int: ...
def is_obligatory(self) -> bool: ...
def to_dict(self) -> dict[str, Any]:
"""The same fields as ``typikon --json``."""
def describe(date: datetime.date, jurisdiction: str | None = None) -> Day: ...
def today(jurisdiction: str | None = None) -> Day: ...
def week(
start: datetime.date, days: int = 7, jurisdiction: str | None = None
) -> list[Day]: ...
def pascha(year: int, jurisdiction: str | None = None) -> datetime.date: ...
def jurisdictions() -> list[dict[str, str]]: ...
View File