Skip to content

Commit 970d97e

Browse files
feat(event_handler): add AppSync events resolver (#6558)
* Adding AppSync events * Adding AppSync events
1 parent 0939463 commit 970d97e

32 files changed

+3222
-128
lines changed

aws_lambda_powertools/event_handler/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
)
1313
from aws_lambda_powertools.event_handler.appsync import AppSyncResolver
1414
from aws_lambda_powertools.event_handler.bedrock_agent import BedrockAgentResolver
15+
from aws_lambda_powertools.event_handler.events_appsync.appsync_events import AppSyncEventsResolver
1516
from aws_lambda_powertools.event_handler.lambda_function_url import (
1617
LambdaFunctionUrlResolver,
1718
)
1819
from aws_lambda_powertools.event_handler.vpc_lattice import VPCLatticeResolver, VPCLatticeV2Resolver
1920

2021
__all__ = [
2122
"AppSyncResolver",
23+
"AppSyncEventsResolver",
2224
"APIGatewayRestResolver",
2325
"APIGatewayHttpResolver",
2426
"ALBResolver",

aws_lambda_powertools/event_handler/api_gateway.py

+6-20
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from typing_extensions import override
1818

1919
from aws_lambda_powertools.event_handler import content_types
20+
from aws_lambda_powertools.event_handler.exception_handling import ExceptionHandlerManager
2021
from aws_lambda_powertools.event_handler.exceptions import NotFoundError, ServiceError
2122
from aws_lambda_powertools.event_handler.openapi.config import OpenAPIConfig
2223
from aws_lambda_powertools.event_handler.openapi.constants import (
@@ -1576,6 +1577,7 @@ def __init__(
15761577
self.processed_stack_frames = []
15771578
self._response_builder_class = ResponseBuilder[BaseProxyEvent]
15781579
self.openapi_config = OpenAPIConfig() # starting an empty dataclass
1580+
self.exception_handler_manager = ExceptionHandlerManager()
15791581
self._has_response_validation_error = response_validation_error_http_code is not None
15801582
self._response_validation_error_http_code = self._validate_response_validation_error_http_code(
15811583
response_validation_error_http_code,
@@ -2498,7 +2500,7 @@ def not_found_handler():
24982500
return Response(status_code=204, content_type=None, headers=_headers, body="")
24992501

25002502
# Customer registered 404 route? Call it.
2501-
custom_not_found_handler = self._lookup_exception_handler(NotFoundError)
2503+
custom_not_found_handler = self.exception_handler_manager.lookup_exception_handler(NotFoundError)
25022504
if custom_not_found_handler:
25032505
return custom_not_found_handler(NotFoundError())
25042506

@@ -2571,26 +2573,10 @@ def not_found(self, func: Callable | None = None):
25712573
return self.exception_handler(NotFoundError)(func)
25722574

25732575
def exception_handler(self, exc_class: type[Exception] | list[type[Exception]]):
2574-
def register_exception_handler(func: Callable):
2575-
if isinstance(exc_class, list): # pragma: no cover
2576-
for exp in exc_class:
2577-
self._exception_handlers[exp] = func
2578-
else:
2579-
self._exception_handlers[exc_class] = func
2580-
return func
2581-
2582-
return register_exception_handler
2583-
2584-
def _lookup_exception_handler(self, exp_type: type) -> Callable | None:
2585-
# Use "Method Resolution Order" to allow for matching against a base class
2586-
# of an exception
2587-
for cls in exp_type.__mro__:
2588-
if cls in self._exception_handlers:
2589-
return self._exception_handlers[cls]
2590-
return None
2576+
return self.exception_handler_manager.exception_handler(exc_class=exc_class)
25912577

25922578
def _call_exception_handler(self, exp: Exception, route: Route) -> ResponseBuilder | None:
2593-
handler = self._lookup_exception_handler(type(exp))
2579+
handler = self.exception_handler_manager.lookup_exception_handler(type(exp))
25942580
if handler:
25952581
try:
25962582
return self._response_builder_class(response=handler(exp), serializer=self._serializer, route=route)
@@ -2686,7 +2672,7 @@ def include_router(self, router: Router, prefix: str | None = None) -> None:
26862672
self._router_middlewares = self._router_middlewares + router._router_middlewares
26872673

26882674
logger.debug("Appending Router exception_handler into App exception_handler.")
2689-
self._exception_handlers.update(router._exception_handlers)
2675+
self.exception_handler_manager.update_exception_handlers(router._exception_handlers)
26902676

26912677
# use pointer to allow context clearance after event is processed e.g., resolve(evt, ctx)
26922678
router.context = self.context

aws_lambda_powertools/event_handler/appsync.py

+4-29
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import warnings
66
from typing import TYPE_CHECKING, Any
77

8+
from aws_lambda_powertools.event_handler.exception_handling import ExceptionHandlerManager
89
from aws_lambda_powertools.event_handler.graphql_appsync.exceptions import InvalidBatchResponse, ResolverNotFoundError
910
from aws_lambda_powertools.event_handler.graphql_appsync.router import Router
1011
from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
@@ -55,6 +56,7 @@ def __init__(self):
5556
"""
5657
super().__init__()
5758
self.context = {} # early init as customers might add context before event resolution
59+
self.exception_handler_manager = ExceptionHandlerManager()
5860
self._exception_handlers: dict[type, Callable] = {}
5961

6062
def __call__(
@@ -153,7 +155,7 @@ def lambda_handler(event, context):
153155
Router.current_event = data_model(event)
154156
response = self._call_single_resolver(event=event, data_model=data_model)
155157
except Exception as exp:
156-
response_builder = self._lookup_exception_handler(type(exp))
158+
response_builder = self.exception_handler_manager.lookup_exception_handler(type(exp))
157159
if response_builder:
158160
return response_builder(exp)
159161
raise
@@ -495,31 +497,4 @@ def exception_handler(self, exc_class: type[Exception] | list[type[Exception]]):
495497
A decorator function that registers the exception handler.
496498
"""
497499

498-
def register_exception_handler(func: Callable):
499-
if isinstance(exc_class, list): # pragma: no cover
500-
for exp in exc_class:
501-
self._exception_handlers[exp] = func
502-
else:
503-
self._exception_handlers[exc_class] = func
504-
return func
505-
506-
return register_exception_handler
507-
508-
def _lookup_exception_handler(self, exp_type: type) -> Callable | None:
509-
"""
510-
Looks up the registered exception handler for the given exception type or its base classes.
511-
512-
Parameters
513-
----------
514-
exp_type (type):
515-
The exception type to look up the handler for.
516-
517-
Returns
518-
-------
519-
Callable | None:
520-
The registered exception handler function if found, otherwise None.
521-
"""
522-
for cls in exp_type.__mro__:
523-
if cls in self._exception_handlers:
524-
return self._exception_handlers[cls]
525-
return None
500+
return self.exception_handler_manager.exception_handler(exc_class=exc_class)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from aws_lambda_powertools.event_handler.events_appsync.appsync_events import AppSyncEventsResolver
2+
3+
__all__ = [
4+
"AppSyncEventsResolver",
5+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
import warnings
5+
from typing import TYPE_CHECKING
6+
7+
from aws_lambda_powertools.event_handler.events_appsync.functions import find_best_route, is_valid_path
8+
from aws_lambda_powertools.warnings import PowertoolsUserWarning
9+
10+
if TYPE_CHECKING:
11+
from collections.abc import Callable
12+
13+
from aws_lambda_powertools.event_handler.events_appsync.types import ResolverTypeDef
14+
15+
16+
logger = logging.getLogger(__name__)
17+
18+
19+
class ResolverEventsRegistry:
20+
def __init__(self, kind_resolver: str):
21+
self.resolvers: dict[str, ResolverTypeDef] = {}
22+
self.kind_resolver = kind_resolver
23+
24+
def register(
25+
self,
26+
path: str = "/default/*",
27+
aggregate: bool = False,
28+
) -> Callable | None:
29+
"""Registers the resolver for path that includes namespace + channel
30+
31+
Parameters
32+
----------
33+
path : str
34+
Path including namespace + channel
35+
aggregate: bool
36+
A flag indicating whether the batch items should be processed at once or individually.
37+
If True, the resolver will process all items as a single event.
38+
If False (default), the resolver will process each item individually.
39+
40+
Return
41+
----------
42+
Callable
43+
A Callable
44+
"""
45+
46+
def _register(func) -> Callable | None:
47+
if not is_valid_path(path):
48+
warnings.warn(
49+
f"The path `{path}` registered for `{self.kind_resolver}` is not valid and will be skipped."
50+
f"A path should always have a namespace starting with '/'"
51+
"A path can have multiple namespaces, all separated by '/'."
52+
"Wildcards are allowed only at the end of the path.",
53+
stacklevel=2,
54+
category=PowertoolsUserWarning,
55+
)
56+
return None
57+
58+
logger.debug(
59+
f"Adding resolver `{func.__name__}` for path `{path}` and kind_resolver `{self.kind_resolver}`",
60+
)
61+
self.resolvers[f"{path}"] = {
62+
"func": func,
63+
"aggregate": aggregate,
64+
}
65+
return func
66+
67+
return _register
68+
69+
def find_resolver(self, path: str) -> ResolverTypeDef | None:
70+
"""Find resolver based on type_name and field_name
71+
72+
Parameters
73+
----------
74+
path : str
75+
Type name
76+
Return
77+
----------
78+
dict | None
79+
A dictionary with the resolver and if this is aggregated or not
80+
"""
81+
logger.debug(f"Looking for resolver for path `{path}` and kind_resolver `{self.kind_resolver}`")
82+
return self.resolvers.get(find_best_route(self.resolvers, path))
83+
84+
def merge(self, other_registry: ResolverEventsRegistry):
85+
"""Update current registry with incoming registry
86+
87+
Parameters
88+
----------
89+
other_registry : ResolverRegistry
90+
Registry to merge from
91+
"""
92+
self.resolvers.update(**other_registry.resolvers)

0 commit comments

Comments
 (0)