25473ed684
- Create gitea/issues_client.py with IssuesClient class (9 methods) - Create gitea/prs_client.py with PullRequestsClient class (17 methods) - Create gitea/files_client.py with FilesClient class (4 methods) - Create gitea/notifications_client.py with NotificationsClient class (2 methods) - Create gitea/repos_client.py with ReposClient class (2 methods) - Create gitea/__init__.py to export all client classes - Remove delegation methods from GiteaClient (now ~70 lines) - Update all callers to use sub-clients (client.issues, client.prs, etc.) - Update test files to mock sub-client attributes GiteaClient is now a facade that provides access to focused sub-clients: - repos: Repository operations (ReposClient) - issues: Issue operations (IssuesClient) - prs: Pull request operations (PullRequestsClient) - files: File and git ref operations (FilesClient) - notifications: Notification operations (NotificationsClient) Refs: #godclass-refactor
249 lines
8.1 KiB
Python
249 lines
8.1 KiB
Python
"""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())
|