diff --git a/requirements.txt b/requirements.txt index b5233af..8562769 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ Click==7.0 -todoist-python==7.0.18 +todoist-python==8.0.0 # Temporarily use this until upstream PR #121 is merged # https://github.com/ralphbean/taskw/pull/121 diff --git a/tests/test_recur.py b/tests/test_recur.py index 93cebce..88329ce 100644 --- a/tests/test_recur.py +++ b/tests/test_recur.py @@ -7,60 +7,68 @@ from todoist_taskwarrior import utils from todoist_taskwarrior import errors +def test_parse_due_object(): + assert utils.parse_recur(None) == None + assert utils.parse_recur({'is_recurring': False}) == None + assert utils.parse_recur({'is_recurring': True, 'string': 'every day'}) == 'daily' + assert utils.parse_recur({'is_recurring': True, 'string': 'every week'}) == 'weekly' + assert utils.parse_recur({'is_recurring': True, 'string': 'every month'}) == 'monthly' + + def test_hourly(): - assert utils.parse_recur('every hour') == 'hourly' - assert utils.parse_recur('every 1 hour') == 'hourly' - assert utils.parse_recur('every 2 hour') == '2 hours' - assert utils.parse_recur('every 2 hours') == '2 hours' - assert utils.parse_recur('every 3 hour') == '3 hours' - assert utils.parse_recur('every 3 hours') == '3 hours' + assert utils.parse_recur_string('every hour') == 'hourly' + assert utils.parse_recur_string('every 1 hour') == 'hourly' + assert utils.parse_recur_string('every 2 hour') == '2 hours' + assert utils.parse_recur_string('every 2 hours') == '2 hours' + assert utils.parse_recur_string('every 3 hour') == '3 hours' + assert utils.parse_recur_string('every 3 hours') == '3 hours' def test_every_n_days(): - assert utils.parse_recur('daily') == 'daily' - assert utils.parse_recur('every day') == 'daily' - assert utils.parse_recur('every 1 day') == 'daily' - assert utils.parse_recur('every 1 days') == 'daily' - assert utils.parse_recur('every other day') == '2 days' - assert utils.parse_recur('every 3 day') == '3 days' - assert utils.parse_recur('every 3 days') == '3 days' + assert utils.parse_recur_string('daily') == 'daily' + assert utils.parse_recur_string('every day') == 'daily' + assert utils.parse_recur_string('every 1 day') == 'daily' + assert utils.parse_recur_string('every 1 days') == 'daily' + assert utils.parse_recur_string('every other day') == '2 days' + assert utils.parse_recur_string('every 3 day') == '3 days' + assert utils.parse_recur_string('every 3 days') == '3 days' # With time (which should be ignored since it's encoded in due_date anyways) - assert utils.parse_recur('every day at 19:00') == 'daily' + assert utils.parse_recur_string('every day at 19:00') == 'daily' def test_special(): # Indicates daily at 9am - the time is saved in the `due` property - assert utils.parse_recur('every morning') == 'daily' + assert utils.parse_recur_string('every morning') == 'daily' # Indicates daily at 7pm - the time is saved in the `due` property - assert utils.parse_recur('every evening') == 'daily' + assert utils.parse_recur_string('every evening') == 'daily' # Weekdays - assert utils.parse_recur('every weekday') == 'weekdays' - assert utils.parse_recur('every workday') == 'weekdays' + assert utils.parse_recur_string('every weekday') == 'weekdays' + assert utils.parse_recur_string('every workday') == 'weekdays' def test_weekly(): - assert utils.parse_recur('every week') == 'weekly' - assert utils.parse_recur('every 1 week') == 'weekly' - assert utils.parse_recur('every 1 weeks') == 'weekly' - assert utils.parse_recur('weekly') == 'weekly' - assert utils.parse_recur('every other week') == '2 weeks' - assert utils.parse_recur('every 3 week') == '3 weeks' - assert utils.parse_recur('every 3 weeks') == '3 weeks' + assert utils.parse_recur_string('every week') == 'weekly' + assert utils.parse_recur_string('every 1 week') == 'weekly' + assert utils.parse_recur_string('every 1 weeks') == 'weekly' + assert utils.parse_recur_string('weekly') == 'weekly' + assert utils.parse_recur_string('every other week') == '2 weeks' + assert utils.parse_recur_string('every 3 week') == '3 weeks' + assert utils.parse_recur_string('every 3 weeks') == '3 weeks' def test_monthly(): - assert utils.parse_recur('every month') == 'monthly' - assert utils.parse_recur('every 1 month') == 'monthly' - assert utils.parse_recur('every 1 months') == 'monthly' - assert utils.parse_recur('monthly') == 'monthly' - assert utils.parse_recur('every other month') == '2 months' - assert utils.parse_recur('every 2 months') == '2 months' + assert utils.parse_recur_string('every month') == 'monthly' + assert utils.parse_recur_string('every 1 month') == 'monthly' + assert utils.parse_recur_string('every 1 months') == 'monthly' + assert utils.parse_recur_string('monthly') == 'monthly' + assert utils.parse_recur_string('every other month') == '2 months' + assert utils.parse_recur_string('every 2 months') == '2 months' # ordinal - assert utils.parse_recur('every 2nd month') == '2 months' - assert utils.parse_recur('every 3rd month') == '3 months' + assert utils.parse_recur_string('every 2nd month') == '2 months' + assert utils.parse_recur_string('every 3rd month') == '3 months' DAYS_OF_WEEK = [ @@ -109,12 +117,12 @@ def test_every_dow_has_weekly_recurrence(dow): """ The actual day should be indicated in the `due` property, so here we just need to ensure that the recurrence is correct. """ - assert utils.parse_recur(f'ev {dow}') == 'weekly' - assert utils.parse_recur(f'every {dow}') == 'weekly' - assert utils.parse_recur(f'every other {dow}') == '2 weeks' + assert utils.parse_recur_string(f'ev {dow}') == 'weekly' + assert utils.parse_recur_string(f'every {dow}') == 'weekly' + assert utils.parse_recur_string(f'every other {dow}') == '2 weeks' # With time (which should be ignored since it's encoded in due_date anyways) - assert utils.parse_recur(f'every {dow} at 17:00') == 'weekly' + assert utils.parse_recur_string(f'every {dow} at 17:00') == 'weekly' @pytest.mark.parametrize('ordinal', [ ('2', 2), @@ -127,61 +135,61 @@ def test_every_dow_has_weekly_recurrence(dow): @pytest.mark.parametrize('dow', DAYS_OF_WEEK) def test_every_dow_ordinal_recurrence(ordinal, dow): ordinal, expected = ordinal - assert utils.parse_recur(f'ev {ordinal} {dow}') == f'{expected} weeks' - assert utils.parse_recur(f'every {ordinal} {dow}') == f'{expected} weeks' + assert utils.parse_recur_string(f'ev {ordinal} {dow}') == f'{expected} weeks' + assert utils.parse_recur_string(f'every {ordinal} {dow}') == f'{expected} weeks' def test_day_of_week_short_forms(): - assert utils.parse_recur('every mo') == 'weekly' - assert utils.parse_recur('every mon') == 'weekly' + assert utils.parse_recur_string('every mo') == 'weekly' + assert utils.parse_recur_string('every mon') == 'weekly' - assert utils.parse_recur('every tu') == 'weekly' - assert utils.parse_recur('every tue') == 'weekly' - assert utils.parse_recur('every tues') == 'weekly' + assert utils.parse_recur_string('every tu') == 'weekly' + assert utils.parse_recur_string('every tue') == 'weekly' + assert utils.parse_recur_string('every tues') == 'weekly' - assert utils.parse_recur('every we') == 'weekly' - assert utils.parse_recur('every wed') == 'weekly' - assert utils.parse_recur('every weds') == 'weekly' + assert utils.parse_recur_string('every we') == 'weekly' + assert utils.parse_recur_string('every wed') == 'weekly' + assert utils.parse_recur_string('every weds') == 'weekly' - assert utils.parse_recur('every th') == 'weekly' - assert utils.parse_recur('every thu') == 'weekly' - assert utils.parse_recur('every thurs') == 'weekly' + assert utils.parse_recur_string('every th') == 'weekly' + assert utils.parse_recur_string('every thu') == 'weekly' + assert utils.parse_recur_string('every thurs') == 'weekly' - assert utils.parse_recur('every fr') == 'weekly' - assert utils.parse_recur('every fri') == 'weekly' + assert utils.parse_recur_string('every fr') == 'weekly' + assert utils.parse_recur_string('every fri') == 'weekly' - assert utils.parse_recur('every sa') == 'weekly' - assert utils.parse_recur('every sat') == 'weekly' + assert utils.parse_recur_string('every sa') == 'weekly' + assert utils.parse_recur_string('every sat') == 'weekly' - assert utils.parse_recur('every su') == 'weekly' - assert utils.parse_recur('every sun') == 'weekly' + assert utils.parse_recur_string('every su') == 'weekly' + assert utils.parse_recur_string('every sun') == 'weekly' def test_day_of_month(): """ The actual due date should be indicated by the `due` property, so here we just need to ensure a monthly recurrence. """ - assert utils.parse_recur('every 1st') == 'monthly' - assert utils.parse_recur('every 2nd') == 'monthly' - assert utils.parse_recur('every 3rd') == 'monthly' - assert utils.parse_recur('every 4th') == 'monthly' - assert utils.parse_recur('every 21st') == 'monthly' - assert utils.parse_recur('every 22nd') == 'monthly' - assert utils.parse_recur('every 23rd') == 'monthly' - assert utils.parse_recur('every 24th') == 'monthly' - assert utils.parse_recur('every last day') == 'monthly' + assert utils.parse_recur_string('every 1st') == 'monthly' + assert utils.parse_recur_string('every 2nd') == 'monthly' + assert utils.parse_recur_string('every 3rd') == 'monthly' + assert utils.parse_recur_string('every 4th') == 'monthly' + assert utils.parse_recur_string('every 21st') == 'monthly' + assert utils.parse_recur_string('every 22nd') == 'monthly' + assert utils.parse_recur_string('every 23rd') == 'monthly' + assert utils.parse_recur_string('every 24th') == 'monthly' + assert utils.parse_recur_string('every last day') == 'monthly' def test_annually(): - assert utils.parse_recur('every year') == 'yearly' - assert utils.parse_recur('every 2 year') == '2 years' - assert utils.parse_recur('every 2 years') == '2 years' + assert utils.parse_recur_string('every year') == 'yearly' + assert utils.parse_recur_string('every 2 year') == '2 years' + assert utils.parse_recur_string('every 2 years') == '2 years' def test_unsupported(): with pytest.raises(errors.UnsupportedRecurrence): - utils.parse_recur('every mon,tues,weds') + utils.parse_recur_string('every mon,tues,weds') with pytest.raises(errors.UnsupportedRecurrence): - utils.parse_recur('every monday,tuesday,wednesday') + utils.parse_recur_string('every monday,tuesday,wednesday') diff --git a/todoist_taskwarrior/cli.py b/todoist_taskwarrior/cli.py index 6dce95f..1470ec8 100644 --- a/todoist_taskwarrior/cli.py +++ b/todoist_taskwarrior/cli.py @@ -146,8 +146,8 @@ def migrate(ctx, interactive, sync, map_project, map_tag): # Dates data['entry'] = utils.parse_date(task['date_added']) - data['due'] = utils.parse_date(task['due_date_utc']) - data['recur'] = parse_recur_or_prompt(task['date_string']) + data['due'] = utils.parse_due(utils.try_get_model_prop(task, 'due')) + data['recur'] = parse_recur_or_prompt(utils.try_get_model_prop(task, 'due')) if not interactive: add_task(**data) @@ -279,11 +279,11 @@ def add_task_interactive(**task_data): return add_task(**task_data) -def parse_recur_or_prompt(value): +def parse_recur_or_prompt(due): try: - return utils.parse_recur(value) + return utils.parse_recur(due) except errors.UnsupportedRecurrence: - io.error('Unsupported recurrence: %s. Please enter a valid value' % value) + io.error("Unsupported recurrence: '%s'. Please enter a valid value" % due['string']) return io.prompt( 'Set recurrence (todoist style)', default='', diff --git a/todoist_taskwarrior/utils.py b/todoist_taskwarrior/utils.py index 233e0da..7f956a9 100644 --- a/todoist_taskwarrior/utils.py +++ b/todoist_taskwarrior/utils.py @@ -1,6 +1,6 @@ import click import re -from datetime import datetime +import dateutil.parser from .errors import UnsupportedRecurrence @@ -14,6 +14,14 @@ def try_map(m, value): return value +def try_get_model_prop(m, key, default=None): + """ The todoist models don't seem to have the `get()` method and throw KeyErrors """ + try: + return m[key] + except KeyError: + return default + + """ Priorities """ PRIORITY_MAP = {1: None, 2: 'L', 3: 'M', 4: 'H'} @@ -38,6 +46,23 @@ def maybe_quote_ws(value): """ Dates """ +def parse_due(due): + """Parse a due date from the due object. + + e.g. { + "date": "2016-12-0T12:00:00", + "timezone": null, + "string": "every day at 12", + "lang": "en", + "is_recurring": true + } + """ + if not due: + return None + + return parse_date(due['date']) + + def parse_date(date): """ Converts a date from Todoist to Taskwarrior. @@ -47,10 +72,17 @@ def parse_date(date): if not date: return None - return datetime.strptime(date, '%a %d %b %Y %H:%M:%S %z').isoformat() + return dateutil.parser.parse(date).isoformat() -def parse_recur(date_string): +def parse_recur(due): + """Given a due object, extracts the recur """ + if not due or not due['is_recurring']: + return None + return parse_recur_string(due['string']) + + +def parse_recur_string(date_string): """ Parses a Todoist `date_string` to extract a `recur` string for Taskwarrior. Field: diff --git a/todoist_taskwarrior/validation.py b/todoist_taskwarrior/validation.py index d867102..40463e2 100644 --- a/todoist_taskwarrior/validation.py +++ b/todoist_taskwarrior/validation.py @@ -18,6 +18,6 @@ def validate_map(ctx, param, value): def validate_recur(value): try: - return utils.parse_recur(value) + return utils.parse_recur_string(value) except errors.UnsupportedRecurrence as e: raise click.BadParameter(e)