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:
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user