From d0b074e0fe3a7f3b72f146daa370949377d813fc Mon Sep 17 00:00:00 2001 From: Gwyndolyn Marchant Date: Wed, 16 Jun 2021 23:02:27 -0400 Subject: [PATCH 01/50] fix for shifted end-of-day --- autodoist.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/autodoist.py b/autodoist.py index 1ef674f..b31b528 100644 --- a/autodoist.py +++ b/autodoist.py @@ -453,10 +453,6 @@ def run_recurring_lists_logic(args, api, item, child_items, child_items_all, reg # Check if the T0 task date has changed if item['due']['date'] != item['date_old']: - # Save the new date for reference us - item.update( - date_old=item['due']['date']) - # Mark children for action based on mode if args.regeneration is not None: @@ -529,6 +525,10 @@ def run_recurring_lists_logic(args, api, item, child_items, child_items_all, reg item.update(due=item_due) # item.update(due={'date': '2020-05-29', 'is_recurring': True, 'string': 'every day'}) + # Save the new date for reference us + item.update( + date_old=item['due']['date']) + except: # If date has never been saved before, create a new entry logging.debug( From 482f27ff50ccdcb211a261a1113aec479fcad303 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Tue, 24 Aug 2021 14:49:34 +0200 Subject: [PATCH 02/50] Added hotfix for detection of underscore replacement of the exclamation mark --- autodoist.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/autodoist.py b/autodoist.py index b31b528..2c7848c 100644 --- a/autodoist.py +++ b/autodoist.py @@ -378,10 +378,12 @@ def check_header(level): method = 0 try: + # Support for legacy structure name = level['name'] method = 1 except: try: + # Current structure content = level['content'] method = 2 except: @@ -391,14 +393,14 @@ def check_header(level): if name[:3] == '** ': header_all_in_level = True level.update(name=name[3:]) - if name[:3] == '!* ': + if name[:3] == '!* ' or name[:3] == '_* ': unheader_all_in_level = True level.update(name=name[3:]) elif method == 2: if content[:3] == '** ': header_all_in_level = True level.update(content=content[3:]) - if content[:3] == '!* ': + if content[:3] == '!* ' or content[:3] == '_* ': unheader_all_in_level = True level.update(content=content[3:]) else: From 7857a397fc34e936dd09f8226327fd6f9497375c Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Tue, 24 Aug 2021 16:14:22 +0200 Subject: [PATCH 03/50] Times in a due date could now be processed. At the moment, only the first 10 string characters are read, which only includes 'YYYY-MM-DD'. This has been updated for all relevant variables which use the due date. --- autodoist.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/autodoist.py b/autodoist.py index 2c7848c..0e92e00 100644 --- a/autodoist.py +++ b/autodoist.py @@ -490,14 +490,17 @@ def run_recurring_lists_logic(args, api, item, child_items, child_items_all, reg t = datetime.today() current_hour = t.hour + if item['content'] == 'b': + print('stop') + # Check if current time is before our end-of-day if (args.end - current_hour) > 0: # Determine the difference in days set by todoist nd = [ - int(x) for x in item['due']['date'].split('-')] + int(x) for x in item['due']['date'][:10].split('-')] od = [ - int(x) for x in item['date_old'].split('-')] + int(x) for x in item['date_old'][:10].split('-')] new_date = datetime( nd[0], nd[1], nd[2]) @@ -529,13 +532,13 @@ def run_recurring_lists_logic(args, api, item, child_items, child_items_all, reg # Save the new date for reference us item.update( - date_old=item['due']['date']) + date_old=item['due']['date'][:10]) except: # If date has never been saved before, create a new entry logging.debug( 'New recurring task detected: %s' % item['content']) - item['date_old'] = item['due']['date'] + item['date_old'] = item['due']['date'][:10] api.items.update(item['id']) except: @@ -804,7 +807,7 @@ def autodoist_magic(args, api, label_id, regen_labels_id): try: 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") + item['due']['date'][:10], "%Y-%m-%d") future_diff = ( due_date - datetime.today()).days if future_diff >= args.hide_future: @@ -861,7 +864,7 @@ def autodoist_magic(args, api, label_id, regen_labels_id): offset = item['content'][f+10:-1] try: - item_due_date = item['due']['date'] + item_due_date = item['due']['date'][:10] item_due_date = datetime.strptime( item_due_date, '%Y-%m-%d') except: From 3a25e05aa3cd266076b0418d9f1983acd172e053 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Mon, 20 Sep 2021 13:26:47 +0200 Subject: [PATCH 04/50] Initial check of 'T0 task date has changed' still included the time, hence check always failed. Now [:10] has been added to exclude a time during the check --- autodoist.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/autodoist.py b/autodoist.py index 0e92e00..a387416 100644 --- a/autodoist.py +++ b/autodoist.py @@ -453,7 +453,7 @@ def run_recurring_lists_logic(args, api, item, child_items, child_items_all, reg if item['due']['is_recurring']: try: # Check if the T0 task date has changed - if item['due']['date'] != item['date_old']: + if item['due']['date'][:10] != item['date_old']: # Mark children for action based on mode if args.regeneration is not None: @@ -490,9 +490,6 @@ def run_recurring_lists_logic(args, api, item, child_items, child_items_all, reg t = datetime.today() current_hour = t.hour - if item['content'] == 'b': - print('stop') - # Check if current time is before our end-of-day if (args.end - current_hour) > 0: From 95a71524cd31a26788ea6596f717e7470d2d4f3f Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sat, 17 Dec 2022 19:31:19 +0100 Subject: [PATCH 05/50] Initial changes made to REST API. Working on a Sync API workaround to find completed tasks now. --- autodoist.py | 79 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/autodoist.py b/autodoist.py index a387416..a12feb0 100644 --- a/autodoist.py +++ b/autodoist.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 -from todoist.api import TodoistAPI +from todoist_api_python.api import TodoistAPI import sys import time import requests @@ -73,15 +73,15 @@ def query_yes_no(question, default="yes"): # Check if label exists, if not, create it - -def verify_label_existance(args, api, label_name, prompt_mode): +def verify_label_existance(api, label_name, prompt_mode): # Check the regeneration label exists - label = api.labels.all(lambda x: x['name'] == label_name) + labels = api.get_labels() + label = [x for x in labels if x.name == label_name] if len(label) > 0: - label_id = label[0]['id'] + label_id = label[0].id logging.debug('Label \'%s\' found as label id %d', - args.label, label_id) + label_name, label_id) else: # Create a new label in Todoist logging.info( @@ -94,11 +94,15 @@ def verify_label_existance(args, api, label_name, prompt_mode): response = True if response: - api.labels.add(label_name) - api.commit() - api.sync() - label = api.labels.all(lambda x: x['name'] == label_name) - label_id = label[0]['id'] + try: + api.add_label(name=label_name) + except Exception as error: + print(error) + + labels = api.get_labels() + label = [x for x in labels if x.name == label_name] + label_id = label[0].id + logging.info("Label '{}' has been created!".format(label_name)) else: logging.info('Exiting Autodoist.') @@ -152,31 +156,33 @@ def initialise(args): logging.debug('Connecting to the Todoist API') api_arguments = {'token': args.api_key} + if args.nocache: logging.debug('Disabling local caching') api_arguments['cache'] = None api = TodoistAPI(**api_arguments) - sync(api) + + logging.info("Autodoist has connected and is running fine!\n") + + # Check if labels exist # If labeling argument is used if args.label is not None: # Verify that the next action label exists; ask user if it needs to be created - label_id = verify_label_existance(args, api, args.label, 1) + label_id = verify_label_existance(api, args.label, 1) else: # Label functionality not needed label_id = None - logging.info("Autodoist has connected and is running fine!\n") - # If regeneration mode is used, verify labels if args.regeneration is not None: # Verify the existance of the regeneraton labels; force creation of label regen_labels_id = [verify_label_existance( - args, api, regen_label, 2) for regen_label in args.regen_label_names] + api, regen_label, 2) for regen_label in args.regen_label_names] else: # Label functionality not needed @@ -567,7 +573,12 @@ def autodoist_magic(args, api, label_id, regen_labels_id): overview_item_ids = {} overview_item_labels = {} - for project in api.projects.all(): + try: + projects = api.get_projects() + except Exception as error: + print(error) + + for project in projects: # To determine if a sequential task was found first_found_project = False @@ -584,16 +595,24 @@ def autodoist_magic(args, api, label_id, regen_labels_id): project['name'], project_type) # Get all items for the project - project_items = api.items.all( - lambda x: x['project_id'] == project['id']) + try: + project_tasks = api.get_tasks(project_id = project.id) + except Exception as error: + print(error) # Run for both none-sectioned and sectioned items - for s in [0, 1]: + + # Get completed tasks: get(api._session, endpoint, api._token, '0')['items'] + + # for s in [0, 1]: # TEMP SECTION ONLY + for s in [1]: if s == 0: - sections = [create_none_section()] + sections = [create_none_section()] # TODO: Rewrite elif s == 1: - sections = api.sections.all( - lambda x: x['project_id'] == project['id']) + try: + sections = api.get_sections(project_id = project.id) + except Exception as error: + print(error) for section in sections: @@ -611,13 +630,13 @@ def autodoist_magic(args, api, label_id, regen_labels_id): section['name'], section_type) # Get all items for the section - items = [x for x in project_items if x['section_id'] - == section['id']] + tasks = [x for x in project_tasks 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 + # Change top parents_id in order to numerically sort later on + for task in tasks: + if not task.parent_id: + task.parent_id = 0 # Sort by parent_id and filter for completable items items = sorted(items, key=lambda x: ( @@ -963,7 +982,7 @@ def main(): # Start main loop while True: start_time = time.time() - sync(api) + # sync(api) # Evaluate projects, sections, and items overview_item_ids, overview_item_labels = autodoist_magic( From 26255470021166bdb794e6d4fb22ce2128ea300b Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Mon, 2 Jan 2023 16:28:16 +0100 Subject: [PATCH 06/50] Basic labelling functionality working again. Still need to fix metadata storage --- Procfile | 1 - autodoist.py | 524 +++++++++++++++++++++++++---------------------- requirements.txt | 4 +- 3 files changed, 279 insertions(+), 250 deletions(-) delete mode 100644 Procfile diff --git a/Procfile b/Procfile deleted file mode 100644 index 4e43088..0000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -nextaction: python nextaction.py -a $TODOIST_API_KEY -l $TODOIST_NEXT_ACTION_LABEL $DEBUG --nocache \ No newline at end of file diff --git a/autodoist.py b/autodoist.py index a12feb0..a79dfbf 100644 --- a/autodoist.py +++ b/autodoist.py @@ -8,6 +8,8 @@ import logging from datetime import datetime, timedelta import time +import sqlite3 +from sqlite3 import Error # Makes --help text wider @@ -79,9 +81,9 @@ def verify_label_existance(api, label_name, prompt_mode): label = [x for x in labels if x.name == label_name] if len(label) > 0: - label_id = label[0].id + next_action_label = label[0].id logging.debug('Label \'%s\' found as label id %d', - label_name, label_id) + label_name, next_action_label) else: # Create a new label in Todoist logging.info( @@ -101,15 +103,14 @@ def verify_label_existance(api, label_name, prompt_mode): labels = api.get_labels() label = [x for x in labels if x.name == label_name] - label_id = label[0].id + next_action_label = label[0].id logging.info("Label '{}' has been created!".format(label_name)) else: logging.info('Exiting Autodoist.') exit(1) - return label_id - + return 0 # Initialisation of Autodoist def initialise(args): @@ -171,11 +172,7 @@ def initialise(args): if args.label is not None: # Verify that the next action label exists; ask user if it needs to be created - label_id = verify_label_existance(api, args.label, 1) - - else: - # Label functionality not needed - label_id = None + verify_label_existance(api, args.label, 1) # If regeneration mode is used, verify labels if args.regeneration is not None: @@ -188,7 +185,7 @@ def initialise(args): # Label functionality not needed regen_labels_id = [None, None, None] - return api, label_id, regen_labels_id + return api # Check for Autodoist update @@ -232,19 +229,20 @@ def check_name(args, name): current_type = 'parallel' elif name[-len_suffix[1]:] == args.ss_suffix: current_type = 'sequential' - elif name[-len_suffix[1]:] == args.ps_suffix: + elif name[-len_suffix[2]:] == args.ps_suffix: current_type = 'p-s' - elif name[-len_suffix[1]:] == args.sp_suffix: + elif name[-len_suffix[3]:] == args.sp_suffix: current_type = 's-p' - # Workaround for section names, which don't allow / symbol. - elif args.ps_suffix == '/-' and name[-2:] == '_-': - current_type = 'p-s' - # Workaround for section names, which don't allow / symbol. - elif args.sp_suffix == '-/' and name[-2:] == '-_': - current_type = 's-p' - # Workaround for section names, which don't allow / symbol. - elif args.pp_suffix == '//' and name[-1:] == '_': - current_type = 'parallel' + #TODO: Remove below workarounds if standard notation is changing. Just messy and no longer needed. + # # Workaround for section names, which don't allow '/' symbol. + # elif args.ps_suffix == '/-' and name[-2:] == '_-': + # current_type = 'p-s' + # # Workaround for section names, which don't allow '/' symbol. + # elif args.sp_suffix == '-/' and name[-2:] == '-_': + # current_type = 's-p' + # # Workaround for section names, which don't allow '/' symbol. + # elif args.pp_suffix == '//' and name[-1:] == '_': + # current_type = 'parallel' else: current_type = None @@ -253,42 +251,45 @@ def check_name(args, name): # Scan the end of a name to find what type it is -def get_type(args, object, key): +def get_type(args, model, key): - object_name = '' + model_name = '' try: - old_type = object[key] + old_type = model[key] #TODO: METADATA: this information used to be part of the metadata, needs to be retreived from own database except: # logging.debug('No defined project_type: %s' % str(e)) old_type = None try: - object_name = object['name'].strip() + model_name = model.name.strip() except: - try: - object_name = object['content'].strip() - except: - pass + #TODO: Old support for legacy tag in v1 API, can likely be removed since moving to v2. + # try: + # + # object_name = object['content'].strip() + # except: + # pass + pass - current_type = check_name(args, object_name) + current_type = check_name(args, model_name) # Check if project type changed with respect to previous run if old_type == current_type: type_changed = 0 else: type_changed = 1 - object[key] = current_type + # model.key = current_type #TODO: METADATA: this information used to be part of the metadata, needs to be retreived from own database return current_type, type_changed # Determine a project type -def get_project_type(args, project_object): +def get_project_type(args, project_model): """Identifies how a project should be handled.""" project_type, project_type_changed = get_type( - args, project_object, 'project_type') + args, project_model, 'project_type') return project_type, project_type_changed @@ -306,65 +307,67 @@ def get_section_type(args, section_object): return section_type, section_type_changed -# Determine an item type +# Determine an task type -def get_item_type(args, item, project_type): - """Identifies how an item with sub items should be handled.""" +def get_task_type(args, task, project_type): + """Identifies how a task with sub tasks should be handled.""" - if project_type is None and item['parent_id'] != 0: + if project_type is None and task.parent_id != 0: try: - item_type = item['parent_type'] - item_type_changed = 1 - item['item_type'] = item_type + task_type = task.parent_type #TODO: METADATA + task_type_changed = 1 + task.task_type = task_type except: - item_type, item_type_changed = get_type(args, item, 'item_type') + task_type, task_type_changed = get_type(args, task, 'task_type') #TODO: METADATA else: - item_type, item_type_changed = get_type(args, item, 'item_type') + task_type, task_type_changed = get_type(args, task, 'task_type') #TODO: METADATA - return item_type, item_type_changed + return task_type, task_type_changed -# Logic to add a label to an item +# Logic to track addition of a label to a task -def add_label(item, label, overview_item_ids, overview_item_labels): - if label not in item['labels']: - labels = item['labels'] - logging.debug('Updating \'%s\' with label', item['content']) +def add_label(task, label, overview_task_ids, overview_task_labels): + if label not in task.labels: + labels = task.labels # Copy other existing labels + logging.debug('Updating \'%s\' with label', task.content) labels.append(label) try: - overview_item_ids[str(item['id'])] += 1 + overview_task_ids[task.id] += 1 except: - overview_item_ids[str(item['id'])] = 1 - overview_item_labels[str(item['id'])] = labels + overview_task_ids[task.id] = 1 + overview_task_labels[task.id] = labels -# Logic to remove a label from an item +# Logic to track removal of a label from a task -def remove_label(item, label, overview_item_ids, overview_item_labels): - if label in item['labels']: - labels = item['labels'] - logging.debug('Removing \'%s\' of its label', item['content']) +def remove_label(task, label, overview_task_ids, overview_task_labels): + if label in task.labels: + labels = task.labels + logging.debug('Removing \'%s\' of its label', task.content) labels.remove(label) try: - overview_item_ids[str(item['id'])] -= 1 + overview_task_ids[task.id] -= 1 except: - overview_item_ids[str(item['id'])] = -1 - overview_item_labels[str(item['id'])] = labels + overview_task_ids[task.id] = -1 + overview_task_labels[task.id] = labels -# Ensure labels are only issued once per item +# Ensure label updates are only issued once per task -def update_labels(api, label_id, overview_item_ids, overview_item_labels): +def update_labels(api, overview_task_ids, overview_task_labels): filtered_overview_ids = [ - k for k, v in overview_item_ids.items() if v != 0] - for item_id in filtered_overview_ids: - labels = overview_item_labels[item_id] - api.items.update(item_id, labels=labels) + k for k, v in overview_task_ids.items() if v != 0] + for task_id in filtered_overview_ids: + labels = overview_task_labels[task_id] + api.update_task(task_id=task_id, labels=labels) -# To handle items which have no sections + return filtered_overview_ids + +# To handle tasks which have no sections def create_none_section(): @@ -390,7 +393,7 @@ def check_header(level): except: try: # Current structure - content = level['content'] + content = level.content method = 2 except: pass @@ -414,38 +417,54 @@ def check_header(level): return header_all_in_level, unheader_all_in_level +# Logic for applying and removing headers +def modify_headers(task, child_tasks, header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t): + if any([header_all_in_p, header_all_in_s, header_all_in_t]): + if task.content[0] != '*': + task.update(content='* ' + task.content) + for ci in child_tasks: + if not ci.content.startswith('*'): + ci.update(content='* ' + ci.content) + + if any([unheader_all_in_p, unheader_all_in_s]): + if task.content[0] == '*': + task.update(content=task.content[2:]) + if unheader_all_in_t: + [ci.update(content=ci.content[2:]) + for ci in child_tasks] + # Check regen mode based on label name def check_regen_mode(api, item, regen_labels_id): - labels = item['labels'] + labels = item.labels overlap = set(labels) & set(regen_labels_id) overlap = [val for val in overlap] if len(overlap) > 1: logging.warning( - 'Multiple regeneration labels used! Please pick only one for item: "{}".'.format(item['content'])) + 'Multiple regeneration labels used! Please pick only one for item: "{}".'.format(item.content)) return None try: - regen_label_id = overlap[0] + regen_next_action_label = overlap[0] except: logging.debug( - 'No regeneration label for item: %s' % item['content']) - regen_label_id = [0] + 'No regeneration label for item: %s' % item.content) + regen_next_action_label = [0] - if regen_label_id == regen_labels_id[0]: + if regen_next_action_label == regen_labels_id[0]: return 0 - elif regen_label_id == regen_labels_id[1]: + elif regen_next_action_label == regen_labels_id[1]: return 1 - elif regen_label_id == regen_labels_id[2]: + elif regen_next_action_label == regen_labels_id[2]: return 2 else: - # label_name = api.labels.get_by_id(regen_label_id)['name'] + # label_name = api.labels.get_by_id(regen_next_action_label)['name'] # logging.debug( - # 'No regeneration label for item: %s' % item['content']) + # 'No regeneration label for item: %s' % item.content) return None @@ -472,10 +491,10 @@ def run_recurring_lists_logic(args, api, item, child_items, child_items_all, reg if regen_mode is None: regen_mode = args.regeneration logging.debug('Using general recurring mode \'%s\' for item: %s', - regen_mode, item['content']) + regen_mode, item.content) else: logging.debug('Using recurring label \'%s\' for item: %s', - regen_mode, item['content']) + regen_mode, item.content) # Apply tags based on mode give_regen_tag = 0 @@ -519,7 +538,7 @@ def run_recurring_lists_logic(args, api, item, child_items, child_items_all, reg # Only apply if overdue and if it's a daily recurring tasks if days_overdue >= 1 and days_difference == 1: - # Find curreny date in string format + # Find current date in string format today_str = [str(x) for x in [ today.year, today.month, today.day]] if len(today_str[1]) == 1: @@ -540,13 +559,13 @@ def run_recurring_lists_logic(args, api, item, child_items, child_items_all, reg except: # If date has never been saved before, create a new entry logging.debug( - 'New recurring task detected: %s' % item['content']) + 'New recurring task detected: %s' % item.content) item['date_old'] = item['due']['date'][:10] api.items.update(item['id']) except: # logging.debug( - # 'Parent not recurring: %s' % item['content']) + # 'Parent not recurring: %s' % item.content) pass if args.regeneration is not None and item['parent_id'] != 0: @@ -561,17 +580,17 @@ def run_recurring_lists_logic(args, api, item, child_items, child_items_all, reg child_item['r_tag'] = 1 except: # logging.debug('Child not recurring: %s' % - # item['content']) + # item.content) pass # Contains all main autodoist functionalities -def autodoist_magic(args, api, label_id, regen_labels_id): +def autodoist_magic(args, api, next_action_label, regen_labels_id): # Preallocate dictionaries - overview_item_ids = {} - overview_item_labels = {} + overview_task_ids = {} + overview_task_labels = {} try: projects = api.get_projects() @@ -586,25 +605,29 @@ def autodoist_magic(args, api, label_id, regen_labels_id): # 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 + # Get project type + if next_action_label is not None: project_type, project_type_changed = get_project_type( args, project) + if project_type is not None: logging.debug('Identified \'%s\' as %s type', - project['name'], project_type) + project.name, project_type) - # Get all items for the project + # Get all tasks for the project try: project_tasks = api.get_tasks(project_id = project.id) except Exception as error: print(error) - # Run for both none-sectioned and sectioned items + # Run for both non-sectioned and sectioned tasks - # Get completed tasks: get(api._session, endpoint, api._token, '0')['items'] + # Get completed tasks: + # endpoint = 'https://api.todoist.com/sync/v9/completed/get_all' + # get(api._session, endpoint, api._token, '0')['items'] + # $ curl https://api.todoist.com/sync/v9/sync-H "Authorization: Bearer e2f750b64e8fc06ae14383d5e15ea0792a2c1bf3" -d commands='[ {"type": "item_add", "temp_id": "63f7ed23-a038-46b5-b2c9-4abda9097ffa", "uuid": "997d4b43-55f1-48a9-9e66-de5785dfd69b", "args": {"content": "Buy Milk", "project_id": "2203306141","labels": ["Food", "Shopping"]}}]' - # for s in [0, 1]: # TEMP SECTION ONLY + # for s in [0, 1]: # TODO: TEMPORARELY SKIP SECTIONLESS TASKS for s in [1]: if s == 0: sections = [create_none_section()] # TODO: Rewrite @@ -627,208 +650,199 @@ def autodoist_magic(args, api, label_id, regen_labels_id): args, section) if section_type is not None: logging.debug('Identified \'%s\' as %s type', - section['name'], section_type) + section.name, section_type) - # Get all items for the section + # Get all tasks for the section tasks = [x for x in project_tasks if x.section_id == section.id] - # Change top parents_id in order to numerically sort later on + # Change top tasks parents_id from 'None' to '0' in order to numerically sort later on for task in tasks: if not task.parent_id: task.parent_id = 0 - # Sort by parent_id and filter for completable items - items = sorted(items, key=lambda x: ( - x['parent_id'], x['child_order'])) + # Sort by parent_id and child order + # In the past, Todoist used to screw up the tasks orders, so originally I processed parentless tasks first such that children could properly inherit porperties. + # With the new API this seems to be in order, but I'm keeping this just in case for now. TODO: Could be used for optimization in the future. + tasks = sorted(tasks, key=lambda x: ( + int(x.parent_id), x.order)) - # If a type has changed, clean label for good measure - if label_id is not None: + # If a type has changed, clean all task labels for good measure + if next_action_label 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_label(task, next_action_label, overview_task_ids, + overview_task_labels) for task in tasks] # Remove parent types - for item in items: - item['parent_type'] = None + # for task in tasks: + # task.parent_type = None #TODO: METADATA - # For all items in this section - for item in items: - active_type = None # Reset + # For all tasks in this section + for task in tasks: + dominant_type = None # Reset # Possible nottes routine for the future # 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] + # Determine which child_tasks exist, both all and the ones that have not been checked yet + non_completed_tasks = list( + filter(lambda x: not x.is_completed, tasks)) + child_tasks_all = list( + filter(lambda x: x.parent_id == task.id, tasks)) + child_tasks = list( + filter(lambda x: x.parent_id == task.id, non_completed_tasks)) - # Logic for recurring lists - if not args.regeneration: - try: - # If old label is present, reset it - if item['r_tag'] == 1: - item['r_tag'] = 0 - api.items.update(item['id']) - except: - pass + # Check if we need to (un)header entire task tree + header_all_in_t, unheader_all_in_t = check_header(task) - # If options turned on, start recurring lists logic - if args.regeneration is not None or args.end: - run_recurring_lists_logic( - args, api, item, child_items, child_items_all, regen_labels_id) + # Modify headers where needed + #TODO: DISABLED FOR NOW, FIX LATER + # modify_headers(header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t) + +#TODO: Check is regeneration is still needed, now that it's part of core Todoist. Disabled for now. + # Logic for recurring lists + # if not args.regeneration: + # try: + # # If old label is present, reset it + # if item.r_tag == 1: #TODO: METADATA + # item.r_tag = 0 #TODO: METADATA + # api.items.update(item.id) + # except: + # pass + + # # If options turned on, start recurring lists logic + # if args.regeneration is not None or args.end: + # run_recurring_lists_logic( + # args, api, item, child_items, child_items_all, regen_labels_id) # If options turned on, start labelling logic - if label_id is not None: - # Skip processing an item if it has already been checked or is a header - if item['checked'] == 1: + if next_action_label is not None: + # Skip processing a task if it has already been checked or is a header + if task.is_completed: continue - if item['content'].startswith('*'): + if task.content.startswith('*'): # Remove next action label if it's still present - remove_label(item, label_id, overview_item_ids,overview_item_labels) + remove_label(task, next_action_label, overview_task_ids, overview_task_labels) continue - # Check item type - item_type, item_type_changed = get_item_type( - args, item, project_type) - if item_type is not None: + # Check task type + task_type, task_type_changed = get_task_type( + args, task, project_type) + if task_type is not None: logging.debug('Identified \'%s\' as %s type', - item['content'], item_type) + task.content, task_type) # Determine hierarchy types for logic - hierarchy_types = [item_type, + hierarchy_types = [task_type, section_type, project_type] - active_types = [type(x) != type(None) + hierarchy_boolean = [type(x) != type(None) for x in hierarchy_types] # If it is a parentless task - if item['parent_id'] == 0: - if active_types[0]: - # Do item types - active_type = item_type + if task.parent_id == 0: + if hierarchy_boolean[0]: + # Inherit task type + dominant_type = task_type add_label( - item, label_id, overview_item_ids, overview_item_labels) + task, next_action_label, overview_task_ids, overview_task_labels) - elif active_types[1]: - # Do section types - active_type = section_type + elif hierarchy_boolean[1]: + # Inherit section type + dominant_type = section_type if section_type == 'sequential' or section_type == 's-p': if not first_found_section: add_label( - item, label_id, overview_item_ids, overview_item_labels) + task, next_action_label, overview_task_ids, overview_task_labels) first_found_section = True elif section_type == 'parallel' or section_type == 'p-s': add_label( - item, label_id, overview_item_ids, overview_item_labels) + task, next_action_label, overview_task_ids, overview_task_labels) - elif active_types[2]: - # Do project types - active_type = project_type + elif hierarchy_boolean[2]: + # Inherit project type + dominant_type = project_type if project_type == 'sequential' or project_type == 's-p': if not first_found_project: add_label( - item, label_id, overview_item_ids, overview_item_labels) + task, next_action_label, overview_task_ids, overview_task_labels) first_found_project = True elif project_type == 'parallel' or project_type == 'p-s': add_label( - item, label_id, overview_item_ids, overview_item_labels) + task, next_action_label, overview_task_ids, overview_task_labels) # Mark other conditions too - if first_found_section == False and active_types[1]: + if first_found_section == False and hierarchy_boolean[1]: first_found_section = True - if first_found_project is False and active_types[2]: + if first_found_project is False and hierarchy_boolean[2]: first_found_project = True # If there are children - if len(child_items) > 0: - # Check if item state has changed, if so clean children for good measure - if item_type_changed == 1: - [remove_label(child_item, label_id, overview_item_ids, overview_item_labels) - for child_item in child_items] + if len(child_tasks) > 0: + # Check if task state has changed, if so clean children for good measure + if task_type_changed == 1: + [remove_label(child_task, next_action_label, overview_task_ids, overview_task_labels) + for child_task in child_tasks] # If a sub-task, inherit parent task type - if item['parent_id'] !=0: + if task.parent_id !=0: try: - active_type = item['parent_type'] + dominant_type = task.parent_type #TODO: METADATA except: pass - # Process sequential tagged items (item_type can overrule project_type) - if active_type == 'sequential' or active_type == 'p-s': - for child_item in child_items: + # Process sequential tagged tasks (task_type can overrule project_type) + if dominant_type == 'sequential' or dominant_type == 'p-s': + for child_task in child_tasks: # Ignore headered children - if child_item['content'].startswith('*'): + if child_task.content.startswith('*'): continue - # Pass item_type down to the children - child_item['parent_type'] = active_type + # Pass task_type down to the children + child_task.parent_type = dominant_type # Pass label down to the first child - if child_item['checked'] == 0 and label_id in item['labels']: + if not child_task.is_completed and next_action_label in task.labels: add_label( - child_item, label_id, overview_item_ids, overview_item_labels) + child_task, next_action_label, overview_task_ids, overview_task_labels) remove_label( - item, label_id, overview_item_ids, overview_item_labels) + task, next_action_label, overview_task_ids, overview_task_labels) else: # Clean for good measure remove_label( - child_item, label_id, overview_item_ids, overview_item_labels) + child_task, next_action_label, overview_task_ids, overview_task_labels) - # Process parallel tagged items or untagged parents - elif active_type == 'parallel' or (active_type == 's-p' and label_id in item['labels']): + # Process parallel tagged tasks or untagged parents + elif dominant_type == 'parallel' or (dominant_type == 's-p' and next_action_label in task.labels): remove_label( - item, label_id, overview_item_ids, overview_item_labels) - for child_item in child_items: + task, next_action_label, overview_task_ids, overview_task_labels) + for child_task in child_tasks: # Ignore headered children - if child_item['content'].startswith('*'): + if child_task.content.startswith('*'): continue - child_item['parent_type'] = active_type - if child_item['checked'] == 0: - # child_first_found = True + child_task.parent_type = dominant_type #TODO: METADATA + if not child_task.is_completed: add_label( - child_item, label_id, overview_item_ids, overview_item_labels) + child_task, next_action_label, overview_task_ids, overview_task_labels) # Remove labels based on start / due dates - # If item is too far in the future, remove the next_action tag and skip + # If task is too far in the future, remove the next_action tag and skip #TODO: FIX THIS 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 task.data and task.due is not None: due_date = datetime.strptime( - item['due']['date'][:10], "%Y-%m-%d") + task.due['date'][:10], "%Y-%m-%d") future_diff = ( due_date - datetime.today()).days if future_diff >= args.hide_future: remove_label( - item, label_id, overview_item_ids, overview_item_labels) + task, next_action_label, overview_task_ids, overview_task_labels) continue except: # Hide-future not set, skip @@ -836,15 +850,15 @@ def autodoist_magic(args, api, label_id, regen_labels_id): # If start-date has not passed yet, remove label try: - f1 = item['content'].find('start=') - f2 = item['content'].find('start=due-') + f1 = task.content.find('start=') + f2 = task.content.find('start=due-') if f1 > -1 and f2 == -1: - f_end = item['content'][f1+6:].find(' ') + f_end = task.content[f1+6:].find(' ') if f_end > -1: - start_date = item['content'][f1 + + start_date = task.content[f1 + 6:f1+6+f_end] else: - start_date = item['content'][f1+6:] + start_date = task.content[f1+6:] # If start-date hasen't passed, remove all labels start_date = datetime.strptime( @@ -853,39 +867,39 @@ def autodoist_magic(args, api, label_id, regen_labels_id): datetime.today()-start_date).days if future_diff < 0: remove_label( - 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] + task, next_action_label, overview_task_ids, overview_task_labels) + [remove_label(child_task, next_action_label, overview_task_ids, + overview_task_labels) for child_task in child_tasks] continue except: logging.warning( - 'Wrong start-date format for item: "%s". Please use "start="', item['content']) + 'Wrong start-date format for task: "%s". Please use "start="', task.content) continue - # Recurring task friendly - remove label with relative change from due date + # Recurring task friendly - remove label with relative change from due date #TODO Fix this logic try: - f = item['content'].find('start=due-') + f = task.content.find('start=due-') if f > -1: - f1a = item['content'].find( + f1a = task.content.find( 'd') # Find 'd' from 'due' - f1b = item['content'].rfind( + f1b = task.content.rfind( 'd') # Find 'd' from days - f2 = item['content'].find('w') - f_end = item['content'][f+10:].find(' ') + f2 = task.content.find('w') + f_end = task.content[f+10:].find(' ') if f_end > -1: - offset = item['content'][f+10:f+10+f_end-1] + offset = task.content[f+10:f+10+f_end-1] else: - offset = item['content'][f+10:-1] + offset = task.content[f+10:-1] try: - item_due_date = item['due']['date'][:10] - item_due_date = datetime.strptime( - item_due_date, '%Y-%m-%d') + task_due_date = task.due['date'][:10] + task_due_date = datetime.strptime( + task_due_date, '%Y-%m-%d') except: logging.warning( - 'No due date to determine start date for item: "%s".', item['content']) + 'No due date to determine start date for task: "%s".', task.content) continue if f1a != f1b and f1b > -1: # To make sure it doesn't trigger if 'w' is chosen @@ -894,22 +908,35 @@ def autodoist_magic(args, api, label_id, regen_labels_id): td = timedelta(weeks=int(offset)) # If we're not in the offset from the due date yet, remove all labels - start_date = item_due_date - td + start_date = task_due_date - td future_diff = ( datetime.today()-start_date).days if future_diff < 0: remove_label( - 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] + task, next_action_label, overview_task_ids, overview_task_labels) + [remove_label(child_task, next_action_label, overview_task_ids, + overview_task_labels) for child_task in child_tasks] continue except: logging.warning( - 'Wrong start-date format for item: %s. Please use "start=due-"', item['content']) + 'Wrong start-date format for task: %s. Please use "start=due-"', task.content) continue - return overview_item_ids, overview_item_labels + return overview_task_ids, overview_task_labels + + +# Connect to SQLite database + +def create_connection(path): + connection = None + try: + connection = sqlite3.connect(path) + print("Connection to SQLite DB successful") + except Error as e: + print(f"The error '{e}' occurred") + + return connection # Main @@ -933,13 +960,13 @@ def main(): 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='//') + '-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='/-') + '-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='-/') + '-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( @@ -955,6 +982,9 @@ def main(): args = parser.parse_args() + # #TODO: Temporary disable this feature for now. Find a way to see completed tasks first, since REST API v2 lost this funcionality. + args.regeneration = 0 + # Addition of regeneration labels args.regen_label_names = ('Regen_off', 'Regen_all', 'Regen_all_if_completed') @@ -977,25 +1007,25 @@ def main(): check_for_update(current_version) # Initialise api - api, label_id, regen_labels_id = initialise(args) + api = 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, regen_labels_id) + # Evaluate projects, sections, and tasks + overview_task_ids, overview_task_labels = autodoist_magic( + args, api, args.label, args.regen_label_names) + + # Commit next action label changes + if args.label is not None: + updated_ids = update_labels(api, overview_task_ids, + overview_task_labels) - # Commit the queue with changes - if label_id is not None: - update_labels(api, label_id, overview_item_ids, - overview_item_labels) + if len(updated_ids): + len_api_q = len(updated_ids) - if len(api.queue): - len_api_q = len(api.queue) - api.commit() if len_api_q == 1: logging.info( '%d change committed to Todoist.', len_api_q) diff --git a/requirements.txt b/requirements.txt index 2a97913..557e7ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -requests>=2.25.1 -todoist_python>=8.1.3 +requests>=2.28.1 +todoist_api_python>=2.0.2 From 374d216a83515a2c2dc22878311948c314ad7d6c Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Tue, 3 Jan 2023 16:06:46 +0100 Subject: [PATCH 07/50] Basic SQLite DB functionality now working. Next: rewrite 'get_task_type()' function, and start some refactoring/debugging. --- autodoist.py | 901 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 563 insertions(+), 338 deletions(-) diff --git a/autodoist.py b/autodoist.py index a79dfbf..988a31a 100644 --- a/autodoist.py +++ b/autodoist.py @@ -1,6 +1,9 @@ #!/usr/bin/python3 from todoist_api_python.api import TodoistAPI +from todoist_api_python.models import Task +from todoist_api_python.models import Section +from todoist_api_python.models import Project import sys import time import requests @@ -10,6 +13,212 @@ import time import sqlite3 from sqlite3 import Error +import os + +# Connect to SQLite database + + +def create_connection(path): + connection = None + try: + connection = sqlite3.connect(path) + logging.info("Connection to SQLite DB successful") + except Exception as e: + logging.error( + f"Could not connect to the SQLite database: the error '{e}' occurred") + sys.exit(1) + + return connection + +# Close conenction to SQLite database + + +def close_connection(connection): + try: + connection.close() + except Exception as e: + logging.error( + f"Could not close the SQLite database: the error '{e}' occurred") + sys.exit(1) + +# Execute any SQLite query passed to it in the form of string + + +def execute_query(connection, query): + cursor = connection.cursor() + try: + cursor.execute(query) + connection.commit() + logging.debug("Query executed: {}".format(query)) + except Exception as e: + logging.debug(f"The error '{e}' occurred") + +# Pass query to select and read record. Outputs a tuple. + + +def execute_read_query(connection, query): + cursor = connection.cursor() + result = None + try: + cursor.execute(query) + result = cursor.fetchall() + logging.debug("Query fetched: {}".format(query)) + return result + except Exception as e: + logging.debug(f"The error '{e}' occurred") + +# Construct query and read a value + + +def db_read_value(connection, model, column): + try: + if isinstance(model, Task): + db_name = 'tasks' + goal = 'task_id' + elif isinstance(model, Section): + db_name = 'sections' + goal = 'section_id' + elif isinstance(model, Project): + db_name = 'projects' + goal = 'project_id' + + query = "SELECT %s FROM %s where %s=%r" % ( + column, db_name, goal, model.id) + + result = execute_read_query(connection, query) + + except Exception as e: + logging.debug(f"The error '{e}' occurred") + + return result + +# Construct query and update a value + + +def db_update_value(connection, model, column, value): + + try: + if isinstance(model, Task): + db_name = 'tasks' + goal = 'task_id' + + elif isinstance(model, Section): + db_name = 'sections' + goal = 'section_id' + + elif isinstance(model, Project): + db_name = 'projects' + goal = 'project_id' + + query = """ + UPDATE + %s + SET + %s = %r + WHERE + %s = %r + """ % (db_name, column, value, goal, model.id) + + result = execute_query(connection, query) + + except Exception as e: + logging.debug(f"The error '{e}' occurred") + + return result + + +# Check if the id of a model exists, if not, add to database + + +def db_check_existance(connection, model): + try: + if isinstance(model, Task): + db_name = 'tasks' + goal = 'task_id' + elif isinstance(model, Section): + db_name = 'sections' + goal = 'section_id' + elif isinstance(model, Project): + db_name = 'projects' + goal = 'project_id' + + q_check_existence = "SELECT EXISTS(SELECT 1 FROM %s WHERE %s=%r)" % ( + db_name, goal, model.id) + existence_result = execute_read_query(connection, q_check_existence) + + if existence_result[0][0] == 0: + if isinstance(model, Task): + q_create = """ + INSERT INTO + tasks (task_id, task_type, parent_type, r_tag) + VALUES + (%r, %s, %s, %i); + """ % (model.id, 'NULL', 'NULL', 0) + + if isinstance(model, Section): + q_create = """ + INSERT INTO + sections (section_id, project_type, section_type) + VALUES + (%r, %s, %s); + """ % (model.id, 'NULL', 'NULL') + + if isinstance(model, Project): + q_create = """ + INSERT INTO + projects (project_id, project_type) + VALUES + (%r, %s); + """ % (model.id, 'NULL') + + execute_query(connection, q_create) + + except Exception as e: + logging.debug(f"The error '{e}' occurred") + + +# Initialise new database tables + +def initialise_sqlite(): + + cwd = os.getcwdb() + db_path = os.path.join(cwd, b'metadata.sqlite') + + connection = create_connection(db_path) + + q_create_projects_table = """ + CREATE TABLE IF NOT EXISTS projects ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_id INTEGER, + project_type TEXT + ); + """ + + q_create_sections_table = """ + CREATE TABLE IF NOT EXISTS sections ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sections_id INTEGER, + project_type TEXT, + section_type + ); + """ + + q_create_tasks_table = """ + CREATE TABLE IF NOT EXISTS tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task_id INTEGER, + task_type TEXT, + parent_type TEXT, + r_tag INTEGER + ); + """ + + execute_query(connection, q_create_projects_table) + execute_query(connection, q_create_sections_table) + execute_query(connection, q_create_tasks_table) + + return connection + # Makes --help text wider @@ -75,6 +284,7 @@ def query_yes_no(question, default="yes"): # Check if label exists, if not, create it + def verify_label_existance(api, label_name, prompt_mode): # Check the regeneration label exists labels = api.get_labels() @@ -113,7 +323,9 @@ def verify_label_existance(api, label_name, prompt_mode): return 0 # Initialisation of Autodoist -def initialise(args): + + +def initialise_api(args): # Check we have a API key if not args.api_key: @@ -132,8 +344,9 @@ def initialise(args): # Check if proper regeneration mode has been selected if args.regeneration is not None: - if not set([0,1,2]) & set([args.regeneration]): - logging.error('Wrong regeneration mode. Please choose a number from 0 to 2. Check --help for more information on the available modes.') + if not set([0, 1, 2]) & set([args.regeneration]): + logging.error( + 'Wrong regeneration mode. Please choose a number from 0 to 2. Check --help for more information on the available modes.') exit(1) # Show which modes are enabled: @@ -157,7 +370,7 @@ def initialise(args): logging.debug('Connecting to the Todoist API') api_arguments = {'token': args.api_key} - + if args.nocache: logging.debug('Disabling local caching') api_arguments['cache'] = None @@ -223,7 +436,10 @@ 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 == None: + current_type = None + pass + elif name == 'Inbox': current_type = args.inbox elif name[-len_suffix[0]:] == args.pp_suffix: current_type = 'parallel' @@ -233,7 +449,7 @@ def check_name(args, name): current_type = 'p-s' elif name[-len_suffix[3]:] == args.sp_suffix: current_type = 's-p' - #TODO: Remove below workarounds if standard notation is changing. Just messy and no longer needed. + # TODO: Remove below workarounds if standard notation is changing. Just messy and no longer needed. # # Workaround for section names, which don't allow '/' symbol. # elif args.ps_suffix == '/-' and name[-2:] == '_-': # current_type = 'p-s' @@ -251,34 +467,32 @@ def check_name(args, name): # Scan the end of a name to find what type it is -def get_type(args, model, key): +def get_type(args, connection, model, key): - model_name = '' + # model_name = '' try: - old_type = model[key] #TODO: METADATA: this information used to be part of the metadata, needs to be retreived from own database + # TODO: METADATA: this information used to be part of the metadata, needs to be retreived from own database + old_type = '' + old_type = db_read_value(connection, model, key)[0][0] + except: # logging.debug('No defined project_type: %s' % str(e)) old_type = None + # model_name = model.name.strip() % TODO: Is this still needed? + try: - model_name = model.name.strip() + current_type = check_name(args, model.content) # Tasks except: - #TODO: Old support for legacy tag in v1 API, can likely be removed since moving to v2. - # try: - # - # object_name = object['content'].strip() - # except: - # pass - pass - - current_type = check_name(args, model_name) + current_type = check_name(args, model.name) # Project and sections # Check if project type changed with respect to previous run if old_type == current_type: type_changed = 0 else: type_changed = 1 + db_update_value(connection, model, key, current_type) # model.key = current_type #TODO: METADATA: this information used to be part of the metadata, needs to be retreived from own database return current_type, type_changed @@ -286,21 +500,21 @@ def get_type(args, model, key): # Determine a project type -def get_project_type(args, project_model): +def get_project_type(args, connection, project_model): """Identifies how a project should be handled.""" project_type, project_type_changed = get_type( - args, project_model, 'project_type') + args, connection, project_model, 'project_type') return project_type, project_type_changed # Determine a section type -def get_section_type(args, section_object): +def get_section_type(args, connection, section_object): """Identifies how a section should be handled.""" if section_object is not None: section_type, section_type_changed = get_type( - args, section_object, 'section_type') + args, connection, section_object, 'section_type') else: section_type = None section_type_changed = 0 @@ -310,18 +524,20 @@ def get_section_type(args, section_object): # Determine an task type -def get_task_type(args, task, project_type): +def get_task_type(args, connection, task, section_type, project_type): """Identifies how a task with sub tasks should be handled.""" - if project_type is None and task.parent_id != 0: + if project_type is None and section_type is None and task.parent_id != 0: #TODO: project type and section type, no? try: - task_type = task.parent_type #TODO: METADATA + task_type = task.parent_type # TODO: METADATA task_type_changed = 1 - task.task_type = task_type + task.task_type = task_type #TODO: METADATA except: - task_type, task_type_changed = get_type(args, task, 'task_type') #TODO: METADATA + task_type, task_type_changed = get_type( + args, connection, task, 'task_type') # TODO: METADATA else: - task_type, task_type_changed = get_type(args, task, 'task_type') #TODO: METADATA + task_type, task_type_changed = get_type( + args, connection, task, 'task_type') # TODO: METADATA return task_type, task_type_changed @@ -330,7 +546,7 @@ def get_task_type(args, task, project_type): def add_label(task, label, overview_task_ids, overview_task_labels): if label not in task.labels: - labels = task.labels # Copy other existing labels + labels = task.labels # Copy other existing labels logging.debug('Updating \'%s\' with label', task.content) labels.append(label) @@ -367,16 +583,6 @@ def update_labels(api, overview_task_ids, overview_task_labels): return filtered_overview_ids -# To handle tasks which have no sections - - -def create_none_section(): - none_sec = { - 'id': None, - 'name': 'None', - 'section_order': 0 - } - return none_sec # Check if header logic needs to be applied @@ -387,7 +593,7 @@ def check_header(level): method = 0 try: - # Support for legacy structure + # Support for legacy structure #TODO: can probably be removed now due to REST API v2 name = level['name'] method = 1 except: @@ -418,7 +624,9 @@ def check_header(level): return header_all_in_level, unheader_all_in_level # Logic for applying and removing headers -def modify_headers(task, child_tasks, header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t): + + +def modify_headers(task, child_tasks, header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t): if any([header_all_in_p, header_all_in_s, header_all_in_t]): if task.content[0] != '*': task.update(content='* ' + task.content) @@ -464,7 +672,7 @@ def check_regen_mode(api, item, regen_labels_id): else: # label_name = api.labels.get_by_id(regen_next_action_label)['name'] # logging.debug( - # 'No regeneration label for item: %s' % item.content) + # 'No regeneration label for item: %s' % item.content) return None @@ -491,17 +699,17 @@ def run_recurring_lists_logic(args, api, item, child_items, child_items_all, reg if regen_mode is None: regen_mode = args.regeneration logging.debug('Using general recurring mode \'%s\' for item: %s', - regen_mode, item.content) + regen_mode, item.content) else: logging.debug('Using recurring label \'%s\' for item: %s', - regen_mode, item.content) + regen_mode, item.content) # Apply tags based on mode give_regen_tag = 0 - if regen_mode == 1: # Regen all + if regen_mode == 1: # Regen all give_regen_tag = 1 - elif regen_mode == 2: # Regen if all sub-tasks completed + elif regen_mode == 2: # Regen if all sub-tasks completed if not child_items: give_regen_tag = 1 @@ -583,14 +791,17 @@ def run_recurring_lists_logic(args, api, item, child_items, child_items_all, reg # item.content) pass + # Contains all main autodoist functionalities -def autodoist_magic(args, api, next_action_label, regen_labels_id): +def autodoist_magic(args, api, connection): - # Preallocate dictionaries + # Preallocate dictionaries and other values overview_task_ids = {} overview_task_labels = {} + next_action_label = args.label + regen_labels_id = args.regen_label_names try: projects = api.get_projects() @@ -599,6 +810,13 @@ def autodoist_magic(args, api, next_action_label, regen_labels_id): for project in projects: + # Skip processing inbox as intended feature + if project.is_inbox_project: + continue + + # Check db existance + db_check_existance(connection, project) + # To determine if a sequential task was found first_found_project = False @@ -608,335 +826,339 @@ def autodoist_magic(args, api, next_action_label, regen_labels_id): # Get project type if next_action_label is not None: project_type, project_type_changed = get_project_type( - args, project) + args, connection, project) if project_type is not None: logging.debug('Identified \'%s\' as %s type', - project.name, project_type) + project.name, project_type) # Get all tasks for the project try: - project_tasks = api.get_tasks(project_id = project.id) + project_tasks = api.get_tasks(project_id=project.id) except Exception as error: print(error) # Run for both non-sectioned and sectioned tasks - # Get completed tasks: + # Get completed tasks: # endpoint = 'https://api.todoist.com/sync/v9/completed/get_all' # get(api._session, endpoint, api._token, '0')['items'] # $ curl https://api.todoist.com/sync/v9/sync-H "Authorization: Bearer e2f750b64e8fc06ae14383d5e15ea0792a2c1bf3" -d commands='[ {"type": "item_add", "temp_id": "63f7ed23-a038-46b5-b2c9-4abda9097ffa", "uuid": "997d4b43-55f1-48a9-9e66-de5785dfd69b", "args": {"content": "Buy Milk", "project_id": "2203306141","labels": ["Food", "Shopping"]}}]' - # for s in [0, 1]: # TODO: TEMPORARELY SKIP SECTIONLESS TASKS - for s in [1]: - if s == 0: - sections = [create_none_section()] # TODO: Rewrite - elif s == 1: - try: - sections = api.get_sections(project_id = project.id) - except Exception as error: - print(error) - - for section in sections: - - # Check if we need to (un)header entire secion - header_all_in_s, unheader_all_in_s = check_header(section) + # for s in [0,1]: + # if s == 0: + # sections = Section(None, None, 0, project.id) + # elif s == 1: + # try: + # sections = api.get_sections(project_id=project.id) + # except Exception as error: + # print(error) - # To determine if a sequential task was found - first_found_section = False + # Get all sections and add the 'None' section too. + try: + sections = api.get_sections(project_id=project.id) + sections.insert(0, Section(None, None, 0, project.id)) + except Exception as error: + print(error) - # Get section type - section_type, section_type_changed = get_section_type( - args, section) - if section_type is not None: - logging.debug('Identified \'%s\' as %s type', - section.name, section_type) + for section in sections: - # Get all tasks for the section - tasks = [x for x in project_tasks if x.section_id - == section.id] + # Check db existance + db_check_existance(connection, section) - # Change top tasks parents_id from 'None' to '0' in order to numerically sort later on - for task in tasks: - if not task.parent_id: - task.parent_id = 0 + # Check if we need to (un)header entire secion + header_all_in_s, unheader_all_in_s = check_header(section) - # Sort by parent_id and child order - # In the past, Todoist used to screw up the tasks orders, so originally I processed parentless tasks first such that children could properly inherit porperties. - # With the new API this seems to be in order, but I'm keeping this just in case for now. TODO: Could be used for optimization in the future. - tasks = sorted(tasks, key=lambda x: ( - int(x.parent_id), x.order)) + # To determine if a sequential task was found + first_found_section = False - # If a type has changed, clean all task labels for good measure + # Get section type + section_type, section_type_changed = get_section_type( + args, connection, section) + if section_type is not None: + logging.debug('Identified \'%s\' as %s type', + section.name, section_type) + + # Get all tasks for the section + tasks = [x for x in project_tasks if x.section_id + == section.id] + + # Change top tasks parents_id from 'None' to '0' in order to numerically sort later on + for task in tasks: + if not task.parent_id: + task.parent_id = 0 + + # Sort by parent_id and child order + # In the past, Todoist used to screw up the tasks orders, so originally I processed parentless tasks first such that children could properly inherit porperties. + # With the new API this seems to be in order, but I'm keeping this just in case for now. TODO: Could be used for optimization in the future. + tasks = sorted(tasks, key=lambda x: ( + int(x.parent_id), x.order)) + + # If a type has changed, clean all task labels for good measure + if next_action_label is not None: + if project_type_changed == 1 or section_type_changed == 1: + # Remove labels + [remove_label(task, next_action_label, overview_task_ids, + overview_task_labels) for task in tasks] + # Remove parent types + # for task in tasks: + # task.parent_type = None #TODO: METADATA + + # For all tasks in this section + for task in tasks: + dominant_type = None # Reset + + db_check_existance(connection, task) + + # Possible nottes routine for the future + # 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_tasks exist, both all and the ones that have not been checked yet + non_completed_tasks = list( + filter(lambda x: not x.is_completed, tasks)) + child_tasks_all = list( + filter(lambda x: x.parent_id == task.id, tasks)) + child_tasks = list( + filter(lambda x: x.parent_id == task.id, non_completed_tasks)) + + # Check if we need to (un)header entire task tree + header_all_in_t, unheader_all_in_t = check_header(task) + + # Modify headers where needed + # TODO: DISABLED FOR NOW, FIX LATER + # modify_headers(header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t) + +# TODO: Check is regeneration is still needed, now that it's part of core Todoist. Disabled for now. + # Logic for recurring lists + # if not args.regeneration: + # try: + # # If old label is present, reset it + # if item.r_tag == 1: #TODO: METADATA + # item.r_tag = 0 #TODO: METADATA + # api.items.update(item.id) + # except: + # pass + + # # If options turned on, start recurring lists logic + # if args.regeneration is not None or args.end: + # run_recurring_lists_logic( + # args, api, item, child_items, child_items_all, regen_labels_id) + + # If options turned on, start labelling logic if next_action_label is not None: - if project_type_changed == 1 or section_type_changed == 1: - # Remove labels - [remove_label(task, next_action_label, overview_task_ids, - overview_task_labels) for task in tasks] - # Remove parent types - # for task in tasks: - # task.parent_type = None #TODO: METADATA - - # For all tasks in this section - for task in tasks: - dominant_type = None # Reset - - # Possible nottes routine for the future - # 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_tasks exist, both all and the ones that have not been checked yet - non_completed_tasks = list( - filter(lambda x: not x.is_completed, tasks)) - child_tasks_all = list( - filter(lambda x: x.parent_id == task.id, tasks)) - child_tasks = list( - filter(lambda x: x.parent_id == task.id, non_completed_tasks)) - - # Check if we need to (un)header entire task tree - header_all_in_t, unheader_all_in_t = check_header(task) - - # Modify headers where needed - #TODO: DISABLED FOR NOW, FIX LATER - # modify_headers(header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t) - -#TODO: Check is regeneration is still needed, now that it's part of core Todoist. Disabled for now. - # Logic for recurring lists - # if not args.regeneration: - # try: - # # If old label is present, reset it - # if item.r_tag == 1: #TODO: METADATA - # item.r_tag = 0 #TODO: METADATA - # api.items.update(item.id) - # except: - # pass - - # # If options turned on, start recurring lists logic - # if args.regeneration is not None or args.end: - # run_recurring_lists_logic( - # args, api, item, child_items, child_items_all, regen_labels_id) - - # If options turned on, start labelling logic - if next_action_label is not None: - # Skip processing a task if it has already been checked or is a header - if task.is_completed: - continue - if task.content.startswith('*'): - # Remove next action label if it's still present - remove_label(task, next_action_label, overview_task_ids, overview_task_labels) - continue - - # Check task type - task_type, task_type_changed = get_task_type( - args, task, project_type) - if task_type is not None: - logging.debug('Identified \'%s\' as %s type', - task.content, task_type) - - # Determine hierarchy types for logic - hierarchy_types = [task_type, - section_type, project_type] - hierarchy_boolean = [type(x) != type(None) - for x in hierarchy_types] - - # If it is a parentless task - if task.parent_id == 0: - if hierarchy_boolean[0]: - # Inherit task type - dominant_type = task_type + # Skip processing a task if it has already been checked or is a header + if task.is_completed: + continue + if task.content.startswith('*'): + # Remove next action label if it's still present + remove_label(task, next_action_label, + overview_task_ids, overview_task_labels) + continue + + # Check task type + task_type, task_type_changed = get_task_type( + args, connection, task, section_type, project_type) + + if task_type is not None: + logging.debug('Identified \'%s\' as %s type', + task.content, task_type) + + # Determine hierarchy types for logic + hierarchy_types = [task_type, + section_type, project_type] + hierarchy_boolean = [type(x) != type(None) + for x in hierarchy_types] + + # If it is a parentless task + if task.parent_id == 0: + if hierarchy_boolean[0]: + # Inherit task type + dominant_type = task_type + add_label( + task, next_action_label, overview_task_ids, overview_task_labels) + + elif hierarchy_boolean[1]: + # Inherit section type + dominant_type = section_type + + if section_type == 'sequential' or section_type == 's-p': + if not first_found_section: + add_label( + task, next_action_label, overview_task_ids, overview_task_labels) + first_found_section = True + elif section_type == 'parallel' or section_type == 'p-s': add_label( task, next_action_label, overview_task_ids, overview_task_labels) - elif hierarchy_boolean[1]: - # Inherit section type - dominant_type = section_type + elif hierarchy_boolean[2]: + # Inherit project type + dominant_type = project_type - if section_type == 'sequential' or section_type == 's-p': - if not first_found_section: - add_label( - task, next_action_label, overview_task_ids, overview_task_labels) - first_found_section = True - elif section_type == 'parallel' or section_type == 'p-s': + if project_type == 'sequential' or project_type == 's-p': + if not first_found_project: add_label( task, next_action_label, overview_task_ids, overview_task_labels) + first_found_project = True - elif hierarchy_boolean[2]: - # Inherit project type - dominant_type = project_type + elif project_type == 'parallel' or project_type == 'p-s': + add_label( + task, next_action_label, overview_task_ids, overview_task_labels) - if project_type == 'sequential' or project_type == 's-p': - if not first_found_project: - add_label( - task, next_action_label, overview_task_ids, overview_task_labels) - first_found_project = True + # Mark other conditions too + if first_found_section == False and hierarchy_boolean[1]: + first_found_section = True + if first_found_project is False and hierarchy_boolean[2]: + first_found_project = True + + # If there are children + if len(child_tasks) > 0: + + # Check if task state has changed, if so clean children for good measure + if task_type_changed == 1: + [remove_label(child_task, next_action_label, overview_task_ids, overview_task_labels) + for child_task in child_tasks] + + # If a sub-task, inherit parent task type + if task.parent_id != 0: + # dominant_type = task.parent_type # TODO: METADATA + dominant_type = db_read_value(connection, task, 'parent_type')[0][0] + + # Process sequential tagged tasks (task_type can overrule project_type) + if dominant_type == 'sequential' or dominant_type == 'p-s': + for child_task in child_tasks: + + # Ignore headered children + if child_task.content.startswith('*'): + continue - elif project_type == 'parallel' or project_type == 'p-s': + # Pass task_type down to the children + child_task.parent_type = dominant_type + # Pass label down to the first child + if not child_task.is_completed and next_action_label in task.labels: add_label( - task, next_action_label, overview_task_ids, overview_task_labels) - - # Mark other conditions too - if first_found_section == False and hierarchy_boolean[1]: - first_found_section = True - if first_found_project is False and hierarchy_boolean[2]: - first_found_project = True - - # If there are children - if len(child_tasks) > 0: - # Check if task state has changed, if so clean children for good measure - if task_type_changed == 1: - [remove_label(child_task, next_action_label, overview_task_ids, overview_task_labels) - for child_task in child_tasks] - - # If a sub-task, inherit parent task type - if task.parent_id !=0: - try: - dominant_type = task.parent_type #TODO: METADATA - except: - pass - - # Process sequential tagged tasks (task_type can overrule project_type) - if dominant_type == 'sequential' or dominant_type == 'p-s': - for child_task in child_tasks: - - # Ignore headered children - if child_task.content.startswith('*'): - continue - - # Pass task_type down to the children - child_task.parent_type = dominant_type - # Pass label down to the first child - if not child_task.is_completed and next_action_label in task.labels: - add_label( - child_task, next_action_label, overview_task_ids, overview_task_labels) - remove_label( - task, next_action_label, overview_task_ids, overview_task_labels) - else: - # Clean for good measure - remove_label( - child_task, next_action_label, overview_task_ids, overview_task_labels) - - # Process parallel tagged tasks or untagged parents - elif dominant_type == 'parallel' or (dominant_type == 's-p' and next_action_label in task.labels): - remove_label( - task, next_action_label, overview_task_ids, overview_task_labels) - for child_task in child_tasks: - - # Ignore headered children - if child_task.content.startswith('*'): - continue - - child_task.parent_type = dominant_type #TODO: METADATA - if not child_task.is_completed: - add_label( - child_task, next_action_label, overview_task_ids, overview_task_labels) - - # Remove labels based on start / due dates - - # If task is too far in the future, remove the next_action tag and skip #TODO: FIX THIS - try: - if args.hide_future > 0 and 'due' in task.data and task.due is not None: - due_date = datetime.strptime( - task.due['date'][:10], "%Y-%m-%d") - future_diff = ( - due_date - datetime.today()).days - if future_diff >= args.hide_future: + child_task, next_action_label, overview_task_ids, overview_task_labels) remove_label( task, next_action_label, overview_task_ids, overview_task_labels) - continue - except: - # Hide-future not set, skip - continue - - # If start-date has not passed yet, remove label - try: - f1 = task.content.find('start=') - f2 = task.content.find('start=due-') - if f1 > -1 and f2 == -1: - f_end = task.content[f1+6:].find(' ') - if f_end > -1: - start_date = task.content[f1 + - 6:f1+6+f_end] else: - start_date = task.content[f1+6:] - - # If start-date hasen't passed, remove all labels - start_date = datetime.strptime( - start_date, args.dateformat) - future_diff = ( - datetime.today()-start_date).days - if future_diff < 0: + # Clean for good measure remove_label( - task, next_action_label, overview_task_ids, overview_task_labels) - [remove_label(child_task, next_action_label, overview_task_ids, - overview_task_labels) for child_task in child_tasks] - continue + child_task, next_action_label, overview_task_ids, overview_task_labels) - except: - logging.warning( - 'Wrong start-date format for task: "%s". Please use "start="', task.content) - continue - - # Recurring task friendly - remove label with relative change from due date #TODO Fix this logic - try: - f = task.content.find('start=due-') - if f > -1: - f1a = task.content.find( - 'd') # Find 'd' from 'due' - f1b = task.content.rfind( - 'd') # Find 'd' from days - f2 = task.content.find('w') - f_end = task.content[f+10:].find(' ') - - if f_end > -1: - offset = task.content[f+10:f+10+f_end-1] - else: - offset = task.content[f+10:-1] - - try: - task_due_date = task.due['date'][:10] - task_due_date = datetime.strptime( - task_due_date, '%Y-%m-%d') - except: - logging.warning( - 'No due date to determine start date for task: "%s".', task.content) - continue + # Process parallel tagged tasks or untagged parents + elif dominant_type == 'parallel' or (dominant_type == 's-p' and next_action_label in task.labels): + remove_label( + task, next_action_label, overview_task_ids, overview_task_labels) + db_update_value(task, 'task_type', 'NULL') - if f1a != f1b and f1b > -1: # To make sure it doesn't trigger if 'w' is chosen - td = timedelta(days=int(offset)) - elif f2 > -1: - td = timedelta(weeks=int(offset)) + for child_task in child_tasks: - # If we're not in the offset from the due date yet, remove all labels - start_date = task_due_date - td - future_diff = ( - datetime.today()-start_date).days - if future_diff < 0: - remove_label( - task, next_action_label, overview_task_ids, overview_task_labels) - [remove_label(child_task, next_action_label, overview_task_ids, - overview_task_labels) for child_task in child_tasks] + # Ignore headered children + if child_task.content.startswith('*'): continue - except: - logging.warning( - 'Wrong start-date format for task: %s. Please use "start=due-"', task.content) - continue + # child_task.parent_type = dominant_type # TODO: METADATA + db_update_value(connection, child_task, 'parent_type', dominant_type) - return overview_task_ids, overview_task_labels - - -# Connect to SQLite database + if not child_task.is_completed: + add_label( + child_task, next_action_label, overview_task_ids, overview_task_labels) + + # Remove labels based on start / due dates + + # If task is too far in the future, remove the next_action tag and skip #TODO: FIX THIS + try: + if args.hide_future > 0 and 'due' in task.data and task.due is not None: + due_date = datetime.strptime( + task.due['date'][:10], "%Y-%m-%d") + future_diff = ( + due_date - datetime.today()).days + if future_diff >= args.hide_future: + remove_label( + task, next_action_label, overview_task_ids, overview_task_labels) + continue + except: + # Hide-future not set, skip + continue + + # If start-date has not passed yet, remove label + try: + f1 = task.content.find('start=') + f2 = task.content.find('start=due-') + if f1 > -1 and f2 == -1: + f_end = task.content[f1+6:].find(' ') + if f_end > -1: + start_date = task.content[f1 + + 6:f1+6+f_end] + else: + start_date = task.content[f1+6:] + + # If start-date hasen't passed, remove all labels + start_date = datetime.strptime( + start_date, args.dateformat) + future_diff = ( + datetime.today()-start_date).days + if future_diff < 0: + remove_label( + task, next_action_label, overview_task_ids, overview_task_labels) + [remove_label(child_task, next_action_label, overview_task_ids, + overview_task_labels) for child_task in child_tasks] + continue + + except: + logging.warning( + 'Wrong start-date format for task: "%s". Please use "start="', task.content) + continue + + # Recurring task friendly - remove label with relative change from due date #TODO Fix this logic + try: + f = task.content.find('start=due-') + if f > -1: + f1a = task.content.find( + 'd') # Find 'd' from 'due' + f1b = task.content.rfind( + 'd') # Find 'd' from days + f2 = task.content.find('w') + f_end = task.content[f+10:].find(' ') + + if f_end > -1: + offset = task.content[f+10:f+10+f_end-1] + else: + offset = task.content[f+10:-1] + + try: + task_due_date = task.due['date'][:10] + task_due_date = datetime.strptime( + task_due_date, '%Y-%m-%d') + except: + logging.warning( + 'No due date to determine start date for task: "%s".', task.content) + continue + + if f1a != f1b and f1b > -1: # To make sure it doesn't trigger if 'w' is chosen + td = timedelta(days=int(offset)) + elif f2 > -1: + td = timedelta(weeks=int(offset)) + + # If we're not in the offset from the due date yet, remove all labels + start_date = task_due_date - td + future_diff = ( + datetime.today()-start_date).days + if future_diff < 0: + remove_label( + task, next_action_label, overview_task_ids, overview_task_labels) + [remove_label(child_task, next_action_label, overview_task_ids, + overview_task_labels) for child_task in child_tasks] + continue -def create_connection(path): - connection = None - try: - connection = sqlite3.connect(path) - print("Connection to SQLite DB successful") - except Error as e: - print(f"The error '{e}' occurred") + except: + logging.warning( + 'Wrong start-date format for task: %s. Please use "start=due-"', task.content) + continue - return connection + # Return all ids and corresponding labels that need to be modified + return overview_task_ids, overview_task_labels # Main @@ -989,7 +1211,7 @@ def main(): args.regen_label_names = ('Regen_off', 'Regen_all', 'Regen_all_if_completed') - # Set debug + # Set logging if args.debug: log_level = logging.DEBUG else: @@ -1007,7 +1229,10 @@ def main(): check_for_update(current_version) # Initialise api - api = initialise(args) + api = initialise_api(args) + + # Initialise SQLite database + connection = initialise_sqlite() # Start main loop while True: @@ -1016,12 +1241,12 @@ def main(): # Evaluate projects, sections, and tasks overview_task_ids, overview_task_labels = autodoist_magic( - args, api, args.label, args.regen_label_names) + args, api, connection) # Commit next action label changes if args.label is not None: updated_ids = update_labels(api, overview_task_ids, - overview_task_labels) + overview_task_labels) if len(updated_ids): len_api_q = len(updated_ids) From 4a236153d0ae16555dcbdb91f776f351b27344fa Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Wed, 4 Jan 2023 10:01:00 +0100 Subject: [PATCH 08/50] Added 'get_all_data' function to use the SYNC API v9 to obtain all the data in your Todoist. Needed for seeing e.g. completed tasks, which the REST API v2 does not provide. --- autodoist.py | 109 ++++++++++++++++++++++++++++----------------------- 1 file changed, 59 insertions(+), 50 deletions(-) diff --git a/autodoist.py b/autodoist.py index 988a31a..de0e128 100644 --- a/autodoist.py +++ b/autodoist.py @@ -4,6 +4,8 @@ from todoist_api_python.models import Task from todoist_api_python.models import Section from todoist_api_python.models import Project +from todoist_api_python.http_requests import get +from urllib.parse import urljoin import sys import time import requests @@ -12,7 +14,6 @@ from datetime import datetime, timedelta import time import sqlite3 -from sqlite3 import Error import os # Connect to SQLite database @@ -429,6 +430,18 @@ def check_for_update(current_version): logging.error("Error while checking for updates: {}".format(e)) return 1 +# Get all data through the SYNC API. Needed to see e.g. any completed tasks. + +def get_all_data(self, api): + BASE_URL = "https://api.todoist.com" + SYNC_VERSION = "v9" + SYNC_API = urljoin(BASE_URL, f"/sync/{SYNC_VERSION}/") + COMPLETED_GET_ALL = "completed/get_all" + endpoint = urljoin(SYNC_API, COMPLETED_GET_ALL) + data = get(api._session, endpoint, api._token) + + return data + # Assign current type based on settings @@ -500,53 +513,55 @@ def get_type(args, connection, model, key): # Determine a project type -def get_project_type(args, connection, project_model): +def get_project_type(args, connection, project): """Identifies how a project should be handled.""" project_type, project_type_changed = get_type( - args, connection, project_model, 'project_type') + args, connection, project, 'project_type') + + if project_type is not None: + logging.debug('Identified \'%s\' as %s type', + project.name, project_type) return project_type, project_type_changed # Determine a section type -def get_section_type(args, connection, section_object): +def get_section_type(args, connection, section): """Identifies how a section should be handled.""" - if section_object is not None: + if section is not None: section_type, section_type_changed = get_type( - args, connection, section_object, 'section_type') + args, connection, section, 'section_type') else: section_type = None section_type_changed = 0 + if section_type is not None: + logging.debug('Identified \'%s\' as %s type', + section.name, section_type) + return section_type, section_type_changed # Determine an task type -def get_task_type(args, connection, task, section_type, project_type): +def get_task_type(args, connection, task): """Identifies how a task with sub tasks should be handled.""" - if project_type is None and section_type is None and task.parent_id != 0: #TODO: project type and section type, no? - try: - task_type = task.parent_type # TODO: METADATA - task_type_changed = 1 - task.task_type = task_type #TODO: METADATA - except: - task_type, task_type_changed = get_type( - args, connection, task, 'task_type') # TODO: METADATA - else: - task_type, task_type_changed = get_type( - args, connection, task, 'task_type') # TODO: METADATA + task_type, task_type_changed = get_type( + args, connection, task, 'task_type') + + if task_type is not None: + logging.debug('Identified \'%s\' as %s type', task.content, task_type) return task_type, task_type_changed # Logic to track addition of a label to a task -def add_label(task, label, overview_task_ids, overview_task_labels): +def add_label(connection, task, dominant_type, label, overview_task_ids, overview_task_labels): if label not in task.labels: - labels = task.labels # Copy other existing labels + labels = task.labels # To also copy other existing labels logging.debug('Updating \'%s\' with label', task.content) labels.append(label) @@ -556,6 +571,8 @@ def add_label(task, label, overview_task_ids, overview_task_labels): overview_task_ids[task.id] = 1 overview_task_labels[task.id] = labels + db_update_value(connection, task, 'task_type', dominant_type) + # Logic to track removal of a label from a task @@ -828,10 +845,6 @@ def autodoist_magic(args, api, connection): project_type, project_type_changed = get_project_type( args, connection, project) - if project_type is not None: - logging.debug('Identified \'%s\' as %s type', - project.name, project_type) - # Get all tasks for the project try: project_tasks = api.get_tasks(project_id=project.id) @@ -875,9 +888,6 @@ def autodoist_magic(args, api, connection): # Get section type section_type, section_type_changed = get_section_type( args, connection, section) - if section_type is not None: - logging.debug('Identified \'%s\' as %s type', - section.name, section_type) # Get all tasks for the section tasks = [x for x in project_tasks if x.section_id @@ -894,7 +904,7 @@ def autodoist_magic(args, api, connection): tasks = sorted(tasks, key=lambda x: ( int(x.parent_id), x.order)) - # If a type has changed, clean all task labels for good measure + # If a type has changed, clean all tasks in this section for good measure if next_action_label is not None: if project_type_changed == 1 or section_type_changed == 1: # Remove labels @@ -930,7 +940,7 @@ def autodoist_magic(args, api, connection): # TODO: DISABLED FOR NOW, FIX LATER # modify_headers(header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t) -# TODO: Check is regeneration is still needed, now that it's part of core Todoist. Disabled for now. + # TODO: Check is regeneration is still needed, now that it's part of core Todoist. Disabled for now. # Logic for recurring lists # if not args.regeneration: # try: @@ -959,11 +969,7 @@ def autodoist_magic(args, api, connection): # Check task type task_type, task_type_changed = get_task_type( - args, connection, task, section_type, project_type) - - if task_type is not None: - logging.debug('Identified \'%s\' as %s type', - task.content, task_type) + args, connection, task) # Determine hierarchy types for logic hierarchy_types = [task_type, @@ -977,7 +983,7 @@ def autodoist_magic(args, api, connection): # Inherit task type dominant_type = task_type add_label( - task, next_action_label, overview_task_ids, overview_task_labels) + connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) elif hierarchy_boolean[1]: # Inherit section type @@ -986,11 +992,11 @@ def autodoist_magic(args, api, connection): if section_type == 'sequential' or section_type == 's-p': if not first_found_section: add_label( - task, next_action_label, overview_task_ids, overview_task_labels) + connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) first_found_section = True elif section_type == 'parallel' or section_type == 'p-s': add_label( - task, next_action_label, overview_task_ids, overview_task_labels) + connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) elif hierarchy_boolean[2]: # Inherit project type @@ -999,12 +1005,12 @@ def autodoist_magic(args, api, connection): if project_type == 'sequential' or project_type == 's-p': if not first_found_project: add_label( - task, next_action_label, overview_task_ids, overview_task_labels) + connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) first_found_project = True elif project_type == 'parallel' or project_type == 'p-s': add_label( - task, next_action_label, overview_task_ids, overview_task_labels) + connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) # Mark other conditions too if first_found_section == False and hierarchy_boolean[1]: @@ -1014,16 +1020,18 @@ def autodoist_magic(args, api, connection): # If there are children if len(child_tasks) > 0: - - # Check if task state has changed, if so clean children for good measure - if task_type_changed == 1: - [remove_label(child_task, next_action_label, overview_task_ids, overview_task_labels) - for child_task in child_tasks] - # If a sub-task, inherit parent task type + #TODO: is this still needed? + # # Check if task state has changed, if so clean children for good measure + # if task_type_changed == 1: + # [remove_label(child_task, next_action_label, overview_task_ids, overview_task_labels) + # for child_task in child_tasks] + + #If a sub-task, inherit parent task type if task.parent_id != 0: # dominant_type = task.parent_type # TODO: METADATA - dominant_type = db_read_value(connection, task, 'parent_type')[0][0] + dominant_type = db_read_value( + connection, task, 'parent_type')[0][0] # Process sequential tagged tasks (task_type can overrule project_type) if dominant_type == 'sequential' or dominant_type == 'p-s': @@ -1038,7 +1046,7 @@ def autodoist_magic(args, api, connection): # Pass label down to the first child if not child_task.is_completed and next_action_label in task.labels: add_label( - child_task, next_action_label, overview_task_ids, overview_task_labels) + connection, child_task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) remove_label( task, next_action_label, overview_task_ids, overview_task_labels) else: @@ -1050,7 +1058,7 @@ def autodoist_magic(args, api, connection): elif dominant_type == 'parallel' or (dominant_type == 's-p' and next_action_label in task.labels): remove_label( task, next_action_label, overview_task_ids, overview_task_labels) - db_update_value(task, 'task_type', 'NULL') + db_update_value(connection, task, 'task_type', None) #TODO: integrate in remove_label funcionality, else a lot of duplicates. #TODO: None not registered, fix bug. for child_task in child_tasks: @@ -1059,11 +1067,12 @@ def autodoist_magic(args, api, connection): continue # child_task.parent_type = dominant_type # TODO: METADATA - db_update_value(connection, child_task, 'parent_type', dominant_type) + db_update_value( + connection, child_task, 'parent_type', dominant_type) if not child_task.is_completed: add_label( - child_task, next_action_label, overview_task_ids, overview_task_labels) + connection, child_task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) # Remove labels based on start / due dates From 0c64e9945574f34d9000fb7f1b331e50e7c24ce9 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Wed, 4 Jan 2023 21:12:39 +0100 Subject: [PATCH 09/50] Core labelling functionality now seems to function. Still need to fix one bug with cleaning children if task_type is set. --- autodoist.py | 268 ++++++++++++++++++++++++++------------------------- 1 file changed, 137 insertions(+), 131 deletions(-) diff --git a/autodoist.py b/autodoist.py index de0e128..f54a863 100644 --- a/autodoist.py +++ b/autodoist.py @@ -45,10 +45,15 @@ def close_connection(connection): # Execute any SQLite query passed to it in the form of string -def execute_query(connection, query): +def execute_query(connection, query, *args): cursor = connection.cursor() try: + value = args[0] + cursor.execute(query,(value,)) # Useful to pass None/NULL value correctly + except: cursor.execute(query) + + try: connection.commit() logging.debug("Query executed: {}".format(query)) except Exception as e: @@ -111,22 +116,15 @@ def db_update_value(connection, model, column, value): db_name = 'projects' goal = 'project_id' - query = """ - UPDATE - %s - SET - %s = %r - WHERE - %s = %r - """ % (db_name, column, value, goal, model.id) + query = """UPDATE %s SET %s = ? WHERE %s = %r""" % (db_name, column, goal, model.id) - result = execute_query(connection, query) + result = execute_query(connection, query, value) + + return result except Exception as e: logging.debug(f"The error '{e}' occurred") - return result - # Check if the id of a model exists, if not, add to database @@ -159,10 +157,10 @@ def db_check_existance(connection, model): if isinstance(model, Section): q_create = """ INSERT INTO - sections (section_id, project_type, section_type) + sections (section_id, section_type) VALUES - (%r, %s, %s); - """ % (model.id, 'NULL', 'NULL') + (%r, %s); + """ % (model.id, 'NULL') if isinstance(model, Project): q_create = """ @@ -198,8 +196,7 @@ def initialise_sqlite(): q_create_sections_table = """ CREATE TABLE IF NOT EXISTS sections ( id INTEGER PRIMARY KEY AUTOINCREMENT, - sections_id INTEGER, - project_type TEXT, + section_id INTEGER, section_type ); """ @@ -571,8 +568,6 @@ def add_label(connection, task, dominant_type, label, overview_task_ids, overvie overview_task_ids[task.id] = 1 overview_task_labels[task.id] = labels - db_update_value(connection, task, 'task_type', dominant_type) - # Logic to track removal of a label from a task @@ -851,6 +846,14 @@ def autodoist_magic(args, api, connection): except Exception as error: print(error) + # If a project type has changed, clean all tasks in this project for good measure + if next_action_label is not None: + if project_type_changed == 1: + for task in project_tasks: + remove_label(task, next_action_label, overview_task_ids, overview_task_labels) + db_update_value(connection, task, 'task_type', None) + db_update_value(connection, task, 'parent_type', None) + # Run for both non-sectioned and sectioned tasks # Get completed tasks: @@ -890,46 +893,39 @@ def autodoist_magic(args, api, connection): args, connection, section) # Get all tasks for the section - tasks = [x for x in project_tasks if x.section_id + section_tasks = [x for x in project_tasks if x.section_id == section.id] # Change top tasks parents_id from 'None' to '0' in order to numerically sort later on - for task in tasks: + for task in section_tasks: if not task.parent_id: task.parent_id = 0 # Sort by parent_id and child order # In the past, Todoist used to screw up the tasks orders, so originally I processed parentless tasks first such that children could properly inherit porperties. # With the new API this seems to be in order, but I'm keeping this just in case for now. TODO: Could be used for optimization in the future. - tasks = sorted(tasks, key=lambda x: ( + section_tasks = sorted(section_tasks, key=lambda x: ( int(x.parent_id), x.order)) # If a type has changed, clean all tasks in this section for good measure if next_action_label is not None: - if project_type_changed == 1 or section_type_changed == 1: - # Remove labels - [remove_label(task, next_action_label, overview_task_ids, - overview_task_labels) for task in tasks] - # Remove parent types - # for task in tasks: - # task.parent_type = None #TODO: METADATA + if section_type_changed == 1: + for task in section_tasks: + remove_label(task, next_action_label, overview_task_ids, overview_task_labels) + db_update_value(connection, task, 'task_type', None) + db_update_value(connection, task, 'parent_type', None) # For all tasks in this section - for task in tasks: + for task in section_tasks: dominant_type = None # Reset db_check_existance(connection, task) - # Possible nottes routine for the future - # 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_tasks exist, both all and the ones that have not been checked yet non_completed_tasks = list( - filter(lambda x: not x.is_completed, tasks)) + filter(lambda x: not x.is_completed, section_tasks)) child_tasks_all = list( - filter(lambda x: x.parent_id == task.id, tasks)) + filter(lambda x: x.parent_id == task.id, section_tasks)) child_tasks = list( filter(lambda x: x.parent_id == task.id, non_completed_tasks)) @@ -971,13 +967,21 @@ def autodoist_magic(args, api, connection): task_type, task_type_changed = get_task_type( args, connection, task) + # If task type has changed, clean all of its children for good measure + if next_action_label is not None: + if task_type_changed == 1: + for child_task in child_tasks: + remove_label(child_task, next_action_label, overview_task_ids, overview_task_labels) + db_update_value(connection, child_task, 'task_type', None) + db_update_value(connection, child_task, 'parent_type', None) + # Determine hierarchy types for logic hierarchy_types = [task_type, section_type, project_type] hierarchy_boolean = [type(x) != type(None) for x in hierarchy_types] - # If it is a parentless task + # If it is a parentless task, set task type based on hierarchy if task.parent_id == 0: if hierarchy_boolean[0]: # Inherit task type @@ -1011,6 +1015,9 @@ def autodoist_magic(args, api, connection): elif project_type == 'parallel' or project_type == 'p-s': add_label( connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + else: + # Parentless task has no type, so skip any children. + continue # Mark other conditions too if first_found_section == False and hierarchy_boolean[1]: @@ -1018,17 +1025,16 @@ def autodoist_magic(args, api, connection): if first_found_project is False and hierarchy_boolean[2]: first_found_project = True - # If there are children + # If a parentless or sub-task which has children if len(child_tasks) > 0: - #TODO: is this still needed? # # Check if task state has changed, if so clean children for good measure # if task_type_changed == 1: # [remove_label(child_task, next_action_label, overview_task_ids, overview_task_labels) # for child_task in child_tasks] - #If a sub-task, inherit parent task type - if task.parent_id != 0: + #If it is a sub-task with no own type, inherit the parent task type instead + if task.parent_id != 0 and task_type == None: # dominant_type = task.parent_type # TODO: METADATA dominant_type = db_read_value( connection, task, 'parent_type')[0][0] @@ -1058,7 +1064,7 @@ def autodoist_magic(args, api, connection): elif dominant_type == 'parallel' or (dominant_type == 's-p' and next_action_label in task.labels): remove_label( task, next_action_label, overview_task_ids, overview_task_labels) - db_update_value(connection, task, 'task_type', None) #TODO: integrate in remove_label funcionality, else a lot of duplicates. #TODO: None not registered, fix bug. + # db_update_value(connection, task, 'task_type', None) #TODO: integrate in remove_label funcionality, else a lot of duplicates. for child_task in child_tasks: @@ -1077,94 +1083,94 @@ def autodoist_magic(args, api, connection): # Remove labels based on start / due dates # If task is too far in the future, remove the next_action tag and skip #TODO: FIX THIS - try: - if args.hide_future > 0 and 'due' in task.data and task.due is not None: - due_date = datetime.strptime( - task.due['date'][:10], "%Y-%m-%d") - future_diff = ( - due_date - datetime.today()).days - if future_diff >= args.hide_future: - remove_label( - task, next_action_label, overview_task_ids, overview_task_labels) - continue - except: - # Hide-future not set, skip - continue - - # If start-date has not passed yet, remove label - try: - f1 = task.content.find('start=') - f2 = task.content.find('start=due-') - if f1 > -1 and f2 == -1: - f_end = task.content[f1+6:].find(' ') - if f_end > -1: - start_date = task.content[f1 + - 6:f1+6+f_end] - else: - start_date = task.content[f1+6:] - - # If start-date hasen't passed, remove all labels - start_date = datetime.strptime( - start_date, args.dateformat) - future_diff = ( - datetime.today()-start_date).days - if future_diff < 0: - remove_label( - task, next_action_label, overview_task_ids, overview_task_labels) - [remove_label(child_task, next_action_label, overview_task_ids, - overview_task_labels) for child_task in child_tasks] - continue - - except: - logging.warning( - 'Wrong start-date format for task: "%s". Please use "start="', task.content) - continue - - # Recurring task friendly - remove label with relative change from due date #TODO Fix this logic - try: - f = task.content.find('start=due-') - if f > -1: - f1a = task.content.find( - 'd') # Find 'd' from 'due' - f1b = task.content.rfind( - 'd') # Find 'd' from days - f2 = task.content.find('w') - f_end = task.content[f+10:].find(' ') - - if f_end > -1: - offset = task.content[f+10:f+10+f_end-1] - else: - offset = task.content[f+10:-1] - - try: - task_due_date = task.due['date'][:10] - task_due_date = datetime.strptime( - task_due_date, '%Y-%m-%d') - except: - logging.warning( - 'No due date to determine start date for task: "%s".', task.content) - continue - - if f1a != f1b and f1b > -1: # To make sure it doesn't trigger if 'w' is chosen - td = timedelta(days=int(offset)) - elif f2 > -1: - td = timedelta(weeks=int(offset)) - - # If we're not in the offset from the due date yet, remove all labels - start_date = task_due_date - td - future_diff = ( - datetime.today()-start_date).days - if future_diff < 0: - remove_label( - task, next_action_label, overview_task_ids, overview_task_labels) - [remove_label(child_task, next_action_label, overview_task_ids, - overview_task_labels) for child_task in child_tasks] - continue - - except: - logging.warning( - 'Wrong start-date format for task: %s. Please use "start=due-"', task.content) - continue + # try: + # if args.hide_future > 0 and 'due' in task.data and task.due is not None: + # due_date = datetime.strptime( + # task.due['date'][:10], "%Y-%m-%d") + # future_diff = ( + # due_date - datetime.today()).days + # if future_diff >= args.hide_future: + # remove_label( + # task, next_action_label, overview_task_ids, overview_task_labels) + # continue + # except: + # # Hide-future not set, skip + # continue + + # If start-date has not passed yet, remove label #TODO: FIX THIS + # try: + # f1 = task.content.find('start=') + # f2 = task.content.find('start=due-') + # if f1 > -1 and f2 == -1: + # f_end = task.content[f1+6:].find(' ') + # if f_end > -1: + # start_date = task.content[f1 + + # 6:f1+6+f_end] + # else: + # start_date = task.content[f1+6:] + + # # If start-date hasen't passed, remove all labels + # start_date = datetime.strptime( + # start_date, args.dateformat) + # future_diff = ( + # datetime.today()-start_date).days + # if future_diff < 0: + # remove_label( + # task, next_action_label, overview_task_ids, overview_task_labels) + # [remove_label(child_task, next_action_label, overview_task_ids, + # overview_task_labels) for child_task in child_tasks] + # continue + + # except: + # logging.warning( + # 'Wrong start-date format for task: "%s". Please use "start="', task.content) + # continue + + # Recurring task friendly - remove label with relative change from due date #TODO FIX THIS + # try: + # f = task.content.find('start=due-') + # if f > -1: + # f1a = task.content.find( + # 'd') # Find 'd' from 'due' + # f1b = task.content.rfind( + # 'd') # Find 'd' from days + # f2 = task.content.find('w') + # f_end = task.content[f+10:].find(' ') + + # if f_end > -1: + # offset = task.content[f+10:f+10+f_end-1] + # else: + # offset = task.content[f+10:-1] + + # try: + # task_due_date = task.due['date'][:10] + # task_due_date = datetime.strptime( + # task_due_date, '%Y-%m-%d') + # except: + # logging.warning( + # 'No due date to determine start date for task: "%s".', task.content) + # continue + + # if f1a != f1b and f1b > -1: # To make sure it doesn't trigger if 'w' is chosen + # td = timedelta(days=int(offset)) + # elif f2 > -1: + # td = timedelta(weeks=int(offset)) + + # # If we're not in the offset from the due date yet, remove all labels + # start_date = task_due_date - td + # future_diff = ( + # datetime.today()-start_date).days + # if future_diff < 0: + # remove_label( + # task, next_action_label, overview_task_ids, overview_task_labels) + # [remove_label(child_task, next_action_label, overview_task_ids, + # overview_task_labels) for child_task in child_tasks] + # continue + + # except: + # logging.warning( + # 'Wrong start-date format for task: %s. Please use "start=due-"', task.content) + # continue # Return all ids and corresponding labels that need to be modified return overview_task_ids, overview_task_labels From fc9aa8da4fb103f3cdc8919fec5953226070f6dc Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sat, 7 Jan 2023 15:07:57 +0100 Subject: [PATCH 10/50] Children are now cleaned correctly after task_type change --- autodoist.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/autodoist.py b/autodoist.py index f54a863..5d94c60 100644 --- a/autodoist.py +++ b/autodoist.py @@ -803,6 +803,20 @@ def run_recurring_lists_logic(args, api, item, child_items, child_items_all, reg # item.content) pass +# Find and clean all children under a task + +def find_and_clean_all_children(task_ids, task, section_tasks): + + child_tasks = list(filter(lambda x: x.parent_id == task.id, section_tasks)) + + if child_tasks != []: + for child_task in child_tasks: + # Children found, go deeper + task_ids.append(child_task.id) + task_ids = find_and_clean_all_children(task_ids, child_task, section_tasks) + + return task_ids + # Contains all main autodoist functionalities @@ -970,7 +984,12 @@ def autodoist_magic(args, api, connection): # If task type has changed, clean all of its children for good measure if next_action_label is not None: if task_type_changed == 1: - for child_task in child_tasks: + + # Find all children under this task + task_ids = find_and_clean_all_children([], task, section_tasks) + child_tasks_all = list(filter(lambda x: x.id in task_ids, section_tasks)) + + for child_task in child_tasks_all: remove_label(child_task, next_action_label, overview_task_ids, overview_task_labels) db_update_value(connection, child_task, 'task_type', None) db_update_value(connection, child_task, 'parent_type', None) From e9b2dd6f21aa41e7033c85898d5c62973d5f9f54 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sat, 7 Jan 2023 15:44:56 +0100 Subject: [PATCH 11/50] Fixed labelling bug with SEQ mode due to legacy code --- autodoist.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/autodoist.py b/autodoist.py index 5d94c60..9539cd8 100644 --- a/autodoist.py +++ b/autodoist.py @@ -1052,12 +1052,16 @@ def autodoist_magic(args, api, connection): # [remove_label(child_task, next_action_label, overview_task_ids, overview_task_labels) # for child_task in child_tasks] - #If it is a sub-task with no own type, inherit the parent task type instead + # If it is a sub-task with no own type, inherit the parent task type instead if task.parent_id != 0 and task_type == None: # dominant_type = task.parent_type # TODO: METADATA dominant_type = db_read_value( connection, task, 'parent_type')[0][0] + # If it is a sub-task with no dominant type (e.g. lower level child with new task_type), use the task type + if task.parent_id != 0 and dominant_type == None: + dominant_type = task_type + # Process sequential tagged tasks (task_type can overrule project_type) if dominant_type == 'sequential' or dominant_type == 'p-s': for child_task in child_tasks: @@ -1067,17 +1071,19 @@ def autodoist_magic(args, api, connection): continue # Pass task_type down to the children - child_task.parent_type = dominant_type + db_update_value( + connection, child_task, 'parent_type', dominant_type) + # Pass label down to the first child if not child_task.is_completed and next_action_label in task.labels: add_label( connection, child_task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) remove_label( task, next_action_label, overview_task_ids, overview_task_labels) - else: - # Clean for good measure - remove_label( - child_task, next_action_label, overview_task_ids, overview_task_labels) + # else: #TODO: is this still needed? + # # Clean for good measure + # remove_label( + # child_task, next_action_label, overview_task_ids, overview_task_labels) # Process parallel tagged tasks or untagged parents elif dominant_type == 'parallel' or (dominant_type == 's-p' and next_action_label in task.labels): From aa274d5132278763df4def511f44404c16024aa1 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sat, 7 Jan 2023 16:13:43 +0100 Subject: [PATCH 12/50] Additional testing of labelling logic --- autodoist.py | 48 ++++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/autodoist.py b/autodoist.py index 9539cd8..78b0514 100644 --- a/autodoist.py +++ b/autodoist.py @@ -817,6 +817,19 @@ def find_and_clean_all_children(task_ids, task, section_tasks): return task_ids +# Logic to pass label data + +def label_according_to_type(hierarchy_type, first_found, connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels): + + if hierarchy_type == 'sequential' or hierarchy_type == 's-p': + if not first_found: + add_label( + connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + first_found = True + elif hierarchy_type == 'parallel' or hierarchy_type == 'p-s': + add_label( + connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + # Contains all main autodoist functionalities @@ -931,10 +944,16 @@ def autodoist_magic(args, api, connection): # For all tasks in this section for task in section_tasks: - dominant_type = None # Reset + + # Reset + dominant_type = None + # Check db existance db_check_existance(connection, task) + # To determine if a sequential task was found + first_found_tasks = False + # Determine which child_tasks exist, both all and the ones that have not been checked yet non_completed_tasks = list( filter(lambda x: not x.is_completed, section_tasks)) @@ -1005,40 +1024,25 @@ def autodoist_magic(args, api, connection): if hierarchy_boolean[0]: # Inherit task type dominant_type = task_type - add_label( - connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + label_according_to_type(task_type , first_found_tasks, connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) elif hierarchy_boolean[1]: # Inherit section type dominant_type = section_type - - if section_type == 'sequential' or section_type == 's-p': - if not first_found_section: - add_label( - connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) - first_found_section = True - elif section_type == 'parallel' or section_type == 'p-s': - add_label( - connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + label_according_to_type(section_type, first_found_section, connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) elif hierarchy_boolean[2]: # Inherit project type dominant_type = project_type - - if project_type == 'sequential' or project_type == 's-p': - if not first_found_project: - add_label( - connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) - first_found_project = True - - elif project_type == 'parallel' or project_type == 'p-s': - add_label( - connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + label_according_to_type(project_type, first_found_project, connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + else: # Parentless task has no type, so skip any children. continue # Mark other conditions too + if first_found_tasks == False and hierarchy_boolean[0]: + first_found_tasks = True if first_found_section == False and hierarchy_boolean[1]: first_found_section = True if first_found_project is False and hierarchy_boolean[2]: From 86eb720f505ec5c7d31643d64c3cd6962f9edc31 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 8 Jan 2023 14:58:49 +0100 Subject: [PATCH 13/50] Overhaul labelling functionality now working on project level. Next: debug section and parentless tasks level. --- autodoist.py | 195 ++++++++++++++++++++++++++------------------------- 1 file changed, 101 insertions(+), 94 deletions(-) diff --git a/autodoist.py b/autodoist.py index 78b0514..fc23a99 100644 --- a/autodoist.py +++ b/autodoist.py @@ -15,6 +15,7 @@ import time import sqlite3 import os +import re # Connect to SQLite database @@ -442,36 +443,38 @@ def get_all_data(self, api): # Assign current type based on settings -def check_name(args, name): - len_suffix = [len(args.pp_suffix), len(args.ss_suffix), - len(args.ps_suffix), len(args.sp_suffix)] +def check_name(args, string, num): - if name == None: - current_type = None - pass - elif name == 'Inbox': - current_type = args.inbox - elif name[-len_suffix[0]:] == args.pp_suffix: - current_type = 'parallel' - elif name[-len_suffix[1]:] == args.ss_suffix: - current_type = 'sequential' - elif name[-len_suffix[2]:] == args.ps_suffix: - current_type = 'p-s' - elif name[-len_suffix[3]:] == args.sp_suffix: - current_type = 's-p' - # TODO: Remove below workarounds if standard notation is changing. Just messy and no longer needed. - # # Workaround for section names, which don't allow '/' symbol. - # elif args.ps_suffix == '/-' and name[-2:] == '_-': - # current_type = 'p-s' - # # Workaround for section names, which don't allow '/' symbol. - # elif args.sp_suffix == '-/' and name[-2:] == '-_': - # current_type = 's-p' - # # Workaround for section names, which don't allow '/' symbol. - # elif args.pp_suffix == '//' and name[-1:] == '_': - # current_type = 'parallel' - else: + try: + # Find inbox or none section as exceptions + if string == None: + current_type = None + pass + elif string == 'Inbox': + current_type = args.inbox + pass + else: + # Find any = or - symbol at the end of the string. Look at last 3 for projects, 2 for sections, and 1 for tasks + regex = '[%s%s]{1,%s}$' % (args.s_suffix, args.p_suffix, str(num)) + re_ind = re.search(regex, string) + suffix = re_ind[0] + + # Somebody put fewer characters than intended. Take last character and apply for every missing one. + if len(suffix) < num: + suffix += suffix[-1] * (num - len(suffix)) + + current_type = '' + for s in suffix: + if s == args.s_suffix: + current_type += 's' + elif s == args.p_suffix: + current_type += 'p' + except: + logging.debug("String {} not recognised.".format(string)) current_type = None + + return current_type # Scan the end of a name to find what type it is @@ -490,12 +493,12 @@ def get_type(args, connection, model, key): # logging.debug('No defined project_type: %s' % str(e)) old_type = None - # model_name = model.name.strip() % TODO: Is this still needed? - - try: - current_type = check_name(args, model.content) # Tasks - except: - current_type = check_name(args, model.name) # Project and sections + if isinstance(model, Task): + current_type = check_name(args, model.content, 1) # Tasks + elif isinstance(model, Section): + current_type = check_name(args, model.name, 2) # Sections + elif isinstance(model, Project): + current_type = check_name(args, model.name, 3) # Projects # Check if project type changed with respect to previous run if old_type == current_type: @@ -817,20 +820,6 @@ def find_and_clean_all_children(task_ids, task, section_tasks): return task_ids -# Logic to pass label data - -def label_according_to_type(hierarchy_type, first_found, connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels): - - if hierarchy_type == 'sequential' or hierarchy_type == 's-p': - if not first_found: - add_label( - connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) - first_found = True - elif hierarchy_type == 'parallel' or hierarchy_type == 'p-s': - add_label( - connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) - - # Contains all main autodoist functionalities @@ -841,6 +830,7 @@ def autodoist_magic(args, api, connection): overview_task_labels = {} next_action_label = args.label regen_labels_id = args.regen_label_names + first_found = [False, False, False] try: projects = api.get_projects() @@ -856,9 +846,6 @@ def autodoist_magic(args, api, connection): # Check db existance db_check_existance(connection, project) - # 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) @@ -866,6 +853,9 @@ def autodoist_magic(args, api, connection): if next_action_label is not None: project_type, project_type_changed = get_project_type( args, connection, project) + else: + project_type = None + project_type_changed = 0 # Get all tasks for the project try: @@ -904,6 +894,9 @@ def autodoist_magic(args, api, connection): except Exception as error: print(error) + # Reset + first_found[0] = False + for section in sections: # Check db existance @@ -912,12 +905,13 @@ def autodoist_magic(args, api, connection): # 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, connection, section) + if next_action_label: + section_type, section_type_changed = get_section_type( + args, connection, section) + else: + section_type = None + section_type_changed = 0 # Get all tasks for the section section_tasks = [x for x in project_tasks if x.section_id @@ -941,6 +935,9 @@ def autodoist_magic(args, api, connection): remove_label(task, next_action_label, overview_task_ids, overview_task_labels) db_update_value(connection, task, 'task_type', None) db_update_value(connection, task, 'parent_type', None) + + # Reset + first_found[1] = False # For all tasks in this section for task in section_tasks: @@ -951,8 +948,6 @@ def autodoist_magic(args, api, connection): # Check db existance db_check_existance(connection, task) - # To determine if a sequential task was found - first_found_tasks = False # Determine which child_tasks exist, both all and the ones that have not been checked yet non_completed_tasks = list( @@ -1017,45 +1012,48 @@ def autodoist_magic(args, api, connection): hierarchy_types = [task_type, section_type, project_type] hierarchy_boolean = [type(x) != type(None) - for x in hierarchy_types] + for x in hierarchy_types] # If it is a parentless task, set task type based on hierarchy if task.parent_id == 0: - if hierarchy_boolean[0]: - # Inherit task type - dominant_type = task_type - label_according_to_type(task_type , first_found_tasks, connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) - - elif hierarchy_boolean[1]: - # Inherit section type - dominant_type = section_type - label_according_to_type(section_type, first_found_section, connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) - - elif hierarchy_boolean[2]: - # Inherit project type - dominant_type = project_type - label_according_to_type(project_type, first_found_project, connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) - - else: + if not True in hierarchy_boolean: # Parentless task has no type, so skip any children. continue + else: + if hierarchy_boolean[1]: + # Inherit task type + dominant_type = task_type + elif hierarchy_boolean[1]: + # Inherit section type + dominant_type = section_type + elif hierarchy_boolean[2]: + # Inherit project type + dominant_type = project_type + + # for ind, char in enumerate(dominant_type): + + if dominant_type[0] == 's': + if not first_found[0]: + + if dominant_type[1] == 's': + if not first_found[1]: + add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + + elif dominant_type[1] == 'p': + add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) - # Mark other conditions too - if first_found_tasks == False and hierarchy_boolean[0]: - first_found_tasks = True - if first_found_section == False and hierarchy_boolean[1]: - first_found_section = True - if first_found_project is False and hierarchy_boolean[2]: - first_found_project = True + elif dominant_type[0] == 'p': + + if dominant_type[1] == 's': + if not first_found[1]: + add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + + elif dominant_type[1] == 'p': + add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) # If a parentless or sub-task which has children if len(child_tasks) > 0: - # # Check if task state has changed, if so clean children for good measure - # if task_type_changed == 1: - # [remove_label(child_task, next_action_label, overview_task_ids, overview_task_labels) - # for child_task in child_tasks] - # If it is a sub-task with no own type, inherit the parent task type instead if task.parent_id != 0 and task_type == None: # dominant_type = task.parent_type # TODO: METADATA @@ -1066,8 +1064,12 @@ def autodoist_magic(args, api, connection): if task.parent_id != 0 and dominant_type == None: dominant_type = task_type + # Only last character is relevant + dominant_type = dominant_type[-1] + # Process sequential tagged tasks (task_type can overrule project_type) - if dominant_type == 'sequential' or dominant_type == 'p-s': + if dominant_type == 's': + for child_task in child_tasks: # Ignore headered children @@ -1090,7 +1092,8 @@ def autodoist_magic(args, api, connection): # child_task, next_action_label, overview_task_ids, overview_task_labels) # Process parallel tagged tasks or untagged parents - elif dominant_type == 'parallel' or (dominant_type == 's-p' and next_action_label in task.labels): + # elif dominant_type == 'parallel' or (dominant_type == 's-p' and next_action_label in task.labels): + elif dominant_type == 'p' and next_action_label in task.labels: remove_label( task, next_action_label, overview_task_ids, overview_task_labels) # db_update_value(connection, task, 'task_type', None) #TODO: integrate in remove_label funcionality, else a lot of duplicates. @@ -1201,6 +1204,14 @@ def autodoist_magic(args, api, connection): # 'Wrong start-date format for task: %s. Please use "start=due-"', task.content) # continue + # Mark first found task in section + if next_action_label is not None and first_found[1] == False: #TODO: is this always true? What about starred tasks? + first_found[1] = True + + # Mark first found section with tasks in project (to account for None section) + if next_action_label is not None and first_found[0] == False and section_tasks: #TODO: is this always true? What about starred tasks? + first_found[0] = True + # Return all ids and corresponding labels that need to be modified return overview_task_ids, overview_task_labels @@ -1226,13 +1237,9 @@ def main(): 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='=-') + '-p', '--p_suffix', help='change suffix for parallel labeling (default "=").', default='=') parser.add_argument( - '-sp', '--sp_suffix', help='change suffix for sequential-parallel labeling (default "-=").', default='-=') + '-s', '--s_suffix', help='change suffix for sequential 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( From a589c159c00695e4e7b5c1fe875fd5cb81ae4e5c Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 8 Jan 2023 15:28:58 +0100 Subject: [PATCH 14/50] Type indication now seems to work on all levels --- autodoist.py | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/autodoist.py b/autodoist.py index fc23a99..913f27c 100644 --- a/autodoist.py +++ b/autodoist.py @@ -440,7 +440,7 @@ def get_all_data(self, api): return data -# Assign current type based on settings +# Find the type based on name suffix. def check_name(args, string, num): @@ -469,6 +469,13 @@ def check_name(args, string, num): current_type += 's' elif s == args.p_suffix: current_type += 'p' + + # Always return a three letter string + if len(current_type) == 2: + current_type = 'x' + current_type + elif len(current_type) == 1: + current_type = 'xx' + current_type + except: logging.debug("String {} not recognised.".format(string)) current_type = None @@ -1020,7 +1027,7 @@ def autodoist_magic(args, api, connection): # Parentless task has no type, so skip any children. continue else: - if hierarchy_boolean[1]: + if hierarchy_boolean[0]: # Inherit task type dominant_type = task_type elif hierarchy_boolean[1]: @@ -1030,8 +1037,7 @@ def autodoist_magic(args, api, connection): # Inherit project type dominant_type = project_type - # for ind, char in enumerate(dominant_type): - + # If indicated on project level if dominant_type[0] == 's': if not first_found[0]: @@ -1051,6 +1057,22 @@ def autodoist_magic(args, api, connection): elif dominant_type[1] == 'p': add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + # If indicated on section level + if dominant_type[0] == 'x' and dominant_type[1] == 's': + if not first_found[1]: + add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + + elif dominant_type[0] == 'x' and dominant_type[1] == 'p': + add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + + # If indicated on parentless task level + if dominant_type[1] == 'x' and dominant_type[2] == 's': + if not first_found[1]: + add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + + elif dominant_type[1] == 'x' and dominant_type[2] == 'p': + add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + # If a parentless or sub-task which has children if len(child_tasks) > 0: @@ -1064,10 +1086,10 @@ def autodoist_magic(args, api, connection): if task.parent_id != 0 and dominant_type == None: dominant_type = task_type - # Only last character is relevant + # Only last character is relevant for subtasks dominant_type = dominant_type[-1] - # Process sequential tagged tasks (task_type can overrule project_type) + # Process sequential tagged tasks if dominant_type == 's': for child_task in child_tasks: @@ -1086,13 +1108,8 @@ def autodoist_magic(args, api, connection): connection, child_task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) remove_label( task, next_action_label, overview_task_ids, overview_task_labels) - # else: #TODO: is this still needed? - # # Clean for good measure - # remove_label( - # child_task, next_action_label, overview_task_ids, overview_task_labels) # Process parallel tagged tasks or untagged parents - # elif dominant_type == 'parallel' or (dominant_type == 's-p' and next_action_label in task.labels): elif dominant_type == 'p' and next_action_label in task.labels: remove_label( task, next_action_label, overview_task_ids, overview_task_labels) From 97a7073f1c28d735f413d80533ff71fd3a4a937f Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 8 Jan 2023 16:21:50 +0100 Subject: [PATCH 15/50] Minor cleanup of code --- autodoist.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/autodoist.py b/autodoist.py index 913f27c..be1dd1b 100644 --- a/autodoist.py +++ b/autodoist.py @@ -492,7 +492,6 @@ def get_type(args, connection, model, key): # model_name = '' try: - # TODO: METADATA: this information used to be part of the metadata, needs to be retreived from own database old_type = '' old_type = db_read_value(connection, model, key)[0][0] @@ -513,7 +512,6 @@ def get_type(args, connection, model, key): else: type_changed = 1 db_update_value(connection, model, key, current_type) - # model.key = current_type #TODO: METADATA: this information used to be part of the metadata, needs to be retreived from own database return current_type, type_changed @@ -614,18 +612,6 @@ def check_header(level): unheader_all_in_level = False method = 0 - try: - # Support for legacy structure #TODO: can probably be removed now due to REST API v2 - name = level['name'] - method = 1 - except: - try: - # Current structure - content = level.content - method = 2 - except: - pass - if method == 1: if name[:3] == '** ': header_all_in_level = True @@ -992,10 +978,22 @@ def autodoist_magic(args, api, connection): # Skip processing a task if it has already been checked or is a header if task.is_completed: continue + + # Remove clean all task and subtask data if task.content.startswith('*'): - # Remove next action label if it's still present remove_label(task, next_action_label, overview_task_ids, overview_task_labels) + db_update_value(connection, task, 'task_type', None) + db_update_value(connection, task, 'parent_type', None) + + task_ids = find_and_clean_all_children([], task, section_tasks) + child_tasks_all = list(filter(lambda x: x.id in task_ids, section_tasks)) + + for child_task in child_tasks_all: + remove_label(child_task, next_action_label, overview_task_ids, overview_task_labels) + db_update_value(connection, child_task, 'task_type', None) + db_update_value(connection, child_task, 'parent_type', None) + continue # Check task type @@ -1078,16 +1076,19 @@ def autodoist_magic(args, api, connection): # If it is a sub-task with no own type, inherit the parent task type instead if task.parent_id != 0 and task_type == None: - # dominant_type = task.parent_type # TODO: METADATA dominant_type = db_read_value( connection, task, 'parent_type')[0][0] # If it is a sub-task with no dominant type (e.g. lower level child with new task_type), use the task type if task.parent_id != 0 and dominant_type == None: dominant_type = task_type - - # Only last character is relevant for subtasks - dominant_type = dominant_type[-1] + + if dominant_type is None: + # Task with parent that has been headered, skip. + continue + else: + # Only last character is relevant for subtasks + dominant_type = dominant_type[-1] # Process sequential tagged tasks if dominant_type == 's': @@ -1113,7 +1114,6 @@ def autodoist_magic(args, api, connection): elif dominant_type == 'p' and next_action_label in task.labels: remove_label( task, next_action_label, overview_task_ids, overview_task_labels) - # db_update_value(connection, task, 'task_type', None) #TODO: integrate in remove_label funcionality, else a lot of duplicates. for child_task in child_tasks: @@ -1121,7 +1121,6 @@ def autodoist_magic(args, api, connection): if child_task.content.startswith('*'): continue - # child_task.parent_type = dominant_type # TODO: METADATA db_update_value( connection, child_task, 'parent_type', dominant_type) From b2f2fa270ececc877f1734fff544b3e53ea901a2 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 8 Jan 2023 17:11:47 +0100 Subject: [PATCH 16/50] Bugfix where manually changing the order of sequential tasks would not remove old labels. --- autodoist.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/autodoist.py b/autodoist.py index be1dd1b..4b5ef13 100644 --- a/autodoist.py +++ b/autodoist.py @@ -1017,7 +1017,13 @@ def autodoist_magic(args, api, connection): hierarchy_types = [task_type, section_type, project_type] hierarchy_boolean = [type(x) != type(None) - for x in hierarchy_types] + for x in hierarchy_types] + + # If task has no type, but has a label, most likely the order has been changed by user. Remove data. + if not True in hierarchy_boolean and next_action_label in task.labels: + remove_label(task, next_action_label, overview_task_ids, overview_task_labels) + db_update_value(connection, task, 'task_type', None) + db_update_value(connection, task, 'parent_type', None) # If it is a parentless task, set task type based on hierarchy if task.parent_id == 0: @@ -1043,6 +1049,10 @@ def autodoist_magic(args, api, connection): if not first_found[1]: add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + elif next_action_label in task.labels: + # Probably the task has been manually moved, so if it has a label, let's remove it. + remove_label(task, next_action_label, overview_task_ids, overview_task_labels) + elif dominant_type[1] == 'p': add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) @@ -1051,6 +1061,10 @@ def autodoist_magic(args, api, connection): if dominant_type[1] == 's': if not first_found[1]: add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + + elif next_action_label in task.labels: + # Probably the task has been manually moved, so if it has a label, let's remove it. + remove_label(task, next_action_label, overview_task_ids, overview_task_labels) elif dominant_type[1] == 'p': add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) @@ -1060,6 +1074,10 @@ def autodoist_magic(args, api, connection): if not first_found[1]: add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + elif next_action_label in task.labels: + # Probably the task has been manually moved, so if it has a label, let's remove it. + remove_label(task, next_action_label, overview_task_ids, overview_task_labels) + elif dominant_type[0] == 'x' and dominant_type[1] == 'p': add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) @@ -1068,6 +1086,10 @@ def autodoist_magic(args, api, connection): if not first_found[1]: add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + if next_action_label in task.labels: + # Probably the task has been manually moved, so if it has a label, let's remove it. + remove_label(task, next_action_label, overview_task_ids, overview_task_labels) + elif dominant_type[1] == 'x' and dominant_type[2] == 'p': add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) @@ -1099,6 +1121,9 @@ def autodoist_magic(args, api, connection): if child_task.content.startswith('*'): continue + # Clean up for good measure. + remove_label(child_task, next_action_label, overview_task_ids, overview_task_labels) + # Pass task_type down to the children db_update_value( connection, child_task, 'parent_type', dominant_type) From 43e598a87a2de046d16f956a18351624505700f9 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 8 Jan 2023 17:37:24 +0100 Subject: [PATCH 17/50] Fixed -hf flag not working correctly --- autodoist.py | 79 ++++++++++++++++++++++++++-------------------------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/autodoist.py b/autodoist.py index 4b5ef13..08d881c 100644 --- a/autodoist.py +++ b/autodoist.py @@ -1156,48 +1156,47 @@ def autodoist_magic(args, api, connection): # Remove labels based on start / due dates # If task is too far in the future, remove the next_action tag and skip #TODO: FIX THIS - # try: - # if args.hide_future > 0 and 'due' in task.data and task.due is not None: - # due_date = datetime.strptime( - # task.due['date'][:10], "%Y-%m-%d") - # future_diff = ( - # due_date - datetime.today()).days - # if future_diff >= args.hide_future: - # remove_label( - # task, next_action_label, overview_task_ids, overview_task_labels) - # continue - # except: - # # Hide-future not set, skip - # continue + try: + if args.hide_future > 0 and task.due.date is not None: + due_date = datetime.strptime( + task.due.date, "%Y-%m-%d") + future_diff = ( + due_date - datetime.today()).days + if future_diff >= args.hide_future: + remove_label( + task, next_action_label, overview_task_ids, overview_task_labels) + except: + # Hide-future not set, skip + pass # If start-date has not passed yet, remove label #TODO: FIX THIS - # try: - # f1 = task.content.find('start=') - # f2 = task.content.find('start=due-') - # if f1 > -1 and f2 == -1: - # f_end = task.content[f1+6:].find(' ') - # if f_end > -1: - # start_date = task.content[f1 + - # 6:f1+6+f_end] - # else: - # start_date = task.content[f1+6:] - - # # If start-date hasen't passed, remove all labels - # start_date = datetime.strptime( - # start_date, args.dateformat) - # future_diff = ( - # datetime.today()-start_date).days - # if future_diff < 0: - # remove_label( - # task, next_action_label, overview_task_ids, overview_task_labels) - # [remove_label(child_task, next_action_label, overview_task_ids, - # overview_task_labels) for child_task in child_tasks] - # continue - - # except: - # logging.warning( - # 'Wrong start-date format for task: "%s". Please use "start="', task.content) - # continue + try: + f1 = task.content.find('start=') + f2 = task.content.find('start=due-') + if f1 > -1 and f2 == -1: + f_end = task.content[f1+6:].find(' ') + if f_end > -1: + start_date = task.content[f1 + + 6:f1+6+f_end] + else: + start_date = task.content[f1+6:] + + # If start-date hasen't passed, remove all labels + start_date = datetime.strptime( + start_date, args.dateformat) + future_diff = ( + datetime.today()-start_date).days + if future_diff < 0: + remove_label( + task, next_action_label, overview_task_ids, overview_task_labels) + [remove_label(child_task, next_action_label, overview_task_ids, + overview_task_labels) for child_task in child_tasks] + pass + + except: + logging.warning( + 'Wrong start-date format for task: "%s". Please use "start="', task.content) + continue # Recurring task friendly - remove label with relative change from due date #TODO FIX THIS # try: From 81769968aed7f824f56b8197610d2adcaf858dac Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 8 Jan 2023 18:13:06 +0100 Subject: [PATCH 18/50] Start=DD-MM-YYYY functionality working again. --- autodoist.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/autodoist.py b/autodoist.py index 08d881c..98126c4 100644 --- a/autodoist.py +++ b/autodoist.py @@ -1155,7 +1155,7 @@ def autodoist_magic(args, api, connection): # Remove labels based on start / due dates - # If task is too far in the future, remove the next_action tag and skip #TODO: FIX THIS + # If task is too far in the future, remove the next_action tag and skip try: if args.hide_future > 0 and task.due.date is not None: due_date = datetime.strptime( @@ -1169,29 +1169,21 @@ def autodoist_magic(args, api, connection): # Hide-future not set, skip pass - # If start-date has not passed yet, remove label #TODO: FIX THIS + # If start-date has not passed yet, remove label try: - f1 = task.content.find('start=') - f2 = task.content.find('start=due-') - if f1 > -1 and f2 == -1: - f_end = task.content[f1+6:].find(' ') - if f_end > -1: - start_date = task.content[f1 + - 6:f1+6+f_end] - else: - start_date = task.content[f1+6:] - - # If start-date hasen't passed, remove all labels + f1 = re.search('start=(\d{2}[-]\d{2}[-]\d{4})', task.content) + if f1: + start_date = f1.groups()[0] start_date = datetime.strptime( start_date, args.dateformat) future_diff = ( datetime.today()-start_date).days + # If start-date hasen't passed, remove all labels if future_diff < 0: remove_label( task, next_action_label, overview_task_ids, overview_task_labels) [remove_label(child_task, next_action_label, overview_task_ids, overview_task_labels) for child_task in child_tasks] - pass except: logging.warning( @@ -1199,6 +1191,7 @@ def autodoist_magic(args, api, connection): continue # Recurring task friendly - remove label with relative change from due date #TODO FIX THIS + f2 = re.search('start=due-(\d)+([dw])', task.content) # try: # f = task.content.find('start=due-') # if f > -1: From 4aad462ce3c24e98311fe78860a827193c2ec8b3 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 8 Jan 2023 18:42:16 +0100 Subject: [PATCH 19/50] Start=due-x functionality working again. Time set by user is now also included in the diff calculation --- autodoist.py | 82 +++++++++++++++++++++++----------------------------- 1 file changed, 36 insertions(+), 46 deletions(-) diff --git a/autodoist.py b/autodoist.py index 98126c4..3282025 100644 --- a/autodoist.py +++ b/autodoist.py @@ -1190,52 +1190,42 @@ def autodoist_magic(args, api, connection): 'Wrong start-date format for task: "%s". Please use "start="', task.content) continue - # Recurring task friendly - remove label with relative change from due date #TODO FIX THIS - f2 = re.search('start=due-(\d)+([dw])', task.content) - # try: - # f = task.content.find('start=due-') - # if f > -1: - # f1a = task.content.find( - # 'd') # Find 'd' from 'due' - # f1b = task.content.rfind( - # 'd') # Find 'd' from days - # f2 = task.content.find('w') - # f_end = task.content[f+10:].find(' ') - - # if f_end > -1: - # offset = task.content[f+10:f+10+f_end-1] - # else: - # offset = task.content[f+10:-1] - - # try: - # task_due_date = task.due['date'][:10] - # task_due_date = datetime.strptime( - # task_due_date, '%Y-%m-%d') - # except: - # logging.warning( - # 'No due date to determine start date for task: "%s".', task.content) - # continue - - # if f1a != f1b and f1b > -1: # To make sure it doesn't trigger if 'w' is chosen - # td = timedelta(days=int(offset)) - # elif f2 > -1: - # td = timedelta(weeks=int(offset)) - - # # If we're not in the offset from the due date yet, remove all labels - # start_date = task_due_date - td - # future_diff = ( - # datetime.today()-start_date).days - # if future_diff < 0: - # remove_label( - # task, next_action_label, overview_task_ids, overview_task_labels) - # [remove_label(child_task, next_action_label, overview_task_ids, - # overview_task_labels) for child_task in child_tasks] - # continue - - # except: - # logging.warning( - # 'Wrong start-date format for task: %s. Please use "start=due-"', task.content) - # continue + # Recurring task friendly - remove label with relative change from due date + if task.due is not None: + try: + f2 = re.search('start=due-(\d+)([dw])', task.content) + + if f2: + offset = f2.groups()[0] + + if f2.groups()[1] == 'd': + td = timedelta(days=int(offset)) + elif f2.groups()[1] == 'w': + td = timedelta(weeks=int(offset)) + + # Determine start-date + try: + due_date = datetime.strptime(task.due.datetime, "%Y-%m-%dT%H:%M:%S") + except: + due_date = datetime.strptime(task.due.date, "%Y-%m-%d") + + start_date = due_date - td + + # If we're not in the offset from the due date yet, remove all labels + future_diff = ( + datetime.today()-start_date).days + + if future_diff < 0: + remove_label( + task, next_action_label, overview_task_ids, overview_task_labels) + [remove_label(child_task, next_action_label, overview_task_ids, + overview_task_labels) for child_task in child_tasks] + continue + + except: + logging.warning( + 'Wrong start-date format for task: %s. Please use "start=due-"', task.content) + continue # Mark first found task in section if next_action_label is not None and first_found[1] == False: #TODO: is this always true? What about starred tasks? From 24d88460fb3a7c1a3cc43b3714ad5e083f7a158a Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 8 Jan 2023 22:23:54 +0100 Subject: [PATCH 20/50] Changed end-of-day functionality, but apparently updating the due-date with the new API will remove the recurring settings. Brilliant. Need to find a fix for this. --- autodoist.py | 153 +++++++++++++++++++++++++-------------------------- 1 file changed, 75 insertions(+), 78 deletions(-) diff --git a/autodoist.py b/autodoist.py index 3282025..285f5df 100644 --- a/autodoist.py +++ b/autodoist.py @@ -150,10 +150,10 @@ def db_check_existance(connection, model): if isinstance(model, Task): q_create = """ INSERT INTO - tasks (task_id, task_type, parent_type, r_tag) + tasks (task_id, task_type, parent_type, due_date, r_tag) VALUES - (%r, %s, %s, %i); - """ % (model.id, 'NULL', 'NULL', 0) + (%r, %s, %s, %s, %i); + """ % (model.id, 'NULL', 'NULL', 'NULL', 0) if isinstance(model, Section): q_create = """ @@ -208,6 +208,7 @@ def initialise_sqlite(): task_id INTEGER, task_type TEXT, parent_type TEXT, + due_date TEXT, r_tag INTEGER ); """ @@ -506,7 +507,7 @@ def get_type(args, connection, model, key): elif isinstance(model, Project): current_type = check_name(args, model.name, 3) # Projects - # Check if project type changed with respect to previous run + # Check if type changed with respect to previous run if old_type == current_type: type_changed = 0 else: @@ -687,43 +688,52 @@ def check_regen_mode(api, item, regen_labels_id): # Recurring lists logic -def run_recurring_lists_logic(args, api, item, child_items, child_items_all, regen_labels_id): +def run_recurring_lists_logic(args, api,connection, task, task_items, task_items_all, regen_labels_id): - if item['parent_id'] == 0: + if task.parent_id == 0: try: - if item['due']['is_recurring']: + if task.due.is_recurring: try: - # Check if the T0 task date has changed - if item['due']['date'][:10] != item['date_old']: - - # Mark children for action based on mode - if args.regeneration is not None: - - # Check if task has a regen label - regen_mode = check_regen_mode( - api, item, regen_labels_id) - - # If no label, use general mode instead - if regen_mode is None: - regen_mode = args.regeneration - logging.debug('Using general recurring mode \'%s\' for item: %s', - regen_mode, item.content) - else: - logging.debug('Using recurring label \'%s\' for item: %s', - regen_mode, item.content) - - # Apply tags based on mode - give_regen_tag = 0 - - if regen_mode == 1: # Regen all - give_regen_tag = 1 - elif regen_mode == 2: # Regen if all sub-tasks completed - if not child_items: - give_regen_tag = 1 - - if give_regen_tag == 1: - for child_item in child_items_all: - child_item['r_tag'] = 1 + db_task_due_date = db_read_value(connection, task, 'due_date')[0][0] + + if db_task_due_date is None: + # If date has never been saved before, create a new entry + logging.debug( + 'New recurring task detected: %s' % task.content) + db_update_value(connection, task, 'due_date', task.due.date) + + # Check if the T0 task date has changed, because a user has checked the task + if task.due.date != db_task_due_date: + + #TODO: reevaluate regeneration mode. Disabled for now. + # # Mark children for action based on mode + # if args.regeneration is not None: + + # # Check if task has a regen label + # regen_mode = check_regen_mode( + # api, item, regen_labels_id) + + # # If no label, use general mode instead + # if regen_mode is None: + # regen_mode = args.regeneration + # logging.debug('Using general recurring mode \'%s\' for item: %s', + # regen_mode, item.content) + # else: + # logging.debug('Using recurring label \'%s\' for item: %s', + # regen_mode, item.content) + + # # Apply tags based on mode + # give_regen_tag = 0 + + # if regen_mode == 1: # Regen all + # give_regen_tag = 1 + # elif regen_mode == 2: # Regen if all sub-tasks completed + # if not child_items: + # give_regen_tag = 1 + + # if give_regen_tag == 1: + # for child_item in child_items_all: + # child_item['r_tag'] = 1 # If alternative end of day, fix due date if needed if args.end is not None: @@ -735,10 +745,8 @@ def run_recurring_lists_logic(args, api, item, child_items, child_items_all, reg if (args.end - current_hour) > 0: # Determine the difference in days set by todoist - nd = [ - int(x) for x in item['due']['date'][:10].split('-')] - od = [ - int(x) for x in item['date_old'][:10].split('-')] + nd = [int(x) for x in task.due.date.split('-')] + od = [int(x) for x in db_task_due_date.split('-')] new_date = datetime( nd[0], nd[1], nd[2]) @@ -755,49 +763,38 @@ def run_recurring_lists_logic(args, api, item, child_items, child_items_all, reg if days_overdue >= 1 and days_difference == 1: # Find current date in string format - today_str = [str(x) for x in [ - today.year, today.month, today.day]] - if len(today_str[1]) == 1: - today_str[1] = ''.join( - ['0', today_str[1]]) + today_str = t.strftime("%Y-%m-%d") # Update due-date to today - item_due = item['due'] - item_due['date'] = '-'.join( - today_str) - item.update(due=item_due) - # item.update(due={'date': '2020-05-29', 'is_recurring': True, 'string': 'every day'}) + api.update_task(task_id=task.id, due_date=today_str) #TODO: Apparently this breaks the reccuring string... # Save the new date for reference us - item.update( - date_old=item['due']['date'][:10]) + db_update_value(connection, task, 'due_date', task.due.date) except: # If date has never been saved before, create a new entry logging.debug( - 'New recurring task detected: %s' % item.content) - item['date_old'] = item['due']['date'][:10] - api.items.update(item['id']) + 'New recurring task detected: %s' % task.content) + db_update_value(connection, task, 'due_date', task.due.date) except: - # logging.debug( - # 'Parent not recurring: %s' % item.content) - pass - - if args.regeneration is not None and item['parent_id'] != 0: - try: - if item['r_tag'] == 1: - item.update(checked=0) - item.update(in_history=0) - item['r_tag'] = 0 - api.items.update(item['id']) - - for child_item in child_items_all: - child_item['r_tag'] = 1 - except: - # logging.debug('Child not recurring: %s' % - # item.content) pass + + #TODO: reevaluate regeneration mode. Disabled for now. + # if args.regeneration is not None and item.parent_id != 0: + # try: + # if item['r_tag'] == 1: + # item.update(checked=0) + # item.update(in_history=0) + # item['r_tag'] = 0 + # api.items.update(item['id']) + + # for child_item in child_items_all: + # child_item['r_tag'] = 1 + # except: + # # logging.debug('Child not recurring: %s' % + # # item.content) + # pass # Find and clean all children under a task @@ -968,10 +965,10 @@ def autodoist_magic(args, api, connection): # except: # pass - # # If options turned on, start recurring lists logic - # if args.regeneration is not None or args.end: - # run_recurring_lists_logic( - # args, api, item, child_items, child_items_all, regen_labels_id) + # If options turned on, start recurring lists logic #TODO: regeneration currently doesn't work, becaue TASK_ENDPOINT doesn't show completed tasks. Use workaround. + if args.regeneration is not None or args.end: + run_recurring_lists_logic( + args, api, connection, task, child_tasks, child_tasks_all, regen_labels_id) # If options turned on, start labelling logic if next_action_label is not None: From af63bb97c0d7a7d0cf2026ca47f5c6b687419e11 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Mon, 9 Jan 2023 21:45:08 +0100 Subject: [PATCH 21/50] Minor addition of logging --- autodoist.py | 1 + 1 file changed, 1 insertion(+) diff --git a/autodoist.py b/autodoist.py index 285f5df..791bd93 100644 --- a/autodoist.py +++ b/autodoist.py @@ -767,6 +767,7 @@ def run_recurring_lists_logic(args, api,connection, task, task_items, task_items # Update due-date to today api.update_task(task_id=task.id, due_date=today_str) #TODO: Apparently this breaks the reccuring string... + logging.info("Update date on task: '%s'" % (task.content)) # Save the new date for reference us db_update_value(connection, task, 'due_date', task.due.date) From 619489a1d08fbdfe698fe625614d84b8f849c13d Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Mon, 9 Jan 2023 22:33:15 +0100 Subject: [PATCH 22/50] Initial update of check_header function. Still to finish and test --- autodoist.py | 199 +++++++++++++++++++++++++++------------------------ 1 file changed, 104 insertions(+), 95 deletions(-) diff --git a/autodoist.py b/autodoist.py index 3282025..da78533 100644 --- a/autodoist.py +++ b/autodoist.py @@ -150,10 +150,10 @@ def db_check_existance(connection, model): if isinstance(model, Task): q_create = """ INSERT INTO - tasks (task_id, task_type, parent_type, r_tag) + tasks (task_id, task_type, parent_type, due_date, r_tag) VALUES - (%r, %s, %s, %i); - """ % (model.id, 'NULL', 'NULL', 0) + (%r, %s, %s, %s, %i); + """ % (model.id, 'NULL', 'NULL', 'NULL', 0) if isinstance(model, Section): q_create = """ @@ -208,6 +208,7 @@ def initialise_sqlite(): task_id INTEGER, task_type TEXT, parent_type TEXT, + due_date TEXT, r_tag INTEGER ); """ @@ -506,7 +507,7 @@ def get_type(args, connection, model, key): elif isinstance(model, Project): current_type = check_name(args, model.name, 3) # Projects - # Check if project type changed with respect to previous run + # Check if type changed with respect to previous run if old_type == current_type: type_changed = 0 else: @@ -607,25 +608,36 @@ def update_labels(api, overview_task_ids, overview_task_labels): # Check if header logic needs to be applied -def check_header(level): +def check_header(api, model): header_all_in_level = False unheader_all_in_level = False - method = 0 + regex_a = '(^[*]{2}\s*)(.*)' + regex_b = '(^!\*\s*)(.*)' - if method == 1: - if name[:3] == '** ': - header_all_in_level = True - level.update(name=name[3:]) - if name[:3] == '!* ' or name[:3] == '_* ': - unheader_all_in_level = True - level.update(name=name[3:]) - elif method == 2: - if content[:3] == '** ': + if isinstance(model, Task): + ra = re.search(regex_a, model.content) + rb = re.search(regex_b, model.content) + prefix_a = ra[0] + prefix_b = rb[0] + + if prefix_a: header_all_in_level = True - level.update(content=content[3:]) - if content[:3] == '!* ' or content[:3] == '_* ': + api.update_task(task_id=model.id, content=ra[2]) + if prefix_b: unheader_all_in_level = True - level.update(content=content[3:]) + api.update_task(task_id=model.id, content=ra[2]) + + # api.update_section(section_id="7025", name="Supermarket") + # api.update_project(project_id="2203306141", name="Things To Buy") + + # elif isinstance(model, Section) or isinstance(model, Project): + # if name[:3] == '** ': + # header_all_in_level = True + # level.update(name=name[3:]) + # if name[:3] == '!* ' or name[:3] == '_* ': + # unheader_all_in_level = True + # level.update(name=name[3:]) + else: pass @@ -687,43 +699,52 @@ def check_regen_mode(api, item, regen_labels_id): # Recurring lists logic -def run_recurring_lists_logic(args, api, item, child_items, child_items_all, regen_labels_id): +def run_recurring_lists_logic(args, api,connection, task, task_items, task_items_all, regen_labels_id): - if item['parent_id'] == 0: + if task.parent_id == 0: try: - if item['due']['is_recurring']: + if task.due.is_recurring: try: - # Check if the T0 task date has changed - if item['due']['date'][:10] != item['date_old']: - - # Mark children for action based on mode - if args.regeneration is not None: - - # Check if task has a regen label - regen_mode = check_regen_mode( - api, item, regen_labels_id) - - # If no label, use general mode instead - if regen_mode is None: - regen_mode = args.regeneration - logging.debug('Using general recurring mode \'%s\' for item: %s', - regen_mode, item.content) - else: - logging.debug('Using recurring label \'%s\' for item: %s', - regen_mode, item.content) - - # Apply tags based on mode - give_regen_tag = 0 - - if regen_mode == 1: # Regen all - give_regen_tag = 1 - elif regen_mode == 2: # Regen if all sub-tasks completed - if not child_items: - give_regen_tag = 1 - - if give_regen_tag == 1: - for child_item in child_items_all: - child_item['r_tag'] = 1 + db_task_due_date = db_read_value(connection, task, 'due_date')[0][0] + + if db_task_due_date is None: + # If date has never been saved before, create a new entry + logging.debug( + 'New recurring task detected: %s' % task.content) + db_update_value(connection, task, 'due_date', task.due.date) + + # Check if the T0 task date has changed, because a user has checked the task + if task.due.date != db_task_due_date: + + #TODO: reevaluate regeneration mode. Disabled for now. + # # Mark children for action based on mode + # if args.regeneration is not None: + + # # Check if task has a regen label + # regen_mode = check_regen_mode( + # api, item, regen_labels_id) + + # # If no label, use general mode instead + # if regen_mode is None: + # regen_mode = args.regeneration + # logging.debug('Using general recurring mode \'%s\' for item: %s', + # regen_mode, item.content) + # else: + # logging.debug('Using recurring label \'%s\' for item: %s', + # regen_mode, item.content) + + # # Apply tags based on mode + # give_regen_tag = 0 + + # if regen_mode == 1: # Regen all + # give_regen_tag = 1 + # elif regen_mode == 2: # Regen if all sub-tasks completed + # if not child_items: + # give_regen_tag = 1 + + # if give_regen_tag == 1: + # for child_item in child_items_all: + # child_item['r_tag'] = 1 # If alternative end of day, fix due date if needed if args.end is not None: @@ -735,10 +756,8 @@ def run_recurring_lists_logic(args, api, item, child_items, child_items_all, reg if (args.end - current_hour) > 0: # Determine the difference in days set by todoist - nd = [ - int(x) for x in item['due']['date'][:10].split('-')] - od = [ - int(x) for x in item['date_old'][:10].split('-')] + nd = [int(x) for x in task.due.date.split('-')] + od = [int(x) for x in db_task_due_date.split('-')] new_date = datetime( nd[0], nd[1], nd[2]) @@ -755,49 +774,39 @@ def run_recurring_lists_logic(args, api, item, child_items, child_items_all, reg if days_overdue >= 1 and days_difference == 1: # Find current date in string format - today_str = [str(x) for x in [ - today.year, today.month, today.day]] - if len(today_str[1]) == 1: - today_str[1] = ''.join( - ['0', today_str[1]]) + today_str = t.strftime("%Y-%m-%d") # Update due-date to today - item_due = item['due'] - item_due['date'] = '-'.join( - today_str) - item.update(due=item_due) - # item.update(due={'date': '2020-05-29', 'is_recurring': True, 'string': 'every day'}) + api.update_task(task_id=task.id, due_date=today_str) #TODO: Apparently this breaks the reccuring string... + logging.info("Update date on task: '%s'" % (task.content)) # Save the new date for reference us - item.update( - date_old=item['due']['date'][:10]) + db_update_value(connection, task, 'due_date', task.due.date) except: # If date has never been saved before, create a new entry logging.debug( - 'New recurring task detected: %s' % item.content) - item['date_old'] = item['due']['date'][:10] - api.items.update(item['id']) - - except: - # logging.debug( - # 'Parent not recurring: %s' % item.content) - pass + 'New recurring task detected: %s' % task.content) + db_update_value(connection, task, 'due_date', task.due.date) - if args.regeneration is not None and item['parent_id'] != 0: - try: - if item['r_tag'] == 1: - item.update(checked=0) - item.update(in_history=0) - item['r_tag'] = 0 - api.items.update(item['id']) - - for child_item in child_items_all: - child_item['r_tag'] = 1 except: - # logging.debug('Child not recurring: %s' % - # item.content) pass + + #TODO: reevaluate regeneration mode. Disabled for now. + # if args.regeneration is not None and item.parent_id != 0: + # try: + # if item['r_tag'] == 1: + # item.update(checked=0) + # item.update(in_history=0) + # item['r_tag'] = 0 + # api.items.update(item['id']) + + # for child_item in child_items_all: + # child_item['r_tag'] = 1 + # except: + # # logging.debug('Child not recurring: %s' % + # # item.content) + # pass # Find and clean all children under a task @@ -840,7 +849,7 @@ def autodoist_magic(args, api, connection): db_check_existance(connection, project) # Check if we need to (un)header entire project - header_all_in_p, unheader_all_in_p = check_header(project) + header_all_in_p, unheader_all_in_p = check_header(api, project) # Get project type if next_action_label is not None: @@ -896,7 +905,7 @@ def autodoist_magic(args, api, connection): db_check_existance(connection, section) # Check if we need to (un)header entire secion - header_all_in_s, unheader_all_in_s = check_header(section) + header_all_in_s, unheader_all_in_s = check_header(api, section) # Get section type if next_action_label: @@ -951,7 +960,7 @@ def autodoist_magic(args, api, connection): filter(lambda x: x.parent_id == task.id, non_completed_tasks)) # Check if we need to (un)header entire task tree - header_all_in_t, unheader_all_in_t = check_header(task) + header_all_in_t, unheader_all_in_t = check_header(api, task) # Modify headers where needed # TODO: DISABLED FOR NOW, FIX LATER @@ -968,10 +977,10 @@ def autodoist_magic(args, api, connection): # except: # pass - # # If options turned on, start recurring lists logic - # if args.regeneration is not None or args.end: - # run_recurring_lists_logic( - # args, api, item, child_items, child_items_all, regen_labels_id) + # If options turned on, start recurring lists logic #TODO: regeneration currently doesn't work, becaue TASK_ENDPOINT doesn't show completed tasks. Use workaround. + if args.regeneration is not None or args.end: + run_recurring_lists_logic( + args, api, connection, task, child_tasks, child_tasks_all, regen_labels_id) # If options turned on, start labelling logic if next_action_label is not None: From ce8044d5a7e400bd05e2078add6cc526a2359e6e Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Tue, 10 Jan 2023 20:20:54 +0100 Subject: [PATCH 23/50] Minor update to get_all_data --- autodoist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autodoist.py b/autodoist.py index 791bd93..1fd8176 100644 --- a/autodoist.py +++ b/autodoist.py @@ -431,7 +431,7 @@ def check_for_update(current_version): # Get all data through the SYNC API. Needed to see e.g. any completed tasks. -def get_all_data(self, api): +def get_all_data(api): BASE_URL = "https://api.todoist.com" SYNC_VERSION = "v9" SYNC_API = urljoin(BASE_URL, f"/sync/{SYNC_VERSION}/") From 7f766a5df7ad59576feae0bfa4041a91bfe6c487 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Tue, 10 Jan 2023 20:24:33 +0100 Subject: [PATCH 24/50] Minor update --- autodoist.py | 1 + 1 file changed, 1 insertion(+) diff --git a/autodoist.py b/autodoist.py index 1fd8176..1c1b373 100644 --- a/autodoist.py +++ b/autodoist.py @@ -5,6 +5,7 @@ from todoist_api_python.models import Section from todoist_api_python.models import Project from todoist_api_python.http_requests import get +from todoist_api_python.http_requests import post from urllib.parse import urljoin import sys import time From 685da743abb5ea6c7b93683d3288de6f1528eab9 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Thu, 12 Jan 2023 21:04:49 +0100 Subject: [PATCH 25/50] Fix found for updating the due task date, which keeps the recurring string intact --- autodoist.py | 2 +- metadata.sqlite | Bin 0 -> 20480 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 metadata.sqlite diff --git a/autodoist.py b/autodoist.py index 1c1b373..5584518 100644 --- a/autodoist.py +++ b/autodoist.py @@ -767,7 +767,7 @@ def run_recurring_lists_logic(args, api,connection, task, task_items, task_items today_str = t.strftime("%Y-%m-%d") # Update due-date to today - api.update_task(task_id=task.id, due_date=today_str) #TODO: Apparently this breaks the reccuring string... + api.update_task(task_id=task.id, due_date=today_str, due_string=task.due.string) #TODO: Apparently this breaks the reccuring string... logging.info("Update date on task: '%s'" % (task.content)) # Save the new date for reference us diff --git a/metadata.sqlite b/metadata.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..cc84a2a81c4b1ecfd2ea2a5687e3da83a4962c1a GIT binary patch literal 20480 zcmeI)Pe>F|90%|>JG<_#`rb1whaz=6M2Z@^5(GiD$&j07rW>?`ER#;*|Iu9ug(%$) zd59>elMr3XLqUh=5Ojh+U>_LA}Q50#eFRh;$G17W!IA#9|wDoAwxTeOV zhdVXZ9#ZRyRnZ0q)L2(sJEHZd-952m(Vm3bp(WJl$@uYDmsoX7>xwtpGp$ugce-aV zJ#89lTsxJUXzG*3SUUOJ=)jbb9MHv0raN&mnbyxar?<~hKTGAHQdK3*j@lKZjQ;fC z_*lw0`U~oCMmJnaZsEFWoN?>Yv)9w@D^eO7q(t^=CPu_vax!I1Oc`VSMyfEB3$Lv( zSvRJS8jWIQ{cO3Et?<0Pr#U%3B#KNqM_<7%&gh0K%q?73p);QUa({4*`gI3u`whdqx!2A)fyicmJ#W3BSXy@M%88PqVwcgYV}Jyo^ii zJNv}mvRmviyTAs-bTkM+00Izz00bZa0SG_<0ucCr1r#M{y)UjdM5N2(@7g2Hk=CZ> zmZp}7Qt3%7FMAT(Jc$Rl-2|(!5}b&wG8bPqnylE3XLczj-D)q)<#+HJ!mT&e<<3mY zz1dp2#S<>Qp=J4SP7fz2_lCWcdBdY()6(3|8Ah#C_M&DQgC;BW#Pv^2R^o{pk|qu3 zx5`ZKuBIV3>}!kBpc}rp{zWOWLklnS$hexPXjKAkV!pP Date: Thu, 12 Jan 2023 21:32:51 +0100 Subject: [PATCH 26/50] check_header function updated --- autodoist.py | 58 +++++++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/autodoist.py b/autodoist.py index da78533..e314384 100644 --- a/autodoist.py +++ b/autodoist.py @@ -612,34 +612,40 @@ def check_header(api, model): header_all_in_level = False unheader_all_in_level = False regex_a = '(^[*]{2}\s*)(.*)' - regex_b = '(^!\*\s*)(.*)' + regex_b = '(^\-\*\s*)(.*)' - if isinstance(model, Task): - ra = re.search(regex_a, model.content) - rb = re.search(regex_b, model.content) - prefix_a = ra[0] - prefix_b = rb[0] - - if prefix_a: - header_all_in_level = True - api.update_task(task_id=model.id, content=ra[2]) - if prefix_b: - unheader_all_in_level = True - api.update_task(task_id=model.id, content=ra[2]) - - # api.update_section(section_id="7025", name="Supermarket") - # api.update_project(project_id="2203306141", name="Things To Buy") - - # elif isinstance(model, Section) or isinstance(model, Project): - # if name[:3] == '** ': - # header_all_in_level = True - # level.update(name=name[3:]) - # if name[:3] == '!* ' or name[:3] == '_* ': - # unheader_all_in_level = True - # level.update(name=name[3:]) + try: + if isinstance(model, Task): + ra = re.search(regex_a, model.content) + rb = re.search(regex_b, model.content) + + if ra: + header_all_in_level = True + api.update_task(task_id=model.id, content=ra[2]) + if rb: + unheader_all_in_level = True + api.update_task(task_id=model.id, content=rb[2]) + else: + ra = re.search(regex_a, model.name) + rb = re.search(regex_b, model.name) - else: - pass + if isinstance(model, Section): + if ra: + header_all_in_level = True + api.update_section(section_id=model.id, name=ra[2]) + if rb: + unheader_all_in_level = True + api.update_section(section_id=model.id, name=rb[2]) + + elif isinstance(model, Project): + if ra: + header_all_in_level = True + api.update_project(project_id=model.id, name=ra[2]) + if rb: + unheader_all_in_level = True + api.update_project(project_id=model.id, name=rb[2]) + except: + logging.debug('check_header: no right model found') return header_all_in_level, unheader_all_in_level From bf6ce05ad82c90a377e7918a37bf2b49a51f0565 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Thu, 12 Jan 2023 21:42:21 +0100 Subject: [PATCH 27/50] bugfix needed: modify_headers does seem to work correctly at parentless task level --- autodoist.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/autodoist.py b/autodoist.py index e314384..21db084 100644 --- a/autodoist.py +++ b/autodoist.py @@ -652,20 +652,21 @@ def check_header(api, model): # Logic for applying and removing headers -def modify_headers(task, child_tasks, header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t): +def modify_headers(api, task, child_tasks, header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t): if any([header_all_in_p, header_all_in_s, header_all_in_t]): if task.content[0] != '*': - task.update(content='* ' + task.content) + api.update_task(task_id=task.id, content='* ' + task.content) + for ci in child_tasks: if not ci.content.startswith('*'): - ci.update(content='* ' + ci.content) + api.update_task(task_id=ci.id, content='* ' + ci.content) if any([unheader_all_in_p, unheader_all_in_s]): if task.content[0] == '*': - task.update(content=task.content[2:]) + api.update_task(task_id=task.id, content=task.content[2:]) + if unheader_all_in_t: - [ci.update(content=ci.content[2:]) - for ci in child_tasks] + [api.update_task(task_id=ci.id, content=ci.content[2:]) for ci in child_tasks] # Check regen mode based on label name @@ -969,8 +970,7 @@ def autodoist_magic(args, api, connection): header_all_in_t, unheader_all_in_t = check_header(api, task) # Modify headers where needed - # TODO: DISABLED FOR NOW, FIX LATER - # modify_headers(header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t) + modify_headers(api, task, child_tasks, header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t) # TODO: Check is regeneration is still needed, now that it's part of core Todoist. Disabled for now. # Logic for recurring lists From 707482cf1769834aa6bc5d56091de15a5a48a2bf Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Fri, 13 Jan 2023 16:35:00 +0100 Subject: [PATCH 28/50] Adding and removing headers defined on parentless task level now processes all children correctly. --- autodoist.py | 51 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/autodoist.py b/autodoist.py index 21db084..fd93c19 100644 --- a/autodoist.py +++ b/autodoist.py @@ -621,9 +621,11 @@ def check_header(api, model): if ra: header_all_in_level = True + model.content = ra[2] # Local record api.update_task(task_id=model.id, content=ra[2]) if rb: unheader_all_in_level = True + model.content = rb[2] # Local record api.update_task(task_id=model.id, content=rb[2]) else: ra = re.search(regex_a, model.name) @@ -652,21 +654,27 @@ def check_header(api, model): # Logic for applying and removing headers -def modify_headers(api, task, child_tasks, header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t): - if any([header_all_in_p, header_all_in_s, header_all_in_t]): - if task.content[0] != '*': - api.update_task(task_id=task.id, content='* ' + task.content) - - for ci in child_tasks: - if not ci.content.startswith('*'): - api.update_task(task_id=ci.id, content='* ' + ci.content) +def modify_task_headers(api, task, section_tasks, header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t): + if any([header_all_in_p, header_all_in_s]): + if task.content[:2] != '* ': + api.update_task(task_id=task.id, content='* ' + task.content) + if any([unheader_all_in_p, unheader_all_in_s]): - if task.content[0] == '*': + if task.content[:2] == '* ': api.update_task(task_id=task.id, content=task.content[2:]) + if header_all_in_t: + if task.content[:2] != '* ': + api.update_task(task_id=task.id, content='* ' + task.content) + find_and_headerify_all_children(api, task, section_tasks, 1) + if unheader_all_in_t: - [api.update_task(task_id=ci.id, content=ci.content[2:]) for ci in child_tasks] + if task.content[:2] == '* ': + api.update_task(task_id=task.id, content=task.content[2:]) + find_and_headerify_all_children(api, task, section_tasks, 2) + + # Check regen mode based on label name @@ -829,6 +837,27 @@ def find_and_clean_all_children(task_ids, task, section_tasks): return task_ids +def find_and_headerify_all_children(api, task, section_tasks, mode): + + child_tasks = list(filter(lambda x: x.parent_id == task.id, section_tasks)) + + if child_tasks != []: + for child_task in child_tasks: + # Children found, go deeper + if mode == 1: + if child_task.content[:2] != '* ': + api.update_task(task_id=child_task.id, content='* ' + child_task.content) + + elif mode == 2: + if child_task.content[:2] == '* ': + api.update_task(task_id=child_task.id, content=child_task.content[2:]) + + find_and_headerify_all_children(api, child_task, section_tasks, mode) + + + + return 0 + # Contains all main autodoist functionalities @@ -970,7 +999,7 @@ def autodoist_magic(args, api, connection): header_all_in_t, unheader_all_in_t = check_header(api, task) # Modify headers where needed - modify_headers(api, task, child_tasks, header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t) + modify_task_headers(api, task, section_tasks, header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t) # TODO: Check is regeneration is still needed, now that it's part of core Todoist. Disabled for now. # Logic for recurring lists From 4dcce138c013335e727fbf8930c44eb2aea96436 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Fri, 13 Jan 2023 16:58:16 +0100 Subject: [PATCH 29/50] Added logic to show correct change counter in terminal --- autodoist.py | 60 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/autodoist.py b/autodoist.py index fd93c19..2ffdd1a 100644 --- a/autodoist.py +++ b/autodoist.py @@ -377,7 +377,7 @@ def initialise_api(args): api = TodoistAPI(**api_arguments) - logging.info("Autodoist has connected and is running fine!\n") + logging.info("Autodoist has successfully connected to Todoist!\n") # Check if labels exist @@ -608,7 +608,7 @@ def update_labels(api, overview_task_ids, overview_task_labels): # Check if header logic needs to be applied -def check_header(api, model): +def check_header(api, model, overview_updated_ids): header_all_in_level = False unheader_all_in_level = False regex_a = '(^[*]{2}\s*)(.*)' @@ -623,10 +623,12 @@ def check_header(api, model): header_all_in_level = True model.content = ra[2] # Local record api.update_task(task_id=model.id, content=ra[2]) + # overview_updated_ids.append(model.id) # Ignore this one, since else it's count double if rb: unheader_all_in_level = True model.content = rb[2] # Local record api.update_task(task_id=model.id, content=rb[2]) + overview_updated_ids.append(model.id) else: ra = re.search(regex_a, model.name) rb = re.search(regex_b, model.name) @@ -635,17 +637,21 @@ def check_header(api, model): if ra: header_all_in_level = True api.update_section(section_id=model.id, name=ra[2]) + overview_updated_ids.append(model.id) if rb: unheader_all_in_level = True api.update_section(section_id=model.id, name=rb[2]) + overview_updated_ids.append(model.id) elif isinstance(model, Project): if ra: header_all_in_level = True api.update_project(project_id=model.id, name=ra[2]) + overview_updated_ids.append(model.id) if rb: unheader_all_in_level = True api.update_project(project_id=model.id, name=rb[2]) + overview_updated_ids.append(model.id) except: logging.debug('check_header: no right model found') @@ -654,25 +660,29 @@ def check_header(api, model): # Logic for applying and removing headers -def modify_task_headers(api, task, section_tasks, header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t): +def modify_task_headers(api, task, section_tasks, overview_updated_ids, header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t): if any([header_all_in_p, header_all_in_s]): if task.content[:2] != '* ': api.update_task(task_id=task.id, content='* ' + task.content) + overview_updated_ids.append(task.id) if any([unheader_all_in_p, unheader_all_in_s]): if task.content[:2] == '* ': api.update_task(task_id=task.id, content=task.content[2:]) + overview_updated_ids.append(task.id) if header_all_in_t: if task.content[:2] != '* ': api.update_task(task_id=task.id, content='* ' + task.content) - find_and_headerify_all_children(api, task, section_tasks, 1) + overview_updated_ids.append(task.id) + find_and_headerify_all_children(api, task, section_tasks, overview_updated_ids, 1) if unheader_all_in_t: if task.content[:2] == '* ': api.update_task(task_id=task.id, content=task.content[2:]) - find_and_headerify_all_children(api, task, section_tasks, 2) + overview_updated_ids.append(task.id) + find_and_headerify_all_children(api, task, section_tasks, overview_updated_ids, 2) @@ -714,7 +724,7 @@ def check_regen_mode(api, item, regen_labels_id): # Recurring lists logic -def run_recurring_lists_logic(args, api,connection, task, task_items, task_items_all, regen_labels_id): +def run_recurring_lists_logic(args, api, connection, overview_updated_ids, task, task_items, task_items_all, regen_labels_id): if task.parent_id == 0: try: @@ -793,7 +803,8 @@ def run_recurring_lists_logic(args, api,connection, task, task_items, task_items # Update due-date to today api.update_task(task_id=task.id, due_date=today_str) #TODO: Apparently this breaks the reccuring string... - logging.info("Update date on task: '%s'" % (task.content)) + logging.debug("Update date on task: '%s'" % (task.content)) + overview_updated_ids.append(task.id) # Save the new date for reference us db_update_value(connection, task, 'due_date', task.due.date) @@ -837,7 +848,7 @@ def find_and_clean_all_children(task_ids, task, section_tasks): return task_ids -def find_and_headerify_all_children(api, task, section_tasks, mode): +def find_and_headerify_all_children(api, task, section_tasks, overview_updated_ids, mode): child_tasks = list(filter(lambda x: x.parent_id == task.id, section_tasks)) @@ -847,12 +858,14 @@ def find_and_headerify_all_children(api, task, section_tasks, mode): if mode == 1: if child_task.content[:2] != '* ': api.update_task(task_id=child_task.id, content='* ' + child_task.content) + overview_updated_ids.append(child_task.id) elif mode == 2: if child_task.content[:2] == '* ': api.update_task(task_id=child_task.id, content=child_task.content[2:]) + overview_updated_ids.append(child_task.id) - find_and_headerify_all_children(api, child_task, section_tasks, mode) + find_and_headerify_all_children(api, child_task, section_tasks, overview_updated_ids, mode) @@ -866,10 +879,12 @@ def autodoist_magic(args, api, connection): # Preallocate dictionaries and other values overview_task_ids = {} overview_task_labels = {} + overview_updated_ids = [] next_action_label = args.label regen_labels_id = args.regen_label_names first_found = [False, False, False] - + + # Get all projects info try: projects = api.get_projects() except Exception as error: @@ -885,7 +900,7 @@ def autodoist_magic(args, api, connection): db_check_existance(connection, project) # Check if we need to (un)header entire project - header_all_in_p, unheader_all_in_p = check_header(api, project) + header_all_in_p, unheader_all_in_p = check_header(api, project, overview_updated_ids) # Get project type if next_action_label is not None: @@ -941,7 +956,7 @@ def autodoist_magic(args, api, connection): db_check_existance(connection, section) # Check if we need to (un)header entire secion - header_all_in_s, unheader_all_in_s = check_header(api, section) + header_all_in_s, unheader_all_in_s = check_header(api, section, overview_updated_ids) # Get section type if next_action_label: @@ -996,10 +1011,10 @@ def autodoist_magic(args, api, connection): filter(lambda x: x.parent_id == task.id, non_completed_tasks)) # Check if we need to (un)header entire task tree - header_all_in_t, unheader_all_in_t = check_header(api, task) + header_all_in_t, unheader_all_in_t = check_header(api, task, overview_updated_ids) # Modify headers where needed - modify_task_headers(api, task, section_tasks, header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t) + modify_task_headers(api, task, section_tasks, overview_updated_ids, header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t) # TODO: Check is regeneration is still needed, now that it's part of core Todoist. Disabled for now. # Logic for recurring lists @@ -1015,7 +1030,7 @@ def autodoist_magic(args, api, connection): # If options turned on, start recurring lists logic #TODO: regeneration currently doesn't work, becaue TASK_ENDPOINT doesn't show completed tasks. Use workaround. if args.regeneration is not None or args.end: run_recurring_lists_logic( - args, api, connection, task, child_tasks, child_tasks_all, regen_labels_id) + args, api, connection, overview_updated_ids, task, child_tasks, child_tasks_all, regen_labels_id) # If options turned on, start labelling logic if next_action_label is not None: @@ -1280,7 +1295,7 @@ def autodoist_magic(args, api, connection): first_found[0] = True # Return all ids and corresponding labels that need to be modified - return overview_task_ids, overview_task_labels + return overview_task_ids, overview_task_labels, overview_updated_ids # Main @@ -1358,23 +1373,22 @@ def main(): # sync(api) # Evaluate projects, sections, and tasks - overview_task_ids, overview_task_labels = autodoist_magic( + overview_task_ids, overview_task_labels, overview_updated_ids = autodoist_magic( args, api, connection) # Commit next action label changes if args.label is not None: updated_ids = update_labels(api, overview_task_ids, overview_task_labels) + num_changes = len(updated_ids)+len(overview_updated_ids) - if len(updated_ids): - len_api_q = len(updated_ids) - - if len_api_q == 1: + if num_changes: + if num_changes == 1: logging.info( - '%d change committed to Todoist.', len_api_q) + '%d change committed to Todoist.', num_changes) else: logging.info( - '%d changes committed to Todoist.', len_api_q) + '%d changes committed to Todoist.', num_changes) else: logging.info('No changes in queue, skipping sync.') From e41dab31564b8b365a8401e2690edc09153bd403 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Fri, 13 Jan 2023 17:02:53 +0100 Subject: [PATCH 30/50] Minor cleanup --- autodoist.py | 222 +++++++++++++++++++++++++++++------------------- metadata.sqlite | Bin 20480 -> 0 bytes 2 files changed, 135 insertions(+), 87 deletions(-) delete mode 100644 metadata.sqlite diff --git a/autodoist.py b/autodoist.py index 93a160f..68a7be6 100644 --- a/autodoist.py +++ b/autodoist.py @@ -51,7 +51,8 @@ def execute_query(connection, query, *args): cursor = connection.cursor() try: value = args[0] - cursor.execute(query,(value,)) # Useful to pass None/NULL value correctly + # Useful to pass None/NULL value correctly + cursor.execute(query, (value,)) except: cursor.execute(query) @@ -118,7 +119,8 @@ def db_update_value(connection, model, column, value): db_name = 'projects' goal = 'project_id' - query = """UPDATE %s SET %s = ? WHERE %s = %r""" % (db_name, column, goal, model.id) + query = """UPDATE %s SET %s = ? WHERE %s = %r""" % ( + db_name, column, goal, model.id) result = execute_query(connection, query, value) @@ -431,7 +433,8 @@ def check_for_update(current_version): return 1 # Get all data through the SYNC API. Needed to see e.g. any completed tasks. - + + def get_all_data(api): BASE_URL = "https://api.todoist.com" SYNC_VERSION = "v9" @@ -461,17 +464,17 @@ def check_name(args, string, num): re_ind = re.search(regex, string) suffix = re_ind[0] - # Somebody put fewer characters than intended. Take last character and apply for every missing one. + # Somebody put fewer characters than intended. Take last character and apply for every missing one. if len(suffix) < num: suffix += suffix[-1] * (num - len(suffix)) - current_type = '' + current_type = '' for s in suffix: if s == args.s_suffix: current_type += 's' elif s == args.p_suffix: current_type += 'p' - + # Always return a three letter string if len(current_type) == 2: current_type = 'x' + current_type @@ -482,8 +485,6 @@ def check_name(args, string, num): logging.debug("String {} not recognised.".format(string)) current_type = None - - return current_type # Scan the end of a name to find what type it is @@ -502,11 +503,11 @@ def get_type(args, connection, model, key): old_type = None if isinstance(model, Task): - current_type = check_name(args, model.content, 1) # Tasks + current_type = check_name(args, model.content, 1) # Tasks elif isinstance(model, Section): - current_type = check_name(args, model.name, 2) # Sections + current_type = check_name(args, model.name, 2) # Sections elif isinstance(model, Project): - current_type = check_name(args, model.name, 3) # Projects + current_type = check_name(args, model.name, 3) # Projects # Check if type changed with respect to previous run if old_type == current_type: @@ -622,12 +623,12 @@ def check_header(api, model, overview_updated_ids): if ra: header_all_in_level = True - model.content = ra[2] # Local record + model.content = ra[2] # Local record api.update_task(task_id=model.id, content=ra[2]) # overview_updated_ids.append(model.id) # Ignore this one, since else it's count double if rb: unheader_all_in_level = True - model.content = rb[2] # Local record + model.content = rb[2] # Local record api.update_task(task_id=model.id, content=rb[2]) overview_updated_ids.append(model.id) else: @@ -667,7 +668,7 @@ def modify_task_headers(api, task, section_tasks, overview_updated_ids, header_a if task.content[:2] != '* ': api.update_task(task_id=task.id, content='* ' + task.content) overview_updated_ids.append(task.id) - + if any([unheader_all_in_p, unheader_all_in_s]): if task.content[:2] == '* ': api.update_task(task_id=task.id, content=task.content[2:]) @@ -677,15 +678,16 @@ def modify_task_headers(api, task, section_tasks, overview_updated_ids, header_a if task.content[:2] != '* ': api.update_task(task_id=task.id, content='* ' + task.content) overview_updated_ids.append(task.id) - find_and_headerify_all_children(api, task, section_tasks, overview_updated_ids, 1) + find_and_headerify_all_children( + api, task, section_tasks, overview_updated_ids, 1) if unheader_all_in_t: if task.content[:2] == '* ': api.update_task(task_id=task.id, content=task.content[2:]) overview_updated_ids.append(task.id) - find_and_headerify_all_children(api, task, section_tasks, overview_updated_ids, 2) + find_and_headerify_all_children( + api, task, section_tasks, overview_updated_ids, 2) - # Check regen mode based on label name @@ -731,19 +733,21 @@ def run_recurring_lists_logic(args, api, connection, overview_updated_ids, task, try: if task.due.is_recurring: try: - db_task_due_date = db_read_value(connection, task, 'due_date')[0][0] + db_task_due_date = db_read_value( + connection, task, 'due_date')[0][0] if db_task_due_date is None: # If date has never been saved before, create a new entry logging.debug( 'New recurring task detected: %s' % task.content) - db_update_value(connection, task, 'due_date', task.due.date) + db_update_value(connection, task, + 'due_date', task.due.date) # Check if the T0 task date has changed, because a user has checked the task if task.due.date != db_task_due_date: - #TODO: reevaluate regeneration mode. Disabled for now. - # # Mark children for action based on mode + # TODO: reevaluate regeneration mode. Disabled for now. + # # Mark children for action based on mode # if args.regeneration is not None: # # Check if task has a regen label @@ -783,7 +787,8 @@ def run_recurring_lists_logic(args, api, connection, overview_updated_ids, task, # Determine the difference in days set by todoist nd = [int(x) for x in task.due.date.split('-')] - od = [int(x) for x in db_task_due_date.split('-')] + od = [int(x) + for x in db_task_due_date.split('-')] new_date = datetime( nd[0], nd[1], nd[2]) @@ -803,22 +808,26 @@ def run_recurring_lists_logic(args, api, connection, overview_updated_ids, task, today_str = t.strftime("%Y-%m-%d") # Update due-date to today - api.update_task(task_id=task.id, due_date=today_str, due_string=task.due.string) #TODO: Apparently this breaks the reccuring string... - logging.info("Update date on task: '%s'" % (task.content)) + api.update_task( + task_id=task.id, due_date=today_str, due_string=task.due.string) + logging.info( + "Update date on task: '%s'" % (task.content)) # Save the new date for reference us - db_update_value(connection, task, 'due_date', task.due.date) + db_update_value(connection, task, + 'due_date', task.due.date) except: # If date has never been saved before, create a new entry logging.debug( 'New recurring task detected: %s' % task.content) - db_update_value(connection, task, 'due_date', task.due.date) + db_update_value(connection, task, + 'due_date', task.due.date) except: pass - - #TODO: reevaluate regeneration mode. Disabled for now. + + # TODO: reevaluate regeneration mode. Disabled for now. # if args.regeneration is not None and item.parent_id != 0: # try: # if item['r_tag'] == 1: @@ -836,6 +845,7 @@ def run_recurring_lists_logic(args, api, connection, overview_updated_ids, task, # Find and clean all children under a task + def find_and_clean_all_children(task_ids, task, section_tasks): child_tasks = list(filter(lambda x: x.parent_id == task.id, section_tasks)) @@ -844,10 +854,12 @@ def find_and_clean_all_children(task_ids, task, section_tasks): for child_task in child_tasks: # Children found, go deeper task_ids.append(child_task.id) - task_ids = find_and_clean_all_children(task_ids, child_task, section_tasks) + task_ids = find_and_clean_all_children( + task_ids, child_task, section_tasks) return task_ids + def find_and_headerify_all_children(api, task, section_tasks, overview_updated_ids, mode): child_tasks = list(filter(lambda x: x.parent_id == task.id, section_tasks)) @@ -857,17 +869,18 @@ def find_and_headerify_all_children(api, task, section_tasks, overview_updated_i # Children found, go deeper if mode == 1: if child_task.content[:2] != '* ': - api.update_task(task_id=child_task.id, content='* ' + child_task.content) + api.update_task(task_id=child_task.id, + content='* ' + child_task.content) overview_updated_ids.append(child_task.id) - + elif mode == 2: if child_task.content[:2] == '* ': - api.update_task(task_id=child_task.id, content=child_task.content[2:]) + api.update_task(task_id=child_task.id, + content=child_task.content[2:]) overview_updated_ids.append(child_task.id) - find_and_headerify_all_children(api, child_task, section_tasks, overview_updated_ids, mode) - - + find_and_headerify_all_children( + api, child_task, section_tasks, overview_updated_ids, mode) return 0 @@ -883,7 +896,7 @@ def autodoist_magic(args, api, connection): next_action_label = args.label regen_labels_id = args.regen_label_names first_found = [False, False, False] - + # Get all projects info try: projects = api.get_projects() @@ -900,7 +913,8 @@ def autodoist_magic(args, api, connection): db_check_existance(connection, project) # Check if we need to (un)header entire project - header_all_in_p, unheader_all_in_p = check_header(api, project, overview_updated_ids) + header_all_in_p, unheader_all_in_p = check_header( + api, project, overview_updated_ids) # Get project type if next_action_label is not None: @@ -920,7 +934,8 @@ def autodoist_magic(args, api, connection): if next_action_label is not None: if project_type_changed == 1: for task in project_tasks: - remove_label(task, next_action_label, overview_task_ids, overview_task_labels) + remove_label(task, next_action_label, + overview_task_ids, overview_task_labels) db_update_value(connection, task, 'task_type', None) db_update_value(connection, task, 'parent_type', None) @@ -956,7 +971,8 @@ def autodoist_magic(args, api, connection): db_check_existance(connection, section) # Check if we need to (un)header entire secion - header_all_in_s, unheader_all_in_s = check_header(api, section, overview_updated_ids) + header_all_in_s, unheader_all_in_s = check_header( + api, section, overview_updated_ids) # Get section type if next_action_label: @@ -968,7 +984,7 @@ def autodoist_magic(args, api, connection): # Get all tasks for the section section_tasks = [x for x in project_tasks if x.section_id - == section.id] + == section.id] # Change top tasks parents_id from 'None' to '0' in order to numerically sort later on for task in section_tasks: @@ -985,23 +1001,23 @@ def autodoist_magic(args, api, connection): if next_action_label is not None: if section_type_changed == 1: for task in section_tasks: - remove_label(task, next_action_label, overview_task_ids, overview_task_labels) + remove_label(task, next_action_label, + overview_task_ids, overview_task_labels) db_update_value(connection, task, 'task_type', None) db_update_value(connection, task, 'parent_type', None) - + # Reset first_found[1] = False # For all tasks in this section for task in section_tasks: - + # Reset - dominant_type = None + dominant_type = None # Check db existance db_check_existance(connection, task) - # Determine which child_tasks exist, both all and the ones that have not been checked yet non_completed_tasks = list( filter(lambda x: not x.is_completed, section_tasks)) @@ -1011,10 +1027,12 @@ def autodoist_magic(args, api, connection): filter(lambda x: x.parent_id == task.id, non_completed_tasks)) # Check if we need to (un)header entire task tree - header_all_in_t, unheader_all_in_t = check_header(api, task, overview_updated_ids) + header_all_in_t, unheader_all_in_t = check_header( + api, task, overview_updated_ids) # Modify headers where needed - modify_task_headers(api, task, section_tasks, overview_updated_ids, header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t) + modify_task_headers(api, task, section_tasks, overview_updated_ids, header_all_in_p, + unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t) # TODO: Check is regeneration is still needed, now that it's part of core Todoist. Disabled for now. # Logic for recurring lists @@ -1045,13 +1063,18 @@ def autodoist_magic(args, api, connection): db_update_value(connection, task, 'task_type', None) db_update_value(connection, task, 'parent_type', None) - task_ids = find_and_clean_all_children([], task, section_tasks) - child_tasks_all = list(filter(lambda x: x.id in task_ids, section_tasks)) + task_ids = find_and_clean_all_children( + [], task, section_tasks) + child_tasks_all = list( + filter(lambda x: x.id in task_ids, section_tasks)) for child_task in child_tasks_all: - remove_label(child_task, next_action_label, overview_task_ids, overview_task_labels) - db_update_value(connection, child_task, 'task_type', None) - db_update_value(connection, child_task, 'parent_type', None) + remove_label(child_task, next_action_label, + overview_task_ids, overview_task_labels) + db_update_value( + connection, child_task, 'task_type', None) + db_update_value( + connection, child_task, 'parent_type', None) continue @@ -1064,13 +1087,18 @@ def autodoist_magic(args, api, connection): if task_type_changed == 1: # Find all children under this task - task_ids = find_and_clean_all_children([], task, section_tasks) - child_tasks_all = list(filter(lambda x: x.id in task_ids, section_tasks)) + task_ids = find_and_clean_all_children( + [], task, section_tasks) + child_tasks_all = list( + filter(lambda x: x.id in task_ids, section_tasks)) for child_task in child_tasks_all: - remove_label(child_task, next_action_label, overview_task_ids, overview_task_labels) - db_update_value(connection, child_task, 'task_type', None) - db_update_value(connection, child_task, 'parent_type', None) + remove_label( + child_task, next_action_label, overview_task_ids, overview_task_labels) + db_update_value( + connection, child_task, 'task_type', None) + db_update_value( + connection, child_task, 'parent_type', None) # Determine hierarchy types for logic hierarchy_types = [task_type, @@ -1080,9 +1108,10 @@ def autodoist_magic(args, api, connection): # If task has no type, but has a label, most likely the order has been changed by user. Remove data. if not True in hierarchy_boolean and next_action_label in task.labels: - remove_label(task, next_action_label, overview_task_ids, overview_task_labels) + remove_label(task, next_action_label, + overview_task_ids, overview_task_labels) db_update_value(connection, task, 'task_type', None) - db_update_value(connection, task, 'parent_type', None) + db_update_value(connection, task, 'parent_type', None) # If it is a parentless task, set task type based on hierarchy if task.parent_id == 0: @@ -1104,53 +1133,65 @@ def autodoist_magic(args, api, connection): if dominant_type[0] == 's': if not first_found[0]: - if dominant_type[1] == 's': + if dominant_type[1] == 's': if not first_found[1]: - add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + add_label( + connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) elif next_action_label in task.labels: # Probably the task has been manually moved, so if it has a label, let's remove it. - remove_label(task, next_action_label, overview_task_ids, overview_task_labels) + remove_label( + task, next_action_label, overview_task_ids, overview_task_labels) elif dominant_type[1] == 'p': - add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + add_label( + connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) elif dominant_type[0] == 'p': - if dominant_type[1] == 's': + if dominant_type[1] == 's': if not first_found[1]: - add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) - + add_label( + connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + elif next_action_label in task.labels: # Probably the task has been manually moved, so if it has a label, let's remove it. - remove_label(task, next_action_label, overview_task_ids, overview_task_labels) + remove_label( + task, next_action_label, overview_task_ids, overview_task_labels) elif dominant_type[1] == 'p': - add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + add_label( + connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) # If indicated on section level - if dominant_type[0] == 'x' and dominant_type[1] == 's': + if dominant_type[0] == 'x' and dominant_type[1] == 's': if not first_found[1]: - add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + add_label( + connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) elif next_action_label in task.labels: # Probably the task has been manually moved, so if it has a label, let's remove it. - remove_label(task, next_action_label, overview_task_ids, overview_task_labels) + remove_label( + task, next_action_label, overview_task_ids, overview_task_labels) elif dominant_type[0] == 'x' and dominant_type[1] == 'p': - add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + add_label(connection, task, dominant_type, next_action_label, + overview_task_ids, overview_task_labels) # If indicated on parentless task level - if dominant_type[1] == 'x' and dominant_type[2] == 's': + if dominant_type[1] == 'x' and dominant_type[2] == 's': if not first_found[1]: - add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + add_label( + connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) if next_action_label in task.labels: - # Probably the task has been manually moved, so if it has a label, let's remove it. - remove_label(task, next_action_label, overview_task_ids, overview_task_labels) + # Probably the task has been manually moved, so if it has a label, let's remove it. + remove_label( + task, next_action_label, overview_task_ids, overview_task_labels) elif dominant_type[1] == 'x' and dominant_type[2] == 'p': - add_label(connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + add_label(connection, task, dominant_type, next_action_label, + overview_task_ids, overview_task_labels) # If a parentless or sub-task which has children if len(child_tasks) > 0: @@ -1181,7 +1222,8 @@ def autodoist_magic(args, api, connection): continue # Clean up for good measure. - remove_label(child_task, next_action_label, overview_task_ids, overview_task_labels) + remove_label( + child_task, next_action_label, overview_task_ids, overview_task_labels) # Pass task_type down to the children db_update_value( @@ -1230,7 +1272,8 @@ def autodoist_magic(args, api, connection): # If start-date has not passed yet, remove label try: - f1 = re.search('start=(\d{2}[-]\d{2}[-]\d{4})', task.content) + f1 = re.search( + 'start=(\d{2}[-]\d{2}[-]\d{4})', task.content) if f1: start_date = f1.groups()[0] start_date = datetime.strptime( @@ -1252,8 +1295,9 @@ def autodoist_magic(args, api, connection): # Recurring task friendly - remove label with relative change from due date if task.due is not None: try: - f2 = re.search('start=due-(\d+)([dw])', task.content) - + f2 = re.search( + 'start=due-(\d+)([dw])', task.content) + if f2: offset = f2.groups()[0] @@ -1264,12 +1308,14 @@ def autodoist_magic(args, api, connection): # Determine start-date try: - due_date = datetime.strptime(task.due.datetime, "%Y-%m-%dT%H:%M:%S") + due_date = datetime.strptime( + task.due.datetime, "%Y-%m-%dT%H:%M:%S") except: - due_date = datetime.strptime(task.due.date, "%Y-%m-%d") + due_date = datetime.strptime( + task.due.date, "%Y-%m-%d") start_date = due_date - td - + # If we're not in the offset from the due date yet, remove all labels future_diff = ( datetime.today()-start_date).days @@ -1278,7 +1324,7 @@ def autodoist_magic(args, api, connection): remove_label( task, next_action_label, overview_task_ids, overview_task_labels) [remove_label(child_task, next_action_label, overview_task_ids, - overview_task_labels) for child_task in child_tasks] + overview_task_labels) for child_task in child_tasks] continue except: @@ -1287,11 +1333,13 @@ def autodoist_magic(args, api, connection): continue # Mark first found task in section - if next_action_label is not None and first_found[1] == False: #TODO: is this always true? What about starred tasks? + # TODO: is this always true? What about starred tasks? + if next_action_label is not None and first_found[1] == False: first_found[1] = True # Mark first found section with tasks in project (to account for None section) - if next_action_label is not None and first_found[0] == False and section_tasks: #TODO: is this always true? What about starred tasks? + # TODO: is this always true? What about starred tasks? + if next_action_label is not None and first_found[0] == False and section_tasks: first_found[0] = True # Return all ids and corresponding labels that need to be modified diff --git a/metadata.sqlite b/metadata.sqlite deleted file mode 100644 index cc84a2a81c4b1ecfd2ea2a5687e3da83a4962c1a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 zcmeI)Pe>F|90%|>JG<_#`rb1whaz=6M2Z@^5(GiD$&j07rW>?`ER#;*|Iu9ug(%$) zd59>elMr3XLqUh=5Ojh+U>_LA}Q50#eFRh;$G17W!IA#9|wDoAwxTeOV zhdVXZ9#ZRyRnZ0q)L2(sJEHZd-952m(Vm3bp(WJl$@uYDmsoX7>xwtpGp$ugce-aV zJ#89lTsxJUXzG*3SUUOJ=)jbb9MHv0raN&mnbyxar?<~hKTGAHQdK3*j@lKZjQ;fC z_*lw0`U~oCMmJnaZsEFWoN?>Yv)9w@D^eO7q(t^=CPu_vax!I1Oc`VSMyfEB3$Lv( zSvRJS8jWIQ{cO3Et?<0Pr#U%3B#KNqM_<7%&gh0K%q?73p);QUa({4*`gI3u`whdqx!2A)fyicmJ#W3BSXy@M%88PqVwcgYV}Jyo^ii zJNv}mvRmviyTAs-bTkM+00Izz00bZa0SG_<0ucCr1r#M{y)UjdM5N2(@7g2Hk=CZ> zmZp}7Qt3%7FMAT(Jc$Rl-2|(!5}b&wG8bPqnylE3XLczj-D)q)<#+HJ!mT&e<<3mY zz1dp2#S<>Qp=J4SP7fz2_lCWcdBdY()6(3|8Ah#C_M&DQgC;BW#Pv^2R^o{pk|qu3 zx5`ZKuBIV3>}!kBpc}rp{zWOWLklnS$hexPXjKAkV!pP Date: Fri, 13 Jan 2023 18:02:46 +0100 Subject: [PATCH 31/50] Manually added a similar fix as proposed in pull request #20 --- autodoist.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/autodoist.py b/autodoist.py index 68a7be6..941574f 100644 --- a/autodoist.py +++ b/autodoist.py @@ -295,7 +295,7 @@ def verify_label_existance(api, label_name, prompt_mode): if len(label) > 0: next_action_label = label[0].id - logging.debug('Label \'%s\' found as label id %d', + logging.debug('Label \'%s\' found as label id %s', label_name, next_action_label) else: # Create a new label in Todoist @@ -535,7 +535,7 @@ def get_project_type(args, connection, project): # Determine a section type -def get_section_type(args, connection, section): +def get_section_type(args, connection, section, project): """Identifies how a section should be handled.""" if section is not None: section_type, section_type_changed = get_type( @@ -545,22 +545,22 @@ def get_section_type(args, connection, section): section_type_changed = 0 if section_type is not None: - logging.debug('Identified \'%s\' as %s type', - section.name, section_type) + logging.debug("Identified '%s > %s' as %s type", + project.name, section.name, section_type) return section_type, section_type_changed # Determine an task type -def get_task_type(args, connection, task): +def get_task_type(args, connection, task, section, project): """Identifies how a task with sub tasks should be handled.""" task_type, task_type_changed = get_type( args, connection, task, 'task_type') if task_type is not None: - logging.debug('Identified \'%s\' as %s type', task.content, task_type) + logging.debug("Identified '%s > %s > %s' as %s type",project.name, section.name, task.content, task_type) return task_type, task_type_changed @@ -977,7 +977,7 @@ def autodoist_magic(args, api, connection): # Get section type if next_action_label: section_type, section_type_changed = get_section_type( - args, connection, section) + args, connection, section, project) else: section_type = None section_type_changed = 0 @@ -1080,7 +1080,7 @@ def autodoist_magic(args, api, connection): # Check task type task_type, task_type_changed = get_task_type( - args, connection, task) + args, connection, task, section, project) # If task type has changed, clean all of its children for good measure if next_action_label is not None: From f47e974c7cf2c1f1b035144610f53bef02e19aa0 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Fri, 13 Jan 2023 18:09:33 +0000 Subject: [PATCH 32/50] Allow setting Todoist API key from environment variable In the Docker world, secrets (e.g. passwords, API keys) are usually shared with an application as environment variables so that they can be imported from separate secure areas: in docker-compose you can specify a file where secrets are imported from as environment variables. Read the API key from the TODOIST_API_KEY environment variable first, then check the command-line argument. Signed-off-by: Christopher Obbard --- autodoist.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/autodoist.py b/autodoist.py index 941574f..09e4539 100644 --- a/autodoist.py +++ b/autodoist.py @@ -333,7 +333,7 @@ def initialise_api(args): # Check we have a API key if not args.api_key: logging.error( - "\n\nNo API key set. Run Autodoist with '-a '\n") + "\n\nNo API key set. Run Autodoist with '-a ' or set the environment variable TODOIST_API_KEY.\n") sys.exit(1) # Check if alternative end of day is used @@ -1356,8 +1356,8 @@ def main(): # Main process functions. parser = argparse.ArgumentParser( formatter_class=make_wide(argparse.HelpFormatter, w=120, h=60)) - parser.add_argument('-a', '--api_key', - help='takes your Todoist API Key.', type=str) + parser.add_argument( + '-a', '--api_key', help='takes your Todoist API Key.', default=os.environ.get('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( From 2ee1088e9cd97b926c09c89138fd6ce88281e979 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Sun, 2 Jan 2022 18:30:14 +0000 Subject: [PATCH 33/50] Add Dockerfile Add a Dockerfile to have the ability to build a container from the app. Signed-off-by: Christopher Obbard --- Dockerfile | 10 ++++++++++ README.md | 11 +++++++++++ 2 files changed, 21 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c91741c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3-slim-bullseye + +WORKDIR /usr/src/app + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +ENTRYPOINT [ "python", "./autodoist.py" ] diff --git a/README.md b/README.md index 2c6e38b..737c9f1 100644 --- a/README.md +++ b/README.md @@ -150,3 +150,14 @@ In addition, if you experience issues with syncing you can increase the api sync For all arguments, please check out the help: python autodoist.py --help + + +## Docker container + +To build the docker container, check out the repository and run: + + docker build . --tag autodoist:latest + +To run autodoist inside the docker container: + + docker run -it autodoist:latest From 0231937b8f5f15e8b2737aa17c5ecf70e45bc203 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Mon, 3 Jan 2022 16:44:06 +0000 Subject: [PATCH 34/50] Build and publish Docker image in GitHub actions Add the ability to build a Docker image in GitHub actions and push that image to the GitHub container registry. Signed-off-by: Christopher Obbard --- .github/workflows/ci.yaml | 41 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/ci.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..6a05e6f --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,41 @@ +name: Build and publish Docker image + +on: + push: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-publish-docker-image: + name: Build and publish Docker image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Log in to the Container registry + uses: docker/login-action@v1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v3 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@v2 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} From b741b83b14561aa341aa3d12056411a79c092eda Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Fri, 13 Jan 2023 21:32:06 +0100 Subject: [PATCH 35/50] Added possibility to ignore section labelling by providing a * either at the start or end of the section name. Useful for additional flexibility with e.g. kanban board. --- autodoist.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/autodoist.py b/autodoist.py index 941574f..db88eb4 100644 --- a/autodoist.py +++ b/autodoist.py @@ -964,9 +964,18 @@ def autodoist_magic(args, api, connection): # Reset first_found[0] = False + disable_section_labelling = 0 for section in sections: + # Check if section labelling is disabled (useful for e.g. Kanban) + if next_action_label is not None: + try: + if section.name.startswith('*') or section.name.endswith('*'): + disable_section_labelling = 1 + except: + pass + # Check db existance db_check_existance(connection, section) @@ -1057,7 +1066,7 @@ def autodoist_magic(args, api, connection): continue # Remove clean all task and subtask data - if task.content.startswith('*'): + if task.content.startswith('*') or disable_section_labelling: remove_label(task, next_action_label, overview_task_ids, overview_task_labels) db_update_value(connection, task, 'task_type', None) From 8541f06d9228f51052554844dc4d757b8486cf5b Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sat, 14 Jan 2023 22:01:09 +0100 Subject: [PATCH 36/50] Implemented SYNC API and commit queue, to ensure task content updates are sent in batches. This to ensure we don't run into the requests limits. --- autodoist.py | 248 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 165 insertions(+), 83 deletions(-) diff --git a/autodoist.py b/autodoist.py index db88eb4..be34e6d 100644 --- a/autodoist.py +++ b/autodoist.py @@ -7,6 +7,7 @@ from todoist_api_python.http_requests import get from todoist_api_python.http_requests import post from urllib.parse import urljoin +from urllib.parse import quote import sys import time import requests @@ -17,6 +18,8 @@ import sqlite3 import os import re +import json + # Connect to SQLite database @@ -238,18 +241,6 @@ def make_wide(formatter, w=120, h=36): logging.error("Argparse help formatter failed, falling back.") return formatter -# Sync with Todoist API - - -def sync(api): - try: - logging.debug('Syncing the current state from the API') - api.sync() - except Exception as e: - logging.exception( - 'Error trying to sync with Todoist API: %s' % str(e)) - quit() - # Simple query for yes/no answer @@ -323,7 +314,7 @@ def verify_label_existance(api, label_name, prompt_mode): logging.info('Exiting Autodoist.') exit(1) - return 0 + return labels # Initialisation of Autodoist @@ -379,9 +370,11 @@ def initialise_api(args): api_arguments['cache'] = None api = TodoistAPI(**api_arguments) - logging.info("Autodoist has successfully connected to Todoist!\n") + sync_api = initialise_sync_api(api) + api.sync_token = sync_api['sync_token'] # Save SYNC API token to enable partial syncs + # Check if labels exist # If labeling argument is used @@ -390,16 +383,17 @@ def initialise_api(args): # Verify that the next action label exists; ask user if it needs to be created verify_label_existance(api, args.label, 1) - # If regeneration mode is used, verify labels - if args.regeneration is not None: + # TODO: Disabled for now + # # If regeneration mode is used, verify labels + # if args.regeneration is not None: - # Verify the existance of the regeneraton labels; force creation of label - regen_labels_id = [verify_label_existance( - api, regen_label, 2) for regen_label in args.regen_label_names] + # # Verify the existance of the regeneraton labels; force creation of label + # regen_labels_id = [verify_label_existance( + # api, regen_label, 2) for regen_label in args.regen_label_names] - else: - # Label functionality not needed - regen_labels_id = [None, None, None] + # else: + # # Label functionality not needed + # regen_labels_id = [None, None, None] return api @@ -445,6 +439,87 @@ def get_all_data(api): return data + +def initialise_sync_api(api): + bearer_token = 'Bearer %s' % api._token + + headers = { + 'Authorization': bearer_token, + 'Content-Type': 'application/x-www-form-urlencoded', + } + data = 'sync_token=*&resource_types=["all"]' + + response = requests.post('https://api.todoist.com/sync/v9/sync', headers=headers, data=data) + + return json.loads(response.text) + +# Commit task content change to queue + + +def commit_content_update(api, task_id, content): + uuid = str(time.perf_counter()) # Create unique request id + data = {"type": "item_update", "uuid": uuid, + "args": {"id": task_id, "content": quote(content)}} + api.queue.append(data) + + return api + +# Ensure label updates are only issued once per task and commit to queue + + +def commit_labels_update(api, overview_task_ids, overview_task_labels): + + filtered_overview_ids = [ + k for k, v in overview_task_ids.items() if v != 0] + + for task_id in filtered_overview_ids: + labels = overview_task_labels[task_id] + + # api.update_task(task_id=task_id, labels=labels) # Not using REST API, since we would get too many single requests + uuid = str(time.perf_counter()) # Create unique request id + data = {"type": "item_update", "uuid": uuid, + "args": {"id": task_id, "labels": labels}} + api.queue.append(data) + + return api + + +# Update tasks in batch with Todoist Sync API + + +def sync(api): + # # This approach does not seem to work correctly. + # BASE_URL = "https://api.todoist.com" + # SYNC_VERSION = "v9" + # SYNC_API = urljoin(BASE_URL, f"/sync/{SYNC_VERSION}/") + # SYNC_ENDPOINT = "sync" + # endpoint = urljoin(SYNC_API, SYNC_ENDPOINT) + # task_data = post(api._session, endpoint, api._token, data=data) + + try: + bearer_token = 'Bearer %s' % api._token + + headers = { + 'Authorization': bearer_token, + 'Content-Type': 'application/x-www-form-urlencoded', + } + + data = 'sync_token=' + api.sync_token + '&commands=' + json.dumps(api.queue) + + response = requests.post( + 'https://api.todoist.com/sync/v9/sync', headers=headers, data=data) + + if response.status_code == 200: + return response.json() + + response.raise_for_status() + return response.ok + + except Exception as e: + logging.exception( + 'Error trying to sync with Todoist API: %s' % str(e)) + quit() + # Find the type based on name suffix. @@ -560,7 +635,8 @@ def get_task_type(args, connection, task, section, project): args, connection, task, 'task_type') if task_type is not None: - logging.debug("Identified '%s > %s > %s' as %s type",project.name, section.name, task.content, task_type) + logging.debug("Identified '%s > %s > %s' as %s type", + project.name, section.name, task.content, task_type) return task_type, task_type_changed @@ -594,23 +670,11 @@ def remove_label(task, label, overview_task_ids, overview_task_labels): overview_task_ids[task.id] = -1 overview_task_labels[task.id] = labels -# Ensure label updates are only issued once per task - - -def update_labels(api, overview_task_ids, overview_task_labels): - filtered_overview_ids = [ - k for k, v in overview_task_ids.items() if v != 0] - for task_id in filtered_overview_ids: - labels = overview_task_labels[task_id] - api.update_task(task_id=task_id, labels=labels) - - return filtered_overview_ids - # Check if header logic needs to be applied -def check_header(api, model, overview_updated_ids): +def check_header(api, model): header_all_in_level = False unheader_all_in_level = False regex_a = '(^[*]{2}\s*)(.*)' @@ -630,7 +694,7 @@ def check_header(api, model, overview_updated_ids): unheader_all_in_level = True model.content = rb[2] # Local record api.update_task(task_id=model.id, content=rb[2]) - overview_updated_ids.append(model.id) + # overview_updated_ids.append(model.id) else: ra = re.search(regex_a, model.name) rb = re.search(regex_b, model.name) @@ -639,54 +703,64 @@ def check_header(api, model, overview_updated_ids): if ra: header_all_in_level = True api.update_section(section_id=model.id, name=ra[2]) - overview_updated_ids.append(model.id) + api.overview_updated_ids.append(model.id) if rb: unheader_all_in_level = True api.update_section(section_id=model.id, name=rb[2]) - overview_updated_ids.append(model.id) + api.overview_updated_ids.append(model.id) elif isinstance(model, Project): if ra: header_all_in_level = True api.update_project(project_id=model.id, name=ra[2]) - overview_updated_ids.append(model.id) + api.overview_updated_ids.append(model.id) if rb: unheader_all_in_level = True api.update_project(project_id=model.id, name=rb[2]) - overview_updated_ids.append(model.id) + api.overview_updated_ids.append(model.id) except: logging.debug('check_header: no right model found') - return header_all_in_level, unheader_all_in_level + return api, header_all_in_level, unheader_all_in_level # Logic for applying and removing headers -def modify_task_headers(api, task, section_tasks, overview_updated_ids, header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t): +def modify_task_headers(api, task, section_tasks, header_all_in_p, unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t): if any([header_all_in_p, header_all_in_s]): if task.content[:2] != '* ': - api.update_task(task_id=task.id, content='* ' + task.content) - overview_updated_ids.append(task.id) + content = '* ' + task.content + api = commit_content_update(api, task.id, content) + # api.update_task(task_id=task.id, content='* ' + task.content) + # overview_updated_ids.append(task.id) if any([unheader_all_in_p, unheader_all_in_s]): if task.content[:2] == '* ': - api.update_task(task_id=task.id, content=task.content[2:]) - overview_updated_ids.append(task.id) + content = task.content[2:] + api = commit_content_update(api, task.id, content) + # api.update_task(task_id=task.id, content=task.content[2:]) + # overview_updated_ids.append(task.id) if header_all_in_t: if task.content[:2] != '* ': - api.update_task(task_id=task.id, content='* ' + task.content) - overview_updated_ids.append(task.id) - find_and_headerify_all_children( - api, task, section_tasks, overview_updated_ids, 1) + content = '* ' + task.content + api = commit_content_update(api, task.id, content) + # api.update_task(task_id=task.id, content='* ' + task.content) + # overview_updated_ids.append(task.id) + api = find_and_headerify_all_children( + api, task, section_tasks, 1) if unheader_all_in_t: if task.content[:2] == '* ': - api.update_task(task_id=task.id, content=task.content[2:]) - overview_updated_ids.append(task.id) - find_and_headerify_all_children( - api, task, section_tasks, overview_updated_ids, 2) + content = task.content[2:] + api = commit_content_update(api, task.id, content) + # api.update_task(task_id=task.id, content=task.content[2:]) + # overview_updated_ids.append(task.id) + api = find_and_headerify_all_children( + api, task, section_tasks, 2) + + return api # Check regen mode based on label name @@ -727,7 +801,7 @@ def check_regen_mode(api, item, regen_labels_id): # Recurring lists logic -def run_recurring_lists_logic(args, api, connection, overview_updated_ids, task, task_items, task_items_all, regen_labels_id): +def run_recurring_lists_logic(args, api, connection, task, task_items, task_items_all, regen_labels_id): if task.parent_id == 0: try: @@ -810,7 +884,7 @@ def run_recurring_lists_logic(args, api, connection, overview_updated_ids, task, # Update due-date to today api.update_task( task_id=task.id, due_date=today_str, due_string=task.due.string) - logging.info( + logging.debug( "Update date on task: '%s'" % (task.content)) # Save the new date for reference us @@ -860,7 +934,7 @@ def find_and_clean_all_children(task_ids, task, section_tasks): return task_ids -def find_and_headerify_all_children(api, task, section_tasks, overview_updated_ids, mode): +def find_and_headerify_all_children(api, task, section_tasks, mode): child_tasks = list(filter(lambda x: x.parent_id == task.id, section_tasks)) @@ -869,18 +943,22 @@ def find_and_headerify_all_children(api, task, section_tasks, overview_updated_i # Children found, go deeper if mode == 1: if child_task.content[:2] != '* ': - api.update_task(task_id=child_task.id, - content='* ' + child_task.content) - overview_updated_ids.append(child_task.id) + api = commit_content_update( + api, child_task.id, '* ' + child_task.content) + # api.update_task(task_id=child_task.id, + # content='* ' + child_task.content) + # overview_updated_ids.append(child_task.id) elif mode == 2: if child_task.content[:2] == '* ': - api.update_task(task_id=child_task.id, - content=child_task.content[2:]) - overview_updated_ids.append(child_task.id) + api = commit_content_update( + api, child_task.id, child_task.content[2:]) + # api.update_task(task_id=child_task.id, + # content=child_task.content[2:]) + # overview_updated_ids.append(child_task.id) find_and_headerify_all_children( - api, child_task, section_tasks, overview_updated_ids, mode) + api, child_task, section_tasks, mode) return 0 @@ -892,10 +970,11 @@ def autodoist_magic(args, api, connection): # Preallocate dictionaries and other values overview_task_ids = {} overview_task_labels = {} - overview_updated_ids = [] next_action_label = args.label regen_labels_id = args.regen_label_names first_found = [False, False, False] + api.queue = [] + api.overview_updated_ids = [] # Get all projects info try: @@ -913,8 +992,8 @@ def autodoist_magic(args, api, connection): db_check_existance(connection, project) # Check if we need to (un)header entire project - header_all_in_p, unheader_all_in_p = check_header( - api, project, overview_updated_ids) + api, header_all_in_p, unheader_all_in_p = check_header( + api, project) # Get project type if next_action_label is not None: @@ -964,12 +1043,12 @@ def autodoist_magic(args, api, connection): # Reset first_found[0] = False - disable_section_labelling = 0 for section in sections: # Check if section labelling is disabled (useful for e.g. Kanban) if next_action_label is not None: + disable_section_labelling = 0 try: if section.name.startswith('*') or section.name.endswith('*'): disable_section_labelling = 1 @@ -980,8 +1059,8 @@ def autodoist_magic(args, api, connection): db_check_existance(connection, section) # Check if we need to (un)header entire secion - header_all_in_s, unheader_all_in_s = check_header( - api, section, overview_updated_ids) + api, header_all_in_s, unheader_all_in_s = check_header( + api, section) # Get section type if next_action_label: @@ -1036,12 +1115,12 @@ def autodoist_magic(args, api, connection): filter(lambda x: x.parent_id == task.id, non_completed_tasks)) # Check if we need to (un)header entire task tree - header_all_in_t, unheader_all_in_t = check_header( - api, task, overview_updated_ids) + api, header_all_in_t, unheader_all_in_t = check_header( + api, task) # Modify headers where needed - modify_task_headers(api, task, section_tasks, overview_updated_ids, header_all_in_p, - unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t) + api = modify_task_headers(api, task, section_tasks, header_all_in_p, + unheader_all_in_p, header_all_in_s, unheader_all_in_s, header_all_in_t, unheader_all_in_t) # TODO: Check is regeneration is still needed, now that it's part of core Todoist. Disabled for now. # Logic for recurring lists @@ -1057,7 +1136,7 @@ def autodoist_magic(args, api, connection): # If options turned on, start recurring lists logic #TODO: regeneration currently doesn't work, becaue TASK_ENDPOINT doesn't show completed tasks. Use workaround. if args.regeneration is not None or args.end: run_recurring_lists_logic( - args, api, connection, overview_updated_ids, task, child_tasks, child_tasks_all, regen_labels_id) + args, api, connection, task, child_tasks, child_tasks_all, regen_labels_id) # If options turned on, start labelling logic if next_action_label is not None: @@ -1352,7 +1431,7 @@ def autodoist_magic(args, api, connection): first_found[0] = True # Return all ids and corresponding labels that need to be modified - return overview_task_ids, overview_task_labels, overview_updated_ids + return overview_task_ids, overview_task_labels # Main @@ -1395,7 +1474,7 @@ def main(): args = parser.parse_args() # #TODO: Temporary disable this feature for now. Find a way to see completed tasks first, since REST API v2 lost this funcionality. - args.regeneration = 0 + args.regeneration = None # Addition of regeneration labels args.regen_label_names = ('Regen_off', 'Regen_all', @@ -1427,17 +1506,20 @@ def main(): # Start main loop while True: start_time = time.time() - # sync(api) # Evaluate projects, sections, and tasks - overview_task_ids, overview_task_labels, overview_updated_ids = autodoist_magic( + overview_task_ids, overview_task_labels = autodoist_magic( args, api, connection) # Commit next action label changes if args.label is not None: - updated_ids = update_labels(api, overview_task_ids, - overview_task_labels) - num_changes = len(updated_ids)+len(overview_updated_ids) + api = commit_labels_update(api, overview_task_ids, + overview_task_labels) + + # Sync all queued up changes + sync(api) + + num_changes = len(api.queue)+len(api.overview_updated_ids) if num_changes: if num_changes == 1: From adb7ae86de0ca23467e9cbff2099f4296611fbb0 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sat, 14 Jan 2023 23:57:22 +0100 Subject: [PATCH 37/50] Build project and section dictionary from api.get_tasks() to save on api requests. Still todo: implement this into the loops. --- autodoist.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/autodoist.py b/autodoist.py index be34e6d..72296dd 100644 --- a/autodoist.py +++ b/autodoist.py @@ -19,6 +19,7 @@ import os import re import json +from collections import defaultdict # Connect to SQLite database @@ -28,7 +29,7 @@ def create_connection(path): connection = None try: connection = sqlite3.connect(path) - logging.info("Connection to SQLite DB successful") + logging.debug("Connection to SQLite DB successful!") except Exception as e: logging.error( f"Could not connect to the SQLite database: the error '{e}' occurred") @@ -223,6 +224,8 @@ def initialise_sqlite(): execute_query(connection, q_create_sections_table) execute_query(connection, q_create_tasks_table) + logging.info("SQLite DB has successfully initialized! \n") + return connection @@ -370,7 +373,7 @@ def initialise_api(args): api_arguments['cache'] = None api = TodoistAPI(**api_arguments) - logging.info("Autodoist has successfully connected to Todoist!\n") + logging.info("Autodoist has successfully connected to Todoist!") sync_api = initialise_sync_api(api) api.sync_token = sync_api['sync_token'] # Save SYNC API token to enable partial syncs @@ -976,9 +979,24 @@ def autodoist_magic(args, api, connection): api.queue = [] api.overview_updated_ids = [] - # Get all projects info + # Get all todoist info try: - projects = api.get_projects() + projects = api.get_projects() # To save on request to stay under the limit + # all_sections = api.get_sections() # To save on request to stay under the limit + all_tasks = api.get_tasks() + + # Build a dict of all project and task numbers + all_projects = [x.project_id for x in all_tasks] + all_sections = [x.section_id for x in all_tasks] + h = defaultdict(list) + for k, v in zip(all_projects, all_sections): + h[k].append(v) + dict_project_section = dict(h) + + # Store only unique values: + for key in dict_project_section: + dict_project_section[key] = list(set(dict_project_section[key])) + except Exception as error: print(error) @@ -1005,7 +1023,7 @@ def autodoist_magic(args, api, connection): # Get all tasks for the project try: - project_tasks = api.get_tasks(project_id=project.id) + project_tasks = api.get_tasks(project_id=project.id) # TODO: call them all once, and use filter here instead. except Exception as error: print(error) @@ -1036,7 +1054,7 @@ def autodoist_magic(args, api, connection): # Get all sections and add the 'None' section too. try: - sections = api.get_sections(project_id=project.id) + sections = api.get_sections(project_id=project.id) # TODO: call them all once, and use filter here instead. sections.insert(0, Section(None, None, 0, project.id)) except Exception as error: print(error) From cf57687068df5c26841367e4b399b40cd1f98b32 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 15 Jan 2023 00:24:17 +0100 Subject: [PATCH 38/50] Implemented the filtered lists. Significant decrease of api requests and noticable speed-up! --- autodoist.py | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/autodoist.py b/autodoist.py index 72296dd..622d5e1 100644 --- a/autodoist.py +++ b/autodoist.py @@ -981,26 +981,14 @@ def autodoist_magic(args, api, connection): # Get all todoist info try: - projects = api.get_projects() # To save on request to stay under the limit - # all_sections = api.get_sections() # To save on request to stay under the limit + all_projects = api.get_projects() # To save on request to stay under the limit + all_sections = api.get_sections() # To save on request to stay under the limit all_tasks = api.get_tasks() - # Build a dict of all project and task numbers - all_projects = [x.project_id for x in all_tasks] - all_sections = [x.section_id for x in all_tasks] - h = defaultdict(list) - for k, v in zip(all_projects, all_sections): - h[k].append(v) - dict_project_section = dict(h) - - # Store only unique values: - for key in dict_project_section: - dict_project_section[key] = list(set(dict_project_section[key])) - except Exception as error: print(error) - for project in projects: + for project in all_projects: # Skip processing inbox as intended feature if project.is_inbox_project: @@ -1023,7 +1011,7 @@ def autodoist_magic(args, api, connection): # Get all tasks for the project try: - project_tasks = api.get_tasks(project_id=project.id) # TODO: call them all once, and use filter here instead. + project_tasks = [t for t in all_tasks if t.project_id == project.id] except Exception as error: print(error) @@ -1054,7 +1042,7 @@ def autodoist_magic(args, api, connection): # Get all sections and add the 'None' section too. try: - sections = api.get_sections(project_id=project.id) # TODO: call them all once, and use filter here instead. + sections = [s for s in all_sections if s.project_id == project.id] sections.insert(0, Section(None, None, 0, project.id)) except Exception as error: print(error) From a66905e88c570738b9e3681c7c94a66f0313e9b0 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 15 Jan 2023 11:02:26 +0100 Subject: [PATCH 39/50] Only sync changes if api.queue has content. Else we'll time out after 45 minutes of no changes. --- autodoist.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/autodoist.py b/autodoist.py index 622d5e1..0d33aa8 100644 --- a/autodoist.py +++ b/autodoist.py @@ -1523,7 +1523,8 @@ def main(): overview_task_labels) # Sync all queued up changes - sync(api) + if api.queue: + sync(api) num_changes = len(api.queue)+len(api.overview_updated_ids) From a4f99fa0c1757424a5e7b97cfb971a15b47b1351 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 15 Jan 2023 13:53:28 +0100 Subject: [PATCH 40/50] Minor clean-up of code --- autodoist.py | 62 +++++++++++++++++++++------------------------------- 1 file changed, 25 insertions(+), 37 deletions(-) diff --git a/autodoist.py b/autodoist.py index 0d33aa8..5e7904e 100644 --- a/autodoist.py +++ b/autodoist.py @@ -5,7 +5,6 @@ from todoist_api_python.models import Section from todoist_api_python.models import Project from todoist_api_python.http_requests import get -from todoist_api_python.http_requests import post from urllib.parse import urljoin from urllib.parse import quote import sys @@ -19,8 +18,6 @@ import os import re import json -from collections import defaultdict - # Connect to SQLite database @@ -365,18 +362,13 @@ def initialise_api(args): # Run the initial sync logging.debug('Connecting to the Todoist API') - api_arguments = {'token': args.api_key} - - if args.nocache: - logging.debug('Disabling local caching') - api_arguments['cache'] = None - api = TodoistAPI(**api_arguments) logging.info("Autodoist has successfully connected to Todoist!") sync_api = initialise_sync_api(api) - api.sync_token = sync_api['sync_token'] # Save SYNC API token to enable partial syncs + # Save SYNC API token to enable partial syncs + api.sync_token = sync_api['sync_token'] # Check if labels exist @@ -452,7 +444,8 @@ def initialise_sync_api(api): } data = 'sync_token=*&resource_types=["all"]' - response = requests.post('https://api.todoist.com/sync/v9/sync', headers=headers, data=data) + response = requests.post( + 'https://api.todoist.com/sync/v9/sync', headers=headers, data=data) return json.loads(response.text) @@ -507,7 +500,8 @@ def sync(api): 'Content-Type': 'application/x-www-form-urlencoded', } - data = 'sync_token=' + api.sync_token + '&commands=' + json.dumps(api.queue) + data = 'sync_token=' + api.sync_token + \ + '&commands=' + json.dumps(api.queue) response = requests.post( 'https://api.todoist.com/sync/v9/sync', headers=headers, data=data) @@ -646,7 +640,7 @@ def get_task_type(args, connection, task, section, project): # Logic to track addition of a label to a task -def add_label(connection, task, dominant_type, label, overview_task_ids, overview_task_labels): +def add_label(task, label, overview_task_ids, overview_task_labels): if label not in task.labels: labels = task.labels # To also copy other existing labels logging.debug('Updating \'%s\' with label', task.content) @@ -981,8 +975,8 @@ def autodoist_magic(args, api, connection): # Get all todoist info try: - all_projects = api.get_projects() # To save on request to stay under the limit - all_sections = api.get_sections() # To save on request to stay under the limit + all_projects = api.get_projects() # To save on request to stay under the limit + all_sections = api.get_sections() # To save on request to stay under the limit all_tasks = api.get_tasks() except Exception as error: @@ -1011,7 +1005,8 @@ def autodoist_magic(args, api, connection): # Get all tasks for the project try: - project_tasks = [t for t in all_tasks if t.project_id == project.id] + project_tasks = [ + t for t in all_tasks if t.project_id == project.id] except Exception as error: print(error) @@ -1025,12 +1020,6 @@ def autodoist_magic(args, api, connection): db_update_value(connection, task, 'parent_type', None) # Run for both non-sectioned and sectioned tasks - - # Get completed tasks: - # endpoint = 'https://api.todoist.com/sync/v9/completed/get_all' - # get(api._session, endpoint, api._token, '0')['items'] - # $ curl https://api.todoist.com/sync/v9/sync-H "Authorization: Bearer e2f750b64e8fc06ae14383d5e15ea0792a2c1bf3" -d commands='[ {"type": "item_add", "temp_id": "63f7ed23-a038-46b5-b2c9-4abda9097ffa", "uuid": "997d4b43-55f1-48a9-9e66-de5785dfd69b", "args": {"content": "Buy Milk", "project_id": "2203306141","labels": ["Food", "Shopping"]}}]' - # for s in [0,1]: # if s == 0: # sections = Section(None, None, 0, project.id) @@ -1223,6 +1212,7 @@ def autodoist_magic(args, api, connection): # Inherit project type dominant_type = project_type + # TODO: optimise below code # If indicated on project level if dominant_type[0] == 's': if not first_found[0]: @@ -1230,7 +1220,7 @@ def autodoist_magic(args, api, connection): if dominant_type[1] == 's': if not first_found[1]: add_label( - connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + task, next_action_label, overview_task_ids, overview_task_labels) elif next_action_label in task.labels: # Probably the task has been manually moved, so if it has a label, let's remove it. @@ -1239,14 +1229,14 @@ def autodoist_magic(args, api, connection): elif dominant_type[1] == 'p': add_label( - connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + task, next_action_label, overview_task_ids, overview_task_labels) elif dominant_type[0] == 'p': if dominant_type[1] == 's': if not first_found[1]: add_label( - connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + task, next_action_label, overview_task_ids, overview_task_labels) elif next_action_label in task.labels: # Probably the task has been manually moved, so if it has a label, let's remove it. @@ -1255,13 +1245,13 @@ def autodoist_magic(args, api, connection): elif dominant_type[1] == 'p': add_label( - connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + task, next_action_label, overview_task_ids, overview_task_labels) # If indicated on section level if dominant_type[0] == 'x' and dominant_type[1] == 's': if not first_found[1]: add_label( - connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + task, next_action_label, overview_task_ids, overview_task_labels) elif next_action_label in task.labels: # Probably the task has been manually moved, so if it has a label, let's remove it. @@ -1269,14 +1259,14 @@ def autodoist_magic(args, api, connection): task, next_action_label, overview_task_ids, overview_task_labels) elif dominant_type[0] == 'x' and dominant_type[1] == 'p': - add_label(connection, task, dominant_type, next_action_label, + add_label(task, next_action_label, overview_task_ids, overview_task_labels) # If indicated on parentless task level if dominant_type[1] == 'x' and dominant_type[2] == 's': if not first_found[1]: add_label( - connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + task, next_action_label, overview_task_ids, overview_task_labels) if next_action_label in task.labels: # Probably the task has been manually moved, so if it has a label, let's remove it. @@ -1284,7 +1274,7 @@ def autodoist_magic(args, api, connection): task, next_action_label, overview_task_ids, overview_task_labels) elif dominant_type[1] == 'x' and dominant_type[2] == 'p': - add_label(connection, task, dominant_type, next_action_label, + add_label(task, next_action_label, overview_task_ids, overview_task_labels) # If a parentless or sub-task which has children @@ -1326,7 +1316,7 @@ def autodoist_magic(args, api, connection): # Pass label down to the first child if not child_task.is_completed and next_action_label in task.labels: add_label( - connection, child_task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + child_task, next_action_label, overview_task_ids, overview_task_labels) remove_label( task, next_action_label, overview_task_ids, overview_task_labels) @@ -1346,7 +1336,7 @@ def autodoist_magic(args, api, connection): if not child_task.is_completed: add_label( - connection, child_task, dominant_type, next_action_label, overview_task_ids, overview_task_labels) + child_task, next_action_label, overview_task_ids, overview_task_labels) # Remove labels based on start / due dates @@ -1432,7 +1422,6 @@ def autodoist_magic(args, api, connection): first_found[1] = True # Mark first found section with tasks in project (to account for None section) - # TODO: is this always true? What about starred tasks? if next_action_label is not None and first_found[0] == False and section_tasks: first_found[0] = True @@ -1455,7 +1444,7 @@ def main(): parser.add_argument( '-l', '--label', help='enable next action labelling. Define which label to use.', type=str) parser.add_argument( - '-r', '--regeneration', help='enable regeneration of sub-tasks in recurring lists. Chose overall mode: 0 - regen off, 1 - regen all (default), 2 - regen only if all sub-tasks are completed. Task labels can be used to overwrite this mode.', nargs='?', const='1', default=None, type=int) + '-r', '--regeneration', help='[CURRENTLY DISABLED FEATURE] enable regeneration of sub-tasks in recurring lists. Chose overall mode: 0 - regen off, 1 - regen all (default), 2 - regen only if all sub-tasks are completed. Task labels can be used to overwrite this mode.', nargs='?', const='1', default=None, type=int) 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( @@ -1465,13 +1454,11 @@ def main(): parser.add_argument( '-s', '--s_suffix', help='change suffix for sequential labeling (default "-").', default='-') parser.add_argument( - '-df', '--dateformat', help='strptime() format of starting date (default "%%d-%%m-%%Y").', default='%d-%m-%Y') + '-df', '--dateformat', help='[CURRENTLY DISABLED FEATURE] 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 debugging and store detailed to a log file.', action='store_true') parser.add_argument('--inbox', help='the method the Inbox should be processed with.', @@ -1492,6 +1479,7 @@ def main(): else: log_level = logging.INFO + # Set logging config settings logging.basicConfig(level=log_level, format='%(asctime)s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S', From 8b23fbd59a5a57e425d0b2681e4bf3cc58d0df56 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 15 Jan 2023 14:05:16 +0100 Subject: [PATCH 41/50] Final preparations for v2.0 --- autodoist.py | 10 +++++----- requirements.txt | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/autodoist.py b/autodoist.py index 5e7904e..1f865d4 100644 --- a/autodoist.py +++ b/autodoist.py @@ -303,7 +303,7 @@ def verify_label_existance(api, label_name, prompt_mode): try: api.add_label(name=label_name) except Exception as error: - print(error) + logging.warning(error) labels = api.get_labels() label = [x for x in labels if x.name == label_name] @@ -980,7 +980,7 @@ def autodoist_magic(args, api, connection): all_tasks = api.get_tasks() except Exception as error: - print(error) + logging.error(error) for project in all_projects: @@ -1008,7 +1008,7 @@ def autodoist_magic(args, api, connection): project_tasks = [ t for t in all_tasks if t.project_id == project.id] except Exception as error: - print(error) + logging.warning(error) # If a project type has changed, clean all tasks in this project for good measure if next_action_label is not None: @@ -1034,7 +1034,7 @@ def autodoist_magic(args, api, connection): sections = [s for s in all_sections if s.project_id == project.id] sections.insert(0, Section(None, None, 0, project.id)) except Exception as error: - print(error) + logging.debug(error) # Reset first_found[0] = False @@ -1434,7 +1434,7 @@ def autodoist_magic(args, api, connection): def main(): # Version - current_version = 'v1.5' + current_version = 'v2.0' # Main process functions. parser = argparse.ArgumentParser( diff --git a/requirements.txt b/requirements.txt index 557e7ae..1fbd608 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -requests>=2.28.1 -todoist_api_python>=2.0.2 +requests==2.28.1 +todoist_api_python==2.0.2 From d6d1ba121f7ba1083208ba6857064dc8fe98e3a2 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 15 Jan 2023 15:42:17 +0100 Subject: [PATCH 42/50] Update README to v2.0.0 functionality. --- README.md | 72 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 2c6e38b..1bf31e2 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This program adds four major functionalities to Todoist to help automate your wo - Limit labels based on a start-date or hide future tasks based on the due date 2) Enable regeneration of sub-tasks in lists with a recurring date. Multiple modes possile. 3) Postpone the end-of-day time to after midnight to finish your daily recurring tasks -4) Make multiple items (un)checkable at the same time +4) Make multiple tasks (un)checkable at the same time If this tool helped you out, I would really appreciate your support by providing me with some coffee! @@ -29,11 +29,11 @@ For your convenience a requirements.txt is provided, which allows you to install # 1. Automatic next action labels -The program looks for pre-defined tags in the name of every project, section, or parentless tasks in your Todoist account to automatically add and remove `@next_action` labels. To create a simple list of all your next actions you can add a new filter in your Todoist with e.g.: @next_action & #project_name. +The program looks for pre-defined tags in the name of every project, section, or parentless tasks in your Todoist account to automatically add and remove `@next_action` labels. Projects, sections, and parentless tasks can be tagged independently from each other to create the required functionality. If this tag is not defined, it will not activate this functionality. The result will be a clear, current and comprehensive list of next actions without the need for further thought. -See the example given at [running Autodoist](#running-autodoist) on how to run this mode. If the label does not exist yet in your Todoist, a possibility is given to automatically create it. Todoist Premium is required in order to use labels and to make this functionality possible. +See the example given at [running Autodoist](#running-autodoist) on how to run this mode. If the label does not exist yet in your Todoist, a possibility is given to automatically create it. ## Useful filter tip @@ -43,33 +43,57 @@ For a more GTD-like workflow, you can use Todoist filters to create a clean and ## Sequential processing -If a project, section, or parentless task ends with `--`, both the parentless tasks and its sub-tasks will be treated as a priority queue and the most important will be labeled. Importance is determined by order in the list. +If a project, section, or parentless task ends with `-`, the tasks will be treated as a priority queue, where only the first task that is found is labeled. If a task contains sub-tasks, the first lowest task is labeled instead. +[UPDATE FIGURE] ![Serial task](https://i.imgur.com/SUkhPiE.gif) ## Parallel processing -If a project, section, or parentless task name ends with `//`, both the parentless tasks and its sub-tasks will be treated as parallel. A waterfall processing is applied, where the lowest possible sub-tasks are labelled. +If a project, section, or parentless task name ends with `=`, all tasks will be treated in parallel. A waterfall processing is applied, where the lowest possible (sub-)tasks are labelled. +[UPDATE FIGURE] ![Parallel task](https://i.imgur.com/NPTLQ8B.gif) ## Advanced labelling -If a project or section ends with `-/`, all parentless tasks are processed sequentially, and its sub-tasks in parallel. +Projects, sections, and (parentless) tasks can be used to specify how the levels under them should behave. This means that: -[See example](https://i.imgur.com/uGJFeXB.gif) +- A project can accept up to three tags, to specify how the sections, parentless tasks, and subtasks should behave. +- A section can accept up to two tags, to specify parentless tasks and subtasks should behave. +- A task at any level can be labelled with one tag, to specifcy how its sub-tasks should behave. -If a project or section ends with `/-`, all parentless tasks are processed in parallel, and its sub-tasks sequentially. +Tags can be applied one each level simultaneously , where the lower level setting will always override the one specified in the levels above. -[See example](https://i.imgur.com/5lZ1BVI.gif) +### Shorthand notation -Any parentless task can also be be given a type by appending `//` or `--` to the name of the task. This works if there is no project type, and will override a previously defined project type. +If fewer tags then needed are specified, the last one is simply copied. E.g. if a project has the tag `=` this is similar to `===`, or if a project has `=-` this is similar to `=--`. Same for sections, `=` is similar to `==`. -[See example 1 with a parallel project](https://i.imgur.com/d9Qfq0v.gif) +### Project labeling examples +- If a project ends with `---`, only the first section has tasks that are handled sequentially. +- If a project ends with `=--`, all sections have tasks that are handled sequentially. +- If a project ends with `-=-`, only the first section has parallel parentless tasks with sequential sub-tasks. +- If a project ends with `--=`, only the first section and first parentless tasks has parallel sub-tasks. +- If a project ends with `==-`, all sections and all parentless tasks will have sub-tasks are handled sequentially. +- If a project ends with `=-=`, all sections will have parentless tasks that are processed sequentially, but all sub-tasks are handled in parallel. +- If a project ends with `-==`, only the first section has parallel tasks. +- If a project ends with `===`, all tasks are handled in parallel. -[See example 2 with a serial project](https://i.imgur.com/JfaAOzZ.gif) +### Section labeling examples +- If a section ends with `--`, only the first parentless task will have sub-tasks that are handled sequentially. +- If a section ends with `=-`, all parentless tasks will have sub-tasks that are handled sequentially. +- If a section ends with `-=`, only the first parentless task has sub-tasks that are handled in parallel. +- If a section ends with `==`, all tasks are handled in parallel. + +### Tasks labeling examples +- If a task ends with `-`, the sub-tasks are handled sequentially. +- If a task ends with `=`, the sub-tasks are handled in parallel. + +### Kanban board labeling +A standard workflow for Kanban boards is to have one actionable task per column/section, which is then moved to the next column when needed. Most often the most right column is the 'done' section. To ensure that every column only has one labelled task and the last column has contains no labelled tasks, you could do either of two things: +- Add the `=--` tag to the project name, and disable labelling for the 'done' section by adding `*` to either the start or end of the section name. +- Add the `--` tag to every section that you want to have labels. -Note: Todoist sections don't like to have a slash in the name, it will automatically change to an underscore. The default label options will recognize this to make it work regardless. Of course you're always free to define your own custom label symbols. ## Start/Due date enhanced experience @@ -83,7 +107,13 @@ Two methods are provided to hide tasks that are not relevant yet. # 2. Regenerate sub-tasks in recurring lists -The program looks for all parentless tasks with a recurring date. If they contain sub-tasks, they will be regenerated in the same order when the parentless task is checked. Todoist Premium is not required for this functionality. +*DISCLAIMER: This feature has been disabled for now due to two reasons:* +- *Regeneration is a [core feature of Todoist nowadays](https://todoist.com/help/articles/can-i-reset-sub-tasks). This was made possible thanks to all of you who are using and supporting Autodoist, which resulted in Doist to include this too! Thank you all for making this happen!* +- *Due to the change in the REST API v2.0 it's not possible to see completed tasks, which makes regeneration a bit difficult.* + +*Nevertheless, the Todoist implementation is still more limited than Autodoist, it does not restore the original order of the sub-tasks, and deeper sub-tasks can't be reset. I therefore believe it is still useful for this feature to be re-enabled in the near future.* + +Autodoist looks for all parentless tasks with a recurring date. If they contain sub-tasks, they will be regenerated in the same order when the parentless task is checked. ![See example](https://i.imgur.com/WKKd14o.gif) @@ -100,15 +130,15 @@ In addition you can override the overall mode by adding the labels `Regen_off`, You have a daily recurring task, but you're up working late and now it's past midnight. When this happens Todoist will automatically mark it overdue, and when checked by you it moves to tomorrow. This means that after a good nights rest you can't complete the task that day! -By setting an alternative time for the end-of-day you can now finish your work after midnight and the new date will automatically be corrected for you. Todoist Premium is not required for this functionality. +By setting an alternative time for the end-of-day you can now finish your work after midnight and the new date will automatically be corrected for you. ![See example 1](https://i.imgur.com/tvnTMOJ.gif) -# 4. Make multiple items uncheckable / re-checkable at the same time +# 4. Make multiple tasks uncheckable / re-checkable at the same time Todoist allows the asterisk symbol `* ` to be used to ensure tasks can't be checked by turning them into headers. Now you are able to do this en masse! -Simply add `** ` or `!* ` in front of a project, section, or top item, to automatically turn all the items that it includes into respectively headers or checkable tasks. +Simply add `** ` or `-* ` in front of a project, section, or parentless task to automatically turn all the tasks that it includes into respectively headers or checkable tasks. # Executing Autodoist @@ -136,10 +166,12 @@ These modes can be run individually, or combined with each other. Several additional arguments can be provided, for example to change the suffix tags for parallel and sequential projects: - python autodoist.py --pp_suffix - python autodoist.py --ss_suffix + python autodoist.py --p_suffix + python autodoist.py --s_suffix + +Note: Be aware that Todoist sections don't like to have a slash '/' in the name, which will automatically change to an underscore. Detection of the tag will not work. -Or if you want to hide all tasks due in the future: +If you want to hide all tasks due in the future: python autodoist.py --hf From 565967cdc15bbaa0aca3eaa4129309959b93f122 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 15 Jan 2023 15:44:37 +0100 Subject: [PATCH 43/50] Update README to v2.0.0 functionality. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1bf31e2..513109f 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ If fewer tags then needed are specified, the last one is simply copied. E.g. if - If a task ends with `=`, the sub-tasks are handled in parallel. ### Kanban board labeling -A standard workflow for Kanban boards is to have one actionable task per column/section, which is then moved to the next column when needed. Most often the most right column is the 'done' section. To ensure that every column only has one labelled task and the last column has contains no labelled tasks, you could do either of two things: +A standard workflow for Kanban boards is to have one actionable task per column/section, which is then moved to the next column when needed. Most often the most right column is the 'done' section. To ensure that every column only has one labelled task and the last column contains no labelled tasks, you could do either of two things: - Add the `=--` tag to the project name, and disable labelling for the 'done' section by adding `*` to either the start or end of the section name. - Add the `--` tag to every section that you want to have labels. From 78dca0483ea8a34942cd3aa0dc093068055116ae Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 15 Jan 2023 15:48:22 +0100 Subject: [PATCH 44/50] Update README to v2.0.0 functionality. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 513109f..1482769 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ Two methods are provided to hide tasks that are not relevant yet. *DISCLAIMER: This feature has been disabled for now due to two reasons:* - *Regeneration is a [core feature of Todoist nowadays](https://todoist.com/help/articles/can-i-reset-sub-tasks). This was made possible thanks to all of you who are using and supporting Autodoist, which resulted in Doist to include this too! Thank you all for making this happen!* -- *Due to the change in the REST API v2.0 it's not possible to see completed tasks, which makes regeneration a bit difficult.* +- *In the new REST API v2 it's currently not possible to see completed tasks, which makes regeneration a bit difficult.* *Nevertheless, the Todoist implementation is still more limited than Autodoist, it does not restore the original order of the sub-tasks, and deeper sub-tasks can't be reset. I therefore believe it is still useful for this feature to be re-enabled in the near future.* From 8fe99f49428033973c4c4d2a3d55ec03a1bcf419 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 15 Jan 2023 15:50:00 +0100 Subject: [PATCH 45/50] Update README to v2.0.0 functionality. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1482769..686ebd0 100644 --- a/README.md +++ b/README.md @@ -43,14 +43,14 @@ For a more GTD-like workflow, you can use Todoist filters to create a clean and ## Sequential processing -If a project, section, or parentless task ends with `-`, the tasks will be treated as a priority queue, where only the first task that is found is labeled. If a task contains sub-tasks, the first lowest task is labeled instead. +If a project, section, or parentless task ends with a dash `-`, the tasks will be treated suquentially in a priority queue, where only the first task that is found is labeled. If a task contains sub-tasks, the first lowest task is labeled instead. [UPDATE FIGURE] ![Serial task](https://i.imgur.com/SUkhPiE.gif) ## Parallel processing -If a project, section, or parentless task name ends with `=`, all tasks will be treated in parallel. A waterfall processing is applied, where the lowest possible (sub-)tasks are labelled. +If a project, section, or parentless task name ends with an equal sign `=`, all tasks will be treated in parallel. A waterfall processing is applied, where the lowest possible (sub-)tasks are labelled. [UPDATE FIGURE] ![Parallel task](https://i.imgur.com/NPTLQ8B.gif) From 956f5176afcf6b882172f5cff695c48715da6f70 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 15 Jan 2023 16:38:34 +0100 Subject: [PATCH 46/50] Final preparations for v2.0 --- autodoist.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/autodoist.py b/autodoist.py index 1f865d4..0801213 100644 --- a/autodoist.py +++ b/autodoist.py @@ -444,8 +444,11 @@ def initialise_sync_api(api): } data = 'sync_token=*&resource_types=["all"]' - response = requests.post( - 'https://api.todoist.com/sync/v9/sync', headers=headers, data=data) + try: + response = requests.post( + 'https://api.todoist.com/sync/v9/sync', headers=headers, data=data) + except Exception as e: + logging.error(f"Error during initialise_sync_api: '{e}'") return json.loads(response.text) From 2ce81df5bde41cfbe1455f6d67ed69ed05c95cc4 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 15 Jan 2023 18:10:43 +0100 Subject: [PATCH 47/50] Update README to v2.0 functionalities --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 686ebd0..8c2ff17 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Autodoist +*Note: v2.0 is a major overhaul of Autodoist, so please be sure to view the README in order to get up to speed with the latest changes. Thanks to everyone for helping out and supporting this project!* + This program adds four major functionalities to Todoist to help automate your workflow: 1) Assign automatic next-action labels for a more GTD-like workflow @@ -45,15 +47,13 @@ For a more GTD-like workflow, you can use Todoist filters to create a clean and If a project, section, or parentless task ends with a dash `-`, the tasks will be treated suquentially in a priority queue, where only the first task that is found is labeled. If a task contains sub-tasks, the first lowest task is labeled instead. -[UPDATE FIGURE] -![Serial task](https://i.imgur.com/SUkhPiE.gif) +![Sequential task labeling](https://i.imgur.com/ZUKbA8E.gif) ## Parallel processing If a project, section, or parentless task name ends with an equal sign `=`, all tasks will be treated in parallel. A waterfall processing is applied, where the lowest possible (sub-)tasks are labelled. -[UPDATE FIGURE] -![Parallel task](https://i.imgur.com/NPTLQ8B.gif) +![Parallel task labeling](https://i.imgur.com/xZZ0kEM.gif) ## Advanced labelling From 8a1bc1d5ec2af407fb367fa96b2de2945fdabfbe Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 15 Jan 2023 21:55:30 +0100 Subject: [PATCH 48/50] Final preparations for v2.0 --- autodoist.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/autodoist.py b/autodoist.py index 0801213..720a9c8 100644 --- a/autodoist.py +++ b/autodoist.py @@ -362,13 +362,19 @@ def initialise_api(args): # Run the initial sync logging.debug('Connecting to the Todoist API') - api_arguments = {'token': args.api_key} - api = TodoistAPI(**api_arguments) - logging.info("Autodoist has successfully connected to Todoist!") + try: + api_arguments = {'token': args.api_key} + api = TodoistAPI(**api_arguments) + sync_api = initialise_sync_api(api) + # Save SYNC API token to enable partial syncs + api.sync_token = sync_api['sync_token'] + + except Exception as e: + logging.error( + f"Could not connect to Todoist: '{e}'") + exit(0) - sync_api = initialise_sync_api(api) - # Save SYNC API token to enable partial syncs - api.sync_token = sync_api['sync_token'] + logging.info("Autodoist has successfully connected to Todoist!") # Check if labels exist From a560a598577c6f7231e5b5353595465135bc23c4 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 15 Jan 2023 22:00:00 +0100 Subject: [PATCH 49/50] Final preparations for v2.0 --- autodoist.py | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/autodoist.py b/autodoist.py index 7c974c3..957fa9d 100644 --- a/autodoist.py +++ b/autodoist.py @@ -368,7 +368,7 @@ def initialise_api(args): sync_api = initialise_sync_api(api) # Save SYNC API token to enable partial syncs api.sync_token = sync_api['sync_token'] - + except Exception as e: logging.error( f"Could not connect to Todoist: '{e}'") diff --git a/setup.py b/setup.py index 084b741..860b26e 100644 --- a/setup.py +++ b/setup.py @@ -2,12 +2,12 @@ setup( name='autodoist', - version='1.5', + version='2.0', py_modules=['autodoist'], url='https://github.com/Hoffelhas/automation-todoist', license='MIT', author='Alexander Haselhoff', - author_email='alexander.haselhoff@outlook.com', + author_email='xela@live.nl', description='Added functionality for Todoist: 1) next-action labels, 2) sub-task regeneration, 3) postpone end of day, and 4) (un)header items simultaneously)', install_requires=[ 'todoist-python', From 6a0b64f256598429cd6231d8fe66d7ef6cb7c682 Mon Sep 17 00:00:00 2001 From: Hoffelhas Date: Sun, 15 Jan 2023 22:11:11 +0100 Subject: [PATCH 50/50] Update README v2.0 --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 2147073..6947609 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ This program adds four major functionalities to Todoist to help automate your workflow: -1) Assign automatic next-action labels for a more GTD-like workflow +1) Assign automatic `@next_action` labels for a more GTD-like workflow - Flexible options to label tasks sequentially or in parallel - Limit labels based on a start-date or hide future tasks based on the due date -2) Enable regeneration of sub-tasks in lists with a recurring date. Multiple modes possile. +2) [Temporary disabled] Enable regeneration of sub-tasks in lists with a recurring date. Multiple modes possible. 3) Postpone the end-of-day time to after midnight to finish your daily recurring tasks 4) Make multiple tasks (un)checkable at the same time @@ -19,7 +19,7 @@ If this tool helped you out, I would really appreciate your support by providing # Requirements -Autodoist has been build with Python 3.9.1, which is the recommended version. Older versions of 3.x should be compatible, however be aware that they have not been tested. +Autodoist has been build with Python 3.11.1, which is the recommended version. Older versions of 3.x should be compatible, however be aware that they have not been tested. To run Autodoist the following packages are required: * ```todoist-python``` @@ -33,19 +33,19 @@ For your convenience a requirements.txt is provided, which allows you to install The program looks for pre-defined tags in the name of every project, section, or parentless tasks in your Todoist account to automatically add and remove `@next_action` labels. -Projects, sections, and parentless tasks can be tagged independently from each other to create the required functionality. If this tag is not defined, it will not activate this functionality. The result will be a clear, current and comprehensive list of next actions without the need for further thought. +Projects, sections, and parentless tasks can be tagged independently of each other to create the required functionality. If this tag is not defined, it will not activate this functionality. The result will be a clear, current and comprehensive list of next actions without the need for further thought. See the example given at [running Autodoist](#running-autodoist) on how to run this mode. If the label does not exist yet in your Todoist, a possibility is given to automatically create it. ## Useful filter tip -For a more GTD-like workflow, you can use Todoist filters to create a clean and cohesive list that only contains your actionable tasks. As a simple example you could use the following filter: +For a more GTD-like workflow, you can use Todoist filters to create a clean and cohesive list that only contains your actionable tasks. As a simple example, you could use the following filter: `@next_action & #PROJECT_NAME` ## Sequential processing -If a project, section, or parentless task ends with a dash `-`, the tasks will be treated suquentially in a priority queue, where only the first task that is found is labeled. If a task contains sub-tasks, the first lowest task is labeled instead. +If a project, section, or parentless task ends with a dash `-`, the tasks will be treated sequentially in a priority queue, where only the first task that is found is labeled. If a task contains sub-tasks, the first lowest task is labeled instead. ![Sequential task labeling](https://i.imgur.com/ZUKbA8E.gif) @@ -61,9 +61,9 @@ Projects, sections, and (parentless) tasks can be used to specify how the levels - A project can accept up to three tags, to specify how the sections, parentless tasks, and subtasks should behave. - A section can accept up to two tags, to specify parentless tasks and subtasks should behave. -- A task at any level can be labelled with one tag, to specifcy how its sub-tasks should behave. +- A task at any level can be labelled with one tag, to specify how its sub-tasks should behave. -Tags can be applied one each level simultaneously , where the lower level setting will always override the one specified in the levels above. +Tags can be applied on each level simultaneously, where the lower level setting will always override the one specified in the levels above. ### Shorthand notation @@ -90,7 +90,7 @@ If fewer tags then needed are specified, the last one is simply copied. E.g. if - If a task ends with `=`, the sub-tasks are handled in parallel. ### Kanban board labeling -A standard workflow for Kanban boards is to have one actionable task per column/section, which is then moved to the next column when needed. Most often the most right column is the 'done' section. To ensure that every column only has one labelled task and the last column contains no labelled tasks, you could do either of two things: +A standard workflow for Kanban boards is to have one actionable task per column/section, which is then moved to the next column when needed. Most often, the most right column is the 'done' section. To ensure that every column only has one labelled task and the last column contains no labelled tasks, you could do either of two things: - Add the `=--` tag to the project name, and disable labelling for the 'done' section by adding `*` to either the start or end of the section name. - Add the `--` tag to every section that you want to have labels. @@ -99,7 +99,7 @@ A standard workflow for Kanban boards is to have one actionable task per column/ Two methods are provided to hide tasks that are not relevant yet. -- Prevent labels by defining a start-date that is added to the task itself. The label is only assigned if this date is reached. You can define the start-date by adding 'start=DD-MM-YYYY'. On the other hand the start date can be defined as several days or weeks before the due-date by using either 'start=due-d' or 'start=due-w'. This is especially useful for recurring tasks! +- Prevent labels by defining a start-date that is added to the task itself. The label is only assigned if this date is reached. You can define the start-date by adding 'start=DD-MM-YYYY'. On the other hand, the start date can be defined as several days or weeks before the due-date by using either 'start=due-d' or 'start=due-w'. This is especially useful for recurring tasks! [See an example of using start-dates](https://i.imgur.com/WJRoJzW.png). - Prevent labels of all tasks if the due date is too far in the future. Define the amount by running with the argument '-hf '. @@ -124,11 +124,11 @@ To give you more flexibility, multiple modes are provided: When this functionality is activated, it is possible to chose which mode is used as overall functionality for your Todoist. See the example given at [running Autodoist](#running-autodoist). -In addition you can override the overall mode by adding the labels `Regen_off`, `Regen_all`, or `Regen_all_if_completed` to one of your main recurrings task. These labels will automatically be created for you. +In addition you can override the overall mode by adding the labels `Regen_off`, `Regen_all`, or `Regen_all_if_completed` to one of your main recurring task. These labels will automatically be created for you. # 3. Postpone the end-of-day -You have a daily recurring task, but you're up working late and now it's past midnight. When this happens Todoist will automatically mark it overdue, and when checked by you it moves to tomorrow. This means that after a good nights rest you can't complete the task that day! +You have a daily recurring task, but you're up working late and now it's past midnight. When this happens, Todoist will automatically mark it overdue and when checked by you it moves to tomorrow. This means that after a good night's rest you can't complete the task that day! By setting an alternative time for the end-of-day you can now finish your work after midnight and the new date will automatically be corrected for you. @@ -152,7 +152,7 @@ If you want to enable labelling mode, run with the `-l` argument: python autodoist.py -a -l -If you want to enable regeneration of sub-tasks in recurring lists, run with the `-r` argument followed by a mode number for the overall functionality (1: no regeneration, 2: regenerate all, 3: regenerate ony if all sub-tasks are completed): +If you want to enable regeneration of sub-tasks in recurring lists, run with the `-r` argument followed by a mode number for the overall functionality (1: no regeneration, 2: regenerate all, 3: regenerate only if all sub-tasks are completed): python autodoist.py -a -r