mirror of https://github.com/Hoffelhas/autodoist
Basic SQLite DB functionality now working. Next: rewrite 'get_task_type()' function, and start some refactoring/debugging.
parent
2625547002
commit
374d216a83
363
autodoist.py
363
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:
|
||||
|
@ -133,7 +345,8 @@ 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.')
|
||||
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:
|
||||
|
@ -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'
|
||||
|
@ -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_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
|
||||
|
||||
|
@ -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,6 +624,8 @@ 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] != '*':
|
||||
|
@ -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,7 +826,7 @@ 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',
|
||||
|
@ -627,18 +845,27 @@ def autodoist_magic(args, api, next_action_label, regen_labels_id):
|
|||
# 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:
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
|
||||
for section in sections:
|
||||
|
||||
# Check db existance
|
||||
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)
|
||||
|
||||
|
@ -647,7 +874,7 @@ def autodoist_magic(args, api, next_action_label, regen_labels_id):
|
|||
|
||||
# Get section type
|
||||
section_type, section_type_changed = get_section_type(
|
||||
args, section)
|
||||
args, connection, section)
|
||||
if section_type is not None:
|
||||
logging.debug('Identified \'%s\' as %s type',
|
||||
section.name, section_type)
|
||||
|
@ -681,6 +908,8 @@ def autodoist_magic(args, api, next_action_label, regen_labels_id):
|
|||
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']]
|
||||
|
@ -724,12 +953,14 @@ def autodoist_magic(args, api, next_action_label, regen_labels_id):
|
|||
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)
|
||||
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)
|
||||
args, connection, task, section_type, project_type)
|
||||
|
||||
if task_type is not None:
|
||||
logging.debug('Identified \'%s\' as %s type',
|
||||
task.content, task_type)
|
||||
|
@ -783,6 +1014,7 @@ def autodoist_magic(args, api, next_action_label, regen_labels_id):
|
|||
|
||||
# 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)
|
||||
|
@ -790,10 +1022,8 @@ def autodoist_magic(args, api, next_action_label, regen_labels_id):
|
|||
|
||||
# If a sub-task, inherit parent task type
|
||||
if task.parent_id != 0:
|
||||
try:
|
||||
dominant_type = task.parent_type #TODO: METADATA
|
||||
except:
|
||||
pass
|
||||
# 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':
|
||||
|
@ -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):
|
||||
remove_label(
|
||||
task, next_action_label, overview_task_ids, overview_task_labels)
|
||||
db_update_value(task, 'task_type', 'NULL')
|
||||
|
||||
for child_task in child_tasks:
|
||||
|
||||
# Ignore headered children
|
||||
if child_task.content.startswith('*'):
|
||||
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:
|
||||
add_label(
|
||||
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)
|
||||
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,7 +1241,7 @@ 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:
|
||||
|
|
Loading…
Reference in New Issue