Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """Small task ledger helper for ProofFrame.""" | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| from typing import Any | |
| ROOT = Path(__file__).resolve().parents[1] | |
| TASKS_PATH = ROOT / "tasks.json" | |
| VALID_STATUSES = {"todo", "doing", "blocked", "done"} | |
| def load_data() -> dict[str, Any]: | |
| with TASKS_PATH.open("r", encoding="utf-8") as f: | |
| return json.load(f) | |
| def save_data(data: dict[str, Any]) -> None: | |
| data["updated_at"] = ( | |
| datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") | |
| ) | |
| tmp_path = TASKS_PATH.with_suffix(".json.tmp") | |
| with tmp_path.open("w", encoding="utf-8") as f: | |
| json.dump(data, f, indent=2, ensure_ascii=False) | |
| f.write("\n") | |
| tmp_path.replace(TASKS_PATH) | |
| def find_task(data: dict[str, Any], task_id: str) -> dict[str, Any]: | |
| task_id = task_id.upper() | |
| for task in data["tasks"]: | |
| if task["id"].upper() == task_id: | |
| return task | |
| raise SystemExit(f"Task not found: {task_id}") | |
| def print_task(task: dict[str, Any]) -> None: | |
| note = f" | {task.get('notes', '')}" if task.get("notes") else "" | |
| print(f"{task['id']} [{task['status']}] {task['phase']} / {task['owner']} - {task['title']}{note}") | |
| def task_search_text(task: dict[str, Any]) -> str: | |
| values = [ | |
| task.get("id", ""), | |
| task.get("title", ""), | |
| task.get("phase", ""), | |
| task.get("owner", ""), | |
| task.get("status", ""), | |
| task.get("done_criteria", ""), | |
| task.get("notes", ""), | |
| ] | |
| return "\n".join(str(value) for value in values).lower() | |
| def insert_task(data: dict[str, Any], task: dict[str, Any], after: str | None = None) -> None: | |
| tasks = data["tasks"] | |
| if any(existing["id"].upper() == task["id"].upper() for existing in tasks): | |
| raise SystemExit(f"Task already exists: {task['id']}") | |
| if not after: | |
| tasks.append(task) | |
| return | |
| after = after.upper() | |
| for index, existing in enumerate(tasks): | |
| if existing["id"].upper() == after: | |
| tasks.insert(index + 1, task) | |
| return | |
| raise SystemExit(f"Task not found for --after: {after}") | |
| def cmd_add(args: argparse.Namespace) -> None: | |
| data = load_data() | |
| task = { | |
| "id": args.task_id.upper(), | |
| "title": args.title, | |
| "phase": args.phase, | |
| "owner": args.owner, | |
| "status": args.status, | |
| "done_criteria": args.done_criteria, | |
| "notes": args.note, | |
| } | |
| insert_task(data, task, args.after) | |
| save_data(data) | |
| print_task(task) | |
| def cmd_list(args: argparse.Namespace) -> None: | |
| data = load_data() | |
| tasks = data["tasks"] | |
| if args.status: | |
| tasks = [task for task in tasks if task["status"] == args.status] | |
| if args.owner: | |
| tasks = [task for task in tasks if task["owner"] == args.owner] | |
| for task in tasks: | |
| print_task(task) | |
| def cmd_show(args: argparse.Namespace) -> None: | |
| data = load_data() | |
| task = find_task(data, args.task_id) | |
| print(json.dumps(task, indent=2, ensure_ascii=False)) | |
| def cmd_search(args: argparse.Namespace) -> None: | |
| data = load_data() | |
| terms = [term.lower() for term in args.query] | |
| tasks = data["tasks"] | |
| if args.status: | |
| tasks = [task for task in tasks if task["status"] == args.status] | |
| if args.owner: | |
| tasks = [task for task in tasks if task["owner"] == args.owner] | |
| for task in tasks: | |
| text = task_search_text(task) | |
| if all(term in text for term in terms): | |
| print_task(task) | |
| def update_status(args: argparse.Namespace, status: str) -> None: | |
| data = load_data() | |
| task = find_task(data, args.task_id) | |
| task["status"] = status | |
| if args.note: | |
| task["notes"] = args.note | |
| save_data(data) | |
| print_task(task) | |
| def build_parser() -> argparse.ArgumentParser: | |
| parser = argparse.ArgumentParser(description="Query and update ProofFrame tasks.") | |
| subparsers = parser.add_subparsers(dest="command", required=True) | |
| add_parser = subparsers.add_parser("add", help="Add a task.") | |
| add_parser.add_argument("task_id") | |
| add_parser.add_argument("title") | |
| add_parser.add_argument("--phase", required=True) | |
| add_parser.add_argument("--owner", required=True) | |
| add_parser.add_argument("--status", choices=sorted(VALID_STATUSES), default="todo") | |
| add_parser.add_argument("--done-criteria", default="") | |
| add_parser.add_argument("--note", default="") | |
| add_parser.add_argument("--after", help="Insert after an existing task id.") | |
| add_parser.set_defaults(func=cmd_add) | |
| list_parser = subparsers.add_parser("list", help="List tasks.") | |
| list_parser.add_argument("--status", choices=sorted(VALID_STATUSES)) | |
| list_parser.add_argument("--owner") | |
| list_parser.set_defaults(func=cmd_list) | |
| show_parser = subparsers.add_parser("show", help="Show one task as JSON.") | |
| show_parser.add_argument("task_id") | |
| show_parser.set_defaults(func=cmd_show) | |
| search_parser = subparsers.add_parser("search", help="Search tasks.") | |
| search_parser.add_argument("query", nargs="+") | |
| search_parser.add_argument("--status", choices=sorted(VALID_STATUSES)) | |
| search_parser.add_argument("--owner") | |
| search_parser.set_defaults(func=cmd_search) | |
| for status in sorted(VALID_STATUSES): | |
| status_parser = subparsers.add_parser(status, help=f"Mark a task as {status}.") | |
| status_parser.add_argument("task_id") | |
| status_parser.add_argument("--note", default="") | |
| status_parser.set_defaults(func=lambda args, status=status: update_status(args, status)) | |
| return parser | |
| def main() -> None: | |
| parser = build_parser() | |
| args = parser.parse_args() | |
| args.func(args) | |
| if __name__ == "__main__": | |
| main() | |