214 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			214 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/usr/bin/python3
 | |
| # -*- coding: utf-8 -*-
 | |
| """
 | |
| iCalendar (parsing) module for Inky-Calendar Project
 | |
| Copyright by aceisace
 | |
| """
 | |
| 
 | |
| """               ---info about iCalendars---
 | |
| • 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!
 | |
| """
 | |
| 
 | |
| import arrow
 | |
| from urllib.request import urlopen
 | |
| import logging
 | |
| import time
 | |
| import os
 | |
| 
 | |
| 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')
 | |
| 
 | |
| 
 | |
| filename = os.path.basename(__file__).split('.py')[0]
 | |
| logger = logging.getLogger(filename)
 | |
| 
 | |
| class iCalendar:
 | |
|   """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
 | |
|     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 type(url) == list:
 | |
|       ical = (Calendar.from_ical(open(path)) for path in filepath)
 | |
|     elif type(url) == str:
 | |
|       ical = (Calendar.from_ical(open(path)))
 | |
|     else:
 | |
|       raise Exception ("Input: '{}' is not a string or list!".format(url))
 | |
| 
 | |
|     self.icalendars += icals
 | |
|     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:
 | |
|       if timezone == None:
 | |
|         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))
 | |
| 
 | |
| 
 | |
| if __name__ == '__main__':
 | |
|   print('running {0} in standalone mode'.format(filename))
 |