feat: implement Trello card management

Add 12 card management tools:
- trello_create_card — create a card on a list
- trello_card_details — view card with members, checklists, comments
- trello_update_card — update title, description, due date
- trello_move_card — move card to a different list
- trello_archive_card — archive a card
- trello_assign_member / trello_remove_member — member management
- trello_add_comment / trello_delete_comment — comments
- trello_add_checklist_item / trello_toggle_checklist_item / trello_delete_checklist_item

Also adds _delete() helper on TrelloClient for DELETE verbs.

73 total tests — all passing.

Issue: #4
This commit is contained in:
Marko (Hermes Implementer)
2026-05-26 22:42:50 +00:00
parent f2d65f8914
commit 034ed2c928
6 changed files with 1161 additions and 1 deletions
+396
View File
@@ -163,6 +163,10 @@ class TrelloClient:
"""Make an authenticated PUT request."""
return self._request("PUT", path, json_body=json_body)
def _delete(self, path: str) -> ApiResponse:
"""Make an authenticated DELETE request."""
return self._request("DELETE", path)
# ------------------------------------------------------------------
# Public API — Auth & Connection
# ------------------------------------------------------------------
@@ -571,6 +575,398 @@ class TrelloClient:
"message": result.get("message", "Failed to move list."),
}
# ------------------------------------------------------------------
# Public API — Card Management
# ------------------------------------------------------------------
def create_card(
self,
name: str,
list_id: str,
desc: str = "",
due: str | None = None,
) -> dict[str, Any]:
"""Create a new card on a Trello list.
Args:
name: Card title.
list_id: Trello list ID.
desc: Card description.
due: Due date in ISO 8601 format.
Returns:
dict with card details on success.
"""
body: dict[str, Any] = {"name": name, "idList": list_id}
if desc:
body["desc"] = desc
if due:
body["due"] = due
result = self._post("/cards", json_body=body)
if _is_success(result):
card = result["data"]
return {
"success": True,
"card": {
"id": card.get("id"),
"name": card.get("name"),
"url": card.get("url"),
"short_url": card.get("shortUrl"),
"id_list": card.get("idList"),
},
}
return {
"success": False,
"message": result.get("message", "Failed to create card."),
}
def card_details(self, card_id: str) -> dict[str, Any]:
"""Fetch detailed information about a card.
Returns card attributes, members, checklists, and comments.
Args:
card_id: Trello card ID.
Returns:
dict with full card details.
"""
result = self._get(
f"/cards/{card_id}",
params={
"fields": "id,name,desc,due,dueComplete,url,shortUrl,idList,closed",
"members": "true",
"member_fields": "id,username,fullName",
"checklists": "all",
"actions": "commentCard",
"actions_limit": "50",
"list": "true",
},
)
if not _is_success(result):
return {
"success": False,
"message": result.get("message", "Failed to fetch card details."),
}
card = result["data"]
return {
"success": True,
"card": {
"id": card.get("id"),
"name": card.get("name"),
"desc": card.get("desc", ""),
"due": card.get("due"),
"due_complete": card.get("dueComplete", False),
"url": card.get("url"),
"short_url": card.get("shortUrl"),
"closed": card.get("closed", False),
"list": {"id": card.get("idList"), "name": (card.get("list") or {}).get("name", "")},
"members": [
{"id": m.get("id"), "username": m.get("username"), "full_name": m.get("fullName")}
for m in card.get("members", [])
],
"checklists": [
{
"id": cl.get("id"),
"name": cl.get("name"),
"items": [
{
"id": item.get("id"),
"name": item.get("name"),
"state": item.get("state"),
}
for item in cl.get("checkItems", [])
],
}
for cl in card.get("checklists", [])
],
"comments": [
{
"id": a.get("id"),
"text": a.get("data", {}).get("text", ""),
"member": a.get("memberCreator", {}).get("username", "unknown"),
"date": a.get("date"),
}
for a in card.get("actions", [])
if a.get("type") == "commentCard"
],
},
}
def update_card(
self,
card_id: str,
name: str | None = None,
desc: str | None = None,
due: str | None = None,
) -> dict[str, Any]:
"""Update a card's core attributes.
Args:
card_id: Trello card ID.
name: New title (omit to keep current).
desc: New description (omit to keep current).
due: New due date ISO 8601, or empty string to clear.
Returns:
dict with updated card details.
"""
body: dict[str, Any] = {}
if name is not None:
body["name"] = name
if desc is not None:
body["desc"] = desc
if due is not None:
body["due"] = due
if not body:
return {
"success": True,
"message": "No changes specified.",
}
result = self._put(f"/cards/{card_id}", json_body=body)
if _is_success(result):
card = result["data"]
return {
"success": True,
"card": {
"id": card.get("id"),
"name": card.get("name"),
"desc": card.get("desc", ""),
"due": card.get("due"),
},
}
return {
"success": False,
"message": result.get("message", "Failed to update card."),
}
def move_card(self, card_id: str, list_id: str) -> dict[str, Any]:
"""Move a card to a different list.
Args:
card_id: Trello card ID.
list_id: Target list ID.
Returns:
dict with success message.
"""
result = self._put(f"/cards/{card_id}", json_body={"idList": list_id})
if _is_success(result):
card = result["data"]
return {
"success": True,
"message": f"Card '{card.get('name', card_id)}' moved to list '{card.get('idList', list_id)}'.",
}
return {
"success": False,
"message": result.get("message", "Failed to move card."),
}
def archive_card(self, card_id: str) -> dict[str, Any]:
"""Archive a card.
Args:
card_id: Trello card ID.
Returns:
dict with success message.
"""
result = self._put(f"/cards/{card_id}", json_body={"closed": True})
if _is_success(result):
card = result["data"]
return {"success": True, "message": f"Card '{card.get('name', card_id)}' archived."}
return {
"success": False,
"message": result.get("message", "Failed to archive card."),
}
def assign_member(self, card_id: str, member_id: str) -> dict[str, Any]:
"""Add a member to a card.
Args:
card_id: Trello card ID.
member_id: Trello member ID.
Returns:
dict with success message.
"""
result = self._post(f"/cards/{card_id}/idMembers", json_body={"value": member_id})
if _is_success(result):
return {"success": True, "message": f"Member {member_id} assigned to card."}
return {
"success": False,
"message": result.get("message", "Failed to assign member."),
}
def remove_member(self, card_id: str, member_id: str) -> dict[str, Any]:
"""Remove a member from a card.
Args:
card_id: Trello card ID.
member_id: Trello member ID.
Returns:
dict with success message.
"""
result = self._delete(f"/cards/{card_id}/idMembers/{member_id}")
if _is_success(result):
return {"success": True, "message": f"Member {member_id} removed from card."}
return {
"success": False,
"message": result.get("message", "Failed to remove member."),
}
def add_comment(self, card_id: str, text: str) -> dict[str, Any]:
"""Add a comment to a card.
Args:
card_id: Trello card ID.
text: Comment text.
Returns:
dict with comment details.
"""
result = self._post(
f"/cards/{card_id}/actions/comments",
json_body={"text": text},
)
if _is_success(result):
action = result["data"]
return {
"success": True,
"comment": {
"id": action.get("id"),
"text": action.get("data", {}).get("text", text),
},
}
return {
"success": False,
"message": result.get("message", "Failed to add comment."),
}
def delete_comment(self, card_id: str, comment_id: str) -> dict[str, Any]:
"""Delete a comment from a card.
Args:
card_id: Trello card ID.
comment_id: Action/comment ID.
Returns:
dict with success message.
"""
result = self._delete(f"/cards/{card_id}/actions/{comment_id}/comments")
if _is_success(result):
return {"success": True, "message": "Comment deleted."}
return {
"success": False,
"message": result.get("message", "Failed to delete comment."),
}
def add_checklist_item(self, card_id: str, name: str, checklist_id: str | None = None) -> dict[str, Any]:
"""Add a checklist item to a card.
Args:
card_id: Trello card ID.
name: Checklist item text.
checklist_id: Specific checklist (fetches first if omitted).
Returns:
dict with item details.
"""
resolved_checklist_id = checklist_id
if not resolved_checklist_id:
# Fetch checklists to find the first one
details = self.card_details(card_id)
if not details.get("success"):
return {
"success": False,
"message": "Card has no checklists. Add one via Trello web first.",
}
checklists = details["card"].get("checklists", [])
if not checklists:
return {
"success": False,
"message": "Card has no checklists. Add one via Trello web first.",
}
resolved_checklist_id = checklists[0]["id"]
result = self._post(
f"/cards/{card_id}/checklistItems",
json_body={
"idChecklist": resolved_checklist_id,
"name": name,
"checked": False,
},
)
if _is_success(result):
item = result["data"]
return {
"success": True,
"item": {
"id": item.get("id"),
"name": item.get("name"),
"state": item.get("state", "incomplete"),
"id_checklist": item.get("idChecklist"),
},
}
return {
"success": False,
"message": result.get("message", "Failed to add checklist item."),
}
def toggle_checklist_item(self, card_id: str, item_id: str, checked: bool) -> dict[str, Any]: # noqa: FBT001
"""Mark a checklist item as complete or incomplete.
Args:
card_id: Trello card ID.
item_id: Checklist item ID.
checked: True for complete, False for incomplete.
Returns:
dict with updated item state.
"""
state = "complete" if checked else "incomplete"
result = self._put(
f"/cards/{card_id}/checklistItem/{item_id}",
json_body={"state": state},
)
if _is_success(result):
item = result["data"]
return {
"success": True,
"item": {
"id": item.get("id"),
"name": item.get("name"),
"state": item.get("state"),
},
}
return {
"success": False,
"message": result.get("message", "Failed to update checklist item."),
}
def delete_checklist_item(self, card_id: str, item_id: str) -> dict[str, Any]:
"""Remove a checklist item.
Args:
card_id: Trello card ID.
item_id: Checklist item ID.
Returns:
dict with success message.
"""
result = self._delete(f"/cards/{card_id}/checklistItems/{item_id}")
if _is_success(result):
return {"success": True, "message": "Checklist item deleted."}
return {
"success": False,
"message": result.get("message", "Failed to delete checklist item."),
}
@staticmethod
def check_requirements() -> bool:
"""Check if the required environment variables are set."""