"""exceptions - Exception classes for USPTO API clients.
This module provides exception classes for USPTO API errors that correspond to
the various response types from the USPTO API. It also includes helper
structures and functions for creating these exceptions.
"""
from dataclasses import asdict, dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import requests
from pyUSPTO.models.patent_data import Document
# --- Exception Classes ---
[docs]
class USPTOApiError(Exception):
"""Base exception for USPTO API errors.
This is the parent class for all USPTO API-specific exceptions. It includes
information about the status code, API's short error message, detailed error
information, and request identifier from the API response.
"""
DEFAULT_UNKNOWN_MESSAGE = "UNK USPTO API ERROR"
[docs]
def __init__(
self,
message: str, # Primary client-facing message for the exception context
status_code: int | None = None,
api_short_error: (
str | None
) = None, # From API 'error' or 'message' (for 413) field
error_details: (
str | dict | None
) = None, # From API 'errorDetails' or 'detailedMessage' field
request_identifier: str | None = None,
):
"""Initialize a USPTOApiError.
Args:
message: The primary message for the exception (often client-generated context).
status_code: The HTTP status code from the API response (e.g., 400, 403).
api_short_error: The short error description from the API (e.g., "Bad Request", "Forbidden").
error_details: The detailed error message or structure from the API.
request_identifier: The request identifier from the API response, if available.
"""
effective_message = message if message else self.DEFAULT_UNKNOWN_MESSAGE
super().__init__(effective_message)
self.status_code = status_code
self.api_short_error = api_short_error
self.error_details = error_details
self.request_identifier = request_identifier
@property
def message(self) -> str:
"""Provides direct access to the primary exception message.
This refers to the first argument passed to the exception,
which is conventionally the main human-readable message.
"""
return str(object=self.args[0])
[docs]
def __str__(self) -> str:
"""Provide a more informative string representation of the error."""
parts = [super().__str__()]
if self.status_code:
parts.append(f"HTTP Status: {self.status_code}")
if self.api_short_error:
parts.append(f"API Error: {self.api_short_error}")
if self.error_details:
details_str = str(self.error_details)
parts.append(f"Details: {details_str}")
if self.request_identifier:
parts.append(f"Request ID: {self.request_identifier}")
if len(parts) > 1:
return " - ".join(filter(None, parts))
else:
return parts[0]
[docs]
class USPTOApiBadRequestError(USPTOApiError):
"""Bad Request error (HTTP 400)."""
pass
[docs]
class USPTOApiAuthError(USPTOApiError):
"""Authentication/Authorization error (HTTP 401/403)."""
pass
[docs]
class USPTOApiRateLimitError(USPTOApiError):
"""Rate limit exceeded error (HTTP 429)."""
pass
[docs]
class USPTOApiNotFoundError(USPTOApiError):
"""Resource not found error (HTTP 404)."""
pass
[docs]
class USPTOApiServerError(USPTOApiError):
"""Internal Server Error (HTTP 500 series)."""
pass
[docs]
class USPTOConnectionError(USPTOApiError):
"""Network-level connection error (DNS failure, refused connection, etc.)."""
pass
[docs]
class USPTOTimeout(USPTOApiError):
"""Request to USPTO API timed out."""
pass
[docs]
class USPTOApiResponseParseError(USPTOApiError):
"""Failed to parse response from USPTO API.
This exception is raised when the API returns a 2xx status code but the
response body cannot be parsed as JSON. This typically indicates the API
returned HTML error pages or other non-JSON content.
"""
pass
# --- Helper Structures and Functions ---
[docs]
@dataclass
class APIErrorArgs:
"""Data structure to hold arguments for API exception constructors."""
message: str
status_code: int | None = None
api_short_error: str | None = None
error_details: str | dict | None = None
request_identifier: str | None = None
[docs]
@classmethod
def from_http_error(
cls,
http_error: "requests.exceptions.HTTPError", # String literal for type hint
client_operation_message: str,
) -> "APIErrorArgs":
"""Create an APIErrorArgs instance by parsing a requests.exceptions.HTTPError.
Args:
http_error: The HTTPError object from the requests library.
client_operation_message: A message describing the client operation that failed.
Returns:
An instance of APIErrorArgs populated with details from the HTTPError.
"""
status_code = http_error.response.status_code
api_short_error_from_response = None
error_details_from_response = None
request_identifier_from_response = None
try:
error_data = http_error.response.json()
if status_code == 413:
api_short_error_from_response = error_data.get("message")
error_details_from_response = error_data.get("detailedMessage")
else:
api_short_error_from_response = error_data.get("error")
error_details_from_response = error_data.get("errorDetails")
request_identifier_from_response = error_data.get("requestIdentifier")
except ValueError: # If response.json() fails (e.g., not JSON)
pass # Values remain None
# Fallback for api_short_error if not found in JSON response
if not api_short_error_from_response and http_error.response.reason:
api_short_error_from_response = http_error.response.reason
# Fallback for error_details if not found in JSON and response text is available
if not error_details_from_response and http_error.response.text:
# Avoid setting very long HTML pages as error_details if JSON parsing failed
if (
"content-type" in http_error.response.headers
and "application/json"
not in http_error.response.headers.get("content-type", "").lower()
):
if len(http_error.response.text) > 500: # Heuristic for "too long"
error_details_from_response = f"Non-JSON error response (status {status_code}). Check response text."
else:
error_details_from_response = http_error.response.text
elif (
http_error.response.text
): # If it might have been JSON but parsing failed
error_details_from_response = http_error.response.text
return cls(
message=client_operation_message,
status_code=status_code,
api_short_error=api_short_error_from_response,
error_details=error_details_from_response,
request_identifier=request_identifier_from_response,
)
[docs]
@classmethod
def from_request_exception(
cls,
request_exception: "requests.exceptions.RequestException", # String for type hint
client_operation_message: str | None = None,
) -> "APIErrorArgs":
"""Create an APIErrorArgs instance.
Create an APIErrorArgs instance from a generic requests.exceptions.RequestException.
(e.g., ConnectionError, Timeout) that is not an HTTPError.
"""
message_prefix = client_operation_message or "API request failed"
return cls(
message=f"{message_prefix} due to a network or request issue: {str(request_exception)}"
# status_code, api_short_error, etc., will be None
)
[docs]
def get_api_exception(error_args: APIErrorArgs) -> USPTOApiError:
"""Determine and instantiate the appropriate USPTOApiError subclass.
Based on the status code in error_args.
Args:
error_args: An instance of APIErrorArgs containing all necessary
information to construct the exception.
Returns:
An instance of a USPTOApiError subclass.
"""
status_code = error_args.status_code
exception_class: type[USPTOApiError]
match status_code:
case 400:
exception_class = USPTOApiBadRequestError
case 401 | 403:
exception_class = USPTOApiAuthError
case 404:
exception_class = USPTOApiNotFoundError
case 413:
exception_class = USPTOApiPayloadTooLargeError
case 429:
exception_class = USPTOApiRateLimitError
case _ if status_code is not None and status_code >= 500:
exception_class = USPTOApiServerError
case (
_
): # Default for other errors or if status_code is None (e.g. network error)
exception_class = USPTOApiError
return exception_class(**asdict(error_args))