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:
+166
@@ -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()
|
||||
Reference in New Issue
Block a user