fix: resolve CI issues with refactored code
This commit is contained in:
221
app/src/api/base.py
Normal file
221
app/src/api/base.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user