| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  | #!/usr/bin/python3 | 
					
						
							|  |  |  | # -*- coding: utf-8 -*- | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | iCalendar (parsing) module for Inky-Calendar Project | 
					
						
							|  |  |  | Copyright by aceisace | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-23 01:45:40 +02:00
										 |  |  | """               ---info about iCalendars---
 | 
					
						
							| 
									
										
										
										
											2020-05-19 03:17:26 +02:00
										 |  |  | • all day events start at midnight, ending at midnight of the next day | 
					
						
							|  |  |  | • iCalendar saves all event timings in UTC -> need to be converted into local | 
					
						
							|  |  |  |   time | 
					
						
							|  |  |  | • Only non-all_day events or multi-day need to be converted to | 
					
						
							|  |  |  |   local timezone. Converting all-day events to local timezone is a problem! | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  | import arrow | 
					
						
							|  |  |  | from urllib.request import urlopen | 
					
						
							|  |  |  | import logging | 
					
						
							| 
									
										
										
										
											2020-05-26 19:10:20 +02:00
										 |  |  | import time | 
					
						
							| 
									
										
										
										
											2020-05-23 01:45:40 +02:00
										 |  |  | import os | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | try: | 
					
						
							|  |  |  |   import recurring_ical_events | 
					
						
							|  |  |  | except ModuleNotFoundError: | 
					
						
							|  |  |  |   print('recurring-ical-events library could not be found.') | 
					
						
							|  |  |  |   print('Please install this with: pip3 install recurring-ical-events') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | try: | 
					
						
							|  |  |  |   from icalendar import Calendar, Event | 
					
						
							|  |  |  | except ModuleNotFoundError: | 
					
						
							|  |  |  |   print('icalendar library could not be found. Please install this with:') | 
					
						
							|  |  |  |   print('pip3 install icalendar') | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-23 01:45:40 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | filename = os.path.basename(__file__).split('.py')[0] | 
					
						
							|  |  |  | logger = logging.getLogger(filename) | 
					
						
							| 
									
										
										
										
											2020-05-29 03:59:44 +02:00
										 |  |  | logger.setLevel(level=logging.ERROR) | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-26 19:10:20 +02:00
										 |  |  | class iCalendar: | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  |   """iCalendar parsing moudule for inkycal.
 | 
					
						
							|  |  |  |   Parses events from given iCalendar URLs / paths"""
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def __init__(self): | 
					
						
							|  |  |  |     self.icalendars = [] | 
					
						
							|  |  |  |     self.parsed_events = [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def load_url(self, url, username=None, password=None): | 
					
						
							|  |  |  |     """Input a string or list of strings containing valid iCalendar URLs
 | 
					
						
							|  |  |  |     example: 'URL1' (single url) OR ['URL1', 'URL2'] (multiple URLs) | 
					
						
							|  |  |  |     add username and password to access protected files | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if type(url) == list: | 
					
						
							|  |  |  |       if (username == None) and (password == None): | 
					
						
							|  |  |  |         ical = [Calendar.from_ical(str(urlopen(_).read().decode())) | 
					
						
							|  |  |  |                                    for _ in url] | 
					
						
							|  |  |  |       else: | 
					
						
							|  |  |  |         ical = [auth_ical(each_url, username, password) for each_url in url] | 
					
						
							|  |  |  |     elif type(url) == str: | 
					
						
							|  |  |  |       if (username == None) and (password == None): | 
					
						
							|  |  |  |         ical = [Calendar.from_ical(str(urlopen(url).read().decode()))] | 
					
						
							|  |  |  |       else: | 
					
						
							|  |  |  |         ical = [auth_ical(url, username, password)] | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |       raise Exception ("Input: '{}' is not a string or list!".format(url)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def auth_ical(url, uname, passwd): | 
					
						
							|  |  |  |       """Authorisation helper for protected ical files""" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       # Credit to Joshka | 
					
						
							|  |  |  |       password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() | 
					
						
							|  |  |  |       password_mgr.add_password(None, url, username, password) | 
					
						
							|  |  |  |       handler = urllib.request.HTTPBasicAuthHandler(password_mgr) | 
					
						
							|  |  |  |       opener = urllib.request.build_opener(handler) | 
					
						
							|  |  |  |       ical = Calendar.from_ical(str(opener.open(url).read().decode())) | 
					
						
							|  |  |  |       return ical | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Add the parsed icalendar/s to the self.icalendars list | 
					
						
							|  |  |  |     if ical: self.icalendars += ical | 
					
						
							| 
									
										
										
										
											2020-05-23 01:45:40 +02:00
										 |  |  |     logger.info('loaded iCalendars from URLs') | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |   def load_from_file(self, filepath): | 
					
						
							|  |  |  |     """Input a string or list of strings containing valid iCalendar filepaths
 | 
					
						
							|  |  |  |     example: 'path1' (single file) OR ['path1', 'path2'] (multiple files) | 
					
						
							|  |  |  |     returns a list of iCalendars as string (raw) | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     if type(url) == list: | 
					
						
							| 
									
										
										
										
											2020-06-12 18:12:14 +02:00
										 |  |  |       ical = (Calendar.from_ical(open(path)) for path in filepath) | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  |     elif type(url) == str: | 
					
						
							| 
									
										
										
										
											2020-06-12 18:12:14 +02:00
										 |  |  |       ical = (Calendar.from_ical(open(path))) | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  |     else: | 
					
						
							|  |  |  |       raise Exception ("Input: '{}' is not a string or list!".format(url)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     self.icalendars += icals | 
					
						
							| 
									
										
										
										
											2020-05-23 01:45:40 +02:00
										 |  |  |     logger.info('loaded iCalendars from filepaths') | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-19 03:17:26 +02:00
										 |  |  |   def get_events(self, timeline_start, timeline_end, timezone=None): | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  |     """Input an arrow (time) object for:
 | 
					
						
							|  |  |  |     * the beginning of timeline (events have to end after this time) | 
					
						
							|  |  |  |     * the end of the timeline (events have to begin before this time) | 
					
						
							| 
									
										
										
										
											2020-05-19 03:17:26 +02:00
										 |  |  |     * timezone if events should be formatted to local time | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  |     Returns a list of events sorted by date | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     if type(timeline_start) == arrow.arrow.Arrow: | 
					
						
							| 
									
										
										
										
											2020-05-19 03:17:26 +02:00
										 |  |  |       if timezone == None: | 
					
						
							|  |  |  |         timezone = 'UTC' | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  |       t_start = timeline_start | 
					
						
							|  |  |  |       t_end = timeline_end | 
					
						
							|  |  |  |     else: | 
					
						
							| 
									
										
										
										
											2020-05-26 19:10:20 +02:00
										 |  |  |       raise Exception('Please input a valid arrow (time) object!') | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-19 03:17:26 +02:00
										 |  |  |     # parse non-recurring events | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     # Recurring events time-span has to be in this format: | 
					
						
							|  |  |  |     # "%Y%m%dT%H%M%SZ" (python strftime) | 
					
						
							| 
									
										
										
										
											2020-06-12 18:12:14 +02:00
										 |  |  |     fmt = lambda date: (date.year, date.month, date.day, date.hour, | 
					
						
							|  |  |  |                         date.minute, date.second) | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-12 18:12:14 +02:00
										 |  |  |     t_start_recurring = fmt(t_start) | 
					
						
							|  |  |  |     t_end_recurring = fmt(t_end) | 
					
						
							| 
									
										
										
										
											2020-05-26 19:10:20 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-12 18:12:14 +02:00
										 |  |  |     # Fetch recurring events | 
					
						
							|  |  |  |     recurring_events = (recurring_ical_events.of(ical).between( | 
					
						
							|  |  |  |                         t_start_recurring, t_end_recurring) | 
					
						
							|  |  |  |                         for ical in self.icalendars) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-19 19:39:28 +02:00
										 |  |  |     events = ( | 
					
						
							| 
									
										
										
										
											2020-06-12 18:12:14 +02:00
										 |  |  |       { | 
					
						
							| 
									
										
										
										
											2020-06-14 22:58:27 +02:00
										 |  |  |       'title': events.get('SUMMARY').lstrip(), | 
					
						
							|  |  |  |        | 
					
						
							|  |  |  |       'begin': arrow.get(events.get('DTSTART').dt).to(timezone) if ( | 
					
						
							|  |  |  |         arrow.get(events.get('dtstart').dt).format('HH:mm') != '00:00') | 
					
						
							|  |  |  |         else arrow.get(events.get('DTSTART').dt).replace(tzinfo=timezone), | 
					
						
							|  |  |  |        | 
					
						
							|  |  |  |       'end':arrow.get(events.get("DTEND").dt).to(timezone) if ( | 
					
						
							|  |  |  |         arrow.get(events.get('dtstart').dt).format('HH:mm') != '00:00') | 
					
						
							|  |  |  |         else arrow.get(events.get('DTEND').dt).replace(tzinfo=timezone) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       } for ical in recurring_events for events in ical) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-18 03:46:49 +02:00
										 |  |  |     # if any recurring events were found, add them to parsed_events | 
					
						
							| 
									
										
										
										
											2020-06-19 19:39:28 +02:00
										 |  |  |     if events: self.parsed_events += list(events) | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-18 03:46:49 +02:00
										 |  |  |     # Sort events by their beginning date | 
					
						
							|  |  |  |     self.sort() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  |     return self.parsed_events | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def sort(self): | 
					
						
							| 
									
										
										
										
											2020-05-23 01:45:40 +02:00
										 |  |  |     """Sort all parsed events in order of beginning time""" | 
					
						
							| 
									
										
										
										
											2020-05-18 03:46:49 +02:00
										 |  |  |     if not self.parsed_events: | 
					
						
							| 
									
										
										
										
											2020-05-23 01:45:40 +02:00
										 |  |  |       logger.debug('no events found to be sorted') | 
					
						
							| 
									
										
										
										
											2020-05-18 03:46:49 +02:00
										 |  |  |     else: | 
					
						
							| 
									
										
										
										
											2020-06-14 22:58:27 +02:00
										 |  |  |       # sort events by date | 
					
						
							| 
									
										
										
										
											2020-05-18 03:46:49 +02:00
										 |  |  |       by_date = lambda event: event['begin'] | 
					
						
							|  |  |  |       self.parsed_events.sort(key=by_date) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-12 18:12:14 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-18 03:46:49 +02:00
										 |  |  |   def clear_events(self): | 
					
						
							|  |  |  |     """clear previously parsed events""" | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-18 03:46:49 +02:00
										 |  |  |     self.parsed_events = [] | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-18 18:31:23 +02:00
										 |  |  |   @staticmethod | 
					
						
							|  |  |  |   def all_day(event): | 
					
						
							|  |  |  |     """Check if an event is an all day event.
 | 
					
						
							|  |  |  |     Returns True if event is all day, else False | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     if not ('end' and 'begin') in event: | 
					
						
							|  |  |  |       print('Events must have a starting and ending time') | 
					
						
							|  |  |  |       raise Exception('This event is not valid!') | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |       begin, end = event['begin'], event['end'] | 
					
						
							|  |  |  |       duration = end - begin | 
					
						
							|  |  |  |       if (begin.format('HH:mm') == '00:00' and end.format('HH:mm') == '00:00' | 
					
						
							|  |  |  |           and duration.days >= 1): | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  |       else: | 
					
						
							|  |  |  |         return False | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-19 03:17:26 +02:00
										 |  |  |   @staticmethod | 
					
						
							|  |  |  |   def get_system_tz(): | 
					
						
							|  |  |  |     """Get the timezone set by the system""" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |       local_tz = time.tzname[1] | 
					
						
							|  |  |  |     except: | 
					
						
							|  |  |  |       print('System timezone could not be parsed!') | 
					
						
							|  |  |  |       print('Please set timezone manually!. Setting timezone to None...') | 
					
						
							|  |  |  |       local_tz = None | 
					
						
							|  |  |  |     return local_tz | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  |   def show_events(self, fmt='DD MMM YY HH:mm'): | 
					
						
							|  |  |  |     """print all parsed events in a more readable way
 | 
					
						
							|  |  |  |     use the format (fmt) parameter to specify the date format | 
					
						
							|  |  |  |     see https://arrow.readthedocs.io/en/latest/#supported-tokens | 
					
						
							|  |  |  |     for more info tokens | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2020-05-18 03:46:49 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  |     if not self.parsed_events: | 
					
						
							| 
									
										
										
										
											2020-05-23 01:45:40 +02:00
										 |  |  |       logger.debug('no events found to be shown') | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  |     else: | 
					
						
							| 
									
										
										
										
											2020-05-19 03:17:26 +02:00
										 |  |  |       line_width = max(len(_['title']) for _ in self.parsed_events) | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  |       for events in self.parsed_events: | 
					
						
							|  |  |  |         title = events['title'] | 
					
						
							|  |  |  |         begin, end = events['begin'].format(fmt), events['end'].format(fmt) | 
					
						
							| 
									
										
										
										
											2020-05-19 03:17:26 +02:00
										 |  |  |         print('{0} {1} | {2} | {3}'.format( | 
					
						
							|  |  |  |           title, ' ' * (line_width - len(title)), begin, end)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-23 01:45:40 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | if __name__ == '__main__': | 
					
						
							| 
									
										
										
										
											2020-06-12 18:12:14 +02:00
										 |  |  |   print('running {0} in standalone mode'.format(filename)) |