Files
coding-agent-gitea/gitea/issues_client.py
T
meeks ae4e2d46ac refactor: replace Any types with specific types and update bad_code.md
- Replace Any with object or specific types across codebase
- Add ReviewRequest dataclass for PR review payloads
- Update bad_code.md: mark 5.1 (Any Type Overuse) as resolved
- Fix summary table with accurate counts and unresolved issues list
2026-07-19 15:35:17 +02:00

249 lines
8.1 KiB
Python

"""Issues client for Gitea API operations."""
import logging
from typing import 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, str | list[str]] = {"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())