This commit is contained in:
root committed 2026-06-06 12:36:25 +00:00
1 parent 206997510c
commit 251bd3e65f
14 files changed
+836

No files matched your search

+213
View File
@@ -0,0 +1,213 @@
"""Main entry point and CLI interface for the Todo CLI application.
This module defines the TodoCLI class that handles command-line argument
parsing and user interaction, as well as the main() function to start
the application.
"""
import argparse
import sys
from typing import List, Optional
from todo import TodoManager
from storage import Storage
class TodoCLI:
"""Command-line interface for managing todo items.
Attributes:
manager: The TodoManager instance handling business logic.
parser: The ArgumentParser for parsing command-line arguments.
"""
def __init__(self, manager: TodoManager) -> None:
"""Initialize the CLI with a TodoManager.
Args:
manager: A TodoManager instance for data operations.
"""
self.manager = manager
self.parser = argparse.ArgumentParser(
description="A simple CLI todo list manager."
)
self._setup_parser()
def _setup_parser(self) -> None:
"""Configure subparsers for each command."""
subparsers = self.parser.add_subparsers(dest="command", help="Available commands")
# add command
add_parser = subparsers.add_parser("add", help="Add a new todo item")
add_parser.add_argument("title", type=str, help="Title of the task")
add_parser.add_argument("-d", "--description", type=str, default="",
help="Description of the task")
add_parser.add_argument("-due", "--due_date", type=str, default="",
help="Due date (e.g., 2025-01-01)")
# list command
list_parser = subparsers.add_parser("list", help="List all todo items")
list_parser.add_argument("-s", "--status", type=str, choices=["done", "pending"],
help="Filter by completion status")
# done command
done_parser = subparsers.add_parser("done", help="Mark a todo item as completed")
done_parser.add_argument("item_id", type=int, help="ID of the item to mark done")
# undone command
undone_parser = subparsers.add_parser("undone", help="Mark a todo item as not completed")
undone_parser.add_argument("item_id", type=int, help="ID of the item to mark undone")
# delete command
delete_parser = subparsers.add_parser("delete", help="Delete a todo item")
delete_parser.add_argument("item_id", type=int, help="ID of the item to delete")
# edit command
edit_parser = subparsers.add_parser("edit", help="Edit a todo item")
edit_parser.add_argument("item_id", type=int, help="ID of the item to edit")
edit_parser.add_argument("--title", type=str, default=None,
help="New title (leave unchanged if omitted)")
edit_parser.add_argument("--description", type=str, default=None,
help="New description (leave unchanged if omitted)")
edit_parser.add_argument("--due_date", type=str, default=None,
help="New due date (leave unchanged if omitted)")
edit_parser.add_argument("--completed", type=str, choices=["true", "false"],
default=None,
help="Set completion status (true/false)")
def run(self, args: Optional[List[str]] = None) -> None:
"""Parse arguments and execute the corresponding command.
Args:
args: Command-line arguments (defaults to sys.argv[1:]).
"""
parsed_args = self.parser.parse_args(args)
if parsed_args.command is None:
self.parser.print_help()
return
command_handlers = {
"add": self._handle_add,
"list": self._handle_list,
"done": self._handle_done,
"undone": self._handle_undone,
"delete": self._handle_delete,
"edit": self._handle_edit,
}
handler = command_handlers.get(parsed_args.command)
if handler:
handler(parsed_args)
else:
self.parser.print_help()
def _handle_add(self, args: argparse.Namespace) -> None:
"""Handle the 'add' command.
Args:
args: Parsed arguments containing title, description, due_date.
"""
item = self.manager.add(args.title, args.description, args.due_date)
print(f"Added todo item #{item.id}: {item.title}")
def _handle_list(self, args: argparse.Namespace) -> None:
"""Handle the 'list' command.
Args:
args: Parsed arguments (may contain status filter).
"""
if args.status == "done":
items = self.manager.list_by_status(True)
elif args.status == "pending":
items = self.manager.list_by_status(False)
else:
items = self.manager.list_all()
if not items:
print("No todo items found.")
return
# Print header
print(f"{'ID':<4} {'Title':<30} {'Completed':<10} {'Due Date':<12} {'Description'}")
print("-" * 80)
for item in items:
completed_str = "✓" if item.completed else "✗"
due = item.due_date if item.due_date else "-"
desc = item.description if item.description else ""
print(f"{item.id:<4} {item.title:<30} {completed_str:<10} {due:<12} {desc}")
def _handle_done(self, args: argparse.Namespace) -> None:
"""Handle the 'done' command.
Args:
args: Parsed arguments containing item_id.
"""
success = self.manager.mark_done(args.item_id)
if success:
print(f"Marked item #{args.item_id} as completed.")
else:
print(f"Error: Item #{args.item_id} not found.")
def _handle_undone(self, args: argparse.Namespace) -> None:
"""Handle the 'undone' command.
Args:
args: Parsed arguments containing item_id.
"""
success = self.manager.mark_undone(args.item_id)
if success:
print(f"Marked item #{args.item_id} as not completed.")
else:
print(f"Error: Item #{args.item_id} not found.")
def _handle_delete(self, args: argparse.Namespace) -> None:
"""Handle the 'delete' command.
Args:
args: Parsed arguments containing item_id.
"""
success = self.manager.delete(args.item_id)
if success:
print(f"Deleted item #{args.item_id}.")
else:
print(f"Error: Item #{args.item_id} not found.")
def _handle_edit(self, args: argparse.Namespace) -> None:
"""Handle the 'edit' command.
Args:
args: Parsed arguments containing item_id and optional fields.
"""
# Convert --completed string to bool if provided
completed = None
if args.completed is not None:
completed = args.completed.lower() == "true"
success = self.manager.edit(
args.item_id,
title=args.title,
description=args.description,
due_date=args.due_date,
completed=completed,
)
if success:
print(f"Updated item #{args.item_id}.")
else:
print(f"Error: Item #{args.item_id} not found.")
def main() -> None:
"""Application entry point.
Creates a Storage instance with default path, initializes TodoManager,
and runs the CLI.
"""
storage = Storage()
manager = TodoManager(storage)
cli = TodoCLI(manager)
cli.run()
if __name__ == "__main__":
main()
+66
View File
@@ -0,0 +1,66 @@
"""Storage module for JSON file persistence.
This module provides the Storage class that handles reading and writing
todo items to a JSON file located at ~/.todo_cli/todos.json.
"""
import json
import os
from typing import List, Dict, Any
class Storage:
"""Handles persistent storage of todo items in a JSON file.
Attributes:
file_path: The absolute path to the JSON storage file.
"""
def __init__(self, file_path: str = None) -> None:
"""Initialize Storage with a file path.
If no file_path is provided, defaults to ~/.todo_cli/todos.json.
Args:
file_path: Optional custom path for the JSON file.
"""
if file_path is None:
home_dir = os.path.expanduser("~")
self.file_path = os.path.join(home_dir, ".todo_cli", "todos.json")
else:
self.file_path = file_path
def load(self) -> List[Dict[str, Any]]:
"""Load todo items from the JSON file.
Returns:
A list of dictionaries representing todo items.
Returns an empty list if the file does not exist or is corrupted.
"""
if not os.path.exists(self.file_path):
return []
try:
with open(self.file_path, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, list):
return data
else:
return []
except (json.JSONDecodeError, IOError):
return []
def save(self, data: List[Dict[str, Any]]) -> None:
"""Save a list of todo item dictionaries to the JSON file.
Creates the directory structure if it does not exist.
Args:
data: A list of dictionaries representing todo items to persist.
"""
directory = os.path.dirname(self.file_path)
if directory:
os.makedirs(directory, exist_ok=True)
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
+233
View File
@@ -0,0 +1,233 @@
"""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