forked from tsc-vfl/hugo-page
		
	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