cli-todo-jd 0.2.1__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.
cli_todo_jd/main.py CHANGED
@@ -1,10 +1,8 @@
1
1
  from pathlib import Path
2
- import questionary
3
2
  from rich.console import Console
4
3
  from rich.table import Table
5
4
  from rich.padding import Padding
6
5
  import sqlite3
7
-
8
6
  from cli_todo_jd.storage.schema import ensure_schema
9
7
  from cli_todo_jd.storage.migrate import migrate_from_json
10
8
 
@@ -14,17 +12,19 @@ def main():
14
12
 
15
13
 
16
14
  class TodoApp:
17
- """
18
- A simple command-line todo application.
19
- """
15
+ """A simple command-line todo application."""
20
16
 
21
17
  def __init__(self, file_path_to_db="./.todo_list.db"):
22
- self.todos = []
23
- self.status = []
18
+ self.todo_ids: list[int] = []
19
+ self.todos: list[str] = []
20
+ self.status: list[int] = []
24
21
  self.file_path_to_db = Path(file_path_to_db)
25
22
  self._check_and_load_todos(self.file_path_to_db)
26
23
  self._console = Console()
27
24
 
25
+ def reload_todos(self) -> None:
26
+ self._check_and_load_todos(self.file_path_to_db)
27
+
28
28
  def add_todo(self, item: str) -> None:
29
29
  item = (item or "").strip()
30
30
  if not item:
@@ -43,7 +43,6 @@ class TodoApp:
43
43
  return
44
44
 
45
45
  print(f'Added todo: "{item}"')
46
- self._check_and_load_todos(self.file_path_to_db)
47
46
 
48
47
  def list_todos(self, *, show: str = "open") -> None:
49
48
  """List todos.
@@ -61,35 +60,52 @@ class TodoApp:
61
60
  # Always read fresh so output reflects the DB
62
61
  self._check_and_load_todos(self.file_path_to_db)
63
62
  if not self.todos:
64
- print("No todos found.")
63
+ print("Your todo list is empty! Start adding some with 'todo add <task>'")
65
64
  return
66
65
 
67
66
  if show == "all":
68
67
  self._table_print(title="Todos")
69
68
  return
70
69
 
71
- # Filter in-memory to keep this change minimal. (You can later filter in SQL.)
70
+ # Filter in-memory to keep this change minimal.
71
+ filtered_ids: list[int] = []
72
72
  filtered_todos: list[str] = []
73
73
  filtered_status: list[int] = []
74
- for todo, done in zip(self.todos, self.status, strict=False):
74
+ for todo_id, todo, done in zip(
75
+ self.todo_ids, self.todos, self.status, strict=False
76
+ ):
75
77
  if show == "open" and not done:
78
+ filtered_ids.append(todo_id)
76
79
  filtered_todos.append(todo)
77
80
  filtered_status.append(done)
78
81
  elif show == "done" and done:
82
+ filtered_ids.append(todo_id)
79
83
  filtered_todos.append(todo)
80
84
  filtered_status.append(done)
81
85
 
82
86
  if not filtered_todos:
83
- print("No todos found.")
87
+ print(f"No todos found when filtering on {show}.")
84
88
  return
85
89
 
86
- original_todos, original_status = self.todos, self.status
90
+ original_ids, original_todos, original_status = (
91
+ self.todo_ids,
92
+ self.todos,
93
+ self.status,
94
+ )
87
95
  try:
88
- self.todos, self.status = filtered_todos, filtered_status
96
+ self.todo_ids, self.todos, self.status = (
97
+ filtered_ids,
98
+ filtered_todos,
99
+ filtered_status,
100
+ )
89
101
  title = "Open todos" if show == "open" else "Completed todos"
90
102
  self._table_print(title=title)
91
103
  finally:
92
- self.todos, self.status = original_todos, original_status
104
+ self.todo_ids, self.todos, self.status = (
105
+ original_ids,
106
+ original_todos,
107
+ original_status,
108
+ )
93
109
 
94
110
  def remove_todo(self, index: int) -> None:
95
111
  # Maintain current UX: index refers to the displayed (1-based) ordering.
@@ -119,7 +135,6 @@ class TodoApp:
119
135
  return
120
136
 
121
137
  print(f'Removed todo: "{removed_item}"')
122
- self._check_and_load_todos(self.file_path_to_db)
123
138
 
124
139
  def clear_all(self) -> None:
125
140
  try:
@@ -127,11 +142,16 @@ class TodoApp:
127
142
  ensure_schema(conn)
128
143
  with conn:
129
144
  conn.execute("DELETE FROM todos;")
145
+ # Reset AUTOINCREMENT counter so ids start from 1 again.
146
+ # This is SQLite-specific and only applies to tables created with AUTOINCREMENT.
147
+ conn.execute("DELETE FROM sqlite_sequence WHERE name = 'todos';")
130
148
  except sqlite3.Error as e:
131
149
  print(f"Error: Failed to clear todos. ({e})")
132
150
  return
133
151
 
152
+ self.todo_ids = []
134
153
  self.todos = []
154
+ self.status = []
135
155
  print("Cleared all todos.")
136
156
 
137
157
  def _check_and_load_todos(self, file_path: Path) -> None:
@@ -151,12 +171,13 @@ class TodoApp:
151
171
  "SELECT id, item, done, created_at, done_at FROM todos ORDER BY id"
152
172
  ).fetchall()
153
173
 
154
- # In-memory list is used by the interactive menu for selection.
155
- # Keep it as a simple list[str] for now.
174
+ # In-memory lists are used by the interactive menu.
175
+ self.todo_ids = [int(row[0]) for row in rows]
156
176
  self.todos = [row[1] for row in rows]
157
177
  self.status = [row[2] for row in rows]
158
178
  except sqlite3.Error as e:
159
179
  print(f"Warning: Failed to load existing todos. Starting fresh. ({e})")
180
+ self.todo_ids = []
160
181
  self.todos = []
161
182
  self.status = []
162
183
 
@@ -172,11 +193,13 @@ class TodoApp:
172
193
  for col in columns:
173
194
  table.add_column(str(col))
174
195
 
175
- for idx, todo in enumerate(self.todos, start=1):
196
+ for todo_id, todo, done in zip(
197
+ self.todo_ids, self.todos, self.status, strict=False
198
+ ):
176
199
  table.add_row(
177
- f"{idx}.",
200
+ str(todo_id),
178
201
  str(todo),
179
- "[green]✔[/green]" if self.status[idx - 1] else "[red]✖[/red]",
202
+ "[green]✔[/green]" if done else "[red]✖[/red]",
180
203
  )
181
204
 
182
205
  self._console.print(Padding(table, (2, 2)))
@@ -211,7 +234,6 @@ class TodoApp:
211
234
  return
212
235
 
213
236
  print(f'Marked todo as not done: "{item}"')
214
- self._check_and_load_todos(self.file_path_to_db)
215
237
 
216
238
  def mark_as_done(self, index: int) -> None:
217
239
  self._check_and_load_todos(self.file_path_to_db)
@@ -243,7 +265,6 @@ class TodoApp:
243
265
  return
244
266
 
245
267
  print(f'Marked todo as done: "{item}"')
246
- self._check_and_load_todos(self.file_path_to_db)
247
268
 
248
269
  def update_done_data(self, index, done_value, done_at_value, todo_id):
249
270
  text_done_value = "done" if done_value == 1 else "not done"
@@ -310,171 +331,101 @@ class TodoApp:
310
331
  return
311
332
 
312
333
  print(f'Edited todo: "{old_item}" to "{new_text}"')
313
- self._check_and_load_todos(self.file_path_to_db)
314
334
 
335
+ def remove_by_id(self, todo_id: int) -> None:
336
+ try:
337
+ with sqlite3.connect(self.file_path_to_db) as conn:
338
+ ensure_schema(conn)
339
+ row = conn.execute(
340
+ "SELECT id, item FROM todos WHERE id = ?;",
341
+ (todo_id,),
342
+ ).fetchone()
343
+ if row is None:
344
+ print("Error: Invalid todo id.")
345
+ return
346
+
347
+ _, removed_item = row
348
+ with conn:
349
+ conn.execute("DELETE FROM todos WHERE id = ?;", (todo_id,))
350
+ except sqlite3.Error as e:
351
+ print(f"Error: Failed to remove todo. ({e})")
352
+ return
353
+
354
+ print(f'Removed todo: "{removed_item}"')
355
+
356
+ def mark_done_by_id(self, todo_id: int) -> None:
357
+ try:
358
+ with sqlite3.connect(self.file_path_to_db) as conn:
359
+ ensure_schema(conn)
360
+ row = conn.execute(
361
+ "SELECT id, item FROM todos WHERE id = ?;",
362
+ (todo_id,),
363
+ ).fetchone()
364
+ if row is None:
365
+ print("Error: Invalid todo id.")
366
+ return
367
+
368
+ _, item = row
369
+ with conn:
370
+ conn.execute(
371
+ "UPDATE todos SET done = 1, done_at = datetime('now') WHERE id = ?;",
372
+ (todo_id,),
373
+ )
374
+ except sqlite3.Error as e:
375
+ print(f"Error: Failed to mark todo as done. ({e})")
376
+ return
315
377
 
316
- def create_list(file_path_to_db: str = "./.todo_list.db"):
317
- """
318
- Create a new todo list.
319
-
320
- Parameters
321
- ----------
322
- file_path_to_db : str, optional
323
- The file path to the JSON file for storing todos, by default "./.todo_list.db"
324
-
325
- Returns
326
- -------
327
- TodoApp
328
- An instance of the TodoApp class.
329
- """
330
- app = TodoApp(file_path_to_db=file_path_to_db)
331
- return app
332
-
333
-
334
- def add_item_to_list(item: str, filepath: str):
335
- """
336
- Add a new item to the todo list.
337
-
338
- Parameters
339
- ----------
340
- item : str
341
- The todo item to add.
342
- filepath : str
343
- The file path to the JSON file for storing todos.
344
- """
345
- app = create_list(file_path_to_db=filepath)
346
- app.add_todo(item)
347
- app.list_todos()
348
-
349
-
350
- def list_items_on_list(filepath: str, show: str = "open"):
351
- """List items in the todo list.
352
-
353
- Parameters
354
- ----------
355
- filepath:
356
- The SQLite database path.
357
- show:
358
- "open" (default), "done", or "all".
359
- """
360
- app = create_list(file_path_to_db=filepath)
361
- app.list_todos(show=show)
362
-
363
-
364
- def remove_item_from_list(index: int, filepath: str):
365
- """
366
- remove an item from the todo list using index
367
-
368
- Parameters
369
- ----------
370
- index : int
371
- The index of the todo item to remove.
372
- filepath : str
373
- The file path to the JSON file for storing todos.
374
- """
375
- app = create_list(file_path_to_db=filepath)
376
- app.remove_todo(index)
377
- app.list_todos()
378
-
379
-
380
- def clear_list_of_items(filepath: str):
381
- """
382
- Clear all items from the todo list.
383
-
384
- Parameters
385
- ----------
386
- filepath : str
387
- The file path to the JSON file for storing todos.
388
- """
389
- app = create_list(file_path_to_db=filepath)
390
- app.clear_all()
391
-
392
-
393
- def mark_item_as_done(index: int, filepath: str):
394
- app = create_list(file_path_to_db=filepath)
395
- app.mark_as_done(index)
396
-
397
-
398
- def mark_item_as_not_done(index: int, filepath: str):
399
- app = create_list(file_path_to_db=filepath)
400
- app.mark_as_not_done(index)
401
-
402
-
403
- def cli_menu(filepath="./.todo_list.db"):
404
- """
405
- Display the command-line interface menu for the todo list.
406
-
407
- Parameters
408
- ----------
409
- filepath : str, optional
410
- The file path to the JSON file for storing todos, by default "./.todo_list.db"
411
- """
412
- app = create_list(file_path_to_db=filepath)
413
- while True:
414
- action = questionary.select(
415
- "What would you like to do?",
416
- choices=[
417
- "Add todo",
418
- "List todos",
419
- "Update todo status",
420
- "Remove todo",
421
- "Clear all todos",
422
- "Exit",
423
- ],
424
- ).ask()
425
-
426
- if action == "Add todo":
427
- item = questionary.text("Enter the todo item:").ask()
428
- app.add_todo(item)
429
- elif action == "List todos":
430
- app.list_todos(show="all")
431
- elif action == "Update todo status":
432
- if not app.todos:
433
- print("No todos to update.")
434
- continue
435
- todo_choice = questionary.select(
436
- "Select the todo to update:",
437
- choices=["<Back>"] + app.todos,
438
- ).ask()
439
-
440
- if todo_choice == "<Back>" or todo_choice is None:
441
- continue
442
-
443
- todo_index = app.todos.index(todo_choice) + 1
444
- status_choice = questionary.select(
445
- "Mark as:",
446
- choices=["Done", "Not Done", "<Back>"],
447
- ).ask()
448
-
449
- if status_choice == "<Back>" or status_choice is None:
450
- continue
451
- elif status_choice == "Done":
452
- app.mark_as_done(todo_index)
453
- elif status_choice == "Not Done":
454
- app.mark_as_not_done(todo_index)
455
- app.list_todos(show="all")
456
- elif action == "Remove todo":
457
- if not app.todos:
458
- print("No todos to remove.")
459
- continue
460
- todo_choice = questionary.select(
461
- "Select the todo to remove:",
462
- choices=["<Back>"] + app.todos,
463
- ).ask()
464
-
465
- if todo_choice == "<Back>" or todo_choice is None:
466
- continue
467
-
468
- todo_to_remove = app.todos.index(todo_choice) + 1
469
- app.remove_todo(todo_to_remove)
470
-
471
- elif action == "Clear all todos":
472
- confirm = questionary.confirm(
473
- "Are you sure you want to clear all todos?"
474
- ).ask()
475
- if confirm:
476
- app.clear_all()
477
- elif action == "Exit":
478
- break
479
- else:
480
- break
378
+ print(f'Marked todo as done: "{item}"')
379
+
380
+ def mark_not_done_by_id(self, todo_id: int) -> None:
381
+ try:
382
+ with sqlite3.connect(self.file_path_to_db) as conn:
383
+ ensure_schema(conn)
384
+ row = conn.execute(
385
+ "SELECT id, item FROM todos WHERE id = ?;",
386
+ (todo_id,),
387
+ ).fetchone()
388
+ if row is None:
389
+ print("Error: Invalid todo id.")
390
+ return
391
+
392
+ _, item = row
393
+ with conn:
394
+ conn.execute(
395
+ "UPDATE todos SET done = 0, done_at = NULL WHERE id = ?;",
396
+ (todo_id,),
397
+ )
398
+ except sqlite3.Error as e:
399
+ print(f"Error: Failed to mark todo as not done. ({e})")
400
+ return
401
+
402
+ print(f'Marked todo as not done: "{item}"')
403
+
404
+ def edit_by_id(self, todo_id: int, new_text: str) -> None:
405
+ new_text = (new_text or "").strip()
406
+ if not new_text:
407
+ print("Error: Todo item cannot be empty.")
408
+ return
409
+
410
+ try:
411
+ with sqlite3.connect(self.file_path_to_db) as conn:
412
+ ensure_schema(conn)
413
+ row = conn.execute(
414
+ "SELECT id, item FROM todos WHERE id = ?;",
415
+ (todo_id,),
416
+ ).fetchone()
417
+ if row is None:
418
+ print("Error: Invalid todo id.")
419
+ return
420
+
421
+ _, old_item = row
422
+ with conn:
423
+ conn.execute(
424
+ "UPDATE todos SET item = ? WHERE id = ?;",
425
+ (new_text, todo_id),
426
+ )
427
+ except sqlite3.Error as e:
428
+ print(f"Error: Failed to edit todo. ({e})")
429
+ return
430
+
431
+ print(f'Edited todo: "{old_item}" to "{new_text}"')
cli_todo_jd/web/app.py ADDED
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ import sqlite3
4
+ from pathlib import Path
5
+
6
+ from flask import Flask, redirect, render_template, request, url_for
7
+
8
+ from cli_todo_jd.storage.schema import ensure_schema
9
+ from cli_todo_jd.storage.migrate import migrate_from_json
10
+
11
+
12
+ def create_app(db_path: Path) -> Flask:
13
+ app = Flask(__name__)
14
+ app.config["TODO_DB_PATH"] = str(db_path)
15
+
16
+ def _connect() -> sqlite3.Connection:
17
+ conn = sqlite3.connect(db_path)
18
+ conn.row_factory = sqlite3.Row
19
+ ensure_schema(conn)
20
+ return conn
21
+
22
+ # Optional one-time JSON migration (mirrors CLI behavior)
23
+ json_path = db_path.with_suffix(".json")
24
+ if json_path.exists() and db_path.suffix == ".db":
25
+ migrate_from_json(json_path=json_path, db_path=db_path, backup=True)
26
+
27
+ @app.get("/")
28
+ def index():
29
+ show = request.args.get("show", "open")
30
+ if show not in {"open", "done", "all"}:
31
+ show = "open"
32
+
33
+ with _connect() as conn:
34
+ if show == "open":
35
+ todos = conn.execute(
36
+ "SELECT id, item, done, created_at, done_at FROM todos WHERE done = ? ORDER BY id DESC",
37
+ (0,),
38
+ ).fetchall()
39
+ elif show == "done":
40
+ todos = conn.execute(
41
+ "SELECT id, item, done, created_at, done_at FROM todos WHERE done = ? ORDER BY id DESC",
42
+ (1,),
43
+ ).fetchall()
44
+ else:
45
+ todos = conn.execute(
46
+ "SELECT id, item, done, created_at, done_at FROM todos ORDER BY id DESC"
47
+ ).fetchall()
48
+
49
+ return render_template("index.html", todos=todos, show=show)
50
+
51
+ @app.post("/add")
52
+ def add():
53
+ item = (request.form.get("item") or "").strip()
54
+ if item:
55
+ with _connect() as conn:
56
+ with conn:
57
+ conn.execute("INSERT INTO todos(item, done) VALUES (?, 0)", (item,))
58
+ return redirect(url_for("index"))
59
+
60
+ @app.post("/toggle/<int:todo_id>")
61
+ def toggle(todo_id: int):
62
+ with _connect() as conn:
63
+ row = conn.execute(
64
+ "SELECT done FROM todos WHERE id = ?", (todo_id,)
65
+ ).fetchone()
66
+ if row is not None:
67
+ new_done = 0 if row["done"] else 1
68
+ with conn:
69
+ if new_done:
70
+ conn.execute(
71
+ "UPDATE todos SET done = 1, done_at = datetime('now') WHERE id = ?",
72
+ (todo_id,),
73
+ )
74
+ else:
75
+ conn.execute(
76
+ "UPDATE todos SET done = 0, done_at = NULL WHERE id = ?",
77
+ (todo_id,),
78
+ )
79
+ return redirect(url_for("index"))
80
+
81
+ @app.post("/delete/<int:todo_id>")
82
+ def delete(todo_id: int):
83
+ with _connect() as conn:
84
+ with conn:
85
+ conn.execute("DELETE FROM todos WHERE id = ?", (todo_id,))
86
+ return redirect(url_for("index"))
87
+
88
+ @app.post("/clear")
89
+ def clear():
90
+ if request.form.get("confirm") == "yes":
91
+ with _connect() as conn:
92
+ with conn:
93
+ conn.execute("DELETE FROM todos")
94
+ return redirect(url_for("index"))
95
+
96
+ return app
97
+
98
+
99
+ def run_web(
100
+ db_path: Path, host: str = "127.0.0.1", port: int = 8000, debug: bool = False
101
+ ) -> None:
102
+ db_path = Path(db_path)
103
+ db_path.parent.mkdir(parents=True, exist_ok=True)
104
+
105
+ app = create_app(db_path)
106
+ app.run(host=host, port=port, debug=debug)