Improve task filters and tag collections

This commit is contained in:
rucki
2026-09-04 11:44:49 +02:00
parent e04385c23d
commit c6309cabd8
12 changed files with 342 additions and 13 deletions
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""Fetch and update remote my2dos tasks via the JSON API.
Configuration is read from environment variables or from .env in the project root:
- MY2DOS_API_URL, e.g. https://my2dos.rucki.ch
- MY2DOS_API_KEY, API key token
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen
ROOT = Path(__file__).resolve().parents[1]
def load_dotenv(path: Path = ROOT / ".env") -> None:
if not path.exists():
return
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
def config() -> tuple[str, str]:
load_dotenv()
base_url = os.environ.get("MY2DOS_API_URL", "").rstrip("/")
api_key = os.environ.get("MY2DOS_API_KEY", "")
if not base_url or not api_key:
print("Missing MY2DOS_API_URL or MY2DOS_API_KEY. Configure them in .env.", file=sys.stderr)
sys.exit(2)
return base_url, api_key
def request_json(method: str, path: str, data: dict[str, Any] | None = None, query: dict[str, str] | None = None) -> Any:
base_url, api_key = config()
url = f"{base_url}{path}"
if query:
url = f"{url}?{urlencode(query)}"
body = json.dumps(data).encode("utf-8") if data is not None else None
request = Request(
url,
data=body,
method=method,
headers={"X-Api-Key": api_key, "Accept": "application/json", "Content-Type": "application/json"},
)
try:
with urlopen(request, timeout=20) as response:
return json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
details = exc.read().decode("utf-8", errors="replace")
print(f"HTTP {exc.code} for {method} {url}: {details}", file=sys.stderr)
sys.exit(1)
except URLError as exc:
print(f"Request failed for {method} {url}: {exc}", file=sys.stderr)
sys.exit(1)
def list_tasks(args: argparse.Namespace) -> None:
payload = request_json("GET", "/api/items/", query={"limit": str(args.limit)})
items = payload.get("items", [])
tasks = [item for item in items if item.get("kind") == "todo"]
if args.open:
tasks = [item for item in tasks if not item.get("is_done")]
for item in tasks:
status = "x" if item.get("is_done") else " "
due = f" due:{item['due_at'][:10]}" if item.get("due_at") else ""
tags = " ".join(f"#{tag}" for tag in item.get("tags", []))
print(f"[{status}] #{item['id']}{due} {item.get('content', '').strip()} {tags}".rstrip())
def mark_done(args: argparse.Namespace) -> None:
item = request_json("PATCH", f"/api/items/{args.id}/", data={"is_done": True})
print(f"Done: #{item['id']} {item.get('content', '').strip()}")
def main() -> None:
parser = argparse.ArgumentParser(description="Work with remote my2dos tasks.")
subparsers = parser.add_subparsers(dest="command")
list_parser = subparsers.add_parser("list", help="List remote todos")
list_parser.add_argument("--all", dest="open", action="store_false", help="include completed todos")
list_parser.add_argument("--limit", type=int, default=500)
list_parser.set_defaults(func=list_tasks, open=True)
done_parser = subparsers.add_parser("done", help="Mark a todo as done")
done_parser.add_argument("id", type=int)
done_parser.set_defaults(func=mark_done)
args = parser.parse_args()
if not hasattr(args, "func"):
args = parser.parse_args(["list"])
args.func(args)
if __name__ == "__main__":
main()