Compare commits

...

8 Commits

Author SHA1 Message Date
Hoffelhas 2ce81df5bd
Update README to v2.0 functionalities 2023-01-15 18:10:43 +01:00
Hoffelhas 956f5176af Final preparations for v2.0 2023-01-15 16:38:34 +01:00
Hoffelhas 8fe99f4942
Update README to v2.0.0 functionality. 2023-01-15 15:50:00 +01:00
Hoffelhas 78dca0483e
Update README to v2.0.0 functionality. 2023-01-15 15:48:22 +01:00
Hoffelhas 565967cdc1
Update README to v2.0.0 functionality. 2023-01-15 15:44:37 +01:00
Hoffelhas d6d1ba121f
Update README to v2.0.0 functionality. 2023-01-15 15:42:17 +01:00
Hoffelhas 8b23fbd59a Final preparations for v2.0 2023-01-15 14:08:25 +01:00
Hoffelhas a4f99fa0c1 Minor clean-up of code 2023-01-15 13:53:28 +01:00
3 changed files with 89 additions and 66 deletions

View File

@ -1,5 +1,7 @@
# Autodoist
*Note: v2.0 is a major overhaul of Autodoist, so please be sure to view the README in order to get up to speed with the latest changes. Thanks to everyone for helping out and supporting this project!*
This program adds four major functionalities to Todoist to help automate your workflow:
1) Assign automatic next-action labels for a more GTD-like workflow
@ -7,7 +9,7 @@ This program adds four major functionalities to Todoist to help automate your wo
- Limit labels based on a start-date or hide future tasks based on the due date
2) Enable regeneration of sub-tasks in lists with a recurring date. Multiple modes possile.
3) Postpone the end-of-day time to after midnight to finish your daily recurring tasks
4) Make multiple items (un)checkable at the same time
4) Make multiple tasks (un)checkable at the same time
If this tool helped you out, I would really appreciate your support by providing me with some coffee!
@ -29,11 +31,11 @@ For your convenience a requirements.txt is provided, which allows you to install
# 1. Automatic next action labels
The program looks for pre-defined tags in the name of every project, section, or parentless tasks in your Todoist account to automatically add and remove `@next_action` labels. To create a simple list of all your next actions you can add a new filter in your Todoist with e.g.: @next_action & #project_name.
The program looks for pre-defined tags in the name of every project, section, or parentless tasks in your Todoist account to automatically add and remove `@next_action` labels.
Projects, sections, and parentless tasks can be tagged independently from each other to create the required functionality. If this tag is not defined, it will not activate this functionality. The result will be a clear, current and comprehensive list of next actions without the need for further thought.
See the example given at [running Autodoist](#running-autodoist) on how to run this mode. If the label does not exist yet in your Todoist, a possibility is given to automatically create it. Todoist Premium is required in order to use labels and to make this functionality possible.
See the example given at [running Autodoist](#running-autodoist) on how to run this mode. If the label does not exist yet in your Todoist, a possibility is given to automatically create it.
## Useful filter tip
@ -43,33 +45,55 @@ For a more GTD-like workflow, you can use Todoist filters to create a clean and
## Sequential processing
If a project, section, or parentless task ends with `--`, both the parentless tasks and its sub-tasks will be treated as a priority queue and the most important will be labeled. Importance is determined by order in the list.
If a project, section, or parentless task ends with a dash `-`, the tasks will be treated suquentially in a priority queue, where only the first task that is found is labeled. If a task contains sub-tasks, the first lowest task is labeled instead.
![Serial task](https://i.imgur.com/SUkhPiE.gif)
![Sequential task labeling](https://i.imgur.com/ZUKbA8E.gif)
## Parallel processing
If a project, section, or parentless task name ends with `//`, both the parentless tasks and its sub-tasks will be treated as parallel. A waterfall processing is applied, where the lowest possible sub-tasks are labelled.
If a project, section, or parentless task name ends with an equal sign `=`, all tasks will be treated in parallel. A waterfall processing is applied, where the lowest possible (sub-)tasks are labelled.
![Parallel task](https://i.imgur.com/NPTLQ8B.gif)
![Parallel task labeling](https://i.imgur.com/xZZ0kEM.gif)
## Advanced labelling
If a project or section ends with `-/`, all parentless tasks are processed sequentially, and its sub-tasks in parallel.
Projects, sections, and (parentless) tasks can be used to specify how the levels under them should behave. This means that:
[See example](https://i.imgur.com/uGJFeXB.gif)
- A project can accept up to three tags, to specify how the sections, parentless tasks, and subtasks should behave.
- A section can accept up to two tags, to specify parentless tasks and subtasks should behave.
- A task at any level can be labelled with one tag, to specifcy how its sub-tasks should behave.
If a project or section ends with `/-`, all parentless tasks are processed in parallel, and its sub-tasks sequentially.
Tags can be applied one each level simultaneously , where the lower level setting will always override the one specified in the levels above.
[See example](https://i.imgur.com/5lZ1BVI.gif)
### Shorthand notation
Any parentless task can also be be given a type by appending `//` or `--` to the name of the task. This works if there is no project type, and will override a previously defined project type.
If fewer tags then needed are specified, the last one is simply copied. E.g. if a project has the tag `=` this is similar to `===`, or if a project has `=-` this is similar to `=--`. Same for sections, `=` is similar to `==`.
[See example 1 with a parallel project](https://i.imgur.com/d9Qfq0v.gif)
### Project labeling examples
- If a project ends with `---`, only the first section has tasks that are handled sequentially.
- If a project ends with `=--`, all sections have tasks that are handled sequentially.
- If a project ends with `-=-`, only the first section has parallel parentless tasks with sequential sub-tasks.
- If a project ends with `--=`, only the first section and first parentless tasks has parallel sub-tasks.
- If a project ends with `==-`, all sections and all parentless tasks will have sub-tasks are handled sequentially.
- If a project ends with `=-=`, all sections will have parentless tasks that are processed sequentially, but all sub-tasks are handled in parallel.
- If a project ends with `-==`, only the first section has parallel tasks.
- If a project ends with `===`, all tasks are handled in parallel.
[See example 2 with a serial project](https://i.imgur.com/JfaAOzZ.gif)
### Section labeling examples
- If a section ends with `--`, only the first parentless task will have sub-tasks that are handled sequentially.
- If a section ends with `=-`, all parentless tasks will have sub-tasks that are handled sequentially.
- If a section ends with `-=`, only the first parentless task has sub-tasks that are handled in parallel.
- If a section ends with `==`, all tasks are handled in parallel.
### Tasks labeling examples
- If a task ends with `-`, the sub-tasks are handled sequentially.
- If a task ends with `=`, the sub-tasks are handled in parallel.
### Kanban board labeling
A standard workflow for Kanban boards is to have one actionable task per column/section, which is then moved to the next column when needed. Most often the most right column is the 'done' section. To ensure that every column only has one labelled task and the last column contains no labelled tasks, you could do either of two things:
- Add the `=--` tag to the project name, and disable labelling for the 'done' section by adding `*` to either the start or end of the section name.
- Add the `--` tag to every section that you want to have labels.
Note: Todoist sections don't like to have a slash in the name, it will automatically change to an underscore. The default label options will recognize this to make it work regardless. Of course you're always free to define your own custom label symbols.
## Start/Due date enhanced experience
@ -83,7 +107,13 @@ Two methods are provided to hide tasks that are not relevant yet.
# 2. Regenerate sub-tasks in recurring lists
The program looks for all parentless tasks with a recurring date. If they contain sub-tasks, they will be regenerated in the same order when the parentless task is checked. Todoist Premium is not required for this functionality.
*DISCLAIMER: This feature has been disabled for now due to two reasons:*
- *Regeneration is a [core feature of Todoist nowadays](https://todoist.com/help/articles/can-i-reset-sub-tasks). This was made possible thanks to all of you who are using and supporting Autodoist, which resulted in Doist to include this too! Thank you all for making this happen!*
- *In the new REST API v2 it's currently not possible to see completed tasks, which makes regeneration a bit difficult.*
*Nevertheless, the Todoist implementation is still more limited than Autodoist, it does not restore the original order of the sub-tasks, and deeper sub-tasks can't be reset. I therefore believe it is still useful for this feature to be re-enabled in the near future.*
Autodoist looks for all parentless tasks with a recurring date. If they contain sub-tasks, they will be regenerated in the same order when the parentless task is checked.
![See example](https://i.imgur.com/WKKd14o.gif)
@ -100,15 +130,15 @@ In addition you can override the overall mode by adding the labels `Regen_off`,
You have a daily recurring task, but you're up working late and now it's past midnight. When this happens Todoist will automatically mark it overdue, and when checked by you it moves to tomorrow. This means that after a good nights rest you can't complete the task that day!
By setting an alternative time for the end-of-day you can now finish your work after midnight and the new date will automatically be corrected for you. Todoist Premium is not required for this functionality.
By setting an alternative time for the end-of-day you can now finish your work after midnight and the new date will automatically be corrected for you.
![See example 1](https://i.imgur.com/tvnTMOJ.gif)
# 4. Make multiple items uncheckable / re-checkable at the same time
# 4. Make multiple tasks uncheckable / re-checkable at the same time
Todoist allows the asterisk symbol `* ` to be used to ensure tasks can't be checked by turning them into headers. Now you are able to do this en masse!
Simply add `** ` or `!* ` in front of a project, section, or top item, to automatically turn all the items that it includes into respectively headers or checkable tasks.
Simply add `** ` or `-* ` in front of a project, section, or parentless task to automatically turn all the tasks that it includes into respectively headers or checkable tasks.
# Executing Autodoist
@ -136,10 +166,12 @@ These modes can be run individually, or combined with each other.
Several additional arguments can be provided, for example to change the suffix tags for parallel and sequential projects:
python autodoist.py --pp_suffix <tag>
python autodoist.py --ss_suffix <tag>
python autodoist.py --p_suffix <tag>
python autodoist.py --s_suffix <tag>
Or if you want to hide all tasks due in the future:
Note: Be aware that Todoist sections don't like to have a slash '/' in the name, which will automatically change to an underscore. Detection of the tag will not work.
If you want to hide all tasks due in the future:
python autodoist.py --hf <NUMBER_OF_DAYS>

View File

@ -5,7 +5,6 @@ from todoist_api_python.models import Task
from todoist_api_python.models import Section
from todoist_api_python.models import Project
from todoist_api_python.http_requests import get
from todoist_api_python.http_requests import post
from urllib.parse import urljoin
from urllib.parse import quote
import sys
@ -19,8 +18,6 @@ import sqlite3
import os
import re
import json
from collections import defaultdict
# Connect to SQLite database
@ -306,7 +303,7 @@ def verify_label_existance(api, label_name, prompt_mode):
try:
api.add_label(name=label_name)
except Exception as error:
print(error)
logging.warning(error)
labels = api.get_labels()
label = [x for x in labels if x.name == label_name]
@ -365,18 +362,13 @@ def initialise_api(args):
# Run the initial sync
logging.debug('Connecting to the Todoist API')
api_arguments = {'token': args.api_key}
if args.nocache:
logging.debug('Disabling local caching')
api_arguments['cache'] = None
api = TodoistAPI(**api_arguments)
logging.info("Autodoist has successfully connected to Todoist!")
sync_api = initialise_sync_api(api)
api.sync_token = sync_api['sync_token'] # Save SYNC API token to enable partial syncs
# Save SYNC API token to enable partial syncs
api.sync_token = sync_api['sync_token']
# Check if labels exist
@ -452,7 +444,11 @@ def initialise_sync_api(api):
}
data = 'sync_token=*&resource_types=["all"]'
response = requests.post('https://api.todoist.com/sync/v9/sync', headers=headers, data=data)
try:
response = requests.post(
'https://api.todoist.com/sync/v9/sync', headers=headers, data=data)
except Exception as e:
logging.error(f"Error during initialise_sync_api: '{e}'")
return json.loads(response.text)
@ -507,7 +503,8 @@ def sync(api):
'Content-Type': 'application/x-www-form-urlencoded',
}
data = 'sync_token=' + api.sync_token + '&commands=' + json.dumps(api.queue)
data = 'sync_token=' + api.sync_token + \
'&commands=' + json.dumps(api.queue)
response = requests.post(
'https://api.todoist.com/sync/v9/sync', headers=headers, data=data)
@ -646,7 +643,7 @@ def get_task_type(args, connection, task, section, project):
# Logic to track addition of a label to a task
def add_label(connection, task, dominant_type, label, overview_task_ids, overview_task_labels):
def add_label(task, label, overview_task_ids, overview_task_labels):
if label not in task.labels:
labels = task.labels # To also copy other existing labels
logging.debug('Updating \'%s\' with label', task.content)
@ -981,12 +978,12 @@ def autodoist_magic(args, api, connection):
# Get all todoist info
try:
all_projects = api.get_projects() # To save on request to stay under the limit
all_sections = api.get_sections() # To save on request to stay under the limit
all_projects = api.get_projects() # To save on request to stay under the limit
all_sections = api.get_sections() # To save on request to stay under the limit
all_tasks = api.get_tasks()
except Exception as error:
print(error)
logging.error(error)
for project in all_projects:
@ -1011,9 +1008,10 @@ def autodoist_magic(args, api, connection):
# Get all tasks for the project
try:
project_tasks = [t for t in all_tasks if t.project_id == project.id]
project_tasks = [
t for t in all_tasks if t.project_id == project.id]
except Exception as error:
print(error)
logging.warning(error)
# If a project type has changed, clean all tasks in this project for good measure
if next_action_label is not None:
@ -1025,12 +1023,6 @@ def autodoist_magic(args, api, connection):
db_update_value(connection, task, 'parent_type', None)
# Run for both non-sectioned and sectioned tasks
# Get completed tasks:
# endpoint = 'https://api.todoist.com/sync/v9/completed/get_all'
# get(api._session, endpoint, api._token, '0')['items']
# $ curl https://api.todoist.com/sync/v9/sync-H "Authorization: Bearer e2f750b64e8fc06ae14383d5e15ea0792a2c1bf3" -d commands='[ {"type": "item_add", "temp_id": "63f7ed23-a038-46b5-b2c9-4abda9097ffa", "uuid": "997d4b43-55f1-48a9-9e66-de5785dfd69b", "args": {"content": "Buy Milk", "project_id": "2203306141","labels": ["Food", "Shopping"]}}]'
# for s in [0,1]:
# if s == 0:
# sections = Section(None, None, 0, project.id)
@ -1045,7 +1037,7 @@ def autodoist_magic(args, api, connection):
sections = [s for s in all_sections if s.project_id == project.id]
sections.insert(0, Section(None, None, 0, project.id))
except Exception as error:
print(error)
logging.debug(error)
# Reset
first_found[0] = False
@ -1223,6 +1215,7 @@ def autodoist_magic(args, api, connection):
# Inherit project type
dominant_type = project_type
# TODO: optimise below code
# If indicated on project level
if dominant_type[0] == 's':
if not first_found[0]:
@ -1230,7 +1223,7 @@ def autodoist_magic(args, api, connection):
if dominant_type[1] == 's':
if not first_found[1]:
add_label(
connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels)
task, next_action_label, overview_task_ids, overview_task_labels)
elif next_action_label in task.labels:
# Probably the task has been manually moved, so if it has a label, let's remove it.
@ -1239,14 +1232,14 @@ def autodoist_magic(args, api, connection):
elif dominant_type[1] == 'p':
add_label(
connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels)
task, next_action_label, overview_task_ids, overview_task_labels)
elif dominant_type[0] == 'p':
if dominant_type[1] == 's':
if not first_found[1]:
add_label(
connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels)
task, next_action_label, overview_task_ids, overview_task_labels)
elif next_action_label in task.labels:
# Probably the task has been manually moved, so if it has a label, let's remove it.
@ -1255,13 +1248,13 @@ def autodoist_magic(args, api, connection):
elif dominant_type[1] == 'p':
add_label(
connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels)
task, next_action_label, overview_task_ids, overview_task_labels)
# If indicated on section level
if dominant_type[0] == 'x' and dominant_type[1] == 's':
if not first_found[1]:
add_label(
connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels)
task, next_action_label, overview_task_ids, overview_task_labels)
elif next_action_label in task.labels:
# Probably the task has been manually moved, so if it has a label, let's remove it.
@ -1269,14 +1262,14 @@ def autodoist_magic(args, api, connection):
task, next_action_label, overview_task_ids, overview_task_labels)
elif dominant_type[0] == 'x' and dominant_type[1] == 'p':
add_label(connection, task, dominant_type, next_action_label,
add_label(task, next_action_label,
overview_task_ids, overview_task_labels)
# If indicated on parentless task level
if dominant_type[1] == 'x' and dominant_type[2] == 's':
if not first_found[1]:
add_label(
connection, task, dominant_type, next_action_label, overview_task_ids, overview_task_labels)
task, next_action_label, overview_task_ids, overview_task_labels)
if next_action_label in task.labels:
# Probably the task has been manually moved, so if it has a label, let's remove it.
@ -1284,7 +1277,7 @@ def autodoist_magic(args, api, connection):
task, next_action_label, overview_task_ids, overview_task_labels)
elif dominant_type[1] == 'x' and dominant_type[2] == 'p':
add_label(connection, task, dominant_type, next_action_label,
add_label(task, next_action_label,
overview_task_ids, overview_task_labels)
# If a parentless or sub-task which has children
@ -1326,7 +1319,7 @@ def autodoist_magic(args, api, connection):
# Pass label down to the first child
if not child_task.is_completed and next_action_label in task.labels:
add_label(
connection, child_task, dominant_type, next_action_label, overview_task_ids, overview_task_labels)
child_task, next_action_label, overview_task_ids, overview_task_labels)
remove_label(
task, next_action_label, overview_task_ids, overview_task_labels)
@ -1346,7 +1339,7 @@ def autodoist_magic(args, api, connection):
if not child_task.is_completed:
add_label(
connection, child_task, dominant_type, next_action_label, overview_task_ids, overview_task_labels)
child_task, next_action_label, overview_task_ids, overview_task_labels)
# Remove labels based on start / due dates
@ -1432,7 +1425,6 @@ def autodoist_magic(args, api, connection):
first_found[1] = True
# Mark first found section with tasks in project (to account for None section)
# TODO: is this always true? What about starred tasks?
if next_action_label is not None and first_found[0] == False and section_tasks:
first_found[0] = True
@ -1445,7 +1437,7 @@ def autodoist_magic(args, api, connection):
def main():
# Version
current_version = 'v1.5'
current_version = 'v2.0'
# Main process functions.
parser = argparse.ArgumentParser(
@ -1455,7 +1447,7 @@ def main():
parser.add_argument(
'-l', '--label', help='enable next action labelling. Define which label to use.', type=str)
parser.add_argument(
'-r', '--regeneration', help='enable regeneration of sub-tasks in recurring lists. Chose overall mode: 0 - regen off, 1 - regen all (default), 2 - regen only if all sub-tasks are completed. Task labels can be used to overwrite this mode.', nargs='?', const='1', default=None, type=int)
'-r', '--regeneration', help='[CURRENTLY DISABLED FEATURE] enable regeneration of sub-tasks in recurring lists. Chose overall mode: 0 - regen off, 1 - regen all (default), 2 - regen only if all sub-tasks are completed. Task labels can be used to overwrite this mode.', nargs='?', const='1', default=None, type=int)
parser.add_argument(
'-e', '--end', help='enable alternative end-of-day time instead of default midnight. Enter a number from 1 to 24 to define which hour is used.', type=int)
parser.add_argument(
@ -1465,13 +1457,11 @@ def main():
parser.add_argument(
'-s', '--s_suffix', help='change suffix for sequential labeling (default "-").', default='-')
parser.add_argument(
'-df', '--dateformat', help='strptime() format of starting date (default "%%d-%%m-%%Y").', default='%d-%m-%Y')
'-df', '--dateformat', help='[CURRENTLY DISABLED FEATURE] strptime() format of starting date (default "%%d-%%m-%%Y").', default='%d-%m-%Y')
parser.add_argument(
'-hf', '--hide_future', help='prevent labelling of future tasks beyond a specified number of days.', default=0, type=int)
parser.add_argument(
'--onetime', help='update Todoist once and exit.', action='store_true')
parser.add_argument(
'--nocache', help='disables caching data to disk for quicker syncing.', action='store_true')
parser.add_argument('--debug', help='enable debugging and store detailed to a log file.',
action='store_true')
parser.add_argument('--inbox', help='the method the Inbox should be processed with.',
@ -1492,6 +1482,7 @@ def main():
else:
log_level = logging.INFO
# Set logging config settings
logging.basicConfig(level=log_level,
format='%(asctime)s %(levelname)-8s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',

View File

@ -1,2 +1,2 @@
requests>=2.28.1
todoist_api_python>=2.0.2
requests==2.28.1
todoist_api_python==2.0.2