gitea_client
Обёртка над Gitea API.
Бот работает через публичный REST API: читает данные PR и диффы, постит ревью. Для приватных репозиториев используется токен из env.
1""" 2Обёртка над Gitea API. 3 4Бот работает через публичный REST API: читает данные PR и диффы, 5постит ревью. Для приватных репозиториев используется токен из env. 6""" 7 8from __future__ import annotations 9 10from typing import Any 11 12import httpx 13from loguru import logger 14 15from config import Config 16 17 18class GiteaClient: 19 """Тонкий клиент для нужных эндпоинтов Gitea API.""" 20 21 def __init__(self, config: Config): 22 self._base = config.gitea_url.rstrip("/") + "/api/v1" 23 self._timeout = httpx.Timeout(60.0) 24 self._headers = {"Authorization": f"token {config.gitea_token}"} 25 26 def get(self, path: str) -> Any: 27 """GET-запрос к Gitea API с токеном, возвращает JSON.""" 28 with httpx.Client(timeout=self._timeout, headers=self._headers) as client: 29 resp = client.get(f"{self._base}{path}") 30 resp.raise_for_status() 31 return resp.json() 32 33 def get_text(self, path: str) -> str: 34 """GET-запрос к Gitea API с токеном, возвращает сырой текст.""" 35 with httpx.Client(timeout=self._timeout, headers=self._headers) as client: 36 resp = client.get(f"{self._base}{path}") 37 resp.raise_for_status() 38 return resp.text 39 40 def get_pr(self, owner: str, repo: str, index: int) -> dict: 41 """Возвращает данные PR (head/base sha, title, draft...).""" 42 return self.get(f"/repos/{owner}/{repo}/pulls/{index}") 43 44 def get_pr_commits(self, owner: str, repo: str, index: int) -> list[str]: 45 """Возвращает список sha коммитов PR в порядке следования.""" 46 data = self.get(f"/repos/{owner}/{repo}/pulls/{index}/commits") 47 return [c["sha"] for c in data] 48 49 def get_commit_diff(self, owner: str, repo: str, sha: str) -> str: 50 """Возвращает unified diff одного коммита.""" 51 return self.get_text(f"/repos/{owner}/{repo}/git/commits/{sha}.diff") 52 53 def get_pr_diff(self, owner: str, repo: str, index: int) -> str: 54 """Возвращает совокупный unified diff PR (vs base-ветки). 55 56 В Gitea 1.19 compare API недоступен (404), поэтому используем 57 pulls/{index}.diff, который отдаёт полный дифф PR за один запрос. 58 """ 59 return self.get_text(f"/repos/{owner}/{repo}/pulls/{index}.diff") 60 61 def get_raw_file(self, owner: str, repo: str, sha: str, path: str) -> str: 62 """Возвращает содержимое файла на конкретном коммите.""" 63 encoded = path.replace("/", "%2F") 64 return self.get_text(f"/repos/{owner}/{repo}/raw/{sha}/{encoded}") 65 66 def list_reviews(self, owner: str, repo: str, index: int) -> list[dict]: 67 """Возвращает список ревью PR.""" 68 return self.get(f"/repos/{owner}/{repo}/pulls/{index}/reviews") 69 70 def post_review( 71 self, 72 owner: str, 73 repo: str, 74 index: int, 75 body: str, 76 event: str, 77 commit_id: str, 78 comments: list[dict] | None = None, 79 ) -> dict: 80 """ 81 Постит ревью на PR. 82 83 event: 'COMMENT' | 'APPROVED' | 'REQUEST_CHANGES'. 84 comments: список {'path': ..., 'body': ..., 'new_position': ..., 'old_position': ...}. 85 """ 86 payload: dict[str, Any] = { 87 "body": body, 88 "event": event, 89 "commit_id": commit_id, 90 } 91 if comments: 92 payload["comments"] = comments 93 with httpx.Client(timeout=self._timeout, headers=self._headers) as client: 94 resp = client.post( 95 f"{self._base}/repos/{owner}/{repo}/pulls/{index}/reviews", 96 json=payload, 97 ) 98 if resp.status_code >= 400: 99 logger.error( 100 f"Gitea: не удалось создать ревью: {resp.status_code} {resp.text[:300]}" 101 ) 102 resp.raise_for_status() 103 return resp.json()
19class GiteaClient: 20 """Тонкий клиент для нужных эндпоинтов Gitea API.""" 21 22 def __init__(self, config: Config): 23 self._base = config.gitea_url.rstrip("/") + "/api/v1" 24 self._timeout = httpx.Timeout(60.0) 25 self._headers = {"Authorization": f"token {config.gitea_token}"} 26 27 def get(self, path: str) -> Any: 28 """GET-запрос к Gitea API с токеном, возвращает JSON.""" 29 with httpx.Client(timeout=self._timeout, headers=self._headers) as client: 30 resp = client.get(f"{self._base}{path}") 31 resp.raise_for_status() 32 return resp.json() 33 34 def get_text(self, path: str) -> str: 35 """GET-запрос к Gitea API с токеном, возвращает сырой текст.""" 36 with httpx.Client(timeout=self._timeout, headers=self._headers) as client: 37 resp = client.get(f"{self._base}{path}") 38 resp.raise_for_status() 39 return resp.text 40 41 def get_pr(self, owner: str, repo: str, index: int) -> dict: 42 """Возвращает данные PR (head/base sha, title, draft...).""" 43 return self.get(f"/repos/{owner}/{repo}/pulls/{index}") 44 45 def get_pr_commits(self, owner: str, repo: str, index: int) -> list[str]: 46 """Возвращает список sha коммитов PR в порядке следования.""" 47 data = self.get(f"/repos/{owner}/{repo}/pulls/{index}/commits") 48 return [c["sha"] for c in data] 49 50 def get_commit_diff(self, owner: str, repo: str, sha: str) -> str: 51 """Возвращает unified diff одного коммита.""" 52 return self.get_text(f"/repos/{owner}/{repo}/git/commits/{sha}.diff") 53 54 def get_pr_diff(self, owner: str, repo: str, index: int) -> str: 55 """Возвращает совокупный unified diff PR (vs base-ветки). 56 57 В Gitea 1.19 compare API недоступен (404), поэтому используем 58 pulls/{index}.diff, который отдаёт полный дифф PR за один запрос. 59 """ 60 return self.get_text(f"/repos/{owner}/{repo}/pulls/{index}.diff") 61 62 def get_raw_file(self, owner: str, repo: str, sha: str, path: str) -> str: 63 """Возвращает содержимое файла на конкретном коммите.""" 64 encoded = path.replace("/", "%2F") 65 return self.get_text(f"/repos/{owner}/{repo}/raw/{sha}/{encoded}") 66 67 def list_reviews(self, owner: str, repo: str, index: int) -> list[dict]: 68 """Возвращает список ревью PR.""" 69 return self.get(f"/repos/{owner}/{repo}/pulls/{index}/reviews") 70 71 def post_review( 72 self, 73 owner: str, 74 repo: str, 75 index: int, 76 body: str, 77 event: str, 78 commit_id: str, 79 comments: list[dict] | None = None, 80 ) -> dict: 81 """ 82 Постит ревью на PR. 83 84 event: 'COMMENT' | 'APPROVED' | 'REQUEST_CHANGES'. 85 comments: список {'path': ..., 'body': ..., 'new_position': ..., 'old_position': ...}. 86 """ 87 payload: dict[str, Any] = { 88 "body": body, 89 "event": event, 90 "commit_id": commit_id, 91 } 92 if comments: 93 payload["comments"] = comments 94 with httpx.Client(timeout=self._timeout, headers=self._headers) as client: 95 resp = client.post( 96 f"{self._base}/repos/{owner}/{repo}/pulls/{index}/reviews", 97 json=payload, 98 ) 99 if resp.status_code >= 400: 100 logger.error( 101 f"Gitea: не удалось создать ревью: {resp.status_code} {resp.text[:300]}" 102 ) 103 resp.raise_for_status() 104 return resp.json()
Тонкий клиент для нужных эндпоинтов Gitea API.
27 def get(self, path: str) -> Any: 28 """GET-запрос к Gitea API с токеном, возвращает JSON.""" 29 with httpx.Client(timeout=self._timeout, headers=self._headers) as client: 30 resp = client.get(f"{self._base}{path}") 31 resp.raise_for_status() 32 return resp.json()
GET-запрос к Gitea API с токеном, возвращает JSON.
34 def get_text(self, path: str) -> str: 35 """GET-запрос к Gitea API с токеном, возвращает сырой текст.""" 36 with httpx.Client(timeout=self._timeout, headers=self._headers) as client: 37 resp = client.get(f"{self._base}{path}") 38 resp.raise_for_status() 39 return resp.text
GET-запрос к Gitea API с токеном, возвращает сырой текст.
41 def get_pr(self, owner: str, repo: str, index: int) -> dict: 42 """Возвращает данные PR (head/base sha, title, draft...).""" 43 return self.get(f"/repos/{owner}/{repo}/pulls/{index}")
Возвращает данные PR (head/base sha, title, draft...).
45 def get_pr_commits(self, owner: str, repo: str, index: int) -> list[str]: 46 """Возвращает список sha коммитов PR в порядке следования.""" 47 data = self.get(f"/repos/{owner}/{repo}/pulls/{index}/commits") 48 return [c["sha"] for c in data]
Возвращает список sha коммитов PR в порядке следования.
50 def get_commit_diff(self, owner: str, repo: str, sha: str) -> str: 51 """Возвращает unified diff одного коммита.""" 52 return self.get_text(f"/repos/{owner}/{repo}/git/commits/{sha}.diff")
Возвращает unified diff одного коммита.
54 def get_pr_diff(self, owner: str, repo: str, index: int) -> str: 55 """Возвращает совокупный unified diff PR (vs base-ветки). 56 57 В Gitea 1.19 compare API недоступен (404), поэтому используем 58 pulls/{index}.diff, который отдаёт полный дифф PR за один запрос. 59 """ 60 return self.get_text(f"/repos/{owner}/{repo}/pulls/{index}.diff")
Возвращает совокупный unified diff PR (vs base-ветки).
В Gitea 1.19 compare API недоступен (404), поэтому используем pulls/{index}.diff, который отдаёт полный дифф PR за один запрос.
62 def get_raw_file(self, owner: str, repo: str, sha: str, path: str) -> str: 63 """Возвращает содержимое файла на конкретном коммите.""" 64 encoded = path.replace("/", "%2F") 65 return self.get_text(f"/repos/{owner}/{repo}/raw/{sha}/{encoded}")
Возвращает содержимое файла на конкретном коммите.
67 def list_reviews(self, owner: str, repo: str, index: int) -> list[dict]: 68 """Возвращает список ревью PR.""" 69 return self.get(f"/repos/{owner}/{repo}/pulls/{index}/reviews")
Возвращает список ревью PR.
71 def post_review( 72 self, 73 owner: str, 74 repo: str, 75 index: int, 76 body: str, 77 event: str, 78 commit_id: str, 79 comments: list[dict] | None = None, 80 ) -> dict: 81 """ 82 Постит ревью на PR. 83 84 event: 'COMMENT' | 'APPROVED' | 'REQUEST_CHANGES'. 85 comments: список {'path': ..., 'body': ..., 'new_position': ..., 'old_position': ...}. 86 """ 87 payload: dict[str, Any] = { 88 "body": body, 89 "event": event, 90 "commit_id": commit_id, 91 } 92 if comments: 93 payload["comments"] = comments 94 with httpx.Client(timeout=self._timeout, headers=self._headers) as client: 95 resp = client.post( 96 f"{self._base}/repos/{owner}/{repo}/pulls/{index}/reviews", 97 json=payload, 98 ) 99 if resp.status_code >= 400: 100 logger.error( 101 f"Gitea: не удалось создать ревью: {resp.status_code} {resp.text[:300]}" 102 ) 103 resp.raise_for_status() 104 return resp.json()
Постит ревью на PR.
event: 'COMMENT' | 'APPROVED' | 'REQUEST_CHANGES'. comments: список {'path': ..., 'body': ..., 'new_position': ..., 'old_position': ...}.