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.
- parch/__init__.py +17 -0
- parch/__main__.py +4 -0
- parch/calendar/__init__.py +39 -0
- parch/calendar/dated_note.py +34 -0
- parch/calendar/day.py +181 -0
- parch/calendar/month.py +83 -0
- parch/calendar/quarter.py +67 -0
- parch/calendar/week.py +71 -0
- parch/cli.py +285 -0
- parch/config.py +125 -0
- parch/data/__init__.py +1 -0
- parch/data/configs/158x210-mos-left-lined.toml +103 -0
- parch/data/configs/158x210-mos-left.toml +99 -0
- parch/data/configs/158x210-mos-right.toml +102 -0
- parch/data/configs/kindle-scribe.toml +97 -0
- parch/data/configs/supernote-nomad-mos-right.toml +116 -0
- parch/data/configs/supernote-nomad.toml +113 -0
- parch/data/locales/en.toml +84 -0
- parch/devices.py +76 -0
- parch/i18n.py +83 -0
- parch/models/__init__.py +22 -0
- parch/models/base.py +35 -0
- parch/models/device.py +400 -0
- parch/models/locale.py +94 -0
- parch/mos/__init__.py +19 -0
- parch/mos/builder.py +120 -0
- parch/mos/components/__init__.py +15 -0
- parch/mos/components/daily_notes.py +49 -0
- parch/mos/components/daily_priorities.py +27 -0
- parch/mos/components/daily_schedule.py +58 -0
- parch/mos/components/little_calendar.py +138 -0
- parch/mos/components/months_menu.py +53 -0
- parch/mos/components/quarters_menu.py +43 -0
- parch/mos/configurator.py +74 -0
- parch/mos/contents_mark.py +107 -0
- parch/mos/coordinator.py +94 -0
- parch/mos/manifest.py +37 -0
- parch/mos/navigation.py +161 -0
- parch/mos/page_data.py +24 -0
- parch/mos/pages/__init__.py +6 -0
- parch/mos/pages/daily.py +112 -0
- parch/mos/pages/monthly.py +126 -0
- parch/mos/pages/quarterly.py +69 -0
- parch/mos/pages/weekly.py +60 -0
- parch/mos/preamble.py +129 -0
- parch/mos/sections/__init__.py +29 -0
- parch/mos/sections/_shared.py +33 -0
- parch/mos/sections/annual.py +85 -0
- parch/mos/sections/cover_plain.py +50 -0
- parch/mos/sections/daily.py +58 -0
- parch/mos/sections/daily_notes.py +108 -0
- parch/mos/sections/habits.py +221 -0
- parch/mos/sections/index.py +126 -0
- parch/mos/sections/meetings.py +216 -0
- parch/mos/sections/monthly.py +77 -0
- parch/mos/sections/projects.py +225 -0
- parch/mos/sections/quarterly.py +86 -0
- parch/mos/sections/review.py +274 -0
- parch/mos/sections/tasks.py +272 -0
- parch/mos/sections/weekly.py +86 -0
- parch/provenance.py +209 -0
- parch/sections/__init__.py +3 -0
- parch/sections/colophon.py +333 -0
- parch/services/__init__.py +4 -0
- parch/services/compile.py +314 -0
- parch/services/config_file.py +356 -0
- parch/services/generate.py +25 -0
- parch/services/preview_svg.py +244 -0
- parch/toml_config.py +545 -0
- parch-0.1.0.dist-info/METADATA +199 -0
- parch-0.1.0.dist-info/RECORD +74 -0
- parch-0.1.0.dist-info/WHEEL +4 -0
- parch-0.1.0.dist-info/entry_points.txt +2 -0
- 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,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})"
|
parch/calendar/month.py
ADDED
|
@@ -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
|