Armenian Lectionary
A self-contained, offline engine that returns the Armenian Church (Տօնացոյց / Ճաշոց) scripture readings for any day. Use it two ways:
pip install armenian-lectionary— import the engine into any Python app and generate readings offline (see Use the engine offline).- Hosted API — one HTTP endpoint on Google Cloud Run for network-tolerant or non-Python callers.
No network is used at runtime. The readings are produced by a calendar algorithm combined with an embedded, cross-year-validated data table.
Live: the service is hosted on Google Cloud Run at https://lectionary.andylitalo.com — try it:
curl "https://lectionary.andylitalo.com/readings" # today
curl "https://lectionary.andylitalo.com/readings?date=2026-04-05" # any date, 2001–2027
How it works
The Armenian lectionary is one of the oldest in Christendom, and most of its sanctoral is movable — commemorations are anchored to feasts, not civil dates (e.g. Sts. Hripsime always falls on Easter + 57, so it drifts year to year). The engine therefore works in two steps:
- Calendar framework (
armenian_lectionary/engine.py) computes each date's liturgical coordinates from the feast chain:- Easter (Meeus/Jones/Butcher, Gregorian as the Armenian Church uses since 1923) anchors Pre-Lent → Lent → Holy Week → Eastertide → Pentecost → post-Pentecost → Transfiguration (Easter + 98).
- Solar feasts — Assumption (Sun closest to Aug 15), Exaltation of the Cross (Sun closest to Sep 14), Heesnak/Advent (Sun closest to Nov 18).
- Fixed feasts — Theophany/Nativity (Jan 6), Presentation (Feb 14), Annunciation (Apr 7), etc.
- Embedded table (
lectionary_data.json) maps each coordinate to the actual readings. It was distilled and cross-year validated from 13 years (2014–2026) of the authoritative Tonatsooyts: an entry is kept only if every year that hit that coordinate agreed on the readings.
Accuracy
Validated against every day of 2001–2026 (9,495 days) from the authoritative Tōnatsooyts. 2027 is also served and was validated separately as a held-out forward year.
| Metric | Result |
|---|---|
| Coverage (any readings shipped) | 100.00% (9,495 / 9,495) |
| Exact reading match | 99.21% (9,420 / 9,495) |
| Wrong (validated tier hit but mismatched) | 0 |
Validated-tier days (validated-*) |
9,273 (all exact) |
| Blank (no readings available) | 0 days |
The hard invariant holds: a validated entry (Source validated-table /
validated-composite) is never wrong — 0 mismatches across all 9,495 days.
Coverage is now complete: every day 2001–2026 ships readings, and there are no
blank (algorithmic-estimate) days — the former summer gaps and winter "hinge"
(Advent → Theophany → pre-Lent) gap are resolved. The remaining ~0.8% that aren't
byte-exact all fall on the labeled best-guess / source-derived tiers (e.g.
first-volume-cohort, generative-composite), where the engine's reading differs
from the sacredtradition.am test-oracle — typically a versification /
verse-boundary convention on the same pericope, not a missing reading. Gate on
Source to include or exclude these. Regenerate these figures with
python dev/compare_app.py.
Feast-name accuracy
Every result also carries the feast/fast name of the day in the "Liturgical Day"
field. As of 1.1.0 this name is locked against the same authoritative ground truth:
the engine's commemoration matches the source on all 9,495 days of 2001–2026 (100%),
with no exceptions and no allowlist (tests/test_observance.py). The engine always serves a
concrete, source-matched name — never a placeholder. As of 1.1.1 the name's
components (calendar-position label, commemoration, eve/status note) are joined with a
— separator that mirrors the source's own <br> structure — e.g. Twentieth day of Eastertide — Remembrance of the Armenian Genocide (1915) — instead of running together.
The match is checked on the commemoration component — the saint/feast identity. The
source string also prepends a year-varying calendar-position label ("Nth day of
<Season>", "Nth Sunday after <Anchor>") that a static engine cannot
byte-reproduce, so that positional prefix is normalized out on both sides before
comparison (dev/observance_names.py); a small set of reviewed companion-enumeration and
transliteration variants are reconciled symmetrically (dev/source_corrections.canonical_commem).
Naming nuances may still be refined as experts review. Audit with python dev/observance_audit.py.
Eight-mode cycle
Every result includes a structured "Mode" object. Its "Tone" is the canonical
Armenian abbreviation for the day's eight-mode (ութ ձայն) assignment—ԱՁ, ԱԿ,
ԲՁ, ԲԿ, ԳՁ, ԳԿ, ԴՁ, or ԴԿ—and its integer "Number" is the corresponding
stable value from 1 through 8. Both values are liturgical identifiers and are unchanged
by language. The cycle is re-anchored at Great Barekendan (ԴԿ, 8), advances daily
from there, and places Easter Sunday at ԱՁ, 1; earlier dates continue from the preceding
Easter instead of restarting on January 1. The calculation is date-only and constant-time.
Tests exercise all 9,495 dates from 2001–2026, with SacredTradition fixtures spanning
every year and the annual reset; calculate_liturgical_mode(date) returns the same
{"Tone": tone, "Number": number} record without resolving the readings.
Source fidelity & known typos
The engine treats the printed Տօնացոյց (Tōnatsooyts) as the primary source and
sacredtradition.am as a test oracle (see dev/source_corrections.py). Systematic
versification-convention differences are corrected on the source-derived tiers; confirmed
typographical errors in the printed source are catalogued in the digitization repo,
next to the source, at
grabar-ocr corpus/TYPOS.md,
and where a source typo would yield a wrong reading the engine ships the corrected value.
For example, the eve of the Presentation of the Lord (Feb 13) Proverbs reading is
printed in the Tōnatsooyts (First Volume p. 462) as Առակ. Ը. 22 … վ. 24
("Proverbs 8:22-24"), but its quoted endpoint «որ զճանապարհս իմ պահիցէ» is verbatim the
close of Proverbs 8:34 (the singular «պահիցէ» distinguishes it from the near-identical
8:32) — a transposed digit, 24 for 34. The engine ships the corrected
Proverbs 8.22-34, which sacredtradition renders unanimously (23/23 Feb-13 eve years,
26/26 Feb-14 feast years). See
grabar-ocr corpus/TYPOS.md
for the full analysis.
Roadmap
A complete engine covering the entire 532-year Great Paschal Cycle — the full period after which the Armenian movable calendar repeats — is forthcoming. It awaits validation against the Տօնացոյց (Tōnatsooyts), currently being transcribed and translated in the grabar-ocr repository (a pipeline for digitizing Grabar / Classical Armenian texts). Until that validation lands, the served range stays within the tested 2001–2027 window.
Setup & run
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt # web layer (Flask, gunicorn)
pip install -e . # the lectionary engine package
python app.py # serves http://127.0.0.1:5001
Endpoint
GET /readings?date=YYYY-MM-DD&language=en
date is optional (defaults to today). Supported range is 2001–2027
(env-overridable via LECTIONARY_MIN_YEAR / LECTIONARY_MAX_YEAR); a date
outside it returns HTTP 400 with a message explaining the current range.
language is optional — en (default) or hy for Classical Armenian (alias
lang); an unsupported value returns HTTP 400. In hy the feast name and the
scripture book names come back in Armenian (?date=2026-04-05&language=hy →
"ՅԱՐՈՒԹԻՒՆ ՏԵԱՌՆ ՄԵՐՈՅ ՅԻՍՈՒՍԻ ՔՐԻՍՏՈՍԻ (Զատիկ)", "Աւետարան ըստ Յովհաննէսի 20.1-18"); provenance fields stay English. Example:
curl "https://lectionary.andylitalo.com/readings?date=2026-06-01"
{
"Date": "2026-06-01",
"Liturgical Day": "Sts. Hripsime and Her Companions",
"Mode": {
"Tone": "ԱԿ",
"Number": 2
},
"Readings": {
"Old Testament": ["Proverbs 31.29-31", "Isaiah 61.10-62.3"],
"Epistle": ["St. Paul's Epistle to the Romans 15.30-16.2"],
"Gospel": ["Matthew 10.26-33"]
},
"ReadingsList": ["Proverbs 31.29-31", "Isaiah 61.10-62.3",
"St. Paul's Epistle to the Romans 15.30-16.2",
"Matthew 10.26-33"],
"ReadingsRefs": [
{"book": "Proverbs", "start_chapter": 31, "start_verse": 29,
"end_chapter": 31, "end_verse": 31, "citation": "Proverbs 31.29-31"},
{"book": "Isaiah", "start_chapter": 61, "start_verse": 10,
"end_chapter": 62, "end_verse": 3, "citation": "Isaiah 61.10-62.3"}
],
"Observances": [
{"id": "hripsime_and_her_companions", "name": "Sts. Hripsime and Her Companions",
"is_fast": false, "is_comm": true}
],
"ObservanceIds": ["hripsime_and_her_companions"],
"Source": "validated-table"
}
ReadingsRefs gives the same readings as structured
{book, start_chapter, start_verse, end_chapter, end_verse, citation} dicts, one per
sub-reference, so a consumer does not have to parse citation strings like
"Mark 15.42-16.1" back apart itself. book is always the canonical English head,
independent of language. A composite citation — currently only the Daniel/Azariah
reading, "Daniel 3.1-23, Azariah. 1-68" — expands to two dicts sharing that
citation string, the back-pointer to their shared ReadingsList entry.
Observances: the day as a list, with its marks
"Liturgical Day" is corrected data, not a stable key — a spelling fix or a source
correction can change the string a given day serves. "Observances" gives that day as the
ordered list it already is internally, one entry per component:
>>> r = compute_armenian_lectionary(datetime.date(2004, 11, 21))
>>> r["Liturgical Day"]
'Eleventh Sunday of the Holy Cross — Presentation of the Holy Mother of God to the Temple — Eve of the Fast of Advent'
>>> r["Observances"]
[{'id': 'eleventh_sunday_of_the_holy_cross', 'name': 'Eleventh Sunday of the Holy Cross',
'is_fast': False, 'is_comm': False},
{'id': 'presentation_of_the_holy_mother', 'name': 'Presentation of the Holy Mother of God to the Temple',
'is_fast': False, 'is_comm': True},
{'id': 'eve_of_fast_of_advent', 'name': 'Eve of the Fast of Advent',
'is_fast': False, 'is_comm': True}]
idis the stable catalog key, independent oflanguage— what a consumer should persist instead of the display string. Once published, an id keeps meaning the same observance forever.nameis that component's served text in the requestedlanguage, so" — ".join(o["name"] for o in r["Observances"])reproduces"Liturgical Day". A consumer never has to split the display string.is_fast/is_commare per-observance marks (below). A future attribute is a new key here, not a new top-level field.
"ObservanceIds" is the id projection of the same list, kept as its own field:
>>> r["ObservanceIds"]
['eleventh_sunday_of_the_holy_cross', 'presentation_of_the_holy_mother', 'eve_of_fast_of_advent']
A day is identified by the whole ordered list, not any single id — some days name two to
four observances at once (a calendar position, a commemoration, an eve note). Resolution is
all or nothing: a day whose components cannot all be resolved (a thin install with no
catalog data, for instance) gets [] rather than a list with a hole in it, since a partial
list would silently identify a different day than the one actually served. So an empty
list means "this day did not resolve" and nothing else — check it before reading the
marks.
The two marks, and why they are independent
is_fast says the observance is a fast. is_comm says it commemorates a person or an
event, as opposed to only locating the day in the calendar. Neither is the other's
negation — all four combinations occur:
is_comm |
not is_comm |
|
|---|---|---|
is_fast |
third_sunday_of_great_lent — Sunday of the Prodigal Son |
wednesday_fast, great_friday |
not is_fast |
appearance_of_the_holy_cross |
third_day_of_nativity |
So a consumer rendering the day's saints and feasts filters on is_comm. Filtering on
not is_fast instead would put Fifth day of Eastertide on the same footing as the
Ascension — of the 390 ids served in range, 284 are not fasts but only 183 are
commemorations; the other 101 are bare position labels, carried on 2,534 days.
>>> r = compute_armenian_lectionary(datetime.date(2026, 5, 3))
>>> r["Liturgical Day"]
'Fifth Sunday of Eastertide — Appearance of the Holy Cross'
>>> [o["name"] for o in r["Observances"] if o["is_comm"]]
['Appearance of the Holy Cross']
Both marks are per-observance, not per-date. is_fast does not answer "is this date a
fast day": the two differ on the 727 days in range that name both a fast and a
commemoration, and deciding the date needs feast/fast precedence rules this engine does not
implement. Likewise 5,070 of the 9,861 days in range carry no is_comm component at
all — the weekly Wed/Fri fast alone is 1,334 — because those days commemorate nobody, so
an empty result there is a normal answer, not an error. A Holy Week day-name is a fast and
not a commemoration for the same reason: what the day commemorates is a separate component
beside it (Great Friday — Remembrance of the Passion, …).
One known gap: the deprecated fast_day id is never served, so on Dec 9 in 2005, 2011,
2016 and 2022 — the four days in range whose position label is the bare fast marker with
nothing more specific — no component is marked is_fast on a day the engine's own tables
call a fast. See CLAUDE.md, "A day's observances, and the marks on them".
An unparseable date returns HTTP 400. GET / returns usage JSON, and
GET /health returns {"status": "ok"} for liveness checks.
Requests are rate-limited per client IP (default 60/min, 600/hour);
exceeding a limit returns HTTP 429. Limits are configurable via
LECTIONARY_RATE_LIMITS.
Use the engine offline (Python package)
Don't want to depend on the hosted API — e.g. for a fully offline app? Install the engine as a package. It has no third-party dependencies (Python 3.9+ standard library only), and all the readings data (the validated table plus the source-derived saint & continua data) ships inside the wheel, so nothing is fetched at runtime:
pip install armenian-lectionary
import datetime
import armenian_lectionary
reading = armenian_lectionary.compute_armenian_lectionary(datetime.date(2026, 4, 5))
print(reading["Liturgical Day"]) # RESURRECTION OF OUR LORD JESUS CHRIST (Easter Sunday)
print(reading["Mode"]) # {'Tone': 'ԱՁ', 'Number': 1}
print(reading["ReadingsList"]) # ['John 20.1-18', 'Acts of the Apostles 1.1-8', ...]
print(reading["ReadingsRefs"][0]) # {'book': 'John', 'start_chapter': 20, 'start_verse': 1,
# 'end_chapter': 20, 'end_verse': 18, 'citation': 'John 20.1-18'}
print(reading["ObservanceIds"]) # ['resurrection_of_our_lord'] -- stable across renames
# Armenian names: pass language="hy" (default "en"). ReadingsRefs' "book" stays English.
hy = armenian_lectionary.compute_armenian_lectionary(datetime.date(2026, 4, 5), language="hy")
print(hy["Liturgical Day"]) # ՅԱՐՈՒԹԻՒՆ ՏԵԱՌՆ ՄԵՐՈՅ ՅԻՍՈՒՍԻ ՔՐԻՍՏՈՍԻ (Զատիկ)
print(hy["ReadingsList"][0]) # Աւետարան ըստ Յովհաննէսի 20.1-18
The distribution name is armenian-lectionary; the import name is
armenian_lectionary. The public API is intentionally small —
compute_armenian_lectionary(date, language="en") and
calculate_gregorian_easter(year), plus the date-only
calculate_liturgical_mode(date) helper, which returns a Mode-shaped record with the
canonical Armenian Tone and integer Number, and the MIN_YEAR / MAX_YEAR bounds of
the supported range.
(Internal calendar helpers and constants remain importable from
armenian_lectionary.engine if you need them.)
Command line
The package installs an armenian-lectionary console script that prints the
readings as JSON in native Armenian script:
armenian-lectionary # today
armenian-lectionary 2026-04-05 # any date
python -m armenian_lectionary.cli 2026-04-05 # equivalent
Gating on Source / Confidence
compute_armenian_lectionary(date) returns a dict and never makes a network call. It
raises ValueError for an unsupported language, or for a date outside 2001–2027
(MIN_YEAR–MAX_YEAR, both exported and overridable via LECTIONARY_MIN_YEAR /
LECTIONARY_MAX_YEAR) — outside that window there is no validated data, and the engine
would otherwise hand back an internal absence-marker as if it were a feast name.
calculate_liturgical_mode(date) is unrestricted: it is pure arithmetic on the paschal
cycle and correct for any date.
Because the output blends tiers of differing certainty, gate on the Source field (and
Confidence where present) rather than assuming every result is authoritative:
validated-*— cross-year-validated against the authoritative Tōnatsooyts; never wrong across all 9,495 tested days.second-volume-cycle/generative-continua/ other generative tiers — best-guess readings derived from the source laydown, flagged as such.algorithmic-estimate— no readings confidently derivable;"ReadingsList"(and"ReadingsRefs") is empty. Gate on an empty list rather than expecting an error.
See the Source values under Accuracy for the full picture.
License & data provenance
The package ships LICENSE (Apache-2.0) and NOTICE in its distribution
metadata. The bundled readings are traditional works of the Armenian Church and
are not claimed as original authorship — see
Data provenance & attribution below.
Deploy (Google Cloud Run)
The app is containerized (Dockerfile) and runs under gunicorn. Deploy from
source (Cloud Build builds the image):
gcloud run deploy lectionary --source . --region us-central1 \
--allow-unauthenticated --memory 512Mi --min-instances 0 --max-instances 3
Scales to zero (~$0/month at < 1000 req/day). A custom domain is attached via
gcloud run domain-mappings create.
Project layout
| Path | Purpose |
|---|---|
pyproject.toml |
Package metadata + build config (hatchling); defines the armenian-lectionary distribution and console script. |
armenian_lectionary/ |
The installable Python package. |
armenian_lectionary/engine.py |
Offline engine: calendar coordinates + table lookup. Public entry: compute_armenian_lectionary. |
armenian_lectionary/cli.py |
armenian-lectionary console entry point. |
armenian_lectionary/data/lectionary_data.json |
Embedded, validated readings table (shipped in the wheel). |
armenian_lectionary/data/{second_volume_cycles,saint_readings,saint_schedule,continua_sequence}.json |
Shipped source-derived saint & continua data (Tōnats'oyts Second Volume laydown + Fast-of-Assumption continua) feeding the second-volume-cycle and generative-continua tiers. |
app.py |
Flask app (one endpoint); imports the package. |
Dockerfile |
Container image for Cloud Run (pip install . + gunicorn). |
dev/ |
Dev-only tooling: ground-truth fetcher, analysis, table builder, comparison harness. Not used at runtime; not shipped in the package. |
Rebuilding / extending the table (dev)
python dev/bulk_fetch.py 2014-01-01 2027-12-31 # cache ground truth (network)
python dev/build_table.py # rebuild & validate -> armenian_lectionary/data/lectionary_data.json
python dev/compare_app.py # runtime accuracy report
sacredtradition.am is used only as the development ground-truth source;
the shipped app never contacts it.
Data provenance & attribution
The readings in lectionary_data.json are distilled and cross-year validated
against the authoritative Տօնացոյց (Tōnatsooyts) and Ճաշոց of the Armenian Church.
Sources:
- Primary — the Տօնացոյց itself: the 4th edition, printed at the St. James Press (Tparan Srbots Hakobeants), Jerusalem, 1915. This is the canonical rubric the readings must be derivable from.
- Development test-oracle — sacredtradition.am:
used only during development to build and validate the table. It is never
contacted at runtime, and its raw cache (
dev/reference_data/) is not distributed with this repository.
Discrepancy policy. A few slight discrepancies have been found between
sacredtradition.am and the printed Տօնացոյց (mostly versification / verse-boundary
conventions on the same pericope). Where a discrepancy is confirmed, the engine
defaults to the Տօնացոյց as the authoritative source and treats the scrape as a
test oracle only. These corrections are recorded, with per-reading rationale, in
dev/source_corrections.py (see also the working
record in reports/lectionary_disagreements.md).
Further investigation of these discrepancies is in progress.
The distilled table is provided for reference and study — for liturgical use, verify against authoritative published sources.
License
Code and configuration in this repository are licensed under the
Apache License 2.0 — see LICENSE and NOTICE. The
underlying liturgical calendar and scripture readings are traditional works of the
Armenian Church and are not claimed as original authorship here; see Data
provenance & attribution above.
Metadata
Release files for armenian-lectionary 2.1.0
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 | |
|---|---|---|---|---|
| armenian_lectionary-2.1.0.tar.gz | 214.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| armenian_lectionary-2.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 439.5 kB
Release files / armenian_lectionary-2.1.0.tar.gz
| Download URL | armenian_lectionary-2.1.0.tar.gz |
|---|---|
| Size | 214.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
60725c7a525999e1e19baa4849ea9aceaf1e4a6c35d8970a29824dd69ed737c3
|
|
BLAKE2b-256 checksum How to use checksums |
1c84f7492f50249dd77d5377fec9e44a43fa6a20ccdedff641d8435ed960c822
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Sep 6, 2026.
Transparency logRelease files / armenian_lectionary-2.1.0-py3-none-any.whl
| Download URL | armenian_lectionary-2.1.0-py3-none-any.whl |
|---|---|
| Size | 225.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3a90b2783608300e484b7703bf25ef51ced22bc9656d42d57719b0a2e120f84c
|
|
BLAKE2b-256 checksum How to use checksums |
1c1bf8515b0f01b07f3d67a43380052d1da4906863ac0f6b06d5ddf66e6534c6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Sep 6, 2026.
Transparency logSecurity for armenian-lectionary 2.1.0
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 armenian_lectionary-2.1.0.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 Sep 6, 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.
To learn more about protecting yourself from malicious actors, visit our package security documentation.