196 lines
6.8 KiB
Python
196 lines
6.8 KiB
Python
"""Клиент 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()
|