trello-tools: плагин и маркетплейс gp-tools для Claude Code

Сервер запускается через uv run --script, ключи запрашиваются
при установке плагина (userConfig) вместо .env.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 14:14:29 +00:00
commit b4cabbf146
14 changed files with 2100 additions and 0 deletions
+166
View File
@@ -0,0 +1,166 @@
"""Клиент qa-docs-service."""
from __future__ import annotations
import os
import httpx
class QaServiceConfigError(RuntimeError):
"""Не заданы обязательные переменные окружения для qa-docs-service."""
def _config() -> tuple[str, str]:
url = os.environ.get("QA_SERVICE_URL")
api_key = os.environ.get("QA_SERVICE_API_KEY")
if not url or not api_key:
raise QaServiceConfigError(
"QA_SERVICE_URL / QA_SERVICE_API_KEY не заданы (проверь .env)"
)
return url.rstrip("/"), api_key
def slug_from(slug_or_url: str) -> str:
"""Принимает и slug, и полную ссылку вида .../docs/<slug>."""
return slug_or_url.strip().rstrip("/").rsplit("/", 1)[-1]
async def publish_doc(
title: str,
markdown: str,
card_url: str | None = None,
problem: str | None = None,
) -> dict:
"""Публикует новый QA-документ, возвращает {"slug": ..., "url": ...}."""
base_url, api_key = _config()
async with httpx.AsyncClient() as client:
response = await client.post(
f"{base_url}/docs",
headers={"X-API-Key": api_key},
json={
"title": title,
"markdown": markdown,
"card_url": card_url,
"problem": problem,
},
)
response.raise_for_status()
return response.json()
async def update_doc(
slug: str,
title: str,
markdown: str,
card_url: str | None = None,
problem: str | None = None,
) -> dict:
"""Перезаписывает уже опубликованный документ по slug (та же ссылка).
Возвращает {"slug": ..., "url": ...}; 404 (через raise_for_status), если
под этим slug ничего не опубликовано.
"""
base_url, api_key = _config()
async with httpx.AsyncClient() as client:
response = await client.put(
f"{base_url}/docs/{slug_from(slug)}",
headers={"X-API-Key": api_key},
json={
"title": title,
"markdown": markdown,
"card_url": card_url,
"problem": problem,
},
)
response.raise_for_status()
return response.json()
async def get_doc(slug: str) -> dict:
"""Скачивает опубликованный документ: исходный markdown, метаданные и
отметки тестировщиков по сценариям (поле scenarios).
404 (через raise_for_status), если под этим slug ничего не опубликовано.
"""
base_url, api_key = _config()
async with httpx.AsyncClient() as client:
response = await client.get(
f"{base_url}/docs/{slug_from(slug)}/raw",
headers={"X-API-Key": api_key},
)
response.raise_for_status()
return response.json()
def _tasks_config() -> tuple[str, dict]:
"""URL сервиса и заголовки для /tasks.
Подходит и основной ключ (у тебя), и QA_TASKS_TOKEN (у Claude на
фронте/бэке, которому доступны только задания).
"""
url = os.environ.get("QA_SERVICE_URL")
token = os.environ.get("QA_SERVICE_API_KEY") or os.environ.get("QA_TASKS_TOKEN")
if not url or not token:
raise QaServiceConfigError(
"QA_SERVICE_URL и QA_SERVICE_API_KEY или QA_TASKS_TOKEN не заданы (проверь .env)"
)
return url.rstrip("/"), {"Authorization": f"Bearer {token}"}
async def publish_task(
target: str, title: str, markdown: str, card_url: str | None = None
) -> dict:
"""Публикует задание для фронта/бэка, возвращает {slug, url, target, ...}."""
base_url, headers = _tasks_config()
async with httpx.AsyncClient() as client:
response = await client.post(
f"{base_url}/tasks",
headers=headers,
json={"title": title, "markdown": markdown, "target": target, "card_url": card_url},
)
response.raise_for_status()
return response.json()
async def update_task(
slug: str, target: str, title: str, markdown: str, card_url: str | None = None
) -> dict:
"""Перезаписывает задание по slug (та же ссылка)."""
base_url, headers = _tasks_config()
async with httpx.AsyncClient() as client:
response = await client.put(
f"{base_url}/tasks/{slug_from(slug)}",
headers=headers,
json={"title": title, "markdown": markdown, "target": target, "card_url": card_url},
)
response.raise_for_status()
return response.json()
async def get_task(slug: str) -> dict:
"""Задание целиком: {slug, url, title, target, card_url, markdown, ...}."""
base_url, headers = _tasks_config()
async with httpx.AsyncClient() as client:
response = await client.get(
f"{base_url}/tasks/{slug_from(slug)}/raw", headers=headers
)
response.raise_for_status()
return response.json()
async def list_tasks(card_url: str | None = None, target: str | None = None) -> list[dict]:
"""Задания без текста, новые первыми; фильтры по карточке и адресату."""
base_url, headers = _tasks_config()
params = {key: value for key, value in (("card", card_url), ("target", target)) if value}
async with httpx.AsyncClient() as client:
response = await client.get(f"{base_url}/tasks", headers=headers, params=params)
response.raise_for_status()
return response.json()