From faebc98c3cf5c1120e01841535735731e0f023c1 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 11 Feb 2026 14:33:37 -0800 Subject: [PATCH] Fix blumeops-tasks for Todoist API v1 migration (#155) ## Summary - Migrate from deprecated Todoist REST API v2 (`410 Gone`) to new unified API v1 - Add cursor-based pagination for project and task listing endpoints - Switch 1Password credential retrieval from `op item get --fields` to `op read` ## Testing - [x] `mise run blumeops-tasks` returns all 9 tasks successfully - [x] Pre-commit hooks pass Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/155 --- ...ix-blumeops-tasks-todoist-api-v1.bugfix.md | 1 + mise-tasks/blumeops-tasks | 52 +++++++++++-------- 2 files changed, 31 insertions(+), 22 deletions(-) create mode 100644 docs/changelog.d/fix-blumeops-tasks-todoist-api-v1.bugfix.md diff --git a/docs/changelog.d/fix-blumeops-tasks-todoist-api-v1.bugfix.md b/docs/changelog.d/fix-blumeops-tasks-todoist-api-v1.bugfix.md new file mode 100644 index 0000000..6225e34 --- /dev/null +++ b/docs/changelog.d/fix-blumeops-tasks-todoist-api-v1.bugfix.md @@ -0,0 +1 @@ +Fix blumeops-tasks: migrate from deprecated Todoist REST API v2 to API v1, handle cursor-based pagination, and use `op read` for 1Password credential retrieval. diff --git a/mise-tasks/blumeops-tasks b/mise-tasks/blumeops-tasks index e3500cd..c83d724 100755 --- a/mise-tasks/blumeops-tasks +++ b/mise-tasks/blumeops-tasks @@ -28,7 +28,7 @@ import httpx from rich.console import Console from rich.text import Text -TODOIST_API_BASE = "https://api.todoist.com/rest/v2" +TODOIST_API_BASE = "https://api.todoist.com/api/v1" PROJECT_NAME = "Blumeops" # Priority mapping: Todoist API uses 1=normal(p4), 2=moderate(p3), 3=high(p2), 4=urgent(p1) @@ -40,17 +40,7 @@ PRIORITY_SORT_ORDER = {4: 1, 3: 2, 1: 3, 2: 4} # Lower = earlier def get_todoist_token() -> str: """Retrieve Todoist API token from 1Password.""" result = subprocess.run( - [ - "op", - "--vault", - "vg6xf6vvfmoh5hqjjhlhbeoaie", - "item", - "get", - "c53h3xnmswhvexa5mntoyvhgpm", - "--fields", - "credential", - "--reveal", - ], + ["op", "read", "op://vg6xf6vvfmoh5hqjjhlhbeoaie/c53h3xnmswhvexa5mntoyvhgpm/credential"], capture_output=True, text=True, ) @@ -61,22 +51,40 @@ def get_todoist_token() -> str: def get_project_id(client: httpx.Client, project_name: str) -> str: """Find project ID by name.""" - response = client.get(f"{TODOIST_API_BASE}/projects") - response.raise_for_status() - projects = response.json() - - for project in projects: - if project["name"] == project_name: - return project["id"] + cursor = None + while True: + params = {} + if cursor: + params["cursor"] = cursor + response = client.get(f"{TODOIST_API_BASE}/projects", params=params) + response.raise_for_status() + data = response.json() + for project in data.get("results", data if isinstance(data, list) else []): + if project["name"] == project_name: + return project["id"] + cursor = data.get("next_cursor") if isinstance(data, dict) else None + if not cursor: + break raise RuntimeError(f"Project '{project_name}' not found in Todoist") def get_tasks(client: httpx.Client, project_id: str) -> list[dict]: """Get all tasks for a project.""" - response = client.get(f"{TODOIST_API_BASE}/tasks", params={"project_id": project_id}) - response.raise_for_status() - return response.json() + tasks = [] + cursor = None + while True: + params = {"project_id": project_id} + if cursor: + params["cursor"] = cursor + response = client.get(f"{TODOIST_API_BASE}/tasks", params=params) + response.raise_for_status() + data = response.json() + tasks.extend(data.get("results", data if isinstance(data, list) else [])) + cursor = data.get("next_cursor") if isinstance(data, dict) else None + if not cursor: + break + return tasks def is_due(task: dict) -> bool: