diff --git a/openhands/integrations/github/github_service.py b/openhands/integrations/github/github_service.py index e74dfd2c26..7813c781d7 100644 --- a/openhands/integrations/github/github_service.py +++ b/openhands/integrations/github/github_service.py @@ -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. diff --git a/openhands/integrations/gitlab/gitlab_service.py b/openhands/integrations/gitlab/gitlab_service.py index 93a3e61fb8..c21c068f60 100644 --- a/openhands/integrations/gitlab/gitlab_service.py +++ b/openhands/integrations/gitlab/gitlab_service.py @@ -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' diff --git a/openhands/integrations/service_types.py b/openhands/integrations/service_types.py index 8747550b46..590c5fbf38 100644 --- a/openhands/integrations/service_types.py +++ b/openhands/integrations/service_types.py @@ -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"""