microsoft-todo-cli 1.0.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.
- microsoft_todo_cli-1.0.0.dist-info/METADATA +296 -0
- microsoft_todo_cli-1.0.0.dist-info/RECORD +37 -0
- microsoft_todo_cli-1.0.0.dist-info/WHEEL +5 -0
- microsoft_todo_cli-1.0.0.dist-info/entry_points.txt +2 -0
- microsoft_todo_cli-1.0.0.dist-info/licenses/LICENSE +22 -0
- microsoft_todo_cli-1.0.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/run_tests.py +39 -0
- tests/test_checklist_cli.py +118 -0
- tests/test_checklist_item_model.py +108 -0
- tests/test_checklist_wrapper.py +62 -0
- tests/test_cli_commands.py +436 -0
- tests/test_cli_output.py +169 -0
- tests/test_cli_url_integration.py +136 -0
- tests/test_datetime_parser.py +233 -0
- tests/test_filters.py +120 -0
- tests/test_json_output.py +223 -0
- tests/test_lst_output.py +135 -0
- tests/test_models.py +235 -0
- tests/test_odata_escape.py +175 -0
- tests/test_recurrence.py +159 -0
- tests/test_update_command.py +192 -0
- tests/test_utils.py +186 -0
- tests/test_wrapper.py +191 -0
- todocli/__init__.py +1 -0
- todocli/cli.py +1356 -0
- todocli/graphapi/__init__.py +0 -0
- todocli/graphapi/oauth.py +136 -0
- todocli/graphapi/wrapper.py +660 -0
- todocli/models/__init__.py +0 -0
- todocli/models/checklistitem.py +59 -0
- todocli/models/todolist.py +27 -0
- todocli/models/todotask.py +105 -0
- todocli/utils/__init__.py +0 -0
- todocli/utils/datetime_util.py +321 -0
- todocli/utils/recurrence_util.py +122 -0
- todocli/utils/update_checker.py +55 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: microsoft-todo-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Fast, minimal command-line client for Microsoft To-Do
|
|
5
|
+
Home-page: https://github.com/underwear/microsoft-todo-cli
|
|
6
|
+
Author: underwear
|
|
7
|
+
Author-email:
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: microsoft todo cli task management
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: pyyaml
|
|
25
|
+
Requires-Dist: requests>=2.28.1
|
|
26
|
+
Requires-Dist: requests_oauthlib
|
|
27
|
+
Dynamic: author
|
|
28
|
+
Dynamic: classifier
|
|
29
|
+
Dynamic: description
|
|
30
|
+
Dynamic: description-content-type
|
|
31
|
+
Dynamic: home-page
|
|
32
|
+
Dynamic: keywords
|
|
33
|
+
Dynamic: license
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
Dynamic: requires-dist
|
|
36
|
+
Dynamic: requires-python
|
|
37
|
+
Dynamic: summary
|
|
38
|
+
|
|
39
|
+
# microsoft-todo-cli
|
|
40
|
+
|
|
41
|
+
Fast, minimal command-line client for Microsoft To-Do
|
|
42
|
+
|
|
43
|
+
[](https://www.python.org/downloads/)
|
|
44
|
+
[](https://github.com/psf/black)
|
|
45
|
+
[](https://opensource.org/licenses/MIT)
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
$ todo tasks
|
|
49
|
+
[0] Buy groceries
|
|
50
|
+
[1] Call mom ! (due: tomorrow)
|
|
51
|
+
[2] Review PR #42
|
|
52
|
+
[x] Check tests
|
|
53
|
+
[ ] Add documentation
|
|
54
|
+
|
|
55
|
+
$ todo new "Deploy v2.0" -d friday -r 9am -I
|
|
56
|
+
Created task 'Deploy v2.0' in 'Tasks'
|
|
57
|
+
|
|
58
|
+
$ todo complete 0
|
|
59
|
+
Completed task 'Buy groceries' in 'Tasks'
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
**Requirements:** Python 3.10+
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install microsoft-todo-cli
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Or install from source:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git clone https://github.com/underwear/microsoft-todo-cli.git
|
|
74
|
+
cd microsoft-todo-cli
|
|
75
|
+
pip install -e .
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then configure Microsoft API access: **[Setup Guide](docs/setup-api.md)** (5 min)
|
|
79
|
+
|
|
80
|
+
## Quick Start
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
todo lists # Show all lists
|
|
84
|
+
todo tasks # Show tasks from default list
|
|
85
|
+
todo tasks Work # Show tasks from "Work" list
|
|
86
|
+
todo new "Buy milk" # Create task
|
|
87
|
+
todo complete "Buy milk" # Mark done (or: todo c "Buy milk")
|
|
88
|
+
todo rm "Old task" # Delete
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Default list**: The first list returned by Microsoft To-Do API (usually "Tasks"). Specify a list explicitly with `-l ListName` or as a positional argument.
|
|
92
|
+
|
|
93
|
+
**Short aliases**: `t` (tasks), `n` (new), `c` (complete), `d` (rm) — see [Aliases](#aliases).
|
|
94
|
+
|
|
95
|
+
## Usage
|
|
96
|
+
|
|
97
|
+
### Tasks
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# View
|
|
101
|
+
todo tasks # Default list
|
|
102
|
+
todo tasks Work # Specific list
|
|
103
|
+
todo tasks --due-today # Due today
|
|
104
|
+
todo tasks --overdue # Past due
|
|
105
|
+
todo tasks --important # High priority
|
|
106
|
+
todo tasks --completed # Done tasks
|
|
107
|
+
todo tasks --all # Everything
|
|
108
|
+
|
|
109
|
+
# Create
|
|
110
|
+
todo new "Task name" # Basic
|
|
111
|
+
todo new "Task" -l Work # In specific list
|
|
112
|
+
todo new "Task" -d tomorrow # With due date
|
|
113
|
+
todo new "Task" -r 2h # With reminder (in 2 hours)
|
|
114
|
+
todo new "Task" -d mon -r 9am # Due Monday, remind at 9am
|
|
115
|
+
todo new "Task" -I # Important
|
|
116
|
+
todo new "Task" -R daily # Recurring
|
|
117
|
+
todo new "Task" -R weekly:mon,fri # Recurring on specific days
|
|
118
|
+
todo new "Task" -S "Step 1" -S "Step 2" # With subtasks
|
|
119
|
+
|
|
120
|
+
# View single task
|
|
121
|
+
todo show "Task" # Show task details
|
|
122
|
+
todo show 0 # Show by index
|
|
123
|
+
|
|
124
|
+
# Manage
|
|
125
|
+
todo complete "Task" # Mark complete
|
|
126
|
+
todo complete 0 1 2 # Complete by index (batch)
|
|
127
|
+
todo uncomplete "Task" # Reopen task
|
|
128
|
+
todo update "Task" --title "New" # Rename
|
|
129
|
+
todo update "Task" -d friday -I # Change due date, make important
|
|
130
|
+
todo rm "Task" # Delete (asks confirmation)
|
|
131
|
+
todo rm "Task" -y # Delete (no confirmation)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Subtasks (Steps)
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
todo new-step "Task" "Step text" # Add step
|
|
138
|
+
todo list-steps "Task" # List steps
|
|
139
|
+
todo complete-step "Task" "Step" # Check off
|
|
140
|
+
todo uncomplete-step "Task" "Step" # Uncheck
|
|
141
|
+
todo rm-step "Task" 0 # Remove by index
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Lists
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
todo lists # Show all lists
|
|
148
|
+
todo new-list "Project X" # Create list
|
|
149
|
+
todo rename-list "Old" "New" # Rename list
|
|
150
|
+
todo rm-list "Project X" # Delete list (asks confirmation)
|
|
151
|
+
todo rm-list "Project X" -y # Delete list (no confirmation)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Date & Time Formats
|
|
155
|
+
|
|
156
|
+
| Type | Examples |
|
|
157
|
+
|------|----------|
|
|
158
|
+
| Relative | `1h`, `30m`, `2d`, `1h30m` |
|
|
159
|
+
| Time | `9:30`, `9am`, `17:00`, `5:30pm` |
|
|
160
|
+
| Days | `tomorrow`, `monday`, `fri` |
|
|
161
|
+
| Date | `2026-12-31`, `31.12.2026`, `12/31/2026` |
|
|
162
|
+
| Keywords | `morning` (7:00), `evening` (18:00) |
|
|
163
|
+
|
|
164
|
+
### Recurrence Patterns
|
|
165
|
+
|
|
166
|
+
| Pattern | Description |
|
|
167
|
+
|---------|-------------|
|
|
168
|
+
| `daily` | Every day |
|
|
169
|
+
| `weekly` | Every week |
|
|
170
|
+
| `monthly` | Every month |
|
|
171
|
+
| `yearly` | Every year |
|
|
172
|
+
| `weekdays` | Monday to Friday |
|
|
173
|
+
| `weekly:mon,wed,fri` | Specific days |
|
|
174
|
+
| `every 2 days` | Custom interval |
|
|
175
|
+
| `every 3 weeks` | Custom interval |
|
|
176
|
+
|
|
177
|
+
## Scripting & Automation
|
|
178
|
+
|
|
179
|
+
### JSON Output
|
|
180
|
+
|
|
181
|
+
Add `--json` to any command for machine-readable output:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
todo tasks --json
|
|
185
|
+
todo lists --json
|
|
186
|
+
todo show "Task" --json
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Example: `todo tasks --json`**
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"list": "Tasks",
|
|
193
|
+
"tasks": [
|
|
194
|
+
{
|
|
195
|
+
"id": "AAMkADU3...",
|
|
196
|
+
"title": "Buy groceries",
|
|
197
|
+
"status": "notStarted",
|
|
198
|
+
"importance": "normal",
|
|
199
|
+
"due_date": null,
|
|
200
|
+
"reminder": null,
|
|
201
|
+
"recurrence": null,
|
|
202
|
+
"steps": []
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"id": "AAMkADU4...",
|
|
206
|
+
"title": "Call mom",
|
|
207
|
+
"status": "notStarted",
|
|
208
|
+
"importance": "high",
|
|
209
|
+
"due_date": "2026-02-06",
|
|
210
|
+
"reminder": "2026-02-06T09:00:00",
|
|
211
|
+
"recurrence": null,
|
|
212
|
+
"steps": [
|
|
213
|
+
{"id": "step1", "name": "Check tests", "completed": true},
|
|
214
|
+
{"id": "step2", "name": "Add documentation", "completed": false}
|
|
215
|
+
]
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Write commands return action confirmation:**
|
|
222
|
+
```bash
|
|
223
|
+
todo new "Task" --json # {"action": "created", "id": "AAMk...", "title": "Task", "list": "Tasks"}
|
|
224
|
+
todo complete "Task" --json # {"action": "completed", "id": "AAMk...", "title": "Task", "list": "Tasks"}
|
|
225
|
+
todo rm "Task" -y --json # {"action": "removed", "id": "AAMk...", "title": "Task", "list": "Tasks"}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Task Identification
|
|
229
|
+
|
|
230
|
+
Tasks can be identified by **name**, **index**, or **ID**. Priority for reliable automation:
|
|
231
|
+
|
|
232
|
+
| Method | Stability | Use case |
|
|
233
|
+
|--------|-----------|----------|
|
|
234
|
+
| `--id "AAMk..."` | Stable | Scripts, automation, agents |
|
|
235
|
+
| Index (`0`, `1`) | Unstable | Interactive use only |
|
|
236
|
+
| Name (`"Task"`) | Unstable | Interactive use, unique names |
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# Get task ID from JSON output
|
|
240
|
+
todo tasks --json | jq -r '.tasks[0].id'
|
|
241
|
+
|
|
242
|
+
# Show IDs inline (human-readable + IDs)
|
|
243
|
+
todo tasks --show-id
|
|
244
|
+
|
|
245
|
+
# Use ID in commands (requires -l for list context)
|
|
246
|
+
todo complete --id "AAMkADU3..." -l Tasks
|
|
247
|
+
todo update --id "AAMkADU3..." --title "New title"
|
|
248
|
+
todo rm --id "AAMkADU3..." -l Tasks -y
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Example: Create and complete a task by ID**
|
|
252
|
+
```bash
|
|
253
|
+
ID=$(todo new "Deploy v2.0" -l Work --json | jq -r '.id')
|
|
254
|
+
# ... later ...
|
|
255
|
+
todo complete --id "$ID" -l Work
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Exit Codes
|
|
259
|
+
|
|
260
|
+
| Code | Meaning |
|
|
261
|
+
|------|---------|
|
|
262
|
+
| `0` | Success |
|
|
263
|
+
| `1` | General error (invalid arguments, task not found, API error) |
|
|
264
|
+
|
|
265
|
+
With `--json`: stdout contains only valid JSON on success. Errors go to stderr.
|
|
266
|
+
|
|
267
|
+
### Tips for Scripts and Agents
|
|
268
|
+
|
|
269
|
+
- **Prefer `--id` over names/indexes**: Names can have duplicates (first match wins). Indexes change as tasks are added/completed/reordered.
|
|
270
|
+
- **Always use `-l ListName`** with `--id` to specify list context.
|
|
271
|
+
- **Capture IDs on creation**: Store the ID from `todo new --json` for later operations.
|
|
272
|
+
- **Use `--json` for parsing**: Human-readable output format may change between versions.
|
|
273
|
+
- **Use `-y` flag** with `rm` commands to skip confirmation prompts.
|
|
274
|
+
|
|
275
|
+
## Aliases
|
|
276
|
+
|
|
277
|
+
| Alias | Command | Alias | Command |
|
|
278
|
+
|-------|---------|-------|---------|
|
|
279
|
+
| `t` | `tasks` | `d` | `rm` |
|
|
280
|
+
| `n` | `new` | `newl` | `new-list` |
|
|
281
|
+
| `c` | `complete` | `reopen` | `uncomplete` |
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
todo t # = todo tasks
|
|
285
|
+
todo n "Task" -d fri # = todo new "Task" -d fri
|
|
286
|
+
todo c 0 1 2 # = todo complete 0 1 2
|
|
287
|
+
todo d "Old" -y # = todo rm "Old" -y
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Credits
|
|
291
|
+
|
|
292
|
+
Forked from [kiblee/tod0](https://github.com/kiblee/tod0) with a redesigned CLI.
|
|
293
|
+
|
|
294
|
+
## License
|
|
295
|
+
|
|
296
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
microsoft_todo_cli-1.0.0.dist-info/licenses/LICENSE,sha256=1xYliB2ZJeYUr4HpX68WiziquLEEHtHs4diJdPCi7ec,1064
|
|
2
|
+
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
tests/run_tests.py,sha256=gi0SgUUTBgav9WjSOmGYBH2pVIfxXokk3NXmrPfgxhQ,1651
|
|
4
|
+
tests/test_checklist_cli.py,sha256=-JhpcXokmYdMKQXhn3FeIDg2xKzD7irfnfzSWjeKvpo,4497
|
|
5
|
+
tests/test_checklist_item_model.py,sha256=t3t5eTDw471dU1ruFHGF6vXdYuD_rHJ8ni8inuhQdKM,3752
|
|
6
|
+
tests/test_checklist_wrapper.py,sha256=fs68UBUaVRu7RXnlgdC84qSKpzT8nHz5d5srK__xBck,2080
|
|
7
|
+
tests/test_cli_commands.py,sha256=9qNShLumjdFOQXkEvikRSC5FezRf4EpK19ozSd7EjIw,17595
|
|
8
|
+
tests/test_cli_output.py,sha256=a55uOtFe3GupKczWv_sPvzz9FbEsbNAtFiHiGklSLc0,6234
|
|
9
|
+
tests/test_cli_url_integration.py,sha256=2zHTLa63olTUFisYn_p0Buu6eMlal7p7xyuXiq0rC-A,4546
|
|
10
|
+
tests/test_datetime_parser.py,sha256=eVurIgTf-P0g4wv09i4fJ3lrAqjB0K6i89UNMWi_9aA,9195
|
|
11
|
+
tests/test_filters.py,sha256=-uFVfyhIucQSLSiLhNvaUJNrW5Zuf92Sl0T6WltlJTs,4171
|
|
12
|
+
tests/test_json_output.py,sha256=ZkZW6N3CuLBz0_j1fRk2nMJ_DN5b3QNdnFuUEgN3jQI,7411
|
|
13
|
+
tests/test_lst_output.py,sha256=EksORiPZIfWS8u7lHm99tBNXAHtfavE8XJzgAp-uuwo,4533
|
|
14
|
+
tests/test_models.py,sha256=uMhHJqWBVqiAFb8xL017z0vhyfrG7TH8C4ZUsgVcovI,8095
|
|
15
|
+
tests/test_odata_escape.py,sha256=P6nECMeUn7UEKyZXkRqFjx3_EwoF4Q84b56aJ01lLPg,7976
|
|
16
|
+
tests/test_recurrence.py,sha256=-pVYKrL5RvtsL3YhTJOfB92MTgKwDFz-0_1JImmlCYU,6386
|
|
17
|
+
tests/test_update_command.py,sha256=Hq--wFbfcUtZ7s83Q8tQVZTFokraWxjqqKRFUG5307E,7740
|
|
18
|
+
tests/test_utils.py,sha256=mXgtng8oB8f1NkUb8qITAM8rnJOHiBYa4zm3eYln6Q4,5003
|
|
19
|
+
tests/test_wrapper.py,sha256=wlO3U4_L-LbMWyMPHPd6dMWOliHt_h7ZCoqF7dvSCvc,6546
|
|
20
|
+
todocli/__init__.py,sha256=J-j-u0itpEFT6irdmWmixQqYMadNl1X91TxUmoiLHMI,22
|
|
21
|
+
todocli/cli.py,sha256=k4zLprrIJUjuD4kBrQhNE6Rns8s2lnQiY6d6wvkoRzU,45458
|
|
22
|
+
todocli/graphapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
+
todocli/graphapi/oauth.py,sha256=AA4X3ZhmCLTEH3ZrbsihemcO7UUfFCzRUxko9tZOIo0,3993
|
|
24
|
+
todocli/graphapi/wrapper.py,sha256=kWiaLrQwDBa6mBG_4qkJKuzzCxI0MGOk9UJurWf83vg,21155
|
|
25
|
+
todocli/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
|
+
todocli/models/checklistitem.py,sha256=KO1CZD8YvL7MqByOW4CQIGuDLyUM4DGCObs0ATpHXYo,1968
|
|
27
|
+
todocli/models/todolist.py,sha256=StdyeA0rY-PDIst2-82FvgSgy_IxvzoqpOVUVJO6rmc,914
|
|
28
|
+
todocli/models/todotask.py,sha256=zRK90WUEftaZfAMmA-ODVR5oE2UCQMevHs21PfAmsjQ,3328
|
|
29
|
+
todocli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
|
+
todocli/utils/datetime_util.py,sha256=fcx1ZK-59Sn270avEenvFOiTOvSgTJPVGD8qe6LxxIg,10283
|
|
31
|
+
todocli/utils/recurrence_util.py,sha256=a-Vcbr3YNPioOtHEMwQMViKGqq5qxwkzdX3gAdhI_Zw,3233
|
|
32
|
+
todocli/utils/update_checker.py,sha256=X--k4dkSq3bhNvoLdPwDBjdu2atGJjCze7349zbAnDo,2049
|
|
33
|
+
microsoft_todo_cli-1.0.0.dist-info/METADATA,sha256=kTbLk9-qCmXU_DQR1XZDOiHjQYTOGevU38bibpuOlbU,8807
|
|
34
|
+
microsoft_todo_cli-1.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
35
|
+
microsoft_todo_cli-1.0.0.dist-info/entry_points.txt,sha256=PqRWcETcJ1IRkrRYErjmilmH6y205R8VPX8QmgWWWGg,42
|
|
36
|
+
microsoft_todo_cli-1.0.0.dist-info/top_level.txt,sha256=EIZMq91YloWqg-5RgfD9gxCiO-nGcrkciT9GnLyMDnc,14
|
|
37
|
+
microsoft_todo_cli-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 kiblee
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
tests/__init__.py
ADDED
|
File without changes
|
tests/run_tests.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Test runner for todocli.
|
|
4
|
+
Runs all unit tests that don't require API credentials.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import unittest
|
|
9
|
+
|
|
10
|
+
# Discover and run all test files matching pattern
|
|
11
|
+
if __name__ == "__main__":
|
|
12
|
+
loader = unittest.TestLoader()
|
|
13
|
+
start_dir = "tests"
|
|
14
|
+
pattern = "test_*.py"
|
|
15
|
+
|
|
16
|
+
# Exclude integration tests that require API credentials
|
|
17
|
+
suite = unittest.TestSuite()
|
|
18
|
+
|
|
19
|
+
# Add each unit test explicitly (excludes test_cli_url_integration.py)
|
|
20
|
+
suite.addTests(loader.loadTestsFromName("tests.test_datetime_parser"))
|
|
21
|
+
suite.addTests(loader.loadTestsFromName("tests.test_cli_commands"))
|
|
22
|
+
suite.addTests(loader.loadTestsFromName("tests.test_models"))
|
|
23
|
+
suite.addTests(loader.loadTestsFromName("tests.test_wrapper"))
|
|
24
|
+
suite.addTests(loader.loadTestsFromName("tests.test_checklist_item_model"))
|
|
25
|
+
suite.addTests(loader.loadTestsFromName("tests.test_checklist_wrapper"))
|
|
26
|
+
suite.addTests(loader.loadTestsFromName("tests.test_checklist_cli"))
|
|
27
|
+
suite.addTests(loader.loadTestsFromName("tests.test_odata_escape"))
|
|
28
|
+
suite.addTests(loader.loadTestsFromName("tests.test_update_command"))
|
|
29
|
+
suite.addTests(loader.loadTestsFromName("tests.test_lst_output"))
|
|
30
|
+
suite.addTests(loader.loadTestsFromName("tests.test_cli_output"))
|
|
31
|
+
suite.addTests(loader.loadTestsFromName("tests.test_json_output"))
|
|
32
|
+
suite.addTests(loader.loadTestsFromName("tests.test_filters"))
|
|
33
|
+
suite.addTests(loader.loadTestsFromName("tests.test_recurrence"))
|
|
34
|
+
|
|
35
|
+
runner = unittest.TextTestRunner(verbosity=2)
|
|
36
|
+
result = runner.run(suite)
|
|
37
|
+
|
|
38
|
+
# Exit with non-zero status if tests failed
|
|
39
|
+
sys.exit(not result.wasSuccessful())
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Unit tests for checklist CLI commands (argument parsing)"""
|
|
3
|
+
|
|
4
|
+
import unittest
|
|
5
|
+
from todocli.cli import setup_parser
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestChecklistCLIArgParsing(unittest.TestCase):
|
|
9
|
+
"""Test that CLI parsers correctly parse checklist command arguments"""
|
|
10
|
+
|
|
11
|
+
def setUp(self):
|
|
12
|
+
self.parser = setup_parser()
|
|
13
|
+
|
|
14
|
+
# new-step
|
|
15
|
+
|
|
16
|
+
def test_new_step_basic(self):
|
|
17
|
+
"""Test new-step command with list/task format"""
|
|
18
|
+
args = self.parser.parse_args(
|
|
19
|
+
["new-step", "Shopping/Buy groceries", "Buy eggs"]
|
|
20
|
+
)
|
|
21
|
+
self.assertEqual(args.task_name, "Shopping/Buy groceries")
|
|
22
|
+
self.assertEqual(args.step_name, "Buy eggs")
|
|
23
|
+
|
|
24
|
+
def test_new_step_default_list(self):
|
|
25
|
+
"""Test new-step command without list prefix"""
|
|
26
|
+
args = self.parser.parse_args(["new-step", "Buy groceries", "Buy eggs"])
|
|
27
|
+
self.assertEqual(args.task_name, "Buy groceries")
|
|
28
|
+
self.assertEqual(args.step_name, "Buy eggs")
|
|
29
|
+
|
|
30
|
+
def test_new_step_with_list_flag(self):
|
|
31
|
+
"""Test new-step with --list flag"""
|
|
32
|
+
args = self.parser.parse_args(
|
|
33
|
+
["new-step", "Buy groceries", "Buy eggs", "-l", "Shopping"]
|
|
34
|
+
)
|
|
35
|
+
self.assertEqual(args.task_name, "Buy groceries")
|
|
36
|
+
self.assertEqual(args.step_name, "Buy eggs")
|
|
37
|
+
self.assertEqual(args.list, "Shopping")
|
|
38
|
+
|
|
39
|
+
# list-steps
|
|
40
|
+
|
|
41
|
+
def test_list_steps_basic(self):
|
|
42
|
+
"""Test list-steps command with list/task format"""
|
|
43
|
+
args = self.parser.parse_args(["list-steps", "Shopping/Buy groceries"])
|
|
44
|
+
self.assertEqual(args.task_name, "Shopping/Buy groceries")
|
|
45
|
+
|
|
46
|
+
def test_list_steps_default_list(self):
|
|
47
|
+
"""Test list-steps without list prefix"""
|
|
48
|
+
args = self.parser.parse_args(["list-steps", "Buy groceries"])
|
|
49
|
+
self.assertEqual(args.task_name, "Buy groceries")
|
|
50
|
+
|
|
51
|
+
def test_list_steps_with_list_flag(self):
|
|
52
|
+
"""Test list-steps with --list flag"""
|
|
53
|
+
args = self.parser.parse_args(["list-steps", "Buy groceries", "-l", "Shopping"])
|
|
54
|
+
self.assertEqual(args.task_name, "Buy groceries")
|
|
55
|
+
self.assertEqual(args.list, "Shopping")
|
|
56
|
+
|
|
57
|
+
# complete-step
|
|
58
|
+
|
|
59
|
+
def test_complete_step_basic(self):
|
|
60
|
+
"""Test complete-step command"""
|
|
61
|
+
args = self.parser.parse_args(
|
|
62
|
+
["complete-step", "Shopping/Buy groceries", "Buy eggs"]
|
|
63
|
+
)
|
|
64
|
+
self.assertEqual(args.task_name, "Shopping/Buy groceries")
|
|
65
|
+
self.assertEqual(args.step_name, "Buy eggs")
|
|
66
|
+
|
|
67
|
+
def test_complete_step_by_index(self):
|
|
68
|
+
"""Test complete-step with numeric step index"""
|
|
69
|
+
args = self.parser.parse_args(["complete-step", "Shopping/Buy groceries", "0"])
|
|
70
|
+
self.assertEqual(args.step_name, "0")
|
|
71
|
+
|
|
72
|
+
def test_complete_step_with_list_flag(self):
|
|
73
|
+
"""Test complete-step with --list flag"""
|
|
74
|
+
args = self.parser.parse_args(
|
|
75
|
+
["complete-step", "Buy groceries", "Buy eggs", "-l", "Shopping"]
|
|
76
|
+
)
|
|
77
|
+
self.assertEqual(args.task_name, "Buy groceries")
|
|
78
|
+
self.assertEqual(args.step_name, "Buy eggs")
|
|
79
|
+
self.assertEqual(args.list, "Shopping")
|
|
80
|
+
|
|
81
|
+
# rm-step
|
|
82
|
+
|
|
83
|
+
def test_rm_step_basic(self):
|
|
84
|
+
"""Test rm-step command"""
|
|
85
|
+
args = self.parser.parse_args(["rm-step", "Shopping/Buy groceries", "Buy eggs"])
|
|
86
|
+
self.assertEqual(args.task_name, "Shopping/Buy groceries")
|
|
87
|
+
self.assertEqual(args.step_name, "Buy eggs")
|
|
88
|
+
|
|
89
|
+
def test_rm_step_by_index(self):
|
|
90
|
+
"""Test rm-step with numeric step index"""
|
|
91
|
+
args = self.parser.parse_args(["rm-step", "Shopping/Buy groceries", "2"])
|
|
92
|
+
self.assertEqual(args.step_name, "2")
|
|
93
|
+
|
|
94
|
+
def test_rm_step_with_list_flag(self):
|
|
95
|
+
"""Test rm-step with --list flag"""
|
|
96
|
+
args = self.parser.parse_args(
|
|
97
|
+
["rm-step", "Buy groceries", "Buy eggs", "-l", "Shopping"]
|
|
98
|
+
)
|
|
99
|
+
self.assertEqual(args.task_name, "Buy groceries")
|
|
100
|
+
self.assertEqual(args.step_name, "Buy eggs")
|
|
101
|
+
self.assertEqual(args.list, "Shopping")
|
|
102
|
+
|
|
103
|
+
# lst --no-steps flag
|
|
104
|
+
|
|
105
|
+
def test_lst_no_steps_flag(self):
|
|
106
|
+
"""Test lst command with --no-steps flag"""
|
|
107
|
+
args = self.parser.parse_args(["lst", "Shopping", "--no-steps"])
|
|
108
|
+
self.assertEqual(args.list_name, "Shopping")
|
|
109
|
+
self.assertTrue(args.no_steps)
|
|
110
|
+
|
|
111
|
+
def test_lst_default_shows_steps(self):
|
|
112
|
+
"""Test lst command defaults to showing steps (no_steps=False)"""
|
|
113
|
+
args = self.parser.parse_args(["lst", "Shopping"])
|
|
114
|
+
self.assertFalse(args.no_steps)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
unittest.main()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Unit tests for ChecklistItem model"""
|
|
3
|
+
|
|
4
|
+
import unittest
|
|
5
|
+
from todocli.models.checklistitem import ChecklistItem
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestChecklistItemModel(unittest.TestCase):
|
|
9
|
+
"""Test ChecklistItem model initialization and properties"""
|
|
10
|
+
|
|
11
|
+
def test_basic_item_creation(self):
|
|
12
|
+
"""Test creating a ChecklistItem from API response"""
|
|
13
|
+
api_response = {
|
|
14
|
+
"id": "step123",
|
|
15
|
+
"displayName": "Buy eggs",
|
|
16
|
+
"isChecked": False,
|
|
17
|
+
"createdDateTime": "2024-01-25T10:00:00.0000000Z",
|
|
18
|
+
}
|
|
19
|
+
item = ChecklistItem(api_response)
|
|
20
|
+
|
|
21
|
+
self.assertEqual(item.id, "step123")
|
|
22
|
+
self.assertEqual(item.display_name, "Buy eggs")
|
|
23
|
+
self.assertFalse(item.is_checked)
|
|
24
|
+
self.assertIsNotNone(item.created_datetime)
|
|
25
|
+
self.assertIsNone(item.checked_datetime)
|
|
26
|
+
|
|
27
|
+
def test_checked_item(self):
|
|
28
|
+
"""Test creating a checked ChecklistItem"""
|
|
29
|
+
api_response = {
|
|
30
|
+
"id": "step456",
|
|
31
|
+
"displayName": "Buy milk",
|
|
32
|
+
"isChecked": True,
|
|
33
|
+
"createdDateTime": "2024-01-25T10:00:00.0000000Z",
|
|
34
|
+
"checkedDateTime": "2024-01-25T11:00:00.0000000Z",
|
|
35
|
+
}
|
|
36
|
+
item = ChecklistItem(api_response)
|
|
37
|
+
|
|
38
|
+
self.assertTrue(item.is_checked)
|
|
39
|
+
self.assertIsNotNone(item.checked_datetime)
|
|
40
|
+
|
|
41
|
+
def test_unchecked_item_no_checked_datetime(self):
|
|
42
|
+
"""Test that unchecked item has no checked_datetime"""
|
|
43
|
+
api_response = {
|
|
44
|
+
"id": "step789",
|
|
45
|
+
"displayName": "Buy bread",
|
|
46
|
+
"isChecked": False,
|
|
47
|
+
"createdDateTime": "2024-01-25T10:00:00.0000000Z",
|
|
48
|
+
}
|
|
49
|
+
item = ChecklistItem(api_response)
|
|
50
|
+
|
|
51
|
+
self.assertFalse(item.is_checked)
|
|
52
|
+
self.assertIsNone(item.checked_datetime)
|
|
53
|
+
|
|
54
|
+
def test_checked_datetime_null_value(self):
|
|
55
|
+
"""Test that null checkedDateTime is handled as None"""
|
|
56
|
+
api_response = {
|
|
57
|
+
"id": "step101",
|
|
58
|
+
"displayName": "Clean kitchen",
|
|
59
|
+
"isChecked": False,
|
|
60
|
+
"createdDateTime": "2024-01-25T10:00:00.0000000Z",
|
|
61
|
+
"checkedDateTime": None,
|
|
62
|
+
}
|
|
63
|
+
item = ChecklistItem(api_response)
|
|
64
|
+
|
|
65
|
+
self.assertFalse(item.is_checked)
|
|
66
|
+
self.assertIsNone(item.checked_datetime)
|
|
67
|
+
|
|
68
|
+
def test_display_name_preserved(self):
|
|
69
|
+
"""Test that display name with special characters is preserved"""
|
|
70
|
+
api_response = {
|
|
71
|
+
"id": "step201",
|
|
72
|
+
"displayName": "Check A/B testing results (urgent!)",
|
|
73
|
+
"isChecked": False,
|
|
74
|
+
"createdDateTime": "2024-01-25T10:00:00.0000000Z",
|
|
75
|
+
}
|
|
76
|
+
item = ChecklistItem(api_response)
|
|
77
|
+
|
|
78
|
+
self.assertEqual(item.display_name, "Check A/B testing results (urgent!)")
|
|
79
|
+
|
|
80
|
+
def test_datetime_without_fractional_seconds(self):
|
|
81
|
+
"""Test parsing datetime without fractional seconds (checklistItems API format)"""
|
|
82
|
+
api_response = {
|
|
83
|
+
"id": "step301",
|
|
84
|
+
"displayName": "Step",
|
|
85
|
+
"isChecked": True,
|
|
86
|
+
"createdDateTime": "2026-02-04T19:08:45Z",
|
|
87
|
+
"checkedDateTime": "2026-02-04T19:10:00Z",
|
|
88
|
+
}
|
|
89
|
+
item = ChecklistItem(api_response)
|
|
90
|
+
|
|
91
|
+
self.assertIsNotNone(item.created_datetime)
|
|
92
|
+
self.assertIsNotNone(item.checked_datetime)
|
|
93
|
+
|
|
94
|
+
def test_datetime_with_fractional_seconds(self):
|
|
95
|
+
"""Test parsing datetime with fractional seconds (standard Graph API format)"""
|
|
96
|
+
api_response = {
|
|
97
|
+
"id": "step401",
|
|
98
|
+
"displayName": "Step",
|
|
99
|
+
"isChecked": False,
|
|
100
|
+
"createdDateTime": "2024-01-25T10:00:00.0000000Z",
|
|
101
|
+
}
|
|
102
|
+
item = ChecklistItem(api_response)
|
|
103
|
+
|
|
104
|
+
self.assertIsNotNone(item.created_datetime)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
unittest.main()
|