diff --git a/aws_lambda_powertools/utilities/parser/parser.py b/aws_lambda_powertools/utilities/parser/parser.py index 42ffbbd22c4..7aa7531edb9 100644 --- a/aws_lambda_powertools/utilities/parser/parser.py +++ b/aws_lambda_powertools/utilities/parser/parser.py @@ -84,9 +84,9 @@ def handler(event: Order, context: LambdaContext): Raises ------ ValidationError - When input event does not conform with model provided + When input event does not conform with the provided model InvalidModelTypeError - When model given does not implement BaseModel or is not provided + When the model given does not implement BaseModel, is not provided InvalidEnvelopeError When envelope given does not implement BaseEnvelope """ @@ -103,17 +103,14 @@ def handler(event: Order, context: LambdaContext): "or as the type hint of `event` in the handler that it wraps", ) - try: - if envelope: - parsed_event = parse(event=event, model=model, envelope=envelope) - else: - parsed_event = parse(event=event, model=model) - - logger.debug(f"Calling handler {handler.__name__}") - return handler(parsed_event, context, **kwargs) - except AttributeError as exc: - raise InvalidModelTypeError(f"Error: {str(exc)}. Please ensure the type you're trying to parse into is correct") + if envelope: + parsed_event = parse(event=event, model=model, envelope=envelope) + else: + parsed_event = parse(event=event, model=model) + logger.debug(f"Calling handler {handler.__name__}") + return handler(parsed_event, context, **kwargs) + @overload def parse(event: dict[str, Any], model: type[T]) -> T: ... # pragma: no cover @@ -192,6 +189,7 @@ def handler(event: Order, context: LambdaContext): adapter = _retrieve_or_set_model_from_cache(model=model) logger.debug("Parsing and validating event model; no envelope used") + return _parse_and_validate_event(data=event, adapter=adapter) # Pydantic raises PydanticSchemaGenerationError when the model is not a Pydantic model @@ -204,4 +202,4 @@ def handler(event: Order, context: LambdaContext): f"Error: {str(exc)}. Please ensure the Input model inherits from BaseModel,\n" "and your payload adheres to the specified Input model structure.\n" f"Model={model}", - ) from exc + ) from exc \ No newline at end of file diff --git a/tests/functional/parser/test_parser.py b/tests/functional/parser/test_parser.py index aa7efde9528..1b4a5770b28 100644 --- a/tests/functional/parser/test_parser.py +++ b/tests/functional/parser/test_parser.py @@ -4,8 +4,8 @@ import pydantic import pytest -from pydantic import ValidationError from typing_extensions import Annotated +from pydantic import ValidationError, BaseModel from aws_lambda_powertools.utilities.parser import event_parser, exceptions, parse from aws_lambda_powertools.utilities.parser.envelopes.sqs import SqsEnvelope @@ -130,6 +130,68 @@ def handler(event, _): with pytest.raises(ValidationError): handler({"project": "powertools"}, LambdaContext()) +def test_parser_validation_error(): + class StrictModel(pydantic.BaseModel): + age: int + name: str + + @event_parser(model=StrictModel) + def handle_validation(event: Dict, _: LambdaContext): + return event + + invalid_event = {"age": "not_a_number", "name": 123} # intentionally wrong types + + with pytest.raises(ValidationError) as exc_info: + handle_validation(event=invalid_event, context=LambdaContext()) + + assert "age" in str(exc_info.value) # Verify the error mentions the invalid field + +def test_parser_type_value_errors(): + class CustomModel(pydantic.BaseModel): + timestamp: datetime + status: Literal["SUCCESS", "FAILURE"] + + @event_parser(model=CustomModel) + def handle_type_validation(event: Dict, _: LambdaContext): + return event + + # Test both TypeError and ValueError scenarios + invalid_events = [ + {"timestamp": "invalid-date", "status": "SUCCESS"}, # Will raise ValueError for invalid date + {"timestamp": datetime.now(), "status": "INVALID"} # Will raise ValueError for invalid literal + ] + + for invalid_event in invalid_events: + with pytest.raises((TypeError, ValueError)): + handle_type_validation(event=invalid_event, context=LambdaContext()) + + +def test_event_parser_no_model(): + with pytest.raises(exceptions.InvalidModelTypeError): + @event_parser + def handler(event, _): + return event + + handler({}, None) + + +class Shopping(BaseModel): + id: int + description: str + +def test_event_parser_invalid_event(): + event = {"id": "forgot-the-id", "description": "really nice blouse"} # 'id' is invalid + + @event_parser(model=Shopping) + def handler(event, _): + return event + + with pytest.raises(ValidationError): + handler(event, None) + + with pytest.raises(ValidationError): + parse(event, model=Shopping) + @pytest.mark.parametrize( "test_input,expected", @@ -138,7 +200,10 @@ def handler(event, _): {"status": "succeeded", "name": "Clifford", "breed": "Labrador"}, "Successfully retrieved Labrador named Clifford", ), - ({"status": "failed", "error": "oh some error"}, "Uh oh. Had a problem: oh some error"), + ( + {"status": "failed", "error": "oh some error"}, + "Uh oh. Had a problem: oh some error", + ), ], ) def test_parser_unions(test_input, expected): @@ -163,7 +228,6 @@ def handler(event, _: Any) -> str: ret = handler(test_input, None) assert ret == expected - @pytest.mark.parametrize( "test_input,expected", [ @@ -171,7 +235,10 @@ def handler(event, _: Any) -> str: {"status": "succeeded", "name": "Clifford", "breed": "Labrador"}, "Successfully retrieved Labrador named Clifford", ), - ({"status": "failed", "error": "oh some error"}, "Uh oh. Had a problem: oh some error"), + ( + {"status": "failed", "error": "oh some error"}, + "Uh oh. Had a problem: oh some error", + ), ], ) def test_parser_unions_with_type_adapter_instance(test_input, expected):