From bc250f3adebb239acdcf8a3d6e302e3c42194d1b Mon Sep 17 00:00:00 2001 From: "Shane R. Spencer" <305301+whardier@users.noreply.github.com> Date: Thu, 15 Jul 2021 10:42:18 -0800 Subject: [PATCH 1/7] Allow for AppSyncResolverEvent subclass to be used for self.current_event --- aws_lambda_powertools/event_handler/appsync.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/aws_lambda_powertools/event_handler/appsync.py b/aws_lambda_powertools/event_handler/appsync.py index 021afaa6654..c3bb98120cd 100644 --- a/aws_lambda_powertools/event_handler/appsync.py +++ b/aws_lambda_powertools/event_handler/appsync.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Callable +from typing import Any, Callable, Type from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent from aws_lambda_powertools.utilities.typing import LambdaContext @@ -37,7 +37,7 @@ def common_field() -> str: # Would match all fieldNames matching 'commonField' return str(uuid.uuid4()) """ - + current_event: AppSyncResolverEvent lambda_context: LambdaContext @@ -62,7 +62,7 @@ def register_resolver(func): return register_resolver - def resolve(self, event: dict, context: LambdaContext) -> Any: + def resolve(self, event: dict, context: LambdaContext, current_event_data_class: Type[AppSyncResolverEvent] = AppSyncResolverEvent) -> Any: """Resolve field_name Parameters @@ -71,6 +71,8 @@ def resolve(self, event: dict, context: LambdaContext) -> Any: Lambda event context : LambdaContext Lambda context + current_event_data_class: + Decode instance of event to class or subclass of AppSyncResolverEvent Returns ------- @@ -82,7 +84,7 @@ def resolve(self, event: dict, context: LambdaContext) -> Any: ValueError If we could not find a field resolver """ - self.current_event = AppSyncResolverEvent(event) + self.current_event = current_event_data_class(event) self.lambda_context = context resolver = self._get_resolver(self.current_event.type_name, self.current_event.field_name) return resolver(**self.current_event.arguments) From 42da9d3dec7fb561a82df1ccdc0e8929d1b63476 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 19 Jul 2021 09:22:48 +0200 Subject: [PATCH 2/7] refactor(appsync): use model param for consistency --- aws_lambda_powertools/event_handler/appsync.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/aws_lambda_powertools/event_handler/appsync.py b/aws_lambda_powertools/event_handler/appsync.py index c3bb98120cd..dff900b008d 100644 --- a/aws_lambda_powertools/event_handler/appsync.py +++ b/aws_lambda_powertools/event_handler/appsync.py @@ -37,7 +37,7 @@ def common_field() -> str: # Would match all fieldNames matching 'commonField' return str(uuid.uuid4()) """ - + current_event: AppSyncResolverEvent lambda_context: LambdaContext @@ -62,7 +62,9 @@ def register_resolver(func): return register_resolver - def resolve(self, event: dict, context: LambdaContext, current_event_data_class: Type[AppSyncResolverEvent] = AppSyncResolverEvent) -> Any: + def resolve( + self, event: dict, context: LambdaContext, model: Type[AppSyncResolverEvent] = AppSyncResolverEvent + ) -> Any: """Resolve field_name Parameters @@ -71,8 +73,8 @@ def resolve(self, event: dict, context: LambdaContext, current_event_data_class: Lambda event context : LambdaContext Lambda context - current_event_data_class: - Decode instance of event to class or subclass of AppSyncResolverEvent + model: + Your data model to decode AppSync event, by default AppSyncResolverEvent Returns ------- @@ -84,7 +86,7 @@ def resolve(self, event: dict, context: LambdaContext, current_event_data_class: ValueError If we could not find a field resolver """ - self.current_event = current_event_data_class(event) + self.current_event = model(event) self.lambda_context = context resolver = self._get_resolver(self.current_event.type_name, self.current_event.field_name) return resolver(**self.current_event.arguments) From 09d6bff325ccc2ae21b37d06500e3f4a01a3e892 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 19 Jul 2021 13:01:57 +0200 Subject: [PATCH 3/7] chore(mypy): disable pretty for IDE support --- mypy.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index 42185a692ad..35be794fec5 100644 --- a/mypy.ini +++ b/mypy.ini @@ -4,7 +4,7 @@ warn_unused_configs=True no_implicit_optional=True warn_redundant_casts=True warn_unused_ignores=True -pretty = True +;pretty = True ; breaks MyPy plugin in PyCharm show_column_numbers = True show_error_codes = True show_error_context = True From cab6d6002aec0496d2ce4904e92f40787ef03141 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 19 Jul 2021 13:02:56 +0200 Subject: [PATCH 4/7] chore: add functional tests --- .../functional/event_handler/test_appsync.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/functional/event_handler/test_appsync.py b/tests/functional/event_handler/test_appsync.py index e260fef89ab..9da8c028564 100644 --- a/tests/functional/event_handler/test_appsync.py +++ b/tests/functional/event_handler/test_appsync.py @@ -138,3 +138,26 @@ async def get_async(): # THEN assert asyncio.run(result) == "value" + + +def test_resolve_custom_model(): + # Check whether we can handle an example appsync direct resolver + mock_event = load_event("appSyncDirectResolver.json") + + class MyCustomModel(AppSyncResolverEvent): + @property + def country_viewer(self): + return self.request_headers.get("cloudfront-viewer-country") + + app = AppSyncResolver() + + @app.resolver(field_name="createSomething") + def create_something(id: str): # noqa AA03 VNE003 + return id + + # Call the implicit handler + result = app(event=mock_event, context=LambdaContext(), model=MyCustomModel) + + assert result == "my identifier" + + assert app.current_event.country_viewer == "US" From 0612b211d66b5621ac3b0e5079177e02571abd7b Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 19 Jul 2021 13:03:39 +0200 Subject: [PATCH 5/7] fix(mypy): types and __call__ --- aws_lambda_powertools/event_handler/appsync.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/aws_lambda_powertools/event_handler/appsync.py b/aws_lambda_powertools/event_handler/appsync.py index dff900b008d..62759be7e25 100644 --- a/aws_lambda_powertools/event_handler/appsync.py +++ b/aws_lambda_powertools/event_handler/appsync.py @@ -1,11 +1,13 @@ import logging -from typing import Any, Callable, Type +from typing import Any, Callable, Optional, Type, TypeVar from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent from aws_lambda_powertools.utilities.typing import LambdaContext logger = logging.getLogger(__name__) +AppSyncResolverEventT = TypeVar("AppSyncResolverEventT", bound=AppSyncResolverEvent) + class AppSyncResolver: """ @@ -38,13 +40,13 @@ def common_field() -> str: return str(uuid.uuid4()) """ - current_event: AppSyncResolverEvent + current_event: AppSyncResolverEventT # type: ignore[valid-type] lambda_context: LambdaContext def __init__(self): self._resolvers: dict = {} - def resolver(self, type_name: str = "*", field_name: str = None): + def resolver(self, type_name: str = "*", field_name: Optional[str] = None): """Registers the resolver for field_name Parameters @@ -112,6 +114,8 @@ def _get_resolver(self, type_name: str, field_name: str) -> Callable: raise ValueError(f"No resolver found for '{full_name}'") return resolver["func"] - def __call__(self, event, context) -> Any: + def __call__( + self, event: dict, context: LambdaContext, model: Type[AppSyncResolverEvent] = AppSyncResolverEvent + ) -> Any: """Implicit lambda handler which internally calls `resolve`""" - return self.resolve(event, context) + return self.resolve(event, context, model) From 453175754eb79acc52f9da1c1b2ca8657267472f Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 19 Jul 2021 14:40:23 +0200 Subject: [PATCH 6/7] docs(appsync_handler): add custom models and examples --- .../event_handler/appsync.py | 48 ++++++++ docs/core/event_handler/appsync.md | 112 ++++++++++++++++++ 2 files changed, 160 insertions(+) diff --git a/aws_lambda_powertools/event_handler/appsync.py b/aws_lambda_powertools/event_handler/appsync.py index 62759be7e25..83837b5adff 100644 --- a/aws_lambda_powertools/event_handler/appsync.py +++ b/aws_lambda_powertools/event_handler/appsync.py @@ -78,6 +78,54 @@ def resolve( model: Your data model to decode AppSync event, by default AppSyncResolverEvent + Example + ------- + + ```python + from aws_lambda_powertools.event_handler import AppSyncResolver + from aws_lambda_powertools.utilities.typing import LambdaContext + + @app.resolver(field_name="createSomething") + def create_something(id: str): # noqa AA03 VNE003 + return id + + def handler(event, context: LambdaContext): + return app.resolve(event, context) + ``` + + **Bringing custom models** + + ```python + from aws_lambda_powertools import Logger, Tracer + + from aws_lambda_powertools.logging import correlation_paths + from aws_lambda_powertools.event_handler import AppSyncResolver + + tracer = Tracer(service="sample_resolver") + logger = Logger(service="sample_resolver") + app = AppSyncResolver() + + + class MyCustomModel(AppSyncResolverEvent): + @property + def country_viewer(self) -> str: + return self.request_headers.get("cloudfront-viewer-country") + + + @app.resolver(field_name="listLocations") + @app.resolver(field_name="locations") + def get_locations(name: str, description: str = ""): + if app.current_event.country_viewer == "US": + ... + return name + description + + + @logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context, model=MyCustomModel) + ``` + Returns ------- Any diff --git a/docs/core/event_handler/appsync.md b/docs/core/event_handler/appsync.md index 67ad1999285..cd9b6452515 100644 --- a/docs/core/event_handler/appsync.md +++ b/docs/core/event_handler/appsync.md @@ -598,6 +598,118 @@ Use the following code for `merchantInfo` and `searchMerchant` functions respect } ``` +### Custom models + +You can subclass `AppSyncResolverEvent` to bring your own set of methods to handle incoming events, by using `model` param in the `resolve` method. + + +=== "custom_model.py" + + ```python hl_lines="11-14 19 26" + from aws_lambda_powertools import Logger, Tracer + + from aws_lambda_powertools.logging import correlation_paths + from aws_lambda_powertools.event_handler import AppSyncResolver + + tracer = Tracer(service="sample_resolver") + logger = Logger(service="sample_resolver") + app = AppSyncResolver() + + + class MyCustomModel(AppSyncResolverEvent): + @property + def country_viewer(self) -> str: + return self.request_headers.get("cloudfront-viewer-country") + + @app.resolver(field_name="listLocations") + @app.resolver(field_name="locations") + def get_locations(name: str, description: str = ""): + if app.current_event.country_viewer == "US": + ... + return name + description + + @logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context, model=MyCustomModel) + ``` + +=== "schema.graphql" + + ```typescript hl_lines="6 20" + schema { + query: Query + } + + type Query { + listLocations: [Location] + } + + type Location { + id: ID! + name: String! + description: String + address: String + } + + type Merchant { + id: String! + name: String! + description: String + locations: [Location] + } + ``` + +=== "listLocations_event.json" + + ```json + { + "arguments": {}, + "identity": null, + "source": null, + "request": { + "headers": { + "x-forwarded-for": "1.2.3.4, 5.6.7.8", + "accept-encoding": "gzip, deflate, br", + "cloudfront-viewer-country": "NL", + "cloudfront-is-tablet-viewer": "false", + "referer": "https://eu-west-1.console.aws.amazon.com/appsync/home?region=eu-west-1", + "via": "2.0 9fce949f3749407c8e6a75087e168b47.cloudfront.net (CloudFront)", + "cloudfront-forwarded-proto": "https", + "origin": "https://eu-west-1.console.aws.amazon.com", + "x-api-key": "da1-c33ullkbkze3jg5hf5ddgcs4fq", + "content-type": "application/json", + "x-amzn-trace-id": "Root=1-606eb2f2-1babc433453a332c43fb4494", + "x-amz-cf-id": "SJw16ZOPuMZMINx5Xcxa9pB84oMPSGCzNOfrbJLvd80sPa0waCXzYQ==", + "content-length": "114", + "x-amz-user-agent": "AWS-Console-AppSync/", + "x-forwarded-proto": "https", + "host": "ldcvmkdnd5az3lm3gnf5ixvcyy.appsync-api.eu-west-1.amazonaws.com", + "accept-language": "en-US,en;q=0.5", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:78.0) Gecko/20100101 Firefox/78.0", + "cloudfront-is-desktop-viewer": "true", + "cloudfront-is-mobile-viewer": "false", + "accept": "*/*", + "x-forwarded-port": "443", + "cloudfront-is-smarttv-viewer": "false" + } + }, + "prev": null, + "info": { + "parentTypeName": "Query", + "selectionSetList": [ + "id", + "name", + "description" + ], + "selectionSetGraphQL": "{\n id\n name\n description\n}", + "fieldName": "listLocations", + "variables": {} + }, + "stash": {} + } + ``` + ## Testing your code You can test your resolvers by passing a mocked or actual AppSync Lambda event that you're expecting. From 3ca178d78525303a9748ed6f50246afab41051cc Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 19 Jul 2021 15:54:56 +0200 Subject: [PATCH 7/7] chore: rename parameter to data_model --- aws_lambda_powertools/event_handler/appsync.py | 14 +++++++------- docs/core/event_handler/appsync.md | 6 +++--- tests/functional/event_handler/test_appsync.py | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/aws_lambda_powertools/event_handler/appsync.py b/aws_lambda_powertools/event_handler/appsync.py index 83837b5adff..69b90c4cbb6 100644 --- a/aws_lambda_powertools/event_handler/appsync.py +++ b/aws_lambda_powertools/event_handler/appsync.py @@ -65,7 +65,7 @@ def register_resolver(func): return register_resolver def resolve( - self, event: dict, context: LambdaContext, model: Type[AppSyncResolverEvent] = AppSyncResolverEvent + self, event: dict, context: LambdaContext, data_model: Type[AppSyncResolverEvent] = AppSyncResolverEvent ) -> Any: """Resolve field_name @@ -75,8 +75,8 @@ def resolve( Lambda event context : LambdaContext Lambda context - model: - Your data model to decode AppSync event, by default AppSyncResolverEvent + data_model: + Your data data_model to decode AppSync event, by default AppSyncResolverEvent Example ------- @@ -123,7 +123,7 @@ def get_locations(name: str, description: str = ""): @logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) @tracer.capture_lambda_handler def lambda_handler(event, context): - return app.resolve(event, context, model=MyCustomModel) + return app.resolve(event, context, data_model=MyCustomModel) ``` Returns @@ -136,7 +136,7 @@ def lambda_handler(event, context): ValueError If we could not find a field resolver """ - self.current_event = model(event) + self.current_event = data_model(event) self.lambda_context = context resolver = self._get_resolver(self.current_event.type_name, self.current_event.field_name) return resolver(**self.current_event.arguments) @@ -163,7 +163,7 @@ def _get_resolver(self, type_name: str, field_name: str) -> Callable: return resolver["func"] def __call__( - self, event: dict, context: LambdaContext, model: Type[AppSyncResolverEvent] = AppSyncResolverEvent + self, event: dict, context: LambdaContext, data_model: Type[AppSyncResolverEvent] = AppSyncResolverEvent ) -> Any: """Implicit lambda handler which internally calls `resolve`""" - return self.resolve(event, context, model) + return self.resolve(event, context, data_model) diff --git a/docs/core/event_handler/appsync.md b/docs/core/event_handler/appsync.md index cd9b6452515..a47b8a4c641 100644 --- a/docs/core/event_handler/appsync.md +++ b/docs/core/event_handler/appsync.md @@ -598,9 +598,9 @@ Use the following code for `merchantInfo` and `searchMerchant` functions respect } ``` -### Custom models +### Custom data models -You can subclass `AppSyncResolverEvent` to bring your own set of methods to handle incoming events, by using `model` param in the `resolve` method. +You can subclass `AppSyncResolverEvent` to bring your own set of methods to handle incoming events, by using `data_model` param in the `resolve` method. === "custom_model.py" @@ -631,7 +631,7 @@ You can subclass `AppSyncResolverEvent` to bring your own set of methods to hand @logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) @tracer.capture_lambda_handler def lambda_handler(event, context): - return app.resolve(event, context, model=MyCustomModel) + return app.resolve(event, context, data_model=MyCustomModel) ``` === "schema.graphql" diff --git a/tests/functional/event_handler/test_appsync.py b/tests/functional/event_handler/test_appsync.py index 9da8c028564..26a3ffdcb1f 100644 --- a/tests/functional/event_handler/test_appsync.py +++ b/tests/functional/event_handler/test_appsync.py @@ -140,7 +140,7 @@ async def get_async(): assert asyncio.run(result) == "value" -def test_resolve_custom_model(): +def test_resolve_custom_data_model(): # Check whether we can handle an example appsync direct resolver mock_event = load_event("appSyncDirectResolver.json") @@ -156,7 +156,7 @@ def create_something(id: str): # noqa AA03 VNE003 return id # Call the implicit handler - result = app(event=mock_event, context=LambdaContext(), model=MyCustomModel) + result = app(event=mock_event, context=LambdaContext(), data_model=MyCustomModel) assert result == "my identifier"