e37a30dadc
Ошибки конфигурации отсылали к .env, которого у плагина нет. README: /plugin configure — обязательный шаг установки. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
167 lines
5.9 KiB
Python
167 lines
5.9 KiB
Python
"""Клиент 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()
|