Skip to content

Commit 36afcf7

Browse files
feat(parser): infer model from type hint (#3181)
* feat(parser): infer model from type hint if possible * chore(parser): remove unnecessary branch * Small changes --------- Co-authored-by: Leandro Damascena <[email protected]>
1 parent 9489ead commit 36afcf7

File tree

4 files changed

+73
-2
lines changed

4 files changed

+73
-2
lines changed

aws_lambda_powertools/utilities/parser/parser.py

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import typing
23
from typing import Any, Callable, Dict, Optional, Type, overload
34

45
from aws_lambda_powertools.utilities.parser.compat import disable_pydantic_v2_warning
@@ -17,7 +18,7 @@ def event_parser(
1718
handler: Callable[[Any, LambdaContext], EventParserReturnType],
1819
event: Dict[str, Any],
1920
context: LambdaContext,
20-
model: Type[Model],
21+
model: Optional[Type[Model]] = None,
2122
envelope: Optional[Type[Envelope]] = None,
2223
) -> EventParserReturnType:
2324
"""Lambda handler decorator to parse & validate events using Pydantic models
@@ -76,10 +77,22 @@ def handler(event: Order, context: LambdaContext):
7677
ValidationError
7778
When input event does not conform with model provided
7879
InvalidModelTypeError
79-
When model given does not implement BaseModel
80+
When model given does not implement BaseModel or is not provided
8081
InvalidEnvelopeError
8182
When envelope given does not implement BaseEnvelope
8283
"""
84+
85+
# The first parameter of a Lambda function is always the event
86+
# This line get the model informed in the event_parser function
87+
# or the first parameter of the function by using typing.get_type_hints
88+
type_hints = typing.get_type_hints(handler)
89+
model = model or (list(type_hints.values())[0] if type_hints else None)
90+
if model is None:
91+
raise InvalidModelTypeError(
92+
"The model must be provided either as the `model` argument to `event_parser`"
93+
"or as the type hint of `event` in the handler that it wraps",
94+
)
95+
8396
parsed_event = parse(event=event, model=model, envelope=envelope) if envelope else parse(event=event, model=model)
8497
logger.debug(f"Calling handler {handler.__name__}")
8598
return handler(parsed_event, context)

docs/utilities/parser.md

+6
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ handler(event=payload, context=LambdaContext())
118118
handler(event=json.dumps(payload), context=LambdaContext()) # also works if event is a JSON string
119119
```
120120

121+
Alternatively, you can automatically extract the model from the `event` without the need to include the model parameter in the `event_parser` function.
122+
123+
```python hl_lines="23 24"
124+
--8<-- "examples/parser/src/using_the_model_from_event.py"
125+
```
126+
121127
#### parse function
122128

123129
Use this standalone function when you want more control over the data validation process, for example returning a 400 error for malformed payloads.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import json
2+
3+
from pydantic import BaseModel, validator
4+
5+
from aws_lambda_powertools.utilities.parser import event_parser
6+
from aws_lambda_powertools.utilities.parser.models import APIGatewayProxyEventV2Model
7+
from aws_lambda_powertools.utilities.typing import LambdaContext
8+
9+
10+
class CancelOrder(BaseModel):
11+
order_id: int
12+
reason: str
13+
14+
15+
class CancelOrderModel(APIGatewayProxyEventV2Model):
16+
body: CancelOrder # type: ignore[assignment]
17+
18+
@validator("body", pre=True)
19+
def transform_body_to_dict(cls, value: str):
20+
return json.loads(value)
21+
22+
23+
@event_parser
24+
def handler(event: CancelOrderModel, context: LambdaContext):
25+
cancel_order: CancelOrder = event.body
26+
27+
assert cancel_order.order_id is not None

tests/functional/parser/test_parser.py

+25
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,28 @@ def handle_no_envelope(event: Union[Dict, str], _: LambdaContext):
9393
return event
9494

9595
handle_no_envelope(dummy_event, LambdaContext())
96+
97+
98+
def test_parser_event_with_type_hint(dummy_event, dummy_schema):
99+
@event_parser
100+
def handler(event: dummy_schema, _: LambdaContext):
101+
assert event.message == "hello world"
102+
103+
handler(dummy_event["payload"], LambdaContext())
104+
105+
106+
def test_parser_event_without_type_hint(dummy_event, dummy_schema):
107+
@event_parser
108+
def handler(event, _):
109+
assert event.message == "hello world"
110+
111+
with pytest.raises(exceptions.InvalidModelTypeError):
112+
handler(dummy_event["payload"], LambdaContext())
113+
114+
115+
def test_parser_event_with_type_hint_and_non_default_argument(dummy_event, dummy_schema):
116+
@event_parser
117+
def handler(evt: dummy_schema, _: LambdaContext):
118+
assert evt.message == "hello world"
119+
120+
handler(dummy_event["payload"], LambdaContext())

0 commit comments

Comments
 (0)