From 64ea15c9c3a65ae5da1b9d4eb14db6033800be63 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Tue, 12 Apr 2022 17:44:39 -0700 Subject: [PATCH 1/2] fix(docs): Extract appsync code examples Changes: - Extract code examples - Run isort, black - Fix yaml examples - Add make task Related to: - #1064 --- Makefile | 8 + docs/core/event_handler/appsync.md | 407 +----------------- .../appsync/app_async_functions.py | 23 + .../event_handler/appsync/app_async_test.py | 11 + .../event_handler/appsync/app_custom_model.py | 28 ++ .../appsync/app_merchant_info.py | 25 ++ .../appsync/app_merchant_search.py | 16 + .../appsync/app_nested_mappings.py | 19 + .../appsync/app_resolver_decorator.py | 46 ++ .../core/event_handler/appsync/app_router.py | 19 + .../core/event_handler/appsync/app_test.py | 8 + .../appsync/resolvers_location.py | 18 + .../core/event_handler/appsync/template.yml | 129 ++++++ .../appsync/test_async_resolver.py | 18 + .../event_handler/appsync/test_resolver.py | 17 + 15 files changed, 408 insertions(+), 384 deletions(-) create mode 100644 docs/examples/core/event_handler/appsync/app_async_functions.py create mode 100644 docs/examples/core/event_handler/appsync/app_async_test.py create mode 100644 docs/examples/core/event_handler/appsync/app_custom_model.py create mode 100644 docs/examples/core/event_handler/appsync/app_merchant_info.py create mode 100644 docs/examples/core/event_handler/appsync/app_merchant_search.py create mode 100644 docs/examples/core/event_handler/appsync/app_nested_mappings.py create mode 100644 docs/examples/core/event_handler/appsync/app_resolver_decorator.py create mode 100644 docs/examples/core/event_handler/appsync/app_router.py create mode 100644 docs/examples/core/event_handler/appsync/app_test.py create mode 100644 docs/examples/core/event_handler/appsync/resolvers_location.py create mode 100644 docs/examples/core/event_handler/appsync/template.yml create mode 100644 docs/examples/core/event_handler/appsync/test_async_resolver.py create mode 100644 docs/examples/core/event_handler/appsync/test_resolver.py diff --git a/Makefile b/Makefile index 73667eb5f58..2aa662b1332 100644 --- a/Makefile +++ b/Makefile @@ -90,3 +90,11 @@ changelog: mypy: poetry run mypy --pretty aws_lambda_powertools + +format-examples: + poetry run isort docs/examples + poetry run black docs/examples/*/*/*/*.py + +lint-examples: + poetry run python3 -m py_compile docs/examples/*/*/*/*.py + cfn-lint docs/examples/*/*/*/*.yml diff --git a/docs/core/event_handler/appsync.md b/docs/core/event_handler/appsync.md index 95457aa7736..bd90251a645 100644 --- a/docs/core/event_handler/appsync.md +++ b/docs/core/event_handler/appsync.md @@ -38,136 +38,8 @@ This is the sample infrastructure we are using for the initial examples with a A === "template.yml" - ```yaml hl_lines="37-42 50-55 61-62 78-91 96-120" - AWSTemplateFormatVersion: '2010-09-09' - Transform: AWS::Serverless-2016-10-31 - Description: Hello world Direct Lambda Resolver - - Globals: - Function: - Timeout: 5 - Runtime: python3.8 - Tracing: Active - Environment: - Variables: - # Powertools env vars: https://awslabs.github.io/aws-lambda-powertools-python/latest/#environment-variables - LOG_LEVEL: INFO - POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 - POWERTOOLS_LOGGER_LOG_EVENT: true - POWERTOOLS_SERVICE_NAME: sample_resolver - - Resources: - HelloWorldFunction: - Type: AWS::Serverless::Function - Properties: - Handler: app.lambda_handler - CodeUri: hello_world - Description: Sample Lambda Powertools Direct Lambda Resolver - Tags: - SOLUTION: LambdaPowertoolsPython - - # IAM Permissions and Roles - - AppSyncServiceRole: - Type: "AWS::IAM::Role" - Properties: - AssumeRolePolicyDocument: - Version: "2012-10-17" - Statement: - - - Effect: "Allow" - Principal: - Service: - - "appsync.amazonaws.com" - Action: - - "sts:AssumeRole" - - InvokeLambdaResolverPolicy: - Type: "AWS::IAM::Policy" - Properties: - PolicyName: "DirectAppSyncLambda" - PolicyDocument: - Version: "2012-10-17" - Statement: - - - Effect: "Allow" - Action: "lambda:invokeFunction" - Resource: - - !GetAtt HelloWorldFunction.Arn - Roles: - - !Ref AppSyncServiceRole - - # GraphQL API - - HelloWorldApi: - Type: "AWS::AppSync::GraphQLApi" - Properties: - Name: HelloWorldApi - AuthenticationType: "API_KEY" - XrayEnabled: true - - HelloWorldApiKey: - Type: AWS::AppSync::ApiKey - Properties: - ApiId: !GetAtt HelloWorldApi.ApiId - - HelloWorldApiSchema: - Type: "AWS::AppSync::GraphQLSchema" - Properties: - ApiId: !GetAtt HelloWorldApi.ApiId - Definition: | - schema { - query:Query - } - - type Query { - getTodo(id: ID!): Todo - listTodos: [Todo] - } - - type Todo { - id: ID! - title: String - description: String - done: Boolean - } - - # Lambda Direct Data Source and Resolver - - HelloWorldFunctionDataSource: - Type: "AWS::AppSync::DataSource" - Properties: - ApiId: !GetAtt HelloWorldApi.ApiId - Name: "HelloWorldLambdaDirectResolver" - Type: "AWS_LAMBDA" - ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn - LambdaConfig: - LambdaFunctionArn: !GetAtt HelloWorldFunction.Arn - - ListTodosResolver: - Type: "AWS::AppSync::Resolver" - Properties: - ApiId: !GetAtt HelloWorldApi.ApiId - TypeName: "Query" - FieldName: "listTodos" - DataSourceName: !GetAtt HelloWorldFunctionDataSource.Name - - GetTodoResolver: - Type: "AWS::AppSync::Resolver" - Properties: - ApiId: !GetAtt HelloWorldApi.ApiId - TypeName: "Query" - FieldName: "getTodo" - DataSourceName: !GetAtt HelloWorldFunctionDataSource.Name - - - Outputs: - HelloWorldFunction: - Description: "Hello World Lambda Function ARN" - Value: !GetAtt HelloWorldFunction.Arn - - HelloWorldAPI: - Value: !GetAtt HelloWorldApi.Arn + ```yaml hl_lines="37-42 50-55 61-62 78-92 96-120" + --8<-- "docs/examples/core/event_handler/appsync/template.yml" ``` ### Resolver decorator @@ -181,54 +53,8 @@ Here's an example where we have two separate functions to resolve `getTodo` and === "app.py" - ```python hl_lines="3-5 9 31-32 39-40 47" - from aws_lambda_powertools import Logger, Tracer - - from aws_lambda_powertools.logging import correlation_paths - from aws_lambda_powertools.event_handler import AppSyncResolver - from aws_lambda_powertools.utilities.data_classes.appsync import scalar_types_utils - - tracer = Tracer(service="sample_resolver") - logger = Logger(service="sample_resolver") - app = AppSyncResolver() - - # Note that `creation_time` isn't available in the schema - # This utility also takes into account what info you make available at API level vs what's stored - TODOS = [ - { - "id": scalar_types_utils.make_id(), # type ID or String - "title": "First task", - "description": "String", - "done": False, - "creation_time": scalar_types_utils.aws_datetime(), # type AWSDateTime - }, - { - "id": scalar_types_utils.make_id(), - "title": "Second task", - "description": "String", - "done": True, - "creation_time": scalar_types_utils.aws_datetime(), - }, - ] - - - @app.resolver(type_name="Query", field_name="getTodo") - def get_todo(id: str = ""): - logger.info(f"Fetching Todo {id}") - todo = [todo for todo in TODOS if todo["id"] == id] - - return todo - - - @app.resolver(type_name="Query", field_name="listTodos") - def list_todos(): - return TODOS - - - @logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) + ```python hl_lines="2-4 8 30-31 38-39 46" + --8<-- "docs/examples/core/event_handler/appsync/app_resolver_decorator.py" ``` === "schema.graphql" @@ -345,25 +171,8 @@ You can nest `app.resolver()` decorator multiple times when resolving fields wit === "nested_mappings.py" - ```python hl_lines="4 8 10-12 18" - from aws_lambda_powertools import Logger, Tracer - - from aws_lambda_powertools.logging import correlation_paths - from aws_lambda_powertools.event_handler import AppSyncResolver - - tracer = Tracer(service="sample_resolver") - logger = Logger(service="sample_resolver") - app = AppSyncResolver() - - @app.resolver(field_name="listLocations") - @app.resolver(field_name="locations") - def get_locations(name: str, description: str = ""): - return name + description - - @logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context) + ```python hl_lines="2 7 10-12 19" + --8<-- "docs/examples/core/event_handler/appsync/app_nested_mappings.py" ``` === "schema.graphql" @@ -396,28 +205,8 @@ You can nest `app.resolver()` decorator multiple times when resolving fields wit For Lambda Python3.8+ runtime, this utility supports async functions when you use in conjunction with `asyncio.run`. -```python hl_lines="5 9 11-13 21" title="Resolving GraphQL resolvers async" -import asyncio -from aws_lambda_powertools import Logger, Tracer - -from aws_lambda_powertools.logging import correlation_paths -from aws_lambda_powertools.event_handler import AppSyncResolver - -tracer = Tracer(service="sample_resolver") -logger = Logger(service="sample_resolver") -app = AppSyncResolver() - -@app.resolver(type_name="Query", field_name="listTodos") -async def list_todos(): - todos = await some_async_io_call() - return todos - -@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) -@tracer.capture_lambda_handler -def lambda_handler(event, context): - result = app.resolve(event, context) - - return asyncio.run(result) +```python hl_lines="4 9 12-14 23" title="Resolving GraphQL resolvers async" +--8<-- "docs/examples/core/event_handler/appsync/app_async_functions.py" ``` ### Amplify GraphQL Transformer @@ -463,53 +252,13 @@ Use the following code for `merchantInfo` and `searchMerchant` functions respect === "merchantInfo/src/app.py" - ```python hl_lines="4-5 9 11-12 15-16 23" - from aws_lambda_powertools import Logger, Tracer - - from aws_lambda_powertools.logging import correlation_paths - from aws_lambda_powertools.event_handler import AppSyncResolver - from aws_lambda_powertools.utilities.data_classes.appsync import scalar_types_utils - - tracer = Tracer(service="sample_graphql_transformer_resolver") - logger = Logger(service="sample_graphql_transformer_resolver") - app = AppSyncResolver() - - @app.resolver(type_name="Query", field_name="listLocations") - def list_locations(page: int = 0, size: int = 10): - return [{"id": 100, "name": "Smooth Grooves"}] - - @app.resolver(field_name="commonField") - def common_field(): - # Would match all fieldNames matching 'commonField' - return scalar_types_utils.make_id() - - @tracer.capture_lambda_handler - @logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) - def lambda_handler(event, context): - app.resolve(event, context) + ```python hl_lines="2 4 8 11-12 16-17 25" + --8<-- "docs/examples/core/event_handler/appsync/app_merchant_info.py" ``` === "searchMerchant/src/app.py" - ```python hl_lines="1 4 6-7" - from aws_lambda_powertools.event_handler import AppSyncResolver - from aws_lambda_powertools.utilities.data_classes.appsync import scalar_types_utils - - app = AppSyncResolver() - - @app.resolver(type_name="Query", field_name="findMerchant") - def find_merchant(search: str): - return [ - { - "id": scalar_types_utils.make_id(), - "name": "Brewer Brewing", - "description": "Mike Brewer's IPA brewing place" - }, - { - "id": scalar_types_utils.make_id(), - "name": "Serverlessa's Bakery", - "description": "Lessa's sourdough place" - }, - ] + ```python hl_lines="1 4 7-8" + --8<-- "docs/examples/core/event_handler/appsync/app_merchant_search.py" ``` **Example AppSync GraphQL Transformer Function resolver events** @@ -604,34 +353,8 @@ You can subclass `AppSyncResolverEvent` to bring your own set of methods to hand === "custom_model.py" - ```python hl_lines="12-15 20 27" - from aws_lambda_powertools import Logger, Tracer - - from aws_lambda_powertools.logging import correlation_paths - from aws_lambda_powertools.event_handler import AppSyncResolver - from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import AppSyncResolverEvent - - tracer = Tracer(service="sample_resolver") - logger = Logger(service="sample_resolver") - app = AppSyncResolver() - - - class MyCustomModel(AppSyncResolverEvent): - @property - def country_viewer(self) -> str: - return self.request_headers.get("cloudfront-viewer-country") - - @app.resolver(field_name="listLocations") - @app.resolver(field_name="locations") - def get_locations(name: str, description: str = ""): - if app.current_event.country_viewer == "US": - ... - return name + description - - @logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) - @tracer.capture_lambda_handler - def lambda_handler(event, context): - return app.resolve(event, context, data_model=MyCustomModel) + ```python hl_lines="11-14 20 28" + --8<-- "docs/examples/core/event_handler/appsync/app_custom_model.py" ``` === "schema.graphql" @@ -723,51 +446,16 @@ Let's assume you have `app.py` as your Lambda function entrypoint and routes in We import **Router** instead of **AppSyncResolver**; syntax wise is exactly the same. - ```python hl_lines="4 7 10 15" - from typing import Any, Dict, List - - from aws_lambda_powertools import Logger - from aws_lambda_powertools.event_handler.appsync import Router - - logger = Logger(child=True) - router = Router() - - - @router.resolver(type_name="Query", field_name="listLocations") - def list_locations(merchant_id: str) -> List[Dict[str, Any]]: - return [{"name": "Location name", "merchant_id": merchant_id}] - - - @router.resolver(type_name="Location", field_name="status") - def resolve_status(merchant_id: str) -> str: - logger.debug(f"Resolve status for merchant_id: {merchant_id}") - return "FOO" - ``` + ```python hl_lines="4 7 10 15" + --8<-- "docs/examples/core/event_handler/appsync/resolvers_location.py" + ``` === "app.py" We use `include_router` method and include all `location` operations registered in the `router` global object. - ```python hl_lines="8 13" - from typing import Dict - - from aws_lambda_powertools import Logger, Tracer - from aws_lambda_powertools.event_handler import AppSyncResolver - from aws_lambda_powertools.logging.correlation_paths import APPSYNC_RESOLVER - from aws_lambda_powertools.utilities.typing import LambdaContext - - from resolvers import location - - tracer = Tracer() - logger = Logger() - app = AppSyncResolver() - app.include_router(location.router) - - - @tracer.capture_lambda_handler - @logger.inject_lambda_context(correlation_id_path=APPSYNC_RESOLVER) - def lambda_handler(event: Dict, context: LambdaContext): - app.resolve(event, context) + ```python hl_lines="3 13" + --8<-- "docs/examples/core/event_handler/appsync/app_router.py" ``` @@ -782,36 +470,13 @@ Here's an example of how you can test your synchronous resolvers: === "test_resolver.py" ```python - import json - import pytest - from pathlib import Path - - from src.index import app # import the instance of AppSyncResolver from your code - - def test_direct_resolver(): - # Load mock event from a file - json_file_path = Path("appSyncDirectResolver.json") - with open(json_file_path) as json_file: - mock_event = json.load(json_file) - - # Call the implicit handler - result = app(mock_event, {}) - - assert result == "created this value" + --8<-- "docs/examples/core/event_handler/appsync/test_resolver.py" ``` === "src/index.py" ```python - - from aws_lambda_powertools.event_handler import AppSyncResolver - - app = AppSyncResolver() - - @app.resolver(field_name="createSomething") - def create_something(): - return "created this value" - + --8<-- "docs/examples/core/event_handler/appsync/app_test.py" ``` === "appSyncDirectResolver.json" @@ -825,39 +490,13 @@ And an example for testing asynchronous resolvers. Note that this requires the ` === "test_async_resolver.py" ```python - import json - import pytest - from pathlib import Path - - from src.index import app # import the instance of AppSyncResolver from your code - - @pytest.mark.asyncio - async def test_direct_resolver(): - # Load mock event from a file - json_file_path = Path("appSyncDirectResolver.json") - with open(json_file_path) as json_file: - mock_event = json.load(json_file) - - # Call the implicit handler - result = await app(mock_event, {}) - - assert result == "created this value" + --8<-- "docs/examples/core/event_handler/appsync/test_async_resolver.py" ``` === "src/index.py" ```python - import asyncio - - from aws_lambda_powertools.event_handler import AppSyncResolver - - app = AppSyncResolver() - - @app.resolver(field_name="createSomething") - async def create_something_async(): - await asyncio.sleep(1) # Do async stuff - return "created this value" - + --8<-- "docs/examples/core/event_handler/appsync/app_async_test.py" ``` === "appSyncDirectResolver.json" diff --git a/docs/examples/core/event_handler/appsync/app_async_functions.py b/docs/examples/core/event_handler/appsync/app_async_functions.py new file mode 100644 index 00000000000..02b91605811 --- /dev/null +++ b/docs/examples/core/event_handler/appsync/app_async_functions.py @@ -0,0 +1,23 @@ +import asyncio + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import AppSyncResolver +from aws_lambda_powertools.logging import correlation_paths + +tracer = Tracer(service="sample_resolver") +logger = Logger(service="sample_resolver") +app = AppSyncResolver() + + +@app.resolver(type_name="Query", field_name="listTodos") +async def list_todos(): + todos = await some_async_io_call() + return todos + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) +@tracer.capture_lambda_handler +def lambda_handler(event, context): + result = app.resolve(event, context) + + return asyncio.run(result) diff --git a/docs/examples/core/event_handler/appsync/app_async_test.py b/docs/examples/core/event_handler/appsync/app_async_test.py new file mode 100644 index 00000000000..24bf8bc0c37 --- /dev/null +++ b/docs/examples/core/event_handler/appsync/app_async_test.py @@ -0,0 +1,11 @@ +import asyncio + +from aws_lambda_powertools.event_handler import AppSyncResolver + +app = AppSyncResolver() + + +@app.resolver(field_name="createSomething") +async def create_something_async(): + await asyncio.sleep(1) # Do async stuff + return "created this value" diff --git a/docs/examples/core/event_handler/appsync/app_custom_model.py b/docs/examples/core/event_handler/appsync/app_custom_model.py new file mode 100644 index 00000000000..26d56975323 --- /dev/null +++ b/docs/examples/core/event_handler/appsync/app_custom_model.py @@ -0,0 +1,28 @@ +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import AppSyncResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import AppSyncResolverEvent + +tracer = Tracer(service="sample_resolver") +logger = Logger(service="sample_resolver") +app = AppSyncResolver() + + +class MyCustomModel(AppSyncResolverEvent): + @property + def country_viewer(self) -> str: + return self.request_headers.get("cloudfront-viewer-country") + + +@app.resolver(field_name="listLocations") +@app.resolver(field_name="locations") +def get_locations(name: str, description: str = ""): + if app.current_event.country_viewer == "US": + ... + return name + description + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) +@tracer.capture_lambda_handler +def lambda_handler(event, context): + return app.resolve(event, context, data_model=MyCustomModel) diff --git a/docs/examples/core/event_handler/appsync/app_merchant_info.py b/docs/examples/core/event_handler/appsync/app_merchant_info.py new file mode 100644 index 00000000000..e19ca996da8 --- /dev/null +++ b/docs/examples/core/event_handler/appsync/app_merchant_info.py @@ -0,0 +1,25 @@ +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import AppSyncResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.data_classes.appsync import scalar_types_utils + +tracer = Tracer(service="sample_graphql_transformer_resolver") +logger = Logger(service="sample_graphql_transformer_resolver") +app = AppSyncResolver() + + +@app.resolver(type_name="Query", field_name="listLocations") +def list_locations(page: int = 0, size: int = 10): + return [{"id": 100, "name": "Smooth Grooves"}] + + +@app.resolver(field_name="commonField") +def common_field(): + # Would match all fieldNames matching 'commonField' + return scalar_types_utils.make_id() + + +@tracer.capture_lambda_handler +@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) +def lambda_handler(event, context): + app.resolve(event, context) diff --git a/docs/examples/core/event_handler/appsync/app_merchant_search.py b/docs/examples/core/event_handler/appsync/app_merchant_search.py new file mode 100644 index 00000000000..9360b7e5148 --- /dev/null +++ b/docs/examples/core/event_handler/appsync/app_merchant_search.py @@ -0,0 +1,16 @@ +from aws_lambda_powertools.event_handler import AppSyncResolver +from aws_lambda_powertools.utilities.data_classes.appsync import scalar_types_utils + +app = AppSyncResolver() + + +@app.resolver(type_name="Query", field_name="findMerchant") +def find_merchant(search: str): + return [ + { + "id": scalar_types_utils.make_id(), + "name": "Brewer Brewing", + "description": "Mike Brewer's IPA brewing place", + }, + {"id": scalar_types_utils.make_id(), "name": "Serverlessa's Bakery", "description": "Lessa's sourdough place"}, + ] diff --git a/docs/examples/core/event_handler/appsync/app_nested_mappings.py b/docs/examples/core/event_handler/appsync/app_nested_mappings.py new file mode 100644 index 00000000000..333b1e44aaa --- /dev/null +++ b/docs/examples/core/event_handler/appsync/app_nested_mappings.py @@ -0,0 +1,19 @@ +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import AppSyncResolver +from aws_lambda_powertools.logging import correlation_paths + +tracer = Tracer(service="sample_resolver") +logger = Logger(service="sample_resolver") +app = AppSyncResolver() + + +@app.resolver(field_name="listLocations") +@app.resolver(field_name="locations") +def get_locations(name: str, description: str = ""): + return name + description + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) +@tracer.capture_lambda_handler +def lambda_handler(event, context): + return app.resolve(event, context) diff --git a/docs/examples/core/event_handler/appsync/app_resolver_decorator.py b/docs/examples/core/event_handler/appsync/app_resolver_decorator.py new file mode 100644 index 00000000000..9ed8d7b6b83 --- /dev/null +++ b/docs/examples/core/event_handler/appsync/app_resolver_decorator.py @@ -0,0 +1,46 @@ +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import AppSyncResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.data_classes.appsync import scalar_types_utils + +tracer = Tracer(service="sample_resolver") +logger = Logger(service="sample_resolver") +app = AppSyncResolver() + +# Note that `creation_time` isn't available in the schema +# This utility also takes into account what info you make available at API level vs what's stored +TODOS = [ + { + "id": scalar_types_utils.make_id(), # type ID or String + "title": "First task", + "description": "String", + "done": False, + "creation_time": scalar_types_utils.aws_datetime(), # type AWSDateTime + }, + { + "id": scalar_types_utils.make_id(), + "title": "Second task", + "description": "String", + "done": True, + "creation_time": scalar_types_utils.aws_datetime(), + }, +] + + +@app.resolver(type_name="Query", field_name="getTodo") +def get_todo(id: str = ""): + logger.info(f"Fetching Todo {id}") + todo = [todo for todo in TODOS if todo["id"] == id] + + return todo + + +@app.resolver(type_name="Query", field_name="listTodos") +def list_todos(): + return TODOS + + +@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) +@tracer.capture_lambda_handler +def lambda_handler(event, context): + return app.resolve(event, context) diff --git a/docs/examples/core/event_handler/appsync/app_router.py b/docs/examples/core/event_handler/appsync/app_router.py new file mode 100644 index 00000000000..50e42f4d55a --- /dev/null +++ b/docs/examples/core/event_handler/appsync/app_router.py @@ -0,0 +1,19 @@ +from typing import Dict + +from resolvers import location + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import AppSyncResolver +from aws_lambda_powertools.logging.correlation_paths import APPSYNC_RESOLVER +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = AppSyncResolver() +app.include_router(location.router) + + +@tracer.capture_lambda_handler +@logger.inject_lambda_context(correlation_id_path=APPSYNC_RESOLVER) +def lambda_handler(event: Dict, context: LambdaContext): + app.resolve(event, context) diff --git a/docs/examples/core/event_handler/appsync/app_test.py b/docs/examples/core/event_handler/appsync/app_test.py new file mode 100644 index 00000000000..0e37a1952f7 --- /dev/null +++ b/docs/examples/core/event_handler/appsync/app_test.py @@ -0,0 +1,8 @@ +from aws_lambda_powertools.event_handler import AppSyncResolver + +app = AppSyncResolver() + + +@app.resolver(field_name="createSomething") +def create_something(): + return "created this value" diff --git a/docs/examples/core/event_handler/appsync/resolvers_location.py b/docs/examples/core/event_handler/appsync/resolvers_location.py new file mode 100644 index 00000000000..67c2c7a4814 --- /dev/null +++ b/docs/examples/core/event_handler/appsync/resolvers_location.py @@ -0,0 +1,18 @@ +from typing import Any, Dict, List + +from aws_lambda_powertools import Logger +from aws_lambda_powertools.event_handler.appsync import Router + +logger = Logger(child=True) +router = Router() + + +@router.resolver(type_name="Query", field_name="listLocations") +def list_locations(merchant_id: str) -> List[Dict[str, Any]]: + return [{"name": "Location name", "merchant_id": merchant_id}] + + +@router.resolver(type_name="Location", field_name="status") +def resolve_status(merchant_id: str) -> str: + logger.debug(f"Resolve status for merchant_id: {merchant_id}") + return "FOO" diff --git a/docs/examples/core/event_handler/appsync/template.yml b/docs/examples/core/event_handler/appsync/template.yml new file mode 100644 index 00000000000..6c14a72b968 --- /dev/null +++ b/docs/examples/core/event_handler/appsync/template.yml @@ -0,0 +1,129 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Hello world Direct Lambda Resolver + +Globals: + Function: + Timeout: 5 + Runtime: python3.9 + Tracing: Active + Environment: + Variables: + # Powertools env vars: https://awslabs.github.io/aws-lambda-powertools-python/latest/#environment-variables + LOG_LEVEL: INFO + POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 + POWERTOOLS_LOGGER_LOG_EVENT: true + POWERTOOLS_SERVICE_NAME: sample_resolver + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + Handler: app.lambda_handler + CodeUri: hello_world + Description: Sample Lambda Powertools Direct Lambda Resolver + Tags: + SOLUTION: LambdaPowertoolsPython + + # IAM Permissions and Roles + + AppSyncServiceRole: + Type: "AWS::IAM::Role" + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - + Effect: "Allow" + Principal: + Service: + - "appsync.amazonaws.com" + Action: + - "sts:AssumeRole" + + InvokeLambdaResolverPolicy: + Type: "AWS::IAM::Policy" + Properties: + PolicyName: "DirectAppSyncLambda" + PolicyDocument: + Version: "2012-10-17" + Statement: + - + Effect: "Allow" + Action: "lambda:invokeFunction" + Resource: + - !GetAtt HelloWorldFunction.Arn + Roles: + - !Ref AppSyncServiceRole + + # GraphQL API + + HelloWorldApi: + Type: "AWS::AppSync::GraphQLApi" + Properties: + Name: HelloWorldApi + AuthenticationType: "API_KEY" + XrayEnabled: true + + HelloWorldApiKey: + Type: AWS::AppSync::ApiKey + Properties: + ApiId: !GetAtt HelloWorldApi.ApiId + + HelloWorldApiSchema: + Type: "AWS::AppSync::GraphQLSchema" + Properties: + ApiId: !GetAtt HelloWorldApi.ApiId + Definition: | + schema { + query:Query + } + + type Query { + getTodo(id: ID!): Todo + listTodos: [Todo] + } + + type Todo { + id: ID! + title: String + description: String + done: Boolean + } + + # Lambda Direct Data Source and Resolver + + HelloWorldFunctionDataSource: + Type: "AWS::AppSync::DataSource" + Properties: + ApiId: !GetAtt HelloWorldApi.ApiId + Name: "HelloWorldLambdaDirectResolver" + Type: "AWS_LAMBDA" + ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn + LambdaConfig: + LambdaFunctionArn: !GetAtt HelloWorldFunction.Arn + + ListTodosResolver: + Type: "AWS::AppSync::Resolver" + Properties: + ApiId: !GetAtt HelloWorldApi.ApiId + TypeName: "Query" + FieldName: "listTodos" + DataSourceName: !GetAtt HelloWorldFunctionDataSource.Name + + GetTodoResolver: + Type: "AWS::AppSync::Resolver" + Properties: + ApiId: !GetAtt HelloWorldApi.ApiId + TypeName: "Query" + FieldName: "getTodo" + DataSourceName: !GetAtt HelloWorldFunctionDataSource.Name + + +Outputs: + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + + HelloWorldAPI: + Value: !GetAtt HelloWorldApi.Arn diff --git a/docs/examples/core/event_handler/appsync/test_async_resolver.py b/docs/examples/core/event_handler/appsync/test_async_resolver.py new file mode 100644 index 00000000000..8b01db6b059 --- /dev/null +++ b/docs/examples/core/event_handler/appsync/test_async_resolver.py @@ -0,0 +1,18 @@ +import json +from pathlib import Path + +import pytest +from src.index import app # import the instance of AppSyncResolver from your code + + +@pytest.mark.asyncio +async def test_direct_resolver(): + # Load mock event from a file + json_file_path = Path("appSyncDirectResolver.json") + with open(json_file_path) as json_file: + mock_event = json.load(json_file) + + # Call the implicit handler + result = await app(mock_event, {}) + + assert result == "created this value" diff --git a/docs/examples/core/event_handler/appsync/test_resolver.py b/docs/examples/core/event_handler/appsync/test_resolver.py new file mode 100644 index 00000000000..ac2f417b176 --- /dev/null +++ b/docs/examples/core/event_handler/appsync/test_resolver.py @@ -0,0 +1,17 @@ +import json +from pathlib import Path + +import pytest +from src.index import app # import the instance of AppSyncResolver from your code + + +def test_direct_resolver(): + # Load mock event from a file + json_file_path = Path("appSyncDirectResolver.json") + with open(json_file_path) as json_file: + mock_event = json.load(json_file) + + # Call the implicit handler + result = app(mock_event, {}) + + assert result == "created this value" From 31fe45ec28d7ac9579d509351be214cabecd9c3e Mon Sep 17 00:00:00 2001 From: Simon Thulbourn Date: Thu, 28 Apr 2022 17:34:36 +0100 Subject: [PATCH 2/2] Revert "fix(parser): Add missing fields for SESEvent (#1027)" (#1190) This reverts commit 797a10afac80544e2d69bcb7d624909436f2b12a. --- .../utilities/parser/models/__init__.py | 8 -- .../utilities/parser/models/ses.py | 34 +----- tests/events/sesEventS3.json | 114 ------------------ tests/functional/parser/test_ses.py | 58 +-------- 4 files changed, 8 insertions(+), 206 deletions(-) delete mode 100644 tests/events/sesEventS3.json diff --git a/aws_lambda_powertools/utilities/parser/models/__init__.py b/aws_lambda_powertools/utilities/parser/models/__init__.py index 34c8e6ce6a1..e3fb50a2d5d 100644 --- a/aws_lambda_powertools/utilities/parser/models/__init__.py +++ b/aws_lambda_powertools/utilities/parser/models/__init__.py @@ -37,11 +37,7 @@ SesModel, SesReceipt, SesReceiptAction, - SesReceiptActionBase, - SesReceiptBounceAction, - SesReceiptS3Action, SesReceiptVerdict, - SesReceiptWorkmailAction, SesRecordModel, ) from .sns import SnsModel, SnsNotificationModel, SnsRecordModel @@ -88,10 +84,6 @@ "SesMailHeaders", "SesReceipt", "SesReceiptAction", - "SesReceiptActionBase", - "SesReceiptBounceAction", - "SesReceiptWorkmailAction", - "SesReceiptS3Action", "SesReceiptVerdict", "SnsModel", "SnsNotificationModel", diff --git a/aws_lambda_powertools/utilities/parser/models/ses.py b/aws_lambda_powertools/utilities/parser/models/ses.py index 7cd655ea28c..70fd2e83978 100644 --- a/aws_lambda_powertools/utilities/parser/models/ses.py +++ b/aws_lambda_powertools/utilities/parser/models/ses.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import List, Optional, Union +from typing import List, Optional from pydantic import BaseModel, Field from pydantic.networks import EmailStr @@ -12,38 +12,12 @@ class SesReceiptVerdict(BaseModel): status: Literal["PASS", "FAIL", "GRAY", "PROCESSING_FAILED"] -class SesReceiptActionBase(BaseModel): - topicArn: Optional[str] - - -class SesReceiptAction(SesReceiptActionBase): +class SesReceiptAction(BaseModel): type: Literal["Lambda"] # noqa A003,VNE003 invocationType: Literal["Event"] functionArn: str -class SesReceiptS3Action(SesReceiptActionBase): - type: Literal["S3"] # noqa A003,VNE003 - topicArn: str - bucketName: str - objectKey: str - - -class SesReceiptBounceAction(SesReceiptActionBase): - type: Literal["Bounce"] # noqa A003,VNE003 - topicArn: str - smtpReplyCode: str - message: str - sender: str - statusCode: str - - -class SesReceiptWorkmailAction(SesReceiptActionBase): - type: Literal["WorkMail"] # noqa A003,VNE003 - topicArn: str - organizationArn: str - - class SesReceipt(BaseModel): timestamp: datetime processingTimeMillis: PositiveInt @@ -51,10 +25,8 @@ class SesReceipt(BaseModel): spamVerdict: SesReceiptVerdict virusVerdict: SesReceiptVerdict spfVerdict: SesReceiptVerdict - dkimVerdict: SesReceiptVerdict dmarcVerdict: SesReceiptVerdict - dmarcPolicy: Optional[Literal["quarantine", "reject", "none"]] - action: Union[SesReceiptAction, SesReceiptS3Action, SesReceiptBounceAction, SesReceiptWorkmailAction] + action: SesReceiptAction class SesMailHeaders(BaseModel): diff --git a/tests/events/sesEventS3.json b/tests/events/sesEventS3.json deleted file mode 100644 index dbea2d42ce1..00000000000 --- a/tests/events/sesEventS3.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "Records": [ - { - "eventVersion": "1.0", - "ses": { - "receipt": { - "timestamp": "2015-09-11T20:32:33.936Z", - "processingTimeMillis": 406, - "recipients": [ - "recipient@example.com" - ], - "spamVerdict": { - "status": "PASS" - }, - "virusVerdict": { - "status": "PASS" - }, - "spfVerdict": { - "status": "PASS" - }, - "dkimVerdict": { - "status": "PASS" - }, - "dmarcVerdict": { - "status": "PASS" - }, - "dmarcPolicy": "reject", - "action": { - "type": "S3", - "topicArn": "arn:aws:sns:us-east-1:012345678912:example-topic", - "bucketName": "my-S3-bucket", - "objectKey": "email" - } - }, - "mail": { - "timestamp": "2015-09-11T20:32:33.936Z", - "source": "0000014fbe1c09cf-7cb9f704-7531-4e53-89a1-5fa9744f5eb6-000000@amazonses.com", - "messageId": "d6iitobk75ur44p8kdnnp7g2n800", - "destination": [ - "recipient@example.com" - ], - "headersTruncated": false, - "headers": [ - { - "name": "Return-Path", - "value": "<0000014fbe1c09cf-7cb9f704-7531-4e53-89a1-5fa9744f5eb6-000000@amazonses.com>" - }, - { - "name": "Received", - "value": "from a9-183.smtp-out.amazonses.com (a9-183.smtp-out.amazonses.com [54.240.9.183]) by inbound-smtp.us-east-1.amazonaws.com with SMTP id d6iitobk75ur44p8kdnnp7g2n800 for recipient@example.com; Fri, 11 Sep 2015 20:32:33 +0000 (UTC)" - }, - { - "name": "DKIM-Signature", - "value": "v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple; s=ug7nbtf4gccmlpwj322ax3p6ow6yfsug; d=amazonses.com; t=1442003552; h=From:To:Subject:MIME-Version:Content-Type:Content-Transfer-Encoding:Date:Message-ID:Feedback-ID; bh=DWr3IOmYWoXCA9ARqGC/UaODfghffiwFNRIb2Mckyt4=; b=p4ukUDSFqhqiub+zPR0DW1kp7oJZakrzupr6LBe6sUuvqpBkig56UzUwc29rFbJF hlX3Ov7DeYVNoN38stqwsF8ivcajXpQsXRC1cW9z8x875J041rClAjV7EGbLmudVpPX 4hHst1XPyX5wmgdHIhmUuh8oZKpVqGi6bHGzzf7g=" - }, - { - "name": "From", - "value": "sender@example.com" - }, - { - "name": "To", - "value": "recipient@example.com" - }, - { - "name": "Subject", - "value": "Example subject" - }, - { - "name": "MIME-Version", - "value": "1.0" - }, - { - "name": "Content-Type", - "value": "text/plain; charset=UTF-8" - }, - { - "name": "Content-Transfer-Encoding", - "value": "7bit" - }, - { - "name": "Date", - "value": "Fri, 11 Sep 2015 20:32:32 +0000" - }, - { - "name": "Message-ID", - "value": "<61967230-7A45-4A9D-BEC9-87CBCF2211C9@example.com>" - }, - { - "name": "X-SES-Outgoing", - "value": "2015.09.11-54.240.9.183" - }, - { - "name": "Feedback-ID", - "value": "1.us-east-1.Krv2FKpFdWV+KUYw3Qd6wcpPJ4Sv/pOPpEPSHn2u2o4=:AmazonSES" - } - ], - "commonHeaders": { - "returnPath": "0000014fbe1c09cf-7cb9f704-7531-4e53-89a1-5fa9744f5eb6-000000@amazonses.com", - "from": [ - "sender@example.com" - ], - "date": "Fri, 11 Sep 2015 20:32:32 +0000", - "to": [ - "recipient@example.com" - ], - "messageId": "<61967230-7A45-4A9D-BEC9-87CBCF2211C9@example.com>", - "subject": "Example subject" - } - } - }, - "eventSource": "aws:ses" - } - ] -} diff --git a/tests/functional/parser/test_ses.py b/tests/functional/parser/test_ses.py index 34a44253514..d434e2350f8 100644 --- a/tests/functional/parser/test_ses.py +++ b/tests/functional/parser/test_ses.py @@ -1,22 +1,11 @@ from aws_lambda_powertools.utilities.parser import event_parser -from aws_lambda_powertools.utilities.parser.models import ( - SesModel, - SesReceiptBounceAction, - SesReceiptWorkmailAction, - SesRecordModel, -) +from aws_lambda_powertools.utilities.parser.models import SesModel, SesRecordModel from aws_lambda_powertools.utilities.typing import LambdaContext from tests.functional.utils import load_event @event_parser(model=SesModel) -def handle_ses(event: SesModel, _: LambdaContext) -> SesModel: - return event - - -def test_ses_trigger_lambda_event(): - event_dict = load_event("sesEvent.json") - event = handle_ses(event_dict, LambdaContext()) +def handle_ses(event: SesModel, _: LambdaContext): expected_address = "johndoe@example.com" records = event.Records record: SesRecordModel = records[0] @@ -40,10 +29,6 @@ def test_ses_trigger_lambda_event(): assert common_headers.to == [expected_address] assert common_headers.messageId == "<0123456789example.com>" assert common_headers.subject == "Test Subject" - assert common_headers.cc is None - assert common_headers.bcc is None - assert common_headers.sender is None - assert common_headers.reply_to is None receipt = record.ses.receipt convert_time = int(round(receipt.timestamp.timestamp() * 1000)) assert convert_time == 0 @@ -53,45 +38,12 @@ def test_ses_trigger_lambda_event(): assert receipt.virusVerdict.status == "PASS" assert receipt.spfVerdict.status == "PASS" assert receipt.dmarcVerdict.status == "PASS" - assert receipt.dmarcVerdict.status == "PASS" - assert receipt.dmarcPolicy is None action = receipt.action assert action.type == "Lambda" assert action.functionArn == "arn:aws:lambda:us-west-2:012345678912:function:Example" assert action.invocationType == "Event" - assert action.topicArn is None - -def test_ses_trigger_event_s3(): - event_dict = load_event("sesEventS3.json") - event = handle_ses(event_dict, LambdaContext()) - records = list(event.Records) - record = records[0] - receipt = record.ses.receipt - assert receipt.dmarcPolicy == "reject" - action = record.ses.receipt.action - assert action.type == "S3" - assert action.topicArn == "arn:aws:sns:us-east-1:012345678912:example-topic" - assert action.bucketName == "my-S3-bucket" - assert action.objectKey == "email" - - -def test_ses_trigger_event_bounce(): - event_dict = { - "type": "Bounce", - "topicArn": "arn:aws:sns:us-east-1:123456789012:topic:my-topic", - "smtpReplyCode": "5.1.1", - "message": "message", - "sender": "sender", - "statusCode": "550", - } - SesReceiptBounceAction(**event_dict) - -def test_ses_trigger_event_work_mail(): - event_dict = { - "type": "WorkMail", - "topicArn": "arn:aws:sns:us-east-1:123456789012:topic:my-topic", - "organizationArn": "arn", - } - SesReceiptWorkmailAction(**event_dict) +def test_ses_trigger_event(): + event_dict = load_event("sesEvent.json") + handle_ses(event_dict, LambdaContext())