mirror of https://github.com/Hoffelhas/autodoist
General clean-up of code
parent
17cc3a8418
commit
376e176d6b
570
autodoist.py
570
autodoist.py
|
@ -8,10 +8,10 @@ import argparse
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import time
|
import time
|
||||||
global overview_item_ids
|
|
||||||
global overview_item_labels
|
|
||||||
|
|
||||||
# Makes --help text wider
|
# Makes --help text wider
|
||||||
|
|
||||||
|
|
||||||
def make_wide(formatter, w=120, h=36):
|
def make_wide(formatter, w=120, h=36):
|
||||||
"""Return a wider HelpFormatter, if possible."""
|
"""Return a wider HelpFormatter, if possible."""
|
||||||
try:
|
try:
|
||||||
|
@ -24,62 +24,10 @@ def make_wide(formatter, w=120, h=36):
|
||||||
logging.error("Argparse help formatter failed, falling back.")
|
logging.error("Argparse help formatter failed, falling back.")
|
||||||
return formatter
|
return formatter
|
||||||
|
|
||||||
def main():
|
# Sync with Todoist API
|
||||||
|
|
||||||
# Version
|
|
||||||
current_version = 'v1.4.1'
|
|
||||||
|
|
||||||
"""Main process function."""
|
def sync(api):
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
formatter_class=make_wide(argparse.HelpFormatter, w=110, h=50))
|
|
||||||
parser.add_argument('-a', '--api_key',
|
|
||||||
help='Takes your Todoist API Key.', type=str)
|
|
||||||
parser.add_argument(
|
|
||||||
'-l', '--label', help='Enable next action labelling. Define which label to use.', type=str)
|
|
||||||
parser.add_argument(
|
|
||||||
'-r', '--recurring', help='Enable regeneration of sub-tasks in recurring lists.', action='store_true')
|
|
||||||
parser.add_argument(
|
|
||||||
'-e', '--end', help='Enable alternative end-of-day time instead of default midnight. Enter a number from 1 to 24 to define which hour is used.', type=int)
|
|
||||||
parser.add_argument(
|
|
||||||
'-d', '--delay', help='Specify the delay in seconds between syncs (default 5).', default=5, type=int)
|
|
||||||
parser.add_argument(
|
|
||||||
'-pp', '--pp_suffix', help='Change suffix for parallel-parallel labeling (default "//").', default='//')
|
|
||||||
parser.add_argument(
|
|
||||||
'-ss', '--ss_suffix', help='Change suffix for sequential-sequential labeling (default "--").', default='--')
|
|
||||||
parser.add_argument(
|
|
||||||
'-ps', '--ps_suffix', help='Change suffix for parallel-sequential labeling (default "/-").', default='/-')
|
|
||||||
parser.add_argument(
|
|
||||||
'-sp', '--sp_suffix', help='Change suffix for sequential-parallel labeling (default "-/").', default='-/')
|
|
||||||
parser.add_argument(
|
|
||||||
'-df', '--dateformat', help='Strptime() format of starting date (default "%%d-%%m-%%Y").', default = '%d-%m-%Y')
|
|
||||||
parser.add_argument(
|
|
||||||
'-hf', '--hide_future', help='Prevent labelling of future tasks beyond a specified number of days.', default = 0, type=int)
|
|
||||||
parser.add_argument(
|
|
||||||
'--onetime', help='Update Todoist once and exit.', action='store_true')
|
|
||||||
parser.add_argument(
|
|
||||||
'--nocache', help='Disables caching data to disk for quicker syncing.', action='store_true')
|
|
||||||
parser.add_argument('--debug', help='Enable detailed debugging in log.',
|
|
||||||
action='store_true')
|
|
||||||
parser.add_argument('--inbox', help='The method the Inbox should be processed with.',
|
|
||||||
default=None, choices=['parallel', 'sequential'])
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# Set debug
|
|
||||||
if args.debug:
|
|
||||||
log_level = logging.DEBUG
|
|
||||||
else:
|
|
||||||
log_level = logging.INFO
|
|
||||||
|
|
||||||
logging.basicConfig(level=log_level,
|
|
||||||
format='%(asctime)s %(levelname)-8s %(message)s',
|
|
||||||
datefmt='%Y-%m-%d %H:%M:%S',
|
|
||||||
handlers=[logging.FileHandler(
|
|
||||||
'debug.log', 'w+', 'utf-8'),
|
|
||||||
logging.StreamHandler()]
|
|
||||||
)
|
|
||||||
# Sync with Todoist API
|
|
||||||
def sync(api):
|
|
||||||
try:
|
try:
|
||||||
logging.debug('Syncing the current state from the API')
|
logging.debug('Syncing the current state from the API')
|
||||||
api.sync()
|
api.sync()
|
||||||
|
@ -88,8 +36,10 @@ def main():
|
||||||
'Error trying to sync with Todoist API: %s' % str(e))
|
'Error trying to sync with Todoist API: %s' % str(e))
|
||||||
quit()
|
quit()
|
||||||
|
|
||||||
# Simple query for yes/no answer
|
# Simple query for yes/no answer
|
||||||
def query_yes_no(question, default="yes"):
|
|
||||||
|
|
||||||
|
def query_yes_no(question, default="yes"):
|
||||||
# """Ask a yes/no question via raw_input() and return their answer.
|
# """Ask a yes/no question via raw_input() and return their answer.
|
||||||
|
|
||||||
# "question" is a string that is presented to the user.
|
# "question" is a string that is presented to the user.
|
||||||
|
@ -121,8 +71,10 @@ def main():
|
||||||
sys.stdout.write("Please respond with 'yes' or 'no' "
|
sys.stdout.write("Please respond with 'yes' or 'no' "
|
||||||
"(or 'y' or 'n').\n")
|
"(or 'y' or 'n').\n")
|
||||||
|
|
||||||
# Initialisation of Autodoist
|
# Initialisation of Autodoist
|
||||||
def initialise(args):
|
|
||||||
|
|
||||||
|
def initialise(args):
|
||||||
|
|
||||||
# Check we have a API key
|
# Check we have a API key
|
||||||
if not args.api_key:
|
if not args.api_key:
|
||||||
|
@ -152,7 +104,8 @@ def main():
|
||||||
logging.info("You are running with the following functionalities:\n\n Next action labelling mode: {}\n Regenerate sub-tasks mode: {}\n Shifted end-of-day mode: {}\n".format(*modes))
|
logging.info("You are running with the following functionalities:\n\n Next action labelling mode: {}\n Regenerate sub-tasks mode: {}\n Shifted end-of-day mode: {}\n".format(*modes))
|
||||||
|
|
||||||
if m_num == 0:
|
if m_num == 0:
|
||||||
logging.info("\n No functionality has been enabled. Please see --help for the available options.\n")
|
logging.info(
|
||||||
|
"\n No functionality has been enabled. Please see --help for the available options.\n")
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
# Run the initial sync
|
# Run the initial sync
|
||||||
|
@ -179,7 +132,8 @@ def main():
|
||||||
logging.info(
|
logging.info(
|
||||||
"\n\nLabel '{}' doesn't exist in your Todoist\n".format(args.label))
|
"\n\nLabel '{}' doesn't exist in your Todoist\n".format(args.label))
|
||||||
# sys.exit(1)
|
# sys.exit(1)
|
||||||
response = query_yes_no('Do you want to automatically create this label?')
|
response = query_yes_no(
|
||||||
|
'Do you want to automatically create this label?')
|
||||||
|
|
||||||
if response:
|
if response:
|
||||||
api.labels.add(args.label)
|
api.labels.add(args.label)
|
||||||
|
@ -200,8 +154,10 @@ def main():
|
||||||
|
|
||||||
return api, label_id
|
return api, label_id
|
||||||
|
|
||||||
# Check for Autodoist update
|
# Check for Autodoist update
|
||||||
def check_for_update(current_version):
|
|
||||||
|
|
||||||
|
def check_for_update(current_version):
|
||||||
updateurl = 'https://api.github.com/repos/Hoffelhas/autodoist/releases'
|
updateurl = 'https://api.github.com/repos/Hoffelhas/autodoist/releases'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -227,9 +183,12 @@ def main():
|
||||||
logging.error("Error while checking for updates: {}".format(e))
|
logging.error("Error while checking for updates: {}".format(e))
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# Assign current type based on settings
|
# Assign current type based on settings
|
||||||
def check_name(name):
|
|
||||||
len_suffix = [len(args.pp_suffix), len(args.ss_suffix), len(args.ps_suffix), len(args.sp_suffix)]
|
|
||||||
|
def check_name(args, name):
|
||||||
|
len_suffix = [len(args.pp_suffix), len(args.ss_suffix),
|
||||||
|
len(args.ps_suffix), len(args.sp_suffix)]
|
||||||
|
|
||||||
if name == 'Inbox':
|
if name == 'Inbox':
|
||||||
current_type = args.inbox
|
current_type = args.inbox
|
||||||
|
@ -241,19 +200,24 @@ def main():
|
||||||
current_type = 'p-s'
|
current_type = 'p-s'
|
||||||
elif name[-len_suffix[1]:] == args.sp_suffix:
|
elif name[-len_suffix[1]:] == args.sp_suffix:
|
||||||
current_type = 's-p'
|
current_type = 's-p'
|
||||||
elif args.ps_suffix == '/-' and name[-2:] == '_-': # Workaround for section names, which don't allow / symbol.
|
# Workaround for section names, which don't allow / symbol.
|
||||||
|
elif args.ps_suffix == '/-' and name[-2:] == '_-':
|
||||||
current_type = 'p-s'
|
current_type = 'p-s'
|
||||||
elif args.sp_suffix == '-/' and name[-2:] == '-_': # Workaround for section names, which don't allow / symbol.
|
# Workaround for section names, which don't allow / symbol.
|
||||||
|
elif args.sp_suffix == '-/' and name[-2:] == '-_':
|
||||||
current_type = 's-p'
|
current_type = 's-p'
|
||||||
elif args.pp_suffix == '//' and name[-1:] == '_': # Workaround for section names, which don't allow / symbol.
|
# Workaround for section names, which don't allow / symbol.
|
||||||
|
elif args.pp_suffix == '//' and name[-1:] == '_':
|
||||||
current_type = 'parallel'
|
current_type = 'parallel'
|
||||||
else:
|
else:
|
||||||
current_type = None
|
current_type = None
|
||||||
|
|
||||||
return current_type
|
return current_type
|
||||||
|
|
||||||
# Scan the end of a name to find what type it is
|
# Scan the end of a name to find what type it is
|
||||||
def get_type(object, key):
|
|
||||||
|
|
||||||
|
def get_type(args, object, key):
|
||||||
|
|
||||||
object_name = ''
|
object_name = ''
|
||||||
|
|
||||||
|
@ -271,7 +235,7 @@ def main():
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
current_type = check_name(object_name)
|
current_type = check_name(args, object_name)
|
||||||
|
|
||||||
# Check if project type changed with respect to previous run
|
# Check if project type changed with respect to previous run
|
||||||
if old_type == current_type:
|
if old_type == current_type:
|
||||||
|
@ -282,28 +246,34 @@ def main():
|
||||||
|
|
||||||
return current_type, type_changed
|
return current_type, type_changed
|
||||||
|
|
||||||
# Determine a project type
|
# Determine a project type
|
||||||
def get_project_type(project_object):
|
|
||||||
|
|
||||||
|
def get_project_type(args, project_object):
|
||||||
"""Identifies how a project should be handled."""
|
"""Identifies how a project should be handled."""
|
||||||
project_type, project_type_changed = get_type(
|
project_type, project_type_changed = get_type(
|
||||||
project_object, 'project_type')
|
args, project_object, 'project_type')
|
||||||
|
|
||||||
return project_type, project_type_changed
|
return project_type, project_type_changed
|
||||||
|
|
||||||
# Determine a section type
|
# Determine a section type
|
||||||
def get_section_type(section_object):
|
|
||||||
|
|
||||||
|
def get_section_type(args, section_object):
|
||||||
"""Identifies how a section should be handled."""
|
"""Identifies how a section should be handled."""
|
||||||
if section_object is not None:
|
if section_object is not None:
|
||||||
section_type, section_type_changed = get_type(
|
section_type, section_type_changed = get_type(
|
||||||
section_object, 'section_type')
|
args, section_object, 'section_type')
|
||||||
else:
|
else:
|
||||||
section_type = None
|
section_type = None
|
||||||
section_type_changed = 0
|
section_type_changed = 0
|
||||||
|
|
||||||
return section_type, section_type_changed
|
return section_type, section_type_changed
|
||||||
|
|
||||||
# Determine an item type
|
# Determine an item type
|
||||||
def get_item_type(item, project_type):
|
|
||||||
|
|
||||||
|
def get_item_type(args, item, project_type):
|
||||||
"""Identifies how an item with sub items should be handled."""
|
"""Identifies how an item with sub items should be handled."""
|
||||||
|
|
||||||
if project_type is None and item['parent_id'] != 0:
|
if project_type is None and item['parent_id'] != 0:
|
||||||
|
@ -312,14 +282,16 @@ def main():
|
||||||
item_type_changed = 1
|
item_type_changed = 1
|
||||||
item['item_type'] = item_type
|
item['item_type'] = item_type
|
||||||
except:
|
except:
|
||||||
item_type, item_type_changed = get_type(item, 'item_type')
|
item_type, item_type_changed = get_type(args, item, 'item_type')
|
||||||
else:
|
else:
|
||||||
item_type, item_type_changed = get_type(item, 'item_type')
|
item_type, item_type_changed = get_type(args, item, 'item_type')
|
||||||
|
|
||||||
return item_type, item_type_changed
|
return item_type, item_type_changed
|
||||||
|
|
||||||
# Logic to add a label to an item
|
# Logic to add a label to an item
|
||||||
def add_label(item, label):
|
|
||||||
|
|
||||||
|
def add_label(item, label, overview_item_ids, overview_item_labels):
|
||||||
if label not in item['labels']:
|
if label not in item['labels']:
|
||||||
labels = item['labels']
|
labels = item['labels']
|
||||||
logging.debug('Updating \'%s\' with label', item['content'])
|
logging.debug('Updating \'%s\' with label', item['content'])
|
||||||
|
@ -331,8 +303,10 @@ def main():
|
||||||
overview_item_ids[str(item['id'])] = 1
|
overview_item_ids[str(item['id'])] = 1
|
||||||
overview_item_labels[str(item['id'])] = labels
|
overview_item_labels[str(item['id'])] = labels
|
||||||
|
|
||||||
# Logic to remove a label from an item
|
# Logic to remove a label from an item
|
||||||
def remove_label(item, label):
|
|
||||||
|
|
||||||
|
def remove_label(item, label, overview_item_ids, overview_item_labels):
|
||||||
if label in item['labels']:
|
if label in item['labels']:
|
||||||
labels = item['labels']
|
labels = item['labels']
|
||||||
logging.debug('Removing \'%s\' of its label', item['content'])
|
logging.debug('Removing \'%s\' of its label', item['content'])
|
||||||
|
@ -344,16 +318,20 @@ def main():
|
||||||
overview_item_ids[str(item['id'])] = -1
|
overview_item_ids[str(item['id'])] = -1
|
||||||
overview_item_labels[str(item['id'])] = labels
|
overview_item_labels[str(item['id'])] = labels
|
||||||
|
|
||||||
# Ensure labels are only issued once per item
|
# Ensure labels are only issued once per item
|
||||||
def update_labels(label_id):
|
|
||||||
|
|
||||||
|
def update_labels(api, label_id, overview_item_ids, overview_item_labels):
|
||||||
filtered_overview_ids = [
|
filtered_overview_ids = [
|
||||||
k for k, v in overview_item_ids.items() if v != 0]
|
k for k, v in overview_item_ids.items() if v != 0]
|
||||||
for item_id in filtered_overview_ids:
|
for item_id in filtered_overview_ids:
|
||||||
labels = overview_item_labels[item_id]
|
labels = overview_item_labels[item_id]
|
||||||
api.items.update(item_id, labels=labels)
|
api.items.update(item_id, labels=labels)
|
||||||
|
|
||||||
# To handle items which have no sections
|
# To handle items which have no sections
|
||||||
def create_none_section():
|
|
||||||
|
|
||||||
|
def create_none_section():
|
||||||
none_sec = {
|
none_sec = {
|
||||||
'id': None,
|
'id': None,
|
||||||
'name': 'None',
|
'name': 'None',
|
||||||
|
@ -361,7 +339,10 @@ def main():
|
||||||
}
|
}
|
||||||
return none_sec
|
return none_sec
|
||||||
|
|
||||||
def check_header(level):
|
# Check if header logic needs to be applied
|
||||||
|
|
||||||
|
|
||||||
|
def check_header(level):
|
||||||
header_all_in_level = False
|
header_all_in_level = False
|
||||||
unheader_all_in_level = False
|
unheader_all_in_level = False
|
||||||
method = 0
|
method = 0
|
||||||
|
@ -395,123 +376,11 @@ def main():
|
||||||
|
|
||||||
return header_all_in_level, unheader_all_in_level
|
return header_all_in_level, unheader_all_in_level
|
||||||
|
|
||||||
# Check for updates
|
# Recurring lists logic
|
||||||
check_for_update(current_version)
|
|
||||||
|
|
||||||
# Initialise api
|
|
||||||
api, label_id = initialise(args)
|
|
||||||
|
|
||||||
# Start main loop
|
def run_recurring_lists_logic(args, api, item, child_items_all):
|
||||||
while True:
|
|
||||||
start_time = time.time()
|
|
||||||
overview_item_ids = {}
|
|
||||||
overview_item_labels = {}
|
|
||||||
sync(api)
|
|
||||||
|
|
||||||
for project in api.projects.all():
|
|
||||||
|
|
||||||
# To determine if a sequential task was found
|
|
||||||
first_found_project = False
|
|
||||||
|
|
||||||
# Check if we need to (un)header entire project
|
|
||||||
header_all_in_p, unheader_all_in_p = check_header(project)
|
|
||||||
|
|
||||||
if label_id is not None:
|
|
||||||
# Get project type
|
|
||||||
project_type, project_type_changed = get_project_type(project)
|
|
||||||
logging.debug('Project \'%s\' being processed as %s',
|
|
||||||
project['name'], project_type)
|
|
||||||
|
|
||||||
# Get all items for the project
|
|
||||||
project_items = api.items.all(lambda x: x['project_id'] == project['id'])
|
|
||||||
|
|
||||||
# Run for both none-sectioned and sectioned items
|
|
||||||
for s in [0,1]:
|
|
||||||
if s == 0:
|
|
||||||
sections = [create_none_section()]
|
|
||||||
elif s == 1:
|
|
||||||
sections = api.sections.all(lambda x: x['project_id'] == project['id'])
|
|
||||||
|
|
||||||
for section in sections:
|
|
||||||
|
|
||||||
# Check if we need to (un)header entire secion
|
|
||||||
header_all_in_s, unheader_all_in_s = check_header(section)
|
|
||||||
|
|
||||||
# To determine if a sequential task was found
|
|
||||||
first_found_section = False
|
|
||||||
|
|
||||||
# Get section type
|
|
||||||
section_type, section_type_changed = get_section_type(
|
|
||||||
section)
|
|
||||||
logging.debug('Identified \'%s\' as %s type',
|
|
||||||
section['name'], section_type)
|
|
||||||
|
|
||||||
# Get all items for the section
|
|
||||||
items = [x for x in project_items if x['section_id'] == section['id']]
|
|
||||||
|
|
||||||
# Change top parents_id in order to sort later on
|
|
||||||
for item in items:
|
|
||||||
if not item['parent_id']:
|
|
||||||
item['parent_id'] = 0
|
|
||||||
|
|
||||||
# Sort by parent_id and filter for completable items
|
|
||||||
items = sorted(items, key=lambda x: (
|
|
||||||
x['parent_id'], x['child_order']))
|
|
||||||
|
|
||||||
# If a type has changed, clean label for good measure
|
|
||||||
if label_id is not None:
|
|
||||||
if project_type_changed == 1 or section_type_changed == 1:
|
|
||||||
# Remove labels
|
|
||||||
[remove_label(item, label_id) for item in items]
|
|
||||||
# Remove parent types
|
|
||||||
for item in items:
|
|
||||||
item['parent_type'] = None
|
|
||||||
|
|
||||||
# For all items in this section
|
|
||||||
for item in items:
|
|
||||||
active_type = None # Reset
|
|
||||||
|
|
||||||
# notes = api.notes.all() TODO: Quick notes test to see what the impact is?
|
|
||||||
# note_content = [x['content'] for x in notes if x['item_id'] == item['id']]
|
|
||||||
# print(note_content)
|
|
||||||
|
|
||||||
# Determine which child_items exist, both all and the ones that have not been checked yet
|
|
||||||
non_checked_items = list(
|
|
||||||
filter(lambda x: x['checked'] == 0, items))
|
|
||||||
child_items_all = list(
|
|
||||||
filter(lambda x: x['parent_id'] == item['id'], items))
|
|
||||||
child_items = list(
|
|
||||||
filter(lambda x: x['parent_id'] == item['id'], non_checked_items))
|
|
||||||
|
|
||||||
# Check if we need to (un)header entire item tree
|
|
||||||
header_all_in_i, unheader_all_in_i = check_header(item)
|
|
||||||
|
|
||||||
# Logic for applying and removing headers
|
|
||||||
if any([header_all_in_p, header_all_in_s, header_all_in_i]):
|
|
||||||
if item['content'][0] != '*':
|
|
||||||
item.update(content='* ' + item['content'])
|
|
||||||
for ci in child_items:
|
|
||||||
if not ci['content'].startswith('*'):
|
|
||||||
ci.update(content='* ' + ci['content'])
|
|
||||||
|
|
||||||
if any([unheader_all_in_p, unheader_all_in_s]):
|
|
||||||
if item['content'][0] == '*':
|
|
||||||
item.update(content=item['content'][2:])
|
|
||||||
if unheader_all_in_i:
|
|
||||||
[ci.update(content=ci['content'][2:]) for ci in child_items]
|
|
||||||
|
|
||||||
# Logic for recurring lists
|
|
||||||
if not args.recurring:
|
|
||||||
try:
|
|
||||||
# If old label is present, reset it
|
|
||||||
if item['r_tag'] == 1:
|
|
||||||
item['r_tag'] = 0
|
|
||||||
api.items.update(item['id'])
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# If options turned on, start recurring lists logic
|
|
||||||
if args.recurring or args.end:
|
|
||||||
if item['parent_id'] == 0:
|
if item['parent_id'] == 0:
|
||||||
try:
|
try:
|
||||||
if item['due']['is_recurring']:
|
if item['due']['is_recurring']:
|
||||||
|
@ -598,6 +467,128 @@ def main():
|
||||||
item['content'])
|
item['content'])
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Contains all main autodoist functionalities
|
||||||
|
|
||||||
|
|
||||||
|
def autodoist_magic(args, api, label_id):
|
||||||
|
|
||||||
|
# Preallocate dictionaries
|
||||||
|
overview_item_ids = {}
|
||||||
|
overview_item_labels = {}
|
||||||
|
|
||||||
|
for project in api.projects.all():
|
||||||
|
|
||||||
|
# To determine if a sequential task was found
|
||||||
|
first_found_project = False
|
||||||
|
|
||||||
|
# Check if we need to (un)header entire project
|
||||||
|
header_all_in_p, unheader_all_in_p = check_header(project)
|
||||||
|
|
||||||
|
if label_id is not None:
|
||||||
|
# Get project type
|
||||||
|
project_type, project_type_changed = get_project_type(
|
||||||
|
args, project)
|
||||||
|
logging.debug('Project \'%s\' being processed as %s',
|
||||||
|
project['name'], project_type)
|
||||||
|
|
||||||
|
# Get all items for the project
|
||||||
|
project_items = api.items.all(
|
||||||
|
lambda x: x['project_id'] == project['id'])
|
||||||
|
|
||||||
|
# Run for both none-sectioned and sectioned items
|
||||||
|
for s in [0, 1]:
|
||||||
|
if s == 0:
|
||||||
|
sections = [create_none_section()]
|
||||||
|
elif s == 1:
|
||||||
|
sections = api.sections.all(
|
||||||
|
lambda x: x['project_id'] == project['id'])
|
||||||
|
|
||||||
|
for section in sections:
|
||||||
|
|
||||||
|
# Check if we need to (un)header entire secion
|
||||||
|
header_all_in_s, unheader_all_in_s = check_header(section)
|
||||||
|
|
||||||
|
# To determine if a sequential task was found
|
||||||
|
first_found_section = False
|
||||||
|
|
||||||
|
# Get section type
|
||||||
|
section_type, section_type_changed = get_section_type(
|
||||||
|
args, section)
|
||||||
|
logging.debug('Identified \'%s\' as %s type',
|
||||||
|
section['name'], section_type)
|
||||||
|
|
||||||
|
# Get all items for the section
|
||||||
|
items = [x for x in project_items if x['section_id']
|
||||||
|
== section['id']]
|
||||||
|
|
||||||
|
# Change top parents_id in order to sort later on
|
||||||
|
for item in items:
|
||||||
|
if not item['parent_id']:
|
||||||
|
item['parent_id'] = 0
|
||||||
|
|
||||||
|
# Sort by parent_id and filter for completable items
|
||||||
|
items = sorted(items, key=lambda x: (
|
||||||
|
x['parent_id'], x['child_order']))
|
||||||
|
|
||||||
|
# If a type has changed, clean label for good measure
|
||||||
|
if label_id is not None:
|
||||||
|
if project_type_changed == 1 or section_type_changed == 1:
|
||||||
|
# Remove labels
|
||||||
|
[remove_label(item, label_id, overview_item_ids,
|
||||||
|
overview_item_labels) for item in items]
|
||||||
|
# Remove parent types
|
||||||
|
for item in items:
|
||||||
|
item['parent_type'] = None
|
||||||
|
|
||||||
|
# For all items in this section
|
||||||
|
for item in items:
|
||||||
|
active_type = None # Reset
|
||||||
|
|
||||||
|
# notes = api.notes.all() TODO: Quick notes test to see what the impact is?
|
||||||
|
# note_content = [x['content'] for x in notes if x['item_id'] == item['id']]
|
||||||
|
# print(note_content)
|
||||||
|
|
||||||
|
# Determine which child_items exist, both all and the ones that have not been checked yet
|
||||||
|
non_checked_items = list(
|
||||||
|
filter(lambda x: x['checked'] == 0, items))
|
||||||
|
child_items_all = list(
|
||||||
|
filter(lambda x: x['parent_id'] == item['id'], items))
|
||||||
|
child_items = list(
|
||||||
|
filter(lambda x: x['parent_id'] == item['id'], non_checked_items))
|
||||||
|
|
||||||
|
# Check if we need to (un)header entire item tree
|
||||||
|
header_all_in_i, unheader_all_in_i = check_header(item)
|
||||||
|
|
||||||
|
# Logic for applying and removing headers
|
||||||
|
if any([header_all_in_p, header_all_in_s, header_all_in_i]):
|
||||||
|
if item['content'][0] != '*':
|
||||||
|
item.update(content='* ' + item['content'])
|
||||||
|
for ci in child_items:
|
||||||
|
if not ci['content'].startswith('*'):
|
||||||
|
ci.update(content='* ' + ci['content'])
|
||||||
|
|
||||||
|
if any([unheader_all_in_p, unheader_all_in_s]):
|
||||||
|
if item['content'][0] == '*':
|
||||||
|
item.update(content=item['content'][2:])
|
||||||
|
if unheader_all_in_i:
|
||||||
|
[ci.update(content=ci['content'][2:])
|
||||||
|
for ci in child_items]
|
||||||
|
|
||||||
|
# Logic for recurring lists
|
||||||
|
if not args.recurring:
|
||||||
|
try:
|
||||||
|
# If old label is present, reset it
|
||||||
|
if item['r_tag'] == 1:
|
||||||
|
item['r_tag'] = 0
|
||||||
|
api.items.update(item['id'])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# If options turned on, start recurring lists logic
|
||||||
|
if args.recurring or args.end:
|
||||||
|
run_recurring_lists_logic(
|
||||||
|
args, api, item, child_items_all)
|
||||||
|
|
||||||
# If options turned on, start labelling logic
|
# If options turned on, start labelling logic
|
||||||
if label_id is not None:
|
if label_id is not None:
|
||||||
# Skip processing an item if it has already been checked or is a header
|
# Skip processing an item if it has already been checked or is a header
|
||||||
|
@ -608,20 +599,23 @@ def main():
|
||||||
|
|
||||||
# Check item type
|
# Check item type
|
||||||
item_type, item_type_changed = get_item_type(
|
item_type, item_type_changed = get_item_type(
|
||||||
item, project_type)
|
args, item, project_type)
|
||||||
logging.debug('Identified \'%s\' as %s type',
|
logging.debug('Identified \'%s\' as %s type',
|
||||||
item['content'], item_type)
|
item['content'], item_type)
|
||||||
|
|
||||||
# Determine hierarchy types for logic
|
# Determine hierarchy types for logic
|
||||||
hierarchy_types = [item_type, section_type, project_type]
|
hierarchy_types = [item_type,
|
||||||
active_types = [type(x) != type(None) for x in hierarchy_types]
|
section_type, project_type]
|
||||||
|
active_types = [type(x) != type(None)
|
||||||
|
for x in hierarchy_types]
|
||||||
|
|
||||||
# If it is a parentless task
|
# If it is a parentless task
|
||||||
if item['parent_id'] == 0:
|
if item['parent_id'] == 0:
|
||||||
if active_types[0]:
|
if active_types[0]:
|
||||||
# Do item types
|
# Do item types
|
||||||
active_type = item_type
|
active_type = item_type
|
||||||
add_label(item, label_id)
|
add_label(
|
||||||
|
item, label_id, overview_item_ids, overview_item_labels)
|
||||||
|
|
||||||
elif active_types[1]:
|
elif active_types[1]:
|
||||||
# Do section types
|
# Do section types
|
||||||
|
@ -629,10 +623,12 @@ def main():
|
||||||
|
|
||||||
if section_type == 'sequential' or section_type == 's-p':
|
if section_type == 'sequential' or section_type == 's-p':
|
||||||
if not first_found_section:
|
if not first_found_section:
|
||||||
add_label(item, label_id)
|
add_label(
|
||||||
|
item, label_id, overview_item_ids, overview_item_labels)
|
||||||
first_found_section = True
|
first_found_section = True
|
||||||
elif section_type == 'parallel' or section_type == 'p-s':
|
elif section_type == 'parallel' or section_type == 'p-s':
|
||||||
add_label(item, label_id)
|
add_label(
|
||||||
|
item, label_id, overview_item_ids, overview_item_labels)
|
||||||
|
|
||||||
elif active_types[2]:
|
elif active_types[2]:
|
||||||
# Do project types
|
# Do project types
|
||||||
|
@ -640,11 +636,13 @@ def main():
|
||||||
|
|
||||||
if project_type == 'sequential' or project_type == 's-p':
|
if project_type == 'sequential' or project_type == 's-p':
|
||||||
if not first_found_project:
|
if not first_found_project:
|
||||||
add_label(item, label_id)
|
add_label(
|
||||||
|
item, label_id, overview_item_ids, overview_item_labels)
|
||||||
first_found_project = True
|
first_found_project = True
|
||||||
|
|
||||||
elif project_type == 'parallel' or project_type == 'p-s':
|
elif project_type == 'parallel' or project_type == 'p-s':
|
||||||
add_label(item, label_id)
|
add_label(
|
||||||
|
item, label_id, overview_item_ids, overview_item_labels)
|
||||||
|
|
||||||
# Mark other conditions too
|
# Mark other conditions too
|
||||||
if first_found_section == False and active_types[1]:
|
if first_found_section == False and active_types[1]:
|
||||||
|
@ -656,7 +654,7 @@ def main():
|
||||||
if len(child_items) > 0:
|
if len(child_items) > 0:
|
||||||
# Check if item state has changed, if so clean children for good measure
|
# Check if item state has changed, if so clean children for good measure
|
||||||
if item_type_changed == 1:
|
if item_type_changed == 1:
|
||||||
[remove_label(child_item, label_id)
|
[remove_label(child_item, label_id, overview_item_ids, overview_item_labels)
|
||||||
for child_item in child_items]
|
for child_item in child_items]
|
||||||
|
|
||||||
# Process sequential tagged items (item_type can overrule project_type)
|
# Process sequential tagged items (item_type can overrule project_type)
|
||||||
|
@ -666,30 +664,38 @@ def main():
|
||||||
child_item['parent_type'] = active_type
|
child_item['parent_type'] = active_type
|
||||||
# Pass label down to the first child
|
# Pass label down to the first child
|
||||||
if child_item['checked'] == 0 and label_id in item['labels']:
|
if child_item['checked'] == 0 and label_id in item['labels']:
|
||||||
add_label(child_item, label_id)
|
add_label(
|
||||||
remove_label(item, label_id)
|
child_item, label_id, overview_item_ids, overview_item_labels)
|
||||||
|
remove_label(
|
||||||
|
item, label_id, overview_item_ids, overview_item_labels)
|
||||||
else:
|
else:
|
||||||
# Clean for good measure
|
# Clean for good measure
|
||||||
remove_label(child_item, label_id)
|
remove_label(
|
||||||
|
child_item, label_id, overview_item_ids, overview_item_labels)
|
||||||
|
|
||||||
# Process parallel tagged items or untagged parents
|
# Process parallel tagged items or untagged parents
|
||||||
elif active_type == 'parallel' or (active_type == 's-p' and label_id in item['labels']):
|
elif active_type == 'parallel' or (active_type == 's-p' and label_id in item['labels']):
|
||||||
remove_label(item, label_id)
|
remove_label(
|
||||||
|
item, label_id, overview_item_ids, overview_item_labels)
|
||||||
for child_item in child_items:
|
for child_item in child_items:
|
||||||
child_item['parent_type'] = active_type
|
child_item['parent_type'] = active_type
|
||||||
if child_item['checked'] == 0:
|
if child_item['checked'] == 0:
|
||||||
# child_first_found = True
|
# child_first_found = True
|
||||||
add_label(child_item, label_id)
|
add_label(
|
||||||
|
child_item, label_id, overview_item_ids, overview_item_labels)
|
||||||
|
|
||||||
# Remove labels based on start / due dates
|
# Remove labels based on start / due dates
|
||||||
|
|
||||||
# If item is too far in the future, remove the next_action tag and skip
|
# If item is too far in the future, remove the next_action tag and skip
|
||||||
try:
|
try:
|
||||||
if args.hide_future > 0 and 'due' in item.data and item['due'] is not None:
|
if args.hide_future > 0 and 'due' in item.data and item['due'] is not None:
|
||||||
due_date = datetime.strptime(item['due']['date'], "%Y-%m-%d")
|
due_date = datetime.strptime(
|
||||||
future_diff = (due_date - datetime.today()).days
|
item['due']['date'], "%Y-%m-%d")
|
||||||
|
future_diff = (
|
||||||
|
due_date - datetime.today()).days
|
||||||
if future_diff >= args.hide_future:
|
if future_diff >= args.hide_future:
|
||||||
remove_label(item, label_id)
|
remove_label(
|
||||||
|
item, label_id, overview_item_ids, overview_item_labels)
|
||||||
continue
|
continue
|
||||||
except:
|
except:
|
||||||
# Hide-future not set, skip
|
# Hide-future not set, skip
|
||||||
|
@ -702,16 +708,21 @@ def main():
|
||||||
if f1 > -1 and f2 == -1:
|
if f1 > -1 and f2 == -1:
|
||||||
f_end = item['content'][f1+6:].find(' ')
|
f_end = item['content'][f1+6:].find(' ')
|
||||||
if f_end > -1:
|
if f_end > -1:
|
||||||
start_date = item['content'][f1+6:f1+6+f_end]
|
start_date = item['content'][f1 +
|
||||||
|
6:f1+6+f_end]
|
||||||
else:
|
else:
|
||||||
start_date = item['content'][f1+6:]
|
start_date = item['content'][f1+6:]
|
||||||
|
|
||||||
# If start-date hasen't passed, remove all labels
|
# If start-date hasen't passed, remove all labels
|
||||||
start_date = datetime.strptime(start_date , args.dateformat)
|
start_date = datetime.strptime(
|
||||||
future_diff = (datetime.today()-start_date).days
|
start_date, args.dateformat)
|
||||||
|
future_diff = (
|
||||||
|
datetime.today()-start_date).days
|
||||||
if future_diff < 0:
|
if future_diff < 0:
|
||||||
remove_label(item, label_id)
|
remove_label(
|
||||||
[remove_label(child_item, label_id) for child_item in child_items]
|
item, label_id, overview_item_ids, overview_item_labels)
|
||||||
|
[remove_label(child_item, label_id, overview_item_ids,
|
||||||
|
overview_item_labels) for child_item in child_items]
|
||||||
continue
|
continue
|
||||||
|
|
||||||
except:
|
except:
|
||||||
|
@ -723,8 +734,10 @@ def main():
|
||||||
try:
|
try:
|
||||||
f = item['content'].find('start=due-')
|
f = item['content'].find('start=due-')
|
||||||
if f > -1:
|
if f > -1:
|
||||||
f1a = item['content'].find('d') # Find 'd' from 'due'
|
f1a = item['content'].find(
|
||||||
f1b = item['content'].rfind('d') # Find 'd' from days
|
'd') # Find 'd' from 'due'
|
||||||
|
f1b = item['content'].rfind(
|
||||||
|
'd') # Find 'd' from days
|
||||||
f2 = item['content'].find('w')
|
f2 = item['content'].find('w')
|
||||||
f_end = item['content'][f+10:].find(' ')
|
f_end = item['content'][f+10:].find(' ')
|
||||||
|
|
||||||
|
@ -735,9 +748,11 @@ def main():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
item_due_date = item['due']['date']
|
item_due_date = item['due']['date']
|
||||||
item_due_date = datetime.strptime(item_due_date, '%Y-%m-%d')
|
item_due_date = datetime.strptime(
|
||||||
|
item_due_date, '%Y-%m-%d')
|
||||||
except:
|
except:
|
||||||
logging.warning('No due date to determine start date for item: "%s".', item['content'])
|
logging.warning(
|
||||||
|
'No due date to determine start date for item: "%s".', item['content'])
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if f1a != f1b and f1b > -1: # To make sure it doesn't trigger if 'w' is chosen
|
if f1a != f1b and f1b > -1: # To make sure it doesn't trigger if 'w' is chosen
|
||||||
|
@ -747,10 +762,13 @@ def main():
|
||||||
|
|
||||||
# If we're not in the offset from the due date yet, remove all labels
|
# If we're not in the offset from the due date yet, remove all labels
|
||||||
start_date = item_due_date - td
|
start_date = item_due_date - td
|
||||||
future_diff = (datetime.today()-start_date).days
|
future_diff = (
|
||||||
|
datetime.today()-start_date).days
|
||||||
if future_diff < 0:
|
if future_diff < 0:
|
||||||
remove_label(item, label_id)
|
remove_label(
|
||||||
[remove_label(child_item, label_id) for child_item in child_items]
|
item, label_id, overview_item_ids, overview_item_labels)
|
||||||
|
[remove_label(child_item, label_id, overview_item_ids,
|
||||||
|
overview_item_labels) for child_item in child_items]
|
||||||
continue
|
continue
|
||||||
|
|
||||||
except:
|
except:
|
||||||
|
@ -758,9 +776,85 @@ def main():
|
||||||
'Wrong start-date format for item: %s. Please use "start=due-<NUM><d or w>"', item['content'])
|
'Wrong start-date format for item: %s. Please use "start=due-<NUM><d or w>"', item['content'])
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
return overview_item_ids, overview_item_labels
|
||||||
|
|
||||||
|
# Main function
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
# Version
|
||||||
|
current_version = 'v1.4.1'
|
||||||
|
|
||||||
|
# Main process functions.
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
formatter_class=make_wide(argparse.HelpFormatter, w=110, h=50))
|
||||||
|
parser.add_argument('-a', '--api_key',
|
||||||
|
help='Takes your Todoist API Key.', type=str)
|
||||||
|
parser.add_argument(
|
||||||
|
'-l', '--label', help='Enable next action labelling. Define which label to use.', type=str)
|
||||||
|
parser.add_argument(
|
||||||
|
'-r', '--recurring', help='Enable regeneration of sub-tasks in recurring lists.', action='store_true')
|
||||||
|
parser.add_argument(
|
||||||
|
'-e', '--end', help='Enable alternative end-of-day time instead of default midnight. Enter a number from 1 to 24 to define which hour is used.', type=int)
|
||||||
|
parser.add_argument(
|
||||||
|
'-d', '--delay', help='Specify the delay in seconds between syncs (default 5).', default=5, type=int)
|
||||||
|
parser.add_argument(
|
||||||
|
'-pp', '--pp_suffix', help='Change suffix for parallel-parallel labeling (default "//").', default='//')
|
||||||
|
parser.add_argument(
|
||||||
|
'-ss', '--ss_suffix', help='Change suffix for sequential-sequential labeling (default "--").', default='--')
|
||||||
|
parser.add_argument(
|
||||||
|
'-ps', '--ps_suffix', help='Change suffix for parallel-sequential labeling (default "/-").', default='/-')
|
||||||
|
parser.add_argument(
|
||||||
|
'-sp', '--sp_suffix', help='Change suffix for sequential-parallel labeling (default "-/").', default='-/')
|
||||||
|
parser.add_argument(
|
||||||
|
'-df', '--dateformat', help='Strptime() format of starting date (default "%%d-%%m-%%Y").', default='%d-%m-%Y')
|
||||||
|
parser.add_argument(
|
||||||
|
'-hf', '--hide_future', help='Prevent labelling of future tasks beyond a specified number of days.', default=0, type=int)
|
||||||
|
parser.add_argument(
|
||||||
|
'--onetime', help='Update Todoist once and exit.', action='store_true')
|
||||||
|
parser.add_argument(
|
||||||
|
'--nocache', help='Disables caching data to disk for quicker syncing.', action='store_true')
|
||||||
|
parser.add_argument('--debug', help='Enable detailed debugging in log.',
|
||||||
|
action='store_true')
|
||||||
|
parser.add_argument('--inbox', help='The method the Inbox should be processed with.',
|
||||||
|
default=None, choices=['parallel', 'sequential'])
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Set debug
|
||||||
|
if args.debug:
|
||||||
|
log_level = logging.DEBUG
|
||||||
|
else:
|
||||||
|
log_level = logging.INFO
|
||||||
|
|
||||||
|
logging.basicConfig(level=log_level,
|
||||||
|
format='%(asctime)s %(levelname)-8s %(message)s',
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S',
|
||||||
|
handlers=[logging.FileHandler(
|
||||||
|
'debug.log', 'w+', 'utf-8'),
|
||||||
|
logging.StreamHandler()]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for updates
|
||||||
|
check_for_update(current_version)
|
||||||
|
|
||||||
|
# Initialise api
|
||||||
|
api, label_id = initialise(args)
|
||||||
|
|
||||||
|
# Start main loop
|
||||||
|
while True:
|
||||||
|
start_time = time.time()
|
||||||
|
sync(api)
|
||||||
|
|
||||||
|
# Evaluate projects, sections, and items
|
||||||
|
overview_item_ids, overview_item_labels = autodoist_magic(
|
||||||
|
args, api, label_id)
|
||||||
|
|
||||||
# Commit the queue with changes
|
# Commit the queue with changes
|
||||||
if label_id is not None:
|
if label_id is not None:
|
||||||
update_labels(label_id)
|
update_labels(api, label_id, overview_item_ids,
|
||||||
|
overview_item_labels)
|
||||||
|
|
||||||
if len(api.queue):
|
if len(api.queue):
|
||||||
len_api_q = len(api.queue)
|
len_api_q = len(api.queue)
|
||||||
|
@ -783,11 +877,13 @@ def main():
|
||||||
delta_time = end_time - start_time
|
delta_time = end_time - start_time
|
||||||
|
|
||||||
if args.delay - delta_time < 0:
|
if args.delay - delta_time < 0:
|
||||||
logging.debug('Computation time %d is larger than the specified delay %d. Sleeping skipped.', delta_time, args.delay)
|
logging.debug(
|
||||||
|
'Computation time %d is larger than the specified delay %d. Sleeping skipped.', delta_time, args.delay)
|
||||||
elif args.delay >= 0:
|
elif args.delay >= 0:
|
||||||
sleep_time = args.delay - delta_time
|
sleep_time = args.delay - delta_time
|
||||||
logging.debug('Sleeping for %d seconds', sleep_time)
|
logging.debug('Sleeping for %d seconds', sleep_time)
|
||||||
time.sleep(sleep_time)
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
Loading…
Reference in New Issue