diff --git a/forklet/core/orchestrator.py b/forklet/core/orchestrator.py index c8dd3d9..c4b22a9 100644 --- a/forklet/core/orchestrator.py +++ b/forklet/core/orchestrator.py @@ -24,6 +24,7 @@ from .progress_tracker import ProgressTracker from .state_controller import StateController +from forklet.infrastructure import RateLimitInfo from forklet.infrastructure.logger import logger diff --git a/forklet/infrastructure/__init__.py b/forklet/infrastructure/__init__.py index 58b0251..422f267 100644 --- a/forklet/infrastructure/__init__.py +++ b/forklet/infrastructure/__init__.py @@ -1,15 +1,16 @@ from .error_handler import ( - DownloadError, RateLimitError, RateLimitError, + DownloadError, RateLimitError, AuthenticationError, RepositoryNotFoundError, handle_api_error, retry_on_error ) -from .rate_limiter import RateLimiter +from .rate_limiter import RateLimiter, RateLimitInfo from .retry_manager import RetryManager +from .cache_manager import CacheManager, CacheEntry __all__ = [ - DownloadError, RateLimitError, RateLimitError, + DownloadError, RateLimitError, AuthenticationError, RepositoryNotFoundError, handle_api_error, retry_on_error, RateLimiter, - RetryManager + RetryManager, RateLimitInfo, CacheManager, CacheEntry ] \ No newline at end of file diff --git a/forklet/infrastructure/error_handler.py b/forklet/infrastructure/error_handler.py index a3f8e2f..b45036e 100644 --- a/forklet/infrastructure/error_handler.py +++ b/forklet/infrastructure/error_handler.py @@ -11,23 +11,17 @@ from forklet.infrastructure.logger import logger - - #### ## DOWNLOAD ERROR MODEL ##### class DownloadError(Exception): """Base exception for download-related errors.""" - - def __init__( - self, - message: str, - original_error: Optional[Exception] = None - ): + + def __init__(self, message: str, original_error: Optional[Exception] = None): super().__init__(message) self.original_error = original_error self.message = message - + def __str__(self) -> str: if self.original_error: return f"{self.message} (Original: {self.original_error})" @@ -39,6 +33,7 @@ def __str__(self) -> str: ##### class RateLimitError(DownloadError): """Exception raised when rate limits are exceeded.""" + pass @@ -47,6 +42,7 @@ class RateLimitError(DownloadError): ##### class AuthenticationError(DownloadError): """Exception raised for authentication failures.""" + pass @@ -55,20 +51,20 @@ class AuthenticationError(DownloadError): ##### class RepositoryNotFoundError(DownloadError): """Exception raised when repository is not found.""" - pass + pass #### -## RROR HANDLER UTILITIES +## ERROR HANDLER UTILITIES ##### def handle_api_error(func: Callable) -> Callable: """ Decorator to handle API errors and convert to appropriate exceptions. - + Args: func: Function to decorate - + Returns: Decorated function """ @@ -81,8 +77,7 @@ def wrapper(*args, **kwargs) -> Any: # Cse of GH Exceptions except GithubException as e: - - if e.status == 403 and 'rate limit' in str(e).lower(): + if e.status == 403 and "rate limit" in str(e).lower(): raise RateLimitError("GitHub API rate limit exceeded", e) from e elif e.status == 401 or e.status == 403: @@ -96,8 +91,7 @@ def wrapper(*args, **kwargs) -> Any: # Request Exceptions except httpx.RequestError as e: - - if '429' in str(e) or 'rate limit' in str(e).lower(): + if "429" in str(e) or "rate limit" in str(e).lower(): raise RateLimitError("Rate limit exceeded", e) from e else: @@ -105,36 +99,31 @@ def wrapper(*args, **kwargs) -> Any: except Exception as e: raise DownloadError(f"Unexpected error: {e}", e) from e - + return wrapper def retry_on_error(max_retries: int = 3) -> Callable: """ Decorator to retry operations on specific errors. - + Args: max_retries: Maximum number of retry attempts - + Returns: - Decorated function + Decorator function """ def decorator(func: Callable) -> Callable: - + @functools.wraps(func) def wrapper(*args, **kwargs) -> Any: last_exception = None - + for attempt in range(max_retries + 1): try: return func(*args, **kwargs) - except ( - RateLimitError, - httpx.RequestError, - ConnectionError - ) as e: - + except (RateLimitError, httpx.RequestError, ConnectionError) as e: last_exception = e if attempt < max_retries: logger.warning( @@ -147,7 +136,9 @@ def wrapper(*args, **kwargs) -> Any: # Don't retry on other errors logger.error(f"Non-retryable error: {e}") raise - + raise last_exception or Exception("All retry attempts failed") + return wrapper + return decorator diff --git a/forklet/models/download.py b/forklet/models/download.py index 49d0b7a..3bfdbbe 100644 --- a/forklet/models/download.py +++ b/forklet/models/download.py @@ -11,7 +11,7 @@ from datetime import datetime from enum import Enum from pathlib import Path -from typing import Dict, List, Optional, Set +from typing import Dict, List, Optional, Set, Any from .github import RepositoryInfo, GitReference diff --git a/forklet/services/github_api.py b/forklet/services/github_api.py index eb49919..9e95340 100644 --- a/forklet/services/github_api.py +++ b/forklet/services/github_api.py @@ -2,22 +2,26 @@ Service for interacting with GitHub API with rate limiting and error handling. """ -from typing import List, Optional, Dict, Any, AsyncIterator - +from typing import List, Optional, Dict, Any, AsyncIterator, Callable +import datetime import asyncio import httpx from github import Github, GithubException # from github.Repository import Repository as GithubRepository -from ..infrastructure.rate_limiter import RateLimiter -from ..infrastructure.retry_manager import RetryManager -from ..infrastructure.error_handler import ( +# from ..infrastructure.rate_limiter import RateLimiter +# from ..infrastructure.retry_manager import RetryManager +from ..infrastructure import ( handle_api_error, RateLimitError, RepositoryNotFoundError, DownloadError, + RateLimiter, + RetryManager, + CacheManager, + RateLimitInfo ) -from ..infrastructure.cache_manager import CacheManager +# from ..infrastructure.cache_manager import CacheManager from ..models import RepositoryInfo, GitReference, RepositoryType, GitHubFile from ..models.constants import USER_AGENT @@ -75,19 +79,19 @@ def _on_rate_limit_update(self, rate_limit_info: RateLimitInfo) -> None: # Configure HTTP client headers = {"Accept": "application/vnd.github.v3+json", "User-Agent": USER_AGENT} - if auth_token: - headers["Authorization"] = f"token {auth_token}" + if self.auth_token: + headers["Authorization"] = f"token {self.auth_token}" self.http_client = httpx.AsyncClient( - headers=headers, timeout=httpx.Timeout(timeout) + headers=headers, timeout=httpx.Timeout(self.timeout) ) # Sync client for PyGithub (used only for metadata) self.github_client = ( Github( - auth_token, retry=self.retry_manager.max_retries, user_agent=USER_AGENT + self.auth_token, retry=self.retry_manager.max_retries, user_agent=USER_AGENT ) - if auth_token + if self.auth_token else Github(retry=self.retry_manager.max_retries, user_agent=USER_AGENT) ) @@ -129,19 +133,19 @@ async def update_rate_limit_info(self, headers: Dict[str, str]) -> None: # Configure HTTP client headers = {"Accept": "application/vnd.github.v3+json", "User-Agent": USER_AGENT} - if auth_token: - headers["Authorization"] = f"token {auth_token}" + if self.auth_token: + headers["Authorization"] = f"token {self.auth_token}" self.http_client = httpx.AsyncClient( - headers=headers, timeout=httpx.Timeout(timeout) + headers=headers, timeout=httpx.Timeout(self.timeout) ) # Sync client for PyGithub (used only for metadata) self.github_client = ( Github( - auth_token, retry=self.retry_manager.max_retries, user_agent=USER_AGENT + self.auth_token, retry=self.retry_manager.max_retries, user_agent=USER_AGENT ) - if auth_token + if self.auth_token else Github(retry=self.retry_manager.max_retries, user_agent=USER_AGENT) )