def build_collection(ctx): try: conf = ctx.obj['conf'] selection = ctx.obj.get('calendar_selection', None) props = dict() for name, cal in conf['calendars'].items(): if selection is None or name in ctx.obj['calendar_selection']: props[name] = { 'name': name, 'path': cal['path'], 'readonly': cal['readonly'], 'color': cal['color'], 'ctype': cal['type'], } collection = khalendar.CalendarCollection( calendars=props, color=ctx.obj['conf']['highlight_days']['color'], locale=ctx.obj['conf']['locale'], dbpath=conf['sqlite']['path'], hmethod=ctx.obj['conf']['highlight_days']['method'], default_color=ctx.obj['conf']['highlight_days']['default_color'], multiple=ctx.obj['conf']['highlight_days']['multiple'], highlight_event_days=ctx.obj['conf']['default'] ['highlight_event_days'], ) except FatalError as error: logger.fatal(error) sys.exit(1) collection._default_calendar_name = conf['default']['default_calendar'] return collection
def build_collection(ctx): try: conf = ctx.obj['conf'] selection = ctx.obj.get('calendar_selection', None) props = dict() for name, cal in conf['calendars'].items(): if selection is None or name in ctx.obj['calendar_selection']: props[name] = { 'name': name, 'path': cal['path'], 'readonly': cal['readonly'], 'color': cal['color'], 'ctype': cal['type'], } collection = khalendar.CalendarCollection( calendars=props, color=ctx.obj['conf']['highlight_days']['color'], locale=ctx.obj['conf']['locale'], dbpath=conf['sqlite']['path'], hmethod=ctx.obj['conf']['highlight_days']['method'], default_color=ctx.obj['conf']['highlight_days']['default_color'], multiple=ctx.obj['conf']['highlight_days']['multiple'], highlight_event_days=ctx.obj['conf']['default']['highlight_event_days'], ) except FatalError as error: logger.fatal(error) sys.exit(1) collection._default_calendar_name = conf['default']['default_calendar'] return collection
def build_collection(ctx): try: conf = ctx.obj['conf'] collection = khalendar.CalendarCollection( hmethod=ctx.obj['conf']['highlight_days']['method'], default_color=ctx.obj['conf']['highlight_days']['default_color'], multiple=ctx.obj['conf']['highlight_days']['multiple'], color=ctx.obj['conf']['highlight_days']['color'], highlight_event_days=ctx.obj['conf']['default']['highlight_event_days'], locale=ctx.obj['conf']['locale']) selection = ctx.obj.get('calendar_selection', None) for name, cal in conf['calendars'].items(): if selection is None or name in ctx.obj['calendar_selection']: collection.append(khalendar.Calendar( name=name, dbpath=conf['sqlite']['path'], path=cal['path'], readonly=cal['readonly'], color=cal['color'], unicode_symbols=conf['locale']['unicode_symbols'], locale=conf['locale'], ctype=cal['type'], )) except FatalError as error: logger.fatal(error) sys.exit(1) collection._default_calendar_name = conf['default']['default_calendar'] return collection
def __init__(self, collection, conf, date_list, location=None, repeat=None): try: event = aux.construct_event( date_list, location=location, repeat=repeat, **conf['locale']) except FatalError: sys.exit(1) event = Event(event, collection.default_calendar_name, locale=conf['locale'], ) try: collection.new(event) except ReadOnlyCalendarError: logger.fatal('ERROR: Cannot modify calendar "{}" as it is ' 'read-only'.format(collection.default_calendar_name)) sys.exit(1) if conf['default']['print_new'] == 'event': echo(event.long()) elif conf['default']['print_new'] == 'path': path = collection._calnames[event.calendar].path + event.href echo(path.encode(conf['locale']['encoding']))
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): try: event = aux.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 e: logger.fatal('ERROR: '+str(e)) sys.exit(1) except FatalError: sys.exit(1) event = Event.fromVEvents( [event], calendar=calendar_name, locale=conf['locale']) try: collection.new(event) except ReadOnlyCalendarError: logger.fatal('ERROR: Cannot modify calendar "{}" as it is ' 'read-only'.format(calendar_name)) sys.exit(1) if conf['default']['print_new'] == 'event': if format is None: format = conf['view']['event_format'] echo(event.format(format, datetime.now(), env=env)) elif conf['default']['print_new'] == 'path': path = collection._calnames[event.calendar].path + event.href echo(path) return event
def get_config(config_path=None): """reads the config file, validates it and return a config dict :param config_path: path to a custom config file, if none is given the default locations will be searched :type config_path: str :returns: configuration :rtype: dict """ if config_path is None: config_path = _find_configuration_file() logger.debug('using the config file at {}'.format(config_path)) config = ConfigObj(DEFAULTSPATH, interpolation=False) try: user_config = ConfigObj(config_path, configspec=SPECPATH, interpolation=False, file_error=True, ) except ConfigObjError as error: logger.fatal('parsing the config file file with the following error: ' '{}'.format(error)) logger.fatal('if you recently updated khal, the config file format ' 'might have changed, in that case please consult the ' 'CHANGELOG or other documentation') sys.exit(1) fdict = {'timezone': is_timezone, 'expand_path': expand_path, } validator = Validator(fdict) results = user_config.validate(validator, preserve_errors=True) if not results: for entry in flatten_errors(config, results): # each entry is a tuple section_list, key, error = entry if key is not None: section_list.append(key) else: section_list.append('[missing section]') section_string = ', '.join(section_list) if error is False: error = 'Missing value or section.' print(section_string, ' = ', error) raise ValueError # TODO own error class config.merge(user_config) config_checks(config) extras = get_extra_values(user_config) for section, value in extras: if section == (): logger.warn('unknown section "{}" in config file'.format(value)) else: section = sectionize(section) logger.warn('unknown key or subsection "{}" in ' 'section "{}"'.format(value, section)) return config
def new_from_string(collection, calendar_name, conf, date_list, location=None, repeat=None, until=None): """construct a new event from a string and add it""" try: event = aux.construct_event( date_list, location=location, repeat=repeat, until=until, locale=conf['locale']) except FatalError: sys.exit(1) event = Event.fromVEvents( [event], calendar=calendar_name, locale=conf['locale']) try: collection.new(event) except ReadOnlyCalendarError: logger.fatal('ERROR: Cannot modify calendar "{}" as it is ' 'read-only'.format(calendar_name)) sys.exit(1) if conf['default']['print_new'] == 'event': echo(event.event_description) elif conf['default']['print_new'] == 'path': path = collection._calnames[event.calendar].path + event.href echo(path.encode(conf['locale']['encoding']))
def __init__(self, collection, conf, date_list, location=None, repeat=None): try: event = aux.construct_event( date_list, location=location, repeat=repeat, **conf['locale']) except FatalError: sys.exit(1) event = Event(event, collection.default_calendar_name, locale=conf['locale'], ) try: collection.new(event) except ReadOnlyCalendarError: logger.fatal('ERROR: Cannot modify calendar "{}" as it is ' 'read-only'.format(collection.default_calendar_name)) sys.exit(1) if conf['default']['print_new'] == 'event': print(event.long()) elif conf['default']['print_new'] == 'path': path = collection._calnames[event.calendar].path + event.href print(path.encode(conf['locale']['encoding']))
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): 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 e: logger.fatal('ERROR: ' + str(e)) sys.exit(1) except FatalError: sys.exit(1) event = Event.fromVEvents([event], calendar=calendar_name, locale=conf['locale']) try: collection.new(event) except ReadOnlyCalendarError: logger.fatal( 'ERROR: Cannot modify calendar "{}" as it is read-only'.format( calendar_name)) sys.exit(1) if conf['default']['print_new'] == 'event': if format is None: format = conf['view']['event_format'] echo(event.format(format, datetime.now(), env=env)) elif conf['default']['print_new'] == 'path': path = collection._calnames[event.calendar].path + event.href echo(path) return event
def configwizard(dry_run=False): config_file = settings.find_configuration_file() if not dry_run and config_file is not None: logger.fatal("Found an existing config file at {}.".format(config_file)) logger.fatal( "If you want to create a new configuration file, " "please remove the old one first. Exiting.") sys.exit(1) dateformat = choose_datetime_format() print() timeformat = choose_time_format() print() vdirs = get_vdirs_from_vdirsyncer_config() print() if not vdirs: vdirs = create_vdir() print() calendars = '[calendars]\n' for name, path, type_ in vdirs: calendars += '''[[{name}]] path = {path} type = {type} '''.format(name=name, path=path, type=type_) locale = '''[locale] timeformat = {timeformat} dateformat = {dateformat} longdateformat = {longdateformat} datetimeformat = {dateformat} {timeformat} longdatetimeformat = {longdateformat} {timeformat} '''.format( timeformat=timeformat, dateformat=dateformat, longdateformat=dateformat, ) config = '\n'.join([calendars, locale]) config_path = join(xdg.BaseDirectory.xdg_config_home, 'khal', 'khal.conf') if not confirm( "Do you want to write the config to {}? " "(Choosing `No` will abort)".format(config_path)): print('Aborting...') sys.exit(1) if dry_run: print(config) sys.exit(0) config_dir = join(xdg.BaseDirectory.xdg_config_home, 'khal') if not exists(config_dir) and not isdir(config_dir): makedirs(config_dir) print('created directory {}'.format(config_dir)) with open(config_path, 'w') as config_file: config_file.write(config) print("Successfully wrote configuration to {}".format(config_path))
def validate(conf, logger): """ validate the config """ rval = True cal_name = conf.default.default_calendar if cal_name is True: conf.default.default_calendar = conf.calendars[0].name else: if cal_name not in [cal.name for cal in conf.calendars]: logger.fatal('{} is not a valid calendar'.format(cal_name)) rval = False if rval: return conf else: return None
def configwizard(): config_file = settings.find_configuration_file() if config_file is not None: logger.fatal( "Found an existing config file at {}.".format(config_file)) logger.fatal("If you want to create a new configuration file, " "please remove the old one first. Exiting.") raise FatalError() dateformat = choose_datetime_format() print() timeformat = choose_time_format() print() vdirs = get_vdirs_from_vdirsyncer_config() print() if not vdirs: try: vdirs = create_vdir() except OSError as error: raise FatalError(error) if not vdirs: print( "\nWARNING: no vdir configured, khal will not be usable like this!\n" ) config = create_config(vdirs, dateformat=dateformat, timeformat=timeformat) config_path = join(xdg.BaseDirectory.xdg_config_home, 'khal', 'config') if not confirm("Do you want to write the config to {}? " "(Choosing `No` will abort)".format(config_path), default=True): raise FatalError('User aborted...') config_dir = join(xdg.BaseDirectory.xdg_config_home, 'khal') if not exists(config_dir) and not isdir(config_dir): try: makedirs(config_dir) except OSError as error: print("Could not write config file at {} because of {}. " "Aborting".format(config_dir, error)) raise FatalError(error) else: print('created directory {}'.format(config_dir)) with open(config_path, 'w') as config_file: config_file.write(config) print("Successfully wrote configuration to {}".format(config_path))
def at(ctx, datetime=None): '''Show all events scheduled for DATETIME. if DATETIME is given (or the string `now`) all events scheduled for this moment are shown, if only a time is given, the date is assumed to be today ''' collection = build_collection(ctx) locale = ctx.obj['conf']['locale'] dtime_list = list(datetime) if dtime_list == [] or dtime_list == ['now']: import datetime dtime = datetime.datetime.now() else: try: dtime, _ = aux.guessdatetimefstr(dtime_list, locale) except ValueError: logger.fatal( '{} is not a valid datetime (matches neither {} nor {} nor' ' {})'.format( ' '.join(dtime_list), locale['timeformat'], locale['datetimeformat'], locale['longdatetimeformat'])) sys.exit(1) dtime = locale['local_timezone'].localize(dtime) dtime = dtime.astimezone(pytz.UTC) events = collection.get_events_at(dtime) event_column = list() term_width, _ = get_terminal_size() for event in events: lines = list() items = event.event_description.splitlines() for item in items: lines += textwrap.wrap(item, term_width) event_column.extend( [colored(line, event.color, bold_for_light_color=ctx.obj['conf']['view']['bold_for_light_color']) for line in lines] ) click.echo( '\n'.join(event_column).encode(ctx.obj['conf']['locale']['encoding']) )
def __init__(self, collection, conf, date_list): try: event = aux.construct_event(date_list, **conf["locale"]) except FatalError: sys.exit(1) event = Event( event, collection.default_calendar_name, local_tz=conf["locale"]["local_timezone"], default_tz=conf["locale"]["default_timezone"], ) try: collection.new(event) except ReadOnlyCalendarError: logger.fatal( 'ERROR: Cannot modify calendar "{}" as it is ' "read-only".format(collection.default_calendar_name) ) sys.exit(1)
def configwizard(): config_file = settings.find_configuration_file() if config_file is not None: logger.fatal("Found an existing config file at {}.".format(config_file)) logger.fatal( "If you want to create a new configuration file, " "please remove the old one first. Exiting.") sys.exit(1) dateformat = choose_datetime_format() print() timeformat = choose_time_format() print() vdirs = get_vdirs_from_vdirsyncer_config() print() if not vdirs: try: vdirs = [create_vdir()] except OSError as error: sys.exit(1) config = create_config(vdirs, dateformat=dateformat, timeformat=timeformat) config_path = join(xdg.BaseDirectory.xdg_config_home, 'khal', 'config') if not confirm( "Do you want to write the config to {}? " "(Choosing `No` will abort)".format(config_path)): print('Aborting...') sys.exit(1) config_dir = join(xdg.BaseDirectory.xdg_config_home, 'khal') if not exists(config_dir) and not isdir(config_dir): try: makedirs(config_dir) except OSError as error: print( "Could not write config file at {} because of {}. " "Aborting".format(config_dir, error) ) sys.exit(1) else: print('created directory {}'.format(config_dir)) with open(config_path, 'w') as config_file: config_file.write(config) print("Successfully wrote configuration to {}".format(config_path))
def __init__(self, collection, conf, date_list, location=None): try: event = aux.construct_event( date_list, location=location, **conf['locale']) except FatalError: sys.exit(1) event = Event(event, collection.default_calendar_name, local_tz=conf['locale']['local_timezone'], default_tz=conf['locale']['default_timezone'], ) try: collection.new(event) except ReadOnlyCalendarError: logger.fatal('ERROR: Cannot modify calendar "{}" as it is ' 'read-only'.format(collection.default_calendar_name)) sys.exit(1)
def at(ctx, datetime=None): '''Show all events scheduled for DATETIME. if DATETIME is given (or the string `now`) all events scheduled for this moment are shown, if only a time is given, the date is assumed to be today ''' collection = build_collection(ctx) locale = ctx.obj['conf']['locale'] dtime_list = list(datetime) if dtime_list == [] or dtime_list == ['now']: import datetime dtime = datetime.datetime.now() else: try: dtime, _ = aux.guessdatetimefstr(dtime_list, locale) except ValueError: logger.fatal( '{} is not a valid datetime (matches neither {} nor {} nor' ' {})'.format(' '.join(dtime_list), locale['timeformat'], locale['datetimeformat'], locale['longdatetimeformat'])) sys.exit(1) dtime = locale['local_timezone'].localize(dtime) dtime = dtime.astimezone(pytz.UTC) events = collection.get_events_at(dtime) event_column = list() term_width, _ = get_terminal_size() for event in events: lines = list() items = event.event_description.splitlines() for item in items: lines += textwrap.wrap(item, term_width) event_column.extend([ colored(line, event.color, bold_for_light_color=ctx.obj['conf']['view'] ['bold_for_light_color']) for line in lines ]) click.echo('\n'.join(event_column).encode( ctx.obj['conf']['locale']['encoding']))
def build_collection(ctx): try: conf = ctx.obj['conf'] collection = khalendar.CalendarCollection() selection = ctx.obj.get('calendar_selection', None) for name, cal in conf['calendars'].items(): if selection is None or name in ctx.obj['calendar_selection']: collection.append(khalendar.Calendar( name=name, dbpath=conf['sqlite']['path'], path=cal['path'], readonly=cal['readonly'], color=cal['color'], unicode_symbols=conf['locale']['unicode_symbols'], locale=conf['locale'], ctype=cal['type'], )) except FatalError as error: logger.fatal(error) sys.exit(1) collection._default_calendar_name = conf['default']['default_calendar'] return collection
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, until = 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 and repeat != "none": if repeat in ["daily", "weekly", "monthly", "yearly"]: rrule_settings = {'freq': repeat} if until : try: # first two elements are a date and a time until_date = datetimefstr(until, datetimeformat, longdatetimeformat) except ValueError: try: # first element is a time until_date = timefstr(until, timeformat) except ValueError: try: # first element is a date (and since second isn't a time this # is an all-day-event until_date = datetimefstr(until, dateformat, longdateformat) except ValueError: logger.fatal("Cannot parse until date: '{}'\nPlease have a look at " "the documentation.".format(until)) raise FatalError() rrule_settings['until'] = until_date event.add('rrule', rrule_settings) 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 construct_event(dtime_list, locale, defaulttimelen=60, defaultdatelen=1, encoding='utf-8', description=None, location=None, repeat=None, until=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 """ # TODO remove if this survives for some time in the wild without getting any reports first_type = type(dtime_list[0]) try: for part in dtime_list: assert first_type == type(part) except AssertionError: logger.error( "An internal error occured, please report the below error message " "to khal's developers at https://github.com/geier/khal/issues or " "via email at [email protected]") logger.error(' '.join(['{} ({})'.format(part, type(part)) for part in dtime_list])) today = datetime.today() try: dtstart, all_day = guessdatetimefstr(dtime_list, locale) except ValueError: logger.fatal("Cannot parse: '{}'\nPlease have a look at " "the documentation.".format(' '.join(dtime_list))) raise FatalError() try: dtend, _ = guessdatetimefstr(dtime_list, locale, dtstart) except ValueError: if all_day: dtend = dtstart + timedelta(days=defaultdatelen - 1) else: dtend = dtstart + timedelta(minutes=defaulttimelen) if all_day: dtend += timedelta(days=1) # 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]) 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(dtime_list[0]).localize(dtstart) dtend = pytz.timezone(dtime_list[0]).localize(dtend) dtime_list.pop(0) except (pytz.UnknownTimeZoneError, UnicodeDecodeError): dtstart = locale['default_timezone'].localize(dtstart) dtend = locale['default_timezone'].localize(dtend) event = icalendar.Event() text = ' '.join(dtime_list) 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 and repeat != "none": 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, 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 event.add('rrule', rrule_settings) 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 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