diff --git a/.dependencies.json b/.dependencies.json new file mode 100644 index 0000000..990b225 --- /dev/null +++ b/.dependencies.json @@ -0,0 +1 @@ +{"docs/system_design/20260606123307.json": ["docs/prd/20260606123307.json"], "docs/task/20260606123307.json": ["docs/system_design/20260606123307.json"], "todo_cli/storage.py": ["docs/system_design/20260606123307.json", "docs/task/20260606123307.json"], "todo_cli/todo.py": ["docs/system_design/20260606123307.json", "docs/task/20260606123307.json"], "todo_cli/main.py": ["docs/system_design/20260606123307.json", "docs/task/20260606123307.json"]} \ No newline at end of file diff --git a/docs/prd/20260606123307.json b/docs/prd/20260606123307.json new file mode 100644 index 0000000..25d8c20 --- /dev/null +++ b/docs/prd/20260606123307.json @@ -0,0 +1 @@ +{"Language":"zh_cn","Programming Language":"Python","Original Requirements":"创建一个Python命令行TODO应用,支持增删改查,数据存JSON文件","Project Name":"todo_cli","Product Goals":["提供简洁高效的命令行交互体验","确保数据持久化与可靠性","支持快速增删改查操作"],"User Stories":["作为用户,我希望能够通过命令行快速添加新的待办事项","作为用户,我希望能够查看所有待办事项及其完成状态","作为用户,我希望能够标记待办事项为已完成","作为用户,我希望能够删除不再需要的待办事项","作为用户,我希望能够修改已有待办事项的内容"],"Competitive Analysis":["Todo.txt CLI: 纯文本文件存储,支持标签和优先级,但无JSON格式","Taskwarrior: 功能强大的命令行任务管理,但学习曲线较陡","t: 轻量级TODO工具,使用JSON存储,但功能单一","GitHub Issues CLI: 与GitHub集成,不适合本地简单使用","Microsoft To Do CLI: 非官方,依赖云服务,离线不可用"],"Competitive Quadrant Chart":"quadrantChart\n title \"命令行TODO工具竞争力分析\"\n x-axis \"功能简单\" --> \"功能丰富\"\n y-axis \"易用性低\" --> \"易用性高\"\n quadrant-1 \"功能丰富且易用\"\n quadrant-2 \"功能简单但易用\"\n quadrant-3 \"功能简单且难用\"\n quadrant-4 \"功能丰富但难用\"\n \"Todo.txt CLI\": [0.3, 0.7]\n \"Taskwarrior\": [0.8, 0.3]\n \"t\": [0.2, 0.6]\n \"GitHub Issues CLI\": [0.7, 0.4]\n \"Microsoft To Do CLI\": [0.5, 0.5]\n \"Our Target Product\": [0.4, 0.8]","Requirement Analysis":"本项目要求开发一个Python命令行TODO应用,核心功能包括增删改查(CRUD),数据以JSON文件形式持久化存储。用户通过命令行参数或交互式菜单操作。需要支持:添加任务(标题、描述、截止日期等可选字段)、列出所有任务(可筛选状态)、标记完成/未完成、删除任务、编辑任务内容。数据存储于本地JSON文件,需处理文件读写异常。命令行界面应清晰易用,提供帮助信息。","Requirement Pool":[["P0","实现添加待办事项功能,支持输入标题,可选描述和截止日期"],["P0","实现查看所有待办事项列表,显示ID、标题、状态、创建时间等"],["P0","实现删除待办事项功能,通过ID或索引删除"],["P0","实现修改待办事项功能,支持更新标题、描述、截止日期、状态"],["P1","实现数据持久化,使用JSON文件存储,支持自动加载和保存"]],"UI Design draft":"命令行界面,无图形UI。用户通过命令参数操作,例如:todo add \"任务标题\" -d \"描述\" -due 2025-01-01;todo list [--all|--done|--pending];todo done ;todo delete ;todo edit --title \"新标题\"。支持--help查看帮助。输出格式为表格或列表,清晰显示任务信息。","Anything UNCLEAR":"无"} \ No newline at end of file diff --git a/docs/requirement.txt b/docs/requirement.txt new file mode 100644 index 0000000..a96f53a --- /dev/null +++ b/docs/requirement.txt @@ -0,0 +1 @@ +创建一个Python命令行TODO应用,支持增删改查,数据存JSON文件 \ No newline at end of file diff --git a/docs/system_design/20260606123307.json b/docs/system_design/20260606123307.json new file mode 100644 index 0000000..6557235 --- /dev/null +++ b/docs/system_design/20260606123307.json @@ -0,0 +1 @@ +{"Implementation approach":"本项目使用Python标准库实现,无需第三方依赖。命令行参数解析采用argparse模块,数据持久化使用json模块,文件存储于用户主目录下的.todo_cli/todos.json。核心逻辑封装在TodoManager类中,提供增删改查方法。CLI接口通过TodoCLI类处理用户输入并调用TodoManager。数据模型TodoItem使用dataclass简化定义。整体设计遵循单一职责原则,确保代码简洁可维护。","File list":["main.py","todo.py","storage.py"],"Data structures and interfaces":"classDiagram\n class TodoItem {\n -int id\n -str title\n -str description\n -str due_date\n -bool completed\n -str created_at\n +__init__(id: int, title: str, description: str = \"\", due_date: str = \"\", completed: bool = False, created_at: str = \"\")\n +to_dict() dict\n +from_dict(data: dict) TodoItem\n }\n class Storage {\n -str file_path\n +__init__(file_path: str)\n +load() list[dict]\n +save(data: list[dict]) None\n }\n class TodoManager {\n -list[TodoItem] items\n -Storage storage\n -int next_id\n +__init__(storage: Storage)\n +add(title: str, description: str = \"\", due_date: str = \"\") TodoItem\n +list_all() list[TodoItem]\n +list_by_status(completed: bool) list[TodoItem]\n +mark_done(item_id: int) bool\n +mark_undone(item_id: int) bool\n +delete(item_id: int) bool\n +edit(item_id: int, title: str = None, description: str = None, due_date: str = None, completed: bool = None) bool\n +_load() None\n +_save() None\n +_find_by_id(item_id: int) TodoItem | None\n }\n class TodoCLI {\n -TodoManager manager\n -argparse.ArgumentParser parser\n +__init__(manager: TodoManager)\n +run(args: list[str] = None) None\n +_setup_parser() None\n +_handle_add(args) None\n +_handle_list(args) None\n +_handle_done(args) None\n +_handle_delete(args) None\n +_handle_edit(args) None\n }\n TodoCLI --> TodoManager\n TodoManager --> Storage\n TodoManager --> TodoItem\n Storage ..> TodoItem : uses to_dict/from_dict\n","Program call flow":"sequenceDiagram\n participant User as 用户\n participant CLI as TodoCLI\n participant Manager as TodoManager\n participant Storage as Storage\n participant File as JSON文件\n User->>CLI: python main.py add \"任务标题\" -d \"描述\" -due 2025-01-01\n CLI->>CLI: _setup_parser() 解析参数\n CLI->>CLI: _handle_add(args)\n CLI->>Manager: add(title, description, due_date)\n Manager->>Manager: 生成新TodoItem,设置id=next_id\n Manager->>Manager: 将item加入items列表\n Manager->>Manager: _save()\n Manager->>Storage: save([item.to_dict() for item in items])\n Storage->>File: 写入JSON文件\n File-->>Storage: 成功\n Storage-->>Manager: None\n Manager-->>CLI: 返回新TodoItem\n CLI-->>User: 打印添加成功信息\n\n User->>CLI: python main.py list\n CLI->>CLI: _handle_list(args)\n CLI->>Manager: list_all()\n Manager->>Manager: 返回items列表\n Manager-->>CLI: list[TodoItem]\n CLI->>CLI: 格式化输出表格\n CLI-->>User: 显示所有任务\n\n User->>CLI: python main.py done 1\n CLI->>CLI: _handle_done(args)\n CLI->>Manager: mark_done(1)\n Manager->>Manager: _find_by_id(1) -> item\n Manager->>Manager: item.completed = True\n Manager->>Manager: _save()\n Manager-->>CLI: True\n CLI-->>User: 打印标记成功\n\n User->>CLI: python main.py delete 1\n CLI->>CLI: _handle_delete(args)\n CLI->>Manager: delete(1)\n Manager->>Manager: _find_by_id(1) -> item\n Manager->>Manager: 从items中移除\n Manager->>Manager: _save()\n Manager-->>CLI: True\n CLI-->>User: 打印删除成功\n\n User->>CLI: python main.py edit 1 --title \"新标题\" --description \"新描述\"\n CLI->>CLI: _handle_edit(args)\n CLI->>Manager: edit(1, title=\"新标题\", description=\"新描述\")\n Manager->>Manager: _find_by_id(1) -> item\n Manager->>Manager: 更新item属性\n Manager->>Manager: _save()\n Manager-->>CLI: True\n CLI-->>User: 打印编辑成功\n","Anything UNCLEAR":"无"} \ No newline at end of file diff --git a/docs/task/20260606123307.json b/docs/task/20260606123307.json new file mode 100644 index 0000000..a6cd450 --- /dev/null +++ b/docs/task/20260606123307.json @@ -0,0 +1 @@ +{"Required packages":[],"Required Other language third-party packages":[],"Logic Analysis":[["storage.py","Contains Storage class with load() and save() methods for JSON file persistence. No dependencies on other project files."],["todo.py","Contains TodoItem dataclass and TodoManager class. TodoManager depends on Storage for data persistence. Imports from storage.py."],["main.py","Contains TodoCLI class and main entry point. TodoCLI depends on TodoManager. Imports from todo.py."]],"Task list":["storage.py","todo.py","main.py"],"Full API spec":"","Shared Knowledge":"Data model: TodoItem with fields id, title, description, due_date, completed, created_at. Storage path: ~/.todo_cli/todos.json. All modules use Python standard library only.","Anything UNCLEAR":"无"} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/resources/competitive_analysis/20260606123307.mmd b/resources/competitive_analysis/20260606123307.mmd new file mode 100644 index 0000000..aac3930 --- /dev/null +++ b/resources/competitive_analysis/20260606123307.mmd @@ -0,0 +1,14 @@ +quadrantChart + title "命令行TODO工具竞争力分析" + x-axis "功能简单" --> "功能丰富" + y-axis "易用性低" --> "易用性高" + quadrant-1 "功能丰富且易用" + quadrant-2 "功能简单但易用" + quadrant-3 "功能简单且难用" + quadrant-4 "功能丰富但难用" + "Todo.txt CLI": [0.3, 0.7] + "Taskwarrior": [0.8, 0.3] + "t": [0.2, 0.6] + "GitHub Issues CLI": [0.7, 0.4] + "Microsoft To Do CLI": [0.5, 0.5] + "Our Target Product": [0.4, 0.8] \ No newline at end of file diff --git a/resources/data_api_design/20260606123307.mmd b/resources/data_api_design/20260606123307.mmd new file mode 100644 index 0000000..f166e1b --- /dev/null +++ b/resources/data_api_design/20260606123307.mmd @@ -0,0 +1,50 @@ +classDiagram + class TodoItem { + -int id + -str title + -str description + -str due_date + -bool completed + -str created_at + +__init__(id: int, title: str, description: str = "", due_date: str = "", completed: bool = False, created_at: str = "") + +to_dict() dict + +from_dict(data: dict) TodoItem + } + class Storage { + -str file_path + +__init__(file_path: str) + +load() list[dict] + +save(data: list[dict]) None + } + class TodoManager { + -list[TodoItem] items + -Storage storage + -int next_id + +__init__(storage: Storage) + +add(title: str, description: str = "", due_date: str = "") TodoItem + +list_all() list[TodoItem] + +list_by_status(completed: bool) list[TodoItem] + +mark_done(item_id: int) bool + +mark_undone(item_id: int) bool + +delete(item_id: int) bool + +edit(item_id: int, title: str = None, description: str = None, due_date: str = None, completed: bool = None) bool + +_load() None + +_save() None + +_find_by_id(item_id: int) TodoItem | None + } + class TodoCLI { + -TodoManager manager + -argparse.ArgumentParser parser + +__init__(manager: TodoManager) + +run(args: list[str] = None) None + +_setup_parser() None + +_handle_add(args) None + +_handle_list(args) None + +_handle_done(args) None + +_handle_delete(args) None + +_handle_edit(args) None + } + TodoCLI --> TodoManager + TodoManager --> Storage + TodoManager --> TodoItem + Storage ..> TodoItem : uses to_dict/from_dict diff --git a/resources/prd/20260606123307.md b/resources/prd/20260606123307.md new file mode 100644 index 0000000..4ff3f8f --- /dev/null +++ b/resources/prd/20260606123307.md @@ -0,0 +1,75 @@ +## Language + +zh_cn + +## Programming Language + +Python + +## Original Requirements + +创建一个Python命令行TODO应用,支持增删改查,数据存JSON文件 + +## Project Name + +todo_cli + +## Product Goals + +- 提供简洁高效的命令行交互体验 +- 确保数据持久化与可靠性 +- 支持快速增删改查操作 + +## User Stories + +- 作为用户,我希望能够通过命令行快速添加新的待办事项 +- 作为用户,我希望能够查看所有待办事项及其完成状态 +- 作为用户,我希望能够标记待办事项为已完成 +- 作为用户,我希望能够删除不再需要的待办事项 +- 作为用户,我希望能够修改已有待办事项的内容 + +## Competitive Analysis + +- Todo.txt CLI: 纯文本文件存储,支持标签和优先级,但无JSON格式 +- Taskwarrior: 功能强大的命令行任务管理,但学习曲线较陡 +- t: 轻量级TODO工具,使用JSON存储,但功能单一 +- GitHub Issues CLI: 与GitHub集成,不适合本地简单使用 +- Microsoft To Do CLI: 非官方,依赖云服务,离线不可用 + +## Competitive Quadrant Chart + +quadrantChart + title "命令行TODO工具竞争力分析" + x-axis "功能简单" --> "功能丰富" + y-axis "易用性低" --> "易用性高" + quadrant-1 "功能丰富且易用" + quadrant-2 "功能简单但易用" + quadrant-3 "功能简单且难用" + quadrant-4 "功能丰富但难用" + "Todo.txt CLI": [0.3, 0.7] + "Taskwarrior": [0.8, 0.3] + "t": [0.2, 0.6] + "GitHub Issues CLI": [0.7, 0.4] + "Microsoft To Do CLI": [0.5, 0.5] + "Our Target Product": [0.4, 0.8] + +## Requirement Analysis + +本项目要求开发一个Python命令行TODO应用,核心功能包括增删改查(CRUD),数据以JSON文件形式持久化存储。用户通过命令行参数或交互式菜单操作。需要支持:添加任务(标题、描述、截止日期等可选字段)、列出所有任务(可筛选状态)、标记完成/未完成、删除任务、编辑任务内容。数据存储于本地JSON文件,需处理文件读写异常。命令行界面应清晰易用,提供帮助信息。 + +## Requirement Pool + +- ['P0', '实现添加待办事项功能,支持输入标题,可选描述和截止日期'] +- ['P0', '实现查看所有待办事项列表,显示ID、标题、状态、创建时间等'] +- ['P0', '实现删除待办事项功能,通过ID或索引删除'] +- ['P0', '实现修改待办事项功能,支持更新标题、描述、截止日期、状态'] +- ['P1', '实现数据持久化,使用JSON文件存储,支持自动加载和保存'] + +## UI Design draft + +命令行界面,无图形UI。用户通过命令参数操作,例如:todo add "任务标题" -d "描述" -due 2025-01-01;todo list [--all|--done|--pending];todo done ;todo delete ;todo edit --title "新标题"。支持--help查看帮助。输出格式为表格或列表,清晰显示任务信息。 + +## Anything UNCLEAR + +无 + diff --git a/resources/seq_flow/20260606123307.mmd b/resources/seq_flow/20260606123307.mmd new file mode 100644 index 0000000..5df6582 --- /dev/null +++ b/resources/seq_flow/20260606123307.mmd @@ -0,0 +1,54 @@ +sequenceDiagram + participant User as 用户 + participant CLI as TodoCLI + participant Manager as TodoManager + participant Storage as Storage + participant File as JSON文件 + User->>CLI: python main.py add "任务标题" -d "描述" -due 2025-01-01 + CLI->>CLI: _setup_parser() 解析参数 + CLI->>CLI: _handle_add(args) + CLI->>Manager: add(title, description, due_date) + Manager->>Manager: 生成新TodoItem,设置id=next_id + Manager->>Manager: 将item加入items列表 + Manager->>Manager: _save() + Manager->>Storage: save([item.to_dict() for item in items]) + Storage->>File: 写入JSON文件 + File-->>Storage: 成功 + Storage-->>Manager: None + Manager-->>CLI: 返回新TodoItem + CLI-->>User: 打印添加成功信息 + + User->>CLI: python main.py list + CLI->>CLI: _handle_list(args) + CLI->>Manager: list_all() + Manager->>Manager: 返回items列表 + Manager-->>CLI: list[TodoItem] + CLI->>CLI: 格式化输出表格 + CLI-->>User: 显示所有任务 + + User->>CLI: python main.py done 1 + CLI->>CLI: _handle_done(args) + CLI->>Manager: mark_done(1) + Manager->>Manager: _find_by_id(1) -> item + Manager->>Manager: item.completed = True + Manager->>Manager: _save() + Manager-->>CLI: True + CLI-->>User: 打印标记成功 + + User->>CLI: python main.py delete 1 + CLI->>CLI: _handle_delete(args) + CLI->>Manager: delete(1) + Manager->>Manager: _find_by_id(1) -> item + Manager->>Manager: 从items中移除 + Manager->>Manager: _save() + Manager-->>CLI: True + CLI-->>User: 打印删除成功 + + User->>CLI: python main.py edit 1 --title "新标题" --description "新描述" + CLI->>CLI: _handle_edit(args) + CLI->>Manager: edit(1, title="新标题", description="新描述") + Manager->>Manager: _find_by_id(1) -> item + Manager->>Manager: 更新item属性 + Manager->>Manager: _save() + Manager-->>CLI: True + CLI-->>User: 打印编辑成功 diff --git a/resources/system_design/20260606123307.md b/resources/system_design/20260606123307.md new file mode 100644 index 0000000..87db667 --- /dev/null +++ b/resources/system_design/20260606123307.md @@ -0,0 +1,126 @@ +## Implementation approach + +本项目使用Python标准库实现,无需第三方依赖。命令行参数解析采用argparse模块,数据持久化使用json模块,文件存储于用户主目录下的.todo_cli/todos.json。核心逻辑封装在TodoManager类中,提供增删改查方法。CLI接口通过TodoCLI类处理用户输入并调用TodoManager。数据模型TodoItem使用dataclass简化定义。整体设计遵循单一职责原则,确保代码简洁可维护。 + +## File list + +- main.py +- todo.py +- storage.py + +## Data structures and interfaces + +classDiagram + class TodoItem { + -int id + -str title + -str description + -str due_date + -bool completed + -str created_at + +__init__(id: int, title: str, description: str = "", due_date: str = "", completed: bool = False, created_at: str = "") + +to_dict() dict + +from_dict(data: dict) TodoItem + } + class Storage { + -str file_path + +__init__(file_path: str) + +load() list[dict] + +save(data: list[dict]) None + } + class TodoManager { + -list[TodoItem] items + -Storage storage + -int next_id + +__init__(storage: Storage) + +add(title: str, description: str = "", due_date: str = "") TodoItem + +list_all() list[TodoItem] + +list_by_status(completed: bool) list[TodoItem] + +mark_done(item_id: int) bool + +mark_undone(item_id: int) bool + +delete(item_id: int) bool + +edit(item_id: int, title: str = None, description: str = None, due_date: str = None, completed: bool = None) bool + +_load() None + +_save() None + +_find_by_id(item_id: int) TodoItem | None + } + class TodoCLI { + -TodoManager manager + -argparse.ArgumentParser parser + +__init__(manager: TodoManager) + +run(args: list[str] = None) None + +_setup_parser() None + +_handle_add(args) None + +_handle_list(args) None + +_handle_done(args) None + +_handle_delete(args) None + +_handle_edit(args) None + } + TodoCLI --> TodoManager + TodoManager --> Storage + TodoManager --> TodoItem + Storage ..> TodoItem : uses to_dict/from_dict + + +## Program call flow + +sequenceDiagram + participant User as 用户 + participant CLI as TodoCLI + participant Manager as TodoManager + participant Storage as Storage + participant File as JSON文件 + User->>CLI: python main.py add "任务标题" -d "描述" -due 2025-01-01 + CLI->>CLI: _setup_parser() 解析参数 + CLI->>CLI: _handle_add(args) + CLI->>Manager: add(title, description, due_date) + Manager->>Manager: 生成新TodoItem,设置id=next_id + Manager->>Manager: 将item加入items列表 + Manager->>Manager: _save() + Manager->>Storage: save([item.to_dict() for item in items]) + Storage->>File: 写入JSON文件 + File-->>Storage: 成功 + Storage-->>Manager: None + Manager-->>CLI: 返回新TodoItem + CLI-->>User: 打印添加成功信息 + + User->>CLI: python main.py list + CLI->>CLI: _handle_list(args) + CLI->>Manager: list_all() + Manager->>Manager: 返回items列表 + Manager-->>CLI: list[TodoItem] + CLI->>CLI: 格式化输出表格 + CLI-->>User: 显示所有任务 + + User->>CLI: python main.py done 1 + CLI->>CLI: _handle_done(args) + CLI->>Manager: mark_done(1) + Manager->>Manager: _find_by_id(1) -> item + Manager->>Manager: item.completed = True + Manager->>Manager: _save() + Manager-->>CLI: True + CLI-->>User: 打印标记成功 + + User->>CLI: python main.py delete 1 + CLI->>CLI: _handle_delete(args) + CLI->>Manager: delete(1) + Manager->>Manager: _find_by_id(1) -> item + Manager->>Manager: 从items中移除 + Manager->>Manager: _save() + Manager-->>CLI: True + CLI-->>User: 打印删除成功 + + User->>CLI: python main.py edit 1 --title "新标题" --description "新描述" + CLI->>CLI: _handle_edit(args) + CLI->>Manager: edit(1, title="新标题", description="新描述") + Manager->>Manager: _find_by_id(1) -> item + Manager->>Manager: 更新item属性 + Manager->>Manager: _save() + Manager-->>CLI: True + CLI-->>User: 打印编辑成功 + + +## Anything UNCLEAR + +无 + diff --git a/todo_cli/main.py b/todo_cli/main.py new file mode 100644 index 0000000..f4cadeb --- /dev/null +++ b/todo_cli/main.py @@ -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() diff --git a/todo_cli/storage.py b/todo_cli/storage.py new file mode 100644 index 0000000..ed5764c --- /dev/null +++ b/todo_cli/storage.py @@ -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) diff --git a/todo_cli/todo.py b/todo_cli/todo.py new file mode 100644 index 0000000..d175e80 --- /dev/null +++ b/todo_cli/todo.py @@ -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