From 297ef84860c96d3aeb5c1440865e7bef667c2306 Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Tue, 21 Nov 2023 18:16:10 +0100 Subject: [PATCH 01/36] chore(docs): added validation and OpenAPI docs --- docs/core/event_handler/api_gateway.md | 36 +- docs/core/event_handler/api_gateway/index.md | 8 + docs/core/event_handler/bedrock_agents.md | 14 + docs/core/event_handler/openapi.md | 310 ++++++++++++++++++ docs/media/swagger.png | Bin 0 -> 54463 bytes .../sam/template.yaml | 41 +++ .../src/body_parsing.json | 40 +++ .../src/body_parsing.py | 28 ++ .../src/body_parsing_embed.json | 40 +++ .../src/body_parsing_embed.py | 29 ++ .../src/body_parsing_embed_output.json | 9 + .../src/body_parsing_output.json | 9 + .../src/customize_api_metadata.py | 33 ++ .../src/customizing_operations.py | 30 ++ .../src/customizing_parameters.py | 30 ++ .../src/generate_openapi_json_spec.json | 136 ++++++++ .../src/generate_openapi_json_spec.py | 32 ++ .../src/generate_openapi_spec.py | 32 ++ .../src/getting_started.json | 42 +++ .../src/getting_started.py | 25 ++ .../src/getting_started_bad_input.json | 42 +++ .../src/getting_started_bad_input_output.json | 9 + .../src/getting_started_output.json | 9 + .../src/getting_started_pydantic.json | 41 +++ .../src/getting_started_pydantic.py | 28 ++ .../src/getting_started_pydantic_output.json | 9 + .../src/requirements.txt | 3 + .../event_handler_validation/src/swagger.py | 29 ++ .../src/swagger_customize.py | 29 ++ .../src/swagger_middlewares.py | 40 +++ mkdocs.yml | 4 +- 31 files changed, 1148 insertions(+), 19 deletions(-) create mode 100644 docs/core/event_handler/api_gateway/index.md create mode 100644 docs/core/event_handler/bedrock_agents.md create mode 100644 docs/core/event_handler/openapi.md create mode 100644 docs/media/swagger.png create mode 100644 examples/event_handler_validation/sam/template.yaml create mode 100644 examples/event_handler_validation/src/body_parsing.json create mode 100644 examples/event_handler_validation/src/body_parsing.py create mode 100644 examples/event_handler_validation/src/body_parsing_embed.json create mode 100644 examples/event_handler_validation/src/body_parsing_embed.py create mode 100644 examples/event_handler_validation/src/body_parsing_embed_output.json create mode 100644 examples/event_handler_validation/src/body_parsing_output.json create mode 100644 examples/event_handler_validation/src/customize_api_metadata.py create mode 100644 examples/event_handler_validation/src/customizing_operations.py create mode 100644 examples/event_handler_validation/src/customizing_parameters.py create mode 100644 examples/event_handler_validation/src/generate_openapi_json_spec.json create mode 100644 examples/event_handler_validation/src/generate_openapi_json_spec.py create mode 100644 examples/event_handler_validation/src/generate_openapi_spec.py create mode 100644 examples/event_handler_validation/src/getting_started.json create mode 100644 examples/event_handler_validation/src/getting_started.py create mode 100644 examples/event_handler_validation/src/getting_started_bad_input.json create mode 100644 examples/event_handler_validation/src/getting_started_bad_input_output.json create mode 100644 examples/event_handler_validation/src/getting_started_output.json create mode 100644 examples/event_handler_validation/src/getting_started_pydantic.json create mode 100644 examples/event_handler_validation/src/getting_started_pydantic.py create mode 100644 examples/event_handler_validation/src/getting_started_pydantic_output.json create mode 100644 examples/event_handler_validation/src/requirements.txt create mode 100644 examples/event_handler_validation/src/swagger.py create mode 100644 examples/event_handler_validation/src/swagger_customize.py create mode 100644 examples/event_handler_validation/src/swagger_middlewares.py diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index e9b15bf8c00..ae651acd26f 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -9,7 +9,7 @@ Event handler for Amazon API Gateway REST and HTTP APIs, Application Loader Bala * Lightweight routing to reduce boilerplate for API Gateway REST/HTTP API, ALB and Lambda Function URLs. * Support for CORS, binary and Gzip compression, Decimals JSON encoding and bring your own JSON serializer -* Built-in integration with [Event Source Data Classes utilities](../../utilities/data_classes.md){target="_blank"} for self-documented event schema +* Built-in integration with [Event Source Data Classes utilities](../../../utilities/data_classes.md){target="_blank"} for self-documented event schema * Works with micro function (one or a few routes) and monolithic functions (all routes) ## Getting started @@ -223,7 +223,7 @@ If you need to accept multiple HTTP methods in a single function, you can use th ### 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`. +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`. That is why you see `app.resolve(event, context)` in every example. This allows Event Handler to resolve requests, and expose data like `app.lambda_context` and `app.current_event`. @@ -434,8 +434,8 @@ Here's a sample middleware that extracts and injects correlation ID, using `APIG #### Global middlewares
-![Combining middlewares](../../media/middlewares_normal_processing-light.svg#only-light) -![Combining middlewares](../../media/middlewares_normal_processing-dark.svg#only-dark) +![Combining middlewares](../../../media/middlewares_normal_processing-light.svg#only-light) +![Combining middlewares](../../../media/middlewares_normal_processing-dark.svg#only-dark) _Request flowing through multiple registered middlewares_
@@ -475,8 +475,8 @@ Event Handler **calls global middlewares first**, then middlewares defined at th #### Returning early
-![Short-circuiting middleware chain](../../media/middlewares_early_return-light.svg#only-light) -![Short-circuiting middleware chain](../../media/middlewares_early_return-dark.svg#only-dark) +![Short-circuiting middleware chain](../../../media/middlewares_early_return-light.svg#only-light) +![Short-circuiting middleware chain](../../../media/middlewares_early_return-dark.svg#only-dark) _Interrupting request flow by returning early_
@@ -523,8 +523,8 @@ While there isn't anything special on how to use [`try/catch`](https://docs.pyth An exception wasn't caught by any middleware during `next_middleware()` block, therefore it propagates all the way back to the client as HTTP 500.
- ![Unhandled exceptions](../../media/middlewares_unhandled_route_exception-light.svg#only-light) - ![Unhandled exceptions](../../media/middlewares_unhandled_route_exception-dark.svg#only-dark) + ![Unhandled exceptions](../../../media/middlewares_unhandled_route_exception-light.svg#only-light) + ![Unhandled exceptions](../../../media/middlewares_unhandled_route_exception-dark.svg#only-dark) _Unhandled route exceptions propagate back to the client_
@@ -534,8 +534,8 @@ While there isn't anything special on how to use [`try/catch`](https://docs.pyth An exception was only caught by the third middleware, resuming the normal execution of each `After` logic for the second and first middleware.
- ![Middleware handling exceptions](../../media/middlewares_catch_route_exception-light.svg#only-light) - ![Middleware handling exceptions](../../media/middlewares_catch_route_exception-dark.svg#only-dark) + ![Middleware handling exceptions](../../../media/middlewares_catch_route_exception-light.svg#only-light) + ![Middleware handling exceptions](../../../media/middlewares_catch_route_exception-dark.svg#only-dark) _Unhandled route exceptions propagate back to the client_
@@ -545,8 +545,8 @@ While there isn't anything special on how to use [`try/catch`](https://docs.pyth The third middleware short-circuited the chain by raising an exception and completely skipping the fourth middleware. Because we only caught it in the first middleware, it skipped the `After` logic in the second middleware.
- ![Catching exceptions](../../media/middlewares_catch_exception-light.svg#only-light) - ![Catching exceptions](../../media/middlewares_catch_exception-dark.svg#only-dark) + ![Catching exceptions](../../../media/middlewares_catch_exception-light.svg#only-light) + ![Catching exceptions](../../../media/middlewares_catch_exception-dark.svg#only-dark) _Middleware handling short-circuit exceptions_
@@ -573,9 +573,9 @@ As a practical example, let's refactor our correlation ID middleware so it accep These are native middlewares that may become native features depending on customer demand. -| Middleware | Purpose | -| ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| [SchemaValidationMiddleware](/lambda/python/latest/api/event_handler/middlewares/schema_validation.html){target="_blank"} | Validates API request body and response against JSON Schema, using [Validation utility](../../utilities/validation.md){target="_blank"} | +| Middleware | Purpose | +| ---------------------------------------------------------------------------------------------------- |--------------------------------------------------------------------------------------------------------------------------------------------| +| [SchemaValidationMiddleware](/lambda/python/latest/api/event_handler/middlewares/schema_validation.html){target="_blank"} | Validates API request body and response against JSON Schema, using [Validation utility](../../../utilities/validation.md){target="_blank"} | #### Being a good citizen @@ -683,7 +683,7 @@ Like `compress` feature, the client must send the `Accept` header with the corre ### Debug mode -You can enable debug mode via `debug` param, or via `POWERTOOLS_DEV` [environment variable](../../index.md#environment-variables){target="_blank"}. +You can enable debug mode via `debug` param, or via `POWERTOOLS_DEV` [environment variable](../../../index.md#environment-variables){target="_blank"}. This will enable full tracebacks errors in the response, print request and responses, and set CORS in development mode. @@ -833,7 +833,7 @@ Both single (monolithic) and multiple functions (micro) offer different set of t #### Monolithic function -![Monolithic function sample](./../../media/monolithic-function.png) +![Monolithic function sample](./../../../media/monolithic-function.png) A monolithic function means that your final code artifact will be deployed to a single function. This is generally the best approach to start. @@ -852,7 +852,7 @@ _**Downsides**_ #### Micro function -![Micro function sample](./../../media/micro-function.png) +![Micro function sample](./../../../media/micro-function.png) A micro function means that your final code artifact will be different to each function deployed. This is generally the approach to start if you're looking for fine-grain control and/or high load on certain parts of your service. diff --git a/docs/core/event_handler/api_gateway/index.md b/docs/core/event_handler/api_gateway/index.md new file mode 100644 index 00000000000..58987e718b4 --- /dev/null +++ b/docs/core/event_handler/api_gateway/index.md @@ -0,0 +1,8 @@ +--- +title: REST API +description: Core utility +--- + + + +--8<-- "docs/core/event_handler/api_gateway.md" diff --git a/docs/core/event_handler/bedrock_agents.md b/docs/core/event_handler/bedrock_agents.md new file mode 100644 index 00000000000..041487a3313 --- /dev/null +++ b/docs/core/event_handler/bedrock_agents.md @@ -0,0 +1,14 @@ +--- +title: Bedrock Agent API +description: Core utility +--- + +## Key Features + +## Getting started + +### Required resources + +## Advanced + +## Testing your code diff --git a/docs/core/event_handler/openapi.md b/docs/core/event_handler/openapi.md new file mode 100644 index 00000000000..1a328064987 --- /dev/null +++ b/docs/core/event_handler/openapi.md @@ -0,0 +1,310 @@ +--- +title: Validation & OpenAPI +description: Core utility +--- + +Comprehensive data validation and OpenAPI generation based on introspection of Python types. + +## Key Features + +* Declarative validation of inputs and outputs +* Support for scalar types, dataclasses and Pydantic models +* Automatic generation of OpenAPI specifications from the API definition +* Embedded Swagger UI for interactive API documentation + +## Getting started + +To use validation and OpenAPI features with our core utility, you must first ensure that pydantic is installed as it is a prerequisite. This utility framework supports both pydantic version 1 and version 2. For detailed guidance on setting up the parser, visit the [Parser documentation](./../../../../utilities/parser/#getting-started). + +This documentation specifically focuses on the utility's validation and OpenAPI capabilities. These features are built on top of the Event Handler, thereby streamlining the process of validating inputs and outputs and automatically generating OpenAPI specifications based on your API definitions. + +### Basic usage + +To enable the validation logic, you need to pass the `enable_validation` parameter to your REST API resolver. This changes the way your resolver gets called. We will inspect +your handler do termine the input and output parameters, and will validate / coerce the data before calling your handler. + +To enable the validation mechanism within your REST API, you'll need to use the `enable_validation` parameter when defining your API resolver. +This modifies the invocation process of your resolver function. Powertools will analyze your handler to identify the input and output parameters. +Once these parameters are determined, we ensure that the data is validated and coerced accordingly before it ever reaches your handler. +This process is designed to enforce a layer of integrity, so that your functions operate on clean and verified inputs, leading to more reliable and maintainable code. + +=== "getting_started.py" + + ```python hl_lines="10 13 15 19" + --8<-- "examples/event_handler_validation/src/getting_started.py" + ``` + +=== "event.json" + + ```json hl_lines="4" + --8<-- "examples/event_handler_validation/src/getting_started.json" + ``` + +=== "output.json" + + ```json hl_lines="2 8" + --8<-- "examples/event_handler_validation/src/getting_started_output.json" + ``` + +If the validation process encounters data that does not conform to the specified input schema, the system triggers a validation error. This results in an HTTP 442 Unprocessable Entity error, which indicates that the input was understood by the server but contained invalid fields. + +Here's an example of what the error response might look like when the validation fails due to bad input: + +=== "bad_input_event.json" + + ```json hl_lines="4" + --8<-- "examples/event_handler_validation/src/getting_started_bad_input.json" + ``` + +=== "output.json" + + ```json hl_lines="2 3" + --8<-- "examples/event_handler_validation/src/getting_started_bad_input_output.json" + ``` + +???+ note "Pydantic v1 vs v2" + Pydantic version 1 and version 2 might describe these validation errors differently. Hence, you should consult the relevant version's documentation to understand the exact format and style of the error messages for the version you are using. + +### Using Pydantic models + +Pydantic models provide a powerful syntax for declaring complex data structures along with the rules to validate the incoming data. These models can be used directly as input parameters or return types, letting you take full advantage of Pydantic's breadth of features, including data coercion, default values, and advanced validation. + +Let's take a look at how you can utilize Pydantic models: + +=== "getting_started_pydantic.py" + + ```python hl_lines="9 12 20 24" + --8<-- "examples/event_handler_validation/src/getting_started_pydantic.py" + ``` + +=== "event.json" + + ```json hl_lines="4 5 15" + --8<-- "examples/event_handler_validation/src/getting_started_pydantic.json" + ``` + +=== "output.json" + + ```json hl_lines="2 3" + --8<-- "examples/event_handler_validation/src/getting_started_pydantic_output.json" + ``` + +### SwaggerUI + +Swagger UI provides a web-based interface for visualizing and interacting with your API's resources. By enabling Swagger UI for your API, you create an interactive documentation page that can be used for testing and exploring your API endpoints in real-time. + +WARNING: this will create a publicly accessible Swagger UI page. See Advanced for how to customize and protect your +Swagger UI + +???+ warning "Publicly accessible by default" + The Swagger UI page will be publicly accessible when enabled. If your API contains sensitive endpoints or you wish to restrict access to the documentation, it's crucial to consider adding authentication mechanisms or other protections. + See the [Customize the Swagger UI](#customizing-the-swagger-ui) section of this documentation to learn details on customizing and securing your Swagger UI, ensuring it suits your specific requirements while providing the necessary protection for your API's interactive documentation. + +```python hl_lines="9 10" +--8<-- "examples/event_handler_validation/src/swagger.py" +``` + +Here's an example of what it looks like by default: + +![Swagger UI](../../media/swagger.png) + +## Advanced + +### Customizing parameters + +Annotations are a useful way to enrich your API's parameters with metadata and validation constraints, thereby enhancing the functionality and documentation of your API. Python's [Annotated type, introduced in PEP 593](https://peps.python.org/pep-0593/), allows you to attach additional metadata to type hints, which can then be used by your validation library or documentation tools. + +If you are working with parameters that are part of the URL path, query strings, or request bodies, certain specialized classes or decorators are often available to assist with defining these parameters more explicitly. This can include specifying default values, validation rules, and descriptions for better OpenAPI generation. + +Here is an example demonstrating how you might customize your API parameters using annotations: + +```python hl_lines="1 7 19 20" +--8<-- "examples/event_handler_validation/src/customizing_parameters.py" +``` + +???+ note + Powertools doesn't have support for files, form data, and header parameters at the moment. If you're interested in this, please [open an issue](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=feature-request%2Ctriage&projects=&template=feature_request.yml&title=Feature+request%3A+TITLE). + +Adding titles and descriptions to your parameters is beneficial because it clarifies the intended use and constraints of the API for end-users and developers alike. +When the API is rendered in OpenAPI documentation tools, these annotations will be converted into readable descriptions, providing a self-explanatory interface for interacting with your API. +This can significantly improve the developer experience and reduce the learning curve for new users of your API. + +Here's a table of all possible customizations you can do: + +| Field name | Type | Description | +|-----------------------|---------------|---------------------------------------------------------------------------------------------------------------------------------------------| +| `alias` | `str` | Alternative name for a field, used when serializing and deserializing data | +| `validation_alias` | `str` | Alternative name for a field during validation (but not serialization) | +| `serialization_alias` | `str` | Alternative name for a field during serialization (but not during validation) | +| `description` | `str` | Human-readable description | +| `gt` | `float` | Greater than. If set, value must be greater than this. Only applicable to numbers | +| `ge` | `float` | Greater than or equal. If set, value must be greater than or equal to this. Only applicable to numbers | +| `lt` | `float` | Less than. If set, value must be less than this. Only applicable to numbers | +| `le` | `float` | Less than or equal. If set, value must be less than or equal to this. Only applicable to numbers | +| `min_length` | `int` | Minimum length for strings | +| `max_length` | `int` | Maximum length for strings | +| `pattern` | `string` | A regular expression that the string must match. | +| `strict` | `bool` | If `True`, strict validation is applied to the field. See [Strict Mode](https://docs.pydantic.dev/latest/concepts/strict_mode/) for details | +| `multiple_of` | `float` | Value must be a multiple of this. Only applicable to numbers | +| `allow_inf_nan` | `bool` | Allow `inf`, `-inf`, `nan`. Only applicable to numbers | +| `max_digits` | `int` | Maximum number of allow digits for strings | +| `decimal_places` | `int` | Maximum number of decimal places allowed for numbers | +| `examples` | `List\[Any\]` | List of examples of the field | +| `deprecated` | `bool` | Marks the field as deprecated | +| `include_in_schema` | `bool` | If `False` the field will not be part of the exported OpenAPI schema | +| `json_schema_extra` | `JsonDict` | Any additional JSON schema data for the schema property | + +### Body parameters + +Handling JSON objects in the body of your API requests is simple with Pydantic models. We automate the parsing of the request bodies using the models you define, +ensuring that the data structures rescived are aligned with your API's expectations. + +Here's how to define and parse body parameters using a Pydantic model: + +=== "body_parsing.py" + + ```python hl_lines="12 19 20" + --8<-- "examples/event_handler_validation/src/body_parsing.py" + ``` + +=== "event.json" + + ```json hl_lines="21 22 33" + --8<-- "examples/event_handler_validation/src/body_parsing.json" + ``` + +=== "output.json" + + ```json hl_lines="3" + --8<-- "examples/event_handler_validation/src/body_parsing_output.json" + ``` + +When using the Body wrapper with embed, your JSON payload will need to be provided as a nested object under a key that matches the name of the parameter: + +=== "body_parsing_embed.py" + + ```python hl_lines="1 7 21" + --8<-- "examples/event_handler_validation/src/body_parsing_embed.py" + ``` + +=== "event.json" + + ```json hl_lines="21 22 33" + --8<-- "examples/event_handler_validation/src/body_parsing_embed.json" + ``` + +=== "output.json" + + ```json hl_lines="3" + --8<-- "examples/event_handler_validation/src/body_parsing_embed_output.json" + ``` + +### Customizing API operations + +Customizing your API endpoints involves adding specific metadata to your endpoint definitions, allowing you to provde descriptive documentation for API consumers and offer additional instructions to the underlying framework. +Below is a detailed explanation of various fields that you can customize: + +| Field Name | Type | Description | +|------------------------|-----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `summary` | `str` | A concise overview of the main functionality of the endpoint. This brief introduction is usually displayed in autogenerated API documentation and helps consumers quickly understand what the endpoint does. | +| `description` | `str` | A more detailed explanation of the endpoint, which can include information about the operation's behavior, including side effects, error states, and other operational guidelines. | +| `responses` | `Dict[int, Dict[str, Any]]` | A dictionary that maps each HTTP status code to a Response Object as defined by the [OpenAPI Specification](https://swagger.io/specification/#response-object). This allows you to describe expected responses, including default or error messages, and their corresponding schemas for different status codes. | +| `response_description` | `str` | Provides the default textual description of the response sent by the endpoint when the operation is successful. It is intended to give a human-readable understanding of the result. | +| `tags` | `List[str]` | Tags are a way to categorize and group endpoints within the API documentation. They can help organize the operations by resources or other heuristic. | +| `operation_id` | `str` | A unique identifier for the operation, which can be used for referencing this operation in documentation or code. This ID must be unique across all operations described in the API. | +| `include_in_schema` | `bool` | A boolean value that determines whether or not this operation should be included in the OpenAPI schema. Setting it to `False` can hide the endpoint from generated documentation and schema exports, which might be useful for private or experimental endpoints. | + +To apply these customizations, you add additional parameters when declaring your routes: + +=== "Customizing API operations metadata" + + ```python hl_lines="11-20" + --8<-- "examples/event_handler_validation/src/customizing_operations.py" + ``` + +### Generating OpenAPI specifications + +OpenAPI specifications are integral to understanding and interacting with modern web APIs. They describe the entire API, including routes, parameters, responses, and more. This specification can be machine-generated from your codebase, ensuring it remains up-to-date with your API's implementation. + +With Powertools, these specifications can be outputted as a Pydantic object or as a raw JSON schema string: + +=== "OpenAPI specification as a Pydantic object" + + ```python hl_lines="32" + --8<-- "examples/event_handler_validation/src/generate_openapi_spec.py" + ``` + +=== "OpenAPI specification as a JSON schema string" + + ```python hl_lines="32" + --8<-- "examples/event_handler_validation/src/generate_openapi_json_spec.py" + ``` + +=== "OpenAPI JSON schema" + + ```json + --8<-- "examples/event_handler_validation/src/generate_openapi_json_spec.json" + ``` + +???+ note "Why opt for the Pydantic object?" + Having the OpenAPI specification as a Pydantic object provides several advantages: + + 1. **Post-Processing:** You may wish to programmatically alter or enrich the OpenAPI specification after it's generated but before you serve it or pass it on. For instance, you could add examples, merge multiple specifications, or adjust descriptions dynamically. + 2. **Internal Use:** Maybe your goal is not to expose the specification externally but to utilize it within your system for validation, mocking, or other quality assurance techniques. + 3. **Dynamic Behavior:** If you need to control the representation of the schema based on conditions not expressible statically in the code (e.g., user permissions, environment variables), a Pydantic object could be manipulated prior to serialization. + 4. **Fragment Reuse:** If your setup involves microservices or a plugin architecture, you might need to generate partial schemas and combine them into a larger API gateway schema. + 5. **Testing and Automation:** For testing purposes, it’s often useful to have the schema in a manipulatable form to validate that certain changes are present or to automate API tests. + +#### Customizing OpenAPI metadata + +Customizing the OpenAPI metadata allows you to provide detailed, top-level information about your API. Here's how you can define and customize this metadata: + +| Field Name | Type | Description | +|--------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `title` | `str` | The title for your API. It should be a concise, specific name that can be used to identify the API in documentation or listings. | +| `version` | `str` | The version of the API you are documenting. This could reflect the release iteration of the API and helps clients understand the evolution of the API. | +| `openapi_version` | `str` | Specifies the version of the OpenAPI Specification on which your API is based. For most contemporary APIs, the default value would be `3.0.0` or higher. | +| `summary` | `str` | A short and informative summary that can provide an overview of what the API does. This can be the same as or different from the title but should add context or information. | +| `description` | `str` | A verbose description that can include Markdown formatting, providing a full explanation of the API's purpose, functionalities, and general usage instructions. | +| `tags` | `List[str]` | A collection of tags that categorize endpoints for better organization and navigation within the documentation. This can group endpoints by their functionality or other criteria. | +| `servers` | `List[Server]` | An array of Server objects, which specify the URL to the server and a description for its environment (production, staging, development, etc.), providing connectivity information. | +| `terms_of_service` | `str` | A URL that points to the terms of service for your API. This could provide legal information and user responsibilities related to the usage of the API. | +| `contact` | `Contact` | A Contact object containing contact details of the organization or individuals maintaining the API. This may include fields such as name, URL, and email. | +| `license_info` | `License` | A License object providing the license details for the API, typically including the name of the license and the URL to the full license text. | + +To apply these customizations, you add additional parameters when exporting your OpenAPI specification: + +=== "Customizing OpenAPI metadata" + + ```python hl_lines="25-31" + --8<-- "examples/event_handler_validation/src/customize_api_metadata.py" + ``` + +### Customizing the Swagger UI + +By default, the Swagger UI may be served under the `/swagger` path, but customization options are often provided to allow you to serve the documentation from a different path, as well as to define where the necessary Swagger UI assets are loaded from. + +Here is an example of how you could configure the loading of the Swagger UI from a custom path or CDN. Additionally, the Swagger UI assets such as the CSS and JavaScript bundles are directed to load from a specified CDN base URL. + +=== "Customizing Swagger path and CDN" + + ```python hl_lines="10" + --8<-- "examples/event_handler_validation/src/swagger_customize.py" + ``` + +???+note "Customizing the Swagger metadata" + The `enable_swagger` method accepts the same metadata as described at [Customizing OpenAPI metadata](#customizing-openapi-metadata). + +=== "Using middlewares with the Swagger UI" + +To complement these customizations, it's possible to introduce middleware on the Swagger UI endpoiunt. +Middleware can be used for tasks like adding security headers, user authentication, or other processing that needs to occur on requests serving the Swagger UI. + + ```python hl_lines="7 13-18 21" + --8<-- "examples/event_handler_validation/src/swagger_middlewares.py" + ``` + +## Testing your code + +For comprehensive guidance on how to test your code effectively, please refer to the documentation specific to the [REST API documentation](../api_gateway/#testing-your-code). +The referenced documentation will provide you with best practices, testing techniques, and examples on how to write tests for your API code. diff --git a/docs/media/swagger.png b/docs/media/swagger.png new file mode 100644 index 0000000000000000000000000000000000000000..3db7786886b08e4909f2f73e2e9f4ecc5f16c359 GIT binary patch literal 54463 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

XMDc7Vh@+(d=~5FzW9bXXyu8VC>x=wN zh8SMCVxQ)+T|9roWF;}!(1DYe4Fl@)9v;LI9DXHmZYm9qhq$9}%6bi}xO}V0_w4X)u5IyNJJa zbmvQ)Y+_Abm_EY4&8z|O;sB~*b%RX20u}ci>=!V3?mi}BRLE-b`MYOMl4R6)3R{79 z{2{ik^Qa8OTIxpAH;5|;&GV9tYcNiVEpJ76f{rlw-*@vYd1|Nux%)cyG?}R{0nAlb zaH3EK=Yv3k?uf!ncbsLsP3#4*;5a4hUS)n*`TUcsNNP0{`y>*Mbw zf*1hHurcrsa77%5+pfMuAjP2Ey8ucFP*VdLNPQ^mJ#L?z>)czGR0iXy0}&R8;^aO) zIEGI032mxcXclJ4Q*Pfe1Ho)AL`@RWFT}3WEg1<}Z*vn68p~0p8tw_%ZlAg%}Jc1kBY8QYYjCj5%i;h9$qeEWw_e--?KI-jU$d3sBL3EHzL8 zY(R3?a}<0b6Yx?PN-vH(0LgSsnXpn^P>2m?kwPs5 z0Uk%Wj<%;r12H%ro=Q02QC4;V%&T6#mH@qbr~vdjCEA-oN76p#W8!3q@O2{ZZA*R# zGTo~;T#F-H+ko{PHX_>=x0ih37!q-q3v1L?H&B;QLgAPSxtLz|c*P$$XO1?&kU_}?%p?@alII(jy*J$DMc$3#~5%FL)oAHH@?P#2U7N7|J z0bdBHsA(XqL~aAZfkYUp5Kn^ztc{784`umD~sLE2xFia3>s+a`qPY)e-+eLW%isRofnOq!r`B+Z)r#uHi zs4FWWq!MhT03N+1cGW?6ch~R=1!u?^q`v@h^*#uwq5T!X)UdW$$NdTnV(E09AY^zE z3y(69f|{@;4QP{o#(t8-y*{KE!6(x&QKQzi>Oid)O0CCegLlK?y>zC6$;->N*k)AB zvGEDkcHiU!p07~^Ek&%4Py?gWtesU7k+#M~kh8IW2rYT`LEHJqZSOn)AV%INPQ`Jp z-JsL)vBu0ZunV}LX8rfJHJ9vwb;<+7b;*~j&G(R?m9wHvW^MvJOC6&Yus1(&8>yNr zcIliK1>WCCs~1g{DS&0JVcY?{<8OwWp{^P?fVXj~)^m8#DOgOlQ~p^+3rOvuDand4sh z_LfPGXlU(n$4h_^S2%|qw`Imb&dBaJj<6V(1Ew)P5b#uCF;-V3&KN62U@I(jcrqP; z^tZSm8UvYnS^4!UloBOQ0L57rA5$PnDLp zi|1g4boof1dlF#XuGECX3jnhai3p4F>O?7BH9F6vc_7O3`5F(@F_JZkN zi!Uv!{t-WF0CZC-S#S@tT@dQhAj5SG4if<9(1-ff1~V+5m3sFN4GuqWpVpzQ5IpLQ zLcVYVq~Uv%43N_k43eZR&JEC1asUpU8Uu9*eb@4PRS&^%i{FD3EM)A;1RQiPR>C%j z!J}gHVhpn&*2`l{wb*+kl=DqvPc0qY(mi(B+jisp)3(}6I++1rM9%>vS4FL&L>lB; zTw+wqUR&%wsmKn=2Zs7kt-CX_Qu93Fj5-CjA7>Ihm^?lW;kZ9tgKV2J)cn5b zi`l{L(CNxt0K$O(qXNu-S7!F#!Mgt`pS%g`cK!>T9!lGRI0=%vX8+e|du^QLd-7Fi zCg!cHjiL&-X9Qz1B(=#C1F9Qa{U6MI@c#yLa@1K^dD^^a;)(a~f9ab~x+e<5SCWuL zZVmY} int: + response = requests.post("https://jsonplaceholder.typicode.com/todos", json=todo.dict(by_alias=True)) + response.raise_for_status() + + return response.json()["id"] + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_validation/src/body_parsing_embed.json b/examples/event_handler_validation/src/body_parsing_embed.json new file mode 100644 index 00000000000..629e8da4958 --- /dev/null +++ b/examples/event_handler_validation/src/body_parsing_embed.json @@ -0,0 +1,40 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/todos", + "rawQueryString": "", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "header1": "value1", + "header2": "value1,value2" + }, + "queryStringParameters": {}, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/todos", + "protocol": "HTTP/1.1", + "sourceIp": "192.0.2.1", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": "{ \"todo\": {\"title\": \"foo\", \"userId\": \"1\", \"completed\": false } }", + "pathParameters": {}, + "isBase64Encoded": false, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } +} diff --git a/examples/event_handler_validation/src/body_parsing_embed.py b/examples/event_handler_validation/src/body_parsing_embed.py new file mode 100644 index 00000000000..7e4d9ab7426 --- /dev/null +++ b/examples/event_handler_validation/src/body_parsing_embed.py @@ -0,0 +1,29 @@ +from typing import Annotated, Optional + +import requests +from pydantic import BaseModel, Field + +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler.openapi.params import Body +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayHttpResolver(enable_validation=True) + + +class Todo(BaseModel): + userId: int + id_: Optional[int] = Field(alias="id", default=None) + title: str + completed: bool + + +@app.post("/todos") +def create_todo(todo: Annotated[Todo, Body(embed=True)]) -> int: + response = requests.post("https://jsonplaceholder.typicode.com/todos", json=todo.dict(by_alias=True)) + response.raise_for_status() + + return response.json()["id"] + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_validation/src/body_parsing_embed_output.json b/examples/event_handler_validation/src/body_parsing_embed_output.json new file mode 100644 index 00000000000..564a5e7a9fb --- /dev/null +++ b/examples/event_handler_validation/src/body_parsing_embed_output.json @@ -0,0 +1,9 @@ +{ + "statusCode": 200, + "body": "2008822", + "isBase64Encoded": false, + "headers": { + "Content-Type": "application/json" + }, + "cookies": [] +} diff --git a/examples/event_handler_validation/src/body_parsing_output.json b/examples/event_handler_validation/src/body_parsing_output.json new file mode 100644 index 00000000000..c01584c86ed --- /dev/null +++ b/examples/event_handler_validation/src/body_parsing_output.json @@ -0,0 +1,9 @@ +{ + "statusCode": 200, + "body": "2008821", + "isBase64Encoded": false, + "headers": { + "Content-Type": "application/json" + }, + "cookies": [] +} diff --git a/examples/event_handler_validation/src/customize_api_metadata.py b/examples/event_handler_validation/src/customize_api_metadata.py new file mode 100644 index 00000000000..74ad6ecc7a3 --- /dev/null +++ b/examples/event_handler_validation/src/customize_api_metadata.py @@ -0,0 +1,33 @@ +import requests + +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler.openapi.models import Contact, Server +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayHttpResolver(enable_validation=True) + + +@app.get("/todos/") +def get_todo_title(todo_id: int) -> str: + todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") + todo.raise_for_status() + + return todo.json()["title"] + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) + + +if __name__ == "__main__": + print( + app.get_openapi_json_schema( + title="TODO's API", + version="1.21.3", + summary="API to manage TODOs", + description="This API implements all the CRUD operations for the TODO app", + tags=["todos"], + servers=[Server(url="https://stg.example.org/orders", description="Staging server")], + contact=Contact(name="John Smith", email="john@smith.com"), + ), + ) diff --git a/examples/event_handler_validation/src/customizing_operations.py b/examples/event_handler_validation/src/customizing_operations.py new file mode 100644 index 00000000000..109e4272a36 --- /dev/null +++ b/examples/event_handler_validation/src/customizing_operations.py @@ -0,0 +1,30 @@ +import requests + +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayHttpResolver(enable_validation=True) + + +@app.get( + "/todos/", + summary="Retrieves a todo item", + description="Loads a todo item identified by the `todo_id`", + response_description="The todo object", + responses={ + 200: {"description": "Todo item found"}, + 404: { + "description": "Item not found", + }, + }, + tags=["Todos"], +) +def get_todo_title(todo_id: int) -> str: + todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") + todo.raise_for_status() + + return todo.json()["title"] + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_validation/src/customizing_parameters.py b/examples/event_handler_validation/src/customizing_parameters.py new file mode 100644 index 00000000000..6659b43ad44 --- /dev/null +++ b/examples/event_handler_validation/src/customizing_parameters.py @@ -0,0 +1,30 @@ +from typing import Annotated + +import requests + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler.openapi.params import Param, Path +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayHttpResolver(enable_validation=True) + + +@app.get("/todos/") +@tracer.capture_method +def get_todo_title( + todo_id: Annotated[int, Path(gt=0, title="Todo ID", description="The ID of the Todo to fetch the title from")], +) -> Annotated[str, Param(title="Title", description="The title of the Todo item")]: + todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") + todo.raise_for_status() + + return todo.json()["title"] + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_validation/src/generate_openapi_json_spec.json b/examples/event_handler_validation/src/generate_openapi_json_spec.json new file mode 100644 index 00000000000..e1ea513ded5 --- /dev/null +++ b/examples/event_handler_validation/src/generate_openapi_json_spec.json @@ -0,0 +1,136 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Powertools API", + "version": "1.0.0" + }, + "servers": [ + { + "url": "/" + } + ], + "paths": { + "/todos": { + "get": { + "summary": "GET /todos", + "operationId": "get_todos_by_email_todos_get", + "parameters": [ + { + "required": true, + "schema": { + "type": "string", + "format": "email", + "title": "Email" + }, + "name": "email", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Todo" + }, + "type": "array", + "title": "Return" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "Todo": { + "properties": { + "userId": { + "type": "integer", + "title": "Userid" + }, + "id": { + "type": "integer", + "title": "Id" + }, + "title": { + "type": "string", + "title": "Title" + }, + "completed": { + "type": "boolean", + "title": "Completed" + } + }, + "type": "object", + "required": [ + "userId", + "id", + "title", + "completed" + ], + "title": "Todo" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + } + } + } +} diff --git a/examples/event_handler_validation/src/generate_openapi_json_spec.py b/examples/event_handler_validation/src/generate_openapi_json_spec.py new file mode 100644 index 00000000000..7563bac9ed9 --- /dev/null +++ b/examples/event_handler_validation/src/generate_openapi_json_spec.py @@ -0,0 +1,32 @@ +from typing import List + +import requests +from pydantic import BaseModel, EmailStr, Field + +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayHttpResolver(enable_validation=True) + + +class Todo(BaseModel): + userId: int + id_: int = Field(alias="id") + title: str + completed: bool + + +@app.get("/todos") +def get_todos_by_email(email: EmailStr) -> List[Todo]: + todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}") + todos.raise_for_status() + + return todos.json() + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) + + +if __name__ == "__main__": + print(app.get_openapi_json_schema()) diff --git a/examples/event_handler_validation/src/generate_openapi_spec.py b/examples/event_handler_validation/src/generate_openapi_spec.py new file mode 100644 index 00000000000..ce559e00fcf --- /dev/null +++ b/examples/event_handler_validation/src/generate_openapi_spec.py @@ -0,0 +1,32 @@ +from typing import List + +import requests +from pydantic import BaseModel, EmailStr, Field + +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayHttpResolver(enable_validation=True) + + +class Todo(BaseModel): + userId: int + id_: int = Field(alias="id") + title: str + completed: bool + + +@app.get("/todos") +def get_todos_by_email(email: EmailStr) -> List[Todo]: + todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}") + todos.raise_for_status() + + return todos.json() + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) + + +if __name__ == "__main__": + print(app.get_openapi_schema().dict()) diff --git a/examples/event_handler_validation/src/getting_started.json b/examples/event_handler_validation/src/getting_started.json new file mode 100644 index 00000000000..e852a909265 --- /dev/null +++ b/examples/event_handler_validation/src/getting_started.json @@ -0,0 +1,42 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/todos/1", + "rawQueryString": "", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "header1": "value1", + "header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "GET", + "path": "/todos/1", + "protocol": "HTTP/1.1", + "sourceIp": "192.0.2.1", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "pathParameters": {}, + "isBase64Encoded": false, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } +} diff --git a/examples/event_handler_validation/src/getting_started.py b/examples/event_handler_validation/src/getting_started.py new file mode 100644 index 00000000000..43c6970fc64 --- /dev/null +++ b/examples/event_handler_validation/src/getting_started.py @@ -0,0 +1,25 @@ +import requests + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayHttpResolver(enable_validation=True) + + +@app.get("/todos/") +@tracer.capture_method +def get_todo_title(todo_id: int) -> str: + todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") + todo.raise_for_status() + + return todo.json()["title"] + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_validation/src/getting_started_bad_input.json b/examples/event_handler_validation/src/getting_started_bad_input.json new file mode 100644 index 00000000000..6fc2636ad9c --- /dev/null +++ b/examples/event_handler_validation/src/getting_started_bad_input.json @@ -0,0 +1,42 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/todos/apples", + "rawQueryString": "", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "header1": "value1", + "header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "GET", + "path": "/todos/apples", + "protocol": "HTTP/1.1", + "sourceIp": "192.0.2.1", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "pathParameters": {}, + "isBase64Encoded": false, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } +} diff --git a/examples/event_handler_validation/src/getting_started_bad_input_output.json b/examples/event_handler_validation/src/getting_started_bad_input_output.json new file mode 100644 index 00000000000..15552c69329 --- /dev/null +++ b/examples/event_handler_validation/src/getting_started_bad_input_output.json @@ -0,0 +1,9 @@ +{ + "statusCode": 422, + "body": "{\"detail\": [{\"type\": \"int_parsing\", \"loc\": [\"path\", \"todo_id\"], \"msg\": \"Input should be a valid integer, unable to parse string as an integer\", \"input\": \"apples\", \"url\": \"https://errors.pydantic.dev/2.5/v/int_parsing\"}]}", + "isBase64Encoded": false, + "headers": { + "Content-Type": "application/json" + }, + "cookies": [] +} diff --git a/examples/event_handler_validation/src/getting_started_output.json b/examples/event_handler_validation/src/getting_started_output.json new file mode 100644 index 00000000000..b05dd2d460c --- /dev/null +++ b/examples/event_handler_validation/src/getting_started_output.json @@ -0,0 +1,9 @@ +{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "cookies": [], + "body": "hello world", + "isBase64Encoded": false +} diff --git a/examples/event_handler_validation/src/getting_started_pydantic.json b/examples/event_handler_validation/src/getting_started_pydantic.json new file mode 100644 index 00000000000..9320e046756 --- /dev/null +++ b/examples/event_handler_validation/src/getting_started_pydantic.json @@ -0,0 +1,41 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/todos", + "rawQueryString": "?email=foo@bar.com", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "header1": "value1", + "header2": "value1,value2" + }, + "queryStringParameters": { + "email": "foo@bar.com" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "GET", + "path": "/todos", + "protocol": "HTTP/1.1", + "sourceIp": "192.0.2.1", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "pathParameters": {}, + "isBase64Encoded": false, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } +} diff --git a/examples/event_handler_validation/src/getting_started_pydantic.py b/examples/event_handler_validation/src/getting_started_pydantic.py new file mode 100644 index 00000000000..dd7f2f68e14 --- /dev/null +++ b/examples/event_handler_validation/src/getting_started_pydantic.py @@ -0,0 +1,28 @@ +from typing import List + +import requests +from pydantic import BaseModel, EmailStr, Field + +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayHttpResolver(enable_validation=True) + + +class Todo(BaseModel): + userId: int + id_: int = Field(alias="id") + title: str + completed: bool + + +@app.get("/todos") +def get_todos_by_email(email: EmailStr) -> List[Todo]: + todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}") + todos.raise_for_status() + + return todos.json() + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_validation/src/getting_started_pydantic_output.json b/examples/event_handler_validation/src/getting_started_pydantic_output.json new file mode 100644 index 00000000000..4a38dacbea6 --- /dev/null +++ b/examples/event_handler_validation/src/getting_started_pydantic_output.json @@ -0,0 +1,9 @@ +{ + "statusCode": 200, + "body": "[{\"completed\": false, \"id\": 1, \"title\": \"delectus aut autem\", \"userId\": 1}, {\"completed\": false, \"id\": 2, \"title\": \"quis ut nam facilis et officia qui\", \"userId\": 1}, {\"completed\": false, \"id\": 3, \"title\": \"fugiat veniam minus\", \"userId\": 1}, {\"completed\": true, \"id\": 4, \"title\": \"et porro tempora\", \"userId\": 1}, {\"completed\": false, \"id\": 5, \"title\": \"laboriosam mollitia et enim quasi adipisci quia provident illum\", \"userId\": 1}, {\"completed\": false, \"id\": 6, \"title\": \"qui ullam ratione quibusdam voluptatem quia omnis\", \"userId\": 1}, {\"completed\": false, \"id\": 7, \"title\": \"illo expedita consequatur quia in\", \"userId\": 1}, {\"completed\": true, \"id\": 8, \"title\": \"quo adipisci enim quam ut ab\", \"userId\": 1}, {\"completed\": false, \"id\": 9, \"title\": \"molestiae perspiciatis ipsa\", \"userId\": 1}, {\"completed\": true, \"id\": 10, \"title\": \"illo est ratione doloremque quia maiores aut\", \"userId\": 1}, {\"completed\": true, \"id\": 11, \"title\": \"vero rerum temporibus dolor\", \"userId\": 1}, {\"completed\": true, \"id\": 12, \"title\": \"ipsa repellendus fugit nisi\", \"userId\": 1}, {\"completed\": false, \"id\": 13, \"title\": \"et doloremque nulla\", \"userId\": 1}, {\"completed\": true, \"id\": 14, \"title\": \"repellendus sunt dolores architecto voluptatum\", \"userId\": 1}, {\"completed\": true, \"id\": 15, \"title\": \"ab voluptatum amet voluptas\", \"userId\": 1}, {\"completed\": true, \"id\": 16, \"title\": \"accusamus eos facilis sint et aut voluptatem\", \"userId\": 1}, {\"completed\": true, \"id\": 17, \"title\": \"quo laboriosam deleniti aut qui\", \"userId\": 1}, {\"completed\": false, \"id\": 18, \"title\": \"dolorum est consequatur ea mollitia in culpa\", \"userId\": 1}, {\"completed\": true, \"id\": 19, \"title\": \"molestiae ipsa aut voluptatibus pariatur dolor nihil\", \"userId\": 1}, {\"completed\": true, \"id\": 20, \"title\": \"ullam nobis libero sapiente ad optio sint\", \"userId\": 1}, {\"completed\": false, \"id\": 21, \"title\": \"suscipit repellat esse quibusdam voluptatem incidunt\", \"userId\": 2}, {\"completed\": true, \"id\": 22, \"title\": \"distinctio vitae autem nihil ut molestias quo\", \"userId\": 2}, {\"completed\": false, \"id\": 23, \"title\": \"et itaque necessitatibus maxime molestiae qui quas velit\", \"userId\": 2}, {\"completed\": false, \"id\": 24, \"title\": \"adipisci non ad dicta qui amet quaerat doloribus ea\", \"userId\": 2}, {\"completed\": true, \"id\": 25, \"title\": \"voluptas quo tenetur perspiciatis explicabo natus\", \"userId\": 2}, {\"completed\": true, \"id\": 26, \"title\": \"aliquam aut quasi\", \"userId\": 2}, {\"completed\": true, \"id\": 27, \"title\": \"veritatis pariatur delectus\", \"userId\": 2}, {\"completed\": false, \"id\": 28, \"title\": \"nesciunt totam sit blanditiis sit\", \"userId\": 2}, {\"completed\": false, \"id\": 29, \"title\": \"laborum aut in quam\", \"userId\": 2}, {\"completed\": true, \"id\": 30, \"title\": \"nemo perspiciatis repellat ut dolor libero commodi blanditiis omnis\", \"userId\": 2}, {\"completed\": false, \"id\": 31, \"title\": \"repudiandae totam in est sint facere fuga\", \"userId\": 2}, {\"completed\": false, \"id\": 32, \"title\": \"earum doloribus ea doloremque quis\", \"userId\": 2}, {\"completed\": false, \"id\": 33, \"title\": \"sint sit aut vero\", \"userId\": 2}, {\"completed\": false, \"id\": 34, \"title\": \"porro aut necessitatibus eaque distinctio\", \"userId\": 2}, {\"completed\": true, \"id\": 35, \"title\": \"repellendus veritatis molestias dicta incidunt\", \"userId\": 2}, {\"completed\": true, \"id\": 36, \"title\": \"excepturi deleniti adipisci voluptatem et neque optio illum ad\", \"userId\": 2}, {\"completed\": false, \"id\": 37, \"title\": \"sunt cum tempora\", \"userId\": 2}, {\"completed\": false, \"id\": 38, \"title\": \"totam quia non\", \"userId\": 2}, {\"completed\": false, \"id\": 39, \"title\": \"doloremque quibusdam asperiores libero corrupti illum qui omnis\", \"userId\": 2}, {\"completed\": true, \"id\": 40, \"title\": \"totam atque quo nesciunt\", \"userId\": 2}, {\"completed\": false, \"id\": 41, \"title\": \"aliquid amet impedit consequatur aspernatur placeat eaque fugiat suscipit\", \"userId\": 3}, {\"completed\": false, \"id\": 42, \"title\": \"rerum perferendis error quia ut eveniet\", \"userId\": 3}, {\"completed\": true, \"id\": 43, \"title\": \"tempore ut sint quis recusandae\", \"userId\": 3}, {\"completed\": true, \"id\": 44, \"title\": \"cum debitis quis accusamus doloremque ipsa natus sapiente omnis\", \"userId\": 3}, {\"completed\": false, \"id\": 45, \"title\": \"velit soluta adipisci molestias reiciendis harum\", \"userId\": 3}, {\"completed\": false, \"id\": 46, \"title\": \"vel voluptatem repellat nihil placeat corporis\", \"userId\": 3}, {\"completed\": false, \"id\": 47, \"title\": \"nam qui rerum fugiat accusamus\", \"userId\": 3}, {\"completed\": false, \"id\": 48, \"title\": \"sit reprehenderit omnis quia\", \"userId\": 3}, {\"completed\": false, \"id\": 49, \"title\": \"ut necessitatibus aut maiores debitis officia blanditiis velit et\", \"userId\": 3}, {\"completed\": true, \"id\": 50, \"title\": \"cupiditate necessitatibus ullam aut quis dolor voluptate\", \"userId\": 3}, {\"completed\": false, \"id\": 51, \"title\": \"distinctio exercitationem ab doloribus\", \"userId\": 3}, {\"completed\": false, \"id\": 52, \"title\": \"nesciunt dolorum quis recusandae ad pariatur ratione\", \"userId\": 3}, {\"completed\": false, \"id\": 53, \"title\": \"qui labore est occaecati recusandae aliquid quam\", \"userId\": 3}, {\"completed\": true, \"id\": 54, \"title\": \"quis et est ut voluptate quam dolor\", \"userId\": 3}, {\"completed\": true, \"id\": 55, \"title\": \"voluptatum omnis minima qui occaecati provident nulla voluptatem ratione\", \"userId\": 3}, {\"completed\": true, \"id\": 56, \"title\": \"deleniti ea temporibus enim\", \"userId\": 3}, {\"completed\": false, \"id\": 57, \"title\": \"pariatur et magnam ea doloribus similique voluptatem rerum quia\", \"userId\": 3}, {\"completed\": false, \"id\": 58, \"title\": \"est dicta totam qui explicabo doloribus qui dignissimos\", \"userId\": 3}, {\"completed\": false, \"id\": 59, \"title\": \"perspiciatis velit id laborum placeat iusto et aliquam odio\", \"userId\": 3}, {\"completed\": true, \"id\": 60, \"title\": \"et sequi qui architecto ut adipisci\", \"userId\": 3}, {\"completed\": true, \"id\": 61, \"title\": \"odit optio omnis qui sunt\", \"userId\": 4}, {\"completed\": false, \"id\": 62, \"title\": \"et placeat et tempore aspernatur sint numquam\", \"userId\": 4}, {\"completed\": true, \"id\": 63, \"title\": \"doloremque aut dolores quidem fuga qui nulla\", \"userId\": 4}, {\"completed\": false, \"id\": 64, \"title\": \"voluptas consequatur qui ut quia magnam nemo esse\", \"userId\": 4}, {\"completed\": false, \"id\": 65, \"title\": \"fugiat pariatur ratione ut asperiores necessitatibus magni\", \"userId\": 4}, {\"completed\": false, \"id\": 66, \"title\": \"rerum eum molestias autem voluptatum sit optio\", \"userId\": 4}, {\"completed\": false, \"id\": 67, \"title\": \"quia voluptatibus voluptatem quos similique maiores repellat\", \"userId\": 4}, {\"completed\": false, \"id\": 68, \"title\": \"aut id perspiciatis voluptatem iusto\", \"userId\": 4}, {\"completed\": false, \"id\": 69, \"title\": \"doloribus sint dolorum ab adipisci itaque dignissimos aliquam suscipit\", \"userId\": 4}, {\"completed\": false, \"id\": 70, \"title\": \"ut sequi accusantium et mollitia delectus sunt\", \"userId\": 4}, {\"completed\": false, \"id\": 71, \"title\": \"aut velit saepe ullam\", \"userId\": 4}, {\"completed\": false, \"id\": 72, \"title\": \"praesentium facilis facere quis harum voluptatibus voluptatem eum\", \"userId\": 4}, {\"completed\": true, \"id\": 73, \"title\": \"sint amet quia totam corporis qui exercitationem commodi\", \"userId\": 4}, {\"completed\": false, \"id\": 74, \"title\": \"expedita tempore nobis eveniet laborum maiores\", \"userId\": 4}, {\"completed\": false, \"id\": 75, \"title\": \"occaecati adipisci est possimus totam\", \"userId\": 4}, {\"completed\": true, \"id\": 76, \"title\": \"sequi dolorem sed\", \"userId\": 4}, {\"completed\": false, \"id\": 77, \"title\": \"maiores aut nesciunt delectus exercitationem vel assumenda eligendi at\", \"userId\": 4}, {\"completed\": false, \"id\": 78, \"title\": \"reiciendis est magnam amet nemo iste recusandae impedit quaerat\", \"userId\": 4}, {\"completed\": true, \"id\": 79, \"title\": \"eum ipsa maxime ut\", \"userId\": 4}, {\"completed\": true, \"id\": 80, \"title\": \"tempore molestias dolores rerum sequi voluptates ipsum consequatur\", \"userId\": 4}, {\"completed\": true, \"id\": 81, \"title\": \"suscipit qui totam\", \"userId\": 5}, {\"completed\": false, \"id\": 82, \"title\": \"voluptates eum voluptas et dicta\", \"userId\": 5}, {\"completed\": true, \"id\": 83, \"title\": \"quidem at rerum quis ex aut sit quam\", \"userId\": 5}, {\"completed\": false, \"id\": 84, \"title\": \"sunt veritatis ut voluptate\", \"userId\": 5}, {\"completed\": true, \"id\": 85, \"title\": \"et quia ad iste a\", \"userId\": 5}, {\"completed\": true, \"id\": 86, \"title\": \"incidunt ut saepe autem\", \"userId\": 5}, {\"completed\": true, \"id\": 87, \"title\": \"laudantium quae eligendi consequatur quia et vero autem\", \"userId\": 5}, {\"completed\": false, \"id\": 88, \"title\": \"vitae aut excepturi laboriosam sint aliquam et et accusantium\", \"userId\": 5}, {\"completed\": true, \"id\": 89, \"title\": \"sequi ut omnis et\", \"userId\": 5}, {\"completed\": true, \"id\": 90, \"title\": \"molestiae nisi accusantium tenetur dolorem et\", \"userId\": 5}, {\"completed\": true, \"id\": 91, \"title\": \"nulla quis consequatur saepe qui id expedita\", \"userId\": 5}, {\"completed\": true, \"id\": 92, \"title\": \"in omnis laboriosam\", \"userId\": 5}, {\"completed\": true, \"id\": 93, \"title\": \"odio iure consequatur molestiae quibusdam necessitatibus quia sint\", \"userId\": 5}, {\"completed\": false, \"id\": 94, \"title\": \"facilis modi saepe mollitia\", \"userId\": 5}, {\"completed\": true, \"id\": 95, \"title\": \"vel nihil et molestiae iusto assumenda nemo quo ut\", \"userId\": 5}, {\"completed\": false, \"id\": 96, \"title\": \"nobis suscipit ducimus enim asperiores voluptas\", \"userId\": 5}, {\"completed\": false, \"id\": 97, \"title\": \"dolorum laboriosam eos qui iure aliquam\", \"userId\": 5}, {\"completed\": true, \"id\": 98, \"title\": \"debitis accusantium ut quo facilis nihil quis sapiente necessitatibus\", \"userId\": 5}, {\"completed\": false, \"id\": 99, \"title\": \"neque voluptates ratione\", \"userId\": 5}, {\"completed\": false, \"id\": 100, \"title\": \"excepturi a et neque qui expedita vel voluptate\", \"userId\": 5}, {\"completed\": false, \"id\": 101, \"title\": \"explicabo enim cumque porro aperiam occaecati minima\", \"userId\": 6}, {\"completed\": false, \"id\": 102, \"title\": \"sed ab consequatur\", \"userId\": 6}, {\"completed\": false, \"id\": 103, \"title\": \"non sunt delectus illo nulla tenetur enim omnis\", \"userId\": 6}, {\"completed\": false, \"id\": 104, \"title\": \"excepturi non laudantium quo\", \"userId\": 6}, {\"completed\": true, \"id\": 105, \"title\": \"totam quia dolorem et illum repellat voluptas optio\", \"userId\": 6}, {\"completed\": true, \"id\": 106, \"title\": \"ad illo quis voluptatem temporibus\", \"userId\": 6}, {\"completed\": false, \"id\": 107, \"title\": \"praesentium facilis omnis laudantium fugit ad iusto nihil nesciunt\", \"userId\": 6}, {\"completed\": true, \"id\": 108, \"title\": \"a eos eaque nihil et exercitationem incidunt delectus\", \"userId\": 6}, {\"completed\": true, \"id\": 109, \"title\": \"autem temporibus harum quisquam in culpa\", \"userId\": 6}, {\"completed\": true, \"id\": 110, \"title\": \"aut aut ea corporis\", \"userId\": 6}, {\"completed\": false, \"id\": 111, \"title\": \"magni accusantium labore et id quis provident\", \"userId\": 6}, {\"completed\": false, \"id\": 112, \"title\": \"consectetur impedit quisquam qui deserunt non rerum consequuntur eius\", \"userId\": 6}, {\"completed\": false, \"id\": 113, \"title\": \"quia atque aliquam sunt impedit voluptatum rerum assumenda nisi\", \"userId\": 6}, {\"completed\": false, \"id\": 114, \"title\": \"cupiditate quos possimus corporis quisquam exercitationem beatae\", \"userId\": 6}, {\"completed\": false, \"id\": 115, \"title\": \"sed et ea eum\", \"userId\": 6}, {\"completed\": true, \"id\": 116, \"title\": \"ipsa dolores vel facilis ut\", \"userId\": 6}, {\"completed\": false, \"id\": 117, \"title\": \"sequi quae est et qui qui eveniet asperiores\", \"userId\": 6}, {\"completed\": false, \"id\": 118, \"title\": \"quia modi consequatur vero fugiat\", \"userId\": 6}, {\"completed\": false, \"id\": 119, \"title\": \"corporis ducimus ea perspiciatis iste\", \"userId\": 6}, {\"completed\": false, \"id\": 120, \"title\": \"dolorem laboriosam vel voluptas et aliquam quasi\", \"userId\": 6}, {\"completed\": true, \"id\": 121, \"title\": \"inventore aut nihil minima laudantium hic qui omnis\", \"userId\": 7}, {\"completed\": true, \"id\": 122, \"title\": \"provident aut nobis culpa\", \"userId\": 7}, {\"completed\": false, \"id\": 123, \"title\": \"esse et quis iste est earum aut impedit\", \"userId\": 7}, {\"completed\": false, \"id\": 124, \"title\": \"qui consectetur id\", \"userId\": 7}, {\"completed\": false, \"id\": 125, \"title\": \"aut quasi autem iste tempore illum possimus\", \"userId\": 7}, {\"completed\": true, \"id\": 126, \"title\": \"ut asperiores perspiciatis veniam ipsum rerum saepe\", \"userId\": 7}, {\"completed\": true, \"id\": 127, \"title\": \"voluptatem libero consectetur rerum ut\", \"userId\": 7}, {\"completed\": false, \"id\": 128, \"title\": \"eius omnis est qui voluptatem autem\", \"userId\": 7}, {\"completed\": false, \"id\": 129, \"title\": \"rerum culpa quis harum\", \"userId\": 7}, {\"completed\": true, \"id\": 130, \"title\": \"nulla aliquid eveniet harum laborum libero alias ut unde\", \"userId\": 7}, {\"completed\": false, \"id\": 131, \"title\": \"qui ea incidunt quis\", \"userId\": 7}, {\"completed\": true, \"id\": 132, \"title\": \"qui molestiae voluptatibus velit iure harum quisquam\", \"userId\": 7}, {\"completed\": true, \"id\": 133, \"title\": \"et labore eos enim rerum consequatur sunt\", \"userId\": 7}, {\"completed\": false, \"id\": 134, \"title\": \"molestiae doloribus et laborum quod ea\", \"userId\": 7}, {\"completed\": false, \"id\": 135, \"title\": \"facere ipsa nam eum voluptates reiciendis vero qui\", \"userId\": 7}, {\"completed\": false, \"id\": 136, \"title\": \"asperiores illo tempora fuga sed ut quasi adipisci\", \"userId\": 7}, {\"completed\": false, \"id\": 137, \"title\": \"qui sit non\", \"userId\": 7}, {\"completed\": true, \"id\": 138, \"title\": \"placeat minima consequatur rem qui ut\", \"userId\": 7}, {\"completed\": false, \"id\": 139, \"title\": \"consequatur doloribus id possimus voluptas a voluptatem\", \"userId\": 7}, {\"completed\": true, \"id\": 140, \"title\": \"aut consectetur in blanditiis deserunt quia sed laboriosam\", \"userId\": 7}, {\"completed\": true, \"id\": 141, \"title\": \"explicabo consectetur debitis voluptates quas quae culpa rerum non\", \"userId\": 8}, {\"completed\": true, \"id\": 142, \"title\": \"maiores accusantium architecto necessitatibus reiciendis ea aut\", \"userId\": 8}, {\"completed\": false, \"id\": 143, \"title\": \"eum non recusandae cupiditate animi\", \"userId\": 8}, {\"completed\": false, \"id\": 144, \"title\": \"ut eum exercitationem sint\", \"userId\": 8}, {\"completed\": false, \"id\": 145, \"title\": \"beatae qui ullam incidunt voluptatem non nisi aliquam\", \"userId\": 8}, {\"completed\": true, \"id\": 146, \"title\": \"molestiae suscipit ratione nihil odio libero impedit vero totam\", \"userId\": 8}, {\"completed\": true, \"id\": 147, \"title\": \"eum itaque quod reprehenderit et facilis dolor autem ut\", \"userId\": 8}, {\"completed\": false, \"id\": 148, \"title\": \"esse quas et quo quasi exercitationem\", \"userId\": 8}, {\"completed\": false, \"id\": 149, \"title\": \"animi voluptas quod perferendis est\", \"userId\": 8}, {\"completed\": false, \"id\": 150, \"title\": \"eos amet tempore laudantium fugit a\", \"userId\": 8}, {\"completed\": true, \"id\": 151, \"title\": \"accusamus adipisci dicta qui quo ea explicabo sed vero\", \"userId\": 8}, {\"completed\": false, \"id\": 152, \"title\": \"odit eligendi recusandae doloremque cumque non\", \"userId\": 8}, {\"completed\": false, \"id\": 153, \"title\": \"ea aperiam consequatur qui repellat eos\", \"userId\": 8}, {\"completed\": true, \"id\": 154, \"title\": \"rerum non ex sapiente\", \"userId\": 8}, {\"completed\": true, \"id\": 155, \"title\": \"voluptatem nobis consequatur et assumenda magnam\", \"userId\": 8}, {\"completed\": true, \"id\": 156, \"title\": \"nam quia quia nulla repellat assumenda quibusdam sit nobis\", \"userId\": 8}, {\"completed\": true, \"id\": 157, \"title\": \"dolorem veniam quisquam deserunt repellendus\", \"userId\": 8}, {\"completed\": true, \"id\": 158, \"title\": \"debitis vitae delectus et harum accusamus aut deleniti a\", \"userId\": 8}, {\"completed\": true, \"id\": 159, \"title\": \"debitis adipisci quibusdam aliquam sed dolore ea praesentium nobis\", \"userId\": 8}, {\"completed\": false, \"id\": 160, \"title\": \"et praesentium aliquam est\", \"userId\": 8}, {\"completed\": true, \"id\": 161, \"title\": \"ex hic consequuntur earum omnis alias ut occaecati culpa\", \"userId\": 9}, {\"completed\": true, \"id\": 162, \"title\": \"omnis laboriosam molestias animi sunt dolore\", \"userId\": 9}, {\"completed\": false, \"id\": 163, \"title\": \"natus corrupti maxime laudantium et voluptatem laboriosam odit\", \"userId\": 9}, {\"completed\": false, \"id\": 164, \"title\": \"reprehenderit quos aut aut consequatur est sed\", \"userId\": 9}, {\"completed\": false, \"id\": 165, \"title\": \"fugiat perferendis sed aut quidem\", \"userId\": 9}, {\"completed\": false, \"id\": 166, \"title\": \"quos quo possimus suscipit minima ut\", \"userId\": 9}, {\"completed\": false, \"id\": 167, \"title\": \"et quis minus quo a asperiores molestiae\", \"userId\": 9}, {\"completed\": false, \"id\": 168, \"title\": \"recusandae quia qui sunt libero\", \"userId\": 9}, {\"completed\": true, \"id\": 169, \"title\": \"ea odio perferendis officiis\", \"userId\": 9}, {\"completed\": false, \"id\": 170, \"title\": \"quisquam aliquam quia doloribus aut\", \"userId\": 9}, {\"completed\": true, \"id\": 171, \"title\": \"fugiat aut voluptatibus corrupti deleniti velit iste odio\", \"userId\": 9}, {\"completed\": false, \"id\": 172, \"title\": \"et provident amet rerum consectetur et voluptatum\", \"userId\": 9}, {\"completed\": false, \"id\": 173, \"title\": \"harum ad aperiam quis\", \"userId\": 9}, {\"completed\": false, \"id\": 174, \"title\": \"similique aut quo\", \"userId\": 9}, {\"completed\": true, \"id\": 175, \"title\": \"laudantium eius officia perferendis provident perspiciatis asperiores\", \"userId\": 9}, {\"completed\": false, \"id\": 176, \"title\": \"magni soluta corrupti ut maiores rem quidem\", \"userId\": 9}, {\"completed\": false, \"id\": 177, \"title\": \"et placeat temporibus voluptas est tempora quos quibusdam\", \"userId\": 9}, {\"completed\": true, \"id\": 178, \"title\": \"nesciunt itaque commodi tempore\", \"userId\": 9}, {\"completed\": true, \"id\": 179, \"title\": \"omnis consequuntur cupiditate impedit itaque ipsam quo\", \"userId\": 9}, {\"completed\": true, \"id\": 180, \"title\": \"debitis nisi et dolorem repellat et\", \"userId\": 9}, {\"completed\": false, \"id\": 181, \"title\": \"ut cupiditate sequi aliquam fuga maiores\", \"userId\": 10}, {\"completed\": true, \"id\": 182, \"title\": \"inventore saepe cumque et aut illum enim\", \"userId\": 10}, {\"completed\": true, \"id\": 183, \"title\": \"omnis nulla eum aliquam distinctio\", \"userId\": 10}, {\"completed\": false, \"id\": 184, \"title\": \"molestias modi perferendis perspiciatis\", \"userId\": 10}, {\"completed\": false, \"id\": 185, \"title\": \"voluptates dignissimos sed doloribus animi quaerat aut\", \"userId\": 10}, {\"completed\": false, \"id\": 186, \"title\": \"explicabo odio est et\", \"userId\": 10}, {\"completed\": false, \"id\": 187, \"title\": \"consequuntur animi possimus\", \"userId\": 10}, {\"completed\": true, \"id\": 188, \"title\": \"vel non beatae est\", \"userId\": 10}, {\"completed\": true, \"id\": 189, \"title\": \"culpa eius et voluptatem et\", \"userId\": 10}, {\"completed\": true, \"id\": 190, \"title\": \"accusamus sint iusto et voluptatem exercitationem\", \"userId\": 10}, {\"completed\": true, \"id\": 191, \"title\": \"temporibus atque distinctio omnis eius impedit tempore molestias pariatur\", \"userId\": 10}, {\"completed\": false, \"id\": 192, \"title\": \"ut quas possimus exercitationem sint voluptates\", \"userId\": 10}, {\"completed\": true, \"id\": 193, \"title\": \"rerum debitis voluptatem qui eveniet tempora distinctio a\", \"userId\": 10}, {\"completed\": false, \"id\": 194, \"title\": \"sed ut vero sit molestiae\", \"userId\": 10}, {\"completed\": true, \"id\": 195, \"title\": \"rerum ex veniam mollitia voluptatibus pariatur\", \"userId\": 10}, {\"completed\": true, \"id\": 196, \"title\": \"consequuntur aut ut fugit similique\", \"userId\": 10}, {\"completed\": true, \"id\": 197, \"title\": \"dignissimos quo nobis earum saepe\", \"userId\": 10}, {\"completed\": true, \"id\": 198, \"title\": \"quis eius est sint explicabo\", \"userId\": 10}, {\"completed\": true, \"id\": 199, \"title\": \"numquam repellendus a magnam\", \"userId\": 10}, {\"completed\": false, \"id\": 200, \"title\": \"ipsam aperiam voluptates qui\", \"userId\": 10}]", + "isBase64Encoded": false, + "headers": { + "Content-Type": "application/json" + }, + "cookies": [] +} diff --git a/examples/event_handler_validation/src/requirements.txt b/examples/event_handler_validation/src/requirements.txt new file mode 100644 index 00000000000..867051207bb --- /dev/null +++ b/examples/event_handler_validation/src/requirements.txt @@ -0,0 +1,3 @@ +requests +aws_lambda_powertools[tracer] +pydantic[email] diff --git a/examples/event_handler_validation/src/swagger.py b/examples/event_handler_validation/src/swagger.py new file mode 100644 index 00000000000..556df9cb366 --- /dev/null +++ b/examples/event_handler_validation/src/swagger.py @@ -0,0 +1,29 @@ +from typing import List + +import requests +from pydantic import BaseModel, EmailStr, Field + +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayHttpResolver(enable_validation=True) +app.enable_swagger() + + +class Todo(BaseModel): + userId: int + id_: int = Field(alias="id") + title: str + completed: bool + + +@app.get("/todos") +def get_todos_by_email(email: EmailStr) -> List[Todo]: + todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}") + todos.raise_for_status() + + return todos.json() + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_validation/src/swagger_customize.py b/examples/event_handler_validation/src/swagger_customize.py new file mode 100644 index 00000000000..2c8dca6815a --- /dev/null +++ b/examples/event_handler_validation/src/swagger_customize.py @@ -0,0 +1,29 @@ +from typing import List + +import requests +from pydantic import BaseModel, EmailStr, Field + +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayHttpResolver(enable_validation=True) +app.enable_swagger(path="/_swagger", swagger_base_url="https://cdn.example.com/path/to/assets/") + + +class Todo(BaseModel): + userId: int + id_: int = Field(alias="id") + title: str + completed: bool + + +@app.get("/todos") +def get_todos_by_email(email: EmailStr) -> List[Todo]: + todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}") + todos.raise_for_status() + + return todos.json() + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_validation/src/swagger_middlewares.py b/examples/event_handler_validation/src/swagger_middlewares.py new file mode 100644 index 00000000000..1c09ebf342e --- /dev/null +++ b/examples/event_handler_validation/src/swagger_middlewares.py @@ -0,0 +1,40 @@ +from typing import List + +import requests +from pydantic import BaseModel, EmailStr, Field + +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver, Response +from aws_lambda_powertools.event_handler.middlewares import NextMiddleware +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayHttpResolver(enable_validation=True) + + +def swagger_middleware(app: APIGatewayHttpResolver, next_middleware: NextMiddleware) -> Response: + is_authenticated = ... + if not is_authenticated: + return Response(status_code=400, body="Unauthorized") + + return next_middleware(app) + + +app.enable_swagger(middlewares=[swagger_middleware]) + + +class Todo(BaseModel): + userId: int + id_: int = Field(alias="id") + title: str + completed: bool + + +@app.get("/todos") +def get_todos_by_email(email: EmailStr) -> List[Todo]: + todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}") + todos.raise_for_status() + + return todos.json() + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/mkdocs.yml b/mkdocs.yml index 0a844fd392f..e9c0b05b39a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,7 +20,9 @@ nav: - Amazon CloudWatch EMF: core/metrics.md - Datadog: core/metrics/datadog.md - Event Handler: - - core/event_handler/api_gateway.md + - REST API: + - core/event_handler/api_gateway/index.md + - core/event_handler/openapi.md - core/event_handler/appsync.md - utilities/parameters.md - utilities/batch.md From 0e5c7affa730358001703d6157146e7ddb61b026 Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Wed, 22 Nov 2023 09:45:44 +0100 Subject: [PATCH 02/36] chore: refactor --- docs/core/event_handler/openapi.md | 87 +++++++++++++----------------- 1 file changed, 38 insertions(+), 49 deletions(-) diff --git a/docs/core/event_handler/openapi.md b/docs/core/event_handler/openapi.md index 1a328064987..b29cef07465 100644 --- a/docs/core/event_handler/openapi.md +++ b/docs/core/event_handler/openapi.md @@ -16,17 +16,12 @@ Comprehensive data validation and OpenAPI generation based on introspection of P To use validation and OpenAPI features with our core utility, you must first ensure that pydantic is installed as it is a prerequisite. This utility framework supports both pydantic version 1 and version 2. For detailed guidance on setting up the parser, visit the [Parser documentation](./../../../../utilities/parser/#getting-started). -This documentation specifically focuses on the utility's validation and OpenAPI capabilities. These features are built on top of the Event Handler, thereby streamlining the process of validating inputs and outputs and automatically generating OpenAPI specifications based on your API definitions. +This documentation specifically focuses on the utility's validation and OpenAPI capabilities. These features are built on top of the Event Handler utility, thereby streamlining the process of validating inputs and outputs and automatically generating OpenAPI specifications based on your API definitions. ### Basic usage -To enable the validation logic, you need to pass the `enable_validation` parameter to your REST API resolver. This changes the way your resolver gets called. We will inspect -your handler do termine the input and output parameters, and will validate / coerce the data before calling your handler. - -To enable the validation mechanism within your REST API, you'll need to use the `enable_validation` parameter when defining your API resolver. -This modifies the invocation process of your resolver function. Powertools will analyze your handler to identify the input and output parameters. -Once these parameters are determined, we ensure that the data is validated and coerced accordingly before it ever reaches your handler. -This process is designed to enforce a layer of integrity, so that your functions operate on clean and verified inputs, leading to more reliable and maintainable code. +Enable the REST API's validation by setting the `enable_validation` parameter in your API resolver. This changes how your resolver is called. +Powertools examines your handler to pinpoint input and output parameters, then validates and coerces the data before it reaches your handler. === "getting_started.py" @@ -46,9 +41,9 @@ This process is designed to enforce a layer of integrity, so that your functions --8<-- "examples/event_handler_validation/src/getting_started_output.json" ``` -If the validation process encounters data that does not conform to the specified input schema, the system triggers a validation error. This results in an HTTP 442 Unprocessable Entity error, which indicates that the input was understood by the server but contained invalid fields. +When data fails to match the input schema during validation, a validation error occurs, leading to an HTTP 422 Unprocessable Entity error, signaling that the server understood the input but found invalid fields. -Here's an example of what the error response might look like when the validation fails due to bad input: +Below is a sample error response for failed validation due to incorrect input: === "bad_input_event.json" @@ -63,13 +58,13 @@ Here's an example of what the error response might look like when the validation ``` ???+ note "Pydantic v1 vs v2" - Pydantic version 1 and version 2 might describe these validation errors differently. Hence, you should consult the relevant version's documentation to understand the exact format and style of the error messages for the version you are using. + Pydantic versions 1 and 2 may report validation errors differently. Refer to the documentation for your specific version to grasp the precise format and style of the error messages. ### Using Pydantic models -Pydantic models provide a powerful syntax for declaring complex data structures along with the rules to validate the incoming data. These models can be used directly as input parameters or return types, letting you take full advantage of Pydantic's breadth of features, including data coercion, default values, and advanced validation. +Pydantic models allow you to define complex data structures and validation rules. Use these models as input parameters or return types to leverage Pydantic's features like data coercion, default values, and advanced validation. -Let's take a look at how you can utilize Pydantic models: +Here's how to use Pydantic models: === "getting_started_pydantic.py" @@ -91,14 +86,11 @@ Let's take a look at how you can utilize Pydantic models: ### SwaggerUI -Swagger UI provides a web-based interface for visualizing and interacting with your API's resources. By enabling Swagger UI for your API, you create an interactive documentation page that can be used for testing and exploring your API endpoints in real-time. - -WARNING: this will create a publicly accessible Swagger UI page. See Advanced for how to customize and protect your -Swagger UI +Swagger UI offers a web interface for visualizing and interacting with your API's resources. Enable Swagger UI to generate an interactive documentation page for testing and exploring your API endpoints in real time. ???+ warning "Publicly accessible by default" - The Swagger UI page will be publicly accessible when enabled. If your API contains sensitive endpoints or you wish to restrict access to the documentation, it's crucial to consider adding authentication mechanisms or other protections. - See the [Customize the Swagger UI](#customizing-the-swagger-ui) section of this documentation to learn details on customizing and securing your Swagger UI, ensuring it suits your specific requirements while providing the necessary protection for your API's interactive documentation. + Enabling Swagger UI makes it public. To protect sensitive API endpoints or restrict documentation access, consider implementing authentication or other security measures. + See the [Customize the Swagger UI](#customizing-the-swagger-ui) section for instructions on customizing and securing your Swagger UI to meet your needs and safeguard your interactive API documentation. ```python hl_lines="9 10" --8<-- "examples/event_handler_validation/src/swagger.py" @@ -112,11 +104,11 @@ Here's an example of what it looks like by default: ### Customizing parameters -Annotations are a useful way to enrich your API's parameters with metadata and validation constraints, thereby enhancing the functionality and documentation of your API. Python's [Annotated type, introduced in PEP 593](https://peps.python.org/pep-0593/), allows you to attach additional metadata to type hints, which can then be used by your validation library or documentation tools. +Use annotations to add metadata and validation constraints to your API's parameters, improving functionality and documentation. Python's [Annotated type from PEP 593](https://peps.python.org/pep-0593/) lets you append metadata to type hints for use by validation libraries or documentation tools. -If you are working with parameters that are part of the URL path, query strings, or request bodies, certain specialized classes or decorators are often available to assist with defining these parameters more explicitly. This can include specifying default values, validation rules, and descriptions for better OpenAPI generation. +For URL path, query string, or request body parameters, use specialized classes or decorators to define parameters with defaults, validation rules, and descriptions for enhanced OpenAPI output. -Here is an example demonstrating how you might customize your API parameters using annotations: +Below is an example of customizing API parameters with annotations: ```python hl_lines="1 7 19 20" --8<-- "examples/event_handler_validation/src/customizing_parameters.py" @@ -125,11 +117,9 @@ Here is an example demonstrating how you might customize your API parameters usi ???+ note Powertools doesn't have support for files, form data, and header parameters at the moment. If you're interested in this, please [open an issue](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=feature-request%2Ctriage&projects=&template=feature_request.yml&title=Feature+request%3A+TITLE). -Adding titles and descriptions to your parameters is beneficial because it clarifies the intended use and constraints of the API for end-users and developers alike. -When the API is rendered in OpenAPI documentation tools, these annotations will be converted into readable descriptions, providing a self-explanatory interface for interacting with your API. -This can significantly improve the developer experience and reduce the learning curve for new users of your API. +Titles and descriptions clarify parameter use and constraints for both end-users and developers. In OpenAPI documentation tools, these annotations become readable descriptions, offering a self-explanatory API interface. This enhances the developer experience and eases the learning curve for new API users. -Here's a table of all possible customizations you can do: +Below is a table detailing all possible parameter customizations: | Field name | Type | Description | |-----------------------|---------------|---------------------------------------------------------------------------------------------------------------------------------------------| @@ -156,10 +146,9 @@ Here's a table of all possible customizations you can do: ### Body parameters -Handling JSON objects in the body of your API requests is simple with Pydantic models. We automate the parsing of the request bodies using the models you define, -ensuring that the data structures rescived are aligned with your API's expectations. +With Pydantic models, managing JSON objects in API request bodies is straightforward. The models you define automatically parse request bodies, confirming that received data structures match your API's specifications. -Here's how to define and parse body parameters using a Pydantic model: +To define and parse body parameters with a Pydantic model, follow this example: === "body_parsing.py" @@ -179,7 +168,7 @@ Here's how to define and parse body parameters using a Pydantic model: --8<-- "examples/event_handler_validation/src/body_parsing_output.json" ``` -When using the Body wrapper with embed, your JSON payload will need to be provided as a nested object under a key that matches the name of the parameter: +When you use the Body wrapper with the `embed` option, nest your JSON payload under a key that corresponds to the parameter name. === "body_parsing_embed.py" @@ -201,8 +190,9 @@ When using the Body wrapper with embed, your JSON payload will need to be provid ### Customizing API operations -Customizing your API endpoints involves adding specific metadata to your endpoint definitions, allowing you to provde descriptive documentation for API consumers and offer additional instructions to the underlying framework. -Below is a detailed explanation of various fields that you can customize: +Customize your API endpoints by adding metadata to endpoint definitions. This provides descriptive documentation for API consumers and gives extra instructions to the framework. + +Here's a breakdown of various customizable fields: | Field Name | Type | Description | |------------------------|-----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -214,7 +204,7 @@ Below is a detailed explanation of various fields that you can customize: | `operation_id` | `str` | A unique identifier for the operation, which can be used for referencing this operation in documentation or code. This ID must be unique across all operations described in the API. | | `include_in_schema` | `bool` | A boolean value that determines whether or not this operation should be included in the OpenAPI schema. Setting it to `False` can hide the endpoint from generated documentation and schema exports, which might be useful for private or experimental endpoints. | -To apply these customizations, you add additional parameters when declaring your routes: +To implement these customizations, include extra parameters when defining your routes: === "Customizing API operations metadata" @@ -224,9 +214,9 @@ To apply these customizations, you add additional parameters when declaring your ### Generating OpenAPI specifications -OpenAPI specifications are integral to understanding and interacting with modern web APIs. They describe the entire API, including routes, parameters, responses, and more. This specification can be machine-generated from your codebase, ensuring it remains up-to-date with your API's implementation. +OpenAPI specifications detail web APIs, covering routes, parameters, responses, etc. They can be auto-generated from your code, keeping them synchronized with your API's actual implementation. -With Powertools, these specifications can be outputted as a Pydantic object or as a raw JSON schema string: +Powertools allows exporting these specifications as a Pydantic object or a JSON schema string: === "OpenAPI specification as a Pydantic object" @@ -247,17 +237,17 @@ With Powertools, these specifications can be outputted as a Pydantic object or a ``` ???+ note "Why opt for the Pydantic object?" - Having the OpenAPI specification as a Pydantic object provides several advantages: + The OpenAPI specification as a Pydantic object offers benefits: - 1. **Post-Processing:** You may wish to programmatically alter or enrich the OpenAPI specification after it's generated but before you serve it or pass it on. For instance, you could add examples, merge multiple specifications, or adjust descriptions dynamically. - 2. **Internal Use:** Maybe your goal is not to expose the specification externally but to utilize it within your system for validation, mocking, or other quality assurance techniques. - 3. **Dynamic Behavior:** If you need to control the representation of the schema based on conditions not expressible statically in the code (e.g., user permissions, environment variables), a Pydantic object could be manipulated prior to serialization. - 4. **Fragment Reuse:** If your setup involves microservices or a plugin architecture, you might need to generate partial schemas and combine them into a larger API gateway schema. - 5. **Testing and Automation:** For testing purposes, it’s often useful to have the schema in a manipulatable form to validate that certain changes are present or to automate API tests. + 1. **Post-Processing:** Alter or enrich the specification programmatically after generation, such as adding examples, merging specs, or updating descriptions. + 2. **Internal Use:** Use the specification within your system for validation, mocking, or other quality assurance methods, rather than exposing it externally. + 3. **Dynamic Behavior:** Manipulate the schema representation before serialization to reflect conditions like user permissions or environment variables. + 4. **Fragment Reuse:** In microservices or plugin architectures, generate partial schemas to assemble into a comprehensive API gateway schema. + 5. **Testing and Automation:** For testing, a manipulatable schema form is useful to confirm changes or automate API tests. #### Customizing OpenAPI metadata -Customizing the OpenAPI metadata allows you to provide detailed, top-level information about your API. Here's how you can define and customize this metadata: +Defining and customizing OpenAPI metadata gives detailed, top-level information about your API. Here's the method to set and tailor this metadata: | Field Name | Type | Description | |--------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -272,7 +262,7 @@ Customizing the OpenAPI metadata allows you to provide detailed, top-level infor | `contact` | `Contact` | A Contact object containing contact details of the organization or individuals maintaining the API. This may include fields such as name, URL, and email. | | `license_info` | `License` | A License object providing the license details for the API, typically including the name of the license and the URL to the full license text. | -To apply these customizations, you add additional parameters when exporting your OpenAPI specification: +Include extra parameters when exporting your OpenAPI specification to apply these customizations: === "Customizing OpenAPI metadata" @@ -282,9 +272,9 @@ To apply these customizations, you add additional parameters when exporting your ### Customizing the Swagger UI -By default, the Swagger UI may be served under the `/swagger` path, but customization options are often provided to allow you to serve the documentation from a different path, as well as to define where the necessary Swagger UI assets are loaded from. +The Swagger UI appears by default at the `/swagger` path, but you can customize this to serve the documentation from another path and specify the source for Swagger UI assets. -Here is an example of how you could configure the loading of the Swagger UI from a custom path or CDN. Additionally, the Swagger UI assets such as the CSS and JavaScript bundles are directed to load from a specified CDN base URL. +Below is an example configuration for serving Swagger UI from a custom path or CDN, with assets like CSS and JavaScript loading from a chosen CDN base URL. === "Customizing Swagger path and CDN" @@ -297,8 +287,7 @@ Here is an example of how you could configure the loading of the Swagger UI from === "Using middlewares with the Swagger UI" -To complement these customizations, it's possible to introduce middleware on the Swagger UI endpoiunt. -Middleware can be used for tasks like adding security headers, user authentication, or other processing that needs to occur on requests serving the Swagger UI. +You can enhance these customizations by adding middleware to the Swagger UI endpoint. Middleware can handle tasks such as adding security headers, user authentication, or other request processing for serving the Swagger UI. ```python hl_lines="7 13-18 21" --8<-- "examples/event_handler_validation/src/swagger_middlewares.py" @@ -306,5 +295,5 @@ Middleware can be used for tasks like adding security headers, user authenticati ## Testing your code -For comprehensive guidance on how to test your code effectively, please refer to the documentation specific to the [REST API documentation](../api_gateway/#testing-your-code). -The referenced documentation will provide you with best practices, testing techniques, and examples on how to write tests for your API code. +For detailed instructions on testing your code, consult the [REST API documentation](../api_gateway/#testing-your-code). +This guide offers best practices, testing methods, and examples for writing API tests. From 96a27ebbc5622d17f3e49d30387736d1170c736e Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Wed, 22 Nov 2023 09:46:43 +0100 Subject: [PATCH 03/36] chore: remove bad file --- docs/core/event_handler/bedrock_agents.md | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 docs/core/event_handler/bedrock_agents.md diff --git a/docs/core/event_handler/bedrock_agents.md b/docs/core/event_handler/bedrock_agents.md deleted file mode 100644 index 041487a3313..00000000000 --- a/docs/core/event_handler/bedrock_agents.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Bedrock Agent API -description: Core utility ---- - -## Key Features - -## Getting started - -### Required resources - -## Advanced - -## Testing your code From b5f438c705d49e7e082d47782972d357b4bdfc3a Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 22 Nov 2023 12:29:10 +0100 Subject: [PATCH 04/36] docs(apigateway): add install section for new data validation feat Signed-off-by: heitorlessa --- docs/core/event_handler/api_gateway.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index ae651acd26f..663f701256f 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -17,6 +17,14 @@ Event handler for Amazon API Gateway REST and HTTP APIs, Application Loader Bala ???+ tip All examples shared in this documentation are available within the [project repository](https://github.com/aws-powertools/powertools-lambda-python/tree/develop/examples){target="_blank"}. +### Install + +!!! info "This is not necessary if you're installing Powertools for AWS Lambda (Python) via [Lambda Layer/SAR](../index.md#lambda-layer){target="_blank"}." + +**When using the data validation feature**, you need to add `pydantic` as a dependency in your preferred tool _e.g., requirements.txt, pyproject.toml_. + +As of now, both Pydantic V1 and V2 are supported. For a future major version, we will only support Pydantic V2. + ### Required resources @@ -573,8 +581,8 @@ As a practical example, let's refactor our correlation ID middleware so it accep These are native middlewares that may become native features depending on customer demand. -| Middleware | Purpose | -| ---------------------------------------------------------------------------------------------------- |--------------------------------------------------------------------------------------------------------------------------------------------| +| Middleware | Purpose | +| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | [SchemaValidationMiddleware](/lambda/python/latest/api/event_handler/middlewares/schema_validation.html){target="_blank"} | Validates API request body and response against JSON Schema, using [Validation utility](../../../utilities/validation.md){target="_blank"} | #### Being a good citizen From d78bfc3f4089a3fd6b060b23be338ccaa79a992f Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 22 Nov 2023 13:15:29 +0100 Subject: [PATCH 05/36] docs(apigateway): add new data validation section Signed-off-by: heitorlessa --- docs/core/event_handler/api_gateway.md | 35 ++++++++++++++++ .../src/data_validation.json | 42 +++++++++++++++++++ .../event_handler_rest/src/data_validation.py | 26 ++++++++++++ .../src/data_validation_output.json | 9 ++++ 4 files changed, 112 insertions(+) create mode 100644 examples/event_handler_rest/src/data_validation.json create mode 100644 examples/event_handler_rest/src/data_validation.py create mode 100644 examples/event_handler_rest/src/data_validation_output.json diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 663f701256f..fa009e9697e 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -229,6 +229,41 @@ If you need to accept multiple HTTP methods in a single function, you can use th ???+ note It is generally better to have separate functions for each HTTP method, as the functionality tends to differ depending on which method is used. +### Data validation + +!!! note "This changes the authoring experience by relying on Python's type annotations" + It's inspired by [FastAPI framework](https://fastapi.tiangolo.com/){target="_blank" rel="nofollow"} to ease migrations in either direction. + +All resolvers can optionally coerce and validate incoming requests by setting `enable_validation=True`. + +With this feature, we can now express how we expect our incoming data and response to look like. This moves data validation responsibilities to Event Handler resolvers, reducing a ton of boilerplate code. + +Any incoming data that does not match what you expect will result in a `HTTP 422: Unprocessable Entity error` response. + +Let's rewrite the previous examples to signal our resolver what shape we expect our data to be. + +=== "data_validation.py" + + ```python hl_lines="10 13 15 20" + --8<-- "examples/event_handler_rest/src/data_validation.py" + ``` + + 1. This enforces data validation at runtime. Any validation error will return `HTTP 422: Unprocessable Entity error`. + 2. Defining a route remains exactly as before. + 3. By default, URL Paths will be `str`. Here we are telling our resolver it should be `int`, so it converts it for us.

We're also saying the return should be `str` instead of a dictionary in the previous example. + +=== "data_validation.json" + + ```json hl_lines="4" + --8<-- "examples/event_handler_rest/src/data_validation.json" + ``` + +=== "data_validation_output.json" + + ```json hl_lines="2 8" + --8<-- "examples/event_handler_rest/src/data_validation_output.json" + ``` + ### 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/examples/event_handler_rest/src/data_validation.json b/examples/event_handler_rest/src/data_validation.json new file mode 100644 index 00000000000..e852a909265 --- /dev/null +++ b/examples/event_handler_rest/src/data_validation.json @@ -0,0 +1,42 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/todos/1", + "rawQueryString": "", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "header1": "value1", + "header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "GET", + "path": "/todos/1", + "protocol": "HTTP/1.1", + "sourceIp": "192.0.2.1", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "pathParameters": {}, + "isBase64Encoded": false, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } +} diff --git a/examples/event_handler_rest/src/data_validation.py b/examples/event_handler_rest/src/data_validation.py new file mode 100644 index 00000000000..e04b46ef659 --- /dev/null +++ b/examples/event_handler_rest/src/data_validation.py @@ -0,0 +1,26 @@ +import requests + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayHttpResolver(enable_validation=True) # (1)! + + +@app.get("/todos/") # (2)! +@tracer.capture_method +def get_todo_title(todo_id: int) -> str: # (3)! + todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") + todo.raise_for_status() + + # We return the task title (str), thus matching our return type + return todo.json()["title"] + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/data_validation_output.json b/examples/event_handler_rest/src/data_validation_output.json new file mode 100644 index 00000000000..b05dd2d460c --- /dev/null +++ b/examples/event_handler_rest/src/data_validation_output.json @@ -0,0 +1,9 @@ +{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "cookies": [], + "body": "hello world", + "isBase64Encoded": false +} From ffb6c0edcad556dd4c80bb85aed4739c02b3f377 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 22 Nov 2023 13:18:11 +0100 Subject: [PATCH 06/36] docs(apigateway): improve wording to emphasize validation error Signed-off-by: heitorlessa --- 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 fa009e9697e..e1585c348c3 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -238,7 +238,7 @@ All resolvers can optionally coerce and validate incoming requests by setting `e With this feature, we can now express how we expect our incoming data and response to look like. This moves data validation responsibilities to Event Handler resolvers, reducing a ton of boilerplate code. -Any incoming data that does not match what you expect will result in a `HTTP 422: Unprocessable Entity error` response. +Any **incoming request that fails validation** will result in a `HTTP 422: Unprocessable Entity error` response. Let's rewrite the previous examples to signal our resolver what shape we expect our data to be. From a7f9662d64131fae8aa8e2da7e9e73266971e22b Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 22 Nov 2023 14:03:51 +0100 Subject: [PATCH 07/36] docs: improve data validation example using a DTO --- docs/core/event_handler/api_gateway.md | 12 +++++++++--- .../event_handler_rest/src/data_validation.py | 17 +++++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index e1585c348c3..e7d83c2a3d8 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -242,15 +242,19 @@ Any **incoming request that fails validation** will result in a `HTTP 422: Unpro Let's rewrite the previous examples to signal our resolver what shape we expect our data to be. + + === "data_validation.py" - ```python hl_lines="10 13 15 20" + ```python hl_lines="13 16 25 29" --8<-- "examples/event_handler_rest/src/data_validation.py" ``` 1. This enforces data validation at runtime. Any validation error will return `HTTP 422: Unprocessable Entity error`. - 2. Defining a route remains exactly as before. - 3. By default, URL Paths will be `str`. Here we are telling our resolver it should be `int`, so it converts it for us.

We're also saying the return should be `str` instead of a dictionary in the previous example. + 2. We create a Pydantic model to define how our data looks like. + 3. Defining a route remains exactly as before. + 4. By default, URL Paths will be `str`. Here, we are telling our resolver it should be `int`, so it converts it for us.

Lastly, we're also saying the return should be our `Todo`. This will help us later when we touch OpenAPI auto-documentation. + 5. We convert our newly fetched todo data into our `Todo` model to ensure it's valid.

Our resolver takes care of converting our validated model into a valid end-user response.

**NOTE**. You're not limited to a Pydantic model, you could also return `dict` like before. === "data_validation.json" @@ -264,6 +268,8 @@ Let's rewrite the previous examples to signal our resolver what shape we expect --8<-- "examples/event_handler_rest/src/data_validation_output.json" ``` + + ### 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/examples/event_handler_rest/src/data_validation.py b/examples/event_handler_rest/src/data_validation.py index e04b46ef659..70bb0adeeff 100644 --- a/examples/event_handler_rest/src/data_validation.py +++ b/examples/event_handler_rest/src/data_validation.py @@ -1,4 +1,7 @@ +from typing import Optional + import requests +from pydantic import BaseModel, Field from aws_lambda_powertools import Logger, Tracer from aws_lambda_powertools.event_handler import APIGatewayHttpResolver @@ -10,14 +13,20 @@ app = APIGatewayHttpResolver(enable_validation=True) # (1)! -@app.get("/todos/") # (2)! +class Todo(BaseModel): # (2)! + userId: int + id_: Optional[int] = Field(alias="id", default=None) + title: str + completed: bool + + +@app.get("/todos/") # (3)! @tracer.capture_method -def get_todo_title(todo_id: int) -> str: # (3)! +def get_todo_by_id(todo_id: int) -> Todo: # (4)! todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") todo.raise_for_status() - # We return the task title (str), thus matching our return type - return todo.json()["title"] + return Todo(**todo.json()) # (5)! @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) From 26cf7c00b0b6f0b1534c3d22eb8b2a836bee9c66 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 22 Nov 2023 14:27:16 +0100 Subject: [PATCH 08/36] docs: add handling validation errors section Signed-off-by: heitorlessa --- docs/core/event_handler/api_gateway.md | 23 +++++++++- .../src/data_validation_error.json | 42 +++++++++++++++++++ .../src/data_validation_error_output.json | 9 ++++ 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 examples/event_handler_rest/src/data_validation_error.json create mode 100644 examples/event_handler_rest/src/data_validation_error_output.json diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index e7d83c2a3d8..a7e0a2278e9 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -238,8 +238,6 @@ All resolvers can optionally coerce and validate incoming requests by setting `e With this feature, we can now express how we expect our incoming data and response to look like. This moves data validation responsibilities to Event Handler resolvers, reducing a ton of boilerplate code. -Any **incoming request that fails validation** will result in a `HTTP 422: Unprocessable Entity error` response. - Let's rewrite the previous examples to signal our resolver what shape we expect our data to be. @@ -270,6 +268,27 @@ Let's rewrite the previous examples to signal our resolver what shape we expect +#### Handling validation errors + +Any **incoming request that fails validation** will result in a `HTTP 422: Unprocessable Entity error` response. + +Below is a sample error response for failed validation due to incorrect input: + +=== "data_validation_error.json" + + ```json hl_lines="4" + --8<-- "examples/event_handler_rest/src/data_validation_error.json" + ``` + +=== "data_validation_error_output.json" + + ```json hl_lines="2 3" + --8<-- "examples/event_handler_rest/src/data_validation_error_output.json" + ``` + +???+ note "Pydantic v1 vs v2" + Pydantic versions 1 and 2 may report validation errors differently. Refer to the documentation for your specific version to grasp the precise format and style of the error messages. + ### 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/examples/event_handler_rest/src/data_validation_error.json b/examples/event_handler_rest/src/data_validation_error.json new file mode 100644 index 00000000000..6fc2636ad9c --- /dev/null +++ b/examples/event_handler_rest/src/data_validation_error.json @@ -0,0 +1,42 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/todos/apples", + "rawQueryString": "", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "header1": "value1", + "header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "GET", + "path": "/todos/apples", + "protocol": "HTTP/1.1", + "sourceIp": "192.0.2.1", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "pathParameters": {}, + "isBase64Encoded": false, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } +} diff --git a/examples/event_handler_rest/src/data_validation_error_output.json b/examples/event_handler_rest/src/data_validation_error_output.json new file mode 100644 index 00000000000..15552c69329 --- /dev/null +++ b/examples/event_handler_rest/src/data_validation_error_output.json @@ -0,0 +1,9 @@ +{ + "statusCode": 422, + "body": "{\"detail\": [{\"type\": \"int_parsing\", \"loc\": [\"path\", \"todo_id\"], \"msg\": \"Input should be a valid integer, unable to parse string as an integer\", \"input\": \"apples\", \"url\": \"https://errors.pydantic.dev/2.5/v/int_parsing\"}]}", + "isBase64Encoded": false, + "headers": { + "Content-Type": "application/json" + }, + "cookies": [] +} From 3c741d8f5eb7541ce6cf94e2f6697d89249d4814 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 22 Nov 2023 14:28:43 +0100 Subject: [PATCH 09/36] docs: bring validation errors pydantic upfront to discuss later --- docs/core/event_handler/api_gateway.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index a7e0a2278e9..b1a745def79 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -270,6 +270,9 @@ Let's rewrite the previous examples to signal our resolver what shape we expect #### Handling validation errors +???+ note "Pydantic v1 vs v2" + Pydantic versions 1 and 2 may report validation errors differently. Refer to the documentation for your specific version to grasp the precise format and style of the error messages. + Any **incoming request that fails validation** will result in a `HTTP 422: Unprocessable Entity error` response. Below is a sample error response for failed validation due to incorrect input: @@ -286,9 +289,6 @@ Below is a sample error response for failed validation due to incorrect input: --8<-- "examples/event_handler_rest/src/data_validation_error_output.json" ``` -???+ note "Pydantic v1 vs v2" - Pydantic versions 1 and 2 may report validation errors differently. Refer to the documentation for your specific version to grasp the precise format and style of the error messages. - ### 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`. From 3beb9c4573bcd84295bf1af6b9ee6e5dded02776 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 22 Nov 2023 15:54:37 +0100 Subject: [PATCH 10/36] docs: add initial validating payloads section Signed-off-by: heitorlessa --- docs/core/event_handler/api_gateway.md | 31 +++++++++++++ .../src/validating_payloads.json | 40 +++++++++++++++++ .../src/validating_payloads.py | 43 +++++++++++++++++++ .../src/validating_payloads_output.json | 9 ++++ 4 files changed, 123 insertions(+) create mode 100644 examples/event_handler_rest/src/validating_payloads.json create mode 100644 examples/event_handler_rest/src/validating_payloads.py create mode 100644 examples/event_handler_rest/src/validating_payloads_output.json diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index b1a745def79..28aec41f26d 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -289,6 +289,37 @@ Below is a sample error response for failed validation due to incorrect input: --8<-- "examples/event_handler_rest/src/data_validation_error_output.json" ``` +#### Validating payloads + +!!! info "We will automatically validate, inject, and convert incoming request payloads based on models via type annotation." + +Let's improve our previous example by handling the creation of todo items via `HTTP POST`. + +What we want is for Event Handler to convert the incoming payload as an instance of our `Todo` model. We handle the creation of that `todo`, and then return the `ID` of the newly created `todo`. + +=== "validating_payloads.py" + + ```python hl_lines="13 16 24 25" + --8<-- "examples/event_handler_rest/src/validating_payloads.py" + ``` + + 1. This enforces data validation at runtime. Any validation error will return `HTTP 422: Unprocessable Entity error`. + 2. We create a Pydantic model to define how our data looks like. + 3. We define `Todo` as our type annotation. Event Handler then uses this model to validate and inject the incoming request as `Todo`. + 4. Lastly, we return the ID of our newly created `todo` item.

Because we specify the return type (`str`), Event Handler will take care of serializing this as a JSON string. + +=== "validating_payloads.json" + + ```json hl_lines="21 22 33" + --8<-- "examples/event_handler_rest/src/validating_payloads.json" + ``` + +=== "body_parsing_output.json" + + ```json hl_lines="3" + --8<-- "examples/event_handler_validation/src/body_parsing_output.json" + ``` + ### 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/examples/event_handler_rest/src/validating_payloads.json b/examples/event_handler_rest/src/validating_payloads.json new file mode 100644 index 00000000000..352d3de35cc --- /dev/null +++ b/examples/event_handler_rest/src/validating_payloads.json @@ -0,0 +1,40 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/todos", + "rawQueryString": "", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "header1": "value1", + "header2": "value1,value2" + }, + "queryStringParameters": {}, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/todos", + "protocol": "HTTP/1.1", + "sourceIp": "192.0.2.1", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": "{\"title\": \"foo\", \"userId\": \"1\", \"completed\": false}", + "pathParameters": {}, + "isBase64Encoded": false, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } +} diff --git a/examples/event_handler_rest/src/validating_payloads.py b/examples/event_handler_rest/src/validating_payloads.py new file mode 100644 index 00000000000..60fc9f03bf8 --- /dev/null +++ b/examples/event_handler_rest/src/validating_payloads.py @@ -0,0 +1,43 @@ +from typing import Optional + +import requests +from pydantic import BaseModel, Field + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayHttpResolver(enable_validation=True) # (1)! + + +class Todo(BaseModel): # (2)! + userId: int + id_: Optional[int] = Field(alias="id", default=None) + title: str + completed: bool + + +@app.post("/todos") +def create_todo(todo: Todo) -> str: # (3)! + response = requests.post("https://jsonplaceholder.typicode.com/todos", json=todo.dict(by_alias=True)) + response.raise_for_status() + + return response.json()["id"] # (4)! + + +@app.get("/todos/") +@tracer.capture_method +def get_todo_by_id(todo_id: int) -> Todo: + todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") + todo.raise_for_status() + + return Todo(**todo.json()) + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/validating_payloads_output.json b/examples/event_handler_rest/src/validating_payloads_output.json new file mode 100644 index 00000000000..c01584c86ed --- /dev/null +++ b/examples/event_handler_rest/src/validating_payloads_output.json @@ -0,0 +1,9 @@ +{ + "statusCode": 200, + "body": "2008821", + "isBase64Encoded": false, + "headers": { + "Content-Type": "application/json" + }, + "cookies": [] +} From 95139596f020b48283a07c2ea65eba36594fb2f0 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 22 Nov 2023 17:49:48 +0100 Subject: [PATCH 11/36] docs(apigateway): complete catching validation errors Signed-off-by: heitorlessa --- docs/core/event_handler/api_gateway.md | 23 ++++++++-- .../src/data_validation_error.py | 46 +++++++++++++++++++ .../src/data_validation_error_output.json | 4 +- ...a_validation_error_unsanitized_output.json | 9 ++++ 4 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 examples/event_handler_rest/src/data_validation_error.py create mode 100644 examples/event_handler_rest/src/data_validation_error_unsanitized_output.json diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 28aec41f26d..70e6ad690ee 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -273,16 +273,29 @@ Let's rewrite the previous examples to signal our resolver what shape we expect ???+ note "Pydantic v1 vs v2" Pydantic versions 1 and 2 may report validation errors differently. Refer to the documentation for your specific version to grasp the precise format and style of the error messages. -Any **incoming request that fails validation** will result in a `HTTP 422: Unprocessable Entity error` response. +Any **incoming request that fails validation** will lead to a `HTTP 422: Unprocessable Entity error` response. When they occur, by default they will look similar to this: -Below is a sample error response for failed validation due to incorrect input: +```json hl_lines="2 3" title="data_validation_error_unsanitized_output.json" +--8<-- "examples/event_handler_rest/src/data_validation_error_output.json" +``` -=== "data_validation_error.json" +However, you can customize the response by catching the `RequestValidationError` exception. - ```json hl_lines="4" - --8<-- "examples/event_handler_rest/src/data_validation_error.json" +???+ question "When is this useful?" + In production, you might want to hide detailed error information as to why validation failed to prevent abuse. + + Alternatively, you might have a standard on how to return API errors across your company. + +Here's an example where we catch validation errors, log all details for further investigation, and return the same `HTTP 422` but with minimum information. + +=== "data_validation_error.py" + + ```python hl_lines="8 24-25 31" + --8<-- "examples/event_handler_rest/src/data_validation_error.py" ``` + 1. We use [exception handler](#exception-handling) decorator to catch **any** request validation errors.

Then, we log the detailed reason as to why it failed while returning a custom `Response` object to hide that from them. + === "data_validation_error_output.json" ```json hl_lines="2 3" diff --git a/examples/event_handler_rest/src/data_validation_error.py b/examples/event_handler_rest/src/data_validation_error.py new file mode 100644 index 00000000000..71849938f48 --- /dev/null +++ b/examples/event_handler_rest/src/data_validation_error.py @@ -0,0 +1,46 @@ +from typing import Optional + +import requests +from pydantic import BaseModel, Field + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response, content_types +from aws_lambda_powertools.event_handler.openapi.exceptions import RequestValidationError +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayRestResolver(enable_validation=True) + + +class Todo(BaseModel): + userId: int + id_: Optional[int] = Field(alias="id", default=None) + title: str + completed: bool + + +@app.exception_handler(RequestValidationError) # (1)! +def handle_validation_error(ex: RequestValidationError): + logger.error("Request failed validation", path=app.current_event.path, errors=ex.errors()) + + return Response( + status_code=422, + content_type=content_types.APPLICATION_JSON, + body="Invalid data", + ) + + +@app.post("/todos") +def create_todo(todo: Todo) -> int: + response = requests.post("https://jsonplaceholder.typicode.com/todos", json=todo.dict(by_alias=True)) + response.raise_for_status() + + return response.json()["id"] + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/data_validation_error_output.json b/examples/event_handler_rest/src/data_validation_error_output.json index 15552c69329..aa6ab7e0d57 100644 --- a/examples/event_handler_rest/src/data_validation_error_output.json +++ b/examples/event_handler_rest/src/data_validation_error_output.json @@ -1,9 +1,9 @@ { "statusCode": 422, - "body": "{\"detail\": [{\"type\": \"int_parsing\", \"loc\": [\"path\", \"todo_id\"], \"msg\": \"Input should be a valid integer, unable to parse string as an integer\", \"input\": \"apples\", \"url\": \"https://errors.pydantic.dev/2.5/v/int_parsing\"}]}", + "body": "Invalid data", "isBase64Encoded": false, "headers": { "Content-Type": "application/json" }, "cookies": [] -} +} \ No newline at end of file diff --git a/examples/event_handler_rest/src/data_validation_error_unsanitized_output.json b/examples/event_handler_rest/src/data_validation_error_unsanitized_output.json new file mode 100644 index 00000000000..15552c69329 --- /dev/null +++ b/examples/event_handler_rest/src/data_validation_error_unsanitized_output.json @@ -0,0 +1,9 @@ +{ + "statusCode": 422, + "body": "{\"detail\": [{\"type\": \"int_parsing\", \"loc\": [\"path\", \"todo_id\"], \"msg\": \"Input should be a valid integer, unable to parse string as an integer\", \"input\": \"apples\", \"url\": \"https://errors.pydantic.dev/2.5/v/int_parsing\"}]}", + "isBase64Encoded": false, + "headers": { + "Content-Type": "application/json" + }, + "cookies": [] +} From 3e8347b1c6a690e0c37677ef8f2a6ea9b0a20595 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 22 Nov 2023 19:17:06 +0100 Subject: [PATCH 12/36] docs: correct validating payload output filename Signed-off-by: heitorlessa --- docs/core/event_handler/api_gateway.md | 4 ++-- ...dy_parsing_output.json => validating_payloads_output.json} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename examples/event_handler_validation/src/{body_parsing_output.json => validating_payloads_output.json} (100%) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 70e6ad690ee..f02cb16660e 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -327,10 +327,10 @@ What we want is for Event Handler to convert the incoming payload as an instance --8<-- "examples/event_handler_rest/src/validating_payloads.json" ``` -=== "body_parsing_output.json" +=== "validating_payloads_output.json" ```json hl_lines="3" - --8<-- "examples/event_handler_validation/src/body_parsing_output.json" + --8<-- "examples/event_handler_validation/src/validating_payloads_output.json" ``` ### Accessing request details diff --git a/examples/event_handler_validation/src/body_parsing_output.json b/examples/event_handler_validation/src/validating_payloads_output.json similarity index 100% rename from examples/event_handler_validation/src/body_parsing_output.json rename to examples/event_handler_validation/src/validating_payloads_output.json From 4db84abf44891f5d624662c540a72397ba1b87cb Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 22 Nov 2023 19:36:21 +0100 Subject: [PATCH 13/36] fix: restore file name in older location --- docs/core/event_handler/api_gateway.md | 2 +- ...s_output.json => body_parsing_output.json} | 0 .../src/validating_query_strings.py | 41 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) rename examples/event_handler_validation/src/{validating_payloads_output.json => body_parsing_output.json} (100%) create mode 100644 examples/event_handler_validation/src/validating_query_strings.py diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index f02cb16660e..f61e8dfddf1 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -330,7 +330,7 @@ What we want is for Event Handler to convert the incoming payload as an instance === "validating_payloads_output.json" ```json hl_lines="3" - --8<-- "examples/event_handler_validation/src/validating_payloads_output.json" + --8<-- "examples/event_handler_rest/src/validating_payloads_output.json" ``` ### Accessing request details diff --git a/examples/event_handler_validation/src/validating_payloads_output.json b/examples/event_handler_validation/src/body_parsing_output.json similarity index 100% rename from examples/event_handler_validation/src/validating_payloads_output.json rename to examples/event_handler_validation/src/body_parsing_output.json diff --git a/examples/event_handler_validation/src/validating_query_strings.py b/examples/event_handler_validation/src/validating_query_strings.py new file mode 100644 index 00000000000..9ed4ad8b56f --- /dev/null +++ b/examples/event_handler_validation/src/validating_query_strings.py @@ -0,0 +1,41 @@ +from typing import Annotated, Optional + +import requests +from pydantic import BaseModel, Field + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler.openapi.params import Query +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayHttpResolver(enable_validation=True) + + +class Todo(BaseModel): + userId: int + id_: Optional[int] = Field(alias="id", default=None) + title: str + completed: bool + + +@app.get("/todos") +@tracer.capture_method +def get_todos(completed: Annotated[Optional[str], Query(min_length=4)] = None) -> Todo: + url = "https://jsonplaceholder.typicode.com/todos" + + if completed is not None: + url = f"{url}/?completed={completed}" + + todo = requests.get(url) + todo.raise_for_status() + + return Todo(**todo.json()[0]) + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) From b4a8fe8deeefd200a44c7385ae8d1a0143fc2f43 Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Wed, 22 Nov 2023 19:51:06 +0100 Subject: [PATCH 14/36] chore: move first example to REST API --- .../sam/template.yaml | 2 +- .../src/getting_started.json | 60 +++++++++---------- .../src/getting_started.py | 4 +- 3 files changed, 30 insertions(+), 36 deletions(-) diff --git a/examples/event_handler_validation/sam/template.yaml b/examples/event_handler_validation/sam/template.yaml index c83d424dd4a..10d7920bc32 100644 --- a/examples/event_handler_validation/sam/template.yaml +++ b/examples/event_handler_validation/sam/template.yaml @@ -33,7 +33,7 @@ Resources: Description: API handler function Events: AnyApiEvent: - Type: HttpApi + Type: Api Properties: # NOTE: this is a catch-all rule to simplify the documentation. # explicit routes and methods are recommended for prod instead (see below) diff --git a/examples/event_handler_validation/src/getting_started.json b/examples/event_handler_validation/src/getting_started.json index e852a909265..e62338ae70e 100644 --- a/examples/event_handler_validation/src/getting_started.json +++ b/examples/event_handler_validation/src/getting_started.json @@ -1,42 +1,36 @@ { - "version": "2.0", - "routeKey": "$default", - "rawPath": "/todos/1", - "rawQueryString": "", - "cookies": [ - "cookie1", - "cookie2" - ], + "version": "1.0", + "resource": "/todos/1", + "path": "/todos/1", + "httpMethod": "GET", "headers": { - "header1": "value1", - "header2": "value1,value2" - }, - "queryStringParameters": { - "parameter1": "value1,value2", - "parameter2": "value" + "Origin": "https://aws.amazon.com" }, + "multiValueHeaders": {}, + "queryStringParameters": {}, + "multiValueQueryStringParameters": {}, "requestContext": { "accountId": "123456789012", - "apiId": "api-id", + "apiId": "id", + "authorizer": { + "claims": null, + "scopes": null + }, "domainName": "id.execute-api.us-east-1.amazonaws.com", "domainPrefix": "id", - "http": { - "method": "GET", - "path": "/todos/1", - "protocol": "HTTP/1.1", - "sourceIp": "192.0.2.1", - "userAgent": "agent" - }, - "requestId": "id", - "routeKey": "$default", - "stage": "$default", - "time": "12/Mar/2020:19:03:58 +0000", - "timeEpoch": 1583348638390 + "extendedRequestId": "request-id", + "httpMethod": "GET", + "path": "/todos", + "protocol": "HTTP/1.1", + "requestId": "id=", + "requestTime": "04/Mar/2020:19:15:17 +0000", + "requestTimeEpoch": 1583349317135, + "resourceId": null, + "resourcePath": "/todos/1", + "stage": "$default" }, - "pathParameters": {}, - "isBase64Encoded": false, - "stageVariables": { - "stageVariable1": "value1", - "stageVariable2": "value2" - } + "pathParameters": null, + "stageVariables": null, + "body": "", + "isBase64Encoded": false } diff --git a/examples/event_handler_validation/src/getting_started.py b/examples/event_handler_validation/src/getting_started.py index 43c6970fc64..f80c97856cb 100644 --- a/examples/event_handler_validation/src/getting_started.py +++ b/examples/event_handler_validation/src/getting_started.py @@ -1,13 +1,13 @@ import requests from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.logging import correlation_paths from aws_lambda_powertools.utilities.typing import LambdaContext tracer = Tracer() logger = Logger() -app = APIGatewayHttpResolver(enable_validation=True) +app = APIGatewayRestResolver(enable_validation=True) @app.get("/todos/") From 47f38944cbea628132379666bcf1806a42e7d0ae Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Wed, 22 Nov 2023 20:04:07 +0100 Subject: [PATCH 15/36] chore: rewrote the other examples --- .../src/body_parsing.json | 54 ++++++++--------- .../src/body_parsing.py | 4 +- .../src/body_parsing_embed.json | 54 ++++++++--------- .../src/body_parsing_embed.py | 4 +- .../src/body_parsing_embed_output.json | 9 +-- .../src/body_parsing_output.json | 9 +-- .../src/customize_api_metadata.py | 4 +- .../src/customizing_operations.py | 4 +- .../src/customizing_parameters.py | 4 +- .../src/generate_openapi_json_spec.py | 4 +- .../src/generate_openapi_spec.py | 4 +- .../src/getting_started.json | 2 +- .../src/getting_started_output.json | 13 ++-- .../src/getting_started_pydantic.json | 59 +++++++++---------- .../src/getting_started_pydantic.py | 4 +- .../src/getting_started_pydantic_output.json | 9 +-- .../event_handler_validation/src/swagger.py | 4 +- .../src/swagger_customize.py | 4 +- .../src/swagger_middlewares.py | 6 +- .../src/validating_query_strings.py | 4 +- 20 files changed, 125 insertions(+), 134 deletions(-) diff --git a/examples/event_handler_validation/src/body_parsing.json b/examples/event_handler_validation/src/body_parsing.json index 352d3de35cc..51d7b68a6fb 100644 --- a/examples/event_handler_validation/src/body_parsing.json +++ b/examples/event_handler_validation/src/body_parsing.json @@ -1,40 +1,36 @@ { - "version": "2.0", - "routeKey": "$default", - "rawPath": "/todos", - "rawQueryString": "", - "cookies": [ - "cookie1", - "cookie2" - ], + "version": "1.0", + "resource": "/todos", + "path": "/todos", + "httpMethod": "POST", "headers": { - "header1": "value1", - "header2": "value1,value2" + "Origin": "https://aws.amazon.com" }, + "multiValueHeaders": {}, "queryStringParameters": {}, + "multiValueQueryStringParameters": {}, "requestContext": { "accountId": "123456789012", - "apiId": "api-id", + "apiId": "id", + "authorizer": { + "claims": null, + "scopes": null + }, "domainName": "id.execute-api.us-east-1.amazonaws.com", "domainPrefix": "id", - "http": { - "method": "POST", - "path": "/todos", - "protocol": "HTTP/1.1", - "sourceIp": "192.0.2.1", - "userAgent": "agent" - }, - "requestId": "id", - "routeKey": "$default", - "stage": "$default", - "time": "12/Mar/2020:19:03:58 +0000", - "timeEpoch": 1583348638390 + "extendedRequestId": "request-id", + "httpMethod": "POST", + "path": "/todos", + "protocol": "HTTP/1.1", + "requestId": "id=", + "requestTime": "04/Mar/2020:19:15:17 +0000", + "requestTimeEpoch": 1583349317135, + "resourceId": null, + "resourcePath": "/todos", + "stage": "$default" }, + "pathParameters": null, + "stageVariables": null, "body": "{\"title\": \"foo\", \"userId\": \"1\", \"completed\": false}", - "pathParameters": {}, - "isBase64Encoded": false, - "stageVariables": { - "stageVariable1": "value1", - "stageVariable2": "value2" - } + "isBase64Encoded": false } diff --git a/examples/event_handler_validation/src/body_parsing.py b/examples/event_handler_validation/src/body_parsing.py index 8d5b104a77b..6bf6865cb48 100644 --- a/examples/event_handler_validation/src/body_parsing.py +++ b/examples/event_handler_validation/src/body_parsing.py @@ -3,10 +3,10 @@ import requests from pydantic import BaseModel, Field -from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.utilities.typing import LambdaContext -app = APIGatewayHttpResolver(enable_validation=True) +app = APIGatewayRestResolver(enable_validation=True) class Todo(BaseModel): diff --git a/examples/event_handler_validation/src/body_parsing_embed.json b/examples/event_handler_validation/src/body_parsing_embed.json index 629e8da4958..d6b3f564ee3 100644 --- a/examples/event_handler_validation/src/body_parsing_embed.json +++ b/examples/event_handler_validation/src/body_parsing_embed.json @@ -1,40 +1,36 @@ { - "version": "2.0", - "routeKey": "$default", - "rawPath": "/todos", - "rawQueryString": "", - "cookies": [ - "cookie1", - "cookie2" - ], + "version": "1.0", + "resource": "/todos", + "path": "/todos", + "httpMethod": "POST", "headers": { - "header1": "value1", - "header2": "value1,value2" + "Origin": "https://aws.amazon.com" }, + "multiValueHeaders": {}, "queryStringParameters": {}, + "multiValueQueryStringParameters": {}, "requestContext": { "accountId": "123456789012", - "apiId": "api-id", + "apiId": "id", + "authorizer": { + "claims": null, + "scopes": null + }, "domainName": "id.execute-api.us-east-1.amazonaws.com", "domainPrefix": "id", - "http": { - "method": "POST", - "path": "/todos", - "protocol": "HTTP/1.1", - "sourceIp": "192.0.2.1", - "userAgent": "agent" - }, - "requestId": "id", - "routeKey": "$default", - "stage": "$default", - "time": "12/Mar/2020:19:03:58 +0000", - "timeEpoch": 1583348638390 + "extendedRequestId": "request-id", + "httpMethod": "POST", + "path": "/todos", + "protocol": "HTTP/1.1", + "requestId": "id=", + "requestTime": "04/Mar/2020:19:15:17 +0000", + "requestTimeEpoch": 1583349317135, + "resourceId": null, + "resourcePath": "/todos", + "stage": "$default" }, + "pathParameters": null, + "stageVariables": null, "body": "{ \"todo\": {\"title\": \"foo\", \"userId\": \"1\", \"completed\": false } }", - "pathParameters": {}, - "isBase64Encoded": false, - "stageVariables": { - "stageVariable1": "value1", - "stageVariable2": "value2" - } + "isBase64Encoded": false } diff --git a/examples/event_handler_validation/src/body_parsing_embed.py b/examples/event_handler_validation/src/body_parsing_embed.py index 7e4d9ab7426..d27fbeb2259 100644 --- a/examples/event_handler_validation/src/body_parsing_embed.py +++ b/examples/event_handler_validation/src/body_parsing_embed.py @@ -3,11 +3,11 @@ import requests from pydantic import BaseModel, Field -from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.event_handler.openapi.params import Body from aws_lambda_powertools.utilities.typing import LambdaContext -app = APIGatewayHttpResolver(enable_validation=True) +app = APIGatewayRestResolver(enable_validation=True) class Todo(BaseModel): diff --git a/examples/event_handler_validation/src/body_parsing_embed_output.json b/examples/event_handler_validation/src/body_parsing_embed_output.json index 564a5e7a9fb..754e3a6c128 100644 --- a/examples/event_handler_validation/src/body_parsing_embed_output.json +++ b/examples/event_handler_validation/src/body_parsing_embed_output.json @@ -2,8 +2,9 @@ "statusCode": 200, "body": "2008822", "isBase64Encoded": false, - "headers": { - "Content-Type": "application/json" - }, - "cookies": [] + "multiValueHeaders": { + "Content-Type": [ + "application/json" + ] + } } diff --git a/examples/event_handler_validation/src/body_parsing_output.json b/examples/event_handler_validation/src/body_parsing_output.json index c01584c86ed..7e413260e12 100644 --- a/examples/event_handler_validation/src/body_parsing_output.json +++ b/examples/event_handler_validation/src/body_parsing_output.json @@ -2,8 +2,9 @@ "statusCode": 200, "body": "2008821", "isBase64Encoded": false, - "headers": { - "Content-Type": "application/json" - }, - "cookies": [] + "multiValueHeaders": { + "Content-Type": [ + "application/json" + ] + } } diff --git a/examples/event_handler_validation/src/customize_api_metadata.py b/examples/event_handler_validation/src/customize_api_metadata.py index 74ad6ecc7a3..cd9ced455d2 100644 --- a/examples/event_handler_validation/src/customize_api_metadata.py +++ b/examples/event_handler_validation/src/customize_api_metadata.py @@ -1,10 +1,10 @@ import requests -from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.event_handler.openapi.models import Contact, Server from aws_lambda_powertools.utilities.typing import LambdaContext -app = APIGatewayHttpResolver(enable_validation=True) +app = APIGatewayRestResolver(enable_validation=True) @app.get("/todos/") diff --git a/examples/event_handler_validation/src/customizing_operations.py b/examples/event_handler_validation/src/customizing_operations.py index 109e4272a36..e455fc7dadd 100644 --- a/examples/event_handler_validation/src/customizing_operations.py +++ b/examples/event_handler_validation/src/customizing_operations.py @@ -1,9 +1,9 @@ import requests -from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.utilities.typing import LambdaContext -app = APIGatewayHttpResolver(enable_validation=True) +app = APIGatewayRestResolver(enable_validation=True) @app.get( diff --git a/examples/event_handler_validation/src/customizing_parameters.py b/examples/event_handler_validation/src/customizing_parameters.py index 6659b43ad44..394a453e41d 100644 --- a/examples/event_handler_validation/src/customizing_parameters.py +++ b/examples/event_handler_validation/src/customizing_parameters.py @@ -3,14 +3,14 @@ import requests from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.event_handler.openapi.params import Param, Path from aws_lambda_powertools.logging import correlation_paths from aws_lambda_powertools.utilities.typing import LambdaContext tracer = Tracer() logger = Logger() -app = APIGatewayHttpResolver(enable_validation=True) +app = APIGatewayRestResolver(enable_validation=True) @app.get("/todos/") diff --git a/examples/event_handler_validation/src/generate_openapi_json_spec.py b/examples/event_handler_validation/src/generate_openapi_json_spec.py index 7563bac9ed9..6a7582d3c75 100644 --- a/examples/event_handler_validation/src/generate_openapi_json_spec.py +++ b/examples/event_handler_validation/src/generate_openapi_json_spec.py @@ -3,10 +3,10 @@ import requests from pydantic import BaseModel, EmailStr, Field -from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.utilities.typing import LambdaContext -app = APIGatewayHttpResolver(enable_validation=True) +app = APIGatewayRestResolver(enable_validation=True) class Todo(BaseModel): diff --git a/examples/event_handler_validation/src/generate_openapi_spec.py b/examples/event_handler_validation/src/generate_openapi_spec.py index ce559e00fcf..21c1f5cd1fe 100644 --- a/examples/event_handler_validation/src/generate_openapi_spec.py +++ b/examples/event_handler_validation/src/generate_openapi_spec.py @@ -3,10 +3,10 @@ import requests from pydantic import BaseModel, EmailStr, Field -from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.utilities.typing import LambdaContext -app = APIGatewayHttpResolver(enable_validation=True) +app = APIGatewayRestResolver(enable_validation=True) class Todo(BaseModel): diff --git a/examples/event_handler_validation/src/getting_started.json b/examples/event_handler_validation/src/getting_started.json index e62338ae70e..ed4a573e7c1 100644 --- a/examples/event_handler_validation/src/getting_started.json +++ b/examples/event_handler_validation/src/getting_started.json @@ -20,7 +20,7 @@ "domainPrefix": "id", "extendedRequestId": "request-id", "httpMethod": "GET", - "path": "/todos", + "path": "/todos/1", "protocol": "HTTP/1.1", "requestId": "id=", "requestTime": "04/Mar/2020:19:15:17 +0000", diff --git a/examples/event_handler_validation/src/getting_started_output.json b/examples/event_handler_validation/src/getting_started_output.json index b05dd2d460c..cd0f41b54b8 100644 --- a/examples/event_handler_validation/src/getting_started_output.json +++ b/examples/event_handler_validation/src/getting_started_output.json @@ -1,9 +1,10 @@ { "statusCode": 200, - "headers": { - "Content-Type": "application/json" - }, - "cookies": [], - "body": "hello world", - "isBase64Encoded": false + "body": "Hello world", + "isBase64Encoded": false, + "multiValueHeaders": { + "Content-Type": [ + "application/json" + ] + } } diff --git a/examples/event_handler_validation/src/getting_started_pydantic.json b/examples/event_handler_validation/src/getting_started_pydantic.json index 9320e046756..76864b672e7 100644 --- a/examples/event_handler_validation/src/getting_started_pydantic.json +++ b/examples/event_handler_validation/src/getting_started_pydantic.json @@ -1,41 +1,36 @@ { - "version": "2.0", - "routeKey": "$default", - "rawPath": "/todos", - "rawQueryString": "?email=foo@bar.com", - "cookies": [ - "cookie1", - "cookie2" - ], + "version": "1.0", + "resource": "/todos", + "path": "/todos", + "httpMethod": "GET", "headers": { - "header1": "value1", - "header2": "value1,value2" - }, - "queryStringParameters": { - "email": "foo@bar.com" + "Origin": "https://aws.amazon.com" }, + "multiValueHeaders": {}, + "queryStringParameters": {}, + "multiValueQueryStringParameters": {}, "requestContext": { "accountId": "123456789012", - "apiId": "api-id", + "apiId": "id", + "authorizer": { + "claims": null, + "scopes": null + }, "domainName": "id.execute-api.us-east-1.amazonaws.com", "domainPrefix": "id", - "http": { - "method": "GET", - "path": "/todos", - "protocol": "HTTP/1.1", - "sourceIp": "192.0.2.1", - "userAgent": "agent" - }, - "requestId": "id", - "routeKey": "$default", - "stage": "$default", - "time": "12/Mar/2020:19:03:58 +0000", - "timeEpoch": 1583348638390 + "extendedRequestId": "request-id", + "httpMethod": "GET", + "path": "/todos", + "protocol": "HTTP/1.1", + "requestId": "id=", + "requestTime": "04/Mar/2020:19:15:17 +0000", + "requestTimeEpoch": 1583349317135, + "resourceId": null, + "resourcePath": "/todos", + "stage": "$default" }, - "pathParameters": {}, - "isBase64Encoded": false, - "stageVariables": { - "stageVariable1": "value1", - "stageVariable2": "value2" - } + "pathParameters": null, + "stageVariables": null, + "body": "", + "isBase64Encoded": false } diff --git a/examples/event_handler_validation/src/getting_started_pydantic.py b/examples/event_handler_validation/src/getting_started_pydantic.py index dd7f2f68e14..481f3f98f44 100644 --- a/examples/event_handler_validation/src/getting_started_pydantic.py +++ b/examples/event_handler_validation/src/getting_started_pydantic.py @@ -3,10 +3,10 @@ import requests from pydantic import BaseModel, EmailStr, Field -from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.utilities.typing import LambdaContext -app = APIGatewayHttpResolver(enable_validation=True) +app = APIGatewayRestResolver(enable_validation=True) class Todo(BaseModel): diff --git a/examples/event_handler_validation/src/getting_started_pydantic_output.json b/examples/event_handler_validation/src/getting_started_pydantic_output.json index 4a38dacbea6..8375de999f0 100644 --- a/examples/event_handler_validation/src/getting_started_pydantic_output.json +++ b/examples/event_handler_validation/src/getting_started_pydantic_output.json @@ -2,8 +2,9 @@ "statusCode": 200, "body": "[{\"completed\": false, \"id\": 1, \"title\": \"delectus aut autem\", \"userId\": 1}, {\"completed\": false, \"id\": 2, \"title\": \"quis ut nam facilis et officia qui\", \"userId\": 1}, {\"completed\": false, \"id\": 3, \"title\": \"fugiat veniam minus\", \"userId\": 1}, {\"completed\": true, \"id\": 4, \"title\": \"et porro tempora\", \"userId\": 1}, {\"completed\": false, \"id\": 5, \"title\": \"laboriosam mollitia et enim quasi adipisci quia provident illum\", \"userId\": 1}, {\"completed\": false, \"id\": 6, \"title\": \"qui ullam ratione quibusdam voluptatem quia omnis\", \"userId\": 1}, {\"completed\": false, \"id\": 7, \"title\": \"illo expedita consequatur quia in\", \"userId\": 1}, {\"completed\": true, \"id\": 8, \"title\": \"quo adipisci enim quam ut ab\", \"userId\": 1}, {\"completed\": false, \"id\": 9, \"title\": \"molestiae perspiciatis ipsa\", \"userId\": 1}, {\"completed\": true, \"id\": 10, \"title\": \"illo est ratione doloremque quia maiores aut\", \"userId\": 1}, {\"completed\": true, \"id\": 11, \"title\": \"vero rerum temporibus dolor\", \"userId\": 1}, {\"completed\": true, \"id\": 12, \"title\": \"ipsa repellendus fugit nisi\", \"userId\": 1}, {\"completed\": false, \"id\": 13, \"title\": \"et doloremque nulla\", \"userId\": 1}, {\"completed\": true, \"id\": 14, \"title\": \"repellendus sunt dolores architecto voluptatum\", \"userId\": 1}, {\"completed\": true, \"id\": 15, \"title\": \"ab voluptatum amet voluptas\", \"userId\": 1}, {\"completed\": true, \"id\": 16, \"title\": \"accusamus eos facilis sint et aut voluptatem\", \"userId\": 1}, {\"completed\": true, \"id\": 17, \"title\": \"quo laboriosam deleniti aut qui\", \"userId\": 1}, {\"completed\": false, \"id\": 18, \"title\": \"dolorum est consequatur ea mollitia in culpa\", \"userId\": 1}, {\"completed\": true, \"id\": 19, \"title\": \"molestiae ipsa aut voluptatibus pariatur dolor nihil\", \"userId\": 1}, {\"completed\": true, \"id\": 20, \"title\": \"ullam nobis libero sapiente ad optio sint\", \"userId\": 1}, {\"completed\": false, \"id\": 21, \"title\": \"suscipit repellat esse quibusdam voluptatem incidunt\", \"userId\": 2}, {\"completed\": true, \"id\": 22, \"title\": \"distinctio vitae autem nihil ut molestias quo\", \"userId\": 2}, {\"completed\": false, \"id\": 23, \"title\": \"et itaque necessitatibus maxime molestiae qui quas velit\", \"userId\": 2}, {\"completed\": false, \"id\": 24, \"title\": \"adipisci non ad dicta qui amet quaerat doloribus ea\", \"userId\": 2}, {\"completed\": true, \"id\": 25, \"title\": \"voluptas quo tenetur perspiciatis explicabo natus\", \"userId\": 2}, {\"completed\": true, \"id\": 26, \"title\": \"aliquam aut quasi\", \"userId\": 2}, {\"completed\": true, \"id\": 27, \"title\": \"veritatis pariatur delectus\", \"userId\": 2}, {\"completed\": false, \"id\": 28, \"title\": \"nesciunt totam sit blanditiis sit\", \"userId\": 2}, {\"completed\": false, \"id\": 29, \"title\": \"laborum aut in quam\", \"userId\": 2}, {\"completed\": true, \"id\": 30, \"title\": \"nemo perspiciatis repellat ut dolor libero commodi blanditiis omnis\", \"userId\": 2}, {\"completed\": false, \"id\": 31, \"title\": \"repudiandae totam in est sint facere fuga\", \"userId\": 2}, {\"completed\": false, \"id\": 32, \"title\": \"earum doloribus ea doloremque quis\", \"userId\": 2}, {\"completed\": false, \"id\": 33, \"title\": \"sint sit aut vero\", \"userId\": 2}, {\"completed\": false, \"id\": 34, \"title\": \"porro aut necessitatibus eaque distinctio\", \"userId\": 2}, {\"completed\": true, \"id\": 35, \"title\": \"repellendus veritatis molestias dicta incidunt\", \"userId\": 2}, {\"completed\": true, \"id\": 36, \"title\": \"excepturi deleniti adipisci voluptatem et neque optio illum ad\", \"userId\": 2}, {\"completed\": false, \"id\": 37, \"title\": \"sunt cum tempora\", \"userId\": 2}, {\"completed\": false, \"id\": 38, \"title\": \"totam quia non\", \"userId\": 2}, {\"completed\": false, \"id\": 39, \"title\": \"doloremque quibusdam asperiores libero corrupti illum qui omnis\", \"userId\": 2}, {\"completed\": true, \"id\": 40, \"title\": \"totam atque quo nesciunt\", \"userId\": 2}, {\"completed\": false, \"id\": 41, \"title\": \"aliquid amet impedit consequatur aspernatur placeat eaque fugiat suscipit\", \"userId\": 3}, {\"completed\": false, \"id\": 42, \"title\": \"rerum perferendis error quia ut eveniet\", \"userId\": 3}, {\"completed\": true, \"id\": 43, \"title\": \"tempore ut sint quis recusandae\", \"userId\": 3}, {\"completed\": true, \"id\": 44, \"title\": \"cum debitis quis accusamus doloremque ipsa natus sapiente omnis\", \"userId\": 3}, {\"completed\": false, \"id\": 45, \"title\": \"velit soluta adipisci molestias reiciendis harum\", \"userId\": 3}, {\"completed\": false, \"id\": 46, \"title\": \"vel voluptatem repellat nihil placeat corporis\", \"userId\": 3}, {\"completed\": false, \"id\": 47, \"title\": \"nam qui rerum fugiat accusamus\", \"userId\": 3}, {\"completed\": false, \"id\": 48, \"title\": \"sit reprehenderit omnis quia\", \"userId\": 3}, {\"completed\": false, \"id\": 49, \"title\": \"ut necessitatibus aut maiores debitis officia blanditiis velit et\", \"userId\": 3}, {\"completed\": true, \"id\": 50, \"title\": \"cupiditate necessitatibus ullam aut quis dolor voluptate\", \"userId\": 3}, {\"completed\": false, \"id\": 51, \"title\": \"distinctio exercitationem ab doloribus\", \"userId\": 3}, {\"completed\": false, \"id\": 52, \"title\": \"nesciunt dolorum quis recusandae ad pariatur ratione\", \"userId\": 3}, {\"completed\": false, \"id\": 53, \"title\": \"qui labore est occaecati recusandae aliquid quam\", \"userId\": 3}, {\"completed\": true, \"id\": 54, \"title\": \"quis et est ut voluptate quam dolor\", \"userId\": 3}, {\"completed\": true, \"id\": 55, \"title\": \"voluptatum omnis minima qui occaecati provident nulla voluptatem ratione\", \"userId\": 3}, {\"completed\": true, \"id\": 56, \"title\": \"deleniti ea temporibus enim\", \"userId\": 3}, {\"completed\": false, \"id\": 57, \"title\": \"pariatur et magnam ea doloribus similique voluptatem rerum quia\", \"userId\": 3}, {\"completed\": false, \"id\": 58, \"title\": \"est dicta totam qui explicabo doloribus qui dignissimos\", \"userId\": 3}, {\"completed\": false, \"id\": 59, \"title\": \"perspiciatis velit id laborum placeat iusto et aliquam odio\", \"userId\": 3}, {\"completed\": true, \"id\": 60, \"title\": \"et sequi qui architecto ut adipisci\", \"userId\": 3}, {\"completed\": true, \"id\": 61, \"title\": \"odit optio omnis qui sunt\", \"userId\": 4}, {\"completed\": false, \"id\": 62, \"title\": \"et placeat et tempore aspernatur sint numquam\", \"userId\": 4}, {\"completed\": true, \"id\": 63, \"title\": \"doloremque aut dolores quidem fuga qui nulla\", \"userId\": 4}, {\"completed\": false, \"id\": 64, \"title\": \"voluptas consequatur qui ut quia magnam nemo esse\", \"userId\": 4}, {\"completed\": false, \"id\": 65, \"title\": \"fugiat pariatur ratione ut asperiores necessitatibus magni\", \"userId\": 4}, {\"completed\": false, \"id\": 66, \"title\": \"rerum eum molestias autem voluptatum sit optio\", \"userId\": 4}, {\"completed\": false, \"id\": 67, \"title\": \"quia voluptatibus voluptatem quos similique maiores repellat\", \"userId\": 4}, {\"completed\": false, \"id\": 68, \"title\": \"aut id perspiciatis voluptatem iusto\", \"userId\": 4}, {\"completed\": false, \"id\": 69, \"title\": \"doloribus sint dolorum ab adipisci itaque dignissimos aliquam suscipit\", \"userId\": 4}, {\"completed\": false, \"id\": 70, \"title\": \"ut sequi accusantium et mollitia delectus sunt\", \"userId\": 4}, {\"completed\": false, \"id\": 71, \"title\": \"aut velit saepe ullam\", \"userId\": 4}, {\"completed\": false, \"id\": 72, \"title\": \"praesentium facilis facere quis harum voluptatibus voluptatem eum\", \"userId\": 4}, {\"completed\": true, \"id\": 73, \"title\": \"sint amet quia totam corporis qui exercitationem commodi\", \"userId\": 4}, {\"completed\": false, \"id\": 74, \"title\": \"expedita tempore nobis eveniet laborum maiores\", \"userId\": 4}, {\"completed\": false, \"id\": 75, \"title\": \"occaecati adipisci est possimus totam\", \"userId\": 4}, {\"completed\": true, \"id\": 76, \"title\": \"sequi dolorem sed\", \"userId\": 4}, {\"completed\": false, \"id\": 77, \"title\": \"maiores aut nesciunt delectus exercitationem vel assumenda eligendi at\", \"userId\": 4}, {\"completed\": false, \"id\": 78, \"title\": \"reiciendis est magnam amet nemo iste recusandae impedit quaerat\", \"userId\": 4}, {\"completed\": true, \"id\": 79, \"title\": \"eum ipsa maxime ut\", \"userId\": 4}, {\"completed\": true, \"id\": 80, \"title\": \"tempore molestias dolores rerum sequi voluptates ipsum consequatur\", \"userId\": 4}, {\"completed\": true, \"id\": 81, \"title\": \"suscipit qui totam\", \"userId\": 5}, {\"completed\": false, \"id\": 82, \"title\": \"voluptates eum voluptas et dicta\", \"userId\": 5}, {\"completed\": true, \"id\": 83, \"title\": \"quidem at rerum quis ex aut sit quam\", \"userId\": 5}, {\"completed\": false, \"id\": 84, \"title\": \"sunt veritatis ut voluptate\", \"userId\": 5}, {\"completed\": true, \"id\": 85, \"title\": \"et quia ad iste a\", \"userId\": 5}, {\"completed\": true, \"id\": 86, \"title\": \"incidunt ut saepe autem\", \"userId\": 5}, {\"completed\": true, \"id\": 87, \"title\": \"laudantium quae eligendi consequatur quia et vero autem\", \"userId\": 5}, {\"completed\": false, \"id\": 88, \"title\": \"vitae aut excepturi laboriosam sint aliquam et et accusantium\", \"userId\": 5}, {\"completed\": true, \"id\": 89, \"title\": \"sequi ut omnis et\", \"userId\": 5}, {\"completed\": true, \"id\": 90, \"title\": \"molestiae nisi accusantium tenetur dolorem et\", \"userId\": 5}, {\"completed\": true, \"id\": 91, \"title\": \"nulla quis consequatur saepe qui id expedita\", \"userId\": 5}, {\"completed\": true, \"id\": 92, \"title\": \"in omnis laboriosam\", \"userId\": 5}, {\"completed\": true, \"id\": 93, \"title\": \"odio iure consequatur molestiae quibusdam necessitatibus quia sint\", \"userId\": 5}, {\"completed\": false, \"id\": 94, \"title\": \"facilis modi saepe mollitia\", \"userId\": 5}, {\"completed\": true, \"id\": 95, \"title\": \"vel nihil et molestiae iusto assumenda nemo quo ut\", \"userId\": 5}, {\"completed\": false, \"id\": 96, \"title\": \"nobis suscipit ducimus enim asperiores voluptas\", \"userId\": 5}, {\"completed\": false, \"id\": 97, \"title\": \"dolorum laboriosam eos qui iure aliquam\", \"userId\": 5}, {\"completed\": true, \"id\": 98, \"title\": \"debitis accusantium ut quo facilis nihil quis sapiente necessitatibus\", \"userId\": 5}, {\"completed\": false, \"id\": 99, \"title\": \"neque voluptates ratione\", \"userId\": 5}, {\"completed\": false, \"id\": 100, \"title\": \"excepturi a et neque qui expedita vel voluptate\", \"userId\": 5}, {\"completed\": false, \"id\": 101, \"title\": \"explicabo enim cumque porro aperiam occaecati minima\", \"userId\": 6}, {\"completed\": false, \"id\": 102, \"title\": \"sed ab consequatur\", \"userId\": 6}, {\"completed\": false, \"id\": 103, \"title\": \"non sunt delectus illo nulla tenetur enim omnis\", \"userId\": 6}, {\"completed\": false, \"id\": 104, \"title\": \"excepturi non laudantium quo\", \"userId\": 6}, {\"completed\": true, \"id\": 105, \"title\": \"totam quia dolorem et illum repellat voluptas optio\", \"userId\": 6}, {\"completed\": true, \"id\": 106, \"title\": \"ad illo quis voluptatem temporibus\", \"userId\": 6}, {\"completed\": false, \"id\": 107, \"title\": \"praesentium facilis omnis laudantium fugit ad iusto nihil nesciunt\", \"userId\": 6}, {\"completed\": true, \"id\": 108, \"title\": \"a eos eaque nihil et exercitationem incidunt delectus\", \"userId\": 6}, {\"completed\": true, \"id\": 109, \"title\": \"autem temporibus harum quisquam in culpa\", \"userId\": 6}, {\"completed\": true, \"id\": 110, \"title\": \"aut aut ea corporis\", \"userId\": 6}, {\"completed\": false, \"id\": 111, \"title\": \"magni accusantium labore et id quis provident\", \"userId\": 6}, {\"completed\": false, \"id\": 112, \"title\": \"consectetur impedit quisquam qui deserunt non rerum consequuntur eius\", \"userId\": 6}, {\"completed\": false, \"id\": 113, \"title\": \"quia atque aliquam sunt impedit voluptatum rerum assumenda nisi\", \"userId\": 6}, {\"completed\": false, \"id\": 114, \"title\": \"cupiditate quos possimus corporis quisquam exercitationem beatae\", \"userId\": 6}, {\"completed\": false, \"id\": 115, \"title\": \"sed et ea eum\", \"userId\": 6}, {\"completed\": true, \"id\": 116, \"title\": \"ipsa dolores vel facilis ut\", \"userId\": 6}, {\"completed\": false, \"id\": 117, \"title\": \"sequi quae est et qui qui eveniet asperiores\", \"userId\": 6}, {\"completed\": false, \"id\": 118, \"title\": \"quia modi consequatur vero fugiat\", \"userId\": 6}, {\"completed\": false, \"id\": 119, \"title\": \"corporis ducimus ea perspiciatis iste\", \"userId\": 6}, {\"completed\": false, \"id\": 120, \"title\": \"dolorem laboriosam vel voluptas et aliquam quasi\", \"userId\": 6}, {\"completed\": true, \"id\": 121, \"title\": \"inventore aut nihil minima laudantium hic qui omnis\", \"userId\": 7}, {\"completed\": true, \"id\": 122, \"title\": \"provident aut nobis culpa\", \"userId\": 7}, {\"completed\": false, \"id\": 123, \"title\": \"esse et quis iste est earum aut impedit\", \"userId\": 7}, {\"completed\": false, \"id\": 124, \"title\": \"qui consectetur id\", \"userId\": 7}, {\"completed\": false, \"id\": 125, \"title\": \"aut quasi autem iste tempore illum possimus\", \"userId\": 7}, {\"completed\": true, \"id\": 126, \"title\": \"ut asperiores perspiciatis veniam ipsum rerum saepe\", \"userId\": 7}, {\"completed\": true, \"id\": 127, \"title\": \"voluptatem libero consectetur rerum ut\", \"userId\": 7}, {\"completed\": false, \"id\": 128, \"title\": \"eius omnis est qui voluptatem autem\", \"userId\": 7}, {\"completed\": false, \"id\": 129, \"title\": \"rerum culpa quis harum\", \"userId\": 7}, {\"completed\": true, \"id\": 130, \"title\": \"nulla aliquid eveniet harum laborum libero alias ut unde\", \"userId\": 7}, {\"completed\": false, \"id\": 131, \"title\": \"qui ea incidunt quis\", \"userId\": 7}, {\"completed\": true, \"id\": 132, \"title\": \"qui molestiae voluptatibus velit iure harum quisquam\", \"userId\": 7}, {\"completed\": true, \"id\": 133, \"title\": \"et labore eos enim rerum consequatur sunt\", \"userId\": 7}, {\"completed\": false, \"id\": 134, \"title\": \"molestiae doloribus et laborum quod ea\", \"userId\": 7}, {\"completed\": false, \"id\": 135, \"title\": \"facere ipsa nam eum voluptates reiciendis vero qui\", \"userId\": 7}, {\"completed\": false, \"id\": 136, \"title\": \"asperiores illo tempora fuga sed ut quasi adipisci\", \"userId\": 7}, {\"completed\": false, \"id\": 137, \"title\": \"qui sit non\", \"userId\": 7}, {\"completed\": true, \"id\": 138, \"title\": \"placeat minima consequatur rem qui ut\", \"userId\": 7}, {\"completed\": false, \"id\": 139, \"title\": \"consequatur doloribus id possimus voluptas a voluptatem\", \"userId\": 7}, {\"completed\": true, \"id\": 140, \"title\": \"aut consectetur in blanditiis deserunt quia sed laboriosam\", \"userId\": 7}, {\"completed\": true, \"id\": 141, \"title\": \"explicabo consectetur debitis voluptates quas quae culpa rerum non\", \"userId\": 8}, {\"completed\": true, \"id\": 142, \"title\": \"maiores accusantium architecto necessitatibus reiciendis ea aut\", \"userId\": 8}, {\"completed\": false, \"id\": 143, \"title\": \"eum non recusandae cupiditate animi\", \"userId\": 8}, {\"completed\": false, \"id\": 144, \"title\": \"ut eum exercitationem sint\", \"userId\": 8}, {\"completed\": false, \"id\": 145, \"title\": \"beatae qui ullam incidunt voluptatem non nisi aliquam\", \"userId\": 8}, {\"completed\": true, \"id\": 146, \"title\": \"molestiae suscipit ratione nihil odio libero impedit vero totam\", \"userId\": 8}, {\"completed\": true, \"id\": 147, \"title\": \"eum itaque quod reprehenderit et facilis dolor autem ut\", \"userId\": 8}, {\"completed\": false, \"id\": 148, \"title\": \"esse quas et quo quasi exercitationem\", \"userId\": 8}, {\"completed\": false, \"id\": 149, \"title\": \"animi voluptas quod perferendis est\", \"userId\": 8}, {\"completed\": false, \"id\": 150, \"title\": \"eos amet tempore laudantium fugit a\", \"userId\": 8}, {\"completed\": true, \"id\": 151, \"title\": \"accusamus adipisci dicta qui quo ea explicabo sed vero\", \"userId\": 8}, {\"completed\": false, \"id\": 152, \"title\": \"odit eligendi recusandae doloremque cumque non\", \"userId\": 8}, {\"completed\": false, \"id\": 153, \"title\": \"ea aperiam consequatur qui repellat eos\", \"userId\": 8}, {\"completed\": true, \"id\": 154, \"title\": \"rerum non ex sapiente\", \"userId\": 8}, {\"completed\": true, \"id\": 155, \"title\": \"voluptatem nobis consequatur et assumenda magnam\", \"userId\": 8}, {\"completed\": true, \"id\": 156, \"title\": \"nam quia quia nulla repellat assumenda quibusdam sit nobis\", \"userId\": 8}, {\"completed\": true, \"id\": 157, \"title\": \"dolorem veniam quisquam deserunt repellendus\", \"userId\": 8}, {\"completed\": true, \"id\": 158, \"title\": \"debitis vitae delectus et harum accusamus aut deleniti a\", \"userId\": 8}, {\"completed\": true, \"id\": 159, \"title\": \"debitis adipisci quibusdam aliquam sed dolore ea praesentium nobis\", \"userId\": 8}, {\"completed\": false, \"id\": 160, \"title\": \"et praesentium aliquam est\", \"userId\": 8}, {\"completed\": true, \"id\": 161, \"title\": \"ex hic consequuntur earum omnis alias ut occaecati culpa\", \"userId\": 9}, {\"completed\": true, \"id\": 162, \"title\": \"omnis laboriosam molestias animi sunt dolore\", \"userId\": 9}, {\"completed\": false, \"id\": 163, \"title\": \"natus corrupti maxime laudantium et voluptatem laboriosam odit\", \"userId\": 9}, {\"completed\": false, \"id\": 164, \"title\": \"reprehenderit quos aut aut consequatur est sed\", \"userId\": 9}, {\"completed\": false, \"id\": 165, \"title\": \"fugiat perferendis sed aut quidem\", \"userId\": 9}, {\"completed\": false, \"id\": 166, \"title\": \"quos quo possimus suscipit minima ut\", \"userId\": 9}, {\"completed\": false, \"id\": 167, \"title\": \"et quis minus quo a asperiores molestiae\", \"userId\": 9}, {\"completed\": false, \"id\": 168, \"title\": \"recusandae quia qui sunt libero\", \"userId\": 9}, {\"completed\": true, \"id\": 169, \"title\": \"ea odio perferendis officiis\", \"userId\": 9}, {\"completed\": false, \"id\": 170, \"title\": \"quisquam aliquam quia doloribus aut\", \"userId\": 9}, {\"completed\": true, \"id\": 171, \"title\": \"fugiat aut voluptatibus corrupti deleniti velit iste odio\", \"userId\": 9}, {\"completed\": false, \"id\": 172, \"title\": \"et provident amet rerum consectetur et voluptatum\", \"userId\": 9}, {\"completed\": false, \"id\": 173, \"title\": \"harum ad aperiam quis\", \"userId\": 9}, {\"completed\": false, \"id\": 174, \"title\": \"similique aut quo\", \"userId\": 9}, {\"completed\": true, \"id\": 175, \"title\": \"laudantium eius officia perferendis provident perspiciatis asperiores\", \"userId\": 9}, {\"completed\": false, \"id\": 176, \"title\": \"magni soluta corrupti ut maiores rem quidem\", \"userId\": 9}, {\"completed\": false, \"id\": 177, \"title\": \"et placeat temporibus voluptas est tempora quos quibusdam\", \"userId\": 9}, {\"completed\": true, \"id\": 178, \"title\": \"nesciunt itaque commodi tempore\", \"userId\": 9}, {\"completed\": true, \"id\": 179, \"title\": \"omnis consequuntur cupiditate impedit itaque ipsam quo\", \"userId\": 9}, {\"completed\": true, \"id\": 180, \"title\": \"debitis nisi et dolorem repellat et\", \"userId\": 9}, {\"completed\": false, \"id\": 181, \"title\": \"ut cupiditate sequi aliquam fuga maiores\", \"userId\": 10}, {\"completed\": true, \"id\": 182, \"title\": \"inventore saepe cumque et aut illum enim\", \"userId\": 10}, {\"completed\": true, \"id\": 183, \"title\": \"omnis nulla eum aliquam distinctio\", \"userId\": 10}, {\"completed\": false, \"id\": 184, \"title\": \"molestias modi perferendis perspiciatis\", \"userId\": 10}, {\"completed\": false, \"id\": 185, \"title\": \"voluptates dignissimos sed doloribus animi quaerat aut\", \"userId\": 10}, {\"completed\": false, \"id\": 186, \"title\": \"explicabo odio est et\", \"userId\": 10}, {\"completed\": false, \"id\": 187, \"title\": \"consequuntur animi possimus\", \"userId\": 10}, {\"completed\": true, \"id\": 188, \"title\": \"vel non beatae est\", \"userId\": 10}, {\"completed\": true, \"id\": 189, \"title\": \"culpa eius et voluptatem et\", \"userId\": 10}, {\"completed\": true, \"id\": 190, \"title\": \"accusamus sint iusto et voluptatem exercitationem\", \"userId\": 10}, {\"completed\": true, \"id\": 191, \"title\": \"temporibus atque distinctio omnis eius impedit tempore molestias pariatur\", \"userId\": 10}, {\"completed\": false, \"id\": 192, \"title\": \"ut quas possimus exercitationem sint voluptates\", \"userId\": 10}, {\"completed\": true, \"id\": 193, \"title\": \"rerum debitis voluptatem qui eveniet tempora distinctio a\", \"userId\": 10}, {\"completed\": false, \"id\": 194, \"title\": \"sed ut vero sit molestiae\", \"userId\": 10}, {\"completed\": true, \"id\": 195, \"title\": \"rerum ex veniam mollitia voluptatibus pariatur\", \"userId\": 10}, {\"completed\": true, \"id\": 196, \"title\": \"consequuntur aut ut fugit similique\", \"userId\": 10}, {\"completed\": true, \"id\": 197, \"title\": \"dignissimos quo nobis earum saepe\", \"userId\": 10}, {\"completed\": true, \"id\": 198, \"title\": \"quis eius est sint explicabo\", \"userId\": 10}, {\"completed\": true, \"id\": 199, \"title\": \"numquam repellendus a magnam\", \"userId\": 10}, {\"completed\": false, \"id\": 200, \"title\": \"ipsam aperiam voluptates qui\", \"userId\": 10}]", "isBase64Encoded": false, - "headers": { - "Content-Type": "application/json" - }, - "cookies": [] + "multiValueHeaders": { + "Content-Type": [ + "application/json" + ] + } } diff --git a/examples/event_handler_validation/src/swagger.py b/examples/event_handler_validation/src/swagger.py index 556df9cb366..2a324b2db67 100644 --- a/examples/event_handler_validation/src/swagger.py +++ b/examples/event_handler_validation/src/swagger.py @@ -3,10 +3,10 @@ import requests from pydantic import BaseModel, EmailStr, Field -from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.utilities.typing import LambdaContext -app = APIGatewayHttpResolver(enable_validation=True) +app = APIGatewayRestResolver(enable_validation=True) app.enable_swagger() diff --git a/examples/event_handler_validation/src/swagger_customize.py b/examples/event_handler_validation/src/swagger_customize.py index 2c8dca6815a..4903ff25443 100644 --- a/examples/event_handler_validation/src/swagger_customize.py +++ b/examples/event_handler_validation/src/swagger_customize.py @@ -3,10 +3,10 @@ import requests from pydantic import BaseModel, EmailStr, Field -from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.utilities.typing import LambdaContext -app = APIGatewayHttpResolver(enable_validation=True) +app = APIGatewayRestResolver(enable_validation=True) app.enable_swagger(path="/_swagger", swagger_base_url="https://cdn.example.com/path/to/assets/") diff --git a/examples/event_handler_validation/src/swagger_middlewares.py b/examples/event_handler_validation/src/swagger_middlewares.py index 1c09ebf342e..49822fecefe 100644 --- a/examples/event_handler_validation/src/swagger_middlewares.py +++ b/examples/event_handler_validation/src/swagger_middlewares.py @@ -3,14 +3,14 @@ import requests from pydantic import BaseModel, EmailStr, Field -from aws_lambda_powertools.event_handler import APIGatewayHttpResolver, Response +from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response from aws_lambda_powertools.event_handler.middlewares import NextMiddleware from aws_lambda_powertools.utilities.typing import LambdaContext -app = APIGatewayHttpResolver(enable_validation=True) +app = APIGatewayRestResolver(enable_validation=True) -def swagger_middleware(app: APIGatewayHttpResolver, next_middleware: NextMiddleware) -> Response: +def swagger_middleware(app: APIGatewayRestResolver, next_middleware: NextMiddleware) -> Response: is_authenticated = ... if not is_authenticated: return Response(status_code=400, body="Unauthorized") diff --git a/examples/event_handler_validation/src/validating_query_strings.py b/examples/event_handler_validation/src/validating_query_strings.py index 9ed4ad8b56f..cd15e1ab683 100644 --- a/examples/event_handler_validation/src/validating_query_strings.py +++ b/examples/event_handler_validation/src/validating_query_strings.py @@ -4,14 +4,14 @@ from pydantic import BaseModel, Field from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.event_handler.openapi.params import Query from aws_lambda_powertools.logging import correlation_paths from aws_lambda_powertools.utilities.typing import LambdaContext tracer = Tracer() logger = Logger() -app = APIGatewayHttpResolver(enable_validation=True) +app = APIGatewayRestResolver(enable_validation=True) class Todo(BaseModel): From 5c55490ee2ed9bde631d655f9166bf9167ac8ac2 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 22 Nov 2023 20:25:28 +0100 Subject: [PATCH 16/36] docs: add validating query strings section Signed-off-by: heitorlessa --- docs/core/event_handler/api_gateway.md | 22 ++++++++++ .../src/validating_query_strings.py | 41 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 examples/event_handler_rest/src/validating_query_strings.py diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index f61e8dfddf1..3843d87e85f 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -333,6 +333,28 @@ What we want is for Event Handler to convert the incoming payload as an instance --8<-- "examples/event_handler_rest/src/validating_payloads_output.json" ``` +#### Validating query strings + +!!! info "We will automatically validate and inject incoming query strings via type annotation." + +With the addition of the [`Annotated` type starting in Python 3.9](https://docs.python.org/3/library/typing.html#typing.Annotated){target="_blank" rel="nofollow"}, types can contain additional metadata, allowing us to represent anything we want. + +We use the `Annotated` type to tell Event Handler that a particular parameter is not only an optional string, but also a query string with constraints. + +In the following example, we use a new `Query` OpenAPI type to add one out of many possible constraints, which should read as: + +* `completed` is a query string with a `None` as its default value +* `completed`, when set, should have at minimum 4 characters +* Doesn't match? Event Handler will return a validation error response + +```python hl_lines="1 8 26" +--8<-- "examples/event_handler_rest/src/validating_query_strings.py" +``` + +1. If you're not using Python 3.9 or higher, you can install and use [`typing_extensions`](https://pypi.org/project/typing-extensions/){target="_blank" rel="nofollow"} to the same effect +2. `Query` is a special OpenAPI type that can add constraints to a query string as well as document them +3. **First time seeing the `Annotated`?**

This special type uses the first argument as the actual type, and subsequent arguments are metadata.

At runtime, static checkers will also see the first argument, but anyone receiving them could inspect them to fetch their metadata. + ### 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/examples/event_handler_rest/src/validating_query_strings.py b/examples/event_handler_rest/src/validating_query_strings.py new file mode 100644 index 00000000000..c88e624bad8 --- /dev/null +++ b/examples/event_handler_rest/src/validating_query_strings.py @@ -0,0 +1,41 @@ +from typing import Annotated, Optional # (1)! + +import requests +from pydantic import BaseModel, Field + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler.openapi.params import Query # (2)! +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayHttpResolver(enable_validation=True) + + +class Todo(BaseModel): + userId: int + id_: Optional[int] = Field(alias="id", default=None) + title: str + completed: bool + + +@app.get("/todos") +@tracer.capture_method +def get_todos(completed: Annotated[Optional[str], Query(min_length=4)] = None) -> Todo: # (3)! + url = "https://jsonplaceholder.typicode.com/todos" + + if completed is not None: + url = f"{url}/?completed={completed}" + + todo = requests.get(url) + todo.raise_for_status() + + return Todo(**todo.json()[0]) + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) From 7e382e018c957f425dc1e80aa26c843a5705a386 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 22 Nov 2023 21:00:51 +0100 Subject: [PATCH 17/36] docs: improve examples with list type inference Signed-off-by: heitorlessa --- docs/core/event_handler/api_gateway.md | 9 ++++++--- examples/event_handler_rest/src/data_validation.py | 2 +- examples/event_handler_rest/src/validating_payloads.py | 10 +++++----- .../event_handler_rest/src/validating_query_strings.py | 6 +++--- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 3843d87e85f..a6416a97725 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -252,7 +252,7 @@ Let's rewrite the previous examples to signal our resolver what shape we expect 2. We create a Pydantic model to define how our data looks like. 3. Defining a route remains exactly as before. 4. By default, URL Paths will be `str`. Here, we are telling our resolver it should be `int`, so it converts it for us.

Lastly, we're also saying the return should be our `Todo`. This will help us later when we touch OpenAPI auto-documentation. - 5. We convert our newly fetched todo data into our `Todo` model to ensure it's valid.

Our resolver takes care of converting our validated model into a valid end-user response.

**NOTE**. You're not limited to a Pydantic model, you could also return `dict` like before. + 5. `todo.json()` returns a dictionary. However, Event Handler knows the response should be `Todo` so it converts and validates accordingly. === "data_validation.json" @@ -310,9 +310,11 @@ Let's improve our previous example by handling the creation of todo items via `H What we want is for Event Handler to convert the incoming payload as an instance of our `Todo` model. We handle the creation of that `todo`, and then return the `ID` of the newly created `todo`. +Even better, we can also let Event Handler validate and convert our response according to type annotations, further reducing boilerplate. + === "validating_payloads.py" - ```python hl_lines="13 16 24 25" + ```python hl_lines="13 16 24 33" --8<-- "examples/event_handler_rest/src/validating_payloads.py" ``` @@ -320,6 +322,7 @@ What we want is for Event Handler to convert the incoming payload as an instance 2. We create a Pydantic model to define how our data looks like. 3. We define `Todo` as our type annotation. Event Handler then uses this model to validate and inject the incoming request as `Todo`. 4. Lastly, we return the ID of our newly created `todo` item.

Because we specify the return type (`str`), Event Handler will take care of serializing this as a JSON string. + 5. Note that the return type is `List[Todo]`.

Event Handler will take the return (`todo.json`), and validate each list item against `Todo` model before returning the response accordingly. === "validating_payloads.json" @@ -347,7 +350,7 @@ In the following example, we use a new `Query` OpenAPI type to add one out of ma * `completed`, when set, should have at minimum 4 characters * Doesn't match? Event Handler will return a validation error response -```python hl_lines="1 8 26" +```python hl_lines="1 8 26" title="validating_query_strings.py" --8<-- "examples/event_handler_rest/src/validating_query_strings.py" ``` diff --git a/examples/event_handler_rest/src/data_validation.py b/examples/event_handler_rest/src/data_validation.py index 70bb0adeeff..581207e2bd8 100644 --- a/examples/event_handler_rest/src/data_validation.py +++ b/examples/event_handler_rest/src/data_validation.py @@ -26,7 +26,7 @@ def get_todo_by_id(todo_id: int) -> Todo: # (4)! todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") todo.raise_for_status() - return Todo(**todo.json()) # (5)! + return todo.json() # (5)! @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) diff --git a/examples/event_handler_rest/src/validating_payloads.py b/examples/event_handler_rest/src/validating_payloads.py index 60fc9f03bf8..8a91e59e7f4 100644 --- a/examples/event_handler_rest/src/validating_payloads.py +++ b/examples/event_handler_rest/src/validating_payloads.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import List, Optional import requests from pydantic import BaseModel, Field @@ -28,13 +28,13 @@ def create_todo(todo: Todo) -> str: # (3)! return response.json()["id"] # (4)! -@app.get("/todos/") +@app.get("/todos") @tracer.capture_method -def get_todo_by_id(todo_id: int) -> Todo: - todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") +def get_todos() -> List[Todo]: + todo = requests.get("https://jsonplaceholder.typicode.com/todos") todo.raise_for_status() - return Todo(**todo.json()) + return todo.json() # (5)! @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) diff --git a/examples/event_handler_rest/src/validating_query_strings.py b/examples/event_handler_rest/src/validating_query_strings.py index c88e624bad8..eaa30626bc4 100644 --- a/examples/event_handler_rest/src/validating_query_strings.py +++ b/examples/event_handler_rest/src/validating_query_strings.py @@ -1,4 +1,4 @@ -from typing import Annotated, Optional # (1)! +from typing import Annotated, List, Optional # (1)! import requests from pydantic import BaseModel, Field @@ -23,7 +23,7 @@ class Todo(BaseModel): @app.get("/todos") @tracer.capture_method -def get_todos(completed: Annotated[Optional[str], Query(min_length=4)] = None) -> Todo: # (3)! +def get_todos(completed: Annotated[Optional[str], Query(min_length=4)] = None) -> List[Todo]: # (3)! url = "https://jsonplaceholder.typicode.com/todos" if completed is not None: @@ -32,7 +32,7 @@ def get_todos(completed: Annotated[Optional[str], Query(min_length=4)] = None) - todo = requests.get(url) todo.raise_for_status() - return Todo(**todo.json()[0]) + return todo.json() @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) From 0ccfc2cb69ea42a09e115eb64af869a0669c204a Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 22 Nov 2023 21:19:47 +0100 Subject: [PATCH 18/36] docs: add path validation section --- docs/core/event_handler/api_gateway.md | 12 +++++++ .../event_handler_rest/src/validating_path.py | 36 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 examples/event_handler_rest/src/validating_path.py diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index a6416a97725..0fbc8fe501f 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -358,6 +358,18 @@ In the following example, we use a new `Query` OpenAPI type to add one out of ma 2. `Query` is a special OpenAPI type that can add constraints to a query string as well as document them 3. **First time seeing the `Annotated`?**

This special type uses the first argument as the actual type, and subsequent arguments are metadata.

At runtime, static checkers will also see the first argument, but anyone receiving them could inspect them to fetch their metadata. +#### Validating path parameters + +Just like we learned in [query string validation](#validating-query-strings), we can use a new `Path` OpenAPI type to add constraints. + +For example, we could validate that `` dynamic path should be no greater than three digits. + +```python title="validating_path.py" +--8<-- "examples/event_handler_rest/src/validating_path.py" +``` + +1. `Path` is a special OpenAPI type that allows us to constrain todo_id to be less than 999. + ### 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/examples/event_handler_rest/src/validating_path.py b/examples/event_handler_rest/src/validating_path.py new file mode 100644 index 00000000000..4b62380c13e --- /dev/null +++ b/examples/event_handler_rest/src/validating_path.py @@ -0,0 +1,36 @@ +from typing import Annotated, Optional + +import requests +from pydantic import BaseModel, Field + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler.openapi.params import Path +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayHttpResolver(enable_validation=True) + + +class Todo(BaseModel): + userId: int + id_: Optional[int] = Field(alias="id", default=None) + title: str + completed: bool + + +@app.get("/todos/") +@tracer.capture_method +def get_todo_by_id(todo_id: Annotated[int, Path(lt=999)]) -> Todo: # (1)! + todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") + todo.raise_for_status() + + return todo.json() + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) From b6117009b93e1cb443e4f4d81a876e843e4e5b4d Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 22 Nov 2023 21:21:06 +0100 Subject: [PATCH 19/36] docs: add missing highlight for path validation --- 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 0fbc8fe501f..537bae1a79a 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -364,7 +364,7 @@ Just like we learned in [query string validation](#validating-query-strings), we For example, we could validate that `` dynamic path should be no greater than three digits. -```python title="validating_path.py" +```python hl_lines="1 8 26" title="validating_path.py" --8<-- "examples/event_handler_rest/src/validating_path.py" ``` From bb404fcd8a1bebd83534cf4a5add032cc646a355 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 22 Nov 2023 21:32:33 +0100 Subject: [PATCH 20/36] docs: document how to inject query strings w/o validation Signed-off-by: heitorlessa --- docs/core/event_handler/api_gateway.md | 30 +++++++++++--- .../src/skip_validating_query_strings.py | 40 +++++++++++++++++++ 2 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 examples/event_handler_rest/src/skip_validating_query_strings.py diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 537bae1a79a..557a6317387 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -350,13 +350,31 @@ In the following example, we use a new `Query` OpenAPI type to add one out of ma * `completed`, when set, should have at minimum 4 characters * Doesn't match? Event Handler will return a validation error response -```python hl_lines="1 8 26" title="validating_query_strings.py" ---8<-- "examples/event_handler_rest/src/validating_query_strings.py" -``` + + +=== "validating_query_strings.py" + + ```python hl_lines="1 8 26" + --8<-- "examples/event_handler_rest/src/validating_query_strings.py" + ``` + + 1. If you're not using Python 3.9 or higher, you can install and use [`typing_extensions`](https://pypi.org/project/typing-extensions/){target="_blank" rel="nofollow"} to the same effect + 2. `Query` is a special OpenAPI type that can add constraints to a query string as well as document them + 3. **First time seeing the `Annotated`?**

This special type uses the first argument as the actual type, and subsequent arguments are metadata.

At runtime, static checkers will also see the first argument, but anyone receiving them could inspect them to fetch their metadata. + +=== "skip_validating_query_strings.py" -1. If you're not using Python 3.9 or higher, you can install and use [`typing_extensions`](https://pypi.org/project/typing-extensions/){target="_blank" rel="nofollow"} to the same effect -2. `Query` is a special OpenAPI type that can add constraints to a query string as well as document them -3. **First time seeing the `Annotated`?**

This special type uses the first argument as the actual type, and subsequent arguments are metadata.

At runtime, static checkers will also see the first argument, but anyone receiving them could inspect them to fetch their metadata. + If you don't want to validate query strings but simply let Event Handler inject them as parameters, you can omit `Query` type annotation. + + This is merely for your convenience. + + ```python hl_lines="1 8 25" + --8<-- "examples/event_handler_rest/src/skip_validating_query_strings.py" + ``` + + 1. `completed` is still the same query string as before, except we simply state it's an string. No `Query` or `Annotated` to validate it. + + #### Validating path parameters diff --git a/examples/event_handler_rest/src/skip_validating_query_strings.py b/examples/event_handler_rest/src/skip_validating_query_strings.py new file mode 100644 index 00000000000..47e2bd10229 --- /dev/null +++ b/examples/event_handler_rest/src/skip_validating_query_strings.py @@ -0,0 +1,40 @@ +from typing import List, Optional + +import requests +from pydantic import BaseModel, Field + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = APIGatewayHttpResolver(enable_validation=True) + + +class Todo(BaseModel): + userId: int + id_: Optional[int] = Field(alias="id", default=None) + title: str + completed: bool + + +@app.get("/todos") +@tracer.capture_method +def get_todos(completed: Optional[str] = None) -> List[Todo]: # (1)! + url = "https://jsonplaceholder.typicode.com/todos" + + if completed is not None: + url = f"{url}/?completed={completed}" + + todo = requests.get(url) + todo.raise_for_status() + + return todo.json() + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) From 2caa5b18d737020849a99fac6bd32df538041d4e Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Wed, 22 Nov 2023 21:53:04 +0100 Subject: [PATCH 21/36] chore: add query strings to Param, Path and Query --- .../event_handler/openapi/params.py | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/aws_lambda_powertools/event_handler/openapi/params.py b/aws_lambda_powertools/event_handler/openapi/params.py index c8099d20404..7b8e2343000 100644 --- a/aws_lambda_powertools/event_handler/openapi/params.py +++ b/aws_lambda_powertools/event_handler/openapi/params.py @@ -115,6 +115,64 @@ def __init__( json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): + """ + Constructs a new Param. + + Parameters + ---------- + default: Any + The default value of the parameter + default_factory: Callable[[], Any], optional + Callable that will be called when a default value is needed for this field + annotation: Any, optional + The type annotation of the parameter + alias: str, optional + The public name of the field + alias_priority: int, optional + Priority of the alias. This affects whether an alias generator is used + validation_alias: str | AliasPath | AliasChoices | None, optional + Alias to be used for validation only + serialization_alias: str | AliasPath | AliasChoices | None, optional + Alias to be used for serialization only + title: str, optional + The title of the parameter + description: str, optional + The description of the parameter + gt: float, optional + Only applies to numbers, required the field to be "greater than" + ge: float, optional + Only applies to numbers, required the field to be "greater than or equal" + lt: float, optional + Only applies to numbers, required the field to be "less than" + le: float, optional + Only applies to numbers, required the field to be "less than or equal" + min_length: int, optional + Only applies to strings, required the field to have a minimum length + max_length: int, optional + Only applies to strings, required the field to have a maximum length + pattern: str, optional + Only applies to strings, requires the field match against a regular expression pattern string + discriminator: str, optional + Parameter field name for discriminating the type in a tagged union + strict: bool, optional + Enables Pydantic's strict mode for the field + multiple_of: float, optional + Only applies to numbers, requires the field to be a multiple of the given value + allow_inf_nan: bool, optional + Only applies to numbers, requires the field to allow infinity and NaN values + max_digits: int, optional + Only applies to Decimals, requires the field to have a maxmium number of digits within the decimal. + decimal_places: int, optional + Only applies to Decimals, requires the field to have at most a number of decimal places + examples: List[Any], optional + A list of examples for the parameter + deprecated: bool, optional + If `True`, the parameter will be marked as deprecated + include_in_schema: bool, optional + If `False`, the parameter will be excluded from the generated OpenAPI schema + json_schema_extra: Dict[str, Any], optional + Extra values to include in the generated OpenAPI schema + """ self.deprecated = deprecated self.include_in_schema = include_in_schema @@ -205,6 +263,64 @@ def __init__( json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): + """ + Constructs a new Path param. + + Parameters + ---------- + default: Any + The default value of the parameter + default_factory: Callable[[], Any], optional + Callable that will be called when a default value is needed for this field + annotation: Any, optional + The type annotation of the parameter + alias: str, optional + The public name of the field + alias_priority: int, optional + Priority of the alias. This affects whether an alias generator is used + validation_alias: str | AliasPath | AliasChoices | None, optional + Alias to be used for validation only + serialization_alias: str | AliasPath | AliasChoices | None, optional + Alias to be used for serialization only + title: str, optional + The title of the parameter + description: str, optional + The description of the parameter + gt: float, optional + Only applies to numbers, required the field to be "greater than" + ge: float, optional + Only applies to numbers, required the field to be "greater than or equal" + lt: float, optional + Only applies to numbers, required the field to be "less than" + le: float, optional + Only applies to numbers, required the field to be "less than or equal" + min_length: int, optional + Only applies to strings, required the field to have a minimum length + max_length: int, optional + Only applies to strings, required the field to have a maximum length + pattern: str, optional + Only applies to strings, requires the field match against a regular expression pattern string + discriminator: str, optional + Parameter field name for discriminating the type in a tagged union + strict: bool, optional + Enables Pydantic's strict mode for the field + multiple_of: float, optional + Only applies to numbers, requires the field to be a multiple of the given value + allow_inf_nan: bool, optional + Only applies to numbers, requires the field to allow infinity and NaN values + max_digits: int, optional + Only applies to Decimals, requires the field to have a maxmium number of digits within the decimal. + decimal_places: int, optional + Only applies to Decimals, requires the field to have at most a number of decimal places + examples: List[Any], optional + A list of examples for the parameter + deprecated: bool, optional + If `True`, the parameter will be marked as deprecated + include_in_schema: bool, optional + If `False`, the parameter will be excluded from the generated OpenAPI schema + json_schema_extra: Dict[str, Any], optional + Extra values to include in the generated OpenAPI schema + """ if default is not ...: raise AssertionError("Path parameters cannot have a default value") @@ -277,6 +393,64 @@ def __init__( json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): + """ + Constructs a new Query param. + + Parameters + ---------- + default: Any + The default value of the parameter + default_factory: Callable[[], Any], optional + Callable that will be called when a default value is needed for this field + annotation: Any, optional + The type annotation of the parameter + alias: str, optional + The public name of the field + alias_priority: int, optional + Priority of the alias. This affects whether an alias generator is used + validation_alias: str | AliasPath | AliasChoices | None, optional + Alias to be used for validation only + serialization_alias: str | AliasPath | AliasChoices | None, optional + Alias to be used for serialization only + title: str, optional + The title of the parameter + description: str, optional + The description of the parameter + gt: float, optional + Only applies to numbers, required the field to be "greater than" + ge: float, optional + Only applies to numbers, required the field to be "greater than or equal" + lt: float, optional + Only applies to numbers, required the field to be "less than" + le: float, optional + Only applies to numbers, required the field to be "less than or equal" + min_length: int, optional + Only applies to strings, required the field to have a minimum length + max_length: int, optional + Only applies to strings, required the field to have a maximum length + pattern: str, optional + Only applies to strings, requires the field match against a regular expression pattern string + discriminator: str, optional + Parameter field name for discriminating the type in a tagged union + strict: bool, optional + Enables Pydantic's strict mode for the field + multiple_of: float, optional + Only applies to numbers, requires the field to be a multiple of the given value + allow_inf_nan: bool, optional + Only applies to numbers, requires the field to allow infinity and NaN values + max_digits: int, optional + Only applies to Decimals, requires the field to have a maxmium number of digits within the decimal. + decimal_places: int, optional + Only applies to Decimals, requires the field to have at most a number of decimal places + examples: List[Any], optional + A list of examples for the parameter + deprecated: bool, optional + If `True`, the parameter will be marked as deprecated + include_in_schema: bool, optional + If `False`, the parameter will be excluded from the generated OpenAPI schema + json_schema_extra: Dict[str, Any], optional + Extra values to include in the generated OpenAPI schema + """ super().__init__( default=default, default_factory=default_factory, From e5bbc0a0a290956867f0b263fbd8fcb1aa9a6b5d Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 22 Nov 2023 22:27:25 +0100 Subject: [PATCH 22/36] docs: add OpenAPI and Swagger UI advanced Signed-off-by: heitorlessa --- docs/core/event_handler/api_gateway.md | 96 +++++++++++++++++++ .../src/customizing_api_metadata.py | 33 +++++++ .../src/customizing_api_operations.py | 30 ++++++ .../src/customizing_swagger.py | 29 ++++++ .../src/customizing_swagger_middlewares.py | 40 ++++++++ .../src/enabling_swagger.py | 29 ++++++ 6 files changed, 257 insertions(+) create mode 100644 examples/event_handler_rest/src/customizing_api_metadata.py create mode 100644 examples/event_handler_rest/src/customizing_api_operations.py create mode 100644 examples/event_handler_rest/src/customizing_swagger.py create mode 100644 examples/event_handler_rest/src/customizing_swagger_middlewares.py create mode 100644 examples/event_handler_rest/src/enabling_swagger.py diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 557a6317387..5306301552a 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -446,6 +446,30 @@ We provide pre-defined errors for the most popular ones such as HTTP 400, 401, 4 --8<-- "examples/event_handler_rest/src/raising_http_errors.py" ``` +### Enabling SwaggerUI + +!!! note "This feature requires [data validation](#data-validation) feature to be enabled." + +Behind the scenes, the [data validation](#data-validation) feature auto-generates an OpenAPI specification from your routes and type annotations. You can use [Swagger UI](https://swagger.io/tools/swagger-ui/){target="_blank" rel="nofollow"} to visualize and interact with your newly auto-documented API. + +There are some important **caveats** that you should know before enabling it: + +| Caveat | Description | +| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Swagger UI is **publicly accessible by default** | When using `enable_swagger` method, you can [protect sensitive API endpoints by implementing a custom middleware](#customizing-swagger-ui) using your preferred authorization mechanism. | +| **No micro-functions support** yet | Swagger UI is enabled on a per resolver instance which will limit its accuracy here. | +| You need to expose **new routes** | You'll need to expose the following paths to Lambda: `/swagger`, `/swagger.css`, `/swagger.js`; ignore if you're routing all paths already. | + +```python hl_lines="9 10" title="enabling_swagger.py" +--8<-- "examples/event_handler_rest/src/enabling_swagger.py" +``` + +1. `enable_swagger` creates a route to serve Swagger UI and allows quick customizations.

You can also include middlewares to protect or enhance the overall experience. + +Here's an example of what it looks like by default: + +![Swagger UI picture](../../../media/swagger.png) + ### Custom Domain API Mappings When using [Custom Domain API Mappings feature](https://docs.aws.amazon.com/apigateway/latest/developerguide/rest-api-mappings.html){target="_blank"}, you must use **`strip_prefixes`** param in the `APIGatewayRestResolver` constructor. @@ -863,6 +887,78 @@ This will enable full tracebacks errors in the response, print request and respo --8<-- "examples/event_handler_rest/src/debug_mode.py" ``` +### OpenAPI + +#### Customizing API operations + +Customize your API endpoints by adding metadata to endpoint definitions. This provides descriptive documentation for API consumers and gives extra instructions to the framework. + +Here's a breakdown of various customizable fields: + +| Field Name | Type | Description | +| ---------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `summary` | `str` | A concise overview of the main functionality of the endpoint. This brief introduction is usually displayed in autogenerated API documentation and helps consumers quickly understand what the endpoint does. | +| `description` | `str` | A more detailed explanation of the endpoint, which can include information about the operation's behavior, including side effects, error states, and other operational guidelines. | +| `responses` | `Dict[int, Dict[str, Any]]` | A dictionary that maps each HTTP status code to a Response Object as defined by the [OpenAPI Specification](https://swagger.io/specification/#response-object). This allows you to describe expected responses, including default or error messages, and their corresponding schemas for different status codes. | +| `response_description` | `str` | Provides the default textual description of the response sent by the endpoint when the operation is successful. It is intended to give a human-readable understanding of the result. | +| `tags` | `List[str]` | Tags are a way to categorize and group endpoints within the API documentation. They can help organize the operations by resources or other heuristic. | +| `operation_id` | `str` | A unique identifier for the operation, which can be used for referencing this operation in documentation or code. This ID must be unique across all operations described in the API. | +| `include_in_schema` | `bool` | A boolean value that determines whether or not this operation should be included in the OpenAPI schema. Setting it to `False` can hide the endpoint from generated documentation and schema exports, which might be useful for private or experimental endpoints. | + +To implement these customizations, include extra parameters when defining your routes: + +```python hl_lines="11-20" title="customizing_api_operations.py" +--8<-- "examples/event_handler_rest/src/customizing_api_operations.py" +``` + +#### Customizing Swagger UI + +???+note "Customizing the Swagger metadata" + The `enable_swagger` method accepts the same metadata as described at [Customizing OpenAPI metadata](#customizing-openapi-metadata). + +The Swagger UI appears by default at the `/swagger` path, but you can customize this to serve the documentation from another path and specify the source for Swagger UI assets. + +Below is an example configuration for serving Swagger UI from a custom path or CDN, with assets like CSS and JavaScript loading from a chosen CDN base URL. + +=== "customizing_swagger.py" + + ```python hl_lines="10" + --8<-- "examples/event_handler_rest/src/customizing_swagger.py" + ``` + +=== "customizing_swagger_middlewares.py" + + A Middleware can handle tasks such as adding security headers, user authentication, or other request processing for serving the Swagger UI. + + ```python hl_lines="7 13-18 21" + --8<-- "examples/event_handler_rest/src/customizing_swagger_middlewares.py" + ``` + +#### Customizing OpenAPI metadata + +Defining and customizing OpenAPI metadata gives detailed, top-level information about your API. Here's the method to set and tailor this metadata: + +| Field Name | Type | Description | +| ------------------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `title` | `str` | The title for your API. It should be a concise, specific name that can be used to identify the API in documentation or listings. | +| `version` | `str` | The version of the API you are documenting. This could reflect the release iteration of the API and helps clients understand the evolution of the API. | +| `openapi_version` | `str` | Specifies the version of the OpenAPI Specification on which your API is based. For most contemporary APIs, the default value would be `3.0.0` or higher. | +| `summary` | `str` | A short and informative summary that can provide an overview of what the API does. This can be the same as or different from the title but should add context or information. | +| `description` | `str` | A verbose description that can include Markdown formatting, providing a full explanation of the API's purpose, functionalities, and general usage instructions. | +| `tags` | `List[str]` | A collection of tags that categorize endpoints for better organization and navigation within the documentation. This can group endpoints by their functionality or other criteria. | +| `servers` | `List[Server]` | An array of Server objects, which specify the URL to the server and a description for its environment (production, staging, development, etc.), providing connectivity information. | +| `terms_of_service` | `str` | A URL that points to the terms of service for your API. This could provide legal information and user responsibilities related to the usage of the API. | +| `contact` | `Contact` | A Contact object containing contact details of the organization or individuals maintaining the API. This may include fields such as name, URL, and email. | +| `license_info` | `License` | A License object providing the license details for the API, typically including the name of the license and the URL to the full license text. | + +Include extra parameters when exporting your OpenAPI specification to apply these customizations: + +=== "customizing_api_metadata.py" + + ```python hl_lines="25-31" + --8<-- "examples/event_handler_rest/src/customizing_api_metadata.py" + ``` + ### Custom serializer You can instruct event handler to use a custom serializer to best suit your needs, for example take into account Enums when serializing. diff --git a/examples/event_handler_rest/src/customizing_api_metadata.py b/examples/event_handler_rest/src/customizing_api_metadata.py new file mode 100644 index 00000000000..cd9ced455d2 --- /dev/null +++ b/examples/event_handler_rest/src/customizing_api_metadata.py @@ -0,0 +1,33 @@ +import requests + +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.event_handler.openapi.models import Contact, Server +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayRestResolver(enable_validation=True) + + +@app.get("/todos/") +def get_todo_title(todo_id: int) -> str: + todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") + todo.raise_for_status() + + return todo.json()["title"] + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) + + +if __name__ == "__main__": + print( + app.get_openapi_json_schema( + title="TODO's API", + version="1.21.3", + summary="API to manage TODOs", + description="This API implements all the CRUD operations for the TODO app", + tags=["todos"], + servers=[Server(url="https://stg.example.org/orders", description="Staging server")], + contact=Contact(name="John Smith", email="john@smith.com"), + ), + ) diff --git a/examples/event_handler_rest/src/customizing_api_operations.py b/examples/event_handler_rest/src/customizing_api_operations.py new file mode 100644 index 00000000000..e455fc7dadd --- /dev/null +++ b/examples/event_handler_rest/src/customizing_api_operations.py @@ -0,0 +1,30 @@ +import requests + +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayRestResolver(enable_validation=True) + + +@app.get( + "/todos/", + summary="Retrieves a todo item", + description="Loads a todo item identified by the `todo_id`", + response_description="The todo object", + responses={ + 200: {"description": "Todo item found"}, + 404: { + "description": "Item not found", + }, + }, + tags=["Todos"], +) +def get_todo_title(todo_id: int) -> str: + todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") + todo.raise_for_status() + + return todo.json()["title"] + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/customizing_swagger.py b/examples/event_handler_rest/src/customizing_swagger.py new file mode 100644 index 00000000000..4903ff25443 --- /dev/null +++ b/examples/event_handler_rest/src/customizing_swagger.py @@ -0,0 +1,29 @@ +from typing import List + +import requests +from pydantic import BaseModel, EmailStr, Field + +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayRestResolver(enable_validation=True) +app.enable_swagger(path="/_swagger", swagger_base_url="https://cdn.example.com/path/to/assets/") + + +class Todo(BaseModel): + userId: int + id_: int = Field(alias="id") + title: str + completed: bool + + +@app.get("/todos") +def get_todos_by_email(email: EmailStr) -> List[Todo]: + todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}") + todos.raise_for_status() + + return todos.json() + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/customizing_swagger_middlewares.py b/examples/event_handler_rest/src/customizing_swagger_middlewares.py new file mode 100644 index 00000000000..49822fecefe --- /dev/null +++ b/examples/event_handler_rest/src/customizing_swagger_middlewares.py @@ -0,0 +1,40 @@ +from typing import List + +import requests +from pydantic import BaseModel, EmailStr, Field + +from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response +from aws_lambda_powertools.event_handler.middlewares import NextMiddleware +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayRestResolver(enable_validation=True) + + +def swagger_middleware(app: APIGatewayRestResolver, next_middleware: NextMiddleware) -> Response: + is_authenticated = ... + if not is_authenticated: + return Response(status_code=400, body="Unauthorized") + + return next_middleware(app) + + +app.enable_swagger(middlewares=[swagger_middleware]) + + +class Todo(BaseModel): + userId: int + id_: int = Field(alias="id") + title: str + completed: bool + + +@app.get("/todos") +def get_todos_by_email(email: EmailStr) -> List[Todo]: + todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}") + todos.raise_for_status() + + return todos.json() + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/enabling_swagger.py b/examples/event_handler_rest/src/enabling_swagger.py new file mode 100644 index 00000000000..c91bac04f55 --- /dev/null +++ b/examples/event_handler_rest/src/enabling_swagger.py @@ -0,0 +1,29 @@ +from typing import List + +import requests +from pydantic import BaseModel, EmailStr, Field + +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayRestResolver(enable_validation=True) +app.enable_swagger(path="/swagger") # (1)! + + +class Todo(BaseModel): + userId: int + id_: int = Field(alias="id") + title: str + completed: bool + + +@app.get("/todos") +def get_todos_by_email(email: EmailStr) -> List[Todo]: + todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}") + todos.raise_for_status() + + return todos.json() + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) From f052551d32bcc9edc2614f016314608dcc854015 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Thu, 23 Nov 2023 08:44:10 +0100 Subject: [PATCH 23/36] docs: add openapi parameters section Signed-off-by: heitorlessa --- docs/core/event_handler/api_gateway.md | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 5306301552a..b7fbdfcdd8c 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -889,6 +889,40 @@ This will enable full tracebacks errors in the response, print request and respo ### OpenAPI +When you enable [Data Validation](#data-validation), we use a combination of Pydantic Models and [OpenAPI](https://www.openapis.org/){target="_blank"} type annotations to add constraints to your API's parameters. + +In OpenAPI documentation tools like [SwaggerUI](#enabling-swaggerui), these annotations become readable descriptions, offering a self-explanatory API interface. This reduces boilerplate code while improving functionality and enabling auto-documentation. + +???+ note + We don't have support for files, form data, and header parameters at the moment. If you're interested in this, please [open an issue](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=feature-request%2Ctriage&projects=&template=feature_request.yml&title=Feature+request%3A+TITLE). + +#### Customizing OpenAPI parameters + +Whenever you use OpenAPI parameters to validate [query strings](#validating-query-strings) or [path parameters](#validating-path-parameters), you can enhance validation and OpenAPI documentation by using any of these parameters: + +| Field name | Type | Description | +| --------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `alias` | `str` | Alternative name for a field, used when serializing and deserializing data | +| `validation_alias` | `str` | Alternative name for a field during validation (but not serialization) | +| `serialization_alias` | `str` | Alternative name for a field during serialization (but not during validation) | +| `description` | `str` | Human-readable description | +| `gt` | `float` | Greater than. If set, value must be greater than this. Only applicable to numbers | +| `ge` | `float` | Greater than or equal. If set, value must be greater than or equal to this. Only applicable to numbers | +| `lt` | `float` | Less than. If set, value must be less than this. Only applicable to numbers | +| `le` | `float` | Less than or equal. If set, value must be less than or equal to this. Only applicable to numbers | +| `min_length` | `int` | Minimum length for strings | +| `max_length` | `int` | Maximum length for strings | +| `pattern` | `string` | A regular expression that the string must match. | +| `strict` | `bool` | If `True`, strict validation is applied to the field. See [Strict Mode](https://docs.pydantic.dev/latest/concepts/strict_mode/){target"_blank" rel="nofollow"} for details | +| `multiple_of` | `float` | Value must be a multiple of this. Only applicable to numbers | +| `allow_inf_nan` | `bool` | Allow `inf`, `-inf`, `nan`. Only applicable to numbers | +| `max_digits` | `int` | Maximum number of allow digits for strings | +| `decimal_places` | `int` | Maximum number of decimal places allowed for numbers | +| `examples` | `List\[Any\]` | List of examples of the field | +| `deprecated` | `bool` | Marks the field as deprecated | +| `include_in_schema` | `bool` | If `False` the field will not be part of the exported OpenAPI schema | +| `json_schema_extra` | `JsonDict` | Any additional JSON schema data for the schema property | + #### Customizing API operations Customize your API endpoints by adding metadata to endpoint definitions. This provides descriptive documentation for API consumers and gives extra instructions to the framework. From d9ae183fdb4617db57aee56710ad9547c9746cbb Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Thu, 23 Nov 2023 08:49:22 +0100 Subject: [PATCH 24/36] docs: link data validation to openapi params section Signed-off-by: heitorlessa --- docs/core/event_handler/api_gateway.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index b7fbdfcdd8c..56f1e09b2d1 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -232,7 +232,7 @@ If you need to accept multiple HTTP methods in a single function, you can use th ### Data validation !!! note "This changes the authoring experience by relying on Python's type annotations" - It's inspired by [FastAPI framework](https://fastapi.tiangolo.com/){target="_blank" rel="nofollow"} to ease migrations in either direction. + It's inspired by [FastAPI framework](https://fastapi.tiangolo.com/){target="_blank" rel="nofollow"} for ergonomics and to ease migrations in either direction. All resolvers can optionally coerce and validate incoming requests by setting `enable_validation=True`. @@ -344,7 +344,7 @@ With the addition of the [`Annotated` type starting in Python 3.9](https://docs. We use the `Annotated` type to tell Event Handler that a particular parameter is not only an optional string, but also a query string with constraints. -In the following example, we use a new `Query` OpenAPI type to add one out of many possible constraints, which should read as: +In the following example, we use a new `Query` OpenAPI type to add [one out of many possible constraints](#customizing-openapi-parameters), which should read as: * `completed` is a query string with a `None` as its default value * `completed`, when set, should have at minimum 4 characters @@ -378,7 +378,7 @@ In the following example, we use a new `Query` OpenAPI type to add one out of ma #### Validating path parameters -Just like we learned in [query string validation](#validating-query-strings), we can use a new `Path` OpenAPI type to add constraints. +Just like we learned in [query string validation](#validating-query-strings), we can use a new `Path` OpenAPI type to [add constraints](#customizing-openapi-parameters). For example, we could validate that `` dynamic path should be no greater than three digits. From 3b8ffabb7b24eadf7256d09517da635de58f06c7 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Thu, 23 Nov 2023 10:01:22 +0100 Subject: [PATCH 25/36] docs: add payload subset subsection Signed-off-by: heitorlessa --- docs/core/event_handler/api_gateway.md | 31 ++++++++++++++-- .../src/validating_payload_subset.json | 36 +++++++++++++++++++ .../src/validating_payload_subset.py | 29 +++++++++++++++ .../src/validating_payload_subset_output.json | 10 ++++++ 4 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 examples/event_handler_rest/src/validating_payload_subset.json create mode 100644 examples/event_handler_rest/src/validating_payload_subset.py create mode 100644 examples/event_handler_rest/src/validating_payload_subset_output.json diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 56f1e09b2d1..76005e42de3 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -336,12 +336,39 @@ Even better, we can also let Event Handler validate and convert our response acc --8<-- "examples/event_handler_rest/src/validating_payloads_output.json" ``` +##### Validating payload subset + +With the addition of the [`Annotated` type starting in Python 3.9](https://docs.python.org/3/library/typing.html#typing.Annotated){target="_blank" rel="nofollow"}, types can contain additional metadata, allowing us to represent anything we want. + +We use the `Annotated` and OpenAPI `Body` type to instruct Event Handler that our payload is located in a particular JSON key. + +!!! note "Event Handler will match the parameter name with the JSON key to validate and inject what you want." + +=== "validating_payload_subset.py" + + ```python hl_lines="1 7 21" + --8<-- "examples/event_handler_rest/src/validating_payload_subset.py" + ``` + + 1. `Body` is a special OpenAPI type that can add additional constraints to a request payload. + 2. `Body(embed=True)` instructs Event Handler to look up inside the payload for a key.

This means Event Handler will look up for a key named `todo`, validate the value against `Todo`, and inject it. + +=== "validating_payload_subset.json" + + ```json hl_lines="3-4 6" + --8<-- "examples/event_handler_rest/src/validating_payload_subset.json" + ``` + +=== "validating_payload_subset_output.json" + + ```json hl_lines="3" + --8<-- "examples/event_handler_rest/src/validating_payload_subset_output.json" + ``` + #### Validating query strings !!! info "We will automatically validate and inject incoming query strings via type annotation." -With the addition of the [`Annotated` type starting in Python 3.9](https://docs.python.org/3/library/typing.html#typing.Annotated){target="_blank" rel="nofollow"}, types can contain additional metadata, allowing us to represent anything we want. - We use the `Annotated` type to tell Event Handler that a particular parameter is not only an optional string, but also a query string with constraints. In the following example, we use a new `Query` OpenAPI type to add [one out of many possible constraints](#customizing-openapi-parameters), which should read as: diff --git a/examples/event_handler_rest/src/validating_payload_subset.json b/examples/event_handler_rest/src/validating_payload_subset.json new file mode 100644 index 00000000000..b786e9287b8 --- /dev/null +++ b/examples/event_handler_rest/src/validating_payload_subset.json @@ -0,0 +1,36 @@ +{ + "version": "1.0", + "body": "{ \"todo\": {\"title\": \"foo\", \"userId\": \"1\", \"completed\": false } }", + "resource": "/todos", + "path": "/todos", + "httpMethod": "POST", + "headers": { + "Origin": "https://aws.amazon.com" + }, + "multiValueHeaders": {}, + "queryStringParameters": {}, + "multiValueQueryStringParameters": {}, + "requestContext": { + "accountId": "123456789012", + "apiId": "id", + "authorizer": { + "claims": null, + "scopes": null + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "extendedRequestId": "request-id", + "httpMethod": "POST", + "path": "/todos", + "protocol": "HTTP/1.1", + "requestId": "id=", + "requestTime": "04/Mar/2020:19:15:17 +0000", + "requestTimeEpoch": 1583349317135, + "resourceId": null, + "resourcePath": "/todos", + "stage": "$default" + }, + "pathParameters": null, + "stageVariables": null, + "isBase64Encoded": false +} \ No newline at end of file diff --git a/examples/event_handler_rest/src/validating_payload_subset.py b/examples/event_handler_rest/src/validating_payload_subset.py new file mode 100644 index 00000000000..f47a68ea1f3 --- /dev/null +++ b/examples/event_handler_rest/src/validating_payload_subset.py @@ -0,0 +1,29 @@ +from typing import Annotated, Optional + +import requests +from pydantic import BaseModel, Field + +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.event_handler.openapi.params import Body # (1)! +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayRestResolver(enable_validation=True) + + +class Todo(BaseModel): + userId: int + id_: Optional[int] = Field(alias="id", default=None) + title: str + completed: bool + + +@app.post("/todos") +def create_todo(todo: Annotated[Todo, Body(embed=True)]) -> int: # (2)! + response = requests.post("https://jsonplaceholder.typicode.com/todos", json=todo.dict(by_alias=True)) + response.raise_for_status() + + return response.json()["id"] + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/validating_payload_subset_output.json b/examples/event_handler_rest/src/validating_payload_subset_output.json new file mode 100644 index 00000000000..754e3a6c128 --- /dev/null +++ b/examples/event_handler_rest/src/validating_payload_subset_output.json @@ -0,0 +1,10 @@ +{ + "statusCode": 200, + "body": "2008822", + "isBase64Encoded": false, + "multiValueHeaders": { + "Content-Type": [ + "application/json" + ] + } +} From c799f0eb10f3c243f727dfadd20ce3a99d66ff5c Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Thu, 23 Nov 2023 10:43:51 +0100 Subject: [PATCH 26/36] docs: update code snippets --- docs/core/event_handler/api_gateway.md | 20 +++--- .../src/data_validation.json | 62 +++++++++---------- .../event_handler_rest/src/data_validation.py | 4 +- .../src/data_validation_output.json | 15 ++--- ....py => data_validation_sanitized_error.py} | 0 ...ta_validation_sanitized_error_output.json} | 0 .../src/skip_validating_query_strings.py | 4 +- .../event_handler_rest/src/validating_path.py | 4 +- .../src/validating_payloads.json | 58 ++++++++--------- .../src/validating_payloads.py | 4 +- .../src/validating_payloads_output.json | 11 ++-- .../src/validating_query_strings.py | 4 +- 12 files changed, 88 insertions(+), 98 deletions(-) rename examples/event_handler_rest/src/{data_validation_error.py => data_validation_sanitized_error.py} (100%) rename examples/event_handler_rest/src/{data_validation_error_output.json => data_validation_sanitized_error_output.json} (100%) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 76005e42de3..9d8719ca3e5 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -262,7 +262,7 @@ Let's rewrite the previous examples to signal our resolver what shape we expect === "data_validation_output.json" - ```json hl_lines="2 8" + ```json hl_lines="2-3" --8<-- "examples/event_handler_rest/src/data_validation_output.json" ``` @@ -276,30 +276,28 @@ Let's rewrite the previous examples to signal our resolver what shape we expect Any **incoming request that fails validation** will lead to a `HTTP 422: Unprocessable Entity error` response. When they occur, by default they will look similar to this: ```json hl_lines="2 3" title="data_validation_error_unsanitized_output.json" ---8<-- "examples/event_handler_rest/src/data_validation_error_output.json" +--8<-- "examples/event_handler_rest/src/data_validation_error_unsanitized_output.json" ``` However, you can customize the response by catching the `RequestValidationError` exception. ???+ question "When is this useful?" - In production, you might want to hide detailed error information as to why validation failed to prevent abuse. - - Alternatively, you might have a standard on how to return API errors across your company. + In production, you might want to hide detailed error information to prevent abuse, or you have a standard for API errors. Here's an example where we catch validation errors, log all details for further investigation, and return the same `HTTP 422` but with minimum information. -=== "data_validation_error.py" +=== "data_validation_sanitized_error.py" ```python hl_lines="8 24-25 31" - --8<-- "examples/event_handler_rest/src/data_validation_error.py" + --8<-- "examples/event_handler_rest/src/data_validation_sanitized_error.py" ``` 1. We use [exception handler](#exception-handling) decorator to catch **any** request validation errors.

Then, we log the detailed reason as to why it failed while returning a custom `Response` object to hide that from them. -=== "data_validation_error_output.json" +=== "data_validation_sanitized_error_output.json" ```json hl_lines="2 3" - --8<-- "examples/event_handler_rest/src/data_validation_error_output.json" + --8<-- "examples/event_handler_rest/src/data_validation_sanitized_error_output.json" ``` #### Validating payloads @@ -326,7 +324,7 @@ Even better, we can also let Event Handler validate and convert our response acc === "validating_payloads.json" - ```json hl_lines="21 22 33" + ```json hl_lines="3 5-6" --8<-- "examples/event_handler_rest/src/validating_payloads.json" ``` @@ -395,7 +393,7 @@ In the following example, we use a new `Query` OpenAPI type to add [one out of m This is merely for your convenience. - ```python hl_lines="1 8 25" + ```python hl_lines="25" --8<-- "examples/event_handler_rest/src/skip_validating_query_strings.py" ``` diff --git a/examples/event_handler_rest/src/data_validation.json b/examples/event_handler_rest/src/data_validation.json index e852a909265..f5814ccaa26 100644 --- a/examples/event_handler_rest/src/data_validation.json +++ b/examples/event_handler_rest/src/data_validation.json @@ -1,42 +1,36 @@ { - "version": "2.0", - "routeKey": "$default", - "rawPath": "/todos/1", - "rawQueryString": "", - "cookies": [ - "cookie1", - "cookie2" - ], + "version": "1.0", + "resource": "/todos/1", + "path": "/todos/1", + "httpMethod": "GET", "headers": { - "header1": "value1", - "header2": "value1,value2" - }, - "queryStringParameters": { - "parameter1": "value1,value2", - "parameter2": "value" + "Origin": "https://aws.amazon.com" }, + "multiValueHeaders": {}, + "queryStringParameters": {}, + "multiValueQueryStringParameters": {}, "requestContext": { "accountId": "123456789012", - "apiId": "api-id", + "apiId": "id", + "authorizer": { + "claims": null, + "scopes": null + }, "domainName": "id.execute-api.us-east-1.amazonaws.com", "domainPrefix": "id", - "http": { - "method": "GET", - "path": "/todos/1", - "protocol": "HTTP/1.1", - "sourceIp": "192.0.2.1", - "userAgent": "agent" - }, - "requestId": "id", - "routeKey": "$default", - "stage": "$default", - "time": "12/Mar/2020:19:03:58 +0000", - "timeEpoch": 1583348638390 + "extendedRequestId": "request-id", + "httpMethod": "GET", + "path": "/todos/1", + "protocol": "HTTP/1.1", + "requestId": "id=", + "requestTime": "04/Mar/2020:19:15:17 +0000", + "requestTimeEpoch": 1583349317135, + "resourceId": null, + "resourcePath": "/todos/1", + "stage": "$default" }, - "pathParameters": {}, - "isBase64Encoded": false, - "stageVariables": { - "stageVariable1": "value1", - "stageVariable2": "value2" - } -} + "pathParameters": null, + "stageVariables": null, + "body": "", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/examples/event_handler_rest/src/data_validation.py b/examples/event_handler_rest/src/data_validation.py index 581207e2bd8..1daa9fb2174 100644 --- a/examples/event_handler_rest/src/data_validation.py +++ b/examples/event_handler_rest/src/data_validation.py @@ -4,13 +4,13 @@ from pydantic import BaseModel, Field from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.logging import correlation_paths from aws_lambda_powertools.utilities.typing import LambdaContext tracer = Tracer() logger = Logger() -app = APIGatewayHttpResolver(enable_validation=True) # (1)! +app = APIGatewayRestResolver(enable_validation=True) # (1)! class Todo(BaseModel): # (2)! diff --git a/examples/event_handler_rest/src/data_validation_output.json b/examples/event_handler_rest/src/data_validation_output.json index b05dd2d460c..ec078c87078 100644 --- a/examples/event_handler_rest/src/data_validation_output.json +++ b/examples/event_handler_rest/src/data_validation_output.json @@ -1,9 +1,10 @@ { "statusCode": 200, - "headers": { - "Content-Type": "application/json" - }, - "cookies": [], - "body": "hello world", - "isBase64Encoded": false -} + "body": "Hello world", + "isBase64Encoded": false, + "multiValueHeaders": { + "Content-Type": [ + "application/json" + ] + } +} \ No newline at end of file diff --git a/examples/event_handler_rest/src/data_validation_error.py b/examples/event_handler_rest/src/data_validation_sanitized_error.py similarity index 100% rename from examples/event_handler_rest/src/data_validation_error.py rename to examples/event_handler_rest/src/data_validation_sanitized_error.py diff --git a/examples/event_handler_rest/src/data_validation_error_output.json b/examples/event_handler_rest/src/data_validation_sanitized_error_output.json similarity index 100% rename from examples/event_handler_rest/src/data_validation_error_output.json rename to examples/event_handler_rest/src/data_validation_sanitized_error_output.json diff --git a/examples/event_handler_rest/src/skip_validating_query_strings.py b/examples/event_handler_rest/src/skip_validating_query_strings.py index 47e2bd10229..882769239a1 100644 --- a/examples/event_handler_rest/src/skip_validating_query_strings.py +++ b/examples/event_handler_rest/src/skip_validating_query_strings.py @@ -4,13 +4,13 @@ from pydantic import BaseModel, Field from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.logging import correlation_paths from aws_lambda_powertools.utilities.typing import LambdaContext tracer = Tracer() logger = Logger() -app = APIGatewayHttpResolver(enable_validation=True) +app = APIGatewayRestResolver(enable_validation=True) class Todo(BaseModel): diff --git a/examples/event_handler_rest/src/validating_path.py b/examples/event_handler_rest/src/validating_path.py index 4b62380c13e..2a8fbcbfa98 100644 --- a/examples/event_handler_rest/src/validating_path.py +++ b/examples/event_handler_rest/src/validating_path.py @@ -4,14 +4,14 @@ from pydantic import BaseModel, Field from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.event_handler.openapi.params import Path from aws_lambda_powertools.logging import correlation_paths from aws_lambda_powertools.utilities.typing import LambdaContext tracer = Tracer() logger = Logger() -app = APIGatewayHttpResolver(enable_validation=True) +app = APIGatewayRestResolver(enable_validation=True) class Todo(BaseModel): diff --git a/examples/event_handler_rest/src/validating_payloads.json b/examples/event_handler_rest/src/validating_payloads.json index 352d3de35cc..125405e0cf2 100644 --- a/examples/event_handler_rest/src/validating_payloads.json +++ b/examples/event_handler_rest/src/validating_payloads.json @@ -1,40 +1,36 @@ { - "version": "2.0", - "routeKey": "$default", - "rawPath": "/todos", - "rawQueryString": "", - "cookies": [ - "cookie1", - "cookie2" - ], + "version": "1.0", + "body": "{\"title\": \"foo\", \"userId\": \"1\", \"completed\": false}", + "resource": "/todos", + "path": "/todos", + "httpMethod": "POST", "headers": { - "header1": "value1", - "header2": "value1,value2" + "Origin": "https://aws.amazon.com" }, + "multiValueHeaders": {}, "queryStringParameters": {}, + "multiValueQueryStringParameters": {}, "requestContext": { "accountId": "123456789012", - "apiId": "api-id", + "apiId": "id", + "authorizer": { + "claims": null, + "scopes": null + }, "domainName": "id.execute-api.us-east-1.amazonaws.com", "domainPrefix": "id", - "http": { - "method": "POST", - "path": "/todos", - "protocol": "HTTP/1.1", - "sourceIp": "192.0.2.1", - "userAgent": "agent" - }, - "requestId": "id", - "routeKey": "$default", - "stage": "$default", - "time": "12/Mar/2020:19:03:58 +0000", - "timeEpoch": 1583348638390 + "extendedRequestId": "request-id", + "httpMethod": "POST", + "path": "/todos", + "protocol": "HTTP/1.1", + "requestId": "id=", + "requestTime": "04/Mar/2020:19:15:17 +0000", + "requestTimeEpoch": 1583349317135, + "resourceId": null, + "resourcePath": "/todos", + "stage": "$default" }, - "body": "{\"title\": \"foo\", \"userId\": \"1\", \"completed\": false}", - "pathParameters": {}, - "isBase64Encoded": false, - "stageVariables": { - "stageVariable1": "value1", - "stageVariable2": "value2" - } -} + "pathParameters": null, + "stageVariables": null, + "isBase64Encoded": false +} \ No newline at end of file diff --git a/examples/event_handler_rest/src/validating_payloads.py b/examples/event_handler_rest/src/validating_payloads.py index 8a91e59e7f4..945cefd8089 100644 --- a/examples/event_handler_rest/src/validating_payloads.py +++ b/examples/event_handler_rest/src/validating_payloads.py @@ -4,13 +4,13 @@ from pydantic import BaseModel, Field from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.logging import correlation_paths from aws_lambda_powertools.utilities.typing import LambdaContext tracer = Tracer() logger = Logger() -app = APIGatewayHttpResolver(enable_validation=True) # (1)! +app = APIGatewayRestResolver(enable_validation=True) # (1)! class Todo(BaseModel): # (2)! diff --git a/examples/event_handler_rest/src/validating_payloads_output.json b/examples/event_handler_rest/src/validating_payloads_output.json index c01584c86ed..9d72764c3c8 100644 --- a/examples/event_handler_rest/src/validating_payloads_output.json +++ b/examples/event_handler_rest/src/validating_payloads_output.json @@ -2,8 +2,9 @@ "statusCode": 200, "body": "2008821", "isBase64Encoded": false, - "headers": { - "Content-Type": "application/json" - }, - "cookies": [] -} + "multiValueHeaders": { + "Content-Type": [ + "application/json" + ] + } +} \ No newline at end of file diff --git a/examples/event_handler_rest/src/validating_query_strings.py b/examples/event_handler_rest/src/validating_query_strings.py index eaa30626bc4..51ca39bcf3f 100644 --- a/examples/event_handler_rest/src/validating_query_strings.py +++ b/examples/event_handler_rest/src/validating_query_strings.py @@ -4,14 +4,14 @@ from pydantic import BaseModel, Field from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler import APIGatewayHttpResolver +from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.event_handler.openapi.params import Query # (2)! from aws_lambda_powertools.logging import correlation_paths from aws_lambda_powertools.utilities.typing import LambdaContext tracer = Tracer() logger = Logger() -app = APIGatewayHttpResolver(enable_validation=True) +app = APIGatewayRestResolver(enable_validation=True) class Todo(BaseModel): From 1edd283eeffb116dd43495b6b24733a04379c19c Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Thu, 23 Nov 2023 11:41:02 +0100 Subject: [PATCH 27/36] docs: use new validation error output; improve wording --- docs/core/event_handler/api_gateway.md | 14 ++++++-------- .../data_validation_error_unsanitized_output.json | 4 ++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 9d8719ca3e5..b590bbacc35 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -270,24 +270,22 @@ Let's rewrite the previous examples to signal our resolver what shape we expect #### Handling validation errors -???+ note "Pydantic v1 vs v2" - Pydantic versions 1 and 2 may report validation errors differently. Refer to the documentation for your specific version to grasp the precise format and style of the error messages. +!!! info "By default, we hide extended error details for security reasons _(e.g., pydantic url, Pydantic code)_." -Any **incoming request that fails validation** will lead to a `HTTP 422: Unprocessable Entity error` response. When they occur, by default they will look similar to this: +Any incoming request that fails validation will lead to a `HTTP 422: Unprocessable Entity error` response that will look similar to this: ```json hl_lines="2 3" title="data_validation_error_unsanitized_output.json" --8<-- "examples/event_handler_rest/src/data_validation_error_unsanitized_output.json" ``` -However, you can customize the response by catching the `RequestValidationError` exception. +You can customize the error message by catching the `RequestValidationError` exception. This is useful when you might have a security policy to return opaque validation errors, or have a company standard for API validation errors. -???+ question "When is this useful?" - In production, you might want to hide detailed error information to prevent abuse, or you have a standard for API errors. - -Here's an example where we catch validation errors, log all details for further investigation, and return the same `HTTP 422` but with minimum information. +Here's an example where we catch validation errors, log all details for further investigation, and return the same `HTTP 422` with an opaque error. === "data_validation_sanitized_error.py" + Note that Pydantic versions [1](https://docs.pydantic.dev/1.10/usage/models/#error-handling){target="_blank" rel="nofollow"} and [2](https://docs.pydantic.dev/latest/errors/errors/){target="_blank" rel="nofollow"} report validation detailed errors differently. + ```python hl_lines="8 24-25 31" --8<-- "examples/event_handler_rest/src/data_validation_sanitized_error.py" ``` diff --git a/examples/event_handler_rest/src/data_validation_error_unsanitized_output.json b/examples/event_handler_rest/src/data_validation_error_unsanitized_output.json index 15552c69329..46d22c00eef 100644 --- a/examples/event_handler_rest/src/data_validation_error_unsanitized_output.json +++ b/examples/event_handler_rest/src/data_validation_error_unsanitized_output.json @@ -1,9 +1,9 @@ { "statusCode": 422, - "body": "{\"detail\": [{\"type\": \"int_parsing\", \"loc\": [\"path\", \"todo_id\"], \"msg\": \"Input should be a valid integer, unable to parse string as an integer\", \"input\": \"apples\", \"url\": \"https://errors.pydantic.dev/2.5/v/int_parsing\"}]}", + "body": "{\"statusCode\": 422, \"detail\": [{\"type\": \"int_parsing\", \"loc\": [\"path\", \"todo_id\"]}]}", "isBase64Encoded": false, "headers": { "Content-Type": "application/json" }, "cookies": [] -} +} \ No newline at end of file From cba319f8845cae29f1aa75731fbabdf20c55fe5b Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Thu, 23 Nov 2023 11:42:47 +0100 Subject: [PATCH 28/36] docs: add OpenAPI and data validation as key features --- docs/core/event_handler/api_gateway.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index b590bbacc35..64d1449bd9c 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -11,6 +11,7 @@ Event handler for Amazon API Gateway REST and HTTP APIs, Application Loader Bala * Support for CORS, binary and Gzip compression, Decimals JSON encoding and bring your own JSON serializer * Built-in integration with [Event Source Data Classes utilities](../../../utilities/data_classes.md){target="_blank"} for self-documented event schema * Works with micro function (one or a few routes) and monolithic functions (all routes) +* Support for OpenAPI and data validation for requests/responses ## Getting started From 63076cb2c1141555cab4326a44e5f37bdba525ed Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Thu, 23 Nov 2023 11:54:55 +0100 Subject: [PATCH 29/36] docs: remove older examples from openapi validation --- .../sam/template.yaml | 41 ------ .../src/body_parsing.json | 36 ----- .../src/body_parsing.py | 28 ---- .../src/body_parsing_embed.json | 36 ----- .../src/body_parsing_embed.py | 29 ---- .../src/body_parsing_embed_output.json | 10 -- .../src/body_parsing_output.json | 10 -- .../src/customize_api_metadata.py | 33 ----- .../src/customizing_operations.py | 30 ---- .../src/customizing_parameters.py | 30 ---- .../src/generate_openapi_json_spec.json | 136 ------------------ .../src/generate_openapi_json_spec.py | 32 ----- .../src/generate_openapi_spec.py | 32 ----- .../src/getting_started.json | 36 ----- .../src/getting_started.py | 25 ---- .../src/getting_started_bad_input.json | 42 ------ .../src/getting_started_bad_input_output.json | 9 -- .../src/getting_started_output.json | 10 -- .../src/getting_started_pydantic.json | 36 ----- .../src/getting_started_pydantic.py | 28 ---- .../src/getting_started_pydantic_output.json | 10 -- .../src/requirements.txt | 3 - .../event_handler_validation/src/swagger.py | 29 ---- .../src/swagger_customize.py | 29 ---- .../src/swagger_middlewares.py | 40 ------ .../src/validating_query_strings.py | 41 ------ 26 files changed, 821 deletions(-) delete mode 100644 examples/event_handler_validation/sam/template.yaml delete mode 100644 examples/event_handler_validation/src/body_parsing.json delete mode 100644 examples/event_handler_validation/src/body_parsing.py delete mode 100644 examples/event_handler_validation/src/body_parsing_embed.json delete mode 100644 examples/event_handler_validation/src/body_parsing_embed.py delete mode 100644 examples/event_handler_validation/src/body_parsing_embed_output.json delete mode 100644 examples/event_handler_validation/src/body_parsing_output.json delete mode 100644 examples/event_handler_validation/src/customize_api_metadata.py delete mode 100644 examples/event_handler_validation/src/customizing_operations.py delete mode 100644 examples/event_handler_validation/src/customizing_parameters.py delete mode 100644 examples/event_handler_validation/src/generate_openapi_json_spec.json delete mode 100644 examples/event_handler_validation/src/generate_openapi_json_spec.py delete mode 100644 examples/event_handler_validation/src/generate_openapi_spec.py delete mode 100644 examples/event_handler_validation/src/getting_started.json delete mode 100644 examples/event_handler_validation/src/getting_started.py delete mode 100644 examples/event_handler_validation/src/getting_started_bad_input.json delete mode 100644 examples/event_handler_validation/src/getting_started_bad_input_output.json delete mode 100644 examples/event_handler_validation/src/getting_started_output.json delete mode 100644 examples/event_handler_validation/src/getting_started_pydantic.json delete mode 100644 examples/event_handler_validation/src/getting_started_pydantic.py delete mode 100644 examples/event_handler_validation/src/getting_started_pydantic_output.json delete mode 100644 examples/event_handler_validation/src/requirements.txt delete mode 100644 examples/event_handler_validation/src/swagger.py delete mode 100644 examples/event_handler_validation/src/swagger_customize.py delete mode 100644 examples/event_handler_validation/src/swagger_middlewares.py delete mode 100644 examples/event_handler_validation/src/validating_query_strings.py diff --git a/examples/event_handler_validation/sam/template.yaml b/examples/event_handler_validation/sam/template.yaml deleted file mode 100644 index 10d7920bc32..00000000000 --- a/examples/event_handler_validation/sam/template.yaml +++ /dev/null @@ -1,41 +0,0 @@ -AWSTemplateFormatVersion: "2010-09-09" -Transform: AWS::Serverless-2016-10-31 -Description: Hello world event handler API Gateway - -Globals: - Api: - TracingEnabled: true - Cors: # see CORS section - AllowOrigin: "'https://example.com'" - AllowHeaders: "'Content-Type,Authorization,X-Amz-Date'" - MaxAge: "'300'" - BinaryMediaTypes: # see Binary responses section - - "*~1*" # converts to */* for any binary type - Function: - Timeout: 5 - Runtime: python3.9 - Tracing: Active - Architectures: - - arm64 - Environment: - Variables: - POWERTOOLS_LOG_LEVEL: INFO - POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 - POWERTOOLS_LOGGER_LOG_EVENT: true - POWERTOOLS_SERVICE_NAME: example - -Resources: - ApiFunction: - Type: AWS::Serverless::Function - Properties: - Handler: getting_started.lambda_handler - CodeUri: ../src - Description: API handler function - Events: - AnyApiEvent: - Type: Api - Properties: - # NOTE: this is a catch-all rule to simplify the documentation. - # explicit routes and methods are recommended for prod instead (see below) - Path: /{proxy+} # Send requests on any path to the lambda function - Method: ANY # Send requests using any http method to the lambda function diff --git a/examples/event_handler_validation/src/body_parsing.json b/examples/event_handler_validation/src/body_parsing.json deleted file mode 100644 index 51d7b68a6fb..00000000000 --- a/examples/event_handler_validation/src/body_parsing.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "version": "1.0", - "resource": "/todos", - "path": "/todos", - "httpMethod": "POST", - "headers": { - "Origin": "https://aws.amazon.com" - }, - "multiValueHeaders": {}, - "queryStringParameters": {}, - "multiValueQueryStringParameters": {}, - "requestContext": { - "accountId": "123456789012", - "apiId": "id", - "authorizer": { - "claims": null, - "scopes": null - }, - "domainName": "id.execute-api.us-east-1.amazonaws.com", - "domainPrefix": "id", - "extendedRequestId": "request-id", - "httpMethod": "POST", - "path": "/todos", - "protocol": "HTTP/1.1", - "requestId": "id=", - "requestTime": "04/Mar/2020:19:15:17 +0000", - "requestTimeEpoch": 1583349317135, - "resourceId": null, - "resourcePath": "/todos", - "stage": "$default" - }, - "pathParameters": null, - "stageVariables": null, - "body": "{\"title\": \"foo\", \"userId\": \"1\", \"completed\": false}", - "isBase64Encoded": false -} diff --git a/examples/event_handler_validation/src/body_parsing.py b/examples/event_handler_validation/src/body_parsing.py deleted file mode 100644 index 6bf6865cb48..00000000000 --- a/examples/event_handler_validation/src/body_parsing.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Optional - -import requests -from pydantic import BaseModel, Field - -from aws_lambda_powertools.event_handler import APIGatewayRestResolver -from aws_lambda_powertools.utilities.typing import LambdaContext - -app = APIGatewayRestResolver(enable_validation=True) - - -class Todo(BaseModel): - userId: int - id_: Optional[int] = Field(alias="id", default=None) - title: str - completed: bool - - -@app.post("/todos") -def create_todo(todo: Todo) -> int: - response = requests.post("https://jsonplaceholder.typicode.com/todos", json=todo.dict(by_alias=True)) - response.raise_for_status() - - return response.json()["id"] - - -def lambda_handler(event: dict, context: LambdaContext) -> dict: - return app.resolve(event, context) diff --git a/examples/event_handler_validation/src/body_parsing_embed.json b/examples/event_handler_validation/src/body_parsing_embed.json deleted file mode 100644 index d6b3f564ee3..00000000000 --- a/examples/event_handler_validation/src/body_parsing_embed.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "version": "1.0", - "resource": "/todos", - "path": "/todos", - "httpMethod": "POST", - "headers": { - "Origin": "https://aws.amazon.com" - }, - "multiValueHeaders": {}, - "queryStringParameters": {}, - "multiValueQueryStringParameters": {}, - "requestContext": { - "accountId": "123456789012", - "apiId": "id", - "authorizer": { - "claims": null, - "scopes": null - }, - "domainName": "id.execute-api.us-east-1.amazonaws.com", - "domainPrefix": "id", - "extendedRequestId": "request-id", - "httpMethod": "POST", - "path": "/todos", - "protocol": "HTTP/1.1", - "requestId": "id=", - "requestTime": "04/Mar/2020:19:15:17 +0000", - "requestTimeEpoch": 1583349317135, - "resourceId": null, - "resourcePath": "/todos", - "stage": "$default" - }, - "pathParameters": null, - "stageVariables": null, - "body": "{ \"todo\": {\"title\": \"foo\", \"userId\": \"1\", \"completed\": false } }", - "isBase64Encoded": false -} diff --git a/examples/event_handler_validation/src/body_parsing_embed.py b/examples/event_handler_validation/src/body_parsing_embed.py deleted file mode 100644 index d27fbeb2259..00000000000 --- a/examples/event_handler_validation/src/body_parsing_embed.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Annotated, Optional - -import requests -from pydantic import BaseModel, Field - -from aws_lambda_powertools.event_handler import APIGatewayRestResolver -from aws_lambda_powertools.event_handler.openapi.params import Body -from aws_lambda_powertools.utilities.typing import LambdaContext - -app = APIGatewayRestResolver(enable_validation=True) - - -class Todo(BaseModel): - userId: int - id_: Optional[int] = Field(alias="id", default=None) - title: str - completed: bool - - -@app.post("/todos") -def create_todo(todo: Annotated[Todo, Body(embed=True)]) -> int: - response = requests.post("https://jsonplaceholder.typicode.com/todos", json=todo.dict(by_alias=True)) - response.raise_for_status() - - return response.json()["id"] - - -def lambda_handler(event: dict, context: LambdaContext) -> dict: - return app.resolve(event, context) diff --git a/examples/event_handler_validation/src/body_parsing_embed_output.json b/examples/event_handler_validation/src/body_parsing_embed_output.json deleted file mode 100644 index 754e3a6c128..00000000000 --- a/examples/event_handler_validation/src/body_parsing_embed_output.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "statusCode": 200, - "body": "2008822", - "isBase64Encoded": false, - "multiValueHeaders": { - "Content-Type": [ - "application/json" - ] - } -} diff --git a/examples/event_handler_validation/src/body_parsing_output.json b/examples/event_handler_validation/src/body_parsing_output.json deleted file mode 100644 index 7e413260e12..00000000000 --- a/examples/event_handler_validation/src/body_parsing_output.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "statusCode": 200, - "body": "2008821", - "isBase64Encoded": false, - "multiValueHeaders": { - "Content-Type": [ - "application/json" - ] - } -} diff --git a/examples/event_handler_validation/src/customize_api_metadata.py b/examples/event_handler_validation/src/customize_api_metadata.py deleted file mode 100644 index cd9ced455d2..00000000000 --- a/examples/event_handler_validation/src/customize_api_metadata.py +++ /dev/null @@ -1,33 +0,0 @@ -import requests - -from aws_lambda_powertools.event_handler import APIGatewayRestResolver -from aws_lambda_powertools.event_handler.openapi.models import Contact, Server -from aws_lambda_powertools.utilities.typing import LambdaContext - -app = APIGatewayRestResolver(enable_validation=True) - - -@app.get("/todos/") -def get_todo_title(todo_id: int) -> str: - todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") - todo.raise_for_status() - - return todo.json()["title"] - - -def lambda_handler(event: dict, context: LambdaContext) -> dict: - return app.resolve(event, context) - - -if __name__ == "__main__": - print( - app.get_openapi_json_schema( - title="TODO's API", - version="1.21.3", - summary="API to manage TODOs", - description="This API implements all the CRUD operations for the TODO app", - tags=["todos"], - servers=[Server(url="https://stg.example.org/orders", description="Staging server")], - contact=Contact(name="John Smith", email="john@smith.com"), - ), - ) diff --git a/examples/event_handler_validation/src/customizing_operations.py b/examples/event_handler_validation/src/customizing_operations.py deleted file mode 100644 index e455fc7dadd..00000000000 --- a/examples/event_handler_validation/src/customizing_operations.py +++ /dev/null @@ -1,30 +0,0 @@ -import requests - -from aws_lambda_powertools.event_handler import APIGatewayRestResolver -from aws_lambda_powertools.utilities.typing import LambdaContext - -app = APIGatewayRestResolver(enable_validation=True) - - -@app.get( - "/todos/", - summary="Retrieves a todo item", - description="Loads a todo item identified by the `todo_id`", - response_description="The todo object", - responses={ - 200: {"description": "Todo item found"}, - 404: { - "description": "Item not found", - }, - }, - tags=["Todos"], -) -def get_todo_title(todo_id: int) -> str: - todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") - todo.raise_for_status() - - return todo.json()["title"] - - -def lambda_handler(event: dict, context: LambdaContext) -> dict: - return app.resolve(event, context) diff --git a/examples/event_handler_validation/src/customizing_parameters.py b/examples/event_handler_validation/src/customizing_parameters.py deleted file mode 100644 index 394a453e41d..00000000000 --- a/examples/event_handler_validation/src/customizing_parameters.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import Annotated - -import requests - -from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler import APIGatewayRestResolver -from aws_lambda_powertools.event_handler.openapi.params import Param, Path -from aws_lambda_powertools.logging import correlation_paths -from aws_lambda_powertools.utilities.typing import LambdaContext - -tracer = Tracer() -logger = Logger() -app = APIGatewayRestResolver(enable_validation=True) - - -@app.get("/todos/") -@tracer.capture_method -def get_todo_title( - todo_id: Annotated[int, Path(gt=0, title="Todo ID", description="The ID of the Todo to fetch the title from")], -) -> Annotated[str, Param(title="Title", description="The title of the Todo item")]: - todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") - todo.raise_for_status() - - return todo.json()["title"] - - -@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) -@tracer.capture_lambda_handler -def lambda_handler(event: dict, context: LambdaContext) -> dict: - return app.resolve(event, context) diff --git a/examples/event_handler_validation/src/generate_openapi_json_spec.json b/examples/event_handler_validation/src/generate_openapi_json_spec.json deleted file mode 100644 index e1ea513ded5..00000000000 --- a/examples/event_handler_validation/src/generate_openapi_json_spec.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Powertools API", - "version": "1.0.0" - }, - "servers": [ - { - "url": "/" - } - ], - "paths": { - "/todos": { - "get": { - "summary": "GET /todos", - "operationId": "get_todos_by_email_todos_get", - "parameters": [ - { - "required": true, - "schema": { - "type": "string", - "format": "email", - "title": "Email" - }, - "name": "email", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/Todo" - }, - "type": "array", - "title": "Return" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail" - } - }, - "type": "object", - "title": "HTTPValidationError" - }, - "Todo": { - "properties": { - "userId": { - "type": "integer", - "title": "Userid" - }, - "id": { - "type": "integer", - "title": "Id" - }, - "title": { - "type": "string", - "title": "Title" - }, - "completed": { - "type": "boolean", - "title": "Completed" - } - }, - "type": "object", - "required": [ - "userId", - "id", - "title", - "completed" - ], - "title": "Todo" - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "integer" - } - ] - }, - "type": "array", - "title": "Location" - }, - "msg": { - "type": "string", - "title": "Message" - }, - "type": { - "type": "string", - "title": "Error Type" - } - }, - "type": "object", - "required": [ - "loc", - "msg", - "type" - ], - "title": "ValidationError" - } - } - } -} diff --git a/examples/event_handler_validation/src/generate_openapi_json_spec.py b/examples/event_handler_validation/src/generate_openapi_json_spec.py deleted file mode 100644 index 6a7582d3c75..00000000000 --- a/examples/event_handler_validation/src/generate_openapi_json_spec.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import List - -import requests -from pydantic import BaseModel, EmailStr, Field - -from aws_lambda_powertools.event_handler import APIGatewayRestResolver -from aws_lambda_powertools.utilities.typing import LambdaContext - -app = APIGatewayRestResolver(enable_validation=True) - - -class Todo(BaseModel): - userId: int - id_: int = Field(alias="id") - title: str - completed: bool - - -@app.get("/todos") -def get_todos_by_email(email: EmailStr) -> List[Todo]: - todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}") - todos.raise_for_status() - - return todos.json() - - -def lambda_handler(event: dict, context: LambdaContext) -> dict: - return app.resolve(event, context) - - -if __name__ == "__main__": - print(app.get_openapi_json_schema()) diff --git a/examples/event_handler_validation/src/generate_openapi_spec.py b/examples/event_handler_validation/src/generate_openapi_spec.py deleted file mode 100644 index 21c1f5cd1fe..00000000000 --- a/examples/event_handler_validation/src/generate_openapi_spec.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import List - -import requests -from pydantic import BaseModel, EmailStr, Field - -from aws_lambda_powertools.event_handler import APIGatewayRestResolver -from aws_lambda_powertools.utilities.typing import LambdaContext - -app = APIGatewayRestResolver(enable_validation=True) - - -class Todo(BaseModel): - userId: int - id_: int = Field(alias="id") - title: str - completed: bool - - -@app.get("/todos") -def get_todos_by_email(email: EmailStr) -> List[Todo]: - todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}") - todos.raise_for_status() - - return todos.json() - - -def lambda_handler(event: dict, context: LambdaContext) -> dict: - return app.resolve(event, context) - - -if __name__ == "__main__": - print(app.get_openapi_schema().dict()) diff --git a/examples/event_handler_validation/src/getting_started.json b/examples/event_handler_validation/src/getting_started.json deleted file mode 100644 index ed4a573e7c1..00000000000 --- a/examples/event_handler_validation/src/getting_started.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "version": "1.0", - "resource": "/todos/1", - "path": "/todos/1", - "httpMethod": "GET", - "headers": { - "Origin": "https://aws.amazon.com" - }, - "multiValueHeaders": {}, - "queryStringParameters": {}, - "multiValueQueryStringParameters": {}, - "requestContext": { - "accountId": "123456789012", - "apiId": "id", - "authorizer": { - "claims": null, - "scopes": null - }, - "domainName": "id.execute-api.us-east-1.amazonaws.com", - "domainPrefix": "id", - "extendedRequestId": "request-id", - "httpMethod": "GET", - "path": "/todos/1", - "protocol": "HTTP/1.1", - "requestId": "id=", - "requestTime": "04/Mar/2020:19:15:17 +0000", - "requestTimeEpoch": 1583349317135, - "resourceId": null, - "resourcePath": "/todos/1", - "stage": "$default" - }, - "pathParameters": null, - "stageVariables": null, - "body": "", - "isBase64Encoded": false -} diff --git a/examples/event_handler_validation/src/getting_started.py b/examples/event_handler_validation/src/getting_started.py deleted file mode 100644 index f80c97856cb..00000000000 --- a/examples/event_handler_validation/src/getting_started.py +++ /dev/null @@ -1,25 +0,0 @@ -import requests - -from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler import APIGatewayRestResolver -from aws_lambda_powertools.logging import correlation_paths -from aws_lambda_powertools.utilities.typing import LambdaContext - -tracer = Tracer() -logger = Logger() -app = APIGatewayRestResolver(enable_validation=True) - - -@app.get("/todos/") -@tracer.capture_method -def get_todo_title(todo_id: int) -> str: - todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}") - todo.raise_for_status() - - return todo.json()["title"] - - -@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) -@tracer.capture_lambda_handler -def lambda_handler(event: dict, context: LambdaContext) -> dict: - return app.resolve(event, context) diff --git a/examples/event_handler_validation/src/getting_started_bad_input.json b/examples/event_handler_validation/src/getting_started_bad_input.json deleted file mode 100644 index 6fc2636ad9c..00000000000 --- a/examples/event_handler_validation/src/getting_started_bad_input.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "version": "2.0", - "routeKey": "$default", - "rawPath": "/todos/apples", - "rawQueryString": "", - "cookies": [ - "cookie1", - "cookie2" - ], - "headers": { - "header1": "value1", - "header2": "value1,value2" - }, - "queryStringParameters": { - "parameter1": "value1,value2", - "parameter2": "value" - }, - "requestContext": { - "accountId": "123456789012", - "apiId": "api-id", - "domainName": "id.execute-api.us-east-1.amazonaws.com", - "domainPrefix": "id", - "http": { - "method": "GET", - "path": "/todos/apples", - "protocol": "HTTP/1.1", - "sourceIp": "192.0.2.1", - "userAgent": "agent" - }, - "requestId": "id", - "routeKey": "$default", - "stage": "$default", - "time": "12/Mar/2020:19:03:58 +0000", - "timeEpoch": 1583348638390 - }, - "pathParameters": {}, - "isBase64Encoded": false, - "stageVariables": { - "stageVariable1": "value1", - "stageVariable2": "value2" - } -} diff --git a/examples/event_handler_validation/src/getting_started_bad_input_output.json b/examples/event_handler_validation/src/getting_started_bad_input_output.json deleted file mode 100644 index 15552c69329..00000000000 --- a/examples/event_handler_validation/src/getting_started_bad_input_output.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "statusCode": 422, - "body": "{\"detail\": [{\"type\": \"int_parsing\", \"loc\": [\"path\", \"todo_id\"], \"msg\": \"Input should be a valid integer, unable to parse string as an integer\", \"input\": \"apples\", \"url\": \"https://errors.pydantic.dev/2.5/v/int_parsing\"}]}", - "isBase64Encoded": false, - "headers": { - "Content-Type": "application/json" - }, - "cookies": [] -} diff --git a/examples/event_handler_validation/src/getting_started_output.json b/examples/event_handler_validation/src/getting_started_output.json deleted file mode 100644 index cd0f41b54b8..00000000000 --- a/examples/event_handler_validation/src/getting_started_output.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "statusCode": 200, - "body": "Hello world", - "isBase64Encoded": false, - "multiValueHeaders": { - "Content-Type": [ - "application/json" - ] - } -} diff --git a/examples/event_handler_validation/src/getting_started_pydantic.json b/examples/event_handler_validation/src/getting_started_pydantic.json deleted file mode 100644 index 76864b672e7..00000000000 --- a/examples/event_handler_validation/src/getting_started_pydantic.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "version": "1.0", - "resource": "/todos", - "path": "/todos", - "httpMethod": "GET", - "headers": { - "Origin": "https://aws.amazon.com" - }, - "multiValueHeaders": {}, - "queryStringParameters": {}, - "multiValueQueryStringParameters": {}, - "requestContext": { - "accountId": "123456789012", - "apiId": "id", - "authorizer": { - "claims": null, - "scopes": null - }, - "domainName": "id.execute-api.us-east-1.amazonaws.com", - "domainPrefix": "id", - "extendedRequestId": "request-id", - "httpMethod": "GET", - "path": "/todos", - "protocol": "HTTP/1.1", - "requestId": "id=", - "requestTime": "04/Mar/2020:19:15:17 +0000", - "requestTimeEpoch": 1583349317135, - "resourceId": null, - "resourcePath": "/todos", - "stage": "$default" - }, - "pathParameters": null, - "stageVariables": null, - "body": "", - "isBase64Encoded": false -} diff --git a/examples/event_handler_validation/src/getting_started_pydantic.py b/examples/event_handler_validation/src/getting_started_pydantic.py deleted file mode 100644 index 481f3f98f44..00000000000 --- a/examples/event_handler_validation/src/getting_started_pydantic.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import List - -import requests -from pydantic import BaseModel, EmailStr, Field - -from aws_lambda_powertools.event_handler import APIGatewayRestResolver -from aws_lambda_powertools.utilities.typing import LambdaContext - -app = APIGatewayRestResolver(enable_validation=True) - - -class Todo(BaseModel): - userId: int - id_: int = Field(alias="id") - title: str - completed: bool - - -@app.get("/todos") -def get_todos_by_email(email: EmailStr) -> List[Todo]: - todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}") - todos.raise_for_status() - - return todos.json() - - -def lambda_handler(event: dict, context: LambdaContext) -> dict: - return app.resolve(event, context) diff --git a/examples/event_handler_validation/src/getting_started_pydantic_output.json b/examples/event_handler_validation/src/getting_started_pydantic_output.json deleted file mode 100644 index 8375de999f0..00000000000 --- a/examples/event_handler_validation/src/getting_started_pydantic_output.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "statusCode": 200, - "body": "[{\"completed\": false, \"id\": 1, \"title\": \"delectus aut autem\", \"userId\": 1}, {\"completed\": false, \"id\": 2, \"title\": \"quis ut nam facilis et officia qui\", \"userId\": 1}, {\"completed\": false, \"id\": 3, \"title\": \"fugiat veniam minus\", \"userId\": 1}, {\"completed\": true, \"id\": 4, \"title\": \"et porro tempora\", \"userId\": 1}, {\"completed\": false, \"id\": 5, \"title\": \"laboriosam mollitia et enim quasi adipisci quia provident illum\", \"userId\": 1}, {\"completed\": false, \"id\": 6, \"title\": \"qui ullam ratione quibusdam voluptatem quia omnis\", \"userId\": 1}, {\"completed\": false, \"id\": 7, \"title\": \"illo expedita consequatur quia in\", \"userId\": 1}, {\"completed\": true, \"id\": 8, \"title\": \"quo adipisci enim quam ut ab\", \"userId\": 1}, {\"completed\": false, \"id\": 9, \"title\": \"molestiae perspiciatis ipsa\", \"userId\": 1}, {\"completed\": true, \"id\": 10, \"title\": \"illo est ratione doloremque quia maiores aut\", \"userId\": 1}, {\"completed\": true, \"id\": 11, \"title\": \"vero rerum temporibus dolor\", \"userId\": 1}, {\"completed\": true, \"id\": 12, \"title\": \"ipsa repellendus fugit nisi\", \"userId\": 1}, {\"completed\": false, \"id\": 13, \"title\": \"et doloremque nulla\", \"userId\": 1}, {\"completed\": true, \"id\": 14, \"title\": \"repellendus sunt dolores architecto voluptatum\", \"userId\": 1}, {\"completed\": true, \"id\": 15, \"title\": \"ab voluptatum amet voluptas\", \"userId\": 1}, {\"completed\": true, \"id\": 16, \"title\": \"accusamus eos facilis sint et aut voluptatem\", \"userId\": 1}, {\"completed\": true, \"id\": 17, \"title\": \"quo laboriosam deleniti aut qui\", \"userId\": 1}, {\"completed\": false, \"id\": 18, \"title\": \"dolorum est consequatur ea mollitia in culpa\", \"userId\": 1}, {\"completed\": true, \"id\": 19, \"title\": \"molestiae ipsa aut voluptatibus pariatur dolor nihil\", \"userId\": 1}, {\"completed\": true, \"id\": 20, \"title\": \"ullam nobis libero sapiente ad optio sint\", \"userId\": 1}, {\"completed\": false, \"id\": 21, \"title\": \"suscipit repellat esse quibusdam voluptatem incidunt\", \"userId\": 2}, {\"completed\": true, \"id\": 22, \"title\": \"distinctio vitae autem nihil ut molestias quo\", \"userId\": 2}, {\"completed\": false, \"id\": 23, \"title\": \"et itaque necessitatibus maxime molestiae qui quas velit\", \"userId\": 2}, {\"completed\": false, \"id\": 24, \"title\": \"adipisci non ad dicta qui amet quaerat doloribus ea\", \"userId\": 2}, {\"completed\": true, \"id\": 25, \"title\": \"voluptas quo tenetur perspiciatis explicabo natus\", \"userId\": 2}, {\"completed\": true, \"id\": 26, \"title\": \"aliquam aut quasi\", \"userId\": 2}, {\"completed\": true, \"id\": 27, \"title\": \"veritatis pariatur delectus\", \"userId\": 2}, {\"completed\": false, \"id\": 28, \"title\": \"nesciunt totam sit blanditiis sit\", \"userId\": 2}, {\"completed\": false, \"id\": 29, \"title\": \"laborum aut in quam\", \"userId\": 2}, {\"completed\": true, \"id\": 30, \"title\": \"nemo perspiciatis repellat ut dolor libero commodi blanditiis omnis\", \"userId\": 2}, {\"completed\": false, \"id\": 31, \"title\": \"repudiandae totam in est sint facere fuga\", \"userId\": 2}, {\"completed\": false, \"id\": 32, \"title\": \"earum doloribus ea doloremque quis\", \"userId\": 2}, {\"completed\": false, \"id\": 33, \"title\": \"sint sit aut vero\", \"userId\": 2}, {\"completed\": false, \"id\": 34, \"title\": \"porro aut necessitatibus eaque distinctio\", \"userId\": 2}, {\"completed\": true, \"id\": 35, \"title\": \"repellendus veritatis molestias dicta incidunt\", \"userId\": 2}, {\"completed\": true, \"id\": 36, \"title\": \"excepturi deleniti adipisci voluptatem et neque optio illum ad\", \"userId\": 2}, {\"completed\": false, \"id\": 37, \"title\": \"sunt cum tempora\", \"userId\": 2}, {\"completed\": false, \"id\": 38, \"title\": \"totam quia non\", \"userId\": 2}, {\"completed\": false, \"id\": 39, \"title\": \"doloremque quibusdam asperiores libero corrupti illum qui omnis\", \"userId\": 2}, {\"completed\": true, \"id\": 40, \"title\": \"totam atque quo nesciunt\", \"userId\": 2}, {\"completed\": false, \"id\": 41, \"title\": \"aliquid amet impedit consequatur aspernatur placeat eaque fugiat suscipit\", \"userId\": 3}, {\"completed\": false, \"id\": 42, \"title\": \"rerum perferendis error quia ut eveniet\", \"userId\": 3}, {\"completed\": true, \"id\": 43, \"title\": \"tempore ut sint quis recusandae\", \"userId\": 3}, {\"completed\": true, \"id\": 44, \"title\": \"cum debitis quis accusamus doloremque ipsa natus sapiente omnis\", \"userId\": 3}, {\"completed\": false, \"id\": 45, \"title\": \"velit soluta adipisci molestias reiciendis harum\", \"userId\": 3}, {\"completed\": false, \"id\": 46, \"title\": \"vel voluptatem repellat nihil placeat corporis\", \"userId\": 3}, {\"completed\": false, \"id\": 47, \"title\": \"nam qui rerum fugiat accusamus\", \"userId\": 3}, {\"completed\": false, \"id\": 48, \"title\": \"sit reprehenderit omnis quia\", \"userId\": 3}, {\"completed\": false, \"id\": 49, \"title\": \"ut necessitatibus aut maiores debitis officia blanditiis velit et\", \"userId\": 3}, {\"completed\": true, \"id\": 50, \"title\": \"cupiditate necessitatibus ullam aut quis dolor voluptate\", \"userId\": 3}, {\"completed\": false, \"id\": 51, \"title\": \"distinctio exercitationem ab doloribus\", \"userId\": 3}, {\"completed\": false, \"id\": 52, \"title\": \"nesciunt dolorum quis recusandae ad pariatur ratione\", \"userId\": 3}, {\"completed\": false, \"id\": 53, \"title\": \"qui labore est occaecati recusandae aliquid quam\", \"userId\": 3}, {\"completed\": true, \"id\": 54, \"title\": \"quis et est ut voluptate quam dolor\", \"userId\": 3}, {\"completed\": true, \"id\": 55, \"title\": \"voluptatum omnis minima qui occaecati provident nulla voluptatem ratione\", \"userId\": 3}, {\"completed\": true, \"id\": 56, \"title\": \"deleniti ea temporibus enim\", \"userId\": 3}, {\"completed\": false, \"id\": 57, \"title\": \"pariatur et magnam ea doloribus similique voluptatem rerum quia\", \"userId\": 3}, {\"completed\": false, \"id\": 58, \"title\": \"est dicta totam qui explicabo doloribus qui dignissimos\", \"userId\": 3}, {\"completed\": false, \"id\": 59, \"title\": \"perspiciatis velit id laborum placeat iusto et aliquam odio\", \"userId\": 3}, {\"completed\": true, \"id\": 60, \"title\": \"et sequi qui architecto ut adipisci\", \"userId\": 3}, {\"completed\": true, \"id\": 61, \"title\": \"odit optio omnis qui sunt\", \"userId\": 4}, {\"completed\": false, \"id\": 62, \"title\": \"et placeat et tempore aspernatur sint numquam\", \"userId\": 4}, {\"completed\": true, \"id\": 63, \"title\": \"doloremque aut dolores quidem fuga qui nulla\", \"userId\": 4}, {\"completed\": false, \"id\": 64, \"title\": \"voluptas consequatur qui ut quia magnam nemo esse\", \"userId\": 4}, {\"completed\": false, \"id\": 65, \"title\": \"fugiat pariatur ratione ut asperiores necessitatibus magni\", \"userId\": 4}, {\"completed\": false, \"id\": 66, \"title\": \"rerum eum molestias autem voluptatum sit optio\", \"userId\": 4}, {\"completed\": false, \"id\": 67, \"title\": \"quia voluptatibus voluptatem quos similique maiores repellat\", \"userId\": 4}, {\"completed\": false, \"id\": 68, \"title\": \"aut id perspiciatis voluptatem iusto\", \"userId\": 4}, {\"completed\": false, \"id\": 69, \"title\": \"doloribus sint dolorum ab adipisci itaque dignissimos aliquam suscipit\", \"userId\": 4}, {\"completed\": false, \"id\": 70, \"title\": \"ut sequi accusantium et mollitia delectus sunt\", \"userId\": 4}, {\"completed\": false, \"id\": 71, \"title\": \"aut velit saepe ullam\", \"userId\": 4}, {\"completed\": false, \"id\": 72, \"title\": \"praesentium facilis facere quis harum voluptatibus voluptatem eum\", \"userId\": 4}, {\"completed\": true, \"id\": 73, \"title\": \"sint amet quia totam corporis qui exercitationem commodi\", \"userId\": 4}, {\"completed\": false, \"id\": 74, \"title\": \"expedita tempore nobis eveniet laborum maiores\", \"userId\": 4}, {\"completed\": false, \"id\": 75, \"title\": \"occaecati adipisci est possimus totam\", \"userId\": 4}, {\"completed\": true, \"id\": 76, \"title\": \"sequi dolorem sed\", \"userId\": 4}, {\"completed\": false, \"id\": 77, \"title\": \"maiores aut nesciunt delectus exercitationem vel assumenda eligendi at\", \"userId\": 4}, {\"completed\": false, \"id\": 78, \"title\": \"reiciendis est magnam amet nemo iste recusandae impedit quaerat\", \"userId\": 4}, {\"completed\": true, \"id\": 79, \"title\": \"eum ipsa maxime ut\", \"userId\": 4}, {\"completed\": true, \"id\": 80, \"title\": \"tempore molestias dolores rerum sequi voluptates ipsum consequatur\", \"userId\": 4}, {\"completed\": true, \"id\": 81, \"title\": \"suscipit qui totam\", \"userId\": 5}, {\"completed\": false, \"id\": 82, \"title\": \"voluptates eum voluptas et dicta\", \"userId\": 5}, {\"completed\": true, \"id\": 83, \"title\": \"quidem at rerum quis ex aut sit quam\", \"userId\": 5}, {\"completed\": false, \"id\": 84, \"title\": \"sunt veritatis ut voluptate\", \"userId\": 5}, {\"completed\": true, \"id\": 85, \"title\": \"et quia ad iste a\", \"userId\": 5}, {\"completed\": true, \"id\": 86, \"title\": \"incidunt ut saepe autem\", \"userId\": 5}, {\"completed\": true, \"id\": 87, \"title\": \"laudantium quae eligendi consequatur quia et vero autem\", \"userId\": 5}, {\"completed\": false, \"id\": 88, \"title\": \"vitae aut excepturi laboriosam sint aliquam et et accusantium\", \"userId\": 5}, {\"completed\": true, \"id\": 89, \"title\": \"sequi ut omnis et\", \"userId\": 5}, {\"completed\": true, \"id\": 90, \"title\": \"molestiae nisi accusantium tenetur dolorem et\", \"userId\": 5}, {\"completed\": true, \"id\": 91, \"title\": \"nulla quis consequatur saepe qui id expedita\", \"userId\": 5}, {\"completed\": true, \"id\": 92, \"title\": \"in omnis laboriosam\", \"userId\": 5}, {\"completed\": true, \"id\": 93, \"title\": \"odio iure consequatur molestiae quibusdam necessitatibus quia sint\", \"userId\": 5}, {\"completed\": false, \"id\": 94, \"title\": \"facilis modi saepe mollitia\", \"userId\": 5}, {\"completed\": true, \"id\": 95, \"title\": \"vel nihil et molestiae iusto assumenda nemo quo ut\", \"userId\": 5}, {\"completed\": false, \"id\": 96, \"title\": \"nobis suscipit ducimus enim asperiores voluptas\", \"userId\": 5}, {\"completed\": false, \"id\": 97, \"title\": \"dolorum laboriosam eos qui iure aliquam\", \"userId\": 5}, {\"completed\": true, \"id\": 98, \"title\": \"debitis accusantium ut quo facilis nihil quis sapiente necessitatibus\", \"userId\": 5}, {\"completed\": false, \"id\": 99, \"title\": \"neque voluptates ratione\", \"userId\": 5}, {\"completed\": false, \"id\": 100, \"title\": \"excepturi a et neque qui expedita vel voluptate\", \"userId\": 5}, {\"completed\": false, \"id\": 101, \"title\": \"explicabo enim cumque porro aperiam occaecati minima\", \"userId\": 6}, {\"completed\": false, \"id\": 102, \"title\": \"sed ab consequatur\", \"userId\": 6}, {\"completed\": false, \"id\": 103, \"title\": \"non sunt delectus illo nulla tenetur enim omnis\", \"userId\": 6}, {\"completed\": false, \"id\": 104, \"title\": \"excepturi non laudantium quo\", \"userId\": 6}, {\"completed\": true, \"id\": 105, \"title\": \"totam quia dolorem et illum repellat voluptas optio\", \"userId\": 6}, {\"completed\": true, \"id\": 106, \"title\": \"ad illo quis voluptatem temporibus\", \"userId\": 6}, {\"completed\": false, \"id\": 107, \"title\": \"praesentium facilis omnis laudantium fugit ad iusto nihil nesciunt\", \"userId\": 6}, {\"completed\": true, \"id\": 108, \"title\": \"a eos eaque nihil et exercitationem incidunt delectus\", \"userId\": 6}, {\"completed\": true, \"id\": 109, \"title\": \"autem temporibus harum quisquam in culpa\", \"userId\": 6}, {\"completed\": true, \"id\": 110, \"title\": \"aut aut ea corporis\", \"userId\": 6}, {\"completed\": false, \"id\": 111, \"title\": \"magni accusantium labore et id quis provident\", \"userId\": 6}, {\"completed\": false, \"id\": 112, \"title\": \"consectetur impedit quisquam qui deserunt non rerum consequuntur eius\", \"userId\": 6}, {\"completed\": false, \"id\": 113, \"title\": \"quia atque aliquam sunt impedit voluptatum rerum assumenda nisi\", \"userId\": 6}, {\"completed\": false, \"id\": 114, \"title\": \"cupiditate quos possimus corporis quisquam exercitationem beatae\", \"userId\": 6}, {\"completed\": false, \"id\": 115, \"title\": \"sed et ea eum\", \"userId\": 6}, {\"completed\": true, \"id\": 116, \"title\": \"ipsa dolores vel facilis ut\", \"userId\": 6}, {\"completed\": false, \"id\": 117, \"title\": \"sequi quae est et qui qui eveniet asperiores\", \"userId\": 6}, {\"completed\": false, \"id\": 118, \"title\": \"quia modi consequatur vero fugiat\", \"userId\": 6}, {\"completed\": false, \"id\": 119, \"title\": \"corporis ducimus ea perspiciatis iste\", \"userId\": 6}, {\"completed\": false, \"id\": 120, \"title\": \"dolorem laboriosam vel voluptas et aliquam quasi\", \"userId\": 6}, {\"completed\": true, \"id\": 121, \"title\": \"inventore aut nihil minima laudantium hic qui omnis\", \"userId\": 7}, {\"completed\": true, \"id\": 122, \"title\": \"provident aut nobis culpa\", \"userId\": 7}, {\"completed\": false, \"id\": 123, \"title\": \"esse et quis iste est earum aut impedit\", \"userId\": 7}, {\"completed\": false, \"id\": 124, \"title\": \"qui consectetur id\", \"userId\": 7}, {\"completed\": false, \"id\": 125, \"title\": \"aut quasi autem iste tempore illum possimus\", \"userId\": 7}, {\"completed\": true, \"id\": 126, \"title\": \"ut asperiores perspiciatis veniam ipsum rerum saepe\", \"userId\": 7}, {\"completed\": true, \"id\": 127, \"title\": \"voluptatem libero consectetur rerum ut\", \"userId\": 7}, {\"completed\": false, \"id\": 128, \"title\": \"eius omnis est qui voluptatem autem\", \"userId\": 7}, {\"completed\": false, \"id\": 129, \"title\": \"rerum culpa quis harum\", \"userId\": 7}, {\"completed\": true, \"id\": 130, \"title\": \"nulla aliquid eveniet harum laborum libero alias ut unde\", \"userId\": 7}, {\"completed\": false, \"id\": 131, \"title\": \"qui ea incidunt quis\", \"userId\": 7}, {\"completed\": true, \"id\": 132, \"title\": \"qui molestiae voluptatibus velit iure harum quisquam\", \"userId\": 7}, {\"completed\": true, \"id\": 133, \"title\": \"et labore eos enim rerum consequatur sunt\", \"userId\": 7}, {\"completed\": false, \"id\": 134, \"title\": \"molestiae doloribus et laborum quod ea\", \"userId\": 7}, {\"completed\": false, \"id\": 135, \"title\": \"facere ipsa nam eum voluptates reiciendis vero qui\", \"userId\": 7}, {\"completed\": false, \"id\": 136, \"title\": \"asperiores illo tempora fuga sed ut quasi adipisci\", \"userId\": 7}, {\"completed\": false, \"id\": 137, \"title\": \"qui sit non\", \"userId\": 7}, {\"completed\": true, \"id\": 138, \"title\": \"placeat minima consequatur rem qui ut\", \"userId\": 7}, {\"completed\": false, \"id\": 139, \"title\": \"consequatur doloribus id possimus voluptas a voluptatem\", \"userId\": 7}, {\"completed\": true, \"id\": 140, \"title\": \"aut consectetur in blanditiis deserunt quia sed laboriosam\", \"userId\": 7}, {\"completed\": true, \"id\": 141, \"title\": \"explicabo consectetur debitis voluptates quas quae culpa rerum non\", \"userId\": 8}, {\"completed\": true, \"id\": 142, \"title\": \"maiores accusantium architecto necessitatibus reiciendis ea aut\", \"userId\": 8}, {\"completed\": false, \"id\": 143, \"title\": \"eum non recusandae cupiditate animi\", \"userId\": 8}, {\"completed\": false, \"id\": 144, \"title\": \"ut eum exercitationem sint\", \"userId\": 8}, {\"completed\": false, \"id\": 145, \"title\": \"beatae qui ullam incidunt voluptatem non nisi aliquam\", \"userId\": 8}, {\"completed\": true, \"id\": 146, \"title\": \"molestiae suscipit ratione nihil odio libero impedit vero totam\", \"userId\": 8}, {\"completed\": true, \"id\": 147, \"title\": \"eum itaque quod reprehenderit et facilis dolor autem ut\", \"userId\": 8}, {\"completed\": false, \"id\": 148, \"title\": \"esse quas et quo quasi exercitationem\", \"userId\": 8}, {\"completed\": false, \"id\": 149, \"title\": \"animi voluptas quod perferendis est\", \"userId\": 8}, {\"completed\": false, \"id\": 150, \"title\": \"eos amet tempore laudantium fugit a\", \"userId\": 8}, {\"completed\": true, \"id\": 151, \"title\": \"accusamus adipisci dicta qui quo ea explicabo sed vero\", \"userId\": 8}, {\"completed\": false, \"id\": 152, \"title\": \"odit eligendi recusandae doloremque cumque non\", \"userId\": 8}, {\"completed\": false, \"id\": 153, \"title\": \"ea aperiam consequatur qui repellat eos\", \"userId\": 8}, {\"completed\": true, \"id\": 154, \"title\": \"rerum non ex sapiente\", \"userId\": 8}, {\"completed\": true, \"id\": 155, \"title\": \"voluptatem nobis consequatur et assumenda magnam\", \"userId\": 8}, {\"completed\": true, \"id\": 156, \"title\": \"nam quia quia nulla repellat assumenda quibusdam sit nobis\", \"userId\": 8}, {\"completed\": true, \"id\": 157, \"title\": \"dolorem veniam quisquam deserunt repellendus\", \"userId\": 8}, {\"completed\": true, \"id\": 158, \"title\": \"debitis vitae delectus et harum accusamus aut deleniti a\", \"userId\": 8}, {\"completed\": true, \"id\": 159, \"title\": \"debitis adipisci quibusdam aliquam sed dolore ea praesentium nobis\", \"userId\": 8}, {\"completed\": false, \"id\": 160, \"title\": \"et praesentium aliquam est\", \"userId\": 8}, {\"completed\": true, \"id\": 161, \"title\": \"ex hic consequuntur earum omnis alias ut occaecati culpa\", \"userId\": 9}, {\"completed\": true, \"id\": 162, \"title\": \"omnis laboriosam molestias animi sunt dolore\", \"userId\": 9}, {\"completed\": false, \"id\": 163, \"title\": \"natus corrupti maxime laudantium et voluptatem laboriosam odit\", \"userId\": 9}, {\"completed\": false, \"id\": 164, \"title\": \"reprehenderit quos aut aut consequatur est sed\", \"userId\": 9}, {\"completed\": false, \"id\": 165, \"title\": \"fugiat perferendis sed aut quidem\", \"userId\": 9}, {\"completed\": false, \"id\": 166, \"title\": \"quos quo possimus suscipit minima ut\", \"userId\": 9}, {\"completed\": false, \"id\": 167, \"title\": \"et quis minus quo a asperiores molestiae\", \"userId\": 9}, {\"completed\": false, \"id\": 168, \"title\": \"recusandae quia qui sunt libero\", \"userId\": 9}, {\"completed\": true, \"id\": 169, \"title\": \"ea odio perferendis officiis\", \"userId\": 9}, {\"completed\": false, \"id\": 170, \"title\": \"quisquam aliquam quia doloribus aut\", \"userId\": 9}, {\"completed\": true, \"id\": 171, \"title\": \"fugiat aut voluptatibus corrupti deleniti velit iste odio\", \"userId\": 9}, {\"completed\": false, \"id\": 172, \"title\": \"et provident amet rerum consectetur et voluptatum\", \"userId\": 9}, {\"completed\": false, \"id\": 173, \"title\": \"harum ad aperiam quis\", \"userId\": 9}, {\"completed\": false, \"id\": 174, \"title\": \"similique aut quo\", \"userId\": 9}, {\"completed\": true, \"id\": 175, \"title\": \"laudantium eius officia perferendis provident perspiciatis asperiores\", \"userId\": 9}, {\"completed\": false, \"id\": 176, \"title\": \"magni soluta corrupti ut maiores rem quidem\", \"userId\": 9}, {\"completed\": false, \"id\": 177, \"title\": \"et placeat temporibus voluptas est tempora quos quibusdam\", \"userId\": 9}, {\"completed\": true, \"id\": 178, \"title\": \"nesciunt itaque commodi tempore\", \"userId\": 9}, {\"completed\": true, \"id\": 179, \"title\": \"omnis consequuntur cupiditate impedit itaque ipsam quo\", \"userId\": 9}, {\"completed\": true, \"id\": 180, \"title\": \"debitis nisi et dolorem repellat et\", \"userId\": 9}, {\"completed\": false, \"id\": 181, \"title\": \"ut cupiditate sequi aliquam fuga maiores\", \"userId\": 10}, {\"completed\": true, \"id\": 182, \"title\": \"inventore saepe cumque et aut illum enim\", \"userId\": 10}, {\"completed\": true, \"id\": 183, \"title\": \"omnis nulla eum aliquam distinctio\", \"userId\": 10}, {\"completed\": false, \"id\": 184, \"title\": \"molestias modi perferendis perspiciatis\", \"userId\": 10}, {\"completed\": false, \"id\": 185, \"title\": \"voluptates dignissimos sed doloribus animi quaerat aut\", \"userId\": 10}, {\"completed\": false, \"id\": 186, \"title\": \"explicabo odio est et\", \"userId\": 10}, {\"completed\": false, \"id\": 187, \"title\": \"consequuntur animi possimus\", \"userId\": 10}, {\"completed\": true, \"id\": 188, \"title\": \"vel non beatae est\", \"userId\": 10}, {\"completed\": true, \"id\": 189, \"title\": \"culpa eius et voluptatem et\", \"userId\": 10}, {\"completed\": true, \"id\": 190, \"title\": \"accusamus sint iusto et voluptatem exercitationem\", \"userId\": 10}, {\"completed\": true, \"id\": 191, \"title\": \"temporibus atque distinctio omnis eius impedit tempore molestias pariatur\", \"userId\": 10}, {\"completed\": false, \"id\": 192, \"title\": \"ut quas possimus exercitationem sint voluptates\", \"userId\": 10}, {\"completed\": true, \"id\": 193, \"title\": \"rerum debitis voluptatem qui eveniet tempora distinctio a\", \"userId\": 10}, {\"completed\": false, \"id\": 194, \"title\": \"sed ut vero sit molestiae\", \"userId\": 10}, {\"completed\": true, \"id\": 195, \"title\": \"rerum ex veniam mollitia voluptatibus pariatur\", \"userId\": 10}, {\"completed\": true, \"id\": 196, \"title\": \"consequuntur aut ut fugit similique\", \"userId\": 10}, {\"completed\": true, \"id\": 197, \"title\": \"dignissimos quo nobis earum saepe\", \"userId\": 10}, {\"completed\": true, \"id\": 198, \"title\": \"quis eius est sint explicabo\", \"userId\": 10}, {\"completed\": true, \"id\": 199, \"title\": \"numquam repellendus a magnam\", \"userId\": 10}, {\"completed\": false, \"id\": 200, \"title\": \"ipsam aperiam voluptates qui\", \"userId\": 10}]", - "isBase64Encoded": false, - "multiValueHeaders": { - "Content-Type": [ - "application/json" - ] - } -} diff --git a/examples/event_handler_validation/src/requirements.txt b/examples/event_handler_validation/src/requirements.txt deleted file mode 100644 index 867051207bb..00000000000 --- a/examples/event_handler_validation/src/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -requests -aws_lambda_powertools[tracer] -pydantic[email] diff --git a/examples/event_handler_validation/src/swagger.py b/examples/event_handler_validation/src/swagger.py deleted file mode 100644 index 2a324b2db67..00000000000 --- a/examples/event_handler_validation/src/swagger.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import List - -import requests -from pydantic import BaseModel, EmailStr, Field - -from aws_lambda_powertools.event_handler import APIGatewayRestResolver -from aws_lambda_powertools.utilities.typing import LambdaContext - -app = APIGatewayRestResolver(enable_validation=True) -app.enable_swagger() - - -class Todo(BaseModel): - userId: int - id_: int = Field(alias="id") - title: str - completed: bool - - -@app.get("/todos") -def get_todos_by_email(email: EmailStr) -> List[Todo]: - todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}") - todos.raise_for_status() - - return todos.json() - - -def lambda_handler(event: dict, context: LambdaContext) -> dict: - return app.resolve(event, context) diff --git a/examples/event_handler_validation/src/swagger_customize.py b/examples/event_handler_validation/src/swagger_customize.py deleted file mode 100644 index 4903ff25443..00000000000 --- a/examples/event_handler_validation/src/swagger_customize.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import List - -import requests -from pydantic import BaseModel, EmailStr, Field - -from aws_lambda_powertools.event_handler import APIGatewayRestResolver -from aws_lambda_powertools.utilities.typing import LambdaContext - -app = APIGatewayRestResolver(enable_validation=True) -app.enable_swagger(path="/_swagger", swagger_base_url="https://cdn.example.com/path/to/assets/") - - -class Todo(BaseModel): - userId: int - id_: int = Field(alias="id") - title: str - completed: bool - - -@app.get("/todos") -def get_todos_by_email(email: EmailStr) -> List[Todo]: - todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}") - todos.raise_for_status() - - return todos.json() - - -def lambda_handler(event: dict, context: LambdaContext) -> dict: - return app.resolve(event, context) diff --git a/examples/event_handler_validation/src/swagger_middlewares.py b/examples/event_handler_validation/src/swagger_middlewares.py deleted file mode 100644 index 49822fecefe..00000000000 --- a/examples/event_handler_validation/src/swagger_middlewares.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import List - -import requests -from pydantic import BaseModel, EmailStr, Field - -from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response -from aws_lambda_powertools.event_handler.middlewares import NextMiddleware -from aws_lambda_powertools.utilities.typing import LambdaContext - -app = APIGatewayRestResolver(enable_validation=True) - - -def swagger_middleware(app: APIGatewayRestResolver, next_middleware: NextMiddleware) -> Response: - is_authenticated = ... - if not is_authenticated: - return Response(status_code=400, body="Unauthorized") - - return next_middleware(app) - - -app.enable_swagger(middlewares=[swagger_middleware]) - - -class Todo(BaseModel): - userId: int - id_: int = Field(alias="id") - title: str - completed: bool - - -@app.get("/todos") -def get_todos_by_email(email: EmailStr) -> List[Todo]: - todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}") - todos.raise_for_status() - - return todos.json() - - -def lambda_handler(event: dict, context: LambdaContext) -> dict: - return app.resolve(event, context) diff --git a/examples/event_handler_validation/src/validating_query_strings.py b/examples/event_handler_validation/src/validating_query_strings.py deleted file mode 100644 index cd15e1ab683..00000000000 --- a/examples/event_handler_validation/src/validating_query_strings.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import Annotated, Optional - -import requests -from pydantic import BaseModel, Field - -from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.event_handler import APIGatewayRestResolver -from aws_lambda_powertools.event_handler.openapi.params import Query -from aws_lambda_powertools.logging import correlation_paths -from aws_lambda_powertools.utilities.typing import LambdaContext - -tracer = Tracer() -logger = Logger() -app = APIGatewayRestResolver(enable_validation=True) - - -class Todo(BaseModel): - userId: int - id_: Optional[int] = Field(alias="id", default=None) - title: str - completed: bool - - -@app.get("/todos") -@tracer.capture_method -def get_todos(completed: Annotated[Optional[str], Query(min_length=4)] = None) -> Todo: - url = "https://jsonplaceholder.typicode.com/todos" - - if completed is not None: - url = f"{url}/?completed={completed}" - - todo = requests.get(url) - todo.raise_for_status() - - return Todo(**todo.json()[0]) - - -@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) -@tracer.capture_lambda_handler -def lambda_handler(event: dict, context: LambdaContext) -> dict: - return app.resolve(event, context) From 7fc2c501e3e800ddb0fe3bb7a10734bd47ce1432 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Thu, 23 Nov 2023 12:04:12 +0100 Subject: [PATCH 30/36] docs: fix swagger UI example Signed-off-by: heitorlessa --- docs/core/event_handler/api_gateway.md | 2 +- .../src/enabling_swagger.py | 21 ++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 64d1449bd9c..87da283c2bb 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -484,7 +484,7 @@ There are some important **caveats** that you should know before enabling it: | **No micro-functions support** yet | Swagger UI is enabled on a per resolver instance which will limit its accuracy here. | | You need to expose **new routes** | You'll need to expose the following paths to Lambda: `/swagger`, `/swagger.css`, `/swagger.js`; ignore if you're routing all paths already. | -```python hl_lines="9 10" title="enabling_swagger.py" +```python hl_lines="12-13" title="enabling_swagger.py" --8<-- "examples/event_handler_rest/src/enabling_swagger.py" ``` diff --git a/examples/event_handler_rest/src/enabling_swagger.py b/examples/event_handler_rest/src/enabling_swagger.py index c91bac04f55..b624af77d32 100644 --- a/examples/event_handler_rest/src/enabling_swagger.py +++ b/examples/event_handler_rest/src/enabling_swagger.py @@ -1,11 +1,14 @@ from typing import List import requests -from pydantic import BaseModel, EmailStr, Field +from pydantic import BaseModel, Field +from aws_lambda_powertools import Logger, Tracer from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.utilities.typing import LambdaContext +tracer = Tracer() +logger = Logger() app = APIGatewayRestResolver(enable_validation=True) app.enable_swagger(path="/swagger") # (1)! @@ -17,12 +20,20 @@ class Todo(BaseModel): completed: bool +@app.post("/todos") +def create_todo(todo: Todo) -> str: + response = requests.post("https://jsonplaceholder.typicode.com/todos", json=todo.dict(by_alias=True)) + response.raise_for_status() + + return response.json()["id"] + + @app.get("/todos") -def get_todos_by_email(email: EmailStr) -> List[Todo]: - todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}") - todos.raise_for_status() +def get_todos() -> List[Todo]: + todo = requests.get("https://jsonplaceholder.typicode.com/todos") + todo.raise_for_status() - return todos.json() + return todo.json() def lambda_handler(event: dict, context: LambdaContext) -> dict: From 0ebc72d095426e60971ed998333dcc44d9ba073a Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Thu, 23 Nov 2023 12:06:36 +0100 Subject: [PATCH 31/36] docs: add note that dataclasses are supported too Signed-off-by: heitorlessa --- docs/core/event_handler/api_gateway.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 87da283c2bb..e0f0fd903ce 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -233,7 +233,9 @@ If you need to accept multiple HTTP methods in a single function, you can use th ### Data validation !!! note "This changes the authoring experience by relying on Python's type annotations" - It's inspired by [FastAPI framework](https://fastapi.tiangolo.com/){target="_blank" rel="nofollow"} for ergonomics and to ease migrations in either direction. + It's inspired by [FastAPI framework](https://fastapi.tiangolo.com/){target="_blank" rel="nofollow"} for ergonomics and to ease migrations in either direction. We support both Pydantic models and Python's dataclass. + + For brevity, we'll focus on Pydantic only. All resolvers can optionally coerce and validate incoming requests by setting `enable_validation=True`. From cc55bf35ccca70ee41a5c0fb4a5366cde81050e9 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Thu, 23 Nov 2023 13:49:32 +0100 Subject: [PATCH 32/36] docs: add swagger UI routes for API GW Signed-off-by: heitorlessa --- .../sam/template.yaml | 4 ++-- examples/event_handler_rest/sam/template.yaml | 20 ++++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/examples/event_handler_lambda_function_url/sam/template.yaml b/examples/event_handler_lambda_function_url/sam/template.yaml index 457f68c3816..0c58c9e87c1 100644 --- a/examples/event_handler_lambda_function_url/sam/template.yaml +++ b/examples/event_handler_lambda_function_url/sam/template.yaml @@ -5,7 +5,7 @@ Description: Hello world event handler Lambda Function URL Globals: Function: Timeout: 5 - Runtime: python3.9 + Runtime: python3.11 Tracing: Active Environment: Variables: @@ -28,4 +28,4 @@ Resources: CodeUri: ../src Description: API handler function FunctionUrlConfig: - AuthType: NONE # AWS_IAM for added security beyond sample documentation + AuthType: NONE # AWS_IAM for added security beyond sample documentation diff --git a/examples/event_handler_rest/sam/template.yaml b/examples/event_handler_rest/sam/template.yaml index 41001011a56..8148dfa491c 100644 --- a/examples/event_handler_rest/sam/template.yaml +++ b/examples/event_handler_rest/sam/template.yaml @@ -13,7 +13,7 @@ Globals: - "*~1*" # converts to */* for any binary type Function: Timeout: 5 - Runtime: python3.9 + Runtime: python3.11 Tracing: Active Environment: Variables: @@ -54,3 +54,21 @@ Resources: # Properties: # Path: /todos # Method: POST + + ## Swagger UI specific routes + + # SwaggerUI: + # Type: Api + # Properties: + # Path: /swagger + # Method: GET + # SwaggerUICSS: + # Type: Api + # Properties: + # Path: /swagger.css + # Method: GET + # SwaggerUIJS: + # Type: Api + # Properties: + # Path: /swagger.js + # Method: GET From 27beb90eb438622eef320c85d0b60c17505e07b8 Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Thu, 23 Nov 2023 14:13:50 +0100 Subject: [PATCH 33/36] chore: remove openapi docs for now --- docs/core/event_handler/api_gateway.md | 36 +-- docs/core/event_handler/api_gateway/index.md | 8 - docs/core/event_handler/openapi.md | 299 ------------------- mkdocs.yml | 4 +- 4 files changed, 19 insertions(+), 328 deletions(-) delete mode 100644 docs/core/event_handler/api_gateway/index.md delete mode 100644 docs/core/event_handler/openapi.md diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index e0f0fd903ce..c92558cd867 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -9,7 +9,7 @@ Event handler for Amazon API Gateway REST and HTTP APIs, Application Loader Bala * Lightweight routing to reduce boilerplate for API Gateway REST/HTTP API, ALB and Lambda Function URLs. * Support for CORS, binary and Gzip compression, Decimals JSON encoding and bring your own JSON serializer -* Built-in integration with [Event Source Data Classes utilities](../../../utilities/data_classes.md){target="_blank"} for self-documented event schema +* Built-in integration with [Event Source Data Classes utilities](../../utilities/data_classes.md){target="_blank"} for self-documented event schema * Works with micro function (one or a few routes) and monolithic functions (all routes) * Support for OpenAPI and data validation for requests/responses @@ -20,7 +20,7 @@ Event handler for Amazon API Gateway REST and HTTP APIs, Application Loader Bala ### Install -!!! info "This is not necessary if you're installing Powertools for AWS Lambda (Python) via [Lambda Layer/SAR](../index.md#lambda-layer){target="_blank"}." +!!! info "This is not necessary if you're installing Powertools for AWS Lambda (Python) via [Lambda Layer/SAR](../../index.md#lambda-layer){target="_blank"}." **When using the data validation feature**, you need to add `pydantic` as a dependency in your preferred tool _e.g., requirements.txt, pyproject.toml_. @@ -416,7 +416,7 @@ For example, we could validate that `` dynamic path should be no greate ### 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`. +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`. That is why you see `app.resolve(event, context)` in every example. This allows Event Handler to resolve requests, and expose data like `app.lambda_context` and `app.current_event`. @@ -494,7 +494,7 @@ There are some important **caveats** that you should know before enabling it: Here's an example of what it looks like by default: -![Swagger UI picture](../../../media/swagger.png) +![Swagger UI picture](../../media/swagger.png) ### Custom Domain API Mappings @@ -651,8 +651,8 @@ Here's a sample middleware that extracts and injects correlation ID, using `APIG #### Global middlewares

-![Combining middlewares](../../../media/middlewares_normal_processing-light.svg#only-light) -![Combining middlewares](../../../media/middlewares_normal_processing-dark.svg#only-dark) +![Combining middlewares](../../media/middlewares_normal_processing-light.svg#only-light) +![Combining middlewares](../../media/middlewares_normal_processing-dark.svg#only-dark) _Request flowing through multiple registered middlewares_
@@ -692,8 +692,8 @@ Event Handler **calls global middlewares first**, then middlewares defined at th #### Returning early
-![Short-circuiting middleware chain](../../../media/middlewares_early_return-light.svg#only-light) -![Short-circuiting middleware chain](../../../media/middlewares_early_return-dark.svg#only-dark) +![Short-circuiting middleware chain](../../media/middlewares_early_return-light.svg#only-light) +![Short-circuiting middleware chain](../../media/middlewares_early_return-dark.svg#only-dark) _Interrupting request flow by returning early_
@@ -740,8 +740,8 @@ While there isn't anything special on how to use [`try/catch`](https://docs.pyth An exception wasn't caught by any middleware during `next_middleware()` block, therefore it propagates all the way back to the client as HTTP 500.
- ![Unhandled exceptions](../../../media/middlewares_unhandled_route_exception-light.svg#only-light) - ![Unhandled exceptions](../../../media/middlewares_unhandled_route_exception-dark.svg#only-dark) + ![Unhandled exceptions](../../media/middlewares_unhandled_route_exception-light.svg#only-light) + ![Unhandled exceptions](../../media/middlewares_unhandled_route_exception-dark.svg#only-dark) _Unhandled route exceptions propagate back to the client_
@@ -751,8 +751,8 @@ While there isn't anything special on how to use [`try/catch`](https://docs.pyth An exception was only caught by the third middleware, resuming the normal execution of each `After` logic for the second and first middleware.
- ![Middleware handling exceptions](../../../media/middlewares_catch_route_exception-light.svg#only-light) - ![Middleware handling exceptions](../../../media/middlewares_catch_route_exception-dark.svg#only-dark) + ![Middleware handling exceptions](../../media/middlewares_catch_route_exception-light.svg#only-light) + ![Middleware handling exceptions](../../media/middlewares_catch_route_exception-dark.svg#only-dark) _Unhandled route exceptions propagate back to the client_
@@ -762,8 +762,8 @@ While there isn't anything special on how to use [`try/catch`](https://docs.pyth The third middleware short-circuited the chain by raising an exception and completely skipping the fourth middleware. Because we only caught it in the first middleware, it skipped the `After` logic in the second middleware.
- ![Catching exceptions](../../../media/middlewares_catch_exception-light.svg#only-light) - ![Catching exceptions](../../../media/middlewares_catch_exception-dark.svg#only-dark) + ![Catching exceptions](../../media/middlewares_catch_exception-light.svg#only-light) + ![Catching exceptions](../../media/middlewares_catch_exception-dark.svg#only-dark) _Middleware handling short-circuit exceptions_
@@ -792,7 +792,7 @@ These are native middlewares that may become native features depending on custom | Middleware | Purpose | | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -| [SchemaValidationMiddleware](/lambda/python/latest/api/event_handler/middlewares/schema_validation.html){target="_blank"} | Validates API request body and response against JSON Schema, using [Validation utility](../../../utilities/validation.md){target="_blank"} | +| [SchemaValidationMiddleware](/lambda/python/latest/api/event_handler/middlewares/schema_validation.html){target="_blank"} | Validates API request body and response against JSON Schema, using [Validation utility](../../utilities/validation.md){target="_blank"} | #### Being a good citizen @@ -900,7 +900,7 @@ Like `compress` feature, the client must send the `Accept` header with the corre ### Debug mode -You can enable debug mode via `debug` param, or via `POWERTOOLS_DEV` [environment variable](../../../index.md#environment-variables){target="_blank"}. +You can enable debug mode via `debug` param, or via `POWERTOOLS_DEV` [environment variable](../../index.md#environment-variables){target="_blank"}. This will enable full tracebacks errors in the response, print request and responses, and set CORS in development mode. @@ -1156,7 +1156,7 @@ Both single (monolithic) and multiple functions (micro) offer different set of t #### Monolithic function -![Monolithic function sample](./../../../media/monolithic-function.png) +![Monolithic function sample](./../../media/monolithic-function.png) A monolithic function means that your final code artifact will be deployed to a single function. This is generally the best approach to start. @@ -1175,7 +1175,7 @@ _**Downsides**_ #### Micro function -![Micro function sample](./../../../media/micro-function.png) +![Micro function sample](./../../media/micro-function.png) A micro function means that your final code artifact will be different to each function deployed. This is generally the approach to start if you're looking for fine-grain control and/or high load on certain parts of your service. diff --git a/docs/core/event_handler/api_gateway/index.md b/docs/core/event_handler/api_gateway/index.md deleted file mode 100644 index 58987e718b4..00000000000 --- a/docs/core/event_handler/api_gateway/index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: REST API -description: Core utility ---- - - - ---8<-- "docs/core/event_handler/api_gateway.md" diff --git a/docs/core/event_handler/openapi.md b/docs/core/event_handler/openapi.md deleted file mode 100644 index b29cef07465..00000000000 --- a/docs/core/event_handler/openapi.md +++ /dev/null @@ -1,299 +0,0 @@ ---- -title: Validation & OpenAPI -description: Core utility ---- - -Comprehensive data validation and OpenAPI generation based on introspection of Python types. - -## Key Features - -* Declarative validation of inputs and outputs -* Support for scalar types, dataclasses and Pydantic models -* Automatic generation of OpenAPI specifications from the API definition -* Embedded Swagger UI for interactive API documentation - -## Getting started - -To use validation and OpenAPI features with our core utility, you must first ensure that pydantic is installed as it is a prerequisite. This utility framework supports both pydantic version 1 and version 2. For detailed guidance on setting up the parser, visit the [Parser documentation](./../../../../utilities/parser/#getting-started). - -This documentation specifically focuses on the utility's validation and OpenAPI capabilities. These features are built on top of the Event Handler utility, thereby streamlining the process of validating inputs and outputs and automatically generating OpenAPI specifications based on your API definitions. - -### Basic usage - -Enable the REST API's validation by setting the `enable_validation` parameter in your API resolver. This changes how your resolver is called. -Powertools examines your handler to pinpoint input and output parameters, then validates and coerces the data before it reaches your handler. - -=== "getting_started.py" - - ```python hl_lines="10 13 15 19" - --8<-- "examples/event_handler_validation/src/getting_started.py" - ``` - -=== "event.json" - - ```json hl_lines="4" - --8<-- "examples/event_handler_validation/src/getting_started.json" - ``` - -=== "output.json" - - ```json hl_lines="2 8" - --8<-- "examples/event_handler_validation/src/getting_started_output.json" - ``` - -When data fails to match the input schema during validation, a validation error occurs, leading to an HTTP 422 Unprocessable Entity error, signaling that the server understood the input but found invalid fields. - -Below is a sample error response for failed validation due to incorrect input: - -=== "bad_input_event.json" - - ```json hl_lines="4" - --8<-- "examples/event_handler_validation/src/getting_started_bad_input.json" - ``` - -=== "output.json" - - ```json hl_lines="2 3" - --8<-- "examples/event_handler_validation/src/getting_started_bad_input_output.json" - ``` - -???+ note "Pydantic v1 vs v2" - Pydantic versions 1 and 2 may report validation errors differently. Refer to the documentation for your specific version to grasp the precise format and style of the error messages. - -### Using Pydantic models - -Pydantic models allow you to define complex data structures and validation rules. Use these models as input parameters or return types to leverage Pydantic's features like data coercion, default values, and advanced validation. - -Here's how to use Pydantic models: - -=== "getting_started_pydantic.py" - - ```python hl_lines="9 12 20 24" - --8<-- "examples/event_handler_validation/src/getting_started_pydantic.py" - ``` - -=== "event.json" - - ```json hl_lines="4 5 15" - --8<-- "examples/event_handler_validation/src/getting_started_pydantic.json" - ``` - -=== "output.json" - - ```json hl_lines="2 3" - --8<-- "examples/event_handler_validation/src/getting_started_pydantic_output.json" - ``` - -### SwaggerUI - -Swagger UI offers a web interface for visualizing and interacting with your API's resources. Enable Swagger UI to generate an interactive documentation page for testing and exploring your API endpoints in real time. - -???+ warning "Publicly accessible by default" - Enabling Swagger UI makes it public. To protect sensitive API endpoints or restrict documentation access, consider implementing authentication or other security measures. - See the [Customize the Swagger UI](#customizing-the-swagger-ui) section for instructions on customizing and securing your Swagger UI to meet your needs and safeguard your interactive API documentation. - -```python hl_lines="9 10" ---8<-- "examples/event_handler_validation/src/swagger.py" -``` - -Here's an example of what it looks like by default: - -![Swagger UI](../../media/swagger.png) - -## Advanced - -### Customizing parameters - -Use annotations to add metadata and validation constraints to your API's parameters, improving functionality and documentation. Python's [Annotated type from PEP 593](https://peps.python.org/pep-0593/) lets you append metadata to type hints for use by validation libraries or documentation tools. - -For URL path, query string, or request body parameters, use specialized classes or decorators to define parameters with defaults, validation rules, and descriptions for enhanced OpenAPI output. - -Below is an example of customizing API parameters with annotations: - -```python hl_lines="1 7 19 20" ---8<-- "examples/event_handler_validation/src/customizing_parameters.py" -``` - -???+ note - Powertools doesn't have support for files, form data, and header parameters at the moment. If you're interested in this, please [open an issue](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=feature-request%2Ctriage&projects=&template=feature_request.yml&title=Feature+request%3A+TITLE). - -Titles and descriptions clarify parameter use and constraints for both end-users and developers. In OpenAPI documentation tools, these annotations become readable descriptions, offering a self-explanatory API interface. This enhances the developer experience and eases the learning curve for new API users. - -Below is a table detailing all possible parameter customizations: - -| Field name | Type | Description | -|-----------------------|---------------|---------------------------------------------------------------------------------------------------------------------------------------------| -| `alias` | `str` | Alternative name for a field, used when serializing and deserializing data | -| `validation_alias` | `str` | Alternative name for a field during validation (but not serialization) | -| `serialization_alias` | `str` | Alternative name for a field during serialization (but not during validation) | -| `description` | `str` | Human-readable description | -| `gt` | `float` | Greater than. If set, value must be greater than this. Only applicable to numbers | -| `ge` | `float` | Greater than or equal. If set, value must be greater than or equal to this. Only applicable to numbers | -| `lt` | `float` | Less than. If set, value must be less than this. Only applicable to numbers | -| `le` | `float` | Less than or equal. If set, value must be less than or equal to this. Only applicable to numbers | -| `min_length` | `int` | Minimum length for strings | -| `max_length` | `int` | Maximum length for strings | -| `pattern` | `string` | A regular expression that the string must match. | -| `strict` | `bool` | If `True`, strict validation is applied to the field. See [Strict Mode](https://docs.pydantic.dev/latest/concepts/strict_mode/) for details | -| `multiple_of` | `float` | Value must be a multiple of this. Only applicable to numbers | -| `allow_inf_nan` | `bool` | Allow `inf`, `-inf`, `nan`. Only applicable to numbers | -| `max_digits` | `int` | Maximum number of allow digits for strings | -| `decimal_places` | `int` | Maximum number of decimal places allowed for numbers | -| `examples` | `List\[Any\]` | List of examples of the field | -| `deprecated` | `bool` | Marks the field as deprecated | -| `include_in_schema` | `bool` | If `False` the field will not be part of the exported OpenAPI schema | -| `json_schema_extra` | `JsonDict` | Any additional JSON schema data for the schema property | - -### Body parameters - -With Pydantic models, managing JSON objects in API request bodies is straightforward. The models you define automatically parse request bodies, confirming that received data structures match your API's specifications. - -To define and parse body parameters with a Pydantic model, follow this example: - -=== "body_parsing.py" - - ```python hl_lines="12 19 20" - --8<-- "examples/event_handler_validation/src/body_parsing.py" - ``` - -=== "event.json" - - ```json hl_lines="21 22 33" - --8<-- "examples/event_handler_validation/src/body_parsing.json" - ``` - -=== "output.json" - - ```json hl_lines="3" - --8<-- "examples/event_handler_validation/src/body_parsing_output.json" - ``` - -When you use the Body wrapper with the `embed` option, nest your JSON payload under a key that corresponds to the parameter name. - -=== "body_parsing_embed.py" - - ```python hl_lines="1 7 21" - --8<-- "examples/event_handler_validation/src/body_parsing_embed.py" - ``` - -=== "event.json" - - ```json hl_lines="21 22 33" - --8<-- "examples/event_handler_validation/src/body_parsing_embed.json" - ``` - -=== "output.json" - - ```json hl_lines="3" - --8<-- "examples/event_handler_validation/src/body_parsing_embed_output.json" - ``` - -### Customizing API operations - -Customize your API endpoints by adding metadata to endpoint definitions. This provides descriptive documentation for API consumers and gives extra instructions to the framework. - -Here's a breakdown of various customizable fields: - -| Field Name | Type | Description | -|------------------------|-----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `summary` | `str` | A concise overview of the main functionality of the endpoint. This brief introduction is usually displayed in autogenerated API documentation and helps consumers quickly understand what the endpoint does. | -| `description` | `str` | A more detailed explanation of the endpoint, which can include information about the operation's behavior, including side effects, error states, and other operational guidelines. | -| `responses` | `Dict[int, Dict[str, Any]]` | A dictionary that maps each HTTP status code to a Response Object as defined by the [OpenAPI Specification](https://swagger.io/specification/#response-object). This allows you to describe expected responses, including default or error messages, and their corresponding schemas for different status codes. | -| `response_description` | `str` | Provides the default textual description of the response sent by the endpoint when the operation is successful. It is intended to give a human-readable understanding of the result. | -| `tags` | `List[str]` | Tags are a way to categorize and group endpoints within the API documentation. They can help organize the operations by resources or other heuristic. | -| `operation_id` | `str` | A unique identifier for the operation, which can be used for referencing this operation in documentation or code. This ID must be unique across all operations described in the API. | -| `include_in_schema` | `bool` | A boolean value that determines whether or not this operation should be included in the OpenAPI schema. Setting it to `False` can hide the endpoint from generated documentation and schema exports, which might be useful for private or experimental endpoints. | - -To implement these customizations, include extra parameters when defining your routes: - -=== "Customizing API operations metadata" - - ```python hl_lines="11-20" - --8<-- "examples/event_handler_validation/src/customizing_operations.py" - ``` - -### Generating OpenAPI specifications - -OpenAPI specifications detail web APIs, covering routes, parameters, responses, etc. They can be auto-generated from your code, keeping them synchronized with your API's actual implementation. - -Powertools allows exporting these specifications as a Pydantic object or a JSON schema string: - -=== "OpenAPI specification as a Pydantic object" - - ```python hl_lines="32" - --8<-- "examples/event_handler_validation/src/generate_openapi_spec.py" - ``` - -=== "OpenAPI specification as a JSON schema string" - - ```python hl_lines="32" - --8<-- "examples/event_handler_validation/src/generate_openapi_json_spec.py" - ``` - -=== "OpenAPI JSON schema" - - ```json - --8<-- "examples/event_handler_validation/src/generate_openapi_json_spec.json" - ``` - -???+ note "Why opt for the Pydantic object?" - The OpenAPI specification as a Pydantic object offers benefits: - - 1. **Post-Processing:** Alter or enrich the specification programmatically after generation, such as adding examples, merging specs, or updating descriptions. - 2. **Internal Use:** Use the specification within your system for validation, mocking, or other quality assurance methods, rather than exposing it externally. - 3. **Dynamic Behavior:** Manipulate the schema representation before serialization to reflect conditions like user permissions or environment variables. - 4. **Fragment Reuse:** In microservices or plugin architectures, generate partial schemas to assemble into a comprehensive API gateway schema. - 5. **Testing and Automation:** For testing, a manipulatable schema form is useful to confirm changes or automate API tests. - -#### Customizing OpenAPI metadata - -Defining and customizing OpenAPI metadata gives detailed, top-level information about your API. Here's the method to set and tailor this metadata: - -| Field Name | Type | Description | -|--------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `title` | `str` | The title for your API. It should be a concise, specific name that can be used to identify the API in documentation or listings. | -| `version` | `str` | The version of the API you are documenting. This could reflect the release iteration of the API and helps clients understand the evolution of the API. | -| `openapi_version` | `str` | Specifies the version of the OpenAPI Specification on which your API is based. For most contemporary APIs, the default value would be `3.0.0` or higher. | -| `summary` | `str` | A short and informative summary that can provide an overview of what the API does. This can be the same as or different from the title but should add context or information. | -| `description` | `str` | A verbose description that can include Markdown formatting, providing a full explanation of the API's purpose, functionalities, and general usage instructions. | -| `tags` | `List[str]` | A collection of tags that categorize endpoints for better organization and navigation within the documentation. This can group endpoints by their functionality or other criteria. | -| `servers` | `List[Server]` | An array of Server objects, which specify the URL to the server and a description for its environment (production, staging, development, etc.), providing connectivity information. | -| `terms_of_service` | `str` | A URL that points to the terms of service for your API. This could provide legal information and user responsibilities related to the usage of the API. | -| `contact` | `Contact` | A Contact object containing contact details of the organization or individuals maintaining the API. This may include fields such as name, URL, and email. | -| `license_info` | `License` | A License object providing the license details for the API, typically including the name of the license and the URL to the full license text. | - -Include extra parameters when exporting your OpenAPI specification to apply these customizations: - -=== "Customizing OpenAPI metadata" - - ```python hl_lines="25-31" - --8<-- "examples/event_handler_validation/src/customize_api_metadata.py" - ``` - -### Customizing the Swagger UI - -The Swagger UI appears by default at the `/swagger` path, but you can customize this to serve the documentation from another path and specify the source for Swagger UI assets. - -Below is an example configuration for serving Swagger UI from a custom path or CDN, with assets like CSS and JavaScript loading from a chosen CDN base URL. - -=== "Customizing Swagger path and CDN" - - ```python hl_lines="10" - --8<-- "examples/event_handler_validation/src/swagger_customize.py" - ``` - -???+note "Customizing the Swagger metadata" - The `enable_swagger` method accepts the same metadata as described at [Customizing OpenAPI metadata](#customizing-openapi-metadata). - -=== "Using middlewares with the Swagger UI" - -You can enhance these customizations by adding middleware to the Swagger UI endpoint. Middleware can handle tasks such as adding security headers, user authentication, or other request processing for serving the Swagger UI. - - ```python hl_lines="7 13-18 21" - --8<-- "examples/event_handler_validation/src/swagger_middlewares.py" - ``` - -## Testing your code - -For detailed instructions on testing your code, consult the [REST API documentation](../api_gateway/#testing-your-code). -This guide offers best practices, testing methods, and examples for writing API tests. diff --git a/mkdocs.yml b/mkdocs.yml index e9c0b05b39a..0a844fd392f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,9 +20,7 @@ nav: - Amazon CloudWatch EMF: core/metrics.md - Datadog: core/metrics/datadog.md - Event Handler: - - REST API: - - core/event_handler/api_gateway/index.md - - core/event_handler/openapi.md + - core/event_handler/api_gateway.md - core/event_handler/appsync.md - utilities/parameters.md - utilities/batch.md From d2223e4b1430818ce217a3ecfe7d3d7a63c417e2 Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Thu, 23 Nov 2023 14:27:26 +0100 Subject: [PATCH 34/36] fix: example for old pythons --- docs/core/event_handler/api_gateway.md | 2 +- examples/event_handler_rest/src/validating_payload_subset.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 30bc3ad402e..a2b204ca5cb 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -345,7 +345,7 @@ We use the `Annotated` and OpenAPI `Body` type to instruct Event Handler that ou === "validating_payload_subset.py" - ```python hl_lines="1 7 21" + ```python hl_lines="7 8 22" --8<-- "examples/event_handler_rest/src/validating_payload_subset.py" ``` diff --git a/examples/event_handler_rest/src/validating_payload_subset.py b/examples/event_handler_rest/src/validating_payload_subset.py index f47a68ea1f3..ac4ee603853 100644 --- a/examples/event_handler_rest/src/validating_payload_subset.py +++ b/examples/event_handler_rest/src/validating_payload_subset.py @@ -1,10 +1,11 @@ -from typing import Annotated, Optional +from typing import Optional import requests from pydantic import BaseModel, Field from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.event_handler.openapi.params import Body # (1)! +from aws_lambda_powertools.shared.types import Annotated from aws_lambda_powertools.utilities.typing import LambdaContext app = APIGatewayRestResolver(enable_validation=True) From 53fdc8c7bb94ec10d7a46adcbb46570c57a07bca Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Thu, 23 Nov 2023 14:35:10 +0100 Subject: [PATCH 35/36] fix: more types --- docs/core/event_handler/api_gateway.md | 2 +- examples/event_handler_rest/src/validating_query_strings.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index a2b204ca5cb..c4c9be509c6 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -380,7 +380,7 @@ In the following example, we use a new `Query` OpenAPI type to add [one out of m === "validating_query_strings.py" - ```python hl_lines="1 8 26" + ```python hl_lines="8 10 27" --8<-- "examples/event_handler_rest/src/validating_query_strings.py" ``` diff --git a/examples/event_handler_rest/src/validating_query_strings.py b/examples/event_handler_rest/src/validating_query_strings.py index 51ca39bcf3f..21d34dbd25a 100644 --- a/examples/event_handler_rest/src/validating_query_strings.py +++ b/examples/event_handler_rest/src/validating_query_strings.py @@ -1,4 +1,4 @@ -from typing import Annotated, List, Optional # (1)! +from typing import List, Optional import requests from pydantic import BaseModel, Field @@ -7,6 +7,7 @@ from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.event_handler.openapi.params import Query # (2)! from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.shared.types import Annotated # (1)! from aws_lambda_powertools.utilities.typing import LambdaContext tracer = Tracer() From 345fd0b8c892d2c2117ce87957a35a102a663162 Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Thu, 23 Nov 2023 14:40:44 +0100 Subject: [PATCH 36/36] fix: one more --- docs/core/event_handler/api_gateway.md | 2 +- examples/event_handler_rest/src/validating_path.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index c4c9be509c6..005ac3a4b7b 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -408,7 +408,7 @@ Just like we learned in [query string validation](#validating-query-strings), we For example, we could validate that `` dynamic path should be no greater than three digits. -```python hl_lines="1 8 26" title="validating_path.py" +```python hl_lines="8 10 27" title="validating_path.py" --8<-- "examples/event_handler_rest/src/validating_path.py" ``` diff --git a/examples/event_handler_rest/src/validating_path.py b/examples/event_handler_rest/src/validating_path.py index 2a8fbcbfa98..e892e1c8597 100644 --- a/examples/event_handler_rest/src/validating_path.py +++ b/examples/event_handler_rest/src/validating_path.py @@ -1,4 +1,4 @@ -from typing import Annotated, Optional +from typing import Optional import requests from pydantic import BaseModel, Field @@ -7,6 +7,7 @@ from aws_lambda_powertools.event_handler import APIGatewayRestResolver from aws_lambda_powertools.event_handler.openapi.params import Path from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.shared.types import Annotated from aws_lambda_powertools.utilities.typing import LambdaContext tracer = Tracer()