WIP Synchronization of local yaml with upstream server automation

This commit is contained in:
Christian Wolf 2025-01-15 19:04:41 +01:00
parent 2614357a12
commit 9148e1f469
2 changed files with 313 additions and 8 deletions

View 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')

View File

@ -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)