From 1fa46508ca3fe6820bd9db6525c0880f26b0fbce Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 10 Mar 2021 01:33:46 -0800 Subject: [PATCH 01/33] feat(data-classes): AppSync Resolver Event --- .../data_classes/appsync_resolver_event.py | 47 ++++++++++++ tests/events/appSyncResolverevent.json | 71 +++++++++++++++++++ .../functional/test_lambda_trigger_events.py | 12 ++++ 3 files changed, 130 insertions(+) create mode 100644 aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py create mode 100644 tests/events/appSyncResolverevent.json diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py new file mode 100644 index 00000000000..71f04761e6a --- /dev/null +++ b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py @@ -0,0 +1,47 @@ +from typing import Dict + +from aws_lambda_powertools.utilities.data_classes.common import DictWrapper + + +class AppSyncResolverEvent(DictWrapper): + """AppSync resolver event + + Documentation: + ------------- + - https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html + """ + + @property + def type_name(self) -> str: + """The name of the parent type for the field that is currently being resolved.""" + return self["typeName"] + + @property + def field_name(self) -> str: + """The name of the field that is currently being resolved.""" + return self["fieldName"] + + @property + def arguments(self) -> Dict[str, any]: + """A map that contains all GraphQL arguments for this field.""" + return self["arguments"] + + @property + def identity(self) -> Dict[str, any]: + """An object that contains information about the caller.""" + return self["identity"] + + @property + def source(self) -> Dict[str, any]: + """A map that contains the resolution of the parent field.""" + return self["source"] + + @property + def request_headers(self) -> Dict[str, str]: + """Request headers""" + return self["request"]["headers"] + + @property + def prev_result(self) -> Dict[str, any]: + """It represents the result of whatever previous operation was executed in a pipeline resolver.""" + return self["prev"]["result"] diff --git a/tests/events/appSyncResolverevent.json b/tests/events/appSyncResolverevent.json new file mode 100644 index 00000000000..84ac71951c6 --- /dev/null +++ b/tests/events/appSyncResolverevent.json @@ -0,0 +1,71 @@ +{ + "typeName": "Merchant", + "fieldName": "locations", + "arguments": { + "page": 2, + "size": 1, + "name": "value" + }, + "identity": { + "claims": { + "sub": "07920713-4526-4642-9c88-2953512de441", + "iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_POOL_ID", + "aud": "58rc9bf5kkti90ctmvioppukm9", + "event_id": "7f4c9383-abf6-48b7-b821-91643968b755", + "token_use": "id", + "auth_time": 1615366261, + "name": "Michael Brewer", + "exp": 1615369861, + "iat": 1615366261 + }, + "defaultAuthStrategy": "ALLOW", + "groups": null, + "issuer": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_POOL_ID", + "sourceIp": [ + "11.215.2.22" + ], + "sub": "07920713-4526-4642-9c88-2953512de441", + "username": "mike" + }, + "source": { + "name": "Value", + "nested": { + "name": "value", + "list": [] + } + }, + "request": { + "headers": { + "x-forwarded-for": "11.215.2.22, 64.44.173.11", + "cloudfront-viewer-country": "US", + "cloudfront-is-tablet-viewer": "false", + "via": "2.0 SOMETHING.cloudfront.net (CloudFront)", + "cloudfront-forwarded-proto": "https", + "origin": "https://console.aws.amazon.com", + "content-length": "156", + "accept-language": "en-US,en;q=0.9", + "host": "SOMETHING.appsync-api.us-east-1.amazonaws.com", + "x-forwarded-proto": "https", + "sec-gpc": "1", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) etc.", + "accept": "*/*", + "cloudfront-is-mobile-viewer": "false", + "cloudfront-is-smarttv-viewer": "false", + "accept-encoding": "gzip, deflate, br", + "referer": "https://console.aws.amazon.com/", + "content-type": "application/json", + "sec-fetch-mode": "cors", + "x-amz-cf-id": "Fo5VIuvP6V6anIEt62WzFDCK45mzM4yEdpt5BYxOl9OFqafd-WR0cA==", + "x-amzn-trace-id": "Root=1-60488877-0b0c4e6727ab2a1c545babd0", + "authorization": "AUTH-HEADER", + "sec-fetch-dest": "empty", + "x-amz-user-agent": "AWS-Console-AppSync/", + "cloudfront-is-desktop-viewer": "true", + "sec-fetch-site": "cross-site", + "x-forwarded-port": "443" + } + }, + "prev": { + "result": {} + } +} diff --git a/tests/functional/test_lambda_trigger_events.py b/tests/functional/test_lambda_trigger_events.py index a6fb82970fc..b9dc39a967b 100644 --- a/tests/functional/test_lambda_trigger_events.py +++ b/tests/functional/test_lambda_trigger_events.py @@ -16,6 +16,7 @@ SNSEvent, SQSEvent, ) +from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import AppSyncResolverEvent from aws_lambda_powertools.utilities.data_classes.cognito_user_pool_event import ( CreateAuthChallengeTriggerEvent, CustomMessageTriggerEvent, @@ -874,3 +875,14 @@ def test_alb_event(): assert event.multi_value_headers == event.get("multiValueHeaders") assert event.body == event["body"] assert event.is_base64_encoded == event["isBase64Encoded"] + + +def test_appsync_resolver_event(): + event = AppSyncResolverEvent(load_event("appSyncResolverEvent.json")) + assert event.type_name == "Merchant" + assert event.field_name == "locations" + assert event.arguments["name"] == "value" + assert event.identity["claims"]["token_use"] == "id" + assert event.source["name"] == "Value" + assert event.request_headers["x-amzn-trace-id"] == "Root=1-60488877-0b0c4e6727ab2a1c545babd0" + assert event.prev_result == {} From 3bfc9cddfd9f499367aca2cfaedd8a5f9bfd2e4d Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 10 Mar 2021 01:37:52 -0800 Subject: [PATCH 02/33] feat(data-classes): export AppSyncResolverEvent --- aws_lambda_powertools/utilities/data_classes/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aws_lambda_powertools/utilities/data_classes/__init__.py b/aws_lambda_powertools/utilities/data_classes/__init__.py index 9c74983f3a9..e4dfb6dbb18 100644 --- a/aws_lambda_powertools/utilities/data_classes/__init__.py +++ b/aws_lambda_powertools/utilities/data_classes/__init__.py @@ -1,5 +1,6 @@ from .alb_event import ALBEvent from .api_gateway_proxy_event import APIGatewayProxyEvent, APIGatewayProxyEventV2 +from .appsync_resolver_event import AppSyncResolverEvent from .cloud_watch_logs_event import CloudWatchLogsEvent from .connect_contact_flow_event import ConnectContactFlowEvent from .dynamo_db_stream_event import DynamoDBStreamEvent @@ -13,6 +14,7 @@ __all__ = [ "APIGatewayProxyEvent", "APIGatewayProxyEventV2", + "AppSyncResolverEvent", "ALBEvent", "CloudWatchLogsEvent", "ConnectContactFlowEvent", From 8583e7a38c78d268573894403d4ae346da0aacc8 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 10 Mar 2021 01:40:59 -0800 Subject: [PATCH 03/33] chore: Correct the import --- tests/functional/test_lambda_trigger_events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_lambda_trigger_events.py b/tests/functional/test_lambda_trigger_events.py index b9dc39a967b..2440f30c229 100644 --- a/tests/functional/test_lambda_trigger_events.py +++ b/tests/functional/test_lambda_trigger_events.py @@ -8,6 +8,7 @@ ALBEvent, APIGatewayProxyEvent, APIGatewayProxyEventV2, + AppSyncResolverEvent, CloudWatchLogsEvent, EventBridgeEvent, KinesisStreamEvent, @@ -16,7 +17,6 @@ SNSEvent, SQSEvent, ) -from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import AppSyncResolverEvent from aws_lambda_powertools.utilities.data_classes.cognito_user_pool_event import ( CreateAuthChallengeTriggerEvent, CustomMessageTriggerEvent, From 0cec3e1fdf6d68b0af0c13cce3e34f78d715f5e0 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 10 Mar 2021 01:51:42 -0800 Subject: [PATCH 04/33] chore: Fix name --- .../{appSyncResolverevent.json => appSyncResolverEvent.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/events/{appSyncResolverevent.json => appSyncResolverEvent.json} (100%) diff --git a/tests/events/appSyncResolverevent.json b/tests/events/appSyncResolverEvent.json similarity index 100% rename from tests/events/appSyncResolverevent.json rename to tests/events/appSyncResolverEvent.json From 35012f5e7a576785ba14bdedc5e25f94412381d8 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 10 Mar 2021 08:08:48 -0800 Subject: [PATCH 05/33] feat(data-classes): Add get_header_value function --- .../data_classes/appsync_resolver_event.py | 24 +++++++++++++++++-- .../utilities/data_classes/common.py | 13 ++++++---- .../functional/test_lambda_trigger_events.py | 4 +++- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py index 71f04761e6a..c5a5d35f47b 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py @@ -1,6 +1,6 @@ -from typing import Dict +from typing import Dict, Optional -from aws_lambda_powertools.utilities.data_classes.common import DictWrapper +from aws_lambda_powertools.utilities.data_classes.common import DictWrapper, get_header_value class AppSyncResolverEvent(DictWrapper): @@ -45,3 +45,23 @@ def request_headers(self) -> Dict[str, str]: def prev_result(self) -> Dict[str, any]: """It represents the result of whatever previous operation was executed in a pipeline resolver.""" return self["prev"]["result"] + + def get_header_value( + self, name: str, default_value: Optional[str] = None, case_sensitive: Optional[bool] = False + ) -> Optional[str]: + """Get header value by name + + Parameters + ---------- + name: str + Header name + default_value: str, optional + Default value if no value was found by name + case_sensitive: bool + Whether to use a case sensitive look up + Returns + ------- + str, optional + Header value + """ + return get_header_value(self.request_headers, name, default_value, case_sensitive) diff --git a/aws_lambda_powertools/utilities/data_classes/common.py b/aws_lambda_powertools/utilities/data_classes/common.py index 94a357b3180..dc76b9d9ff9 100644 --- a/aws_lambda_powertools/utilities/data_classes/common.py +++ b/aws_lambda_powertools/utilities/data_classes/common.py @@ -20,6 +20,14 @@ def get(self, key: str) -> Optional[Any]: return self._data.get(key) +def get_header_value(headers: dict, name: str, default_value: str, case_sensitive: bool) -> Optional[str]: + """Get header value by name""" + if case_sensitive: + return headers.get(name, default_value) + + return next((value for key, value in headers.items() if name.lower() == key.lower()), default_value) + + class BaseProxyEvent(DictWrapper): @property def headers(self) -> Dict[str, str]: @@ -72,7 +80,4 @@ def get_header_value( str, optional Header value """ - if case_sensitive: - return self.headers.get(name, default_value) - - return next((value for key, value in self.headers.items() if name.lower() == key.lower()), default_value) + return get_header_value(self.headers, name, default_value, case_sensitive) diff --git a/tests/functional/test_lambda_trigger_events.py b/tests/functional/test_lambda_trigger_events.py index 2440f30c229..1317c41c63c 100644 --- a/tests/functional/test_lambda_trigger_events.py +++ b/tests/functional/test_lambda_trigger_events.py @@ -884,5 +884,7 @@ def test_appsync_resolver_event(): assert event.arguments["name"] == "value" assert event.identity["claims"]["token_use"] == "id" assert event.source["name"] == "Value" - assert event.request_headers["x-amzn-trace-id"] == "Root=1-60488877-0b0c4e6727ab2a1c545babd0" + assert event.get_header_value("X-amzn-trace-id") == "Root=1-60488877-0b0c4e6727ab2a1c545babd0" + assert event.get_header_value("X-amzn-trace-id", case_sensitive=True) is None + assert event.get_header_value("missing", default_value="Foo") == "Foo" assert event.prev_result == {} From d3ecf01e27ae4aff83494f4c4b8b938d4d0081a7 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 10 Mar 2021 09:01:23 -0800 Subject: [PATCH 06/33] feat(data-classes): Add AppSyncIdentityCognito --- .../data_classes/appsync_resolver_event.py | 96 ++++++++++++++++++- .../functional/test_lambda_trigger_events.py | 5 + 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py index c5a5d35f47b..423746574c3 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py @@ -1,8 +1,98 @@ -from typing import Dict, Optional +from typing import Dict, List, Optional, Union from aws_lambda_powertools.utilities.data_classes.common import DictWrapper, get_header_value +class AppSyncIdentityIAM(DictWrapper): + """AWS_IAM authorization""" + + @property + def source_ip(self) -> List[str]: + """The source IP address of the caller received by AWS AppSync. """ + return self["sourceIp"] + + @property + def username(self) -> str: + """The user name of the authenticated user. IAM user principal""" + return self["username"] + + @property + def account_id(self) -> str: + """The AWS account ID of the caller.""" + return self["accountId"] + + @property + def cognito_identity_pool_id(self) -> str: + """The Amazon Cognito identity pool ID associated with the caller.""" + return self["cognitoIdentityPoolId"] + + @property + def user_arn(self) -> str: + return self["userArn"] + + @property + def cognito_identity_auth_type(self) -> str: + """Either authenticated or unauthenticated based on the identity type.""" + return self["cognitoIdentityAuthType"] + + @property + def cognito_identity_auth_provider(self) -> str: + """A comma separated list of external identity provider information used in obtaining the + credentials used to sign the request.""" + return self["cognitoIdentityAuthProvider"] + + +class AppSyncIdentityCognito(DictWrapper): + """AMAZON_COGNITO_USER_POOLS authorization""" + + @property + def source_ip(self) -> List[str]: + """The source IP address of the caller received by AWS AppSync. """ + return self["sourceIp"] + + @property + def username(self) -> str: + """The user name of the authenticated user.""" + return self["username"] + + @property + def sub(self) -> str: + """The UUID of the authenticated user.""" + return self["sub"] + + @property + def claims(self) -> Dict[str, str]: + """The claims that the user has.""" + return self["claims"] + + @property + def default_auth_strategy(self) -> str: + """The default authorization strategy for this caller (ALLOW or DENY).""" + return self["defaultAuthStrategy"] + + @property + def groups(self) -> any: + return self.get("groups") + + @property + def issuer(self) -> str: + """The token issuer.""" + return self["issuer"] + + +def get_identity_object(identity_object: Optional[dict]) -> any: + # API_KEY authorization + if identity_object is None: + return None + + # AMAZON_COGNITO_USER_POOLS authorization + if "sub" in identity_object: + return AppSyncIdentityCognito(identity_object) + + # AWS_IAM authorization + return AppSyncIdentityIAM(identity_object) + + class AppSyncResolverEvent(DictWrapper): """AppSync resolver event @@ -27,9 +117,9 @@ def arguments(self) -> Dict[str, any]: return self["arguments"] @property - def identity(self) -> Dict[str, any]: + def identity(self) -> Union[None, AppSyncIdentityIAM, AppSyncIdentityCognito]: """An object that contains information about the caller.""" - return self["identity"] + return get_identity_object(self["identity"]) @property def source(self) -> Dict[str, any]: diff --git a/tests/functional/test_lambda_trigger_events.py b/tests/functional/test_lambda_trigger_events.py index 1317c41c63c..6388e3c5d19 100644 --- a/tests/functional/test_lambda_trigger_events.py +++ b/tests/functional/test_lambda_trigger_events.py @@ -17,6 +17,7 @@ SNSEvent, SQSEvent, ) +from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import AppSyncIdentityCognito from aws_lambda_powertools.utilities.data_classes.cognito_user_pool_event import ( CreateAuthChallengeTriggerEvent, CustomMessageTriggerEvent, @@ -888,3 +889,7 @@ def test_appsync_resolver_event(): assert event.get_header_value("X-amzn-trace-id", case_sensitive=True) is None assert event.get_header_value("missing", default_value="Foo") == "Foo" assert event.prev_result == {} + assert isinstance(event.identity, AppSyncIdentityCognito) + identity: AppSyncIdentityCognito = event.identity + assert identity.claims is not None + assert identity.sub == "07920713-4526-4642-9c88-2953512de441" From bc0e20545e3ca66686161123224af298d74a4259 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 10 Mar 2021 09:22:02 -0800 Subject: [PATCH 07/33] tests(data-classes): Add test_get_identity_object_iam --- .../data_classes/appsync_resolver_event.py | 32 +++++++++------- .../functional/test_lambda_trigger_events.py | 38 ++++++++++++++++++- 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py index 423746574c3..ccba03cb2b7 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py @@ -3,6 +3,20 @@ from aws_lambda_powertools.utilities.data_classes.common import DictWrapper, get_header_value +def get_identity_object(identity_object: Optional[dict]) -> any: + """Get the identity object with the best detected type""" + # API_KEY authorization + if identity_object is None: + return None + + # AMAZON_COGNITO_USER_POOLS authorization + if "sub" in identity_object: + return AppSyncIdentityCognito(identity_object) + + # AWS_IAM authorization + return AppSyncIdentityIAM(identity_object) + + class AppSyncIdentityIAM(DictWrapper): """AWS_IAM authorization""" @@ -26,6 +40,11 @@ def cognito_identity_pool_id(self) -> str: """The Amazon Cognito identity pool ID associated with the caller.""" return self["cognitoIdentityPoolId"] + @property + def cognito_identity_id(self) -> str: + """The Amazon Cognito identity ID of the caller.""" + return self["cognitoIdentityId"] + @property def user_arn(self) -> str: return self["userArn"] @@ -80,19 +99,6 @@ def issuer(self) -> str: return self["issuer"] -def get_identity_object(identity_object: Optional[dict]) -> any: - # API_KEY authorization - if identity_object is None: - return None - - # AMAZON_COGNITO_USER_POOLS authorization - if "sub" in identity_object: - return AppSyncIdentityCognito(identity_object) - - # AWS_IAM authorization - return AppSyncIdentityIAM(identity_object) - - class AppSyncResolverEvent(DictWrapper): """AppSync resolver event diff --git a/tests/functional/test_lambda_trigger_events.py b/tests/functional/test_lambda_trigger_events.py index 6388e3c5d19..28d1e3f8be1 100644 --- a/tests/functional/test_lambda_trigger_events.py +++ b/tests/functional/test_lambda_trigger_events.py @@ -17,7 +17,11 @@ SNSEvent, SQSEvent, ) -from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import AppSyncIdentityCognito +from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import ( + AppSyncIdentityCognito, + AppSyncIdentityIAM, + get_identity_object, +) from aws_lambda_powertools.utilities.data_classes.cognito_user_pool_event import ( CreateAuthChallengeTriggerEvent, CustomMessageTriggerEvent, @@ -893,3 +897,35 @@ def test_appsync_resolver_event(): identity: AppSyncIdentityCognito = event.identity assert identity.claims is not None assert identity.sub == "07920713-4526-4642-9c88-2953512de441" + assert len(identity.source_ip) == 1 + assert identity.username == "mike" + assert identity.default_auth_strategy == "ALLOW" + assert identity.groups is None + assert identity.issuer == identity["issuer"] + + +def test_get_identity_object_is_none(): + assert get_identity_object(None) is None + + +def test_get_identity_object_iam(): + identity = { + "accountId": "string", + "cognitoIdentityPoolId": "string", + "cognitoIdentityId": "string", + "sourceIp": ["string"], + "username": "string", + "userArn": "string", + "cognitoIdentityAuthType": "string", + "cognitoIdentityAuthProvider": "string", + } + identity_object = get_identity_object(identity) + assert isinstance(identity_object, AppSyncIdentityIAM) + assert identity_object.account_id == identity["accountId"] + assert identity_object.cognito_identity_pool_id == identity["cognitoIdentityPoolId"] + assert identity_object.cognito_identity_id == identity["cognitoIdentityId"] + assert identity_object.source_ip == identity["sourceIp"] + assert identity_object.username == identity["username"] + assert identity_object.user_arn == identity["userArn"] + assert identity_object.cognito_identity_auth_type == identity["cognitoIdentityAuthType"] + assert identity_object.cognito_identity_auth_provider == identity["cognitoIdentityAuthProvider"] From 522a02a83836d47f77ec9e285432667d331b446e Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 10 Mar 2021 09:35:06 -0800 Subject: [PATCH 08/33] feat(logging): Add correlation path for APP_SYNC_RESOLVER --- aws_lambda_powertools/logging/correlation_paths.py | 1 + .../utilities/data_classes/appsync_resolver_event.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/aws_lambda_powertools/logging/correlation_paths.py b/aws_lambda_powertools/logging/correlation_paths.py index 73227754363..9e273ceaeb5 100644 --- a/aws_lambda_powertools/logging/correlation_paths.py +++ b/aws_lambda_powertools/logging/correlation_paths.py @@ -2,5 +2,6 @@ API_GATEWAY_REST = "requestContext.requestId" API_GATEWAY_HTTP = API_GATEWAY_REST +APP_SYNC_RESOLVER = "request.headers.x-amzn-trace-id" APPLICATION_LOAD_BALANCER = "headers.x-amzn-trace-id" EVENT_BRIDGE = "id" diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py index ccba03cb2b7..a85ef563110 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py @@ -47,6 +47,7 @@ def cognito_identity_id(self) -> str: @property def user_arn(self) -> str: + """The ARN of the IAM user.""" return self["userArn"] @property @@ -105,6 +106,7 @@ class AppSyncResolverEvent(DictWrapper): Documentation: ------------- - https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html + - https://docs.amplify.aws/cli/graphql-transformer/function#structure-of-the-function-event """ @property From 6d960eb4f1946eca35d455fd8b33e5a8520ab616 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 10 Mar 2021 10:34:47 -0800 Subject: [PATCH 09/33] chore: Code review changes --- aws_lambda_powertools/logging/correlation_paths.py | 2 +- .../utilities/data_classes/appsync_resolver_event.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/aws_lambda_powertools/logging/correlation_paths.py b/aws_lambda_powertools/logging/correlation_paths.py index 9e273ceaeb5..f5c9b636a88 100644 --- a/aws_lambda_powertools/logging/correlation_paths.py +++ b/aws_lambda_powertools/logging/correlation_paths.py @@ -2,6 +2,6 @@ API_GATEWAY_REST = "requestContext.requestId" API_GATEWAY_HTTP = API_GATEWAY_REST -APP_SYNC_RESOLVER = "request.headers.x-amzn-trace-id" +APPSYNC_RESOLVER = "request.headers.x-amzn-trace-id" APPLICATION_LOAD_BALANCER = "headers.x-amzn-trace-id" EVENT_BRIDGE = "id" diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py index a85ef563110..8746ecc5eda 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py @@ -1,9 +1,9 @@ -from typing import Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Union from aws_lambda_powertools.utilities.data_classes.common import DictWrapper, get_header_value -def get_identity_object(identity_object: Optional[dict]) -> any: +def get_identity_object(identity_object: Optional[dict]) -> Any: """Get the identity object with the best detected type""" # API_KEY authorization if identity_object is None: @@ -91,8 +91,9 @@ def default_auth_strategy(self) -> str: return self["defaultAuthStrategy"] @property - def groups(self) -> any: - return self.get("groups") + def groups(self) -> List[str]: + """Array of OIDC groups""" + return self["groups"] @property def issuer(self) -> str: From 789d5db6828f76cc563d5a66fcaf8b157eb1f087 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 10 Mar 2021 11:51:20 -0800 Subject: [PATCH 10/33] feat(data-classes): Add AppSyncResolverEventInfo --- .../data_classes/appsync_resolver_event.py | 70 ++++++++++++++++--- .../utilities/data_classes/common.py | 11 ++- .../functional/test_lambda_trigger_events.py | 32 +++++++++ 3 files changed, 102 insertions(+), 11 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py index 8746ecc5eda..d6d460cdd1c 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py @@ -3,18 +3,18 @@ from aws_lambda_powertools.utilities.data_classes.common import DictWrapper, get_header_value -def get_identity_object(identity_object: Optional[dict]) -> Any: - """Get the identity object with the best detected type""" +def get_identity_object(identity: Optional[dict]) -> Any: + """Get the identity object based on the best detected type""" # API_KEY authorization - if identity_object is None: + if identity is None: return None # AMAZON_COGNITO_USER_POOLS authorization - if "sub" in identity_object: - return AppSyncIdentityCognito(identity_object) + if "sub" in identity: + return AppSyncIdentityCognito(identity) # AWS_IAM authorization - return AppSyncIdentityIAM(identity_object) + return AppSyncIdentityIAM(identity) class AppSyncIdentityIAM(DictWrapper): @@ -101,9 +101,43 @@ def issuer(self) -> str: return self["issuer"] +class AppSyncResolverEventInfo(DictWrapper): + """The info section contains information about the GraphQL request""" + + @property + def field_name(self) -> str: + """The name of the field that is currently being resolved.""" + return self["fieldName"] + + @property + def parent_type_name(self) -> str: + """The name of the parent type for the field that is currently being resolved.""" + return self["parentTypeName"] + + @property + def variables(self) -> Dict[str, str]: + """A map which holds all variables that are passed into the GraphQL request.""" + return self["variables"] + + @property + def selection_set_list(self) -> List[str]: + """A list representation of the fields in the GraphQL selection set. Fields that are aliased will + only be referenced by the alias name, not the field name. The following example shows this in detail.""" + return self.get("selectionSetList") + + @property + def selection_set_graphql(self) -> Optional[str]: + """A string representation of the selection set, formatted as GraphQL schema definition language (SDL). + Although fragments are not be merged into the selection set, inline fragments are preserved.""" + return self.get("selectionSetGraphQL") + + class AppSyncResolverEvent(DictWrapper): """AppSync resolver event + NOTE: AppSync Resolver Events can come in various shapes this data class supports what + Amplify GraphQL Transformer produces + Documentation: ------------- - https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html @@ -127,13 +161,20 @@ def arguments(self) -> Dict[str, any]: @property def identity(self) -> Union[None, AppSyncIdentityIAM, AppSyncIdentityCognito]: - """An object that contains information about the caller.""" - return get_identity_object(self["identity"]) + """An object that contains information about the caller. + + Depending of the type of identify found: + + - API_KEY authorization - returns None + - AWS_IAM authorization - returns AppSyncIdentityIAM + - AMAZON_COGNITO_USER_POOLS authorization - returns AppSyncIdentityCognito + """ + return get_identity_object(self.get("identity")) @property def source(self) -> Dict[str, any]: """A map that contains the resolution of the parent field.""" - return self["source"] + return self.get("source") @property def request_headers(self) -> Dict[str, str]: @@ -145,6 +186,17 @@ def prev_result(self) -> Dict[str, any]: """It represents the result of whatever previous operation was executed in a pipeline resolver.""" return self["prev"]["result"] + @property + def info(self) -> Optional[AppSyncResolverEventInfo]: + """The info section contains information about the GraphQL request. + + NOTE: This is not present for Amplify GraphQL Transformer functions + """ + info_dict = self.get("info") + if info_dict is None: + return None + return AppSyncResolverEventInfo(info_dict) + def get_header_value( self, name: str, default_value: Optional[str] = None, case_sensitive: Optional[bool] = False ) -> Optional[str]: diff --git a/aws_lambda_powertools/utilities/data_classes/common.py b/aws_lambda_powertools/utilities/data_classes/common.py index dc76b9d9ff9..da51d8e0d5b 100644 --- a/aws_lambda_powertools/utilities/data_classes/common.py +++ b/aws_lambda_powertools/utilities/data_classes/common.py @@ -20,12 +20,19 @@ def get(self, key: str) -> Optional[Any]: return self._data.get(key) -def get_header_value(headers: dict, name: str, default_value: str, case_sensitive: bool) -> Optional[str]: +def get_header_value(headers: Dict[str, str], name: str, default_value: str, case_sensitive: bool) -> Optional[str]: """Get header value by name""" if case_sensitive: return headers.get(name, default_value) - return next((value for key, value in headers.items() if name.lower() == key.lower()), default_value) + name_lower = name.lower() + + return next( + # Iterate over the dict and do a case insensitive key comparison + (value for key, value in headers.items() if key.lower() == name_lower), + # Default value is returned if no matches was found + default_value, + ) class BaseProxyEvent(DictWrapper): diff --git a/tests/functional/test_lambda_trigger_events.py b/tests/functional/test_lambda_trigger_events.py index 28d1e3f8be1..2117680b723 100644 --- a/tests/functional/test_lambda_trigger_events.py +++ b/tests/functional/test_lambda_trigger_events.py @@ -20,6 +20,7 @@ from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import ( AppSyncIdentityCognito, AppSyncIdentityIAM, + AppSyncResolverEventInfo, get_identity_object, ) from aws_lambda_powertools.utilities.data_classes.cognito_user_pool_event import ( @@ -884,6 +885,7 @@ def test_alb_event(): def test_appsync_resolver_event(): event = AppSyncResolverEvent(load_event("appSyncResolverEvent.json")) + assert event.type_name == "Merchant" assert event.field_name == "locations" assert event.arguments["name"] == "value" @@ -893,6 +895,7 @@ def test_appsync_resolver_event(): assert event.get_header_value("X-amzn-trace-id", case_sensitive=True) is None assert event.get_header_value("missing", default_value="Foo") == "Foo" assert event.prev_result == {} + assert event.info is None assert isinstance(event.identity, AppSyncIdentityCognito) identity: AppSyncIdentityCognito = event.identity assert identity.claims is not None @@ -906,6 +909,8 @@ def test_appsync_resolver_event(): def test_get_identity_object_is_none(): assert get_identity_object(None) is None + event = AppSyncResolverEvent({}) + assert event.identity is None def test_get_identity_object_iam(): @@ -919,7 +924,9 @@ def test_get_identity_object_iam(): "cognitoIdentityAuthType": "string", "cognitoIdentityAuthProvider": "string", } + identity_object = get_identity_object(identity) + assert isinstance(identity_object, AppSyncIdentityIAM) assert identity_object.account_id == identity["accountId"] assert identity_object.cognito_identity_pool_id == identity["cognitoIdentityPoolId"] @@ -929,3 +936,28 @@ def test_get_identity_object_iam(): assert identity_object.user_arn == identity["userArn"] assert identity_object.cognito_identity_auth_type == identity["cognitoIdentityAuthType"] assert identity_object.cognito_identity_auth_provider == identity["cognitoIdentityAuthProvider"] + + +def test_appsync_resolver_event_info(): + info_dict = { + "fieldName": "getPost", + "parentTypeName": "Query", + "variables": {"postId": "123", "authorId": "456"}, + "selectionSetList": ["postId", "title"], + "selectionSetGraphQL": "{\n getPost(id: $postId) {\n postId\n etc..", + } + event = {"info": info_dict} + + event = AppSyncResolverEvent(event) + + assert event.source is None + assert event.identity is None + assert event.info is not None + assert isinstance(event.info, AppSyncResolverEventInfo) + info: AppSyncResolverEventInfo = event.info + assert info.field_name == info_dict["fieldName"] + assert info.parent_type_name == info_dict["parentTypeName"] + assert info.variables == info_dict["variables"] + assert info.variables["postId"] == "123" + assert info.selection_set_list == info_dict["selectionSetList"] + assert info.selection_set_graphql == info_dict["selectionSetGraphQL"] From c3fa1173770f2e5f03ac0c2dcce41f3a8bf373bf Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 10 Mar 2021 12:56:17 -0800 Subject: [PATCH 11/33] fix(logging): Correct paths for AppSync --- aws_lambda_powertools/logging/correlation_paths.py | 4 ++-- tests/events/appSyncDirectResolver.json | 0 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 tests/events/appSyncDirectResolver.json diff --git a/aws_lambda_powertools/logging/correlation_paths.py b/aws_lambda_powertools/logging/correlation_paths.py index f5c9b636a88..cbccd85637f 100644 --- a/aws_lambda_powertools/logging/correlation_paths.py +++ b/aws_lambda_powertools/logging/correlation_paths.py @@ -2,6 +2,6 @@ API_GATEWAY_REST = "requestContext.requestId" API_GATEWAY_HTTP = API_GATEWAY_REST -APPSYNC_RESOLVER = "request.headers.x-amzn-trace-id" -APPLICATION_LOAD_BALANCER = "headers.x-amzn-trace-id" +APPSYNC_RESOLVER = 'request.headers."x-amzn-trace-id"' +APPLICATION_LOAD_BALANCER = 'headers."x-amzn-trace-id"' EVENT_BRIDGE = "id" diff --git a/tests/events/appSyncDirectResolver.json b/tests/events/appSyncDirectResolver.json new file mode 100644 index 00000000000..e69de29bb2d From 006eeff2d4202fc96fa34861d7fed0821f0eb9d0 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 10 Mar 2021 12:57:10 -0800 Subject: [PATCH 12/33] tests(data-classes): Add test_appsync_resolver_direct --- tests/events/appSyncDirectResolver.json | 74 +++++++++++++++++++ .../functional/test_lambda_trigger_events.py | 11 +++ 2 files changed, 85 insertions(+) diff --git a/tests/events/appSyncDirectResolver.json b/tests/events/appSyncDirectResolver.json index e69de29bb2d..08c3d00b203 100644 --- a/tests/events/appSyncDirectResolver.json +++ b/tests/events/appSyncDirectResolver.json @@ -0,0 +1,74 @@ +{ + "arguments": { + "id": "my identifier" + }, + "identity": { + "claims": { + "sub": "192879fc-a240-4bf1-ab5a-d6a00f3063f9", + "email_verified": true, + "iss": "https://cognito-idp.us-west-2.amazonaws.com/us-west-xxxxxxxxxxx", + "phone_number_verified": false, + "cognito:username": "jdoe", + "aud": "7471s60os7h0uu77i1tk27sp9n", + "event_id": "bc334ed8-a938-4474-b644-9547e304e606", + "token_use": "id", + "auth_time": 1599154213, + "phone_number": "+19999999999", + "exp": 1599157813, + "iat": 1599154213, + "email": "jdoe@email.com" + }, + "defaultAuthStrategy": "ALLOW", + "groups": null, + "issuer": "https://cognito-idp.us-west-2.amazonaws.com/us-west-xxxxxxxxxxx", + "sourceIp": [ + "1.1.1.1" + ], + "sub": "192879fc-a240-4bf1-ab5a-d6a00f3063f9", + "username": "jdoe" + }, + "source": null, + "request": { + "headers": { + "x-forwarded-for": "1.1.1.1, 2.2.2.2", + "cloudfront-viewer-country": "US", + "cloudfront-is-tablet-viewer": "false", + "via": "2.0 xxxxxxxxxxxxxxxx.cloudfront.net (CloudFront)", + "cloudfront-forwarded-proto": "https", + "origin": "https://us-west-1.console.aws.amazon.com", + "content-length": "217", + "accept-language": "en-US,en;q=0.9", + "host": "xxxxxxxxxxxxxxxx.appsync-api.us-west-1.amazonaws.com", + "x-forwarded-proto": "https", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36", + "accept": "*/*", + "cloudfront-is-mobile-viewer": "false", + "cloudfront-is-smarttv-viewer": "false", + "accept-encoding": "gzip, deflate, br", + "referer": "https://us-west-1.console.aws.amazon.com/appsync/home?region=us-west-1", + "content-type": "application/json", + "sec-fetch-mode": "cors", + "x-amz-cf-id": "3aykhqlUwQeANU-HGY7E_guV5EkNeMMtwyOgiA==", + "x-amzn-trace-id": "Root=1-5f512f51-fac632066c5e848ae714", + "authorization": "eyJraWQiOiJScWFCSlJqYVJlM0hrSnBTUFpIcVRXazNOW...", + "sec-fetch-dest": "empty", + "x-amz-user-agent": "AWS-Console-AppSync/", + "cloudfront-is-desktop-viewer": "true", + "sec-fetch-site": "cross-site", + "x-forwarded-port": "443" + } + }, + "prev": null, + "info": { + "selectionSetList": [ + "id", + "field1", + "field2" + ], + "selectionSetGraphQL": "{\n id\n field1\n field2\n}", + "parentTypeName": "Mutation", + "fieldName": "createSomething", + "variables": {} + }, + "stash": {} +} diff --git a/tests/functional/test_lambda_trigger_events.py b/tests/functional/test_lambda_trigger_events.py index 2117680b723..6ec6ce5f2dd 100644 --- a/tests/functional/test_lambda_trigger_events.py +++ b/tests/functional/test_lambda_trigger_events.py @@ -938,6 +938,17 @@ def test_get_identity_object_iam(): assert identity_object.cognito_identity_auth_provider == identity["cognitoIdentityAuthProvider"] +def test_appsync_resolver_direct(): + event = AppSyncResolverEvent(load_event("appSyncDirectResolver.json")) + + assert event.source is None + assert event.arguments["id"] == "my identifier" + assert isinstance(event.identity, AppSyncIdentityCognito) + info = event.info + assert isinstance(info, AppSyncResolverEventInfo) + assert info.selection_set_list is not None + + def test_appsync_resolver_event_info(): info_dict = { "fieldName": "getPost", From cf26506936d496eca1cddfbc978af557f7d05a35 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 10 Mar 2021 12:59:43 -0800 Subject: [PATCH 13/33] docs(data-classes): Add AppSync Resolver docs --- docs/utilities/data_classes.md | 107 ++++++++++++++++++++++++++------- 1 file changed, 85 insertions(+), 22 deletions(-) diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index ca71a928a41..3016514070f 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -50,8 +50,10 @@ Event Source | Data_class ------------------------------------------------- | --------------------------------------------------------------------------------- [API Gateway Proxy](#api-gateway-proxy) | `APIGatewayProxyEvent` [API Gateway Proxy event v2](#api-gateway-proxy-v2) | `APIGatewayProxyEventV2` +[AppSync Resolver](#appsync-resolver) | `AppSyncResolverEvent` [CloudWatch Logs](#cloudwatch-logs) | `CloudWatchLogsEvent` [Cognito User Pool](#cognito-user-pool) | Multiple available under `cognito_user_pool_event` +[Connect Contact Flow](#connect-contact-flow) | `ConnectContactFlowEvent` [DynamoDB streams](#dynamodb-streams) | `DynamoDBStreamEvent`, `DynamoDBRecordEventName` [EventBridge](#eventbridge) | `EventBridgeEvent` [Kinesis Data Stream](#kinesis-streams) | `KinesisStreamEvent` @@ -101,6 +103,69 @@ Typically used for API Gateway REST API or HTTP API using v1 proxy event. do_something_with(event.body, query_string_parameters) ``` +### AppSync Resolver + +Used when building a Lambda GraphQL Resolvers with [Amplify GraphQL Transform Library](https://docs.amplify.aws/cli/graphql-transformer/function) + +=== "lambda_app.py" + + ```python + from aws_lambda_powertools.logging import Logger, correlation_paths + from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import ( + AppSyncResolverEvent, + AppSyncIdentityCognito + ) + + logger = Logger() + + def get_locations(name: str = None, size: int = 0, page: int = 0): + pass + + @logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) + def lambda_handler(event, context): + event = AppSyncResolverEvent(event) + + # Case insensitive look up of request headers + x_forwarded_for = event.get_header_value("x-forwarded-for") + + # Support for AppSyncIdentityCognito or AppSyncIdentityIAM identity types + assert isinstance(event.identity, AppSyncIdentityCognito) + identity: AppSyncIdentityCognito = event.identity + + # Logging with correlation_id + logger.info({ + "x-forwarded-for": x_forwarded_for, + "username": identity.username + }) + + if event.type_name == "Merchant" and event.field_name == "locations": + return get_locations(**event.arguments) + + raise ValueError(f"Unsupported field resolver: {event.field_name}") + + ``` +=== "CloudWatch Log" + + ```json + { + "level":"INFO", + "location":"lambda_handler:22", + "message":{ + "x-forwarded-for":"11.215.2.22, 64.44.173.11", + "username":"mike" + }, + "timestamp":"2021-03-10 12:38:40,062", + "service":"service_undefined", + "sampling_rate":0.0, + "cold_start":true, + "function_name":"func_name", + "function_memory_size":512, + "function_arn":"func_arn", + "function_request_id":"6735a29c-c000-4ae3-94e6-1f1c934f7f94", + "correlation_id":"Root=1-60488877-0b0c4e6727ab2a1c545babd0" + } + ``` + ### CloudWatch Logs CloudWatch Logs events by default are compressed and base64 encoded. You can use the helper function provided to decode, @@ -151,6 +216,26 @@ Verify Auth Challenge | `data_classes.cognito_user_pool_event.VerifyAuthChalleng do_something_with(user_attributes) ``` +### Connect Contact Flow + +=== "lambda_app.py" + + ```python + from aws_lambda_powertools.utilities.data_classes.connect_contact_flow_event import ( + ConnectContactFlowChannel, + ConnectContactFlowEndpointType, + ConnectContactFlowEvent, + ConnectContactFlowInitiationMethod, + ) + + def lambda_handler(event, context): + event: ConnectContactFlowEvent = ConnectContactFlowEvent(event) + assert event.contact_data.attributes == {"Language": "en-US"} + assert event.contact_data.channel == ConnectContactFlowChannel.VOICE + assert event.contact_data.customer_endpoint.endpoint_type == ConnectContactFlowEndpointType.TELEPHONE_NUMBER + assert event.contact_data.initiation_method == ConnectContactFlowInitiationMethod.API + ``` + ### DynamoDB Streams The DynamoDB data class utility provides the base class for `DynamoDBStreamEvent`, a typed class for @@ -280,25 +365,3 @@ or plain text, depending on the original payload. for record in event.records: do_something_with(record.body) ``` - -### Connect - -**Connect Contact Flow** - -=== "lambda_app.py" - - ```python - from aws_lambda_powertools.utilities.data_classes.connect_contact_flow_event import ( - ConnectContactFlowChannel, - ConnectContactFlowEndpointType, - ConnectContactFlowEvent, - ConnectContactFlowInitiationMethod, - ) - - def lambda_handler(event, context): - event: ConnectContactFlowEvent = ConnectContactFlowEvent(event) - assert event.contact_data.attributes == {"Language": "en-US"} - assert event.contact_data.channel == ConnectContactFlowChannel.VOICE - assert event.contact_data.customer_endpoint.endpoint_type == ConnectContactFlowEndpointType.TELEPHONE_NUMBER - assert event.contact_data.initiation_method == ConnectContactFlowInitiationMethod.API - ``` From 0920999330fa3f2f9d2db16938f987b63e191e97 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 10 Mar 2021 13:02:34 -0800 Subject: [PATCH 14/33] chore: bump ci --- docs/utilities/data_classes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index 3016514070f..5aefcabb6be 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -106,6 +106,7 @@ Typically used for API Gateway REST API or HTTP API using v1 proxy event. ### AppSync Resolver Used when building a Lambda GraphQL Resolvers with [Amplify GraphQL Transform Library](https://docs.amplify.aws/cli/graphql-transformer/function) +and can also be used for AppSync Direct Lambda Resolvers. === "lambda_app.py" From 81346b971b222e3ef1366b6bebb89f031c70435e Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 10 Mar 2021 14:09:25 -0800 Subject: [PATCH 15/33] feat(data-classes): Add AppSyncResolverEvent.stash --- .../data_classes/appsync_resolver_event.py | 15 +++++++++++++-- tests/functional/test_lambda_trigger_events.py | 6 ++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py index d6d460cdd1c..0c6142328ec 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py @@ -182,9 +182,12 @@ def request_headers(self) -> Dict[str, str]: return self["request"]["headers"] @property - def prev_result(self) -> Dict[str, any]: + def prev_result(self) -> Optional[Dict[str, any]]: """It represents the result of whatever previous operation was executed in a pipeline resolver.""" - return self["prev"]["result"] + prev = self.get("prev") + if not prev: + return None + return prev.get("result") @property def info(self) -> Optional[AppSyncResolverEventInfo]: @@ -197,6 +200,14 @@ def info(self) -> Optional[AppSyncResolverEventInfo]: return None return AppSyncResolverEventInfo(info_dict) + @property + def stash(self) -> Optional[dict]: + """The stash is a map that is made available inside each resolver and function mapping template. + The same stash instance lives through a single resolver execution. This means that you can use the + stash to pass arbitrary data across request and response mapping templates, and across functions in + a pipeline resolver.""" + return self.get("stash") + def get_header_value( self, name: str, default_value: Optional[str] = None, case_sensitive: Optional[bool] = False ) -> Optional[str]: diff --git a/tests/functional/test_lambda_trigger_events.py b/tests/functional/test_lambda_trigger_events.py index 6ec6ce5f2dd..e0b0982fc84 100644 --- a/tests/functional/test_lambda_trigger_events.py +++ b/tests/functional/test_lambda_trigger_events.py @@ -943,10 +943,16 @@ def test_appsync_resolver_direct(): assert event.source is None assert event.arguments["id"] == "my identifier" + assert event.stash == {} + assert event.prev_result is None assert isinstance(event.identity, AppSyncIdentityCognito) + info = event.info assert isinstance(info, AppSyncResolverEventInfo) assert info.selection_set_list is not None + assert info.selection_set_list == info["selectionSetList"] + assert info.selection_set_graphql == info["selectionSetGraphQL"] + assert info.parent_type_name == info["parentTypeName"] def test_appsync_resolver_event_info(): From bf5ebecf0feda1462dcf7b9bd839b59bfb3d0558 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 10 Mar 2021 17:36:11 -0800 Subject: [PATCH 16/33] refactor(data-classes): Support direct and amplify --- .../data_classes/appsync_resolver_event.py | 22 +++++++++------ docs/core/logger.md | 2 +- docs/utilities/data_classes.md | 5 ++-- .../functional/test_lambda_trigger_events.py | 27 ++++++++++++++++++- 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py index 0c6142328ec..490e5bae8ee 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py @@ -117,7 +117,7 @@ def parent_type_name(self) -> str: @property def variables(self) -> Dict[str, str]: """A map which holds all variables that are passed into the GraphQL request.""" - return self["variables"] + return self.get("variables") @property def selection_set_list(self) -> List[str]: @@ -144,15 +144,24 @@ class AppSyncResolverEvent(DictWrapper): - https://docs.amplify.aws/cli/graphql-transformer/function#structure-of-the-function-event """ + def __init__(self, data: dict): + super().__init__(data) + + info: dict = data.get("info") + if not info: + info = {"fieldName": self.get("fieldName"), "parentTypeName": self.get("typeName")} + + self._info = AppSyncResolverEventInfo(info) + @property def type_name(self) -> str: """The name of the parent type for the field that is currently being resolved.""" - return self["typeName"] + return self.info.parent_type_name @property def field_name(self) -> str: """The name of the field that is currently being resolved.""" - return self["fieldName"] + return self.info.field_name @property def arguments(self) -> Dict[str, any]: @@ -190,15 +199,12 @@ def prev_result(self) -> Optional[Dict[str, any]]: return prev.get("result") @property - def info(self) -> Optional[AppSyncResolverEventInfo]: + def info(self) -> AppSyncResolverEventInfo: """The info section contains information about the GraphQL request. NOTE: This is not present for Amplify GraphQL Transformer functions """ - info_dict = self.get("info") - if info_dict is None: - return None - return AppSyncResolverEventInfo(info_dict) + return self._info @property def stash(self) -> Optional[dict]: diff --git a/docs/core/logger.md b/docs/core/logger.md index 27cbd725f80..dc5e0c0d647 100644 --- a/docs/core/logger.md +++ b/docs/core/logger.md @@ -516,7 +516,7 @@ When logging exceptions, Logger will add new keys named `exception_name` and `ex "timestamp": "2020-08-28 18:11:38,886", "service": "service_undefined", "sampling_rate": 0.0, - "exception_name":"ValueError", + "exception_name": "ValueError", "exception": "Traceback (most recent call last):\n File \"\", line 2, in \nValueError: something went wrong" } ``` diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index 5aefcabb6be..a4e1b90cdf4 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -120,6 +120,7 @@ and can also be used for AppSync Direct Lambda Resolvers. logger = Logger() def get_locations(name: str = None, size: int = 0, page: int = 0): + """Your resolver logic here""" pass @logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) @@ -134,7 +135,7 @@ and can also be used for AppSync Direct Lambda Resolvers. identity: AppSyncIdentityCognito = event.identity # Logging with correlation_id - logger.info({ + logger.debug({ "x-forwarded-for": x_forwarded_for, "username": identity.username }) @@ -149,7 +150,7 @@ and can also be used for AppSync Direct Lambda Resolvers. ```json { - "level":"INFO", + "level":"DEBUG", "location":"lambda_handler:22", "message":{ "x-forwarded-for":"11.215.2.22, 64.44.173.11", diff --git a/tests/functional/test_lambda_trigger_events.py b/tests/functional/test_lambda_trigger_events.py index e0b0982fc84..40dd374960c 100644 --- a/tests/functional/test_lambda_trigger_events.py +++ b/tests/functional/test_lambda_trigger_events.py @@ -895,7 +895,17 @@ def test_appsync_resolver_event(): assert event.get_header_value("X-amzn-trace-id", case_sensitive=True) is None assert event.get_header_value("missing", default_value="Foo") == "Foo" assert event.prev_result == {} - assert event.info is None + assert event.stash is None + + info = event.info + assert info is not None + assert isinstance(info, AppSyncResolverEventInfo) + assert info.field_name == event["fieldName"] + assert info.parent_type_name == event["typeName"] + assert info.variables is None + assert info.selection_set_list is None + assert info.selection_set_graphql is None + assert isinstance(event.identity, AppSyncIdentityCognito) identity: AppSyncIdentityCognito = event.identity assert identity.claims is not None @@ -909,6 +919,7 @@ def test_appsync_resolver_event(): def test_get_identity_object_is_none(): assert get_identity_object(None) is None + event = AppSyncResolverEvent({}) assert event.identity is None @@ -948,11 +959,16 @@ def test_appsync_resolver_direct(): assert isinstance(event.identity, AppSyncIdentityCognito) info = event.info + assert info is not None assert isinstance(info, AppSyncResolverEventInfo) assert info.selection_set_list is not None assert info.selection_set_list == info["selectionSetList"] assert info.selection_set_graphql == info["selectionSetGraphQL"] assert info.parent_type_name == info["parentTypeName"] + assert info.field_name == info["fieldName"] + + assert event.type_name == info.parent_type_name + assert event.field_name == info.field_name def test_appsync_resolver_event_info(): @@ -973,8 +989,17 @@ def test_appsync_resolver_event_info(): assert isinstance(event.info, AppSyncResolverEventInfo) info: AppSyncResolverEventInfo = event.info assert info.field_name == info_dict["fieldName"] + assert event.field_name == info.field_name assert info.parent_type_name == info_dict["parentTypeName"] + assert event.type_name == info.parent_type_name assert info.variables == info_dict["variables"] assert info.variables["postId"] == "123" assert info.selection_set_list == info_dict["selectionSetList"] assert info.selection_set_graphql == info_dict["selectionSetGraphQL"] + + +def test_appsync_resolver_event_empty(): + event = AppSyncResolverEvent({}) + + assert event.info.field_name is None + assert event.info.parent_type_name is None From 1c65b142e7da131b37dd0b5e931329e27df46040 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 10 Mar 2021 17:39:33 -0800 Subject: [PATCH 17/33] docs(data-classes): Correct docs --- .../utilities/data_classes/appsync_resolver_event.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py index 490e5bae8ee..6962348902a 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py @@ -135,7 +135,7 @@ def selection_set_graphql(self) -> Optional[str]: class AppSyncResolverEvent(DictWrapper): """AppSync resolver event - NOTE: AppSync Resolver Events can come in various shapes this data class supports what + **NOTE:** AppSync Resolver Events can come in various shapes this data class supports what Amplify GraphQL Transformer produces Documentation: @@ -200,10 +200,7 @@ def prev_result(self) -> Optional[Dict[str, any]]: @property def info(self) -> AppSyncResolverEventInfo: - """The info section contains information about the GraphQL request. - - NOTE: This is not present for Amplify GraphQL Transformer functions - """ + """The info section contains information about the GraphQL request.""" return self._info @property From fa72167a3bfc3748fa5a15091ae2fce16bd1fd1a Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Thu, 11 Mar 2021 08:05:11 +0200 Subject: [PATCH 18/33] docs(data-classes): Clean up docs for review --- docs/utilities/data_classes.md | 51 ++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index a4e1b90cdf4..653b391b6e5 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -70,7 +70,7 @@ Event Source | Data_class ### API Gateway Proxy -Typically used for API Gateway REST API or HTTP API using v1 proxy event. +Typically, used for API Gateway REST API or HTTP API using v1 proxy event. === "lambda_app.py" @@ -105,12 +105,12 @@ Typically used for API Gateway REST API or HTTP API using v1 proxy event. ### AppSync Resolver -Used when building a Lambda GraphQL Resolvers with [Amplify GraphQL Transform Library](https://docs.amplify.aws/cli/graphql-transformer/function) -and can also be used for AppSync Direct Lambda Resolvers. +Used when building a Lambda GraphQL Resolvers with [Amplify GraphQL Transform Library](https://docs.amplify.aws/cli/graphql-transformer/function){target="_blank"} +and can also be used for [AppSync Direct Lambda Resolvers](https://aws.amazon.com/blogs/mobile/appsync-direct-lambda/){target="_blank"}. === "lambda_app.py" - ```python + ```python hl_lines="2-5 12 14 19 21 29-30" from aws_lambda_powertools.logging import Logger, correlation_paths from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import ( AppSyncResolverEvent, @@ -121,11 +121,10 @@ and can also be used for AppSync Direct Lambda Resolvers. def get_locations(name: str = None, size: int = 0, page: int = 0): """Your resolver logic here""" - pass @logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) def lambda_handler(event, context): - event = AppSyncResolverEvent(event) + event: AppSyncResolverEvent = AppSyncResolverEvent(event) # Case insensitive look up of request headers x_forwarded_for = event.get_header_value("x-forwarded-for") @@ -146,14 +145,44 @@ and can also be used for AppSync Direct Lambda Resolvers. raise ValueError(f"Unsupported field resolver: {event.field_name}") ``` -=== "CloudWatch Log" +=== "Example AppSync Event" + + ```json hl_lines="2-8 14 19 20" + { + "typeName": "Merchant", + "fieldName": "locations", + "arguments": { + "page": 2, + "size": 1, + "name": "value" + }, + "identity": { + "claims": { + "iat": 1615366261 + ... + }, + "username": "mike", + ... + }, + "request": { + "headers": { + "x-amzn-trace-id": "Root=1-60488877-0b0c4e6727ab2a1c545babd0", + "x-forwarded-for": "127.0.0.1" + ... + } + }, + ... + } + ``` - ```json +=== "Example CloudWatch Log" + + ```json hl_lines="5 6 16" { "level":"DEBUG", "location":"lambda_handler:22", "message":{ - "x-forwarded-for":"11.215.2.22, 64.44.173.11", + "x-forwarded-for":"127.0.0.1", "username":"mike" }, "timestamp":"2021-03-10 12:38:40,062", @@ -285,7 +314,6 @@ or plain text, depending on the original payload. ```python from aws_lambda_powertools.utilities.data_classes import KinesisStreamEvent - def lambda_handler(event, context): event: KinesisStreamEvent = KinesisStreamEvent(event) kinesis_record = next(event.records).kinesis @@ -304,6 +332,7 @@ or plain text, depending on the original payload. === "lambda_app.py" ```python + from urllib.parse import unquote_plus from aws_lambda_powertools.utilities.data_classes import S3Event def lambda_handler(event, context): @@ -312,7 +341,7 @@ or plain text, depending on the original payload. # Multiple records can be delivered in a single event for record in event.records: - object_key = record.s3.get_object.key + object_key = unquote_plus(record.s3.get_object.key) do_something_with(f'{bucket_name}/{object_key}') ``` From 6137896e5a972c8e5604712f15fcbdf5e4fc69f1 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Thu, 11 Mar 2021 19:35:12 -0800 Subject: [PATCH 19/33] feat(data-classes): Add AppSync resolver utilities Changes: * Add helper functions to generate GraphQL scalar types * AppSyncResolver decorator which works with AppSyncResolverEvent --- .../data_classes/appsync_resolver_utils.py | 136 ++++++++++++++++++ .../appsync/test_appsync_resolver_utils.py | 109 ++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 aws_lambda_powertools/utilities/data_classes/appsync_resolver_utils.py create mode 100644 tests/functional/appsync/test_appsync_resolver_utils.py diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_utils.py b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_utils.py new file mode 100644 index 00000000000..569d7530905 --- /dev/null +++ b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_utils.py @@ -0,0 +1,136 @@ +import datetime +import time +import uuid +from typing import Any, Dict + +from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent +from aws_lambda_powertools.utilities.typing import LambdaContext + + +def make_id(): + """A unique identifier for an object. This scalar is serialized like a String but isn't meant to be + human-readable.""" + return str(uuid.uuid4()) + + +def aws_date(): + """AWSDate - An extended ISO 8601 date string in the format YYYY-MM-DD""" + now = datetime.datetime.utcnow().date() + return now.strftime("%Y-%m-%d") + + +def aws_time(): + """AWSTime - An extended ISO 8601 time string in the format hh:mm:ss.sss""" + now = datetime.datetime.utcnow().time() + return now.strftime("%H:%M:%S") + + +def aws_datetime(): + """AWSDateTime - An extended ISO 8601 date and time string in the format YYYY-MM-DDThh:mm:ss.sssZ.""" + now = datetime.datetime.utcnow() + return now.strftime("%Y-%m-%dT%H:%M:%SZ") + + +def aws_timestamp(): + """AWSTimestamp - An integer value representing the number of seconds before or after 1970-01-01-T00:00Z.""" + return int(time.time()) + + +class AppSyncResolver: + """AppSync resolver decorator utility""" + + def __init__(self): + self._resolvers: dict = {} + + def resolver(self, type_name: str = "*", field_name: str = None, **kwargs): + """Registers the resolver for field_name + + Parameters + ---------- + type_name : str + Type name + field_name : str + Field name + kwargs : + Keyword arguments + """ + + def register_resolver(func): + self._resolvers[f"{type_name}.{field_name}"] = { + "func": func, + "config": kwargs, + } + return func + + return register_resolver + + def resolve(self, event: dict, context: LambdaContext) -> Any: + """Resolve field_name + + Parameters + ---------- + event : dict + Lambda event + context : LambdaContext + Lambda context + + Returns + ------- + Any + Returns the result of the resolver + + Raises + ------- + ValueError + If we could not find a field resolver + """ + event = AppSyncResolverEvent(event) + resolver, config = self._resolver(event.type_name, event.field_name) + kwargs = self._kwargs(event, context, config) + return resolver(**kwargs) + + def _resolver(self, type_name: str, field_name: str) -> tuple: + """Find resolver for field_name + + Parameters + ---------- + type_name : str + Type name + field_name : str + Field name + + Returns + ------- + tuple + callable function and configuration + """ + full_name = f"{type_name}.{field_name}" + resolver = self._resolvers.get(full_name, self._resolvers.get(f"*.{field_name}")) + if not resolver: + raise ValueError(f"No resolver found for '{full_name}'") + return resolver["func"], resolver["config"] + + @staticmethod + def _kwargs(event: AppSyncResolverEvent, context: LambdaContext, config: dict) -> Dict[str, Any]: + """Get the keyword arguments + + Parameters + ---------- + event : AppSyncResolverEvent + Lambda event + context : LambdaContext + Lambda context + config : dict + Configuration settings + + Returns + ------- + dict + Returns keyword arguments + """ + kwargs = {**event.arguments} + if config.get("include_event", False): + kwargs["event"] = event + if config.get("include_context", False): + kwargs["context"] = context + return kwargs diff --git a/tests/functional/appsync/test_appsync_resolver_utils.py b/tests/functional/appsync/test_appsync_resolver_utils.py new file mode 100644 index 00000000000..649f573f28e --- /dev/null +++ b/tests/functional/appsync/test_appsync_resolver_utils.py @@ -0,0 +1,109 @@ +import datetime +import json +import os + +import pytest + +from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent +from aws_lambda_powertools.utilities.data_classes.appsync_resolver_utils import ( + AppSyncResolver, + aws_date, + aws_datetime, + aws_time, + aws_timestamp, + make_id, +) + + +def load_event(file_name: str) -> dict: + full_file_name = os.path.dirname(os.path.realpath(__file__)) + "/../../events/" + file_name + with open(full_file_name) as fp: + return json.load(fp) + + +def test_direct_resolver(): + _event = load_event("appSyncDirectResolver.json") + + app = AppSyncResolver() + + @app.resolver(field_name="createSomething", include_context=True) + def create_something(context, id: str): # noqa AA03 VNE003 + assert context == {} + return id + + def handler(event, context): + return app.resolve(event, context) + + result = handler(_event, {}) + assert result == "my identifier" + + +def test_amplify_resolver(): + _event = load_event("appSyncResolverEvent.json") + + app = AppSyncResolver() + + @app.resolver(type_name="Merchant", field_name="locations", include_event=True) + def get_location(event: AppSyncResolverEvent, page: int, size: int, name: str): + assert event is not None + assert page == 2 + assert size == 1 + return name + + def handler(event, context): + return app.resolve(event, context) + + result = handler(_event, {}) + assert result == "value" + + +def test_resolver_no_params(): + app = AppSyncResolver() + + @app.resolver(type_name="Query", field_name="noParams") + def no_params(): + return "no_params has no params" + + event = {"typeName": "Query", "fieldName": "noParams", "arguments": {}} + result = app.resolve(event, None) + + assert result == "no_params has no params" + + +def test_resolver_value_error(): + app = AppSyncResolver() + + with pytest.raises(ValueError) as exp: + event = {"typeName": "type", "fieldName": "field", "arguments": {}} + app.resolve(event, None) + + assert exp.value.args[0] == "No resolver found for 'type.field'" + + +def test_make_id(): + uuid: str = make_id() + assert isinstance(uuid, str) + assert len(uuid) == 36 + + +def test_aws_date(): + date_str = aws_date() + assert isinstance(date_str, str) + assert datetime.datetime.strptime(date_str, "%Y-%m-%d") + + +def test_aws_time(): + time_str = aws_time() + assert isinstance(time_str, str) + assert datetime.datetime.strptime(time_str, "%H:%M:%S") + + +def test_aws_datetime(): + datetime_str = aws_datetime() + assert isinstance(datetime_str, str) + assert datetime.datetime.strptime(datetime_str, "%Y-%m-%dT%H:%M:%SZ") + + +def test_aws_timestamp(): + timestamp = aws_timestamp() + assert isinstance(timestamp, int) From 7c5b6e9f50816e832c178700b58c9124fb0477f6 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Thu, 11 Mar 2021 22:01:06 -0800 Subject: [PATCH 20/33] feat(data-classes): Include include_event and include_context --- .../data_classes/appsync_resolver_utils.py | 17 +++++- .../appsync/test_appsync_resolver_utils.py | 58 +++++++++++++++++-- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_utils.py b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_utils.py index 569d7530905..953c0957cd7 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_utils.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_utils.py @@ -42,7 +42,14 @@ class AppSyncResolver: def __init__(self): self._resolvers: dict = {} - def resolver(self, type_name: str = "*", field_name: str = None, **kwargs): + def resolver( + self, + type_name: str = "*", + field_name: str = None, + include_event: bool = False, + include_context: bool = False, + **kwargs, + ): """Registers the resolver for field_name Parameters @@ -51,11 +58,17 @@ def resolver(self, type_name: str = "*", field_name: str = None, **kwargs): Type name field_name : str Field name + include_event: bool + Whether to include the lambda event + include_context: bool + Whether to include the lambda context kwargs : - Keyword arguments + Extra options via kwargs """ def register_resolver(func): + kwargs["include_event"] = include_event + kwargs["include_context"] = include_context self._resolvers[f"{type_name}.{field_name}"] = { "func": func, "config": kwargs, diff --git a/tests/functional/appsync/test_appsync_resolver_utils.py b/tests/functional/appsync/test_appsync_resolver_utils.py index 649f573f28e..72c22a133d9 100644 --- a/tests/functional/appsync/test_appsync_resolver_utils.py +++ b/tests/functional/appsync/test_appsync_resolver_utils.py @@ -13,6 +13,7 @@ aws_timestamp, make_id, ) +from aws_lambda_powertools.utilities.typing import LambdaContext def load_event(file_name: str) -> dict: @@ -22,7 +23,8 @@ def load_event(file_name: str) -> dict: def test_direct_resolver(): - _event = load_event("appSyncDirectResolver.json") + # Check whether we can handle an example appsync direct resolver + mock_event = load_event("appSyncDirectResolver.json") app = AppSyncResolver() @@ -34,12 +36,13 @@ def create_something(context, id: str): # noqa AA03 VNE003 def handler(event, context): return app.resolve(event, context) - result = handler(_event, {}) + result = handler(mock_event, {}) assert result == "my identifier" def test_amplify_resolver(): - _event = load_event("appSyncResolverEvent.json") + # Check whether we can handle an example appsync resolver + mock_event = load_event("appSyncResolverEvent.json") app = AppSyncResolver() @@ -53,11 +56,12 @@ def get_location(event: AppSyncResolverEvent, page: int, size: int, name: str): def handler(event, context): return app.resolve(event, context) - result = handler(_event, {}) + result = handler(mock_event, {}) assert result == "value" def test_resolver_no_params(): + # GIVEN app = AppSyncResolver() @app.resolver(type_name="Query", field_name="noParams") @@ -65,18 +69,60 @@ def no_params(): return "no_params has no params" event = {"typeName": "Query", "fieldName": "noParams", "arguments": {}} - result = app.resolve(event, None) + # WHEN + result = app.resolve(event, LambdaContext()) + + # THEN assert result == "no_params has no params" +def test_resolver_include_event(): + # GIVEN + app = AppSyncResolver() + + mock_event = {"typeName": "Query", "fieldName": "field", "arguments": {}} + + @app.resolver(field_name="field", include_event=True) + def get_value(event: AppSyncResolverEvent): + return event + + # WHEN + result = app.resolve(mock_event, LambdaContext()) + + # THEN + assert result._data == mock_event + assert isinstance(result, AppSyncResolverEvent) + + +def test_resolver_include_context(): + # GIVEN + app = AppSyncResolver() + + mock_event = {"typeName": "Query", "fieldName": "field", "arguments": {}} + + @app.resolver(field_name="field", include_context=True) + def get_value(context: LambdaContext): + return context + + # WHEN + mock_context = LambdaContext() + result = app.resolve(mock_event, mock_context) + + # THEN + assert result == mock_context + + def test_resolver_value_error(): + # GIVEN no defined field resolver app = AppSyncResolver() + # WHEN with pytest.raises(ValueError) as exp: event = {"typeName": "type", "fieldName": "field", "arguments": {}} - app.resolve(event, None) + app.resolve(event, LambdaContext()) + # THEN assert exp.value.args[0] == "No resolver found for 'type.field'" From 87fb84812d905d29b11019b3f23553479c0aa52a Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Thu, 11 Mar 2021 22:18:03 -0800 Subject: [PATCH 21/33] tests(data-clasess): Verify async and yield works --- .../appsync/test_appsync_resolver_utils.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/functional/appsync/test_appsync_resolver_utils.py b/tests/functional/appsync/test_appsync_resolver_utils.py index 72c22a133d9..a9e2eb2a525 100644 --- a/tests/functional/appsync/test_appsync_resolver_utils.py +++ b/tests/functional/appsync/test_appsync_resolver_utils.py @@ -1,3 +1,4 @@ +import asyncio import datetime import json import os @@ -126,6 +127,43 @@ def test_resolver_value_error(): assert exp.value.args[0] == "No resolver found for 'type.field'" +def test_resolver_yield(): + # GIVEN + app = AppSyncResolver() + + mock_event = {"typeName": "Customer", "fieldName": "field", "arguments": {}} + + @app.resolver(field_name="field") + def func_yield(): + yield "value" + + # WHEN + mock_context = LambdaContext() + result = app.resolve(mock_event, mock_context) + + # THEN + assert next(result) == "value" + + +def test_resolver_async(): + # GIVEN + app = AppSyncResolver() + + mock_event = {"typeName": "Customer", "fieldName": "field", "arguments": {}} + + @app.resolver(field_name="field") + async def get_async(): + await asyncio.sleep(0.0001) + return "value" + + # WHEN + mock_context = LambdaContext() + result = app.resolve(mock_event, mock_context) + + # THEN + assert asyncio.run(result) == "value" + + def test_make_id(): uuid: str = make_id() assert isinstance(uuid, str) From fc03fdd41682e1229b6dbd3ce9baa8ad9b0c298c Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Thu, 11 Mar 2021 22:27:03 -0800 Subject: [PATCH 22/33] test(data-classes): only run async test on new python versions --- tests/functional/appsync/test_appsync_resolver_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/functional/appsync/test_appsync_resolver_utils.py b/tests/functional/appsync/test_appsync_resolver_utils.py index a9e2eb2a525..2e1be2caf10 100644 --- a/tests/functional/appsync/test_appsync_resolver_utils.py +++ b/tests/functional/appsync/test_appsync_resolver_utils.py @@ -2,6 +2,7 @@ import datetime import json import os +import sys import pytest @@ -145,6 +146,7 @@ def func_yield(): assert next(result) == "value" +@pytest.mark.skipif(sys.version_info < (3, 8), reason="only for python versions that support asyncio.run") def test_resolver_async(): # GIVEN app = AppSyncResolver() From 3875d2f0303327f8669ad6200245d11a30443665 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Thu, 11 Mar 2021 22:45:39 -0800 Subject: [PATCH 23/33] test(data-classes): Verify we can support multiple mappings --- .../appsync/test_appsync_resolver_utils.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/functional/appsync/test_appsync_resolver_utils.py b/tests/functional/appsync/test_appsync_resolver_utils.py index 2e1be2caf10..a94904fca34 100644 --- a/tests/functional/appsync/test_appsync_resolver_utils.py +++ b/tests/functional/appsync/test_appsync_resolver_utils.py @@ -146,6 +146,30 @@ def func_yield(): assert next(result) == "value" +def test_resolver_multiple_mappings(): + # GIVEN + app = AppSyncResolver() + + @app.resolver(field_name="listLocations") + @app.resolver(field_name="locations") + def get_locations(name: str, description: str = ""): + return name + description + + # WHEN + mock_event1 = {"typeName": "Query", "fieldName": "listLocations", "arguments": {"name": "value"}} + mock_event2 = { + "typeName": "Merchant", + "fieldName": "locations", + "arguments": {"name": "value2", "description": "description"}, + } + result1 = app.resolve(mock_event1, LambdaContext()) + result2 = app.resolve(mock_event2, LambdaContext()) + + # THEN + assert result1 == "value" + assert result2 == "value2description" + + @pytest.mark.skipif(sys.version_info < (3, 8), reason="only for python versions that support asyncio.run") def test_resolver_async(): # GIVEN From d17eadaeb59efb454fc8d41919d97a660340d21e Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Fri, 12 Mar 2021 07:26:22 -0800 Subject: [PATCH 24/33] chore: Update docs/utilities/data_classes.md Co-authored-by: Heitor Lessa --- docs/utilities/data_classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index 653b391b6e5..7bd64f03852 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -108,7 +108,7 @@ Typically, used for API Gateway REST API or HTTP API using v1 proxy event. Used when building a Lambda GraphQL Resolvers with [Amplify GraphQL Transform Library](https://docs.amplify.aws/cli/graphql-transformer/function){target="_blank"} and can also be used for [AppSync Direct Lambda Resolvers](https://aws.amazon.com/blogs/mobile/appsync-direct-lambda/){target="_blank"}. -=== "lambda_app.py" +=== "app.py" ```python hl_lines="2-5 12 14 19 21 29-30" from aws_lambda_powertools.logging import Logger, correlation_paths From 092f51bc1dbb6864dae1d302f02b5a476559fc4b Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Fri, 12 Mar 2021 07:26:38 -0800 Subject: [PATCH 25/33] chore: Update docs/utilities/data_classes.md Co-authored-by: Heitor Lessa --- docs/utilities/data_classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index 7bd64f03852..9d1e273fb74 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -70,7 +70,7 @@ Event Source | Data_class ### API Gateway Proxy -Typically, used for API Gateway REST API or HTTP API using v1 proxy event. +It is used for either API Gateway REST API or HTTP API using v1 proxy event. === "lambda_app.py" From 8d8fe4ab0eb7f59fd937df5906005ae6f6ae37f4 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Fri, 12 Mar 2021 07:27:04 -0800 Subject: [PATCH 26/33] chore: Update aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py Co-authored-by: Heitor Lessa --- .../utilities/data_classes/appsync_resolver_event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py index 6962348902a..68d5a59112d 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py @@ -92,7 +92,7 @@ def default_auth_strategy(self) -> str: @property def groups(self) -> List[str]: - """Array of OIDC groups""" + """List of OIDC groups""" return self["groups"] @property From 1bfdbfbb8629e204e698df1cedae306ad83b28ed Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Fri, 12 Mar 2021 07:29:33 -0800 Subject: [PATCH 27/33] chore: Correct docs --- .../utilities/data_classes/appsync_resolver_event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py index 68d5a59112d..85fc5d525b4 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py @@ -122,7 +122,7 @@ def variables(self) -> Dict[str, str]: @property def selection_set_list(self) -> List[str]: """A list representation of the fields in the GraphQL selection set. Fields that are aliased will - only be referenced by the alias name, not the field name. The following example shows this in detail.""" + only be referenced by the alias name, not the field name.""" return self.get("selectionSetList") @property From 2c148adb8419f067b36e8469d2af503ae6de0531 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Fri, 12 Mar 2021 07:31:31 -0800 Subject: [PATCH 28/33] chore: Correct docs --- .../utilities/data_classes/appsync_resolver_event.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py index 85fc5d525b4..a6f9e6c58a4 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py @@ -135,8 +135,8 @@ def selection_set_graphql(self) -> Optional[str]: class AppSyncResolverEvent(DictWrapper): """AppSync resolver event - **NOTE:** AppSync Resolver Events can come in various shapes this data class supports what - Amplify GraphQL Transformer produces + **NOTE:** AppSync Resolver Events can come in various shapes this data class + supports both Amplify GraphQL directive @function and Direct Lambda Resolver Documentation: ------------- From 9315f8186bf4722b174017f5c23c0f1a5bc9caf2 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Fri, 12 Mar 2021 07:41:42 -0800 Subject: [PATCH 29/33] refactor(data-classes): AppSync location --- aws_lambda_powertools/utilities/data_classes/__init__.py | 2 +- .../utilities/data_classes/appsync/__init__.py | 0 .../data_classes/{ => appsync}/appsync_resolver_event.py | 0 .../{appsync_resolver_utils.py => appsync/resolver_utils.py} | 0 tests/functional/appsync/test_appsync_resolver_utils.py | 2 +- tests/functional/test_lambda_trigger_events.py | 2 +- 6 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 aws_lambda_powertools/utilities/data_classes/appsync/__init__.py rename aws_lambda_powertools/utilities/data_classes/{ => appsync}/appsync_resolver_event.py (100%) rename aws_lambda_powertools/utilities/data_classes/{appsync_resolver_utils.py => appsync/resolver_utils.py} (100%) diff --git a/aws_lambda_powertools/utilities/data_classes/__init__.py b/aws_lambda_powertools/utilities/data_classes/__init__.py index e4dfb6dbb18..f40b6ade4ad 100644 --- a/aws_lambda_powertools/utilities/data_classes/__init__.py +++ b/aws_lambda_powertools/utilities/data_classes/__init__.py @@ -1,6 +1,6 @@ from .alb_event import ALBEvent from .api_gateway_proxy_event import APIGatewayProxyEvent, APIGatewayProxyEventV2 -from .appsync_resolver_event import AppSyncResolverEvent +from .appsync.appsync_resolver_event import AppSyncResolverEvent from .cloud_watch_logs_event import CloudWatchLogsEvent from .connect_contact_flow_event import ConnectContactFlowEvent from .dynamo_db_stream_event import DynamoDBStreamEvent diff --git a/aws_lambda_powertools/utilities/data_classes/appsync/__init__.py b/aws_lambda_powertools/utilities/data_classes/appsync/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py b/aws_lambda_powertools/utilities/data_classes/appsync/appsync_resolver_event.py similarity index 100% rename from aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py rename to aws_lambda_powertools/utilities/data_classes/appsync/appsync_resolver_event.py diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_utils.py b/aws_lambda_powertools/utilities/data_classes/appsync/resolver_utils.py similarity index 100% rename from aws_lambda_powertools/utilities/data_classes/appsync_resolver_utils.py rename to aws_lambda_powertools/utilities/data_classes/appsync/resolver_utils.py diff --git a/tests/functional/appsync/test_appsync_resolver_utils.py b/tests/functional/appsync/test_appsync_resolver_utils.py index a94904fca34..b3ec85c7205 100644 --- a/tests/functional/appsync/test_appsync_resolver_utils.py +++ b/tests/functional/appsync/test_appsync_resolver_utils.py @@ -7,7 +7,7 @@ import pytest from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent -from aws_lambda_powertools.utilities.data_classes.appsync_resolver_utils import ( +from aws_lambda_powertools.utilities.data_classes.appsync.resolver_utils import ( AppSyncResolver, aws_date, aws_datetime, diff --git a/tests/functional/test_lambda_trigger_events.py b/tests/functional/test_lambda_trigger_events.py index 40dd374960c..061bd56b6e9 100644 --- a/tests/functional/test_lambda_trigger_events.py +++ b/tests/functional/test_lambda_trigger_events.py @@ -17,7 +17,7 @@ SNSEvent, SQSEvent, ) -from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import ( +from aws_lambda_powertools.utilities.data_classes.appsync.appsync_resolver_event import ( AppSyncIdentityCognito, AppSyncIdentityIAM, AppSyncResolverEventInfo, From 8ba44958e50032966452af55743f2480d5e34cd0 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Fri, 12 Mar 2021 08:07:24 -0800 Subject: [PATCH 30/33] docs(data-classes): Added sample usage --- .../data_classes/appsync/resolver_utils.py | 33 ++++++++++++++++++- examples/__init__.py | 0 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 examples/__init__.py diff --git a/aws_lambda_powertools/utilities/data_classes/appsync/resolver_utils.py b/aws_lambda_powertools/utilities/data_classes/appsync/resolver_utils.py index 953c0957cd7..40042ec784b 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync/resolver_utils.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync/resolver_utils.py @@ -37,7 +37,38 @@ def aws_timestamp(): class AppSyncResolver: - """AppSync resolver decorator utility""" + """ + AppSync resolver decorator utility + + Example + ------- + **Sample usage** + + from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent + from aws_lambda_powertools.utilities.data_classes.appsync.resolver_utils import AppSyncResolver + + app = AppSyncResolver() + + + @app.resolver(type_name="Query", field_name="listLocations", include_event=True) + def list_locations(event: AppSyncResolverEvent, page: int = 0, size: int = 10): + # Your logic to fetch locations + + + @app.resolver(type_name="Merchant", field_name="extraInfo", include_event=True) + def get_extra_info(event: AppSyncResolverEvent): + # Can use `event.source` to filter within the parent context + + + @app.resolver(field_name="commonField") + def common_field(): + # Would match all fieldNames matching `commonField` + + + def handle(event, context): + app.resolve(event, context) + + """ def __init__(self): self._resolvers: dict = {} diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 9562008b5fb43731e67d44bba910c85f882434ef Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Fri, 12 Mar 2021 08:14:24 -0800 Subject: [PATCH 31/33] chore: fix docs rendering --- .../utilities/data_classes/appsync/resolver_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync/resolver_utils.py b/aws_lambda_powertools/utilities/data_classes/appsync/resolver_utils.py index 40042ec784b..81d944323a3 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync/resolver_utils.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync/resolver_utils.py @@ -57,12 +57,12 @@ def list_locations(event: AppSyncResolverEvent, page: int = 0, size: int = 10): @app.resolver(type_name="Merchant", field_name="extraInfo", include_event=True) def get_extra_info(event: AppSyncResolverEvent): - # Can use `event.source` to filter within the parent context + # Can use "event.source" to filter within the parent context @app.resolver(field_name="commonField") def common_field(): - # Would match all fieldNames matching `commonField` + # Would match all fieldNames matching "commonField" def handle(event, context): From d1cde30cdc6a22653ef4971da92176920deaf8e5 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Fri, 12 Mar 2021 08:30:48 -0800 Subject: [PATCH 32/33] refactor: Remove docstrings and relocate data class --- .../utilities/data_classes/__init__.py | 3 +- .../data_classes/appsync/resolver_utils.py | 104 ------------------ .../{appsync => }/appsync_resolver_event.py | 0 examples/__init__.py | 0 .../functional/test_lambda_trigger_events.py | 2 +- 5 files changed, 3 insertions(+), 106 deletions(-) rename aws_lambda_powertools/utilities/data_classes/{appsync => }/appsync_resolver_event.py (100%) delete mode 100644 examples/__init__.py diff --git a/aws_lambda_powertools/utilities/data_classes/__init__.py b/aws_lambda_powertools/utilities/data_classes/__init__.py index f40b6ade4ad..28179bfd291 100644 --- a/aws_lambda_powertools/utilities/data_classes/__init__.py +++ b/aws_lambda_powertools/utilities/data_classes/__init__.py @@ -1,6 +1,7 @@ +from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import AppSyncResolverEvent + from .alb_event import ALBEvent from .api_gateway_proxy_event import APIGatewayProxyEvent, APIGatewayProxyEventV2 -from .appsync.appsync_resolver_event import AppSyncResolverEvent from .cloud_watch_logs_event import CloudWatchLogsEvent from .connect_contact_flow_event import ConnectContactFlowEvent from .dynamo_db_stream_event import DynamoDBStreamEvent diff --git a/aws_lambda_powertools/utilities/data_classes/appsync/resolver_utils.py b/aws_lambda_powertools/utilities/data_classes/appsync/resolver_utils.py index 81d944323a3..848a24619d4 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync/resolver_utils.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync/resolver_utils.py @@ -8,68 +8,29 @@ def make_id(): - """A unique identifier for an object. This scalar is serialized like a String but isn't meant to be - human-readable.""" return str(uuid.uuid4()) def aws_date(): - """AWSDate - An extended ISO 8601 date string in the format YYYY-MM-DD""" now = datetime.datetime.utcnow().date() return now.strftime("%Y-%m-%d") def aws_time(): - """AWSTime - An extended ISO 8601 time string in the format hh:mm:ss.sss""" now = datetime.datetime.utcnow().time() return now.strftime("%H:%M:%S") def aws_datetime(): - """AWSDateTime - An extended ISO 8601 date and time string in the format YYYY-MM-DDThh:mm:ss.sssZ.""" now = datetime.datetime.utcnow() return now.strftime("%Y-%m-%dT%H:%M:%SZ") def aws_timestamp(): - """AWSTimestamp - An integer value representing the number of seconds before or after 1970-01-01-T00:00Z.""" return int(time.time()) class AppSyncResolver: - """ - AppSync resolver decorator utility - - Example - ------- - **Sample usage** - - from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent - from aws_lambda_powertools.utilities.data_classes.appsync.resolver_utils import AppSyncResolver - - app = AppSyncResolver() - - - @app.resolver(type_name="Query", field_name="listLocations", include_event=True) - def list_locations(event: AppSyncResolverEvent, page: int = 0, size: int = 10): - # Your logic to fetch locations - - - @app.resolver(type_name="Merchant", field_name="extraInfo", include_event=True) - def get_extra_info(event: AppSyncResolverEvent): - # Can use "event.source" to filter within the parent context - - - @app.resolver(field_name="commonField") - def common_field(): - # Would match all fieldNames matching "commonField" - - - def handle(event, context): - app.resolve(event, context) - - """ - def __init__(self): self._resolvers: dict = {} @@ -81,22 +42,6 @@ def resolver( include_context: bool = False, **kwargs, ): - """Registers the resolver for field_name - - Parameters - ---------- - type_name : str - Type name - field_name : str - Field name - include_event: bool - Whether to include the lambda event - include_context: bool - Whether to include the lambda context - kwargs : - Extra options via kwargs - """ - def register_resolver(func): kwargs["include_event"] = include_event kwargs["include_context"] = include_context @@ -109,45 +54,12 @@ def register_resolver(func): return register_resolver def resolve(self, event: dict, context: LambdaContext) -> Any: - """Resolve field_name - - Parameters - ---------- - event : dict - Lambda event - context : LambdaContext - Lambda context - - Returns - ------- - Any - Returns the result of the resolver - - Raises - ------- - ValueError - If we could not find a field resolver - """ event = AppSyncResolverEvent(event) resolver, config = self._resolver(event.type_name, event.field_name) kwargs = self._kwargs(event, context, config) return resolver(**kwargs) def _resolver(self, type_name: str, field_name: str) -> tuple: - """Find resolver for field_name - - Parameters - ---------- - type_name : str - Type name - field_name : str - Field name - - Returns - ------- - tuple - callable function and configuration - """ full_name = f"{type_name}.{field_name}" resolver = self._resolvers.get(full_name, self._resolvers.get(f"*.{field_name}")) if not resolver: @@ -156,22 +68,6 @@ def _resolver(self, type_name: str, field_name: str) -> tuple: @staticmethod def _kwargs(event: AppSyncResolverEvent, context: LambdaContext, config: dict) -> Dict[str, Any]: - """Get the keyword arguments - - Parameters - ---------- - event : AppSyncResolverEvent - Lambda event - context : LambdaContext - Lambda context - config : dict - Configuration settings - - Returns - ------- - dict - Returns keyword arguments - """ kwargs = {**event.arguments} if config.get("include_event", False): kwargs["event"] = event diff --git a/aws_lambda_powertools/utilities/data_classes/appsync/appsync_resolver_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py similarity index 100% rename from aws_lambda_powertools/utilities/data_classes/appsync/appsync_resolver_event.py rename to aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py diff --git a/examples/__init__.py b/examples/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/functional/test_lambda_trigger_events.py b/tests/functional/test_lambda_trigger_events.py index 061bd56b6e9..40dd374960c 100644 --- a/tests/functional/test_lambda_trigger_events.py +++ b/tests/functional/test_lambda_trigger_events.py @@ -17,7 +17,7 @@ SNSEvent, SQSEvent, ) -from aws_lambda_powertools.utilities.data_classes.appsync.appsync_resolver_event import ( +from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import ( AppSyncIdentityCognito, AppSyncIdentityIAM, AppSyncResolverEventInfo, From 445f626333a3d56ca66c6b19ceb96bce10b91910 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Fri, 12 Mar 2021 08:48:59 -0800 Subject: [PATCH 33/33] docs(data-classes): Expanded on the scope and named app.py consistently --- docs/utilities/data_classes.md | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index 9d1e273fb74..92d4c8b0f70 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -3,7 +3,8 @@ title: Event Source Data Classes description: Utility --- -The event source data classes utility provides classes describing the schema of common Lambda events triggers. +Event Source Data Classes utility provides classes self-describing Lambda event sources, including API decorators when +applicable. ## Key Features @@ -72,7 +73,7 @@ Event Source | Data_class It is used for either API Gateway REST API or HTTP API using v1 proxy event. -=== "lambda_app.py" +=== "app.py" ```python from aws_lambda_powertools.utilities.data_classes import APIGatewayProxyEvent @@ -89,7 +90,7 @@ It is used for either API Gateway REST API or HTTP API using v1 proxy event. ### API Gateway Proxy v2 -=== "lambda_app.py" +=== "app.py" ```python from aws_lambda_powertools.utilities.data_classes import APIGatewayProxyEventV2 @@ -202,7 +203,7 @@ and can also be used for [AppSync Direct Lambda Resolvers](https://aws.amazon.co CloudWatch Logs events by default are compressed and base64 encoded. You can use the helper function provided to decode, decompress and parse json data from the event. -=== "lambda_app.py" +=== "app.py" ```python from aws_lambda_powertools.utilities.data_classes import CloudWatchLogsEvent @@ -235,7 +236,7 @@ Define Auth Challenge | `data_classes.cognito_user_pool_event.DefineAuthChalleng Create Auth Challenge | `data_classes.cognito_user_pool_event.CreateAuthChallengeTriggerEvent` Verify Auth Challenge | `data_classes.cognito_user_pool_event.VerifyAuthChallengeResponseTriggerEvent` -=== "lambda_app.py" +=== "app.py" ```python from aws_lambda_powertools.utilities.data_classes.cognito_user_pool_event import PostConfirmationTriggerEvent @@ -249,7 +250,7 @@ Verify Auth Challenge | `data_classes.cognito_user_pool_event.VerifyAuthChalleng ### Connect Contact Flow -=== "lambda_app.py" +=== "app.py" ```python from aws_lambda_powertools.utilities.data_classes.connect_contact_flow_event import ( @@ -273,7 +274,7 @@ The DynamoDB data class utility provides the base class for `DynamoDBStreamEvent attributes values (`AttributeValue`), as well as enums for stream view type (`StreamViewType`) and event type (`DynamoDBRecordEventName`). -=== "lambda_app.py" +=== "app.py" ```python from aws_lambda_powertools.utilities.data_classes.dynamo_db_stream_event import ( @@ -293,7 +294,7 @@ attributes values (`AttributeValue`), as well as enums for stream view type (`St ### EventBridge -=== "lambda_app.py" +=== "app.py" ```python from aws_lambda_powertools.utilities.data_classes import EventBridgeEvent @@ -309,7 +310,7 @@ attributes values (`AttributeValue`), as well as enums for stream view type (`St Kinesis events by default contain base64 encoded data. You can use the helper function to access the data either as json or plain text, depending on the original payload. -=== "lambda_app.py" +=== "app.py" ```python from aws_lambda_powertools.utilities.data_classes import KinesisStreamEvent @@ -329,7 +330,7 @@ or plain text, depending on the original payload. ### S3 -=== "lambda_app.py" +=== "app.py" ```python from urllib.parse import unquote_plus @@ -348,7 +349,7 @@ or plain text, depending on the original payload. ### SES -=== "lambda_app.py" +=== "app.py" ```python from aws_lambda_powertools.utilities.data_classes import SESEvent @@ -366,7 +367,7 @@ or plain text, depending on the original payload. ### SNS -=== "lambda_app.py" +=== "app.py" ```python from aws_lambda_powertools.utilities.data_classes import SNSEvent @@ -384,7 +385,7 @@ or plain text, depending on the original payload. ### SQS -=== "lambda_app.py" +=== "app.py" ```python from aws_lambda_powertools.utilities.data_classes import SQSEvent