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 @@ from datetime import datetime, timedelta 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 - try: - model_name = model.name.strip() - 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 + # model_name = model.name.strip() % TODO: Is this still needed? - current_type = check_name(args, model_name) + try: + current_type = check_name(args, model.content) # Tasks + except: + 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,336 +826,340 @@ 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 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) - for section in sections: + # 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) - # Check if we need to (un)header entire secion - header_all_in_s, unheader_all_in_s = check_header(section) + for section in sections: - # To determine if a sequential task was found - first_found_section = False + # Check db existance + db_check_existance(connection, section) - # 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) + # Check if we need to (un)header entire secion + header_all_in_s, unheader_all_in_s = check_header(section) - # Get all tasks for the section - tasks = [x for x in project_tasks if x.section_id - == section.id] + # To determine if a sequential task was found + first_found_section = False - # 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 + # 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) - # 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)) + # Get all tasks for the section + tasks = [x for x in project_tasks if x.section_id + == section.id] - # If a type has changed, clean all task labels for good measure + # 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 + # 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 - # For all tasks in this section - for task in tasks: - dominant_type = None # Reset + # Check task type + task_type, task_type_changed = get_task_type( + args, connection, task, section_type, project_type) - # 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) + if task_type is not None: + logging.debug('Identified \'%s\' as %s type', + task.content, task_type) - # 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)) + # Determine hierarchy types for logic + hierarchy_types = [task_type, + section_type, project_type] + hierarchy_boolean = [type(x) != type(None) + for x in hierarchy_types] - # Check if we need to (un)header entire task tree - header_all_in_t, unheader_all_in_t = check_header(task) + # 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) - # 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) + elif hierarchy_boolean[1]: + # Inherit section type + dominant_type = section_type -#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 + 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 - elif project_type == 'parallel' or project_type == 'p-s': + # 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 + + # 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) - # 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 + # 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 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] + 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 + # 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) + # child_task.parent_type = dominant_type # TODO: METADATA + db_update_value(connection, child_task, 'parent_type', dominant_type) - # Process parallel tagged tasks or untagged parents - elif dominant_type == 'parallel' or (dominant_type == 's-p' and next_action_label in task.labels): + 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) - for child_task in child_tasks: + continue + except: + # Hide-future not set, skip + continue - # Ignore headered children - if child_task.content.startswith('*'): - 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:] - 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) + # 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 - # Remove labels based on start / due dates + except: + logging.warning( + 'Wrong start-date format for task: "%s". Please use "start="', task.content) + continue - # 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 + # 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 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 f_end > -1: + offset = task.content[f+10:f+10+f_end-1] + else: + offset = task.content[f+10:-1] - # 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 + 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 - except: - logging.warning( - 'Wrong start-date format for task: "%s". Please use "start="', 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)) - # 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 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 - 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 + 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 - -# 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 @@ -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)