From aea2f31bf39b012776696a04153c05391e285fe2 Mon Sep 17 00:00:00 2001 From: Matthew Evans Date: Mon, 25 Aug 2025 21:32:44 +0100 Subject: [PATCH] Try to better isolate filtertransformers modules --- .../filtertransformers/base_transformer.py | 34 +++++++++++++++---- optimade/filtertransformers/elasticsearch.py | 6 ++-- optimade/filtertransformers/mongo.py | 22 +++++++++--- optimade/server/exception_handlers.py | 2 ++ 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/optimade/filtertransformers/base_transformer.py b/optimade/filtertransformers/base_transformer.py index 3a984c29c..8e89662cb 100644 --- a/optimade/filtertransformers/base_transformer.py +++ b/optimade/filtertransformers/base_transformer.py @@ -11,9 +11,31 @@ from lark import Transformer, Tree, v_args -from optimade.exceptions import BadRequest -from optimade.server.mappers import BaseResourceMapper -from optimade.warnings import UnknownProviderProperty +if TYPE_CHECKING: + from optimade.server.mappers import BaseResourceMapper + +try: + from optimade.warnings import UnknownProviderProperty +except ImportError: + + class UnknownProviderProperty(UserWarning): # type: ignore[no-redef] + """A property with a provider-specific prefix is not recognized by this implementation. + This shim class allows the filtertransformer to be used without the full optimade package. + + """ + + +class FilterTransformerError(Exception): + """Base class for exceptions in this module.""" + + status_code: int + detail: str = "There was an error while processing the filter." + + def __init__(self, status_code: int = 400, detail: str | None = None): + self.status_code = status_code + if detail is not None: + self.detail = detail + if TYPE_CHECKING: # pragma: no cover pass @@ -82,7 +104,7 @@ class BaseTransformer(Transformer, abc.ABC): """ - mapper: type[BaseResourceMapper] | None = None + mapper: type["BaseResourceMapper"] | None = None operator_map: dict[str, str | None] = { "<": None, "<=": None, @@ -106,7 +128,7 @@ class BaseTransformer(Transformer, abc.ABC): _quantity_type: type[Quantity] = Quantity _quantities = None - def __init__(self, mapper: type[BaseResourceMapper] | None = None): + def __init__(self, mapper: type["BaseResourceMapper"] | None = None): """Initialise the transformer object, optionally loading in a resource mapper for use when post-processing. @@ -264,7 +286,7 @@ def property(self, args: list) -> Any: return quantity_name - raise BadRequest( + raise FilterTransformerError( detail=f"'{quantity_name}' is not a known or searchable quantity" ) diff --git a/optimade/filtertransformers/elasticsearch.py b/optimade/filtertransformers/elasticsearch.py index 41c919d2b..3d0da55c3 100644 --- a/optimade/filtertransformers/elasticsearch.py +++ b/optimade/filtertransformers/elasticsearch.py @@ -4,7 +4,9 @@ from lark import v_args from optimade.filtertransformers import BaseTransformer, Quantity -from optimade.server.mappers import BaseResourceMapper + +if TYPE_CHECKING: + from optimade.server.mappers import BaseResourceMapper __all__ = ("ElasticTransformer",) @@ -101,7 +103,7 @@ class ElasticTransformer(BaseTransformer): def __init__( self, - mapper: type[BaseResourceMapper], + mapper: type["BaseResourceMapper"], quantities: dict[str, Quantity] | None = None, ): if quantities is not None: diff --git a/optimade/filtertransformers/mongo.py b/optimade/filtertransformers/mongo.py index f252b987f..5f9076914 100755 --- a/optimade/filtertransformers/mongo.py +++ b/optimade/filtertransformers/mongo.py @@ -10,9 +10,21 @@ from lark import Token, v_args -from optimade.exceptions import BadRequest -from optimade.filtertransformers.base_transformer import BaseTransformer, Quantity -from optimade.warnings import TimestampNotRFCCompliant +from optimade.filtertransformers.base_transformer import ( + BaseTransformer, + FilterTransformerError, + Quantity, +) + +try: + from optimade.warnings import TimestampNotRFCCompliant +except ImportError: + + class TimestampNotRFCCompliant(UserWarning): # type: ignore[no-redef] + """A timestamp value was not RFC3339 compliant, which may lead to undefined behaviour. + This shim class is only used if the `optimade` package is not installed. + """ + __all__ = ("MongoTransformer",) @@ -425,7 +437,9 @@ def replace_only_filter(subdict: dict, prop: str, expr: dict): "relationships.references.data.id", "relationships.structures.data.id", ): - raise BadRequest(f"Unable to query on unrecognised field {prop}.") + raise FilterTransformerError( + detail=f"Unable to query on unrecognised field {prop}." + ) first_part_prop = ".".join(prop.split(".")[:-1]) subdict["$and"].append( { diff --git a/optimade/server/exception_handlers.py b/optimade/server/exception_handlers.py index 68c6d29a4..4c9338cd6 100644 --- a/optimade/server/exception_handlers.py +++ b/optimade/server/exception_handlers.py @@ -8,6 +8,7 @@ from pydantic import ValidationError from optimade.exceptions import BadRequest, OptimadeHTTPException +from optimade.filtertransformers.base_transformer import FilterTransformerError from optimade.models import ErrorResponse, ErrorSource, OptimadeError from optimade.server.config import CONFIG from optimade.server.logger import LOGGER @@ -228,6 +229,7 @@ def general_exception_handler(request: Request, exc: Exception) -> JSONAPIRespon ] = [ (StarletteHTTPException, http_exception_handler), (OptimadeHTTPException, http_exception_handler), + (FilterTransformerError, http_exception_handler), (RequestValidationError, request_validation_exception_handler), (ValidationError, validation_exception_handler), (VisitError, grammar_not_implemented_handler),