From cff37cb30b7f2e22cb96ea3ad2c97c9172e5d8e0 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Tue, 28 May 2024 11:53:33 +0100 Subject: [PATCH 1/4] Adding exception when working with unsupported objetcs --- .../event_handler/openapi/encoders.py | 145 +++++++++--------- .../event_handler/openapi/exceptions.py | 6 + docs/core/event_handler/api_gateway.md | 6 + .../event_handler/test_openapi_encoders.py | 9 ++ 4 files changed, 97 insertions(+), 69 deletions(-) diff --git a/aws_lambda_powertools/event_handler/openapi/encoders.py b/aws_lambda_powertools/event_handler/openapi/encoders.py index c12aa0164e1..8fc2cd149a7 100644 --- a/aws_lambda_powertools/event_handler/openapi/encoders.py +++ b/aws_lambda_powertools/event_handler/openapi/encoders.py @@ -14,6 +14,7 @@ from pydantic.types import SecretBytes, SecretStr from aws_lambda_powertools.event_handler.openapi.compat import _model_dump +from aws_lambda_powertools.event_handler.openapi.exceptions import EncoderError from aws_lambda_powertools.event_handler.openapi.types import IncEx """ @@ -69,88 +70,94 @@ def jsonable_encoder( # noqa: PLR0911 if exclude is not None and not isinstance(exclude, (set, dict)): exclude = set(exclude) - # Pydantic models - if isinstance(obj, BaseModel): - return _dump_base_model( - obj=obj, - include=include, - exclude=exclude, - by_alias=by_alias, - exclude_unset=exclude_unset, - exclude_none=exclude_none, - exclude_defaults=exclude_defaults, - ) + try: + # Pydantic models + if isinstance(obj, BaseModel): + return _dump_base_model( + obj=obj, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_none=exclude_none, + exclude_defaults=exclude_defaults, + ) - # Dataclasses - if dataclasses.is_dataclass(obj): - obj_dict = dataclasses.asdict(obj) - return jsonable_encoder( - obj_dict, - include=include, - exclude=exclude, - by_alias=by_alias, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) + # Dataclasses + if dataclasses.is_dataclass(obj): + obj_dict = dataclasses.asdict(obj) + return jsonable_encoder( + obj_dict, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) - # Enums - if isinstance(obj, Enum): - return obj.value + # Enums + if isinstance(obj, Enum): + return obj.value - # Paths - if isinstance(obj, PurePath): - return str(obj) + # Paths + if isinstance(obj, PurePath): + return str(obj) - # Scalars - if isinstance(obj, (str, int, float, type(None))): - return obj + # Scalars + if isinstance(obj, (str, int, float, type(None))): + return obj - # Dictionaries - if isinstance(obj, dict): - return _dump_dict( - obj=obj, - include=include, - exclude=exclude, - by_alias=by_alias, - exclude_none=exclude_none, - exclude_unset=exclude_unset, - ) + # Dictionaries + if isinstance(obj, dict): + return _dump_dict( + obj=obj, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_none=exclude_none, + exclude_unset=exclude_unset, + ) + + # Sequences + if isinstance(obj, (list, set, frozenset, GeneratorType, tuple, deque)): + return _dump_sequence( + obj=obj, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_none=exclude_none, + exclude_defaults=exclude_defaults, + exclude_unset=exclude_unset, + ) - # Sequences - if isinstance(obj, (list, set, frozenset, GeneratorType, tuple, deque)): - return _dump_sequence( + # Other types + if type(obj) in ENCODERS_BY_TYPE: + return ENCODERS_BY_TYPE[type(obj)](obj) + + for encoder, classes_tuple in encoders_by_class_tuples.items(): + if isinstance(obj, classes_tuple): + return encoder(obj) + + # Use custom serializer if present + if custom_serializer: + return custom_serializer(obj) + + # Default + return _dump_other( obj=obj, include=include, exclude=exclude, by_alias=by_alias, exclude_none=exclude_none, - exclude_defaults=exclude_defaults, exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, ) - - # Other types - if type(obj) in ENCODERS_BY_TYPE: - return ENCODERS_BY_TYPE[type(obj)](obj) - - for encoder, classes_tuple in encoders_by_class_tuples.items(): - if isinstance(obj, classes_tuple): - return encoder(obj) - - # Use custom serializer if present - if custom_serializer: - return custom_serializer(obj) - - # Default - return _dump_other( - obj=obj, - include=include, - exclude=exclude, - by_alias=by_alias, - exclude_none=exclude_none, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - ) + except ValueError as exc: + raise EncoderError( + f"Unable to serializer the object {obj} as it is not a supported type. Error details: {str(exc)}", + "See: https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/#serializing-objects", + ) from exc def _dump_base_model( diff --git a/aws_lambda_powertools/event_handler/openapi/exceptions.py b/aws_lambda_powertools/event_handler/openapi/exceptions.py index fdd829ba9b1..30ac086fc09 100644 --- a/aws_lambda_powertools/event_handler/openapi/exceptions.py +++ b/aws_lambda_powertools/event_handler/openapi/exceptions.py @@ -21,3 +21,9 @@ class RequestValidationError(ValidationException): def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None: super().__init__(errors) self.body = body + + +class EncoderError(Exception): + """ + Base exception for all encoding errors + """ diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index aa667f5f169..7b63c1fb33f 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -458,6 +458,12 @@ In the following example, we use a new `Header` OpenAPI type to add [one out of 1. `cloudfront_viewer_country` is a list that must contain values from the `CountriesAllowed` enumeration. +#### Serializing objects + +We support the serialization of various Python objects, including Pydantic models, dataclasses, enumerations, file paths, scalar types (strings, integers, floats, and None), dictionaries, various sequence types (lists, sets, frozen sets, generators, tuples, and deques), and others defined [here](https://github.com/aws-powertools/powertools-lambda-python/blob/develop/aws_lambda_powertools/event_handler/openapi/encoders.py#L24). + +For objects we do not support, such as SQLAlchemy models, we suggest providing your own [custom serializer](#custom-serializer). + ### Accessing request details Event Handler integrates with [Event Source Data Classes utilities](../../utilities/data_classes.md){target="_blank"}, and it exposes their respective resolver request details and convenient methods under `app.current_event`. diff --git a/tests/functional/event_handler/test_openapi_encoders.py b/tests/functional/event_handler/test_openapi_encoders.py index 45c65623849..5b52bf5dc0e 100644 --- a/tests/functional/event_handler/test_openapi_encoders.py +++ b/tests/functional/event_handler/test_openapi_encoders.py @@ -7,6 +7,7 @@ from pydantic.color import Color from aws_lambda_powertools.event_handler.openapi.encoders import jsonable_encoder +from aws_lambda_powertools.event_handler.openapi.exceptions import EncoderError def test_openapi_encode_include(): @@ -184,3 +185,11 @@ def __init__(self, name: str): result = jsonable_encoder(User(name="John")) assert result == {"name": "John"} + + +def test_openapi_encode_with_error(): + class MyClass: + __slots__ = [] + + with pytest.raises(EncoderError, match="Unable to serializer the object*"): + jsonable_encoder(MyClass()) From 05f9168b98acf1e70994a5c04a1143663f9b0151 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Tue, 4 Jun 2024 13:08:38 +0100 Subject: [PATCH 2/4] Addressing Simon's feedback --- aws_lambda_powertools/event_handler/openapi/encoders.py | 4 ++-- aws_lambda_powertools/event_handler/openapi/exceptions.py | 2 +- tests/functional/event_handler/test_openapi_encoders.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/aws_lambda_powertools/event_handler/openapi/encoders.py b/aws_lambda_powertools/event_handler/openapi/encoders.py index 8fc2cd149a7..6f208008b11 100644 --- a/aws_lambda_powertools/event_handler/openapi/encoders.py +++ b/aws_lambda_powertools/event_handler/openapi/encoders.py @@ -14,7 +14,7 @@ from pydantic.types import SecretBytes, SecretStr from aws_lambda_powertools.event_handler.openapi.compat import _model_dump -from aws_lambda_powertools.event_handler.openapi.exceptions import EncoderError +from aws_lambda_powertools.event_handler.openapi.exceptions import SerializationError from aws_lambda_powertools.event_handler.openapi.types import IncEx """ @@ -154,7 +154,7 @@ def jsonable_encoder( # noqa: PLR0911 exclude_defaults=exclude_defaults, ) except ValueError as exc: - raise EncoderError( + raise SerializationError( f"Unable to serializer the object {obj} as it is not a supported type. Error details: {str(exc)}", "See: https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/#serializing-objects", ) from exc diff --git a/aws_lambda_powertools/event_handler/openapi/exceptions.py b/aws_lambda_powertools/event_handler/openapi/exceptions.py index 30ac086fc09..41130a94250 100644 --- a/aws_lambda_powertools/event_handler/openapi/exceptions.py +++ b/aws_lambda_powertools/event_handler/openapi/exceptions.py @@ -23,7 +23,7 @@ def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None: self.body = body -class EncoderError(Exception): +class SerializationError(Exception): """ Base exception for all encoding errors """ diff --git a/tests/functional/event_handler/test_openapi_encoders.py b/tests/functional/event_handler/test_openapi_encoders.py index 5b52bf5dc0e..fcf591f2be7 100644 --- a/tests/functional/event_handler/test_openapi_encoders.py +++ b/tests/functional/event_handler/test_openapi_encoders.py @@ -7,7 +7,7 @@ from pydantic.color import Color from aws_lambda_powertools.event_handler.openapi.encoders import jsonable_encoder -from aws_lambda_powertools.event_handler.openapi.exceptions import EncoderError +from aws_lambda_powertools.event_handler.openapi.exceptions import SerializationError def test_openapi_encode_include(): @@ -191,5 +191,5 @@ def test_openapi_encode_with_error(): class MyClass: __slots__ = [] - with pytest.raises(EncoderError, match="Unable to serializer the object*"): + with pytest.raises(SerializationError, match="Unable to serializer the object*"): jsonable_encoder(MyClass()) From 81cf9d365f76b7db502e58b604a6ee850907b64b Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Thu, 6 Jun 2024 22:43:07 +0100 Subject: [PATCH 3/4] Addressing Heitor's feedback --- .../event_handler/openapi/encoders.py | 2 +- docs/core/event_handler/api_gateway.md | 19 ++++++++++++++++--- .../event_handler/test_openapi_encoders.py | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/aws_lambda_powertools/event_handler/openapi/encoders.py b/aws_lambda_powertools/event_handler/openapi/encoders.py index 6f208008b11..520c0d71509 100644 --- a/aws_lambda_powertools/event_handler/openapi/encoders.py +++ b/aws_lambda_powertools/event_handler/openapi/encoders.py @@ -155,7 +155,7 @@ def jsonable_encoder( # noqa: PLR0911 ) except ValueError as exc: raise SerializationError( - f"Unable to serializer the object {obj} as it is not a supported type. Error details: {str(exc)}", + f"Unable to serialize the object {obj} as it is not a supported type. Error details: {exc}", "See: https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/#serializing-objects", ) from exc diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 7b63c1fb33f..b7f7ac577d1 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -460,9 +460,22 @@ In the following example, we use a new `Header` OpenAPI type to add [one out of #### Serializing objects -We support the serialization of various Python objects, including Pydantic models, dataclasses, enumerations, file paths, scalar types (strings, integers, floats, and None), dictionaries, various sequence types (lists, sets, frozen sets, generators, tuples, and deques), and others defined [here](https://github.com/aws-powertools/powertools-lambda-python/blob/develop/aws_lambda_powertools/event_handler/openapi/encoders.py#L24). - -For objects we do not support, such as SQLAlchemy models, we suggest providing your own [custom serializer](#custom-serializer). +With data validation enabled, we natively support serializing the following data types to JSON: + +| Data type | Serialized type | +| -------------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| **Pydantic models** | `dict` | +| **Python Dataclasses** | `dict` | +| **Enum** | Enum values | +| **Datetime** | Datetime ISO format string | +| **Decimal** | `int` if no exponent, or `float` | +| **Path** | `str` | +| **UUID** | `str` | +| **Set** | `list` | +| **Python primitives** _(dict, string, sequences, numbers, booleans)_ | [Python's default JSON serializable types](https://docs.python.org/3/library/json.html#encoders-and-decoders){target="_blank" rel="nofollow"} | + +???+ info "See [custom serializer section](#custom-serializer) for bringing your own." + Otherwise, we will raise `SerializationError` for any unsupported types _e.g., SQLAlchemy models_. ### Accessing request details diff --git a/tests/functional/event_handler/test_openapi_encoders.py b/tests/functional/event_handler/test_openapi_encoders.py index fcf591f2be7..dee9c21b84c 100644 --- a/tests/functional/event_handler/test_openapi_encoders.py +++ b/tests/functional/event_handler/test_openapi_encoders.py @@ -191,5 +191,5 @@ def test_openapi_encode_with_error(): class MyClass: __slots__ = [] - with pytest.raises(SerializationError, match="Unable to serializer the object*"): + with pytest.raises(SerializationError, match="Unable to serialize the object*"): jsonable_encoder(MyClass()) From 6c79a9a11d84dd4b137b31c39d97f17ead18ce99 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Mon, 10 Jun 2024 09:01:41 +0100 Subject: [PATCH 4/4] Addressing Heitor's feedback --- docs/core/event_handler/api_gateway.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index b7f7ac577d1..4f491229505 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -458,7 +458,7 @@ In the following example, we use a new `Header` OpenAPI type to add [one out of 1. `cloudfront_viewer_country` is a list that must contain values from the `CountriesAllowed` enumeration. -#### Serializing objects +#### Supported types for response serialization With data validation enabled, we natively support serializing the following data types to JSON: