Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions forklet/core/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
9 changes: 5 additions & 4 deletions forklet/infrastructure/__init__.py
Original file line number Diff line number Diff line change
@@ -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
]
51 changes: 21 additions & 30 deletions forklet/infrastructure/error_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})"
Expand All @@ -39,6 +33,7 @@ def __str__(self) -> str:
#####
class RateLimitError(DownloadError):
"""Exception raised when rate limits are exceeded."""

pass


Expand All @@ -47,6 +42,7 @@ class RateLimitError(DownloadError):
#####
class AuthenticationError(DownloadError):
"""Exception raised for authentication failures."""

pass


Expand All @@ -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
"""
Expand All @@ -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:
Expand All @@ -96,45 +91,39 @@ 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:
raise DownloadError(f"Network error: {e}", e) from e

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(
Expand All @@ -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
2 changes: 1 addition & 1 deletion forklet/models/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
36 changes: 20 additions & 16 deletions forklet/services/github_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
)

Expand Down Expand Up @@ -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)
)

Expand Down
Loading