Basic SQLite DB functionality now working. Next: rewrite 'get_task_type()' function, and start some refactoring/debugging.

pull/30/head
Hoffelhas 2023-01-03 16:06:46 +01:00
parent 2625547002
commit 374d216a83
1 changed files with 549 additions and 324 deletions

View File

@ -1,6 +1,9 @@
#!/usr/bin/python3 #!/usr/bin/python3
from todoist_api_python.api import TodoistAPI 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 sys
import time import time
import requests import requests
@ -10,6 +13,212 @@ from datetime import datetime, timedelta
import time import time
import sqlite3 import sqlite3
from sqlite3 import Error 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 # Makes --help text wider
@ -75,6 +284,7 @@ def query_yes_no(question, default="yes"):
# Check if label exists, if not, create it # Check if label exists, if not, create it
def verify_label_existance(api, label_name, prompt_mode): def verify_label_existance(api, label_name, prompt_mode):
# Check the regeneration label exists # Check the regeneration label exists
labels = api.get_labels() labels = api.get_labels()
@ -113,7 +323,9 @@ def verify_label_existance(api, label_name, prompt_mode):
return 0 return 0
# Initialisation of Autodoist # Initialisation of Autodoist
def initialise(args):
def initialise_api(args):
# Check we have a API key # Check we have a API key
if not args.api_key: if not args.api_key:
@ -132,8 +344,9 @@ def initialise(args):
# Check if proper regeneration mode has been selected # Check if proper regeneration mode has been selected
if args.regeneration is not None: if args.regeneration is not None:
if not set([0,1,2]) & set([args.regeneration]): 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.') logging.error(
'Wrong regeneration mode. Please choose a number from 0 to 2. Check --help for more information on the available modes.')
exit(1) exit(1)
# Show which modes are enabled: # Show which modes are enabled:
@ -223,7 +436,10 @@ def check_name(args, name):
len_suffix = [len(args.pp_suffix), len(args.ss_suffix), len_suffix = [len(args.pp_suffix), len(args.ss_suffix),
len(args.ps_suffix), len(args.sp_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 current_type = args.inbox
elif name[-len_suffix[0]:] == args.pp_suffix: elif name[-len_suffix[0]:] == args.pp_suffix:
current_type = 'parallel' current_type = 'parallel'
@ -233,7 +449,7 @@ def check_name(args, name):
current_type = 'p-s' current_type = 'p-s'
elif name[-len_suffix[3]:] == args.sp_suffix: elif name[-len_suffix[3]:] == args.sp_suffix:
current_type = 's-p' 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. # # Workaround for section names, which don't allow '/' symbol.
# elif args.ps_suffix == '/-' and name[-2:] == '_-': # elif args.ps_suffix == '/-' and name[-2:] == '_-':
# current_type = 'p-s' # 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 # 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: 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: except:
# logging.debug('No defined project_type: %s' % str(e)) # logging.debug('No defined project_type: %s' % str(e))
old_type = None old_type = None
try: # model_name = model.name.strip() % TODO: Is this still needed?
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
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 # Check if project type changed with respect to previous run
if old_type == current_type: if old_type == current_type:
type_changed = 0 type_changed = 0
else: else:
type_changed = 1 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 # 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 return current_type, type_changed
@ -286,21 +500,21 @@ def get_type(args, model, key):
# Determine a project type # 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.""" """Identifies how a project should be handled."""
project_type, project_type_changed = get_type( project_type, project_type_changed = get_type(
args, project_model, 'project_type') args, connection, project_model, 'project_type')
return project_type, project_type_changed return project_type, project_type_changed
# Determine a section type # Determine a section type
def get_section_type(args, section_object): def get_section_type(args, connection, section_object):
"""Identifies how a section should be handled.""" """Identifies how a section should be handled."""
if section_object is not None: if section_object is not None:
section_type, section_type_changed = get_type( section_type, section_type_changed = get_type(
args, section_object, 'section_type') args, connection, section_object, 'section_type')
else: else:
section_type = None section_type = None
section_type_changed = 0 section_type_changed = 0
@ -310,18 +524,20 @@ def get_section_type(args, section_object):
# Determine an task type # 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.""" """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: try:
task_type = task.parent_type #TODO: METADATA task_type = task.parent_type # TODO: METADATA
task_type_changed = 1 task_type_changed = 1
task.task_type = task_type task.task_type = task_type #TODO: METADATA
except: 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: 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 return task_type, task_type_changed
@ -367,16 +583,6 @@ def update_labels(api, overview_task_ids, overview_task_labels):
return filtered_overview_ids 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 # Check if header logic needs to be applied
@ -387,7 +593,7 @@ def check_header(level):
method = 0 method = 0
try: try:
# Support for legacy structure # Support for legacy structure #TODO: can probably be removed now due to REST API v2
name = level['name'] name = level['name']
method = 1 method = 1
except: except:
@ -418,6 +624,8 @@ def check_header(level):
return header_all_in_level, unheader_all_in_level return header_all_in_level, unheader_all_in_level
# Logic for applying and removing headers # 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 any([header_all_in_p, header_all_in_s, header_all_in_t]):
if task.content[0] != '*': if task.content[0] != '*':
@ -583,14 +791,17 @@ def run_recurring_lists_logic(args, api, item, child_items, child_items_all, reg
# item.content) # item.content)
pass pass
# Contains all main autodoist functionalities # 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_ids = {}
overview_task_labels = {} overview_task_labels = {}
next_action_label = args.label
regen_labels_id = args.regen_label_names
try: try:
projects = api.get_projects() projects = api.get_projects()
@ -599,6 +810,13 @@ def autodoist_magic(args, api, next_action_label, regen_labels_id):
for project in projects: 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 # To determine if a sequential task was found
first_found_project = False first_found_project = False
@ -608,7 +826,7 @@ def autodoist_magic(args, api, next_action_label, regen_labels_id):
# Get project type # Get project type
if next_action_label is not None: if next_action_label is not None:
project_type, project_type_changed = get_project_type( project_type, project_type_changed = get_project_type(
args, project) args, connection, project)
if project_type is not None: if project_type is not None:
logging.debug('Identified \'%s\' as %s type', logging.debug('Identified \'%s\' as %s type',
@ -616,7 +834,7 @@ def autodoist_magic(args, api, next_action_label, regen_labels_id):
# Get all tasks for the project # Get all tasks for the project
try: try:
project_tasks = api.get_tasks(project_id = project.id) project_tasks = api.get_tasks(project_id=project.id)
except Exception as error: except Exception as error:
print(error) print(error)
@ -627,18 +845,27 @@ def autodoist_magic(args, api, next_action_label, regen_labels_id):
# get(api._session, endpoint, api._token, '0')['items'] # 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"]}}]' # $ 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 [0,1]:
for s in [1]: # if s == 0:
if s == 0: # sections = Section(None, None, 0, project.id)
sections = [create_none_section()] # TODO: Rewrite # elif s == 1:
elif s == 1: # try:
# sections = api.get_sections(project_id=project.id)
# except Exception as error:
# print(error)
# Get all sections and add the 'None' section too.
try: try:
sections = api.get_sections(project_id = project.id) sections = api.get_sections(project_id=project.id)
sections.insert(0, Section(None, None, 0, project.id))
except Exception as error: except Exception as error:
print(error) print(error)
for section in sections: for section in sections:
# Check db existance
db_check_existance(connection, section)
# Check if we need to (un)header entire secion # 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(section)
@ -647,7 +874,7 @@ def autodoist_magic(args, api, next_action_label, regen_labels_id):
# Get section type # Get section type
section_type, section_type_changed = get_section_type( section_type, section_type_changed = get_section_type(
args, section) args, connection, section)
if section_type is not None: if section_type is not None:
logging.debug('Identified \'%s\' as %s type', logging.debug('Identified \'%s\' as %s type',
section.name, section_type) section.name, section_type)
@ -681,6 +908,8 @@ def autodoist_magic(args, api, next_action_label, regen_labels_id):
for task in tasks: for task in tasks:
dominant_type = None # Reset dominant_type = None # Reset
db_check_existance(connection, task)
# Possible nottes routine for the future # Possible nottes routine for the future
# notes = api.notes.all() TODO: Quick notes test to see what the impact is? # 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']] # note_content = [x['content'] for x in notes if x['item_id'] == item['id']]
@ -698,10 +927,10 @@ def autodoist_magic(args, api, next_action_label, regen_labels_id):
header_all_in_t, unheader_all_in_t = check_header(task) header_all_in_t, unheader_all_in_t = check_header(task)
# Modify headers where needed # Modify headers where needed
#TODO: DISABLED FOR NOW, FIX LATER # 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(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 # Logic for recurring lists
# if not args.regeneration: # if not args.regeneration:
# try: # try:
@ -724,12 +953,14 @@ def autodoist_magic(args, api, next_action_label, regen_labels_id):
continue continue
if task.content.startswith('*'): if task.content.startswith('*'):
# Remove next action label if it's still present # Remove next action label if it's still present
remove_label(task, next_action_label, overview_task_ids, overview_task_labels) remove_label(task, next_action_label,
overview_task_ids, overview_task_labels)
continue continue
# Check task type # Check task type
task_type, task_type_changed = get_task_type( task_type, task_type_changed = get_task_type(
args, task, project_type) args, connection, task, section_type, project_type)
if task_type is not None: if task_type is not None:
logging.debug('Identified \'%s\' as %s type', logging.debug('Identified \'%s\' as %s type',
task.content, task_type) task.content, task_type)
@ -783,17 +1014,16 @@ def autodoist_magic(args, api, next_action_label, regen_labels_id):
# If there are children # If there are children
if len(child_tasks) > 0: if len(child_tasks) > 0:
# Check if task state has changed, if so clean children for good measure # Check if task state has changed, if so clean children for good measure
if task_type_changed == 1: if task_type_changed == 1:
[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)
for child_task in child_tasks] for child_task in child_tasks]
# If a sub-task, inherit parent task type # If a sub-task, inherit parent task type
if task.parent_id !=0: if task.parent_id != 0:
try: # dominant_type = task.parent_type # TODO: METADATA
dominant_type = task.parent_type #TODO: METADATA dominant_type = db_read_value(connection, task, 'parent_type')[0][0]
except:
pass
# Process sequential tagged tasks (task_type can overrule project_type) # Process sequential tagged tasks (task_type can overrule project_type)
if dominant_type == 'sequential' or dominant_type == 'p-s': if dominant_type == 'sequential' or dominant_type == 'p-s':
@ -820,13 +1050,17 @@ def autodoist_magic(args, api, next_action_label, regen_labels_id):
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):
remove_label( remove_label(
task, next_action_label, overview_task_ids, overview_task_labels) task, next_action_label, overview_task_ids, overview_task_labels)
db_update_value(task, 'task_type', 'NULL')
for child_task in child_tasks: for child_task in child_tasks:
# Ignore headered children # Ignore headered children
if child_task.content.startswith('*'): if child_task.content.startswith('*'):
continue continue
child_task.parent_type = dominant_type #TODO: METADATA # child_task.parent_type = dominant_type # TODO: METADATA
db_update_value(connection, child_task, 'parent_type', dominant_type)
if not child_task.is_completed: if not child_task.is_completed:
add_label( add_label(
child_task, next_action_label, overview_task_ids, overview_task_labels) child_task, next_action_label, overview_task_ids, overview_task_labels)
@ -923,21 +1157,9 @@ def autodoist_magic(args, api, next_action_label, regen_labels_id):
'Wrong start-date format for task: %s. Please use "start=due-<NUM><d or w>"', task.content) 'Wrong start-date format for task: %s. Please use "start=due-<NUM><d or w>"', task.content)
continue continue
# 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
# 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 # Main
@ -989,7 +1211,7 @@ def main():
args.regen_label_names = ('Regen_off', 'Regen_all', args.regen_label_names = ('Regen_off', 'Regen_all',
'Regen_all_if_completed') 'Regen_all_if_completed')
# Set debug # Set logging
if args.debug: if args.debug:
log_level = logging.DEBUG log_level = logging.DEBUG
else: else:
@ -1007,7 +1229,10 @@ def main():
check_for_update(current_version) check_for_update(current_version)
# Initialise api # Initialise api
api = initialise(args) api = initialise_api(args)
# Initialise SQLite database
connection = initialise_sqlite()
# Start main loop # Start main loop
while True: while True:
@ -1016,7 +1241,7 @@ def main():
# Evaluate projects, sections, and tasks # Evaluate projects, sections, and tasks
overview_task_ids, overview_task_labels = autodoist_magic( overview_task_ids, overview_task_labels = autodoist_magic(
args, api, args.label, args.regen_label_names) args, api, connection)
# Commit next action label changes # Commit next action label changes
if args.label is not None: if args.label is not None: