Files
trello-mcp/qa_client.py
T
maxerton b4cabbf146 trello-tools: плагин и маркетплейс gp-tools для Claude Code
Сервер запускается через uv run --script, ключи запрашиваются
при установке плагина (userConfig) вместо .env.

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

167 lines
5.8 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 не заданы (проверь .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()