diff --git a/LICENSE b/LICENSE index 495c706..5b9d05f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ The MIT License (MIT) Copyright (c) 2014 Adam Kramer +Copyright (c) 2015 Andrew Williams Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Procfile b/Procfile deleted file mode 100644 index 1b951d1..0000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -nextaction: python nextaction.py \ No newline at end of file diff --git a/README.md b/README.md index b41d241..6f51124 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ NextAction A more GTD-like workflow for Todoist. Uses the REST API to add and remove a `@next_action` label from tasks. This program looks at every list in your Todoist account. -Any list that ends with `-` or `=` is treated specially, and processed by NextAction. +Any list that ends with `_` or `.` is treated specially, and processed by NextAction. Note that NextAction requires Todoist Premium to function properly, as labels are a premium feature. @@ -19,36 +19,22 @@ Activating NextAction Sequential list processing -------------------------- -If a list ends with `-`, the top level of tasks will be treated as a priority queue and the most important will be labeled `@next_action`. +If a list ends with `_`, the top level of tasks will be treated as a priority queue and the most important will be labeled `@next_action`. Importance is determined by order in the list Parallel list processing ------------------------ -If a list name ends with `=`, the top level of tasks will be treated as parallel `@next_action`s. -The waterfall processing will be applied the same way as sequential lists - every parent task will be treated as sequential. This can be overridden by appending `=` to the name of the parent task. +If a list name ends with `.`, the top level of tasks will be treated as parallel `@next_action`s. +The waterfall processing will be applied the same way as sequential lists - every parent task will be treated as sequential. This can be overridden by appending `_` to the name of the parent task. Executing NextAction ==================== -You can run NexAction from any system that supports Python, and also deploy to Heroku as a constant running service +You can run NexAction from any system that supports Python. Running NextAction ------------------ NextAction will read your environment to retrieve your Todoist API key, so to run on a Linux/Mac OSX you can use the following commandline - TODOIST_API_KEY="XYZ" python nextaction.py - -Heroku Support --------------- - -[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) - -This package is ready to be pushed to a Heroku instance with minimal configuration values: - -* ```TODOIST_API_KEY``` - Your Todoist API Key -* ```TODOIST_NEXT_ACTION_LABEL``` - The label to use in Todoist for next actions (defaults to next_action) -* ```TODOIST_SYNC_DELAY``` - The number of seconds to wait between syncs. (defaults to 5) -* ```TODOIST_INBOX_HANDLING``` - What method to use for the Inbox, sequence or parallel (defaults to parallel) -* ```TODODIST_PARALLEL_SUFFIX``` - What sequence of characters to use to identify parallel processed projects (defaults to =) -* ```TODODIST_SERIAL_SUFFIX``` - What sequence of characters to use to identify serial processed projects (defaults to -) \ No newline at end of file + python nextaction.py -a diff --git a/app.json b/app.json deleted file mode 100644 index b96f697..0000000 --- a/app.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "NextAction", - "description": "Todoist API application to provide a auto-populated next action label", - "repository": "https://github.com/nikdoof/NextAction", - "keywords": ["python"], - "env": { - "TODOIST_API_KEY": { - "description": "Your Todoist API Key", - "required": true - }, - "TODOIST_NEXT_ACTION_LABEL": { - "description": "The Todoist label to use for next actions.", - "value": "next_action", - "required": false - }, - "TODOIST_SYNC_DELAY": { - "description": "The number of seconds to wait between syncs.", - "value": "5", - "required": false - }, - "TODOIST_INBOX_HANDLING": { - "description": "What method to use for the Inbox, sequence or parallel", - "value": "parallel", - "required": false - }, - "TODODIST_PARALLEL_SUFFIX": { - "description": "What sequence of characters to use to identify parallel processed projects", - "value": "=", - "required": false - }, - "TODODIST_SERIAL_SUFFIX": { - "description": "What sequence of characters to use to identify serial processed projects", - "value": "-", - "required": false - } - } -} \ No newline at end of file diff --git a/nextaction.py b/nextaction.py index f683d10..77d7a83 100755 --- a/nextaction.py +++ b/nextaction.py @@ -1,14 +1,15 @@ #!/usr/bin/env python -import time import logging -import os -import sys import argparse -from datetime import datetime +# noinspection PyPackageRequirements from todoist.api import TodoistAPI +import time +import sys +from datetime import datetime + def get_subitems(items, parent_item=None): """Search a flat item list for child items""" @@ -34,24 +35,20 @@ def get_subitems(items, parent_item=None): def main(): parser = argparse.ArgumentParser() - parser.add_argument('-a', '--api_key', help='Todoist API Key', - default=os.environ.get('TODOIST_API_KEY', None)) - parser.add_argument('-l', '--label', help='The next action label to use', - default=os.environ.get('TODOIST_NEXT_ACTION_LABEL', 'next_action')) - parser.add_argument('-d', '--delay', help='Specify the delay in seconds between syncs', - default=int(os.environ.get('TODOIST_SYNC_DELAY', '5')), type=int) + parser.add_argument('-a', '--api_key', help='Todoist API Key') + parser.add_argument('-l', '--label', help='The next action label to use', default='next_action') + parser.add_argument('-d', '--delay', help='Specify the delay in seconds between syncs', default=5, type=int) parser.add_argument('--debug', help='Enable debugging', action='store_true') parser.add_argument('--inbox', help='The method the Inbox project should be processed', - default=os.environ.get('TODOIST_INBOX_HANDLING', 'parallel'), - choices=['parallel', 'serial']) - parser.add_argument('--parallel_suffix', default=os.environ.get('TODOIST_PARALLEL_SUFFIX', '=')) - parser.add_argument('--serial_suffix', default=os.environ.get('TODOIST_SERIAL_SUFFIX', '-')) + default='parallel', choices=['parallel', 'serial']) + parser.add_argument('--parallel_suffix', default='.') + parser.add_argument('--serial_suffix', default='_') parser.add_argument('--hide_future', help='Hide future dated next actions until the specified number of days', - default=int(os.environ.get('TODOIST_HIDE_FUTURE', '7')), type=int) + default=7, type=int) args = parser.parse_args() # Set debug - if args.debug or os.environ.get('TODOIST_DEBUG', None): + if args.debug: log_level = logging.DEBUG else: log_level = logging.INFO @@ -89,47 +86,51 @@ def main(): # Main loop while True: - api.sync(resource_types=['projects', 'labels', 'items']) - for project in api.projects.all(): - project_type = get_project_type(project) - if project_type: - logging.debug('Project %s being processed as %s', project['name'], project_type) + try: + api.sync(resource_types=['projects', 'labels', 'items']) + except Exception as e: + logging.exception('Error trying to sync with Todoist API: %s' % str(e)) + else: + for project in api.projects.all(): + project_type = get_project_type(project) + if project_type: + logging.debug('Project %s being processed as %s', project['name'], project_type) - items = sorted(api.items.all(lambda x: x['project_id'] == project['id']), key=lambda x: x['item_order']) + items = sorted(api.items.all(lambda x: x['project_id'] == project['id']), key=lambda x: x['item_order']) - for item in items: - labels = item['labels'] + for item in items: + labels = item['labels'] - # If its too far in the future, remove the next_action tag and skip - if args.hide_future > 0 and 'due_date_utc' in item.data and item['due_date_utc'] is not None: - due_date = datetime.strptime(item['due_date_utc'], '%a %d %b %Y %H:%M:%S +0000') - future_diff = (due_date - datetime.utcnow()).total_seconds() - if future_diff >= (args.hide_future * 86400): - if label_id in labels: - labels.remove(label_id) - logging.debug('Updating %s without label as its too far in the future', item['content']) - item.update(labels=labels) - continue + # If its too far in the future, remove the next_action tag and skip + if args.hide_future > 0 and 'due_date_utc' in item.data and item['due_date_utc'] is not None: + due_date = datetime.strptime(item['due_date_utc'], '%a %d %b %Y %H:%M:%S +0000') + future_diff = (due_date - datetime.utcnow()).total_seconds() + if future_diff >= (args.hide_future * 86400): + if label_id in labels: + labels.remove(label_id) + logging.debug('Updating %s without label as its too far in the future', item['content']) + item.update(labels=labels) + continue - # Process item - if project_type == 'serial': - if item['item_order'] == 1: + # Process item + if project_type == 'serial': + if item['item_order'] == 1: + if label_id not in labels: + labels.append(label_id) + logging.debug('Updating %s with label', item['content']) + item.update(labels=labels) + else: + if label_id in labels: + labels.remove(label_id) + logging.debug('Updating %s without label', item['content']) + item.update(labels=labels) + elif project_type == 'parallel': if label_id not in labels: - labels.append(label_id) logging.debug('Updating %s with label', item['content']) + labels.append(label_id) item.update(labels=labels) - else: - if label_id in labels: - labels.remove(label_id) - logging.debug('Updating %s without label', item['content']) - item.update(labels=labels) - elif project_type == 'parallel': - if label_id not in labels: - logging.debug('Updating %s with label', item['content']) - labels.append(label_id) - item.update(labels=labels) - api.commit() + api.commit() logging.debug('Sleeping for %d seconds', args.delay) time.sleep(args.delay) diff --git a/setup.py b/setup.py index 3954474..7f3632b 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,8 @@ -from distutils.core import setup +from setuptools import setup setup( name='NextAction', - version='0.1', + version='0.2', py_modules=['nextaction'], url='https://github.com/nikdoof/NextAction', license='MIT', @@ -10,8 +10,11 @@ setup( author_email='andy@tensixtyone.com', description='A more GTD-like workflow for Todoist. Uses the REST API to add and remove a @next_action label from tasks.', entry_points={ - "distutils.commands": [ - "nextaction = nextaction:main", + "console_scripts": [ + "nextaction=nextaction:main", ], - } + }, + install_requires=[ + 'todoist-python', + ] )