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.
@@ -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
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
44
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
45
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ todo = todocli.cli:main
@@ -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
+
@@ -0,0 +1,2 @@
1
+ tests
2
+ todocli
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()