feat: implement Trello authentication & connection plugin

Add TrelloClient for Trello REST API interaction with env-var-based
credential management (TRELLO_API_KEY, TRELLO_TOKEN).

Implements:
- trello_verify_credentials — verify API key/token against Trello API
- trello_list_boards — list accessible Trello boards
- trello_disconnect — clear in-memory credentials

Includes full test suite (18 tests), spec document, and plugin metadata.

Issue: #1
This commit is contained in:
Marko (Hermes Implementer)
2026-05-26 21:33:29 +00:00
parent 5a90c37ed6
commit 6dac5d225e
8 changed files with 873 additions and 1 deletions
+175
View File
@@ -0,0 +1,175 @@
"""
Trello Plugin — Hermes Agent plugin for Trello board integration.
Authentication & Connection module.
Provides the TrelloClient for interacting with the Trello REST API.
"""
from __future__ import annotations
import os
from typing import Any
import requests
TRELLO_API_BASE = "https://api.trello.com/1"
class TrelloClient:
"""Client for the Trello REST API.
Reads credentials from environment variables TRELLO_API_KEY and TRELLO_TOKEN
unless explicitly passed. All public methods return plain dicts suitable for
JSON serialization by Hermes tool handlers.
"""
def __init__(
self,
api_key: str | None = None,
token: str | None = None,
session: requests.Session | None = None,
) -> None:
self.api_key = api_key or os.environ.get("TRELLO_API_KEY", "")
self.token = token or os.environ.get("TRELLO_TOKEN", "")
self._session = session or requests.Session()
# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------
def _auth_params(self) -> dict[str, str]:
"""Return query parameters common to every Trello API call."""
return {"key": self.api_key, "token": self.token}
def _get(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
"""Make an authenticated GET request and return the JSON response."""
if not self.api_key or not self.token:
return {
"success": False,
"message": "TRELLO_API_KEY and TRELLO_TOKEN must both be set as environment variables.",
}
url = f"{TRELLO_API_BASE}{path}"
merged_params = self._auth_params()
if params:
merged_params.update(params)
try:
resp = self._session.get(url, params=merged_params, timeout=15)
resp.raise_for_status()
data: dict[str, Any] = resp.json()
return {"success": True, "data": data}
except requests.exceptions.HTTPError as exc:
status = exc.response.status_code if exc.response is not None else 0
if status == 401:
return {
"success": False,
"message": "Trello authentication failed (401). Check your TRELLO_API_KEY and TRELLO_TOKEN.",
}
if status == 403:
return {
"success": False,
"message": "Trello access denied (403). Your token may not have the required scopes.",
}
if status == 429:
return {
"success": False,
"message": "Trello rate limit exceeded. Try again later.",
}
return {
"success": False,
"message": f"Trello API error ({status}): {exc}",
}
except requests.exceptions.ConnectionError:
return {
"success": False,
"message": "Could not connect to Trello API. Check your network connection.",
}
except requests.exceptions.Timeout:
return {
"success": False,
"message": "Trello API request timed out. Try again later.",
}
except requests.exceptions.RequestException as exc:
return {
"success": False,
"message": f"Trello API request failed: {exc}",
}
# ------------------------------------------------------------------
# Public API — Auth & Connection
# ------------------------------------------------------------------
def verify_credentials(self) -> dict[str, Any]:
"""Verify that the stored credentials are valid by fetching member info.
Returns:
dict with ``success`` bool and ``message`` string. On success the
``member`` key contains the Trello member id and username.
"""
result = self._get("/members/me")
if result["success"]:
member = result["data"]
return {
"success": True,
"message": (
f"✓ Trello credentials verified successfully "
f"(authenticated as {member.get('username', 'unknown')})"
),
"member": {
"id": member.get("id"),
"username": member.get("username"),
"full_name": member.get("fullName"),
},
}
return {
"success": False,
"message": result.get("message", "Trello credential verification failed."),
}
def list_boards(self) -> dict[str, Any]:
"""Fetch all boards accessible to the authenticated user.
Returns:
dict with ``success`` bool and a ``boards`` list (each containing
``id``, ``name``, ``url``, ``closed``, ``starred``).
"""
result = self._get("/members/me/boards", params={"fields": "id,name,url,closed,starred"})
if not result["success"]:
return {
"success": False,
"message": result.get("message", "Failed to list Trello boards."),
}
boards = [
{
"id": b.get("id"),
"name": b.get("name"),
"url": b.get("url"),
"closed": b.get("closed", False),
"starred": b.get("starred", False),
}
for b in result["data"]
]
return {"success": True, "boards": boards}
def disconnect(self) -> dict[str, Any]:
"""Clear credentials from the in-memory client.
Note: this does NOT revoke the Trello token. The user must invalidate
it via Trello's account settings if they want full revocation.
"""
self.api_key = ""
self.token = ""
return {
"success": True,
"message": (
"Trello credentials cleared. "
"Set TRELLO_API_KEY and TRELLO_TOKEN again to reconnect."
),
}
@staticmethod
def check_requirements() -> bool:
"""Check if the required environment variables are set."""
return bool(os.environ.get("TRELLO_API_KEY")) and bool(os.environ.get("TRELLO_TOKEN"))