# -*- coding: utf-8 -*-
"""
:Module: pydplus.errors.exceptions
:Synopsis: Collection of exception classes relating to the pydplus library
:Created By: Jeff Shurtliff
:Last Modified: Jeff Shurtliff
:Modified Date: 30 Mar 2026
"""
from __future__ import annotations
from typing import Optional, Union
from ..constants import _EXCEPTION_CLASSES, API_REQUEST_TYPES
# -----------------------------
# Base Exception
# -----------------------------
# Define base exception class
[docs]
class PyDPlusError(Exception):
"""This is the base class for PyDPlus exceptions."""
pass
# -----------------------------
# General Exceptions
# -----------------------------
[docs]
class CurrentlyUnsupportedError(PyDPlusError):
"""Exception used when a feature or functionality being used is currently unsupported."""
def __init__(self, *args, **kwargs):
default_msg = 'This feature is currently unsupported at this time.'
if not (args or kwargs):
args = (default_msg,)
elif _EXCEPTION_CLASSES._MESSAGE in kwargs:
args = (kwargs[_EXCEPTION_CLASSES._MESSAGE],)
else:
custom_msg = f"The '{args[0]}' {default_msg.split('This ')[1]}"
args = (custom_msg,)
super().__init__(*args)
[docs]
class DataMismatchError(PyDPlusError):
"""Exception used when there is a mismatch between two data sources."""
def __init__(self, *args, **kwargs):
default_msg = 'A data mismatch was found with the data sources.'
if not (args or kwargs):
args = (default_msg,)
elif _EXCEPTION_CLASSES._DATA in kwargs:
multi_types = [list, tuple, set]
if isinstance(kwargs[_EXCEPTION_CLASSES._DATA], str):
custom_msg = (
f"{default_msg.split('the data')[0]}the '{kwargs[_EXCEPTION_CLASSES._DATA]}'"
f'{default_msg.split("with the")[1]}'
)
custom_msg = custom_msg.replace('sources', 'source')
args = (custom_msg,)
elif type(kwargs[_EXCEPTION_CLASSES._DATA]) in multi_types and len(kwargs[_EXCEPTION_CLASSES._DATA]) == 2:
custom_section = f"'{kwargs[_EXCEPTION_CLASSES._DATA][0]}' and '{kwargs[_EXCEPTION_CLASSES._DATA][1]}'"
custom_msg = f'{default_msg.split("data sources")[0]}{custom_section}{default_msg.split("with the")[1]}'
args = (custom_msg,)
super().__init__(*args)
[docs]
class InvalidParameterError(PyDPlusError):
"""Exception used when an invalid parameter is provided."""
def __init__(self, *args, **kwargs):
default_msg = 'The parameter that was provided is invalid.'
if not (args or kwargs):
args = (default_msg,)
elif _EXCEPTION_CLASSES._VAL in kwargs:
custom_msg = f"{default_msg.split('parameter ')[0]}'{kwargs[_EXCEPTION_CLASSES._VAL]}'{default_msg.split('The')[1]}"
args = (custom_msg,)
super().__init__(*args)
[docs]
class InvalidFieldError(PyDPlusError):
"""Exception used when an invalid field is provided."""
def __init__(self, *args, **kwargs):
default_msg = 'The field that was provided is invalid.'
if not (args or kwargs):
args = (default_msg,)
elif _EXCEPTION_CLASSES._VAL in kwargs:
custom_msg = f"{default_msg.split('field ')[0]}'{kwargs[_EXCEPTION_CLASSES._VAL]}'{default_msg.split('The')[1]}"
args = (custom_msg,)
super().__init__(*args)
[docs]
class InvalidURLError(PyDPlusError):
"""Exception used when a provided URL is invalid."""
def __init__(self, *args, **kwargs):
default_msg = 'The provided URL is invalid'
if not (args or kwargs):
args = (default_msg,)
elif _EXCEPTION_CLASSES._URL in kwargs:
custom_msg = (
f"{default_msg.split('is')[0]}'{kwargs[_EXCEPTION_CLASSES._URL]}'{default_msg.split(_EXCEPTION_CLASSES._URL)[1]}"
)
args = (custom_msg,)
super().__init__(*args)
[docs]
class MissingRequiredDataError(PyDPlusError):
"""Exception used when a function or method is missing one or more required arguments."""
def __init__(self, *args, **kwargs):
default_msg = 'Missing one or more required parameters'
init_msg = 'The object failed to initialize as it is missing one or more required arguments.'
param_msg = "The required parameter 'PARAMETER_NAME' is not defined"
if not (args or kwargs):
args = (default_msg,)
elif _EXCEPTION_CLASSES._INIT in args or _EXCEPTION_CLASSES._INITIALIZE in args:
if _EXCEPTION_CLASSES._OBJECT in kwargs:
custom_msg = (
f'{init_msg.split(_EXCEPTION_CLASSES._OBJECT)[0]}'
f"'{kwargs[_EXCEPTION_CLASSES._OBJECT]}'"
f'{init_msg.split("The")[1]}'
)
args = (custom_msg,)
else:
args = (init_msg,)
elif _EXCEPTION_CLASSES._PARAM in kwargs:
args = (param_msg.replace('PARAMETER_NAME', kwargs[_EXCEPTION_CLASSES._PARAM]),)
super().__init__(*args)
[docs]
class UnknownFileTypeError(PyDPlusError):
"""Exception used when a file type for a given file cannot be identified."""
def __init__(self, *args, **kwargs):
default_msg = 'The file type of the given file path cannot be identified.'
if not (args or kwargs):
args = (default_msg,)
elif _EXCEPTION_CLASSES._FILE in kwargs:
custom_msg = f"{default_msg.split('path')[0]}'{kwargs[_EXCEPTION_CLASSES._FILE]}'{default_msg.split('path')[1]}"
args = (custom_msg,)
super().__init__(*args)
[docs]
class IDPlusCredentialError(PyDPlusError):
"""Exception used for RSA ID Plus legacy credential parsing and persistence errors."""
def __init__(self, *args, **kwargs):
default_msg = 'The RSA ID Plus credential material is invalid or could not be processed securely.'
if not (args or kwargs):
args = (default_msg,)
super().__init__(*args)
# -----------------------------
# Generic API Exceptions
# -----------------------------
[docs]
class APIConnectionError(PyDPlusError):
"""Exception used when the API query could not be completed due to connection aborts and/or timeouts."""
def __init__(self, *args, **kwargs):
default_msg = 'The API query could not be completed due to connection aborts and/or timeouts.'
if not (args or kwargs):
args = (default_msg,)
super().__init__(*args)
[docs]
class APIMethodError(PyDPlusError, ValueError):
"""Exception used when an invalid API call method is used when performing an API call."""
def __init__(self, *args, **kwargs):
default_msg = 'A valid API call method (GET or POST or PATCH or PUT) must be defined.'
if not (args or kwargs):
args = (default_msg,)
super().__init__(*args)
[docs]
class APIRequestError(PyDPlusError):
"""Exception used for generic API request errors when there is not a more specific exception."""
def __init__(self, *args, **kwargs):
default_msg = _EXCEPTION_CLASSES._API_DEFAULT_MSG.format(type='API')
if not (args or kwargs):
args = (default_msg,)
super().__init__(*args)
[docs]
class APIResponseConversionError(PyDPlusError):
"""Exception used for errors when attempting to convert an API response to another data format. (e.g. JSON)"""
def __init__(self, *args, **kwargs):
default_msg = 'The API response failed to be converted to the specified data format.'
if not (args or kwargs):
args = (default_msg,)
super().__init__(*args)
[docs]
class DELETERequestError(PyDPlusError):
"""Exception used for generic DELETE request errors when there is not a more specific exception."""
def __init__(self, *args, **kwargs):
default_msg = _EXCEPTION_CLASSES._API_DEFAULT_MSG.format(type=API_REQUEST_TYPES.DELETE)
if _EXCEPTION_CLASSES._STATUS_CODE in kwargs or _EXCEPTION_CLASSES._MESSAGE in kwargs:
custom_msg = _construct_api_custom_message(
_request_type=API_REQUEST_TYPES.DELETE,
_message=kwargs.get(_EXCEPTION_CLASSES._MESSAGE, None),
_status_code=kwargs.get(_EXCEPTION_CLASSES._STATUS_CODE, None),
)
args = (custom_msg,)
elif not (args or kwargs):
args = (default_msg,)
super().__init__(*args)
[docs]
class GETRequestError(PyDPlusError):
"""Exception used for generic GET request errors when there is not a more specific exception."""
def __init__(self, *args, **kwargs):
default_msg = _EXCEPTION_CLASSES._API_DEFAULT_MSG.format(type=API_REQUEST_TYPES.GET)
if _EXCEPTION_CLASSES._STATUS_CODE in kwargs or _EXCEPTION_CLASSES._MESSAGE in kwargs:
custom_msg = _construct_api_custom_message(
_request_type=API_REQUEST_TYPES.GET,
_message=kwargs.get(_EXCEPTION_CLASSES._MESSAGE, None),
_status_code=kwargs.get(_EXCEPTION_CLASSES._STATUS_CODE, None),
)
args = (custom_msg,)
elif not (args or kwargs):
args = (default_msg,)
super().__init__(*args)
[docs]
class InvalidEndpointError(PyDPlusError):
"""Exception used when an invalid API endpoint or service is provided."""
def __init__(self, *args, **kwargs):
default_msg = 'The supplied endpoint for the API is not recognized.'
if not (args or kwargs):
args = (default_msg,)
super().__init__(*args)
[docs]
class InvalidLookupTypeError(PyDPlusError, ValueError):
"""Exception used when an invalid API lookup type is provided."""
def __init__(self, *args, **kwargs):
default_msg = (
'The supplied lookup type for the API is not recognized. (Examples of valid '
+ "lookup types include 'id' and 'email')"
)
if not (args or kwargs):
args = (default_msg,)
super().__init__(*args)
[docs]
class InvalidPayloadValueError(PyDPlusError):
"""Exception used when an invalid value is provided for a payload field."""
def __init__(self, *args, **kwargs):
default_msg = 'An invalid payload value was provided.'
custom_msg = "The invalid payload value 'X' was provided for the 'Y' field."
if not (args or kwargs):
args = (default_msg,)
elif _EXCEPTION_CLASSES._VALUE in kwargs:
if _EXCEPTION_CLASSES._FIELD in kwargs:
custom_msg = custom_msg.replace('X', kwargs[_EXCEPTION_CLASSES._VALUE])
custom_msg = custom_msg.replace('Y', kwargs[_EXCEPTION_CLASSES._FIELD])
else:
custom_msg = f'{custom_msg.replace("X", kwargs[_EXCEPTION_CLASSES._VALUE]).split(" for the")[0]}.'
args = (custom_msg,)
super().__init__(*args)
[docs]
class InvalidRequestTypeError(PyDPlusError, ValueError):
"""Exception used when an invalid API request type is provided."""
def __init__(self, *args, **kwargs):
default_msg = (
'The supplied request type for the API is not recognized. (Examples of valid '
+ "request types include 'POST' and 'PUT')"
)
if not (args or kwargs):
args = (default_msg,)
super().__init__(*args)
[docs]
class LookupMismatchError(PyDPlusError):
"""Exception used when a lookup value does not match the supplied lookup type."""
def __init__(self, *args, **kwargs):
default_msg = 'The supplied lookup type for the API does not match the value that was provided.'
if not (args or kwargs):
args = (default_msg,)
super().__init__(*args)
[docs]
class NotFoundResponseError(PyDPlusError):
"""Exception used when an API query returns a 404 response and there is not a more specific class."""
def __init__(self, *args, **kwargs):
default_msg = 'The API query returned a 404 response.'
if not (args or kwargs):
args = (default_msg,)
super().__init__(*args)
[docs]
class PATCHRequestError(PyDPlusError):
"""This exception is used for generic PATCH request errors when there is not a more specific exception."""
def __init__(self, *args, **kwargs):
default_msg = _EXCEPTION_CLASSES._API_DEFAULT_MSG.format(type=API_REQUEST_TYPES.PATCH)
if _EXCEPTION_CLASSES._STATUS_CODE in kwargs or _EXCEPTION_CLASSES._MESSAGE in kwargs:
custom_msg = _construct_api_custom_message(
_request_type=API_REQUEST_TYPES.PATCH,
_message=kwargs.get(_EXCEPTION_CLASSES._MESSAGE, None),
_status_code=kwargs.get(_EXCEPTION_CLASSES._STATUS_CODE, None),
)
args = (custom_msg,)
elif not (args or kwargs):
args = (default_msg,)
super().__init__(*args)
[docs]
class PayloadMismatchError(PyDPlusError):
"""Exception used when more than one payload is supplied for an API request."""
def __init__(self, *args, **kwargs):
default_msg = 'More than one payload was provided for the API call when only one is permitted.'
if not (args or kwargs):
args = (default_msg,)
elif kwargs[_EXCEPTION_CLASSES._REQUEST_TYPE]:
custom_msg = default_msg.replace('API call', f'{kwargs[_EXCEPTION_CLASSES._REQUEST_TYPE].upper()} request')
args = (custom_msg,)
super().__init__(*args)
[docs]
class POSTRequestError(PyDPlusError):
"""Exception used for generic POST request errors when there is not a more specific exception."""
def __init__(self, *args, **kwargs):
default_msg = _EXCEPTION_CLASSES._API_DEFAULT_MSG.format(type=API_REQUEST_TYPES.POST)
if _EXCEPTION_CLASSES._STATUS_CODE in kwargs or _EXCEPTION_CLASSES._MESSAGE in kwargs:
custom_msg = _construct_api_custom_message(
_request_type=API_REQUEST_TYPES.POST,
_message=kwargs.get(_EXCEPTION_CLASSES._MESSAGE, None),
_status_code=kwargs.get(_EXCEPTION_CLASSES._STATUS_CODE, None),
)
args = (custom_msg,)
elif not (args or kwargs):
args = (default_msg,)
super().__init__(*args)
[docs]
class PUTRequestError(PyDPlusError):
"""Exception used for generic PUT request errors when there is not a more specific exception."""
def __init__(self, *args, **kwargs):
default_msg = _EXCEPTION_CLASSES._API_DEFAULT_MSG.format(type=API_REQUEST_TYPES.PUT)
if _EXCEPTION_CLASSES._STATUS_CODE in kwargs or _EXCEPTION_CLASSES._MESSAGE in kwargs:
custom_msg = _construct_api_custom_message(
_request_type=API_REQUEST_TYPES.PUT,
_message=kwargs.get(_EXCEPTION_CLASSES._MESSAGE, None),
_status_code=kwargs.get(_EXCEPTION_CLASSES._STATUS_CODE, None),
)
args = (custom_msg,)
elif not (args or kwargs):
args = (default_msg,)
super().__init__(*args)
# -----------------------------
# Helper Exceptions
# -----------------------------
[docs]
class InvalidHelperFileTypeError(PyDPlusError, ValueError):
"""Exception used when an invalid file type is provided for the helper file."""
def __init__(self, *args, **kwargs):
if not (args or kwargs):
args = (_EXCEPTION_CLASSES._INVALID_HELPER_DEFAULT_MSG,)
super().__init__(*args)
[docs]
class InvalidHelperArgumentsError(PyDPlusError):
"""Exception used when the helper function was called with arguments instead of keyword arguments."""
def __init__(self, *args, **kwargs):
default_msg = "The helper configuration file only accepts basic keyword arguments. (e.g. arg_name='arg_value')"
if not (args or kwargs):
args = (default_msg,)
super().__init__(*args)
[docs]
class HelperFunctionNotFoundError(PyDPlusError):
"""Exception used when a function referenced in the helper config file does not exist."""
def __init__(self, *args, **kwargs):
default_msg = 'The function referenced in the helper configuration file could not be found.'
if not (args or kwargs):
args = (default_msg,)
super().__init__(*args)
# -----------------------------
# Constructor Functions
# -----------------------------
def _construct_api_custom_message(
_request_type: str, _message: Optional[str] = None, _status_code: Union[Optional[str], Optional[int]] = None
) -> str:
"""Construct the exception message for an API-related exception class.
:param _request_type: The associated API request type (``GET``, ``PATCH``, ``POST``, ``PUT``, or ``DELETE``)
:type _request_type: str
:param _message: A specific message to append to the base message (optional)
:type _message: str
:param _status_code: The status code returned from the API request (optional)
:type _status_code: str, int, None
:returns: The constructed custom message to use when raising the exception
"""
# Define the base custom message
_custom_msg = _EXCEPTION_CLASSES._API_CUSTOM_MSG.format(type=_request_type.upper())
# Define the status code custom message if a status code was provided
if _status_code:
_status_code_msg = f'returned the {_status_code} status code'
_custom_msg = _custom_msg.replace('failed', _status_code_msg)
# Construct the standard custom message if a custom message string was provided
if _message:
_custom_msg = f'{_custom_msg} {_message}'
elif _status_code:
# Adjust the status code message
_custom_msg = _custom_msg.split(_EXCEPTION_CLASSES._WITH_THE_FOLLOWING_SEGMENT)[0] + '.'
else:
# Revert back to the default message if a custom message was not provided
_custom_msg = _EXCEPTION_CLASSES._API_DEFAULT_MSG.format(type=_request_type.upper())
# Return the constructed message
return _custom_msg