def rrulefstr(repeat, until, locale): if repeat in ["daily", "weekly", "monthly", "yearly"]: rrule_settings = {'freq': repeat} if until: until_date = None for fun, dformat in [(datetimefstr, locale['datetimeformat']), (datetimefstr, locale['longdatetimeformat']), (timefstr, locale['timeformat']), (datetimefstr, locale['dateformat']), (datetimefstr, locale['longdateformat'])]: try: until_date = fun(until.split(' '), dformat) break except ValueError: pass if until_date is None: logger.fatal("Cannot parse until date: '{}'\nPlease have a look " "at the documentation.".format(until)) raise FatalError() rrule_settings['until'] = until_date return rrule_settings else: logger.fatal("Invalid value for the repeat option. \ Possible values are: daily, weekly, monthly or yearly") raise FatalError()
def new_from_args(collection, calendar_name, conf, dtstart=None, dtend=None, summary=None, description=None, allday=None, location=None, categories=None, repeat=None, until=None, alarms=None, timezone=None, format=None, env=None): """Create a new event from arguments and add to vdirs""" try: event = utils.new_event( locale=conf['locale'], location=location, categories=categories, repeat=repeat, until=until, alarms=alarms, dtstart=dtstart, dtend=dtend, summary=summary, description=description, timezone=timezone, ) except ValueError as error: raise FatalError(error) event = Event.fromVEvents( [event], calendar=calendar_name, locale=conf['locale']) try: collection.new(event) except ReadOnlyCalendarError: raise FatalError( 'ERROR: Cannot modify calendar "{}" as it is read-only'.format(calendar_name) ) if conf['default']['print_new'] == 'event': if format is None: format = conf['view']['event_format'] echo(event.format(format, dt.datetime.now(), env=env)) elif conf['default']['print_new'] == 'path': path = os.path.join( collection._calendars[event.calendar]['path'], event.href ) echo(path) return event
def calendar(collection, agenda_format=None, notstarted=False, once=False, daterange=None, day_format=None, locale=None, conf=None, firstweekday=0, weeknumber=False, monthdisplay='firstday', hmethod='fg', default_color='', multiple='', color='', highlight_event_days=0, full=False, bold_for_light_color=True, env=None, ): term_width, _ = get_terminal_size() lwidth = 27 if conf['locale']['weeknumbers'] == 'right' else 25 rwidth = term_width - lwidth - 4 try: start, end = start_end_from_daterange( daterange, locale, default_timedelta_date=conf['default']['timedelta'], default_timedelta_datetime=conf['default']['timedelta'], ) except ValueError as error: raise FatalError(error) event_column = khal_list( collection, daterange, conf=conf, agenda_format=agenda_format, day_format=day_format, once=once, notstarted=notstarted, width=rwidth, env=env, ) calendar_column = calendar_display.vertical_month( month=start.month, year=start.year, count=max(3, (end.year - start.year) * 12 + end.month - start.month + 1), firstweekday=firstweekday, weeknumber=weeknumber, monthdisplay=monthdisplay, collection=collection, hmethod=hmethod, default_color=default_color, multiple=multiple, color=color, highlight_event_days=highlight_event_days, locale=locale, bold_for_light_color=bold_for_light_color) return merge_columns(calendar_column, event_column, width=lwidth)
def rrulefstr(repeat, until, locale): if repeat in ["daily", "weekly", "monthly", "yearly"]: rrule_settings = {'freq': repeat} if until: until_dt, is_date = guessdatetimefstr(until.split(' '), locale) rrule_settings['until'] = until_dt return rrule_settings else: logger.fatal("Invalid value for the repeat option. \ Possible values are: daily, weekly, monthly or yearly") raise FatalError()
def khal_list(collection, daterange=None, conf=None, agenda_format=None, day_format=None, once=False, notstarted=False, width=False, env=None, datepoint=None): assert daterange is not None or datepoint is not None """returns a list of all events in `daterange`""" # because empty strings are also Falsish if agenda_format is None: agenda_format = conf['view']['agenda_event_format'] if daterange is not None: if day_format is None: day_format = conf['view']['agenda_day_format'] start, end = start_end_from_daterange( daterange, conf['locale'], default_timedelta_date=conf['default']['timedelta'], default_timedelta_datetime=conf['default']['timedelta'], ) logger.debug('Getting all events between {} and {}'.format(start, end)) elif datepoint is not None: if not datepoint: datepoint = ['now'] try: start, allday = parse_datetime.guessdatetimefstr( datepoint, conf['locale'], dt.date.today(), ) except ValueError: raise FatalError('Invalid value of `{}` for a datetime'.format( ' '.join(datepoint))) if allday: logger.debug('Got date {}'.format(start)) raise FatalError('Please supply a datetime, not a date.') end = start + dt.timedelta(seconds=1) if day_format is None: day_format = style( start.strftime(conf['locale']['longdatetimeformat']), bold=True, ) logger.debug('Getting all events between {} and {}'.format(start, end)) event_column = [] once = set() if once else None if env is None: env = {} original_start = conf['locale']['local_timezone'].localize(start) while start < end: if start.date() == end.date(): day_end = end else: day_end = dt.datetime.combine(start.date(), dt.time.max) current_events = get_events_between( collection, locale=conf['locale'], agenda_format=agenda_format, start=start, end=day_end, notstarted=notstarted, original_start=original_start, env=env, seen=once, width=width, ) if day_format and (conf['default']['show_all_days'] or current_events): event_column.append( format_day(start.date(), day_format, conf['locale'])) event_column.extend(current_events) start = dt.datetime(*start.date().timetuple()[:3]) + dt.timedelta( days=1) if event_column == []: event_column = [style('No events', bold=True)] return event_column
def get_events_between(collection, locale, start, end, agenda_format=None, notstarted=False, env=None, width=None, seen=None, original_start=None): """returns a list of events scheduled between start and end. Start and end are strings or datetimes (of some kind). :param collection: :type collection: khalendar.CalendarCollection :param start: the start datetime :param end: the end datetime :param agenda_format: a format string that can be used in python string formatting :type agenda_format: str :param env: a collection of "static" values like calendar names and color :type env: dict :param nostarted: True if each event should start after start (instead of be active between start and end) :type nostarted: bool :param original_start: start datetime to compare against of notstarted is set :type original_start: datetime.datetime :returns: a list to be printed as the agenda for the given days :rtype: list(str) """ assert not (notstarted and not original_start) event_list = [] if env is None: env = {} assert start assert end start_local = locale['local_timezone'].localize(start) end_local = locale['local_timezone'].localize(end) start = start_local.replace(tzinfo=None) end = end_local.replace(tzinfo=None) events = sorted(collection.get_localized(start_local, end_local)) events_float = sorted(collection.get_floating(start, end)) events = sorted(events + events_float) for event in events: # yes the logic could be simplified, but I believe it's easier # to understand what's going on here this way if notstarted: if event.allday and event.start < original_start.date(): continue elif not event.allday and event.start_local < original_start: continue if seen is not None and event.uid in seen: continue try: event_string = event.format(agenda_format, relative_to=(start, end), env=env) except KeyError as error: raise FatalError(error) if width: event_list += utils.color_wrap(event_string, width) else: event_list.append(event_string) if seen is not None: seen.add(event.uid) return event_list
def guessrangefstr( daterange, locale, default_timedelta_date=dt.timedelta(days=1), default_timedelta_datetime=dt.timedelta(hours=1), adjust_reasonably=False, ): """parses a range string :param daterange: date1 [date2 | timedelta] :type daterange: str or list :param locale: :returns: start and end of the date(time) range and if this is an all-day time range or not, **NOTE**: the end is *exclusive* if this is an allday event :rtype: (datetime, datetime, bool) """ range_list = daterange if isinstance(daterange, str): range_list = daterange.split(' ') if range_list == ['week']: today_weekday = dt.datetime.today().weekday() start = dt.datetime.today() - dt.timedelta( days=(today_weekday - locale['firstweekday'])) end = start + dt.timedelta(days=8) return start, end, True for i in reversed(range(1, len(range_list) + 1)): start = ' '.join(range_list[:i]) end = ' '.join(range_list[i:]) allday = False try: # figuring out start split = start.split(" ") start, allday = guessdatetimefstr(split, locale) if len(split) != 0: continue # and end if len(end) == 0: if allday: end = start + default_timedelta_date else: end = start + default_timedelta_datetime elif end.lower() == 'eod': end = dt.datetime.combine(start.date(), dt.time.max) elif end.lower() == 'week': start -= dt.timedelta(days=(start.weekday() - locale['firstweekday'])) end = start + dt.timedelta(days=8) else: try: delta = guesstimedeltafstr(end) if allday and delta.total_seconds() % (3600 * 24): # TODO better error class, no logging in here logger.fatal( "Cannot give delta containing anything but whole days for allday events" ) raise FatalError() elif delta.total_seconds() == 0: logger.fatal( "Events that last no time are not allowed") raise FatalError() end = start + delta except (ValueError, DateTimeParseError): split = end.split(" ") end, end_allday = guessdatetimefstr( split, locale, default_day=start.date(), in_future=False) if len(split) != 0: continue if allday: end += dt.timedelta(days=1) if adjust_reasonably: if allday: # test if end's year is this year, but start's year is not today = dt.datetime.today() if end.year == today.year and start.year != today.year: end = dt.datetime(start.year, *end.timetuple()[1:6]) if end < start: end = dt.datetime(end.year + 1, *end.timetuple()[1:6]) if end < start: end = dt.datetime(*start.timetuple()[0:3] + end.timetuple()[3:5]) if end < start: end = end + dt.timedelta(days=1) return start, end, allday except (ValueError, DateTimeParseError): pass raise DateTimeParseError( "Could not parse \"{}\".\nPlease check your configuration or run " "`khal printformats` to see if this does match your configured " "[long](date|time|datetime)format.\nIf you suspect a bug, please " "file an issue at https://github.com/pimutils/khal/issues/ " "".format(daterange))
def construct_event(date_list, timeformat, dateformat, longdateformat, datetimeformat, longdatetimeformat, default_timezone, defaulttimelen=60, defaultdatelen=1, encoding='utf-8', description=None, location=None, repeat=None, _now=datetime.now, **kwargs): """takes a list of strings and constructs a vevent from it :param encoding: the encoding of your terminal, should be a valid encoding :type encoding: str :param _now: function that returns now, used for testing the parts of the list can be either of these: * datetime datetime description start and end datetime specified, if no year is given, this year is used, if the second datetime has no year, the same year as for the first datetime object will be used, unless that would make the event end before it begins, in which case the next year is used * datetime time description end date will be same as start date, unless that would make the event end before it has started, then the next day is used as end date * datetime description event will last for defaulttime * time time description event starting today at the first time and ending today at the second time, unless that would make the event end before it has started, then the next day is used as end date * time description event starting today at time, lasting for the default length * date date description all day event starting on the first and ending on the last event * date description all day event starting at given date and lasting for default length datetime should match datetimeformat or longdatetimeformat time should match timeformat where description is the unused part of the list see tests for examples """ today = datetime.today() all_day = False # looking for start datetime try: # first two elements are a date and a time dtstart = datetimefstr(date_list, datetimeformat, longdatetimeformat) except ValueError: try: # first element is a time dtstart = timefstr(date_list, timeformat) except ValueError: try: # first element is a date (and since second isn't a time this # is an all-day-event dtstart = datetimefstr(date_list, dateformat, longdateformat) all_day = True except ValueError: logger.fatal("Cannot parse: '{}'\nPlease have a look at " "the documentation.".format(' '.join(date_list))) raise FatalError() # now looking for the end if all_day: try: # second element must be a date, too dtend = datetimefstr(date_list, dateformat, longdateformat) dtend = dtend + timedelta(days=1) except ValueError: # if it isn't we expect it to be the summary and use defaultdatelen # as event length dtend = dtstart + timedelta(days=defaultdatelen) # test if dtend's year is this year, but dtstart's year is not if dtend.year == today.year and dtstart.year != today.year: dtend = datetime(dtstart.year, *dtend.timetuple()[1:6]) if dtend < dtstart: dtend = datetime(dtend.year + 1, *dtend.timetuple()[1:6]) else: try: # next element datetime dtend = datetimefstr(date_list, datetimeformat, longdatetimeformat, dtstart.year) except ValueError: try: # next element time only dtend = timefstr(date_list, timeformat) dtend = datetime(*(dtstart.timetuple()[:3] + dtend.timetuple()[3:5])) except ValueError: dtend = dtstart + timedelta(minutes=defaulttimelen) if dtend < dtstart: dtend = datetime(*dtstart.timetuple()[0:3] + dtend.timetuple()[3:5]) if dtend < dtstart: dtend = dtend + timedelta(days=1) if all_day: dtstart = dtstart.date() dtend = dtend.date() else: try: # next element is a valid Olson db timezone string dtstart = pytz.timezone(date_list[0]).localize(dtstart) dtend = pytz.timezone(date_list[0]).localize(dtend) date_list.pop(0) except (pytz.UnknownTimeZoneError, UnicodeDecodeError): dtstart = default_timezone.localize(dtstart) dtend = default_timezone.localize(dtend) event = icalendar.Event() text = to_unicode(' '.join(date_list), encoding) if not description or not location: summary = text.split(' :: ', 1)[0] try: description = text.split(' :: ', 1)[1] except IndexError: pass else: summary = text if description: event.add('description', description) if location: event.add('location', location) if repeat: if repeat in ["daily", "weekly", "monthly", "yearly"]: event.add('rrule', {'freq': repeat}) else: logger.fatal("Invalid value for the repeat option. \ Possible values are: daily, weekly, monthly or yearly") raise FatalError() event.add('dtstart', dtstart) event.add('dtend', dtend) event.add('dtstamp', _now()) event.add('summary', summary) event.add('uid', generate_random_uid()) # TODO add proper UID return event
def guessrangefstr( daterange, locale, adjust_reasonably=False, default_timedelta_date=timedelta(days=1), default_timedelta_datetime=timedelta(hours=1), ): """parses a range string :param daterange: date1 [date2 | timedelta] :type daterange: str or list :param locale: :returns: start and end of the date(time) range and if this is an all-day time range or not, **NOTE**: the end is *exclusive* if this is an allday event :rtype: (datetime, datetime, bool) """ range_list = daterange if isinstance(daterange, str): range_list = daterange.split(' ') if range_list == ['week']: today_weekday = datetime.today().weekday() start = datetime.today() - timedelta(days=(today_weekday - locale['firstweekday'])) end = start + timedelta(days=8) return start, end, True for i in reversed(range(1, len(range_list) + 1)): start = ' '.join(range_list[:i]) end = ' '.join(range_list[i:]) allday = False try: # figuring out start if len(start) == 0: raise # used to be: start = datetime_fillin(end=False) else: split = start.split(" ") start, allday = guessdatetimefstr(split, locale) if len(split) != 0: continue # and end if len(end) == 0: if allday: end = start + default_timedelta_date else: end = start + default_timedelta_datetime elif end.lower() == 'eod': end = datetime.combine(start.date(), time.max) elif end.lower() == 'week': start -= timedelta(days=(start.weekday() - locale['firstweekday'])) end = start + timedelta(days=8) else: try: delta = guesstimedeltafstr(end) if allday and delta.total_seconds() % (3600 * 24): # TODO better error class, no logging in here logger.fatal( "Cannot give delta containing anything but whole days for allday events" ) raise FatalError() elif delta.total_seconds() == 0: logger.fatal( "Events that last no time are not allowed") raise FatalError() end = start + delta except ValueError: split = end.split(" ") end, end_allday = guessdatetimefstr( split, locale, default_day=start.date()) if len(split) != 0: continue if allday: end += timedelta(days=1) if adjust_reasonably: if allday: # test if end's year is this year, but start's year is not today = datetime.today() if end.year == today.year and start.year != today.year: end = datetime(start.year, *end.timetuple()[1:6]) if end < start: end = datetime(end.year + 1, *end.timetuple()[1:6]) if end < start: end = datetime(*start.timetuple()[0:3] + end.timetuple()[3:5]) if end < start: end = end + timedelta(days=1) return start, end, allday except ValueError: pass raise ValueError('Could not parse `{}` as a daterange'.format(daterange))
def new_from_args(collection, calendar_name, conf, dtstart=None, dtend=None, summary=None, description=None, allday=None, location=None, categories=None, repeat=None, until=None, alarms=None, timezone=None, url=None, format=None, json=None, env=None): """Create a new event from arguments and add to vdirs""" if isinstance(categories, str): categories = [category.strip() for category in categories.split(',')] try: event = new_vevent( locale=conf['locale'], location=location, categories=categories, repeat=repeat, until=until, alarms=alarms, dtstart=dtstart, dtend=dtend, summary=summary, description=description, timezone=timezone, url=url, ) except ValueError as error: raise FatalError(error) event = Event.fromVEvents([event], calendar=calendar_name, locale=conf['locale']) try: collection.new(event) except ReadOnlyCalendarError: raise FatalError( f'ERROR: Cannot modify calendar "{calendar_name}" as it is read-only' ) if conf['default']['print_new'] == 'event': if json is None or len(json) == 0: if format is None: format = conf['view']['event_format'] formatter = human_formatter(format) else: formatter = json_formatter(json) echo(formatter(event.attributes(dt.datetime.now(), env=env))) elif conf['default']['print_new'] == 'path': path = os.path.join(collection._calendars[event.calendar]['path'], event.href) echo(path) return event