Source code for nepse_client.exceptions

"""
Custom exceptions for NEPSE Client.

This module provides a comprehensive exception hierarchy for handling
various error scenarios when interacting with the NEPSE API.
"""

from typing import Any, Optional


[docs] class NepseError(Exception): """ Base exception class for all NEPSE-related errors. All custom exceptions in this library inherit from this class, allowing for easy catch-all error handling. Attributes: message: Human-readable error description status_code: HTTP status code (if applicable) response_data: Raw response data from the API request_data: Original request data """
[docs] def __init__( self, message: str, status_code: Optional[int] = None, response_data: Optional[Any] = None, request_data: Optional[dict[str, Any]] = None, ): """ Initialize NepseError. Args: message: Error description status_code: HTTP status code response_data: Response from the API request_data: Original request data """ super().__init__(message, status_code, response_data, request_data) self.message = message self.status_code = status_code self.response_data = response_data self.request_data = request_data
[docs] def __str__(self) -> str: """Return string representation of the error.""" base_msg = self.message if self.status_code: base_msg = f"[{self.status_code}] {base_msg}" return base_msg
[docs] def __repr__(self) -> str: """Return detailed representation of the error.""" return ( f"{self.__class__.__name__}(" f"message={self.message!r}, " f"status_code={self.status_code}, " f"response_data={self.response_data!r})" )
[docs] def to_dict(self) -> dict[str, Any]: """ Convert exception to dictionary for logging/serialization. Returns: Dictionary representation of the exception """ return { "error_type": self.__class__.__name__, "message": self.message, "status_code": self.status_code, "response_data": self.response_data, }
[docs] class NepseClientError(NepseError): """ Raised when client sends an invalid request (4xx errors). This typically indicates: - Invalid parameters - Missing required fields - Malformed request data - Invalid company symbol Example: >>> try: ... client.getCompanyDetails("INVALID") ... except NepseClientError as e: ... print(f"Invalid request: {e}") """
[docs] def __init__( self, message: str = "Invalid client request", status_code: int = 400, response_data: Optional[Any] = None, request_data: Optional[dict[str, Any]] = None, ): """Initialize the exception with a default message.""" super().__init__(message, status_code, response_data, request_data)
[docs] class NepseAuthenticationError(NepseError): """ Raised when access token has expired (401 Unauthorized). This exception is typically handled automatically by the client, which will refresh the token and retry the request. Note: Users usually don't need to handle this exception directly as the client manages token refresh automatically. """
[docs] def __init__( self, message: str = "Authentication token expired", status_code: int = 401, response_data: Optional[Any] = None, ): """Initialize the exception with a default message.""" super().__init__(message, status_code, response_data)
[docs] class NepseBadGatewayError(NepseError): """ Raised when server returns 502 Bad Gateway. This typically indicates: - Server temporarily unavailable - Upstream server issues - Network problems between servers Recommended action: Retry the request after a short delay. """
[docs] def __init__( self, message: str = "Bad Gateway - Server temporarily unavailable", status_code: int = 502, response_data: Optional[Any] = None, ): """Initialize the exception with a default message.""" super().__init__(message, status_code, response_data)
[docs] class NepseServerError(NepseError): """ Generic server error for 5xx status codes. This indicates an error on the NEPSE server side. Common causes: - Internal server error (500) - Service unavailable (503) - Gateway timeout (504) Recommended action: Retry with exponential backoff or contact support. """
[docs] def __init__( self, message: str = "Server error occurred", status_code: int = 500, response_data: Optional[Any] = None, ): """Initialize the exception with a default message.""" super().__init__(message, status_code, response_data)
[docs] class NepseNetworkError(NepseError): """ Raised for general network or unexpected HTTP issues. This covers: - Connection timeouts - DNS resolution failures - SSL/TLS errors - Unexpected response formats - Network interruptions Recommended action: Check network connectivity and retry. """
[docs] def __init__( self, message: str = "Network error occurred", status_code: Optional[int] = None, response_data: Optional[Any] = None, ): """Initialize the exception with a default message.""" super().__init__(message, status_code, response_data)
[docs] class NepseValidationError(NepseError): """ Raised when input validation fails before making API request. This is raised for client-side validation errors such as: - Invalid date formats - Out of range values - Missing required parameters - Invalid data types Example: >>> try: ... client.getCompanyPriceVolumeHistory("NABIL", start_date="invalid") ... except NepseValidationError as e: ... print(f"Validation error: {e}") """
[docs] def __init__( self, message: str = "Input validation failed", field: Optional[str] = None, value: Optional[Any] = None, ): """ Initialize validation error. Args: message: Error description field: Name of the invalid field value: Invalid value provided """ full_message = message if field: full_message = f"{message} (field: {field})" super().__init__(full_message, None, None) self.field = field self.value = value
[docs] class NepseRateLimitError(NepseError): """ Raised when API rate limit is exceeded (429 Too Many Requests). Attributes: retry_after: Seconds to wait before retrying (if provided by server) """
[docs] def __init__( self, message: str = "Rate limit exceeded", status_code: int = 429, retry_after: Optional[int] = None, ): """ Initialize rate limit error. Args: message: Error description status_code: HTTP status code retry_after: Seconds to wait before retry """ if retry_after: message = f"{message}. Retry after {retry_after} seconds" super().__init__(message, status_code, None) self.retry_after = retry_after
[docs] class NepseDataNotFoundError(NepseError): """ Raised when requested data is not found. This is used when: - Company symbol doesn't exist - No data available for requested date - Empty result sets Example: >>> try: ... client.getFloorSheetOf("INVALID", "2024-01-01") ... except NepseDataNotFoundError as e: ... print(f"Data not found: {e}") """
[docs] def __init__( self, message: str = "Requested data not found", resource: Optional[str] = None, ): """ Initialize data not found error. Args: message: Error description resource: Resource that was not found """ if resource: message = f"{message}: {resource}" super().__init__(message, None) self.resource = resource
[docs] class NepseTimeoutError(NepseError): """ Raised when request times out. This occurs when the server doesn't respond within the specified timeout period. Attributes: timeout: Timeout value in seconds """
[docs] def __init__( self, message: str = "Request timeout", timeout: Optional[float] = None, ): """ Initialize timeout error. Args: message: Error description timeout: Timeout value in seconds """ if timeout: message = f"{message} after {timeout} seconds" """Initialize the exception with a default message.""" super().__init__(message, None) self.timeout = timeout
[docs] class NepseConnectionError(NepseError): """ Raised when connection to NEPSE server fails. This is different from NepseNetworkError as it specifically indicates inability to establish a connection. """
[docs] def __init__(self, message: str = "Failed to connect to NEPSE server"): """Initialize the exception with a default message.""" super().__init__(message)
[docs] class NepseConfigurationError(NepseError): """ Raised when there's an issue with client configuration. This includes: - Missing required configuration files - Invalid configuration values - Corrupted data files """
[docs] def __init__(self, message: str = "Configuration error"): """Initialize the exception with a default message.""" super().__init__(message)
NepseErrorType = type[NepseError] # Exception mapping for HTTP status codes HTTP_STATUS_EXCEPTIONS: dict[int, NepseErrorType] = { 400: NepseClientError, 401: NepseAuthenticationError, 404: NepseDataNotFoundError, 429: NepseRateLimitError, 502: NepseBadGatewayError, 503: NepseServerError, 504: NepseServerError, }
[docs] def get_exception_for_status( status_code: int, message: str, response_data: Optional[Any] = None, ) -> NepseError: """ Get appropriate exception class for HTTP status code. Args: status_code: HTTP status code message: Error message response_data: Response data from API Returns: Appropriate exception instance """ exception_class: type[NepseError] = HTTP_STATUS_EXCEPTIONS.get( status_code, NepseServerError if 500 <= status_code < 600 else NepseError, ) return exception_class(message, status_code, response_data)
__all__ = [ "NepseError", "NepseClientError", "NepseAuthenticationError", "NepseBadGatewayError", "NepseServerError", "NepseNetworkError", "NepseValidationError", "NepseRateLimitError", "NepseDataNotFoundError", "NepseTimeoutError", "NepseConnectionError", "NepseConfigurationError", "get_exception_for_status", ]