parch 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. parch/__init__.py +17 -0
  2. parch/__main__.py +4 -0
  3. parch/calendar/__init__.py +39 -0
  4. parch/calendar/dated_note.py +34 -0
  5. parch/calendar/day.py +181 -0
  6. parch/calendar/month.py +83 -0
  7. parch/calendar/quarter.py +67 -0
  8. parch/calendar/week.py +71 -0
  9. parch/cli.py +285 -0
  10. parch/config.py +125 -0
  11. parch/data/__init__.py +1 -0
  12. parch/data/configs/158x210-mos-left-lined.toml +103 -0
  13. parch/data/configs/158x210-mos-left.toml +99 -0
  14. parch/data/configs/158x210-mos-right.toml +102 -0
  15. parch/data/configs/kindle-scribe.toml +97 -0
  16. parch/data/configs/supernote-nomad-mos-right.toml +116 -0
  17. parch/data/configs/supernote-nomad.toml +113 -0
  18. parch/data/locales/en.toml +84 -0
  19. parch/devices.py +76 -0
  20. parch/i18n.py +83 -0
  21. parch/models/__init__.py +22 -0
  22. parch/models/base.py +35 -0
  23. parch/models/device.py +400 -0
  24. parch/models/locale.py +94 -0
  25. parch/mos/__init__.py +19 -0
  26. parch/mos/builder.py +120 -0
  27. parch/mos/components/__init__.py +15 -0
  28. parch/mos/components/daily_notes.py +49 -0
  29. parch/mos/components/daily_priorities.py +27 -0
  30. parch/mos/components/daily_schedule.py +58 -0
  31. parch/mos/components/little_calendar.py +138 -0
  32. parch/mos/components/months_menu.py +53 -0
  33. parch/mos/components/quarters_menu.py +43 -0
  34. parch/mos/configurator.py +74 -0
  35. parch/mos/contents_mark.py +107 -0
  36. parch/mos/coordinator.py +94 -0
  37. parch/mos/manifest.py +37 -0
  38. parch/mos/navigation.py +161 -0
  39. parch/mos/page_data.py +24 -0
  40. parch/mos/pages/__init__.py +6 -0
  41. parch/mos/pages/daily.py +112 -0
  42. parch/mos/pages/monthly.py +126 -0
  43. parch/mos/pages/quarterly.py +69 -0
  44. parch/mos/pages/weekly.py +60 -0
  45. parch/mos/preamble.py +129 -0
  46. parch/mos/sections/__init__.py +29 -0
  47. parch/mos/sections/_shared.py +33 -0
  48. parch/mos/sections/annual.py +85 -0
  49. parch/mos/sections/cover_plain.py +50 -0
  50. parch/mos/sections/daily.py +58 -0
  51. parch/mos/sections/daily_notes.py +108 -0
  52. parch/mos/sections/habits.py +221 -0
  53. parch/mos/sections/index.py +126 -0
  54. parch/mos/sections/meetings.py +216 -0
  55. parch/mos/sections/monthly.py +77 -0
  56. parch/mos/sections/projects.py +225 -0
  57. parch/mos/sections/quarterly.py +86 -0
  58. parch/mos/sections/review.py +274 -0
  59. parch/mos/sections/tasks.py +272 -0
  60. parch/mos/sections/weekly.py +86 -0
  61. parch/provenance.py +209 -0
  62. parch/sections/__init__.py +3 -0
  63. parch/sections/colophon.py +333 -0
  64. parch/services/__init__.py +4 -0
  65. parch/services/compile.py +314 -0
  66. parch/services/config_file.py +356 -0
  67. parch/services/generate.py +25 -0
  68. parch/services/preview_svg.py +244 -0
  69. parch/toml_config.py +545 -0
  70. parch-0.1.0.dist-info/METADATA +199 -0
  71. parch-0.1.0.dist-info/RECORD +74 -0
  72. parch-0.1.0.dist-info/WHEEL +4 -0
  73. parch-0.1.0.dist-info/entry_points.txt +2 -0
  74. parch-0.1.0.dist-info/licenses/LICENSE +25 -0
parch/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """parch: Python port of kudrykv/LYP yearly e-ink planner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
6
+
7
+
8
+ class Error(Exception):
9
+ """Base package error."""
10
+
11
+
12
+ class InternalError(Error):
13
+ """Programming / invariant error."""
14
+
15
+
16
+ class ConfigError(Error):
17
+ """Invalid or incomplete planner configuration."""
parch/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from parch.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,39 @@
1
+ """Calendar entities (Day / Week / Month / Quarter) with Ruby-style succ ranges."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from typing import Protocol, TypeVar
7
+
8
+ from parch.calendar.day import WEEKDAYS, Day
9
+ from parch.calendar.month import Month
10
+ from parch.calendar.quarter import Quarter
11
+ from parch.calendar.week import Week
12
+ from parch.calendar.dated_note import DatedNote
13
+
14
+ T = TypeVar("T", bound="ComparableSucc")
15
+
16
+
17
+ class ComparableSucc(Protocol):
18
+ def succ(self) -> T: ... # type: ignore[misc]
19
+
20
+ def __le__(self, other: object) -> bool: ...
21
+
22
+
23
+ def walk(start: T, end: T) -> Iterator[T]:
24
+ """Inclusive range ``start..end`` using ``succ``, like Ruby."""
25
+ current = start
26
+ while current <= end:
27
+ yield current
28
+ current = current.succ() # type: ignore[assignment]
29
+
30
+
31
+ __all__ = [
32
+ "WEEKDAYS",
33
+ "Day",
34
+ "Week",
35
+ "Month",
36
+ "Quarter",
37
+ "DatedNote",
38
+ "walk",
39
+ ]
@@ -0,0 +1,34 @@
1
+ """Dated extra-note page — port of LYP::Entities::DatedNote."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from parch.calendar.day import Day
9
+
10
+
11
+ class DatedNote:
12
+ def __init__(self, weekday_start: str, day: Day, page: int = 1) -> None:
13
+ self.weekday_start = weekday_start
14
+ self.day = day
15
+ self.page = page
16
+
17
+ @property
18
+ def id(self) -> str:
19
+ return f"daily-note-{self.day.strftime('%Y-%m-%d')}-page-{self.page}"
20
+
21
+ def __eq__(self, other: object) -> bool:
22
+ if not isinstance(other, DatedNote):
23
+ return False
24
+ return (
25
+ self.weekday_start == other.weekday_start
26
+ and self.day == other.day
27
+ and self.page == other.page
28
+ )
29
+
30
+ def __hash__(self) -> int:
31
+ return hash((self.weekday_start, self.day, self.page))
32
+
33
+ def __repr__(self) -> str:
34
+ return f"DatedNote({self.id})"
parch/calendar/day.py ADDED
@@ -0,0 +1,181 @@
1
+ """Day entity — port of LYP::Entities::Calendar::Day."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import calendar as pycal
6
+ from datetime import date, timedelta
7
+ from typing import TYPE_CHECKING
8
+
9
+ from parch import InternalError
10
+
11
+ if TYPE_CHECKING:
12
+ from parch.calendar.month import Month
13
+ from parch.calendar.quarter import Quarter
14
+ from parch.calendar.week import Week
15
+
16
+ WEEKDAYS = (
17
+ "monday",
18
+ "tuesday",
19
+ "wednesday",
20
+ "thursday",
21
+ "friday",
22
+ "saturday",
23
+ "sunday",
24
+ )
25
+
26
+ _WEEKDAY_INDEX = {name: i for i, name in enumerate(WEEKDAYS)}
27
+ _MONTH_NAMES = (
28
+ "january",
29
+ "february",
30
+ "march",
31
+ "april",
32
+ "may",
33
+ "june",
34
+ "july",
35
+ "august",
36
+ "september",
37
+ "october",
38
+ "november",
39
+ "december",
40
+ )
41
+
42
+
43
+ def normalize_weekday(value: str) -> str:
44
+ name = str(value).strip().lower()
45
+ if name not in _WEEKDAY_INDEX:
46
+ raise ValueError(f"Bad weekday {value!r}, allowed values are {list(WEEKDAYS)}")
47
+ return name
48
+
49
+
50
+ def add_months(d: date, months: int) -> date:
51
+ """ActiveSupport-style month arithmetic (clamp to last day of target month)."""
52
+ month_index = d.month - 1 + months
53
+ year = d.year + month_index // 12
54
+ month = month_index % 12 + 1
55
+ last = pycal.monthrange(year, month)[1]
56
+ return date(year, month, min(d.day, last))
57
+
58
+
59
+ class Day:
60
+ def __init__(self, weekday_start: str, day: date) -> None:
61
+ if type(day) is not date:
62
+ raise TypeError("day must be datetime.date")
63
+ self.weekday_start = normalize_weekday(weekday_start)
64
+ self.day = day
65
+
66
+ @property
67
+ def year(self) -> int:
68
+ return self.day.year
69
+
70
+ def strftime(self, fmt: str) -> str:
71
+ # Ruby %e is space-padded day-of-month; Python's %e is not portable.
72
+ if "%e" in fmt:
73
+ fmt = fmt.replace("%e", f"{self.day.day:2d}")
74
+ if "%k" in fmt:
75
+ # not used on Day, but keep for completeness
76
+ pass
77
+ return self.day.strftime(fmt)
78
+
79
+ @property
80
+ def id(self) -> str:
81
+ return self.day.strftime("%Y-%m-%d")
82
+
83
+ def beginning_of_month(self) -> Day:
84
+ return Day(weekday_start=self.weekday_start, day=self.day.replace(day=1))
85
+
86
+ def end_of_month(self) -> Day:
87
+ last = pycal.monthrange(self.day.year, self.day.month)[1]
88
+ return Day(weekday_start=self.weekday_start, day=self.day.replace(day=last))
89
+
90
+ def next_month(self) -> Day:
91
+ return Day(weekday_start=self.weekday_start, day=add_months(self.day, 1))
92
+
93
+ def month(self) -> Month:
94
+ from parch.calendar.month import Month
95
+
96
+ return Month(weekday_start=self.weekday_start, day=self)
97
+
98
+ def quarter(self) -> Quarter:
99
+ from parch.calendar.quarter import Quarter
100
+
101
+ return Quarter(weekday_start=self.weekday_start, day=self)
102
+
103
+ def week(self) -> Week:
104
+ from parch.calendar.week import Week
105
+
106
+ return Week(weekday_start=self.weekday_start, day=self.beginning_of_week())
107
+
108
+ @property
109
+ def quarter_number(self) -> int:
110
+ return (self.day.month - 1) // 3 + 1
111
+
112
+ def next_quarter(self) -> Day:
113
+ return Day(weekday_start=self.weekday_start, day=add_months(self.day, 3))
114
+
115
+ def beginning_of_week(self) -> Day:
116
+ start = _WEEKDAY_INDEX[self.weekday_start]
117
+ delta = (self.day.weekday() - start) % 7
118
+ return Day(weekday_start=self.weekday_start, day=self.day - timedelta(days=delta))
119
+
120
+ def end_of_week(self) -> Day:
121
+ return self.beginning_of_week() + 6
122
+
123
+ @property
124
+ def month_day(self) -> int:
125
+ return self.day.day
126
+
127
+ @property
128
+ def weekday_name(self) -> str:
129
+ return WEEKDAYS[self.day.weekday()]
130
+
131
+ def succ(self) -> Day:
132
+ return Day(weekday_start=self.weekday_start, day=self.day + timedelta(days=1))
133
+
134
+ def beginning_of_quarter(self) -> Day:
135
+ start_month = (self.quarter_number - 1) * 3 + 1
136
+ return Day(weekday_start=self.weekday_start, day=date(self.day.year, start_month, 1))
137
+
138
+ def __add__(self, other: object) -> Day:
139
+ if not isinstance(other, int):
140
+ return NotImplemented
141
+ return Day(weekday_start=self.weekday_start, day=self.day + timedelta(days=other))
142
+
143
+ def __radd__(self, other: object) -> Day:
144
+ return self.__add__(other)
145
+
146
+ def _require_same(self, other: object, kind: str) -> Day:
147
+ if not isinstance(other, Day):
148
+ raise TypeError(f"must be {kind}")
149
+ if other.weekday_start != self.weekday_start:
150
+ raise ValueError("weekday start must match")
151
+ return other
152
+
153
+ def __lt__(self, other: object) -> bool:
154
+ other_day = self._require_same(other, "Day")
155
+ return self.day < other_day.day
156
+
157
+ def __le__(self, other: object) -> bool:
158
+ other_day = self._require_same(other, "Day")
159
+ return self.day <= other_day.day
160
+
161
+ def __gt__(self, other: object) -> bool:
162
+ other_day = self._require_same(other, "Day")
163
+ return self.day > other_day.day
164
+
165
+ def __ge__(self, other: object) -> bool:
166
+ other_day = self._require_same(other, "Day")
167
+ return self.day >= other_day.day
168
+
169
+ def __eq__(self, other: object) -> bool:
170
+ if not isinstance(other, Day):
171
+ return False
172
+ return self.weekday_start == other.weekday_start and self.day == other.day
173
+
174
+ def __hash__(self) -> int:
175
+ return hash((self.weekday_start, self.day))
176
+
177
+ def __repr__(self) -> str:
178
+ return f"Day({self.day} wd={self.weekday_start})"
179
+
180
+ def __str__(self) -> str:
181
+ return f"{self.day} (wd: {self.weekday_start}, {self.weekday_name})"
@@ -0,0 +1,83 @@
1
+ """Month entity — port of LYP::Entities::Calendar::Month."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from parch import InternalError
8
+
9
+ if TYPE_CHECKING:
10
+ from parch.calendar.day import Day
11
+ from parch.calendar.quarter import Quarter
12
+
13
+ _MONTH_NAMES = (
14
+ "january",
15
+ "february",
16
+ "march",
17
+ "april",
18
+ "may",
19
+ "june",
20
+ "july",
21
+ "august",
22
+ "september",
23
+ "october",
24
+ "november",
25
+ "december",
26
+ )
27
+
28
+
29
+ class Month:
30
+ def __init__(self, weekday_start: str, day: Day) -> None:
31
+ from parch.calendar.day import Day as DayCls
32
+
33
+ if not isinstance(day, DayCls):
34
+ raise InternalError("Month.day must be a Day")
35
+ self.weekday_start = weekday_start
36
+ self.day = day.beginning_of_month()
37
+
38
+ @property
39
+ def id(self) -> str:
40
+ return f"month-{self.day.id}"
41
+
42
+ @property
43
+ def name(self) -> str:
44
+ return _MONTH_NAMES[self.day.day.month - 1]
45
+
46
+ def succ(self) -> Month:
47
+ return Month(weekday_start=self.weekday_start, day=self.day.next_month())
48
+
49
+ def quarter(self) -> Quarter:
50
+ return self.day.quarter()
51
+
52
+ def _require_same(self, other: object) -> Month:
53
+ if not isinstance(other, Month):
54
+ raise TypeError("must be Month")
55
+ if self.weekday_start != other.weekday_start:
56
+ raise ValueError("weekday start must match")
57
+ return other
58
+
59
+ def __lt__(self, other: object) -> bool:
60
+ return self.day < self._require_same(other).day
61
+
62
+ def __le__(self, other: object) -> bool:
63
+ return self.day <= self._require_same(other).day
64
+
65
+ def __gt__(self, other: object) -> bool:
66
+ return self.day > self._require_same(other).day
67
+
68
+ def __ge__(self, other: object) -> bool:
69
+ return self.day >= self._require_same(other).day
70
+
71
+ def __eq__(self, other: object) -> bool:
72
+ if not isinstance(other, Month):
73
+ return False
74
+ return self.weekday_start == other.weekday_start and self.day == other.day
75
+
76
+ def __hash__(self) -> int:
77
+ return hash((self.weekday_start, self.day))
78
+
79
+ def __repr__(self) -> str:
80
+ return f"Month({self.day.strftime('%Y-%m')} wd={self.weekday_start})"
81
+
82
+ def __str__(self) -> str:
83
+ return f"{self.day.strftime('%Y, %B')} (wd: {self.weekday_start})"
@@ -0,0 +1,67 @@
1
+ """Quarter entity — port of LYP::Entities::Calendar::Quarter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from parch.calendar.day import Day
9
+ from parch.calendar.month import Month
10
+
11
+
12
+ class Quarter:
13
+ def __init__(self, weekday_start: str, day: Day) -> None:
14
+ self.weekday_start = weekday_start
15
+ self.day = day.beginning_of_quarter()
16
+
17
+ @property
18
+ def id(self) -> str:
19
+ return f"quarter-{self.day.year}-{self.number}"
20
+
21
+ @property
22
+ def number(self) -> int:
23
+ return self.day.quarter_number
24
+
25
+ def months(self) -> list[Month]:
26
+ from parch.calendar.month import Month
27
+
28
+ first = Month(weekday_start=self.weekday_start, day=self.day)
29
+ second = Month(weekday_start=self.weekday_start, day=self.day.next_month())
30
+ third = Month(weekday_start=self.weekday_start, day=self.day.next_month().next_month())
31
+ return [first, second, third]
32
+
33
+ def succ(self) -> Quarter:
34
+ return Quarter(weekday_start=self.weekday_start, day=self.day.next_quarter())
35
+
36
+ def _require_same(self, other: object) -> Quarter:
37
+ if not isinstance(other, Quarter):
38
+ raise TypeError("must be Quarter")
39
+ if other.weekday_start != self.weekday_start:
40
+ raise ValueError("weekday start must match")
41
+ return other
42
+
43
+ def __lt__(self, other: object) -> bool:
44
+ return self.day < self._require_same(other).day
45
+
46
+ def __le__(self, other: object) -> bool:
47
+ return self.day <= self._require_same(other).day
48
+
49
+ def __gt__(self, other: object) -> bool:
50
+ return self.day > self._require_same(other).day
51
+
52
+ def __ge__(self, other: object) -> bool:
53
+ return self.day >= self._require_same(other).day
54
+
55
+ def __eq__(self, other: object) -> bool:
56
+ if not isinstance(other, Quarter):
57
+ return False
58
+ return self.weekday_start == other.weekday_start and self.day == other.day
59
+
60
+ def __hash__(self) -> int:
61
+ return hash((self.weekday_start, self.day))
62
+
63
+ def __repr__(self) -> str:
64
+ return f"Quarter({self.id} wd={self.weekday_start})"
65
+
66
+ def __str__(self) -> str:
67
+ return f"Q{self.number} (wd: {self.weekday_start})"
parch/calendar/week.py ADDED
@@ -0,0 +1,71 @@
1
+ """Week entity — port of LYP::Entities::Calendar::Week."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from parch import InternalError
8
+
9
+ if TYPE_CHECKING:
10
+ from parch.calendar.day import Day
11
+ from parch.calendar.month import Month
12
+ from parch.calendar.quarter import Quarter
13
+
14
+
15
+ class Week:
16
+ def __init__(self, weekday_start: str, day: Day) -> None:
17
+ from parch.calendar.day import Day as DayCls
18
+
19
+ if not isinstance(day, DayCls):
20
+ raise InternalError("Week.day must be a Day")
21
+ self.weekday_start = weekday_start
22
+ self.day = day.beginning_of_week()
23
+
24
+ @property
25
+ def id(self) -> str:
26
+ return self._iso_label()
27
+
28
+ def _iso_label(self) -> str:
29
+ iso = self.day.day.isocalendar()
30
+ return f"{iso.year}W{iso.week:02d}"
31
+
32
+ @property
33
+ def number(self) -> int:
34
+ return self.day.day.isocalendar().week
35
+
36
+ def days(self) -> list[Day]:
37
+ return [self.day + i for i in range(7)]
38
+
39
+ def in_months(self) -> list[Month]:
40
+ first = self.days()[0]
41
+ last = self.days()[-1]
42
+ months = [first.month(), last.month()]
43
+ out: list[Month] = []
44
+ for month in months:
45
+ if month not in out:
46
+ out.append(month)
47
+ return out
48
+
49
+ def in_quarters(self) -> list[Quarter]:
50
+ first = self.days()[0]
51
+ last = self.days()[-1]
52
+ quarters = [first.quarter(), last.quarter()]
53
+ out: list[Quarter] = []
54
+ for quarter in quarters:
55
+ if quarter not in out:
56
+ out.append(quarter)
57
+ return out
58
+
59
+ def __eq__(self, other: object) -> bool:
60
+ if not isinstance(other, Week):
61
+ return False
62
+ return self.weekday_start == other.weekday_start and self.day == other.day
63
+
64
+ def __hash__(self) -> int:
65
+ return hash((self.weekday_start, self.day))
66
+
67
+ def __repr__(self) -> str:
68
+ return f"Week({self.id} wd={self.weekday_start})"
69
+
70
+ def __str__(self) -> str:
71
+ return self.id