"""Issues client for Gitea API operations.""" import logging from typing import Any, Callable, Optional import httpx from .models import ( CommentModel, IssueModel, LabelModel, RepositoryModel, UserModel, ) logger: logging.Logger = logging.getLogger("gitea.issues_client") class IssuesClient: """HTTP client for Gitea Issues API operations.""" def __init__( self, base_url: str, client: httpx.Client, get_user: Callable[[], UserModel] | None = None, get_repos: Callable[[], list[RepositoryModel]] | None = None, ) -> None: """Initialize the IssuesClient. Args: base_url: The base URL for the Gitea API. client: The httpx client for making requests. get_user: Optional callable to get the authenticated user. get_repos: Optional callable to get all user repos. """ self.base_url: str = base_url self.client: httpx.Client = client self._get_user: Callable[[], UserModel] | None = get_user self._get_repos: Callable[[], list[RepositoryModel]] | None = get_repos def list_repo_issues( self, owner: str, repo: str, state: str = "open" ) -> list[IssueModel]: """List issues for a repository. Args: owner: Repository owner. repo: Repository name. state: Issue state filter (open, closed, all). Returns: List of issues matching the criteria. """ url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues?type=issues&state={state}" response = self.client.get(url) response.raise_for_status() return [IssueModel(**item) for item in response.json()] def get_issue(self, owner: str, repo: str, issue_number: int) -> IssueModel: """Get a specific issue. Args: owner: Repository owner. repo: Repository name. issue_number: Issue number. Returns: The requested issue. """ url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}" response = self.client.get(url) response.raise_for_status() return IssueModel(**response.json()) def close_issue(self, owner: str, repo: str, issue_number: int) -> IssueModel: """Close an issue. Args: owner: Repository owner. repo: Repository name. issue_number: Issue number. Returns: The updated issue. """ url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}" data: dict[str, str] = {"state": "closed"} response = self.client.patch(url, json=data) response.raise_for_status() return IssueModel(**response.json()) def get_issue_comments( self, owner: str, repo: str, issue_number: int ) -> list[CommentModel]: """Get comments on an issue. Args: owner: Repository owner. repo: Repository name. issue_number: Issue number. Returns: List of comments on the issue. """ url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments" response = self.client.get(url) response.raise_for_status() return [CommentModel(**item) for item in response.json()] def list_assigned_issues(self, owner: str = "", repo: str = "") -> list[IssueModel]: """List issues assigned to the authenticated user. Args: owner: Optional repository owner to filter by. repo: Optional repository name to filter by. Returns: List of issues assigned to the authenticated user. """ try: if self._get_user is None or self._get_repos is None: logger.error("get_user and get_repos callables are required") return [] user = self._get_user() if not user: return [] username: str = user.login if owner and repo: response = self.client.get( f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues?assignee={username}&state=open&type=issues", ) response.raise_for_status() return [IssueModel(**item) for item in response.json()] all_issues: list[IssueModel] = [] repos = self._get_repos() for r in repos: repo_owner = r.owner repo_name = r.name resp = self.client.get( f"{self.base_url}/api/v1/repos/{repo_owner}/{repo_name}/issues?assignee={username}&state=open&type=issues", ) if resp.status_code == 200: for item in resp.json(): issue = IssueModel(**item) # Backfill repository if Gitea omitted it if issue.repository is None: issue = issue.model_copy(update={"repository": r}) all_issues.append(issue) return all_issues except Exception as e: logger.error(f"Error listing assigned issues: {e}", exc_info=True) return [] def assign_issue( self, owner: str, repo: str, issue_number: int, username: str ) -> IssueModel: """Assign an issue to a user. Args: owner: Repository owner. repo: Repository name. issue_number: Issue number. username: Username to assign. Returns: The updated issue. """ url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}" data: dict[str, list[str]] = {"assignees": [username]} response = self.client.patch(url, json=data) response.raise_for_status() return IssueModel(**response.json()) def create_issue( self, owner: str, repo: str, title: str, body: str, labels: list[str] | None = None, assignees: list[str] | None = None, ) -> IssueModel: """Create a new issue. Args: owner: Repository owner. repo: Repository name. title: Issue title. body: Issue body/description. labels: Optional list of label IDs. assignees: Optional list of usernames to assign. Returns: The created issue. """ url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues" data: dict[str, Any] = {"title": title, "body": body} if labels: data["labels"] = labels if assignees: data["assignees"] = assignees response = self.client.post(url, json=data) response.raise_for_status() return IssueModel(**response.json()) def add_comment( self, owner: str, repo: str, issue_number: int, body: str ) -> CommentModel: """Add a comment to an issue. Args: owner: Repository owner. repo: Repository name. issue_number: Issue number. body: Comment body. Returns: The created comment. """ url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments" data: dict[str, str] = {"body": body} response = self.client.post(url, json=data) response.raise_for_status() return CommentModel(**response.json()) def add_label( self, owner: str, repo: str, issue_number: int, label: str ) -> LabelModel: """Add a label to an issue. Args: owner: Repository owner. repo: Repository name. issue_number: Issue number. label: Label name or ID. Returns: The added label. """ url = ( f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}/labels" ) data: list[str] = [label] response = self.client.post(url, json=data) response.raise_for_status() return LabelModel(**response.json())