From 245daaa3fb04b8ef84d6dd5800c58a0cf0b1c335 Mon Sep 17 00:00:00 2001 From: Matt Snider Date: Fri, 23 Nov 2018 22:36:32 +0100 Subject: [PATCH] Refactor into module structure --- migrate.py | 163 -------------------------------- todoist_taskwarrior/__init__.py | 0 todoist_taskwarrior/cli.py | 92 ++++++++++++++++++ todoist_taskwarrior/utils.py | 89 +++++++++++++++++ 4 files changed, 181 insertions(+), 163 deletions(-) delete mode 100644 migrate.py create mode 100644 todoist_taskwarrior/__init__.py create mode 100644 todoist_taskwarrior/cli.py create mode 100644 todoist_taskwarrior/utils.py diff --git a/migrate.py b/migrate.py deleted file mode 100644 index 9eeddbc..0000000 --- a/migrate.py +++ /dev/null @@ -1,163 +0,0 @@ -import click -import re -import os -from datetime import datetime -from taskw import TaskWarrior -from todoist.api import TodoistAPI - -todoist = None -taskwarrior = None - -""" CLI Commands """ - -@click.group() -def cli(): - pass - - -@cli.command() -@click.option('-i', '--interactive', is_flag=True, default=False) -@click.option('--no-sync', is_flag=True, default=False) -def migrate(interactive, no_sync): - if not no_sync: - important('Syncing tasks with todoist... ', nl=False) - todoist.sync() - success('OK') - - tasks = todoist.items.all() - important(f'Starting migration of {len(tasks)}...') - for task in todoist.items.all(): - tid = task['id'] - name = task['content'] - project = todoist.projects.get_by_id(task['project_id'])['name'] - priority = taskwarrior_priority(task['priority']) - tags = [ - todoist.labels.get_by_id(l_id)['name'] - for l_id in task['labels'] - ] - entry = taskwarrior_date(task['date_added']) - due = taskwarrior_date(task['due_date_utc']) - recur = taskwarrior_recur(task['date_string']) - - if interactive and not click.confirm(f"Import '{name}'?"): - continue - - add_task(tid, name, project, tags, priority, entry, due, recur) - - -def add_task(tid, name, project, tags, priority, entry, due, recur): - """Add a taskwarrior task from todoist task - - Returns the taskwarrior task. - """ - info(f"Importing '{name}' ({project}) - ", nl=False) - try: - tw_task = taskwarrior.task_add(name, project=project, tags=tags, - priority=priority, entry=entry, due=due, recur=recur) - except: - error('FAILED') - else: - success('OK') - return tw_task - - -""" Utils """ - -def important(msg, **kwargs): - click.echo(click.style(msg, fg='blue', bold=True), **kwargs) - -def info(msg, **kwargs): - click.echo(msg, **kwargs) - -def success(msg, **kwargs): - click.echo(click.style(msg, fg='green', bold=True)) - -def error(msg, **kwargs): - click.echo(click.style(msg, fg='red', bold=True)) - -PRIORITIES = {1: None, 2: 'L', 3: 'M', 4: 'H'} -def taskwarrior_priority(priority): - """Converts a priority from Todiost (1-4) to taskwarrior (None, L, M, H) """ - return PRIORITIES[priority] - -def taskwarrior_date(date): - """ Converts a date from Todoist to taskwarrior - - Todoist: Fri 26 Sep 2014 08:25:05 +0000 (what is this called)? - taskwarrior: ISO-8601 - """ - if not date: - return None - return datetime.strptime(date, '%a %d %b %Y %H:%M:%S %z').isoformat() - - -def taskwarrior_recur(desc): - """ Converts a repeating interval from Todoist to taskwarrior. - - Field: - - Todoist: date_string - - taskwarrior: recur - - Examples: - - every other `interval` `period` -> 2 `period` - - every `interval` `period` -> `interval` `period` - - every `day of week` -> weekly - - _Note_: just because Todoist sets `date_string` doesn't mean - that the task is repeating. Mostly it just indicates that the - user input via string and not date selector. - """ - if not desc: - return - return _match_every(desc) or _match_weekly(desc) - - -RE_INTERVAL = 'other|\d+' -RE_PERIOD = 'day|week|month|year|morning|evening|weekday|workday|last\s+day' -RE_REPEAT_EVERY = re.compile( - f'^\s*ev(ery)?\s+((?P{RE_INTERVAL})\s+)?(?P{RE_PERIOD})s?\s*$' -) - -def _match_every(desc): - match = RE_REPEAT_EVERY.match(desc.lower()) - if not match: - return - - interval = match.group('interval') - period = match.group('period') - - # every other -> every 2 - if interval == 'other': - interval = 2 - # every morning -> every 1 day at 9am (the time will be stored in `due`) - # every evening -> every 1 day at 7pm (the time will be stored in `due`) - elif period == 'morning' or period == 'evening': - interval = 1 - period = 'day' - # every weekday -> weekdays - elif period == 'weekday' or period == 'workday': - interval = '' - period = 'weekdays' - - return f'{interval} {period}' - - -RE_REPEAT_WEEKLY = re.compile( - '^\s*every\s+(mon|monday|tues|tuesday|weds|wednesday|thurs|thursday|fri|friday|sat|saturday|sun|sunday)\s*' -) - -def _match_weekly(desc): - return ('weekly' if RE_REPEAT_WEEKLY.match(desc.lower()) else None) - - -""" Entrypoint """ - -if __name__ == '__main__': - todoist_api_key = os.getenv('TODOIST_API_KEY') - if todoist_api_key is None: - exit('TODOIST_API_KEY environment variable not specified. Exiting.') - - todoist = TodoistAPI(todoist_api_key) - taskwarrior = TaskWarrior() - cli() - diff --git a/todoist_taskwarrior/__init__.py b/todoist_taskwarrior/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/todoist_taskwarrior/cli.py b/todoist_taskwarrior/cli.py new file mode 100644 index 0000000..d4ac76f --- /dev/null +++ b/todoist_taskwarrior/cli.py @@ -0,0 +1,92 @@ +import click +import os +import sys + +from taskw import TaskWarrior +from todoist.api import TodoistAPI +from . import utils + +todoist = None +taskwarrior = None + + +""" CLI Commands """ + +@click.group() +def cli(): + pass + + +@cli.command() +@click.option('-i', '--interactive', is_flag=True, default=False) +@click.option('--no-sync', is_flag=True, default=False) +def migrate(interactive, no_sync): + if not no_sync: + important('Syncing tasks with todoist... ', nl=False) + todoist.sync() + success('OK') + + tasks = todoist.items.all() + important(f'Starting migration of {len(tasks)}...') + for task in todoist.items.all(): + tid = task['id'] + name = task['content'] + project = todoist.projects.get_by_id(task['project_id'])['name'] + priority = utils.parse_priority(task['priority']) + tags = [ + todoist.labels.get_by_id(l_id)['name'] + for l_id in task['labels'] + ] + entry = utils.parse_date(task['date_added']) + due = utils.parse_date(task['due_date_utc']) + recur = utils.parse_recur(task['date_string']) + + if interactive and not click.confirm(f"Import '{name}'?"): + continue + + add_task(tid, name, project, tags, priority, entry, due, recur) + + +def add_task(tid, name, project, tags, priority, entry, due, recur): + """Add a taskwarrior task from todoist task + + Returns the taskwarrior task. + """ + info(f"Importing '{name}' ({project}) - ", nl=False) + try: + tw_task = taskwarrior.task_add(name, project=project, tags=tags, + priority=priority, entry=entry, due=due, recur=recur) + except: + error('FAILED') + else: + success('OK') + return tw_task + + +""" Utils """ + +def important(msg, **kwargs): + click.echo(click.style(msg, fg='blue', bold=True), **kwargs) + +def info(msg, **kwargs): + click.echo(msg, **kwargs) + +def success(msg, **kwargs): + click.echo(click.style(msg, fg='green', bold=True)) + +def error(msg, **kwargs): + click.echo(click.style(msg, fg='red', bold=True)) + + +""" Entrypoint """ + +if __name__ == '__main__': + is_help_cmd = '-h' in sys.argv or '--help' in sys.argv + todoist_api_key = os.getenv('TODOIST_API_KEY') + if todoist_api_key is None and not is_help_cmd: + exit('TODOIST_API_KEY environment variable not specified. Exiting.') + + todoist = TodoistAPI(todoist_api_key) + taskwarrior = TaskWarrior() + cli() + diff --git a/todoist_taskwarrior/utils.py b/todoist_taskwarrior/utils.py new file mode 100644 index 0000000..34b98ef --- /dev/null +++ b/todoist_taskwarrior/utils.py @@ -0,0 +1,89 @@ +import re +from datetime import datetime + +""" Priorities """ + +PRIORITY_MAP = {1: None, 2: 'L', 3: 'M', 4: 'H'} + +def parse_priority(priority): + """ Converts a priority from Todoist to Taskwarrior. + + Todoist saves priorities as 1, 2, 3, 4, whereas Taskwarrior uses L, M, H. + These values map very easily to eachother, as Todoist priority 1 indicates that + no priority has been set. + """ + return PRIORITY_MAP[priority] + + +""" Dates """ + +def parse_date(date): + """ Converts a date from Todoist to Taskwarrior. + + Todoist: Fri 26 Sep 2014 08:25:05 +0000 (what is this called)? + taskwarrior: ISO-8601 + """ + if not date: + return None + + return datetime.strptime(date, '%a %d %b %Y %H:%M:%S %z').isoformat() + + +def parse_recur(date_string): + """ Parses a Todoist `date_string` to extract a `recur` string for Taskwarrior. + + Field: + - Todoist: date_string + - taskwarrior: recur + + Examples: + - every other `interval` `period` -> 2 `period` + - every `interval` `period` -> `interval` `period` + - every `day of week` -> weekly + + _Note_: just because Todoist sets `date_string` doesn't mean + that the task is repeating. Mostly it just indicates that the + user input via string and not date selector. + """ + if not date_string: + return + return _match_every(date_string) or _match_weekly(date_string) + + +RE_INTERVAL = 'other|\d+' +RE_PERIOD = 'day|week|month|year|morning|evening|weekday|workday|last\s+day' +RE_REPEAT_EVERY = re.compile( + f'^\s*ev(ery)?\s+((?P{RE_INTERVAL})\s+)?(?P{RE_PERIOD})s?\s*$' +) + +def _match_every(desc): + match = RE_REPEAT_EVERY.match(desc.lower()) + if not match: + return + + interval = match.group('interval') + period = match.group('period') + + # every other -> every 2 + if interval == 'other': + interval = 2 + # every morning -> every 1 day at 9am (the time will be stored in `due`) + # every evening -> every 1 day at 7pm (the time will be stored in `due`) + elif period == 'morning' or period == 'evening': + interval = 1 + period = 'day' + # every weekday -> weekdays + elif period == 'weekday' or period == 'workday': + interval = '' + period = 'weekdays' + + return f'{interval} {period}' + + +RE_REPEAT_WEEKLY = re.compile( + '^\s*every\s+(mon|monday|tues|tuesday|weds|wednesday|thurs|thursday|fri|friday|sat|saturday|sun|sunday)\s*' +) + +def _match_weekly(desc): + return ('weekly' if RE_REPEAT_WEEKLY.match(desc.lower()) else None) +