import logging, yaml, pprint, json, re import hashlib, uuid import caldav import datetime as dt from . import login, debug _l = logging.getLogger(__name__) def buildSubparser(subparser): subparser.add_argument('--schedule', default='../../data/schedule.yaml') subparser.add_argument('--holidays', default='../../data/holidays.yaml') subparser.add_argument('-n', '--dry', action='store_true') subparser.add_argument('--recreate-all', action='store_true') def run(args): loginData, schedule, holidays = _loadRawData(args) packedSchedule = _unpackSchedules(schedule) fixedSchedule = _addHolidayExceptions(packedSchedule, holidays) _synchonizeCalendars(args, fixedSchedule, loginData, holidays) def _loadRawData(args): _l.info('Loading data from hard disk') loginData = login.loadLoginData() with open(args.schedule, 'r') as f: schedule = yaml.safe_load(f.read()) _l.log(5, 'Schedule data:\n%s', pprint.pformat(schedule, indent=4, width=100)) with open(args.holidays, 'r') as f: holidays = yaml.safe_load(f.read()) _l.log(5, 'Holidays data:\n%s', pprint.pformat(holidays, indent=4, width=100)) _l.info('Data was read from hard disk') return loginData, schedule, holidays class Event: def __init__(self, day, start, duration, title): self.day = day self.start = start self.duration = duration self.title = title self.age = None self.external = False self.description = '' self.exceptions = [] def __repr__(self): wAge = f' ({self.age})' if self.age is not None else '' ext = 'Ext' if self.external else '' return f'{ext}Ev({self.title}{wAge}) [{self.day}, {self.start}, {self.duration}]' def getHash(self, holidays): def fixHolidays(holidays): def fixDate(d): return d.strftime('%Y-%m-%d') def fixFeast(f): return fixDate(f) def fixHoliday(h): return { 'from': fixDate(h['from']), 'to': fixDate(h['to']), } return { **holidays, 'feasts': [fixFeast(f) for f in holidays['feasts']], 'holidays': [fixHoliday(h) for h in holidays['holidays']], } data = { 'day': self.day, 'start': self.start, 'duration': self.duration, 'title': self.title, 'age': self.age, 'external': self.external, 'description': self.description, 'exceptions': self.exceptions, "holidays": fixHolidays(holidays), } # pprint.pprint(data, indent=4, width=100) hasher = hashlib.sha1() hasher.update(json.dumps(data, sort_keys=True).encode('utf-8')) first = hasher.hexdigest() second = uuid.uuid4().hex return f'{first}___{second}', first dDay = dt.timedelta(days=1) _MAP_WEEKDAY_ICAL = { 'Mon': 'MO', 'Tue': 'TU', 'Wed': 'WE', 'Thu': 'TH', 'Fri': 'FR', 'Sat': 'SA', 'Sun': 'SU', } def addToCalendar(self, calendar: caldav.Calendar, holidays): from . import cal_helper def getFirstTimeInYear(): now = dt.datetime.now() d = dt.datetime(year=now.year, month=1, day=1) pass helper = cal_helper.IcalHelper() startDay, start = helper.getStart(self, holidays) if start is None: _l.warning('Could not get start time for event %s in the current year', self) return uid, uidFirst = self.getHash(holidays) icalData = { 'uid': uid, 'DTSTART': start, 'DTEND': start + dt.timedelta(minutes=self.duration), 'SUMMARY': self.title, 'RRULE': { 'FREQ': 'DAILY', 'BYDAY': self._MAP_WEEKDAY_ICAL[self.day] } } # debug.debugger() holidayExceptions = helper.getHolidayExceptions(self, startDay, holidays) if holidayExceptions is not None: icalData['EXDATE'] = holidayExceptions desc = '' if self.age is not None: desc += f'Jahrgänge: {self.age}' if len(self.description) > 0: desc += '\n\n' desc += self.description if len(desc) > 0: icalData['DESCRIPTION'] = desc _l.log(5, 'Adding event\n%s', pprint.pformat(icalData, indent=4, width=100)) ical = caldav.lib.vcal.create_ical(**icalData) _l.log(5, 'Created event\n%s', pprint.pformat(ical, indent=4, width=100)) _l.log(5, 'Created event\n%s', ical) calendar.add_event(**icalData) def _unpackSchedules(schedule): def packSingleCalendar(cal): def parseSingleEvent(ev): e = Event( day=ev['day'], start=ev['start'], duration=ev['duration'], title=ev['title'] ) if 'age' in ev: e.age = ev['age'] if ev.get('extern', False) or ev.get('external', False): e.external = True if 'desc' in ev: e.description = ev['desc'] if 'exceptions' in ev: raise Exception('Not yet implemented to have exceptions') return e _l.log(5, 'Unpacking calendar %s', cal) ret = { **cal } if 'schedule' in ret: ret['schedule'] = [ parseSingleEvent(e) for e in ret['schedule'] ] _l.log(5, 'Unpacked calendar %s', ret) return ret ret = {} for calName in schedule['calendars']: if schedule['calendars'][calName].get('ignore', False): _l.info('Ignoring calendar %s', calName) continue ret[calName] = packSingleCalendar(schedule['calendars'][calName]) _l.log(5, 'Unpacked schedule:\n%s', pprint.pformat(ret, indent=4, width=100)) return ret def _addHolidayExceptions(schedule, holidays): return schedule class CalendarSynchonizer: def __init__(self, args, calId, calName, loginData): self.args = args self.calId = calId self.calName = calName self.loginData = loginData self._calDav = None def _getUrl(self): return f'{self.loginData.base}/remote.php/dav/calendars' def _getCalDav(self): if self._calDav is None: self._calDav = caldav.DAVClient( url=self._getUrl(), username=self.loginData.loginName, password=self.loginData.appPassword ) return self._calDav def _getCalendar(self): cd = self._getCalDav() return cd.principal().calendar(cal_id=self.calId) def synchonize(self, calendars, holidays): downstreamEvents = self._getDownstreamEvents(calendars[self.calName]['schedule'], holidays) _l.debug('Downstream events:\n%s', pprint.pformat(downstreamEvents, indent=4, width=100)) upstreamEvents = self._getUpstreamEvents() _l.debug('Upstream events:\n%s', pprint.pformat(upstreamEvents, indent=4, width=100)) newEvents = self._getNewEvents(upstreamEvents, downstreamEvents) _l.log(5, 'New events:\n%s', pprint.pformat(newEvents, indent=4, width=100)) deletedEvents = self._getDeletedEvents(upstreamEvents, downstreamEvents) _l.log(5, 'Events marked for deletion:\n%s', pprint.pformat(deletedEvents, indent=4, width=100)) self._deleteEvents(deletedEvents) self._addEvents(newEvents, holidays) def _getUpstreamEvents(self): _l.debug('Fetching upstream calendar entries') cd = self._getCalDav() # principal = cd.principal() # _l.log(5, 'Principal %s', principal) # calendar = principal.calendar(cal_id=self.calId) # _l.log(5, 'Calendar %s', calendar) calendar = self._getCalendar() # children = calendar.children() # _l.debug('Getting children for calendar %s:\n%s', self.calId, children) events = calendar.events() ret = {} for e in events: uidRaw = str(e.icalendar_component['uid']) uid = uidRaw.split('___', 1)[0] _l.log(5, 'Event with uid %s was found.', uid) ret[uid] = e return ret def _getDownstreamEvents(self, schedule, holidays): _l.debug('Preparing local calendar events') ret = {} for e in schedule: uid, uidFirst = e.getHash(holidays) _l.log(5, 'Event with uid part %s was found.', uidFirst) ret[uidFirst] = e return ret def _getNewEvents(self, upstreamEvents, downstreamEvents): if self.args.recreate_all: return downstreamEvents.values() upstreamUids = set(upstreamEvents.keys()) downstreamUids = set(downstreamEvents.keys()) newUids = downstreamUids - upstreamUids return [downstreamEvents[uid] for uid in newUids] def _getDeletedEvents(self, upstreamEvents, downstreamEvents): if self.args.recreate_all: return upstreamEvents.values() upstreamUids = set(upstreamEvents.keys()) downstreamUids = set(downstreamEvents.keys()) toDeleteUids = upstreamUids - downstreamUids return [upstreamEvents[uid] for uid in toDeleteUids] def _addEvents(self, newEvents, holidays): cal = self._getCalendar() for e in newEvents: _l.debug('Adding event %s', e) if self.args.dry: _l.info('Skipping add event %s (dry mode)', e) else: # e.save() e.addToCalendar(cal, holidays) def _deleteEvents(self, oldEvents): for e in oldEvents: _l.debug('Deleting event %s', e) if self.args.dry: _l.info('Skipping delete event %s (dry mode)', e) else: e.delete() def _synchonizeCalendars(args, calendars, loginData, holidays): for calName in calendars: calendar = calendars[calName] _l.info('Synching calendar %s', calName) calendarSynchonizer = CalendarSynchonizer(args, calendar['id'], calName, loginData) calendarSynchonizer.synchonize(calendars, holidays)