Files
Todoist-Actual-Backup/export_todoist.py
2025-10-17 07:25:07 -04:00

173 lines
6.0 KiB
Python

import os
import sys
import json
import requests
from datetime import datetime, timedelta
from todoist_api_python.api import TodoistAPI
from jinja2 import Environment, FileSystemLoader, select_autoescape
ATTACHMENTS_DIR = "attachments"
def usage():
print("""
Todoist Export Script
---------------------
Exports all active and completed tasks from the Todoist API to a JSON file, including attachments and comments, and generates a human-readable HTML backup using Jinja2.
Usage:
python export_todoist.py export
- Exports all data and generates JSON and HTML files.
python export_todoist.py [any other argument or none]
- Shows this help message.
""")
def get_api_key():
key = os.environ.get("TODOIST_KEY")
if not key:
print("Error: TODOIST_KEY environment variable not set.")
sys.exit(1)
return key
def ensure_attachments_dir():
if not os.path.exists(ATTACHMENTS_DIR):
os.makedirs(ATTACHMENTS_DIR)
def download_attachment(url, filename):
local_path = os.path.join(ATTACHMENTS_DIR, filename)
if os.path.exists(local_path):
return local_path
print(f"Downloading attachment {url}")
r = requests.get(url, stream=True)
if r.status_code == 200:
with open(local_path, 'wb') as f:
for chunk in r.iter_content(1024):
f.write(chunk)
return local_path
else:
return None
def fetch_all_projects(api):
ret = []
projects_iter = api.get_projects()
for projects in projects_iter:
for project in projects:
name = getattr(project, 'name', None)
id = getattr(project, 'id', None)
print(f"Found project {name} with ID {id}")
ret.append(project)
return ret
def fetch_all_completed_tasks(api, project_id):
# Fetch all completed tasks for a project using get_completed_tasks_by_completion_date
# The API only allows up to 3 months per call, so we fetch just once for the last 3 months
all_completed = []
since = (datetime.now() - timedelta(days=90)).replace(hour=0, minute=0, second=0, microsecond=0)
until = datetime.now()
try:
completed_iter = api.get_completed_tasks_by_completion_date(since=since, until=until)
for completed_list in completed_iter:
for task in completed_list:
if hasattr(task, 'project_id') and str(task.project_id) == str(project_id):
all_completed.append(task)
except Exception as e:
print(f"Error fetching completed tasks for {since} to {until}: {e}")
print(f"Found {len(all_completed)} completed tasks for project {project_id}")
return all_completed
def fetch_all_tasks(api, project_id, completed=False):
if completed:
return fetch_all_completed_tasks(api, project_id)
else:
tasks = []
try:
tasks_iter = api.get_tasks(project_id=project_id)
for batch in tasks_iter:
for task in batch:
tasks.append(task)
except Exception as e:
print(f"Error fetching active tasks for project {project_id}: {e}")
print(f"Found {len(tasks)} completed tasks for project {project_id}")
return tasks
def fetch_comments(api, task_id):
comments = []
try:
comments_iter = api.get_comments(task_id=task_id)
for batch in comments_iter:
for comment in batch:
comments.append(comment)
except Exception:
return []
return comments
def process_task(api, task, completed=False):
task_dict = task.__dict__.copy()
# Attachments (if any)
attachments = []
if hasattr(task, 'attachments') and task.attachments:
for att in task.attachments:
att_dict = att.__dict__.copy()
if 'file_url' in att_dict and att_dict['file_url']:
filename = att_dict.get('file_name') or os.path.basename(att_dict['file_url'])
local_path = download_attachment(att_dict['file_url'], filename)
if local_path:
att_dict['local_file'] = os.path.relpath(local_path)
attachments.append(att_dict)
if attachments:
task_dict['attachments'] = attachments
# Comments
comments = fetch_comments(api, task.id)
if comments:
task_dict['comments'] = [c.__dict__ for c in comments]
return task_dict
def main():
if len(sys.argv) != 2 or sys.argv[1] != "export":
usage()
return
ensure_attachments_dir()
api = TodoistAPI(get_api_key())
projects = fetch_all_projects(api)
data = []
for project in projects:
project_dict = project.__dict__.copy()
project_id = project.id
# Active tasks
active_tasks = fetch_all_tasks(api, project_id, completed=False)
# Completed tasks
completed_tasks = fetch_all_tasks(api, project_id, completed=True)
project_dict['tasks'] = [process_task(api, t, completed=False) for t in active_tasks]
project_dict['completed_tasks'] = [process_task(api, t, completed=True) for t in completed_tasks]
data.append(project_dict)
# Write JSON
today = datetime.now().strftime("%Y-%m-%d")
json_filename = f"Todoist-Actual-Backup-{today}.json"
def json_serial(obj):
if isinstance(obj, datetime):
return obj.isoformat()
return str(obj)
with open(json_filename, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2, default=json_serial)
print(f"Exported data to {json_filename}")
# Write HTML
env = Environment(
loader=FileSystemLoader(os.path.dirname(__file__)),
autoescape=select_autoescape(['html', 'xml'])
)
template = env.get_template("todoist_backup_template.html")
html_filename = f"Todoist-Actual-Backup-{today}.html"
with open(html_filename, "w", encoding="utf-8") as f:
f.write(template.render(projects=data, date=today))
print(f"Generated HTML backup at {html_filename}")
if __name__ == "__main__":
main()