2018-11-23 22:36:32 +01:00
|
|
|
import click
|
|
|
|
import os
|
|
|
|
import sys
|
2019-08-05 04:16:17 +02:00
|
|
|
import datetime
|
|
|
|
import dateutil.parser
|
|
|
|
import io
|
2018-11-23 22:36:32 +01:00
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
import yaml
|
2018-11-23 22:36:32 +01:00
|
|
|
from taskw import TaskWarrior
|
|
|
|
from todoist.api import TodoistAPI
|
2019-08-05 04:16:17 +02:00
|
|
|
from . import errors, log, utils, validation
|
2019-03-25 22:08:24 +01:00
|
|
|
|
|
|
|
# This is the location where the todoist
|
|
|
|
# data will be cached.
|
|
|
|
TODOIST_CACHE = '~/.todoist-sync/'
|
2019-08-05 04:16:17 +02:00
|
|
|
TITWSYNCRC = '~/.titwsyncrc.yaml'
|
2019-03-25 22:08:24 +01:00
|
|
|
|
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
config = None
|
2018-11-23 22:36:32 +01:00
|
|
|
todoist = None
|
|
|
|
taskwarrior = None
|
|
|
|
|
|
|
|
""" CLI Commands """
|
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
|
2018-11-23 22:36:32 +01:00
|
|
|
@click.group()
|
|
|
|
def cli():
|
2019-08-05 04:16:17 +02:00
|
|
|
"""Two-way sync of Todoist and Taskwarrior. """
|
|
|
|
global config, todoist, taskwarrior
|
|
|
|
|
|
|
|
is_help_cmd = '-h' in sys.argv or '--help' in sys.argv
|
|
|
|
rcfile = os.path.expanduser(TITWSYNCRC)
|
|
|
|
with open(rcfile, 'r') as stream:
|
|
|
|
config = yaml.safe_load(stream)
|
|
|
|
|
|
|
|
if 'todoist' not in config or 'api_key' not in config['todoist'] \
|
|
|
|
and not is_help_cmd:
|
|
|
|
log.error('Run configure first. Exiting.')
|
|
|
|
exit(1)
|
|
|
|
|
|
|
|
todoist = TodoistAPI(config['todoist']['api_key'], cache=TODOIST_CACHE)
|
|
|
|
|
|
|
|
# Create the TaskWarrior client, overriding config to
|
|
|
|
# create a `todoist_id` field which we'll use to
|
|
|
|
# prevent duplicates
|
|
|
|
taskwarrior = TaskWarrior(config_overrides={
|
|
|
|
'uda.todoist_id.type': 'string',
|
|
|
|
'uda.todoist_sync.type': 'date',
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
@cli.command()
|
|
|
|
@click.option('-p', '--map-project', metavar='SRC=DST', multiple=True,
|
|
|
|
callback=validation.validate_map,
|
|
|
|
help='Project names specified will be translated '
|
|
|
|
'from SRC to DST. '
|
|
|
|
'If DST is omitted, the project will be unset when SRC matches.')
|
|
|
|
@click.option('-t', '--map-tag', metavar='SRC=DST', multiple=True,
|
|
|
|
callback=validation.validate_map,
|
|
|
|
help='Tags specified will be translated from SRC to DST. '
|
|
|
|
'If DST is omitted, the tag will be removed when SRC matches.')
|
|
|
|
@click.argument('todoist_api_key')
|
|
|
|
def configure(map_project, map_tag, todoist_api_key):
|
|
|
|
"""Configure sync.
|
|
|
|
|
|
|
|
Use --map-project to change or remove the project. Project hierarchies will
|
|
|
|
be period-delimited during conversion. For example in the following,
|
|
|
|
'Work Errands' and 'House Errands' will be both be changed to 'errands',
|
|
|
|
'Programming.Open Source' will be changed to 'oss', and the project will be
|
|
|
|
removed when it is 'Taxes':
|
|
|
|
\r
|
|
|
|
--map-project 'Work Errands'=errands
|
|
|
|
--map-project 'House Errands'=errands
|
|
|
|
--map-project 'Programming.Open Source'=oss
|
|
|
|
--map-project Taxes=
|
|
|
|
"""
|
|
|
|
data = {'todoist': {'api_key': todoist_api_key}, 'taskwarrior': {}}
|
|
|
|
data['todoist']['project_map'] = map_project
|
|
|
|
data['todoist']['tag_map'] = map_tag
|
|
|
|
data['taskwarrior']['project_sync'] = {
|
|
|
|
k: True for k in map_project.values()}
|
|
|
|
|
|
|
|
rcfile = os.path.expanduser(TITWSYNCRC)
|
|
|
|
with io.open(rcfile, 'w', encoding='utf8') as outfile:
|
|
|
|
yaml.dump(data, outfile, default_flow_style=False, allow_unicode=True)
|
2018-11-23 22:36:32 +01:00
|
|
|
|
|
|
|
|
2019-03-23 20:56:26 +01:00
|
|
|
@cli.command()
|
|
|
|
def synchronize():
|
2019-03-23 22:01:51 +01:00
|
|
|
"""Update the local Todoist task cache.
|
2019-03-30 18:34:11 +01:00
|
|
|
|
2019-03-23 22:01:51 +01:00
|
|
|
This command accesses Todoist via the API and updates a local
|
|
|
|
cache before exiting. This can be useful to pre-load the tasks,
|
|
|
|
and means `migrate` can be run without a network connection.
|
|
|
|
|
|
|
|
NOTE - the local Todoist data cache is usually located at:
|
|
|
|
|
|
|
|
~/.todoist-sync
|
|
|
|
"""
|
2019-08-05 04:16:17 +02:00
|
|
|
with log.with_feedback('Syncing tasks with todoist'):
|
2019-03-24 16:25:20 +01:00
|
|
|
todoist.sync()
|
2019-03-23 20:56:26 +01:00
|
|
|
|
|
|
|
|
2019-03-25 22:08:24 +01:00
|
|
|
@cli.command()
|
2019-08-05 04:16:17 +02:00
|
|
|
@click.confirmation_option(
|
|
|
|
prompt=f'Are you sure you want to delete {TODOIST_CACHE}?')
|
2019-03-25 22:08:24 +01:00
|
|
|
def clean():
|
|
|
|
"""Remove the data stored in the Todoist task cache.
|
|
|
|
|
|
|
|
NOTE - the local Todoist data cache is usually located at:
|
|
|
|
|
|
|
|
~/.todoist-sync
|
|
|
|
"""
|
|
|
|
cache_dir = os.path.expanduser(TODOIST_CACHE)
|
|
|
|
|
|
|
|
# Delete all files in directory
|
|
|
|
for file_entry in os.scandir(cache_dir):
|
2019-08-05 04:16:17 +02:00
|
|
|
with log.with_feedback(f'Removing file {file_entry.path}'):
|
2019-03-25 22:08:24 +01:00
|
|
|
os.remove(file_entry)
|
|
|
|
|
|
|
|
# Delete directory
|
2019-08-05 04:16:17 +02:00
|
|
|
with log.with_feedback(f'Removing directory {cache_dir}'):
|
2019-03-25 22:08:24 +01:00
|
|
|
os.rmdir(cache_dir)
|
|
|
|
|
|
|
|
|
2018-11-23 22:36:32 +01:00
|
|
|
@cli.command()
|
2019-03-23 20:56:26 +01:00
|
|
|
@click.pass_context
|
2019-08-05 04:16:17 +02:00
|
|
|
def sync(ctx):
|
|
|
|
"""Sync tasks between Todoist and Taskwarrior.
|
2019-03-30 18:34:11 +01:00
|
|
|
|
2019-03-23 22:01:51 +01:00
|
|
|
This command can be run multiple times and will not duplicate tasks.
|
|
|
|
This is tracked in Taskwarrior by setting and detecting the
|
|
|
|
`todoist_id` property on the task.
|
|
|
|
"""
|
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
# Sync todoist to cache
|
|
|
|
ctx.invoke(synchronize)
|
|
|
|
|
|
|
|
ti_project_list = _ti_project_list()
|
|
|
|
default_project = utils.try_map(config['todoist']['project_map'], 'Inbox')
|
|
|
|
|
|
|
|
config_ps = config['taskwarrior']['project_sync']
|
2018-11-23 22:36:32 +01:00
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
# Sync Taskwarrior->Todoist
|
|
|
|
tw_tasks = taskwarrior.load_tasks()
|
|
|
|
log.important(f'Starting to sync tasks from Taskwarrior...')
|
|
|
|
for tw_task in tw_tasks['pending']:
|
|
|
|
if 'project' not in tw_task:
|
|
|
|
tw_task['project'] = default_project
|
|
|
|
|
|
|
|
desc = tw_task['description']
|
|
|
|
project = tw_task['project']
|
|
|
|
|
|
|
|
if (tw_task['project'] not in config_ps or
|
|
|
|
not config_ps[tw_task['project']]):
|
|
|
|
log.warn(f'Ignoring Task {desc} ({project})')
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Log message
|
|
|
|
log.important(f'Sync Task {desc} ({project})')
|
|
|
|
|
|
|
|
if 'todoist_id' in tw_task:
|
|
|
|
ti_task = todoist.items.get_by_id(tw_task['todoist_id'])
|
|
|
|
if ti_task is None:
|
|
|
|
# Ti task has been deleted
|
|
|
|
taskwarrior.task_delete(uuid=tw_task['uuid'])
|
|
|
|
continue
|
|
|
|
|
|
|
|
ti_task = ti_task['item']
|
|
|
|
c_ti_task = _convert_ti_task(ti_task, ti_project_list)
|
|
|
|
|
|
|
|
# Sync Todoist with Taskwarrior task
|
|
|
|
_sync_task(tw_task, c_ti_task, ti_project_list)
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Add Todoist task
|
|
|
|
_ti_add_task(tw_task, ti_project_list)
|
|
|
|
|
|
|
|
with log.with_feedback('Syncing tasks with todoist'):
|
|
|
|
todoist.commit()
|
|
|
|
todoist.sync()
|
|
|
|
|
|
|
|
# Sync Todoist->Taskwarrior
|
2018-11-23 22:36:32 +01:00
|
|
|
tasks = todoist.items.all()
|
2019-08-05 04:16:17 +02:00
|
|
|
log.important(f'Starting sync of {len(tasks)} tasks from Todoist...')
|
|
|
|
for idx, ti_task in enumerate(tasks):
|
|
|
|
c_ti_task = _convert_ti_task(ti_task, ti_project_list)
|
2019-06-21 01:25:14 +02:00
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
desc = c_ti_task['description']
|
|
|
|
project = c_ti_task['project']
|
|
|
|
|
|
|
|
if (c_ti_task['project'] not in config_ps or
|
|
|
|
not config_ps[c_ti_task['project']]):
|
|
|
|
log.warn(f'Ignoring Task {desc} ({project})')
|
2019-07-13 12:30:33 +02:00
|
|
|
continue
|
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
# Log message
|
|
|
|
log.important(f'Sync Task {desc} ({project})')
|
2019-06-21 01:25:14 +02:00
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
# Sync Todoist with Taskwarrior task
|
|
|
|
_, tw_task = taskwarrior.get_task(todoist_id=ti_task['id'])
|
|
|
|
if bool(tw_task):
|
|
|
|
if 'project' not in tw_task:
|
|
|
|
tw_task['project'] = default_project
|
|
|
|
_sync_task(tw_task, c_ti_task, ti_project_list)
|
|
|
|
continue
|
2019-06-21 01:25:14 +02:00
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
# Add Taskwarrior task
|
|
|
|
_tw_add_task(c_ti_task)
|
2019-06-21 01:25:14 +02:00
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
with log.with_feedback('Syncing tasks with todoist'):
|
|
|
|
todoist.commit()
|
|
|
|
todoist.sync()
|
2019-06-21 01:25:14 +02:00
|
|
|
|
2018-11-23 22:36:32 +01:00
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
def _convert_ti_task(ti_task, ti_project_list):
|
|
|
|
data = {}
|
|
|
|
data['tid'] = ti_task['id']
|
|
|
|
data['description'] = ti_task['content']
|
|
|
|
|
|
|
|
# Project
|
|
|
|
project_name = ''
|
|
|
|
for project_name, p in ti_project_list.items():
|
|
|
|
if p['id'] == ti_task['project_id']:
|
|
|
|
break
|
|
|
|
|
|
|
|
data['project'] = project_name
|
|
|
|
|
|
|
|
# Priority
|
|
|
|
data['priority'] = utils.ti_priority_to_tw(ti_task['priority'])
|
|
|
|
|
|
|
|
# Tags
|
|
|
|
data['tags'] = [
|
|
|
|
utils.try_map(config['todoist']['tag_map'],
|
|
|
|
todoist.labels.get_by_id(l_id)['name'])
|
|
|
|
for l_id in ti_task['labels']
|
|
|
|
]
|
2019-01-21 21:17:38 +01:00
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
# Dates
|
|
|
|
data['entry'] = utils.parse_date(ti_task['date_added'])
|
|
|
|
data['due'] = utils.parse_due(utils.try_get_model_prop(ti_task, 'due'))
|
|
|
|
data['recur'] = parse_recur_or_prompt(
|
|
|
|
utils.try_get_model_prop(ti_task, 'due'))
|
2019-01-21 21:17:38 +01:00
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
data['status'] = 'completed' if ti_task['checked'] == 1 else 'pending'
|
2018-11-23 22:36:32 +01:00
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
return data
|
2018-11-23 22:36:32 +01:00
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
|
|
|
|
def _sync_task(tw_task, ti_task, ti_project_list):
|
|
|
|
if 'todoist_sync' in tw_task:
|
|
|
|
ti_stamp = dateutil.parser.parse(tw_task['todoist_sync']).timestamp()
|
|
|
|
tw_stamp = dateutil.parser.parse(tw_task['modified']).timestamp()
|
|
|
|
|
|
|
|
if tw_stamp > ti_stamp:
|
|
|
|
_ti_update_task(tw_task, ti_project_list)
|
|
|
|
else:
|
|
|
|
_tw_update_task(tw_task, ti_task)
|
|
|
|
else:
|
|
|
|
_tw_update_task(tw_task, ti_task)
|
|
|
|
|
|
|
|
|
|
|
|
def _tw_add_task(ti_task):
|
2018-11-23 22:36:32 +01:00
|
|
|
"""Add a taskwarrior task from todoist task
|
|
|
|
|
|
|
|
Returns the taskwarrior task.
|
|
|
|
"""
|
2019-08-05 04:16:17 +02:00
|
|
|
description = ti_task['description']
|
|
|
|
project = ti_task['project']
|
|
|
|
with log.with_feedback(f"Taskwarrior add '{description}' ({project})"):
|
2019-03-24 16:25:20 +01:00
|
|
|
return taskwarrior.task_add(
|
2019-08-05 04:16:17 +02:00
|
|
|
ti_task['description'],
|
|
|
|
project=ti_task['project'],
|
|
|
|
tags=ti_task['tags'],
|
|
|
|
priority=ti_task['priority'],
|
|
|
|
entry=ti_task['entry'],
|
|
|
|
due=ti_task['due'],
|
|
|
|
recur=ti_task['recur'],
|
|
|
|
status=ti_task['status'],
|
|
|
|
todoist_id=ti_task['tid'],
|
|
|
|
todoist_sync=datetime.datetime.now(),
|
2019-01-21 21:17:38 +01:00
|
|
|
)
|
2018-11-23 22:36:32 +01:00
|
|
|
|
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
def _tw_update_task(tw_task, ti_task):
|
2018-11-24 18:48:48 +01:00
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
def _compare_value(item):
|
|
|
|
return ((ti_task[item] and item not in tw_task) or
|
|
|
|
(item in tw_task and tw_task[item] != ti_task[item]))
|
|
|
|
|
|
|
|
description = ti_task['description']
|
|
|
|
project = ti_task['project']
|
|
|
|
with log.on_error(f"TW updating '{description}' ({project})"):
|
|
|
|
changed = False
|
|
|
|
|
|
|
|
if tw_task['description'] != ti_task['description']:
|
|
|
|
tw_task['description'] = ti_task['description']
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
if tw_task['project'] != ti_task['project']:
|
|
|
|
tw_task['project'] = ti_task['project']
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
if _compare_value('tags'):
|
|
|
|
tw_task['tags'] = ti_task['tags']
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
if _compare_value('priority'):
|
|
|
|
tw_task['priority'] = ti_task['priority']
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
if _compare_value('entry'):
|
|
|
|
tw_task['entry'] = ti_task['entry']
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
if _compare_value('due'):
|
|
|
|
tw_task['due'] = ti_task['due']
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
if _compare_value('recur'):
|
|
|
|
tw_task['recur'] = ti_task['recur']
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
if tw_task['status'] != ti_task['status']:
|
|
|
|
tw_task['status'] = ti_task['status']
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
if changed:
|
|
|
|
tid = ti_task['tid']
|
|
|
|
log.info(f'TW updating (todoist_id={tid})...', nl=False)
|
|
|
|
log.success('OK')
|
|
|
|
|
|
|
|
tw_task['todoist_sync'] = datetime.datetime.now()
|
|
|
|
taskwarrior.task_update(tw_task)
|
|
|
|
|
|
|
|
|
|
|
|
def _ti_update_task(tw_task, ti_project_list):
|
|
|
|
description = tw_task['description']
|
|
|
|
project = tw_task['project']
|
|
|
|
with log.on_error(f"Todoist update '{description}' ({project})"):
|
|
|
|
changed = False
|
|
|
|
|
|
|
|
ti_task = todoist.items.get_by_id(tw_task['todoist_id'])
|
|
|
|
|
|
|
|
if tw_task['description'] != ti_task['item']['content']:
|
|
|
|
ti_task['item']['content'] = tw_task['description']
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
project = ti_project_list[tw_task['project']]
|
|
|
|
if ti_task['item']['project_id'] != project['id']:
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
priority = 0
|
|
|
|
if 'priority' in tw_task:
|
|
|
|
priority = utils.tw_priority_to_ti(tw_task['priority'])
|
|
|
|
if ti_task['item']['priority'] != priority:
|
|
|
|
ti_task['item']['priority'] = priority
|
|
|
|
changed = True
|
|
|
|
|
|
|
|
if ((ti_task['item']['checked'] == 0 and
|
|
|
|
tw_task['status'] == 'completed') or
|
|
|
|
(ti_task['item']['checked'] == 1 and
|
|
|
|
tw_task['status'] == 'pending')):
|
|
|
|
changed = True
|
2018-11-24 18:48:48 +01:00
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
if changed:
|
|
|
|
tid = ti_task['item']['id']
|
2018-11-24 18:48:48 +01:00
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
log.info(f'Updating (todoist_id={tid})', nl=False)
|
|
|
|
log.success('OK')
|
2018-11-24 18:48:48 +01:00
|
|
|
|
2019-08-05 04:16:17 +02:00
|
|
|
todoist.items.update(tid, **ti_task)
|
|
|
|
|
|
|
|
# Move to another project
|
|
|
|
if ti_task['item']['project_id'] != project['id']:
|
|
|
|
todoist.items.move(tid, project_id=project['id'])
|
|
|
|
|
|
|
|
# Open/close ti task
|
|
|
|
if ti_task['item']['checked'] == 1 and \
|
|
|
|
tw_task['status'] == 'pending':
|
|
|
|
todoist.items.uncomplete(tid)
|
|
|
|
elif ti_task['item']['checked'] == 0 and \
|
|
|
|
tw_task['status'] == 'completed':
|
|
|
|
todoist.items.complete(tid)
|
2019-10-09 10:14:27 +02:00
|
|
|
elif tw_task['status'] == 'waiting':
|
|
|
|
# taskwarrior doesn't like status=waiting
|
|
|
|
del(tw_task['status'])
|
2019-08-05 04:16:17 +02:00
|
|
|
|
2019-10-09 10:14:27 +02:00
|
|
|
tw_task['todoist_sync'] = datetime.datetime.now().isoformat()
|
2019-08-05 04:16:17 +02:00
|
|
|
taskwarrior.task_update(tw_task)
|
|
|
|
else:
|
|
|
|
# Always set latest sync time so no more sync accures
|
|
|
|
tid = ti_task['item']['id']
|
|
|
|
log.info(f'TI updating (todoist_id={tid})...', nl=False)
|
|
|
|
log.success('OK')
|
|
|
|
|
2019-10-09 10:14:27 +02:00
|
|
|
# taskwarrior doesn't like status=waiting
|
|
|
|
if tw_task['status'] == 'waiting':
|
|
|
|
del(tw_task['status'])
|
|
|
|
|
|
|
|
tw_task['todoist_sync'] = datetime.datetime.now().isoformat()
|
2019-08-05 04:16:17 +02:00
|
|
|
taskwarrior.task_update(tw_task)
|
|
|
|
|
|
|
|
|
|
|
|
def _ti_add_task(tw_task, ti_project_list):
|
|
|
|
description = tw_task['description']
|
|
|
|
project = tw_task['project']
|
|
|
|
with log.on_error(f"Todoist add '{description}' ({project})"):
|
|
|
|
# Add the item and commit the change
|
|
|
|
data = {}
|
|
|
|
|
|
|
|
if tw_task['project'] not in ti_project_list:
|
|
|
|
project = tw_task['project']
|
|
|
|
log.error(f'Project "{project}" not found on Todoist.')
|
|
|
|
return
|
|
|
|
|
|
|
|
data['project_id'] = ti_project_list[tw_task['project']]['id']
|
|
|
|
if 'priority' in tw_task:
|
|
|
|
data['priority'] = utils.tw_priority_to_ti(tw_task['priority'])
|
|
|
|
|
|
|
|
ti_task = todoist.items.add(tw_task['description'], **data)
|
|
|
|
todoist.commit()
|
|
|
|
|
|
|
|
tid = ti_task['id']
|
|
|
|
log.info(f'TI add (todoist_id={tid})')
|
|
|
|
|
|
|
|
tw_task['todoist_id'] = tid
|
|
|
|
tw_task['todoist_sync'] = datetime.datetime.now()
|
|
|
|
taskwarrior.task_update(tw_task)
|
|
|
|
|
|
|
|
|
|
|
|
def _ti_project_list():
|
|
|
|
result = {}
|
|
|
|
for p in todoist.projects.all():
|
|
|
|
project_hierarchy = [p]
|
|
|
|
pp = p
|
|
|
|
while pp['parent_id']:
|
|
|
|
pp = todoist.projects.get_by_id(p['parent_id'])
|
|
|
|
project_hierarchy.insert(0, pp)
|
|
|
|
|
|
|
|
project_name = '.'.join(p['name'] for p in project_hierarchy)
|
|
|
|
project_name = utils.try_map(
|
|
|
|
config['todoist']['project_map'],
|
|
|
|
project_name
|
|
|
|
)
|
|
|
|
result[utils.maybe_quote_ws(project_name)] = p
|
|
|
|
|
|
|
|
return result
|
2018-11-24 18:48:48 +01:00
|
|
|
|
|
|
|
|
2019-07-13 13:33:17 +02:00
|
|
|
def parse_recur_or_prompt(due):
|
2019-07-13 12:29:55 +02:00
|
|
|
try:
|
2019-07-13 13:33:17 +02:00
|
|
|
return utils.parse_recur(due)
|
2019-07-13 12:29:55 +02:00
|
|
|
except errors.UnsupportedRecurrence:
|
2019-08-05 04:16:17 +02:00
|
|
|
log.error(
|
|
|
|
"Unsupported recurrence: '%s'. "
|
|
|
|
"Please enter a valid value" % due['string'])
|
|
|
|
return log.prompt(
|
2019-07-13 12:29:55 +02:00
|
|
|
'Set recurrence (todoist style)',
|
|
|
|
default='',
|
|
|
|
value_proc=validation.validate_recur,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2018-11-23 22:36:32 +01:00
|
|
|
""" Entrypoint """
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
cli()
|