Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(parser): remove AttributeError validation from event_parser function #5742

Merged
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 additions & 13 deletions aws_lambda_powertools/utilities/parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
)
anafalcao marked this conversation as resolved.
Show resolved Hide resolved
103 changes: 86 additions & 17 deletions tests/functional/parser/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@

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
Expand Down Expand Up @@ -130,6 +129,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",
Expand All @@ -138,7 +199,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):
Expand All @@ -151,27 +215,31 @@ class FailedCallback(pydantic.BaseModel):
status: Literal["failed"]
error: str

DogCallback = Annotated[Union[SuccessfulCallback, FailedCallback], pydantic.Field(discriminator="status")]
class DogCallback(pydantic.BaseModel):
root: Union[SuccessfulCallback, FailedCallback] = pydantic.Field(discriminator="status")

@event_parser(model=DogCallback)
def handler(event, _: Any) -> str:
if isinstance(event, FailedCallback):
return f"Uh oh. Had a problem: {event.error}"
if isinstance(event.root, FailedCallback):
return f"Uh oh. Had a problem: {event.root.error}"

return f"Successfully retrieved {event.breed} named {event.name}"
return f"Successfully retrieved {event.root.breed} named {event.root.name}"

ret = handler(test_input, None)
wrapped_input = {"root": test_input}
ret = handler(wrapped_input, None)
assert ret == expected


@pytest.mark.parametrize(
"test_input,expected",
[
(
{"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):
Expand All @@ -184,17 +252,18 @@ class FailedCallback(pydantic.BaseModel):
status: Literal["failed"]
error: str

DogCallback = Annotated[Union[SuccessfulCallback, FailedCallback], pydantic.Field(discriminator="status")]
DogCallbackTypeAdapter = pydantic.TypeAdapter(DogCallback)
class DogCallbackModel(pydantic.BaseModel):
data: Union[SuccessfulCallback, FailedCallback] = pydantic.Field(discriminator="status")

@event_parser(model=DogCallbackTypeAdapter)
@event_parser(model=DogCallbackModel)
def handler(event, _: Any) -> str:
if isinstance(event, FailedCallback):
return f"Uh oh. Had a problem: {event.error}"
if isinstance(event.data, FailedCallback):
return f"Uh oh. Had a problem: {event.data.error}"

return f"Successfully retrieved {event.breed} named {event.name}"
return f"Successfully retrieved {event.data.breed} named {event.data.name}"

ret = handler(test_input, None)
wrapped_input = {"data": test_input}
ret = handler(wrapped_input, None)
anafalcao marked this conversation as resolved.
Show resolved Hide resolved
assert ret == expected


Expand Down
Loading