Source code for scholar_flux.exceptions.api_exceptions

# /exceptions/api_exceptions.py
"""Implements exceptions involving the creation of requests and retrieval of responses from API Providers."""
import requests
from json import JSONDecodeError
from typing import Any, Optional
import logging
from scholar_flux.utils.response_protocol import ResponseProtocol, response_supports_json
from scholar_flux.utils.helpers import get_nested_data, as_str

logger = logging.getLogger(__name__)


[docs] class APIException(Exception): """Base exception for API-related errors.""" pass
[docs] class MissingAPIKeyException(ValueError): """Exception raised when a blank string is provided yet invalid.""" pass
[docs] class MissingAPISpecificParameterException(ValueError): """Exception raised when an API specific parameter is required but not provided in the config.""" pass
[docs] class MissingProviderException(ValueError): """Exception raised when the specification of a provider is required but not provided in the config.""" pass
[docs] class MissingResponseException(ValueError): """Exception raised when a response or response-like object is required but not provided.""" pass
[docs] class NoRecordsAvailableException(APIException): """Exception raised when an operation depends on the presence of records but none exist.""" pass
[docs] class RateLimitExceededException(APIException): """Exception raised when the API's rate limit is exceeded.""" pass
[docs] class RequestFailedException(APIException): """Exception raised for failed API requests."""
[docs] @classmethod def extract_error_details(cls, response: requests.Response | ResponseProtocol) -> str: """Extracts detailed error message from response body.""" try: json_data: dict[str, Any] = response.json() if response_supports_json(response) else {} error_details = get_nested_data(json_data, "error.message") return as_str(error_details) if error_details else "" except (ValueError, KeyError, AttributeError, JSONDecodeError): return ""
[docs] class PageUnavailableFromCacheException(RequestFailedException): """Exception raised when a valid response cannot be retrieved from the session cache."""
[docs] def __init__(self, *args: Any, message: str = "", **kwargs: Any) -> None: """Initializes the `PageUnavailableFromCacheException` class with a response or response-like parameter.""" self.message = f"{message}" or "" super().__init__(self.message, *args, **kwargs)
@property def response(self) -> None: """Added for interface compatibility.""" return None @property def error_details(self) -> str: """Added for interface compatibility.""" return ""
[docs] class RequestCreationException(APIException): """Exception raised when the preparation of an API request fails."""
[docs] class RecordNormalizationException(APIException): """Exception raised when the normalization of a response record cannot be completed."""
[docs] class QueryValidationException(APIException): """Exception raised when a requested resource is not found.""" pass
[docs] class APIParameterException(APIException): """Exception raised for API Parameter-related errors.""" pass
[docs] class RequestCacheException(APIException): """Exception raised for API request-cache related errors.""" pass
[docs] class InvalidResponseStructureException(APIException): """Exception raised when encountering a non-response/response-like object where a valid response is expected.""" pass
[docs] class InvalidResponseReconstructionException(InvalidResponseStructureException): """Exception raised on the attempted creation of a ReconstructedResponse if an exception is encountered.""" pass
[docs] class RetryAfterDelayExceededException(RequestFailedException): """Exception raised when a Retry-After field from a rate limited (429) response exceeds the user-specified limit."""
[docs] def __init__( self, response: Optional[requests.Response | ResponseProtocol], *args: Any, message: str = "", **kwargs: Any ) -> None: """Initializes the `RetryAfterDelayExceededException` class with a response or response-like parameter.""" self.response: Optional[requests.Response | ResponseProtocol] = response self.error_details: str = self.extract_error_details(response) if response is not None else "" self.message = f"{message}: {self.error_details}" if self.error_details else message super().__init__(self.message, *args, **kwargs)
[docs] class InvalidResponseException(RequestFailedException): """Exception raised for invalid responses from the API."""
[docs] def __init__( self, response: Optional[requests.Response | ResponseProtocol] = None, *args: Any, **kwargs: Any ) -> None: """Initializes the `InvalidResponseException` class with a response or response-like parameter.""" self.response: Optional[requests.Response | ResponseProtocol] = ( response if (isinstance(response, requests.Response) or isinstance(response, ResponseProtocol)) else None ) self.error_details: str = self.extract_error_details(response) if response is not None else "" if response is not None: error_message = f"HTTP error occurred: {response} - Status code: {getattr(response,'status_code')}." if self.error_details: error_message += f" Details: {self.error_details}" else: error_message = f"An error occurred when making the request - Received a nonresponse: {type(response)}" self.message = error_message super().__init__(self.message, *args, **kwargs)
[docs] class RetryLimitExceededException(APIException): """Exception raised when the retry limit is exceeded.""" pass
__all__ = [ "APIException", "MissingAPIKeyException", "MissingAPISpecificParameterException", "MissingProviderException", "MissingResponseException", "NoRecordsAvailableException", "InvalidResponseException", "RetryAfterDelayExceededException", "RequestCreationException", "RequestFailedException", "PageUnavailableFromCacheException", "RateLimitExceededException", "RetryLimitExceededException", "APIParameterException", "RequestCacheException", "InvalidResponseStructureException", "InvalidResponseReconstructionException", "QueryValidationException", ]