Migrate to central appointment database #45
46
scripts/nc-cal-sync/calendar_synchronizer/cal_helper.py
Normal file
46
scripts/nc-cal-sync/calendar_synchronizer/cal_helper.py
Normal file
@ -0,0 +1,46 @@
|
||||
import datetime as dt
|
||||
import re, logging
|
||||
|
||||
from .sync import Event
|
||||
|
||||
class IcalHelper:
|
||||
|
||||
_WEEKDAY_MAP = {
|
||||
'Mon': 0,
|
||||
'Tue': 1,
|
||||
'Wed': 2,
|
||||
'Thu': 3,
|
||||
'Fri': 4,
|
||||
'Sat': 5,
|
||||
'Sun': 6
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._l = logging.getLogger(__name__)
|
||||
self.dDay = dt.timedelta(days=1)
|
||||
self.firstDay = dt.datetime(year=dt.datetime.now().year, month=1, day=1)
|
||||
self.reTime = re.compile(r'(\d{1,2}):(\d{2})')
|
||||
|
||||
def getStart(self, event: Event, holidays):
|
||||
self._getFirstWeekdayInYear(event.day)
|
||||
self._getFirstOccurence(event, holidays)
|
||||
pass
|
||||
|
||||
def _getFirstWeekdayInYear(self, weekday):
|
||||
candidate = self.firstDay
|
||||
while candidate.weekday() != self._WEEKDAY_MAP[weekday]:
|
||||
candidate += self.dDay
|
||||
|
||||
self._l.log(5, 'First %s in year is %s', weekday, candidate)
|
||||
|
||||
return candidate
|
||||
|
||||
def _getSortedHolidays(self, holidays):
|
||||
return sorted(holidays['holidays'], key=lambda h: h['from'])
|
||||
|
||||
def _getSortedFeasts(self, holidays):
|
||||
return sorted(holidays['feasts'])
|
||||
|
||||
def _getFirstOccurence(self, event: Event, holidays):
|
||||
firstWeekday = self._getFirstWeekdayInYear(event.day)
|
||||
firstWeekday.tzinfo = dt.timezone.tzname('Europe/Berlin')
|
@ -1,22 +1,281 @@
|
||||
import logging, json
|
||||
import logging, yaml, pprint, json, re
|
||||
import hashlib
|
||||
import caldav
|
||||
import datetime as dt
|
||||
|
||||
from . import login
|
||||
|
||||
_l = logging.getLogger(__name__)
|
||||
|
||||
def buildSubparser(subparser):
|
||||
subparser.add_argument('--schedule', default='../../data/schedule.json')
|
||||
subparser.add_argument('--holidays', default='../../data/holidays.json')
|
||||
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):
|
||||
_l.info('Loading data from hard disc')
|
||||
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 = json.load(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 = json.load(f)
|
||||
holidays = yaml.safe_load(f.read())
|
||||
|
||||
_l.info('Data was read from hard disc')
|
||||
_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 ''
|
||||
return f'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'))
|
||||
return hasher.hexdigest()
|
||||
|
||||
dDay = dt.timedelta(days=1)
|
||||
|
||||
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()
|
||||
helper.getStart(self, holidays)
|
||||
|
||||
icalData = {
|
||||
'uid': self.getHash(holidays),
|
||||
'DTSTART': dt.datetime.now(),
|
||||
'DTEND': dt.datetime.now() + dt.timedelta(minutes=self.duration),
|
||||
'SUMMARY': self.title
|
||||
}
|
||||
# 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):
|
||||
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:
|
||||
uid = str(e.icalendar_component['uid'])
|
||||
_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 = e.getHash(holidays)
|
||||
_l.log(5, 'Event with uid %s was found.', uid)
|
||||
ret[uid] = 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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user