From ce80a906440854eec93e0c06540d3b3d8b0f4629 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Thu, 25 Mar 2021 08:44:03 -0700 Subject: [PATCH 01/15] feat(event-handler): Add AppSync handler decorator --- .../utilities/data_classes/__init__.py | 3 +- .../data_classes/appsync/resolver_utils.py | 50 ------ .../utilities/event_handler/__init__.py | 3 + .../utilities/event_handler/appsync.py | 146 ++++++++++++++++++ docs/utilities/data_classes.md | 3 +- tests/functional/event_handler/__init__.py | 0 .../test_appsync.py} | 54 +------ ...trigger_events.py => test_data_classes.py} | 52 +++++++ 8 files changed, 204 insertions(+), 107 deletions(-) delete mode 100644 aws_lambda_powertools/utilities/data_classes/appsync/resolver_utils.py create mode 100644 aws_lambda_powertools/utilities/event_handler/__init__.py create mode 100644 aws_lambda_powertools/utilities/event_handler/appsync.py create mode 100644 tests/functional/event_handler/__init__.py rename tests/functional/{appsync/test_appsync_resolver_utils.py => event_handler/test_appsync.py} (76%) rename tests/functional/{test_lambda_trigger_events.py => test_data_classes.py} (97%) diff --git a/aws_lambda_powertools/utilities/data_classes/__init__.py b/aws_lambda_powertools/utilities/data_classes/__init__.py index 28179bfd291..e4dfb6dbb18 100644 --- a/aws_lambda_powertools/utilities/data_classes/__init__.py +++ b/aws_lambda_powertools/utilities/data_classes/__init__.py @@ -1,7 +1,6 @@ -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_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 deleted file mode 100644 index 9329b8effe8..00000000000 --- a/aws_lambda_powertools/utilities/data_classes/appsync/resolver_utils.py +++ /dev/null @@ -1,50 +0,0 @@ -from typing import Any, Dict - -from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent -from aws_lambda_powertools.utilities.typing import LambdaContext - - -class AppSyncResolver: - def __init__(self): - self._resolvers: dict = {} - - def resolver( - self, - type_name: str = "*", - field_name: str = None, - include_event: bool = False, - include_context: bool = False, - **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, - } - return func - - return register_resolver - - def resolve(self, _event: dict, context: LambdaContext) -> Any: - 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: - 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]: - 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/aws_lambda_powertools/utilities/event_handler/__init__.py b/aws_lambda_powertools/utilities/event_handler/__init__.py new file mode 100644 index 00000000000..023a035e253 --- /dev/null +++ b/aws_lambda_powertools/utilities/event_handler/__init__.py @@ -0,0 +1,3 @@ +from .appsync import AppSyncResolver + +__all__ = ["AppSyncResolver"] diff --git a/aws_lambda_powertools/utilities/event_handler/appsync.py b/aws_lambda_powertools/utilities/event_handler/appsync.py new file mode 100644 index 00000000000..40cd3d67233 --- /dev/null +++ b/aws_lambda_powertools/utilities/event_handler/appsync.py @@ -0,0 +1,146 @@ +from typing import Any, Dict + +from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent +from aws_lambda_powertools.utilities.typing import LambdaContext + + +class AppSyncResolver: + """ + AppSync resolver decorator + + Example + ------- + + **Sample usage** + + from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent + from aws_lambda_powertools.utilities.event_handler 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 = {} + + 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 + ---------- + 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 + 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/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index fa85c719243..dc56ed8ec41 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -3,8 +3,7 @@ title: Event Source Data Classes description: Utility --- -Event Source Data Classes utility provides classes self-describing Lambda event sources, including API decorators when -applicable. +Event Source Data Classes utility provides classes self-describing Lambda event sources. ## Key Features diff --git a/tests/functional/event_handler/__init__.py b/tests/functional/event_handler/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/appsync/test_appsync_resolver_utils.py b/tests/functional/event_handler/test_appsync.py similarity index 76% rename from tests/functional/appsync/test_appsync_resolver_utils.py rename to tests/functional/event_handler/test_appsync.py index a1388a1fb5c..d03d0af95e5 100644 --- a/tests/functional/appsync/test_appsync_resolver_utils.py +++ b/tests/functional/event_handler/test_appsync.py @@ -1,5 +1,4 @@ import asyncio -import datetime import json import os import sys @@ -7,15 +6,7 @@ import pytest from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent -from aws_lambda_powertools.utilities.data_classes.appsync.resolver_utils import AppSyncResolver -from aws_lambda_powertools.utilities.data_classes.appsync.scalar_types_utils import ( - _formatted_time, - aws_date, - aws_datetime, - aws_time, - aws_timestamp, - make_id, -) +from aws_lambda_powertools.utilities.event_handler import AppSyncResolver from aws_lambda_powertools.utilities.typing import LambdaContext @@ -189,46 +180,3 @@ async def get_async(): # THEN assert asyncio.run(result) == "value" - - -def test_make_id(): - uuid: str = make_id() - assert isinstance(uuid, str) - assert len(uuid) == 36 - - -def test_aws_date_utc(): - date_str = aws_date() - assert isinstance(date_str, str) - assert datetime.datetime.strptime(date_str, "%Y-%m-%dZ") - - -def test_aws_time_utc(): - time_str = aws_time() - assert isinstance(time_str, str) - assert datetime.datetime.strptime(time_str, "%H:%M:%SZ") - - -def test_aws_datetime_utc(): - 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) - - -def test_format_time_positive(): - now = datetime.datetime(2022, 1, 22) - datetime_str = _formatted_time(now, "%Y-%m-%d", 8) - assert isinstance(datetime_str, str) - assert datetime_str == "2022-01-22+08:00:00" - - -def test_format_time_negative(): - now = datetime.datetime(2022, 1, 22, 14, 22, 33) - datetime_str = _formatted_time(now, "%H:%M:%S", -12) - assert isinstance(datetime_str, str) - assert datetime_str == "02:22:33-12:00:00" diff --git a/tests/functional/test_lambda_trigger_events.py b/tests/functional/test_data_classes.py similarity index 97% rename from tests/functional/test_lambda_trigger_events.py rename to tests/functional/test_data_classes.py index 62bcb50762c..bc14bf2eeb7 100644 --- a/tests/functional/test_lambda_trigger_events.py +++ b/tests/functional/test_data_classes.py @@ -1,4 +1,5 @@ import base64 +import datetime import json import os from secrets import compare_digest @@ -17,6 +18,14 @@ SNSEvent, SQSEvent, ) +from aws_lambda_powertools.utilities.data_classes.appsync.scalar_types_utils import ( + _formatted_time, + aws_date, + aws_datetime, + aws_time, + aws_timestamp, + make_id, +) from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import ( AppSyncIdentityCognito, AppSyncIdentityIAM, @@ -1059,3 +1068,46 @@ def test_s3_object_event_temp_credentials(): assert session_attributes is not None assert session_attributes.mfa_authenticated == session_context["attributes"]["mfaAuthenticated"] assert session_attributes.creation_date == session_context["attributes"]["creationDate"] + + +def test_make_id(): + uuid: str = make_id() + assert isinstance(uuid, str) + assert len(uuid) == 36 + + +def test_aws_date_utc(): + date_str = aws_date() + assert isinstance(date_str, str) + assert datetime.datetime.strptime(date_str, "%Y-%m-%dZ") + + +def test_aws_time_utc(): + time_str = aws_time() + assert isinstance(time_str, str) + assert datetime.datetime.strptime(time_str, "%H:%M:%SZ") + + +def test_aws_datetime_utc(): + 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) + + +def test_format_time_positive(): + now = datetime.datetime(2022, 1, 22) + datetime_str = _formatted_time(now, "%Y-%m-%d", 8) + assert isinstance(datetime_str, str) + assert datetime_str == "2022-01-22+08:00:00" + + +def test_format_time_negative(): + now = datetime.datetime(2022, 1, 22, 14, 22, 33) + datetime_str = _formatted_time(now, "%H:%M:%S", -12) + assert isinstance(datetime_str, str) + assert datetime_str == "02:22:33-12:00:00" From c7709bf5c3e0d0e4646572cacfb9148b03cc4950 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Thu, 25 Mar 2021 10:46:17 -0700 Subject: [PATCH 02/15] test(event-handler): Use pathlib --- tests/functional/event_handler/test_appsync.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/functional/event_handler/test_appsync.py b/tests/functional/event_handler/test_appsync.py index d03d0af95e5..d133d4628f8 100644 --- a/tests/functional/event_handler/test_appsync.py +++ b/tests/functional/event_handler/test_appsync.py @@ -1,7 +1,7 @@ import asyncio import json -import os import sys +from pathlib import Path import pytest @@ -11,9 +11,8 @@ 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) + path = Path(str(Path(__file__).parent.parent.parent) + "/events/" + file_name) + return json.loads(path.read_text()) def test_direct_resolver(): From f145c04f5b9d91720c4963f45b95bcebe6ec8fe9 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Thu, 25 Mar 2021 11:34:57 -0700 Subject: [PATCH 03/15] fix(tracer): Correct type hint for MyPy Closes #360 --- aws_lambda_powertools/tracing/tracer.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/aws_lambda_powertools/tracing/tracer.py b/aws_lambda_powertools/tracing/tracer.py index 698ac6fb8b6..5e2e545e356 100644 --- a/aws_lambda_powertools/tracing/tracer.py +++ b/aws_lambda_powertools/tracing/tracer.py @@ -244,7 +244,7 @@ def patch(self, modules: Tuple[str] = None): def capture_lambda_handler( self, - lambda_handler: Callable[[Dict, Any, Optional[Dict]], Any] = None, + lambda_handler: Union[Callable[[Dict, Any], Any], Callable[[Dict, Any, Optional[Dict]], Any]] = None, capture_response: Optional[bool] = None, capture_error: Optional[bool] = None, ): @@ -517,7 +517,7 @@ async def async_tasks(): def _decorate_async_function( self, - method: Callable = None, + method: Callable, capture_response: Optional[Union[bool, str]] = None, capture_error: Optional[Union[bool, str]] = None, method_name: str = None, @@ -544,7 +544,7 @@ async def decorate(*args, **kwargs): def _decorate_generator_function( self, - method: Callable = None, + method: Callable, capture_response: Optional[Union[bool, str]] = None, capture_error: Optional[Union[bool, str]] = None, method_name: str = None, @@ -571,7 +571,7 @@ def decorate(*args, **kwargs): def _decorate_generator_function_with_context_manager( self, - method: Callable = None, + method: Callable, capture_response: Optional[Union[bool, str]] = None, capture_error: Optional[Union[bool, str]] = None, method_name: str = None, @@ -599,7 +599,7 @@ def decorate(*args, **kwargs): def _decorate_sync_function( self, - method: Callable = None, + method: Callable, capture_response: Optional[Union[bool, str]] = None, capture_error: Optional[Union[bool, str]] = None, method_name: str = None, @@ -654,20 +654,20 @@ def _add_response_as_metadata( def _add_full_exception_as_metadata( self, - method_name: str = None, - error: Exception = None, - subsegment: BaseSegment = None, + method_name: str, + error: Exception, + subsegment: BaseSegment, capture_error: Optional[bool] = None, ): """Add full exception object as metadata for given subsegment Parameters ---------- - method_name : str, optional + method_name : str method name to add as metadata key, by default None - error : Exception, optional + error : Exception error to add as subsegment metadata, by default None - subsegment : BaseSegment, optional + subsegment : BaseSegment existing subsegment to add metadata on, by default None capture_error : bool, optional Do not include error as metadata, by default True @@ -717,7 +717,7 @@ def __build_config( service: str = None, disabled: bool = None, auto_patch: bool = None, - patch_modules: List = None, + patch_modules: Union[List, Tuple] = None, provider: BaseProvider = None, ): """ Populates Tracer config for new and existing initializations """ From 2b8264217c9d5e77394190b33d1463e910e694c5 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Thu, 25 Mar 2021 19:43:10 -0700 Subject: [PATCH 04/15] docs(event-handler): Initial markdown docs --- .../utilities/data_classes/__init__.py | 4 + .../utilities/event_handler/__init__.py | 4 + docs/index.md | 1 + docs/utilities/event_handler.md | 152 ++++++++++++++++++ mkdocs.yml | 1 + 5 files changed, 162 insertions(+) create mode 100644 docs/utilities/event_handler.md diff --git a/aws_lambda_powertools/utilities/data_classes/__init__.py b/aws_lambda_powertools/utilities/data_classes/__init__.py index e4dfb6dbb18..58464ebcf99 100644 --- a/aws_lambda_powertools/utilities/data_classes/__init__.py +++ b/aws_lambda_powertools/utilities/data_classes/__init__.py @@ -1,3 +1,7 @@ +""" +Event Source Data Classes utility provides classes self-describing Lambda event sources. +""" + from .alb_event import ALBEvent from .api_gateway_proxy_event import APIGatewayProxyEvent, APIGatewayProxyEventV2 from .appsync_resolver_event import AppSyncResolverEvent diff --git a/aws_lambda_powertools/utilities/event_handler/__init__.py b/aws_lambda_powertools/utilities/event_handler/__init__.py index 023a035e253..0475982e377 100644 --- a/aws_lambda_powertools/utilities/event_handler/__init__.py +++ b/aws_lambda_powertools/utilities/event_handler/__init__.py @@ -1,3 +1,7 @@ +""" +Event handler decorators for common Lambda events +""" + from .appsync import AppSyncResolver __all__ = ["AppSyncResolver"] diff --git a/docs/index.md b/docs/index.md index 1f347b017e1..ab295dc85f3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -152,6 +152,7 @@ aws serverlessrepo list-application-versions \ | [Event source data classes](./utilities/data_classes) | Data classes describing the schema of common Lambda event triggers | [Parser](./utilities/parser) | Data parsing and deep validation using Pydantic | [Idempotency](./utilities/idempotency) | Idempotent Lambda handler +| [Event handler](./utilities/event_handler) | Event handler decorators for common Lambda events ## Environment variables diff --git a/docs/utilities/event_handler.md b/docs/utilities/event_handler.md new file mode 100644 index 00000000000..1013828e3a9 --- /dev/null +++ b/docs/utilities/event_handler.md @@ -0,0 +1,152 @@ +--- +title: Event Handler +description: Utility +--- + +Event handler decorators for common Lambda events + + +### AppSync Resolver Decorator + +> New in 1.14.0 + + +=== "app.py" + + ```python hl_lines="1-3 6 8-9 13-14 18-19 23 25" + from aws_lambda_powertools.logging import Logger, correlation_paths + from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent + from aws_lambda_powertools.utilities.event_handler import AppSyncResolver + + logger = Logger() + 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' + ... + + @logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) + def handle(event, context): + app.resolve(event, context) + ``` +=== "schema.graphql" + + ```graphql hl_lines="7-10 17-18 22-23" + @model + type Merchant + { + id: String! + name: String! + description: String + # Resolves to `get_extra_info` + extraInfo: ExtraInfo @function(name: "merchantInformation-${env}") + # Resolves to `common_field` + commonField: String @function(name: "merchantInformation-${env}") + } + + type Location { + id: ID! + name: String! + address: Address + # Resolves to `common_field` + commonField: String @function(name: "merchantInformation-${env}") + } + + type Query { + # List of locations resolves to `list_locations` + listLocations(page: Int, size: Int): [Location] @function(name: "merchantInformation-${env}") + } + ``` +=== "Example list_locations event" + + ```json hl_lines="2-7" + { + "typeName": "Query", + "fieldName": "listLocations", + "arguments": { + "page": 2, + "size": 1 + }, + "identity": { + "claims": { + "iat": 1615366261 + ... + }, + "username": "mike", + ... + }, + "request": { + "headers": { + "x-amzn-trace-id": "Root=1-60488877-0b0c4e6727ab2a1c545babd0", + "x-forwarded-for": "127.0.0.1" + ... + } + }, + ... + } + ``` + +=== "Example get_extra_info event" + + ```json hl_lines="2 3" + { + "typeName": "Merchant", + "fieldName": "extraInfo", + "arguments": { + }, + "identity": { + "claims": { + "iat": 1615366261 + ... + }, + "username": "mike", + ... + }, + "request": { + "headers": { + "x-amzn-trace-id": "Root=1-60488877-0b0c4e6727ab2a1c545babd0", + "x-forwarded-for": "127.0.0.1" + ... + } + }, + ... + } + ``` + +=== "Example common_field event" + + ```json hl_lines="2 3" + { + "typeName": "Merchant", + "fieldName": "commonField", + "arguments": { + }, + "identity": { + "claims": { + "iat": 1615366261 + ... + }, + "username": "mike", + ... + }, + "request": { + "headers": { + "x-amzn-trace-id": "Root=1-60488877-0b0c4e6727ab2a1c545babd0", + "x-forwarded-for": "127.0.0.1" + ... + } + }, + ... + } + ``` diff --git a/mkdocs.yml b/mkdocs.yml index d8d37830369..0c88d7cbcda 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,6 +18,7 @@ nav: - utilities/data_classes.md - utilities/parser.md - utilities/idempotency.md + - utilities/event_handler.md theme: name: material From 9f4e3e8701a4df3c65c6228a6458de6017d8027a Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Thu, 25 Mar 2021 21:12:27 -0700 Subject: [PATCH 05/15] feat(event-handler): Add an implicit appsync handler --- aws_lambda_powertools/utilities/event_handler/appsync.py | 8 ++++---- tests/functional/event_handler/test_appsync.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/aws_lambda_powertools/utilities/event_handler/appsync.py b/aws_lambda_powertools/utilities/event_handler/appsync.py index 40cd3d67233..aa5e81ba0be 100644 --- a/aws_lambda_powertools/utilities/event_handler/appsync.py +++ b/aws_lambda_powertools/utilities/event_handler/appsync.py @@ -32,10 +32,6 @@ def get_extra_info(event: AppSyncResolverEvent): def common_field(): # Would match all fieldNames matching 'commonField' ... - - def handle(event, context): - app.resolve(event, context) - """ def __init__(self): @@ -144,3 +140,7 @@ def _kwargs(event: AppSyncResolverEvent, context: LambdaContext, config: dict) - if config.get("include_context", False): kwargs["context"] = context return kwargs + + def __call__(self, event, context) -> Any: + """Implicit AppSync handler""" + return self.resolve(event, context) diff --git a/tests/functional/event_handler/test_appsync.py b/tests/functional/event_handler/test_appsync.py index d133d4628f8..158a7f56778 100644 --- a/tests/functional/event_handler/test_appsync.py +++ b/tests/functional/event_handler/test_appsync.py @@ -26,10 +26,9 @@ def create_something(context, id: str): # noqa AA03 VNE003 assert context == {} return id - def handler(event, context): - return app.resolve(event, context) + # Call the implicit handler + result = app(mock_event, {}) - result = handler(mock_event, {}) assert result == "my identifier" @@ -47,6 +46,7 @@ def get_location(event: AppSyncResolverEvent, page: int, size: int, name: str): return name def handler(event, context): + # Call the explicit resolve function return app.resolve(event, context) result = handler(mock_event, {}) From 3f664d74f704256ba2a91e12ab274a715236beab Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Thu, 25 Mar 2021 21:30:29 -0700 Subject: [PATCH 06/15] docs(event-handler): Add implicit handler example --- docs/utilities/event_handler.md | 96 ++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 30 deletions(-) diff --git a/docs/utilities/event_handler.md b/docs/utilities/event_handler.md index 1013828e3a9..74fb266cac6 100644 --- a/docs/utilities/event_handler.md +++ b/docs/utilities/event_handler.md @@ -10,8 +10,40 @@ Event handler decorators for common Lambda events > New in 1.14.0 +=== "schema.graphql" + + ```graphql hl_lines="7-10 17-18 22-25" + @model + type Merchant + { + id: String! + name: String! + description: String + # Resolves to `get_extra_info` + extraInfo: ExtraInfo @function(name: "merchantInformation-${env}") + # Resolves to `common_field` + commonField: String @function(name: "merchantInformation-${env}") + } + + type Location { + id: ID! + name: String! + address: Address + # Resolves to `common_field` + commonField: String @function(name: "merchantInformation-${env}") + } -=== "app.py" + type Query { + # List of locations resolves to `list_locations` + listLocations(page: Int, size: Int): [Location] @function(name: "merchantInformation-${env}") + # List of locations resolves to `list_locations` + findMerchant(search: str): [Merchant] @function(name: "searchMerchant-${env}") + } + ``` + +Example lambda implementation + +=== "merchantInformation app.py" ```python hl_lines="1-3 6 8-9 13-14 18-19 23 25" from aws_lambda_powertools.logging import Logger, correlation_paths @@ -28,7 +60,7 @@ Event handler decorators for common Lambda events @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["id"]` to filter within the Merchant context ... @app.resolver(field_name="commonField") @@ -40,35 +72,22 @@ Event handler decorators for common Lambda events def handle(event, context): app.resolve(event, context) ``` -=== "schema.graphql" +=== "searchMerchant app.py" - ```graphql hl_lines="7-10 17-18 22-23" - @model - type Merchant - { - id: String! - name: String! - description: String - # Resolves to `get_extra_info` - extraInfo: ExtraInfo @function(name: "merchantInformation-${env}") - # Resolves to `common_field` - commonField: String @function(name: "merchantInformation-${env}") - } + ```python hl_lines="1 3 5-6" + from aws_lambda_powertools.utilities.event_handler import AppSyncResolver - type Location { - id: ID! - name: String! - address: Address - # Resolves to `common_field` - commonField: String @function(name: "merchantInformation-${env}") - } + app = AppSyncResolver() - type Query { - # List of locations resolves to `list_locations` - listLocations(page: Int, size: Int): [Location] @function(name: "merchantInformation-${env}") - } + @app.resolver(type_name="Query", field_name="findMerchant") + def find_merchant(search: str): + # Your special search function + ... ``` -=== "Example list_locations event" + +Example AppSync resolver events + +=== "Query.listLocations event" ```json hl_lines="2-7" { @@ -97,9 +116,9 @@ Event handler decorators for common Lambda events } ``` -=== "Example get_extra_info event" +=== "Merchant.extraInfo event" - ```json hl_lines="2 3" + ```json hl_lines="2 3 14-17" { "typeName": "Merchant", "fieldName": "extraInfo", @@ -113,6 +132,10 @@ Event handler decorators for common Lambda events "username": "mike", ... }, + "source": { + "id": "12345", + "name: "Pizza Parlor" + }, "request": { "headers": { "x-amzn-trace-id": "Root=1-60488877-0b0c4e6727ab2a1c545babd0", @@ -124,7 +147,7 @@ Event handler decorators for common Lambda events } ``` -=== "Example common_field event" +=== "commonField event" ```json hl_lines="2 3" { @@ -150,3 +173,16 @@ Event handler decorators for common Lambda events ... } ``` + +=== "Query.findMerchant event" + + ```json hl_lines="2-6" + { + "typeName": "Query", + "fieldName": "findMerchant", + "arguments": { + "search": "Brewers Coffee" + }, + ... + } + ``` From 4f433651fd10d6be429fd890d55c58bb7d1a5179 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Thu, 25 Mar 2021 22:54:36 -0700 Subject: [PATCH 07/15] docs(event-handler): Add full amplify example --- docs/utilities/event_handler.md | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/docs/utilities/event_handler.md b/docs/utilities/event_handler.md index 74fb266cac6..b7a087ced40 100644 --- a/docs/utilities/event_handler.md +++ b/docs/utilities/event_handler.md @@ -10,6 +10,10 @@ Event handler decorators for common Lambda events > New in 1.14.0 +#### Amplify Example + +Create a new GraphQL api via `amplify add api` and add the following to the new `schema.graphql` + === "schema.graphql" ```graphql hl_lines="7-10 17-18 22-25" @@ -41,9 +45,10 @@ Event handler decorators for common Lambda events } ``` -Example lambda implementation +Create two sample Python functions via `amplify add function` and run `pipenv install aws-lambda-powertools`. Add +the following example lambda implementation -=== "merchantInformation app.py" +=== "merchantInformation/src/app.py" ```python hl_lines="1-3 6 8-9 13-14 18-19 23 25" from aws_lambda_powertools.logging import Logger, correlation_paths @@ -72,7 +77,7 @@ Example lambda implementation def handle(event, context): app.resolve(event, context) ``` -=== "searchMerchant app.py" +=== "searchMerchant/src/app.py" ```python hl_lines="1 3 5-6" from aws_lambda_powertools.utilities.event_handler import AppSyncResolver @@ -118,7 +123,7 @@ Example AppSync resolver events === "Merchant.extraInfo event" - ```json hl_lines="2 3 14-17" + ```json hl_lines="2-5 14-17" { "typeName": "Merchant", "fieldName": "extraInfo", @@ -147,7 +152,7 @@ Example AppSync resolver events } ``` -=== "commonField event" +=== "*.commonField event" ```json hl_lines="2 3" { @@ -183,6 +188,21 @@ Example AppSync resolver events "arguments": { "search": "Brewers Coffee" }, + "identity": { + "claims": { + "iat": 1615366261 + ... + }, + "username": "mike", + ... + }, + "request": { + "headers": { + "x-amzn-trace-id": "Root=1-60488877-0b0c4e6727ab2a1c545babd0", + "x-forwarded-for": "127.0.0.1" + ... + } + }, ... } ``` From 36b088e35c5a6cc77588fc87b9c5421d734c7881 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Fri, 26 Mar 2021 20:57:57 -0700 Subject: [PATCH 08/15] chore(event-handler): Add more docs --- .../utilities/event_handler/appsync.py | 2 +- docs/utilities/event_handler.md | 40 +++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/aws_lambda_powertools/utilities/event_handler/appsync.py b/aws_lambda_powertools/utilities/event_handler/appsync.py index aa5e81ba0be..ce2ee8b4aa7 100644 --- a/aws_lambda_powertools/utilities/event_handler/appsync.py +++ b/aws_lambda_powertools/utilities/event_handler/appsync.py @@ -142,5 +142,5 @@ def _kwargs(event: AppSyncResolverEvent, context: LambdaContext, config: dict) - return kwargs def __call__(self, event, context) -> Any: - """Implicit AppSync handler""" + """Implicit lambda handler which internally calls `resolve`""" return self.resolve(event, context) diff --git a/docs/utilities/event_handler.md b/docs/utilities/event_handler.md index b7a087ced40..f5dd4f60ecd 100644 --- a/docs/utilities/event_handler.md +++ b/docs/utilities/event_handler.md @@ -10,13 +10,27 @@ Event handler decorators for common Lambda events > New in 1.14.0 -#### Amplify Example +AppSync resolver decorator is a concise way to create lambda functions to handle AppSync resolvers for multiple +`typeName` and `fieldName` declarations. This decorator builds on top of the +[AppSync Resolver ](/utilities/data_classes#appsync-resolver) data class and therefore works with [Amplify GraphQL Transform Library](https://docs.amplify.aws/cli/graphql-transformer/function){target="_blank"} (`@function`), +and [AppSync Direct Lambda Resolvers](https://aws.amazon.com/blogs/mobile/appsync-direct-lambda/){target="_blank"} + +#### Key Features + +* Works with any of the existing Powertools utilities by allow you to create your own `lambda_handler` function +* Supports an implicit handler where in `app = AppSyncResolver()` can be invoked directly as `app(event, context)` +* `resolver` decorator has flexible or strict matching against `fieldName` +* Arguments are automatically passed into your function +* `include_event` and `include_context` options can be used to pass in the original `AppSyncResolver` or `LambdaContext` + objects + +#### Amplify GraphQL Example Create a new GraphQL api via `amplify add api` and add the following to the new `schema.graphql` === "schema.graphql" - ```graphql hl_lines="7-10 17-18 22-25" + ```typescript hl_lines="7-10 17-18 22-25" @model type Merchant { @@ -24,9 +38,9 @@ Create a new GraphQL api via `amplify add api` and add the following to the new name: String! description: String # Resolves to `get_extra_info` - extraInfo: ExtraInfo @function(name: "merchantInformation-${env}") + extraInfo: ExtraInfo @function(name: "merchantInfo-${env}") # Resolves to `common_field` - commonField: String @function(name: "merchantInformation-${env}") + commonField: String @function(name: "merchantInfo-${env}") } type Location { @@ -34,27 +48,28 @@ Create a new GraphQL api via `amplify add api` and add the following to the new name: String! address: Address # Resolves to `common_field` - commonField: String @function(name: "merchantInformation-${env}") + commonField: String @function(name: "merchantInfo-${env}") } type Query { # List of locations resolves to `list_locations` - listLocations(page: Int, size: Int): [Location] @function(name: "merchantInformation-${env}") + listLocations(page: Int, size: Int): [Location] @function(name: "merchantInfo-${env}") # List of locations resolves to `list_locations` findMerchant(search: str): [Merchant] @function(name: "searchMerchant-${env}") } ``` -Create two sample Python functions via `amplify add function` and run `pipenv install aws-lambda-powertools`. Add -the following example lambda implementation +Create two new simple Python functions via `amplify add function` and run `pipenv install aws-lambda-powertools` to +add Powertools as a dependency. Add the following example lambda implementation -=== "merchantInformation/src/app.py" +=== "merchantInfo/src/app.py" - ```python hl_lines="1-3 6 8-9 13-14 18-19 23 25" - from aws_lambda_powertools.logging import Logger, correlation_paths + ```python hl_lines="1-3 7 9-10 14-15 19-20 25 27" + from aws_lambda_powertools.logging import Logger, Tracer, correlation_paths from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent from aws_lambda_powertools.utilities.event_handler import AppSyncResolver + tracer = Tracer() logger = Logger() app = AppSyncResolver() @@ -73,8 +88,9 @@ the following example lambda implementation # Would match all fieldNames matching 'commonField' ... + @tracer.capture_lambda_handler @logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_RESOLVER) - def handle(event, context): + def lambda_handler(event, context): app.resolve(event, context) ``` === "searchMerchant/src/app.py" From 347091f01ae5cda0f9881cdefa32e0198b0821e8 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 28 Mar 2021 18:26:21 -0700 Subject: [PATCH 09/15] chore(event-handler): Add current_event --- .../utilities/event_handler/appsync.py | 23 +++++++++---------- .../functional/event_handler/test_appsync.py | 2 ++ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/aws_lambda_powertools/utilities/event_handler/appsync.py b/aws_lambda_powertools/utilities/event_handler/appsync.py index ce2ee8b4aa7..61109ec4b79 100644 --- a/aws_lambda_powertools/utilities/event_handler/appsync.py +++ b/aws_lambda_powertools/utilities/event_handler/appsync.py @@ -34,6 +34,9 @@ def common_field(): ... """ + current_event: AppSyncResolverEvent + lambda_context: LambdaContext + def __init__(self): self._resolvers: dict = {} @@ -92,9 +95,10 @@ def resolve(self, _event: dict, context: LambdaContext) -> Any: 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) + self.current_event = AppSyncResolverEvent(_event) + self.lambda_context = context + resolver, config = self._resolver(self.current_event.type_name, self.current_event.field_name) + kwargs = self._kwargs(config) return resolver(**kwargs) def _resolver(self, type_name: str, field_name: str) -> tuple: @@ -118,15 +122,10 @@ def _resolver(self, type_name: str, field_name: str) -> tuple: 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]: + def _kwargs(self, config: dict) -> Dict[str, Any]: """Get the keyword arguments Parameters ---------- - event : AppSyncResolverEvent - Lambda event - context : LambdaContext - Lambda context config : dict Configuration settings Returns @@ -134,11 +133,11 @@ def _kwargs(event: AppSyncResolverEvent, context: LambdaContext, config: dict) - dict Returns keyword arguments """ - kwargs = {**event.arguments} + kwargs = {**self.current_event.arguments} if config.get("include_event", False): - kwargs["event"] = event + kwargs["event"] = self.current_event if config.get("include_context", False): - kwargs["context"] = context + kwargs["context"] = self.lambda_context return kwargs def __call__(self, event, context) -> Any: diff --git a/tests/functional/event_handler/test_appsync.py b/tests/functional/event_handler/test_appsync.py index 158a7f56778..8d26b3b02bb 100644 --- a/tests/functional/event_handler/test_appsync.py +++ b/tests/functional/event_handler/test_appsync.py @@ -24,6 +24,7 @@ def test_direct_resolver(): @app.resolver(field_name="createSomething", include_context=True) def create_something(context, id: str): # noqa AA03 VNE003 assert context == {} + assert app.lambda_context == context return id # Call the implicit handler @@ -41,6 +42,7 @@ def test_amplify_resolver(): @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 app.current_event == event assert page == 2 assert size == 1 return name From 7d987155f5a38b667b20197f96a32868962aeec3 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Tue, 30 Mar 2021 01:57:01 -0700 Subject: [PATCH 10/15] refactor(event-handler): Remove include_event and incluce_context options --- .../utilities/event_handler/appsync.py | 69 +++++-------------- docs/utilities/event_handler.md | 12 ++-- .../functional/event_handler/test_appsync.py | 51 ++------------ 3 files changed, 29 insertions(+), 103 deletions(-) diff --git a/aws_lambda_powertools/utilities/event_handler/appsync.py b/aws_lambda_powertools/utilities/event_handler/appsync.py index 61109ec4b79..983ad8c897c 100644 --- a/aws_lambda_powertools/utilities/event_handler/appsync.py +++ b/aws_lambda_powertools/utilities/event_handler/appsync.py @@ -1,4 +1,4 @@ -from typing import Any, Dict +from typing import Any, Callable from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent from aws_lambda_powertools.utilities.typing import LambdaContext @@ -18,14 +18,14 @@ class 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): + @app.resolver(type_name="Query", field_name="listLocations") + def list_locations(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(type_name="Merchant", field_name="extraInfo") + def get_extra_info(): + # Can use "app.current_event.source" to filter within the parent context ... @app.resolver(field_name="commonField") @@ -40,14 +40,7 @@ def common_field(): def __init__(self): self._resolvers: dict = {} - def resolver( - self, - type_name: str = "*", - field_name: str = None, - include_event: bool = False, - include_context: bool = False, - **kwargs, - ): + def resolver(self, type_name: str = "*", field_name: str = None): """Registers the resolver for field_name Parameters @@ -56,31 +49,20 @@ def resolver( 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 - self._resolvers[f"{type_name}.{field_name}"] = { - "func": func, - "config": kwargs, - } + self._resolvers[f"{type_name}.{field_name}"] = {"func": func} return func return register_resolver - def resolve(self, _event: dict, context: LambdaContext) -> Any: + def resolve(self, event: dict, context: LambdaContext) -> Any: """Resolve field_name Parameters ---------- - _event : dict + event : dict Lambda event context : LambdaContext Lambda context @@ -95,13 +77,12 @@ 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 = AppSyncResolverEvent(event) self.lambda_context = context - resolver, config = self._resolver(self.current_event.type_name, self.current_event.field_name) - kwargs = self._kwargs(config) - return resolver(**kwargs) + resolver = self._resolver(self.current_event.type_name, self.current_event.field_name) + return resolver(**self.current_event.arguments) - def _resolver(self, type_name: str, field_name: str) -> tuple: + def _resolver(self, type_name: str, field_name: str) -> Callable: """Find resolver for field_name Parameters @@ -113,32 +94,14 @@ def _resolver(self, type_name: str, field_name: str) -> tuple: Returns ------- - tuple + Callable 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"] - - def _kwargs(self, config: dict) -> Dict[str, Any]: - """Get the keyword arguments - Parameters - ---------- - config : dict - Configuration settings - Returns - ------- - dict - Returns keyword arguments - """ - kwargs = {**self.current_event.arguments} - if config.get("include_event", False): - kwargs["event"] = self.current_event - if config.get("include_context", False): - kwargs["context"] = self.lambda_context - return kwargs + return resolver["func"] def __call__(self, event, context) -> Any: """Implicit lambda handler which internally calls `resolve`""" diff --git a/docs/utilities/event_handler.md b/docs/utilities/event_handler.md index f5dd4f60ecd..4fc69cffc56 100644 --- a/docs/utilities/event_handler.md +++ b/docs/utilities/event_handler.md @@ -21,7 +21,7 @@ and [AppSync Direct Lambda Resolvers](https://aws.amazon.com/blogs/mobile/appsyn * Supports an implicit handler where in `app = AppSyncResolver()` can be invoked directly as `app(event, context)` * `resolver` decorator has flexible or strict matching against `fieldName` * Arguments are automatically passed into your function -* `include_event` and `include_context` options can be used to pass in the original `AppSyncResolver` or `LambdaContext` +* AppSyncResolver includes `current_event` and `lambda_cotext` fields can be used to pass in the original `AppSyncResolver` or `LambdaContext` objects #### Amplify GraphQL Example @@ -73,14 +73,14 @@ add Powertools as a dependency. Add the following example lambda implementation logger = Logger() app = AppSyncResolver() - @app.resolver(type_name="Query", field_name="listLocations", include_event=True) - def list_locations(event: AppSyncResolverEvent, page: int = 0, size: int = 10): + @app.resolver(type_name="Query", field_name="listLocations") + def list_locations(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["id"]` to filter within the Merchant context + @app.resolver(type_name="Merchant", field_name="extraInfo") + def get_extra_info(): + # Can use `app.current_event.source["id"]` to filter within the Merchant context ... @app.resolver(field_name="commonField") diff --git a/tests/functional/event_handler/test_appsync.py b/tests/functional/event_handler/test_appsync.py index 8d26b3b02bb..b45ce317f93 100644 --- a/tests/functional/event_handler/test_appsync.py +++ b/tests/functional/event_handler/test_appsync.py @@ -21,10 +21,9 @@ def test_direct_resolver(): app = AppSyncResolver() - @app.resolver(field_name="createSomething", include_context=True) - def create_something(context, id: str): # noqa AA03 VNE003 - assert context == {} - assert app.lambda_context == context + @app.resolver(field_name="createSomething") + def create_something(id: str): # noqa AA03 VNE003 + assert app.lambda_context == {} return id # Call the implicit handler @@ -39,10 +38,10 @@ def test_amplify_resolver(): 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 app.current_event == event + @app.resolver(type_name="Merchant", field_name="locations") + def get_location(page: int, size: int, name: str): + assert app.current_event is not None + assert isinstance(app.current_event, AppSyncResolverEvent) assert page == 2 assert size == 1 return name @@ -72,42 +71,6 @@ def no_params(): 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() From 240b0a60423baa0fd67d20e260ccfabe140f535e Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Tue, 30 Mar 2021 02:00:31 -0700 Subject: [PATCH 11/15] chore: bump ci From 8f04851d0994460fc7711c65b46cd2e671c4ee2a Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Tue, 30 Mar 2021 10:10:37 -0700 Subject: [PATCH 12/15] refactor(event-handler): Move to up to --- .../{utilities => }/event_handler/__init__.py | 0 .../{utilities => }/event_handler/appsync.py | 3 +-- docs/{utilities => core}/event_handler.md | 7 +++---- docs/index.md | 2 +- mkdocs.yml | 2 +- tests/functional/event_handler/test_appsync.py | 2 +- 6 files changed, 7 insertions(+), 9 deletions(-) rename aws_lambda_powertools/{utilities => }/event_handler/__init__.py (100%) rename aws_lambda_powertools/{utilities => }/event_handler/appsync.py (94%) rename docs/{utilities => core}/event_handler.md (95%) diff --git a/aws_lambda_powertools/utilities/event_handler/__init__.py b/aws_lambda_powertools/event_handler/__init__.py similarity index 100% rename from aws_lambda_powertools/utilities/event_handler/__init__.py rename to aws_lambda_powertools/event_handler/__init__.py diff --git a/aws_lambda_powertools/utilities/event_handler/appsync.py b/aws_lambda_powertools/event_handler/appsync.py similarity index 94% rename from aws_lambda_powertools/utilities/event_handler/appsync.py rename to aws_lambda_powertools/event_handler/appsync.py index 983ad8c897c..14583d83937 100644 --- a/aws_lambda_powertools/utilities/event_handler/appsync.py +++ b/aws_lambda_powertools/event_handler/appsync.py @@ -13,8 +13,7 @@ class AppSyncResolver: **Sample usage** - from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent - from aws_lambda_powertools.utilities.event_handler import AppSyncResolver + from aws_lambda_powertools.event_handler import AppSyncResolver app = AppSyncResolver() diff --git a/docs/utilities/event_handler.md b/docs/core/event_handler.md similarity index 95% rename from docs/utilities/event_handler.md rename to docs/core/event_handler.md index 4fc69cffc56..8de4e83c4ad 100644 --- a/docs/utilities/event_handler.md +++ b/docs/core/event_handler.md @@ -64,10 +64,9 @@ add Powertools as a dependency. Add the following example lambda implementation === "merchantInfo/src/app.py" - ```python hl_lines="1-3 7 9-10 14-15 19-20 25 27" + ```python hl_lines="1-2 6 8-9 13-14 18-19 24 26" + from aws_lambda_powertools.event_handler import AppSyncResolver from aws_lambda_powertools.logging import Logger, Tracer, correlation_paths - from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent - from aws_lambda_powertools.utilities.event_handler import AppSyncResolver tracer = Tracer() logger = Logger() @@ -96,7 +95,7 @@ add Powertools as a dependency. Add the following example lambda implementation === "searchMerchant/src/app.py" ```python hl_lines="1 3 5-6" - from aws_lambda_powertools.utilities.event_handler import AppSyncResolver + from aws_lambda_powertools.event_handler import AppSyncResolver app = AppSyncResolver() diff --git a/docs/index.md b/docs/index.md index ab295dc85f3..2e8a46cc3b8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -144,6 +144,7 @@ aws serverlessrepo list-application-versions \ | [Tracing](./core/tracer) | Decorators and utilities to trace Lambda function handlers, and both synchronous and asynchronous functions | [Logger](./core/logger) | Structured logging made easier, and decorator to enrich structured logging with key Lambda context details | [Metrics](./core/metrics) | Custom Metrics created asynchronously via CloudWatch Embedded Metric Format (EMF) +| [Event handler](./core/event_handler) | Event handler decorators for common Lambda events | [Middleware factory](./utilities/middleware_factory) | Decorator factory to create your own middleware to run logic before, and after each Lambda invocation | [Parameters](./utilities/parameters) | Retrieve parameter values from AWS Systems Manager Parameter Store, AWS Secrets Manager, or Amazon DynamoDB, and cache them for a specific amount of time | [Batch processing](./utilities/batch) | Handle partial failures for AWS SQS batch processing @@ -152,7 +153,6 @@ aws serverlessrepo list-application-versions \ | [Event source data classes](./utilities/data_classes) | Data classes describing the schema of common Lambda event triggers | [Parser](./utilities/parser) | Data parsing and deep validation using Pydantic | [Idempotency](./utilities/idempotency) | Idempotent Lambda handler -| [Event handler](./utilities/event_handler) | Event handler decorators for common Lambda events ## Environment variables diff --git a/mkdocs.yml b/mkdocs.yml index 0c88d7cbcda..0aa95693354 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,6 +9,7 @@ nav: - core/tracer.md - core/logger.md - core/metrics.md + - core/event_handler.md - Utilities: - utilities/middleware_factory.md - utilities/parameters.md @@ -18,7 +19,6 @@ nav: - utilities/data_classes.md - utilities/parser.md - utilities/idempotency.md - - utilities/event_handler.md theme: name: material diff --git a/tests/functional/event_handler/test_appsync.py b/tests/functional/event_handler/test_appsync.py index b45ce317f93..c72331c32f1 100644 --- a/tests/functional/event_handler/test_appsync.py +++ b/tests/functional/event_handler/test_appsync.py @@ -5,8 +5,8 @@ import pytest +from aws_lambda_powertools.event_handler import AppSyncResolver from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent -from aws_lambda_powertools.utilities.event_handler import AppSyncResolver from aws_lambda_powertools.utilities.typing import LambdaContext From daa56f93168ec0c0d5112ce93b3919ddfbdd2ca6 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Tue, 30 Mar 2021 13:22:21 -0700 Subject: [PATCH 13/15] chore(event-handler): Add debug logging to appsync resolver --- aws_lambda_powertools/event_handler/appsync.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/aws_lambda_powertools/event_handler/appsync.py b/aws_lambda_powertools/event_handler/appsync.py index 14583d83937..3b9e3ff685a 100644 --- a/aws_lambda_powertools/event_handler/appsync.py +++ b/aws_lambda_powertools/event_handler/appsync.py @@ -1,8 +1,11 @@ +import logging from typing import Any, Callable from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent from aws_lambda_powertools.utilities.typing import LambdaContext +logger = logging.getLogger(__name__) + class AppSyncResolver: """ @@ -51,6 +54,7 @@ def resolver(self, type_name: str = "*", field_name: str = None): """ def register_resolver(func): + logger.debug(f"Adding resolver `{func.__name__}` for field `{type_name}.{field_name}`") self._resolvers[f"{type_name}.{field_name}"] = {"func": func} return func From 02818db19c102e9c107b565f3224bb1f0ce68e58 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Tue, 30 Mar 2021 14:15:25 -0700 Subject: [PATCH 14/15] docs(event-handler): Add more detailed example to docstring --- aws_lambda_powertools/event_handler/appsync.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/aws_lambda_powertools/event_handler/appsync.py b/aws_lambda_powertools/event_handler/appsync.py index 3b9e3ff685a..fa3594ca274 100644 --- a/aws_lambda_powertools/event_handler/appsync.py +++ b/aws_lambda_powertools/event_handler/appsync.py @@ -21,19 +21,21 @@ class AppSyncResolver: app = AppSyncResolver() @app.resolver(type_name="Query", field_name="listLocations") - def list_locations(page: int = 0, size: int = 10): - # Your logic to fetch locations - ... + def list_locations(page: int = 0, size: int = 10) -> list: + # Your logic to fetch locations with arguments passed in + return [{"id": 100, "name": "Smooth Grooves"}] @app.resolver(type_name="Merchant", field_name="extraInfo") - def get_extra_info(): + def get_extra_info() -> dict: # Can use "app.current_event.source" to filter within the parent context - ... + account_type = app.current_event.source["accountType"] + method = "BTC" if account_type == "NEW" else "USD" + return {"preferredPaymentMethod": method} @app.resolver(field_name="commonField") - def common_field(): + def common_field() -> str: # Would match all fieldNames matching 'commonField' - ... + return str(uuid.uuid4()) """ current_event: AppSyncResolverEvent From e778cf0d1b1b09c3cf7fbcddb6429321a1b9a507 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 31 Mar 2021 00:09:50 -0700 Subject: [PATCH 15/15] chore(event-handler): Rename to _get_resolver --- aws_lambda_powertools/event_handler/appsync.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aws_lambda_powertools/event_handler/appsync.py b/aws_lambda_powertools/event_handler/appsync.py index fa3594ca274..021afaa6654 100644 --- a/aws_lambda_powertools/event_handler/appsync.py +++ b/aws_lambda_powertools/event_handler/appsync.py @@ -84,11 +84,11 @@ def resolve(self, event: dict, context: LambdaContext) -> Any: """ self.current_event = AppSyncResolverEvent(event) self.lambda_context = context - resolver = self._resolver(self.current_event.type_name, self.current_event.field_name) + resolver = self._get_resolver(self.current_event.type_name, self.current_event.field_name) return resolver(**self.current_event.arguments) - def _resolver(self, type_name: str, field_name: str) -> Callable: - """Find resolver for field_name + def _get_resolver(self, type_name: str, field_name: str) -> Callable: + """Get resolver for field_name Parameters ----------