diff --git a/commands/add.py b/commands/add.py index 1b1a943..0af24ca 100644 --- a/commands/add.py +++ b/commands/add.py @@ -1,37 +1,23 @@ -"""Add task command.""" +"""Command to add a new task.""" -import json -from pathlib import Path +from utils.paths import get_tasks_file +from utils.validation import validate_description -def get_tasks_file(): - """Get path to tasks file.""" - return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" +def add_task(description: str) -> None: + """Add a new task with the given description. + Args: + description: The description for the new task. -def validate_description(description): - """Validate task description.""" - # NOTE: Validation logic scattered here - should be in utils (refactor bounty) - if not description: - raise ValueError("Description cannot be empty") - if len(description) > 200: - raise ValueError("Description too long (max 200 chars)") - return description.strip() - - -def add_task(description): - """Add a new task.""" - description = validate_description(description) + Raises: + ValueError: If the description is empty. + """ + validate_description(description) tasks_file = get_tasks_file() - tasks_file.parent.mkdir(parents=True, exist_ok=True) - - tasks = [] - if tasks_file.exists(): - tasks = json.loads(tasks_file.read_text()) - task_id = len(tasks) + 1 - tasks.append({"id": task_id, "description": description, "done": False}) + with open(tasks_file, "a") as f: + f.write(f"{description.strip()}\n") - tasks_file.write_text(json.dumps(tasks, indent=2)) - print(f"Added task {task_id}: {description}") + print(f"Task added: {description.strip()}") diff --git a/commands/done.py b/commands/done.py index c9dfd42..b4704e6 100644 --- a/commands/done.py +++ b/commands/done.py @@ -1,37 +1,37 @@ -"""Mark task done command.""" +"""Command to mark a task as done.""" -import json -from pathlib import Path +from utils.paths import get_tasks_file +from utils.validation import validate_task_file, validate_task_id -def get_tasks_file(): - """Get path to tasks file.""" - return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" +def mark_done(task_id: str) -> None: + """Mark the task with the given ID as done (removes it from the list). + Args: + task_id: The 1-based index of the task to mark as done. -def validate_task_id(tasks, task_id): - """Validate task ID exists.""" - # NOTE: Validation logic scattered here - should be in utils (refactor bounty) - if task_id < 1 or task_id > len(tasks): - raise ValueError(f"Invalid task ID: {task_id}") - return task_id + Raises: + ValueError: If the task ID is not a valid positive integer or out of range. + FileNotFoundError: If the tasks file does not exist. + """ + tid = validate_task_id(task_id) - -def mark_done(task_id): - """Mark a task as complete.""" tasks_file = get_tasks_file() - if not tasks_file.exists(): - print("No tasks found!") - return + validate_task_file(tasks_file) + + with open(tasks_file, "r") as f: + lines = f.readlines() + + tasks = [line for line in lines if line.strip()] + + if tid > len(tasks): + raise ValueError( + f"Task ID {tid} is out of range. There are only {len(tasks)} task(s)." + ) - tasks = json.loads(tasks_file.read_text()) - task_id = validate_task_id(tasks, task_id) + completed_task = tasks.pop(tid - 1).strip() - for task in tasks: - if task["id"] == task_id: - task["done"] = True - tasks_file.write_text(json.dumps(tasks, indent=2)) - print(f"Marked task {task_id} as done: {task['description']}") - return + with open(tasks_file, "w") as f: + f.writelines(tasks) - print(f"Task {task_id} not found") + print(f"Task {tid} marked as done: {completed_task}") diff --git a/commands/list.py b/commands/list.py index 714315d..43a9a2d 100644 --- a/commands/list.py +++ b/commands/list.py @@ -1,37 +1,26 @@ -"""List tasks command.""" +"""Command to list all tasks.""" -import json -from pathlib import Path +from utils.paths import get_tasks_file +from utils.validation import validate_task_file -def get_tasks_file(): - """Get path to tasks file.""" - return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" +def list_tasks() -> None: + """List all tasks from the tasks file. - -def validate_task_file(): - """Validate tasks file exists.""" - # NOTE: Validation logic scattered here - should be in utils (refactor bounty) + Raises: + FileNotFoundError: If the tasks file does not exist. + """ tasks_file = get_tasks_file() - if not tasks_file.exists(): - return [] - return tasks_file - - -def list_tasks(): - """List all tasks.""" - # NOTE: No --json flag support yet (feature bounty) - tasks_file = validate_task_file() - if not tasks_file: - print("No tasks yet!") - return + validate_task_file(tasks_file) - tasks = json.loads(tasks_file.read_text()) + with open(tasks_file, "r") as f: + lines = f.readlines() - if not tasks: - print("No tasks yet!") + if not lines: + print("No tasks found.") return - for task in tasks: - status = "✓" if task["done"] else " " - print(f"[{status}] {task['id']}. {task['description']}") + for index, line in enumerate(lines, start=1): + task = line.strip() + if task: + print(f"{index}. {task}") diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..11fa064 --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1 @@ +# Utils package for shared helpers and validation logic diff --git a/utils/paths.py b/utils/paths.py new file mode 100644 index 0000000..39e2c5f --- /dev/null +++ b/utils/paths.py @@ -0,0 +1,8 @@ +"""Shared path helpers for the task manager application.""" + +import os + + +def get_tasks_file() -> str: + """Return the path to the tasks file, defaulting to 'tasks.txt'.""" + return os.environ.get("TASKS_FILE", "tasks.txt") diff --git a/utils/validation.py b/utils/validation.py new file mode 100644 index 0000000..f647b80 --- /dev/null +++ b/utils/validation.py @@ -0,0 +1,52 @@ +"""Shared validation helpers for the task manager application.""" + +import os + + +def validate_description(description: str) -> None: + """Validate that a task description is non-empty. + + Args: + description: The task description string to validate. + + Raises: + ValueError: If the description is empty or contains only whitespace. + """ + if not description or not description.strip(): + raise ValueError("Task description cannot be empty.") + + +def validate_task_file(tasks_file: str) -> None: + """Validate that the tasks file exists. + + Args: + tasks_file: Path to the tasks file. + + Raises: + FileNotFoundError: If the tasks file does not exist. + """ + if not os.path.exists(tasks_file): + raise FileNotFoundError(f"Tasks file not found: {tasks_file}") + + +def validate_task_id(task_id: str) -> int: + """Validate that a task ID is a positive integer. + + Args: + task_id: The task ID string to validate. + + Returns: + The task ID as a positive integer. + + Raises: + ValueError: If the task ID is not a valid positive integer. + """ + try: + tid = int(task_id) + except (TypeError, ValueError): + raise ValueError(f"Invalid task ID '{task_id}': must be a positive integer.") + + if tid <= 0: + raise ValueError(f"Invalid task ID '{task_id}': must be a positive integer.") + + return tid