""" High-level API client that converts between API responses and domain models. """ from datetime import datetime from typing import List, Optional, Dict, Any from .http_client import GiteaHttpClient from .models import Issue, Milestone, Label, User, IssueCreateData, IssueUpdateData, MilestoneCreateData, LabelCreateData from .config import GiteaConfig from .exceptions import GiteaNotFoundError, GiteaError class GiteaApiClient: """High-level API client with domain model conversion.""" def __init__(self, config: GiteaConfig): self.config = config self.http = GiteaHttpClient(config) # Issue operations def get_issue(self, issue_number: int) -> Issue: """Get a specific issue by number.""" try: url = f"{self.config.issues_api_url}/{issue_number}" data = self.http.get(url) return self._parse_issue(data) except GiteaError as e: if "not found" in str(e).lower(): raise GiteaNotFoundError(f"Issue #{issue_number} not found") raise def list_issues(self, state: str = "all", page: int = 1, per_page: int = 50) -> List[Issue]: """List issues with optional filtering.""" params = {"page": str(page), "limit": str(per_page)} if state != "all": params["state"] = state data = self.http.get(self.config.issues_api_url, params) if not isinstance(data, list): raise GiteaError("Invalid response format: expected list of issues") return [self._parse_issue(issue_data) for issue_data in data] def create_issue(self, issue_data: IssueCreateData) -> Issue: """Create a new issue.""" payload = { "title": issue_data.title, "body": issue_data.body, } if issue_data.assignees: payload["assignees"] = issue_data.assignees if issue_data.milestone: payload["milestone"] = issue_data.milestone if issue_data.labels: # Convert label names to label IDs payload["labels"] = self._resolve_label_ids(issue_data.labels) data = self.http.post(self.config.issues_api_url, payload) return self._parse_issue(data) def update_issue(self, issue_number: int, update_data: IssueUpdateData) -> Issue: """Update an existing issue.""" payload = {} if update_data.title is not None: payload["title"] = update_data.title if update_data.body is not None: payload["body"] = update_data.body if update_data.state is not None: payload["state"] = update_data.state if update_data.assignees is not None: payload["assignees"] = update_data.assignees if update_data.milestone is not None: payload["milestone"] = update_data.milestone if update_data.labels is not None: payload["labels"] = update_data.labels url = f"{self.config.issues_api_url}/{issue_number}" data = self.http.patch(url, payload) return self._parse_issue(data) # Milestone operations def list_milestones(self, state: str = "all") -> List[Milestone]: """List repository milestones.""" params = {} if state != "all": params["state"] = state data = self.http.get(self.config.milestones_api_url, params) if not isinstance(data, list): raise GiteaError("Invalid response format: expected list of milestones") return [self._parse_milestone(milestone_data) for milestone_data in data] def create_milestone(self, milestone_data: MilestoneCreateData) -> Milestone: """Create a new milestone.""" payload = { "title": milestone_data.title, "description": milestone_data.description, } if milestone_data.due_on: payload["due_on"] = milestone_data.due_on data = self.http.post(self.config.milestones_api_url, payload) return self._parse_milestone(data) # Label operations def list_labels(self) -> List[Label]: """List repository labels.""" data = self.http.get(self.config.labels_api_url) if not isinstance(data, list): raise GiteaError("Invalid response format: expected list of labels") return [self._parse_label(label_data) for label_data in data] def create_label(self, label_data: LabelCreateData) -> Label: """Create a new label.""" payload = { "name": label_data.name, "color": label_data.color, "description": label_data.description, } data = self.http.post(self.config.labels_api_url, payload) return self._parse_label(data) # Parsing methods def _parse_issue(self, data: Dict[str, Any]) -> Issue: """Parse issue data from API response.""" try: # Parse labels labels = [] if data.get('labels'): labels = [self._parse_label(label_data) for label_data in data['labels']] # Parse assignee assignee = None if data.get('assignee'): assignee = self._parse_user(data['assignee']) # Parse milestone milestone = None if data.get('milestone'): milestone = self._parse_milestone(data['milestone']) # Check if this is an error response if 'message' in data and 'url' in data and 'number' not in data and 'id' not in data: raise GiteaError(f"API Error: {data.get('message', 'Unknown error')} (URL: {data.get('url', 'N/A')})") # Handle both 'number' and 'id' fields (Gitea API might use either) issue_number = data.get('number') or data.get('id') if issue_number is None: raise GiteaError(f"Issue response missing both 'number' and 'id' fields. Available fields: {list(data.keys())}") return Issue( number=issue_number, title=data['title'], body=data.get('body', ''), state=data['state'], created_at=self._parse_datetime(data['created_at']), updated_at=self._parse_datetime(data['updated_at']), html_url=data['html_url'], assignee=assignee, labels=labels, milestone=milestone ) except (KeyError, ValueError) as e: raise GiteaError(f"Failed to parse issue data: {e}") def _parse_milestone(self, data: Dict[str, Any]) -> Milestone: """Parse milestone data from API response.""" return Milestone( id=data['id'], title=data['title'], description=data.get('description', ''), state=data['state'], open_issues=data.get('open_issues', 0), closed_issues=data.get('closed_issues', 0), due_on=data.get('due_on'), created_at=self._parse_datetime(data.get('created_at')) if data.get('created_at') else None, updated_at=self._parse_datetime(data.get('updated_at')) if data.get('updated_at') else None ) def _parse_label(self, data: Dict[str, Any]) -> Label: """Parse label data from API response.""" return Label( id=data['id'], name=data['name'], color=data['color'], description=data.get('description', '') ) def _parse_user(self, data: Dict[str, Any]) -> User: """Parse user data from API response.""" return User( id=data['id'], login=data['login'], full_name=data.get('full_name', ''), email=data.get('email', ''), avatar_url=data.get('avatar_url', '') ) def _parse_datetime(self, date_str: str) -> datetime: """Parse datetime from API response.""" # Remove Z and microseconds for consistent parsing date_str = date_str.replace('Z', '').split('.')[0] return datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S') def _resolve_label_ids(self, label_names: List[str]) -> List[int]: """Convert label names to label IDs for API calls.""" try: # Get all labels for the repository labels_data = self.http.get(self.config.labels_api_url) if not isinstance(labels_data, list): raise GiteaError("Invalid labels response format") # Create name-to-ID mapping label_map = {label_data['name']: label_data['id'] for label_data in labels_data} # Resolve names to IDs label_ids = [] for name in label_names: if name in label_map: label_ids.append(label_map[name]) else: # If label doesn't exist, we could create it or skip it # For now, let's skip non-existent labels print(f"Warning: Label '{name}' not found, skipping") return label_ids except Exception as e: # If label resolution fails, proceed without labels rather than failing entirely print(f"Warning: Could not resolve labels: {e}") return []