microsoft-todo-cli 1.0.0__tar.gz → 1.2.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.2.0}/CHANGES.txt +9 -0
- {microsoft_todo_cli-1.0.0/microsoft_todo_cli.egg-info → microsoft_todo_cli-1.2.0}/PKG-INFO +19 -1
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/README.md +18 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0/microsoft_todo_cli.egg-info}/PKG-INFO +19 -1
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/microsoft_todo_cli.egg-info/SOURCES.txt +2 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/tests/test_cli_commands.py +16 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/tests/test_models.py +93 -0
- microsoft_todo_cli-1.2.0/tests/test_note_cli.py +104 -0
- microsoft_todo_cli-1.2.0/tests/test_note_wrapper.py +52 -0
- microsoft_todo_cli-1.2.0/todocli/__init__.py +1 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/todocli/cli.py +163 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/todocli/graphapi/wrapper.py +69 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.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.2.0}/LICENSE +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/MANIFEST.in +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/docs/setup-api.md +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/microsoft_todo_cli.egg-info/dependency_links.txt +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/microsoft_todo_cli.egg-info/entry_points.txt +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/microsoft_todo_cli.egg-info/requires.txt +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/microsoft_todo_cli.egg-info/top_level.txt +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/setup.cfg +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/setup.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/tests/__init__.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/tests/run_tests.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/tests/test_checklist_cli.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/tests/test_checklist_item_model.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/tests/test_checklist_wrapper.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/tests/test_cli_output.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/tests/test_cli_url_integration.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/tests/test_datetime_parser.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/tests/test_filters.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/tests/test_json_output.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/tests/test_lst_output.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/tests/test_odata_escape.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/tests/test_recurrence.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/tests/test_update_command.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/tests/test_utils.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/tests/test_wrapper.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/todocli/graphapi/__init__.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/todocli/graphapi/oauth.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/todocli/models/__init__.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/todocli/models/checklistitem.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/todocli/models/todolist.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/todocli/utils/__init__.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/todocli/utils/datetime_util.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/todocli/utils/recurrence_util.py +0 -0
- {microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/todocli/utils/update_checker.py +0 -0
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
v1.2.0, 2026-02-05
|
|
2
|
+
- Added -N/--note flag to 'new' command for creating tasks with notes
|
|
3
|
+
|
|
4
|
+
v1.1.0, 2026-02-05
|
|
5
|
+
- Added note support: note, show-note, clear-note commands
|
|
6
|
+
- Added sn, cn aliases for show-note and clear-note
|
|
7
|
+
- Notes are now displayed in show command and JSON output
|
|
8
|
+
- Added note field to Task model
|
|
9
|
+
|
|
1
10
|
v1.0.0, 2026-02-04
|
|
2
11
|
- Fork from kiblee/tod0 with redesigned CLI
|
|
3
12
|
- 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.2.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
|
|
@@ -116,6 +116,7 @@ todo new "Task" -I # Important
|
|
|
116
116
|
todo new "Task" -R daily # Recurring
|
|
117
117
|
todo new "Task" -R weekly:mon,fri # Recurring on specific days
|
|
118
118
|
todo new "Task" -S "Step 1" -S "Step 2" # With subtasks
|
|
119
|
+
todo new "Task" -N "Note content" # With note
|
|
119
120
|
|
|
120
121
|
# View single task
|
|
121
122
|
todo show "Task" # Show task details
|
|
@@ -141,6 +142,16 @@ todo uncomplete-step "Task" "Step" # Uncheck
|
|
|
141
142
|
todo rm-step "Task" 0 # Remove by index
|
|
142
143
|
```
|
|
143
144
|
|
|
145
|
+
### Notes
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
todo note "Task" "Note content" # Add/update note
|
|
149
|
+
todo show-note "Task" # Display note (alias: sn)
|
|
150
|
+
todo clear-note "Task" # Remove note (alias: cn)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Notes are text content attached to a task. Use `todo show "Task"` to see the note along with other task details.
|
|
154
|
+
|
|
144
155
|
### Lists
|
|
145
156
|
|
|
146
157
|
```bash
|
|
@@ -279,6 +290,7 @@ With `--json`: stdout contains only valid JSON on success. Errors go to stderr.
|
|
|
279
290
|
| `t` | `tasks` | `d` | `rm` |
|
|
280
291
|
| `n` | `new` | `newl` | `new-list` |
|
|
281
292
|
| `c` | `complete` | `reopen` | `uncomplete` |
|
|
293
|
+
| `sn` | `show-note` | `cn` | `clear-note` |
|
|
282
294
|
|
|
283
295
|
```bash
|
|
284
296
|
todo t # = todo tasks
|
|
@@ -287,6 +299,12 @@ todo c 0 1 2 # = todo complete 0 1 2
|
|
|
287
299
|
todo d "Old" -y # = todo rm "Old" -y
|
|
288
300
|
```
|
|
289
301
|
|
|
302
|
+
## Claude Code
|
|
303
|
+
|
|
304
|
+
A skill for [Claude Code](https://claude.ai/download) is available:
|
|
305
|
+
|
|
306
|
+
**[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.
|
|
307
|
+
|
|
290
308
|
## Credits
|
|
291
309
|
|
|
292
310
|
Forked from [kiblee/tod0](https://github.com/kiblee/tod0) with a redesigned CLI.
|
|
@@ -78,6 +78,7 @@ todo new "Task" -I # Important
|
|
|
78
78
|
todo new "Task" -R daily # Recurring
|
|
79
79
|
todo new "Task" -R weekly:mon,fri # Recurring on specific days
|
|
80
80
|
todo new "Task" -S "Step 1" -S "Step 2" # With subtasks
|
|
81
|
+
todo new "Task" -N "Note content" # With note
|
|
81
82
|
|
|
82
83
|
# View single task
|
|
83
84
|
todo show "Task" # Show task details
|
|
@@ -103,6 +104,16 @@ todo uncomplete-step "Task" "Step" # Uncheck
|
|
|
103
104
|
todo rm-step "Task" 0 # Remove by index
|
|
104
105
|
```
|
|
105
106
|
|
|
107
|
+
### Notes
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
todo note "Task" "Note content" # Add/update note
|
|
111
|
+
todo show-note "Task" # Display note (alias: sn)
|
|
112
|
+
todo clear-note "Task" # Remove note (alias: cn)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Notes are text content attached to a task. Use `todo show "Task"` to see the note along with other task details.
|
|
116
|
+
|
|
106
117
|
### Lists
|
|
107
118
|
|
|
108
119
|
```bash
|
|
@@ -241,6 +252,7 @@ With `--json`: stdout contains only valid JSON on success. Errors go to stderr.
|
|
|
241
252
|
| `t` | `tasks` | `d` | `rm` |
|
|
242
253
|
| `n` | `new` | `newl` | `new-list` |
|
|
243
254
|
| `c` | `complete` | `reopen` | `uncomplete` |
|
|
255
|
+
| `sn` | `show-note` | `cn` | `clear-note` |
|
|
244
256
|
|
|
245
257
|
```bash
|
|
246
258
|
todo t # = todo tasks
|
|
@@ -249,6 +261,12 @@ todo c 0 1 2 # = todo complete 0 1 2
|
|
|
249
261
|
todo d "Old" -y # = todo rm "Old" -y
|
|
250
262
|
```
|
|
251
263
|
|
|
264
|
+
## Claude Code
|
|
265
|
+
|
|
266
|
+
A skill for [Claude Code](https://claude.ai/download) is available:
|
|
267
|
+
|
|
268
|
+
**[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.
|
|
269
|
+
|
|
252
270
|
## Credits
|
|
253
271
|
|
|
254
272
|
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.2.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
|
|
@@ -116,6 +116,7 @@ todo new "Task" -I # Important
|
|
|
116
116
|
todo new "Task" -R daily # Recurring
|
|
117
117
|
todo new "Task" -R weekly:mon,fri # Recurring on specific days
|
|
118
118
|
todo new "Task" -S "Step 1" -S "Step 2" # With subtasks
|
|
119
|
+
todo new "Task" -N "Note content" # With note
|
|
119
120
|
|
|
120
121
|
# View single task
|
|
121
122
|
todo show "Task" # Show task details
|
|
@@ -141,6 +142,16 @@ todo uncomplete-step "Task" "Step" # Uncheck
|
|
|
141
142
|
todo rm-step "Task" 0 # Remove by index
|
|
142
143
|
```
|
|
143
144
|
|
|
145
|
+
### Notes
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
todo note "Task" "Note content" # Add/update note
|
|
149
|
+
todo show-note "Task" # Display note (alias: sn)
|
|
150
|
+
todo clear-note "Task" # Remove note (alias: cn)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Notes are text content attached to a task. Use `todo show "Task"` to see the note along with other task details.
|
|
154
|
+
|
|
144
155
|
### Lists
|
|
145
156
|
|
|
146
157
|
```bash
|
|
@@ -279,6 +290,7 @@ With `--json`: stdout contains only valid JSON on success. Errors go to stderr.
|
|
|
279
290
|
| `t` | `tasks` | `d` | `rm` |
|
|
280
291
|
| `n` | `new` | `newl` | `new-list` |
|
|
281
292
|
| `c` | `complete` | `reopen` | `uncomplete` |
|
|
293
|
+
| `sn` | `show-note` | `cn` | `clear-note` |
|
|
282
294
|
|
|
283
295
|
```bash
|
|
284
296
|
todo t # = todo tasks
|
|
@@ -287,6 +299,12 @@ todo c 0 1 2 # = todo complete 0 1 2
|
|
|
287
299
|
todo d "Old" -y # = todo rm "Old" -y
|
|
288
300
|
```
|
|
289
301
|
|
|
302
|
+
## Claude Code
|
|
303
|
+
|
|
304
|
+
A skill for [Claude Code](https://claude.ai/download) is available:
|
|
305
|
+
|
|
306
|
+
**[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.
|
|
307
|
+
|
|
290
308
|
## Credits
|
|
291
309
|
|
|
292
310
|
Forked from [kiblee/tod0](https://github.com/kiblee/tod0) with a redesigned CLI.
|
|
@@ -129,6 +129,22 @@ class TestCLIArgumentParsing(unittest.TestCase):
|
|
|
129
129
|
args = self.parser.parse_args(["new", "buy milk"])
|
|
130
130
|
self.assertEqual(args.step, [])
|
|
131
131
|
|
|
132
|
+
def test_new_command_with_note_flag(self):
|
|
133
|
+
"""Test 'new' command with -N flag"""
|
|
134
|
+
args = self.parser.parse_args(["new", "-N", "Remember to check prices", "buy milk"])
|
|
135
|
+
self.assertEqual(args.task_name, "buy milk")
|
|
136
|
+
self.assertEqual(args.note, "Remember to check prices")
|
|
137
|
+
|
|
138
|
+
def test_new_command_with_note_long_flag(self):
|
|
139
|
+
"""Test 'new' command with --note flag"""
|
|
140
|
+
args = self.parser.parse_args(["new", "--note", "My note", "buy milk"])
|
|
141
|
+
self.assertEqual(args.note, "My note")
|
|
142
|
+
|
|
143
|
+
def test_new_command_without_note(self):
|
|
144
|
+
"""Test 'new' command defaults note to None"""
|
|
145
|
+
args = self.parser.parse_args(["new", "buy milk"])
|
|
146
|
+
self.assertIsNone(args.note)
|
|
147
|
+
|
|
132
148
|
def test_new_command_with_all_flags(self):
|
|
133
149
|
"""Test 'new' command with all flags"""
|
|
134
150
|
args = self.parser.parse_args(
|
|
@@ -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.2.0"
|
|
@@ -152,6 +152,7 @@ def new(args):
|
|
|
152
152
|
due_datetime = parse_datetime(due_date_time_str)
|
|
153
153
|
|
|
154
154
|
recurrence = parse_recurrence(args.recurrence)
|
|
155
|
+
note_content = getattr(args, "note", None)
|
|
155
156
|
|
|
156
157
|
task_id = wrapper.create_task(
|
|
157
158
|
name,
|
|
@@ -160,6 +161,7 @@ def new(args):
|
|
|
160
161
|
due_datetime=due_datetime,
|
|
161
162
|
important=args.important,
|
|
162
163
|
recurrence=recurrence,
|
|
164
|
+
note=note_content,
|
|
163
165
|
)
|
|
164
166
|
|
|
165
167
|
steps = getattr(args, "step", []) or []
|
|
@@ -175,6 +177,8 @@ def new(args):
|
|
|
175
177
|
msg = f"Created task '{name}' in '{task_list}'"
|
|
176
178
|
if steps:
|
|
177
179
|
msg += f" with {len(steps)} step(s)"
|
|
180
|
+
if note_content:
|
|
181
|
+
msg += " with note"
|
|
178
182
|
|
|
179
183
|
result = {
|
|
180
184
|
"action": "created",
|
|
@@ -185,6 +189,8 @@ def new(args):
|
|
|
185
189
|
}
|
|
186
190
|
if step_ids:
|
|
187
191
|
result["step_ids"] = step_ids
|
|
192
|
+
if note_content:
|
|
193
|
+
result["note"] = note_content
|
|
188
194
|
|
|
189
195
|
_output_result(args, result)
|
|
190
196
|
|
|
@@ -837,6 +843,122 @@ def rm_step(args):
|
|
|
837
843
|
print(result["message"])
|
|
838
844
|
|
|
839
845
|
|
|
846
|
+
def note(args):
|
|
847
|
+
"""Add or update a note on a task."""
|
|
848
|
+
task_id = getattr(args, "task_id", None)
|
|
849
|
+
use_json = getattr(args, "json", False)
|
|
850
|
+
note_content = args.note_content
|
|
851
|
+
|
|
852
|
+
if task_id:
|
|
853
|
+
list_name = getattr(args, "list", None) or "Tasks"
|
|
854
|
+
returned_id, title, content = wrapper.update_task_note(
|
|
855
|
+
note_content=note_content,
|
|
856
|
+
list_name=list_name,
|
|
857
|
+
task_id=task_id,
|
|
858
|
+
)
|
|
859
|
+
result = {
|
|
860
|
+
"action": "updated",
|
|
861
|
+
"id": returned_id,
|
|
862
|
+
"title": title,
|
|
863
|
+
"note": content,
|
|
864
|
+
"list": list_name,
|
|
865
|
+
"message": f"Updated note on task '{title}'",
|
|
866
|
+
}
|
|
867
|
+
else:
|
|
868
|
+
task_list, task_name = parse_task_path(
|
|
869
|
+
args.task_name, getattr(args, "list", None)
|
|
870
|
+
)
|
|
871
|
+
returned_id, title, content = wrapper.update_task_note(
|
|
872
|
+
note_content=note_content,
|
|
873
|
+
list_name=task_list,
|
|
874
|
+
task_name=try_parse_as_int(task_name),
|
|
875
|
+
)
|
|
876
|
+
result = {
|
|
877
|
+
"action": "updated",
|
|
878
|
+
"id": returned_id,
|
|
879
|
+
"title": title,
|
|
880
|
+
"note": content,
|
|
881
|
+
"list": task_list,
|
|
882
|
+
"message": f"Updated note on task '{title}' in '{task_list}'",
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if use_json:
|
|
886
|
+
print(json.dumps(result, indent=2))
|
|
887
|
+
else:
|
|
888
|
+
print(result["message"])
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def show_note(args):
|
|
892
|
+
"""Display the note of a task."""
|
|
893
|
+
task_id = getattr(args, "task_id", None)
|
|
894
|
+
use_json = getattr(args, "json", False)
|
|
895
|
+
|
|
896
|
+
if task_id:
|
|
897
|
+
task_list = getattr(args, "list", None) or "Tasks"
|
|
898
|
+
task = wrapper.get_task(list_name=task_list, task_id=task_id)
|
|
899
|
+
else:
|
|
900
|
+
task_list, task_name = parse_task_path(
|
|
901
|
+
args.task_name, getattr(args, "list", None)
|
|
902
|
+
)
|
|
903
|
+
task = wrapper.get_task(
|
|
904
|
+
list_name=task_list, task_name=try_parse_as_int(task_name)
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
if use_json:
|
|
908
|
+
output = {
|
|
909
|
+
"id": task.id,
|
|
910
|
+
"title": task.title,
|
|
911
|
+
"note": task.note if task.note else None,
|
|
912
|
+
"list": task_list,
|
|
913
|
+
}
|
|
914
|
+
print(json.dumps(output, indent=2))
|
|
915
|
+
else:
|
|
916
|
+
if task.note:
|
|
917
|
+
print(task.note)
|
|
918
|
+
else:
|
|
919
|
+
print(f"No note on task '{task.title}'")
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def clear_note(args):
|
|
923
|
+
"""Clear the note from a task."""
|
|
924
|
+
task_id = getattr(args, "task_id", None)
|
|
925
|
+
use_json = getattr(args, "json", False)
|
|
926
|
+
|
|
927
|
+
if task_id:
|
|
928
|
+
list_name = getattr(args, "list", None) or "Tasks"
|
|
929
|
+
returned_id, title = wrapper.clear_task_note(
|
|
930
|
+
list_name=list_name,
|
|
931
|
+
task_id=task_id,
|
|
932
|
+
)
|
|
933
|
+
result = {
|
|
934
|
+
"action": "cleared",
|
|
935
|
+
"id": returned_id,
|
|
936
|
+
"title": title,
|
|
937
|
+
"list": list_name,
|
|
938
|
+
"message": f"Cleared note from task '{title}'",
|
|
939
|
+
}
|
|
940
|
+
else:
|
|
941
|
+
task_list, task_name = parse_task_path(
|
|
942
|
+
args.task_name, getattr(args, "list", None)
|
|
943
|
+
)
|
|
944
|
+
returned_id, title = wrapper.clear_task_note(
|
|
945
|
+
list_name=task_list,
|
|
946
|
+
task_name=try_parse_as_int(task_name),
|
|
947
|
+
)
|
|
948
|
+
result = {
|
|
949
|
+
"action": "cleared",
|
|
950
|
+
"id": returned_id,
|
|
951
|
+
"title": title,
|
|
952
|
+
"list": task_list,
|
|
953
|
+
"message": f"Cleared note from task '{title}' in '{task_list}'",
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if use_json:
|
|
957
|
+
print(json.dumps(result, indent=2))
|
|
958
|
+
else:
|
|
959
|
+
print(result["message"])
|
|
960
|
+
|
|
961
|
+
|
|
840
962
|
def show(args):
|
|
841
963
|
"""Display all details of a task."""
|
|
842
964
|
task_id = getattr(args, "task_id", None)
|
|
@@ -875,6 +997,8 @@ def show(args):
|
|
|
875
997
|
if task.reminder_datetime:
|
|
876
998
|
print(f"Reminder: {task.reminder_datetime.strftime('%Y-%m-%d %H:%M')}")
|
|
877
999
|
print(f"Created: {format_date(task.created_datetime, date_fmt)}")
|
|
1000
|
+
if task.note:
|
|
1001
|
+
print(f"Note: {task.note}")
|
|
878
1002
|
if steps:
|
|
879
1003
|
print("Steps:")
|
|
880
1004
|
for i, step in enumerate(steps):
|
|
@@ -1082,6 +1206,12 @@ def setup_parser():
|
|
|
1082
1206
|
default=[],
|
|
1083
1207
|
help="Add a step (checklist item); can be repeated",
|
|
1084
1208
|
)
|
|
1209
|
+
subparser.add_argument(
|
|
1210
|
+
"-N",
|
|
1211
|
+
"--note",
|
|
1212
|
+
help="Add a note to the task",
|
|
1213
|
+
metavar="TEXT",
|
|
1214
|
+
)
|
|
1085
1215
|
_add_list_flag(subparser)
|
|
1086
1216
|
_add_json_flag(subparser)
|
|
1087
1217
|
subparser.set_defaults(func=new)
|
|
@@ -1272,6 +1402,39 @@ def setup_parser():
|
|
|
1272
1402
|
_add_json_flag(subparser)
|
|
1273
1403
|
subparser.set_defaults(func=rm_step)
|
|
1274
1404
|
|
|
1405
|
+
# 'note' command - add or update a note on a task
|
|
1406
|
+
subparser = subparsers.add_parser("note", help="Add or update a note on a task")
|
|
1407
|
+
subparser.add_argument("task_name", nargs="?", help=helptext_task_name)
|
|
1408
|
+
subparser.add_argument("note_content", help="Note content to add to the task")
|
|
1409
|
+
_add_list_flag(subparser)
|
|
1410
|
+
_add_id_flag(subparser)
|
|
1411
|
+
_add_json_flag(subparser)
|
|
1412
|
+
subparser.set_defaults(func=note)
|
|
1413
|
+
|
|
1414
|
+
# 'show-note' command (primary) and 'sn' alias
|
|
1415
|
+
for cmd_name in ["show-note", "sn"]:
|
|
1416
|
+
subparser = subparsers.add_parser(
|
|
1417
|
+
cmd_name,
|
|
1418
|
+
help="Display the note of a task" if cmd_name == "show-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=show_note)
|
|
1425
|
+
|
|
1426
|
+
# 'clear-note' command (primary) and 'cn' alias
|
|
1427
|
+
for cmd_name in ["clear-note", "cn"]:
|
|
1428
|
+
subparser = subparsers.add_parser(
|
|
1429
|
+
cmd_name,
|
|
1430
|
+
help="Clear the note from a task" if cmd_name == "clear-note" else argparse.SUPPRESS,
|
|
1431
|
+
)
|
|
1432
|
+
subparser.add_argument("task_name", nargs="?", help=helptext_task_name)
|
|
1433
|
+
_add_list_flag(subparser)
|
|
1434
|
+
_add_id_flag(subparser)
|
|
1435
|
+
_add_json_flag(subparser)
|
|
1436
|
+
subparser.set_defaults(func=clear_note)
|
|
1437
|
+
|
|
1275
1438
|
return parser
|
|
1276
1439
|
|
|
1277
1440
|
|
|
@@ -172,6 +172,7 @@ def create_task(
|
|
|
172
172
|
due_datetime: datetime | None = None,
|
|
173
173
|
important: bool = False,
|
|
174
174
|
recurrence: dict | None = None,
|
|
175
|
+
note: str | None = None,
|
|
175
176
|
):
|
|
176
177
|
_require_list(list_name, list_id)
|
|
177
178
|
|
|
@@ -191,6 +192,8 @@ def create_task(
|
|
|
191
192
|
"importance": TaskImportance.HIGH if important else TaskImportance.NORMAL,
|
|
192
193
|
"recurrence": recurrence,
|
|
193
194
|
}
|
|
195
|
+
if note:
|
|
196
|
+
request_body["body"] = {"content": note, "contentType": "text"}
|
|
194
197
|
session = get_oauth_session()
|
|
195
198
|
response = session.post(endpoint, json=request_body)
|
|
196
199
|
if response.ok:
|
|
@@ -658,3 +661,69 @@ def get_step_id(
|
|
|
658
661
|
raise StepNotFoundByName(step_name, task_name)
|
|
659
662
|
else:
|
|
660
663
|
raise TypeError(f"step_name must be str or int, got {type(step_name).__name__}")
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
# --- Note functions ---
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def update_task_note(
|
|
670
|
+
note_content: str,
|
|
671
|
+
list_name: str = None,
|
|
672
|
+
task_name: Union[str, int] = None,
|
|
673
|
+
list_id: str = None,
|
|
674
|
+
task_id: str = None,
|
|
675
|
+
content_type: str = "text",
|
|
676
|
+
):
|
|
677
|
+
"""Update the note (body) of a task. Returns (task_id, task_title, note_content)."""
|
|
678
|
+
_require_list(list_name, list_id)
|
|
679
|
+
_require_task(task_name, task_id)
|
|
680
|
+
|
|
681
|
+
if list_id is None:
|
|
682
|
+
list_id = get_list_id_by_name(list_name)
|
|
683
|
+
if task_id is None:
|
|
684
|
+
task_id = get_task_id_by_name(list_name, task_name)
|
|
685
|
+
|
|
686
|
+
endpoint = f"{BASE_URL}/{list_id}/tasks/{task_id}"
|
|
687
|
+
request_body = {
|
|
688
|
+
"body": {
|
|
689
|
+
"content": note_content,
|
|
690
|
+
"contentType": content_type,
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
session = get_oauth_session()
|
|
694
|
+
response = session.patch(endpoint, json=request_body)
|
|
695
|
+
if response.ok:
|
|
696
|
+
data = json.loads(response.content.decode())
|
|
697
|
+
body = data.get("body", {})
|
|
698
|
+
return task_id, data.get("title", ""), body.get("content", "")
|
|
699
|
+
response.raise_for_status()
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def clear_task_note(
|
|
703
|
+
list_name: str = None,
|
|
704
|
+
task_name: Union[str, int] = None,
|
|
705
|
+
list_id: str = None,
|
|
706
|
+
task_id: str = None,
|
|
707
|
+
):
|
|
708
|
+
"""Clear the note (body) of a task. Returns (task_id, task_title)."""
|
|
709
|
+
_require_list(list_name, list_id)
|
|
710
|
+
_require_task(task_name, task_id)
|
|
711
|
+
|
|
712
|
+
if list_id is None:
|
|
713
|
+
list_id = get_list_id_by_name(list_name)
|
|
714
|
+
if task_id is None:
|
|
715
|
+
task_id = get_task_id_by_name(list_name, task_name)
|
|
716
|
+
|
|
717
|
+
endpoint = f"{BASE_URL}/{list_id}/tasks/{task_id}"
|
|
718
|
+
request_body = {
|
|
719
|
+
"body": {
|
|
720
|
+
"content": "",
|
|
721
|
+
"contentType": "text",
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
session = get_oauth_session()
|
|
725
|
+
response = session.patch(endpoint, json=request_body)
|
|
726
|
+
if response.ok:
|
|
727
|
+
data = json.loads(response.content.decode())
|
|
728
|
+
return task_id, data.get("title", "")
|
|
729
|
+
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.2.0}/microsoft_todo_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.0}/microsoft_todo_cli.egg-info/requires.txt
RENAMED
|
File without changes
|
{microsoft_todo_cli-1.0.0 → microsoft_todo_cli-1.2.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
|