| 
									
										
										
										
											2022-04-02 01:30:17 +02:00
										 |  |  | #!python3 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  | """
 | 
					
						
							| 
									
										
										
										
											2022-10-02 00:49:27 +02:00
										 |  |  | Inkycal iCalendar parsing module | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  | Copyright by aceisace | 
					
						
							|  |  |  | """
 | 
					
						
							| 
									
										
										
										
											2022-04-10 06:35:08 +02:00
										 |  |  | import urllib | 
					
						
							|  |  |  | import arrow | 
					
						
							|  |  |  | from urllib.request import urlopen | 
					
						
							|  |  |  | import logging | 
					
						
							|  |  |  | import time | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import recurring_ical_events | 
					
						
							|  |  |  | from icalendar import Calendar | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											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! | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-03 02:56:04 +02:00
										 |  |  | logger = logging.getLogger(__name__) | 
					
						
							| 
									
										
										
										
											2020-05-23 01:45:40 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-15 02:58:26 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-26 19:10:20 +02:00
										 |  |  | class iCalendar: | 
					
						
							| 
									
										
										
										
											2022-04-02 01:30:17 +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 | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-10 06:35:08 +02:00
										 |  |  |         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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-02 01:30:17 +02:00
										 |  |  |         if type(url) == list: | 
					
						
							| 
									
										
										
										
											2022-04-10 06:35:08 +02:00
										 |  |  |             if (username is None) and (password is None): | 
					
						
							| 
									
										
										
										
											2022-04-02 01:30:17 +02:00
										 |  |  |                 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: | 
					
						
							| 
									
										
										
										
											2022-04-10 06:35:08 +02:00
										 |  |  |             if (username is None) and (password is None): | 
					
						
							| 
									
										
										
										
											2022-04-02 01:30:17 +02:00
										 |  |  |                 ical = [Calendar.from_ical(str(urlopen(url).read().decode()))] | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 ical = [auth_ical(url, username, password)] | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             raise Exception(f"Input: '{url}' is not a string or list!") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Add the parsed icalendar/s to the self.icalendars list | 
					
						
							|  |  |  |         if ical: self.icalendars += ical | 
					
						
							|  |  |  |         logger.info('loaded iCalendars from URLs') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     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 isinstance(filepath, list): | 
					
						
							|  |  |  |             for path in filepath: | 
					
						
							|  |  |  |                 with open(path, mode='r') as ical_file: | 
					
						
							|  |  |  |                     ical = (Calendar.from_ical(ical_file.read())) | 
					
						
							| 
									
										
										
										
											2022-08-29 17:16:19 +01:00
										 |  |  |                     self.icalendars.append(ical) | 
					
						
							| 
									
										
										
										
											2022-04-02 01:30:17 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         elif isinstance(filepath, str): | 
					
						
							|  |  |  |             with open(filepath, mode='r') as ical_file: | 
					
						
							|  |  |  |                 ical = (Calendar.from_ical(ical_file.read())) | 
					
						
							| 
									
										
										
										
											2022-08-29 17:16:19 +01:00
										 |  |  |                 self.icalendars.append(ical) | 
					
						
							| 
									
										
										
										
											2022-04-02 01:30:17 +02:00
										 |  |  |         else: | 
					
						
							|  |  |  |             raise Exception(f"Input: '{filepath}' is not a string or list!") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         logger.info('loaded iCalendars from filepaths') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_events(self, timeline_start, timeline_end, timezone=None): | 
					
						
							|  |  |  |         """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) | 
					
						
							|  |  |  |         * timezone if events should be formatted to local time | 
					
						
							|  |  |  |         Returns a list of events sorted by date | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         if type(timeline_start) == arrow.arrow.Arrow: | 
					
						
							| 
									
										
										
										
											2022-04-10 06:35:08 +02:00
										 |  |  |             if timezone is None: | 
					
						
							| 
									
										
										
										
											2022-04-02 01:30:17 +02:00
										 |  |  |                 timezone = 'UTC' | 
					
						
							|  |  |  |             t_start = timeline_start | 
					
						
							|  |  |  |             t_end = timeline_end | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             raise Exception('Please input a valid arrow (time) object!') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # parse non-recurring events | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Recurring events time-span has to be in this format: | 
					
						
							|  |  |  |         # "%Y%m%dT%H%M%SZ" (python strftime) | 
					
						
							|  |  |  |         fmt = lambda date: (date.year, date.month, date.day, date.hour, | 
					
						
							|  |  |  |                             date.minute, date.second) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         t_start_recurring = fmt(t_start) | 
					
						
							|  |  |  |         t_end_recurring = fmt(t_end) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Fetch recurring events | 
					
						
							|  |  |  |         recurring_events = (recurring_ical_events.of(ical).between( | 
					
						
							|  |  |  |             t_start_recurring, t_end_recurring) | 
					
						
							|  |  |  |             for ical in self.icalendars) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         events = ( | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 '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) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # if any recurring events were found, add them to parsed_events | 
					
						
							|  |  |  |         if events: self.parsed_events += list(events) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Sort events by their beginning date | 
					
						
							|  |  |  |         self.sort() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return self.parsed_events | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def sort(self): | 
					
						
							|  |  |  |         """Sort all parsed events in order of beginning time""" | 
					
						
							|  |  |  |         if not self.parsed_events: | 
					
						
							|  |  |  |             logger.debug('no events found to be sorted') | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             # sort events by date | 
					
						
							|  |  |  |             by_date = lambda event: event['begin'] | 
					
						
							|  |  |  |             self.parsed_events.sort(key=by_date) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def clear_events(self): | 
					
						
							|  |  |  |         """clear previously parsed events""" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.parsed_events = [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @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 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @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 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     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 | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if not self.parsed_events: | 
					
						
							|  |  |  |             logger.debug('no events found to be shown') | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             line_width = max(len(_['title']) for _ in self.parsed_events) | 
					
						
							|  |  |  |             for events in self.parsed_events: | 
					
						
							|  |  |  |                 title = events['title'] | 
					
						
							|  |  |  |                 begin, end = events['begin'].format(fmt), events['end'].format(fmt) | 
					
						
							|  |  |  |                 print('{0} {1} | {2} | {3}'.format( | 
					
						
							|  |  |  |                     title, ' ' * (line_width - len(title)), begin, end)) | 
					
						
							| 
									
										
										
										
											2020-05-19 03:17:26 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-23 01:45:40 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | if __name__ == '__main__': | 
					
						
							| 
									
										
										
										
											2022-10-03 02:56:04 +02:00
										 |  |  |     print(f'running {__name__} in standalone mode') |