From 15488fe3656609430a7b5edab34e15e430c62134 Mon Sep 17 00:00:00 2001 From: Tony Sherman Date: Wed, 6 Sep 2023 11:33:23 -0400 Subject: [PATCH 1/8] docs: add micro function examples --- docs/core/event_handler/api_gateway.md | 26 ++++++- docs/utilities/data_classes.md | 2 +- .../src/micro_function_all_users_route.py | 65 ++++++++++++++++ .../src/micro_function_user_by_id_route.py | 74 +++++++++++++++++++ 4 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 examples/event_handler_rest/src/micro_function_all_users_route.py create mode 100644 examples/event_handler_rest/src/micro_function_user_by_id_route.py diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index dcfa38f6f9a..685669f6e10 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -624,7 +624,7 @@ A micro function means that your final code artifact will be different to each f **Benefits** * **Granular scaling**. A micro function can benefit from the [Lambda scaling model](https://docs.aws.amazon.com/lambda/latest/dg/invocation-scaling.html){target="_blank"} to scale differently depending on each part of your application. Concurrency controls and provisioned concurrency can also be used at a granular level for capacity management. -* **Discoverability**. Micro functions are easier do visualize when using distributed tracing. Their high-level architectures can be self-explanatory, and complexity is highly visible — assuming each function is named to the business purpose it serves. +* **Discoverability**. Micro functions are easier to visualize when using distributed tracing. Their high-level architectures can be self-explanatory, and complexity is highly visible — assuming each function is named to the business purpose it serves. * **Package size**. An independent function can be significant smaller (KB vs MB) depending on external dependencies it require to perform its purpose. Conversely, a monolithic approach can benefit from [Lambda Layers](https://docs.aws.amazon.com/lambda/latest/dg/invocation-layers.html){target="_blank"} to optimize builds for external dependencies. **Downsides** @@ -636,6 +636,30 @@ your development, building, deployment tooling need to accommodate the distinct * **Slower safe deployments**. Safely deploying multiple functions require coordination — AWS CodeDeploy deploys and verifies each function sequentially. This increases lead time substantially (minutes to hours) depending on the deployment strategy you choose. You can mitigate it by selectively enabling it in prod-like environments only, and where the risk profile is applicable. * Automated testing, operational and security reviews are essential to stability in either approaches. +**Example** + +Consider a simplified micro function structured REST API that has two routes: + +* `/users` - an endpoint that will return all users of the application on `GET` requests +* `/users/` - an endpoint that looks up a single users details by ID on `GET` requests + +Each endpoint will be it's own Lambda function that is configured as a [Lambda integration](https://docs.aws.amazon.com/apigateway/latest/developerguide/getting-started-with-lambda-integration.html){target="_blank"}. This allows you to set different configurations for each lambda (memory size, layers, etc.) + +=== "`/users` Endpoint" + ```python + --8<-- "examples/event_handler_rest/src/micro_function_all_users_route.py" + ``` + +=== "`/users/` Endpoint" + ```python + --8<-- "examples/event_handler_rest/src/micro_function_user_by_id_route.py" + ``` + +=== " + +???+ note + You can see some of the downsides in this example such as some code reuse. If set up with proper build tooling, the `User` class could be shared across functions. + ## Testing your code You can test your routes by passing a proxy event request with required params. diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index 7b3aa74e275..aa0e2735211 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -197,7 +197,7 @@ Use **`APIGatewayAuthorizerRequestEvent`** for type `REQUEST` and **`APIGatewayA if user.get("isAdmin", False): policy.allow_all_routes() else: - policy.allow_route(HttpVerb.GET, "/user-profile") + policy.allow_route(HttpVerb.GET.value, "/user-profile") return policy.asdict() ``` diff --git a/examples/event_handler_rest/src/micro_function_all_users_route.py b/examples/event_handler_rest/src/micro_function_all_users_route.py new file mode 100644 index 00000000000..ba6d3681220 --- /dev/null +++ b/examples/event_handler_rest/src/micro_function_all_users_route.py @@ -0,0 +1,65 @@ +import json +from http import HTTPStatus + +from aws_lambda_powertools import Logger +from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response +from aws_lambda_powertools.utilities.parser import BaseModel +from aws_lambda_powertools.utilities.typing import LambdaContext + +logger = Logger() + +# This would likely be a db lookup +users = [ + { + "user_id": "b0b2a5bf-ee1e-4c5e-9a86-91074052739e", + "email": "john.doe@example.com", + "active": True, + }, + { + "user_id": "3a9df6b1-938c-4e80-bd4a-0c966f4b1c1e", + "email": "jane.smith@example.com", + "active": True, + }, + { + "user_id": "aa0d3d09-9cb9-42b9-9e63-1fb17ea52981", + "email": "alex.wilson@example.com", + "active": True, + }, + { + "user_id": "67a6c17d-b7f0-4f79-aae0-79f4a53c113b", + "email": "lisa.johnson@example.com", + "active": True, + }, + { + "user_id": "6e85cf66-47af-4dbf-8aa2-2db3c24f29c1", + "email": "michael.brown@example.com", + "active": False, + }, +] + + +class User(BaseModel): + user_id: str + email: str + active: bool + + +app = APIGatewayRestResolver() + + +@app.get("/users") +def all_active_users(): + """HTTP Response for all active users""" + all_users = [User(**user) for user in users] + all_active_users = [user.dict() for user in all_users if user.active] + + return Response( + status_code=HTTPStatus.OK.value, + content_type="application/json", + body=json.dumps(all_active_users), + ) + + +@logger.inject_lambda_context() +def handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/micro_function_user_by_id_route.py b/examples/event_handler_rest/src/micro_function_user_by_id_route.py new file mode 100644 index 00000000000..177ddbea039 --- /dev/null +++ b/examples/event_handler_rest/src/micro_function_user_by_id_route.py @@ -0,0 +1,74 @@ +import json +from http import HTTPStatus + +from aws_lambda_powertools import Logger +from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response +from aws_lambda_powertools.utilities.parser import BaseModel +from aws_lambda_powertools.utilities.typing import LambdaContext + +logger = Logger() + +# This would likely be a db lookup +users = [ + { + "user_id": "b0b2a5bf-ee1e-4c5e-9a86-91074052739e", + "email": "john.doe@example.com", + "active": True, + }, + { + "user_id": "3a9df6b1-938c-4e80-bd4a-0c966f4b1c1e", + "email": "jane.smith@example.com", + "active": True, + }, + { + "user_id": "aa0d3d09-9cb9-42b9-9e63-1fb17ea52981", + "email": "alex.wilson@example.com", + "active": True, + }, + { + "user_id": "67a6c17d-b7f0-4f79-aae0-79f4a53c113b", + "email": "lisa.johnson@example.com", + "active": True, + }, + { + "user_id": "6e85cf66-47af-4dbf-8aa2-2db3c24f29c1", + "email": "michael.brown@example.com", + "active": False, + }, +] + + +class User(BaseModel): + user_id: str + email: str + active: bool + + +def get_user_by_id(user_id: str): + for user in users: + if user["user_id"] == user_id: + return User(**user) + + +app = APIGatewayRestResolver() + + +@app.get("/users/") +def all_active_users(user_id: str): + """HTTP Response for all active users""" + user = get_user_by_id(user_id) + + if user: + return Response( + status_code=HTTPStatus.OK.value, + content_type="application/json", + body=json.dumps(user.dict()), + ) + + else: + return Response(status_code=HTTPStatus.NO_CONTENT) + + +@logger.inject_lambda_context() +def handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) From 65e79b74217bfd11fd2edd1c7027d4e72912fd96 Mon Sep 17 00:00:00 2001 From: Tony Sherman Date: Thu, 7 Sep 2023 10:42:14 -0400 Subject: [PATCH 2/8] add sam template --- docs/core/event_handler/api_gateway.md | 5 +- .../sam/micro_function_template.yaml | 63 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 examples/event_handler_rest/sam/micro_function_template.yaml diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 685669f6e10..cb1619bcec6 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -655,7 +655,10 @@ Each endpoint will be it's own Lambda function that is configured as a [Lambda i --8<-- "examples/event_handler_rest/src/micro_function_user_by_id_route.py" ``` -=== " +=== "Micro Function Example SAM Template" + ```yaml + --8<-- "examples/event_handler_rest/sam/micro_function_template.yaml" + ``` ???+ note You can see some of the downsides in this example such as some code reuse. If set up with proper build tooling, the `User` class could be shared across functions. diff --git a/examples/event_handler_rest/sam/micro_function_template.yaml b/examples/event_handler_rest/sam/micro_function_template.yaml new file mode 100644 index 00000000000..db30023cfc2 --- /dev/null +++ b/examples/event_handler_rest/sam/micro_function_template.yaml @@ -0,0 +1,63 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + micro-function-example + +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.10 + +Resources: + # Lambda Function Solely For /users endpoint + AllUsersFunction: + Type: AWS::Serverless::Function + Properties: + Handler: app.lambda_handler + CodeUri: users + Description: Function for /users endpoint + Architectures: + - x86_64 + Tracing: Active + Events: + UsersPath: + Type: Api + Properties: + Path: /users + Method: GET + MemorySize: 512 # This Lambda Function can have a higher memory size set + Environment: + Variables: + LOG_LEVEL: INFO + Tags: + LambdaPowertools: python + + # Lambda Function Solely For /users/{id} endpoint + UserByIdFunction: + Type: AWS::Serverless::Function + Properties: + Handler: app.lambda_handler + CodeUri: users_by_id + Description: Function for /users/{id} endpoint + Architectures: + - x86_64 + Tracing: Active + Events: + UsersByIdPath: + Type: Api + Properties: + Path: /users/{id+} + Method: GET + MemorySize: 128 + Environment: + Variables: + LOG_LEVEL: INFO From 7e597955c26136c06b499487ae7301bdfa3cbd45 Mon Sep 17 00:00:00 2001 From: Tony Sherman Date: Tue, 12 Sep 2023 08:07:50 -0400 Subject: [PATCH 3/8] docs: update example to return correct response code --- .../event_handler_rest/src/micro_function_user_by_id_route.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/event_handler_rest/src/micro_function_user_by_id_route.py b/examples/event_handler_rest/src/micro_function_user_by_id_route.py index 177ddbea039..baa083d97f5 100644 --- a/examples/event_handler_rest/src/micro_function_user_by_id_route.py +++ b/examples/event_handler_rest/src/micro_function_user_by_id_route.py @@ -66,7 +66,7 @@ def all_active_users(user_id: str): ) else: - return Response(status_code=HTTPStatus.NO_CONTENT) + return Response(status_code=HTTPStatus.NOT_FOUND) @logger.inject_lambda_context() From ed3ba21de03b511bc45c8c0e98d716769d35dd17 Mon Sep 17 00:00:00 2001 From: Tony Sherman <100969281+TonySherman@users.noreply.github.com> Date: Thu, 14 Sep 2023 08:22:50 -0400 Subject: [PATCH 4/8] Add missing . Co-authored-by: Leandro Damascena Signed-off-by: Tony Sherman <100969281+TonySherman@users.noreply.github.com> --- 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 f7d838ce882..161b3b8d4b5 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -866,7 +866,7 @@ Consider a simplified micro function structured REST API that has two routes: * `/users` - an endpoint that will return all users of the application on `GET` requests * `/users/` - an endpoint that looks up a single users details by ID on `GET` requests -Each endpoint will be it's own Lambda function that is configured as a [Lambda integration](https://docs.aws.amazon.com/apigateway/latest/developerguide/getting-started-with-lambda-integration.html){target="_blank"}. This allows you to set different configurations for each lambda (memory size, layers, etc.) +Each endpoint will be it's own Lambda function that is configured as a [Lambda integration](https://docs.aws.amazon.com/apigateway/latest/developerguide/getting-started-with-lambda-integration.html){target="_blank"}. This allows you to set different configurations for each lambda (memory size, layers, etc.). === "`/users` Endpoint" ```python From aac0d4b6b31eb260a7dc5812b4dd0ec91d7dafd5 Mon Sep 17 00:00:00 2001 From: Tony Sherman <100969281+TonySherman@users.noreply.github.com> Date: Thu, 14 Sep 2023 08:23:41 -0400 Subject: [PATCH 5/8] Update python version in example template Co-authored-by: Leandro Damascena Signed-off-by: Tony Sherman <100969281+TonySherman@users.noreply.github.com> --- examples/event_handler_rest/sam/micro_function_template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/event_handler_rest/sam/micro_function_template.yaml b/examples/event_handler_rest/sam/micro_function_template.yaml index db30023cfc2..f932045a399 100644 --- a/examples/event_handler_rest/sam/micro_function_template.yaml +++ b/examples/event_handler_rest/sam/micro_function_template.yaml @@ -15,7 +15,7 @@ Globals: Function: Timeout: 5 - Runtime: python3.10 + Runtime: python3.11 Resources: # Lambda Function Solely For /users endpoint From cfc93a6384865d6fa859f0fe508e9cef246fb0c1 Mon Sep 17 00:00:00 2001 From: Tony Sherman <100969281+TonySherman@users.noreply.github.com> Date: Thu, 14 Sep 2023 08:25:48 -0400 Subject: [PATCH 6/8] Apply suggestions from code review Co-authored-by: Leandro Damascena Signed-off-by: Tony Sherman <100969281+TonySherman@users.noreply.github.com> --- .../sam/micro_function_template.yaml | 2 +- .../src/micro_function_all_users_route.py | 22 +++++-------------- .../src/micro_function_user_by_id_route.py | 17 ++++---------- 3 files changed, 11 insertions(+), 30 deletions(-) diff --git a/examples/event_handler_rest/sam/micro_function_template.yaml b/examples/event_handler_rest/sam/micro_function_template.yaml index f932045a399..8f82076cc64 100644 --- a/examples/event_handler_rest/sam/micro_function_template.yaml +++ b/examples/event_handler_rest/sam/micro_function_template.yaml @@ -34,7 +34,7 @@ Resources: Properties: Path: /users Method: GET - MemorySize: 512 # This Lambda Function can have a higher memory size set + MemorySize: 128 # This Lambda Function can have a higher memory size set Environment: Variables: LOG_LEVEL: INFO diff --git a/examples/event_handler_rest/src/micro_function_all_users_route.py b/examples/event_handler_rest/src/micro_function_all_users_route.py index ba6d3681220..cd4b79923af 100644 --- a/examples/event_handler_rest/src/micro_function_all_users_route.py +++ b/examples/event_handler_rest/src/micro_function_all_users_route.py @@ -3,7 +3,7 @@ from aws_lambda_powertools import Logger from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response -from aws_lambda_powertools.utilities.parser import BaseModel +from dataclasses import dataclass from aws_lambda_powertools.utilities.typing import LambdaContext logger = Logger() @@ -18,27 +18,17 @@ { "user_id": "3a9df6b1-938c-4e80-bd4a-0c966f4b1c1e", "email": "jane.smith@example.com", - "active": True, + "active": False, }, { "user_id": "aa0d3d09-9cb9-42b9-9e63-1fb17ea52981", "email": "alex.wilson@example.com", "active": True, - }, - { - "user_id": "67a6c17d-b7f0-4f79-aae0-79f4a53c113b", - "email": "lisa.johnson@example.com", - "active": True, - }, - { - "user_id": "6e85cf66-47af-4dbf-8aa2-2db3c24f29c1", - "email": "michael.brown@example.com", - "active": False, - }, ] -class User(BaseModel): +@dataclass +class User(): user_id: str email: str active: bool @@ -51,7 +41,7 @@ class User(BaseModel): def all_active_users(): """HTTP Response for all active users""" all_users = [User(**user) for user in users] - all_active_users = [user.dict() for user in all_users if user.active] + all_active_users = [user.__dict__ for user in all_users if user.active] return Response( status_code=HTTPStatus.OK.value, @@ -61,5 +51,5 @@ def all_active_users(): @logger.inject_lambda_context() -def handler(event: dict, context: LambdaContext) -> dict: +def lambda_handler(event: dict, context: LambdaContext) -> dict: return app.resolve(event, context) diff --git a/examples/event_handler_rest/src/micro_function_user_by_id_route.py b/examples/event_handler_rest/src/micro_function_user_by_id_route.py index baa083d97f5..a84176c647b 100644 --- a/examples/event_handler_rest/src/micro_function_user_by_id_route.py +++ b/examples/event_handler_rest/src/micro_function_user_by_id_route.py @@ -3,7 +3,7 @@ from aws_lambda_powertools import Logger from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response -from aws_lambda_powertools.utilities.parser import BaseModel +from dataclasses import dataclass from aws_lambda_powertools.utilities.typing import LambdaContext logger = Logger() @@ -25,20 +25,11 @@ "email": "alex.wilson@example.com", "active": True, }, - { - "user_id": "67a6c17d-b7f0-4f79-aae0-79f4a53c113b", - "email": "lisa.johnson@example.com", - "active": True, - }, - { - "user_id": "6e85cf66-47af-4dbf-8aa2-2db3c24f29c1", - "email": "michael.brown@example.com", - "active": False, - }, ] -class User(BaseModel): +@dataclass +class User(): user_id: str email: str active: bool @@ -70,5 +61,5 @@ def all_active_users(user_id: str): @logger.inject_lambda_context() -def handler(event: dict, context: LambdaContext) -> dict: +def lambda_handler(event: dict, context: LambdaContext) -> dict: return app.resolve(event, context) From db82dfd340d30ef1d24199c1e0ba10f2b9e2b310 Mon Sep 17 00:00:00 2001 From: Tony Sherman Date: Thu, 14 Sep 2023 08:56:44 -0400 Subject: [PATCH 7/8] additional updates to review suggestions --- docs/core/event_handler/api_gateway.md | 2 +- .../event_handler_rest/sam/micro_function_template.yaml | 4 ++-- .../src/micro_function_all_users_route.py | 5 +++-- .../src/micro_function_user_by_id_route.py | 8 ++++---- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 161b3b8d4b5..257c17ec13a 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -884,7 +884,7 @@ Each endpoint will be it's own Lambda function that is configured as a [Lambda i ``` ???+ note - You can see some of the downsides in this example such as some code reuse. If set up with proper build tooling, the `User` class could be shared across functions. + You can see some of the downsides in this example such as some code reuse. If set up with proper build tooling, the `User` class could be shared across functions. This could be accomplished by packaging shared code as a Lambda Layer. ## Testing your code diff --git a/examples/event_handler_rest/sam/micro_function_template.yaml b/examples/event_handler_rest/sam/micro_function_template.yaml index 8f82076cc64..fb27206fddf 100644 --- a/examples/event_handler_rest/sam/micro_function_template.yaml +++ b/examples/event_handler_rest/sam/micro_function_template.yaml @@ -34,7 +34,7 @@ Resources: Properties: Path: /users Method: GET - MemorySize: 128 # This Lambda Function can have a higher memory size set + MemorySize: 128 # Each Lambda Function can have it's own memory configuration Environment: Variables: LOG_LEVEL: INFO @@ -57,7 +57,7 @@ Resources: Properties: Path: /users/{id+} Method: GET - MemorySize: 128 + MemorySize: 128 # Each Lambda Function can have it's own memory configuration Environment: Variables: LOG_LEVEL: INFO diff --git a/examples/event_handler_rest/src/micro_function_all_users_route.py b/examples/event_handler_rest/src/micro_function_all_users_route.py index cd4b79923af..1a809634b45 100644 --- a/examples/event_handler_rest/src/micro_function_all_users_route.py +++ b/examples/event_handler_rest/src/micro_function_all_users_route.py @@ -1,9 +1,9 @@ import json +from dataclasses import dataclass from http import HTTPStatus from aws_lambda_powertools import Logger from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response -from dataclasses import dataclass from aws_lambda_powertools.utilities.typing import LambdaContext logger = Logger() @@ -24,11 +24,12 @@ "user_id": "aa0d3d09-9cb9-42b9-9e63-1fb17ea52981", "email": "alex.wilson@example.com", "active": True, + }, ] @dataclass -class User(): +class User: user_id: str email: str active: bool diff --git a/examples/event_handler_rest/src/micro_function_user_by_id_route.py b/examples/event_handler_rest/src/micro_function_user_by_id_route.py index a84176c647b..8d8c9524fac 100644 --- a/examples/event_handler_rest/src/micro_function_user_by_id_route.py +++ b/examples/event_handler_rest/src/micro_function_user_by_id_route.py @@ -1,9 +1,9 @@ import json +from dataclasses import dataclass from http import HTTPStatus from aws_lambda_powertools import Logger from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response -from dataclasses import dataclass from aws_lambda_powertools.utilities.typing import LambdaContext logger = Logger() @@ -18,7 +18,7 @@ { "user_id": "3a9df6b1-938c-4e80-bd4a-0c966f4b1c1e", "email": "jane.smith@example.com", - "active": True, + "active": False, }, { "user_id": "aa0d3d09-9cb9-42b9-9e63-1fb17ea52981", @@ -29,7 +29,7 @@ @dataclass -class User(): +class User: user_id: str email: str active: bool @@ -53,7 +53,7 @@ def all_active_users(user_id: str): return Response( status_code=HTTPStatus.OK.value, content_type="application/json", - body=json.dumps(user.dict()), + body=json.dumps(user.__dict__), ) else: From 09bea9754096e621a4a7a5a8299667ee648dd4d4 Mon Sep 17 00:00:00 2001 From: Tony Sherman <100969281+TonySherman@users.noreply.github.com> Date: Thu, 14 Sep 2023 20:10:31 -0400 Subject: [PATCH 8/8] Apply suggestions from code review Co-authored-by: Leandro Damascena Signed-off-by: Tony Sherman <100969281+TonySherman@users.noreply.github.com> --- .../src/micro_function_user_by_id_route.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/examples/event_handler_rest/src/micro_function_user_by_id_route.py b/examples/event_handler_rest/src/micro_function_user_by_id_route.py index 8d8c9524fac..4f6e7add85a 100644 --- a/examples/event_handler_rest/src/micro_function_user_by_id_route.py +++ b/examples/event_handler_rest/src/micro_function_user_by_id_route.py @@ -35,10 +35,16 @@ class User: active: bool -def get_user_by_id(user_id: str): - for user in users: - if user["user_id"] == user_id: - return User(**user) +def get_user_by_id(user_id: str) -> Union[User, None]: + for user_data in users: + if user_data["user_id"] == user_id: + return User( + user_id=str(user_data["user_id"]), + email=str(user_data["email"]), + active=bool(user_data["active"]), + ) + + return None app = APIGatewayRestResolver()