diff --git a/api/authentication/base.py b/api/authentication/base.py index 405fb3a32e..a2e37c13d6 100644 --- a/api/authentication/base.py +++ b/api/authentication/base.py @@ -8,6 +8,7 @@ from werkzeug.datastructures import Authorization from core.analytics import Analytics +from core.exceptions import BasePalaceException from core.integration.base import HasLibraryIntegrationConfiguration from core.integration.settings import BaseSettings from core.model import CirculationEvent, Library, Patron, get_one_or_create @@ -113,7 +114,7 @@ def get_credential_from_header(self, auth: Authorization) -> str | None: ] -class CannotCreateLocalPatron(Exception): +class CannotCreateLocalPatron(BasePalaceException): """A remote system provided information about a patron, but we could not put it into our database schema. diff --git a/api/circulation_exceptions.py b/api/circulation_exceptions.py index 67dcfd1dee..5ad9b4afc4 100644 --- a/api/circulation_exceptions.py +++ b/api/circulation_exceptions.py @@ -36,8 +36,8 @@ class CirculationException(IntegrationException, BaseProblemDetailException, ABC def __init__( self, message: str | None = None, debug_info: str | None = None ) -> None: - self.message = message super().__init__(message or self.__class__.__name__, debug_info) + self.message = message @property def detail(self) -> str | None: diff --git a/api/lcp/hash.py b/api/lcp/hash.py index 5fd98a75c7..fd17ab2997 100644 --- a/api/lcp/hash.py +++ b/api/lcp/hash.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from enum import Enum -from core.exceptions import BaseError +from core.exceptions import BasePalaceException class HashingAlgorithm(Enum): @@ -10,7 +10,7 @@ class HashingAlgorithm(Enum): SHA512 = "http://www.w3.org/2001/04/xmlenc#sha512" -class HashingError(BaseError): +class HashingError(BasePalaceException): """Raised in the case of errors occurred during hashing""" diff --git a/api/saml/configuration/model.py b/api/saml/configuration/model.py index f19f43e43c..19a00094c0 100644 --- a/api/saml/configuration/model.py +++ b/api/saml/configuration/model.py @@ -29,7 +29,7 @@ SAMLSubjectPatronIDExtractor, ) from api.saml.metadata.parser import SAMLMetadataParser, SAMLMetadataParsingError -from core.exceptions import BaseError +from core.exceptions import BasePalaceException from core.integration.settings import ( ConfigurationFormItem, ConfigurationFormItemType, @@ -42,7 +42,7 @@ from core.util.log import LoggerMixin -class SAMLConfigurationError(BaseError): +class SAMLConfigurationError(BasePalaceException): """Raised in the case of any configuration errors.""" diff --git a/api/saml/metadata/federations/loader.py b/api/saml/metadata/federations/loader.py index 0b440dc2e3..3e4e21a359 100644 --- a/api/saml/metadata/federations/loader.py +++ b/api/saml/metadata/federations/loader.py @@ -9,11 +9,11 @@ ) from api.saml.metadata.federations.validator import SAMLFederatedMetadataValidator from api.saml.metadata.parser import SAMLMetadataParser -from core.exceptions import BaseError +from core.exceptions import BasePalaceException from core.util import first_or_default -class SAMLMetadataLoadingError(BaseError): +class SAMLMetadataLoadingError(BasePalaceException): """Raised in the case of any errors occurred during loading of SAML metadata from a remote source""" @@ -41,7 +41,7 @@ def load_idp_metadata(self, url=None): try: xml_metadata = OneLogin_Saml2_IdPMetadataParser.get_metadata(url) except Exception as exception: - raise SAMLMetadataLoadingError(inner_exception=exception) + raise SAMLMetadataLoadingError() from exception self._logger.info(f"Finished loading IdP XML metadata from {url}") diff --git a/api/saml/metadata/federations/validator.py b/api/saml/metadata/federations/validator.py index 1fab512c65..1679f21ca2 100644 --- a/api/saml/metadata/federations/validator.py +++ b/api/saml/metadata/federations/validator.py @@ -6,11 +6,11 @@ from onelogin.saml2.xmlparser import fromstring from api.saml.metadata.federations.model import SAMLFederation -from core.exceptions import BaseError +from core.exceptions import BasePalaceException from core.util.datetime_helpers import from_timestamp, utc_now -class SAMLFederatedMetadataValidationError(BaseError): +class SAMLFederatedMetadataValidationError(BasePalaceException): """Raised in the case of any errors happened during SAML metadata validation.""" @@ -113,8 +113,8 @@ def validate(self, federation: SAMLFederation, metadata: str | bytes) -> None: root = fromstring(metadata) except Exception as exception: raise SAMLFederatedMetadataValidationError( - "Metadata's XML is not valid", exception - ) + "Metadata's XML is not valid" + ) from exception if "EntitiesDescriptor" not in root.tag: raise SAMLFederatedMetadataValidationError( @@ -182,7 +182,7 @@ def validate(self, federation, metadata): metadata, federation.certificate, raise_exceptions=True ) except Exception as exception: - raise SAMLFederatedMetadataValidationError(str(exception), exception) + raise SAMLFederatedMetadataValidationError(str(exception)) from exception self._logger.info( "Finished verifying the validity of the metadata's signature belonging to {}".format( diff --git a/api/saml/metadata/filter.py b/api/saml/metadata/filter.py index 615cb4f009..f1fdb0538c 100644 --- a/api/saml/metadata/filter.py +++ b/api/saml/metadata/filter.py @@ -6,22 +6,18 @@ SAMLNameID, SAMLSubject, ) -from core.exceptions import BaseError +from core.exceptions import BasePalaceException from core.python_expression_dsl.evaluator import DSLEvaluator -class SAMLSubjectFilterError(BaseError): +class SAMLSubjectFilterError(BasePalaceException): """Raised in the case of any errors during execution of a filter expression.""" - def __init__(self, inner_exception): - """Initialize a new instance of SAMLSubjectFilterError class. - - :param inner_exception: Inner exception - :type inner_exception: Exception - """ + def __init__(self, inner_exception: Exception) -> None: + """Initialize a new instance of SAMLSubjectFilterError class.""" message = f"Incorrect filter expression: {str(inner_exception)}" - super().__init__(message, inner_exception) + super().__init__(message) class SAMLSubjectFilter: @@ -76,7 +72,7 @@ def execute(self, expression, subject): ], ) except Exception as exception: - raise SAMLSubjectFilterError(exception) + raise SAMLSubjectFilterError(exception) from exception self._logger.info( "Finished applying expression '{}' to {}: {}".format( @@ -104,4 +100,4 @@ def validate(self, expression): try: self._dsl_evaluator.parser.parse(expression) except Exception as exception: - raise SAMLSubjectFilterError(exception) + raise SAMLSubjectFilterError(exception) from exception diff --git a/api/saml/metadata/parser.py b/api/saml/metadata/parser.py index d4f14a31da..4453e8bbd7 100644 --- a/api/saml/metadata/parser.py +++ b/api/saml/metadata/parser.py @@ -22,10 +22,10 @@ SAMLSubject, SAMLUIInfo, ) -from core.exceptions import BaseError +from core.exceptions import BasePalaceException -class SAMLMetadataParsingError(BaseError): +class SAMLMetadataParsingError(BasePalaceException): """Raised in the case of any errors occurred during parsing of SAML metadata""" @@ -116,7 +116,7 @@ def _convert_xml_string_to_dom( "An unhandled exception occurred during converting XML string containing SAML metadata into XML DOM" ) - raise SAMLMetadataParsingError(inner_exception=exception) + raise SAMLMetadataParsingError() from exception self._logger.debug( "Finished converting XML string containing SAML metadata into XML DOM" @@ -149,7 +149,7 @@ def _parse_certificates(self, certificate_nodes): certificates.append(certificate) except XMLSyntaxError as exception: - raise SAMLMetadataParsingError(inner_exception=exception) + raise SAMLMetadataParsingError() from exception self._logger.debug( "Finished parsing {} certificates: {}".format( @@ -605,7 +605,7 @@ def parse(self, xml_metadata): "An unexpected error occurred during parsing an XML string containing SAML metadata" ) - raise SAMLMetadataParsingError(inner_exception=exception) + raise SAMLMetadataParsingError() from exception self._logger.info("Finished parsing an XML string containing SAML metadata") diff --git a/api/selftest.py b/api/selftest.py index e1e5624491..2fb4c2b5a7 100644 --- a/api/selftest.py +++ b/api/selftest.py @@ -6,7 +6,7 @@ from sqlalchemy.orm.session import Session from core.config import IntegrationException -from core.exceptions import BaseError +from core.exceptions import BasePalaceException from core.model import Collection, Library, LicensePool, Patron from core.model.integration import IntegrationConfiguration from core.selftest import HasSelfTests, SelfTestResult @@ -20,7 +20,7 @@ class HasPatronSelfTests(HasSelfTests, ABC): on behalf of a specific patron. """ - class _NoValidLibrarySelfTestPatron(BaseError): + class _NoValidLibrarySelfTestPatron(BasePalaceException): """Exception raised when no valid self-test patron found for library. Attributes: diff --git a/core/exceptions.py b/core/exceptions.py index 24028fc845..477a91131c 100644 --- a/core/exceptions.py +++ b/core/exceptions.py @@ -1,50 +1,16 @@ -class BaseError(Exception): - """Base class for all errors""" +class BasePalaceException(Exception): + """Base class for all Exceptions in the Palace manager.""" - def __init__( - self, message: str | None = None, inner_exception: Exception | None = None - ): - """Initializes a new instance of BaseError class + def __init__(self, message: str | None = None): + """Initializes a new instance of BasePalaceException class - :param message: String containing description of the error occurred - :param inner_exception: (Optional) Inner exception + :param message: String containing description of the exception that occurred """ - if inner_exception and not message: - message = str(inner_exception) - super().__init__(message) - - self._inner_exception = str(inner_exception) if inner_exception else None - - def __hash__(self): - return hash(str(self)) - - @property - def inner_exception(self) -> str | None: - """Returns an inner exception - - :return: Inner exception - """ - return self._inner_exception - - def __eq__(self, other: object) -> bool: - """Compares two BaseError objects - - :param other: BaseError object - :return: Boolean value indicating whether two items are equal - """ - if not isinstance(other, BaseError): - return False - - return str(self) == str(other) - - def __repr__(self): - return "".format( - (self), self.inner_exception - ) + self.message = message -class IntegrationException(Exception): +class IntegrationException(BasePalaceException): """An exception that happens when the site's connection to a third-party service is broken. diff --git a/core/external_search.py b/core/external_search.py index 1af769eda9..fd128c84d4 100644 --- a/core/external_search.py +++ b/core/external_search.py @@ -32,6 +32,7 @@ GradeLevelClassifier, KeywordBasedClassifier, ) +from core.exceptions import BasePalaceException from core.facets import FacetConstants from core.lane import Pagination from core.metadata_layer import IdentifierData @@ -1334,7 +1335,7 @@ def _parse_json_join(self, query: dict) -> dict: @define -class QueryParseException(Exception): +class QueryParseException(BasePalaceException): detail: str = "" diff --git a/core/feed/opds.py b/core/feed/opds.py index 97accc03a3..42dd9a2534 100644 --- a/core/feed/opds.py +++ b/core/feed/opds.py @@ -5,6 +5,7 @@ from werkzeug.datastructures import MIMEAccept +from core.exceptions import BasePalaceException from core.feed.base import FeedInterface from core.feed.serializer.base import SerializerInterface from core.feed.serializer.opds import OPDS1Serializer @@ -95,7 +96,7 @@ def entry_as_response( return response -class UnfulfillableWork(Exception): +class UnfulfillableWork(BasePalaceException): """Raise this exception when it turns out a Work currently cannot be fulfilled through any means, *and* this is a problem sufficient to cancel the creation of an for the Work. diff --git a/core/jobs/integration_test.py b/core/jobs/integration_test.py index 6128ca9e54..f3c8a37452 100644 --- a/core/jobs/integration_test.py +++ b/core/jobs/integration_test.py @@ -13,6 +13,7 @@ from OpenSSL.crypto import FILETYPE_PEM, X509, load_certificate from core.crypt.aes import CryptAESCBC +from core.exceptions import BasePalaceException from core.scripts import Script from core.util.datetime_helpers import utc_now from core.util.http import ( @@ -165,7 +166,7 @@ def do_run(self) -> None: self._run_test(test) except FailedIntegrationTest as ex: self.log.error( - f"Test run failed for {test.name} {test.endpoint}: {ex.args[0]}" + f"Test run failed for {test.name} {test.endpoint}: {ex.message}" ) if ex.exception: self.log.error(f"Test run exception {ex.exception}") @@ -182,7 +183,7 @@ def _run_test(self, test: IntegrationTestDetails) -> None: ) except (RequestNetworkException, RequestTimedOut, BadResponseException) as ex: raise FailedIntegrationTest( - f"Network Failure: {ex.url}", exception=ex.args[0] + f"Network Failure: {ex.url}", exception=ex.message ) # Run tests on the SSL certificate @@ -243,7 +244,7 @@ def _test_ssl_validity(self, test: IntegrationTestDetails): raise FailedIntegrationTest(f"The SSL certificate expires on {expires}") -class FailedIntegrationTest(Exception): - def __init__(self, *args: object, exception=None) -> None: +class FailedIntegrationTest(BasePalaceException): + def __init__(self, message: str, exception: str | None = None) -> None: self.exception = exception - super().__init__(*args) + super().__init__(message) diff --git a/core/lcp/exceptions.py b/core/lcp/exceptions.py index dce068cccb..161b305133 100644 --- a/core/lcp/exceptions.py +++ b/core/lcp/exceptions.py @@ -1,5 +1,5 @@ -from core.exceptions import BaseError +from core.exceptions import BasePalaceException -class LCPError(BaseError): +class LCPError(BasePalaceException): """Base class for all LCP related exceptions""" diff --git a/core/model/collection.py b/core/model/collection.py index 5db35bab84..87a17f8226 100644 --- a/core/model/collection.py +++ b/core/model/collection.py @@ -19,6 +19,7 @@ from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import and_, or_ +from core.exceptions import BasePalaceException from core.integration.goals import Goals from core.model import Base, create from core.model.constants import DataSourceConstants, EditionConstants @@ -617,7 +618,7 @@ class CollectionIdentifier: pass -class CollectionMissing(Exception): +class CollectionMissing(BasePalaceException): """An operation was attempted that can only happen within the context of a Collection, but there was no Collection available. """ diff --git a/core/model/devicetokens.py b/core/model/devicetokens.py index faab4b93e4..de6e58f39e 100644 --- a/core/model/devicetokens.py +++ b/core/model/devicetokens.py @@ -4,6 +4,7 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Mapped, backref, relationship +from core.exceptions import BasePalaceException from core.model import Base from core.model.patron import Patron @@ -82,9 +83,9 @@ def create( return device -class InvalidTokenTypeError(Exception): +class InvalidTokenTypeError(BasePalaceException): pass -class DuplicateDeviceTokenError(Exception): +class DuplicateDeviceTokenError(BasePalaceException): pass diff --git a/core/model/identifier.py b/core/model/identifier.py index 8804574cd2..482d4a3698 100644 --- a/core/model/identifier.py +++ b/core/model/identifier.py @@ -28,6 +28,7 @@ from sqlalchemy.sql import select from sqlalchemy.sql.expression import and_, or_ +from core.exceptions import BasePalaceException from core.model import ( Base, PresentationCalculationPolicy, @@ -421,7 +422,7 @@ def work(self): if lp.work: return lp.work - class UnresolvableIdentifierException(Exception): + class UnresolvableIdentifierException(BasePalaceException): # Raised when an identifier that can't be resolved into a LicensePool # is provided in a context that requires a resolvable identifier pass diff --git a/core/model/licensing.py b/core/model/licensing.py index 9fbde77a00..ce5640ff90 100644 --- a/core/model/licensing.py +++ b/core/model/licensing.py @@ -14,6 +14,7 @@ from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import or_ +from core.exceptions import BasePalaceException from core.model import Base, flush, get_one, get_one_or_create from core.model.circulationevent import CirculationEvent from core.model.constants import ( @@ -37,7 +38,7 @@ ) -class PolicyException(Exception): +class PolicyException(BasePalaceException): pass diff --git a/core/monitor.py b/core/monitor.py index 4f4eac40b4..f04dc56a80 100644 --- a/core/monitor.py +++ b/core/monitor.py @@ -8,6 +8,7 @@ from sqlalchemy.orm import defer from sqlalchemy.sql.expression import and_, or_ +from core.exceptions import BasePalaceException from core.metadata_layer import TimestampData from core.model import ( Base, @@ -776,7 +777,7 @@ def prepare(self, work): return failures -class CoverageProvidersFailed(Exception): +class CoverageProvidersFailed(BasePalaceException): """We tried to run CoverageProviders on a Work's identifier, but some of the providers failed. """ diff --git a/core/python_expression_dsl/evaluator.py b/core/python_expression_dsl/evaluator.py index 14e02d8c8e..cde2b61925 100644 --- a/core/python_expression_dsl/evaluator.py +++ b/core/python_expression_dsl/evaluator.py @@ -5,7 +5,7 @@ from multipledispatch import dispatch -from core.exceptions import BaseError +from core.exceptions import BasePalaceException from core.python_expression_dsl.ast import ( BinaryArithmeticExpression, BinaryBooleanExpression, @@ -26,7 +26,7 @@ from core.python_expression_dsl.parser import DSLParser -class DSLEvaluationError(BaseError): +class DSLEvaluationError(BasePalaceException): """Raised when evaluation of a DSL expression fails.""" diff --git a/core/python_expression_dsl/parser.py b/core/python_expression_dsl/parser.py index 4c5d0da784..62e1e1afb1 100644 --- a/core/python_expression_dsl/parser.py +++ b/core/python_expression_dsl/parser.py @@ -14,7 +14,7 @@ alphas, ) -from core.exceptions import BaseError +from core.exceptions import BasePalaceException from core.python_expression_dsl.ast import Node, Operator from core.python_expression_dsl.util import ( _parse_binary_arithmetic_expression, @@ -32,7 +32,7 @@ ) -class DSLParseError(BaseError): +class DSLParseError(BasePalaceException): """Raised when expression has an incorrect format.""" @@ -192,4 +192,4 @@ def parse(self, expression: str) -> Node: except ParseException as exception: error_message = self._parse_error_message(exception) - raise DSLParseError(error_message, inner_exception=exception) + raise DSLParseError(error_message) from exception diff --git a/core/saml/wayfless.py b/core/saml/wayfless.py index d40305e1fc..4d793fc82e 100644 --- a/core/saml/wayfless.py +++ b/core/saml/wayfless.py @@ -1,6 +1,6 @@ from flask_babel import lazy_gettext as _ -from core.exceptions import BaseError +from core.exceptions import BasePalaceException from core.integration.settings import ( BaseSettings, ConfigurationFormItem, @@ -50,5 +50,5 @@ class SAMLWAYFlessSetttings(BaseSettings): ) -class SAMLWAYFlessFulfillmentError(BaseError): +class SAMLWAYFlessFulfillmentError(BasePalaceException): pass diff --git a/core/search/migrator.py b/core/search/migrator.py index 2aff641024..d123adf759 100644 --- a/core/search/migrator.py +++ b/core/search/migrator.py @@ -2,12 +2,13 @@ from abc import ABC, abstractmethod from collections.abc import Iterable +from core.exceptions import BasePalaceException from core.search.revision import SearchSchemaRevision from core.search.revision_directory import SearchRevisionDirectory from core.search.service import SearchService, SearchServiceFailedDocument -class SearchMigrationException(Exception): +class SearchMigrationException(BasePalaceException): """The type of exceptions raised by the search migrator.""" def __init__(self, fatal: bool, message: str): diff --git a/core/search/service.py b/core/search/service.py index b1a39a9d2c..2bb0a149b2 100644 --- a/core/search/service.py +++ b/core/search/service.py @@ -8,6 +8,7 @@ from opensearch_dsl import MultiSearch, Search from opensearchpy import NotFoundError, OpenSearch, RequestError +from core.exceptions import BasePalaceException from core.search.revision import SearchSchemaRevision @@ -27,7 +28,7 @@ def target_name(self) -> str: return f"{self.base_name}-v{self.version}" -class SearchServiceException(Exception): +class SearchServiceException(BasePalaceException): """The type of exceptions raised by the search service.""" def __init__(self, message: str): diff --git a/core/util/problem_detail.py b/core/util/problem_detail.py index ac574f3250..dd7d9af7e2 100644 --- a/core/util/problem_detail.py +++ b/core/util/problem_detail.py @@ -11,7 +11,7 @@ from flask_babel import LazyString from pydantic import BaseModel -from core.exceptions import BaseError +from core.exceptions import BasePalaceException JSON_MEDIA_TYPE = "application/api-problem+json" @@ -150,7 +150,7 @@ def __eq__(self, other: object) -> bool: ) -class BaseProblemDetailException(Exception, ABC): +class BaseProblemDetailException(BasePalaceException, ABC): """Mixin for exceptions that can be converted into a ProblemDetail.""" @property @@ -160,7 +160,7 @@ def problem_detail(self) -> ProblemDetail: ... -class ProblemDetailException(BaseError, BaseProblemDetailException): +class ProblemDetailException(BaseProblemDetailException): """Exception class allowing to raise and catch ProblemDetail objects.""" def __init__(self, problem_detail: ProblemDetail) -> None: @@ -172,7 +172,7 @@ def __init__(self, problem_detail: ProblemDetail) -> None: raise ValueError( 'Argument "problem_detail" must be an instance of ProblemDetail class' ) - + super().__init__(problem_detail.title) self._problem_detail = problem_detail @property diff --git a/tests/core/jobs/test_integration_test.py b/tests/core/jobs/test_integration_test.py index 3f265141f9..880054aa16 100644 --- a/tests/core/jobs/test_integration_test.py +++ b/tests/core/jobs/test_integration_test.py @@ -202,7 +202,7 @@ def test__do_run(self, integration_test: IntegrationTestFixture): # Test the exception case run_test.side_effect = FailedIntegrationTest( - "Error", exception=Exception("...") + "Error", exception="Nested Error" ) # Script does not fail script.do_run()