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()
class GiteaClient:
 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.

GiteaClient(config: config.Config)
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}"}
def get(self, path: str) -> Any:
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.

def get_text(self, path: str) -> str:
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 с токеном, возвращает сырой текст.

def get_pr(self, owner: str, repo: str, index: int) -> dict:
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...).

def get_pr_commits(self, owner: str, repo: str, index: int) -> list[str]:
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 в порядке следования.

def get_commit_diff(self, owner: str, repo: str, sha: str) -> str:
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 одного коммита.

def get_pr_diff(self, owner: str, repo: str, index: int) -> str:
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 за один запрос.

def get_raw_file(self, owner: str, repo: str, sha: str, path: str) -> str:
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}")

Возвращает содержимое файла на конкретном коммите.

def list_reviews(self, owner: str, repo: str, index: int) -> list[dict]:
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.

def post_review( self, owner: str, repo: str, index: int, body: str, event: str, commit_id: str, comments: list[dict] | None = None) -> dict:
 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': ...}.