"""Forgejo API client service for making REST API calls.""" from __future__ import annotations import asyncio from typing import TYPE_CHECKING import httpx from app.core.logging import get_logger if TYPE_CHECKING: from collections.abc import AsyncGenerator logger = get_logger(__name__) class ForgejoClientError(Exception): """Base exception for Forgejo API client errors.""" class ForgejoAPIClient: """HTTP client for Forgejo REST API calls.""" def __init__( self, base_url: str, token: str | None = None, timeout_connect: float = 5.0, timeout_read: float = 30.0, ) -> None: """Initialize the Forgejo API client.""" self.base_url = base_url.rstrip("/") self.token = token self.timeout_connect = timeout_connect self.timeout_read = timeout_read self._client: httpx.AsyncClient | None = None async def __aenter__(self) -> ForgejoAPIClient: """Enter async context manager.""" self._client = httpx.AsyncClient( base_url=self.base_url, headers=self._build_headers(), timeout=httpx.Timeout( connect=self.timeout_connect, read=self.timeout_read, write=10.0, pool=5.0, ), ) return self async def __aexit__(self, *args: object) -> None: """Exit async context manager.""" if self._client: await self._client.aclose() self._client = None def _build_headers(self) -> dict[str, str]: """Build request headers including auth and User-Agent.""" headers = { "User-Agent": "Pipeline/ForgejoClient/1.0", "Accept": "application/json", } if self.token: headers["Authorization"] = f"token {self.token}" return headers async def _get_client(self) -> httpx.AsyncClient: """Get or create the HTTP client.""" if self._client is None: raise RuntimeError("ForgejoAPIClient must be used as async context manager") return self._client async def get_user(self) -> dict[str, object]: """Get authenticated user info — useful for connection validation.""" client = await self._get_client() response = await client.get("/api/v1/user") response.raise_for_status() return response.json() async def list_issues( self, owner: str, repo: str, state: str = "open", page: int = 1, limit: int = 30, ) -> dict[str, object]: """ List issues for a repository (excluding pull requests). Args: owner: Repository owner repo: Repository name state: Issue state (open, closed, all) page: Page number limit: Items per page Returns: API response as dict """ client = await self._get_client() params = { "state": state, "page": page, "per_page": limit, "type": "issues", # Exclude pull requests } response = await client.get(f"/api/v1/repos/{owner}/{repo}/issues", params=params) response.raise_for_status() return response.json() async def close_issue( self, owner: str, repo: str, issue_number: int, ) -> dict[str, object]: """ Close a specific issue. Args: owner: Repository owner repo: Repository name issue_number: Issue number to close Returns: Updated issue data as dict """ client = await self._get_client() payload = {"state": "closed"} response = await client.patch( f"/api/v1/repos/{owner}/{repo}/issues/{issue_number}", json=payload, ) response.raise_for_status() return response.json() async def get_repository( self, owner: str, repo: str, ) -> dict[str, object]: """ Get repository metadata. Args: owner: Repository owner repo: Repository name Returns: Repository data as dict """ client = await self._get_client() response = await client.get(f"/api/v1/repos/{owner}/{repo}") response.raise_for_status() return response.json() async def list_user_repos( self, limit: int = 50, page: int = 1, ) -> list[dict[str, object]]: """ List repositories accessible to the authenticated token. Uses /api/v1/repos/search which returns repos the token can access, including private repos and org repos. Returns: List of repository dicts with name, full_name, owner, default_branch, etc. """ client = await self._get_client() params = {"limit": limit, "page": page, "token": ""} response = await client.get("/api/v1/repos/search", params={"limit": limit, "page": page}) response.raise_for_status() data = response.json() # Forgejo /repos/search returns {"data": [...], "ok": true} if isinstance(data, dict): return list(data.get("data", [])) return list(data) def get_forgejo_client( connection: object, ) -> ForgejoAPIClient: """ Factory function to create a ForgejoAPIClient from a connection object. Args: connection: ForgejoConnection object with base_url and token Returns: Configured ForgejoAPIClient instance """ base_url = connection.base_url.rstrip("/") # Remove /api/v1 if present to get base URL if "/api/v1" in base_url: import re match = re.match(r"(https?://[^/]+)", base_url) if match: base_url = match.group(1).rstrip("/") return ForgejoAPIClient( base_url=base_url, token=getattr(connection, "token", None), )