Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f63cff7bd | ||
|
|
0131b25c8b | ||
|
|
5dd1eb6aa0 | ||
|
|
770533cc1e | ||
|
|
f9282fba09 | ||
|
|
4d1f37a9f4 | ||
|
|
a287091e18 | ||
|
|
65fe867cf5 | ||
|
|
43acd5bfbf | ||
|
|
6dac5d225e |
@@ -0,0 +1,5 @@
|
||||
__pycache__/
|
||||
*.egg-info/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.pytest_cache/
|
||||
@@ -1,3 +1,95 @@
|
||||
# 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
|
||||
```
|
||||
@@ -0,0 +1,119 @@
|
||||
# Trello Plugin — Authentication & Connection
|
||||
|
||||
**Feature:** US: Authenticate and Connect to Trello Account
|
||||
**Issue:** #1
|
||||
**Branch:** `feature/trello-auth`
|
||||
**Doc:** 001
|
||||
|
||||
## 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. Since credentials are stored in environment variables, the next tool call will automatically re-read them and reconnect. To fully disconnect, also unset ``TRELLO_API_KEY`` and ``TRELLO_TOKEN`` from the Hermes profile config.
|
||||
|
||||
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 from memory. Next tool call will re-read TRELLO_API_KEY and TRELLO_TOKEN from environment and reconnect automatically."
|
||||
}
|
||||
```
|
||||
|
||||
## API Contract
|
||||
|
||||
### Internal: `TrelloClient`
|
||||
|
||||
```python
|
||||
class TrelloClient:
|
||||
api_key: str
|
||||
token: str
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
The client uses `TypedDict` response contracts (`SuccessResponse`/`ErrorResponse`) with a `TypeGuard` helper `_is_success()` for type-safe narrowing. Internal helpers include `_request()`, `_get()`, `_post()`, `_put()`, `_check_credentials()`, and `_handle_http_error()`.
|
||||
|
||||
### 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
|
||||
@@ -0,0 +1,117 @@
|
||||
# Trello Plugin — Manage Trello Boards
|
||||
|
||||
**Feature:** US: Manage Trello Boards
|
||||
**Issue:** #2
|
||||
**Branch:** `feature/manage-boards`
|
||||
**Doc:** 002
|
||||
|
||||
## Overview
|
||||
|
||||
Extends the Trello plugin with board management capabilities: create, rename, close/archive, open, and view details of Trello boards.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- **Extends existing TrelloClient** — All board operations go through the same `TrelloClient` class from the auth feature.
|
||||
- **Board selection by ID** — The Trello API identifies boards by ID. The tool accepts both ID and name (client resolves name to ID).
|
||||
- **Resolves board name to ID** — When a user provides a board name instead of ID, the client fetches all boards and matches by name.
|
||||
|
||||
## Tools
|
||||
|
||||
### `trello_create_board`
|
||||
Create a new Trello board.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `name` | string | Yes | Board name |
|
||||
| `default_lists` | bool | No | Whether to create the default lists (default: true) |
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"board": {"id": "abc123", "name": "My Board", "url": "https://trello.com/b/abc123"}
|
||||
}
|
||||
```
|
||||
|
||||
### `trello_rename_board`
|
||||
|
||||
Rename an existing board.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `board_id` | string | Yes | Board ID or name |
|
||||
| `name` | string | Yes | New board name |
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{"success": true, "board": {"id": "abc123", "name": "New Name"}}
|
||||
```
|
||||
|
||||
### `trello_archive_board`
|
||||
|
||||
Close/archive a board.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `board_id` | string | Yes | Board ID or name |
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{"success": true, "message": "Board 'My Board' archived."}
|
||||
```
|
||||
|
||||
### `trello_open_board`
|
||||
|
||||
Re-open a closed/archived board.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `board_id` | string | Yes | Board ID or name |
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{"success": true, "message": "Board 'My Board' opened."}
|
||||
```
|
||||
|
||||
### `trello_board_details`
|
||||
|
||||
View details of a specific board, including its lists and members.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `board_id` | string | Yes | Board ID or name |
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"board": {
|
||||
"id": "abc123",
|
||||
"name": "My Board",
|
||||
"url": "https://trello.com/b/abc123",
|
||||
"desc": "",
|
||||
"closed": false,
|
||||
"starred": false,
|
||||
"lists": [{"id": "l1", "name": "To Do"}, {"id": "l2", "name": "In Progress"}],
|
||||
"members": [{"id": "m1", "username": "user1", "full_name": "User One"}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Trello API Endpoints
|
||||
|
||||
| Purpose | Method | Endpoint |
|
||||
|---------|--------|----------|
|
||||
| Create board | POST | `/1/boards/` |
|
||||
| Update board | PUT | `/1/boards/{id}` |
|
||||
| Get board details | GET | `/1/boards/{id}` (with lists and members fields) |
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Board not found → clear message suggesting `trello_list_boards` to find the ID
|
||||
- Validation errors → surfaced directly from Trello API
|
||||
@@ -0,0 +1,89 @@
|
||||
# Trello Plugin — Manage Trello Lists
|
||||
|
||||
**Feature:** US: Manage Trello Lists
|
||||
**Issue:** #3
|
||||
**Branch:** `feature/manage-lists`
|
||||
**Doc:** 003
|
||||
|
||||
## Overview
|
||||
|
||||
Extends the Trello plugin with list management capabilities: create, rename, archive, and reposition lists on a Trello board.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- **Lists require a board context** — Creating a list needs a board ID. Other operations (rename, archive, move) use the list's Trello ID which is globally unique.
|
||||
- **Position parameter** — Uses Trello's `pos` field which accepts `"top"`, `"bottom"`, or a positive number.
|
||||
|
||||
## Tools
|
||||
|
||||
### `trello_create_list`
|
||||
|
||||
Create a new list on a board.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `name` | string | Yes | List name |
|
||||
| `board_id` | string | Yes | Board ID or name |
|
||||
| `pos` | string | No | Position: `"top"`, `"bottom"`, or number (default: `"bottom"`) |
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{"success": true, "list": {"id": "l1", "name": "My List", "id_board": "b1"}}
|
||||
```
|
||||
|
||||
### `trello_rename_list`
|
||||
|
||||
Rename an existing list.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `list_id` | string | Yes | List ID |
|
||||
| `name` | string | Yes | New name |
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{"success": true, "list": {"id": "l1", "name": "Renamed List"}}
|
||||
```
|
||||
|
||||
### `trello_archive_list`
|
||||
|
||||
Archive a list.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `list_id` | string | Yes | List ID |
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{"success": true, "message": "List 'My List' archived."}
|
||||
```
|
||||
|
||||
### `trello_move_list`
|
||||
|
||||
Move a list to a different position.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `list_id` | string | Yes | List ID |
|
||||
| `pos` | string | Yes | Position: `"top"`, `"bottom"`, or a number |
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{"success": true, "message": "List 'My List' moved."}
|
||||
```
|
||||
|
||||
## Trello API Endpoints
|
||||
|
||||
| Purpose | Method | Endpoint |
|
||||
|---------|--------|----------|
|
||||
| Create list | POST | `/1/lists` |
|
||||
| Update list (rename, archive, move) | PUT | `/1/lists/{id}` |
|
||||
|
||||
## Error Handling
|
||||
|
||||
- List not found → clear error message
|
||||
- Board not found when creating → propagate board lookup error
|
||||
@@ -0,0 +1,163 @@
|
||||
# Trello Plugin — Manage Trello Cards
|
||||
|
||||
**Feature:** US: Manage Trello Cards
|
||||
**Issue:** #4
|
||||
**Branch:** `feature/manage-cards`
|
||||
**Doc:** 004
|
||||
|
||||
## Overview
|
||||
|
||||
Extends the Trello plugin with full card management capabilities: create, view, update, move, archive, assign members, comments, and checklist management.
|
||||
|
||||
## Tools
|
||||
|
||||
### `trello_create_card`
|
||||
Create a new card on a selected list.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `name` | string | Yes | Card title |
|
||||
| `list_id` | string | Yes | List ID |
|
||||
| `desc` | string | No | Card description |
|
||||
| `due` | string | No | Due date (ISO 8601) |
|
||||
|
||||
**Returns:** Card ID, name, URL, and short URL.
|
||||
|
||||
### `trello_card_details`
|
||||
View detailed information about a card.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `card_id` | string | Yes | Card ID |
|
||||
|
||||
**Returns:** Card details including title, description, due date, members, checklists, comments, URL.
|
||||
|
||||
### `trello_update_card`
|
||||
Update a card's core attributes (title, description, due date).
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `card_id` | string | Yes | Card ID |
|
||||
| `name` | string | No | New title |
|
||||
| `desc` | string | No | New description |
|
||||
| `due` | string | No | New due date (ISO 8601) |
|
||||
|
||||
**Returns:** Updated card details.
|
||||
|
||||
### `trello_move_card`
|
||||
Move a card to a different list.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `card_id` | string | Yes | Card ID |
|
||||
| `list_id` | string | Yes | Target list ID |
|
||||
|
||||
**Returns:** Success message with new list location.
|
||||
|
||||
### `trello_archive_card`
|
||||
Archive a card.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `card_id` | string | Yes | Card ID |
|
||||
|
||||
**Returns:** Success message.
|
||||
|
||||
### `trello_assign_member`
|
||||
Add a member to a card.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `card_id` | string | Yes | Card ID |
|
||||
| `member_id` | string | Yes | Trello member ID |
|
||||
|
||||
**Returns:** Success message.
|
||||
|
||||
### `trello_remove_member`
|
||||
Remove a member from a card.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `card_id` | string | Yes | Card ID |
|
||||
| `member_id` | string | Yes | Trello member ID |
|
||||
|
||||
**Returns:** Success message.
|
||||
|
||||
### `trello_add_comment`
|
||||
Add a comment to a card.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `card_id` | string | Yes | Card ID |
|
||||
| `text` | string | Yes | Comment text |
|
||||
|
||||
**Returns:** Comment ID and success.
|
||||
|
||||
### `trello_delete_comment`
|
||||
Delete a comment from a card.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `card_id` | string | Yes | Card ID |
|
||||
| `comment_id` | string | Yes | Comment/action ID |
|
||||
|
||||
**Returns:** Success message.
|
||||
|
||||
### `trello_add_checklist_item`
|
||||
Add a checklist item to a card.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `card_id` | string | Yes | Card ID |
|
||||
| `name` | string | Yes | Checklist item text |
|
||||
| `checklist_id` | string | No | Specific checklist ID (if card has multiple) |
|
||||
|
||||
**Returns:** Checklist item details.
|
||||
|
||||
### `trello_toggle_checklist_item`
|
||||
Mark a checklist item as complete or incomplete.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `card_id` | string | Yes | Card ID |
|
||||
| `item_id` | string | Yes | Checklist item ID |
|
||||
| `checked` | bool | Yes | True = complete, False = incomplete |
|
||||
|
||||
**Returns:** Updated item state.
|
||||
|
||||
### `trello_delete_checklist_item`
|
||||
Remove a checklist item.
|
||||
|
||||
**Parameters:**
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `card_id` | string | Yes | Card ID |
|
||||
| `item_id` | string | Yes | Checklist item ID |
|
||||
|
||||
**Returns:** Success message.
|
||||
|
||||
## Trello API Endpoints
|
||||
|
||||
| Purpose | Method | Endpoint |
|
||||
|---------|--------|----------|
|
||||
| Create card | POST | `/1/cards` |
|
||||
| Get card | GET | `/1/cards/{id}` |
|
||||
| Update card | PUT | `/1/cards/{id}` |
|
||||
| Add member | POST | `/1/cards/{id}/idMembers` |
|
||||
| Remove member | DELETE | `/1/cards/{id}/idMembers/{memberId}` |
|
||||
| Add comment | POST | `/1/cards/{id}/actions/comments` |
|
||||
| Delete comment | DELETE | `/1/cards/{id}/actions/{commentId}/comments` |
|
||||
| Add checklist item | POST | `/1/cards/{id}/checklistItems` |
|
||||
| Update checklist item | PUT | `/1/cards/{id}/checklistItem/{itemId}/state` |
|
||||
| Delete checklist item | DELETE | `/1/cards/{id}/checklistItems/{itemId}`
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
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_add_checklist_item,
|
||||
trello_add_comment,
|
||||
trello_archive_board,
|
||||
trello_archive_card,
|
||||
trello_archive_list,
|
||||
trello_assign_member,
|
||||
trello_board_details,
|
||||
trello_card_details,
|
||||
trello_create_board,
|
||||
trello_create_card,
|
||||
trello_create_list,
|
||||
trello_delete_checklist_item,
|
||||
trello_delete_comment,
|
||||
trello_disconnect,
|
||||
trello_list_boards,
|
||||
trello_move_card,
|
||||
trello_move_list,
|
||||
trello_open_board,
|
||||
trello_remove_member,
|
||||
trello_rename_board,
|
||||
trello_rename_list,
|
||||
trello_toggle_checklist_item,
|
||||
trello_update_card,
|
||||
trello_verify_credentials,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"PLUGIN_NAME",
|
||||
"PLUGIN_DESCRIPTION",
|
||||
"PLUGIN_VERSION",
|
||||
"PLUGIN_TOOLS",
|
||||
"check_requirements",
|
||||
"trello_verify_credentials",
|
||||
"trello_list_boards",
|
||||
"trello_disconnect",
|
||||
"trello_create_board",
|
||||
"trello_rename_board",
|
||||
"trello_archive_board",
|
||||
"trello_open_board",
|
||||
"trello_board_details",
|
||||
"trello_create_list",
|
||||
"trello_rename_list",
|
||||
"trello_archive_list",
|
||||
"trello_move_list",
|
||||
"trello_create_card",
|
||||
"trello_card_details",
|
||||
"trello_update_card",
|
||||
"trello_move_card",
|
||||
"trello_archive_card",
|
||||
"trello_assign_member",
|
||||
"trello_remove_member",
|
||||
"trello_add_comment",
|
||||
"trello_delete_comment",
|
||||
"trello_add_checklist_item",
|
||||
"trello_toggle_checklist_item",
|
||||
"trello_delete_checklist_item",
|
||||
]
|
||||
@@ -0,0 +1,975 @@
|
||||
"""
|
||||
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, TypeGuard, TypedDict
|
||||
|
||||
import requests
|
||||
|
||||
# Base URL for the Trello REST API v1.
|
||||
# See https://developer.atlassian.com/cloud/trello/rest
|
||||
TRELLO_API_BASE = "https://api.trello.com/1"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Typed response contracts
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SuccessResponse(TypedDict):
|
||||
"""A successful API response carrying data."""
|
||||
|
||||
success: bool
|
||||
data: dict[str, Any]
|
||||
|
||||
|
||||
class ErrorResponse(TypedDict):
|
||||
"""An unsuccessful API response with a human-readable message."""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
|
||||
|
||||
ApiResponse = SuccessResponse | ErrorResponse
|
||||
|
||||
|
||||
def _is_success(resp: ApiResponse) -> TypeGuard[SuccessResponse]:
|
||||
"""Narrow an ApiResponse union to its success branch."""
|
||||
return bool(resp.get("success", False)) # noqa: FBT003
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Client
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
api_key: str
|
||||
token: str
|
||||
_session: requests.Session
|
||||
|
||||
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 _check_credentials(self) -> ErrorResponse | None:
|
||||
"""Return an ErrorResponse if credentials are missing, else None."""
|
||||
if not self.api_key or not self.token:
|
||||
return ErrorResponse(
|
||||
success=False,
|
||||
message="TRELLO_API_KEY and TRELLO_TOKEN must both be set as environment variables.",
|
||||
)
|
||||
return None
|
||||
|
||||
def _handle_http_error(self, exc: requests.exceptions.HTTPError) -> ErrorResponse:
|
||||
"""Map known HTTP status codes to user-facing error messages."""
|
||||
status = exc.response.status_code if exc.response is not None else 0
|
||||
messages: dict[int, str] = {
|
||||
401: "Trello authentication failed (401). Check your TRELLO_API_KEY and TRELLO_TOKEN.",
|
||||
403: "Trello access denied (403). Your token may not have the required scopes.",
|
||||
429: "Trello rate limit exceeded. Try again later.",
|
||||
}
|
||||
message = messages.get(
|
||||
status, f"Trello API error ({status}): {exc}"
|
||||
)
|
||||
return ErrorResponse(success=False, message=message)
|
||||
|
||||
def _request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
params: dict[str, Any] | None = None,
|
||||
json_body: dict[str, Any] | None = None,
|
||||
) -> ApiResponse:
|
||||
"""Make an authenticated HTTP request and return the JSON response.
|
||||
|
||||
Handles credential checks, HTTP errors, network errors, and timeouts.
|
||||
"""
|
||||
cred_error = self._check_credentials()
|
||||
if cred_error is not None:
|
||||
return cred_error
|
||||
|
||||
url = f"{TRELLO_API_BASE}{path}"
|
||||
merged_params = self._auth_params()
|
||||
if params:
|
||||
merged_params.update(params)
|
||||
|
||||
try:
|
||||
resp = self._session.request(
|
||||
method, url, params=merged_params, json=json_body, timeout=15
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data: dict[str, Any] = resp.json()
|
||||
return SuccessResponse(success=True, data=data)
|
||||
except requests.exceptions.HTTPError as exc:
|
||||
return self._handle_http_error(exc)
|
||||
except requests.exceptions.ConnectionError:
|
||||
return ErrorResponse(
|
||||
success=False,
|
||||
message="Could not connect to Trello API. Check your network connection.",
|
||||
)
|
||||
except requests.exceptions.Timeout:
|
||||
return ErrorResponse(
|
||||
success=False,
|
||||
message="Trello API request timed out. Try again later.",
|
||||
)
|
||||
except requests.exceptions.RequestException as exc:
|
||||
return ErrorResponse(
|
||||
success=False, message=f"Trello API request failed: {exc}"
|
||||
)
|
||||
|
||||
def _get(
|
||||
self, path: str, params: dict[str, Any] | None = None
|
||||
) -> ApiResponse:
|
||||
"""Make an authenticated GET request."""
|
||||
return self._request("GET", path, params=params)
|
||||
|
||||
def _post(
|
||||
self, path: str, json_body: dict[str, Any] | None = None
|
||||
) -> ApiResponse:
|
||||
"""Make an authenticated POST request."""
|
||||
return self._request("POST", path, json_body=json_body)
|
||||
|
||||
def _put(
|
||||
self, path: str, json_body: dict[str, Any] | None = None
|
||||
) -> ApiResponse:
|
||||
"""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
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
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 _is_success(result):
|
||||
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 _is_success(result):
|
||||
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.
|
||||
|
||||
Since credentials are stored in environment variables, the next tool call
|
||||
will automatically re-read them and reconnect. To fully disconnect, also
|
||||
unset TRELLO_API_KEY and TRELLO_TOKEN from the Hermes profile config.
|
||||
|
||||
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 from memory. "
|
||||
"Next tool call will re-read TRELLO_API_KEY and TRELLO_TOKEN "
|
||||
"from environment and reconnect automatically."
|
||||
),
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API — Board Management
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _resolve_board_id(self, board_id: str) -> dict[str, Any]:
|
||||
"""Resolve a board name or ID to a Trello board ID.
|
||||
|
||||
Returns board info dict with at least an ``id`` key on success,
|
||||
or an error dict with ``success: False``.
|
||||
"""
|
||||
# Try as ID first (Trello IDs are 8-24 char hex strings)
|
||||
result = self._get(f"/boards/{board_id}", params={"fields": "id,name,url"})
|
||||
if _is_success(result):
|
||||
board = result["data"]
|
||||
return {"id": board["id"], "name": board.get("name", ""), "url": board.get("url", "")}
|
||||
|
||||
# If the error is about missing credentials, propagate that directly
|
||||
error_msg = result.get("message", "")
|
||||
if "TRELLO_API_KEY" in error_msg:
|
||||
return {"success": False, "message": error_msg}
|
||||
|
||||
# Try resolving by name
|
||||
boards_result = self.list_boards()
|
||||
if not _is_success(boards_result): # type: ignore[arg-type]
|
||||
return {
|
||||
"success": False,
|
||||
"message": (
|
||||
f"Board '{board_id}' not found. "
|
||||
"Use trello_list_boards to see available boards."
|
||||
),
|
||||
}
|
||||
|
||||
matches = [b for b in boards_result["boards"] if b["name"] == board_id] # type: ignore[typeddict-item]
|
||||
if len(matches) == 1:
|
||||
return dict(matches[0]) # already has id, name, url
|
||||
|
||||
if len(matches) > 1:
|
||||
return {
|
||||
"success": False,
|
||||
"message": (
|
||||
f"Multiple boards named '{board_id}' found. "
|
||||
"Use the board ID instead of the name."
|
||||
),
|
||||
}
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"message": (
|
||||
f"Board '{board_id}' not found. "
|
||||
"Use trello_list_boards to see available boards."
|
||||
),
|
||||
}
|
||||
|
||||
def create_board(
|
||||
self, name: str, default_lists: bool = True
|
||||
) -> dict[str, Any]:
|
||||
"""Create a new Trello board.
|
||||
|
||||
Args:
|
||||
name: The name for the new board.
|
||||
default_lists: Whether to create the default lists (default: True).
|
||||
|
||||
Returns:
|
||||
dict with board details on success, or an error dict.
|
||||
"""
|
||||
body: dict[str, Any] = {
|
||||
"name": name,
|
||||
"defaultLists": default_lists,
|
||||
}
|
||||
result = self._post("/boards", json_body=body)
|
||||
if _is_success(result):
|
||||
board = result["data"]
|
||||
return {
|
||||
"success": True,
|
||||
"board": {
|
||||
"id": board.get("id"),
|
||||
"name": board.get("name"),
|
||||
"url": board.get("url"),
|
||||
},
|
||||
}
|
||||
return {
|
||||
"success": False,
|
||||
"message": result.get("message", "Failed to create board."),
|
||||
}
|
||||
|
||||
def rename_board(self, board_id: str, name: str) -> dict[str, Any]:
|
||||
"""Rename an existing Trello board.
|
||||
|
||||
Args:
|
||||
board_id: Board ID or name.
|
||||
name: The new name for the board.
|
||||
|
||||
Returns:
|
||||
dict with board details on success, or an error dict.
|
||||
"""
|
||||
resolved = self._resolve_board_id(board_id)
|
||||
if "success" in resolved and resolved["success"] is False:
|
||||
return resolved # type: ignore[typeddict-item]
|
||||
|
||||
trello_id = resolved["id"]
|
||||
result = self._put(
|
||||
f"/boards/{trello_id}", json_body={"name": name}
|
||||
)
|
||||
if _is_success(result):
|
||||
board = result["data"]
|
||||
return {
|
||||
"success": True,
|
||||
"board": {
|
||||
"id": board.get("id"),
|
||||
"name": board.get("name"),
|
||||
"url": board.get("url"),
|
||||
},
|
||||
}
|
||||
return {
|
||||
"success": False,
|
||||
"message": result.get("message", "Failed to rename board."),
|
||||
}
|
||||
|
||||
def archive_board(self, board_id: str) -> dict[str, Any]:
|
||||
"""Close/archive a Trello board.
|
||||
|
||||
Args:
|
||||
board_id: Board ID or name.
|
||||
|
||||
Returns:
|
||||
dict with success message or error.
|
||||
"""
|
||||
resolved = self._resolve_board_id(board_id)
|
||||
if "success" in resolved and resolved["success"] is False:
|
||||
return resolved # type: ignore[typeddict-item]
|
||||
|
||||
trello_id = resolved["id"]
|
||||
board_name = resolved.get("name", trello_id)
|
||||
result = self._put(
|
||||
f"/boards/{trello_id}", json_body={"closed": True}
|
||||
)
|
||||
if _is_success(result):
|
||||
return {"success": True, "message": f"Board '{board_name}' archived."}
|
||||
return {
|
||||
"success": False,
|
||||
"message": result.get("message", "Failed to archive board."),
|
||||
}
|
||||
|
||||
def open_board(self, board_id: str) -> dict[str, Any]:
|
||||
"""Re-open a closed/archived Trello board.
|
||||
|
||||
Args:
|
||||
board_id: Board ID or name.
|
||||
|
||||
Returns:
|
||||
dict with success message or error.
|
||||
"""
|
||||
resolved = self._resolve_board_id(board_id)
|
||||
if "success" in resolved and resolved["success"] is False:
|
||||
return resolved # type: ignore[typeddict-item]
|
||||
|
||||
trello_id = resolved["id"]
|
||||
board_name = resolved.get("name", trello_id)
|
||||
result = self._put(
|
||||
f"/boards/{trello_id}", json_body={"closed": False}
|
||||
)
|
||||
if _is_success(result):
|
||||
return {"success": True, "message": f"Board '{board_name}' opened."}
|
||||
return {
|
||||
"success": False,
|
||||
"message": result.get("message", "Failed to open board."),
|
||||
}
|
||||
|
||||
def board_details(self, board_id: str) -> dict[str, Any]:
|
||||
"""Fetch detailed information about a Trello board.
|
||||
|
||||
Returns the board's lists, members, and a direct URL.
|
||||
|
||||
Args:
|
||||
board_id: Board ID or name.
|
||||
|
||||
Returns:
|
||||
dict with board details including lists and members on success.
|
||||
"""
|
||||
resolved = self._resolve_board_id(board_id)
|
||||
if "success" in resolved and resolved["success"] is False:
|
||||
return resolved # type: ignore[typeddict-item]
|
||||
|
||||
trello_id = resolved["id"]
|
||||
result = self._get(
|
||||
f"/boards/{trello_id}",
|
||||
params={
|
||||
"fields": "id,name,desc,url,closed,starred",
|
||||
"lists": "all",
|
||||
"members": "all",
|
||||
"members_fields": "id,username,fullName",
|
||||
},
|
||||
)
|
||||
if not _is_success(result):
|
||||
return {
|
||||
"success": False,
|
||||
"message": result.get("message", "Failed to fetch board details."),
|
||||
}
|
||||
|
||||
board = result["data"]
|
||||
return {
|
||||
"success": True,
|
||||
"board": {
|
||||
"id": board.get("id"),
|
||||
"name": board.get("name"),
|
||||
"url": board.get("url"),
|
||||
"desc": board.get("desc", ""),
|
||||
"closed": board.get("closed", False),
|
||||
"starred": board.get("starred", False),
|
||||
"lists": [
|
||||
{"id": lst.get("id"), "name": lst.get("name")}
|
||||
for lst in board.get("lists", [])
|
||||
],
|
||||
"members": [
|
||||
{
|
||||
"id": m.get("id"),
|
||||
"username": m.get("username"),
|
||||
"full_name": m.get("fullName"),
|
||||
}
|
||||
for m in board.get("members", [])
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API — List Management
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def create_list(self, name: str, board_id: str, pos: str = "bottom") -> dict[str, Any]:
|
||||
"""Create a new list on a Trello board.
|
||||
|
||||
Args:
|
||||
name: The name for the new list.
|
||||
board_id: Board ID or name.
|
||||
pos: Position — ``"top"``, ``"bottom"``, or a number.
|
||||
|
||||
Returns:
|
||||
dict with list details on success, or an error dict.
|
||||
"""
|
||||
resolved = self._resolve_board_id(board_id)
|
||||
if "success" in resolved and resolved["success"] is False:
|
||||
return resolved # type: ignore[typeddict-item]
|
||||
|
||||
body: dict[str, Any] = {
|
||||
"name": name,
|
||||
"idBoard": resolved["id"],
|
||||
"pos": pos,
|
||||
}
|
||||
result = self._post("/lists", json_body=body)
|
||||
if _is_success(result):
|
||||
lst = result["data"]
|
||||
return {
|
||||
"success": True,
|
||||
"list": {
|
||||
"id": lst.get("id"),
|
||||
"name": lst.get("name"),
|
||||
"id_board": lst.get("idBoard"),
|
||||
},
|
||||
}
|
||||
return {
|
||||
"success": False,
|
||||
"message": result.get("message", "Failed to create list."),
|
||||
}
|
||||
|
||||
def rename_list(self, list_id: str, name: str) -> dict[str, Any]:
|
||||
"""Rename an existing list.
|
||||
|
||||
Args:
|
||||
list_id: Trello list ID.
|
||||
name: The new name.
|
||||
|
||||
Returns:
|
||||
dict with list details on success, or an error dict.
|
||||
"""
|
||||
result = self._put(f"/lists/{list_id}", json_body={"name": name})
|
||||
if _is_success(result):
|
||||
lst = result["data"]
|
||||
return {
|
||||
"success": True,
|
||||
"list": {
|
||||
"id": lst.get("id"),
|
||||
"name": lst.get("name"),
|
||||
},
|
||||
}
|
||||
return {
|
||||
"success": False,
|
||||
"message": result.get("message", "Failed to rename list."),
|
||||
}
|
||||
|
||||
def archive_list(self, list_id: str) -> dict[str, Any]:
|
||||
"""Archive a list.
|
||||
|
||||
Args:
|
||||
list_id: Trello list ID.
|
||||
|
||||
Returns:
|
||||
dict with success message or error.
|
||||
"""
|
||||
result = self._put(f"/lists/{list_id}", json_body={"closed": True})
|
||||
if _is_success(result):
|
||||
lst = result["data"]
|
||||
list_name = lst.get("name", list_id)
|
||||
return {"success": True, "message": f"List '{list_name}' archived."}
|
||||
return {
|
||||
"success": False,
|
||||
"message": result.get("message", "Failed to archive list."),
|
||||
}
|
||||
|
||||
def move_list(self, list_id: str, pos: str = "bottom") -> dict[str, Any]:
|
||||
"""Move a list to a different position on the board.
|
||||
|
||||
Args:
|
||||
list_id: Trello list ID.
|
||||
pos: Position — ``"top"``, ``"bottom"``, or a number.
|
||||
|
||||
Returns:
|
||||
dict with success message or error.
|
||||
"""
|
||||
result = self._put(f"/lists/{list_id}", json_body={"pos": pos})
|
||||
if _is_success(result):
|
||||
lst = result["data"]
|
||||
list_name = lst.get("name", list_id)
|
||||
return {"success": True, "message": f"List '{list_name}' moved."}
|
||||
return {
|
||||
"success": False,
|
||||
"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."""
|
||||
return bool(os.environ.get("TRELLO_API_KEY")) and bool(
|
||||
os.environ.get("TRELLO_TOKEN")
|
||||
)
|
||||
@@ -0,0 +1,554 @@
|
||||
"""
|
||||
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 from memory. Next tool call will re-read TRELLO_API_KEY and TRELLO_TOKEN from environment and reconnect automatically."})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tools — Board Management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def trello_create_board(name: str, default_lists: bool = True) -> str: # noqa: FBT001, FBT002
|
||||
"""Create a new Trello board.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The name for the new board.
|
||||
default_lists : bool, optional
|
||||
Whether to create default lists (default: True).
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with board id, name, and URL on success.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.create_board(name=name, default_lists=default_lists)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_rename_board(board_id: str, name: str) -> str:
|
||||
"""Rename an existing Trello board.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
board_id : str
|
||||
Board ID or name.
|
||||
name : str
|
||||
The new name for the board.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with updated board details on success.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.rename_board(board_id=board_id, name=name)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_archive_board(board_id: str) -> str:
|
||||
"""Close/archive a Trello board.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
board_id : str
|
||||
Board ID or name.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with success message.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.archive_board(board_id=board_id)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_open_board(board_id: str) -> str:
|
||||
"""Re-open a closed/archived Trello board.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
board_id : str
|
||||
Board ID or name.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with success message.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.open_board(board_id=board_id)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_board_details(board_id: str) -> str:
|
||||
"""View details of a Trello board, including its lists and members.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
board_id : str
|
||||
Board ID or name.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with board details including lists and members.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.board_details(board_id=board_id)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tools — List Management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def trello_create_list(name: str, board_id: str, pos: str = "bottom") -> str:
|
||||
"""Create a new list on a Trello board.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The name for the new list.
|
||||
board_id : str
|
||||
Board ID or name.
|
||||
pos : str, optional
|
||||
Position: ``"top"``, ``"bottom"``, or a number (default: ``"bottom"``).
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with list details on success.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.create_list(name=name, board_id=board_id, pos=pos)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_rename_list(list_id: str, name: str) -> str:
|
||||
"""Rename an existing list.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
list_id : str
|
||||
Trello list ID.
|
||||
name : str
|
||||
The new name.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with updated list details on success.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.rename_list(list_id=list_id, name=name)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_archive_list(list_id: str) -> str:
|
||||
"""Archive a list.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
list_id : str
|
||||
Trello list ID.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with success message.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.archive_list(list_id=list_id)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_move_list(list_id: str, pos: str = "bottom") -> str:
|
||||
"""Move a list to a different position on the board.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
list_id : str
|
||||
Trello list ID.
|
||||
pos : str, optional
|
||||
Position: ``"top"``, ``"bottom"``, or a number (default: ``"bottom"``).
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with success message.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.move_list(list_id=list_id, pos=pos)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tools — Card Management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def trello_create_card(name: str, list_id: str, desc: str = "", due: str | None = None) -> str:
|
||||
"""Create a new card on a Trello list.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
Card title.
|
||||
list_id : str
|
||||
List ID.
|
||||
desc : str, optional
|
||||
Card description.
|
||||
due : str, optional
|
||||
Due date in ISO 8601 format.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with card details.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.create_card(name=name, list_id=list_id, desc=desc, due=due)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_card_details(card_id: str) -> str:
|
||||
"""View detailed information about a Trello card.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
card_id : str
|
||||
Card ID.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with full card details including members, checklists, and comments.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.card_details(card_id=card_id)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_update_card(card_id: str, name: str | None = None, desc: str | None = None, due: str | None = None) -> str:
|
||||
"""Update a card's core attributes (title, description, due date).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
card_id : str
|
||||
Card ID.
|
||||
name : str, optional
|
||||
New title.
|
||||
desc : str, optional
|
||||
New description.
|
||||
due : str, optional
|
||||
New due date ISO 8601.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with updated card details.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.update_card(card_id=card_id, name=name, desc=desc, due=due)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_move_card(card_id: str, list_id: str) -> str:
|
||||
"""Move a card to a different list.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
card_id : str
|
||||
Card ID.
|
||||
list_id : str
|
||||
Target list ID.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with success message.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.move_card(card_id=card_id, list_id=list_id)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_archive_card(card_id: str) -> str:
|
||||
"""Archive a Trello card.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
card_id : str
|
||||
Card ID.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with success message.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.archive_card(card_id=card_id)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_assign_member(card_id: str, member_id: str) -> str:
|
||||
"""Add a member to a Trello card.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
card_id : str
|
||||
Card ID.
|
||||
member_id : str
|
||||
Trello member ID.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with success message.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.assign_member(card_id=card_id, member_id=member_id)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_remove_member(card_id: str, member_id: str) -> str:
|
||||
"""Remove a member from a Trello card.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
card_id : str
|
||||
Card ID.
|
||||
member_id : str
|
||||
Trello member ID.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with success message.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.remove_member(card_id=card_id, member_id=member_id)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_add_comment(card_id: str, text: str) -> str:
|
||||
"""Add a comment to a Trello card.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
card_id : str
|
||||
Card ID.
|
||||
text : str
|
||||
Comment text.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with comment ID.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.add_comment(card_id=card_id, text=text)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_delete_comment(card_id: str, comment_id: str) -> str:
|
||||
"""Delete a comment from a Trello card.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
card_id : str
|
||||
Card ID.
|
||||
comment_id : str
|
||||
Comment/action ID.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with success message.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.delete_comment(card_id=card_id, comment_id=comment_id)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_add_checklist_item(card_id: str, name: str, checklist_id: str | None = None) -> str:
|
||||
"""Add a checklist item to a Trello card.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
card_id : str
|
||||
Card ID.
|
||||
name : str
|
||||
Checklist item text.
|
||||
checklist_id : str, optional
|
||||
Specific checklist ID (auto-discovers the first if omitted).
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with item details.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.add_checklist_item(card_id=card_id, name=name, checklist_id=checklist_id)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_toggle_checklist_item(card_id: str, item_id: str, checked: bool) -> str: # noqa: FBT001
|
||||
"""Mark a checklist item as complete or incomplete.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
card_id : str
|
||||
Card ID.
|
||||
item_id : str
|
||||
Checklist item ID.
|
||||
checked : bool
|
||||
True = complete, False = incomplete.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with updated item state.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.toggle_checklist_item(card_id=card_id, item_id=item_id, checked=checked)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
def trello_delete_checklist_item(card_id: str, item_id: str) -> str:
|
||||
"""Remove a checklist item from a Trello card.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
card_id : str
|
||||
Card ID.
|
||||
item_id : str
|
||||
Checklist item ID.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
JSON with success message.
|
||||
"""
|
||||
client = _get_client()
|
||||
result = client.delete_checklist_item(card_id=card_id, item_id=item_id)
|
||||
return _respond(result)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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,
|
||||
trello_create_board,
|
||||
trello_rename_board,
|
||||
trello_archive_board,
|
||||
trello_open_board,
|
||||
trello_board_details,
|
||||
trello_create_list,
|
||||
trello_rename_list,
|
||||
trello_archive_list,
|
||||
trello_move_list,
|
||||
trello_create_card,
|
||||
trello_card_details,
|
||||
trello_update_card,
|
||||
trello_move_card,
|
||||
trello_archive_card,
|
||||
trello_assign_member,
|
||||
trello_remove_member,
|
||||
trello_add_comment,
|
||||
trello_delete_comment,
|
||||
trello_add_checklist_item,
|
||||
trello_toggle_checklist_item,
|
||||
trello_delete_checklist_item,
|
||||
]
|
||||
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()
|
||||
@@ -0,0 +1,329 @@
|
||||
"""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
|
||||
from pytest import MonkeyPatch
|
||||
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(monkeypatch: MonkeyPatch) -> Generator[None, None, None]:
|
||||
"""Remove Trello env vars before each test so state is predictable."""
|
||||
monkeypatch.delenv("TRELLO_API_KEY", raising=False)
|
||||
monkeypatch.delenv("TRELLO_TOKEN", raising=False)
|
||||
yield
|
||||
|
||||
|
||||
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) == 24
|
||||
assert all(callable(t) for t in PLUGIN_TOOLS)
|
||||
@@ -0,0 +1,406 @@
|
||||
"""Tests for the Trello plugin — board management feature."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("clear_env")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_env(monkeypatch: pytest.MonkeyPatch) -> Any:
|
||||
"""Remove Trello env vars before each test so state is predictable."""
|
||||
monkeypatch.delenv("TRELLO_API_KEY", raising=False)
|
||||
monkeypatch.delenv("TRELLO_TOKEN", raising=False)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TrelloClient — create_board
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestCreateBoard:
|
||||
"""Tests for TrelloClient.create_board()."""
|
||||
|
||||
def test_success(self, requests_mock: Any) -> None:
|
||||
"""Happy path: creates a board and returns details."""
|
||||
from trello_plugin.client import TrelloClient
|
||||
|
||||
requests_mock.post(
|
||||
"https://api.trello.com/1/boards",
|
||||
json={"id": "b1", "name": "New Board", "url": "https://trello.com/b/b1"},
|
||||
status_code=200,
|
||||
)
|
||||
client = TrelloClient(api_key="key", token="tok")
|
||||
result = client.create_board(name="New Board")
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["board"]["name"] == "New Board"
|
||||
assert result["board"]["id"] == "b1"
|
||||
|
||||
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.create_board(name="Board")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "TRELLO_API_KEY" in result["message"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TrelloClient — rename_board
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestRenameBoard:
|
||||
"""Tests for TrelloClient.rename_board()."""
|
||||
|
||||
def test_success_by_id(self, requests_mock: Any) -> None:
|
||||
"""Renaming a board by ID works."""
|
||||
from trello_plugin.client import TrelloClient
|
||||
|
||||
# Resolve board
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/boards/b1",
|
||||
json={"id": "b1", "name": "Old Name", "url": "https://trello.com/b/b1"},
|
||||
)
|
||||
# Rename
|
||||
requests_mock.put(
|
||||
"https://api.trello.com/1/boards/b1",
|
||||
json={"id": "b1", "name": "New Name", "url": "https://trello.com/b/b1"},
|
||||
)
|
||||
|
||||
client = TrelloClient(api_key="key", token="tok")
|
||||
result = client.rename_board(board_id="b1", name="New Name")
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["board"]["name"] == "New Name"
|
||||
|
||||
def test_board_not_found(self, requests_mock: Any) -> None:
|
||||
"""Non-existent board returns a clear error."""
|
||||
from trello_plugin.client import TrelloClient
|
||||
|
||||
# Board lookup fails
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/boards/nonexistent",
|
||||
status_code=404,
|
||||
)
|
||||
# List boards returns empty
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/members/me/boards",
|
||||
json=[],
|
||||
)
|
||||
|
||||
client = TrelloClient(api_key="key", token="tok")
|
||||
result = client.rename_board(board_id="nonexistent", name="New")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "not found" in result["message"].lower()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TrelloClient — archive_board
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestArchiveBoard:
|
||||
"""Tests for TrelloClient.archive_board()."""
|
||||
|
||||
def test_success(self, requests_mock: Any) -> None:
|
||||
"""Archiving a board returns success message."""
|
||||
from trello_plugin.client import TrelloClient
|
||||
|
||||
# Resolve
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/boards/b1",
|
||||
json={"id": "b1", "name": "My Board", "url": "https://trello.com/b/b1"},
|
||||
)
|
||||
# Archive
|
||||
requests_mock.put(
|
||||
"https://api.trello.com/1/boards/b1",
|
||||
json={"id": "b1", "name": "My Board", "closed": True},
|
||||
)
|
||||
|
||||
client = TrelloClient(api_key="key", token="tok")
|
||||
result = client.archive_board(board_id="b1")
|
||||
|
||||
assert result["success"] is True
|
||||
assert "archived" in result["message"].lower()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TrelloClient — open_board
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestOpenBoard:
|
||||
"""Tests for TrelloClient.open_board()."""
|
||||
|
||||
def test_success(self, requests_mock: Any) -> None:
|
||||
"""Opening a closed board returns success message."""
|
||||
from trello_plugin.client import TrelloClient
|
||||
|
||||
# Resolve
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/boards/b1",
|
||||
json={"id": "b1", "name": "My Board", "url": "https://trello.com/b/b1"},
|
||||
)
|
||||
# Open
|
||||
requests_mock.put(
|
||||
"https://api.trello.com/1/boards/b1",
|
||||
json={"id": "b1", "name": "My Board", "closed": False},
|
||||
)
|
||||
|
||||
client = TrelloClient(api_key="key", token="tok")
|
||||
result = client.open_board(board_id="b1")
|
||||
|
||||
assert result["success"] is True
|
||||
assert "opened" in result["message"].lower()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TrelloClient — board_details
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBoardDetails:
|
||||
"""Tests for TrelloClient.board_details()."""
|
||||
|
||||
def test_success(self, requests_mock: Any) -> None:
|
||||
"""Board details return lists and members."""
|
||||
from trello_plugin.client import TrelloClient
|
||||
|
||||
# Resolve
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/boards/b1",
|
||||
json={"id": "b1", "name": "My Board", "url": "https://trello.com/b/b1"},
|
||||
)
|
||||
# Details fetch
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/boards/b1",
|
||||
json={
|
||||
"id": "b1",
|
||||
"name": "My Board",
|
||||
"url": "https://trello.com/b/b1",
|
||||
"desc": "A test board",
|
||||
"closed": False,
|
||||
"starred": False,
|
||||
"lists": [
|
||||
{"id": "l1", "name": "To Do"},
|
||||
{"id": "l2", "name": "Done"},
|
||||
],
|
||||
"members": [
|
||||
{"id": "m1", "username": "user1", "fullName": "User One"},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
client = TrelloClient(api_key="key", token="tok")
|
||||
result = client.board_details(board_id="b1")
|
||||
|
||||
assert result["success"] is True
|
||||
assert len(result["board"]["lists"]) == 2
|
||||
assert len(result["board"]["members"]) == 1
|
||||
assert result["board"]["lists"][0]["name"] == "To Do"
|
||||
|
||||
def test_not_found(self, requests_mock: Any) -> None:
|
||||
"""Non-existent board returns error."""
|
||||
from trello_plugin.client import TrelloClient
|
||||
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/boards/nonexistent",
|
||||
status_code=404,
|
||||
)
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/members/me/boards",
|
||||
json=[],
|
||||
)
|
||||
|
||||
client = TrelloClient(api_key="key", token="tok")
|
||||
result = client.board_details(board_id="nonexistent")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "not found" in result["message"].lower()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TrelloClient — _resolve_board_id
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestResolveBoardId:
|
||||
"""Tests for TrelloClient._resolve_board_id()."""
|
||||
|
||||
def test_resolves_by_id(self, requests_mock: Any) -> None:
|
||||
"""Resolves a valid ID directly."""
|
||||
from trello_plugin.client import TrelloClient
|
||||
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/boards/b1",
|
||||
json={"id": "b1", "name": "My Board", "url": "https://trello.com/b/b1"},
|
||||
)
|
||||
|
||||
client = TrelloClient(api_key="key", token="tok")
|
||||
result = client._resolve_board_id("b1")
|
||||
assert result["id"] == "b1"
|
||||
|
||||
def test_resolves_by_name(self, requests_mock: Any) -> None:
|
||||
"""Resolves a board name to its ID."""
|
||||
from trello_plugin.client import TrelloClient
|
||||
|
||||
# ID lookup fails
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/boards/My%20Board",
|
||||
status_code=404,
|
||||
)
|
||||
# List boards for name matching
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/members/me/boards",
|
||||
json=[
|
||||
{"id": "b1", "name": "My Board", "url": "https://trello.com/b/b1", "closed": False, "starred": False},
|
||||
{"id": "b2", "name": "Other Board", "url": "https://trello.com/b/b2", "closed": False, "starred": False},
|
||||
],
|
||||
)
|
||||
|
||||
client = TrelloClient(api_key="key", token="tok")
|
||||
result = client._resolve_board_id("My Board")
|
||||
assert result["id"] == "b1"
|
||||
|
||||
def test_duplicate_name_error(self, requests_mock: Any) -> None:
|
||||
"""Multiple boards with same name returns error."""
|
||||
from trello_plugin.client import TrelloClient
|
||||
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/boards/Duplicate",
|
||||
status_code=404,
|
||||
)
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/members/me/boards",
|
||||
json=[
|
||||
{"id": "b1", "name": "Duplicate", "url": "", "closed": False, "starred": False},
|
||||
{"id": "b2", "name": "Duplicate", "url": "", "closed": False, "starred": False},
|
||||
],
|
||||
)
|
||||
|
||||
client = TrelloClient(api_key="key", token="tok")
|
||||
result = client._resolve_board_id("Duplicate")
|
||||
assert "success" in result and result["success"] is False
|
||||
assert "multiple" in result["message"].lower()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool functions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBoardToolFunctions:
|
||||
"""Tests for the Hermes board management tool wrappers."""
|
||||
|
||||
def test_trello_create_board_tool(self, requests_mock: Any) -> None:
|
||||
"""Create board tool returns valid JSON."""
|
||||
from trello_plugin.tools import trello_create_board
|
||||
|
||||
os.environ["TRELLO_API_KEY"] = "key"
|
||||
os.environ["TRELLO_TOKEN"] = "tok"
|
||||
|
||||
requests_mock.post(
|
||||
"https://api.trello.com/1/boards",
|
||||
json={"id": "b1", "name": "New Board", "url": "https://trello.com/b/b1"},
|
||||
)
|
||||
|
||||
result = json.loads(trello_create_board(name="New Board"))
|
||||
assert result["success"] is True
|
||||
assert result["board"]["name"] == "New Board"
|
||||
|
||||
def test_trello_rename_board_tool(self, requests_mock: Any) -> None:
|
||||
"""Rename board tool returns valid JSON."""
|
||||
from trello_plugin.tools import trello_rename_board
|
||||
|
||||
os.environ["TRELLO_API_KEY"] = "key"
|
||||
os.environ["TRELLO_TOKEN"] = "tok"
|
||||
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/boards/b1",
|
||||
json={"id": "b1", "name": "Old", "url": ""},
|
||||
)
|
||||
requests_mock.put(
|
||||
"https://api.trello.com/1/boards/b1",
|
||||
json={"id": "b1", "name": "Renamed", "url": ""},
|
||||
)
|
||||
|
||||
result = json.loads(trello_rename_board(board_id="b1", name="Renamed"))
|
||||
assert result["success"] is True
|
||||
assert result["board"]["name"] == "Renamed"
|
||||
|
||||
def test_trello_archive_board_tool(self, requests_mock: Any) -> None:
|
||||
"""Archive board tool returns valid JSON."""
|
||||
from trello_plugin.tools import trello_archive_board
|
||||
|
||||
os.environ["TRELLO_API_KEY"] = "key"
|
||||
os.environ["TRELLO_TOKEN"] = "tok"
|
||||
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/boards/b1",
|
||||
json={"id": "b1", "name": "Test Board", "url": ""},
|
||||
)
|
||||
requests_mock.put(
|
||||
"https://api.trello.com/1/boards/b1",
|
||||
json={"id": "b1", "name": "Test Board", "closed": True},
|
||||
)
|
||||
|
||||
result = json.loads(trello_archive_board(board_id="b1"))
|
||||
assert result["success"] is True
|
||||
assert "archived" in result["message"].lower()
|
||||
|
||||
def test_trello_open_board_tool(self, requests_mock: Any) -> None:
|
||||
"""Open board tool returns valid JSON."""
|
||||
from trello_plugin.tools import trello_open_board
|
||||
|
||||
os.environ["TRELLO_API_KEY"] = "key"
|
||||
os.environ["TRELLO_TOKEN"] = "tok"
|
||||
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/boards/b1",
|
||||
json={"id": "b1", "name": "Test Board", "url": ""},
|
||||
)
|
||||
requests_mock.put(
|
||||
"https://api.trello.com/1/boards/b1",
|
||||
json={"id": "b1", "name": "Test Board", "closed": False},
|
||||
)
|
||||
|
||||
result = json.loads(trello_open_board(board_id="b1"))
|
||||
assert result["success"] is True
|
||||
assert "opened" in result["message"].lower()
|
||||
|
||||
def test_trello_board_details_tool(self, requests_mock: Any) -> None:
|
||||
"""Board details tool returns valid JSON."""
|
||||
from trello_plugin.tools import trello_board_details
|
||||
|
||||
os.environ["TRELLO_API_KEY"] = "key"
|
||||
os.environ["TRELLO_TOKEN"] = "tok"
|
||||
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/boards/b1",
|
||||
json={"id": "b1", "name": "Test", "url": ""},
|
||||
)
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/boards/b1",
|
||||
json={
|
||||
"id": "b1",
|
||||
"name": "Test",
|
||||
"url": "",
|
||||
"desc": "",
|
||||
"closed": False,
|
||||
"starred": False,
|
||||
"lists": [{"id": "l1", "name": "To Do"}],
|
||||
"members": [],
|
||||
},
|
||||
)
|
||||
|
||||
result = json.loads(trello_board_details(board_id="b1"))
|
||||
assert result["success"] is True
|
||||
assert len(result["board"]["lists"]) == 1
|
||||
@@ -0,0 +1,313 @@
|
||||
"""Tests for the Trello plugin — card management feature."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("clear_env")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_env(monkeypatch: pytest.MonkeyPatch) -> Any:
|
||||
monkeypatch.delenv("TRELLO_API_KEY", raising=False)
|
||||
monkeypatch.delenv("TRELLO_TOKEN", raising=False)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TrelloClient — create_card
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestCreateCard:
|
||||
def test_success(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.client import TrelloClient
|
||||
requests_mock.post("https://api.trello.com/1/cards", json={"id": "c1", "name": "My Card", "url": "https://trello.com/c/c1", "shortUrl": "https://trello.com/c/abc", "idList": "l1"})
|
||||
client = TrelloClient(api_key="k", token="t")
|
||||
result = client.create_card(name="My Card", list_id="l1")
|
||||
assert result["success"]
|
||||
assert result["card"]["name"] == "My Card"
|
||||
|
||||
def test_with_description_and_due(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.client import TrelloClient
|
||||
requests_mock.post("https://api.trello.com/1/cards", json={"id": "c1", "name": "Card", "url": "", "shortUrl": "", "idList": "l1"})
|
||||
client = TrelloClient(api_key="k", token="t")
|
||||
result = client.create_card(name="Card", list_id="l1", desc="Desc", due="2026-06-01T00:00:00Z")
|
||||
assert result["success"]
|
||||
|
||||
def test_missing_credentials(self) -> None:
|
||||
from trello_plugin.client import TrelloClient
|
||||
client = TrelloClient(api_key="", token="")
|
||||
result = client.create_card(name="Card", list_id="l1")
|
||||
assert not result["success"]
|
||||
assert "TRELLO_API_KEY" in result["message"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TrelloClient — card_details
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestCardDetails:
|
||||
def test_success(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.client import TrelloClient
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/cards/c1",
|
||||
json={
|
||||
"id": "c1", "name": "My Card", "desc": "Description", "due": None,
|
||||
"dueComplete": False, "url": "https://trello.com/c/c1", "shortUrl": "https://trello.com/c/abc",
|
||||
"idList": "l1", "closed": False, "list": {"id": "l1", "name": "To Do"},
|
||||
"members": [{"id": "m1", "username": "user1", "fullName": "User One"}],
|
||||
"checklists": [{"id": "cl1", "name": "Checklist", "checkItems": [{"id": "ci1", "name": "Item", "state": "incomplete"}]}],
|
||||
"actions": [{"id": "a1", "type": "commentCard", "data": {"text": "Nice card"}, "memberCreator": {"username": "user1"}, "date": "2026-05-01T00:00:00Z"}],
|
||||
},
|
||||
)
|
||||
client = TrelloClient(api_key="k", token="t")
|
||||
result = client.card_details(card_id="c1")
|
||||
assert result["success"]
|
||||
assert result["card"]["name"] == "My Card"
|
||||
assert len(result["card"]["members"]) == 1
|
||||
assert len(result["card"]["checklists"]) == 1
|
||||
assert len(result["card"]["comments"]) == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TrelloClient — update_card
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestUpdateCard:
|
||||
def test_update_name(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.client import TrelloClient
|
||||
requests_mock.put("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "Updated", "desc": "", "due": None})
|
||||
client = TrelloClient(api_key="k", token="t")
|
||||
result = client.update_card(card_id="c1", name="Updated")
|
||||
assert result["success"]
|
||||
assert result["card"]["name"] == "Updated"
|
||||
|
||||
def test_no_changes(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.client import TrelloClient
|
||||
client = TrelloClient(api_key="k", token="t")
|
||||
result = client.update_card(card_id="c1")
|
||||
assert result["success"]
|
||||
assert "No changes" in result["message"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TrelloClient — move_card, archive_card
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestMoveCard:
|
||||
def test_success(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.client import TrelloClient
|
||||
requests_mock.put("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "Card", "idList": "l2"})
|
||||
client = TrelloClient(api_key="k", token="t")
|
||||
result = client.move_card(card_id="c1", list_id="l2")
|
||||
assert result["success"]
|
||||
|
||||
|
||||
class TestArchiveCard:
|
||||
def test_success(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.client import TrelloClient
|
||||
requests_mock.put("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "Card", "closed": True})
|
||||
client = TrelloClient(api_key="k", token="t")
|
||||
result = client.archive_card(card_id="c1")
|
||||
assert result["success"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TrelloClient — member operations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestMemberOps:
|
||||
def test_assign_member(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.client import TrelloClient
|
||||
requests_mock.post("https://api.trello.com/1/cards/c1/idMembers", json={"id": "c1"}, status_code=200)
|
||||
client = TrelloClient(api_key="k", token="t")
|
||||
result = client.assign_member(card_id="c1", member_id="m1")
|
||||
assert result["success"]
|
||||
|
||||
def test_remove_member(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.client import TrelloClient
|
||||
requests_mock.delete("https://api.trello.com/1/cards/c1/idMembers/m1", status_code=200, json={})
|
||||
client = TrelloClient(api_key="k", token="t")
|
||||
result = client.remove_member(card_id="c1", member_id="m1")
|
||||
assert result["success"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TrelloClient — comments
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestComments:
|
||||
def test_add_comment(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.client import TrelloClient
|
||||
requests_mock.post("https://api.trello.com/1/cards/c1/actions/comments", json={"id": "a1", "data": {"text": "Great!"}})
|
||||
client = TrelloClient(api_key="k", token="t")
|
||||
result = client.add_comment(card_id="c1", text="Great!")
|
||||
assert result["success"]
|
||||
assert result["comment"]["id"] == "a1"
|
||||
|
||||
def test_delete_comment(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.client import TrelloClient
|
||||
requests_mock.delete("https://api.trello.com/1/cards/c1/actions/a1/comments", status_code=200, json={})
|
||||
client = TrelloClient(api_key="k", token="t")
|
||||
result = client.delete_comment(card_id="c1", comment_id="a1")
|
||||
assert result["success"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TrelloClient — checklists
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestChecklistItems:
|
||||
def test_add_item_with_checklist_id(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.client import TrelloClient
|
||||
requests_mock.post("https://api.trello.com/1/cards/c1/checklistItems", json={"id": "ci1", "name": "Task", "state": "incomplete", "idChecklist": "cl1"})
|
||||
client = TrelloClient(api_key="k", token="t")
|
||||
result = client.add_checklist_item(card_id="c1", name="Task", checklist_id="cl1")
|
||||
assert result["success"]
|
||||
assert result["item"]["name"] == "Task"
|
||||
|
||||
def test_add_item_auto_discover(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.client import TrelloClient
|
||||
# card_details call for auto-discover
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/cards/c1",
|
||||
json={
|
||||
"id": "c1", "name": "Card", "desc": "", "due": None,
|
||||
"dueComplete": False, "url": "", "shortUrl": "", "idList": "l1", "closed": False,
|
||||
"list": {"id": "l1", "name": "List"},
|
||||
"members": [], "checklists": [{"id": "cl1", "name": "Checklist", "checkItems": []}],
|
||||
"actions": [],
|
||||
},
|
||||
)
|
||||
requests_mock.post("https://api.trello.com/1/cards/c1/checklistItems", json={"id": "ci1", "name": "Auto", "state": "incomplete", "idChecklist": "cl1"})
|
||||
client = TrelloClient(api_key="k", token="t")
|
||||
result = client.add_checklist_item(card_id="c1", name="Auto")
|
||||
assert result["success"]
|
||||
|
||||
def test_toggle_complete(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.client import TrelloClient
|
||||
requests_mock.put("https://api.trello.com/1/cards/c1/checklistItem/ci1", json={"id": "ci1", "name": "Task", "state": "complete"})
|
||||
client = TrelloClient(api_key="k", token="t")
|
||||
result = client.toggle_checklist_item(card_id="c1", item_id="ci1", checked=True)
|
||||
assert result["success"]
|
||||
assert result["item"]["state"] == "complete"
|
||||
|
||||
def test_toggle_incomplete(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.client import TrelloClient
|
||||
requests_mock.put("https://api.trello.com/1/cards/c1/checklistItem/ci1", json={"id": "ci1", "name": "Task", "state": "incomplete"})
|
||||
client = TrelloClient(api_key="k", token="t")
|
||||
result = client.toggle_checklist_item(card_id="c1", item_id="ci1", checked=False)
|
||||
assert result["success"]
|
||||
assert result["item"]["state"] == "incomplete"
|
||||
|
||||
def test_delete_item(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.client import TrelloClient
|
||||
requests_mock.delete("https://api.trello.com/1/cards/c1/checklistItems/ci1", status_code=200, json={})
|
||||
client = TrelloClient(api_key="k", token="t")
|
||||
result = client.delete_checklist_item(card_id="c1", item_id="ci1")
|
||||
assert result["success"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool functions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestCardToolFunctions:
|
||||
def test_trello_create_card_tool(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.tools import trello_create_card
|
||||
os.environ["TRELLO_API_KEY"] = "k"
|
||||
os.environ["TRELLO_TOKEN"] = "t"
|
||||
requests_mock.post("https://api.trello.com/1/cards", json={"id": "c1", "name": "Card", "url": "", "shortUrl": "", "idList": "l1"})
|
||||
result = json.loads(trello_create_card(name="Card", list_id="l1"))
|
||||
assert result["success"]
|
||||
|
||||
def test_trello_card_details_tool(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.tools import trello_card_details
|
||||
os.environ["TRELLO_API_KEY"] = "k"
|
||||
os.environ["TRELLO_TOKEN"] = "t"
|
||||
requests_mock.get("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "Card", "desc": "", "due": None, "dueComplete": False, "url": "", "shortUrl": "", "idList": "l1", "closed": False, "list": {"id": "l1", "name": "List"}, "members": [], "checklists": [], "actions": []})
|
||||
result = json.loads(trello_card_details(card_id="c1"))
|
||||
assert result["success"]
|
||||
|
||||
def test_trello_update_card_tool(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.tools import trello_update_card
|
||||
os.environ["TRELLO_API_KEY"] = "k"
|
||||
os.environ["TRELLO_TOKEN"] = "t"
|
||||
requests_mock.put("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "New", "desc": "", "due": None})
|
||||
result = json.loads(trello_update_card(card_id="c1", name="New"))
|
||||
assert result["success"]
|
||||
|
||||
def test_trello_move_card_tool(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.tools import trello_move_card
|
||||
os.environ["TRELLO_API_KEY"] = "k"
|
||||
os.environ["TRELLO_TOKEN"] = "t"
|
||||
requests_mock.put("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "Card", "idList": "l2"})
|
||||
result = json.loads(trello_move_card(card_id="c1", list_id="l2"))
|
||||
assert result["success"]
|
||||
|
||||
def test_trello_archive_card_tool(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.tools import trello_archive_card
|
||||
os.environ["TRELLO_API_KEY"] = "k"
|
||||
os.environ["TRELLO_TOKEN"] = "t"
|
||||
requests_mock.put("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "Card", "closed": True})
|
||||
result = json.loads(trello_archive_card(card_id="c1"))
|
||||
assert result["success"]
|
||||
|
||||
def test_trello_assign_member_tool(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.tools import trello_assign_member
|
||||
os.environ["TRELLO_API_KEY"] = "k"
|
||||
os.environ["TRELLO_TOKEN"] = "t"
|
||||
requests_mock.post("https://api.trello.com/1/cards/c1/idMembers", json={}, status_code=200)
|
||||
result = json.loads(trello_assign_member(card_id="c1", member_id="m1"))
|
||||
assert result["success"]
|
||||
|
||||
def test_trello_remove_member_tool(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.tools import trello_remove_member
|
||||
os.environ["TRELLO_API_KEY"] = "k"
|
||||
os.environ["TRELLO_TOKEN"] = "t"
|
||||
requests_mock.delete("https://api.trello.com/1/cards/c1/idMembers/m1", status_code=200, json={})
|
||||
result = json.loads(trello_remove_member(card_id="c1", member_id="m1"))
|
||||
assert result["success"]
|
||||
|
||||
def test_trello_add_comment_tool(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.tools import trello_add_comment
|
||||
os.environ["TRELLO_API_KEY"] = "k"
|
||||
os.environ["TRELLO_TOKEN"] = "t"
|
||||
requests_mock.post("https://api.trello.com/1/cards/c1/actions/comments", json={"id": "a1", "data": {"text": "Nice"}})
|
||||
result = json.loads(trello_add_comment(card_id="c1", text="Nice"))
|
||||
assert result["success"]
|
||||
|
||||
def test_trello_delete_comment_tool(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.tools import trello_delete_comment
|
||||
os.environ["TRELLO_API_KEY"] = "k"
|
||||
os.environ["TRELLO_TOKEN"] = "t"
|
||||
requests_mock.delete("https://api.trello.com/1/cards/c1/actions/a1/comments", status_code=200, json={})
|
||||
result = json.loads(trello_delete_comment(card_id="c1", comment_id="a1"))
|
||||
assert result["success"]
|
||||
|
||||
def test_trello_add_checklist_item_tool(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.tools import trello_add_checklist_item
|
||||
os.environ["TRELLO_API_KEY"] = "k"
|
||||
os.environ["TRELLO_TOKEN"] = "t"
|
||||
requests_mock.post("https://api.trello.com/1/cards/c1/checklistItems", json={"id": "ci1", "name": "Task", "state": "incomplete", "idChecklist": "cl1"})
|
||||
result = json.loads(trello_add_checklist_item(card_id="c1", name="Task", checklist_id="cl1"))
|
||||
assert result["success"]
|
||||
|
||||
def test_trello_toggle_checklist_item_tool(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.tools import trello_toggle_checklist_item
|
||||
os.environ["TRELLO_API_KEY"] = "k"
|
||||
os.environ["TRELLO_TOKEN"] = "t"
|
||||
requests_mock.put("https://api.trello.com/1/cards/c1/checklistItem/ci1", json={"id": "ci1", "name": "Task", "state": "complete"})
|
||||
result = json.loads(trello_toggle_checklist_item(card_id="c1", item_id="ci1", checked=True))
|
||||
assert result["success"]
|
||||
|
||||
def test_trello_delete_checklist_item_tool(self, requests_mock: Any) -> None:
|
||||
from trello_plugin.tools import trello_delete_checklist_item
|
||||
os.environ["TRELLO_API_KEY"] = "k"
|
||||
os.environ["TRELLO_TOKEN"] = "t"
|
||||
requests_mock.delete("https://api.trello.com/1/cards/c1/checklistItems/ci1", status_code=200, json={})
|
||||
result = json.loads(trello_delete_checklist_item(card_id="c1", item_id="ci1"))
|
||||
assert result["success"]
|
||||
@@ -0,0 +1,226 @@
|
||||
"""Tests for the Trello plugin — list management feature."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("clear_env")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_env(monkeypatch: pytest.MonkeyPatch) -> Any:
|
||||
"""Remove Trello env vars before each test so state is predictable."""
|
||||
monkeypatch.delenv("TRELLO_API_KEY", raising=False)
|
||||
monkeypatch.delenv("TRELLO_TOKEN", raising=False)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TrelloClient — create_list
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestCreateList:
|
||||
"""Tests for TrelloClient.create_list()."""
|
||||
|
||||
def test_success(self, requests_mock: Any) -> None:
|
||||
"""Happy path: creates a list and returns details."""
|
||||
from trello_plugin.client import TrelloClient
|
||||
|
||||
# Resolve board
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/boards/b1",
|
||||
json={"id": "b1", "name": "My Board", "url": ""},
|
||||
)
|
||||
# Create list
|
||||
requests_mock.post(
|
||||
"https://api.trello.com/1/lists",
|
||||
json={"id": "l1", "name": "To Do", "idBoard": "b1"},
|
||||
)
|
||||
|
||||
client = TrelloClient(api_key="key", token="tok")
|
||||
result = client.create_list(name="To Do", board_id="b1")
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["list"]["name"] == "To Do"
|
||||
assert result["list"]["id_board"] == "b1"
|
||||
|
||||
def test_board_not_found(self, requests_mock: Any) -> None:
|
||||
"""Non-existent board returns error."""
|
||||
from trello_plugin.client import TrelloClient
|
||||
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/boards/nonexistent",
|
||||
status_code=404,
|
||||
)
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/members/me/boards",
|
||||
json=[],
|
||||
)
|
||||
|
||||
client = TrelloClient(api_key="key", token="tok")
|
||||
result = client.create_list(name="List", board_id="nonexistent")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "not found" in result["message"].lower()
|
||||
|
||||
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.create_list(name="List", board_id="b1")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "TRELLO_API_KEY" in result["message"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TrelloClient — rename_list
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestRenameList:
|
||||
"""Tests for TrelloClient.rename_list()."""
|
||||
|
||||
def test_success(self, requests_mock: Any) -> None:
|
||||
"""Renaming a list works."""
|
||||
from trello_plugin.client import TrelloClient
|
||||
|
||||
requests_mock.put(
|
||||
"https://api.trello.com/1/lists/l1",
|
||||
json={"id": "l1", "name": "Renamed List"},
|
||||
)
|
||||
|
||||
client = TrelloClient(api_key="key", token="tok")
|
||||
result = client.rename_list(list_id="l1", name="Renamed List")
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["list"]["name"] == "Renamed List"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TrelloClient — archive_list
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestArchiveList:
|
||||
"""Tests for TrelloClient.archive_list()."""
|
||||
|
||||
def test_success(self, requests_mock: Any) -> None:
|
||||
"""Archiving a list returns success message."""
|
||||
from trello_plugin.client import TrelloClient
|
||||
|
||||
requests_mock.put(
|
||||
"https://api.trello.com/1/lists/l1",
|
||||
json={"id": "l1", "name": "My List", "closed": True},
|
||||
)
|
||||
|
||||
client = TrelloClient(api_key="key", token="tok")
|
||||
result = client.archive_list(list_id="l1")
|
||||
|
||||
assert result["success"] is True
|
||||
assert "archived" in result["message"].lower()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TrelloClient — move_list
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestMoveList:
|
||||
"""Tests for TrelloClient.move_list()."""
|
||||
|
||||
def test_success(self, requests_mock: Any) -> None:
|
||||
"""Moving a list returns success message."""
|
||||
from trello_plugin.client import TrelloClient
|
||||
|
||||
requests_mock.put(
|
||||
"https://api.trello.com/1/lists/l1",
|
||||
json={"id": "l1", "name": "My List", "pos": 1},
|
||||
)
|
||||
|
||||
client = TrelloClient(api_key="key", token="tok")
|
||||
result = client.move_list(list_id="l1", pos="top")
|
||||
|
||||
assert result["success"] is True
|
||||
assert "moved" in result["message"].lower()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool functions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestListToolFunctions:
|
||||
"""Tests for the Hermes list management tool wrappers."""
|
||||
|
||||
def test_trello_create_list_tool(self, requests_mock: Any) -> None:
|
||||
"""Create list tool returns valid JSON."""
|
||||
from trello_plugin.tools import trello_create_list
|
||||
|
||||
os.environ["TRELLO_API_KEY"] = "key"
|
||||
os.environ["TRELLO_TOKEN"] = "tok"
|
||||
|
||||
requests_mock.get(
|
||||
"https://api.trello.com/1/boards/b1",
|
||||
json={"id": "b1", "name": "Board", "url": ""},
|
||||
)
|
||||
requests_mock.post(
|
||||
"https://api.trello.com/1/lists",
|
||||
json={"id": "l1", "name": "New List", "idBoard": "b1"},
|
||||
)
|
||||
|
||||
result = json.loads(trello_create_list(name="New List", board_id="b1"))
|
||||
assert result["success"] is True
|
||||
assert result["list"]["name"] == "New List"
|
||||
|
||||
def test_trello_rename_list_tool(self, requests_mock: Any) -> None:
|
||||
"""Rename list tool returns valid JSON."""
|
||||
from trello_plugin.tools import trello_rename_list
|
||||
|
||||
os.environ["TRELLO_API_KEY"] = "key"
|
||||
os.environ["TRELLO_TOKEN"] = "tok"
|
||||
|
||||
requests_mock.put(
|
||||
"https://api.trello.com/1/lists/l1",
|
||||
json={"id": "l1", "name": "Renamed"},
|
||||
)
|
||||
|
||||
result = json.loads(trello_rename_list(list_id="l1", name="Renamed"))
|
||||
assert result["success"] is True
|
||||
assert result["list"]["name"] == "Renamed"
|
||||
|
||||
def test_trello_archive_list_tool(self, requests_mock: Any) -> None:
|
||||
"""Archive list tool returns valid JSON."""
|
||||
from trello_plugin.tools import trello_archive_list
|
||||
|
||||
os.environ["TRELLO_API_KEY"] = "key"
|
||||
os.environ["TRELLO_TOKEN"] = "tok"
|
||||
|
||||
requests_mock.put(
|
||||
"https://api.trello.com/1/lists/l1",
|
||||
json={"id": "l1", "name": "My List", "closed": True},
|
||||
)
|
||||
|
||||
result = json.loads(trello_archive_list(list_id="l1"))
|
||||
assert result["success"] is True
|
||||
assert "archived" in result["message"].lower()
|
||||
|
||||
def test_trello_move_list_tool(self, requests_mock: Any) -> None:
|
||||
"""Move list tool returns valid JSON."""
|
||||
from trello_plugin.tools import trello_move_list
|
||||
|
||||
os.environ["TRELLO_API_KEY"] = "key"
|
||||
os.environ["TRELLO_TOKEN"] = "tok"
|
||||
|
||||
requests_mock.put(
|
||||
"https://api.trello.com/1/lists/l1",
|
||||
json={"id": "l1", "name": "My List", "pos": 1},
|
||||
)
|
||||
|
||||
result = json.loads(trello_move_list(list_id="l1", pos="top"))
|
||||
assert result["success"] is True
|
||||
assert "moved" in result["message"].lower()
|
||||
Reference in New Issue
Block a user