Files
luk/src/tasks/backend.py
2025-12-18 14:34:29 -05:00

260 lines
6.2 KiB
Python

"""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