diff options
Diffstat (limited to 'calendar-cli.py')
-rwxr-xr-x | calendar-cli.py | 103 |
1 files changed, 71 insertions, 32 deletions
diff --git a/calendar-cli.py b/calendar-cli.py index 9398942..8368b69 100755 --- a/calendar-cli.py +++ b/calendar-cli.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2 +#!/usr/bin/env python2 """ calendar-cli.py - high-level cli against caldav servers @@ -33,6 +33,7 @@ import os import logging import sys import re +import urllib3 __version__ = "0.11.0.dev0" __author__ = "Tobias Brox" @@ -47,6 +48,14 @@ __status__ = "Development" __product__ = "calendar-cli" __description__ = "high-level cli against caldav servers" +def _date(ts): + """ + helper function to get a date out of a Date or Datetime object. + """ + if hasattr(ts, 'date'): + return ts.date() + return ts + def _force_datetime(t, args): """ date objects cannot be compared with timestamp objects, neither in python2 nor python3. Silly. @@ -131,9 +140,25 @@ def _calendar_addics(caldav_conn, ics, uid, args): raise ValueError("Nothing to do/invalid option combination for 'calendar add'-mode; either both --icalendar and --nocaldav should be set, or none of them") return - c = find_calendar(caldav_conn, args) - c.add_event(ics) - + try: + c = find_calendar(caldav_conn, args) + if re.search(r'^METHOD:[A-Z]+[\r\n]+',ics,flags=re.MULTILINE) and args.ignoremethod: + ics = re.sub(r'^METHOD:[A-Z]+[\r\n]+', '', ics, flags=re.MULTILINE) + print ("METHOD property found and ignored") + c.add_event(ics) + except caldav.lib.error.AuthorizationError as e: + print("Error logging in"); + sys.exit(2) + """ + Peter Havekes: This needs more checking. It works for me when connecting to O365 + + except caldav.lib.error.PutError as e: + if "200 OK" in str(e): + print("Duplicate") + else: + raise + """ + def calendar_addics(caldav_conn, args): """ Takes an ics from external source and puts it into the calendar. @@ -158,10 +183,10 @@ def calendar_addics(caldav_conn, args): ## since the icalendar library doesn't offer methods out of the ## hat for doing such kind of things entries = c.subcomponents - + ## Timezones should be duplicated into each ics, ref the RFC timezones = [x for x in entries if x.name == 'VTIMEZONE'] - + ## Make a mapping from UID to the other components uids = {} for x in entries: @@ -176,12 +201,12 @@ def calendar_addics(caldav_conn, args): def interactive_config(args, config, remaining_argv): import readline - + new_config = False section = 'default' backup = {} modified = False - + print("Welcome to the interactive calendar configuration mode") print("Warning - untested code ahead, raise issues at t-calendar-cli@tobixen.no") if not config or not hasattr(config, 'keys'): @@ -240,7 +265,7 @@ def interactive_config(args, config, remaining_argv): os.rename(args.config_file, "%s.%s.bak" % (args.config_file, int(time.time()))) with open(args.config_file, 'w') as outfile: json.dump(config, outfile, indent=4) - + if args.config_section == 'default' and section != 'default': config['default'] = config[section] @@ -252,7 +277,7 @@ def create_alarm(message, relative_timedelta): alarm.add('DESCRIPTION', message) alarm.add('TRIGGER', relative_timedelta, parameters={'VALUE':'DURATION'}) return alarm - + def calendar_add(caldav_conn, args): cal = Calendar() cal.add('prodid', '-//{author_short}//{product}//{language}'.format(author_short=__author_short__, product=__product__, language=args.language)) @@ -278,9 +303,18 @@ def calendar_add(caldav_conn, args): ## TODO: error handling event_duration_secs = int(event_duration[:-1]) * time_units[event_duration[-1:]] dtstart = dateutil.parser.parse(event_spec[0]) - event.add('dtstart', dtstart) - ## TODO: handle duration and end-time as options. default 3600s by now. - event.add('dtend', dtstart + timedelta(0,event_duration_secs)) + if args.whole_day: + if event_spec[1][-1:] != 'd': + raise ValueError('Duration of whole-day event must be multiple of 1d') + duration = int(event_spec[1][:-1]) + dtstart = dateutil.parser.parse(event_spec[0]) + dtend = dtstart + timedelta(days=duration) + event.add('dtstart', _date(dtstart.date())) + event.add('dtend', _date(dtend.date())) + else: + event.add('dtstart', dtstart) + ## TODO: handle duration and end-time as options. default 3600s by now. + event.add('dtend', dtstart + timedelta(0,event_duration_secs)) ## TODO: what does the cryptic comment here really mean, and why was the dtstamp commented out? dtstamp is required according to the RFC. ## not really correct, and it breaks i.e. with google calendar event.add('dtstamp', _now()) @@ -339,7 +373,7 @@ def journal_add(caldav_conn, args): _calendar_addics(caldav_conn, cal.to_ical(), uid, args) print("Added journal item with uid=%s" % uid) ## FULL STOP - should do some major refactoring before doing more work here! - + def todo_add(caldav_conn, args): ## TODO: copied from calendar_add, should probably be consolidated if args.icalendar or args.nocaldav: @@ -372,7 +406,7 @@ def todo_add(caldav_conn, args): rt.params['RELTYPE']=['CHILD'] rt.value = str(uid) t.save() - + for attr in vtodo_txt_one: if attr == 'summary': continue @@ -420,7 +454,7 @@ def calendar_agenda(caldav_conn, args): events = [] if args.icalendar: for ical in events_: - print(ical.data) + print(ical.data).encode('utf-8').strip() else: ## flatten. A recurring event may be a list of events. ## jeez ... zimbra and DaviCal does completely different things here @@ -521,8 +555,8 @@ def todo_edit(caldav_conn, args): ## you may now access task.data to edit the raw ical, or ## task.instance.vtodo to edit a vobject instance task.save() - - + + def todo_postpone(caldav_conn, args): if args.nocaldav: raise ValueError("No caldav connection, aborting") @@ -535,8 +569,8 @@ def todo_postpone(caldav_conn, args): else: new_ts = dateutil.parser.parse(args.until) if not new_ts.time(): - new_ts = new_ts.date() - + new_ts = _date(new_ts) + tasks = todo_select(caldav_conn, args) for task in tasks: if new_ts: @@ -553,12 +587,12 @@ def todo_postpone(caldav_conn, args): if type(task.instance.vtodo.dtstart.value) != type(task.instance.vtodo.due.value): ## RFC states they must be of the same type if isinstance(task.instance.vtodo.dtstart.value, date): - task.instance.vtodo.due.value = task.instance.vtodo.due.value.date() + task.instance.vtodo.due.value = _date(task.instance.vtodo.due.value) else: d = task.instance.vtodo.due.value task.instance.vtodo.due.value = datetime(d.year, d.month, d.day) ## RFC also states that due cannot be before dtstart (and that makes sense) - if task.instance.vtodo.dtstart.value > task.instance.vtodo.due.value: + if _force_datetime(task.instance.vtodo.dtstart.value, args) > _force_datetime(task.instance.vtodo.due.value, args): task.instance.vtodo.due.value = task.instance.vtodo.dtstart.value task.save() @@ -638,7 +672,7 @@ def todo_complete(caldav_conn, args): continue task.complete() - + def todo_delete(caldav_conn, args): if args.nocaldav: @@ -646,7 +680,7 @@ def todo_delete(caldav_conn, args): tasks = todo_select(caldav_conn, args) for task in tasks: task.delete() - + def config_section(config, section='default'): if section in config and 'inherits' in config[section]: ret = config_section(config, config[section]['inherits']) @@ -655,13 +689,13 @@ def config_section(config, section='default'): if section in config: ret.update(config[section]) return ret - + def main(): """ the main function does (almost) nothing but parsing command line parameters """ ## This boilerplate pattern is from - ## http://stackoverflow.com/questions/3609852 + ## http://stackoverflow.com/questions/3609852 ## We want defaults for the command line options to be fetched from the config file # Parse any conf_file specification @@ -732,6 +766,7 @@ def main(): parser.add_argument("--ssl-verify-cert", help="verification of the SSL cert - 'yes' to use the OS-provided CA-bundle, 'no' to trust any cert and the path to a CA-bundle") parser.add_argument("--debug-logging", help="turn on debug logging", action="store_true") parser.add_argument("--calendar-url", help="URL for calendar to be used (may be absolute or relative to caldav URL, or just the name of the calendar)") + parser.add_argument("--ignoremethod", help="Ignores METHOD property if exists in the request. This violates RFC4791 but is sometimes appended by some calendar servers", action="store_true") ## TODO: check sys.argv[0] to find command ## TODO: set up logging @@ -754,8 +789,8 @@ def main(): for attr in vtodo_txt_one + vtodo_txt_many: todo_parser.add_argument('--no'+attr, help="for filtering tasks", action='store_true') - - #todo_parser.add_argument('--priority', ....) + + #todo_parser.add_argument('--priority', ....) #todo_parser.add_argument('--sort-by', ....) #todo_parser.add_argument('--due-before', ....) todo_subparsers = todo_parser.add_subparsers(title='tasks subcommand') @@ -772,12 +807,12 @@ def main(): help="specifies a time at which a reminder should be presented for this task, " \ "relative to the start time of the task (as a timestamp delta)") todo_add_parser.set_defaults(func=todo_add) - + todo_list_parser = todo_subparsers.add_parser('list') todo_list_parser.add_argument('--todo-template', help="Template for printing out the event", default="{dtstart}{dtstart_passed_mark} {due}{due_passed_mark} {summary}") todo_list_parser.add_argument('--default-due', help="Default number of days from a task is submitted until it's considered due", type=int, default=14) todo_list_parser.add_argument('--list-categories', help="Instead of listing the todo-items, list the unique categories used", action='store_true') - todo_list_parser.add_argument('--timestamp-format', help="strftime-style format string for the output timestamps", default="%F (%a)") + todo_list_parser.add_argument('--timestamp-format', help="strftime-style format string for the output timestamps", default="%Y-%m-%d (%a)") todo_list_parser.set_defaults(func=todo_list) todo_edit_parser = todo_subparsers.add_parser('edit') @@ -801,7 +836,7 @@ def main(): ## journal journal_parser = subparsers.add_parser('journal') - journal_subparsers = journal_parser.add_subparsers(title='tasks subcommand') + journal_subparsers = journal_parser.add_subparsers(title='journal subcommand') journal_add_parser = journal_subparsers.add_parser('add') journal_add_parser.add_argument('summaryline', nargs='+') journal_add_parser.set_defaults(func=journal_add) @@ -812,6 +847,7 @@ def main(): calendar_add_parser.add_argument('event_time', help="Timestamp and duration of the event. See the documentation for event_time specifications") calendar_add_parser.add_argument('summary', nargs='+') calendar_add_parser.set_defaults(func=calendar_add) + calendar_add_parser.add_argument('--whole-day', help='Whole-day event', action='store_true', default=False) for attr in vcal_txt_one + vcal_txt_many: calendar_add_parser.add_argument('--set-'+attr, help='Set '+attr) @@ -826,7 +862,7 @@ def main(): calendar_agenda_parser.add_argument('--agenda-mins', help="Fetch calendar for so many minutes", type=int) calendar_agenda_parser.add_argument('--agenda-days', help="Fetch calendar for so many days", type=int, default=7) calendar_agenda_parser.add_argument('--event-template', help="Template for printing out the event", default="{dstart} {summary}") - calendar_agenda_parser.add_argument('--timestamp-format', help="strftime-style format string for the output timestamps", default="%F %H:%M (%a)") + calendar_agenda_parser.add_argument('--timestamp-format', help="strftime-style format string for the output timestamps", default="%Y-%m-%d %H:%M (%a)") calendar_agenda_parser.set_defaults(func=calendar_agenda) calendar_delete_parser = calendar_subparsers.add_parser('delete') @@ -841,6 +877,9 @@ def main(): caldav_conn = caldav_connect(args) else: caldav_conn = None + + if args.ssl_verify_cert == 'no': + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) ret = args.func(caldav_conn, args) |