lvl3dev-todoist-cli 0.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.
- lvl3dev_todoist_cli-0.1.0/PKG-INFO +101 -0
- lvl3dev_todoist_cli-0.1.0/README.md +93 -0
- lvl3dev_todoist_cli-0.1.0/lvl3dev_todoist_cli.egg-info/PKG-INFO +101 -0
- lvl3dev_todoist_cli-0.1.0/lvl3dev_todoist_cli.egg-info/SOURCES.txt +12 -0
- lvl3dev_todoist_cli-0.1.0/lvl3dev_todoist_cli.egg-info/dependency_links.txt +1 -0
- lvl3dev_todoist_cli-0.1.0/lvl3dev_todoist_cli.egg-info/entry_points.txt +2 -0
- lvl3dev_todoist_cli-0.1.0/lvl3dev_todoist_cli.egg-info/requires.txt +1 -0
- lvl3dev_todoist_cli-0.1.0/lvl3dev_todoist_cli.egg-info/top_level.txt +2 -0
- lvl3dev_todoist_cli-0.1.0/pyproject.toml +22 -0
- lvl3dev_todoist_cli-0.1.0/setup.cfg +4 -0
- lvl3dev_todoist_cli-0.1.0/todoist_cli/__init__.py +3 -0
- lvl3dev_todoist_cli-0.1.0/todoist_cli/cli.py +700 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lvl3dev-todoist-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Simple command-line interface for the Todoist API Python SDK.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: todoist-api-python>=3.1.0
|
|
8
|
+
|
|
9
|
+
# lvl3dev-todoist-cli
|
|
10
|
+
|
|
11
|
+
Simple command-line interface for Todoist using the official `todoist-api-python` SDK.
|
|
12
|
+
|
|
13
|
+
## Development setup (uv)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv sync
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Run commands through `uv`:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv run todoist --help
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Build and install (uv + pipx)
|
|
26
|
+
|
|
27
|
+
Build a wheel with `uv`:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv build
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Install globally with `pipx` from the built wheel:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pipx install dist/lvl3dev_todoist_cli-*.whl
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
For local iteration, reinstall after changes with:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pipx install --force dist/lvl3dev_todoist_cli-*.whl
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Authentication
|
|
46
|
+
|
|
47
|
+
Set your API token:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
export TODOIST_API_TOKEN="YOUR_API_TOKEN"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
You can also pass `--token` per command.
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
todoist --help
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Tasks
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
todoist tasks list
|
|
65
|
+
todoist tasks add "Pay rent" --due-string "tomorrow 9am" --priority 1
|
|
66
|
+
todoist tasks get <task_id>
|
|
67
|
+
todoist tasks update <task_id> --content "Pay rent and utilities"
|
|
68
|
+
todoist tasks complete <task_id>
|
|
69
|
+
todoist tasks delete <task_id>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Priority values are user-facing Todoist priorities: `p1` highest, `p4` lowest.
|
|
73
|
+
|
|
74
|
+
### Projects
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
todoist projects list
|
|
78
|
+
todoist projects add "Operations"
|
|
79
|
+
todoist projects update <project_id> "Ops"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Comments
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
todoist comments list --task-id <task_id>
|
|
86
|
+
todoist comments add --task-id <task_id> "Started work"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Labels
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
todoist labels list
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### JSON Output
|
|
96
|
+
|
|
97
|
+
Use `--json` to get structured output:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
todoist --json tasks list
|
|
101
|
+
```
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# lvl3dev-todoist-cli
|
|
2
|
+
|
|
3
|
+
Simple command-line interface for Todoist using the official `todoist-api-python` SDK.
|
|
4
|
+
|
|
5
|
+
## Development setup (uv)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv sync
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Run commands through `uv`:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv run todoist --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Build and install (uv + pipx)
|
|
18
|
+
|
|
19
|
+
Build a wheel with `uv`:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv build
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Install globally with `pipx` from the built wheel:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pipx install dist/lvl3dev_todoist_cli-*.whl
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For local iteration, reinstall after changes with:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pipx install --force dist/lvl3dev_todoist_cli-*.whl
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Authentication
|
|
38
|
+
|
|
39
|
+
Set your API token:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export TODOIST_API_TOKEN="YOUR_API_TOKEN"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
You can also pass `--token` per command.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
todoist --help
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Tasks
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
todoist tasks list
|
|
57
|
+
todoist tasks add "Pay rent" --due-string "tomorrow 9am" --priority 1
|
|
58
|
+
todoist tasks get <task_id>
|
|
59
|
+
todoist tasks update <task_id> --content "Pay rent and utilities"
|
|
60
|
+
todoist tasks complete <task_id>
|
|
61
|
+
todoist tasks delete <task_id>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Priority values are user-facing Todoist priorities: `p1` highest, `p4` lowest.
|
|
65
|
+
|
|
66
|
+
### Projects
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
todoist projects list
|
|
70
|
+
todoist projects add "Operations"
|
|
71
|
+
todoist projects update <project_id> "Ops"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Comments
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
todoist comments list --task-id <task_id>
|
|
78
|
+
todoist comments add --task-id <task_id> "Started work"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Labels
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
todoist labels list
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### JSON Output
|
|
88
|
+
|
|
89
|
+
Use `--json` to get structured output:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
todoist --json tasks list
|
|
93
|
+
```
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lvl3dev-todoist-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Simple command-line interface for the Todoist API Python SDK.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: todoist-api-python>=3.1.0
|
|
8
|
+
|
|
9
|
+
# lvl3dev-todoist-cli
|
|
10
|
+
|
|
11
|
+
Simple command-line interface for Todoist using the official `todoist-api-python` SDK.
|
|
12
|
+
|
|
13
|
+
## Development setup (uv)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv sync
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Run commands through `uv`:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv run todoist --help
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Build and install (uv + pipx)
|
|
26
|
+
|
|
27
|
+
Build a wheel with `uv`:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv build
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Install globally with `pipx` from the built wheel:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pipx install dist/lvl3dev_todoist_cli-*.whl
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
For local iteration, reinstall after changes with:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pipx install --force dist/lvl3dev_todoist_cli-*.whl
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Authentication
|
|
46
|
+
|
|
47
|
+
Set your API token:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
export TODOIST_API_TOKEN="YOUR_API_TOKEN"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
You can also pass `--token` per command.
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
todoist --help
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Tasks
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
todoist tasks list
|
|
65
|
+
todoist tasks add "Pay rent" --due-string "tomorrow 9am" --priority 1
|
|
66
|
+
todoist tasks get <task_id>
|
|
67
|
+
todoist tasks update <task_id> --content "Pay rent and utilities"
|
|
68
|
+
todoist tasks complete <task_id>
|
|
69
|
+
todoist tasks delete <task_id>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Priority values are user-facing Todoist priorities: `p1` highest, `p4` lowest.
|
|
73
|
+
|
|
74
|
+
### Projects
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
todoist projects list
|
|
78
|
+
todoist projects add "Operations"
|
|
79
|
+
todoist projects update <project_id> "Ops"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Comments
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
todoist comments list --task-id <task_id>
|
|
86
|
+
todoist comments add --task-id <task_id> "Started work"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Labels
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
todoist labels list
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### JSON Output
|
|
96
|
+
|
|
97
|
+
Use `--json` to get structured output:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
todoist --json tasks list
|
|
101
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
./todoist_cli/__init__.py
|
|
4
|
+
./todoist_cli/cli.py
|
|
5
|
+
lvl3dev_todoist_cli.egg-info/PKG-INFO
|
|
6
|
+
lvl3dev_todoist_cli.egg-info/SOURCES.txt
|
|
7
|
+
lvl3dev_todoist_cli.egg-info/dependency_links.txt
|
|
8
|
+
lvl3dev_todoist_cli.egg-info/entry_points.txt
|
|
9
|
+
lvl3dev_todoist_cli.egg-info/requires.txt
|
|
10
|
+
lvl3dev_todoist_cli.egg-info/top_level.txt
|
|
11
|
+
todoist_cli/__init__.py
|
|
12
|
+
todoist_cli/cli.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
todoist-api-python>=3.1.0
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lvl3dev-todoist-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Simple command-line interface for the Todoist API Python SDK."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"todoist-api-python>=3.1.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
todoist = "todoist_cli.cli:main"
|
|
17
|
+
|
|
18
|
+
[tool.setuptools]
|
|
19
|
+
package-dir = {"" = "."}
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["."]
|
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from datetime import date, datetime, timedelta
|
|
6
|
+
from typing import Any, Iterable
|
|
7
|
+
|
|
8
|
+
from requests.exceptions import HTTPError
|
|
9
|
+
from todoist_api_python.api import TodoistAPI
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_labels(raw: str | None) -> list[str] | None:
|
|
13
|
+
if not raw:
|
|
14
|
+
return None
|
|
15
|
+
labels = [label.strip() for label in raw.split(",")]
|
|
16
|
+
labels = [label for label in labels if label]
|
|
17
|
+
return labels or None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def user_priority_to_api(priority: int | None) -> int | None:
|
|
21
|
+
if priority is None:
|
|
22
|
+
return None
|
|
23
|
+
return 5 - priority
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def api_priority_to_user(priority: Any) -> Any:
|
|
27
|
+
if isinstance(priority, int) and 1 <= priority <= 4:
|
|
28
|
+
return 5 - priority
|
|
29
|
+
return priority
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def flatten_paged(result: Any) -> list[Any]:
|
|
33
|
+
if result is None:
|
|
34
|
+
return []
|
|
35
|
+
if isinstance(result, list):
|
|
36
|
+
if result and isinstance(result[0], list):
|
|
37
|
+
flat: list[Any] = []
|
|
38
|
+
for page in result:
|
|
39
|
+
flat.extend(page)
|
|
40
|
+
return flat
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
if isinstance(result, Iterable) and not isinstance(result, (str, bytes, dict)):
|
|
44
|
+
flat = []
|
|
45
|
+
for item in result:
|
|
46
|
+
if isinstance(item, list):
|
|
47
|
+
flat.extend(item)
|
|
48
|
+
else:
|
|
49
|
+
flat.append(item)
|
|
50
|
+
return flat
|
|
51
|
+
|
|
52
|
+
return [result]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def to_dict(obj: Any) -> dict[str, Any]:
|
|
56
|
+
if obj is None:
|
|
57
|
+
return {}
|
|
58
|
+
if isinstance(obj, dict):
|
|
59
|
+
return obj
|
|
60
|
+
for method_name in ("to_dict", "model_dump", "dict"):
|
|
61
|
+
method = getattr(obj, method_name, None)
|
|
62
|
+
if callable(method):
|
|
63
|
+
try:
|
|
64
|
+
value = method()
|
|
65
|
+
if isinstance(value, dict):
|
|
66
|
+
return value
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
data = getattr(obj, "__dict__", None)
|
|
70
|
+
if isinstance(data, dict):
|
|
71
|
+
return {k: v for k, v in data.items() if not k.startswith("_")}
|
|
72
|
+
return {"value": str(obj)}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def print_json(data: Any) -> None:
|
|
76
|
+
print(json.dumps(data, indent=2, default=str))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def require_token(args: argparse.Namespace) -> str:
|
|
80
|
+
token = args.token or os.getenv("TODOIST_API_TOKEN")
|
|
81
|
+
if not token:
|
|
82
|
+
raise SystemExit(
|
|
83
|
+
"Missing API token. Pass --token or set TODOIST_API_TOKEN in your environment."
|
|
84
|
+
)
|
|
85
|
+
return token
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def task_due_text(task: Any) -> str:
|
|
89
|
+
due = getattr(task, "due", None)
|
|
90
|
+
if due is None:
|
|
91
|
+
return ""
|
|
92
|
+
return (
|
|
93
|
+
getattr(due, "string", None)
|
|
94
|
+
or getattr(due, "date", None)
|
|
95
|
+
or getattr(due, "datetime", None)
|
|
96
|
+
or str(due)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def task_due_date(task: Any) -> date | None:
|
|
101
|
+
due = getattr(task, "due", None)
|
|
102
|
+
if due is None:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
due_date_raw = getattr(due, "date", None)
|
|
106
|
+
if isinstance(due_date_raw, date):
|
|
107
|
+
return due_date_raw
|
|
108
|
+
if isinstance(due_date_raw, str):
|
|
109
|
+
try:
|
|
110
|
+
return date.fromisoformat(due_date_raw[:10])
|
|
111
|
+
except ValueError:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
due_datetime_raw = getattr(due, "datetime", None)
|
|
115
|
+
if isinstance(due_datetime_raw, datetime):
|
|
116
|
+
return due_datetime_raw.date()
|
|
117
|
+
if isinstance(due_datetime_raw, str):
|
|
118
|
+
normalized = due_datetime_raw.replace("Z", "+00:00")
|
|
119
|
+
try:
|
|
120
|
+
return datetime.fromisoformat(normalized).date()
|
|
121
|
+
except ValueError:
|
|
122
|
+
try:
|
|
123
|
+
return date.fromisoformat(due_datetime_raw[:10])
|
|
124
|
+
except ValueError:
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def render_task_line(task: Any) -> str:
|
|
131
|
+
tid = getattr(task, "id", "")
|
|
132
|
+
content = getattr(task, "content", "")
|
|
133
|
+
priority = api_priority_to_user(getattr(task, "priority", ""))
|
|
134
|
+
due_text = task_due_text(task)
|
|
135
|
+
suffix = f" | p{priority}" if priority else ""
|
|
136
|
+
if due_text:
|
|
137
|
+
suffix += f" | due: {due_text}"
|
|
138
|
+
return f"{tid}\t{content}{suffix}"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def parse_date_arg(raw: str) -> date:
|
|
142
|
+
try:
|
|
143
|
+
return date.fromisoformat(raw)
|
|
144
|
+
except ValueError:
|
|
145
|
+
raise argparse.ArgumentTypeError(f"Invalid date '{raw}'. Use YYYY-MM-DD.")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def format_calendar_text(tasks_by_day: list[tuple[date, list[Any]]]) -> str:
|
|
149
|
+
lines: list[str] = []
|
|
150
|
+
for day, day_tasks in tasks_by_day:
|
|
151
|
+
lines.append(f"{day.isoformat()}\t{day.strftime('%A')} ({len(day_tasks)})")
|
|
152
|
+
lines.extend(f" {render_task_line(task)}" for task in day_tasks)
|
|
153
|
+
return "\n".join(lines)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def calendar_tasks_in_range(api: TodoistAPI, args: argparse.Namespace, start: date, end: date) -> Any:
|
|
157
|
+
kwargs: dict[str, Any] = {}
|
|
158
|
+
if args.project_id:
|
|
159
|
+
kwargs["project_id"] = args.project_id
|
|
160
|
+
if args.label:
|
|
161
|
+
kwargs["label"] = args.label
|
|
162
|
+
if args.limit:
|
|
163
|
+
kwargs["limit"] = args.limit
|
|
164
|
+
|
|
165
|
+
tasks = flatten_paged(api.get_tasks(**kwargs))
|
|
166
|
+
buckets: dict[date, list[Any]] = {}
|
|
167
|
+
for task in tasks:
|
|
168
|
+
due_day = task_due_date(task)
|
|
169
|
+
if due_day is None or due_day < start or due_day > end:
|
|
170
|
+
continue
|
|
171
|
+
buckets.setdefault(due_day, []).append(task)
|
|
172
|
+
|
|
173
|
+
tasks_by_day = sorted(
|
|
174
|
+
((day, day_tasks) for day, day_tasks in buckets.items()),
|
|
175
|
+
key=lambda item: item[0],
|
|
176
|
+
)
|
|
177
|
+
for _, day_tasks in tasks_by_day:
|
|
178
|
+
day_tasks.sort(
|
|
179
|
+
key=lambda task: (
|
|
180
|
+
task_due_text(task),
|
|
181
|
+
str(getattr(task, "id", "")),
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if args.json:
|
|
186
|
+
return {
|
|
187
|
+
"from": start.isoformat(),
|
|
188
|
+
"to": end.isoformat(),
|
|
189
|
+
"days": [
|
|
190
|
+
{
|
|
191
|
+
"date": day.isoformat(),
|
|
192
|
+
"weekday": day.strftime("%A"),
|
|
193
|
+
"tasks": [to_dict(task) for task in day_tasks],
|
|
194
|
+
}
|
|
195
|
+
for day, day_tasks in tasks_by_day
|
|
196
|
+
],
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if not tasks_by_day:
|
|
200
|
+
if start == end:
|
|
201
|
+
return f"No tasks due on {start.isoformat()}."
|
|
202
|
+
return f"No tasks due between {start.isoformat()} and {end.isoformat()}."
|
|
203
|
+
|
|
204
|
+
return format_calendar_text(tasks_by_day)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def task_list(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
208
|
+
kwargs: dict[str, Any] = {}
|
|
209
|
+
if args.project_id:
|
|
210
|
+
kwargs["project_id"] = args.project_id
|
|
211
|
+
if args.label:
|
|
212
|
+
kwargs["label"] = args.label
|
|
213
|
+
if args.limit:
|
|
214
|
+
kwargs["limit"] = args.limit
|
|
215
|
+
|
|
216
|
+
tasks = flatten_paged(api.get_tasks(**kwargs))
|
|
217
|
+
if args.json:
|
|
218
|
+
return [to_dict(task) for task in tasks]
|
|
219
|
+
if not tasks:
|
|
220
|
+
return "No tasks found."
|
|
221
|
+
|
|
222
|
+
return "\n".join(render_task_line(task) for task in tasks)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def task_get(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
226
|
+
task = api.get_task(args.task_id)
|
|
227
|
+
if args.json:
|
|
228
|
+
return to_dict(task)
|
|
229
|
+
tid = getattr(task, "id", "")
|
|
230
|
+
content = getattr(task, "content", "")
|
|
231
|
+
description = getattr(task, "description", "")
|
|
232
|
+
return f"{tid}\t{content}\n{description}".strip()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def task_add(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
236
|
+
kwargs: dict[str, Any] = {
|
|
237
|
+
"content": args.content,
|
|
238
|
+
}
|
|
239
|
+
if args.project_id:
|
|
240
|
+
kwargs["project_id"] = args.project_id
|
|
241
|
+
if args.description:
|
|
242
|
+
kwargs["description"] = args.description
|
|
243
|
+
if args.due_string:
|
|
244
|
+
kwargs["due_string"] = args.due_string
|
|
245
|
+
if args.priority is not None:
|
|
246
|
+
kwargs["priority"] = user_priority_to_api(args.priority)
|
|
247
|
+
labels = parse_labels(args.labels)
|
|
248
|
+
if labels:
|
|
249
|
+
kwargs["labels"] = labels
|
|
250
|
+
task = api.add_task(**kwargs)
|
|
251
|
+
return to_dict(task) if args.json else f"Created task {getattr(task, 'id', '')}"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def task_update(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
255
|
+
kwargs: dict[str, Any] = {}
|
|
256
|
+
if args.content is not None:
|
|
257
|
+
kwargs["content"] = args.content
|
|
258
|
+
if args.description is not None:
|
|
259
|
+
kwargs["description"] = args.description
|
|
260
|
+
if args.due_string is not None:
|
|
261
|
+
kwargs["due_string"] = args.due_string
|
|
262
|
+
if args.priority is not None:
|
|
263
|
+
kwargs["priority"] = user_priority_to_api(args.priority)
|
|
264
|
+
if args.labels is not None:
|
|
265
|
+
kwargs["labels"] = parse_labels(args.labels) or []
|
|
266
|
+
if not kwargs:
|
|
267
|
+
raise SystemExit("No fields provided to update.")
|
|
268
|
+
task = api.update_task(args.task_id, **kwargs)
|
|
269
|
+
return to_dict(task) if args.json else f"Updated task {args.task_id}"
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def task_complete(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
273
|
+
if hasattr(api, "complete_task"):
|
|
274
|
+
api.complete_task(args.task_id)
|
|
275
|
+
else:
|
|
276
|
+
api.close_task(args.task_id)
|
|
277
|
+
return {"status": "ok", "task_id": args.task_id} if args.json else f"Completed task {args.task_id}"
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def task_delete(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
281
|
+
api.delete_task(args.task_id)
|
|
282
|
+
return {"status": "ok", "task_id": args.task_id} if args.json else f"Deleted task {args.task_id}"
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def project_list(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
286
|
+
projects = flatten_paged(api.get_projects())
|
|
287
|
+
if args.json:
|
|
288
|
+
return [to_dict(project) for project in projects]
|
|
289
|
+
if not projects:
|
|
290
|
+
return "No projects found."
|
|
291
|
+
return "\n".join(
|
|
292
|
+
f"{getattr(project, 'id', '')}\t{getattr(project, 'name', '')}" for project in projects
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def project_add(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
297
|
+
kwargs: dict[str, Any] = {"name": args.name}
|
|
298
|
+
if args.view_style is not None:
|
|
299
|
+
kwargs["view_style"] = args.view_style
|
|
300
|
+
project = api.add_project(**kwargs)
|
|
301
|
+
return to_dict(project) if args.json else f"Created project {getattr(project, 'id', '')}"
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def project_update(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
305
|
+
project = api.update_project(args.project_id, name=args.name)
|
|
306
|
+
return to_dict(project) if args.json else f"Updated project {args.project_id}"
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def project_view_style(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
310
|
+
project = api.update_project(args.project_id, view_style=args.style)
|
|
311
|
+
if args.json:
|
|
312
|
+
return to_dict(project)
|
|
313
|
+
return f"Set project {args.project_id} view style to {args.style}"
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def comment_list(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
317
|
+
comments = flatten_paged(api.get_comments(task_id=args.task_id))
|
|
318
|
+
if args.json:
|
|
319
|
+
return [to_dict(comment) for comment in comments]
|
|
320
|
+
if not comments:
|
|
321
|
+
return "No comments found."
|
|
322
|
+
return "\n".join(
|
|
323
|
+
f"{getattr(comment, 'id', '')}\t{getattr(comment, 'content', '')}" for comment in comments
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def comment_add(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
328
|
+
comment = api.add_comment(task_id=args.task_id, content=args.content)
|
|
329
|
+
return to_dict(comment) if args.json else f"Created comment {getattr(comment, 'id', '')}"
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def label_list(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
333
|
+
labels = flatten_paged(api.get_labels())
|
|
334
|
+
if args.json:
|
|
335
|
+
return [to_dict(label) for label in labels]
|
|
336
|
+
if not labels:
|
|
337
|
+
return "No labels found."
|
|
338
|
+
return "\n".join(f"{getattr(label, 'id', '')}\t{getattr(label, 'name', '')}" for label in labels)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def section_list(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
342
|
+
kwargs: dict[str, Any] = {}
|
|
343
|
+
if args.project_id:
|
|
344
|
+
kwargs["project_id"] = args.project_id
|
|
345
|
+
if args.limit:
|
|
346
|
+
kwargs["limit"] = args.limit
|
|
347
|
+
|
|
348
|
+
sections = flatten_paged(api.get_sections(**kwargs))
|
|
349
|
+
if args.json:
|
|
350
|
+
return [to_dict(section) for section in sections]
|
|
351
|
+
if not sections:
|
|
352
|
+
return "No sections found."
|
|
353
|
+
|
|
354
|
+
ordered_sections = sorted(
|
|
355
|
+
sections,
|
|
356
|
+
key=lambda section: (
|
|
357
|
+
str(getattr(section, "project_id", "")),
|
|
358
|
+
getattr(section, "order", 0),
|
|
359
|
+
str(getattr(section, "name", "")),
|
|
360
|
+
),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
lines = []
|
|
364
|
+
for section in ordered_sections:
|
|
365
|
+
section_id = getattr(section, "id", "")
|
|
366
|
+
name = getattr(section, "name", "")
|
|
367
|
+
project_id = getattr(section, "project_id", "")
|
|
368
|
+
order = getattr(section, "order", "")
|
|
369
|
+
suffix = []
|
|
370
|
+
if project_id:
|
|
371
|
+
suffix.append(f"project: {project_id}")
|
|
372
|
+
if order != "":
|
|
373
|
+
suffix.append(f"order: {order}")
|
|
374
|
+
details = f" | {' | '.join(suffix)}" if suffix else ""
|
|
375
|
+
lines.append(f"{section_id}\t{name}{details}")
|
|
376
|
+
return "\n".join(lines)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def section_add(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
380
|
+
kwargs: dict[str, Any] = {"name": args.name, "project_id": args.project_id}
|
|
381
|
+
if args.order is not None:
|
|
382
|
+
kwargs["order"] = args.order
|
|
383
|
+
section = api.add_section(**kwargs)
|
|
384
|
+
return to_dict(section) if args.json else f"Created section {getattr(section, 'id', '')}"
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def section_update(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
388
|
+
section = api.update_section(args.section_id, args.name)
|
|
389
|
+
return to_dict(section) if args.json else f"Updated section {args.section_id}"
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def section_delete(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
393
|
+
api.delete_section(args.section_id)
|
|
394
|
+
return {"status": "ok", "section_id": args.section_id} if args.json else f"Deleted section {args.section_id}"
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def board_show(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
398
|
+
kwargs: dict[str, Any] = {}
|
|
399
|
+
if args.limit:
|
|
400
|
+
kwargs["limit"] = args.limit
|
|
401
|
+
|
|
402
|
+
sections = flatten_paged(api.get_sections(project_id=args.project_id, **kwargs))
|
|
403
|
+
tasks = flatten_paged(api.get_tasks(project_id=args.project_id, **kwargs))
|
|
404
|
+
|
|
405
|
+
ordered_sections = sorted(
|
|
406
|
+
sections,
|
|
407
|
+
key=lambda section: (
|
|
408
|
+
getattr(section, "order", 0),
|
|
409
|
+
str(getattr(section, "name", "")),
|
|
410
|
+
),
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
columns: list[dict[str, Any]] = []
|
|
414
|
+
columns_by_section_id: dict[Any, dict[str, Any]] = {}
|
|
415
|
+
|
|
416
|
+
for section in ordered_sections:
|
|
417
|
+
section_id = getattr(section, "id", None)
|
|
418
|
+
column = {
|
|
419
|
+
"section_id": section_id,
|
|
420
|
+
"name": getattr(section, "name", ""),
|
|
421
|
+
"order": getattr(section, "order", None),
|
|
422
|
+
"tasks": [],
|
|
423
|
+
}
|
|
424
|
+
columns.append(column)
|
|
425
|
+
columns_by_section_id[section_id] = column
|
|
426
|
+
|
|
427
|
+
no_section_column: dict[str, Any] = {
|
|
428
|
+
"section_id": None,
|
|
429
|
+
"name": "No section",
|
|
430
|
+
"order": None,
|
|
431
|
+
"tasks": [],
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
for task in tasks:
|
|
435
|
+
task_section_id = getattr(task, "section_id", None)
|
|
436
|
+
target_column = columns_by_section_id.get(task_section_id, no_section_column)
|
|
437
|
+
if args.json:
|
|
438
|
+
task_data = to_dict(task)
|
|
439
|
+
if "priority" in task_data:
|
|
440
|
+
task_data["priority"] = api_priority_to_user(task_data["priority"])
|
|
441
|
+
target_column["tasks"].append(task_data)
|
|
442
|
+
else:
|
|
443
|
+
target_column["tasks"].append(task)
|
|
444
|
+
|
|
445
|
+
if no_section_column["tasks"]:
|
|
446
|
+
columns.append(no_section_column)
|
|
447
|
+
|
|
448
|
+
if args.json:
|
|
449
|
+
return {"project_id": args.project_id, "columns": columns}
|
|
450
|
+
|
|
451
|
+
if not columns:
|
|
452
|
+
return "No sections or tasks found."
|
|
453
|
+
|
|
454
|
+
lines = []
|
|
455
|
+
for column in columns:
|
|
456
|
+
section_id = column["section_id"]
|
|
457
|
+
section_name = column["name"]
|
|
458
|
+
column_tasks = column["tasks"]
|
|
459
|
+
section_id_text = section_id if section_id is not None else "-"
|
|
460
|
+
lines.append(f"{section_id_text}\t{section_name} ({len(column_tasks)})")
|
|
461
|
+
if column_tasks:
|
|
462
|
+
lines.extend(f" {render_task_line(task)}" for task in column_tasks)
|
|
463
|
+
else:
|
|
464
|
+
lines.append(" (empty)")
|
|
465
|
+
return "\n".join(lines)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def board_move(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
469
|
+
api.move_task(args.task_id, section_id=args.section_id)
|
|
470
|
+
if args.json:
|
|
471
|
+
return {"status": "ok", "task_id": args.task_id, "section_id": args.section_id}
|
|
472
|
+
return f"Moved task {args.task_id} to section {args.section_id}"
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def calendar_today(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
476
|
+
today = date.today()
|
|
477
|
+
return calendar_tasks_in_range(api, args, today, today)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def calendar_week(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
481
|
+
start = date.today()
|
|
482
|
+
end = start + timedelta(days=6)
|
|
483
|
+
return calendar_tasks_in_range(api, args, start, end)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def calendar_range(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
487
|
+
if args.date_from > args.date_to:
|
|
488
|
+
raise SystemExit("--from must be less than or equal to --to.")
|
|
489
|
+
return calendar_tasks_in_range(api, args, args.date_from, args.date_to)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def calendar_reschedule(api: TodoistAPI, args: argparse.Namespace) -> Any:
|
|
493
|
+
kwargs: dict[str, Any] = {}
|
|
494
|
+
if args.due_string is not None:
|
|
495
|
+
kwargs["due_string"] = args.due_string
|
|
496
|
+
if args.due_date is not None:
|
|
497
|
+
kwargs["due_date"] = args.due_date
|
|
498
|
+
task = api.update_task(args.task_id, **kwargs)
|
|
499
|
+
if args.json:
|
|
500
|
+
return to_dict(task)
|
|
501
|
+
if args.due_string is not None:
|
|
502
|
+
return f"Rescheduled task {args.task_id} to '{args.due_string}'"
|
|
503
|
+
return f"Rescheduled task {args.task_id} to {args.due_date.isoformat()}"
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
507
|
+
parser = argparse.ArgumentParser(prog="todoist", description="Todoist CLI powered by todoist-api-python.")
|
|
508
|
+
parser.add_argument("--token", help="Todoist API token. Defaults to TODOIST_API_TOKEN env var.")
|
|
509
|
+
parser.add_argument("--json", action="store_true", help="Print output as JSON.")
|
|
510
|
+
|
|
511
|
+
resources = parser.add_subparsers(dest="resource", required=True)
|
|
512
|
+
|
|
513
|
+
tasks = resources.add_parser("tasks", help="Manage tasks.")
|
|
514
|
+
task_cmds = tasks.add_subparsers(dest="action", required=True)
|
|
515
|
+
|
|
516
|
+
task_list_p = task_cmds.add_parser("list", help="List active tasks.")
|
|
517
|
+
task_list_p.add_argument("--project-id")
|
|
518
|
+
task_list_p.add_argument("--label", help="Filter by label name.")
|
|
519
|
+
task_list_p.add_argument("--limit", type=int, help="Max items per page (1-200).")
|
|
520
|
+
task_list_p.set_defaults(handler=task_list)
|
|
521
|
+
|
|
522
|
+
task_get_p = task_cmds.add_parser("get", help="Get a task.")
|
|
523
|
+
task_get_p.add_argument("task_id")
|
|
524
|
+
task_get_p.set_defaults(handler=task_get)
|
|
525
|
+
|
|
526
|
+
task_add_p = task_cmds.add_parser("add", help="Create a task.")
|
|
527
|
+
task_add_p.add_argument("content")
|
|
528
|
+
task_add_p.add_argument("--project-id")
|
|
529
|
+
task_add_p.add_argument("--description")
|
|
530
|
+
task_add_p.add_argument("--due-string")
|
|
531
|
+
task_add_p.add_argument(
|
|
532
|
+
"--priority",
|
|
533
|
+
type=int,
|
|
534
|
+
choices=[1, 2, 3, 4],
|
|
535
|
+
help="Priority value where p1 is highest and p4 is lowest.",
|
|
536
|
+
)
|
|
537
|
+
task_add_p.add_argument("--labels", help="Comma-separated labels.")
|
|
538
|
+
task_add_p.set_defaults(handler=task_add)
|
|
539
|
+
|
|
540
|
+
task_up_p = task_cmds.add_parser("update", help="Update a task.")
|
|
541
|
+
task_up_p.add_argument("task_id")
|
|
542
|
+
task_up_p.add_argument("--content")
|
|
543
|
+
task_up_p.add_argument("--description")
|
|
544
|
+
task_up_p.add_argument("--due-string")
|
|
545
|
+
task_up_p.add_argument(
|
|
546
|
+
"--priority",
|
|
547
|
+
type=int,
|
|
548
|
+
choices=[1, 2, 3, 4],
|
|
549
|
+
help="Priority value where p1 is highest and p4 is lowest.",
|
|
550
|
+
)
|
|
551
|
+
task_up_p.add_argument("--labels", help="Comma-separated labels. Use empty string to clear.")
|
|
552
|
+
task_up_p.set_defaults(handler=task_update)
|
|
553
|
+
|
|
554
|
+
task_complete_p = task_cmds.add_parser("complete", help="Complete a task.")
|
|
555
|
+
task_complete_p.add_argument("task_id")
|
|
556
|
+
task_complete_p.set_defaults(handler=task_complete)
|
|
557
|
+
|
|
558
|
+
task_delete_p = task_cmds.add_parser("delete", help="Delete a task.")
|
|
559
|
+
task_delete_p.add_argument("task_id")
|
|
560
|
+
task_delete_p.set_defaults(handler=task_delete)
|
|
561
|
+
|
|
562
|
+
projects = resources.add_parser("projects", help="Manage projects.")
|
|
563
|
+
project_cmds = projects.add_subparsers(dest="action", required=True)
|
|
564
|
+
|
|
565
|
+
project_list_p = project_cmds.add_parser("list", help="List projects.")
|
|
566
|
+
project_list_p.set_defaults(handler=project_list)
|
|
567
|
+
|
|
568
|
+
project_add_p = project_cmds.add_parser("add", help="Create a project.")
|
|
569
|
+
project_add_p.add_argument("name")
|
|
570
|
+
project_add_p.add_argument("--view-style", choices=["list", "board"])
|
|
571
|
+
project_add_p.set_defaults(handler=project_add)
|
|
572
|
+
|
|
573
|
+
project_up_p = project_cmds.add_parser("update", help="Update a project name.")
|
|
574
|
+
project_up_p.add_argument("project_id")
|
|
575
|
+
project_up_p.add_argument("name")
|
|
576
|
+
project_up_p.set_defaults(handler=project_update)
|
|
577
|
+
|
|
578
|
+
project_view_style_p = project_cmds.add_parser(
|
|
579
|
+
"view-style",
|
|
580
|
+
help="Set a project's view style (list or board).",
|
|
581
|
+
)
|
|
582
|
+
project_view_style_p.add_argument("project_id")
|
|
583
|
+
project_view_style_p.add_argument("style", choices=["list", "board"])
|
|
584
|
+
project_view_style_p.set_defaults(handler=project_view_style)
|
|
585
|
+
|
|
586
|
+
sections = resources.add_parser("sections", help="Manage sections.")
|
|
587
|
+
section_cmds = sections.add_subparsers(dest="action", required=True)
|
|
588
|
+
|
|
589
|
+
section_list_p = section_cmds.add_parser("list", help="List sections.")
|
|
590
|
+
section_list_p.add_argument("--project-id")
|
|
591
|
+
section_list_p.add_argument("--limit", type=int, help="Max items per page (1-200).")
|
|
592
|
+
section_list_p.set_defaults(handler=section_list)
|
|
593
|
+
|
|
594
|
+
section_add_p = section_cmds.add_parser("add", help="Create a section.")
|
|
595
|
+
section_add_p.add_argument("--project-id", required=True)
|
|
596
|
+
section_add_p.add_argument("name")
|
|
597
|
+
section_add_p.add_argument("--order", type=int, help="Sort order within project.")
|
|
598
|
+
section_add_p.set_defaults(handler=section_add)
|
|
599
|
+
|
|
600
|
+
section_up_p = section_cmds.add_parser("update", help="Rename a section.")
|
|
601
|
+
section_up_p.add_argument("section_id")
|
|
602
|
+
section_up_p.add_argument("name")
|
|
603
|
+
section_up_p.set_defaults(handler=section_update)
|
|
604
|
+
|
|
605
|
+
section_del_p = section_cmds.add_parser("delete", help="Delete a section.")
|
|
606
|
+
section_del_p.add_argument("section_id")
|
|
607
|
+
section_del_p.set_defaults(handler=section_delete)
|
|
608
|
+
|
|
609
|
+
comments = resources.add_parser("comments", help="Manage comments.")
|
|
610
|
+
comment_cmds = comments.add_subparsers(dest="action", required=True)
|
|
611
|
+
|
|
612
|
+
comment_list_p = comment_cmds.add_parser("list", help="List comments for a task.")
|
|
613
|
+
comment_list_p.add_argument("--task-id", required=True)
|
|
614
|
+
comment_list_p.set_defaults(handler=comment_list)
|
|
615
|
+
|
|
616
|
+
comment_add_p = comment_cmds.add_parser("add", help="Add comment to a task.")
|
|
617
|
+
comment_add_p.add_argument("--task-id", required=True)
|
|
618
|
+
comment_add_p.add_argument("content")
|
|
619
|
+
comment_add_p.set_defaults(handler=comment_add)
|
|
620
|
+
|
|
621
|
+
labels = resources.add_parser("labels", help="List labels.")
|
|
622
|
+
label_cmds = labels.add_subparsers(dest="action", required=True)
|
|
623
|
+
label_list_p = label_cmds.add_parser("list", help="List labels.")
|
|
624
|
+
label_list_p.set_defaults(handler=label_list)
|
|
625
|
+
|
|
626
|
+
boards = resources.add_parser("boards", help="Board view helpers.")
|
|
627
|
+
board_cmds = boards.add_subparsers(dest="action", required=True)
|
|
628
|
+
|
|
629
|
+
board_show_p = board_cmds.add_parser("show", help="Show project tasks grouped by section.")
|
|
630
|
+
board_show_p.add_argument("--project-id", required=True)
|
|
631
|
+
board_show_p.add_argument("--limit", type=int, help="Max items per page (1-200).")
|
|
632
|
+
board_show_p.set_defaults(handler=board_show)
|
|
633
|
+
|
|
634
|
+
board_move_p = board_cmds.add_parser("move", help="Move a task to a board section.")
|
|
635
|
+
board_move_p.add_argument("task_id")
|
|
636
|
+
board_move_p.add_argument("--section-id", required=True)
|
|
637
|
+
board_move_p.set_defaults(handler=board_move)
|
|
638
|
+
|
|
639
|
+
calendar = resources.add_parser("calendar", help="Calendar and scheduling helpers.")
|
|
640
|
+
calendar_cmds = calendar.add_subparsers(dest="action", required=True)
|
|
641
|
+
|
|
642
|
+
calendar_today_p = calendar_cmds.add_parser("today", help="Show tasks due today.")
|
|
643
|
+
calendar_today_p.add_argument("--project-id")
|
|
644
|
+
calendar_today_p.add_argument("--label", help="Filter by label name.")
|
|
645
|
+
calendar_today_p.add_argument("--limit", type=int, help="Max items per page (1-200).")
|
|
646
|
+
calendar_today_p.set_defaults(handler=calendar_today)
|
|
647
|
+
|
|
648
|
+
calendar_week_p = calendar_cmds.add_parser("week", help="Show tasks due in the next 7 days.")
|
|
649
|
+
calendar_week_p.add_argument("--project-id")
|
|
650
|
+
calendar_week_p.add_argument("--label", help="Filter by label name.")
|
|
651
|
+
calendar_week_p.add_argument("--limit", type=int, help="Max items per page (1-200).")
|
|
652
|
+
calendar_week_p.set_defaults(handler=calendar_week)
|
|
653
|
+
|
|
654
|
+
calendar_range_p = calendar_cmds.add_parser("range", help="Show tasks due in a date range.")
|
|
655
|
+
calendar_range_p.add_argument("--from", dest="date_from", required=True, type=parse_date_arg)
|
|
656
|
+
calendar_range_p.add_argument("--to", dest="date_to", required=True, type=parse_date_arg)
|
|
657
|
+
calendar_range_p.add_argument("--project-id")
|
|
658
|
+
calendar_range_p.add_argument("--label", help="Filter by label name.")
|
|
659
|
+
calendar_range_p.add_argument("--limit", type=int, help="Max items per page (1-200).")
|
|
660
|
+
calendar_range_p.set_defaults(handler=calendar_range)
|
|
661
|
+
|
|
662
|
+
calendar_reschedule_p = calendar_cmds.add_parser("reschedule", help="Reschedule a task.")
|
|
663
|
+
calendar_reschedule_p.add_argument("task_id")
|
|
664
|
+
reschedule_target = calendar_reschedule_p.add_mutually_exclusive_group(required=True)
|
|
665
|
+
reschedule_target.add_argument("--due-string", help="Natural language due value, e.g. 'tomorrow 9am'.")
|
|
666
|
+
reschedule_target.add_argument("--to", dest="due_date", type=parse_date_arg, help="Due date as YYYY-MM-DD.")
|
|
667
|
+
calendar_reschedule_p.set_defaults(handler=calendar_reschedule)
|
|
668
|
+
|
|
669
|
+
return parser
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def main(argv: list[str] | None = None) -> int:
|
|
673
|
+
parser = build_parser()
|
|
674
|
+
args = parser.parse_args(argv)
|
|
675
|
+
|
|
676
|
+
try:
|
|
677
|
+
token = require_token(args)
|
|
678
|
+
with TodoistAPI(token) as api:
|
|
679
|
+
result = args.handler(api, args)
|
|
680
|
+
if args.json:
|
|
681
|
+
print_json(result)
|
|
682
|
+
elif result is not None:
|
|
683
|
+
print(result)
|
|
684
|
+
return 0
|
|
685
|
+
except HTTPError as err:
|
|
686
|
+
response = getattr(err, "response", None)
|
|
687
|
+
status = getattr(response, "status_code", "unknown")
|
|
688
|
+
text = getattr(response, "text", "") if response is not None else str(err)
|
|
689
|
+
print(f"Todoist API error (status {status}): {text}", file=sys.stderr)
|
|
690
|
+
return 1
|
|
691
|
+
except KeyboardInterrupt:
|
|
692
|
+
print("Interrupted.", file=sys.stderr)
|
|
693
|
+
return 130
|
|
694
|
+
except Exception as err:
|
|
695
|
+
print(f"Error: {err}", file=sys.stderr)
|
|
696
|
+
return 1
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
if __name__ == "__main__":
|
|
700
|
+
raise SystemExit(main())
|