- Create new gitea/ package with clean API facade - Establish proper separation of concerns: tddai uses gitea, not vice versa - Replace duplicate curl+subprocess patterns with unified HTTP client - Add rich domain models with properties (issue.priority, issue.status) - Maintain full backwards compatibility in tddai modules - Reduce code complexity: -373 lines, +151 lines (net -222 lines) - Improve testability and maintainability through clean interfaces Architecture: - gitea.client.GiteaClient - main facade with sub-clients - gitea.api_client - high-level API with model conversion - gitea.http_client - low-level HTTP operations - gitea.models - rich domain objects (Issue, Milestone, Label) - gitea.config - gitea-specific configuration - gitea.exceptions - clean exception hierarchy 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
195 lines
7.0 KiB
Python
195 lines
7.0 KiB
Python
"""
|
|
Main Gitea client facade.
|
|
|
|
This provides a clean, organized interface for all Gitea operations,
|
|
following the facade pattern to hide complexity and provide a stable API.
|
|
"""
|
|
|
|
from typing import List, Optional
|
|
|
|
from .config import GiteaConfig
|
|
from .api_client import GiteaApiClient
|
|
from .models import Issue, Milestone, Label, IssueCreateData, IssueUpdateData, MilestoneCreateData, LabelCreateData, ProjectState, Priority
|
|
|
|
|
|
class IssuesClient:
|
|
"""Client for issue operations."""
|
|
|
|
def __init__(self, api_client: GiteaApiClient):
|
|
self._api = api_client
|
|
|
|
def get(self, issue_number: int) -> Issue:
|
|
"""Get a specific issue by number."""
|
|
return self._api.get_issue(issue_number)
|
|
|
|
def list(self, state: str = "all", page: int = 1, per_page: int = 50) -> List[Issue]:
|
|
"""List issues with optional filtering."""
|
|
return self._api.list_issues(state, page, per_page)
|
|
|
|
def list_open(self) -> List[Issue]:
|
|
"""List only open issues."""
|
|
return self._api.list_issues("open")
|
|
|
|
def list_closed(self) -> List[Issue]:
|
|
"""List only closed issues."""
|
|
return self._api.list_issues("closed")
|
|
|
|
def create(self, title: str, body: str = "", **kwargs) -> Issue:
|
|
"""Create a new issue."""
|
|
issue_data = IssueCreateData(
|
|
title=title,
|
|
body=body,
|
|
assignees=kwargs.get('assignees', []),
|
|
milestone=kwargs.get('milestone'),
|
|
labels=kwargs.get('labels', [])
|
|
)
|
|
return self._api.create_issue(issue_data)
|
|
|
|
def update(self, issue_number: int, **kwargs) -> Issue:
|
|
"""Update an existing issue."""
|
|
update_data = IssueUpdateData(
|
|
title=kwargs.get('title'),
|
|
body=kwargs.get('body'),
|
|
state=kwargs.get('state'),
|
|
assignees=kwargs.get('assignees'),
|
|
milestone=kwargs.get('milestone'),
|
|
labels=kwargs.get('labels')
|
|
)
|
|
return self._api.update_issue(issue_number, update_data)
|
|
|
|
def close(self, issue_number: int) -> Issue:
|
|
"""Close an issue."""
|
|
return self.update(issue_number, state="closed")
|
|
|
|
def reopen(self, issue_number: int) -> Issue:
|
|
"""Reopen an issue."""
|
|
return self.update(issue_number, state="open")
|
|
|
|
def add_labels(self, issue_number: int, labels: List[str]) -> Issue:
|
|
"""Add labels to an issue."""
|
|
issue = self.get(issue_number)
|
|
existing_labels = [label.name for label in issue.labels]
|
|
new_labels = list(set(existing_labels + labels))
|
|
return self.update(issue_number, labels=new_labels)
|
|
|
|
def remove_labels(self, issue_number: int, labels: List[str]) -> Issue:
|
|
"""Remove labels from an issue."""
|
|
issue = self.get(issue_number)
|
|
existing_labels = [label.name for label in issue.labels]
|
|
new_labels = [label for label in existing_labels if label not in labels]
|
|
return self.update(issue_number, labels=new_labels)
|
|
|
|
def set_priority(self, issue_number: int, priority: Priority) -> Issue:
|
|
"""Set issue priority."""
|
|
issue = self.get(issue_number)
|
|
labels = [label.name for label in issue.labels if not label.name.startswith('priority:')]
|
|
labels.append(priority.value)
|
|
return self.update(issue_number, labels=labels)
|
|
|
|
def set_status(self, issue_number: int, status: ProjectState) -> Issue:
|
|
"""Set issue status."""
|
|
issue = self.get(issue_number)
|
|
labels = [label.name for label in issue.labels if not label.name.startswith('status:')]
|
|
labels.append(status.value)
|
|
return self.update(issue_number, labels=labels)
|
|
|
|
|
|
class MilestonesClient:
|
|
"""Client for milestone operations."""
|
|
|
|
def __init__(self, api_client: GiteaApiClient):
|
|
self._api = api_client
|
|
|
|
def list(self, state: str = "all") -> List[Milestone]:
|
|
"""List milestones."""
|
|
return self._api.list_milestones(state)
|
|
|
|
def list_open(self) -> List[Milestone]:
|
|
"""List open milestones."""
|
|
return self._api.list_milestones("open")
|
|
|
|
def list_closed(self) -> List[Milestone]:
|
|
"""List closed milestones."""
|
|
return self._api.list_milestones("closed")
|
|
|
|
def create(self, title: str, description: str = "", due_on: str = None) -> Milestone:
|
|
"""Create a new milestone."""
|
|
milestone_data = MilestoneCreateData(
|
|
title=title,
|
|
description=description,
|
|
due_on=due_on
|
|
)
|
|
return self._api.create_milestone(milestone_data)
|
|
|
|
|
|
class LabelsClient:
|
|
"""Client for label operations."""
|
|
|
|
def __init__(self, api_client: GiteaApiClient):
|
|
self._api = api_client
|
|
|
|
def list(self) -> List[Label]:
|
|
"""List all labels."""
|
|
return self._api.list_labels()
|
|
|
|
def create(self, name: str, color: str, description: str = "") -> Label:
|
|
"""Create a new label."""
|
|
label_data = LabelCreateData(
|
|
name=name,
|
|
color=color,
|
|
description=description
|
|
)
|
|
return self._api.create_label(label_data)
|
|
|
|
def ensure_project_labels(self) -> None:
|
|
"""Ensure all standard project management labels exist."""
|
|
existing_labels = [label.name for label in self.list()]
|
|
|
|
# Define standard project labels
|
|
standard_labels = [
|
|
("status:todo", "d73a4a", "Ready to work on"),
|
|
("status:active", "0075ca", "Currently being worked on"),
|
|
("status:review", "fbca04", "Ready for review"),
|
|
("status:done", "0e8a16", "Completed work"),
|
|
("status:blocked", "b60205", "Blocked by dependencies"),
|
|
("priority:low", "c5def5", "Low priority"),
|
|
("priority:medium", "a2eeef", "Medium priority"),
|
|
("priority:high", "fef2c0", "High priority"),
|
|
("priority:critical", "d93f0b", "Critical priority"),
|
|
]
|
|
|
|
for name, color, description in standard_labels:
|
|
if name not in existing_labels:
|
|
self.create(name, color, description)
|
|
|
|
|
|
class GiteaClient:
|
|
"""Main Gitea client facade."""
|
|
|
|
def __init__(self, config: Optional[GiteaConfig] = None):
|
|
"""Initialize Gitea client.
|
|
|
|
Args:
|
|
config: GiteaConfig instance. If None, loads from environment.
|
|
"""
|
|
if config is None:
|
|
config = GiteaConfig.from_environment()
|
|
config.validate()
|
|
|
|
self.config = config
|
|
self._api = GiteaApiClient(config)
|
|
|
|
# Initialize sub-clients
|
|
self.issues = IssuesClient(self._api)
|
|
self.milestones = MilestonesClient(self._api)
|
|
self.labels = LabelsClient(self._api)
|
|
|
|
@classmethod
|
|
def from_tddai_config(cls, tddai_config) -> 'GiteaClient':
|
|
"""Create client from legacy TddaiConfig for backwards compatibility."""
|
|
gitea_config = GiteaConfig.from_tddai_config(tddai_config)
|
|
return cls(gitea_config)
|
|
|
|
def setup_project_management(self) -> None:
|
|
"""Setup standard project management labels and structure."""
|
|
self.labels.ensure_project_labels() |