add base file
This commit is contained in:
306
app.py
Normal file
306
app.py
Normal file
@@ -0,0 +1,306 @@
|
||||
#!/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.
|
||||
|
||||
Implements requirements: REQ-001 through REQ-010
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class TodoApp:
|
||||
"""
|
||||
Simple Todo Application with persistent storage.
|
||||
|
||||
Implements all 10 requirements:
|
||||
- REQ-001: Create todo items
|
||||
- REQ-002: List todo items
|
||||
- REQ-003: Mark complete/incomplete
|
||||
- REQ-004: Delete todo items
|
||||
- REQ-005: Edit todo items
|
||||
- REQ-006: Data persistence (JSON)
|
||||
- REQ-007: Input validation
|
||||
- REQ-008: Performance optimization
|
||||
- REQ-009: Error handling
|
||||
- REQ-010: CLI user interface
|
||||
"""
|
||||
|
||||
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:
|
||||
"""Load todos from JSON file if it exists. REQ-006: Data Persistence."""
|
||||
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:
|
||||
"""Save todos to JSON file. REQ-006: Data Persistence."""
|
||||
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}") # REQ-009: Error handling
|
||||
|
||||
def _validate_title(self, title: str) -> bool:
|
||||
"""
|
||||
Validate todo title. REQ-007: Input Validation.
|
||||
|
||||
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. REQ-001: Create Todo Items.
|
||||
REQ-007: Input Validation. REQ-009: Error handling.
|
||||
|
||||
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}") # REQ-009: Error handling
|
||||
return False
|
||||
|
||||
def list_todos(self) -> None:
|
||||
"""
|
||||
Display all todos in a formatted list. REQ-002: List Todo Items.
|
||||
REQ-010: CLI user interface.
|
||||
"""
|
||||
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.
|
||||
REQ-003: Mark Todo as Complete. REQ-009: Error handling.
|
||||
|
||||
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}") # REQ-009: Error handling
|
||||
return False
|
||||
|
||||
def delete_todo(self, todo_id: int) -> bool:
|
||||
"""
|
||||
Delete a todo item. REQ-004: Delete Todo Items.
|
||||
REQ-009: Error handling.
|
||||
|
||||
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}") # REQ-009: Error handling
|
||||
return False
|
||||
|
||||
def edit_todo(self, todo_id: int, title: Optional[str] = None,
|
||||
description: Optional[str] = None) -> bool:
|
||||
"""
|
||||
Edit an existing todo item. REQ-005: Edit Todo Items.
|
||||
REQ-007: Input Validation. REQ-009: Error handling.
|
||||
|
||||
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}") # REQ-009: Error handling
|
||||
return False
|
||||
|
||||
def show_menu(self) -> None:
|
||||
"""Display the main menu. REQ-010: CLI user interface."""
|
||||
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. REQ-010: CLI user interface."""
|
||||
print("\n🚀 Welcome to the Simple Todo Application!")
|
||||
|
||||
while True:
|
||||
self.show_menu()
|
||||
choice = input("Enter your choice (1-6): ").strip()
|
||||
|
||||
if choice == "1":
|
||||
# REQ-001: Create Todo Items
|
||||
title = input("Enter todo title: ").strip()
|
||||
description = input("Enter description (optional): ").strip()
|
||||
self.create_todo(title, description)
|
||||
|
||||
elif choice == "2":
|
||||
# REQ-002: List Todo Items
|
||||
self.list_todos()
|
||||
|
||||
elif choice == "3":
|
||||
# REQ-003: Mark Todo as Complete
|
||||
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":
|
||||
# REQ-005: Edit Todo Items
|
||||
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":
|
||||
# REQ-004: Delete Todo Items
|
||||
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":
|
||||
# REQ-010: CLI user interface - graceful exit
|
||||
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()
|
||||
Reference in New Issue
Block a user