Files
chatbottren/gitea_client.py

196 lines
6.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.
"""Клиент Gitea API для работы с issues."""
import re
from datetime import datetime, timedelta, timezone
import httpx
import config
BASE = f"{config.GITEA_URL}/api/v1"
HEADERS = {"Authorization": f"token {config.GITEA_TOKEN}"}
def _url(path: str) -> str:
return f"{BASE}{path}"
async def get_assignees() -> list[dict]:
"""Список пользователей, которых можно назначить на issues (username, full_name, id)."""
async with httpx.AsyncClient() as client:
r = await client.get(
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/assignees"),
headers=HEADERS,
timeout=15.0,
)
r.raise_for_status()
return r.json()
def resolve_assignee_by_name(name: str, assignees: list[dict]) -> str | None:
"""Найти username по имени/подстроке в assignees. Возвращает login или None."""
name = (name or "").strip().lower()
if not name:
return None
for u in assignees:
login = (u.get("login") or "").lower()
full = (u.get("full_name") or "").lower()
if name in login or name in full or (name and (login == name or full == name)):
return u.get("login")
return None
def parse_deadline(s: str) -> str | None:
"""Парсит дату из строки (завтра, ISO date). Возвращает ISO date YYYY-MM-DD или None."""
if not s or not isinstance(s, str):
return None
s = s.strip().lower()
if s == "завтра":
d = datetime.now(timezone.utc) + timedelta(days=1)
return d.strftime("%Y-%m-%d")
if s == "сегодня":
d = datetime.now(timezone.utc)
return d.strftime("%Y-%m-%d")
# ISO-like date
m = re.match(r"(\d{4})-(\d{2})-(\d{2})", s)
if m:
return f"{m.group(1)}-{m.group(2)}-{m.group(3)}"
return None
async def create_issue(
title: str,
body: str = "",
assignees: list[str] | None = None,
deadline: str | None = None,
) -> dict:
"""Создать issue. assignees — список Gitea username. deadline — ISO date YYYY-MM-DD."""
payload = {"title": title}
if body:
payload["body"] = body
if assignees:
payload["assignees"] = assignees
due = parse_deadline(deadline) if deadline else None
if due:
payload["due_date"] = due + "T12:00:00Z"
async with httpx.AsyncClient() as client:
r = await client.post(
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/issues"),
headers={**HEADERS, "Content-Type": "application/json"},
json=payload,
timeout=15.0,
)
r.raise_for_status()
return r.json()
async def get_issues(
state: str = "open",
assignee: str | None = None,
page: int = 1,
limit: int = 20,
) -> list[dict]:
"""Список issues. state: open|closed. assignee: Gitea username."""
params = {"state": state, "page": page, "limit": limit}
if assignee:
params["assignee"] = assignee
async with httpx.AsyncClient() as client:
r = await client.get(
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/issues"),
headers=HEADERS,
params=params,
timeout=15.0,
)
r.raise_for_status()
return r.json()
async def add_comment(issue_number: int, body: str) -> dict:
"""Добавить комментарий к задаче."""
async with httpx.AsyncClient() as client:
r = await client.post(
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/issues/{issue_number}/comments"),
headers={**HEADERS, "Content-Type": "application/json"},
json={"body": body},
timeout=15.0,
)
r.raise_for_status()
return r.json()
async def close_issue(issue_number: int) -> dict:
"""Закрыть задачу."""
async with httpx.AsyncClient() as client:
r = await client.patch(
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/issues/{issue_number}"),
headers={**HEADERS, "Content-Type": "application/json"},
json={"state": "closed"},
timeout=15.0,
)
r.raise_for_status()
return r.json()
async def start_stopwatch(issue_number: int) -> dict:
"""Запустить таймер по задаче."""
async with httpx.AsyncClient() as client:
r = await client.post(
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/issues/{issue_number}/stopwatch/start"),
headers=HEADERS,
timeout=15.0,
)
r.raise_for_status()
return r.json()
async def stop_stopwatch(issue_number: int) -> dict:
"""Остановить таймер по задаче."""
async with httpx.AsyncClient() as client:
r = await client.post(
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/issues/{issue_number}/stopwatch/stop"),
headers=HEADERS,
timeout=15.0,
)
r.raise_for_status()
return r.json()
async def add_tracked_time(issue_number: int, seconds: int) -> dict:
"""Добавить учтённое время к задаче (в секундах)."""
async with httpx.AsyncClient() as client:
r = await client.post(
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/issues/{issue_number}/times"),
headers={**HEADERS, "Content-Type": "application/json"},
json={"time": seconds},
timeout=15.0,
)
r.raise_for_status()
return r.json()
async def set_issue_deadline(issue_number: int, deadline: str | None) -> dict:
"""Установить или снять срок задачи. deadline — дата YYYY-MM-DD или None."""
parsed = parse_deadline(deadline) if deadline else None
due = (parsed + "T12:00:00Z") if parsed else None
async with httpx.AsyncClient() as client:
r = await client.post(
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/issues/{issue_number}/deadline"),
headers={**HEADERS, "Content-Type": "application/json"},
json={"due_date": due},
timeout=15.0,
)
r.raise_for_status()
return r.json()
async def set_issue_assignees(issue_number: int, assignees: list[str]) -> dict:
"""Заменить ответственных по задаче (список Gitea username)."""
async with httpx.AsyncClient() as client:
r = await client.patch(
_url(f"/repos/{config.REPO_OWNER}/{config.REPO_NAME}/issues/{issue_number}"),
headers={**HEADERS, "Content-Type": "application/json"},
json={"assignees": assignees},
timeout=15.0,
)
r.raise_for_status()
return r.json()