mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
Add RateLimitError and handle rate limiting in GitLab and GitHub services (#8003)
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
This commit is contained in:
parent
8f3ff1210e
commit
039fe295a4
@ -5,9 +5,7 @@ from typing import Any
|
||||
import httpx
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.service_types import (
|
||||
AuthenticationError,
|
||||
BaseGitService,
|
||||
GitService,
|
||||
ProviderType,
|
||||
@ -45,6 +43,10 @@ class GitHubService(BaseGitService, GitService):
|
||||
if base_domain:
|
||||
self.BASE_URL = f'https://{base_domain}/api/v3'
|
||||
|
||||
@property
|
||||
def provider(self) -> str:
|
||||
return ProviderType.GITHUB.value
|
||||
|
||||
async def _get_github_headers(self) -> dict:
|
||||
"""Retrieve the GH Token from settings store to construct the headers."""
|
||||
if not self.token:
|
||||
@ -100,15 +102,9 @@ class GitHubService(BaseGitService, GitService):
|
||||
return response.json(), headers
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code == 401:
|
||||
raise AuthenticationError('Invalid Github token')
|
||||
|
||||
logger.warning(f'Status error on GH API: {e}')
|
||||
raise UnknownException('Unknown error')
|
||||
|
||||
raise self.handle_http_status_error(e)
|
||||
except httpx.HTTPError as e:
|
||||
logger.warning(f'HTTP error on GH API: {e}')
|
||||
raise UnknownException('Unknown error')
|
||||
raise self.handle_http_error(e)
|
||||
|
||||
async def get_user(self) -> User:
|
||||
url = f'{self.BASE_URL}/user'
|
||||
@ -264,15 +260,9 @@ class GitHubService(BaseGitService, GitService):
|
||||
return dict(result)
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code == 401:
|
||||
raise AuthenticationError('Invalid Github token')
|
||||
|
||||
logger.warning(f'Status error on GH API: {e}')
|
||||
raise UnknownException('Unknown error')
|
||||
|
||||
raise self.handle_http_status_error(e)
|
||||
except httpx.HTTPError as e:
|
||||
logger.warning(f'HTTP error on GH API: {e}')
|
||||
raise UnknownException('Unknown error')
|
||||
raise self.handle_http_error(e)
|
||||
|
||||
async def get_suggested_tasks(self) -> list[SuggestedTask]:
|
||||
"""Get suggested tasks for the authenticated user across all repositories.
|
||||
|
||||
@ -4,9 +4,7 @@ from typing import Any
|
||||
import httpx
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.service_types import (
|
||||
AuthenticationError,
|
||||
BaseGitService,
|
||||
GitService,
|
||||
ProviderType,
|
||||
@ -24,6 +22,7 @@ class GitLabService(BaseGitService, GitService):
|
||||
GRAPHQL_URL = 'https://gitlab.com/api/graphql'
|
||||
token: SecretStr = SecretStr('')
|
||||
refresh = False
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -44,6 +43,10 @@ class GitLabService(BaseGitService, GitService):
|
||||
self.BASE_URL = f'https://{base_domain}/api/v4'
|
||||
self.GRAPHQL_URL = f'https://{base_domain}/api/graphql'
|
||||
|
||||
@property
|
||||
def provider(self) -> str:
|
||||
return ProviderType.GITLAB.value
|
||||
|
||||
async def _get_gitlab_headers(self) -> dict[str, Any]:
|
||||
"""
|
||||
Retrieve the GitLab Token to construct the headers
|
||||
@ -100,15 +103,9 @@ class GitLabService(BaseGitService, GitService):
|
||||
return response.json(), headers
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code == 401:
|
||||
raise AuthenticationError('Invalid GitLab token')
|
||||
|
||||
logger.warning(f'Status error on GL API: {e}')
|
||||
raise UnknownException('Unknown error')
|
||||
|
||||
raise self.handle_http_status_error(e)
|
||||
except httpx.HTTPError as e:
|
||||
logger.warning(f'HTTP error on GL API: {e}')
|
||||
raise UnknownException('Unknown error')
|
||||
raise self.handle_http_error(e)
|
||||
|
||||
async def execute_graphql_query(self, query: str, variables: dict[str, Any]) -> Any:
|
||||
"""
|
||||
@ -156,15 +153,9 @@ class GitLabService(BaseGitService, GitService):
|
||||
|
||||
return result.get('data')
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code == 401:
|
||||
raise AuthenticationError('Invalid GitLab token')
|
||||
|
||||
logger.warning(f'Status error on GL API: {e}')
|
||||
raise UnknownException('Unknown error')
|
||||
|
||||
raise self.handle_http_status_error(e)
|
||||
except httpx.HTTPError as e:
|
||||
logger.warning(f'HTTP error on GL API: {e}')
|
||||
raise UnknownException('Unknown error')
|
||||
raise self.handle_http_error(e)
|
||||
|
||||
async def get_user(self) -> User:
|
||||
url = f'{self.BASE_URL}/user'
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from typing import Protocol
|
||||
from typing import Any, Protocol
|
||||
|
||||
from httpx import AsyncClient
|
||||
from httpx import AsyncClient, HTTPError, HTTPStatusError
|
||||
from pydantic import BaseModel, SecretStr
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.types import AppMode
|
||||
|
||||
|
||||
@ -58,12 +60,31 @@ class UnknownException(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class RateLimitError(ValueError):
|
||||
"""Raised when the git provider's API rate limits are exceeded."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RequestMethod(Enum):
|
||||
POST = 'post'
|
||||
GET = 'get'
|
||||
|
||||
|
||||
class BaseGitService:
|
||||
class BaseGitService(ABC):
|
||||
@property
|
||||
def provider(self) -> str:
|
||||
raise NotImplementedError('Subclasses must implement the provider property')
|
||||
|
||||
# Method used to satisfy mypy for abstract class definition
|
||||
@abstractmethod
|
||||
async def _make_request(
|
||||
self,
|
||||
url: str,
|
||||
params: dict | None = None,
|
||||
method: RequestMethod = RequestMethod.GET,
|
||||
) -> tuple[Any, dict]: ...
|
||||
|
||||
async def execute_request(
|
||||
self,
|
||||
client: AsyncClient,
|
||||
@ -76,6 +97,22 @@ class BaseGitService:
|
||||
return await client.post(url, headers=headers, json=params)
|
||||
return await client.get(url, headers=headers, params=params)
|
||||
|
||||
def handle_http_status_error(
|
||||
self, e: HTTPStatusError
|
||||
) -> AuthenticationError | RateLimitError | UnknownException:
|
||||
if e.response.status_code == 401:
|
||||
return AuthenticationError(f'Invalid {self.provider} token')
|
||||
elif e.response.status_code == 429:
|
||||
logger.warning(f'Rate limit exceeded on {self.provider} API: {e}')
|
||||
return RateLimitError('GitHub API rate limit exceeded')
|
||||
|
||||
logger.warning(f'Status error on {self.provider} API: {e}')
|
||||
return UnknownException('Unknown error')
|
||||
|
||||
def handle_http_error(self, e: HTTPError) -> UnknownException:
|
||||
logger.warning(f'HTTP error on {self.provider} API: {e}')
|
||||
return UnknownException('Unknown error')
|
||||
|
||||
|
||||
class GitService(Protocol):
|
||||
"""Protocol defining the interface for Git service providers"""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user