1 | # |
2 | # The MIT License (MIT) |
3 | # |
4 | # Copyright (c) 2018 Daniel Moch |
5 | # |
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy |
7 | # of this software and associated documentation files (the "Software"), to deal |
8 | # in the Software without restriction, including without limitation the rights |
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
10 | # copies of the Software, and to permit persons to whom the Software is |
11 | # furnished to do so, subject to the following conditions: |
12 | # |
13 | # The above copyright notice and this permission notice shall be included in all |
14 | # copies or substantial portions of the Software. |
15 | # |
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
22 | # SOFTWARE. |
23 | # |
24 | import json |
25 | import os |
26 | |
27 | DAYS_OF_WEEK = ( |
28 | "Sunday", |
29 | "Monday", |
30 | "Tuesday", |
31 | "Wednesday", |
32 | "Thursday", |
33 | "Friday", |
34 | "Saturday", |
35 | ) |
36 | |
37 | MONTHS = ( |
38 | ('January', 31,), |
39 | ('February', 28,), |
40 | ('March', 31,), |
41 | ('April', 30,), |
42 | ('May', 31,), |
43 | ('June', 30,), |
44 | ('July', 31,), |
45 | ('August', 31,), |
46 | ('September', 30,), |
47 | ('October', 31,), |
48 | ('November', 30,), |
49 | ('December', 31,), |
50 | ) |
51 | |
52 | class ReadingPlanError(Exception): |
53 | """ |
54 | Exception thrown by the ReadingPlan class |
55 | |
56 | Attributes: |
57 | message - The error message |
58 | """ |
59 | message = None |
60 | |
61 | def __init__(self, message): |
62 | super().__init__() |
63 | self.message = message |
64 | |
65 | class ReadingPlan: |
66 | """ |
67 | Once initialized, maintain state of the plan so the schedule |
68 | function can simply ask for the next day's reading. |
69 | """ |
70 | _initialized = False |
71 | _current_month = 0 |
72 | _current_day = 1 |
73 | _day_of_week = 0 |
74 | |
75 | _sections = {} |
76 | _days_sections = {} |
77 | _days_sections_fallback = {} |
78 | _reading_dictionaries = {} |
79 | _reading_indices = {} |
80 | done = False |
81 | |
82 | @staticmethod |
83 | def _print_dbg(method, message): |
84 | if 'READINGPLAN_DEBUG' in os.environ.keys(): |
85 | print('ReadingPlan.' + method + ': ' + message) |
86 | |
87 | def _general_init(self, config): |
88 | jan1_day_of_week = config['general']['jan1_day_of_week'] |
89 | for idx, day in enumerate(DAYS_OF_WEEK): |
90 | if day.upper() == jan1_day_of_week.upper(): |
91 | self._day_of_week = idx |
92 | break |
93 | |
94 | def _days_sections_init(self, config, section): |
95 | for day in DAYS_OF_WEEK: |
96 | day_key = day.lower() |
97 | if day_key in config[section].keys(): |
98 | self._days_sections[day] = config[section][day_key] |
99 | else: |
100 | self._print_dbg('_days_sections_init', 'No readings ' \ |
101 | + 'planned for ' + day + 's') |
102 | self._days_sections[day] = None |
103 | |
104 | def _days_sections_fallback_init(self, config, section): |
105 | for day in DAYS_OF_WEEK: |
106 | day_key = day.lower() |
107 | if day_key in config[section].keys(): |
108 | self._days_sections_fallback[day] = \ |
109 | config[section][day_key] |
110 | else: |
111 | self._print_dbg('_days_sections_fallback_init', 'No ' \ |
112 | + 'fallback readings planned for ' + day + 's') |
113 | self._days_sections_fallback[day] = None |
114 | |
115 | def _section_init(self, config, section): |
116 | if 'name' in config[section].keys(): |
117 | self._sections[section] = config[section]['name'] |
118 | else: |
119 | self._print_dbg('_section_init', 'No section name ' + \ |
120 | 'specified for ' + section) |
121 | self._print_dbg('_section_init', 'Using ' + section + \ |
122 | ' as name') |
123 | self._sections[section] = section |
124 | reading_dictionary = [] |
125 | try: |
126 | with open('sections/' + section + '.json', 'r') as sfile: |
127 | reading_dictionary = json.load(sfile) |
128 | self._reading_dictionaries[section] = reading_dictionary |
129 | except FileNotFoundError as error: |
130 | raise ReadingPlanError(section + ' provided in ' + \ |
131 | 'configuration file, but ' + error.filename + \ |
132 | ' does not exist') |
133 | if 'book' in config[section].keys(): |
134 | book = config[section]['book'] |
135 | if 'chapter' in config[section].keys(): |
136 | chapter = config[section]['chapter'] |
137 | else: |
138 | chapter = '1' |
139 | for dictionary in reading_dictionary: |
140 | if dictionary['book'] == book and \ |
141 | dictionary['reading'] == chapter: |
142 | self._reading_indices[section] = \ |
143 | dictionary['index'] |
144 | if section not in self._reading_indices.keys(): |
145 | self._reading_indices[section] = 0 |
146 | |
147 | def _print_plan_info(self): |
148 | message = 'Sections configured: ' |
149 | for section in self._sections.values(): |
150 | message += section + ', ' |
151 | message = message[0:-2] |
152 | print(message) |
153 | print('Jan. 1 configured to ' + DAYS_OF_WEEK[self._day_of_week]) |
154 | print('Configured daily readings:') |
155 | for day in self._days_sections.keys(): |
156 | if self._days_sections[day] is None: |
157 | print('\t' + day + ': No reading') |
158 | else: |
159 | message = '\t' + day + ': ' |
160 | for section in self._days_sections[day].split(','): |
161 | message += self._sections[section] + ', ' |
162 | message = message[0:-2] |
163 | print(message) |
164 | print('Configured fallback readings:') |
165 | for day in self._days_sections_fallback.keys(): |
166 | if self._days_sections_fallback[day] is None: |
167 | print('\t' + day + ': No reading') |
168 | else: |
169 | message = '\t' + day + ': ' |
170 | for section in self._days_sections_fallback[day].split(','): |
171 | message += self._sections[section] + ', ' |
172 | message = message[0:-2] |
173 | print(message) |
174 | |
175 | def build_plan(self, config): |
176 | for section in config.sections(): |
177 | if section == 'general': |
178 | self._general_init(config) |
179 | elif section == 'days_sections': |
180 | self._days_sections_init(config, section) |
181 | elif section == 'days_sections_fallback': |
182 | self._days_sections_fallback_init(config, section) |
183 | else: |
184 | self._section_init(config, section) |
185 | self._print_plan_info() |
186 | self._initialized = True |
187 | |
188 | def _next_day(self): |
189 | if self._current_day == MONTHS[self._current_month][1]: |
190 | self._current_day = 1 |
191 | self._current_month += 1 |
192 | if self._current_month == len(MONTHS): |
193 | self.done = True |
194 | else: |
195 | self._current_day += 1 |
196 | |
197 | last_day = len(DAYS_OF_WEEK) - 1 |
198 | if self._day_of_week == last_day: |
199 | self._day_of_week = 0 |
200 | else: |
201 | self._day_of_week += 1 |
202 | |
203 | def _readings(self, sections): |
204 | readings = [] |
205 | if sections is not None: |
206 | for section in sections.split(','): |
207 | if section not in self._sections.keys(): |
208 | raise ValueError(section + ' is not a known section') |
209 | |
210 | section_dict = self._reading_dictionaries[section] |
211 | for reading_dict in section_dict: |
212 | if reading_dict['index'] == self._reading_indices[section]: |
213 | reading = {} |
214 | reading['section'] = self._sections[section] |
215 | reading['book'] = reading_dict['book'] |
216 | reading['reading'] = reading_dict['reading'] |
217 | readings.append(reading) |
218 | self._reading_indices[section] += 1 |
219 | break |
220 | return readings |
221 | |
222 | def next_reading(self): |
223 | if not self._initialized: |
224 | raise RuntimeError('Attempting to use an uninitialized ReadingPlan') |
225 | results = {} |
226 | readings = [] |
227 | today_sections = self._days_sections[DAYS_OF_WEEK[self._day_of_week]] |
228 | today_sections_fallback = \ |
229 | self._days_sections_fallback[DAYS_OF_WEEK[self._day_of_week]] |
230 | for reading in self._readings(today_sections): |
231 | readings.append(reading) |
232 | |
233 | if readings == []: |
234 | for reading in self._readings(today_sections_fallback): |
235 | readings.append(reading) |
236 | results = { |
237 | 'day_of_week': DAYS_OF_WEEK[self._day_of_week], |
238 | 'day': self._current_day, |
239 | 'month': MONTHS[self._current_month][0], |
240 | 'readings': readings |
241 | } |
242 | self._next_day() |
243 | return results |