Files
my2dos/scripts/my2dos_remote_tasks.py
2026-09-04 11:44:49 +02:00

108 lines
3.8 KiB
Python
Executable File

#!/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()