Improve task filters and tag collections
This commit is contained in:
Executable
+107
@@ -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()
|
||||
Reference in New Issue
Block a user