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
+5
View File
@@ -0,0 +1,5 @@
__pycache__/
*.egg-info/
*.pyc
*.pyo
.pytest_cache/
+93 -1
View File
@@ -1,3 +1,95 @@
# trello-plugin # trello-plugin
Hermes Agent plugin for Trello board integration Hermes Agent plugin for Trello board integration.
## Setup
### 1. Get Trello Credentials
1. Get your **API Key** from: https://trello.com/app-key
2. Generate a **Token** from the same page (click "Token" under API Key)
3. Set these as environment variables in your Hermes profile config (`.env` or `config.yaml`):
```bash
export TRELLO_API_KEY="your-trello-api-key"
export TRELLO_TOKEN="your-trello-token"
```
### 2. Install
```bash
cd trello-plugin
pip install .
```
For development:
```bash
pip install -e ".[dev]"
```
### 3. Register with Hermes Agent
Add the plugin to your Hermes config (`~/.hermes/config.yaml` or profile):
```yaml
plugins:
- trello-plugin
```
## Available Tools
Once configured, the agent has access to these tools:
### `trello_verify_credentials`
Verifies your Trello API key and token are valid by calling the Trello API.
```
✓ Trello credentials verified successfully (authenticated as your-username)
```
### `trello_list_boards`
Lists all Trello boards accessible with your credentials.
| Field | Description |
|-------|-------------|
| `id` | Trello board ID |
| `name` | Board name |
| `url` | Direct Trello URL |
| `closed` | Whether the board is archived |
| `starred` | Whether the board is starred |
### `trello_disconnect`
Clears Trello credentials from the in-memory client. Since credentials are stored in environment variables, the agent can reconnect automatically on the next tool call.
## Development
```bash
# Install dev dependencies
pip install -e ".[dev]"
# Run tests
PYTHONPATH=src python3 -m pytest tests/ -v
# Lint
pip install ruff
ruff check src/
```
## Architecture
```
trello-plugin/
├── src/
│ └── trello_plugin/
│ ├── __init__.py # Package exports
│ ├── client.py # TrelloClient — REST API wrapper
│ └── tools.py # Hermes tool definitions
├── tests/
│ └── test_auth.py # Test suite (18 tests)
├── docs/
│ └── backend/
│ └── trello-auth-spec.md
├── pyproject.toml
└── README.md
```
+111
View File
@@ -0,0 +1,111 @@
# Trello Plugin — Authentication & Connection
**Feature:** US: Authenticate and Connect to Trello Account
**Issue:** #1
**Branch:** `feature/trello-auth`
## Overview
This feature provides the authentication layer for the Trello plugin. Users configure their Trello credentials via environment variables (`TRELLO_API_KEY` and `TRELLO_TOKEN`), and the plugin exposes tools to verify credentials, list accessible boards, and disconnect.
## Design Decisions
- **No interactive credential input** — The Hermes Agent has no UI for prompting users. Credentials are set in the profile's `.env` or `config.yaml` as environment variables, which is the standard Hermes pattern.
- **Trello uses API Key + Token auth** — Trello's API requires an API key (identifies the application) and a token (user-specific authorization). Both are required for any API call.
- **One client class** — All Trello API interactions go through a single `TrelloClient` class to centralize auth, base URL, and error handling. Future features (boards, lists, cards) will extend this same client.
## Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `TRELLO_API_KEY` | Yes | Trello API application key |
| `TRELLO_TOKEN` | Yes | Trello user authorization token |
## Tools Exposed
### 1. `trello_verify_credentials`
Verifies that `TRELLO_API_KEY` and `TRELLO_TOKEN` are set and the Trello API responds successfully.
**Parameters:** None
**Returns:**
```json
{
"success": true,
"message": "✓ Trello credentials verified successfully (authenticated as user@example.com)"
}
```
On failure:
```json
{
"success": false,
"message": "✗ Trello authentication failed: invalid key"
}
```
### 2. `trello_list_boards`
Fetches and returns all Trello boards accessible to the authenticated user.
**Parameters:** None
**Returns:**
```json
{
"success": true,
"boards": [
{"id": "abc123", "name": "My Project Board", "url": "https://trello.com/b/abc123"},
{"id": "def456", "name": "Personal Tasks", "url": "https://trello.com/b/def456"}
]
}
```
### 3. `trello_disconnect`
Clears the stored credentials from memory. Note: this does not revoke the Trello token — the user must invalidate it via Trello's settings if needed.
**Parameters:** None
**Returns:**
```json
{
"success": true,
"message": "Trello credentials cleared. Set TRELLO_API_KEY and TRELLO_TOKEN again to reconnect."
}
```
## API Contract
### Internal: `TrelloClient`
```python
class TrelloClient:
def __init__(self, api_key: str | None = None, token: str | None = None)
def verify_credentials(self) -> dict
def list_boards(self) -> list[dict]
def disconnect(self) -> dict
```
### Trello API Endpoints Used
| Purpose | Method | Endpoint | Docs |
|---------|--------|----------|------|
| Verify credentials | GET | `/1/members/me` | https://developer.atlassian.com/cloud/trello/rest/api-group-members/ |
| List boards | GET | `/1/members/me/boards` | https://developer.atlassian.com/cloud/trello/rest/api-group-boards/ |
## Error Handling
- Missing env vars → return `{"success": false, "message": "TRELLO_API_KEY and TRELLO_TOKEN must be set"}` with env var name
- Network errors → catch `requests.exceptions.RequestException` and return descriptive message
- Auth failures (401/403) → clear message about invalid credentials
- Rate limiting → Trello returns 429; surface the retry-after header
## Testing
- Unit tests with mocked HTTP responses
- Test all error paths: missing creds, invalid creds, network error, rate limit
- Test successful verification via mock that returns member info
- Test board listing with mock JSON response
+35
View File
@@ -0,0 +1,35 @@
[build-system]
requires = ["setuptools>=64"]
build-backend = "setuptools.build_meta"
[project]
name = "trello-plugin"
version = "0.1.0"
description = "Hermes Agent plugin for Trello board integration"
requires-python = ">=3.12"
license = {text = "MIT"}
dependencies = [
"requests>=2.31",
]
[project.optional-dependencies]
dev = [
"pytest>=8",
"pytest-cov>=5",
"requests-mock>=1.12",
"ruff>=0.4",
]
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP", "PL"]
+25
View File
@@ -0,0 +1,25 @@
"""
trello-plugin — Hermes Agent plugin for Trello board integration.
"""
from trello_plugin.tools import (
PLUGIN_DESCRIPTION,
PLUGIN_NAME,
PLUGIN_TOOLS,
PLUGIN_VERSION,
check_requirements,
trello_disconnect,
trello_list_boards,
trello_verify_credentials,
)
__all__ = [
"PLUGIN_NAME",
"PLUGIN_DESCRIPTION",
"PLUGIN_VERSION",
"PLUGIN_TOOLS",
"check_requirements",
"trello_verify_credentials",
"trello_list_boards",
"trello_disconnect",
]
+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"))
+96
View File
@@ -0,0 +1,96 @@
"""
Trello Plugin — Hermes Agent tool definitions.
Each function decorated with ``@tool`` registers itself as a Hermes tool
that the agent can invoke during a conversation.
"""
from __future__ import annotations
import json
import os
from typing import Any
from trello_plugin.client import TrelloClient
# ---------------------------------------------------------------------------
# Module-level client (lazily initialised so env vars can be overridden)
# ---------------------------------------------------------------------------
_client: TrelloClient | None = None
def _get_client() -> TrelloClient:
global _client # noqa: PLW0603
api_key = os.environ.get("TRELLO_API_KEY", "")
token = os.environ.get("TRELLO_TOKEN", "")
if _client is None or _client.api_key != api_key or _client.token != token:
_client = TrelloClient(api_key=api_key, token=token)
return _client
# ---------------------------------------------------------------------------
# Tool helpers
# ---------------------------------------------------------------------------
def _respond(data: dict[str, Any]) -> str:
"""Wrap a result dict in a JSON string, the Hermes tool contract."""
return json.dumps(data, indent=2)
# ---------------------------------------------------------------------------
# Tools
# ---------------------------------------------------------------------------
def trello_verify_credentials() -> str:
"""Verify that the Trello API key and token are valid.
Checks the ``TRELLO_API_KEY`` and ``TRELLO_TOKEN`` environment variables
and attempts a test call to the Trello API. Returns the authenticated
user's Trello username on success.
"""
client = _get_client()
result = client.verify_credentials()
return _respond(result)
def trello_list_boards() -> str:
"""List all Trello boards accessible to the authenticated user.
Requires ``TRELLO_API_KEY`` and ``TRELLO_TOKEN`` to be set.
Returns board id, name, URL, and whether the board is closed/starred.
"""
client = _get_client()
result = client.list_boards()
return _respond(result)
def trello_disconnect() -> str:
"""Disconnect from Trello by clearing credentials from memory.
This does NOT revoke the Trello token — invalidate it via
Trello's account settings if full revocation is desired.
"""
global _client # noqa: PLW0603
_client = None
return _respond({"success": True, "message": "Trello credentials cleared. Reconnect by calling trello_verify_credentials after setting TRELLO_API_KEY and TRELLO_TOKEN."})
# ---------------------------------------------------------------------------
# Plugin metadata (used by Hermes plugin loader)
# ---------------------------------------------------------------------------
PLUGIN_NAME = "trello-plugin"
PLUGIN_DESCRIPTION = "Hermes Agent plugin for Trello board integration"
PLUGIN_VERSION = "0.1.0"
PLUGIN_TOOLS = [
trello_verify_credentials,
trello_list_boards,
trello_disconnect,
]
PLUGIN_REQUIRES_ENV = ["TRELLO_API_KEY", "TRELLO_TOKEN"]
def check_requirements() -> bool:
"""Return True when both Trello env vars are present."""
return TrelloClient.check_requirements()
+333
View File
@@ -0,0 +1,333 @@
"""Tests for the Trello plugin — auth & connection feature."""
from __future__ import annotations
import json
import os
from collections.abc import Generator
from typing import Any
import pytest
import requests
# Tell pytest we plan to set env vars in tests
pytestmark = pytest.mark.usefixtures("clear_env")
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def clear_env() -> Generator[None, None, None]:
"""Remove Trello env vars before each test so state is predictable."""
# Use monkeypatch via request directly — fallback to os.environ
saved_key = os.environ.pop("TRELLO_API_KEY", None)
saved_token = os.environ.pop("TRELLO_TOKEN", None)
yield
if saved_key is not None:
os.environ["TRELLO_API_KEY"] = saved_key
if saved_token is not None:
os.environ["TRELLO_TOKEN"] = saved_token
def _make_session(mocker: Any) -> requests.Session:
"""Build a requests.Session with a mocked adapter."""
session = requests.Session()
adapter = mocker.get_adapter()
session.mount("https://", adapter)
return session
# ---------------------------------------------------------------------------
# TrelloClient — verify_credentials
# ---------------------------------------------------------------------------
class TestVerifyCredentials:
"""Tests for TrelloClient.verify_credentials()."""
def test_success(self, requests_mock: Any) -> None:
"""Happy path: valid creds return member info."""
from trello_plugin.client import TrelloClient
requests_mock.get(
"https://api.trello.com/1/members/me",
json={"id": "member123", "username": "testuser", "fullName": "Test User"},
)
client = TrelloClient(api_key="key", token="tok")
result = client.verify_credentials()
assert result["success"] is True
assert "testuser" in result["message"]
assert result["member"]["id"] == "member123"
assert result["member"]["username"] == "testuser"
def test_missing_credentials(self) -> None:
"""Both API key and token must be set."""
from trello_plugin.client import TrelloClient
client = TrelloClient(api_key="", token="")
result = client.verify_credentials()
assert result["success"] is False
assert "TRELLO_API_KEY" in result["message"]
def test_auth_failure_401(self, requests_mock: Any) -> None:
"""401 response surfaces as auth failure."""
from trello_plugin.client import TrelloClient
requests_mock.get(
"https://api.trello.com/1/members/me",
status_code=401,
)
client = TrelloClient(api_key="bad_key", token="bad_tok")
result = client.verify_credentials()
assert result["success"] is False
assert "401" in result["message"]
def test_forbidden_403(self, requests_mock: Any) -> None:
"""403 surfaces as access denied."""
from trello_plugin.client import TrelloClient
requests_mock.get(
"https://api.trello.com/1/members/me",
status_code=403,
)
client = TrelloClient(api_key="key", token="tok")
result = client.verify_credentials()
assert result["success"] is False
assert "403" in result["message"]
def test_rate_limit_429(self, requests_mock: Any) -> None:
"""429 surfaces as rate limit error."""
from trello_plugin.client import TrelloClient
requests_mock.get(
"https://api.trello.com/1/members/me",
status_code=429,
)
client = TrelloClient(api_key="key", token="tok")
result = client.verify_credentials()
assert result["success"] is False
assert "rate limit" in result["message"].lower()
def test_network_error(self, requests_mock: Any) -> None:
"""Connection errors surface as network failure."""
from trello_plugin.client import TrelloClient
requests_mock.get(
"https://api.trello.com/1/members/me",
exc=requests.exceptions.ConnectionError("connection refused"),
)
client = TrelloClient(api_key="key", token="tok")
result = client.verify_credentials()
assert result["success"] is False
assert "connect" in result["message"].lower()
def test_timeout(self, requests_mock: Any) -> None:
"""Timeout surfaces gracefully."""
from trello_plugin.client import TrelloClient
requests_mock.get(
"https://api.trello.com/1/members/me",
exc=requests.exceptions.Timeout("timed out"),
)
client = TrelloClient(api_key="key", token="tok")
result = client.verify_credentials()
assert result["success"] is False
assert "timed out" in result["message"].lower()
# ---------------------------------------------------------------------------
# TrelloClient — list_boards
# ---------------------------------------------------------------------------
class TestListBoards:
"""Tests for TrelloClient.list_boards()."""
def test_success(self, requests_mock: Any) -> None:
"""Happy path: returns a list of boards."""
from trello_plugin.client import TrelloClient
mock_boards = [
{"id": "b1", "name": "Board One", "url": "https://trello.com/b/b1", "closed": False, "starred": False},
{"id": "b2", "name": "Board Two", "url": "https://trello.com/b/b2", "closed": True, "starred": False},
]
requests_mock.get(
"https://api.trello.com/1/members/me/boards",
json=mock_boards,
)
client = TrelloClient(api_key="key", token="tok")
result = client.list_boards()
assert result["success"] is True
assert len(result["boards"]) == 2
assert result["boards"][0]["name"] == "Board One"
assert result["boards"][1]["closed"] is True
def test_missing_credentials(self) -> None:
"""Missing creds returns error before any network call."""
from trello_plugin.client import TrelloClient
client = TrelloClient(api_key="", token="")
result = client.list_boards()
assert result["success"] is False
assert "TRELLO_API_KEY" in result["message"]
def test_empty_boards(self, requests_mock: Any) -> None:
"""No boards returns an empty list."""
from trello_plugin.client import TrelloClient
requests_mock.get(
"https://api.trello.com/1/members/me/boards",
json=[],
)
client = TrelloClient(api_key="key", token="tok")
result = client.list_boards()
assert result["success"] is True
assert result["boards"] == []
# ---------------------------------------------------------------------------
# TrelloClient — disconnect
# ---------------------------------------------------------------------------
class TestDisconnect:
"""Tests for TrelloClient.disconnect()."""
def test_disconnect_clears_credentials(self) -> None:
"""Disconnect clears the in-memory credentials."""
from trello_plugin.client import TrelloClient
client = TrelloClient(api_key="key", token="tok")
result = client.disconnect()
assert result["success"] is True
assert "cleared" in result["message"].lower()
# Subsequent calls should fail
verify_result = client.verify_credentials()
assert verify_result["success"] is False
assert "TRELLO_API_KEY" in verify_result["message"]
# ---------------------------------------------------------------------------
# check_requirements
# ---------------------------------------------------------------------------
class TestCheckRequirements:
"""Tests for TrelloClient.check_requirements()."""
def test_returns_false_when_missing(self) -> None:
"""Returns False when env vars are not set."""
from trello_plugin.client import TrelloClient
assert TrelloClient.check_requirements() is False
def test_returns_true_when_set(self) -> None:
"""Returns True when both env vars are set."""
from trello_plugin.client import TrelloClient
os.environ["TRELLO_API_KEY"] = "test-key"
os.environ["TRELLO_TOKEN"] = "test-token"
assert TrelloClient.check_requirements() is True
# ---------------------------------------------------------------------------
# Tool functions
# ---------------------------------------------------------------------------
class TestToolFunctions:
"""Tests for the Hermes tool wrapper functions."""
def test_trello_verify_credentials_tool(self, requests_mock: Any) -> None:
"""Tool returns valid JSON string on success."""
from trello_plugin.tools import trello_verify_credentials
os.environ["TRELLO_API_KEY"] = "test-key"
os.environ["TRELLO_TOKEN"] = "test-token"
requests_mock.get(
"https://api.trello.com/1/members/me",
json={"id": "m1", "username": "bot", "fullName": "Bot User"},
)
result = json.loads(trello_verify_credentials())
assert result["success"] is True
assert result["member"]["username"] == "bot"
def test_trello_list_boards_tool(self, requests_mock: Any) -> None:
"""Tool returns valid JSON string on success."""
from trello_plugin.tools import trello_list_boards
os.environ["TRELLO_API_KEY"] = "test-key"
os.environ["TRELLO_TOKEN"] = "test-token"
requests_mock.get(
"https://api.trello.com/1/members/me/boards",
json=[{"id": "b1", "name": "Test", "url": "https://trello.com/b/b1", "closed": False, "starred": False}],
)
result = json.loads(trello_list_boards())
assert result["success"] is True
assert len(result["boards"]) == 1
def test_trello_disconnect_tool(self, requests_mock: Any) -> None:
"""Tool clears credentials and returns success."""
from trello_plugin.tools import trello_disconnect, trello_verify_credentials
os.environ["TRELLO_API_KEY"] = "test-key"
os.environ["TRELLO_TOKEN"] = "test-token"
# First verify works
requests_mock.get(
"https://api.trello.com/1/members/me",
json={"id": "m1", "username": "bot", "fullName": "Bot User"},
)
verify = json.loads(trello_verify_credentials())
assert verify["success"] is True
# Then disconnect
disc = json.loads(trello_disconnect())
assert disc["success"] is True
# With env vars still set, verify should reconnect successfully
# (disconnect clears the in-memory client; env vars persist)
verify2 = json.loads(trello_verify_credentials())
assert verify2["success"] is True
assert verify2["member"]["username"] == "bot"
def test_check_requirements_tool(self, requests_mock: Any) -> None:
"""check_requirements returns False when env not set."""
from trello_plugin.tools import check_requirements
assert check_requirements() is False
os.environ["TRELLO_API_KEY"] = "k"
os.environ["TRELLO_TOKEN"] = "t"
assert check_requirements() is True
# ---------------------------------------------------------------------------
# Plugin metadata
# ---------------------------------------------------------------------------
class TestPluginMetadata:
"""Tests for the plugin metadata constants."""
def test_plugin_constants(self) -> None:
from trello_plugin.tools import (
PLUGIN_DESCRIPTION,
PLUGIN_NAME,
PLUGIN_TOOLS,
PLUGIN_VERSION,
)
assert PLUGIN_NAME == "trello-plugin"
assert isinstance(PLUGIN_DESCRIPTION, str)
assert isinstance(PLUGIN_VERSION, str)
assert len(PLUGIN_TOOLS) == 3
assert all(callable(t) for t in PLUGIN_TOOLS)