aws-lambda-powertools 3.10.1a11__py3-none-any.whl → 3.11.0__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.
Files changed (20) hide show
  1. aws_lambda_powertools/event_handler/__init__.py +2 -0
  2. aws_lambda_powertools/event_handler/api_gateway.py +6 -20
  3. aws_lambda_powertools/event_handler/appsync.py +4 -29
  4. aws_lambda_powertools/event_handler/events_appsync/__init__.py +5 -0
  5. aws_lambda_powertools/event_handler/events_appsync/_registry.py +92 -0
  6. aws_lambda_powertools/event_handler/events_appsync/appsync_events.py +422 -0
  7. aws_lambda_powertools/event_handler/events_appsync/base.py +44 -0
  8. aws_lambda_powertools/event_handler/events_appsync/exceptions.py +25 -0
  9. aws_lambda_powertools/event_handler/events_appsync/functions.py +106 -0
  10. aws_lambda_powertools/event_handler/events_appsync/router.py +199 -0
  11. aws_lambda_powertools/event_handler/events_appsync/types.py +21 -0
  12. aws_lambda_powertools/event_handler/exception_handling.py +118 -0
  13. aws_lambda_powertools/shared/version.py +1 -1
  14. aws_lambda_powertools/utilities/data_classes/__init__.py +2 -0
  15. aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py +41 -34
  16. aws_lambda_powertools/utilities/data_classes/appsync_resolver_events_event.py +56 -0
  17. {aws_lambda_powertools-3.10.1a11.dist-info → aws_lambda_powertools-3.11.0.dist-info}/METADATA +1 -1
  18. {aws_lambda_powertools-3.10.1a11.dist-info → aws_lambda_powertools-3.11.0.dist-info}/RECORD +20 -10
  19. {aws_lambda_powertools-3.10.1a11.dist-info → aws_lambda_powertools-3.11.0.dist-info}/LICENSE +0 -0
  20. {aws_lambda_powertools-3.10.1a11.dist-info → aws_lambda_powertools-3.11.0.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._lookup_exception_handler(NotFoundError)
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
- 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)
2591
2577
 
2592
2578
  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))
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._exception_handlers.update(router._exception_handlers)
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._lookup_exception_handler(type(exp))
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
- 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)
@@ -0,0 +1,5 @@
1
+ from aws_lambda_powertools.event_handler.events_appsync.appsync_events import AppSyncEventsResolver
2
+
3
+ __all__ = [
4
+ "AppSyncEventsResolver",
5
+ ]
@@ -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