Files
simple_app/app.py

280 lines
9.0 KiB
Python

#!/usr/bin/env python3
"""
Simple Todo Application
This application implements a command-line todo list manager that persists
data to JSON and provides full CRUD operations with validation.
"""
import json
import os
import sys
from pathlib import Path
from typing import Optional, List, Dict, Any
from datetime import datetime
class TodoApp:
DATA_FILE = "todos.json"
MAX_TITLE_LENGTH = 200
def __init__(self):
"""Initialize the application with data persistence."""
self.todos: List[Dict[str, Any]] = []
self._load_todos()
def _load_todos(self) -> None:
try:
if Path(self.DATA_FILE).exists():
with open(self.DATA_FILE, 'r') as f:
self.todos = json.load(f)
except (json.JSONDecodeError, IOError) as e:
print(f"⚠️ Could not load todos: {e}. Starting with empty list.")
self.todos = []
def _save_todos(self) -> None:
try:
with open(self.DATA_FILE, 'w') as f:
json.dump(self.todos, f, indent=2)
except IOError as e:
raise RuntimeError(f"Failed to save todos: {e}")
def _validate_title(self, title: str) -> bool:
"""
Validate todo title.
Args:
title: The title to validate
Returns:
bool: True if valid, False otherwise
"""
if not title or not title.strip():
print("❌ Error: Title cannot be empty")
return False
if len(title) > self.MAX_TITLE_LENGTH:
print(f"❌ Error: Title cannot exceed {self.MAX_TITLE_LENGTH} characters")
return False
return True
def create_todo(self, title: str, description: str = "") -> bool:
"""
Create a new todo item.
Args:
title: The todo title
description: Optional todo description
Returns:
bool: True if created successfully
"""
if not self._validate_title(title):
return False
try:
todo = {
"id": len(self.todos) + 1,
"title": title.strip(),
"description": description.strip(),
"completed": False,
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat()
}
self.todos.append(todo)
self._save_todos()
print(f"✅ Todo created: '{title}'")
return True
except Exception as e:
print(f"❌ Error creating todo: {e}")
return False
def list_todos(self) -> None:
"""
Display all todos in a formatted list.
"""
if not self.todos:
print("\n📋 No todos yet. Create one to get started!")
return
print("\n📋 Your Todos:")
print("=" * 70)
for todo in self.todos:
status = "" if todo["completed"] else ""
completed_text = "[DONE]" if todo["completed"] else "[TODO]"
print(f"\n {status} [{todo['id']}] {completed_text} {todo['title']}")
if todo["description"]:
print(f" Description: {todo['description']}")
print(f" Created: {todo['created_at'][:10]}")
print("\n" + "=" * 70)
def mark_complete(self, todo_id: int) -> bool:
"""
Mark a todo as complete or toggle completion status.
Args:
todo_id: The ID of the todo to mark complete
Returns:
bool: True if successful
"""
try:
for todo in self.todos:
if todo["id"] == todo_id:
todo["completed"] = not todo["completed"]
todo["updated_at"] = datetime.now().isoformat()
self._save_todos()
status = "completed" if todo["completed"] else "reopened"
print(f"✅ Todo {status}: '{todo['title']}'")
return True
print(f"❌ Error: Todo with ID {todo_id} not found")
return False
except Exception as e:
print(f"❌ Error marking todo: {e}")
return False
def delete_todo(self, todo_id: int) -> bool:
"""
Delete a todo item.
Args:
todo_id: The ID of the todo to delete
Returns:
bool: True if successful
"""
try:
original_length = len(self.todos)
self.todos = [t for t in self.todos if t["id"] != todo_id]
if len(self.todos) < original_length:
self._save_todos()
print(f"✅ Todo deleted")
return True
else:
print(f"❌ Error: Todo with ID {todo_id} not found")
return False
except Exception as e:
print(f"❌ Error deleting todo: {e}")
return False
def edit_todo(self, todo_id: int, title: Optional[str] = None,
description: Optional[str] = None) -> bool:
"""
Edit an existing todo item.
Args:
todo_id: The ID of the todo to edit
title: New title (optional)
description: New description (optional)
Returns:
bool: True if successful
"""
try:
for todo in self.todos:
if todo["id"] == todo_id:
if title is not None:
if not self._validate_title(title):
return False
todo["title"] = title.strip()
if description is not None:
todo["description"] = description.strip()
todo["updated_at"] = datetime.now().isoformat()
self._save_todos()
print(f"✅ Todo updated: '{todo['title']}'")
return True
print(f"❌ Error: Todo with ID {todo_id} not found")
return False
except Exception as e:
print(f"❌ Error editing todo: {e}")
return False
def show_menu(self) -> None:
"""Display the main menu."""
print("\n" + "=" * 70)
print("📝 Todo Application")
print("=" * 70)
print("1. Create new todo")
print("2. List all todos")
print("3. Mark todo complete/incomplete")
print("4. Edit todo")
print("5. Delete todo")
print("6. Exit")
print("=" * 70)
def run(self) -> None:
"""Main application loop."""
print("\n🚀 Welcome to the Simple Todo Application!")
while True:
self.show_menu()
choice = input("Enter your choice (1-6): ").strip()
if choice == "1":
title = input("Enter todo title: ").strip()
description = input("Enter description (optional): ").strip()
self.create_todo(title, description)
elif choice == "2":
self.list_todos()
elif choice == "3":
self.list_todos()
try:
todo_id = int(input("Enter todo ID to toggle: "))
self.mark_complete(todo_id)
except ValueError:
print("❌ Error: Please enter a valid ID number")
elif choice == "4":
self.list_todos()
try:
todo_id = int(input("Enter todo ID to edit: "))
title = input("Enter new title (leave empty to skip): ").strip()
description = input("Enter new description (leave empty to skip): ").strip()
new_title = title if title else None
new_description = description if description else None
self.edit_todo(todo_id, new_title, new_description)
except ValueError:
print("❌ Error: Please enter a valid ID number")
elif choice == "5":
self.list_todos()
try:
todo_id = int(input("Enter todo ID to delete: "))
confirm = input(f"Are you sure? (yes/no): ").strip().lower()
if confirm == "yes":
self.delete_todo(todo_id)
except ValueError:
print("❌ Error: Please enter a valid ID number")
elif choice == "6":
print("\n👋 Goodbye! Your todos have been saved.")
break
else:
print("❌ Error: Invalid choice. Please enter 1-6.")
if __name__ == "__main__":
app = TodoApp()
app.run()