def check_support(vevent: icalendar.cal.Event, href: str, calendar: str): """test if all icalendar features used in this event are supported, raise `UpdateFailed` otherwise. :param vevent: event to test :param href: href of this event, only used for logging """ rec_id = vevent.get(RECURRENCE_ID) if rec_id is not None and rec_id.params.get('RANGE') == THISANDPRIOR: raise UpdateFailed( 'The parameter `THISANDPRIOR` is not (and will not be) ' 'supported by khal (as applications supporting the latest ' 'standard MUST NOT create those. Therefore event {0} from ' 'calendar {1} will not be shown in khal' .format(href, calendar) ) rdate = vevent.get('RDATE') if rdate is not None and hasattr(rdate, 'params') and rdate.params.get('VALUE') == 'PERIOD': raise UpdateFailed( '`RDATE;VALUE=PERIOD` is currently not supported by khal. ' 'Therefore event {0} from calendar {1} will not be shown in khal.\n' 'Please post exemplary events (please remove any private data) ' 'to https://github.com/pimutils/khal/issues/152 .' .format(href, calendar) )
def _update_impl(self, vevent: icalendar.cal.Event, href: str, calendar: str) -> None: """insert `vevent` into the database expand `vevent`'s recurrence rules (if needed) and insert all instance in the respective tables than insert non-recurring and original recurring (those with an RRULE property) events into table `events` """ # TODO FIXME this function is a steaming pile of shit rec_id = vevent.get(RECURRENCE_ID) if rec_id is None: rrange = None else: rrange = rec_id.params.get('RANGE') # testing on datetime.date won't work as datetime is a child of date if not isinstance(vevent['DTSTART'].dt, dt.datetime): dtype = EventType.DATE else: dtype = EventType.DATETIME if ('TZID' in vevent['DTSTART'].params and dtype == EventType.DATETIME) or \ getattr(vevent['DTSTART'].dt, 'tzinfo', None): recs_table = 'recs_loc' else: recs_table = 'recs_float' thisandfuture = (rrange == THISANDFUTURE) if thisandfuture: start_shift, duration = calc_shift_deltas(vevent) start_shift_seconds = start_shift.days * 3600 * 24 + start_shift.seconds duration_seconds = duration.days * 3600 * 24 + duration.seconds dtstartend = utils.expand(vevent, href) if not dtstartend: # Does this event even have dates? Technically it is possible for # events to be empty/non-existent by deleting all their recurrences # through EXDATE. return for dtstart, dtend in dtstartend: if dtype == EventType.DATE: dbstart = utils.to_unix_time(dtstart) dbend = utils.to_unix_time(dtend) else: dbstart = utils.to_unix_time(dtstart) dbend = utils.to_unix_time(dtend) if rec_id is not None: ref = rec_inst = str(utils.to_unix_time(rec_id.dt)) else: rec_inst = str(dbstart) ref = PROTO if thisandfuture: recs_sql_s = ( 'UPDATE {0} SET dtstart = rec_inst + ?, dtend = rec_inst + ?, ref = ? ' 'WHERE rec_inst >= ? AND href = ? AND calendar = ?;'. format(recs_table)) stuple_f = ( start_shift_seconds, start_shift_seconds + duration_seconds, ref, rec_inst, href, calendar, ) self.sql_ex(recs_sql_s, stuple_f) else: recs_sql_s = ( 'INSERT OR REPLACE INTO {0} ' '(dtstart, dtend, href, ref, dtype, rec_inst, calendar)' 'VALUES (?, ?, ?, ?, ?, ?, ?);'.format(recs_table)) stuple_n = (dbstart, dbend, href, ref, dtype, rec_inst, calendar) self.sql_ex(recs_sql_s, stuple_n)
def _update_impl(self, vevent: icalendar.cal.Event, href: str, calendar: str) -> None: """insert `vevent` into the database expand `vevent`'s recurrence rules (if needed) and insert all instance in the respective tables than insert non-recurring and original recurring (those with an RRULE property) events into table `events` """ # TODO FIXME this function is a steaming pile of shit rec_id = vevent.get(RECURRENCE_ID) if rec_id is None: rrange = None else: rrange = rec_id.params.get('RANGE') # testing on datetime.date won't work as datetime is a child of date if not isinstance(vevent['DTSTART'].dt, dt.datetime): dtype = EventType.DATE else: dtype = EventType.DATETIME if ('TZID' in vevent['DTSTART'].params and dtype == EventType.DATETIME) or \ getattr(vevent['DTSTART'].dt, 'tzinfo', None): recs_table = 'recs_loc' else: recs_table = 'recs_float' thisandfuture = (rrange == THISANDFUTURE) if thisandfuture: start_shift, duration = calc_shift_deltas(vevent) start_shift_seconds = start_shift.days * 3600 * 24 + start_shift.seconds duration_seconds = duration.days * 3600 * 24 + duration.seconds dtstartend = utils.expand(vevent, href) if not dtstartend: # Does this event even have dates? Technically it is possible for # events to be empty/non-existent by deleting all their recurrences # through EXDATE. return for dtstart, dtend in dtstartend: if dtype == EventType.DATE: dbstart = utils.to_unix_time(dtstart) dbend = utils.to_unix_time(dtend) else: dbstart = utils.to_unix_time(dtstart) dbend = utils.to_unix_time(dtend) if rec_id is not None: ref = rec_inst = str(utils.to_unix_time(rec_id.dt)) else: rec_inst = str(dbstart) ref = PROTO if thisandfuture: recs_sql_s = ( 'UPDATE {0} SET dtstart = rec_inst + ?, dtend = rec_inst + ?, ref = ? ' 'WHERE rec_inst >= ? AND href = ? AND calendar = ?;'.format(recs_table)) stuple_f = ( start_shift_seconds, start_shift_seconds + duration_seconds, ref, rec_inst, href, calendar, ) self.sql_ex(recs_sql_s, stuple_f) else: recs_sql_s = ( 'INSERT OR REPLACE INTO {0} ' '(dtstart, dtend, href, ref, dtype, rec_inst, calendar)' 'VALUES (?, ?, ?, ?, ?, ?, ?);'.format(recs_table)) stuple_n = (dbstart, dbend, href, ref, dtype, rec_inst, calendar) self.sql_ex(recs_sql_s, stuple_n)
def _processEventAlarms(self, event: icalendar.cal.Event, eventDatetime: datetime.datetime): # Create current datetime object. currentUTCTime = time.time() currentDatetime = datetime.datetime.utcfromtimestamp(currentUTCTime) currentDatetime = currentDatetime.replace(tzinfo=pytz.UTC) for alarm in event.walk("VALARM"): if "TRIGGER" in alarm: # Get time delta when reminder is triggered. trigger = alarm.get("TRIGGER") # Get time when reminder is triggered. # When trigger time is a delta, then calculate the actual time. if type(trigger.dt) == datetime.timedelta: triggerDatetime = eventDatetime + trigger.dt # When trigger time is an actual time, use this. elif type(trigger.dt) == datetime.datetime: triggerDatetime = trigger.dt # Use the same timezone as the event when # no is given. if triggerDatetime.tzinfo is None: triggerDatetime = triggerDatetime.replace( tzinfo=eventDatetime.tzinfo) # When trigger time is only a date, start at midnight, # however, use the same timezone as the event. elif type(trigger.dt) == datetime.date: triggerUTCTime = calendar.timegm(trigger.dt.timetuple()) triggerDatetime = datetime.datetime.utcfromtimestamp( triggerUTCTime) # Since we just overwrite the timezone here, we do not # care about conversion from UTC since the new datetime # object starts at midnight in the given timezone. triggerDatetime = triggerDatetime.replace( tzinfo=eventDatetime.tzinfo) else: logging.error("[%s] Error: Do not know how to handle " % self.fileName + "trigger type '%s'." % trigger.dt.__class__) continue # Uid of event is needed. uid = event.get("UID") # Get time when event starts. dtstart = event.get("DTSTART") # Check if we already triggered an alarm for the event # with this uid and the given alarm trigger time. if (uid, triggerDatetime) in self.alreadyTriggered: break # Check if the alarm trigger time lies in the past but not # more than 1 day. if ((currentDatetime - self.timedelta1day) <= triggerDatetime <= currentDatetime): title = event.get("SUMMARY") # Get description if event has one. evDescription = "" if "DESCRIPTION" in event: evDescription = event.get("DESCRIPTION") # Get location if event has one. location = "" if "LOCATION" in event: location = event.get("LOCATION") # Create the utc unix timestamp for the start of the event. unixStart = datetime.datetime.utcfromtimestamp(0) unixStart = unixStart.replace(tzinfo=pytz.UTC) utcDtstart = int( (eventDatetime - unixStart).total_seconds()) # Create the utc unix timestamp for the reminder trigger. utcTrigger = int( (triggerDatetime - unixStart).total_seconds()) eventDateStr = time.strftime("%D %H:%M:%S", time.localtime(utcDtstart)) msg = "Reminder for event '%s' at %s" % (title, eventDateStr) # Create sensor alert. sensorAlert = SensorObjSensorAlert() sensorAlert.clientSensorId = self.id sensorAlert.state = 1 sensorAlert.hasOptionalData = True sensorAlert.optionalData = { "message": msg, "calendar": self.name, "type": "reminder", "title": title, "description": evDescription, "location": location, "trigger": utcTrigger, "start": utcDtstart } sensorAlert.changeState = False sensorAlert.hasLatestData = False sensorAlert.dataType = SensorDataType.NONE self.reminderAlertQueue.put(sensorAlert) # Store the event uid and the alarm trigger time # as already triggered. self.alreadyTriggered.add((uid, triggerDatetime))
def _processEvent(self, event: icalendar.cal.Event): # Get time when event starts. dtstart = event.get("DTSTART") # Get current time in timezone of event. eventDatetime = None # Create a datetime starting at midnight # if we have an "all day" event. This event is in the local # timezone. if type(dtstart.dt) == datetime.date: eventUTCTime = calendar.timegm(dtstart.dt.timetuple()) eventDatetime = datetime.datetime.utcfromtimestamp(eventUTCTime) # Since we just overwrite the timezone here, we do not # care about conversion from UTC since the new datetime # object starts at midnight in the given timezone. eventDatetime = eventDatetime.replace(tzinfo=dateutil.tz.tzlocal()) # Copy the date time if we have a "normal" event. elif type(dtstart.dt) == datetime.datetime: eventDatetime = dtstart.dt if eventDatetime.tzinfo is None: eventDatetime = eventDatetime.replace(tzinfo=pytz.UTC) else: logging.debug("[%s] Do not know how to handle type '%s' " % (self.fileName, dtstart.dt.__class__) + "of event start.") return # Create current datetime object. currentUTCTime = time.time() currentDatetime = datetime.datetime.utcfromtimestamp(currentUTCTime) currentDatetime = currentDatetime.replace(tzinfo=pytz.UTC) # Process rrule if event has one (rrule means event is repeating). if "RRULE" in event: eventRule = event.get("RRULE") if "UNTIL" in eventRule: # Sometimes the rrule will fail to parse the event rule if # we have a mix of "date times" with timezone and an # "until" without it. if type(eventRule.get("until")[0]) == datetime.datetime: timezone = eventRule.get("until")[0].tzinfo # "RRULE values must be specified in UTC when # DTSTART is timezone-aware" if timezone is None: eventRule["UNTIL"][0] = eventRule["UNTIL"][0].replace( tzinfo=pytz.UTC) # Since date objects do not have a timezone but rrule needs # one, we replace the date object with a datetime object # in UTC time. elif type(eventRule.get("until")[0]) == datetime.date: tempUntil = eventRule.get("until")[0] ruleUTCTime = calendar.timegm(tempUntil.timetuple()) ruleDatetime = datetime.datetime.utcfromtimestamp( ruleUTCTime) ruleDatetime = ruleDatetime.replace(tzinfo=pytz.UTC) eventRule["UNTIL"][0] = ruleDatetime # Use python dateutil for parsing the rrule. eventDatetimeAfter = None eventDatetimeBefore = None try: rrset = dateutil.rrule.rruleset() rule_str = eventRule.to_ical().decode("ascii") rrulestr = dateutil.rrule.rrulestr(rule_str, dtstart=eventDatetime) rrset.rrule(rrulestr) # Get first event that occurs before # and after the current time. eventDatetimeAfter = rrset.after(currentDatetime) eventDatetimeBefore = rrset.before(currentDatetime) except: logging.exception("[%s] Not able to parse rrule for '%s'." % (self.fileName, event.get("SUMMARY"))) # Process the event alarms for the first event occurring after the # current time. if eventDatetimeAfter: self._processEventAlarms(event, eventDatetimeAfter) # Process the event alarms for the first event occurring before the # current time (needed because of edge cases with alarms 0 seconds # before event). But only check event if it is not older than # 10 minutes. if eventDatetimeBefore: if (eventDatetimeBefore >= (currentDatetime - self.timedelta10min)): self._processEventAlarms(event, eventDatetimeBefore) # Process "normal" events. else: # Check if the event is in the past (minus 10 minutes). if eventDatetime <= (currentDatetime - self.timedelta10min): return self._processEventAlarms(event, eventDatetime)