microsoft-todo-cli 1.0.0__tar.gz → 1.1.0__tar.gz
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.
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/CHANGES.txt +6 -0
- {microsoft_todo_cli-1.0.0/microsoft_todo_cli.egg-info → microsoft_todo_cli-1.1.0}/PKG-INFO +18 -1
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/README.md +17 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0/microsoft_todo_cli.egg-info}/PKG-INFO +18 -1
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/microsoft_todo_cli.egg-info/SOURCES.txt +2 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/tests/test_models.py +93 -0
- microsoft_todo_cli-1.1.0/tests/test_note_cli.py +104 -0
- microsoft_todo_cli-1.1.0/tests/test_note_wrapper.py +52 -0
- microsoft_todo_cli-1.1.0/todocli/__init__.py +1 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/todocli/cli.py +151 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/todocli/graphapi/wrapper.py +66 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/todocli/models/todotask.py +9 -0
- microsoft_todo_cli-1.0.0/todocli/__init__.py +0 -1
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/LICENSE +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/MANIFEST.in +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/docs/setup-api.md +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/microsoft_todo_cli.egg-info/dependency_links.txt +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/microsoft_todo_cli.egg-info/entry_points.txt +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/microsoft_todo_cli.egg-info/requires.txt +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/microsoft_todo_cli.egg-info/top_level.txt +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/setup.cfg +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/setup.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/tests/__init__.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/tests/run_tests.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/tests/test_checklist_cli.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/tests/test_checklist_item_model.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/tests/test_checklist_wrapper.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/tests/test_cli_commands.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/tests/test_cli_output.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/tests/test_cli_url_integration.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/tests/test_datetime_parser.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/tests/test_filters.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/tests/test_json_output.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/tests/test_lst_output.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/tests/test_odata_escape.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/tests/test_recurrence.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/tests/test_update_command.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/tests/test_utils.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/tests/test_wrapper.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/todocli/graphapi/__init__.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/todocli/graphapi/oauth.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/todocli/models/__init__.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/todocli/models/checklistitem.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/todocli/models/todolist.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/todocli/utils/__init__.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/todocli/utils/datetime_util.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/todocli/utils/recurrence_util.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/todocli/utils/update_checker.py +0 -0
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
v1.1.0, 2026-02-05
|
|
2
|
+
- Added note support: note, show-note, clear-note commands
|
|
3
|
+
- Added sn, cn aliases for show-note and clear-note
|
|
4
|
+
- Notes are now displayed in show command and JSON output
|
|
5
|
+
- Added note field to Task model
|
|
6
|
+
|
|
1
7
|
v1.0.0, 2026-02-04
|
|
2
8
|
- Fork from kiblee/tod0 with redesigned CLI
|
|
3
9
|
- Removed interactive TUI, CLI-only
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microsoft-todo-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Fast, minimal command-line client for Microsoft To-Do
|
|
5
5
|
Home-page: https://github.com/underwear/microsoft-todo-cli
|
|
6
6
|
Author: underwear
|
|
@@ -141,6 +141,16 @@ todo uncomplete-step "Task" "Step" # Uncheck
|
|
|
141
141
|
todo rm-step "Task" 0 # Remove by index
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
+
### Notes
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
todo note "Task" "Note content" # Add/update note
|
|
148
|
+
todo show-note "Task" # Display note (alias: sn)
|
|
149
|
+
todo clear-note "Task" # Remove note (alias: cn)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Notes are text content attached to a task. Use `todo show "Task"` to see the note along with other task details.
|
|
153
|
+
|
|
144
154
|
### Lists
|
|
145
155
|
|
|
146
156
|
```bash
|
|
@@ -279,6 +289,7 @@ With `--json`: stdout contains only valid JSON on success. Errors go to stderr.
|
|
|
279
289
|
| `t` | `tasks` | `d` | `rm` |
|
|
280
290
|
| `n` | `new` | `newl` | `new-list` |
|
|
281
291
|
| `c` | `complete` | `reopen` | `uncomplete` |
|
|
292
|
+
| `sn` | `show-note` | `cn` | `clear-note` |
|
|
282
293
|
|
|
283
294
|
```bash
|
|
284
295
|
todo t # = todo tasks
|
|
@@ -287,6 +298,12 @@ todo c 0 1 2 # = todo complete 0 1 2
|
|
|
287
298
|
todo d "Old" -y # = todo rm "Old" -y
|
|
288
299
|
```
|
|
289
300
|
|
|
301
|
+
## Claude Code
|
|
302
|
+
|
|
303
|
+
A skill for [Claude Code](https://claude.ai/download) is available:
|
|
304
|
+
|
|
305
|
+
**[todo skill](https://github.com/underwear/claude-code-underwear-skills/blob/main/skills/todo/SKILL.md)** — enables Claude to manage your Microsoft To-Do tasks directly.
|
|
306
|
+
|
|
290
307
|
## Credits
|
|
291
308
|
|
|
292
309
|
Forked from [kiblee/tod0](https://github.com/kiblee/tod0) with a redesigned CLI.
|
|
@@ -103,6 +103,16 @@ todo uncomplete-step "Task" "Step" # Uncheck
|
|
|
103
103
|
todo rm-step "Task" 0 # Remove by index
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
+
### Notes
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
todo note "Task" "Note content" # Add/update note
|
|
110
|
+
todo show-note "Task" # Display note (alias: sn)
|
|
111
|
+
todo clear-note "Task" # Remove note (alias: cn)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Notes are text content attached to a task. Use `todo show "Task"` to see the note along with other task details.
|
|
115
|
+
|
|
106
116
|
### Lists
|
|
107
117
|
|
|
108
118
|
```bash
|
|
@@ -241,6 +251,7 @@ With `--json`: stdout contains only valid JSON on success. Errors go to stderr.
|
|
|
241
251
|
| `t` | `tasks` | `d` | `rm` |
|
|
242
252
|
| `n` | `new` | `newl` | `new-list` |
|
|
243
253
|
| `c` | `complete` | `reopen` | `uncomplete` |
|
|
254
|
+
| `sn` | `show-note` | `cn` | `clear-note` |
|
|
244
255
|
|
|
245
256
|
```bash
|
|
246
257
|
todo t # = todo tasks
|
|
@@ -249,6 +260,12 @@ todo c 0 1 2 # = todo complete 0 1 2
|
|
|
249
260
|
todo d "Old" -y # = todo rm "Old" -y
|
|
250
261
|
```
|
|
251
262
|
|
|
263
|
+
## Claude Code
|
|
264
|
+
|
|
265
|
+
A skill for [Claude Code](https://claude.ai/download) is available:
|
|
266
|
+
|
|
267
|
+
**[todo skill](https://github.com/underwear/claude-code-underwear-skills/blob/main/skills/todo/SKILL.md)** — enables Claude to manage your Microsoft To-Do tasks directly.
|
|
268
|
+
|
|
252
269
|
## Credits
|
|
253
270
|
|
|
254
271
|
Forked from [kiblee/tod0](https://github.com/kiblee/tod0) with a redesigned CLI.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microsoft-todo-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Fast, minimal command-line client for Microsoft To-Do
|
|
5
5
|
Home-page: https://github.com/underwear/microsoft-todo-cli
|
|
6
6
|
Author: underwear
|
|
@@ -141,6 +141,16 @@ todo uncomplete-step "Task" "Step" # Uncheck
|
|
|
141
141
|
todo rm-step "Task" 0 # Remove by index
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
+
### Notes
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
todo note "Task" "Note content" # Add/update note
|
|
148
|
+
todo show-note "Task" # Display note (alias: sn)
|
|
149
|
+
todo clear-note "Task" # Remove note (alias: cn)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Notes are text content attached to a task. Use `todo show "Task"` to see the note along with other task details.
|
|
153
|
+
|
|
144
154
|
### Lists
|
|
145
155
|
|
|
146
156
|
```bash
|
|
@@ -279,6 +289,7 @@ With `--json`: stdout contains only valid JSON on success. Errors go to stderr.
|
|
|
279
289
|
| `t` | `tasks` | `d` | `rm` |
|
|
280
290
|
| `n` | `new` | `newl` | `new-list` |
|
|
281
291
|
| `c` | `complete` | `reopen` | `uncomplete` |
|
|
292
|
+
| `sn` | `show-note` | `cn` | `clear-note` |
|
|
282
293
|
|
|
283
294
|
```bash
|
|
284
295
|
todo t # = todo tasks
|
|
@@ -287,6 +298,12 @@ todo c 0 1 2 # = todo complete 0 1 2
|
|
|
287
298
|
todo d "Old" -y # = todo rm "Old" -y
|
|
288
299
|
```
|
|
289
300
|
|
|
301
|
+
## Claude Code
|
|
302
|
+
|
|
303
|
+
A skill for [Claude Code](https://claude.ai/download) is available:
|
|
304
|
+
|
|
305
|
+
**[todo skill](https://github.com/underwear/claude-code-underwear-skills/blob/main/skills/todo/SKILL.md)** — enables Claude to manage your Microsoft To-Do tasks directly.
|
|
306
|
+
|
|
290
307
|
## Credits
|
|
291
308
|
|
|
292
309
|
Forked from [kiblee/tod0](https://github.com/kiblee/tod0) with a redesigned CLI.
|
|
@@ -230,6 +230,99 @@ class TestTaskModel(unittest.TestCase):
|
|
|
230
230
|
|
|
231
231
|
self.assertIsNotNone(task.body_last_modified_datetime)
|
|
232
232
|
|
|
233
|
+
def test_task_with_note(self):
|
|
234
|
+
"""Test task with note (body content)"""
|
|
235
|
+
api_response = {
|
|
236
|
+
"id": "task123",
|
|
237
|
+
"title": "Task with note",
|
|
238
|
+
"importance": "normal",
|
|
239
|
+
"status": "notStarted",
|
|
240
|
+
"createdDateTime": "2024-01-25T10:00:00.0000000Z",
|
|
241
|
+
"lastModifiedDateTime": "2024-01-25T11:00:00.0000000Z",
|
|
242
|
+
"isReminderOn": False,
|
|
243
|
+
"body": {
|
|
244
|
+
"content": "This is my note content",
|
|
245
|
+
"contentType": "text",
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
task = Task(api_response)
|
|
249
|
+
|
|
250
|
+
self.assertEqual(task.note, "This is my note content")
|
|
251
|
+
self.assertEqual(task.note_content_type, "text")
|
|
252
|
+
|
|
253
|
+
def test_task_without_note(self):
|
|
254
|
+
"""Test task without note (no body field)"""
|
|
255
|
+
api_response = {
|
|
256
|
+
"id": "task123",
|
|
257
|
+
"title": "Task without note",
|
|
258
|
+
"importance": "normal",
|
|
259
|
+
"status": "notStarted",
|
|
260
|
+
"createdDateTime": "2024-01-25T10:00:00.0000000Z",
|
|
261
|
+
"lastModifiedDateTime": "2024-01-25T11:00:00.0000000Z",
|
|
262
|
+
"isReminderOn": False,
|
|
263
|
+
}
|
|
264
|
+
task = Task(api_response)
|
|
265
|
+
|
|
266
|
+
self.assertEqual(task.note, "")
|
|
267
|
+
self.assertEqual(task.note_content_type, "text")
|
|
268
|
+
|
|
269
|
+
def test_task_with_empty_note(self):
|
|
270
|
+
"""Test task with empty note (body with empty content)"""
|
|
271
|
+
api_response = {
|
|
272
|
+
"id": "task123",
|
|
273
|
+
"title": "Task with empty note",
|
|
274
|
+
"importance": "normal",
|
|
275
|
+
"status": "notStarted",
|
|
276
|
+
"createdDateTime": "2024-01-25T10:00:00.0000000Z",
|
|
277
|
+
"lastModifiedDateTime": "2024-01-25T11:00:00.0000000Z",
|
|
278
|
+
"isReminderOn": False,
|
|
279
|
+
"body": {
|
|
280
|
+
"content": "",
|
|
281
|
+
"contentType": "text",
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
task = Task(api_response)
|
|
285
|
+
|
|
286
|
+
self.assertEqual(task.note, "")
|
|
287
|
+
|
|
288
|
+
def test_task_note_in_to_dict(self):
|
|
289
|
+
"""Test that note is included in to_dict output"""
|
|
290
|
+
api_response = {
|
|
291
|
+
"id": "task123",
|
|
292
|
+
"title": "Task with note",
|
|
293
|
+
"importance": "normal",
|
|
294
|
+
"status": "notStarted",
|
|
295
|
+
"createdDateTime": "2024-01-25T10:00:00.0000000Z",
|
|
296
|
+
"lastModifiedDateTime": "2024-01-25T11:00:00.0000000Z",
|
|
297
|
+
"isReminderOn": False,
|
|
298
|
+
"body": {
|
|
299
|
+
"content": "Note for JSON",
|
|
300
|
+
"contentType": "text",
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
task = Task(api_response)
|
|
304
|
+
task_dict = task.to_dict()
|
|
305
|
+
|
|
306
|
+
self.assertIn("note", task_dict)
|
|
307
|
+
self.assertEqual(task_dict["note"], "Note for JSON")
|
|
308
|
+
|
|
309
|
+
def test_task_empty_note_in_to_dict(self):
|
|
310
|
+
"""Test that empty note is None in to_dict output"""
|
|
311
|
+
api_response = {
|
|
312
|
+
"id": "task123",
|
|
313
|
+
"title": "Task without note",
|
|
314
|
+
"importance": "normal",
|
|
315
|
+
"status": "notStarted",
|
|
316
|
+
"createdDateTime": "2024-01-25T10:00:00.0000000Z",
|
|
317
|
+
"lastModifiedDateTime": "2024-01-25T11:00:00.0000000Z",
|
|
318
|
+
"isReminderOn": False,
|
|
319
|
+
}
|
|
320
|
+
task = Task(api_response)
|
|
321
|
+
task_dict = task.to_dict()
|
|
322
|
+
|
|
323
|
+
self.assertIn("note", task_dict)
|
|
324
|
+
self.assertIsNone(task_dict["note"])
|
|
325
|
+
|
|
233
326
|
|
|
234
327
|
if __name__ == "__main__":
|
|
235
328
|
unittest.main()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Unit tests for note CLI commands (argument parsing)"""
|
|
3
|
+
|
|
4
|
+
import unittest
|
|
5
|
+
from todocli.cli import setup_parser
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestNoteCLIArgParsing(unittest.TestCase):
|
|
9
|
+
"""Test that CLI parsers correctly parse note command arguments"""
|
|
10
|
+
|
|
11
|
+
def setUp(self):
|
|
12
|
+
self.parser = setup_parser()
|
|
13
|
+
|
|
14
|
+
# note command
|
|
15
|
+
|
|
16
|
+
def test_note_basic(self):
|
|
17
|
+
"""Test note command with task name and content"""
|
|
18
|
+
args = self.parser.parse_args(["note", "Buy groceries", "Remember to check prices"])
|
|
19
|
+
self.assertEqual(args.task_name, "Buy groceries")
|
|
20
|
+
self.assertEqual(args.note_content, "Remember to check prices")
|
|
21
|
+
|
|
22
|
+
def test_note_with_list_flag(self):
|
|
23
|
+
"""Test note command with --list flag"""
|
|
24
|
+
args = self.parser.parse_args(
|
|
25
|
+
["note", "Buy groceries", "Remember to check prices", "-l", "Shopping"]
|
|
26
|
+
)
|
|
27
|
+
self.assertEqual(args.task_name, "Buy groceries")
|
|
28
|
+
self.assertEqual(args.note_content, "Remember to check prices")
|
|
29
|
+
self.assertEqual(args.list, "Shopping")
|
|
30
|
+
|
|
31
|
+
def test_note_with_id_flag(self):
|
|
32
|
+
"""Test note command with --id flag"""
|
|
33
|
+
args = self.parser.parse_args(
|
|
34
|
+
["note", "--id", "task123", "This is a note"]
|
|
35
|
+
)
|
|
36
|
+
self.assertEqual(args.task_id, "task123")
|
|
37
|
+
self.assertEqual(args.note_content, "This is a note")
|
|
38
|
+
|
|
39
|
+
def test_note_with_json_flag(self):
|
|
40
|
+
"""Test note command with --json flag"""
|
|
41
|
+
args = self.parser.parse_args(
|
|
42
|
+
["note", "Buy groceries", "Note content", "--json"]
|
|
43
|
+
)
|
|
44
|
+
self.assertTrue(args.json)
|
|
45
|
+
|
|
46
|
+
# show-note command
|
|
47
|
+
|
|
48
|
+
def test_show_note_basic(self):
|
|
49
|
+
"""Test show-note command with task name"""
|
|
50
|
+
args = self.parser.parse_args(["show-note", "Buy groceries"])
|
|
51
|
+
self.assertEqual(args.task_name, "Buy groceries")
|
|
52
|
+
|
|
53
|
+
def test_show_note_with_list_flag(self):
|
|
54
|
+
"""Test show-note command with --list flag"""
|
|
55
|
+
args = self.parser.parse_args(["show-note", "Buy groceries", "-l", "Shopping"])
|
|
56
|
+
self.assertEqual(args.task_name, "Buy groceries")
|
|
57
|
+
self.assertEqual(args.list, "Shopping")
|
|
58
|
+
|
|
59
|
+
def test_show_note_with_id_flag(self):
|
|
60
|
+
"""Test show-note command with --id flag"""
|
|
61
|
+
args = self.parser.parse_args(["show-note", "--id", "task123"])
|
|
62
|
+
self.assertEqual(args.task_id, "task123")
|
|
63
|
+
|
|
64
|
+
def test_show_note_with_json_flag(self):
|
|
65
|
+
"""Test show-note command with --json flag"""
|
|
66
|
+
args = self.parser.parse_args(["show-note", "Buy groceries", "--json"])
|
|
67
|
+
self.assertTrue(args.json)
|
|
68
|
+
|
|
69
|
+
def test_show_note_alias_sn(self):
|
|
70
|
+
"""Test sn alias for show-note command"""
|
|
71
|
+
args = self.parser.parse_args(["sn", "Buy groceries"])
|
|
72
|
+
self.assertEqual(args.task_name, "Buy groceries")
|
|
73
|
+
|
|
74
|
+
# clear-note command
|
|
75
|
+
|
|
76
|
+
def test_clear_note_basic(self):
|
|
77
|
+
"""Test clear-note command with task name"""
|
|
78
|
+
args = self.parser.parse_args(["clear-note", "Buy groceries"])
|
|
79
|
+
self.assertEqual(args.task_name, "Buy groceries")
|
|
80
|
+
|
|
81
|
+
def test_clear_note_with_list_flag(self):
|
|
82
|
+
"""Test clear-note command with --list flag"""
|
|
83
|
+
args = self.parser.parse_args(["clear-note", "Buy groceries", "-l", "Shopping"])
|
|
84
|
+
self.assertEqual(args.task_name, "Buy groceries")
|
|
85
|
+
self.assertEqual(args.list, "Shopping")
|
|
86
|
+
|
|
87
|
+
def test_clear_note_with_id_flag(self):
|
|
88
|
+
"""Test clear-note command with --id flag"""
|
|
89
|
+
args = self.parser.parse_args(["clear-note", "--id", "task123"])
|
|
90
|
+
self.assertEqual(args.task_id, "task123")
|
|
91
|
+
|
|
92
|
+
def test_clear_note_with_json_flag(self):
|
|
93
|
+
"""Test clear-note command with --json flag"""
|
|
94
|
+
args = self.parser.parse_args(["clear-note", "Buy groceries", "--json"])
|
|
95
|
+
self.assertTrue(args.json)
|
|
96
|
+
|
|
97
|
+
def test_clear_note_alias_cn(self):
|
|
98
|
+
"""Test cn alias for clear-note command"""
|
|
99
|
+
args = self.parser.parse_args(["cn", "Buy groceries"])
|
|
100
|
+
self.assertEqual(args.task_name, "Buy groceries")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
unittest.main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Unit tests for note wrapper functions"""
|
|
3
|
+
|
|
4
|
+
import unittest
|
|
5
|
+
from todocli.graphapi.wrapper import BASE_URL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestNoteEndpointPattern(unittest.TestCase):
|
|
9
|
+
"""Test that the note endpoint URL is correctly constructed"""
|
|
10
|
+
|
|
11
|
+
def test_task_endpoint_for_note_update(self):
|
|
12
|
+
"""Test the task endpoint follows the expected pattern for note updates"""
|
|
13
|
+
list_id = "list123"
|
|
14
|
+
task_id = "task456"
|
|
15
|
+
endpoint = f"{BASE_URL}/{list_id}/tasks/{task_id}"
|
|
16
|
+
|
|
17
|
+
self.assertIn(list_id, endpoint)
|
|
18
|
+
self.assertIn(task_id, endpoint)
|
|
19
|
+
self.assertTrue(endpoint.startswith("https://graph.microsoft.com"))
|
|
20
|
+
self.assertIn("/tasks/", endpoint)
|
|
21
|
+
|
|
22
|
+
def test_note_request_body_format(self):
|
|
23
|
+
"""Test that note request body has correct structure"""
|
|
24
|
+
note_content = "Test note content"
|
|
25
|
+
content_type = "text"
|
|
26
|
+
|
|
27
|
+
request_body = {
|
|
28
|
+
"body": {
|
|
29
|
+
"content": note_content,
|
|
30
|
+
"contentType": content_type,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
self.assertIn("body", request_body)
|
|
35
|
+
self.assertEqual(request_body["body"]["content"], note_content)
|
|
36
|
+
self.assertEqual(request_body["body"]["contentType"], content_type)
|
|
37
|
+
|
|
38
|
+
def test_clear_note_request_body_format(self):
|
|
39
|
+
"""Test that clear note request body has correct structure (empty content)"""
|
|
40
|
+
request_body = {
|
|
41
|
+
"body": {
|
|
42
|
+
"content": "",
|
|
43
|
+
"contentType": "text",
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
self.assertIn("body", request_body)
|
|
48
|
+
self.assertEqual(request_body["body"]["content"], "")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
unittest.main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.1.0"
|
|
@@ -837,6 +837,122 @@ def rm_step(args):
|
|
|
837
837
|
print(result["message"])
|
|
838
838
|
|
|
839
839
|
|
|
840
|
+
def note(args):
|
|
841
|
+
"""Add or update a note on a task."""
|
|
842
|
+
task_id = getattr(args, "task_id", None)
|
|
843
|
+
use_json = getattr(args, "json", False)
|
|
844
|
+
note_content = args.note_content
|
|
845
|
+
|
|
846
|
+
if task_id:
|
|
847
|
+
list_name = getattr(args, "list", None) or "Tasks"
|
|
848
|
+
returned_id, title, content = wrapper.update_task_note(
|
|
849
|
+
note_content=note_content,
|
|
850
|
+
list_name=list_name,
|
|
851
|
+
task_id=task_id,
|
|
852
|
+
)
|
|
853
|
+
result = {
|
|
854
|
+
"action": "updated",
|
|
855
|
+
"id": returned_id,
|
|
856
|
+
"title": title,
|
|
857
|
+
"note": content,
|
|
858
|
+
"list": list_name,
|
|
859
|
+
"message": f"Updated note on task '{title}'",
|
|
860
|
+
}
|
|
861
|
+
else:
|
|
862
|
+
task_list, task_name = parse_task_path(
|
|
863
|
+
args.task_name, getattr(args, "list", None)
|
|
864
|
+
)
|
|
865
|
+
returned_id, title, content = wrapper.update_task_note(
|
|
866
|
+
note_content=note_content,
|
|
867
|
+
list_name=task_list,
|
|
868
|
+
task_name=try_parse_as_int(task_name),
|
|
869
|
+
)
|
|
870
|
+
result = {
|
|
871
|
+
"action": "updated",
|
|
872
|
+
"id": returned_id,
|
|
873
|
+
"title": title,
|
|
874
|
+
"note": content,
|
|
875
|
+
"list": task_list,
|
|
876
|
+
"message": f"Updated note on task '{title}' in '{task_list}'",
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if use_json:
|
|
880
|
+
print(json.dumps(result, indent=2))
|
|
881
|
+
else:
|
|
882
|
+
print(result["message"])
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def show_note(args):
|
|
886
|
+
"""Display the note of a task."""
|
|
887
|
+
task_id = getattr(args, "task_id", None)
|
|
888
|
+
use_json = getattr(args, "json", False)
|
|
889
|
+
|
|
890
|
+
if task_id:
|
|
891
|
+
task_list = getattr(args, "list", None) or "Tasks"
|
|
892
|
+
task = wrapper.get_task(list_name=task_list, task_id=task_id)
|
|
893
|
+
else:
|
|
894
|
+
task_list, task_name = parse_task_path(
|
|
895
|
+
args.task_name, getattr(args, "list", None)
|
|
896
|
+
)
|
|
897
|
+
task = wrapper.get_task(
|
|
898
|
+
list_name=task_list, task_name=try_parse_as_int(task_name)
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
if use_json:
|
|
902
|
+
output = {
|
|
903
|
+
"id": task.id,
|
|
904
|
+
"title": task.title,
|
|
905
|
+
"note": task.note if task.note else None,
|
|
906
|
+
"list": task_list,
|
|
907
|
+
}
|
|
908
|
+
print(json.dumps(output, indent=2))
|
|
909
|
+
else:
|
|
910
|
+
if task.note:
|
|
911
|
+
print(task.note)
|
|
912
|
+
else:
|
|
913
|
+
print(f"No note on task '{task.title}'")
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def clear_note(args):
|
|
917
|
+
"""Clear the note from a task."""
|
|
918
|
+
task_id = getattr(args, "task_id", None)
|
|
919
|
+
use_json = getattr(args, "json", False)
|
|
920
|
+
|
|
921
|
+
if task_id:
|
|
922
|
+
list_name = getattr(args, "list", None) or "Tasks"
|
|
923
|
+
returned_id, title = wrapper.clear_task_note(
|
|
924
|
+
list_name=list_name,
|
|
925
|
+
task_id=task_id,
|
|
926
|
+
)
|
|
927
|
+
result = {
|
|
928
|
+
"action": "cleared",
|
|
929
|
+
"id": returned_id,
|
|
930
|
+
"title": title,
|
|
931
|
+
"list": list_name,
|
|
932
|
+
"message": f"Cleared note from task '{title}'",
|
|
933
|
+
}
|
|
934
|
+
else:
|
|
935
|
+
task_list, task_name = parse_task_path(
|
|
936
|
+
args.task_name, getattr(args, "list", None)
|
|
937
|
+
)
|
|
938
|
+
returned_id, title = wrapper.clear_task_note(
|
|
939
|
+
list_name=task_list,
|
|
940
|
+
task_name=try_parse_as_int(task_name),
|
|
941
|
+
)
|
|
942
|
+
result = {
|
|
943
|
+
"action": "cleared",
|
|
944
|
+
"id": returned_id,
|
|
945
|
+
"title": title,
|
|
946
|
+
"list": task_list,
|
|
947
|
+
"message": f"Cleared note from task '{title}' in '{task_list}'",
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if use_json:
|
|
951
|
+
print(json.dumps(result, indent=2))
|
|
952
|
+
else:
|
|
953
|
+
print(result["message"])
|
|
954
|
+
|
|
955
|
+
|
|
840
956
|
def show(args):
|
|
841
957
|
"""Display all details of a task."""
|
|
842
958
|
task_id = getattr(args, "task_id", None)
|
|
@@ -875,6 +991,8 @@ def show(args):
|
|
|
875
991
|
if task.reminder_datetime:
|
|
876
992
|
print(f"Reminder: {task.reminder_datetime.strftime('%Y-%m-%d %H:%M')}")
|
|
877
993
|
print(f"Created: {format_date(task.created_datetime, date_fmt)}")
|
|
994
|
+
if task.note:
|
|
995
|
+
print(f"Note: {task.note}")
|
|
878
996
|
if steps:
|
|
879
997
|
print("Steps:")
|
|
880
998
|
for i, step in enumerate(steps):
|
|
@@ -1272,6 +1390,39 @@ def setup_parser():
|
|
|
1272
1390
|
_add_json_flag(subparser)
|
|
1273
1391
|
subparser.set_defaults(func=rm_step)
|
|
1274
1392
|
|
|
1393
|
+
# 'note' command - add or update a note on a task
|
|
1394
|
+
subparser = subparsers.add_parser("note", help="Add or update a note on a task")
|
|
1395
|
+
subparser.add_argument("task_name", nargs="?", help=helptext_task_name)
|
|
1396
|
+
subparser.add_argument("note_content", help="Note content to add to the task")
|
|
1397
|
+
_add_list_flag(subparser)
|
|
1398
|
+
_add_id_flag(subparser)
|
|
1399
|
+
_add_json_flag(subparser)
|
|
1400
|
+
subparser.set_defaults(func=note)
|
|
1401
|
+
|
|
1402
|
+
# 'show-note' command (primary) and 'sn' alias
|
|
1403
|
+
for cmd_name in ["show-note", "sn"]:
|
|
1404
|
+
subparser = subparsers.add_parser(
|
|
1405
|
+
cmd_name,
|
|
1406
|
+
help="Display the note of a task" if cmd_name == "show-note" else argparse.SUPPRESS,
|
|
1407
|
+
)
|
|
1408
|
+
subparser.add_argument("task_name", nargs="?", help=helptext_task_name)
|
|
1409
|
+
_add_list_flag(subparser)
|
|
1410
|
+
_add_id_flag(subparser)
|
|
1411
|
+
_add_json_flag(subparser)
|
|
1412
|
+
subparser.set_defaults(func=show_note)
|
|
1413
|
+
|
|
1414
|
+
# 'clear-note' command (primary) and 'cn' alias
|
|
1415
|
+
for cmd_name in ["clear-note", "cn"]:
|
|
1416
|
+
subparser = subparsers.add_parser(
|
|
1417
|
+
cmd_name,
|
|
1418
|
+
help="Clear the note from a task" if cmd_name == "clear-note" else argparse.SUPPRESS,
|
|
1419
|
+
)
|
|
1420
|
+
subparser.add_argument("task_name", nargs="?", help=helptext_task_name)
|
|
1421
|
+
_add_list_flag(subparser)
|
|
1422
|
+
_add_id_flag(subparser)
|
|
1423
|
+
_add_json_flag(subparser)
|
|
1424
|
+
subparser.set_defaults(func=clear_note)
|
|
1425
|
+
|
|
1275
1426
|
return parser
|
|
1276
1427
|
|
|
1277
1428
|
|
|
@@ -658,3 +658,69 @@ def get_step_id(
|
|
|
658
658
|
raise StepNotFoundByName(step_name, task_name)
|
|
659
659
|
else:
|
|
660
660
|
raise TypeError(f"step_name must be str or int, got {type(step_name).__name__}")
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
# --- Note functions ---
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def update_task_note(
|
|
667
|
+
note_content: str,
|
|
668
|
+
list_name: str = None,
|
|
669
|
+
task_name: Union[str, int] = None,
|
|
670
|
+
list_id: str = None,
|
|
671
|
+
task_id: str = None,
|
|
672
|
+
content_type: str = "text",
|
|
673
|
+
):
|
|
674
|
+
"""Update the note (body) of a task. Returns (task_id, task_title, note_content)."""
|
|
675
|
+
_require_list(list_name, list_id)
|
|
676
|
+
_require_task(task_name, task_id)
|
|
677
|
+
|
|
678
|
+
if list_id is None:
|
|
679
|
+
list_id = get_list_id_by_name(list_name)
|
|
680
|
+
if task_id is None:
|
|
681
|
+
task_id = get_task_id_by_name(list_name, task_name)
|
|
682
|
+
|
|
683
|
+
endpoint = f"{BASE_URL}/{list_id}/tasks/{task_id}"
|
|
684
|
+
request_body = {
|
|
685
|
+
"body": {
|
|
686
|
+
"content": note_content,
|
|
687
|
+
"contentType": content_type,
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
session = get_oauth_session()
|
|
691
|
+
response = session.patch(endpoint, json=request_body)
|
|
692
|
+
if response.ok:
|
|
693
|
+
data = json.loads(response.content.decode())
|
|
694
|
+
body = data.get("body", {})
|
|
695
|
+
return task_id, data.get("title", ""), body.get("content", "")
|
|
696
|
+
response.raise_for_status()
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def clear_task_note(
|
|
700
|
+
list_name: str = None,
|
|
701
|
+
task_name: Union[str, int] = None,
|
|
702
|
+
list_id: str = None,
|
|
703
|
+
task_id: str = None,
|
|
704
|
+
):
|
|
705
|
+
"""Clear the note (body) of a task. Returns (task_id, task_title)."""
|
|
706
|
+
_require_list(list_name, list_id)
|
|
707
|
+
_require_task(task_name, task_id)
|
|
708
|
+
|
|
709
|
+
if list_id is None:
|
|
710
|
+
list_id = get_list_id_by_name(list_name)
|
|
711
|
+
if task_id is None:
|
|
712
|
+
task_id = get_task_id_by_name(list_name, task_name)
|
|
713
|
+
|
|
714
|
+
endpoint = f"{BASE_URL}/{list_id}/tasks/{task_id}"
|
|
715
|
+
request_body = {
|
|
716
|
+
"body": {
|
|
717
|
+
"content": "",
|
|
718
|
+
"contentType": "text",
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
session = get_oauth_session()
|
|
722
|
+
response = session.patch(endpoint, json=request_body)
|
|
723
|
+
if response.ok:
|
|
724
|
+
data = json.loads(response.content.decode())
|
|
725
|
+
return task_id, data.get("title", "")
|
|
726
|
+
response.raise_for_status()
|
|
@@ -77,6 +77,14 @@ class Task:
|
|
|
77
77
|
else:
|
|
78
78
|
self.body_last_modified_datetime = None
|
|
79
79
|
|
|
80
|
+
# Note (body content)
|
|
81
|
+
if "body" in query_result and query_result["body"]:
|
|
82
|
+
self.note = query_result["body"].get("content", "")
|
|
83
|
+
self.note_content_type = query_result["body"].get("contentType", "text")
|
|
84
|
+
else:
|
|
85
|
+
self.note = ""
|
|
86
|
+
self.note_content_type = "text"
|
|
87
|
+
|
|
80
88
|
def to_dict(self):
|
|
81
89
|
"""Convert task to dictionary for JSON serialization."""
|
|
82
90
|
return {
|
|
@@ -85,6 +93,7 @@ class Task:
|
|
|
85
93
|
"status": self.status.value,
|
|
86
94
|
"importance": self.importance.value,
|
|
87
95
|
"is_reminder_on": self.is_reminder_on,
|
|
96
|
+
"note": self.note if self.note else None,
|
|
88
97
|
"created_datetime": (
|
|
89
98
|
self.created_datetime.isoformat() if self.created_datetime else None
|
|
90
99
|
),
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.0.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/microsoft_todo_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/microsoft_todo_cli.egg-info/requires.txt
RENAMED
|
File without changes
|
{microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.1.0}/microsoft_todo_cli.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|