2020-05-09 11:08:53 -04:00
#!/usr/bin/python3
2022-12-17 13:31:19 -05:00
from todoist_api_python . api import TodoistAPI
2023-01-03 10:06:46 -05:00
from todoist_api_python . models import Task
from todoist_api_python . models import Section
from todoist_api_python . models import Project
2023-01-04 04:01:00 -05:00
from todoist_api_python . http_requests import get
from urllib . parse import urljoin
2023-01-14 16:01:09 -05:00
from urllib . parse import quote
2020-05-24 11:46:51 -04:00
import sys
import time
import requests
import argparse
import logging
2020-12-29 18:35:07 -05:00
from datetime import datetime , timedelta
2020-12-29 16:56:55 -05:00
import time
2023-01-02 10:28:16 -05:00
import sqlite3
2023-01-03 10:06:46 -05:00
import os
2023-01-08 08:58:49 -05:00
import re
2023-01-14 16:01:09 -05:00
import json
2023-01-03 10:06:46 -05:00
# Connect to SQLite database
def create_connection ( path ) :
connection = None
try :
connection = sqlite3 . connect ( path )
2023-01-14 17:57:22 -05:00
logging . debug ( " Connection to SQLite DB successful! " )
2023-01-03 10:06:46 -05:00
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
2023-01-04 15:12:39 -05:00
def execute_query ( connection , query , * args ) :
2023-01-03 10:06:46 -05:00
cursor = connection . cursor ( )
try :
2023-01-04 15:12:39 -05:00
value = args [ 0 ]
2023-01-13 11:02:53 -05:00
# Useful to pass None/NULL value correctly
cursor . execute ( query , ( value , ) )
2023-01-04 15:12:39 -05:00
except :
2023-01-03 10:06:46 -05:00
cursor . execute ( query )
2023-01-04 15:12:39 -05:00
try :
2023-01-03 10:06:46 -05:00
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 '
2023-01-13 11:02:53 -05:00
query = """ UPDATE %s SET %s = ? WHERE %s = %r """ % (
db_name , column , goal , model . id )
2023-01-03 10:06:46 -05:00
2023-01-04 15:12:39 -05:00
result = execute_query ( connection , query , value )
return result
2023-01-03 10:06:46 -05:00
except Exception as e :
logging . debug ( f " The error ' { e } ' occurred " )
# 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
2023-01-08 16:23:54 -05:00
tasks ( task_id , task_type , parent_type , due_date , r_tag )
2023-01-03 10:06:46 -05:00
VALUES
2023-01-08 16:23:54 -05:00
( % r , % s , % s , % s , % i ) ;
""" % (model.id, ' NULL ' , ' NULL ' , ' NULL ' , 0)
2023-01-03 10:06:46 -05:00
if isinstance ( model , Section ) :
q_create = """
INSERT INTO
2023-01-04 15:12:39 -05:00
sections ( section_id , section_type )
2023-01-03 10:06:46 -05:00
VALUES
2023-01-04 15:12:39 -05:00
( % r , % s ) ;
""" % (model.id, ' NULL ' )
2023-01-03 10:06:46 -05:00
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 ,
2023-01-04 15:12:39 -05:00
section_id INTEGER ,
2023-01-03 10:06:46 -05:00
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 ,
2023-01-08 16:23:54 -05:00
due_date TEXT ,
2023-01-03 10:06:46 -05:00
r_tag INTEGER
) ;
"""
execute_query ( connection , q_create_projects_table )
execute_query ( connection , q_create_sections_table )
execute_query ( connection , q_create_tasks_table )
2023-01-14 17:57:22 -05:00
logging . info ( " SQLite DB has successfully initialized! \n " )
2023-01-03 10:06:46 -05:00
return connection
2020-05-24 09:55:47 -04:00
2020-12-29 16:56:55 -05:00
# Makes --help text wider
2021-01-16 11:14:22 -05:00
2020-05-30 07:14:07 -04:00
def make_wide ( formatter , w = 120 , h = 36 ) :
""" Return a wider HelpFormatter, if possible. """
try :
# https://stackoverflow.com/a/5464440
# beware: "Only the name of this class is considered a public API."
kwargs = { ' width ' : w , ' max_help_position ' : h }
formatter ( None , * * kwargs )
return lambda prog : formatter ( prog , * * kwargs )
except TypeError :
2020-05-30 07:49:37 -04:00
logging . error ( " Argparse help formatter failed, falling back. " )
2020-05-30 07:14:07 -04:00
return formatter
2021-01-16 11:14:22 -05:00
# Simple query for yes/no answer
def query_yes_no ( question , default = " yes " ) :
# """Ask a yes/no question via raw_input() and return their answer.
# "question" is a string that is presented to the user.
# "default" is the presumed answer if the user just hits <Enter>.
# It must be "yes" (the default), "no" or None (meaning
# an answer is required of the user).
# The "answer" return value is True for "yes" or False for "no".
# """
valid = { " yes " : True , " y " : True , " ye " : True ,
" no " : False , " n " : False }
if default is None :
prompt = " [y/n] "
elif default == " yes " :
prompt = " [Y/n] "
elif default == " no " :
prompt = " [y/N] "
else :
raise ValueError ( " invalid default answer: ' %s ' " % default )
while True :
sys . stdout . write ( question + prompt )
choice = input ( ) . lower ( )
if default is not None and choice == ' ' :
return valid [ default ]
elif choice in valid :
return valid [ choice ]
else :
sys . stdout . write ( " Please respond with ' yes ' or ' no ' "
" (or ' y ' or ' n ' ). \n " )
2021-01-17 08:58:14 -05:00
# Check if label exists, if not, create it
2023-01-03 10:06:46 -05:00
2022-12-17 13:31:19 -05:00
def verify_label_existance ( api , label_name , prompt_mode ) :
2021-01-16 16:42:46 -05:00
# Check the regeneration label exists
2022-12-17 13:31:19 -05:00
labels = api . get_labels ( )
label = [ x for x in labels if x . name == label_name ]
2021-01-16 16:42:46 -05:00
if len ( label ) > 0 :
2023-01-02 10:28:16 -05:00
next_action_label = label [ 0 ] . id
2023-01-13 12:02:46 -05:00
logging . debug ( ' Label \' %s \' found as label id %s ' ,
2023-01-02 10:28:16 -05:00
label_name , next_action_label )
2021-01-16 16:42:46 -05:00
else :
# Create a new label in Todoist
logging . info (
" \n \n Label ' {} ' doesn ' t exist in your Todoist \n " . format ( label_name ) )
# sys.exit(1)
if prompt_mode == 1 :
response = query_yes_no (
' Do you want to automatically create this label? ' )
else :
response = True
if response :
2022-12-17 13:31:19 -05:00
try :
api . add_label ( name = label_name )
except Exception as error :
2023-01-15 08:05:16 -05:00
logging . warning ( error )
2022-12-17 13:31:19 -05:00
labels = api . get_labels ( )
label = [ x for x in labels if x . name == label_name ]
2023-01-02 10:28:16 -05:00
next_action_label = label [ 0 ] . id
2022-12-17 13:31:19 -05:00
2021-01-16 16:42:46 -05:00
logging . info ( " Label ' {} ' has been created! " . format ( label_name ) )
else :
logging . info ( ' Exiting Autodoist. ' )
exit ( 1 )
2021-01-16 11:14:22 -05:00
2023-01-14 16:01:09 -05:00
return labels
2021-01-16 11:14:22 -05:00
2021-01-16 16:42:46 -05:00
# Initialisation of Autodoist
2023-01-03 10:06:46 -05:00
def initialise_api ( args ) :
2021-01-16 11:14:22 -05:00
# Check we have a API key
if not args . api_key :
logging . error (
2023-01-13 13:09:33 -05:00
" \n \n No API key set. Run Autodoist with ' -a <YOUR_API_KEY> ' or set the environment variable TODOIST_API_KEY. \n " )
2021-01-16 11:14:22 -05:00
sys . exit ( 1 )
# Check if alternative end of day is used
if args . end is not None :
if args . end < 1 or args . end > 24 :
logging . error (
" \n \n Please choose a number from 1 to 24 to indicate which hour is used as alternative end-of-day time. \n " )
sys . exit ( 1 )
else :
pass
2021-01-17 08:58:14 -05:00
# Check if proper regeneration mode has been selected
if args . regeneration is not None :
2023-01-03 10:06:46 -05:00
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. ' )
2021-01-17 08:58:14 -05:00
exit ( 1 )
2021-01-16 11:14:22 -05:00
# Show which modes are enabled:
modes = [ ]
m_num = 0
2021-01-17 08:58:14 -05:00
for x in [ args . label , args . regeneration , args . end ] :
2021-01-16 11:14:22 -05:00
if x :
modes . append ( ' Enabled ' )
m_num + = 1
else :
modes . append ( ' Disabled ' )
logging . info ( " You are running with the following functionalities: \n \n Next action labelling mode: {} \n Regenerate sub-tasks mode: {} \n Shifted end-of-day mode: {} \n " . format ( * modes ) )
if m_num == 0 :
logging . info (
" \n No functionality has been enabled. Please see --help for the available options. \n " )
exit ( 0 )
# Run the initial sync
logging . debug ( ' Connecting to the Todoist API ' )
2023-01-15 15:55:30 -05:00
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 ' ]
2023-01-15 16:00:00 -05:00
2023-01-15 15:55:30 -05:00
except Exception as e :
logging . error (
f " Could not connect to Todoist: ' { e } ' " )
exit ( 0 )
2022-12-17 13:31:19 -05:00
2023-01-15 15:55:30 -05:00
logging . info ( " Autodoist has successfully connected to Todoist! " )
2023-01-14 16:01:09 -05:00
2022-12-17 13:31:19 -05:00
# Check if labels exist
2021-01-16 11:14:22 -05:00
2021-01-16 16:42:46 -05:00
# If labeling argument is used
2021-01-16 11:14:22 -05:00
if args . label is not None :
2021-01-16 16:42:46 -05:00
# Verify that the next action label exists; ask user if it needs to be created
2023-01-02 10:28:16 -05:00
verify_label_existance ( api , args . label , 1 )
2021-01-16 11:14:22 -05:00
2023-01-14 16:01:09 -05:00
# TODO: Disabled for now
# # If regeneration mode is used, verify labels
# if args.regeneration is not None:
2021-01-16 16:42:46 -05:00
2023-01-14 16:01:09 -05:00
# # 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]
2021-01-16 16:42:46 -05:00
2023-01-14 16:01:09 -05:00
# else:
# # Label functionality not needed
# regen_labels_id = [None, None, None]
2021-01-16 11:14:22 -05:00
2023-01-02 10:28:16 -05:00
return api
2021-01-16 11:14:22 -05:00
# Check for Autodoist update
def check_for_update ( current_version ) :
updateurl = ' https://api.github.com/repos/Hoffelhas/autodoist/releases '
try :
r = requests . get ( updateurl )
r . raise_for_status ( )
release_info_json = r . json ( )
if not current_version == release_info_json [ 0 ] [ ' tag_name ' ] :
logging . warning ( " \n \n Your version is not up-to-date! \n Your version: {} . Latest version: {} \n Find the latest version at: {} \n " . format (
current_version , release_info_json [ 0 ] [ ' tag_name ' ] , release_info_json [ 0 ] [ ' html_url ' ] ) )
return 1
else :
return 0
except requests . exceptions . ConnectionError as e :
logging . error (
" Error while checking for updates (Connection error): {} " . format ( e ) )
return 1
except requests . exceptions . HTTPError as e :
logging . error (
" Error while checking for updates (HTTP error): {} " . format ( e ) )
return 1
except requests . exceptions . RequestException as e :
logging . error ( " Error while checking for updates: {} " . format ( e ) )
return 1
2023-01-04 04:01:00 -05:00
# Get all data through the SYNC API. Needed to see e.g. any completed tasks.
2023-01-13 11:02:53 -05:00
2023-01-10 14:20:54 -05:00
def get_all_data ( api ) :
2023-01-04 04:01:00 -05:00
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
2023-01-14 16:01:09 -05:00
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 " ] '
2023-01-15 10:38:34 -05:00
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 } ' " )
2023-01-14 16:01:09 -05:00
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 ' ,
}
2023-01-15 07:53:28 -05:00
data = ' sync_token= ' + api . sync_token + \
' &commands= ' + json . dumps ( api . queue )
2023-01-14 16:01:09 -05:00
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 ( )
2023-01-08 09:28:58 -05:00
# Find the type based on name suffix.
2021-01-16 11:14:22 -05:00
2023-01-08 08:58:49 -05:00
def check_name ( args , string , num ) :
2021-01-16 11:14:22 -05:00
2023-01-08 08:58:49 -05:00
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 ]
2023-01-13 11:02:53 -05:00
# Somebody put fewer characters than intended. Take last character and apply for every missing one.
2023-01-08 08:58:49 -05:00
if len ( suffix ) < num :
suffix + = suffix [ - 1 ] * ( num - len ( suffix ) )
2023-01-13 11:02:53 -05:00
current_type = ' '
2023-01-08 08:58:49 -05:00
for s in suffix :
if s == args . s_suffix :
current_type + = ' s '
elif s == args . p_suffix :
current_type + = ' p '
2023-01-13 11:02:53 -05:00
2023-01-08 09:28:58 -05:00
# 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
2023-01-08 08:58:49 -05:00
except :
logging . debug ( " String {} not recognised. " . format ( string ) )
2021-01-16 11:14:22 -05:00
current_type = None
return current_type
# Scan the end of a name to find what type it is
2023-01-03 10:06:46 -05:00
def get_type ( args , connection , model , key ) :
2021-01-16 11:14:22 -05:00
2023-01-03 10:06:46 -05:00
# model_name = ''
2021-01-16 11:14:22 -05:00
try :
2023-01-03 10:06:46 -05:00
old_type = ' '
old_type = db_read_value ( connection , model , key ) [ 0 ] [ 0 ]
2021-01-16 11:14:22 -05:00
except :
# logging.debug('No defined project_type: %s' % str(e))
old_type = None
2023-01-08 08:58:49 -05:00
if isinstance ( model , Task ) :
2023-01-13 11:02:53 -05:00
current_type = check_name ( args , model . content , 1 ) # Tasks
2023-01-08 08:58:49 -05:00
elif isinstance ( model , Section ) :
2023-01-13 11:02:53 -05:00
current_type = check_name ( args , model . name , 2 ) # Sections
2023-01-08 08:58:49 -05:00
elif isinstance ( model , Project ) :
2023-01-13 11:02:53 -05:00
current_type = check_name ( args , model . name , 3 ) # Projects
2021-01-16 11:14:22 -05:00
2023-01-08 16:23:54 -05:00
# Check if type changed with respect to previous run
2021-01-16 11:14:22 -05:00
if old_type == current_type :
type_changed = 0
else :
type_changed = 1
2023-01-03 10:06:46 -05:00
db_update_value ( connection , model , key , current_type )
2021-01-16 11:14:22 -05:00
return current_type , type_changed
# Determine a project type
2023-01-04 04:01:00 -05:00
def get_project_type ( args , connection , project ) :
2021-01-16 11:14:22 -05:00
""" Identifies how a project should be handled. """
project_type , project_type_changed = get_type (
2023-01-04 04:01:00 -05:00
args , connection , project , ' project_type ' )
if project_type is not None :
logging . debug ( ' Identified \' %s \' as %s type ' ,
project . name , project_type )
2021-01-16 11:14:22 -05:00
return project_type , project_type_changed
# Determine a section type
2023-01-13 12:02:46 -05:00
def get_section_type ( args , connection , section , project ) :
2021-01-16 11:14:22 -05:00
""" Identifies how a section should be handled. """
2023-01-04 04:01:00 -05:00
if section is not None :
2021-01-16 11:14:22 -05:00
section_type , section_type_changed = get_type (
2023-01-04 04:01:00 -05:00
args , connection , section , ' section_type ' )
2021-01-16 11:14:22 -05:00
else :
section_type = None
section_type_changed = 0
2023-01-04 04:01:00 -05:00
if section_type is not None :
2023-01-13 12:02:46 -05:00
logging . debug ( " Identified ' %s > %s ' as %s type " ,
project . name , section . name , section_type )
2023-01-04 04:01:00 -05:00
2021-01-16 11:14:22 -05:00
return section_type , section_type_changed
2023-01-02 10:28:16 -05:00
# Determine an task type
2021-01-16 11:14:22 -05:00
2023-01-13 12:02:46 -05:00
def get_task_type ( args , connection , task , section , project ) :
2023-01-02 10:28:16 -05:00
""" Identifies how a task with sub tasks should be handled. """
2021-01-16 11:14:22 -05:00
2023-01-04 04:01:00 -05:00
task_type , task_type_changed = get_type (
args , connection , task , ' task_type ' )
if task_type is not None :
2023-01-14 16:01:09 -05:00
logging . debug ( " Identified ' %s > %s > %s ' as %s type " ,
project . name , section . name , task . content , task_type )
2021-01-16 11:14:22 -05:00
2023-01-02 10:28:16 -05:00
return task_type , task_type_changed
2021-01-16 11:14:22 -05:00
2023-01-02 10:28:16 -05:00
# Logic to track addition of a label to a task
2021-01-16 11:14:22 -05:00
2023-01-15 07:53:28 -05:00
def add_label ( task , label , overview_task_ids , overview_task_labels ) :
2023-01-02 10:28:16 -05:00
if label not in task . labels :
2023-01-04 04:01:00 -05:00
labels = task . labels # To also copy other existing labels
2023-01-02 10:28:16 -05:00
logging . debug ( ' Updating \' %s \' with label ' , task . content )
2021-01-16 11:14:22 -05:00
labels . append ( label )
try :
2023-01-02 10:28:16 -05:00
overview_task_ids [ task . id ] + = 1
2021-01-16 11:14:22 -05:00
except :
2023-01-02 10:28:16 -05:00
overview_task_ids [ task . id ] = 1
overview_task_labels [ task . id ] = labels
2021-01-16 11:14:22 -05:00
2023-01-02 10:28:16 -05:00
# Logic to track removal of a label from a task
2021-01-16 11:14:22 -05:00
2023-01-02 10:28:16 -05:00
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 )
2021-01-16 11:14:22 -05:00
labels . remove ( label )
try :
2023-01-02 10:28:16 -05:00
overview_task_ids [ task . id ] - = 1
2021-01-16 11:14:22 -05:00
except :
2023-01-02 10:28:16 -05:00
overview_task_ids [ task . id ] = - 1
overview_task_labels [ task . id ] = labels
2021-01-16 11:14:22 -05:00
# Check if header logic needs to be applied
2023-01-14 16:01:09 -05:00
def check_header ( api , model ) :
2021-01-16 11:14:22 -05:00
header_all_in_level = False
unheader_all_in_level = False
2023-01-09 16:33:15 -05:00
regex_a = ' (^[*] {2} \ s*)(.*) '
2023-01-12 15:32:51 -05:00
regex_b = ' (^ \ - \ * \ s*)(.*) '
2021-01-16 11:14:22 -05:00
2023-01-12 15:32:51 -05:00
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
2023-01-13 11:02:53 -05:00
model . content = ra [ 2 ] # Local record
2023-01-12 15:32:51 -05:00
api . update_task ( task_id = model . id , content = ra [ 2 ] )
2023-01-13 10:58:16 -05:00
# overview_updated_ids.append(model.id) # Ignore this one, since else it's count double
2023-01-12 15:32:51 -05:00
if rb :
unheader_all_in_level = True
2023-01-13 11:02:53 -05:00
model . content = rb [ 2 ] # Local record
2023-01-12 15:32:51 -05:00
api . update_task ( task_id = model . id , content = rb [ 2 ] )
2023-01-14 16:01:09 -05:00
# overview_updated_ids.append(model.id)
2023-01-12 15:32:51 -05:00
else :
ra = re . search ( regex_a , model . name )
rb = re . search ( regex_b , model . name )
2023-01-09 16:33:15 -05:00
2023-01-12 15:32:51 -05:00
if isinstance ( model , Section ) :
if ra :
header_all_in_level = True
api . update_section ( section_id = model . id , name = ra [ 2 ] )
2023-01-14 16:01:09 -05:00
api . overview_updated_ids . append ( model . id )
2023-01-12 15:32:51 -05:00
if rb :
unheader_all_in_level = True
api . update_section ( section_id = model . id , name = rb [ 2 ] )
2023-01-14 16:01:09 -05:00
api . overview_updated_ids . append ( model . id )
2023-01-12 15:32:51 -05:00
elif isinstance ( model , Project ) :
if ra :
header_all_in_level = True
api . update_project ( project_id = model . id , name = ra [ 2 ] )
2023-01-14 16:01:09 -05:00
api . overview_updated_ids . append ( model . id )
2023-01-12 15:32:51 -05:00
if rb :
unheader_all_in_level = True
api . update_project ( project_id = model . id , name = rb [ 2 ] )
2023-01-14 16:01:09 -05:00
api . overview_updated_ids . append ( model . id )
2023-01-12 15:32:51 -05:00
except :
logging . debug ( ' check_header: no right model found ' )
2021-01-16 11:14:22 -05:00
2023-01-14 16:01:09 -05:00
return api , header_all_in_level , unheader_all_in_level
2021-01-16 11:14:22 -05:00
2023-01-02 10:28:16 -05:00
# Logic for applying and removing headers
2023-01-03 10:06:46 -05:00
2023-01-14 16:01:09 -05:00
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 ) :
2023-01-02 10:28:16 -05:00
2023-01-13 10:35:00 -05:00
if any ( [ header_all_in_p , header_all_in_s ] ) :
if task . content [ : 2 ] != ' * ' :
2023-01-14 16:01:09 -05:00
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)
2023-01-13 11:02:53 -05:00
2023-01-02 10:28:16 -05:00
if any ( [ unheader_all_in_p , unheader_all_in_s ] ) :
2023-01-13 10:35:00 -05:00
if task . content [ : 2 ] == ' * ' :
2023-01-14 16:01:09 -05:00
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)
2023-01-12 15:42:21 -05:00
2023-01-13 10:35:00 -05:00
if header_all_in_t :
if task . content [ : 2 ] != ' * ' :
2023-01-14 16:01:09 -05:00
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 )
2023-01-13 10:35:00 -05:00
2023-01-02 10:28:16 -05:00
if unheader_all_in_t :
2023-01-13 10:35:00 -05:00
if task . content [ : 2 ] == ' * ' :
2023-01-14 16:01:09 -05:00
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
2023-01-13 10:35:00 -05:00
2023-01-02 10:28:16 -05:00
2021-01-16 16:42:46 -05:00
# Check regen mode based on label name
2021-01-17 08:58:14 -05:00
2021-01-16 16:42:46 -05:00
def check_regen_mode ( api , item , regen_labels_id ) :
2023-01-02 10:28:16 -05:00
labels = item . labels
2021-01-16 16:42:46 -05:00
overlap = set ( labels ) & set ( regen_labels_id )
overlap = [ val for val in overlap ]
2021-01-16 11:14:22 -05:00
2021-01-16 16:42:46 -05:00
if len ( overlap ) > 1 :
logging . warning (
2023-01-02 10:28:16 -05:00
' Multiple regeneration labels used! Please pick only one for item: " {} " . ' . format ( item . content ) )
2021-01-16 16:42:46 -05:00
return None
2021-01-16 11:14:22 -05:00
2021-01-23 15:29:06 -05:00
try :
2023-01-02 10:28:16 -05:00
regen_next_action_label = overlap [ 0 ]
2021-01-23 15:29:06 -05:00
except :
logging . debug (
2023-01-02 10:28:16 -05:00
' No regeneration label for item: %s ' % item . content )
regen_next_action_label = [ 0 ]
2021-01-17 08:58:14 -05:00
2023-01-02 10:28:16 -05:00
if regen_next_action_label == regen_labels_id [ 0 ] :
2021-01-16 16:42:46 -05:00
return 0
2023-01-02 10:28:16 -05:00
elif regen_next_action_label == regen_labels_id [ 1 ] :
2021-01-16 16:42:46 -05:00
return 1
2023-01-02 10:28:16 -05:00
elif regen_next_action_label == regen_labels_id [ 2 ] :
2021-01-16 16:42:46 -05:00
return 2
else :
2023-01-02 10:28:16 -05:00
# label_name = api.labels.get_by_id(regen_next_action_label)['name']
2021-01-23 15:29:06 -05:00
# logging.debug(
2023-01-03 10:06:46 -05:00
# 'No regeneration label for item: %s' % item.content)
2021-01-16 16:42:46 -05:00
return None
# Recurring lists logic
2021-01-17 08:58:14 -05:00
2023-01-14 16:01:09 -05:00
def run_recurring_lists_logic ( args , api , connection , task , task_items , task_items_all , regen_labels_id ) :
2021-01-16 11:14:22 -05:00
2023-01-08 16:23:54 -05:00
if task . parent_id == 0 :
2021-01-16 11:14:22 -05:00
try :
2023-01-08 16:23:54 -05:00
if task . due . is_recurring :
2021-01-16 11:14:22 -05:00
try :
2023-01-13 11:02:53 -05:00
db_task_due_date = db_read_value (
connection , task , ' due_date ' ) [ 0 ] [ 0 ]
2023-01-08 16:23:54 -05:00
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 )
2023-01-13 11:02:53 -05:00
db_update_value ( connection , task ,
' due_date ' , task . due . date )
2023-01-08 16:23:54 -05:00
# Check if the T0 task date has changed, because a user has checked the task
if task . due . date != db_task_due_date :
2023-01-13 11:02:53 -05:00
# TODO: reevaluate regeneration mode. Disabled for now.
# # Mark children for action based on mode
2023-01-08 16:23:54 -05:00
# 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
2021-01-16 11:14:22 -05:00
# If alternative end of day, fix due date if needed
if args . end is not None :
# Determine current hour
t = datetime . today ( )
current_hour = t . hour
# Check if current time is before our end-of-day
if ( args . end - current_hour ) > 0 :
# Determine the difference in days set by todoist
2023-01-08 16:23:54 -05:00
nd = [ int ( x ) for x in task . due . date . split ( ' - ' ) ]
2023-01-13 11:02:53 -05:00
od = [ int ( x )
for x in db_task_due_date . split ( ' - ' ) ]
2021-01-16 11:14:22 -05:00
new_date = datetime (
nd [ 0 ] , nd [ 1 ] , nd [ 2 ] )
old_date = datetime (
od [ 0 ] , od [ 1 ] , od [ 2 ] )
today = datetime (
t . year , t . month , t . day )
days_difference = (
new_date - today ) . days
days_overdue = (
today - old_date ) . days
# Only apply if overdue and if it's a daily recurring tasks
if days_overdue > = 1 and days_difference == 1 :
2023-01-02 10:28:16 -05:00
# Find current date in string format
2023-01-08 16:23:54 -05:00
today_str = t . strftime ( " % Y- % m- %d " )
2021-01-16 11:14:22 -05:00
# Update due-date to today
2023-01-13 11:02:53 -05:00
api . update_task (
task_id = task . id , due_date = today_str , due_string = task . due . string )
2023-01-14 16:01:09 -05:00
logging . debug (
2023-01-13 11:02:53 -05:00
" Update date on task: ' %s ' " % ( task . content ) )
2021-01-16 11:14:22 -05:00
2021-06-16 23:02:27 -04:00
# Save the new date for reference us
2023-01-13 11:02:53 -05:00
db_update_value ( connection , task ,
' due_date ' , task . due . date )
2021-06-16 23:02:27 -04:00
2021-01-16 11:14:22 -05:00
except :
# If date has never been saved before, create a new entry
logging . debug (
2023-01-08 16:23:54 -05:00
' New recurring task detected: %s ' % task . content )
2023-01-13 11:02:53 -05:00
db_update_value ( connection , task ,
' due_date ' , task . due . date )
2021-01-16 11:14:22 -05:00
except :
pass
2023-01-13 11:02:53 -05:00
# TODO: reevaluate regeneration mode. Disabled for now.
2023-01-08 16:23:54 -05:00
# 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
2021-01-16 11:14:22 -05:00
2023-01-07 09:07:57 -05:00
# Find and clean all children under a task
2023-01-13 11:02:53 -05:00
2023-01-07 09:07:57 -05:00
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 )
2023-01-13 11:02:53 -05:00
task_ids = find_and_clean_all_children (
task_ids , child_task , section_tasks )
2023-01-07 09:07:57 -05:00
return task_ids
2023-01-13 11:02:53 -05:00
2023-01-14 16:01:09 -05:00
def find_and_headerify_all_children ( api , task , section_tasks , mode ) :
2023-01-13 10:35:00 -05:00
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 ] != ' * ' :
2023-01-14 16:01:09 -05:00
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)
2023-01-13 11:02:53 -05:00
2023-01-13 10:35:00 -05:00
elif mode == 2 :
if child_task . content [ : 2 ] == ' * ' :
2023-01-14 16:01:09 -05:00
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)
2023-01-13 10:35:00 -05:00
2023-01-13 11:02:53 -05:00
find_and_headerify_all_children (
2023-01-14 16:01:09 -05:00
api , child_task , section_tasks , mode )
2023-01-13 10:35:00 -05:00
return 0
2021-01-16 11:14:22 -05:00
# Contains all main autodoist functionalities
2023-01-03 10:06:46 -05:00
def autodoist_magic ( args , api , connection ) :
2021-01-16 11:14:22 -05:00
2023-01-03 10:06:46 -05:00
# Preallocate dictionaries and other values
2023-01-02 10:28:16 -05:00
overview_task_ids = { }
overview_task_labels = { }
2023-01-03 10:06:46 -05:00
next_action_label = args . label
regen_labels_id = args . regen_label_names
2023-01-08 08:58:49 -05:00
first_found = [ False , False , False ]
2023-01-14 16:01:09 -05:00
api . queue = [ ]
api . overview_updated_ids = [ ]
2023-01-13 11:02:53 -05:00
2023-01-14 17:57:22 -05:00
# Get all todoist info
2022-12-17 13:31:19 -05:00
try :
2023-01-15 07:53:28 -05:00
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
2023-01-14 17:57:22 -05:00
all_tasks = api . get_tasks ( )
2022-12-17 13:31:19 -05:00
except Exception as error :
2023-01-15 08:05:16 -05:00
logging . error ( error )
2022-12-17 13:31:19 -05:00
2023-01-14 18:24:17 -05:00
for project in all_projects :
2021-01-16 11:14:22 -05:00
2023-01-03 10:06:46 -05:00
# Skip processing inbox as intended feature
if project . is_inbox_project :
continue
# Check db existance
db_check_existance ( connection , project )
2021-01-16 11:14:22 -05:00
# Check if we need to (un)header entire project
2023-01-14 16:01:09 -05:00
api , header_all_in_p , unheader_all_in_p = check_header (
api , project )
2021-01-16 11:14:22 -05:00
2023-01-02 10:28:16 -05:00
# Get project type
if next_action_label is not None :
2021-01-16 11:14:22 -05:00
project_type , project_type_changed = get_project_type (
2023-01-03 10:06:46 -05:00
args , connection , project )
2023-01-08 08:58:49 -05:00
else :
project_type = None
project_type_changed = 0
2023-01-02 10:28:16 -05:00
# Get all tasks for the project
2022-12-17 13:31:19 -05:00
try :
2023-01-15 07:53:28 -05:00
project_tasks = [
t for t in all_tasks if t . project_id == project . id ]
2022-12-17 13:31:19 -05:00
except Exception as error :
2023-01-15 08:05:16 -05:00
logging . warning ( error )
2021-01-16 11:14:22 -05:00
2023-01-04 15:12:39 -05:00
# 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 :
2023-01-13 11:02:53 -05:00
remove_label ( task , next_action_label ,
overview_task_ids , overview_task_labels )
2023-01-04 15:12:39 -05:00
db_update_value ( connection , task , ' task_type ' , None )
db_update_value ( connection , task , ' parent_type ' , None )
2023-01-02 10:28:16 -05:00
# Run for both non-sectioned and sectioned tasks
2023-01-03 10:06:46 -05:00
# 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)
2021-01-16 11:14:22 -05:00
2023-01-03 10:06:46 -05:00
# Get all sections and add the 'None' section too.
try :
2023-01-14 18:24:17 -05:00
sections = [ s for s in all_sections if s . project_id == project . id ]
2023-01-03 10:06:46 -05:00
sections . insert ( 0 , Section ( None , None , 0 , project . id ) )
except Exception as error :
2023-01-15 08:05:16 -05:00
logging . debug ( error )
2021-01-16 11:14:22 -05:00
2023-01-08 08:58:49 -05:00
# Reset
first_found [ 0 ] = False
2023-01-03 10:06:46 -05:00
for section in sections :
2021-01-16 11:14:22 -05:00
2023-01-13 15:32:06 -05:00
# Check if section labelling is disabled (useful for e.g. Kanban)
if next_action_label is not None :
2023-01-14 16:01:09 -05:00
disable_section_labelling = 0
2023-01-13 15:32:06 -05:00
try :
if section . name . startswith ( ' * ' ) or section . name . endswith ( ' * ' ) :
disable_section_labelling = 1
except :
pass
2023-01-03 10:06:46 -05:00
# Check db existance
db_check_existance ( connection , section )
2021-01-16 11:14:22 -05:00
2023-01-03 10:06:46 -05:00
# Check if we need to (un)header entire secion
2023-01-14 16:01:09 -05:00
api , header_all_in_s , unheader_all_in_s = check_header (
api , section )
2021-01-16 11:14:22 -05:00
2023-01-03 10:06:46 -05:00
# Get section type
2023-01-08 08:58:49 -05:00
if next_action_label :
section_type , section_type_changed = get_section_type (
2023-01-13 12:02:46 -05:00
args , connection , section , project )
2023-01-08 08:58:49 -05:00
else :
section_type = None
section_type_changed = 0
2023-01-03 10:06:46 -05:00
# Get all tasks for the section
2023-01-04 15:12:39 -05:00
section_tasks = [ x for x in project_tasks if x . section_id
2023-01-13 11:02:53 -05:00
== section . id ]
2023-01-03 10:06:46 -05:00
# Change top tasks parents_id from 'None' to '0' in order to numerically sort later on
2023-01-04 15:12:39 -05:00
for task in section_tasks :
2023-01-03 10:06:46 -05:00
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.
2023-01-04 15:12:39 -05:00
section_tasks = sorted ( section_tasks , key = lambda x : (
2023-01-03 10:06:46 -05:00
int ( x . parent_id ) , x . order ) )
2023-01-04 04:01:00 -05:00
# If a type has changed, clean all tasks in this section for good measure
2023-01-03 10:06:46 -05:00
if next_action_label is not None :
2023-01-04 15:12:39 -05:00
if section_type_changed == 1 :
for task in section_tasks :
2023-01-13 11:02:53 -05:00
remove_label ( task , next_action_label ,
overview_task_ids , overview_task_labels )
2023-01-04 15:12:39 -05:00
db_update_value ( connection , task , ' task_type ' , None )
db_update_value ( connection , task , ' parent_type ' , None )
2023-01-13 11:02:53 -05:00
2023-01-08 08:58:49 -05:00
# Reset
first_found [ 1 ] = False
2023-01-03 10:06:46 -05:00
# For all tasks in this section
2023-01-04 15:12:39 -05:00
for task in section_tasks :
2023-01-13 11:02:53 -05:00
2023-01-07 10:13:43 -05:00
# Reset
2023-01-13 11:02:53 -05:00
dominant_type = None
2023-01-03 10:06:46 -05:00
2023-01-07 10:13:43 -05:00
# Check db existance
2023-01-03 10:06:46 -05:00
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 (
2023-01-04 15:12:39 -05:00
filter ( lambda x : not x . is_completed , section_tasks ) )
2023-01-03 10:06:46 -05:00
child_tasks_all = list (
2023-01-04 15:12:39 -05:00
filter ( lambda x : x . parent_id == task . id , section_tasks ) )
2023-01-03 10:06:46 -05:00
child_tasks = list (
filter ( lambda x : x . parent_id == task . id , non_completed_tasks ) )
# Check if we need to (un)header entire task tree
2023-01-14 16:01:09 -05:00
api , header_all_in_t , unheader_all_in_t = check_header (
api , task )
2023-01-03 10:06:46 -05:00
# Modify headers where needed
2023-01-14 16:01:09 -05:00
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 )
2023-01-03 10:06:46 -05:00
2023-01-04 04:01:00 -05:00
# TODO: Check is regeneration is still needed, now that it's part of core Todoist. Disabled for now.
2023-01-03 10:06:46 -05:00
# 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
2023-01-08 16:23:54 -05:00
# 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 (
2023-01-14 16:01:09 -05:00
args , api , connection , task , child_tasks , child_tasks_all , regen_labels_id )
2023-01-03 10:06:46 -05:00
# If options turned on, start labelling logic
2023-01-02 10:28:16 -05:00
if next_action_label is not None :
2023-01-03 10:06:46 -05:00
# Skip processing a task if it has already been checked or is a header
if task . is_completed :
continue
2023-01-08 10:21:50 -05:00
# Remove clean all task and subtask data
2023-01-13 15:32:06 -05:00
if task . content . startswith ( ' * ' ) or disable_section_labelling :
2023-01-03 10:06:46 -05:00
remove_label ( task , next_action_label ,
overview_task_ids , overview_task_labels )
2023-01-08 10:21:50 -05:00
db_update_value ( connection , task , ' task_type ' , None )
db_update_value ( connection , task , ' parent_type ' , None )
2023-01-13 11:02:53 -05:00
task_ids = find_and_clean_all_children (
[ ] , task , section_tasks )
child_tasks_all = list (
filter ( lambda x : x . id in task_ids , section_tasks ) )
2023-01-08 10:21:50 -05:00
for child_task in child_tasks_all :
2023-01-13 11:02:53 -05:00
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 )
2023-01-08 10:21:50 -05:00
2023-01-03 10:06:46 -05:00
continue
# Check task type
task_type , task_type_changed = get_task_type (
2023-01-13 12:02:46 -05:00
args , connection , task , section , project )
2023-01-03 10:06:46 -05:00
2023-01-04 15:12:39 -05:00
# 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 :
2023-01-07 09:07:57 -05:00
# Find all children under this task
2023-01-13 11:02:53 -05:00
task_ids = find_and_clean_all_children (
[ ] , task , section_tasks )
child_tasks_all = list (
filter ( lambda x : x . id in task_ids , section_tasks ) )
2023-01-07 09:07:57 -05:00
for child_task in child_tasks_all :
2023-01-13 11:02:53 -05:00
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 )
2023-01-04 15:12:39 -05:00
2023-01-03 10:06:46 -05:00
# Determine hierarchy types for logic
hierarchy_types = [ task_type ,
section_type , project_type ]
hierarchy_boolean = [ type ( x ) != type ( None )
2023-01-08 11:11:47 -05:00
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 :
2023-01-13 11:02:53 -05:00
remove_label ( task , next_action_label ,
overview_task_ids , overview_task_labels )
2023-01-08 11:11:47 -05:00
db_update_value ( connection , task , ' task_type ' , None )
2023-01-13 11:02:53 -05:00
db_update_value ( connection , task , ' parent_type ' , None )
2023-01-03 10:06:46 -05:00
2023-01-04 15:12:39 -05:00
# If it is a parentless task, set task type based on hierarchy
2023-01-03 10:06:46 -05:00
if task . parent_id == 0 :
2023-01-08 08:58:49 -05:00
if not True in hierarchy_boolean :
2023-01-04 15:12:39 -05:00
# Parentless task has no type, so skip any children.
continue
2023-01-08 08:58:49 -05:00
else :
2023-01-08 09:28:58 -05:00
if hierarchy_boolean [ 0 ] :
2023-01-08 08:58:49 -05:00
# 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
2023-01-15 07:53:28 -05:00
# TODO: optimise below code
2023-01-08 09:28:58 -05:00
# If indicated on project level
2023-01-08 08:58:49 -05:00
if dominant_type [ 0 ] == ' s ' :
if not first_found [ 0 ] :
2023-01-13 11:02:53 -05:00
if dominant_type [ 1 ] == ' s ' :
2023-01-08 08:58:49 -05:00
if not first_found [ 1 ] :
2023-01-13 11:02:53 -05:00
add_label (
2023-01-15 07:53:28 -05:00
task , next_action_label , overview_task_ids , overview_task_labels )
2023-01-08 08:58:49 -05:00
2023-01-08 11:11:47 -05:00
elif next_action_label in task . labels :
# Probably the task has been manually moved, so if it has a label, let's remove it.
2023-01-13 11:02:53 -05:00
remove_label (
task , next_action_label , overview_task_ids , overview_task_labels )
2023-01-08 11:11:47 -05:00
2023-01-08 08:58:49 -05:00
elif dominant_type [ 1 ] == ' p ' :
2023-01-13 11:02:53 -05:00
add_label (
2023-01-15 07:53:28 -05:00
task , next_action_label , overview_task_ids , overview_task_labels )
2021-01-16 11:14:22 -05:00
2023-01-08 08:58:49 -05:00
elif dominant_type [ 0 ] == ' p ' :
2023-01-13 11:02:53 -05:00
if dominant_type [ 1 ] == ' s ' :
2023-01-08 08:58:49 -05:00
if not first_found [ 1 ] :
2023-01-13 11:02:53 -05:00
add_label (
2023-01-15 07:53:28 -05:00
task , next_action_label , overview_task_ids , overview_task_labels )
2023-01-13 11:02:53 -05:00
2023-01-08 11:11:47 -05:00
elif next_action_label in task . labels :
# Probably the task has been manually moved, so if it has a label, let's remove it.
2023-01-13 11:02:53 -05:00
remove_label (
task , next_action_label , overview_task_ids , overview_task_labels )
2023-01-08 08:58:49 -05:00
elif dominant_type [ 1 ] == ' p ' :
2023-01-13 11:02:53 -05:00
add_label (
2023-01-15 07:53:28 -05:00
task , next_action_label , overview_task_ids , overview_task_labels )
2023-01-03 10:06:46 -05:00
2023-01-08 09:28:58 -05:00
# If indicated on section level
2023-01-13 11:02:53 -05:00
if dominant_type [ 0 ] == ' x ' and dominant_type [ 1 ] == ' s ' :
2023-01-08 09:28:58 -05:00
if not first_found [ 1 ] :
2023-01-13 11:02:53 -05:00
add_label (
2023-01-15 07:53:28 -05:00
task , next_action_label , overview_task_ids , overview_task_labels )
2023-01-08 09:28:58 -05:00
2023-01-08 11:11:47 -05:00
elif next_action_label in task . labels :
# Probably the task has been manually moved, so if it has a label, let's remove it.
2023-01-13 11:02:53 -05:00
remove_label (
task , next_action_label , overview_task_ids , overview_task_labels )
2023-01-08 11:11:47 -05:00
2023-01-08 09:28:58 -05:00
elif dominant_type [ 0 ] == ' x ' and dominant_type [ 1 ] == ' p ' :
2023-01-15 07:53:28 -05:00
add_label ( task , next_action_label ,
2023-01-13 11:02:53 -05:00
overview_task_ids , overview_task_labels )
2023-01-08 09:28:58 -05:00
# If indicated on parentless task level
2023-01-13 11:02:53 -05:00
if dominant_type [ 1 ] == ' x ' and dominant_type [ 2 ] == ' s ' :
2023-01-08 09:28:58 -05:00
if not first_found [ 1 ] :
2023-01-13 11:02:53 -05:00
add_label (
2023-01-15 07:53:28 -05:00
task , next_action_label , overview_task_ids , overview_task_labels )
2023-01-08 09:28:58 -05:00
2023-01-08 11:11:47 -05:00
if next_action_label in task . labels :
2023-01-13 11:02:53 -05:00
# 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 )
2023-01-08 11:11:47 -05:00
2023-01-08 09:28:58 -05:00
elif dominant_type [ 1 ] == ' x ' and dominant_type [ 2 ] == ' p ' :
2023-01-15 07:53:28 -05:00
add_label ( task , next_action_label ,
2023-01-13 11:02:53 -05:00
overview_task_ids , overview_task_labels )
2023-01-08 09:28:58 -05:00
2023-01-04 15:12:39 -05:00
# If a parentless or sub-task which has children
2023-01-03 10:06:46 -05:00
if len ( child_tasks ) > 0 :
2023-01-07 09:44:56 -05:00
# If it is a sub-task with no own type, inherit the parent task type instead
2023-01-04 15:12:39 -05:00
if task . parent_id != 0 and task_type == None :
2023-01-04 04:01:00 -05:00
dominant_type = db_read_value (
connection , task , ' parent_type ' ) [ 0 ] [ 0 ]
2023-01-03 10:06:46 -05:00
2023-01-07 09:44:56 -05:00
# 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
2023-01-08 10:21:50 -05:00
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 ]
2023-01-08 08:58:49 -05:00
2023-01-08 09:28:58 -05:00
# Process sequential tagged tasks
2023-01-08 08:58:49 -05:00
if dominant_type == ' s ' :
2023-01-03 10:06:46 -05:00
for child_task in child_tasks :
# Ignore headered children
if child_task . content . startswith ( ' * ' ) :
continue
2021-01-16 11:14:22 -05:00
2023-01-08 11:11:47 -05:00
# Clean up for good measure.
2023-01-13 11:02:53 -05:00
remove_label (
child_task , next_action_label , overview_task_ids , overview_task_labels )
2023-01-08 11:11:47 -05:00
2023-01-03 10:06:46 -05:00
# Pass task_type down to the children
2023-01-07 09:44:56 -05:00
db_update_value (
connection , child_task , ' parent_type ' , dominant_type )
2023-01-03 10:06:46 -05:00
# Pass label down to the first child
if not child_task . is_completed and next_action_label in task . labels :
2021-01-16 11:14:22 -05:00
add_label (
2023-01-15 07:53:28 -05:00
child_task , next_action_label , overview_task_ids , overview_task_labels )
2021-01-16 11:14:22 -05:00
remove_label (
2023-01-02 10:28:16 -05:00
task , next_action_label , overview_task_ids , overview_task_labels )
2021-01-16 11:14:22 -05:00
2023-01-03 10:06:46 -05:00
# Process parallel tagged tasks or untagged parents
2023-01-08 08:58:49 -05:00
elif dominant_type == ' p ' and next_action_label in task . labels :
2023-01-03 10:06:46 -05:00
remove_label (
task , next_action_label , overview_task_ids , overview_task_labels )
2021-01-16 11:14:22 -05:00
2023-01-03 10:06:46 -05:00
for child_task in child_tasks :
2021-01-16 11:14:22 -05:00
2023-01-03 10:06:46 -05:00
# Ignore headered children
if child_task . content . startswith ( ' * ' ) :
2021-01-16 11:14:22 -05:00
continue
2023-01-04 04:01:00 -05:00
db_update_value (
connection , child_task , ' parent_type ' , dominant_type )
2021-01-16 11:14:22 -05:00
2023-01-03 10:06:46 -05:00
if not child_task . is_completed :
add_label (
2023-01-15 07:53:28 -05:00
child_task , next_action_label , overview_task_ids , overview_task_labels )
2023-01-03 10:06:46 -05:00
# Remove labels based on start / due dates
2023-01-08 12:13:06 -05:00
# If task is too far in the future, remove the next_action tag and skip
2023-01-08 11:37:24 -05:00
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
2023-01-04 15:12:39 -05:00
2023-01-08 12:13:06 -05:00
# If start-date has not passed yet, remove label
2023-01-08 11:37:24 -05:00
try :
2023-10-25 15:58:39 -04:00
# When a user inputs a custom dateformat, they will use DD as day, MM as month, YYYY as year
# These values will be used as keys to retrieve a list of [regex, date format code]
# This list will then be used to reconstruct the proper regex search and date formatter below
date_dict = {
' DD ' : [ r ' \ d {2} ' , ' %d ' ] ,
' MM ' : [ r ' \ d {2} ' , ' % m ' ] ,
' YYYY ' : [ r ' \ d {4} ' , ' % Y ' ]
}
# Currently we allow custom dates to be delimitted by '-' only.
# In the future logic could be added to hanlde '.' and '/' delimitters
custom_dateformat = args . dateformat . split ( " - " )
# If there is a cleaner way retrieve the correct value from the dict above I'm all ears.
# For now I've made a tradeoff between readability and verbosity
regex_dateformat = f ' { date_dict [ custom_dateformat [ 0 ] ] [ 0 ] } [-] { date_dict [ custom_dateformat [ 1 ] ] [ 0 ] } [-] { date_dict [ custom_dateformat [ 2 ] ] [ 0 ] } '
parsed_dateformat = f ' { date_dict [ custom_dateformat [ 0 ] ] [ 1 ] } - { date_dict [ custom_dateformat [ 1 ] ] [ 1 ] } - { date_dict [ custom_dateformat [ 2 ] ] [ 1 ] } '
f1 = re . search ( f ' start=( { regex_dateformat } ) ' , task . content )
2023-01-08 12:13:06 -05:00
if f1 :
start_date = f1 . groups ( ) [ 0 ]
2023-01-08 11:37:24 -05:00
start_date = datetime . strptime (
2023-10-25 15:58:39 -04:00
start_date , parsed_dateformat )
2023-01-08 11:37:24 -05:00
future_diff = (
datetime . today ( ) - start_date ) . days
2023-01-08 12:13:06 -05:00
# If start-date hasen't passed, remove all labels
2023-01-08 11:37:24 -05:00
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 ]
except :
logging . warning (
2023-10-25 16:12:22 -04:00
' Wrong start-date format for task: " %s " ' , task . content )
2023-01-08 11:37:24 -05:00
continue
2023-01-04 15:12:39 -05:00
2023-01-08 12:42:16 -05:00
# Recurring task friendly - remove label with relative change from due date
if task . due is not None :
try :
2023-01-13 11:02:53 -05:00
f2 = re . search (
' start=due-( \ d+)([dw]) ' , task . content )
2023-01-08 12:42:16 -05:00
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 :
2023-01-13 11:02:53 -05:00
due_date = datetime . strptime (
task . due . datetime , " % Y- % m- %d T % H: % M: % S " )
2023-01-08 12:42:16 -05:00
except :
2023-01-13 11:02:53 -05:00
due_date = datetime . strptime (
task . due . date , " % Y- % m- %d " )
2023-01-08 12:42:16 -05:00
start_date = due_date - td
2023-01-13 11:02:53 -05:00
2023-01-08 12:42:16 -05:00
# 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 ,
2023-01-13 11:02:53 -05:00
overview_task_labels ) for child_task in child_tasks ]
2023-01-08 12:42:16 -05:00
continue
except :
logging . warning (
' Wrong start-date format for task: %s . Please use " start=due-<NUM><d or w> " ' , task . content )
continue
2023-01-02 10:28:16 -05:00
2023-01-08 08:58:49 -05:00
# Mark first found task in section
2023-01-13 11:02:53 -05:00
# TODO: is this always true? What about starred tasks?
if next_action_label is not None and first_found [ 1 ] == False :
2023-01-08 08:58:49 -05:00
first_found [ 1 ] = True
# Mark first found section with tasks in project (to account for None section)
2023-01-13 11:02:53 -05:00
if next_action_label is not None and first_found [ 0 ] == False and section_tasks :
2023-01-08 08:58:49 -05:00
first_found [ 0 ] = True
2023-01-03 10:06:46 -05:00
# Return all ids and corresponding labels that need to be modified
2023-01-14 16:01:09 -05:00
return overview_task_ids , overview_task_labels
2021-01-16 11:14:22 -05:00
2021-01-17 08:58:14 -05:00
# Main
2021-01-16 11:14:22 -05:00
2020-05-09 11:08:53 -04:00
def main ( ) :
2020-05-24 05:11:17 -04:00
# Version
2023-01-15 08:05:16 -05:00
current_version = ' v2.0 '
2020-05-24 05:11:17 -04:00
2021-01-16 11:14:22 -05:00
# Main process functions.
2020-05-30 07:14:07 -04:00
parser = argparse . ArgumentParser (
2021-01-17 09:14:33 -05:00
formatter_class = make_wide ( argparse . HelpFormatter , w = 120 , h = 60 ) )
2023-01-13 13:09:33 -05:00
parser . add_argument (
' -a ' , ' --api_key ' , help = ' takes your Todoist API Key. ' , default = os . environ . get ( ' TODOIST_API_KEY ' ) , type = str )
2020-05-24 05:11:17 -04:00
parser . add_argument (
2021-01-17 08:58:14 -05:00
' -l ' , ' --label ' , help = ' enable next action labelling. Define which label to use. ' , type = str )
2020-05-24 05:11:17 -04:00
parser . add_argument (
2023-01-15 07:53:28 -05:00
' -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 )
2020-05-24 05:11:17 -04:00
parser . add_argument (
2021-01-17 08:58:14 -05:00
' -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 )
2020-05-24 05:11:17 -04:00
parser . add_argument (
2021-01-17 08:58:14 -05:00
' -d ' , ' --delay ' , help = ' specify the delay in seconds between syncs (default 5). ' , default = 5 , type = int )
2020-05-24 05:11:17 -04:00
parser . add_argument (
2023-01-08 08:58:49 -05:00
' -p ' , ' --p_suffix ' , help = ' change suffix for parallel labeling (default " = " ). ' , default = ' = ' )
2020-06-13 05:13:03 -04:00
parser . add_argument (
2023-01-08 08:58:49 -05:00
' -s ' , ' --s_suffix ' , help = ' change suffix for sequential labeling (default " - " ). ' , default = ' - ' )
2020-06-13 09:26:56 -04:00
parser . add_argument (
2023-10-25 16:12:22 -04:00
' -df ' , ' --dateformat ' , help = ' Date format of starting date (default " DD-MM-YYYY " ). ' , default = ' DD-MM-YYYY ' )
2020-06-13 09:26:56 -04:00
parser . add_argument (
2021-01-17 08:58:14 -05:00
' -hf ' , ' --hide_future ' , help = ' prevent labelling of future tasks beyond a specified number of days. ' , default = 0 , type = int )
2020-05-30 07:14:07 -04:00
parser . add_argument (
2021-01-17 08:58:14 -05:00
' --onetime ' , help = ' update Todoist once and exit. ' , action = ' store_true ' )
2021-01-17 09:14:33 -05:00
parser . add_argument ( ' --debug ' , help = ' enable debugging and store detailed to a log file. ' ,
2020-05-30 07:14:07 -04:00
action = ' store_true ' )
2021-01-17 09:14:33 -05:00
parser . add_argument ( ' --inbox ' , help = ' the method the Inbox should be processed with. ' ,
2020-05-30 07:14:07 -04:00
default = None , choices = [ ' parallel ' , ' sequential ' ] )
2020-06-13 09:26:56 -04:00
2020-05-09 11:08:53 -04:00
args = parser . parse_args ( )
2023-01-02 10:28:16 -05:00
# #TODO: Temporary disable this feature for now. Find a way to see completed tasks first, since REST API v2 lost this funcionality.
2023-01-14 16:01:09 -05:00
args . regeneration = None
2023-01-02 10:28:16 -05:00
2021-01-17 08:58:14 -05:00
# Addition of regeneration labels
args . regen_label_names = ( ' Regen_off ' , ' Regen_all ' ,
' Regen_all_if_completed ' )
2021-01-16 16:42:46 -05:00
2023-01-03 10:06:46 -05:00
# Set logging
2020-05-24 11:46:51 -04:00
if args . debug :
log_level = logging . DEBUG
else :
log_level = logging . INFO
2020-05-09 16:03:43 -04:00
2023-01-15 07:53:28 -05:00
# Set logging config settings
2020-05-24 11:46:51 -04:00
logging . basicConfig ( level = log_level ,
format = ' %(asctime)s %(levelname)-8s %(message)s ' ,
datefmt = ' % Y- % m- %d % H: % M: % S ' ,
handlers = [ logging . FileHandler (
' debug.log ' , ' w+ ' , ' utf-8 ' ) ,
logging . StreamHandler ( ) ]
)
2020-12-30 07:21:16 -05:00
2020-05-24 05:11:17 -04:00
# Check for updates
check_for_update ( current_version )
2020-05-09 11:08:53 -04:00
# Initialise api
2023-01-03 10:06:46 -05:00
api = initialise_api ( args )
# Initialise SQLite database
connection = initialise_sqlite ( )
2020-05-09 11:08:53 -04:00
2020-12-29 16:56:55 -05:00
# Start main loop
2020-05-09 11:08:53 -04:00
while True :
2020-12-29 16:56:55 -05:00
start_time = time . time ( )
2020-05-24 09:55:47 -04:00
2023-01-02 10:28:16 -05:00
# Evaluate projects, sections, and tasks
2023-01-14 16:01:09 -05:00
overview_task_ids , overview_task_labels = autodoist_magic (
2023-01-03 10:06:46 -05:00
args , api , connection )
2023-01-02 10:28:16 -05:00
# Commit next action label changes
if args . label is not None :
2023-01-14 16:01:09 -05:00
api = commit_labels_update ( api , overview_task_ids ,
overview_task_labels )
# Sync all queued up changes
2023-01-15 05:02:26 -05:00
if api . queue :
sync ( api )
2023-01-14 16:01:09 -05:00
num_changes = len ( api . queue ) + len ( api . overview_updated_ids )
2020-12-29 18:35:07 -05:00
2023-01-13 10:58:16 -05:00
if num_changes :
if num_changes == 1 :
2020-05-24 11:46:51 -04:00
logging . info (
2023-01-13 10:58:16 -05:00
' %d change committed to Todoist. ' , num_changes )
2020-05-24 11:46:51 -04:00
else :
logging . info (
2023-01-13 10:58:16 -05:00
' %d changes committed to Todoist. ' , num_changes )
2020-05-24 09:55:47 -04:00
else :
2020-05-24 11:46:51 -04:00
logging . info ( ' No changes in queue, skipping sync. ' )
2020-05-09 11:08:53 -04:00
# If onetime is set, exit after first execution.
if args . onetime :
break
2021-01-16 11:14:22 -05:00
2020-12-30 07:21:16 -05:00
# Set a delay before next sync
2020-12-29 16:56:55 -05:00
end_time = time . time ( )
delta_time = end_time - start_time
2020-05-09 11:08:53 -04:00
2020-12-29 16:56:55 -05:00
if args . delay - delta_time < 0 :
2021-01-16 11:14:22 -05:00
logging . debug (
' Computation time %d is larger than the specified delay %d . Sleeping skipped. ' , delta_time , args . delay )
2020-12-29 16:56:55 -05:00
elif args . delay > = 0 :
sleep_time = args . delay - delta_time
logging . debug ( ' Sleeping for %d seconds ' , sleep_time )
time . sleep ( sleep_time )
2020-06-07 08:59:47 -04:00
2021-01-16 11:14:22 -05:00
2020-05-09 11:08:53 -04:00
if __name__ == ' __main__ ' :
2021-01-16 11:14:22 -05:00
main ( )