personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add add/done/cancel/upcoming CLI commands to todos app

Add four todos CLI commands in apps/todos/call.py: add, done, cancel, and upcoming.\nAdd corresponding tests in apps/todos/tests/test_call.py.

+203
+96
apps/todos/call.py
··· 81 81 typer.echo(f"## {f}") 82 82 _print_day_facet(day, f) 83 83 typer.echo() 84 + 85 + 86 + @app.command("add") 87 + def add_todo( 88 + day: str = typer.Argument(help="Journal day in YYYYMMDD format."), 89 + text: str = typer.Argument(help="Todo item text."), 90 + facet: str = typer.Option(..., "--facet", "-f", help="Facet name."), 91 + ) -> None: 92 + """Add a new todo item.""" 93 + from datetime import datetime 94 + 95 + from think.utils import get_journal 96 + 97 + get_journal() 98 + 99 + # Reject past dates 100 + try: 101 + todo_date = datetime.strptime(day, "%Y%m%d").date() 102 + except ValueError: 103 + typer.echo(f"Error: invalid day format '{day}'", err=True) 104 + raise typer.Exit(1) 105 + 106 + if todo_date < datetime.now().date(): 107 + typer.echo(f"Error: cannot add todo to past date {day}", err=True) 108 + raise typer.Exit(1) 109 + 110 + try: 111 + checklist = todo.TodoChecklist.load(day, facet) 112 + line_number = len(checklist.items) + 1 113 + checklist.add_entry(line_number, text) 114 + typer.echo(checklist.display()) 115 + except todo.TodoEmptyTextError: 116 + typer.echo("Error: todo text cannot be empty", err=True) 117 + raise typer.Exit(1) 118 + 119 + 120 + @app.command("done") 121 + def done_todo( 122 + day: str = typer.Argument(help="Journal day in YYYYMMDD format."), 123 + line_number: int = typer.Argument(help="1-based line number of the todo."), 124 + facet: str = typer.Option(..., "--facet", "-f", help="Facet name."), 125 + ) -> None: 126 + """Mark a todo item as done.""" 127 + from think.utils import get_journal 128 + 129 + get_journal() 130 + 131 + try: 132 + checklist = todo.TodoChecklist.load(day, facet) 133 + checklist.mark_done(line_number) 134 + typer.echo(checklist.display()) 135 + except FileNotFoundError: 136 + typer.echo(f"Error: no todos found for facet '{facet}' on {day}", err=True) 137 + raise typer.Exit(1) 138 + except IndexError as exc: 139 + typer.echo(f"Error: {exc}", err=True) 140 + raise typer.Exit(1) 141 + 142 + 143 + @app.command("cancel") 144 + def cancel_todo( 145 + day: str = typer.Argument(help="Journal day in YYYYMMDD format."), 146 + line_number: int = typer.Argument(help="1-based line number of the todo."), 147 + facet: str = typer.Option(..., "--facet", "-f", help="Facet name."), 148 + ) -> None: 149 + """Cancel a todo item.""" 150 + from think.utils import get_journal 151 + 152 + get_journal() 153 + 154 + try: 155 + checklist = todo.TodoChecklist.load(day, facet) 156 + checklist.cancel_entry(line_number) 157 + typer.echo(checklist.display()) 158 + except FileNotFoundError: 159 + typer.echo(f"Error: no todos found for facet '{facet}' on {day}", err=True) 160 + raise typer.Exit(1) 161 + except IndexError as exc: 162 + typer.echo(f"Error: {exc}", err=True) 163 + raise typer.Exit(1) 164 + 165 + 166 + @app.command("upcoming") 167 + def upcoming_todos( 168 + limit: int = typer.Option(20, "--limit", "-l", help="Maximum number of todos."), 169 + facet: str | None = typer.Option( 170 + None, "--facet", "-f", help="Facet name. Omit to show all facets." 171 + ), 172 + ) -> None: 173 + """Show upcoming todos across future days.""" 174 + from think.utils import get_journal 175 + 176 + get_journal() 177 + 178 + result = todo.upcoming(limit=limit, facet=facet) 179 + typer.echo(result)
+107
apps/todos/tests/test_call.py
··· 54 54 ) 55 55 assert result.exit_code == 1 56 56 assert "Error" in result.output 57 + 58 + 59 + class TestTodosAdd: 60 + """Tests for 'sol call todos add' command.""" 61 + 62 + def test_add_todo(self, todo_env): 63 + """Add a todo to a future day.""" 64 + todo_env([], day="29991231") 65 + result = runner.invoke( 66 + call_app, 67 + ["todos", "add", "29991231", "Ship feature", "--facet", "personal"], 68 + ) 69 + assert result.exit_code == 0 70 + assert "Ship feature" in result.output 71 + 72 + def test_add_appends_to_existing(self, todo_env): 73 + """Add appends after existing items.""" 74 + todo_env([{"text": "First"}], day="29991231") 75 + result = runner.invoke( 76 + call_app, ["todos", "add", "29991231", "Second", "--facet", "personal"] 77 + ) 78 + assert result.exit_code == 0 79 + assert "First" in result.output 80 + assert "Second" in result.output 81 + 82 + def test_add_past_date_rejected(self, todo_env): 83 + """Adding to a past date fails.""" 84 + todo_env([], day="20200101") 85 + result = runner.invoke( 86 + call_app, ["todos", "add", "20200101", "Nope", "--facet", "personal"] 87 + ) 88 + assert result.exit_code == 1 89 + 90 + def test_add_empty_text_rejected(self, todo_env): 91 + """Adding empty text fails.""" 92 + todo_env([], day="29991231") 93 + result = runner.invoke( 94 + call_app, ["todos", "add", "29991231", " ", "--facet", "personal"] 95 + ) 96 + assert result.exit_code == 1 97 + 98 + 99 + class TestTodosDone: 100 + """Tests for 'sol call todos done' command.""" 101 + 102 + def test_done_marks_complete(self, todo_env): 103 + """Mark a todo as done.""" 104 + todo_env([{"text": "Buy milk"}], day="20240101") 105 + result = runner.invoke( 106 + call_app, ["todos", "done", "20240101", "1", "--facet", "personal"] 107 + ) 108 + assert result.exit_code == 0 109 + assert "[x]" in result.output 110 + 111 + def test_done_invalid_line_number(self, todo_env): 112 + """Invalid line number fails.""" 113 + todo_env([{"text": "Only one"}], day="20240101") 114 + result = runner.invoke( 115 + call_app, ["todos", "done", "20240101", "5", "--facet", "personal"] 116 + ) 117 + assert result.exit_code == 1 118 + 119 + 120 + class TestTodosCancel: 121 + """Tests for 'sol call todos cancel' command.""" 122 + 123 + def test_cancel_entry(self, todo_env): 124 + """Cancel a todo.""" 125 + todo_env([{"text": "Buy milk"}], day="20240101") 126 + result = runner.invoke( 127 + call_app, ["todos", "cancel", "20240101", "1", "--facet", "personal"] 128 + ) 129 + assert result.exit_code == 0 130 + assert "cancelled" in result.output 131 + 132 + def test_cancel_invalid_line_number(self, todo_env): 133 + """Invalid line number fails.""" 134 + todo_env([{"text": "Only one"}], day="20240101") 135 + result = runner.invoke( 136 + call_app, ["todos", "cancel", "20240101", "5", "--facet", "personal"] 137 + ) 138 + assert result.exit_code == 1 139 + 140 + 141 + class TestTodosUpcoming: 142 + """Tests for 'sol call todos upcoming' command.""" 143 + 144 + def test_upcoming_shows_future(self, todo_env): 145 + """Upcoming shows future todos.""" 146 + todo_env([{"text": "Future task"}], day="29991231") 147 + result = runner.invoke(call_app, ["todos", "upcoming"]) 148 + assert result.exit_code == 0 149 + assert "Future task" in result.output 150 + 151 + def test_upcoming_with_facet_filter(self, todo_env): 152 + """Upcoming filters by facet.""" 153 + todo_env([{"text": "Work task"}], day="29991231", facet="work") 154 + result = runner.invoke(call_app, ["todos", "upcoming", "--facet", "work"]) 155 + assert result.exit_code == 0 156 + assert "Work task" in result.output 157 + 158 + def test_upcoming_no_future_todos(self, todo_env): 159 + """No future todos shows appropriate message.""" 160 + todo_env([], day="20200101") 161 + result = runner.invoke(call_app, ["todos", "upcoming"]) 162 + assert result.exit_code == 0 163 + assert "No upcoming todos" in result.output