mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-25 21:36:52 +08:00
100 lines
3.4 KiB
Python
100 lines
3.4 KiB
Python
"""HTTP Client Protocol for Git Service Integrations."""
|
|
|
|
from abc import ABC, abstractmethod
|
|
from typing import Any
|
|
|
|
from httpx import AsyncClient, HTTPError, HTTPStatusError
|
|
from pydantic import SecretStr
|
|
|
|
from openhands.core.logger import openhands_logger as logger
|
|
from openhands.integrations.service_types import (
|
|
AuthenticationError,
|
|
RateLimitError,
|
|
RequestMethod,
|
|
ResourceNotFoundError,
|
|
UnknownException,
|
|
)
|
|
|
|
|
|
class HTTPClient(ABC):
|
|
"""Abstract base class defining the HTTP client interface for Git service integrations.
|
|
|
|
This class abstracts the common HTTP client functionality needed by all
|
|
Git service providers (GitHub, GitLab, Bitbucket, Azure DevOps) while keeping inheritance in place.
|
|
"""
|
|
|
|
# Default attributes (subclasses may override)
|
|
token: SecretStr = SecretStr('')
|
|
refresh: bool = False
|
|
external_auth_id: str | None = None
|
|
external_auth_token: SecretStr | None = None
|
|
external_token_manager: bool = False
|
|
base_domain: str | None = None
|
|
|
|
# Provider identification must be implemented by subclasses
|
|
@property
|
|
@abstractmethod
|
|
def provider(self) -> str: ...
|
|
|
|
# Abstract methods that concrete classes must implement
|
|
@abstractmethod
|
|
async def get_latest_token(self) -> SecretStr | None:
|
|
"""Get the latest working token for the service."""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def _get_headers(self) -> dict[str, Any]:
|
|
"""Get HTTP headers for API requests."""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def _make_request(
|
|
self,
|
|
url: str,
|
|
params: dict | None = None,
|
|
method: RequestMethod = RequestMethod.GET,
|
|
) -> tuple[Any, dict]:
|
|
"""Make an HTTP request to the Git service API."""
|
|
...
|
|
|
|
def _has_token_expired(self, status_code: int) -> bool:
|
|
"""Check if the token has expired based on HTTP status code."""
|
|
return status_code == 401
|
|
|
|
async def execute_request(
|
|
self,
|
|
client: AsyncClient,
|
|
url: str,
|
|
headers: dict,
|
|
params: dict | None,
|
|
method: RequestMethod = RequestMethod.GET,
|
|
):
|
|
"""Execute an HTTP request using the provided client."""
|
|
if method == RequestMethod.POST:
|
|
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 | ResourceNotFoundError | UnknownException
|
|
):
|
|
"""Handle HTTP status errors and convert them to appropriate exceptions."""
|
|
if e.response.status_code == 401:
|
|
return AuthenticationError(f'Invalid {self.provider} token')
|
|
elif e.response.status_code == 404:
|
|
return ResourceNotFoundError(
|
|
f'Resource not found on {self.provider} API: {e}'
|
|
)
|
|
elif e.response.status_code == 429:
|
|
logger.warning(f'Rate limit exceeded on {self.provider} API: {e}')
|
|
return RateLimitError(f'{self.provider} API rate limit exceeded')
|
|
|
|
logger.warning(f'Status error on {self.provider} API: {e}')
|
|
return UnknownException(f'Unknown error: {e}')
|
|
|
|
def handle_http_error(self, e: HTTPError) -> UnknownException:
|
|
"""Handle general HTTP errors."""
|
|
logger.warning(f'HTTP error on {self.provider} API: {type(e).__name__} : {e}')
|
|
return UnknownException(f'HTTP error {type(e).__name__} : {e}')
|