From 18184aa3515432d0c4a5ed8cea67366e358b2646 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Sun, 1 Feb 2026 07:21:30 +0000 Subject: [PATCH] fix: resolve CI issues with refactored code --- app/src/api/base.py | 221 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 app/src/api/base.py diff --git a/app/src/api/base.py b/app/src/api/base.py new file mode 100644 index 0000000..3a6ca61 --- /dev/null +++ b/app/src/api/base.py @@ -0,0 +1,221 @@ +"""Base API client with common functionality.""" + +import os +import time +from abc import ABC, abstractmethod +from collections.abc import Iterator +from typing import Any + + +class APIClientError(Exception): + """Base exception for API client errors.""" + + pass + + +class RateLimitError(APIClientError): + """Exception raised when API rate limit is exceeded.""" + + pass + + +class AuthenticationError(APIClientError): + """Exception raised for authentication failures.""" + + pass + + +class BaseAPIClient(ABC): + """Abstract base class for API clients.""" + + def __init__(self, repo: str): + """Initialize the API client. + + Args: + repo: Repository identifier (owner/repo format). + """ + self.repo = repo + self.owner, self.repo_name = self._parse_repo(repo) + self.max_retries = 3 + self.retry_delay = 1.0 + + def _parse_repo(self, repo: str) -> tuple[str, str]: + """Parse repository identifier into owner and name. + + Args: + repo: Repository identifier (owner/repo or just repo name). + + Returns: + Tuple of (owner, repo_name). + """ + if "/" in repo: + parts = repo.split("/") + return parts[0], parts[-1] + return "", repo + + def _get_token(self, token_env: str) -> str | None: + """Get API token from environment variable. + + Args: + token_env: Environment variable name for the token. + + Returns: + Token string or None if not found. + """ + return os.getenv(token_env) + + @abstractmethod + def get_current_user(self) -> str: + """Get the authenticated username. + + Returns: + Username string. + """ + pass + + @abstractmethod + def get_repository(self) -> dict: + """Get repository information. + + Returns: + Repository data dictionary. + """ + pass + + @abstractmethod + def get_pull_requests(self, state: str = "open") -> list[dict]: + """Get pull requests. + + Args: + state: PR state (open, closed, all). + + Returns: + List of PR data dictionaries. + """ + pass + + @abstractmethod + def get_issues(self, state: str = "open") -> list[dict]: + """Get issues. + + Args: + state: Issue state (open, closed, all). + + Returns: + List of issue data dictionaries. + """ + pass + + @abstractmethod + def get_workflows(self) -> list[dict]: + """Get workflows. + + Returns: + List of workflow data dictionaries. + """ + pass + + @abstractmethod + def get_workflow_runs(self, limit: int = 10) -> list[dict]: + """Get workflow runs. + + Args: + limit: Maximum number of runs to return. + + Returns: + List of workflow run data dictionaries. + """ + pass + + def _check_response_status(self, response: Any) -> None: + """Check response status and raise appropriate errors. + + Args: + response: Response object to check. + + Raises: + AuthenticationError: On 401 status. + RateLimitError: On 403 with rate limit message. + APIClientError: On 404 or other client errors. + """ + if response.status_code == 401: + raise AuthenticationError("Authentication failed. Check your API token.") + + if response.status_code == 403: + if "rate limit" in response.text.lower(): + raise RateLimitError("API rate limit exceeded.") + + if response.status_code == 404: + raise APIClientError("Resource not found") + + response.raise_for_status() + + def _request_with_retry( + self, + method: str, + url: str, + **kwargs, + ) -> dict: + """Make an API request with retry logic. + + Args: + method: HTTP method. + url: Request URL. + **kwargs: Additional request arguments. + + Returns: + Response JSON data. + """ + last_error = None + + for attempt in range(self.max_retries): + try: + response = self._make_request(method, url, **kwargs) + self._check_response_status(response) + return response.json() + + except RateLimitError: + wait_time = self.retry_delay * (2 ** attempt) + time.sleep(wait_time) + except AuthenticationError: + raise + except APIClientError: + if attempt < self.max_retries - 1: + wait_time = self.retry_delay * (2 ** attempt) + time.sleep(wait_time) + else: + raise + except Exception as e: + last_error = e + if attempt < self.max_retries - 1: + wait_time = self.retry_delay * (2 ** attempt) + time.sleep(wait_time) + + raise APIClientError(f"Request failed after {self.max_retries} attempts: {last_error}") + + @abstractmethod + def _make_request(self, method: str, url: str, **kwargs) -> Any: + """Make an HTTP request. + + Args: + method: HTTP method. + url: Request URL. + **kwargs: Additional request arguments. + + Returns: + Response object. + """ + pass + + @abstractmethod + def _paginate(self, url: str, **kwargs) -> Iterator[dict]: + """Iterate over paginated API results. + + Args: + url: API endpoint URL. + **kwargs: Additional request arguments. + + Yields: + Resource dictionaries from each page. + """ + pass