trelloctl 0.2.2__tar.gz → 0.3.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.
- {trelloctl-0.2.2 → trelloctl-0.3.0}/PKG-INFO +12 -2
- {trelloctl-0.2.2 → trelloctl-0.3.0}/README.md +11 -1
- {trelloctl-0.2.2 → trelloctl-0.3.0}/pyproject.toml +3 -3
- {trelloctl-0.2.2 → trelloctl-0.3.0}/src/trelloctl/client.py +63 -6
- {trelloctl-0.2.2 → trelloctl-0.3.0}/src/trelloctl/commands/auth/__init__.py +1 -0
- trelloctl-0.3.0/src/trelloctl/commands/checklist/__init__.py +329 -0
- {trelloctl-0.2.2 → trelloctl-0.3.0}/src/trelloctl/resolver.py +71 -9
- trelloctl-0.2.2/src/trelloctl/commands/checklist/__init__.py +0 -154
- {trelloctl-0.2.2 → trelloctl-0.3.0}/src/trelloctl/__init__.py +0 -0
- {trelloctl-0.2.2 → trelloctl-0.3.0}/src/trelloctl/cli.py +0 -0
- {trelloctl-0.2.2 → trelloctl-0.3.0}/src/trelloctl/commands/__init__.py +0 -0
- {trelloctl-0.2.2 → trelloctl-0.3.0}/src/trelloctl/commands/board/__init__.py +0 -0
- {trelloctl-0.2.2 → trelloctl-0.3.0}/src/trelloctl/commands/card/__init__.py +0 -0
- {trelloctl-0.2.2 → trelloctl-0.3.0}/src/trelloctl/commands/list/__init__.py +0 -0
- {trelloctl-0.2.2 → trelloctl-0.3.0}/src/trelloctl/config.py +0 -0
- {trelloctl-0.2.2 → trelloctl-0.3.0}/src/trelloctl/output.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: trelloctl
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A command-line interface for Trello
|
|
5
5
|
Author: Werner Robitza
|
|
6
6
|
Author-email: Werner Robitza <werner.robitza@gmail.com>
|
|
@@ -170,10 +170,20 @@ Checklist management commands.
|
|
|
170
170
|
| `checklist list <card_id>` | List all checklists and items on a card |
|
|
171
171
|
| `checklist create <card_id> --name <name>` | Create a checklist on a card |
|
|
172
172
|
| `checklist delete <checklist_id>` | Delete a checklist |
|
|
173
|
-
| `checklist add-item <checklist_id> --name <name> [--checked]` | Add an item to a checklist |
|
|
173
|
+
| `checklist add-item <checklist_id> --name <name> [--checked] [--member <member>] [--due <date>] [--due-reminder <minutes>]` | Add an item to a checklist |
|
|
174
174
|
| `checklist check <card_id> <item_id> [--uncheck]` | Mark a checklist item as complete or incomplete |
|
|
175
|
+
| `checklist assign <card_id> <item_id> <member>` | Assign a member to a checklist item |
|
|
176
|
+
| `checklist unassign <card_id> <item_id>` | Remove the assigned member from a checklist item |
|
|
177
|
+
| `checklist set-due <card_id> <item_id> <due> [--reminder <minutes>]` | Set or clear a checklist item's due date (`null` to clear) |
|
|
175
178
|
| `checklist delete-item <checklist_id> <item_id>` | Delete a checklist item |
|
|
176
179
|
|
|
180
|
+
The `<member>` argument accepts a member ID, username, or full name (partial,
|
|
181
|
+
case-insensitive matching), the same way board, list, and card names are resolved.
|
|
182
|
+
|
|
183
|
+
Per-item members and due dates (`add-item --member`/`--due`, `assign`, `unassign`,
|
|
184
|
+
`set-due`) use Trello's advanced checklists, which require a paid Trello plan. On free
|
|
185
|
+
plans the API accepts the request without error but does not store the value.
|
|
186
|
+
|
|
177
187
|
### Multiple Profiles
|
|
178
188
|
|
|
179
189
|
You can use multiple Trello accounts by specifying a profile:
|
|
@@ -157,10 +157,20 @@ Checklist management commands.
|
|
|
157
157
|
| `checklist list <card_id>` | List all checklists and items on a card |
|
|
158
158
|
| `checklist create <card_id> --name <name>` | Create a checklist on a card |
|
|
159
159
|
| `checklist delete <checklist_id>` | Delete a checklist |
|
|
160
|
-
| `checklist add-item <checklist_id> --name <name> [--checked]` | Add an item to a checklist |
|
|
160
|
+
| `checklist add-item <checklist_id> --name <name> [--checked] [--member <member>] [--due <date>] [--due-reminder <minutes>]` | Add an item to a checklist |
|
|
161
161
|
| `checklist check <card_id> <item_id> [--uncheck]` | Mark a checklist item as complete or incomplete |
|
|
162
|
+
| `checklist assign <card_id> <item_id> <member>` | Assign a member to a checklist item |
|
|
163
|
+
| `checklist unassign <card_id> <item_id>` | Remove the assigned member from a checklist item |
|
|
164
|
+
| `checklist set-due <card_id> <item_id> <due> [--reminder <minutes>]` | Set or clear a checklist item's due date (`null` to clear) |
|
|
162
165
|
| `checklist delete-item <checklist_id> <item_id>` | Delete a checklist item |
|
|
163
166
|
|
|
167
|
+
The `<member>` argument accepts a member ID, username, or full name (partial,
|
|
168
|
+
case-insensitive matching), the same way board, list, and card names are resolved.
|
|
169
|
+
|
|
170
|
+
Per-item members and due dates (`add-item --member`/`--due`, `assign`, `unassign`,
|
|
171
|
+
`set-due`) use Trello's advanced checklists, which require a paid Trello plan. On free
|
|
172
|
+
plans the API accepts the request without error but does not store the value.
|
|
173
|
+
|
|
164
174
|
### Multiple Profiles
|
|
165
175
|
|
|
166
176
|
You can use multiple Trello accounts by specifying a profile:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "trelloctl"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "A command-line interface for Trello"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -18,14 +18,14 @@ dependencies = [
|
|
|
18
18
|
trelloctl = "trelloctl.cli:main"
|
|
19
19
|
|
|
20
20
|
[build-system]
|
|
21
|
-
requires = ["uv_build>=0.9.26,<0.
|
|
21
|
+
requires = ["uv_build>=0.9.26,<1.0.0"]
|
|
22
22
|
build-backend = "uv_build"
|
|
23
23
|
|
|
24
24
|
[dependency-groups]
|
|
25
25
|
dev = [
|
|
26
|
-
"mypy>=1.19.1",
|
|
27
26
|
"pre-commit>=4.2.0",
|
|
28
27
|
"pytest>=9.0.2",
|
|
29
28
|
"pytest-mock>=3.14.0",
|
|
30
29
|
"ruff>=0.15.0",
|
|
30
|
+
"ty>=0.0.24",
|
|
31
31
|
]
|
|
@@ -178,6 +178,10 @@ class TrelloClient:
|
|
|
178
178
|
"""Get all checklists on a card."""
|
|
179
179
|
return self.get(f"/cards/{card_id}/checklists")
|
|
180
180
|
|
|
181
|
+
def get_checklist(self, checklist_id: str) -> dict:
|
|
182
|
+
"""Get a checklist by ID."""
|
|
183
|
+
return self.get(f"/checklists/{checklist_id}")
|
|
184
|
+
|
|
181
185
|
def create_checklist(self, card_id: str, name: str) -> dict:
|
|
182
186
|
"""Create a checklist on a card."""
|
|
183
187
|
return self.post(f"/cards/{card_id}/checklists", params={"name": name})
|
|
@@ -187,13 +191,30 @@ class TrelloClient:
|
|
|
187
191
|
self.delete(f"/checklists/{checklist_id}")
|
|
188
192
|
|
|
189
193
|
def add_checklist_item(
|
|
190
|
-
self,
|
|
194
|
+
self,
|
|
195
|
+
checklist_id: str,
|
|
196
|
+
name: str,
|
|
197
|
+
checked: bool = False,
|
|
198
|
+
id_member: str | None = None,
|
|
199
|
+
due: str | None = None,
|
|
200
|
+
due_reminder: int | None = None,
|
|
191
201
|
) -> dict:
|
|
192
|
-
"""Add an item to a checklist.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
202
|
+
"""Add an item to a checklist.
|
|
203
|
+
|
|
204
|
+
If id_member or due is given, the item is assigned to that member or
|
|
205
|
+
given that due date. due_reminder is the number of minutes before the
|
|
206
|
+
due date to send a reminder. Per-item members and due dates require a
|
|
207
|
+
paid Trello plan (advanced checklists); on free plans the API accepts
|
|
208
|
+
the values but does not store them.
|
|
209
|
+
"""
|
|
210
|
+
params: dict[str, str] = {"name": name, "checked": str(checked).lower()}
|
|
211
|
+
if id_member:
|
|
212
|
+
params["idMember"] = id_member
|
|
213
|
+
if due:
|
|
214
|
+
params["due"] = due
|
|
215
|
+
if due_reminder is not None:
|
|
216
|
+
params["dueReminder"] = str(due_reminder)
|
|
217
|
+
return self.post(f"/checklists/{checklist_id}/checkItems", params=params)
|
|
197
218
|
|
|
198
219
|
def update_checklist_item(
|
|
199
220
|
self, card_id: str, check_item_id: str, state: str
|
|
@@ -204,6 +225,42 @@ class TrelloClient:
|
|
|
204
225
|
params={"state": state},
|
|
205
226
|
)
|
|
206
227
|
|
|
228
|
+
def set_checklist_item_member(
|
|
229
|
+
self, card_id: str, check_item_id: str, id_member: str
|
|
230
|
+
) -> dict:
|
|
231
|
+
"""Assign a member to a checklist item.
|
|
232
|
+
|
|
233
|
+
Pass an empty string for id_member to remove the assigned member. A
|
|
234
|
+
checklist item holds at most one member. Requires a paid Trello plan
|
|
235
|
+
(advanced checklists); on free plans the value is silently dropped.
|
|
236
|
+
"""
|
|
237
|
+
return self.put(
|
|
238
|
+
f"/cards/{card_id}/checkItem/{check_item_id}",
|
|
239
|
+
params={"idMember": id_member},
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def set_checklist_item_due(
|
|
243
|
+
self,
|
|
244
|
+
card_id: str,
|
|
245
|
+
check_item_id: str,
|
|
246
|
+
due: str,
|
|
247
|
+
due_reminder: int | None = None,
|
|
248
|
+
) -> dict:
|
|
249
|
+
"""Set a checklist item's due date.
|
|
250
|
+
|
|
251
|
+
Pass an empty string for due to remove the due date. due_reminder is
|
|
252
|
+
the number of minutes before the due date to send a reminder (-1 to
|
|
253
|
+
clear it). Requires a paid Trello plan (advanced checklists); on free
|
|
254
|
+
plans the value is silently dropped.
|
|
255
|
+
"""
|
|
256
|
+
params: dict[str, str] = {"due": due}
|
|
257
|
+
if due_reminder is not None:
|
|
258
|
+
params["dueReminder"] = str(due_reminder)
|
|
259
|
+
return self.put(
|
|
260
|
+
f"/cards/{card_id}/checkItem/{check_item_id}",
|
|
261
|
+
params=params,
|
|
262
|
+
)
|
|
263
|
+
|
|
207
264
|
def delete_checklist_item(self, checklist_id: str, check_item_id: str) -> None:
|
|
208
265
|
"""Delete a checklist item."""
|
|
209
266
|
self.delete(f"/checklists/{checklist_id}/checkItems/{check_item_id}")
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Checklist commands."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from trelloctl.cli import Context, pass_context
|
|
8
|
+
from trelloctl.output import (
|
|
9
|
+
format_output,
|
|
10
|
+
print_error,
|
|
11
|
+
print_success,
|
|
12
|
+
print_warning,
|
|
13
|
+
)
|
|
14
|
+
from trelloctl.resolver import is_trello_id
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.group()
|
|
18
|
+
def checklist() -> None:
|
|
19
|
+
"""Checklist management commands."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@checklist.command("list")
|
|
24
|
+
@click.argument("card_id")
|
|
25
|
+
@pass_context
|
|
26
|
+
def list_checklists(ctx: Context, card_id: str) -> None:
|
|
27
|
+
"""List all checklists and items on a card.
|
|
28
|
+
|
|
29
|
+
CARD_ID must be a Trello card ID.
|
|
30
|
+
"""
|
|
31
|
+
client = ctx.ensure_client()
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
checklists = client.get_card_checklists(card_id)
|
|
35
|
+
except Exception as e:
|
|
36
|
+
print_error(f"Failed to get checklists: {e}")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
member_names = _resolve_item_members(client, card_id, checklists)
|
|
40
|
+
|
|
41
|
+
data = []
|
|
42
|
+
for cl in checklists:
|
|
43
|
+
items = cl.get("checkItems", [])
|
|
44
|
+
if not items:
|
|
45
|
+
data.append(
|
|
46
|
+
{
|
|
47
|
+
"checklist": cl["name"],
|
|
48
|
+
"checklist_id": cl["id"],
|
|
49
|
+
"item": "",
|
|
50
|
+
"item_id": "",
|
|
51
|
+
"state": "",
|
|
52
|
+
"member": "",
|
|
53
|
+
"due": "",
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
for item in sorted(items, key=lambda i: i.get("pos", 0)):
|
|
58
|
+
state = "x" if item.get("state") == "complete" else " "
|
|
59
|
+
member_id = item.get("idMember") or ""
|
|
60
|
+
data.append(
|
|
61
|
+
{
|
|
62
|
+
"checklist": cl["name"],
|
|
63
|
+
"checklist_id": cl["id"],
|
|
64
|
+
"item": f"[{state}] {item['name']}",
|
|
65
|
+
"item_id": item["id"],
|
|
66
|
+
"state": item.get("state", ""),
|
|
67
|
+
"member": member_names.get(member_id, member_id),
|
|
68
|
+
"due": item.get("due") or "",
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
format_output(
|
|
73
|
+
data,
|
|
74
|
+
ctx.format,
|
|
75
|
+
columns=[
|
|
76
|
+
("checklist", "Checklist"),
|
|
77
|
+
("item", "Item"),
|
|
78
|
+
("member", "Member"),
|
|
79
|
+
("due", "Due"),
|
|
80
|
+
("checklist_id", "Checklist ID"),
|
|
81
|
+
("item_id", "Item ID"),
|
|
82
|
+
],
|
|
83
|
+
title="Checklists",
|
|
84
|
+
template="{checklist}: {item}",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _resolve_item_members(
|
|
89
|
+
client: Any, card_id: str, checklists: list[dict]
|
|
90
|
+
) -> dict[str, str]:
|
|
91
|
+
"""Build a map of member ID to display name for assigned checklist items.
|
|
92
|
+
|
|
93
|
+
Returns an empty map (so callers fall back to raw IDs) when no items are
|
|
94
|
+
assigned or the member lookup fails.
|
|
95
|
+
"""
|
|
96
|
+
has_members = any(
|
|
97
|
+
item.get("idMember") for cl in checklists for item in cl.get("checkItems", [])
|
|
98
|
+
)
|
|
99
|
+
if not has_members:
|
|
100
|
+
return {}
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
card = client.get_card(card_id)
|
|
104
|
+
members = client.get_board_members(card["idBoard"])
|
|
105
|
+
except Exception as e:
|
|
106
|
+
print_warning(f"Could not resolve member names, showing IDs instead: {e}")
|
|
107
|
+
return {}
|
|
108
|
+
|
|
109
|
+
return {m["id"]: m.get("fullName") or m.get("username") or m["id"] for m in members}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _member_id_from_card(
|
|
113
|
+
ctx: Context, client: Any, card_id: str, member_ref: str
|
|
114
|
+
) -> str:
|
|
115
|
+
"""Resolve a member reference (ID, username, or name) via the card's board."""
|
|
116
|
+
if is_trello_id(member_ref):
|
|
117
|
+
return member_ref
|
|
118
|
+
card = client.get_card(card_id)
|
|
119
|
+
return ctx.resolver.resolve_member(card["idBoard"], member_ref)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _member_id_from_checklist(
|
|
123
|
+
ctx: Context, client: Any, checklist_id: str, member_ref: str
|
|
124
|
+
) -> str:
|
|
125
|
+
"""Resolve a member reference (ID, username, or name) via the checklist's board."""
|
|
126
|
+
if is_trello_id(member_ref):
|
|
127
|
+
return member_ref
|
|
128
|
+
cl = client.get_checklist(checklist_id)
|
|
129
|
+
return ctx.resolver.resolve_member(cl["idBoard"], member_ref)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@checklist.command("create")
|
|
133
|
+
@click.argument("card_id")
|
|
134
|
+
@click.option("--name", "-n", required=True, help="Checklist name")
|
|
135
|
+
@pass_context
|
|
136
|
+
def create_checklist(ctx: Context, card_id: str, name: str) -> None:
|
|
137
|
+
"""Create a checklist on a card.
|
|
138
|
+
|
|
139
|
+
CARD_ID must be a Trello card ID.
|
|
140
|
+
"""
|
|
141
|
+
client = ctx.ensure_client()
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
cl = client.create_checklist(card_id, name)
|
|
145
|
+
print_success(f"Created checklist: {cl['name']} ({cl['id']})")
|
|
146
|
+
except Exception as e:
|
|
147
|
+
print_error(f"Failed to create checklist: {e}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@checklist.command("delete")
|
|
151
|
+
@click.argument("checklist_id")
|
|
152
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this checklist?")
|
|
153
|
+
@pass_context
|
|
154
|
+
def delete_checklist(ctx: Context, checklist_id: str) -> None:
|
|
155
|
+
"""Delete a checklist."""
|
|
156
|
+
client = ctx.ensure_client()
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
client.delete_checklist(checklist_id)
|
|
160
|
+
print_success(f"Deleted checklist: {checklist_id}")
|
|
161
|
+
except Exception as e:
|
|
162
|
+
print_error(f"Failed to delete checklist: {e}")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@checklist.command("add-item")
|
|
166
|
+
@click.argument("checklist_id")
|
|
167
|
+
@click.option("--name", "-n", required=True, help="Item name")
|
|
168
|
+
@click.option("--checked", is_flag=True, help="Mark as checked")
|
|
169
|
+
@click.option(
|
|
170
|
+
"--member",
|
|
171
|
+
"-m",
|
|
172
|
+
help="Member ID, username, or name to assign (see 'board members')",
|
|
173
|
+
)
|
|
174
|
+
@click.option("--due", help="Due date (ISO format, e.g. 2026-07-01)")
|
|
175
|
+
@click.option(
|
|
176
|
+
"--due-reminder",
|
|
177
|
+
type=int,
|
|
178
|
+
help="Minutes before the due date to send a reminder",
|
|
179
|
+
)
|
|
180
|
+
@pass_context
|
|
181
|
+
def add_item(
|
|
182
|
+
ctx: Context,
|
|
183
|
+
checklist_id: str,
|
|
184
|
+
name: str,
|
|
185
|
+
checked: bool,
|
|
186
|
+
member: str | None,
|
|
187
|
+
due: str | None,
|
|
188
|
+
due_reminder: int | None,
|
|
189
|
+
) -> None:
|
|
190
|
+
"""Add an item to a checklist.
|
|
191
|
+
|
|
192
|
+
Use --member and --due to assign a member or due date to the item; both
|
|
193
|
+
require a paid Trello plan (advanced checklists).
|
|
194
|
+
"""
|
|
195
|
+
client = ctx.ensure_client()
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
member_id = (
|
|
199
|
+
_member_id_from_checklist(ctx, client, checklist_id, member)
|
|
200
|
+
if member
|
|
201
|
+
else None
|
|
202
|
+
)
|
|
203
|
+
item = client.add_checklist_item(
|
|
204
|
+
checklist_id,
|
|
205
|
+
name,
|
|
206
|
+
checked=checked,
|
|
207
|
+
id_member=member_id,
|
|
208
|
+
due=due,
|
|
209
|
+
due_reminder=due_reminder,
|
|
210
|
+
)
|
|
211
|
+
print_success(f"Added item: {item['name']} ({item['id']})")
|
|
212
|
+
except Exception as e:
|
|
213
|
+
print_error(f"Failed to add item: {e}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@checklist.command("assign")
|
|
217
|
+
@click.argument("card_id")
|
|
218
|
+
@click.argument("item_id")
|
|
219
|
+
@click.argument("member")
|
|
220
|
+
@pass_context
|
|
221
|
+
def assign_item(ctx: Context, card_id: str, item_id: str, member: str) -> None:
|
|
222
|
+
"""Assign a member to a checklist item.
|
|
223
|
+
|
|
224
|
+
CARD_ID is the card containing the item.
|
|
225
|
+
ITEM_ID is the checklist item to assign.
|
|
226
|
+
MEMBER is the member ID, username, or name (see 'board members').
|
|
227
|
+
|
|
228
|
+
Requires a paid Trello plan (advanced checklists).
|
|
229
|
+
"""
|
|
230
|
+
client = ctx.ensure_client()
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
member_id = _member_id_from_card(ctx, client, card_id, member)
|
|
234
|
+
client.set_checklist_item_member(card_id, item_id, member_id)
|
|
235
|
+
print_success(f"Assigned {member} to item {item_id}")
|
|
236
|
+
except Exception as e:
|
|
237
|
+
print_error(f"Failed to assign member: {e}")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@checklist.command("unassign")
|
|
241
|
+
@click.argument("card_id")
|
|
242
|
+
@click.argument("item_id")
|
|
243
|
+
@pass_context
|
|
244
|
+
def unassign_item(ctx: Context, card_id: str, item_id: str) -> None:
|
|
245
|
+
"""Remove the assigned member from a checklist item.
|
|
246
|
+
|
|
247
|
+
CARD_ID is the card containing the item.
|
|
248
|
+
ITEM_ID is the checklist item to clear.
|
|
249
|
+
"""
|
|
250
|
+
client = ctx.ensure_client()
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
client.set_checklist_item_member(card_id, item_id, "")
|
|
254
|
+
print_success(f"Removed assigned member from item {item_id}")
|
|
255
|
+
except Exception as e:
|
|
256
|
+
print_error(f"Failed to unassign member: {e}")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@checklist.command("set-due")
|
|
260
|
+
@click.argument("card_id")
|
|
261
|
+
@click.argument("item_id")
|
|
262
|
+
@click.argument("due")
|
|
263
|
+
@click.option(
|
|
264
|
+
"--reminder",
|
|
265
|
+
type=int,
|
|
266
|
+
help="Minutes before the due date to send a reminder (-1 to clear)",
|
|
267
|
+
)
|
|
268
|
+
@pass_context
|
|
269
|
+
def set_item_due(
|
|
270
|
+
ctx: Context, card_id: str, item_id: str, due: str, reminder: int | None
|
|
271
|
+
) -> None:
|
|
272
|
+
"""Set or clear the due date on a checklist item.
|
|
273
|
+
|
|
274
|
+
CARD_ID is the card containing the item.
|
|
275
|
+
ITEM_ID is the checklist item to update.
|
|
276
|
+
DUE is an ISO date (e.g. 2026-07-01), or 'null' to remove the due date.
|
|
277
|
+
|
|
278
|
+
Requires a paid Trello plan (advanced checklists).
|
|
279
|
+
"""
|
|
280
|
+
client = ctx.ensure_client()
|
|
281
|
+
due_value = "" if due.lower() == "null" else due
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
client.set_checklist_item_due(
|
|
285
|
+
card_id, item_id, due_value, due_reminder=reminder
|
|
286
|
+
)
|
|
287
|
+
if due_value:
|
|
288
|
+
print_success(f"Set due date on item {item_id} to {due_value}")
|
|
289
|
+
else:
|
|
290
|
+
print_success(f"Removed due date from item {item_id}")
|
|
291
|
+
except Exception as e:
|
|
292
|
+
print_error(f"Failed to set due date: {e}")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@checklist.command("check")
|
|
296
|
+
@click.argument("card_id")
|
|
297
|
+
@click.argument("item_id")
|
|
298
|
+
@click.option("--uncheck", is_flag=True, help="Mark as incomplete instead")
|
|
299
|
+
@pass_context
|
|
300
|
+
def check_item(ctx: Context, card_id: str, item_id: str, uncheck: bool) -> None:
|
|
301
|
+
"""Mark a checklist item as complete (or incomplete with --uncheck).
|
|
302
|
+
|
|
303
|
+
CARD_ID is the card containing the item.
|
|
304
|
+
ITEM_ID is the checklist item to update.
|
|
305
|
+
"""
|
|
306
|
+
client = ctx.ensure_client()
|
|
307
|
+
state = "incomplete" if uncheck else "complete"
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
client.update_checklist_item(card_id, item_id, state)
|
|
311
|
+
action = "Unchecked" if uncheck else "Checked"
|
|
312
|
+
print_success(f"{action} item: {item_id}")
|
|
313
|
+
except Exception as e:
|
|
314
|
+
print_error(f"Failed to update item: {e}")
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@checklist.command("delete-item")
|
|
318
|
+
@click.argument("checklist_id")
|
|
319
|
+
@click.argument("item_id")
|
|
320
|
+
@pass_context
|
|
321
|
+
def delete_item(ctx: Context, checklist_id: str, item_id: str) -> None:
|
|
322
|
+
"""Delete a checklist item."""
|
|
323
|
+
client = ctx.ensure_client()
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
client.delete_checklist_item(checklist_id, item_id)
|
|
327
|
+
print_success(f"Deleted item: {item_id}")
|
|
328
|
+
except Exception as e:
|
|
329
|
+
print_error(f"Failed to delete item: {e}")
|
|
@@ -8,6 +8,11 @@ if TYPE_CHECKING:
|
|
|
8
8
|
from trelloctl.client import TrelloClient
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
def is_trello_id(ref: str) -> bool:
|
|
12
|
+
"""Return True if ref looks like a 24-character hex Trello ID."""
|
|
13
|
+
return len(ref) == 24 and all(c in "0123456789abcdef" for c in ref.lower())
|
|
14
|
+
|
|
15
|
+
|
|
11
16
|
class Resolver:
|
|
12
17
|
"""Resolves names to Trello IDs."""
|
|
13
18
|
|
|
@@ -15,6 +20,7 @@ class Resolver:
|
|
|
15
20
|
self.client = client
|
|
16
21
|
self._boards_cache: list[dict] | None = None
|
|
17
22
|
self._lists_cache: dict[str, list[dict]] = {}
|
|
23
|
+
self._members_cache: dict[str, list[dict]] = {}
|
|
18
24
|
|
|
19
25
|
def _get_boards(self) -> list[dict]:
|
|
20
26
|
"""Get all boards (cached)."""
|
|
@@ -30,6 +36,12 @@ class Resolver:
|
|
|
30
36
|
)
|
|
31
37
|
return self._lists_cache[board_id]
|
|
32
38
|
|
|
39
|
+
def _get_members(self, board_id: str) -> list[dict]:
|
|
40
|
+
"""Get all members for a board (cached)."""
|
|
41
|
+
if board_id not in self._members_cache:
|
|
42
|
+
self._members_cache[board_id] = self.client.get_board_members(board_id)
|
|
43
|
+
return self._members_cache[board_id]
|
|
44
|
+
|
|
33
45
|
def resolve_board(self, board_ref: str) -> str:
|
|
34
46
|
"""Resolve a board reference (ID or name) to an ID.
|
|
35
47
|
|
|
@@ -43,9 +55,7 @@ class Resolver:
|
|
|
43
55
|
ValueError: If board not found or multiple matches
|
|
44
56
|
"""
|
|
45
57
|
# If it looks like an ID (24 hex chars), try it directly
|
|
46
|
-
if
|
|
47
|
-
c in "0123456789abcdef" for c in board_ref.lower()
|
|
48
|
-
):
|
|
58
|
+
if is_trello_id(board_ref):
|
|
49
59
|
return board_ref
|
|
50
60
|
|
|
51
61
|
boards = self._get_boards()
|
|
@@ -82,9 +92,7 @@ class Resolver:
|
|
|
82
92
|
board_id = self.resolve_board(board_ref)
|
|
83
93
|
|
|
84
94
|
# If it looks like an ID, try it directly
|
|
85
|
-
if
|
|
86
|
-
c in "0123456789abcdef" for c in list_ref.lower()
|
|
87
|
-
):
|
|
95
|
+
if is_trello_id(list_ref):
|
|
88
96
|
return list_ref
|
|
89
97
|
|
|
90
98
|
lists = self._get_lists(board_id)
|
|
@@ -124,9 +132,7 @@ class Resolver:
|
|
|
124
132
|
list_id = self.resolve_list(board_ref, list_ref)
|
|
125
133
|
|
|
126
134
|
# If it looks like an ID, try it directly
|
|
127
|
-
if
|
|
128
|
-
c in "0123456789abcdef" for c in card_ref.lower()
|
|
129
|
-
):
|
|
135
|
+
if is_trello_id(card_ref):
|
|
130
136
|
return card_ref
|
|
131
137
|
|
|
132
138
|
cards = self.client.get_list_cards(list_id)
|
|
@@ -146,3 +152,59 @@ class Resolver:
|
|
|
146
152
|
raise ValueError(f"Multiple cards match '{card_ref}': {', '.join(names)}")
|
|
147
153
|
|
|
148
154
|
raise ValueError(f"Card not found: '{card_ref}' in list '{list_ref}'")
|
|
155
|
+
|
|
156
|
+
def resolve_member(self, board_ref: str, member_ref: str) -> str:
|
|
157
|
+
"""Resolve a member reference (ID, username, or full name) to an ID.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
board_ref: Board ID or name the member belongs to
|
|
161
|
+
member_ref: Member ID, username, or full name (partial,
|
|
162
|
+
case-insensitive)
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Member ID
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
ValueError: If member not found or multiple matches
|
|
169
|
+
"""
|
|
170
|
+
# If it looks like an ID, try it directly
|
|
171
|
+
if is_trello_id(member_ref):
|
|
172
|
+
return member_ref
|
|
173
|
+
|
|
174
|
+
board_id = self.resolve_board(board_ref)
|
|
175
|
+
members = self._get_members(board_id)
|
|
176
|
+
member_ref_lower = member_ref.lower()
|
|
177
|
+
|
|
178
|
+
# Try exact username match first
|
|
179
|
+
username_matches = [
|
|
180
|
+
m for m in members if m.get("username", "").lower() == member_ref_lower
|
|
181
|
+
]
|
|
182
|
+
if len(username_matches) == 1:
|
|
183
|
+
return username_matches[0]["id"]
|
|
184
|
+
|
|
185
|
+
# Then exact full name match
|
|
186
|
+
fullname_matches = [
|
|
187
|
+
m for m in members if m.get("fullName", "").lower() == member_ref_lower
|
|
188
|
+
]
|
|
189
|
+
if len(fullname_matches) == 1:
|
|
190
|
+
return fullname_matches[0]["id"]
|
|
191
|
+
|
|
192
|
+
# Fall back to partial match on username or full name
|
|
193
|
+
partial_matches = [
|
|
194
|
+
m
|
|
195
|
+
for m in members
|
|
196
|
+
if member_ref_lower in m.get("username", "").lower()
|
|
197
|
+
or member_ref_lower in m.get("fullName", "").lower()
|
|
198
|
+
]
|
|
199
|
+
if len(partial_matches) == 1:
|
|
200
|
+
return partial_matches[0]["id"]
|
|
201
|
+
if len(partial_matches) > 1:
|
|
202
|
+
names = [
|
|
203
|
+
m.get("username") or m.get("fullName") or m["id"]
|
|
204
|
+
for m in partial_matches
|
|
205
|
+
]
|
|
206
|
+
raise ValueError(
|
|
207
|
+
f"Multiple members match '{member_ref}': {', '.join(names)}"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
raise ValueError(f"Member not found: '{member_ref}' in board '{board_ref}'")
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
"""Checklist commands."""
|
|
2
|
-
|
|
3
|
-
import click
|
|
4
|
-
|
|
5
|
-
from trelloctl.cli import Context, pass_context
|
|
6
|
-
from trelloctl.output import format_output, print_error, print_success
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@click.group()
|
|
10
|
-
def checklist() -> None:
|
|
11
|
-
"""Checklist management commands."""
|
|
12
|
-
pass
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@checklist.command("list")
|
|
16
|
-
@click.argument("card_id")
|
|
17
|
-
@pass_context
|
|
18
|
-
def list_checklists(ctx: Context, card_id: str) -> None:
|
|
19
|
-
"""List all checklists and items on a card.
|
|
20
|
-
|
|
21
|
-
CARD_ID must be a Trello card ID.
|
|
22
|
-
"""
|
|
23
|
-
client = ctx.ensure_client()
|
|
24
|
-
|
|
25
|
-
try:
|
|
26
|
-
checklists = client.get_card_checklists(card_id)
|
|
27
|
-
except Exception as e:
|
|
28
|
-
print_error(f"Failed to get checklists: {e}")
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
data = []
|
|
32
|
-
for cl in checklists:
|
|
33
|
-
items = cl.get("checkItems", [])
|
|
34
|
-
if not items:
|
|
35
|
-
data.append(
|
|
36
|
-
{
|
|
37
|
-
"checklist": cl["name"],
|
|
38
|
-
"checklist_id": cl["id"],
|
|
39
|
-
"item": "",
|
|
40
|
-
"item_id": "",
|
|
41
|
-
"state": "",
|
|
42
|
-
}
|
|
43
|
-
)
|
|
44
|
-
else:
|
|
45
|
-
for item in sorted(items, key=lambda i: i.get("pos", 0)):
|
|
46
|
-
state = "x" if item.get("state") == "complete" else " "
|
|
47
|
-
data.append(
|
|
48
|
-
{
|
|
49
|
-
"checklist": cl["name"],
|
|
50
|
-
"checklist_id": cl["id"],
|
|
51
|
-
"item": f"[{state}] {item['name']}",
|
|
52
|
-
"item_id": item["id"],
|
|
53
|
-
"state": item.get("state", ""),
|
|
54
|
-
}
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
format_output(
|
|
58
|
-
data,
|
|
59
|
-
ctx.format,
|
|
60
|
-
columns=[
|
|
61
|
-
("checklist", "Checklist"),
|
|
62
|
-
("item", "Item"),
|
|
63
|
-
("checklist_id", "Checklist ID"),
|
|
64
|
-
("item_id", "Item ID"),
|
|
65
|
-
],
|
|
66
|
-
title="Checklists",
|
|
67
|
-
template="{checklist}: {item}",
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
@checklist.command("create")
|
|
72
|
-
@click.argument("card_id")
|
|
73
|
-
@click.option("--name", "-n", required=True, help="Checklist name")
|
|
74
|
-
@pass_context
|
|
75
|
-
def create_checklist(ctx: Context, card_id: str, name: str) -> None:
|
|
76
|
-
"""Create a checklist on a card.
|
|
77
|
-
|
|
78
|
-
CARD_ID must be a Trello card ID.
|
|
79
|
-
"""
|
|
80
|
-
client = ctx.ensure_client()
|
|
81
|
-
|
|
82
|
-
try:
|
|
83
|
-
cl = client.create_checklist(card_id, name)
|
|
84
|
-
print_success(f"Created checklist: {cl['name']} ({cl['id']})")
|
|
85
|
-
except Exception as e:
|
|
86
|
-
print_error(f"Failed to create checklist: {e}")
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
@checklist.command("delete")
|
|
90
|
-
@click.argument("checklist_id")
|
|
91
|
-
@click.confirmation_option(prompt="Are you sure you want to delete this checklist?")
|
|
92
|
-
@pass_context
|
|
93
|
-
def delete_checklist(ctx: Context, checklist_id: str) -> None:
|
|
94
|
-
"""Delete a checklist."""
|
|
95
|
-
client = ctx.ensure_client()
|
|
96
|
-
|
|
97
|
-
try:
|
|
98
|
-
client.delete_checklist(checklist_id)
|
|
99
|
-
print_success(f"Deleted checklist: {checklist_id}")
|
|
100
|
-
except Exception as e:
|
|
101
|
-
print_error(f"Failed to delete checklist: {e}")
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
@checklist.command("add-item")
|
|
105
|
-
@click.argument("checklist_id")
|
|
106
|
-
@click.option("--name", "-n", required=True, help="Item name")
|
|
107
|
-
@click.option("--checked", is_flag=True, help="Mark as checked")
|
|
108
|
-
@pass_context
|
|
109
|
-
def add_item(ctx: Context, checklist_id: str, name: str, checked: bool) -> None:
|
|
110
|
-
"""Add an item to a checklist."""
|
|
111
|
-
client = ctx.ensure_client()
|
|
112
|
-
|
|
113
|
-
try:
|
|
114
|
-
item = client.add_checklist_item(checklist_id, name, checked=checked)
|
|
115
|
-
print_success(f"Added item: {item['name']} ({item['id']})")
|
|
116
|
-
except Exception as e:
|
|
117
|
-
print_error(f"Failed to add item: {e}")
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
@checklist.command("check")
|
|
121
|
-
@click.argument("card_id")
|
|
122
|
-
@click.argument("item_id")
|
|
123
|
-
@click.option("--uncheck", is_flag=True, help="Mark as incomplete instead")
|
|
124
|
-
@pass_context
|
|
125
|
-
def check_item(ctx: Context, card_id: str, item_id: str, uncheck: bool) -> None:
|
|
126
|
-
"""Mark a checklist item as complete (or incomplete with --uncheck).
|
|
127
|
-
|
|
128
|
-
CARD_ID is the card containing the item.
|
|
129
|
-
ITEM_ID is the checklist item to update.
|
|
130
|
-
"""
|
|
131
|
-
client = ctx.ensure_client()
|
|
132
|
-
state = "incomplete" if uncheck else "complete"
|
|
133
|
-
|
|
134
|
-
try:
|
|
135
|
-
client.update_checklist_item(card_id, item_id, state)
|
|
136
|
-
action = "Unchecked" if uncheck else "Checked"
|
|
137
|
-
print_success(f"{action} item: {item_id}")
|
|
138
|
-
except Exception as e:
|
|
139
|
-
print_error(f"Failed to update item: {e}")
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
@checklist.command("delete-item")
|
|
143
|
-
@click.argument("checklist_id")
|
|
144
|
-
@click.argument("item_id")
|
|
145
|
-
@pass_context
|
|
146
|
-
def delete_item(ctx: Context, checklist_id: str, item_id: str) -> None:
|
|
147
|
-
"""Delete a checklist item."""
|
|
148
|
-
client = ctx.ensure_client()
|
|
149
|
-
|
|
150
|
-
try:
|
|
151
|
-
client.delete_checklist_item(checklist_id, item_id)
|
|
152
|
-
print_success(f"Deleted item: {item_id}")
|
|
153
|
-
except Exception as e:
|
|
154
|
-
print_error(f"Failed to delete item: {e}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|