"""Клиент 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()