mirror of
https://git.webmeisterei.com/webmeisterei/todoist-taskwarrior.git
synced 2023-12-21 10:23:00 +01:00
Refactor into module structure
This commit is contained in:
parent
c843376d74
commit
245daaa3fb
163
migrate.py
163
migrate.py
@ -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<interval>{RE_INTERVAL})\s+)?(?P<period>{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 <period> -> every 2 <period>
|
|
||||||
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()
|
|
||||||
|
|
0
todoist_taskwarrior/__init__.py
Normal file
0
todoist_taskwarrior/__init__.py
Normal file
92
todoist_taskwarrior/cli.py
Normal file
92
todoist_taskwarrior/cli.py
Normal file
@ -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()
|
||||||
|
|
89
todoist_taskwarrior/utils.py
Normal file
89
todoist_taskwarrior/utils.py
Normal file
@ -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<interval>{RE_INTERVAL})\s+)?(?P<period>{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 <period> -> every 2 <period>
|
||||||
|
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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user