Files
trello-mcp/qa_client.py
T
maxerton e37a30dadc trello-tools: подсказывать /plugin configure, если ключи не заданы
Ошибки конфигурации отсылали к .env, которого у плагина нет.
README: /plugin configure — обязательный шаг установки.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 16:19:11 +00:00

167 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Клиент 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 не заданы (плагин: /plugin configure trello-tools@gp-tools, локально: .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 не заданы (плагин: /plugin configure trello-tools@gp-tools, локально: .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()