aws-lambda-powertools 3.10.1a11__py3-none-any.whl → 3.11.1a0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aws_lambda_powertools/event_handler/__init__.py +2 -0
- aws_lambda_powertools/event_handler/api_gateway.py +6 -20
- aws_lambda_powertools/event_handler/appsync.py +4 -29
- aws_lambda_powertools/event_handler/events_appsync/__init__.py +5 -0
- aws_lambda_powertools/event_handler/events_appsync/_registry.py +92 -0
- aws_lambda_powertools/event_handler/events_appsync/appsync_events.py +422 -0
- aws_lambda_powertools/event_handler/events_appsync/base.py +44 -0
- aws_lambda_powertools/event_handler/events_appsync/exceptions.py +25 -0
- aws_lambda_powertools/event_handler/events_appsync/functions.py +106 -0
- aws_lambda_powertools/event_handler/events_appsync/router.py +199 -0
- aws_lambda_powertools/event_handler/events_appsync/types.py +21 -0
- aws_lambda_powertools/event_handler/exception_handling.py +118 -0
- aws_lambda_powertools/shared/version.py +1 -1
- aws_lambda_powertools/utilities/data_classes/__init__.py +2 -0
- aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py +41 -34
- aws_lambda_powertools/utilities/data_classes/appsync_resolver_events_event.py +56 -0
- {aws_lambda_powertools-3.10.1a11.dist-info → aws_lambda_powertools-3.11.1a0.dist-info}/METADATA +1 -1
- {aws_lambda_powertools-3.10.1a11.dist-info → aws_lambda_powertools-3.11.1a0.dist-info}/RECORD +20 -10
- {aws_lambda_powertools-3.10.1a11.dist-info → aws_lambda_powertools-3.11.1a0.dist-info}/LICENSE +0 -0
- {aws_lambda_powertools-3.10.1a11.dist-info → aws_lambda_powertools-3.11.1a0.dist-info}/WHEEL +0 -0
@@ -12,6 +12,7 @@ from aws_lambda_powertools.event_handler.api_gateway import (
|
|
12
12
|
)
|
13
13
|
from aws_lambda_powertools.event_handler.appsync import AppSyncResolver
|
14
14
|
from aws_lambda_powertools.event_handler.bedrock_agent import BedrockAgentResolver
|
15
|
+
from aws_lambda_powertools.event_handler.events_appsync.appsync_events import AppSyncEventsResolver
|
15
16
|
from aws_lambda_powertools.event_handler.lambda_function_url import (
|
16
17
|
LambdaFunctionUrlResolver,
|
17
18
|
)
|
@@ -19,6 +20,7 @@ from aws_lambda_powertools.event_handler.vpc_lattice import VPCLatticeResolver,
|
|
19
20
|
|
20
21
|
__all__ = [
|
21
22
|
"AppSyncResolver",
|
23
|
+
"AppSyncEventsResolver",
|
22
24
|
"APIGatewayRestResolver",
|
23
25
|
"APIGatewayHttpResolver",
|
24
26
|
"ALBResolver",
|
@@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Any, Generic, Literal, Match, Pattern, TypeVar
|
|
17
17
|
from typing_extensions import override
|
18
18
|
|
19
19
|
from aws_lambda_powertools.event_handler import content_types
|
20
|
+
from aws_lambda_powertools.event_handler.exception_handling import ExceptionHandlerManager
|
20
21
|
from aws_lambda_powertools.event_handler.exceptions import NotFoundError, ServiceError
|
21
22
|
from aws_lambda_powertools.event_handler.openapi.config import OpenAPIConfig
|
22
23
|
from aws_lambda_powertools.event_handler.openapi.constants import (
|
@@ -1576,6 +1577,7 @@ class ApiGatewayResolver(BaseRouter):
|
|
1576
1577
|
self.processed_stack_frames = []
|
1577
1578
|
self._response_builder_class = ResponseBuilder[BaseProxyEvent]
|
1578
1579
|
self.openapi_config = OpenAPIConfig() # starting an empty dataclass
|
1580
|
+
self.exception_handler_manager = ExceptionHandlerManager()
|
1579
1581
|
self._has_response_validation_error = response_validation_error_http_code is not None
|
1580
1582
|
self._response_validation_error_http_code = self._validate_response_validation_error_http_code(
|
1581
1583
|
response_validation_error_http_code,
|
@@ -2498,7 +2500,7 @@ class ApiGatewayResolver(BaseRouter):
|
|
2498
2500
|
return Response(status_code=204, content_type=None, headers=_headers, body="")
|
2499
2501
|
|
2500
2502
|
# Customer registered 404 route? Call it.
|
2501
|
-
custom_not_found_handler = self.
|
2503
|
+
custom_not_found_handler = self.exception_handler_manager.lookup_exception_handler(NotFoundError)
|
2502
2504
|
if custom_not_found_handler:
|
2503
2505
|
return custom_not_found_handler(NotFoundError())
|
2504
2506
|
|
@@ -2571,26 +2573,10 @@ class ApiGatewayResolver(BaseRouter):
|
|
2571
2573
|
return self.exception_handler(NotFoundError)(func)
|
2572
2574
|
|
2573
2575
|
def exception_handler(self, exc_class: type[Exception] | list[type[Exception]]):
|
2574
|
-
|
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)
|
2591
2577
|
|
2592
2578
|
def _call_exception_handler(self, exp: Exception, route: Route) -> ResponseBuilder | None:
|
2593
|
-
handler = self.
|
2579
|
+
handler = self.exception_handler_manager.lookup_exception_handler(type(exp))
|
2594
2580
|
if handler:
|
2595
2581
|
try:
|
2596
2582
|
return self._response_builder_class(response=handler(exp), serializer=self._serializer, route=route)
|
@@ -2686,7 +2672,7 @@ class ApiGatewayResolver(BaseRouter):
|
|
2686
2672
|
self._router_middlewares = self._router_middlewares + router._router_middlewares
|
2687
2673
|
|
2688
2674
|
logger.debug("Appending Router exception_handler into App exception_handler.")
|
2689
|
-
self.
|
2675
|
+
self.exception_handler_manager.update_exception_handlers(router._exception_handlers)
|
2690
2676
|
|
2691
2677
|
# use pointer to allow context clearance after event is processed e.g., resolve(evt, ctx)
|
2692
2678
|
router.context = self.context
|
@@ -5,6 +5,7 @@ import logging
|
|
5
5
|
import warnings
|
6
6
|
from typing import TYPE_CHECKING, Any
|
7
7
|
|
8
|
+
from aws_lambda_powertools.event_handler.exception_handling import ExceptionHandlerManager
|
8
9
|
from aws_lambda_powertools.event_handler.graphql_appsync.exceptions import InvalidBatchResponse, ResolverNotFoundError
|
9
10
|
from aws_lambda_powertools.event_handler.graphql_appsync.router import Router
|
10
11
|
from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
|
@@ -55,6 +56,7 @@ class AppSyncResolver(Router):
|
|
55
56
|
"""
|
56
57
|
super().__init__()
|
57
58
|
self.context = {} # early init as customers might add context before event resolution
|
59
|
+
self.exception_handler_manager = ExceptionHandlerManager()
|
58
60
|
self._exception_handlers: dict[type, Callable] = {}
|
59
61
|
|
60
62
|
def __call__(
|
@@ -153,7 +155,7 @@ class AppSyncResolver(Router):
|
|
153
155
|
Router.current_event = data_model(event)
|
154
156
|
response = self._call_single_resolver(event=event, data_model=data_model)
|
155
157
|
except Exception as exp:
|
156
|
-
response_builder = self.
|
158
|
+
response_builder = self.exception_handler_manager.lookup_exception_handler(type(exp))
|
157
159
|
if response_builder:
|
158
160
|
return response_builder(exp)
|
159
161
|
raise
|
@@ -495,31 +497,4 @@ class AppSyncResolver(Router):
|
|
495
497
|
A decorator function that registers the exception handler.
|
496
498
|
"""
|
497
499
|
|
498
|
-
|
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)
|
@@ -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,0 +1,422 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import logging
|
5
|
+
import warnings
|
6
|
+
from typing import TYPE_CHECKING, Any
|
7
|
+
|
8
|
+
from aws_lambda_powertools.event_handler.events_appsync.exceptions import UnauthorizedException
|
9
|
+
from aws_lambda_powertools.event_handler.events_appsync.router import Router
|
10
|
+
from aws_lambda_powertools.utilities.data_classes.appsync_resolver_events_event import AppSyncResolverEventsEvent
|
11
|
+
from aws_lambda_powertools.warnings import PowertoolsUserWarning
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from collections.abc import Callable
|
15
|
+
|
16
|
+
from aws_lambda_powertools.event_handler.events_appsync.types import ResolverTypeDef
|
17
|
+
from aws_lambda_powertools.utilities.typing.lambda_context import LambdaContext
|
18
|
+
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
|
23
|
+
class AppSyncEventsResolver(Router):
|
24
|
+
"""
|
25
|
+
AppSync Events API Resolver for handling publish and subscribe operations.
|
26
|
+
|
27
|
+
This class extends the Router to process AppSync real-time API events, managing
|
28
|
+
both synchronous and asynchronous resolvers for event publishing and subscribing.
|
29
|
+
|
30
|
+
Attributes
|
31
|
+
----------
|
32
|
+
context: dict
|
33
|
+
Dictionary to store context information accessible across resolvers
|
34
|
+
lambda_context: LambdaContext
|
35
|
+
Lambda context from the AWS Lambda function
|
36
|
+
current_event: AppSyncResolverEventsEvent
|
37
|
+
Current event being processed
|
38
|
+
|
39
|
+
Examples
|
40
|
+
--------
|
41
|
+
Define a simple AppSync events resolver for a chat application:
|
42
|
+
|
43
|
+
>>> from aws_lambda_powertools.event_handler import AppSyncEventsResolver
|
44
|
+
>>> app = AppSyncEventsResolver()
|
45
|
+
>>>
|
46
|
+
>>> # Using aggregate mode to process multiple messages at once
|
47
|
+
>>> @app.on_publish(channel_path="/default/*", aggregate=True)
|
48
|
+
>>> def handle_batch_messages(payload):
|
49
|
+
>>> processed_messages = []
|
50
|
+
>>> for message in payload:
|
51
|
+
>>> # Process each message
|
52
|
+
>>> processed_messages.append({
|
53
|
+
>>> "messageId": f"msg-{message.get('id')}",
|
54
|
+
>>> "processed": True
|
55
|
+
>>> })
|
56
|
+
>>> return processed_messages
|
57
|
+
>>>
|
58
|
+
>>> # Asynchronous resolver
|
59
|
+
>>> @app.async_on_publish(channel_path="/default/*")
|
60
|
+
>>> async def handle_async_messages(event):
|
61
|
+
>>> # Perform async operations (e.g., DB queries, HTTP calls)
|
62
|
+
>>> await asyncio.sleep(0.1) # Simulate async work
|
63
|
+
>>> return {
|
64
|
+
>>> "messageId": f"async-{event.get('id')}",
|
65
|
+
>>> "processed": True
|
66
|
+
>>> }
|
67
|
+
>>>
|
68
|
+
>>> # Lambda handler
|
69
|
+
>>> def lambda_handler(event, context):
|
70
|
+
>>> return events.resolve(event, context)
|
71
|
+
"""
|
72
|
+
|
73
|
+
def __init__(self):
|
74
|
+
"""Initialize the AppSyncEventsResolver."""
|
75
|
+
super().__init__()
|
76
|
+
self.context = {} # early init as customers might add context before event resolution
|
77
|
+
self._exception_handlers: dict[type, Callable] = {}
|
78
|
+
|
79
|
+
def __call__(
|
80
|
+
self,
|
81
|
+
event: dict | AppSyncResolverEventsEvent,
|
82
|
+
context: LambdaContext,
|
83
|
+
) -> Any:
|
84
|
+
"""
|
85
|
+
Implicit lambda handler which internally calls `resolve`.
|
86
|
+
|
87
|
+
Parameters
|
88
|
+
----------
|
89
|
+
event: dict or AppSyncResolverEventsEvent
|
90
|
+
The AppSync event to process
|
91
|
+
context: LambdaContext
|
92
|
+
The Lambda context
|
93
|
+
|
94
|
+
Returns
|
95
|
+
-------
|
96
|
+
Any
|
97
|
+
The resolver's response
|
98
|
+
"""
|
99
|
+
return self.resolve(event, context)
|
100
|
+
|
101
|
+
def resolve(
|
102
|
+
self,
|
103
|
+
event: dict | AppSyncResolverEventsEvent,
|
104
|
+
context: LambdaContext,
|
105
|
+
) -> Any:
|
106
|
+
"""
|
107
|
+
Resolves the response based on the provided event and decorator operation.
|
108
|
+
|
109
|
+
Parameters
|
110
|
+
----------
|
111
|
+
event: dict or AppSyncResolverEventsEvent
|
112
|
+
The AppSync event to process
|
113
|
+
context: LambdaContext
|
114
|
+
The Lambda context
|
115
|
+
|
116
|
+
Returns
|
117
|
+
-------
|
118
|
+
Any
|
119
|
+
The resolver's response based on the operation type
|
120
|
+
|
121
|
+
Examples
|
122
|
+
--------
|
123
|
+
>>> events = AppSyncEventsResolver()
|
124
|
+
>>>
|
125
|
+
>>> # Explicit call to resolve in Lambda handler
|
126
|
+
>>> def lambda_handler(event, context):
|
127
|
+
>>> return events.resolve(event, context)
|
128
|
+
"""
|
129
|
+
|
130
|
+
self._setup_context(event, context)
|
131
|
+
|
132
|
+
if self.current_event.info.operation == "PUBLISH":
|
133
|
+
response = self._publish_events(payload=self.current_event.events)
|
134
|
+
else:
|
135
|
+
response = self._subscribe_events()
|
136
|
+
|
137
|
+
self.clear_context()
|
138
|
+
|
139
|
+
return response
|
140
|
+
|
141
|
+
def _subscribe_events(self) -> Any:
|
142
|
+
"""
|
143
|
+
Handle subscribe events.
|
144
|
+
|
145
|
+
Returns
|
146
|
+
-------
|
147
|
+
Any
|
148
|
+
Any response
|
149
|
+
"""
|
150
|
+
channel_path = self.current_event.info.channel_path
|
151
|
+
logger.debug(f"Processing subscribe events for path {channel_path}")
|
152
|
+
|
153
|
+
resolver = self._subscribe_registry.find_resolver(channel_path)
|
154
|
+
if resolver:
|
155
|
+
try:
|
156
|
+
resolver["func"]()
|
157
|
+
return None # Must return None in subscribe events
|
158
|
+
except UnauthorizedException:
|
159
|
+
raise
|
160
|
+
except Exception as error:
|
161
|
+
return {"error": self._format_error_response(error)}
|
162
|
+
|
163
|
+
self._warn_no_resolver("subscribe", channel_path)
|
164
|
+
return None
|
165
|
+
|
166
|
+
def _publish_events(self, payload: list[dict[str, Any]]) -> list[dict[str, Any]] | dict[str, Any]:
|
167
|
+
"""
|
168
|
+
Handle publish events.
|
169
|
+
|
170
|
+
Parameters
|
171
|
+
----------
|
172
|
+
payload: list[dict[str, Any]]
|
173
|
+
The events payload to process
|
174
|
+
|
175
|
+
Returns
|
176
|
+
-------
|
177
|
+
list[dict[str, Any]] or dict[str, Any]
|
178
|
+
Processed events or error response
|
179
|
+
"""
|
180
|
+
|
181
|
+
channel_path = self.current_event.info.channel_path
|
182
|
+
|
183
|
+
logger.debug(f"Processing publish events for path {channel_path}")
|
184
|
+
|
185
|
+
resolver = self._publish_registry.find_resolver(channel_path)
|
186
|
+
async_resolver = self._async_publish_registry.find_resolver(channel_path)
|
187
|
+
|
188
|
+
if resolver and async_resolver:
|
189
|
+
warnings.warn(
|
190
|
+
f"Both synchronous and asynchronous resolvers found for the same event and field."
|
191
|
+
f"The synchronous resolver takes precedence. Executing: {resolver['func'].__name__}",
|
192
|
+
stacklevel=2,
|
193
|
+
category=PowertoolsUserWarning,
|
194
|
+
)
|
195
|
+
|
196
|
+
if resolver:
|
197
|
+
logger.debug(f"Found sync resolver: {resolver}")
|
198
|
+
return self._process_publish_event_sync_resolver(resolver)
|
199
|
+
|
200
|
+
if async_resolver:
|
201
|
+
logger.debug(f"Found async resolver: {async_resolver}")
|
202
|
+
return asyncio.run(self._call_publish_event_async_resolver(async_resolver))
|
203
|
+
|
204
|
+
# No resolver found
|
205
|
+
# Warning and returning AS IS
|
206
|
+
self._warn_no_resolver("publish", channel_path, return_as_is=True)
|
207
|
+
return {"events": payload}
|
208
|
+
|
209
|
+
def _process_publish_event_sync_resolver(
|
210
|
+
self,
|
211
|
+
resolver: ResolverTypeDef,
|
212
|
+
) -> list[dict[str, Any]] | dict[str, Any]:
|
213
|
+
"""
|
214
|
+
Process events using a synchronous resolver.
|
215
|
+
|
216
|
+
Parameters
|
217
|
+
----------
|
218
|
+
resolver : ResolverTypeDef
|
219
|
+
The resolver to use for processing events
|
220
|
+
|
221
|
+
Returns
|
222
|
+
-------
|
223
|
+
list[dict[str, Any]] or dict[str, Any]
|
224
|
+
Processed events or error response
|
225
|
+
|
226
|
+
Notes
|
227
|
+
-----
|
228
|
+
If the resolver is configured with aggregate=True, all events are processed
|
229
|
+
as a batch. Otherwise, each event is processed individually.
|
230
|
+
"""
|
231
|
+
|
232
|
+
# Checks whether the entire batch should be processed at once
|
233
|
+
if resolver["aggregate"]:
|
234
|
+
try:
|
235
|
+
# Process the entire batch
|
236
|
+
response = resolver["func"](payload=self.current_event.events)
|
237
|
+
|
238
|
+
if not isinstance(response, list):
|
239
|
+
warnings.warn(
|
240
|
+
"Response must be a list when using aggregate, AppSync will drop those events.",
|
241
|
+
stacklevel=2,
|
242
|
+
category=PowertoolsUserWarning,
|
243
|
+
)
|
244
|
+
|
245
|
+
return {"events": response}
|
246
|
+
except UnauthorizedException:
|
247
|
+
raise
|
248
|
+
except Exception as error:
|
249
|
+
return {"error": self._format_error_response(error)}
|
250
|
+
|
251
|
+
# By default, we gracefully append `None` for any records that failed processing
|
252
|
+
results = []
|
253
|
+
for idx, event in enumerate(self.current_event.events):
|
254
|
+
try:
|
255
|
+
result_return = resolver["func"](payload=event.get("payload"))
|
256
|
+
results.append({"id": event.get("id"), "payload": result_return})
|
257
|
+
except Exception as error:
|
258
|
+
logger.debug(f"Failed to process event number {idx}")
|
259
|
+
error_return = {"id": event.get("id"), "error": self._format_error_response(error)}
|
260
|
+
results.append(error_return)
|
261
|
+
|
262
|
+
return {"events": results}
|
263
|
+
|
264
|
+
async def _call_publish_event_async_resolver(
|
265
|
+
self,
|
266
|
+
resolver: ResolverTypeDef,
|
267
|
+
) -> list[dict[str, Any]] | dict[str, Any]:
|
268
|
+
"""
|
269
|
+
Process events using an asynchronous resolver.
|
270
|
+
|
271
|
+
Parameters
|
272
|
+
----------
|
273
|
+
resolver: ResolverTypeDef
|
274
|
+
The async resolver to use for processing events
|
275
|
+
|
276
|
+
Returns
|
277
|
+
-------
|
278
|
+
list[Any]
|
279
|
+
Processed events or error responses
|
280
|
+
|
281
|
+
Notes
|
282
|
+
-----
|
283
|
+
If the resolver is configured with aggregate=True, all events are processed
|
284
|
+
as a batch. Otherwise, each event is processed individually and in parallel.
|
285
|
+
"""
|
286
|
+
|
287
|
+
# Checks whether the entire batch should be processed at once
|
288
|
+
if resolver["aggregate"]:
|
289
|
+
try:
|
290
|
+
# Process the entire batch
|
291
|
+
response = await resolver["func"](payload=self.current_event.events)
|
292
|
+
if not isinstance(response, list):
|
293
|
+
warnings.warn(
|
294
|
+
"Response must be a list when using aggregate, AppSync will drop those events.",
|
295
|
+
stacklevel=2,
|
296
|
+
category=PowertoolsUserWarning,
|
297
|
+
)
|
298
|
+
|
299
|
+
return {"events": response}
|
300
|
+
except UnauthorizedException:
|
301
|
+
raise
|
302
|
+
except Exception as error:
|
303
|
+
return {"error": self._format_error_response(error)}
|
304
|
+
|
305
|
+
response_async: list = []
|
306
|
+
|
307
|
+
# Prime coroutines
|
308
|
+
tasks = [resolver["func"](payload=e.get("payload")) for e in self.current_event.events]
|
309
|
+
|
310
|
+
# Aggregate results and exceptions, then filter them out
|
311
|
+
# Use `None` upon exception for graceful error handling at GraphQL engine level
|
312
|
+
#
|
313
|
+
# NOTE: asyncio.gather(return_exceptions=True) catches and includes exceptions in the results
|
314
|
+
# this will become useful when we support exception handling in AppSync resolver
|
315
|
+
# Aggregate results and exceptions, then filter them out
|
316
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
317
|
+
response_async.extend(
|
318
|
+
[
|
319
|
+
(
|
320
|
+
{"id": e.get("id"), "error": self._format_error_response(ret)}
|
321
|
+
if isinstance(ret, Exception)
|
322
|
+
else {"id": e.get("id"), "payload": ret}
|
323
|
+
)
|
324
|
+
for e, ret in zip(self.current_event.events, results)
|
325
|
+
],
|
326
|
+
)
|
327
|
+
|
328
|
+
return {"events": response_async}
|
329
|
+
|
330
|
+
def include_router(self, router: Router) -> None:
|
331
|
+
"""
|
332
|
+
Add all resolvers defined in a router to this resolver.
|
333
|
+
|
334
|
+
Parameters
|
335
|
+
----------
|
336
|
+
router : Router
|
337
|
+
A router containing resolvers to include
|
338
|
+
|
339
|
+
Examples
|
340
|
+
--------
|
341
|
+
>>> # Create main resolver and a router
|
342
|
+
>>> app = AppSyncEventsResolver()
|
343
|
+
>>> router = Router()
|
344
|
+
>>>
|
345
|
+
>>> # Define resolvers in the router
|
346
|
+
>>> @router.publish(path="/chat/message")
|
347
|
+
>>> def handle_chat_message(payload):
|
348
|
+
>>> return {"processed": True, "messageId": payload.get("id")}
|
349
|
+
>>>
|
350
|
+
>>> # Include the router in the main resolver
|
351
|
+
>>> app.include_router(chat_router)
|
352
|
+
>>>
|
353
|
+
>>> # Now events can handle "/chat/message" channel_path
|
354
|
+
"""
|
355
|
+
|
356
|
+
# Merge app and router context
|
357
|
+
logger.debug("Merging router and app context")
|
358
|
+
self.context.update(**router.context)
|
359
|
+
|
360
|
+
# use pointer to allow context clearance after event is processed e.g., resolve(evt, ctx)
|
361
|
+
router.context = self.context
|
362
|
+
|
363
|
+
logger.debug("Merging router resolver registries")
|
364
|
+
self._publish_registry.merge(router._publish_registry)
|
365
|
+
self._async_publish_registry.merge(router._async_publish_registry)
|
366
|
+
self._subscribe_registry.merge(router._subscribe_registry)
|
367
|
+
|
368
|
+
def _format_error_response(self, error=None) -> str:
|
369
|
+
"""
|
370
|
+
Format error responses consistently.
|
371
|
+
|
372
|
+
Parameters
|
373
|
+
----------
|
374
|
+
error: Exception or None
|
375
|
+
The error to format
|
376
|
+
|
377
|
+
Returns
|
378
|
+
-------
|
379
|
+
str
|
380
|
+
Formatted error message
|
381
|
+
"""
|
382
|
+
if isinstance(error, Exception):
|
383
|
+
return f"{error.__class__.__name__} - {str(error)}"
|
384
|
+
return "An unknown error occurred"
|
385
|
+
|
386
|
+
def _warn_no_resolver(self, operation_type: str, path: str, return_as_is: bool = False) -> None:
|
387
|
+
"""
|
388
|
+
Generate consistent warning messages for missing resolvers.
|
389
|
+
|
390
|
+
Parameters
|
391
|
+
----------
|
392
|
+
operation_type : str
|
393
|
+
Type of operation (e.g., "publish", "subscribe")
|
394
|
+
path : str
|
395
|
+
The channel path that's missing a resolver
|
396
|
+
return_as_is : bool, optional
|
397
|
+
Whether payload will be returned as is, by default False
|
398
|
+
"""
|
399
|
+
message = (
|
400
|
+
f"No resolvers were found for {operation_type} operations with path {path}"
|
401
|
+
f"{'. We will return the entire payload as is' if return_as_is else ''}"
|
402
|
+
)
|
403
|
+
warnings.warn(message, stacklevel=3, category=PowertoolsUserWarning)
|
404
|
+
|
405
|
+
def _setup_context(self, event: dict | AppSyncResolverEventsEvent, context: LambdaContext) -> None:
|
406
|
+
"""
|
407
|
+
Set up the context and event for processing.
|
408
|
+
|
409
|
+
Parameters
|
410
|
+
----------
|
411
|
+
event : dict or AppSyncResolverEventsEvent
|
412
|
+
The AppSync event to process
|
413
|
+
context : LambdaContext
|
414
|
+
The Lambda context
|
415
|
+
"""
|
416
|
+
self.lambda_context = context
|
417
|
+
Router.lambda_context = context
|
418
|
+
|
419
|
+
Router.current_event = (
|
420
|
+
event if isinstance(event, AppSyncResolverEventsEvent) else AppSyncResolverEventsEvent(event)
|
421
|
+
)
|
422
|
+
self.current_event = Router.current_event
|