cli-todo-jd 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,243 @@
1
+ from __future__ import annotations
2
+
3
+ from argparse import ArgumentParser
4
+ from cli_todo_jd.helpers import (
5
+ add_item_to_list,
6
+ remove_item_from_list,
7
+ remove_item_from_list_by_id,
8
+ list_items_on_list,
9
+ clear_list_of_items,
10
+ mark_item_as_done,
11
+ mark_item_as_not_done,
12
+ mark_item_as_done_by_id,
13
+ mark_item_as_not_done_by_id,
14
+ edit_item_in_list_by_id,
15
+ )
16
+ from cli_todo_jd.cli.cli_menu import cli_menu
17
+ from cli_todo_jd.web.app import run_web
18
+ from pathlib import Path
19
+ import typer
20
+
21
+ app = typer.Typer(help="A tiny todo CLI built with Typer.")
22
+
23
+
24
+ @app.command()
25
+ def add(
26
+ text: list[str] = typer.Argument(..., help="Todo item text (no quotes needed)."),
27
+ filepath: Path = typer.Option(
28
+ Path(".todo_list.db"),
29
+ "--filepath",
30
+ "-f",
31
+ help="Path to the JSON file used for storage.",
32
+ ),
33
+ ) -> None:
34
+ full_text = " ".join(text).strip()
35
+ if not full_text:
36
+ raise typer.BadParameter("Todo item text cannot be empty.")
37
+
38
+ add_item_to_list(full_text, filepath)
39
+ typer.echo(f"Added: {full_text}")
40
+
41
+
42
+ @app.command(name="list")
43
+ def list_(
44
+ filepath: Path = typer.Option(Path(".todo_list.db"), "--filepath", "-f"),
45
+ show_all: bool = typer.Option(
46
+ False, "--all", "-a", help="Show all todos (open + done)."
47
+ ),
48
+ show_done: bool = typer.Option(
49
+ False, "--done", "-d", help="Show only completed todos."
50
+ ),
51
+ show_open: bool = typer.Option(
52
+ False, "--open", "-o", help="Show only open todos (default)."
53
+ ),
54
+ ) -> None:
55
+ """List todos.
56
+
57
+ Examples
58
+ --------
59
+ - todo list
60
+ - todo list --done
61
+ - todo list --all
62
+ - todo list -a
63
+ """
64
+
65
+ # Choose filter. If nothing specified, default to open.
66
+ # If the user specifies multiple flags, error out.
67
+ flags = [show_all, show_done, show_open]
68
+ if sum(1 for f in flags if f) > 1:
69
+ raise typer.BadParameter(
70
+ "Use only one of: --all / -a, --done / -d, --open / -o"
71
+ )
72
+
73
+ if show_all:
74
+ show = "all"
75
+ elif show_done:
76
+ show = "done"
77
+ else:
78
+ # default is open (or explicit --open)
79
+ show = "open"
80
+
81
+ list_items_on_list(filepath, show=show)
82
+
83
+
84
+ @app.command()
85
+ def remove(
86
+ todo_id: int | None = typer.Argument(None, help="Todo ID to remove (preferred)."),
87
+ index: int | None = typer.Option(
88
+ None,
89
+ "--index",
90
+ "-i",
91
+ help="1-based display index (legacy; use ID instead).",
92
+ ),
93
+ filepath: Path = typer.Option(Path(".todo_list.db"), "--filepath", "-f"),
94
+ ) -> None:
95
+ if todo_id is None and index is None:
96
+ raise typer.BadParameter("Provide either TODO_ID argument or --index/-i")
97
+ if todo_id is not None and index is not None:
98
+ raise typer.BadParameter("Provide either TODO_ID or --index/-i, not both")
99
+
100
+ if todo_id is not None:
101
+ remove_item_from_list_by_id(todo_id, filepath)
102
+ else:
103
+ remove_item_from_list(index, filepath)
104
+
105
+
106
+ @app.command()
107
+ def clear(
108
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
109
+ filepath: Path = typer.Option(Path(".todo_list.db"), "--filepath", "-f"),
110
+ ) -> None:
111
+ if not yes and not typer.confirm(f"Clear all todos in {filepath}?"):
112
+ typer.echo("Cancelled.")
113
+ raise typer.Exit(code=1)
114
+
115
+ clear_list_of_items(filepath)
116
+
117
+
118
+ @app.command()
119
+ def edit(
120
+ todo_id: int = typer.Argument(..., help="Todo ID to edit."),
121
+ new_text: list[str] = typer.Argument(..., help="New text for the todo item."),
122
+ filepath: Path = typer.Option(Path(".todo_list.db"), "--filepath", "-f"),
123
+ ) -> None:
124
+ new_text_stripped = " ".join(new_text).strip()
125
+ if not new_text_stripped:
126
+ raise typer.BadParameter("New todo item text cannot be empty.")
127
+
128
+ edit_item_in_list_by_id(todo_id, new_text_stripped, filepath)
129
+ typer.echo(f'Edited todo ID {todo_id} to: "{new_text_stripped}"')
130
+
131
+
132
+ @app.command(name="menu")
133
+ def menu_(
134
+ filepath: Path = typer.Option(
135
+ Path(".todo_list.db"),
136
+ "--filepath",
137
+ "-f",
138
+ help="Path to the JSON file used for storage.",
139
+ ),
140
+ ) -> None:
141
+ cli_menu(filepath)
142
+ typer.echo("Exited menu.")
143
+
144
+
145
+ @app.command()
146
+ def done(
147
+ todo_id: int | None = typer.Argument(
148
+ None, help="Todo ID to mark as done (preferred)."
149
+ ),
150
+ index: int | None = typer.Option(
151
+ None,
152
+ "--index",
153
+ "-i",
154
+ help="1-based display index (legacy; use ID instead).",
155
+ ),
156
+ filepath: Path = typer.Option(Path(".todo_list.db"), "--filepath", "-f"),
157
+ ) -> None:
158
+ if todo_id is None and index is None:
159
+ raise typer.BadParameter("Provide either TODO_ID argument or --index/-i")
160
+ if todo_id is not None and index is not None:
161
+ raise typer.BadParameter("Provide either TODO_ID or --index/-i, not both")
162
+
163
+ if todo_id is not None:
164
+ mark_item_as_done_by_id(todo_id, filepath)
165
+ else:
166
+ mark_item_as_done(index, filepath)
167
+
168
+ list_items_on_list(filepath=filepath, show="all")
169
+
170
+
171
+ @app.command(name="not-done")
172
+ def not_done(
173
+ todo_id: int | None = typer.Argument(
174
+ None, help="Todo ID to mark as not done (preferred)."
175
+ ),
176
+ index: int | None = typer.Option(
177
+ None,
178
+ "--index",
179
+ "-i",
180
+ help="1-based display index (legacy; use ID instead).",
181
+ ),
182
+ filepath: Path = typer.Option(Path(".todo_list.db"), "--filepath", "-f"),
183
+ ) -> None:
184
+ if todo_id is None and index is None:
185
+ raise typer.BadParameter("Provide either TODO_ID argument or --index/-i")
186
+ if todo_id is not None and index is not None:
187
+ raise typer.BadParameter("Provide either TODO_ID or --index/-i, not both")
188
+
189
+ if todo_id is not None:
190
+ mark_item_as_not_done_by_id(todo_id, filepath)
191
+ else:
192
+ mark_item_as_not_done(index, filepath)
193
+
194
+ list_items_on_list(filepath=filepath, show="all")
195
+
196
+
197
+ @app.command()
198
+ def web(
199
+ filepath: Path = typer.Option(Path(".todo_list.db"), "--filepath", "-f"),
200
+ host: str = typer.Option(
201
+ "127.0.0.1", help="Host interface to bind the web server."
202
+ ),
203
+ port: int = typer.Option(8000, help="Port to run the web server on."),
204
+ debug: bool = typer.Option(False, help="Run Flask in debug mode."),
205
+ ) -> None:
206
+ """Run a local web UI for your todo list."""
207
+ run_web(filepath, host=host, port=port, debug=debug)
208
+
209
+
210
+ def parser_optional_args(parser: ArgumentParser):
211
+ parser.add_argument(
212
+ "-f",
213
+ "--filepath",
214
+ help="Path to the file to process",
215
+ default="./.todo_list.db",
216
+ )
217
+
218
+
219
+ def todo_menu():
220
+ parser = ArgumentParser(description="Todo List CLI Menu")
221
+ parser_optional_args(parser)
222
+ args = parser.parse_args()
223
+
224
+ cli_menu(filepath=args.filepath)
225
+
226
+
227
+ def todo_web():
228
+ parser = ArgumentParser(description="Todo List Web Server")
229
+ parser_optional_args(parser)
230
+ parser.add_argument(
231
+ "--host", help="Host interface to bind the web server.", default="127.0.0.1"
232
+ )
233
+ parser.add_argument(
234
+ "--port", help="Port to run the web server on.", default=8000, type=int
235
+ )
236
+ parser.add_argument("--debug", help="Run Flask in debug mode.", action="store_true")
237
+ args = parser.parse_args()
238
+
239
+ run_web(db_path=args.filepath, host=args.host, port=args.port, debug=args.debug)
240
+
241
+
242
+ if __name__ == "__main__":
243
+ app()
@@ -0,0 +1,129 @@
1
+ from questionary import Style
2
+ import questionary
3
+ from cli_todo_jd.helpers import create_list
4
+
5
+ custom_style = Style(
6
+ [
7
+ ("qmark", "fg:#ff9d00 bold"),
8
+ ("question", "bold"),
9
+ ("answer", "fg:#ff9d00 bold"),
10
+ ("pointer", "fg:#ff9d00 bold"),
11
+ ("highlighted", "fg:#ff9d00 bold"),
12
+ ("selected", "fg:#ff9d00 bold"),
13
+ ("separator", "fg:#ff9d00 bold"),
14
+ ("instruction", ""), # default
15
+ ("text", ""),
16
+ ("disabled", "fg:#ff9d00 italic"),
17
+ ]
18
+ )
19
+
20
+
21
+ def cli_menu(filepath="./.todo_list.db"):
22
+ """
23
+ Display the command-line interface menu for the todo list.
24
+
25
+ Parameters
26
+ ----------
27
+ filepath : str, optional
28
+ The file path to the JSON file for storing todos, by default "./.todo_list.db"
29
+ """
30
+ app = create_list(file_path_to_db=filepath)
31
+ while True:
32
+ action = questionary.select(
33
+ "What would you like to do?",
34
+ choices=[
35
+ "Add todo",
36
+ "List todos",
37
+ "Update todo status",
38
+ "Remove todo",
39
+ "Edit todo",
40
+ "Clear all todos",
41
+ "Exit",
42
+ ],
43
+ style=custom_style,
44
+ ).ask()
45
+
46
+ if action == "Add todo":
47
+ item = questionary.text("Enter the todo item:", style=custom_style).ask()
48
+ app.add_todo(item)
49
+ elif action == "List todos":
50
+ app.list_todos(show="all")
51
+ elif action == "Update todo status":
52
+ app.reload_todos()
53
+ if not app.todos:
54
+ print("No todos to update.")
55
+ continue
56
+ todo_choice = questionary.select(
57
+ "Select the todo to update:",
58
+ choices=["<Back>"] + app.todos,
59
+ style=custom_style,
60
+ ).ask()
61
+
62
+ if todo_choice == "<Back>" or todo_choice is None:
63
+ continue
64
+
65
+ todo_index = app.todos.index(todo_choice) + 1
66
+ status_choice = questionary.select(
67
+ "Mark as:",
68
+ choices=["Done", "Not Done", "<Back>"],
69
+ style=custom_style,
70
+ ).ask()
71
+
72
+ if status_choice == "<Back>" or status_choice is None:
73
+ continue
74
+ elif status_choice == "Done":
75
+ app.mark_as_done(todo_index)
76
+ elif status_choice == "Not Done":
77
+ app.mark_as_not_done(todo_index)
78
+ app.list_todos(show="all")
79
+ elif action == "Remove todo":
80
+ app.reload_todos()
81
+ if not app.todos:
82
+ print("No todos to remove.")
83
+ continue
84
+ todo_choice = questionary.select(
85
+ "Select the todo to remove:",
86
+ choices=["<Back>"] + app.todos,
87
+ style=custom_style,
88
+ ).ask()
89
+
90
+ if todo_choice == "<Back>" or todo_choice is None:
91
+ continue
92
+
93
+ todo_to_remove = app.todos.index(todo_choice) + 1
94
+ app.remove_todo(todo_to_remove)
95
+ elif action == "Edit todo":
96
+ app.reload_todos()
97
+ if not app.todos:
98
+ print("No todos to edit.")
99
+ continue
100
+ todo_choice = questionary.select(
101
+ "Select the todo to edit:",
102
+ choices=["<Back>"] + app.todos,
103
+ style=custom_style,
104
+ ).ask()
105
+
106
+ if todo_choice == "<Back>" or todo_choice is None:
107
+ continue
108
+
109
+ todo_index = app.todos.index(todo_choice) + 1
110
+ new_text = questionary.text(
111
+ "Enter the new text for the todo:",
112
+ default=todo_choice,
113
+ style=custom_style,
114
+ ).ask()
115
+
116
+ if new_text is None:
117
+ continue
118
+ app.edit_entry(todo_index, new_text)
119
+
120
+ elif action == "Clear all todos":
121
+ confirm = questionary.confirm(
122
+ "Are you sure you want to clear all todos?", style=custom_style
123
+ ).ask()
124
+ if confirm:
125
+ app.clear_all()
126
+ elif action == "Exit":
127
+ break
128
+ else:
129
+ break
cli_todo_jd/helpers.py ADDED
@@ -0,0 +1,109 @@
1
+ from cli_todo_jd.main import TodoApp
2
+
3
+
4
+ def create_list(file_path_to_db: str = "./.todo_list.db"):
5
+ """
6
+ Create a new todo list.
7
+
8
+ Parameters
9
+ ----------
10
+ file_path_to_db : str, optional
11
+ The file path to the JSON file for storing todos, by default "./.todo_list.db"
12
+
13
+ Returns
14
+ -------
15
+ TodoApp
16
+ An instance of the TodoApp class.
17
+ """
18
+ app = TodoApp(file_path_to_db=file_path_to_db)
19
+ return app
20
+
21
+
22
+ def add_item_to_list(item: str, filepath: str):
23
+ """
24
+ Add a new item to the todo list.
25
+
26
+ Parameters
27
+ ----------
28
+ item : str
29
+ The todo item to add.
30
+ filepath : str
31
+ The file path to the JSON file for storing todos.
32
+ """
33
+ app = create_list(file_path_to_db=filepath)
34
+ app.add_todo(item)
35
+ app.list_todos()
36
+
37
+
38
+ def list_items_on_list(filepath: str, show: str = "open"):
39
+ """List items in the todo list.
40
+
41
+ Parameters
42
+ ----------
43
+ filepath:
44
+ The SQLite database path.
45
+ show:
46
+ "open" (default), "done", or "all".
47
+ """
48
+ app = create_list(file_path_to_db=filepath)
49
+ app.list_todos(show=show)
50
+
51
+
52
+ def remove_item_from_list(index: int, filepath: str):
53
+ """
54
+ remove an item from the todo list using index
55
+
56
+ Parameters
57
+ ----------
58
+ index : int
59
+ The index of the todo item to remove.
60
+ filepath : str
61
+ The file path to the JSON file for storing todos.
62
+ """
63
+ app = create_list(file_path_to_db=filepath)
64
+ app.remove_todo(index)
65
+ app.list_todos()
66
+
67
+
68
+ def clear_list_of_items(filepath: str):
69
+ """
70
+ Clear all items from the todo list.
71
+
72
+ Parameters
73
+ ----------
74
+ filepath : str
75
+ The file path to the JSON file for storing todos.
76
+ """
77
+ app = create_list(file_path_to_db=filepath)
78
+ app.clear_all()
79
+
80
+
81
+ def mark_item_as_done(index: int, filepath: str):
82
+ app = create_list(file_path_to_db=filepath)
83
+ app.mark_as_done(index)
84
+
85
+
86
+ def mark_item_as_not_done(index: int, filepath: str):
87
+ app = create_list(file_path_to_db=filepath)
88
+ app.mark_as_not_done(index)
89
+
90
+
91
+ def remove_item_from_list_by_id(todo_id: int, filepath: str):
92
+ app = create_list(file_path_to_db=filepath)
93
+ app.remove_by_id(todo_id)
94
+ app.list_todos(show="all")
95
+
96
+
97
+ def mark_item_as_done_by_id(todo_id: int, filepath: str):
98
+ app = create_list(file_path_to_db=filepath)
99
+ app.mark_done_by_id(todo_id)
100
+
101
+
102
+ def mark_item_as_not_done_by_id(todo_id: int, filepath: str):
103
+ app = create_list(file_path_to_db=filepath)
104
+ app.mark_not_done_by_id(todo_id)
105
+
106
+
107
+ def edit_item_in_list_by_id(todo_id: int, new_text: str, filepath: str):
108
+ app = create_list(file_path_to_db=filepath)
109
+ app.edit_by_id(todo_id, new_text)