readingplan.py in cbr-schedule

at master

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#
24import json
25import os
26
27DAYS_OF_WEEK = (
28 "Sunday",
29 "Monday",
30 "Tuesday",
31 "Wednesday",
32 "Thursday",
33 "Friday",
34 "Saturday",
35 )
36
37MONTHS = (
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
52class 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
65class 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