WIP Synchronization of local yaml with upstream server automation
This commit is contained in:
parent
2614357a12
commit
9148e1f469
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
|
from . import login
|
||||||
|
|
||||||
_l = logging.getLogger(__name__)
|
_l = logging.getLogger(__name__)
|
||||||
|
|
||||||
def buildSubparser(subparser):
|
def buildSubparser(subparser):
|
||||||
subparser.add_argument('--schedule', default='../../data/schedule.json')
|
subparser.add_argument('--schedule', default='../../data/schedule.yaml')
|
||||||
subparser.add_argument('--holidays', default='../../data/holidays.json')
|
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):
|
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()
|
loginData = login.loadLoginData()
|
||||||
|
|
||||||
with open(args.schedule, 'r') as f:
|
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:
|
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