"""Todo item data model and manager module. This module defines the TodoItem dataclass and the TodoManager class that provides CRUD operations for todo items, backed by a Storage instance. """ import datetime from dataclasses import dataclass, field, asdict from typing import List, Optional, Dict, Any from storage import Storage @dataclass class TodoItem: """Represents a single todo item. Attributes: id: Unique identifier for the item. title: Short title of the task. description: Optional detailed description. due_date: Optional due date string (e.g., '2025-01-01'). completed: Whether the task is completed. created_at: ISO-formatted creation timestamp. """ id: int title: str description: str = "" due_date: str = "" completed: bool = False created_at: str = field(default_factory=lambda: datetime.datetime.now().isoformat()) def to_dict(self) -> Dict[str, Any]: """Convert the TodoItem to a dictionary for JSON serialization. Returns: A dictionary with all fields. """ return asdict(self) @classmethod def from_dict(cls, data: Dict[str, Any]) -> "TodoItem": """Create a TodoItem from a dictionary (e.g., loaded from JSON). Args: data: Dictionary containing todo item fields. Returns: A new TodoItem instance. """ return cls( id=data["id"], title=data["title"], description=data.get("description", ""), due_date=data.get("due_date", ""), completed=data.get("completed", False), created_at=data.get("created_at", ""), ) class TodoManager: """Manages a collection of TodoItems with persistent storage. Attributes: items: List of current TodoItem objects. storage: Storage instance for data persistence. next_id: The next available ID for new items. """ def __init__(self, storage: Storage) -> None: """Initialize the manager with a Storage instance and load existing data. Args: storage: A Storage object for reading/writing JSON data. """ self.storage = storage self.items: List[TodoItem] = [] self.next_id: int = 1 self._load() def add(self, title: str, description: str = "", due_date: str = "") -> TodoItem: """Add a new todo item. Args: title: The title of the task. description: Optional description. due_date: Optional due date string. Returns: The newly created TodoItem. """ item = TodoItem( id=self.next_id, title=title, description=description, due_date=due_date, ) self.items.append(item) self.next_id += 1 self._save() return item def list_all(self) -> List[TodoItem]: """Return all todo items. Returns: A list of all TodoItem objects. """ return self.items def list_by_status(self, completed: bool) -> List[TodoItem]: """Return todo items filtered by completion status. Args: completed: True for completed items, False for pending. Returns: A list of matching TodoItem objects. """ return [item for item in self.items if item.completed == completed] def mark_done(self, item_id: int) -> bool: """Mark a todo item as completed. Args: item_id: The ID of the item to mark. Returns: True if the item was found and updated, False otherwise. """ item = self._find_by_id(item_id) if item is None: return False item.completed = True self._save() return True def mark_undone(self, item_id: int) -> bool: """Mark a todo item as not completed. Args: item_id: The ID of the item to mark. Returns: True if the item was found and updated, False otherwise. """ item = self._find_by_id(item_id) if item is None: return False item.completed = False self._save() return True def delete(self, item_id: int) -> bool: """Delete a todo item by its ID. Args: item_id: The ID of the item to delete. Returns: True if the item was found and removed, False otherwise. """ item = self._find_by_id(item_id) if item is None: return False self.items.remove(item) self._save() return True def edit( self, item_id: int, title: Optional[str] = None, description: Optional[str] = None, due_date: Optional[str] = None, completed: Optional[bool] = None, ) -> bool: """Edit fields of an existing todo item. Only fields that are not None will be updated. Args: item_id: The ID of the item to edit. title: New title (or None to keep unchanged). description: New description (or None to keep unchanged). due_date: New due date (or None to keep unchanged). completed: New completion status (or None to keep unchanged). Returns: True if the item was found and updated, False otherwise. """ item = self._find_by_id(item_id) if item is None: return False if title is not None: item.title = title if description is not None: item.description = description if due_date is not None: item.due_date = due_date if completed is not None: item.completed = completed self._save() return True def _load(self) -> None: """Load todo items from storage and initialize next_id.""" raw_data = self.storage.load() self.items = [TodoItem.from_dict(d) for d in raw_data] if self.items: max_id = max(item.id for item in self.items) self.next_id = max_id + 1 else: self.next_id = 1 def _save(self) -> None: """Save current todo items to storage.""" data = [item.to_dict() for item in self.items] self.storage.save(data) def _find_by_id(self, item_id: int) -> Optional[TodoItem]: """Find a todo item by its ID. Args: item_id: The ID to search for. Returns: The TodoItem if found, None otherwise. """ for item in self.items: if item.id == item_id: return item return None