"""Task backend abstraction for Tasks TUI. This module defines the abstract interface that all task backends must implement, allowing the TUI to work with different task management systems (dstask, taskwarrior, etc.) """ from abc import ABC, abstractmethod from dataclasses import dataclass, field from datetime import datetime, timezone from enum import Enum from typing import Optional class TaskStatus(Enum): """Task status values.""" PENDING = "pending" ACTIVE = "active" DONE = "done" DELETED = "deleted" class TaskPriority(Enum): """Task priority levels (P0 = highest, P3 = lowest).""" P0 = "P0" # Critical P1 = "P1" # High P2 = "P2" # Normal P3 = "P3" # Low @classmethod def from_string(cls, value: str) -> "TaskPriority": """Parse priority from string.""" value = value.upper().strip() if value in ("P0", "0", "CRITICAL"): return cls.P0 elif value in ("P1", "1", "HIGH", "H"): return cls.P1 elif value in ("P2", "2", "NORMAL", "MEDIUM", "M"): return cls.P2 elif value in ("P3", "3", "LOW", "L"): return cls.P3 return cls.P2 # Default to normal @dataclass class Task: """Unified task representation across backends.""" uuid: str id: int # Short numeric ID for display summary: str status: TaskStatus = TaskStatus.PENDING priority: TaskPriority = TaskPriority.P2 project: str = "" tags: list[str] = field(default_factory=list) notes: str = "" due: Optional[datetime] = None created: Optional[datetime] = None resolved: Optional[datetime] = None @property def is_overdue(self) -> bool: """Check if task is overdue.""" if self.due is None or self.status == TaskStatus.DONE: return False # Use timezone-aware now() if due date is timezone-aware now = datetime.now(timezone.utc) if self.due.tzinfo else datetime.now() return now > self.due @dataclass class Project: """Project information.""" name: str task_count: int = 0 resolved_count: int = 0 active: bool = True priority: TaskPriority = TaskPriority.P2 class TaskBackend(ABC): """Abstract base class for task management backends.""" @abstractmethod def get_tasks( self, project: Optional[str] = None, tags: Optional[list[str]] = None, status: Optional[TaskStatus] = None, ) -> list[Task]: """Get tasks, optionally filtered by project, tags, or status. Args: project: Filter by project name tags: Filter by tags (tasks must have all specified tags) status: Filter by status Returns: List of matching tasks """ pass @abstractmethod def get_next_tasks(self) -> list[Task]: """Get the 'next' tasks to work on (priority-sorted actionable tasks).""" pass @abstractmethod def get_task(self, task_id: str) -> Optional[Task]: """Get a single task by ID or UUID. Args: task_id: Task ID (numeric) or UUID Returns: Task if found, None otherwise """ pass @abstractmethod def add_task( self, summary: str, project: Optional[str] = None, tags: Optional[list[str]] = None, priority: Optional[TaskPriority] = None, due: Optional[datetime] = None, notes: Optional[str] = None, ) -> Task: """Create a new task. Args: summary: Task description project: Project name tags: List of tags priority: Task priority due: Due date notes: Additional notes Returns: The created task """ pass @abstractmethod def complete_task(self, task_id: str) -> bool: """Mark a task as complete. Args: task_id: Task ID or UUID Returns: True if successful """ pass @abstractmethod def delete_task(self, task_id: str) -> bool: """Delete a task. Args: task_id: Task ID or UUID Returns: True if successful """ pass @abstractmethod def start_task(self, task_id: str) -> bool: """Start working on a task (mark as active). Args: task_id: Task ID or UUID Returns: True if successful """ pass @abstractmethod def stop_task(self, task_id: str) -> bool: """Stop working on a task (mark as pending). Args: task_id: Task ID or UUID Returns: True if successful """ pass @abstractmethod def modify_task( self, task_id: str, summary: Optional[str] = None, project: Optional[str] = None, tags: Optional[list[str]] = None, priority: Optional[TaskPriority] = None, due: Optional[datetime] = None, notes: Optional[str] = None, ) -> bool: """Modify a task. Args: task_id: Task ID or UUID summary: New summary (if provided) project: New project (if provided) tags: New tags (if provided) priority: New priority (if provided) due: New due date (if provided) notes: New notes (if provided) Returns: True if successful """ pass @abstractmethod def get_projects(self) -> list[Project]: """Get all projects. Returns: List of projects with task counts """ pass @abstractmethod def get_tags(self) -> list[str]: """Get all tags. Returns: List of tag names """ pass @abstractmethod def sync(self) -> bool: """Sync tasks with remote (if supported). Returns: True if successful (or not applicable) """ pass @abstractmethod def edit_task_interactive(self, task_id: str) -> bool: """Open task in editor for interactive editing. Args: task_id: Task ID or UUID Returns: True if successful """ pass @abstractmethod def edit_note_interactive(self, task_id: str) -> bool: """Open task notes in editor for interactive editing. Args: task_id: Task ID or UUID Returns: True if successful """ pass @abstractmethod def get_context(self) -> Optional[str]: """Get the current context filter. Returns: Current context string, or None if no context is set """ pass @abstractmethod def set_context(self, context: Optional[str]) -> bool: """Set the context filter. Args: context: Context string (e.g., "+work", "project:foo") or None to clear Returns: True if successful """ pass @abstractmethod def get_contexts(self) -> list[str]: """Get available predefined contexts. For taskwarrior, returns named contexts from config. For dstask, may return common tag-based contexts. Returns: List of context names/filters """ pass