commit 11bbfddbe17e0bd43e128601a070a59492526da4 Author: funkpower Date: Fri Sep 18 09:09:32 2026 -0400 the Orthodox liturgical day, computed rather than fetched Feasts, seasons and the fast for twenty-three jurisdictions, derived from the paschalion and the fixed calendar so it needs no network and cannot go stale. The three things calendar code usually conflates are kept apart: the paschalion that gives Pascha, the menologion the fixed feasts are read on, and the fasting rule that is kept. Almost every Orthodox Church keeps the Julian Paschalion; what divides them is the menologion, so one file of commemorations indexed by ecclesiastical date serves old and new calendars alike. Six fasting levels rather than three, because the difference between xerophagy and wine-and-oil is the difference the books actually make. Level and obligation are carried separately: the Orthodox rule reports everything as customary, and Ruthenian particular law reports both how a day is kept and the far smaller minimum that binds. The menologion covers all 366 days and is dedicated to the public domain. Sets published by someone else are fetched, never redistributed. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06dff85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +*.py[cod] +.venv/ +build/ +dist/ +*.egg-info/ +.pytest_cache/ + +# Supplementary commemoration sets are fetched, never committed — they are +# other people's work. See typikon/menaia.py. +typikon/data/*.json +!typikon/data/menologion.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9efa6fb --- /dev/null +++ b/LICENSE @@ -0,0 +1,338 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Moe Ghoul, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..66bcca5 --- /dev/null +++ b/README.md @@ -0,0 +1,186 @@ +# typikon + +The Orthodox liturgical day — feasts, seasons and the fast — computed rather +than fetched. + +Everything derives from the paschalion and the fixed calendar, so it needs no +network, cannot go stale, and answers for any year you ask about. No API key, +no service that might be gone next year, no cache to invalidate. + +```console +$ typikon + Wednesday 18 March 2026 — 5 March in the church calendar + Russian Orthodox Church Outside Russia · the Typikon + + 🥬 The Holy Martyr Conon + + xerophagy — no oil or wine + a weekday of the Great Fast + Great Fast +``` + +## Install + +```console +$ pip install typikon +``` + +Or run it straight out of a checkout — it has no dependencies and needs only +Python 3.9. + +```console +$ git clone https://git.pumpin.online/funkpower/typikon +$ cd typikon && python -m typikon.cli +``` + +## Which calendar? + +Three things get conflated constantly, and keeping them apart is most of what +this library is for: + +| | | +|---|---| +| **Paschalion** | which table gives Pascha — Julian or Gregorian | +| **Menologion** | which calendar the fixed feasts are read on | +| **Rule** | which fasting discipline is kept | + +Almost every Orthodox Church keeps the **Julian Paschalion**. What divides them +is the menologion. The old-calendar churches read the fixed feasts on the +Julian calendar too; the new-calendar churches read them on the Revised Julian, +which agrees with the Gregorian for every date until 2800. So a new-calendar +church keeps the Nativity with the West and Pascha with the East — not a +compromise but exactly what the 1923 congress decided. + +Pick a reckoning without naming a church: + +```console +$ typikon --calendar old # Julian throughout +$ typikon --calendar new # Revised Julian fixed feasts, Julian Pascha +$ typikon --calendar western # Gregorian throughout +``` + +Or name one, which also picks up its fasting rule and any local saints: + +```console +$ typikon --jurisdiction rocor +$ typikon --jurisdiction antiochian --days 7 +$ typikon --list # every accepted value +``` + +The first run writes your choice to `~/.config/typikon/config.toml` and stops +asking. Change it with `--set-default`, override it for one run with the flag, +or set `TYPIKON_JURISDICTION`. + +Twenty-three jurisdictions ship: the old-calendar churches (Moscow, ROCOR, +Serbia, Georgia, Jerusalem, the Holy Mountain, Poland, Macedonia), the +new-calendar ones (Constantinople, Greece, GOARCH, Antioch, the OCA, Romania, +Bulgaria, Cyprus, Albania, Ukraine), the Finnish Orthodox Church — alone among +the Orthodox in keeping the Gregorian Paschalion — and, as an add-on, four +Byzantine Catholic churches. + +## The fast + +Six levels, ordered from strictest to laxest. They are the Typikon's own +distinctions, not a simplification of them: + +| | | +|---|---| +| `strict` | no food until evening | +| `xerophagy` | no oil, no wine; no meat, fish, eggs or dairy | +| `wine_oil` | wine and oil permitted | +| `fish` | fish, wine and oil permitted | +| `dairy` | dairy, eggs and fish permitted; no meat | +| `none` | no fast | + +A calendar that reports xerophagy and wine-and-oil both as "fasting" has thrown +away the information its reader wanted. + +**Level and obligation are separate questions.** The Orthodox Church legislates +fasting as tradition rather than as precept, so every Orthodox day here reports +as customary. The Eastern Catholic churches do have law about it, and it +requires very much less than the Typikon describes — so those jurisdictions +report both the level at which the day is kept and the minimum that actually +binds. A calendar saying "obligatory" where nothing obliges is worse than no +calendar; one reporting only the binding minimum makes the obligatory days look +laxer than the voluntary ones beside them. + +Where usages differ — Great Thursday, the Exaltation, Lazarus Saturday — this +follows the common published rule and names it in the day's reason so you can +disagree with it. Monastic practice, and the Holy Mountain in particular, is +stricter than all of it. A calendar cannot settle that; your bishop and your +spiritual father can. + +## Output + +```console +$ typikon --json --days 7 # for a bar, a widget, a script +$ typikon --ics --days 365 > orthodox.ics +$ typikon --year 2027 # a year's key dates, to check against yours +``` + +The JSON shape is flat and stable. Front ends are the thing most likely to be +written by someone else, and a schema that shifts underneath them is a bad +neighbour. + +## As a library + +```python +import typikon + +d = typikon.today("rocor") +d.church_date # '5 September' +d.fast # 'xerophagy' +d.title # 'The Holy Prophet Zachariah, father of the Forerunner' +d.is_obligatory # False + +typikon.julian_pascha(2027) # datetime.date(2027, 5, 2) +typikon.gregorian_pascha(2027) # datetime.date(2027, 3, 28) + +for day in typikon.week(date, 7, "antiochian"): + ... +``` + +## The menologion + +The fixed calendar ships as data and is indexed by **ecclesiastical** date, so +one file serves every jurisdiction. An old-calendar church and a new-calendar +church both keep the Nativity on 25 December; they disagree about which morning +that is, and that disagreement lives in the calendar conversion, not in the +data. + +`typikon/data/menologion.json` covers all 366 days with the principal +commemorations of the common Byzantine tradition, ranked so the fasting rules +know when a feast lifts a Wednesday. It was composed for this project and is +under the same licence as the code. It is a working set, not a complete +Synaxarion; additions are welcome. + +Some churches also keep their own saints, and those lists are usually published +by a body that holds the copyright in the wording. Those are **not** shipped. +Fetch one onto your own machine instead: + +```console +$ typikon --list-menaia +$ typikon --update-menaion ruthenian +``` + +It lands beside the packaged menologion and is overlaid automatically. + +## Accuracy + +`typikon` is pinned by 60 tests against published calendars — the paschalion +for six years by both reckonings, the Julian drift across the 2100 boundary, +the fast on every day that is not open to interpretation, and a full year in +every jurisdiction. + +It is a calendar, not an authority. If it disagrees with the one your parish +prints, your parish is right. Please open an issue. + +## Front ends + +[`horologion`](https://git.pumpin.online/funkpower/horologion) — desktop +widgets for Hyprland (Quickshell), waybar and eww. + +## Licence + +GPL-2.0-only — see [LICENSE](LICENSE). The menologion is under it too; +`typikon/data/LICENSE` says so and notes what the fetched sets are not. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..df0a393 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,43 @@ +[build-system] +requires = ["setuptools>=64"] +build-backend = "setuptools.build_meta" + +[project] +name = "typikon" +version = "0.1.0" +description = "The Orthodox liturgical day — feasts, seasons and the fast, computed rather than fetched" +readme = "README.md" +license = { text = "GPL-2.0-only" } +requires-python = ">=3.9" +authors = [{ name = "funkpower", email = "funkpower@pumpin.online" }] +keywords = ["orthodox", "liturgical", "calendar", "byzantine", "pascha", + "menologion", "typikon", "fasting"] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", + "Programming Language :: Python :: 3", + "Topic :: Religion", + "Topic :: Utilities", +] +dependencies = [] + +[project.optional-dependencies] +dev = ["pytest>=7"] + +[project.urls] +Homepage = "https://git.pumpin.online/funkpower/typikon" +Source = "https://git.pumpin.online/funkpower/typikon" + +[project.scripts] +typikon = "typikon.cli:main" + +[tool.setuptools.packages.find] +include = ["typikon*"] + +[tool.setuptools.package-data] +typikon = ["data/*.json"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/tests/test_typikon.py b/tests/test_typikon.py new file mode 100644 index 0000000..7c82a32 --- /dev/null +++ b/tests/test_typikon.py @@ -0,0 +1,230 @@ +"""Pinned against published calendars and against dates whose answer is not +open to interpretation.""" + +import datetime as dt + +import pytest + +from typikon import describe, gregorian_pascha, julian_pascha, resolve +from typikon.calendars import from_julian, julian_offset, to_julian +from typikon.day import week +from typikon.export import to_ics, to_json +from typikon.fasting import (DAIRY, FISH, NONE, OBLIGATORY, STRICT, WINE_OIL, + XEROPHAGY) + + +# ───────────────────────────── the paschalion ───────────────────────────── + +@pytest.mark.parametrize("year,expected", [ + (2024, "2024-05-05"), (2025, "2025-04-20"), (2026, "2026-04-12"), + (2027, "2027-05-02"), (2028, "2028-04-16"), (2030, "2030-04-28"), +]) +def test_julian_pascha(year, expected): + assert julian_pascha(year).isoformat() == expected + + +@pytest.mark.parametrize("year,expected", [ + (2024, "2024-03-31"), (2025, "2025-04-20"), (2026, "2026-04-05"), + (2027, "2027-03-28"), (2028, "2028-04-16"), +]) +def test_gregorian_pascha(year, expected): + assert gregorian_pascha(year).isoformat() == expected + + +def test_the_two_paschalions_agree_in_2025_and_differ_in_2026(): + assert julian_pascha(2025) == gregorian_pascha(2025) + assert (julian_pascha(2026) - gregorian_pascha(2026)).days == 7 + + +# ──────────────────────────── calendar drift ────────────────────────────── + +def test_julian_lags_thirteen_days_now_and_fourteen_after_2100(): + assert julian_offset(dt.date(2026, 9, 18)) == 13 + assert to_julian(dt.date(2026, 9, 18)) == (2026, 9, 5) + # Julian 2100 is a leap year and Gregorian 2100 is not, so the gap + # widens the day after the Julian 29 February — 15 March by the civil + # calendar. Nothing here hardcodes it. + assert julian_offset(dt.date(2100, 3, 14)) == 13 + assert julian_offset(dt.date(2100, 3, 15)) == 14 + + +def test_old_calendar_nativity_falls_on_7_january(): + assert from_julian(2026, 12, 25) == dt.date(2027, 1, 7) + + +def test_old_and_new_disagree_about_the_day_not_the_date(): + old = describe(dt.date(2027, 1, 7), "rocor") + new = describe(dt.date(2026, 12, 25), "goarch") + assert old.church_date == new.church_date == "25 December" + assert old.feast == new.feast + assert old.rank == new.rank == "great" + + +# ───────────────────────── the Orthodox fast ────────────────────────────── + +def test_great_fast_weekdays_are_xerophagy_and_weekends_wine_and_oil(): + # 2026: Orthodox Pascha 12 April, so Clean Monday is 23 February. + assert describe(dt.date(2026, 2, 25), "new").fast == XEROPHAGY # Wednesday + assert describe(dt.date(2026, 2, 28), "new").fast == WINE_OIL # Saturday + assert describe(dt.date(2026, 3, 1), "new").fast == WINE_OIL # Sunday + + +def test_the_two_strictest_days_of_the_year(): + assert describe(dt.date(2026, 2, 23), "new").fast == STRICT # Clean Monday + assert describe(dt.date(2026, 4, 10), "new").fast == STRICT # Great Friday + # The one Saturday kept without oil. + assert describe(dt.date(2026, 4, 11), "new").fast == XEROPHAGY + + +def test_annunciation_and_palm_sunday_admit_fish_inside_the_great_fast(): + ann = describe(dt.date(2026, 3, 25), "new") + assert ann.season == "Great Fast" and ann.fast == FISH + assert describe(dt.date(2026, 4, 5), "new").fast == FISH # Palm Sunday + + +def test_cheesefare_week_keeps_dairy_on_wednesday_and_friday(): + d = describe(dt.date(2026, 2, 18), "new") # Wednesday + assert d.season == "Cheesefare Week" and d.fast == DAIRY + + +def test_the_fast_free_stretches(): + assert describe(dt.date(2026, 4, 17), "new").fast == NONE # Bright Friday + assert describe(dt.date(2026, 1, 2), "new").fast == NONE # Christmastide + assert describe(dt.date(2026, 6, 3), "new").fast == NONE # week after Pentecost + # The week after the Publican and the Pharisee, kept fast-free precisely + # because the Pharisee boasted of fasting twice a week. + assert describe(dt.date(2026, 2, 6), "new").fast == NONE + + +def test_nativity_fast_tightens_for_its_last_five_days(): + # A Saturday before 20 December admits fish; one after it does not. + assert describe(dt.date(2026, 12, 12), "new").fast == FISH + assert describe(dt.date(2026, 12, 26), "new").season != "Nativity Fast" + late = describe(dt.date(2026, 12, 19), "new") + assert late.fast == FISH + assert describe(dt.date(2026, 12, 22), "new").fast == WINE_OIL # Tuesday + assert describe(dt.date(2026, 12, 24), "new").fast == XEROPHAGY # the eve + + +def test_a_great_feast_lifts_an_ordinary_friday_to_fish(): + d = describe(dt.date(2026, 8, 6), "goarch") # Transfiguration + assert d.weekday == "Thursday" and d.fast == FISH + # 2026-01-01, the Circumcision, is a Thursday; take a great feast on a + # Wednesday instead — the Meeting of our Lord, 2 February 2028. + m = describe(dt.date(2028, 2, 2), "goarch") + assert m.weekday == "Wednesday" and m.rank == "great" and m.fast == FISH + + +def test_ordinary_wednesday_and_friday_are_xerophagy(): + d = describe(dt.date(2026, 9, 4), "new") # a Friday + assert d.season is None and d.rank is None and d.fast == XEROPHAGY + + +def test_a_polyeleos_saint_lifts_an_ordinary_friday_to_wine_and_oil(): + # 9 October 2026 is a Friday and the Apostle James son of Alphaeus. The + # Typikon gives wine and oil for a saint of this rank, which is why the + # published calendars mark so many Fridays that way. + d = describe(dt.date(2026, 10, 9), "new") + assert d.weekday == "Friday" and d.rank == "polyeleos" + assert d.fast == WINE_OIL + + +def test_the_orthodox_rule_never_reports_an_obligation(): + days = week(dt.date(2026, 1, 1), 365, "new") + assert not any(d.obligation == OBLIGATORY for d in days) + + +# ──────────────────── Byzantine Catholic particular law ─────────────────── + +def test_ruthenian_pascha_is_the_gregorian_one(): + assert describe(dt.date(2026, 6, 1), "ruthenian").pascha == dt.date(2026, 4, 5) + + +def test_ruthenian_clean_monday_is_16_february_2026(): + # As published by the Metropolitan Cantor Institute. + assert describe(dt.date(2026, 2, 16), "ruthenian").reason.startswith( + "the first day") + + +def test_only_four_kinds_of_day_bind_under_ruthenian_law(): + obligatory = [d for d in week(dt.date(2026, 1, 1), 365, "ruthenian") + if d.obligation == OBLIGATORY] + # Clean Monday, Great Friday, and the Wednesdays and Fridays of the Fast. + assert dt.date(2026, 2, 16) in [d.date for d in obligatory] + assert dt.date(2026, 4, 3) in [d.date for d in obligatory] + assert all(d.season in ("Great Fast", "Great and Holy Week") + for d in obligatory) + + +def test_the_voluntary_seasons_never_report_as_binding(): + for when in (dt.date(2026, 11, 20), dt.date(2026, 8, 5)): + assert describe(when, "ruthenian").obligation == "customary" + + +def test_christmas_on_a_friday_does_not_ask_you_to_abstain(): + d = describe(dt.date(2026, 12, 25), "ruthenian") + assert d.weekday == "Friday" and d.fast == NONE + + +def test_ruthenian_strict_days_bind_strictly_and_the_rest_bind_to_meat(): + assert describe(dt.date(2026, 2, 16), "ruthenian").binds == WINE_OIL + assert describe(dt.date(2026, 4, 3), "ruthenian").binds == WINE_OIL + assert describe(dt.date(2026, 2, 18), "ruthenian").binds == DAIRY + assert describe(dt.date(2026, 10, 9), "ruthenian").binds == NONE + + +# ──────────────────────────── the menologion ────────────────────────────── + +def test_every_day_of_a_leap_year_has_a_commemoration(): + for d in week(dt.date(2024, 1, 1), 366, "new"): + assert d.commemorations, d.date + + +def test_the_twelve_great_feasts_are_all_present(): + fixed = [(9, 8), (9, 14), (11, 21), (12, 25), (1, 6), (2, 2), (3, 25), + (8, 6), (8, 15)] + for month, day in fixed: + d = describe(dt.date(2026, month, day), "goarch") + assert d.rank == "great", (month, day) + for when in (dt.date(2026, 4, 5), dt.date(2026, 5, 21), dt.date(2026, 5, 31)): + assert describe(when, "goarch").rank == "great", when + + +def test_a_movable_feast_outranks_the_fixed_calendar(): + d = describe(dt.date(2026, 4, 12), "new") + assert "Pascha" in d.feast + + +# ──────────────────────────────── output ────────────────────────────────── + +def test_json_round_trips(): + import json + days = week(dt.date(2026, 4, 10), 3, "new") + parsed = json.loads(to_json(days)) + assert len(parsed["days"]) == 3 + assert parsed["days"][0]["fast"] == STRICT + + +def test_ics_is_well_formed_and_folds_long_lines(): + text = to_ics(week(dt.date(2026, 4, 10), 7, "new")) + assert text.startswith("BEGIN:VCALENDAR") + assert text.rstrip().endswith("END:VCALENDAR") + assert text.count("BEGIN:VEVENT") == text.count("END:VEVENT") + for line in text.split("\r\n"): + assert len(line.encode()) < 200 + + +# ─────────────────────────── every jurisdiction ─────────────────────────── + +@pytest.mark.parametrize("key", list(__import__("typikon").JURISDICTIONS)) +def test_every_jurisdiction_reckons_a_whole_year_without_raising(key): + days = week(dt.date(2026, 1, 1), 365, key) + assert len(days) == 365 + assert all(d.fast for d in days) + + +def test_a_calendar_name_resolves_without_naming_a_church(): + assert resolve("old").menologion == "julian" + assert resolve("new").menologion == "revised_julian" + assert resolve("western").paschalion == "gregorian" + assert resolve(None).key == "new" diff --git a/typikon/__init__.py b/typikon/__init__.py new file mode 100644 index 0000000..09677de --- /dev/null +++ b/typikon/__init__.py @@ -0,0 +1,31 @@ +"""typikon — the Orthodox liturgical day, computed rather than fetched. + + >>> import typikon + >>> d = typikon.today("rocor") + >>> d.church_date, d.fast, d.title + +Everything is derived from the paschalion and the fixed calendar, so it needs +no network, cannot go stale, and answers for any year you ask about. +""" + +import datetime as dt + +from .day import Day, describe, week, year_of +from .fasting import (DAIRY, FISH, LEVELS, NONE, STRICT, WINE_OIL, XEROPHAGY, + Fast) +from .jurisdictions import CALENDARS, JURISDICTIONS, Jurisdiction, resolve +from .paschalion import gregorian_pascha, julian_pascha, movable_cycle, pascha + +__version__ = "0.1.0" + +__all__ = [ + "Day", "Fast", "Jurisdiction", + "describe", "today", "week", "year_of", "resolve", + "pascha", "julian_pascha", "gregorian_pascha", "movable_cycle", + "JURISDICTIONS", "CALENDARS", "LEVELS", + "STRICT", "XEROPHAGY", "WINE_OIL", "FISH", "DAIRY", "NONE", +] + + +def today(jurisdiction=None): + return describe(dt.date.today(), jurisdiction) diff --git a/typikon/calendars.py b/typikon/calendars.py new file mode 100644 index 0000000..9f0e020 --- /dev/null +++ b/typikon/calendars.py @@ -0,0 +1,122 @@ +"""Conversion between the civil calendar and the calendar a church reckons in. + +Every date in this library exists twice. There is the **civil** date — what a +phone says, always Gregorian — and the **ecclesiastical** date, the day the +Church calls it. For a church on the new calendar those are the same date. For +one on the old calendar they differ by the drift between the Julian and +Gregorian reckonings: thirteen days from 1900 to 2099, fourteen from 2100. + +Keeping both is what makes a single menologion serve every jurisdiction. The +Nativity is 25 December in the Julian and the Gregorian books alike; the two +traditions disagree about which morning that is, not about which date it is. So +the fixed calendar is stored once, indexed by ecclesiastical date, and a +jurisdiction decides how a civil date maps onto it. + +The drift is computed, never hardcoded, so nothing here breaks in 2100. +""" + +import datetime as dt + +__all__ = [ + "gregorian_to_jdn", "julian_to_jdn", "jdn_to_gregorian", "jdn_to_julian", + "to_julian", "from_julian", "julian_offset", +] + + +def gregorian_to_jdn(year, month, day): + """Julian Day Number for a proleptic Gregorian date.""" + a = (14 - month) // 12 + y = year + 4800 - a + m = month + 12 * a - 3 + return (day + (153 * m + 2) // 5 + 365 * y + + y // 4 - y // 100 + y // 400 - 32045) + + +def julian_to_jdn(year, month, day): + """Julian Day Number for a proleptic Julian date.""" + a = (14 - month) // 12 + y = year + 4800 - a + m = month + 12 * a - 3 + return day + (153 * m + 2) // 5 + 365 * y + y // 4 - 32083 + + +def jdn_to_gregorian(jdn): + a = jdn + 32044 + b, c = divmod(a, 146097) + d = (4 * c + 3) // 146097 + e = c - (146097 * d) // 4 + f = (4 * e + 3) // 1461 + g = e - (1461 * f) // 4 + h = (5 * g + 2) // 153 + day = g - (153 * h + 2) // 5 + 1 + month = h + 3 - 12 * (h // 10) + year = 400 * b + 100 * d + f - 4800 + h // 10 + return year, month, day + + +def jdn_to_julian(jdn): + c = jdn + 32082 + d = (4 * c + 3) // 1461 + e = c - (1461 * d) // 4 + m = (5 * e + 2) // 153 + day = e - (153 * m + 2) // 5 + 1 + month = m + 3 - 12 * (m // 10) + year = d - 4800 + m // 10 + return year, month, day + + +def to_julian(date): + """The Julian calendar date of a civil (Gregorian) date. + + Returns a plain (year, month, day) triple rather than a ``date``, because + 29 February in the Julian calendar is a real day that ``datetime`` will not + hold in a Gregorian common year. + """ + return jdn_to_julian(gregorian_to_jdn(date.year, date.month, date.day)) + + +def from_julian(year, month, day): + """The civil (Gregorian) date of a Julian calendar date.""" + y, m, d = jdn_to_gregorian(julian_to_jdn(year, month, day)) + return dt.date(y, m, d) + + +def julian_offset(date): + """How many days the Julian reckoning currently lags the Gregorian one. + + Thirteen for our lifetimes; fourteen from 14 March 2100. Reported rather + than assumed so a caller can show it. + """ + jy, jm, jd = to_julian(date) + return (gregorian_to_jdn(date.year, date.month, date.day) + - gregorian_to_jdn(jy, jm, jd)) + + +def fixed_of_civil(date, menologion): + """The ecclesiastical (year, month, day) a civil date is reckoned as. + + On the Revised Julian menologion this is the civil date itself — the two + agree for every date until 2800, so the distinction costs nothing and stops + mattering only long after anyone reading this. + """ + if menologion == "julian": + return to_julian(date) + return (date.year, date.month, date.day) + + +def civil_of_fixed(year, month, day, menologion): + """The civil date of an ecclesiastical date in a given ecclesiastical year.""" + if menologion == "julian": + return from_julian(year, month, day) + return dt.date(year, month, day) + + +def fixed_between(md, start, end): + """Is (month, day) within an inclusive fixed-date range, wrapping the year? + + The Nativity Fast does not wrap; the fast-free stretch from the Nativity to + the eve of Theophany does. Both are written the same way here. + """ + if start <= end: + return start <= md <= end + return md >= start or md <= end diff --git a/typikon/cli.py b/typikon/cli.py new file mode 100644 index 0000000..2bb2393 --- /dev/null +++ b/typikon/cli.py @@ -0,0 +1,210 @@ +"""The command line.""" + +import argparse +import datetime as dt +import sys + +from . import config +from .day import describe, week, year_of +from .export import to_ics, to_json +from .fasting import OBLIGATORY +from .jurisdictions import CALENDARS, JURISDICTIONS, resolve +from .rules import get as get_rule + +__version__ = "0.1.0" + + +def _list_jurisdictions(): + print("\n Calendars — a reckoning without naming a church:\n") + for j in CALENDARS.values(): + print(f" {j.key:20} {j.name}") + for line in _wrap(j.note, 56): + print(f" {'':20} {line}") + print("\n Jurisdictions:\n") + width = max(len(k) for k in JURISDICTIONS) + for j in JURISDICTIONS.values(): + cal = {"old": "old calendar", "new": "new calendar", + "western": "gregorian"}[j.calendar] + print(f" {j.key:{width}} {j.name}") + print(f" {'':{width}} {cal}; {j.rule} fasting rule") + if j.note: + for line in _wrap(j.note, 66): + print(f" {'':{width}} {line}") + print() + + +def _wrap(text, width): + words, line, out = text.split(), "", [] + for w in words: + if len(line) + len(w) + 1 > width: + out.append(line) + line = w + else: + line = f"{line} {w}".strip() + if line: + out.append(line) + return out + + +def _print_days(days, jur, rule, source, verbose): + if len(days) == 1: + d = days[0] + head = f" {d.weekday} {d.date.strftime('%-d %B %Y')}" + if d.church_date != d.date.strftime("%-d %B"): + head += f" — {d.church_date} in the church calendar" + print(f"\n{head}") + print(f" {jur.name} · {rule['title']}" + + (f" (from {source})" if source != "flag" else "")) + print() + if d.feast: + print(f" {d.emoji} {d.feast}") + else: + print(f" {d.emoji} {d.title}") + if d.cycle: + print(f" {d.cycle}") + print() + mark = " — obligatory" if d.obligation == OBLIGATORY else "" + print(f" {d.fast_label}{mark}") + if d.reason: + print(f" {d.reason}") + if d.season: + print(f" {d.season}") + rest = [c for c in d.commemorations if c != d.title] + if rest: + print() + for c in rest: + print(f" · {c}") + if verbose: + print() + print(f" Pascha {d.pascha} · {d.days_to_pascha} days") + print() + return + + print(f"\n {jur.name} · {rule['title']}\n") + for d in days: + mark = "*" if d.obligation == OBLIGATORY else " " + when = d.date.strftime("%a %-d %b") + print(f" {d.emoji} {when:>10}{mark} {d.fast_label:<32}{d.title[:44]}") + if any(d.obligation == OBLIGATORY for d in days): + print("\n * obligatory. Everything unmarked is customary and does not bind.") + print() + + +def main(argv=None): + p = argparse.ArgumentParser( + prog="typikon", + description="The Orthodox liturgical day — feasts, seasons and the fast.") + p.add_argument("-j", "--jurisdiction", "-c", "--calendar", dest="jurisdiction", + help="a church or a reckoning; see --list") + p.add_argument("-d", "--date", help="YYYY-MM-DD (default: today)") + p.add_argument("-n", "--days", type=int, default=1, + help="how many days from the start date, inclusive") + p.add_argument("--json", action="store_true", help="machine-readable output") + p.add_argument("--ics", action="store_true", help="an iCalendar feed") + p.add_argument("--indent", type=int, default=None, help="indent JSON output") + p.add_argument("--year", type=int, help="print a year's key dates and exit") + p.add_argument("--list", action="store_true", + help="list every jurisdiction and calendar") + p.add_argument("--set-default", metavar="JURISDICTION", + help="remember a jurisdiction and exit") + p.add_argument("--update-menaion", metavar="NAME", + help="fetch a supplementary commemoration set (see --list-menaia)") + p.add_argument("--list-menaia", action="store_true", + help="list the supplementary sets that can be fetched") + p.add_argument("-v", "--verbose", action="store_true") + p.add_argument("--version", action="version", version=f"typikon {__version__}") + args = p.parse_args(argv) + + if args.list: + _list_jurisdictions() + return 0 + + if args.list_menaia: + from .menaia import SOURCES, target_dir + print("\n Supplementary commemorations, fetched on request:\n") + for key, src in SOURCES.items(): + print(f" {key:14} {src['title']}") + print(f" {'':14} {src['url']}") + for line in _wrap(src["note"], 62): + print(f" {'':14} {line}") + print() + print(f" They are written to {target_dir()}\n") + return 0 + + if args.update_menaion: + from .menaia import update + try: + result = update(args.update_menaion) + except KeyError as exc: + print(f"typikon: {exc.args[0]}", file=sys.stderr) + return 2 + except Exception as exc: + print(f"typikon: could not fetch: {exc}", file=sys.stderr) + return 1 + print(f" {result['days']} days written to {result['path']}") + print(f" Source: {result['source']['title']}") + if result["corrections"]: + print(f" source typos corrected: {', '.join(result['corrections'])}") + if result["missing"]: + print(f" MISSING: {', '.join(result['missing'])}") + return 1 + return 0 + + if args.set_default: + try: + jur = resolve(args.set_default) + except KeyError: + print(f"typikon: unknown jurisdiction {args.set_default!r};" + " try --list", file=sys.stderr) + return 2 + where = config.write(jur.key) + print(f" {jur.name} — written to {where}") + return 0 + + if args.jurisdiction: + name, source = args.jurisdiction, "flag" + else: + name, source = config.default_jurisdiction() + + try: + jur = resolve(name) + except KeyError: + print(f"typikon: unknown jurisdiction {name!r}; try --list", file=sys.stderr) + return 2 + + if args.year: + mv = year_of(args.year, jur) + print(f"\n {args.year} · {jur.name}\n") + for k in ("publican_pharisee", "meatfare", "cheesefare", "clean_monday", + "great_fast_end", "lazarus_saturday", "palm_sunday", + "great_friday", "pascha", "ascension", "pentecost", + "all_saints", "apostles_start", "apostles_end", + "dormition", "nativity", "theophany"): + d = mv[k] + print(f" {k.replace('_', ' '):20} {d.isoformat()} {d.strftime('%a')}") + print() + return 0 + + start = dt.date.fromisoformat(args.date) if args.date else dt.date.today() + days = week(start, args.days, jur) + + if args.json: + print(to_json(days, args.indent)) + return 0 + if args.ics: + sys.stdout.write(to_ics(days, name=f"{jur.name} — Typikon")) + return 0 + + _print_days(days, jur, get_rule(jur.rule), source, args.verbose) + + # The first run should say what it assumed, and then stop saying it. + if source == "default": + where = config.write(jur.key) + print(f" Reckoning by the {jur.name.lower()}, written to {where}.") + print(" Change it with `typikon --set-default `," + " or see `typikon --list`.\n") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/typikon/config.py b/typikon/config.py new file mode 100644 index 0000000..8d1718e --- /dev/null +++ b/typikon/config.py @@ -0,0 +1,54 @@ +"""Where the default jurisdiction is remembered. + +Choosing a reckoning is a once-in-a-lifetime decision for most people, so it +should not be a flag typed every morning. The first run writes the choice down; +every run after reads it. An explicit flag always wins over the file. +""" + +import os + +try: + import tomllib +except ModuleNotFoundError: # Python < 3.11 + tomllib = None + +__all__ = ["path", "read", "write", "default_jurisdiction"] + +ENV_VAR = "TYPIKON_JURISDICTION" + + +def path(): + base = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config") + return os.path.join(base, "typikon", "config.toml") + + +def read(): + p = path() + if not os.path.exists(p) or tomllib is None: + return {} + try: + with open(p, "rb") as fh: + return tomllib.load(fh) + except (OSError, ValueError): + return {} + + +def write(jurisdiction): + p = path() + os.makedirs(os.path.dirname(p), exist_ok=True) + with open(p, "w", encoding="utf-8") as fh: + fh.write("# Which reckoning typikon uses when no --jurisdiction is given.\n" + "# Run `typikon --list` to see every accepted value.\n" + f'jurisdiction = "{jurisdiction}"\n') + return p + + +def default_jurisdiction(): + """The environment beats the config file; both beat the built-in default.""" + env = os.environ.get(ENV_VAR) + if env: + return env, "environment" + conf = read().get("jurisdiction") + if conf: + return conf, "config" + return "new", "default" diff --git a/typikon/data/LICENSE b/typikon/data/LICENSE new file mode 100644 index 0000000..9b54a5e --- /dev/null +++ b/typikon/data/LICENSE @@ -0,0 +1,12 @@ +The menologion in this directory — menologion.json — is covered by the GNU +General Public License version 2, the same as the rest of this project. See +the LICENSE file at the root. + +It was composed for this project from the common Byzantine tradition, in the +standard English formulae. The tradition is nobody's property; the wording +here is this project's contribution to it, and the licence asks only that +what you build on it stays as free as what you were given. + +Supplementary sets fetched by `typikon --update-menaion` are NOT covered by +this and are NOT distributed here. They belong to whoever published them; the +fetcher puts a copy on your machine for your own use. See typikon/menaia.py. diff --git a/typikon/data/menologion.json b/typikon/data/menologion.json new file mode 100644 index 0000000..cd04a46 --- /dev/null +++ b/typikon/data/menologion.json @@ -0,0 +1,2063 @@ +{ + "01-01": { + "commemorations": [ + "The Circumcision according to the Flesh of our Lord, God and Saviour Jesus Christ", + "Our Holy Father Basil the Great, Archbishop of Caesarea in Cappadocia" + ], + "rank": "great" + }, + "01-02": { + "commemorations": [ + "Our Holy Father Sylvester, Pope of Rome", + "Our Venerable Father Seraphim of Sarov" + ], + "cycle": "forefeast of the Theophany" + }, + "01-03": { + "commemorations": [ + "The Holy Prophet Malachi", + "The Holy Martyr Gordius" + ], + "cycle": "forefeast of the Theophany" + }, + "01-04": { + "commemorations": [ + "The Synaxis of the Holy Seventy Apostles" + ], + "cycle": "forefeast of the Theophany" + }, + "01-05": { + "commemorations": [ + "The Holy Martyrs Theopemptus and Theonas", + "Our Venerable Mother Syncletica" + ], + "cycle": "the eve of the Theophany" + }, + "01-06": { + "commemorations": [ + "The Holy Theophany of our Lord, God and Saviour Jesus Christ" + ], + "rank": "great" + }, + "01-07": { + "commemorations": [ + "The Synaxis of the Honourable and Glorious Prophet, Forerunner and Baptist John" + ], + "cycle": "afterfeast of the Theophany", + "rank": "vigil" + }, + "01-08": { + "commemorations": [ + "Our Venerable Father George the Chozebite", + "Our Venerable Mother Domnica" + ], + "cycle": "afterfeast of the Theophany" + }, + "01-09": { + "commemorations": [ + "The Holy Martyr Polyeuctus" + ], + "cycle": "afterfeast of the Theophany" + }, + "01-10": { + "commemorations": [ + "Our Holy Father Gregory, Bishop of Nyssa", + "Our Venerable Father Dometian, Bishop of Melitene" + ], + "cycle": "afterfeast of the Theophany" + }, + "01-11": { + "commemorations": [ + "Our Venerable Father Theodosius the Cenobiarch" + ], + "cycle": "afterfeast of the Theophany", + "rank": "polyeleos" + }, + "01-12": { + "commemorations": [ + "The Holy Martyr Tatiana" + ], + "cycle": "afterfeast of the Theophany" + }, + "01-13": { + "commemorations": [ + "The Holy Martyrs Hermylus and Stratonicus" + ], + "cycle": "afterfeast of the Theophany" + }, + "01-14": { + "commemorations": [ + "Our Venerable Fathers slain at Sinai and Raithu" + ], + "cycle": "leave-taking of the Theophany" + }, + "01-15": { + "commemorations": [ + "Our Venerable Fathers Paul of Thebes and John the Hut-dweller" + ] + }, + "01-16": { + "commemorations": [ + "The Veneration of the Precious Chains of the Holy Apostle Peter" + ] + }, + "01-17": { + "commemorations": [ + "Our Venerable and God-bearing Father Anthony the Great" + ], + "rank": "vigil" + }, + "01-18": { + "commemorations": [ + "Our Holy Fathers Athanasius and Cyril, Archbishops of Alexandria" + ], + "rank": "polyeleos" + }, + "01-19": { + "commemorations": [ + "Our Venerable Father Macarius of Egypt" + ] + }, + "01-20": { + "commemorations": [ + "Our Venerable and God-bearing Father Euthymius the Great" + ], + "rank": "vigil" + }, + "01-21": { + "commemorations": [ + "Our Venerable Father Maximus the Confessor", + "The Holy Martyr Neophytus" + ] + }, + "01-22": { + "commemorations": [ + "The Holy Apostle Timothy", + "Our Venerable Martyr Anastasius the Persian" + ] + }, + "01-23": { + "commemorations": [ + "The Holy Hieromartyr Clement, Bishop of Ancyra" + ] + }, + "01-24": { + "commemorations": [ + "Our Venerable Mother Xenia of Rome", + "Blessed Xenia of St Petersburg" + ] + }, + "01-25": { + "commemorations": [ + "Our Holy Father Gregory the Theologian, Archbishop of Constantinople" + ], + "rank": "vigil" + }, + "01-26": { + "commemorations": [ + "Our Venerable Father Xenophon and his companions" + ] + }, + "01-27": { + "commemorations": [ + "The Translation of the Relics of our Holy Father John Chrysostom" + ], + "rank": "polyeleos" + }, + "01-28": { + "commemorations": [ + "Our Venerable Father Ephraim the Syrian" + ] + }, + "01-29": { + "commemorations": [ + "The Translation of the Relics of the Holy Hieromartyr Ignatius the God-bearer" + ] + }, + "01-30": { + "commemorations": [ + "The Three Holy Hierarchs Basil the Great, Gregory the Theologian and John Chrysostom" + ], + "rank": "vigil" + }, + "01-31": { + "commemorations": [ + "The Holy Unmercenaries and Wonderworkers Cyrus and John" + ] + }, + "02-01": { + "commemorations": [ + "The Holy Martyr Tryphon" + ], + "cycle": "forefeast of the Meeting" + }, + "02-02": { + "commemorations": [ + "The Meeting of our Lord, God and Saviour Jesus Christ in the Temple" + ], + "rank": "great" + }, + "02-03": { + "commemorations": [ + "The Holy and Righteous Symeon the God-receiver and Anna the Prophetess" + ], + "cycle": "afterfeast of the Meeting", + "rank": "polyeleos" + }, + "02-04": { + "commemorations": [ + "Our Venerable Father Isidore of Pelusium" + ], + "cycle": "afterfeast of the Meeting" + }, + "02-05": { + "commemorations": [ + "The Holy Martyr Agatha" + ], + "cycle": "afterfeast of the Meeting" + }, + "02-06": { + "commemorations": [ + "Our Venerable Father Bucolus, Bishop of Smyrna", + "Our Holy Father Photius, Patriarch of Constantinople" + ], + "cycle": "afterfeast of the Meeting" + }, + "02-07": { + "commemorations": [ + "Our Venerable Father Parthenius, Bishop of Lampsacus", + "Our Venerable Father Luke of Hellas" + ], + "cycle": "afterfeast of the Meeting" + }, + "02-08": { + "commemorations": [ + "The Holy Great Martyr Theodore the Commander", + "The Holy Prophet Zachariah" + ], + "cycle": "afterfeast of the Meeting", + "rank": "polyeleos" + }, + "02-09": { + "commemorations": [ + "The Holy Martyr Nicephorus" + ], + "cycle": "leave-taking of the Meeting" + }, + "02-10": { + "commemorations": [ + "The Holy Hieromartyr Charalampus" + ], + "rank": "polyeleos" + }, + "02-11": { + "commemorations": [ + "The Holy Hieromartyr Blaise, Bishop of Sebaste" + ] + }, + "02-12": { + "commemorations": [ + "Our Holy Father Meletius, Archbishop of Antioch" + ] + }, + "02-13": { + "commemorations": [ + "Our Venerable Father Martinian" + ] + }, + "02-14": { + "commemorations": [ + "Our Venerable Father Auxentius", + "Our Holy Father Cyril, Equal-to-the-Apostles, Teacher of the Slavs" + ] + }, + "02-15": { + "commemorations": [ + "The Holy Apostle Onesimus" + ] + }, + "02-16": { + "commemorations": [ + "The Holy Martyrs Pamphilus and Porphyrius" + ] + }, + "02-17": { + "commemorations": [ + "The Holy Great Martyr Theodore the Recruit" + ], + "rank": "polyeleos" + }, + "02-18": { + "commemorations": [ + "Our Holy Father Leo the Great, Pope of Rome" + ] + }, + "02-19": { + "commemorations": [ + "The Holy Apostles Archippus and Philemon" + ] + }, + "02-20": { + "commemorations": [ + "Our Venerable Father Leo, Bishop of Catania" + ] + }, + "02-21": { + "commemorations": [ + "Our Venerable Father Timothy of Symbola", + "Our Holy Father Eustathius, Archbishop of Antioch" + ] + }, + "02-22": { + "commemorations": [ + "The Finding of the Relics of the Holy Martyrs at the Gate of Eugenius" + ] + }, + "02-23": { + "commemorations": [ + "The Holy Hieromartyr Polycarp, Bishop of Smyrna" + ] + }, + "02-24": { + "commemorations": [ + "The First and Second Findings of the Precious Head of the Forerunner" + ], + "rank": "polyeleos" + }, + "02-25": { + "commemorations": [ + "Our Holy Father Tarasius, Archbishop of Constantinople" + ] + }, + "02-26": { + "commemorations": [ + "Our Holy Father Porphyrius, Bishop of Gaza" + ] + }, + "02-27": { + "commemorations": [ + "Our Venerable Father Procopius the Decapolite" + ] + }, + "02-28": { + "commemorations": [ + "Our Venerable Father Basil the Confessor" + ] + }, + "02-29": { + "commemorations": [ + "Our Venerable Father Cassian the Roman" + ] + }, + "03-01": { + "commemorations": [ + "The Holy Venerable Martyr Eudocia" + ] + }, + "03-02": { + "commemorations": [ + "The Holy Hieromartyr Theodotus, Bishop of Cyrenia" + ] + }, + "03-03": { + "commemorations": [ + "The Holy Martyrs Eutropius, Cleonicus and Basiliscus" + ] + }, + "03-04": { + "commemorations": [ + "Our Venerable Father Gerasimus of the Jordan" + ] + }, + "03-05": { + "commemorations": [ + "The Holy Martyr Conon" + ] + }, + "03-06": { + "commemorations": [ + "The Forty-two Holy Martyrs of Amorion" + ] + }, + "03-07": { + "commemorations": [ + "The Holy Hieromartyrs of Cherson" + ] + }, + "03-08": { + "commemorations": [ + "Our Venerable Father Theophylact, Bishop of Nicomedia" + ] + }, + "03-09": { + "commemorations": [ + "The Holy Forty Martyrs of Sebaste" + ], + "rank": "vigil" + }, + "03-10": { + "commemorations": [ + "The Holy Martyr Quadratus of Corinth" + ] + }, + "03-11": { + "commemorations": [ + "Our Holy Father Sophronius, Patriarch of Jerusalem" + ] + }, + "03-12": { + "commemorations": [ + "Our Venerable Father Theophanes the Confessor", + "Our Holy Father Gregory the Dialogist, Pope of Rome" + ] + }, + "03-13": { + "commemorations": [ + "The Translation of the Relics of our Holy Father Nicephorus of Constantinople" + ] + }, + "03-14": { + "commemorations": [ + "Our Venerable Father Benedict of Nursia" + ] + }, + "03-15": { + "commemorations": [ + "The Holy Martyr Agapius and his companions" + ] + }, + "03-16": { + "commemorations": [ + "The Holy Martyrs Sabinus and Papas" + ] + }, + "03-17": { + "commemorations": [ + "Our Venerable Father Alexis the Man of God" + ] + }, + "03-18": { + "commemorations": [ + "Our Holy Father Cyril, Archbishop of Jerusalem" + ] + }, + "03-19": { + "commemorations": [ + "The Holy Martyrs Chrysanthus and Daria" + ] + }, + "03-20": { + "commemorations": [ + "Our Venerable Fathers slain at the Monastery of St Sabbas" + ] + }, + "03-21": { + "commemorations": [ + "Our Holy Father James the Confessor" + ] + }, + "03-22": { + "commemorations": [ + "The Holy Hieromartyr Basil of Ancyra" + ] + }, + "03-23": { + "commemorations": [ + "The Holy Venerable Martyr Nicon and his companions" + ] + }, + "03-24": { + "commemorations": [ + "Our Venerable Father Zachariah" + ], + "cycle": "forefeast of the Annunciation" + }, + "03-25": { + "commemorations": [ + "The Annunciation to our Most Holy Lady the Theotokos and Ever-Virgin Mary" + ], + "rank": "great" + }, + "03-26": { + "commemorations": [ + "The Synaxis of the Archangel Gabriel" + ], + "cycle": "leave-taking of the Annunciation", + "rank": "polyeleos" + }, + "03-27": { + "commemorations": [ + "The Holy Martyr Matrona of Thessalonica" + ] + }, + "03-28": { + "commemorations": [ + "Our Venerable Father Hilarion the New" + ] + }, + "03-29": { + "commemorations": [ + "Our Venerable Father Mark, Bishop of Arethusa" + ] + }, + "03-30": { + "commemorations": [ + "Our Venerable Father John Climacus, author of The Ladder" + ], + "rank": "polyeleos" + }, + "03-31": { + "commemorations": [ + "Our Holy Father Hypatius, Bishop of Gangra" + ] + }, + "04-01": { + "commemorations": [ + "Our Venerable Mother Mary of Egypt" + ], + "rank": "polyeleos" + }, + "04-02": { + "commemorations": [ + "Our Venerable Father Titus the Wonderworker" + ] + }, + "04-03": { + "commemorations": [ + "Our Venerable Father Nicetas the Confessor" + ] + }, + "04-04": { + "commemorations": [ + "Our Venerable Father Joseph the Hymnographer", + "Our Venerable Father George of Maleon" + ] + }, + "04-05": { + "commemorations": [ + "The Holy Martyrs Claudius, Diodorus and Victor" + ] + }, + "04-06": { + "commemorations": [ + "Our Holy Father Eutychius, Patriarch of Constantinople", + "Our Holy Father Methodius, Equal-to-the-Apostles, Teacher of the Slavs" + ] + }, + "04-07": { + "commemorations": [ + "Our Holy Father George, Bishop of Mytilene" + ] + }, + "04-08": { + "commemorations": [ + "The Holy Apostles Herodion, Agabus, Rufus and their companions" + ] + }, + "04-09": { + "commemorations": [ + "The Holy Martyr Eupsychius" + ] + }, + "04-10": { + "commemorations": [ + "The Holy Martyrs Terence, Pompeius and their companions" + ] + }, + "04-11": { + "commemorations": [ + "The Holy Hieromartyr Antipas, Bishop of Pergamum" + ] + }, + "04-12": { + "commemorations": [ + "Our Venerable Father Basil the Confessor, Bishop of Parium" + ] + }, + "04-13": { + "commemorations": [ + "The Holy Hieromartyr Artemon" + ] + }, + "04-14": { + "commemorations": [ + "Our Holy Father Martin the Confessor, Pope of Rome" + ] + }, + "04-15": { + "commemorations": [ + "The Holy Apostles Aristarchus, Pudens and Trophimus" + ] + }, + "04-16": { + "commemorations": [ + "The Holy Martyrs Agape, Irene and Chionia" + ] + }, + "04-17": { + "commemorations": [ + "Our Venerable Father Symeon of Persia", + "Our Venerable Father Acacius, Bishop of Melitene" + ] + }, + "04-18": { + "commemorations": [ + "Our Venerable Father John, disciple of St Gregory the Decapolite" + ] + }, + "04-19": { + "commemorations": [ + "Our Venerable Father John of the Old Cave" + ] + }, + "04-20": { + "commemorations": [ + "Our Venerable Father Theodore Trichinas" + ] + }, + "04-21": { + "commemorations": [ + "The Holy Hieromartyr Januarius and his companions" + ] + }, + "04-22": { + "commemorations": [ + "Our Venerable Father Theodore the Syceote" + ] + }, + "04-23": { + "commemorations": [ + "The Holy and Glorious Great Martyr George the Trophy-bearer" + ], + "rank": "vigil" + }, + "04-24": { + "commemorations": [ + "The Holy Martyr Sabbas the General" + ] + }, + "04-25": { + "commemorations": [ + "The Holy Apostle and Evangelist Mark" + ], + "rank": "vigil" + }, + "04-26": { + "commemorations": [ + "The Holy Hieromartyr Basil, Bishop of Amasea" + ] + }, + "04-27": { + "commemorations": [ + "The Holy Hieromartyr Symeon, kinsman of the Lord" + ] + }, + "04-28": { + "commemorations": [ + "The Holy Apostles Jason and Sosipater" + ] + }, + "04-29": { + "commemorations": [ + "The Nine Holy Martyrs of Cyzicus" + ] + }, + "04-30": { + "commemorations": [ + "The Holy Apostle James, brother of St John the Theologian" + ], + "rank": "polyeleos" + }, + "05-01": { + "commemorations": [ + "The Holy Prophet Jeremiah" + ] + }, + "05-02": { + "commemorations": [ + "The Translation of the Relics of our Holy Father Athanasius the Great" + ] + }, + "05-03": { + "commemorations": [ + "The Holy Martyrs Timothy and Maura" + ] + }, + "05-04": { + "commemorations": [ + "The Holy Martyr Pelagia of Tarsus" + ] + }, + "05-05": { + "commemorations": [ + "The Holy Great Martyr Irene" + ], + "rank": "polyeleos" + }, + "05-06": { + "commemorations": [ + "The Holy and Righteous Job the Long-suffering" + ] + }, + "05-07": { + "commemorations": [ + "The Commemoration of the Appearance of the Sign of the Cross over Jerusalem" + ] + }, + "05-08": { + "commemorations": [ + "The Holy Apostle and Evangelist John the Theologian" + ], + "rank": "vigil" + }, + "05-09": { + "commemorations": [ + "The Holy Prophet Isaiah", + "The Translation of the Relics of our Holy Father Nicholas of Myra" + ], + "rank": "polyeleos" + }, + "05-10": { + "commemorations": [ + "The Holy Apostle Simon the Zealot" + ] + }, + "05-11": { + "commemorations": [ + "Our Holy Fathers Cyril and Methodius, Equals-to-the-Apostles, Teachers of the Slavs" + ], + "rank": "polyeleos" + }, + "05-12": { + "commemorations": [ + "Our Holy Father Epiphanius, Bishop of Cyprus", + "Our Holy Father Germanus, Patriarch of Constantinople" + ] + }, + "05-13": { + "commemorations": [ + "The Holy Martyr Glyceria" + ] + }, + "05-14": { + "commemorations": [ + "The Holy Martyr Isidore of Chios" + ] + }, + "05-15": { + "commemorations": [ + "Our Venerable Father Pachomius the Great" + ] + }, + "05-16": { + "commemorations": [ + "Our Venerable Father Theodore the Sanctified" + ] + }, + "05-17": { + "commemorations": [ + "The Holy Apostle Andronicus and his fellow labourer Junia" + ] + }, + "05-18": { + "commemorations": [ + "The Holy Martyrs Peter, Dionysius and their companions" + ] + }, + "05-19": { + "commemorations": [ + "The Holy Hieromartyr Patrick, Bishop of Prusa" + ] + }, + "05-20": { + "commemorations": [ + "The Holy Martyr Thalelaeus" + ] + }, + "05-21": { + "commemorations": [ + "The Holy Emperor Constantine and his mother Helen, Equals-to-the-Apostles" + ], + "rank": "vigil" + }, + "05-22": { + "commemorations": [ + "The Holy Martyr Basiliscus" + ] + }, + "05-23": { + "commemorations": [ + "Our Venerable Father Michael the Confessor, Bishop of Synada" + ] + }, + "05-24": { + "commemorations": [ + "Our Venerable Father Symeon the Stylite of the Wonderful Mountain" + ] + }, + "05-25": { + "commemorations": [ + "The Third Finding of the Precious Head of the Forerunner" + ], + "rank": "polyeleos" + }, + "05-26": { + "commemorations": [ + "The Holy Apostle Carpus" + ] + }, + "05-27": { + "commemorations": [ + "The Holy Hieromartyr Therapon" + ] + }, + "05-28": { + "commemorations": [ + "Our Venerable Father Nicetas, Bishop of Chalcedon" + ] + }, + "05-29": { + "commemorations": [ + "The Holy Venerable Martyr Theodosia" + ] + }, + "05-30": { + "commemorations": [ + "Our Venerable Father Isaac of Dalmatia" + ] + }, + "05-31": { + "commemorations": [ + "The Holy Apostle Hermas", + "The Holy Martyr Hermias" + ] + }, + "06-01": { + "commemorations": [ + "The Holy Martyr Justin the Philosopher" + ] + }, + "06-02": { + "commemorations": [ + "Our Holy Father Nicephorus, Patriarch of Constantinople" + ] + }, + "06-03": { + "commemorations": [ + "The Holy Martyr Lucillian and his companions" + ] + }, + "06-04": { + "commemorations": [ + "Our Holy Father Metrophanes, Patriarch of Constantinople" + ] + }, + "06-05": { + "commemorations": [ + "The Holy Hieromartyr Dorotheus, Bishop of Tyre" + ] + }, + "06-06": { + "commemorations": [ + "Our Venerable Father Bessarion the Wonderworker", + "Our Venerable Father Hilarion the New" + ] + }, + "06-07": { + "commemorations": [ + "The Holy Hieromartyr Theodotus of Ancyra" + ] + }, + "06-08": { + "commemorations": [ + "The Translation of the Relics of the Holy Great Martyr Theodore the Commander" + ] + }, + "06-09": { + "commemorations": [ + "Our Holy Father Cyril, Archbishop of Alexandria" + ] + }, + "06-10": { + "commemorations": [ + "The Holy Hieromartyr Timothy, Bishop of Prusa" + ] + }, + "06-11": { + "commemorations": [ + "The Holy Apostles Bartholomew and Barnabas" + ], + "rank": "polyeleos" + }, + "06-12": { + "commemorations": [ + "Our Venerable Father Onuphrius the Great", + "Our Venerable Father Peter of Athos" + ] + }, + "06-13": { + "commemorations": [ + "The Holy Martyr Aquilina" + ] + }, + "06-14": { + "commemorations": [ + "The Holy Prophet Elisha", + "Our Holy Father Methodius, Patriarch of Constantinople" + ] + }, + "06-15": { + "commemorations": [ + "The Holy Prophet Amos" + ] + }, + "06-16": { + "commemorations": [ + "Our Holy Father Tychon the Wonderworker, Bishop of Amathus" + ] + }, + "06-17": { + "commemorations": [ + "The Holy Martyrs Manuel, Sabel and Ishmael" + ] + }, + "06-18": { + "commemorations": [ + "The Holy Martyr Leontius" + ] + }, + "06-19": { + "commemorations": [ + "The Holy Apostle Jude, the Brother of the Lord" + ], + "rank": "polyeleos" + }, + "06-20": { + "commemorations": [ + "The Holy Hieromartyr Methodius, Bishop of Patara" + ] + }, + "06-21": { + "commemorations": [ + "The Holy Martyr Julian of Tarsus" + ] + }, + "06-22": { + "commemorations": [ + "The Holy Hieromartyr Eusebius, Bishop of Samosata" + ] + }, + "06-23": { + "commemorations": [ + "The Holy Martyr Agrippina" + ] + }, + "06-24": { + "commemorations": [ + "The Nativity of the Honourable and Glorious Prophet, Forerunner and Baptist John" + ], + "rank": "vigil" + }, + "06-25": { + "commemorations": [ + "The Holy Venerable Martyr Febronia" + ] + }, + "06-26": { + "commemorations": [ + "Our Venerable Father David of Thessalonica" + ] + }, + "06-27": { + "commemorations": [ + "Our Venerable Father Sampson the Hospitable" + ] + }, + "06-28": { + "commemorations": [ + "The Translation of the Relics of the Holy Unmercenaries Cyrus and John" + ], + "cycle": "the eve of Ss Peter and Paul" + }, + "06-29": { + "commemorations": [ + "The Holy, Glorious and All-praised Apostles Peter and Paul" + ], + "rank": "great" + }, + "06-30": { + "commemorations": [ + "The Synaxis of the Holy, Glorious and All-praised Twelve Apostles" + ], + "rank": "vigil" + }, + "07-01": { + "commemorations": [ + "The Holy Unmercenaries and Wonderworkers Cosmas and Damian of Rome" + ] + }, + "07-02": { + "commemorations": [ + "The Deposition of the Precious Robe of the Theotokos at Blachernae" + ], + "rank": "polyeleos" + }, + "07-03": { + "commemorations": [ + "The Holy Martyr Hyacinth" + ] + }, + "07-04": { + "commemorations": [ + "Our Holy Father Andrew, Archbishop of Crete" + ] + }, + "07-05": { + "commemorations": [ + "Our Venerable Father Athanasius of Athos" + ], + "rank": "polyeleos" + }, + "07-06": { + "commemorations": [ + "Our Venerable Father Sisoes the Great" + ] + }, + "07-07": { + "commemorations": [ + "Our Venerable Fathers Thomas of Maleon and Acacius" + ] + }, + "07-08": { + "commemorations": [ + "The Holy Great Martyr Procopius" + ], + "rank": "polyeleos" + }, + "07-09": { + "commemorations": [ + "The Holy Hieromartyr Pancratius, Bishop of Taormina" + ] + }, + "07-10": { + "commemorations": [ + "The Forty-five Holy Martyrs of Nicopolis in Armenia" + ] + }, + "07-11": { + "commemorations": [ + "The Holy Great Martyr Euphemia the All-praised", + "The Holy Olga, Equal-to-the-Apostles, Princess of Kiev" + ], + "rank": "polyeleos" + }, + "07-12": { + "commemorations": [ + "The Holy Martyrs Proclus and Hilary" + ] + }, + "07-13": { + "commemorations": [ + "The Synaxis of the Archangel Gabriel" + ], + "rank": "polyeleos" + }, + "07-14": { + "commemorations": [ + "The Holy Apostle Aquila" + ] + }, + "07-15": { + "commemorations": [ + "The Holy Martyrs Cyricus and Julitta", + "The Holy Vladimir, Equal-to-the-Apostles, Great Prince of Kiev" + ], + "rank": "polyeleos" + }, + "07-16": { + "commemorations": [ + "The Holy Hieromartyr Athenogenes and his ten disciples" + ] + }, + "07-17": { + "commemorations": [ + "The Holy Great Martyr Marina" + ], + "rank": "polyeleos" + }, + "07-18": { + "commemorations": [ + "The Holy Martyr Emilian" + ] + }, + "07-19": { + "commemorations": [ + "Our Venerable Mother Macrina, sister of St Basil the Great", + "Our Venerable Father Dius" + ] + }, + "07-20": { + "commemorations": [ + "The Holy and Glorious Prophet Elias" + ], + "rank": "vigil" + }, + "07-21": { + "commemorations": [ + "Our Venerable Fathers Symeon the Fool-for-Christ and John his companion" + ] + }, + "07-22": { + "commemorations": [ + "The Holy Myrrh-bearer and Equal-to-the-Apostles Mary Magdalene" + ], + "rank": "polyeleos" + }, + "07-23": { + "commemorations": [ + "The Holy Martyrs Trophimus and Theophilus" + ] + }, + "07-24": { + "commemorations": [ + "The Holy Martyr Christina", + "The Holy Passion-bearers Boris and Gleb" + ], + "rank": "polyeleos" + }, + "07-25": { + "commemorations": [ + "The Dormition of the Holy Anna, mother of the Most Holy Theotokos" + ], + "rank": "polyeleos" + }, + "07-26": { + "commemorations": [ + "The Holy Hieromartyr Hermolaus and his companions" + ] + }, + "07-27": { + "commemorations": [ + "The Holy Great Martyr and Healer Panteleimon" + ], + "rank": "vigil" + }, + "07-28": { + "commemorations": [ + "The Holy Apostles and Deacons Prochorus, Nicanor, Timon and Parmenas" + ] + }, + "07-29": { + "commemorations": [ + "The Holy Martyr Callinicus" + ] + }, + "07-30": { + "commemorations": [ + "The Holy Apostles Silas and Silvanus" + ] + }, + "07-31": { + "commemorations": [ + "The Holy and Righteous Eudocimus" + ], + "cycle": "the eve of the Procession of the Cross" + }, + "08-01": { + "commemorations": [ + "The Procession of the Precious and Life-giving Cross", + "The Seven Holy Maccabee Martyrs, their mother Solomone and their teacher Eleazar" + ], + "cycle": "the Dormition Fast begins", + "rank": "polyeleos" + }, + "08-02": { + "commemorations": [ + "The Translation of the Relics of the Holy Protomartyr Stephen" + ] + }, + "08-03": { + "commemorations": [ + "Our Venerable Fathers Isaac, Dalmatus and Faustus" + ] + }, + "08-04": { + "commemorations": [ + "The Seven Holy Youths of Ephesus" + ] + }, + "08-05": { + "commemorations": [ + "The Holy Martyr Eusignius" + ], + "cycle": "forefeast of the Transfiguration" + }, + "08-06": { + "commemorations": [ + "The Holy Transfiguration of our Lord, God and Saviour Jesus Christ" + ], + "rank": "great" + }, + "08-07": { + "commemorations": [ + "The Holy Venerable Martyr Dometius" + ], + "cycle": "afterfeast of the Transfiguration" + }, + "08-08": { + "commemorations": [ + "Our Holy Father Emilian the Confessor, Bishop of Cyzicus" + ], + "cycle": "afterfeast of the Transfiguration" + }, + "08-09": { + "commemorations": [ + "The Holy Apostle Matthias" + ], + "cycle": "afterfeast of the Transfiguration", + "rank": "polyeleos" + }, + "08-10": { + "commemorations": [ + "The Holy Martyr and Archdeacon Laurence" + ], + "cycle": "afterfeast of the Transfiguration" + }, + "08-11": { + "commemorations": [ + "The Holy Martyr Euplus" + ], + "cycle": "afterfeast of the Transfiguration" + }, + "08-12": { + "commemorations": [ + "The Holy Martyrs Photius and Anicetas" + ], + "cycle": "afterfeast of the Transfiguration" + }, + "08-13": { + "commemorations": [ + "Our Venerable Father Maximus the Confessor" + ], + "cycle": "leave-taking of the Transfiguration" + }, + "08-14": { + "commemorations": [ + "The Holy Prophet Micah" + ], + "cycle": "forefeast of the Dormition" + }, + "08-15": { + "commemorations": [ + "The Dormition of our Most Holy Lady the Theotokos and Ever-Virgin Mary" + ], + "rank": "great" + }, + "08-16": { + "commemorations": [ + "The Translation of the Holy Image of our Lord Not-made-by-hands" + ], + "cycle": "afterfeast of the Dormition", + "rank": "polyeleos" + }, + "08-17": { + "commemorations": [ + "The Holy Martyr Myron" + ], + "cycle": "afterfeast of the Dormition" + }, + "08-18": { + "commemorations": [ + "The Holy Martyrs Florus and Laurus" + ], + "cycle": "afterfeast of the Dormition" + }, + "08-19": { + "commemorations": [ + "The Holy Martyr Andrew the General and his companions" + ], + "cycle": "afterfeast of the Dormition" + }, + "08-20": { + "commemorations": [ + "The Holy Prophet Samuel" + ], + "cycle": "afterfeast of the Dormition" + }, + "08-21": { + "commemorations": [ + "The Holy Apostle Thaddeus" + ], + "cycle": "afterfeast of the Dormition" + }, + "08-22": { + "commemorations": [ + "The Holy Martyr Agathonicus and his companions" + ], + "cycle": "afterfeast of the Dormition" + }, + "08-23": { + "commemorations": [ + "The Holy Hieromartyr Lupus" + ], + "cycle": "leave-taking of the Dormition" + }, + "08-24": { + "commemorations": [ + "The Holy Hieromartyr Eutyches" + ] + }, + "08-25": { + "commemorations": [ + "The Return of the Relics of the Holy Apostle Bartholomew", + "The Holy Apostle Titus" + ], + "rank": "polyeleos" + }, + "08-26": { + "commemorations": [ + "The Holy Martyrs Adrian and Natalia" + ] + }, + "08-27": { + "commemorations": [ + "Our Venerable Father Poemen the Great" + ] + }, + "08-28": { + "commemorations": [ + "Our Venerable Father Moses the Black" + ] + }, + "08-29": { + "commemorations": [ + "The Beheading of the Honourable and Glorious Prophet, Forerunner and Baptist John" + ], + "rank": "vigil" + }, + "08-30": { + "commemorations": [ + "Our Holy Fathers Alexander, John and Paul the New, Patriarchs of Constantinople" + ] + }, + "08-31": { + "commemorations": [ + "The Deposition of the Precious Sash of our Most Holy Lady the Theotokos" + ], + "rank": "polyeleos" + }, + "09-01": { + "commemorations": [ + "The Beginning of the Indiction, the Church New Year", + "Our Venerable Father Symeon the Stylite and his mother Martha" + ], + "rank": "vigil" + }, + "09-02": { + "commemorations": [ + "The Holy Martyr Mamas", + "Our Father John the Faster, Patriarch of Constantinople" + ] + }, + "09-03": { + "commemorations": [ + "The Holy Hieromartyr Anthimus, Bishop of Nicomedia", + "Our Venerable Father Theoctistus" + ] + }, + "09-04": { + "commemorations": [ + "The Holy Hieromartyr Babylas, Bishop of Antioch", + "The Holy Prophet and God-seer Moses" + ] + }, + "09-05": { + "commemorations": [ + "The Holy Prophet Zachariah, father of the Forerunner" + ] + }, + "09-06": { + "commemorations": [ + "The Commemoration of the Miracle of the Archangel Michael at Colossae" + ], + "rank": "polyeleos" + }, + "09-07": { + "commemorations": [ + "The Holy Martyr Sozon" + ], + "cycle": "forefeast of the Nativity of the Theotokos" + }, + "09-08": { + "commemorations": [ + "The Nativity of our Most Holy Lady the Theotokos and Ever-Virgin Mary" + ], + "rank": "great" + }, + "09-09": { + "commemorations": [ + "The Holy and Righteous Ancestors of God Joachim and Anna", + "The Holy Martyr Severian" + ], + "cycle": "afterfeast of the Nativity of the Theotokos", + "rank": "polyeleos" + }, + "09-10": { + "commemorations": [ + "The Holy Martyrs Menodora, Metrodora and Nymphodora" + ], + "cycle": "afterfeast of the Nativity of the Theotokos" + }, + "09-11": { + "commemorations": [ + "Our Venerable Mother Theodora of Alexandria" + ], + "cycle": "afterfeast of the Nativity of the Theotokos" + }, + "09-12": { + "commemorations": [ + "The Holy Hieromartyr Autonomus" + ], + "cycle": "leave-taking of the Nativity of the Theotokos" + }, + "09-13": { + "commemorations": [ + "The Dedication of the Church of the Holy Resurrection", + "The Holy Hieromartyr Cornelius the Centurion" + ], + "cycle": "forefeast of the Exaltation of the Cross", + "rank": "polyeleos" + }, + "09-14": { + "commemorations": [ + "The Universal Exaltation of the Precious and Life-giving Cross" + ], + "rank": "great" + }, + "09-15": { + "commemorations": [ + "The Holy Great Martyr Nicetas" + ], + "cycle": "afterfeast of the Exaltation" + }, + "09-16": { + "commemorations": [ + "The Holy Great Martyr Euphemia the All-praised" + ], + "cycle": "afterfeast of the Exaltation", + "rank": "polyeleos" + }, + "09-17": { + "commemorations": [ + "The Holy Martyrs Sophia and her daughters Faith, Hope and Love" + ], + "cycle": "afterfeast of the Exaltation" + }, + "09-18": { + "commemorations": [ + "Our Venerable Father Eumenius, Bishop of Gortyna" + ], + "cycle": "afterfeast of the Exaltation" + }, + "09-19": { + "commemorations": [ + "The Holy Martyrs Trophimus, Sabbatius and Dorymedon" + ], + "cycle": "afterfeast of the Exaltation" + }, + "09-20": { + "commemorations": [ + "The Holy Great Martyr Eustathius Placidas and his companions" + ], + "cycle": "afterfeast of the Exaltation", + "rank": "polyeleos" + }, + "09-21": { + "commemorations": [ + "The Holy Apostle Quadratus" + ], + "cycle": "leave-taking of the Exaltation" + }, + "09-22": { + "commemorations": [ + "The Holy Hieromartyr Phocas, Bishop of Sinope" + ] + }, + "09-23": { + "commemorations": [ + "The Conception of the Honourable and Glorious Prophet, Forerunner and Baptist John" + ], + "rank": "polyeleos" + }, + "09-24": { + "commemorations": [ + "The Holy Protomartyr and Equal-to-the-Apostles Thecla" + ], + "rank": "polyeleos" + }, + "09-25": { + "commemorations": [ + "Our Venerable Mother Euphrosyne of Alexandria" + ] + }, + "09-26": { + "commemorations": [ + "The Repose of the Holy Apostle and Evangelist John the Theologian" + ], + "rank": "vigil" + }, + "09-27": { + "commemorations": [ + "The Holy Martyr Callistratus and his companions" + ] + }, + "09-28": { + "commemorations": [ + "Our Venerable Father Chariton the Confessor" + ] + }, + "09-29": { + "commemorations": [ + "Our Venerable Father Cyriacus the Hermit" + ] + }, + "09-30": { + "commemorations": [ + "The Holy Hieromartyr Gregory, Enlightener of Armenia" + ] + }, + "10-01": { + "commemorations": [ + "The Protection of our Most Holy Lady the Theotokos", + "The Holy Apostle Ananias", + "Our Venerable Father Romanus the Melodist" + ], + "rank": "vigil" + }, + "10-02": { + "commemorations": [ + "The Holy Hieromartyr Cyprian and the Holy Martyr Justina" + ] + }, + "10-03": { + "commemorations": [ + "The Holy Hieromartyr Dionysius the Areopagite" + ], + "rank": "polyeleos" + }, + "10-04": { + "commemorations": [ + "The Holy Hieromartyr Hierotheus, Bishop of Athens" + ] + }, + "10-05": { + "commemorations": [ + "The Holy Martyr Charitina" + ] + }, + "10-06": { + "commemorations": [ + "The Holy and Glorious Apostle Thomas" + ], + "rank": "vigil" + }, + "10-07": { + "commemorations": [ + "The Holy Martyrs Sergius and Bacchus" + ] + }, + "10-08": { + "commemorations": [ + "Our Venerable Mother Pelagia" + ] + }, + "10-09": { + "commemorations": [ + "The Holy Apostle James, son of Alphaeus", + "Our Venerable Father Andronicus and his wife Athanasia" + ], + "rank": "polyeleos" + }, + "10-10": { + "commemorations": [ + "The Holy Martyrs Eulampius and Eulampia" + ] + }, + "10-11": { + "commemorations": [ + "The Holy Apostle Philip, one of the Seven Deacons", + "Our Venerable Father Theophanes the Branded" + ] + }, + "10-12": { + "commemorations": [ + "The Holy Martyrs Probus, Tarachus and Andronicus" + ] + }, + "10-13": { + "commemorations": [ + "The Holy Martyrs Carpus, Papylus, Agathodorus and Agathonica" + ] + }, + "10-14": { + "commemorations": [ + "Our Venerable Mother Paraskeva of Serbia", + "The Holy Martyrs Nazarius, Gervase, Protase and Celsus" + ], + "rank": "polyeleos" + }, + "10-15": { + "commemorations": [ + "Our Venerable Father Euthymius the New", + "The Holy Venerable Martyr Lucian of Antioch" + ] + }, + "10-16": { + "commemorations": [ + "The Holy Martyr Longinus the Centurion" + ] + }, + "10-17": { + "commemorations": [ + "The Holy Prophet Hosea", + "Our Venerable Martyr Andrew of Crete" + ] + }, + "10-18": { + "commemorations": [ + "The Holy Apostle and Evangelist Luke" + ], + "rank": "vigil" + }, + "10-19": { + "commemorations": [ + "The Holy Prophet Joel", + "The Holy Martyr Varus", + "Our Venerable Father John of Kronstadt" + ] + }, + "10-20": { + "commemorations": [ + "The Holy Great Martyr Artemius" + ], + "rank": "polyeleos" + }, + "10-21": { + "commemorations": [ + "Our Venerable Father Hilarion the Great" + ], + "rank": "polyeleos" + }, + "10-22": { + "commemorations": [ + "Our Holy Father Abercius, Equal-to-the-Apostles and Wonderworker of Hierapolis", + "The Seven Holy Youths of Ephesus" + ], + "rank": "polyeleos" + }, + "10-23": { + "commemorations": [ + "The Holy Apostle James, the Brother of the Lord" + ], + "rank": "vigil" + }, + "10-24": { + "commemorations": [ + "The Holy Martyr Arethas and his companions" + ] + }, + "10-25": { + "commemorations": [ + "The Holy Martyrs and Notaries Marcian and Martyrius" + ] + }, + "10-26": { + "commemorations": [ + "The Holy and Glorious Great Martyr Demetrius the Myrrh-streaming" + ], + "rank": "vigil" + }, + "10-27": { + "commemorations": [ + "The Holy Martyr Nestor" + ] + }, + "10-28": { + "commemorations": [ + "The Holy Martyrs Terence and Neonilla", + "Our Venerable Father Stephen of St Sabbas" + ] + }, + "10-29": { + "commemorations": [ + "The Holy Venerable Martyr Anastasia the Roman", + "Our Venerable Father Abramius the Recluse" + ] + }, + "10-30": { + "commemorations": [ + "The Holy Hieromartyr Zenobius and his sister Zenobia" + ] + }, + "10-31": { + "commemorations": [ + "The Holy Apostles Stachys, Amplias and their companions", + "The Holy Martyr Epimachus" + ] + }, + "11-01": { + "commemorations": [ + "The Holy Unmercenaries and Wonderworkers Cosmas and Damian of Asia" + ], + "rank": "polyeleos" + }, + "11-02": { + "commemorations": [ + "The Holy Martyrs Acindynus, Pegasius, Aphthonius, Elpidephorus and Anempodistus" + ] + }, + "11-03": { + "commemorations": [ + "The Holy Martyrs Acepsimas, Joseph and Aithalas" + ] + }, + "11-04": { + "commemorations": [ + "Our Venerable Father Joannicius the Great" + ] + }, + "11-05": { + "commemorations": [ + "The Holy Martyrs Galaction and Episteme" + ] + }, + "11-06": { + "commemorations": [ + "Our Holy Father Paul the Confessor, Archbishop of Constantinople" + ] + }, + "11-07": { + "commemorations": [ + "The Thirty-three Holy Martyrs of Melitene", + "Our Venerable Father Lazarus of Mount Galesius" + ] + }, + "11-08": { + "commemorations": [ + "The Synaxis of the Archangel Michael and all the Bodiless Powers of Heaven" + ], + "rank": "vigil" + }, + "11-09": { + "commemorations": [ + "The Holy Martyrs Onesiphorus and Porphyrius", + "Our Venerable Mother Matrona" + ] + }, + "11-10": { + "commemorations": [ + "The Holy Apostles Erastus, Olympas, Rodion and their companions" + ] + }, + "11-11": { + "commemorations": [ + "The Holy Great Martyr Menas", + "The Holy Martyr Victor", + "Our Venerable Father Theodore the Studite" + ], + "rank": "polyeleos" + }, + "11-12": { + "commemorations": [ + "Our Holy Father John the Merciful, Patriarch of Alexandria", + "Our Venerable Father Nilus the Faster" + ] + }, + "11-13": { + "commemorations": [ + "Our Holy Father John Chrysostom, Archbishop of Constantinople" + ], + "rank": "vigil" + }, + "11-14": { + "commemorations": [ + "The Holy and All-praised Apostle Philip" + ], + "rank": "vigil" + }, + "11-15": { + "commemorations": [ + "The Holy Martyrs Gurias, Samonas and Habib" + ], + "cycle": "the Nativity Fast begins" + }, + "11-16": { + "commemorations": [ + "The Holy Apostle and Evangelist Matthew" + ], + "rank": "vigil" + }, + "11-17": { + "commemorations": [ + "Our Holy Father Gregory the Wonderworker, Bishop of Neocaesarea" + ] + }, + "11-18": { + "commemorations": [ + "The Holy Martyr Plato", + "The Holy Martyr Romanus" + ] + }, + "11-19": { + "commemorations": [ + "The Holy Prophet Obadiah", + "The Holy Martyr Barlaam" + ] + }, + "11-20": { + "commemorations": [ + "Our Venerable Father Gregory the Decapolite", + "Our Holy Father Proclus, Archbishop of Constantinople" + ], + "cycle": "forefeast of the Entrance of the Theotokos" + }, + "11-21": { + "commemorations": [ + "The Entrance of our Most Holy Lady the Theotokos into the Temple" + ], + "rank": "great" + }, + "11-22": { + "commemorations": [ + "The Holy Apostle Philemon and his companions" + ], + "cycle": "afterfeast of the Entrance" + }, + "11-23": { + "commemorations": [ + "Our Holy Father Amphilochius, Bishop of Iconium" + ], + "cycle": "afterfeast of the Entrance" + }, + "11-24": { + "commemorations": [ + "The Holy Great Martyr Catherine", + "The Holy Great Martyr Mercurius" + ], + "cycle": "afterfeast of the Entrance", + "rank": "polyeleos" + }, + "11-25": { + "commemorations": [ + "The Holy Hieromartyr Clement, Pope of Rome", + "The Holy Hieromartyr Peter of Alexandria" + ], + "cycle": "leave-taking of the Entrance", + "rank": "polyeleos" + }, + "11-26": { + "commemorations": [ + "Our Venerable Father Alypius the Stylite" + ] + }, + "11-27": { + "commemorations": [ + "The Holy Great Martyr James the Persian" + ], + "rank": "polyeleos" + }, + "11-28": { + "commemorations": [ + "Our Venerable Martyr Stephen the New" + ] + }, + "11-29": { + "commemorations": [ + "The Holy Martyr Paramonus and his companions" + ] + }, + "11-30": { + "commemorations": [ + "The Holy and All-praised Apostle Andrew the First-called" + ], + "rank": "vigil" + }, + "12-01": { + "commemorations": [ + "The Holy Prophet Nahum" + ] + }, + "12-02": { + "commemorations": [ + "The Holy Prophet Habakkuk" + ] + }, + "12-03": { + "commemorations": [ + "The Holy Prophet Zephaniah" + ] + }, + "12-04": { + "commemorations": [ + "The Holy Great Martyr Barbara", + "Our Venerable Father John of Damascus" + ], + "rank": "polyeleos" + }, + "12-05": { + "commemorations": [ + "Our Venerable and God-bearing Father Sabbas the Sanctified" + ], + "rank": "vigil" + }, + "12-06": { + "commemorations": [ + "Our Holy Father Nicholas the Wonderworker, Archbishop of Myra in Lycia" + ], + "rank": "vigil" + }, + "12-07": { + "commemorations": [ + "Our Holy Father Ambrose, Bishop of Milan" + ] + }, + "12-08": { + "commemorations": [ + "Our Venerable Father Patapius" + ] + }, + "12-09": { + "commemorations": [ + "The Conception of the Most Holy Theotokos by the Holy Anna" + ], + "rank": "polyeleos" + }, + "12-10": { + "commemorations": [ + "The Holy Martyrs Menas, Hermogenes and Eugraphus" + ] + }, + "12-11": { + "commemorations": [ + "Our Venerable Father Daniel the Stylite" + ] + }, + "12-12": { + "commemorations": [ + "Our Holy Father Spyridon the Wonderworker, Bishop of Trimythous" + ], + "rank": "vigil" + }, + "12-13": { + "commemorations": [ + "The Holy Martyrs Eustratius, Auxentius, Eugene, Mardarius and Orestes" + ], + "rank": "polyeleos" + }, + "12-14": { + "commemorations": [ + "The Holy Martyrs Thyrsus, Leucius and Philemon" + ] + }, + "12-15": { + "commemorations": [ + "The Holy Hieromartyr Eleutherius" + ] + }, + "12-16": { + "commemorations": [ + "The Holy Prophet Haggai" + ] + }, + "12-17": { + "commemorations": [ + "The Holy Prophet Daniel and the Three Holy Youths" + ], + "rank": "polyeleos" + }, + "12-18": { + "commemorations": [ + "The Holy Martyr Sebastian and his companions" + ] + }, + "12-19": { + "commemorations": [ + "The Holy Martyr Boniface" + ] + }, + "12-20": { + "commemorations": [ + "The Holy Hieromartyr Ignatius the God-bearer" + ], + "cycle": "forefeast of the Nativity", + "rank": "polyeleos" + }, + "12-21": { + "commemorations": [ + "The Holy Martyr Juliana" + ], + "cycle": "forefeast of the Nativity" + }, + "12-22": { + "commemorations": [ + "The Holy Great Martyr Anastasia the Deliverer from Potions" + ], + "cycle": "forefeast of the Nativity" + }, + "12-23": { + "commemorations": [ + "The Ten Holy Martyrs of Crete" + ], + "cycle": "forefeast of the Nativity" + }, + "12-24": { + "commemorations": [ + "The Holy Venerable Martyr Eugenia" + ], + "cycle": "the eve of the Nativity" + }, + "12-25": { + "commemorations": [ + "The Nativity according to the Flesh of our Lord, God and Saviour Jesus Christ" + ], + "rank": "great" + }, + "12-26": { + "commemorations": [ + "The Synaxis of our Most Holy Lady the Theotokos" + ], + "cycle": "afterfeast of the Nativity", + "rank": "polyeleos" + }, + "12-27": { + "commemorations": [ + "The Holy Protomartyr and Archdeacon Stephen" + ], + "cycle": "afterfeast of the Nativity", + "rank": "polyeleos" + }, + "12-28": { + "commemorations": [ + "The Twenty Thousand Holy Martyrs burned at Nicomedia" + ], + "cycle": "afterfeast of the Nativity" + }, + "12-29": { + "commemorations": [ + "The Holy Innocents slain at Bethlehem" + ], + "cycle": "afterfeast of the Nativity" + }, + "12-30": { + "commemorations": [ + "The Holy Martyr Anysia" + ], + "cycle": "afterfeast of the Nativity" + }, + "12-31": { + "commemorations": [ + "Our Venerable Mother Melania the Younger" + ], + "cycle": "leave-taking of the Nativity" + } +} \ No newline at end of file diff --git a/typikon/day.py b/typikon/day.py new file mode 100644 index 0000000..1f8bdca --- /dev/null +++ b/typikon/day.py @@ -0,0 +1,176 @@ +"""A single day, fully reckoned. + +``Day`` is what everything else in the library produces and what every +front end consumes. It carries the civil date, the ecclesiastical date, the +season, the commemorations and the fast — and it carries the jurisdiction that +produced it, so a caller can always ask by whose reckoning. +""" + +import datetime as dt +from dataclasses import dataclass, field + +from . import menologion as _menologion +from . import rules as _rules +from .calendars import civil_of_fixed, fixed_of_civil, julian_offset +from .fasting import NONE, OBLIGATORY +from .jurisdictions import resolve +from .paschalion import movable_cycle +from .seasons import season_for + +__all__ = ["Day", "describe", "week", "year_of"] + + +@dataclass +class _Context: + """What a fasting rule is handed. Deliberately small.""" + civil: dt.date + fixed: tuple # ecclesiastical (month, day) + fixed_year: int + weekday: int # 0 = Monday + mv: dict + season: str + feast: str + rank: str + menologion_kind: str + + def civil_of_fixed(self, month, day): + """The civil date of an ecclesiastical date in this day's church year.""" + return civil_of_fixed(self.fixed_year, month, day, self.menologion_kind) + + +@dataclass +class Day: + date: dt.date + weekday: str + jurisdiction: str + calendar: str + church_date: str # the ecclesiastical date, ISO-ish + season: str = None + cycle: str = None # forefeast / afterfeast / leave-taking + feast: str = None + rank: str = None + commemorations: list = field(default_factory=list) + fast: str = NONE + fast_label: str = "" + emoji: str = "" + reason: str = "" + obligation: str = None + binds: str = NONE + pascha: dt.date = None + days_to_pascha: int = 0 + julian_offset: int = 0 + + @property + def is_obligatory(self): + return self.obligation == OBLIGATORY + + @property + def title(self): + """What to put on one line: the feast if there is one, else the day's + principal commemoration.""" + if self.feast: + return self.feast + return self.commemorations[0] if self.commemorations else "" + + def to_dict(self): + out = { + "date": self.date.isoformat(), + "weekday": self.weekday, + "jurisdiction": self.jurisdiction, + "calendar": self.calendar, + "churchDate": self.church_date, + "season": self.season, + "cycle": self.cycle, + "feast": self.feast, + "rank": self.rank, + "title": self.title, + "commemorations": self.commemorations, + "fast": self.fast, + "fastLabel": self.fast_label, + "emoji": self.emoji, + "reason": self.reason, + "obligation": self.obligation, + "binds": self.binds, + "pascha": self.pascha.isoformat() if self.pascha else None, + "daysToPascha": self.days_to_pascha, + "julianOffset": self.julian_offset, + } + return out + + +_WEEKDAYS = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", + "Saturday", "Sunday") +_MONTHS = ("January", "February", "March", "April", "May", "June", "July", + "August", "September", "October", "November", "December") + + +def describe(date, jurisdiction=None): + """Reckon one civil date in one jurisdiction.""" + jur = jurisdiction if hasattr(jurisdiction, "paschalion") else resolve(jurisdiction) + rule = _rules.get(jur.rule) + men = _menologion.for_jurisdiction(jur) + + fy, fm, fd = fixed_of_civil(date, jur.menologion) + mv = movable_cycle(date.year, jur.paschalion) + apostles_end = civil_of_fixed(date.year, 6, 28, jur.menologion) + season = season_for(date, (fm, fd), mv, apostles_end) + + entry = men.get(fm, fd) + commemorations = list(entry.get("commemorations", [])) + rank = entry.get("rank") + cycle = entry.get("cycle") + + # A movable feast outranks whatever the fixed calendar holds that day. + mov_title, mov_rank = _menologion.movable_feast_for(date, mv) + if mov_title: + feast = mov_title + rank = mov_rank or rank + else: + feast = commemorations[0] if rank in ("great", "vigil") else None + + ctx = _Context( + civil=date, fixed=(fm, fd), fixed_year=fy, weekday=date.weekday(), + mv=mv, season=season, feast=feast or (commemorations[0] if commemorations else ""), + rank=rank, menologion_kind=jur.menologion, + ) + fast = rule["fast_for"](ctx) + label, emoji = rule["vocabulary"].get(fast.level, (fast.level, "")) + + return Day( + date=date, + weekday=_WEEKDAYS[date.weekday()], + jurisdiction=jur.key, + calendar=jur.calendar, + church_date=f"{fd} {_MONTHS[fm - 1]}", + season=season, + cycle=cycle, + feast=feast, + rank=rank, + commemorations=commemorations, + fast=fast.level, + fast_label=label, + emoji=emoji, + reason=fast.reason, + obligation=fast.obligation, + binds=fast.binds, + pascha=mv["pascha"], + days_to_pascha=(mv["pascha"] - date).days, + julian_offset=julian_offset(date), + ) + + +def week(start, days=7, jurisdiction=None): + """`days` consecutive days from `start`.""" + return [describe(start + dt.timedelta(n), jurisdiction) + for n in range(max(1, days))] + + +def year_of(year, jurisdiction=None): + """Every key date of a year, for checking against a published calendar.""" + jur = jurisdiction if hasattr(jurisdiction, "paschalion") else resolve(jurisdiction) + mv = movable_cycle(year, jur.paschalion) + mv["apostles_end"] = civil_of_fixed(year, 6, 28, jur.menologion) + mv["nativity"] = civil_of_fixed(year, 12, 25, jur.menologion) + mv["theophany"] = civil_of_fixed(year, 1, 6, jur.menologion) + mv["dormition"] = civil_of_fixed(year, 8, 15, jur.menologion) + return mv diff --git a/typikon/export/__init__.py b/typikon/export/__init__.py new file mode 100644 index 0000000..1c51295 --- /dev/null +++ b/typikon/export/__init__.py @@ -0,0 +1,4 @@ +from .ics import to_ics +from .jsonout import to_json + +__all__ = ["to_json", "to_ics"] diff --git a/typikon/export/ics.py b/typikon/export/ics.py new file mode 100644 index 0000000..470de00 --- /dev/null +++ b/typikon/export/ics.py @@ -0,0 +1,93 @@ +"""iCalendar, so the calendar can live where the rest of your year does. + +Each day becomes one all-day event. The summary is the feast or the principal +commemoration; the description carries the fast, the season and the rest of the +day's commemorations. Days with nothing to say — no feast, no fast — are left +out, because a subscribed calendar that marks all 365 days is one nobody keeps. +""" + +import datetime as dt + +__all__ = ["to_ics"] + +_FOLD = 73 + + +_BACKSLASH = chr(92) + +# Order matters: the backslash has to be doubled before anything else +# introduces one, or the escapes escape each other. +_ESCAPES = ( + (_BACKSLASH, _BACKSLASH * 2), + (";", _BACKSLASH + ";"), + (",", _BACKSLASH + ","), + ("\n", _BACKSLASH + "n"), +) + + +def _esc(text): + out = str(text) + for char, replacement in _ESCAPES: + out = out.replace(char, replacement) + return out + + +def _fold(line): + """RFC 5545 wants lines under 75 octets, continued with a leading space.""" + out = [line[:_FOLD]] + rest = line[_FOLD:] + while rest: + out.append(" " + rest[:_FOLD - 1]) + rest = rest[_FOLD - 1:] + return "\r\n".join(out) + + +def to_ics(days, name="Typikon", stamp=None): + stamp = (stamp or dt.datetime.now(dt.timezone.utc)).strftime("%Y%m%dT%H%M%SZ") + lines = [ + "BEGIN:VCALENDAR", + "VERSION:2.0", + "PRODID:-//typikon//EN", + "CALSCALE:GREGORIAN", + "METHOD:PUBLISH", + f"X-WR-CALNAME:{_esc(name)}", + ] + + for d in days: + if not d.feast and d.fast == "none": + continue + start = d.date.strftime("%Y%m%d") + end = (d.date + dt.timedelta(1)).strftime("%Y%m%d") + summary = d.title or d.fast_label + if d.emoji: + summary = f"{d.emoji} {summary}" + + body = [] + if d.fast != "none": + body.append(f"Fast: {d.fast_label}" + + (f" ({d.reason})" if d.reason else "")) + if d.obligation == "obligatory": + body.append("Obligatory.") + if d.season: + body.append(f"Season: {d.season}") + if d.cycle: + body.append(d.cycle[0].upper() + d.cycle[1:]) + body.append(f"Church date: {d.church_date}") + for c in d.commemorations: + body.append(c) + + uid = f"{start}-{d.jurisdiction}@typikon" + lines += [ + "BEGIN:VEVENT", + f"UID:{uid}", + f"DTSTAMP:{stamp}", + f"DTSTART;VALUE=DATE:{start}", + f"DTEND;VALUE=DATE:{end}", + _fold(f"SUMMARY:{_esc(summary)}"), + _fold(f"DESCRIPTION:{_esc(chr(10).join(body))}"), + "TRANSP:TRANSPARENT", + "END:VEVENT", + ] + + lines.append("END:VCALENDAR") + return "\r\n".join(lines) + "\r\n" diff --git a/typikon/export/jsonout.py b/typikon/export/jsonout.py new file mode 100644 index 0000000..e2f6266 --- /dev/null +++ b/typikon/export/jsonout.py @@ -0,0 +1,19 @@ +"""JSON, which is how every front end reads this. + +The shape is flat and stable: front ends are the thing most likely to be +written by someone else, and a schema that shifts underneath them is a bad +neighbour. One day is an object; several are ``{"days": [...]}``. +""" + +import json + +__all__ = ["to_json"] + + +def to_json(days, indent=None): + if not isinstance(days, (list, tuple)): + return json.dumps(days.to_dict(), ensure_ascii=False, indent=indent) + if len(days) == 1: + return json.dumps(days[0].to_dict(), ensure_ascii=False, indent=indent) + return json.dumps({"days": [d.to_dict() for d in days]}, + ensure_ascii=False, indent=indent) diff --git a/typikon/fasting.py b/typikon/fasting.py new file mode 100644 index 0000000..f6c94a3 --- /dev/null +++ b/typikon/fasting.py @@ -0,0 +1,82 @@ +"""What is kept on a day, and whether anything about it binds. + +Six levels, ordered from strictest to laxest. They are the Typikon's own +distinctions, not a simplification of them: the difference between a day of +xerophagy and a day of wine and oil is the difference the books actually make, +and a calendar that reports both as "fasting" has thrown away the information +its reader wanted. + + strict no food until evening + xerophagy no oil, no wine; no meat, fish, eggs or dairy + wine_oil wine and oil permitted; no meat, fish, eggs or dairy + fish fish, wine and oil permitted; no meat, eggs or dairy + dairy dairy, eggs and fish permitted; no meat + none no fast + +The same six serve a tradition that makes fewer distinctions. Ruthenian +particular law knows three degrees, and each maps exactly onto one of these — +its "strict abstinence" is ``wine_oil`` (it does not regulate oil or wine), its +"abstinence" is ``dairy`` (meat alone is set aside). So a ruleset emits the +subset it uses and names those levels in its own vocabulary, rather than every +tradition being forced through one set of words. + +**Level and obligation are separate questions.** A day can be kept strictly by +universal custom and bind no one, and that is the ordinary case in the Eastern +Catholic churches, whose particular law requires very little. Reporting the +binding minimum as the day's level makes the obligatory days look laxer than +the voluntary ones beside them — true of the obligation, false of the day. So +both are carried, and a caller that wants one must say which. +""" + +from dataclasses import dataclass + +__all__ = [ + "STRICT", "XEROPHAGY", "WINE_OIL", "FISH", "DAIRY", "NONE", + "LEVELS", "stricter", "strictest", "laxest", + "OBLIGATORY", "CUSTOMARY", "Fast", +] + +STRICT = "strict" +XEROPHAGY = "xerophagy" +WINE_OIL = "wine_oil" +FISH = "fish" +DAIRY = "dairy" +NONE = "none" + +# Strictest first. Index into this is the comparison. +LEVELS = (STRICT, XEROPHAGY, WINE_OIL, FISH, DAIRY, NONE) + +OBLIGATORY = "obligatory" +CUSTOMARY = "customary" # encouraged, traditional, not binding + + +def stricter(a, b): + """True if `a` is a stricter level than `b`.""" + return LEVELS.index(a) < LEVELS.index(b) + + +def strictest(*levels): + return min(levels, key=LEVELS.index) + + +def laxest(*levels): + return max(levels, key=LEVELS.index) + + +@dataclass(frozen=True) +class Fast: + """How a day is kept, and what about it binds. + + ``level`` is the day as the tradition keeps it. ``binds`` is what the + Church actually requires, which for most jurisdictions is nothing — it is + ``none`` unless a ruleset has particular law to point at. ``reason`` names + the rule that decided it, so the answer can be argued with. + """ + level: str + reason: str = "" + obligation: str = None # OBLIGATORY, CUSTOMARY, or None for a free day + binds: str = NONE # the binding minimum, where one exists + + @property + def is_fast(self): + return self.level != NONE diff --git a/typikon/jurisdictions.py b/typikon/jurisdictions.py new file mode 100644 index 0000000..d5c1840 --- /dev/null +++ b/typikon/jurisdictions.py @@ -0,0 +1,151 @@ +"""Who keeps which calendar, and by whose rule. + +A jurisdiction is four independent choices, and conflating any two of them is +how calendar code usually goes wrong: + + paschalion which table gives Pascha — julian or gregorian + menologion which calendar the fixed feasts are read on + rule which fasting discipline is kept + menaia which supplementary commemorations to load, if any + +The pairings are not free. Almost every Orthodox Church keeps the Julian +Paschalion; what divides them is the menologion — the "old calendar" churches +read the fixed feasts on the Julian calendar too, the "new calendar" churches +read them on the Revised Julian, which agrees with the Gregorian for every date +until 2800. So a new-calendar church keeps Christmas with the West and Pascha +with the East, which is not a compromise but exactly what the 1923 congress +decided. + +Only two bodies here use the Gregorian Paschalion: the Finnish Orthodox Church, +alone among the Orthodox, and the Byzantine Catholic churches. + +Where a church is mid-transition or its parishes differ, the entry says so in +``note`` rather than pretending to a precision that does not exist. +""" + +from dataclasses import dataclass, field + +__all__ = ["Jurisdiction", "JURISDICTIONS", "CALENDARS", "resolve", "names"] + + +@dataclass(frozen=True) +class Jurisdiction: + key: str + name: str + paschalion: str # "julian" | "gregorian" + menologion: str # "julian" | "revised_julian" + rule: str = "orthodox" # which fasting discipline + menaia: tuple = () # extra commemoration sets to overlay + note: str = "" + + @property + def calendar(self): + """The short name people actually use for this combination.""" + if self.paschalion == "gregorian" and self.menologion != "julian": + return "western" + return "old" if self.menologion == "julian" else "new" + + +# The three combinations, for anyone who wants a reckoning without naming a +# church. `--calendar new` is the default and covers most English-speaking +# Orthodox parishes. +CALENDARS = { + "old": Jurisdiction( + "old", "Old Calendar", "julian", "julian", + note="Julian throughout — the reckoning of Moscow, Serbia, Georgia, " + "Jerusalem and the Holy Mountain."), + "new": Jurisdiction( + "new", "New Calendar", "julian", "revised_julian", + note="Revised Julian fixed feasts with the Julian Paschalion — " + "Constantinople, Greece, Antioch, Romania, Bulgaria, the OCA."), + "western": Jurisdiction( + "western", "Gregorian", "gregorian", "revised_julian", + note="Gregorian throughout, Pascha included. Kept by the Finnish " + "Orthodox Church and by most Byzantine Catholics."), +} + + +_ORTHODOX = [ + # ── old calendar + ("moscow", "Russian Orthodox Church", "julian", "julian", "orthodox", (), ""), + ("rocor", "Russian Orthodox Church Outside Russia", "julian", "julian", + "orthodox", (), ""), + ("serbian", "Serbian Orthodox Church", "julian", "julian", "orthodox", (), ""), + ("georgian", "Georgian Orthodox Church", "julian", "julian", "orthodox", (), ""), + ("jerusalem", "Patriarchate of Jerusalem", "julian", "julian", "orthodox", (), ""), + ("athos", "The Holy Mountain", "julian", "julian", "orthodox", (), + "Athonite practice is stricter than the printed Typikon at many points."), + ("polish", "Polish Orthodox Church", "julian", "julian", "orthodox", (), + "Returned to the Julian menologion in 2014 after decades on the Revised " + "Julian; some parishes kept the new calendar."), + ("macedonian", "Macedonian Orthodox Church", "julian", "julian", + "orthodox", (), ""), + + # ── new calendar + ("constantinople", "Ecumenical Patriarchate", "julian", "revised_julian", + "orthodox", (), ""), + ("goarch", "Greek Orthodox Archdiocese of America", "julian", + "revised_julian", "orthodox", (), ""), + ("greece", "Church of Greece", "julian", "revised_julian", "orthodox", (), ""), + ("antiochian", "Antiochian Orthodox Church", "julian", "revised_julian", + "orthodox", (), ""), + ("oca", "Orthodox Church in America", "julian", "revised_julian", + "orthodox", (), + "Most parishes keep the new calendar; a minority, chiefly of Carpatho-" + "Russian and Alaskan background, keep the old."), + ("romanian", "Romanian Orthodox Church", "julian", "revised_julian", + "orthodox", (), ""), + ("bulgarian", "Bulgarian Orthodox Church", "julian", "revised_julian", + "orthodox", (), ""), + ("cyprus", "Church of Cyprus", "julian", "revised_julian", "orthodox", (), ""), + ("albanian", "Orthodox Church of Albania", "julian", "revised_julian", + "orthodox", (), ""), + ("ukrainian", "Orthodox Church of Ukraine", "julian", "revised_julian", + "orthodox", (), + "Moved the fixed feasts to the Revised Julian calendar on 1 September " + "2023. The Julian Paschalion is retained."), + + # ── gregorian throughout + ("finnish", "Finnish Orthodox Church", "gregorian", "revised_julian", + "orthodox", (), + "The only Orthodox Church keeping the Gregorian Paschalion."), +] + +_EASTERN_CATHOLIC = [ + ("ruthenian", "Byzantine Catholic Metropolia of Pittsburgh", "gregorian", + "revised_julian", "ruthenian", ("ruthenian",), + "Gregorian throughout. Fasting is reported under Ruthenian particular " + "law, which binds far less than the Typikon prescribes."), + ("ugcc", "Ukrainian Greek Catholic Church", "julian", "revised_julian", + "ruthenian", ("ruthenian",), + "Moved the fixed feasts to the Revised Julian calendar on 1 September " + "2023, keeping the Julian Paschalion. Eparchies outside Ukraine vary; " + "several keep the Gregorian Paschalion."), + ("melkite", "Melkite Greek Catholic Church", "gregorian", "revised_julian", + "ruthenian", (), + "Practice is not uniform: parishes in the Middle East commonly keep the " + "Julian Paschalion for the sake of the local Orthodox."), + ("romanian-catholic", "Romanian Greek Catholic Church", "gregorian", + "revised_julian", "ruthenian", (), ""), +] + +JURISDICTIONS = {} +for _k, _n, _p, _m, _r, _mn, _note in _ORTHODOX + _EASTERN_CATHOLIC: + JURISDICTIONS[_k] = Jurisdiction(_k, _n, _p, _m, _r, _mn, _note) + + +def resolve(name): + """A jurisdiction key, a calendar name, or None for the default.""" + if name is None: + return CALENDARS["new"] + key = name.strip().lower() + if key in JURISDICTIONS: + return JURISDICTIONS[key] + if key in CALENDARS: + return CALENDARS[key] + raise KeyError(name) + + +def names(): + """Everything ``resolve`` accepts, calendars first.""" + return list(CALENDARS) + list(JURISDICTIONS) diff --git a/typikon/menaia.py b/typikon/menaia.py new file mode 100644 index 0000000..9154a51 --- /dev/null +++ b/typikon/menaia.py @@ -0,0 +1,143 @@ +"""Supplementary commemorations, fetched rather than shipped. + +The menologion this library ships is the common Byzantine one, composed from +the tradition and given away (see data/LICENSE). Some churches also keep their +own saints, and those lists are usually published by a body that holds the +copyright in the wording. Redistributing them would be taking someone else's +work; fetching them onto your own machine, for your own use, is not. + +So supplementary sets are **not** in the repository. ``typikon +--update-menaion ruthenian`` fetches one into your data directory, where the +menologion loader picks it up automatically and overlays it on the common set. + +Each source declares where it lives and how to read it. Adding one is a dict. +""" + +import datetime as dt +import json +import os +import re +import urllib.request + +__all__ = ["SOURCES", "update", "target_dir"] + +MONTHS = ("January February March April May June July August " + "September October November December").split() + +SOURCES = { + "ruthenian": { + "title": "Byzantine Catholic Metropolia of Pittsburgh", + "url": "https://mci.archpitt.org/liturgy/Calendar_of_Saints.html", + "note": "Published by the Metropolitan Cantor Institute. Fetched for " + "your own use; not redistributed with this package.", + }, +} + + +def target_dir(): + """Where a fetched set is written. + + The package directory if it is writable — a checkout usually is — and the + user's data directory otherwise, so an installed copy still works. + """ + packaged = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") + if os.access(packaged, os.W_OK): + return packaged + base = os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share") + path = os.path.join(base, "typikon") + os.makedirs(path, exist_ok=True) + return path + + +def _strip_html(html): + txt = re.sub(r"|", "", html, flags=re.S) + txt = re.sub(r"<(br|/p|/div|/li|/h\d)[^>]*>", "\n", txt, flags=re.I) + txt = re.sub(r"<[^>]+>", "\n", txt) + for a, b in ((" ", " "), ("&", "&"), ("’", "'"), + ("’", "'"), ("—", "—")): + txt = txt.replace(a, b) + return [x for x in (re.sub(r"\s+", " ", l).strip() for l in txt.split("\n")) if x] + + +def _parse_mci(html): + """MCI publishes prose, not a table, so entries split on the date headings. + + The page has at least one typo — 14 October is written "October 1" — which + silently overwrites the real entry for that day, taking the Protection of + the Theotokos with it. A date that goes backwards inside a month is the + giveaway, and is corrected to follow the previous one. + """ + lines = _strip_html(html) + idx = {m: i + 1 for i, m in enumerate(MONTHS)} + dated = re.compile(r"^(" + "|".join(MONTHS) + r")\s+(\d{1,2})\s*(.*)$") + heading = re.compile(r"^(" + "|".join(MONTHS) + r")\.?$") + + out, cur, buf, corrections = {}, None, [], [] + prev_month = prev_day = None + + def flush(): + if not (cur and buf): + return + items = [] + for piece in buf: + for part in re.split(r"(?<=\.)\s+(?=[A-Z(])", piece): + part = part.strip(" .;") + if len(part) > 3 and not dated.match(part) and not heading.match(part): + items.append(part) + if items: + out[cur] = {"commemorations": items} + + for line in lines: + if heading.match(line): + continue + m = dated.match(line) + if m: + flush() + month, day = idx[m.group(1)], int(m.group(2)) + if prev_month == month and day <= prev_day: + corrections.append( + f"{month:02d}-{day:02d} -> {month:02d}-{prev_day + 1:02d}") + day = prev_day + 1 + prev_month, prev_day = month, day + cur = f"{month:02d}-{day:02d}" + buf = [m.group(3)] if m.group(3).strip() else [] + elif cur: + buf.append(line) + flush() + return out, corrections + + +_PARSERS = {"ruthenian": _parse_mci} + + +def update(name): + """Fetch a supplementary set and write it beside the packaged menologion.""" + try: + source = SOURCES[name] + except KeyError: + raise KeyError(f"unknown menaion: {name!r}; " + f"try one of {', '.join(SOURCES)}") from None + + req = urllib.request.Request(source["url"], + headers={"User-Agent": "typikon"}) + with urllib.request.urlopen(req, timeout=30) as r: + html = r.read().decode("utf-8", "replace") + + entries, corrections = _PARSERS[name](html) + + expected = set() + for month in range(1, 13): + for day in range(1, 32): + try: + dt.date(2024, month, day) + expected.add(f"{month:02d}-{day:02d}") + except ValueError: + pass + missing = sorted(expected - set(entries)) + + path = os.path.join(target_dir(), f"{name}.json") + with open(path, "w", encoding="utf-8") as fh: + json.dump(entries, fh, ensure_ascii=False, indent=1, sort_keys=True) + + return {"path": path, "days": len(entries), "corrections": corrections, + "missing": missing, "source": source} diff --git a/typikon/menologion.py b/typikon/menologion.py new file mode 100644 index 0000000..86321c3 --- /dev/null +++ b/typikon/menologion.py @@ -0,0 +1,106 @@ +"""The fixed calendar, and the movable feasts that outrank it. + +The menologion ships once and serves every jurisdiction, because it is indexed +by **ecclesiastical** date. A church on the old calendar and a church on the new +both keep the Nativity on 25 December; they disagree about which morning that +is, and that disagreement lives in the calendar conversion, not here. + +Supplementary sets — a local church's own saints — overlay the common one +rather than replacing it, so a jurisdiction can add without forking the file. +""" + +import json +import os + +__all__ = ["Menologion", "load", "MOVABLE_FEASTS", "movable_feast_for"] + +_DATA = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") +_cache = {} + + +class Menologion: + def __init__(self, entries): + self.entries = entries + + def get(self, month, day): + return self.entries.get(f"{month:02d}-{day:02d}", {}) + + def overlay(self, other): + """Add another set on top of this one, keeping both days' entries.""" + merged = {k: dict(v) for k, v in self.entries.items()} + for key, add in other.entries.items(): + if key not in merged: + merged[key] = dict(add) + continue + base = merged[key] + base["commemorations"] = (list(base.get("commemorations", [])) + + list(add.get("commemorations", []))) + # A supplementary set may raise a day's rank but never lower it. + for field in ("rank", "cycle"): + if add.get(field): + base[field] = add[field] + return Menologion(merged) + + +def load(name="menologion"): + """Load a menologion by name from the packaged data directory.""" + if name in _cache: + return _cache[name] + path = os.path.join(_DATA, f"{name}.json") + if not os.path.exists(path): + raise FileNotFoundError(path) + with open(path, encoding="utf-8") as fh: + m = Menologion(json.load(fh)) + _cache[name] = m + return m + + +def for_jurisdiction(jur): + """The common menologion with any supplementary sets overlaid.""" + m = load() + for extra in jur.menaia: + try: + m = m.overlay(load(extra)) + except FileNotFoundError: + # A supplementary set is optional; its absence is not an error. + pass + return m + + +# ── the movable feasts, named by their offset from Pascha +# +# These outrank whatever the fixed calendar has for the day. The keys are +# entries in the movable cycle; the value is (title, rank). +MOVABLE_FEASTS = { + "pascha": ("The Radiant and Life-giving Resurrection of our Lord — Holy Pascha", "great"), + "palm_sunday": ("The Entry of our Lord into Jerusalem", "great"), + "ascension": ("The Ascension of our Lord, God and Saviour Jesus Christ", "great"), + "pentecost": ("Holy Pentecost — the Descent of the Holy Spirit", "great"), + "lazarus_saturday": ("Lazarus Saturday", "vigil"), + "great_thursday": ("Great and Holy Thursday", "vigil"), + "great_friday": ("Great and Holy Friday", "vigil"), + "holy_saturday": ("Great and Holy Saturday", "vigil"), + "thomas_sunday": ("Thomas Sunday — the Antipascha", "polyeleos"), + "myrrhbearers": ("The Sunday of the Holy Myrrh-bearing Women", "polyeleos"), + "midfeast": ("The Midfeast of Pentecost", "polyeleos"), + "all_saints": ("The Sunday of All Saints", "polyeleos"), + "publican_pharisee": ("The Sunday of the Publican and the Pharisee", None), + "prodigal_son": ("The Sunday of the Prodigal Son", None), + "meatfare": ("Meatfare Sunday — the Sunday of the Last Judgement", None), + "cheesefare": ("Cheesefare Sunday — the Expulsion of Adam, Forgiveness Sunday", None), + "orthodoxy_sunday": ("The Sunday of Orthodoxy", "polyeleos"), + "gregory_palamas": ("The Sunday of St Gregory Palamas", None), + "cross_veneration": ("The Sunday of the Veneration of the Cross", "polyeleos"), + "john_climacus": ("The Sunday of St John Climacus", None), + "mary_of_egypt": ("The Sunday of St Mary of Egypt", None), + "fathers_first_council": ("The Sunday of the Fathers of the First Council", None), + "clean_monday": ("Clean Monday — the first day of the Great Fast", None), +} + + +def movable_feast_for(civil, mv): + """(title, rank) if the day is a movable feast, else (None, None).""" + for key, (title, rank) in MOVABLE_FEASTS.items(): + if mv.get(key) == civil: + return title, rank + return None, None diff --git a/typikon/paschalion.py b/typikon/paschalion.py new file mode 100644 index 0000000..141d0c1 --- /dev/null +++ b/typikon/paschalion.py @@ -0,0 +1,110 @@ +"""Pascha, and the movable cycle that hangs off it. + +Two reckonings are in use. The **Julian Paschalion** is what nearly every +Orthodox Church keeps: the old computus, worked in the Julian calendar and then +read off onto a civil date. The **Gregorian Paschalion** is the Western one, +kept by the Finnish Orthodox Church and by most Byzantine Catholic churches. + +Neither is a variant of the other. They are separate tables that happen to +agree in some years — 2025 falls on 20 April by both — and can diverge by as +much as five weeks. +""" + +import datetime as dt + +from .calendars import from_julian + +__all__ = ["julian_pascha", "gregorian_pascha", "pascha", "movable_cycle"] + + +def julian_pascha(year): + """Pascha by the Julian Paschalion, as a civil (Gregorian) date. + + Meeus' Julian algorithm. It yields a date in the Julian calendar, which is + then read onto the civil calendar — the step that is easy to forget and + that puts the answer thirteen days out when it is. + """ + a = year % 4 + b = year % 7 + c = year % 19 + d = (19 * c + 15) % 30 + e = (2 * a + 4 * b - d + 34) % 7 + month = (d + e + 114) // 31 + day = ((d + e + 114) % 31) + 1 + return from_julian(year, month, day) + + +def gregorian_pascha(year): + """Pascha by the Gregorian Paschalion — the Western computus.""" + a = year % 19 + b, c = divmod(year, 100) + d, e = divmod(b, 4) + g = (8 * b + 13) // 25 + h = (19 * a + b - d - g + 15) % 30 + i, k = divmod(c, 4) + ll = (32 + 2 * e + 2 * i - h - k) % 7 + m = (a + 11 * h + 19 * ll) // 433 + month = (h + ll - 7 * m + 90) // 25 + day = (h + ll - 7 * m + 33 * month + 19) % 32 + return dt.date(year, month, day) + + +def pascha(year, paschalion="julian"): + if paschalion == "julian": + return julian_pascha(year) + if paschalion == "gregorian": + return gregorian_pascha(year) + raise ValueError(f"unknown paschalion: {paschalion!r}") + + +def movable_cycle(year, paschalion="julian"): + """Every movable date the calendar needs, keyed off Pascha. + + All of these are counted in real days from Pascha, so they are civil dates + already and need no calendar conversion — the distance from Pascha to + Pentecost is fifty days in every reckoning. + """ + p = pascha(year, paschalion) + d = dt.timedelta + + return { + "pascha": p, + + # ── the Triodion, counted backwards + "publican_pharisee": p - d(70), + "prodigal_son": p - d(63), + "meatfare": p - d(56), # last day meat is eaten + "cheesefare": p - d(49), # Forgiveness Sunday, last day of dairy + "cheesefare_week_start": p - d(55), + "clean_monday": p - d(48), # the Great Fast begins + "orthodoxy_sunday": p - d(42), + "gregory_palamas": p - d(35), + "cross_veneration": p - d(28), + "john_climacus": p - d(21), + "mary_of_egypt": p - d(14), + "great_fast_end": p - d(9), # Friday before Lazarus Saturday + "lazarus_saturday": p - d(8), + "palm_sunday": p - d(7), + "great_week_start": p - d(6), + "great_thursday": p - d(3), + "great_friday": p - d(2), + "holy_saturday": p - d(1), + + # ── the Pentecostarion, counted forwards + "bright_week_end": p + d(6), + "thomas_sunday": p + d(7), + "myrrhbearers": p + d(14), + "midfeast": p + d(24), + "ascension": p + d(39), + "fathers_first_council": p + d(42), + "pentecost": p + d(49), + "pentecost_week_end": p + d(55), + "all_saints": p + d(56), + + # The Apostles' Fast runs from the Monday after All Saints to the eve + # of Ss Peter and Paul. A late Pascha can leave it with no days at all, + # which is correct rather than a bug — the fast is simply not kept that + # year. Its end is a fixed ecclesiastical date, so the jurisdiction + # resolves it; only the start belongs here. + "apostles_start": p + d(57), + } diff --git a/typikon/rules/__init__.py b/typikon/rules/__init__.py new file mode 100644 index 0000000..efcb3da --- /dev/null +++ b/typikon/rules/__init__.py @@ -0,0 +1,19 @@ +"""Fasting disciplines. One module per tradition; ``get`` finds it by name.""" + +from . import orthodox, ruthenian + +_RULES = { + "orthodox": orthodox.RULE, + "ruthenian": ruthenian.RULE, +} + + +def get(name): + try: + return _RULES[name] + except KeyError: + raise KeyError(f"unknown fasting rule: {name!r}") from None + + +def names(): + return list(_RULES) diff --git a/typikon/rules/orthodox.py b/typikon/rules/orthodox.py new file mode 100644 index 0000000..1aa2a4f --- /dev/null +++ b/typikon/rules/orthodox.py @@ -0,0 +1,161 @@ +"""The Typikon's fast, as the Orthodox Church keeps it. + +This is the discipline printed in the Typikon and reproduced, with small +variations, by every Orthodox jurisdiction's published calendar. It binds no +one in the canonical sense — the Orthodox Church legislates fasting as +tradition rather than as precept — so every day here is reported as customary. +That is not a softening. It is the difference between a rule of life and a law, +and the Eastern Catholic churches, which do have law about this, are handled in +their own module. + +Where usages differ, this follows the common published rule and says so in the +reason. The known divergences: + + - **Great Thursday** is given wine and oil here. Some typika keep xerophagy. + - **The Exaltation of the Cross** and the **Beheading of the Forerunner** are + given wine and oil. Several jurisdictions print them as strict. + - **Lazarus Saturday** is given wine and oil; caviar is the traditional + addition, which this vocabulary has no level for. + - **Monastic practice**, and the Holy Mountain in particular, is stricter + than any of this throughout. + +A calendar cannot settle those. Your bishop and your spiritual father can. +""" + +import datetime as dt + +from ..fasting import (Fast, STRICT, XEROPHAGY, WINE_OIL, FISH, DAIRY, NONE, + CUSTOMARY) +from ..seasons import (GREAT_FAST, GREAT_WEEK, BRIGHT_WEEK, CHEESEFARE_WEEK, + APOSTLES, DORMITION, NATIVITY_FAST, PENTECOST_WEEK, + CHRISTMASTIDE) + +MON, TUE, WED, THU, FRI, SAT, SUN = range(7) + +VOCABULARY = { + STRICT: ("no food until evening", "⛔"), + XEROPHAGY: ("xerophagy — no oil or wine", "\U0001f96c"), + WINE_OIL: ("wine and oil permitted", "\U0001fad2"), + FISH: ("fish, wine and oil", "\U0001f41f"), + DAIRY: ("dairy and eggs; no meat", "\U0001f9c0"), + NONE: ("no fast", "\U0001f356"), +} + + +def _relaxed_by_feast(rank): + """How far a feast lifts an ordinary Wednesday or Friday fast.""" + if rank == "great": + return FISH + if rank in ("vigil", "polyeleos"): + return WINE_OIL + return None + + +def fast_for(ctx): + md, wd, mv = ctx.fixed, ctx.weekday, ctx.mv + season = ctx.season + weekend = wd in (SAT, SUN) + + # ── The fast-free stretches, settled before anything else can override + # them. A Friday inside Bright Week is not a fast day, and the ordinary + # Friday rule further down would say otherwise. + if season == BRIGHT_WEEK: + return Fast(NONE, "Bright Week — no fasting", None) + if season == PENTECOST_WEEK: + return Fast(NONE, "the week after Pentecost — no fasting", None) + if season == CHRISTMASTIDE: + return Fast(NONE, "the Nativity to the eve of Theophany", None) + if mv["publican_pharisee"] <= ctx.civil <= mv["publican_pharisee"] + dt.timedelta(6): + # Fast-free precisely because the Pharisee boasted of fasting twice a week. + return Fast(NONE, "fast-free week (the Publican and the Pharisee)", None) + + # ── The Great Fast and Great Week + if season in (GREAT_FAST, GREAT_WEEK): + if ctx.civil == mv["clean_monday"]: + return Fast(STRICT, "the first day of the Great Fast", CUSTOMARY) + if ctx.civil == mv["great_friday"]: + return Fast(STRICT, "Great and Holy Friday", CUSTOMARY) + if ctx.civil == mv["holy_saturday"]: + # The one Saturday of the year kept without oil. + return Fast(XEROPHAGY, "Great and Holy Saturday", CUSTOMARY) + if md == (3, 25): + return Fast(FISH, "the Annunciation", CUSTOMARY) + if ctx.civil == mv["great_thursday"]: + return Fast(WINE_OIL, "Great and Holy Thursday", CUSTOMARY) + if weekend: + return Fast(WINE_OIL, f"Saturday and Sunday of the {season}", CUSTOMARY) + return Fast(XEROPHAGY, f"a weekday of the {season}", CUSTOMARY) + + if ctx.civil == mv["palm_sunday"]: + return Fast(FISH, "Palm Sunday", CUSTOMARY) + if ctx.civil == mv["lazarus_saturday"]: + return Fast(WINE_OIL, "Lazarus Saturday", CUSTOMARY) + + # ── Cheesefare: meat is set aside for good, everything else stays, + # Wednesday and Friday included. + if season == CHEESEFARE_WEEK: + return Fast(DAIRY, "Cheesefare Week", CUSTOMARY) + if ctx.civil == mv["meatfare"]: + return Fast(NONE, "Meatfare Sunday — the last day of meat", None) + + # ── The Nativity Fast, which tightens for its last five days + if season == NATIVITY_FAST: + if md == (12, 24): + return Fast(XEROPHAGY, "the eve of the Nativity", CUSTOMARY) + if md == (11, 21): + return Fast(FISH, "the Entrance of the Theotokos", CUSTOMARY) + if md >= (12, 20): + if wd in (TUE, THU) or weekend: + return Fast(WINE_OIL, "the forefeast of the Nativity", CUSTOMARY) + return Fast(XEROPHAGY, "the forefeast of the Nativity", CUSTOMARY) + if weekend: + return Fast(FISH, "Saturday and Sunday of the Nativity Fast", CUSTOMARY) + if wd in (TUE, THU): + return Fast(WINE_OIL, "Tuesday and Thursday of the Nativity Fast", CUSTOMARY) + return Fast(XEROPHAGY, "Monday, Wednesday and Friday of the Nativity Fast", + CUSTOMARY) + + # ── The Apostles' Fast, kept the same way + if season == APOSTLES: + if weekend: + return Fast(FISH, "Saturday and Sunday of the Apostles' Fast", CUSTOMARY) + if wd in (TUE, THU): + return Fast(WINE_OIL, "Tuesday and Thursday of the Apostles' Fast", CUSTOMARY) + return Fast(XEROPHAGY, "Monday, Wednesday and Friday of the Apostles' Fast", + CUSTOMARY) + + # ── The Dormition Fast, kept like the Great Fast + if season == DORMITION: + if md == (8, 6): + return Fast(FISH, "the Transfiguration", CUSTOMARY) + if weekend: + return Fast(WINE_OIL, "Saturday and Sunday of the Dormition Fast", CUSTOMARY) + return Fast(XEROPHAGY, "a weekday of the Dormition Fast", CUSTOMARY) + + # ── The three fixed fast days outside any season + if md == (1, 5): + return Fast(XEROPHAGY, "the eve of Theophany", CUSTOMARY) + if md == (9, 14): + return Fast(WINE_OIL, "the Exaltation of the Holy Cross", CUSTOMARY) + if md == (8, 29): + return Fast(WINE_OIL, "the Beheading of the Forerunner", CUSTOMARY) + + # ── Ordinary Wednesdays and Fridays, which a feast can lift + if wd in (WED, FRI): + relaxed = _relaxed_by_feast(ctx.rank) + if relaxed: + return Fast(relaxed, f"{ctx.feast} — the fast is relaxed", CUSTOMARY) + return Fast(XEROPHAGY, "Wednesday and Friday", CUSTOMARY) + + return Fast(NONE, "", None) + + +RULE = { + "name": "orthodox", + "title": "the Typikon", + "vocabulary": VOCABULARY, + "fast_for": fast_for, + # The Orthodox Church does not legislate the fast as precept, so nothing + # here is reported as binding. See the module docstring. + "has_obligation": False, +} diff --git a/typikon/rules/ruthenian.py b/typikon/rules/ruthenian.py new file mode 100644 index 0000000..4c96c9f --- /dev/null +++ b/typikon/rules/ruthenian.py @@ -0,0 +1,126 @@ +"""Byzantine Catholic particular law, alongside the tradition it sits on. + +The Eastern Catholic churches differ from the Orthodox here in a way worth +being exact about: they have *law* on fasting, and it requires very much less +than the Typikon describes. Under current Ruthenian particular law the Church +binds only: + + - strict abstinence — no meat, fish, eggs or dairy — on the first day of the + Great Fast and on Great and Holy Friday; + - abstinence from meat on the Wednesdays and Fridays of the Great Fast. + +Everything else — the Apostles', Dormition and Nativity Fasts, the fuller +keeping of the Great Fast, ordinary Fridays through the year — is encouraged +and traditional and does **not** bind. So this module reports two things at +once: the level at which the day is kept by tradition, and the minimum that +actually obliges. A calendar that says "obligatory" where nothing obliges is +worse than no calendar; one that reports only the binding minimum makes the +obligatory days look laxer than the voluntary ones beside them. + +The three degrees this law knows map exactly onto the shared vocabulary: +its strict abstinence is ``wine_oil`` (it does not regulate oil or wine) and +its simple abstinence is ``dairy`` (meat alone is set aside). + +Source: the Byzantine Catholic Metropolitan Church of Pittsburgh, whose +published rule on fasting and abstinence is the basis for the obligations +above. Other Eastern Catholic churches legislate their own; where this library +maps them onto this ruleset it is an approximation, and the jurisdiction entry +says so. +""" + +import datetime as dt + +from ..fasting import (Fast, WINE_OIL, DAIRY, NONE, OBLIGATORY, CUSTOMARY) +from ..seasons import (GREAT_FAST, GREAT_WEEK, BRIGHT_WEEK, CHEESEFARE_WEEK, + APOSTLES, DORMITION, NATIVITY_FAST, PENTECOST_WEEK, + CHRISTMASTIDE) + +MON, TUE, WED, THU, FRI, SAT, SUN = range(7) + +VOCABULARY = { + WINE_OIL: ("no meat, fish, eggs or dairy", "\U0001f96c"), + DAIRY: ("no meat", "\U0001f41f"), + NONE: ("no fast", "\U0001f356"), +} + + +def _off_weekend(day): + """Vigil fasts are kept on the preceding Friday when they land on a weekend. + + "If the vigils of Christmas or Theophany fall on Saturday or Sunday, the + fasting and abstinence is done on the previous Friday." + """ + if day.weekday() == SAT: + return day - dt.timedelta(1) + if day.weekday() == SUN: + return day - dt.timedelta(2) + return day + + +def fast_for(ctx): + md, wd, mv = ctx.fixed, ctx.weekday, ctx.mv + season = ctx.season + + # ── the two days the Church binds strictly + if ctx.civil == mv["clean_monday"]: + return Fast(WINE_OIL, "the first day of the Great Fast", OBLIGATORY, WINE_OIL) + if ctx.civil == mv["great_friday"]: + return Fast(WINE_OIL, "Great and Holy Friday", OBLIGATORY, WINE_OIL) + + # ── and the days it binds to abstinence from meat. Kept strictly by + # tradition; binding only as far as meat. + if season == GREAT_FAST and wd in (WED, FRI): + return Fast(WINE_OIL, "Wednesday and Friday of the Great Fast", + OBLIGATORY, DAIRY) + + # ── fixed vigils and the Exaltation, moved off a weekend where the rule says + for when, label in ( + (ctx.civil_of_fixed(9, 14), "the Exaltation of the Holy Cross"), + (_off_weekend(ctx.civil_of_fixed(12, 24)), "the vigil of the Nativity"), + (_off_weekend(ctx.civil_of_fixed(1, 5)), "the vigil of Theophany"), + ): + if ctx.civil == when: + return Fast(WINE_OIL, label, CUSTOMARY) + + # ── the fast-free stretches, which have to be settled before the ordinary + # Wednesday and Friday rule below or they get overridden by it. Christmas + # Day 2026 is a Friday, and telling you to abstain from meat on it would + # be plainly wrong. + if season == BRIGHT_WEEK: + return Fast(NONE, "Bright Week — no fasting", None) + if season == PENTECOST_WEEK: + return Fast(NONE, "the week after Pentecost — no fasting", None) + if season == CHRISTMASTIDE: + return Fast(NONE, "Christmastide — no fasting", None) + if mv["publican_pharisee"] <= ctx.civil <= mv["publican_pharisee"] + dt.timedelta(6): + return Fast(NONE, "fast-free week (the Publican and the Pharisee)", None) + + # ── the rest of the Great Fast and Great Week + if season in (GREAT_FAST, GREAT_WEEK): + if wd in (SAT, SUN): + return Fast(DAIRY, season, CUSTOMARY) + return Fast(WINE_OIL, season, CUSTOMARY) + + if season == CHEESEFARE_WEEK: + return Fast(DAIRY, "Cheesefare Week", CUSTOMARY) + + # ── the three voluntary seasons + if season in (APOSTLES, DORMITION, NATIVITY_FAST): + return Fast(DAIRY, season, CUSTOMARY) + + # ── a great feast releases the ordinary weekday abstinence + if wd in (WED, FRI): + if ctx.rank == "great": + return Fast(NONE, f"{ctx.feast} — the fast is released", None) + return Fast(DAIRY, "Friday" if wd == FRI else "Wednesday", CUSTOMARY) + + return Fast(NONE, "", None) + + +RULE = { + "name": "ruthenian", + "title": "Ruthenian particular law", + "vocabulary": VOCABULARY, + "fast_for": fast_for, + "has_obligation": True, +} diff --git a/typikon/seasons.py b/typikon/seasons.py new file mode 100644 index 0000000..0cdfc81 --- /dev/null +++ b/typikon/seasons.py @@ -0,0 +1,65 @@ +"""Which season of the year a day falls in. + +The movable seasons are counted from Pascha and so are compared as civil +dates. The fixed ones — the Nativity and Dormition Fasts, the fast-free days +after the Nativity — are compared in **ecclesiastical** space, against the date +the Church calls the day. That is the whole trick that lets one set of rules +serve the old and new calendars at once: the Nativity Fast begins on 15 +November in both, and only the mornings differ. +""" + +from .calendars import fixed_between + +__all__ = ["GREAT_FAST", "GREAT_WEEK", "BRIGHT_WEEK", "CHEESEFARE_WEEK", + "APOSTLES", "DORMITION", "NATIVITY_FAST", "season_for", "FASTING_SEASONS"] + +GREAT_FAST = "Great Fast" +GREAT_WEEK = "Great and Holy Week" +BRIGHT_WEEK = "Bright Week" +PRE_LENTEN = "Triodion" +CHEESEFARE_WEEK = "Cheesefare Week" +APOSTLES = "Apostles' Fast" +DORMITION = "Dormition Fast" +NATIVITY_FAST = "Nativity Fast" +PENTECOST_WEEK = "Week after Pentecost" +CHRISTMASTIDE = "Christmastide" + +FASTING_SEASONS = (GREAT_FAST, GREAT_WEEK, CHEESEFARE_WEEK, + APOSTLES, DORMITION, NATIVITY_FAST) + + +def season_for(civil, fixed_md, mv, apostles_end): + """The season a day belongs to, or None for ordinary time. + + ``fixed_md`` is the ecclesiastical (month, day). ``apostles_end`` is the + civil date of 28 June reckoned in this jurisdiction's menologion — the eve + of Ss Peter and Paul, where the Apostles' Fast stops. + """ + # Movable first: Great Week outranks the Great Fast it follows, and Bright + # Week outranks everything. + if mv["pascha"] <= civil <= mv["bright_week_end"]: + return BRIGHT_WEEK + if mv["great_week_start"] <= civil <= mv["holy_saturday"]: + return GREAT_WEEK + if mv["clean_monday"] <= civil <= mv["great_fast_end"]: + return GREAT_FAST + if mv["cheesefare_week_start"] <= civil <= mv["cheesefare"]: + return CHEESEFARE_WEEK + if mv["publican_pharisee"] <= civil < mv["cheesefare_week_start"]: + return PRE_LENTEN + if mv["pentecost"] <= civil <= mv["pentecost_week_end"]: + return PENTECOST_WEEK + + # The Apostles' Fast can be empty in a year with a late Pascha, which the + # comparison handles without a special case. + if mv["apostles_start"] <= civil <= apostles_end: + return APOSTLES + + # Fixed seasons, in ecclesiastical space. + if fixed_between(fixed_md, (8, 1), (8, 14)): + return DORMITION + if fixed_between(fixed_md, (11, 15), (12, 24)): + return NATIVITY_FAST + if fixed_between(fixed_md, (12, 25), (1, 4)): + return CHRISTMASTIDE + return None