blumeops/mise-tasks/blumeops-tasks
Erich Blume 234c46c302 Filter blumeops-tasks to hide future-dated tasks (#124)
## Summary
- Tasks with a due date are now only shown when due today or earlier
- Recurring tasks stay hidden until their next occurrence is actionable
- Tasks without a due date continue to always display

## Test plan
- [x] Ran `mise run blumeops-tasks` — verified 18 undated tasks display correctly
- [x] Confirmed "BlumeOps doc review" (due tomorrow) is correctly hidden

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/124
2026-02-08 10:38:44 -08:00

156 lines
4.6 KiB
Text
Executable file

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = ["httpx>=0.27.0", "rich>=13.0.0"]
# ///
#MISE description="List Blumeops tasks from Todoist sorted by priority"
"""Fetch and display Blumeops tasks from Todoist, sorted by priority.
This script is specific to Erich Blume's personal development workflow and
is not intended for general use. It requires:
- A 1Password CLI (`op`) configured with access to the author's vault
- A Todoist account with a project named "Blumeops"
The script fetches tasks and displays them sorted by a custom priority order:
p1 (urgent), p2 (high), p4 (normal/default), p3 (backlog). The p3-last ordering
reflects a deliberate choice to treat p3 as "backlog" rather than moderate
priority.
Usage: mise run blumeops-tasks
"""
import subprocess
import sys
from datetime import date
import httpx
from rich.console import Console
from rich.text import Text
TODOIST_API_BASE = "https://api.todoist.com/rest/v2"
PROJECT_NAME = "Blumeops"
# Priority mapping: Todoist API uses 1=normal(p4), 2=moderate(p3), 3=high(p2), 4=urgent(p1)
# User wants order: p1, p2, p4, p3 (p3 is backlog, goes last)
PRIORITY_LABELS = {4: "p1", 3: "p2", 1: "p4", 2: "p3"}
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",
],
capture_output=True,
text=True,
)
if result.returncode != 0:
raise RuntimeError(f"Failed to get Todoist token from 1Password: {result.stderr}")
return result.stdout.strip()
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"]
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()
def is_due(task: dict) -> bool:
"""Check if a task should be displayed based on its due date.
Tasks without a due date are always shown. Tasks with a due date
are only shown when the date is today or in the past.
"""
due = task.get("due")
if due is None:
return True
due_date = date.fromisoformat(due["date"][:10])
return due_date <= date.today()
def sort_tasks(tasks: list[dict]) -> list[dict]:
"""Sort tasks by custom priority order: p1, p2, p4, p3."""
return sorted(tasks, key=lambda t: PRIORITY_SORT_ORDER.get(t["priority"], 5))
def main() -> int:
console = Console()
# Get API token
try:
token = get_todoist_token()
except RuntimeError as e:
console.print(f"[red]Error:[/red] {e}")
return 1
# Create HTTP client with auth header
with httpx.Client(headers={"Authorization": f"Bearer {token}"}) as client:
# Find project
try:
project_id = get_project_id(client, PROJECT_NAME)
except RuntimeError as e:
console.print(f"[red]Error:[/red] {e}")
return 1
# Get, filter, and sort tasks
tasks = get_tasks(client, project_id)
tasks = [t for t in tasks if is_due(t)]
sorted_tasks = sort_tasks(tasks)
if not sorted_tasks:
console.print("No tasks found in Blumeops project")
return 0
# Display tasks
console.print(f"[bold]Blumeops Tasks[/bold] ({len(sorted_tasks)} tasks)")
console.print("=" * 40)
console.print()
for task in sorted_tasks:
priority = task["priority"]
label = PRIORITY_LABELS.get(priority, "p?")
content = task["content"]
description = task.get("description", "")
# Header line with priority and content
header = Text()
header.append(f"[{label}]", style="bold")
header.append(f" {content}")
console.print(header)
# Description indented
if description:
for line in description.split("\n"):
console.print(f" {line}", style="dim")
console.print()
return 0
if __name__ == "__main__":
sys.exit(main())