aws-lambda-powertools 3.0.0__py3-none-any.whl → 3.0.0.dev2__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 (48) hide show
  1. aws_lambda_powertools/event_handler/api_gateway.py +54 -227
  2. aws_lambda_powertools/event_handler/appsync.py +97 -343
  3. aws_lambda_powertools/event_handler/bedrock_agent.py +0 -121
  4. aws_lambda_powertools/event_handler/exceptions.py +1 -1
  5. aws_lambda_powertools/event_handler/middlewares/openapi_validation.py +1 -1
  6. aws_lambda_powertools/event_handler/openapi/compat.py +7 -4
  7. aws_lambda_powertools/event_handler/openapi/constants.py +4 -0
  8. aws_lambda_powertools/event_handler/openapi/encoders.py +70 -94
  9. aws_lambda_powertools/event_handler/openapi/exceptions.py +0 -12
  10. aws_lambda_powertools/event_handler/openapi/models.py +9 -41
  11. aws_lambda_powertools/event_handler/openapi/swagger_ui/html.py +4 -1
  12. aws_lambda_powertools/event_handler/openapi/swagger_ui/oauth2.py +1 -1
  13. aws_lambda_powertools/event_handler/openapi/types.py +7 -8
  14. aws_lambda_powertools/event_handler/util.py +2 -55
  15. aws_lambda_powertools/logging/utils.py +5 -5
  16. aws_lambda_powertools/metrics/provider/cloudwatch_emf/metric_properties.py +0 -1
  17. aws_lambda_powertools/shared/cookies.py +2 -2
  18. aws_lambda_powertools/shared/dynamodb_deserializer.py +0 -13
  19. aws_lambda_powertools/shared/version.py +1 -1
  20. aws_lambda_powertools/tracing/tracer.py +2 -4
  21. aws_lambda_powertools/utilities/batch/base.py +4 -13
  22. aws_lambda_powertools/utilities/data_classes/api_gateway_authorizer_event.py +1 -101
  23. aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py +1 -57
  24. aws_lambda_powertools/utilities/data_classes/cognito_user_pool_event.py +1 -260
  25. aws_lambda_powertools/utilities/data_classes/common.py +1 -114
  26. aws_lambda_powertools/utilities/data_classes/kafka_event.py +2 -2
  27. aws_lambda_powertools/utilities/data_classes/shared_functions.py +0 -141
  28. aws_lambda_powertools/utilities/data_classes/vpc_lattice.py +3 -8
  29. aws_lambda_powertools/utilities/data_masking/base.py +4 -4
  30. aws_lambda_powertools/utilities/feature_flags/appconfig.py +2 -21
  31. aws_lambda_powertools/utilities/idempotency/idempotency.py +2 -22
  32. aws_lambda_powertools/utilities/idempotency/persistence/base.py +1 -4
  33. aws_lambda_powertools/utilities/idempotency/persistence/datarecord.py +0 -21
  34. aws_lambda_powertools/utilities/parameters/secrets.py +1 -1
  35. aws_lambda_powertools/utilities/parser/models/__init__.py +0 -6
  36. aws_lambda_powertools/utilities/parser/models/apigw.py +1 -12
  37. aws_lambda_powertools/utilities/parser/models/apigwv2.py +1 -7
  38. aws_lambda_powertools/utilities/validation/base.py +2 -27
  39. aws_lambda_powertools/utilities/validation/validator.py +4 -49
  40. {aws_lambda_powertools-3.0.0.dist-info → aws_lambda_powertools-3.0.0.dev2.dist-info}/METADATA +2 -12
  41. {aws_lambda_powertools-3.0.0.dist-info → aws_lambda_powertools-3.0.0.dev2.dist-info}/RECORD +43 -48
  42. aws_lambda_powertools/event_handler/graphql_appsync/__init__.py +0 -0
  43. aws_lambda_powertools/event_handler/graphql_appsync/_registry.py +0 -78
  44. aws_lambda_powertools/event_handler/graphql_appsync/base.py +0 -160
  45. aws_lambda_powertools/event_handler/graphql_appsync/exceptions.py +0 -10
  46. aws_lambda_powertools/event_handler/graphql_appsync/router.py +0 -62
  47. {aws_lambda_powertools-3.0.0.dist-info → aws_lambda_powertools-3.0.0.dev2.dist-info}/LICENSE +0 -0
  48. {aws_lambda_powertools-3.0.0.dist-info → aws_lambda_powertools-3.0.0.dev2.dist-info}/WHEEL +0 -0
@@ -1,80 +1,100 @@
1
1
  from __future__ import annotations
2
2
 
3
- import asyncio
4
3
  import logging
5
- import warnings
6
- from typing import TYPE_CHECKING, Any, Callable
4
+ from typing import TYPE_CHECKING, Any, Callable, TypeVar
7
5
 
8
- from aws_lambda_powertools.event_handler.graphql_appsync.exceptions import InvalidBatchResponse, ResolverNotFoundError
9
- from aws_lambda_powertools.event_handler.graphql_appsync.router import Router
10
6
  from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
11
7
 
12
8
  if TYPE_CHECKING:
13
9
  from aws_lambda_powertools.utilities.typing import LambdaContext
14
10
 
15
- from aws_lambda_powertools.warnings import PowertoolsUserWarning
16
-
17
11
  logger = logging.getLogger(__name__)
18
12
 
13
+ AppSyncResolverEventT = TypeVar("AppSyncResolverEventT", bound=AppSyncResolverEvent)
14
+
15
+
16
+ class BaseRouter:
17
+ current_event: AppSyncResolverEventT # type: ignore[valid-type]
18
+ lambda_context: LambdaContext
19
+ context: dict
20
+
21
+ def __init__(self):
22
+ self._resolvers: dict = {}
19
23
 
20
- class AppSyncResolver(Router):
24
+ def resolver(self, type_name: str = "*", field_name: str | None = None):
25
+ """Registers the resolver for field_name
26
+
27
+ Parameters
28
+ ----------
29
+ type_name : str
30
+ Type name
31
+ field_name : str
32
+ Field name
33
+ """
34
+
35
+ def register_resolver(func):
36
+ logger.debug(f"Adding resolver `{func.__name__}` for field `{type_name}.{field_name}`")
37
+ self._resolvers[f"{type_name}.{field_name}"] = {"func": func}
38
+ return func
39
+
40
+ return register_resolver
41
+
42
+ def append_context(self, **additional_context):
43
+ """Append key=value data as routing context"""
44
+ self.context.update(**additional_context)
45
+
46
+ def clear_context(self):
47
+ """Resets routing context"""
48
+ self.context.clear()
49
+
50
+
51
+ class AppSyncResolver(BaseRouter):
21
52
  """
22
- AppSync GraphQL API Resolver
53
+ AppSync resolver decorator
23
54
 
24
55
  Example
25
56
  -------
26
- ```python
27
- from aws_lambda_powertools.event_handler import AppSyncResolver
28
-
29
- app = AppSyncResolver()
30
-
31
- @app.resolver(type_name="Query", field_name="listLocations")
32
- def list_locations(page: int = 0, size: int = 10) -> list:
33
- # Your logic to fetch locations with arguments passed in
34
- return [{"id": 100, "name": "Smooth Grooves"}]
35
-
36
- @app.resolver(type_name="Merchant", field_name="extraInfo")
37
- def get_extra_info() -> dict:
38
- # Can use "app.current_event.source" to filter within the parent context
39
- account_type = app.current_event.source["accountType"]
40
- method = "BTC" if account_type == "NEW" else "USD"
41
- return {"preferredPaymentMethod": method}
42
-
43
- @app.resolver(field_name="commonField")
44
- def common_field() -> str:
45
- # Would match all fieldNames matching 'commonField'
46
- return str(uuid.uuid4())
47
- ```
57
+
58
+ **Sample usage**
59
+
60
+ from aws_lambda_powertools.event_handler import AppSyncResolver
61
+
62
+ app = AppSyncResolver()
63
+
64
+ @app.resolver(type_name="Query", field_name="listLocations")
65
+ def list_locations(page: int = 0, size: int = 10) -> list:
66
+ # Your logic to fetch locations with arguments passed in
67
+ return [{"id": 100, "name": "Smooth Grooves"}]
68
+
69
+ @app.resolver(type_name="Merchant", field_name="extraInfo")
70
+ def get_extra_info() -> dict:
71
+ # Can use "app.current_event.source" to filter within the parent context
72
+ account_type = app.current_event.source["accountType"]
73
+ method = "BTC" if account_type == "NEW" else "USD"
74
+ return {"preferredPaymentMethod": method}
75
+
76
+ @app.resolver(field_name="commonField")
77
+ def common_field() -> str:
78
+ # Would match all fieldNames matching 'commonField'
79
+ return str(uuid.uuid4())
48
80
  """
49
81
 
50
82
  def __init__(self):
51
- """
52
- Initialize a new instance of the AppSyncResolver.
53
- """
54
83
  super().__init__()
55
84
  self.context = {} # early init as customers might add context before event resolution
56
85
 
57
- def __call__(
58
- self,
59
- event: dict,
60
- context: LambdaContext,
61
- data_model: type[AppSyncResolverEvent] = AppSyncResolverEvent,
62
- ) -> Any:
63
- """Implicit lambda handler which internally calls `resolve`"""
64
- return self.resolve(event, context, data_model)
65
-
66
86
  def resolve(
67
87
  self,
68
- event: dict | list[dict],
88
+ event: dict,
69
89
  context: LambdaContext,
70
90
  data_model: type[AppSyncResolverEvent] = AppSyncResolverEvent,
71
91
  ) -> Any:
72
- """Resolves the response based on the provide event and decorator routes
92
+ """Resolve field_name
73
93
 
74
94
  Parameters
75
95
  ----------
76
- event : dict | list[Dict]
77
- Lambda event either coming from batch processing endpoint or from standard processing endpoint
96
+ event : dict
97
+ Lambda event
78
98
  context : LambdaContext
79
99
  Lambda context
80
100
  data_model:
@@ -138,214 +158,45 @@ class AppSyncResolver(Router):
138
158
  ValueError
139
159
  If we could not find a field resolver
140
160
  """
161
+ # Maintenance: revisit generics/overload to fix [attr-defined] in mypy usage
162
+ BaseRouter.current_event = data_model(event)
163
+ BaseRouter.lambda_context = context
141
164
 
142
- self.lambda_context = context
143
- Router.lambda_context = context
144
-
145
- if isinstance(event, list):
146
- Router.current_batch_event = [data_model(e) for e in event]
147
- response = self._call_batch_resolver(event=event, data_model=data_model)
148
- else:
149
- Router.current_event = data_model(event)
150
- response = self._call_single_resolver(event=event, data_model=data_model)
151
-
165
+ resolver = self._get_resolver(BaseRouter.current_event.type_name, BaseRouter.current_event.field_name)
166
+ response = resolver(**BaseRouter.current_event.arguments)
152
167
  self.clear_context()
153
168
 
154
169
  return response
155
170
 
156
- def _call_single_resolver(self, event: dict, data_model: type[AppSyncResolverEvent]) -> Any:
157
- """Call single event resolver
171
+ def _get_resolver(self, type_name: str, field_name: str) -> Callable:
172
+ """Get resolver for field_name
158
173
 
159
174
  Parameters
160
175
  ----------
161
- event : dict
162
- Event
163
- data_model : type[AppSyncResolverEvent]
164
- Data_model to decode AppSync event, by default it is of AppSyncResolverEvent type or subclass of it
165
- """
166
-
167
- logger.debug("Processing direct resolver event")
168
-
169
- self.current_event = data_model(event)
170
- resolver = self._resolver_registry.find_resolver(self.current_event.type_name, self.current_event.field_name)
171
- if not resolver:
172
- raise ValueError(f"No resolver found for '{self.current_event.type_name}.{self.current_event.field_name}'")
173
- return resolver["func"](**self.current_event.arguments)
174
-
175
- def _call_sync_batch_resolver(
176
- self,
177
- resolver: Callable,
178
- raise_on_error: bool = False,
179
- aggregate: bool = True,
180
- ) -> list[Any]:
181
- """
182
- Calls a synchronous batch resolver function for each event in the current batch.
183
-
184
- Parameters
185
- ----------
186
- resolver: Callable
187
- The callable function to resolve events.
188
- raise_on_error: bool
189
- A flag indicating whether to raise an error when processing batches
190
- with failed items. Defaults to False, which means errors are handled without raising exceptions.
191
- aggregate: bool
192
- A flag indicating whether the batch items should be processed at once or individually.
193
- If True (default), the batch resolver will process all items in the batch as a single event.
194
- If False, the batch resolver will process each item in the batch individually.
176
+ type_name : str
177
+ Type name
178
+ field_name : str
179
+ Field name
195
180
 
196
181
  Returns
197
182
  -------
198
- list[Any]
199
- A list of results corresponding to the resolved events.
183
+ Callable
184
+ callable function and configuration
200
185
  """
186
+ full_name = f"{type_name}.{field_name}"
187
+ resolver = self._resolvers.get(full_name, self._resolvers.get(f"*.{field_name}"))
188
+ if not resolver:
189
+ raise ValueError(f"No resolver found for '{full_name}'")
190
+ return resolver["func"]
201
191
 
202
- logger.debug(f"Graceful error handling flag {raise_on_error=}")
203
-
204
- # Checks whether the entire batch should be processed at once
205
- if aggregate:
206
- # Process the entire batch
207
- response = resolver(event=self.current_batch_event)
208
-
209
- if not isinstance(response, list):
210
- raise InvalidBatchResponse("The response must be a List when using batch resolvers")
211
-
212
- return response
213
-
214
- # Non aggregated events, so we call this event list x times
215
- # Stop on first exception we encounter
216
- if raise_on_error:
217
- return [
218
- resolver(event=appconfig_event, **appconfig_event.arguments)
219
- for appconfig_event in self.current_batch_event
220
- ]
221
-
222
- # By default, we gracefully append `None` for any records that failed processing
223
- results = []
224
- for idx, event in enumerate(self.current_batch_event):
225
- try:
226
- results.append(resolver(event=event, **event.arguments))
227
- except Exception:
228
- logger.debug(f"Failed to process event number {idx} from field '{event.info.field_name}'")
229
- results.append(None)
230
-
231
- return results
232
-
233
- async def _call_async_batch_resolver(
192
+ def __call__(
234
193
  self,
235
- resolver: Callable,
236
- raise_on_error: bool = False,
237
- aggregate: bool = True,
238
- ) -> list[Any]:
239
- """
240
- Asynchronously call a batch resolver for each event in the current batch.
241
-
242
- Parameters
243
- ----------
244
- resolver: Callable
245
- The asynchronous resolver function.
246
- raise_on_error: bool
247
- A flag indicating whether to raise an error when processing batches
248
- with failed items. Defaults to False, which means errors are handled without raising exceptions.
249
- aggregate: bool
250
- A flag indicating whether the batch items should be processed at once or individually.
251
- If True (default), the batch resolver will process all items in the batch as a single event.
252
- If False, the batch resolver will process each item in the batch individually.
253
-
254
- Returns
255
- -------
256
- list[Any]
257
- A list of results corresponding to the resolved events.
258
- """
259
-
260
- logger.debug(f"Graceful error handling flag {raise_on_error=}")
261
-
262
- # Checks whether the entire batch should be processed at once
263
- if aggregate:
264
- # Process the entire batch
265
- ret = await resolver(event=self.current_batch_event)
266
- if not isinstance(ret, list):
267
- raise InvalidBatchResponse("The response must be a List when using batch resolvers")
268
-
269
- return ret
270
-
271
- response: list = []
272
-
273
- # Prime coroutines
274
- tasks = [resolver(event=e, **e.arguments) for e in self.current_batch_event]
275
-
276
- # Aggregate results or raise at first error
277
- if raise_on_error:
278
- response.extend(await asyncio.gather(*tasks))
279
- return response
280
-
281
- # Aggregate results and exceptions, then filter them out
282
- # Use `None` upon exception for graceful error handling at GraphQL engine level
283
- #
284
- # NOTE: asyncio.gather(return_exceptions=True) catches and includes exceptions in the results
285
- # this will become useful when we support exception handling in AppSync resolver
286
- results = await asyncio.gather(*tasks, return_exceptions=True)
287
- response.extend(None if isinstance(ret, Exception) else ret for ret in results)
288
-
289
- return response
290
-
291
- def _call_batch_resolver(self, event: list[dict], data_model: type[AppSyncResolverEvent]) -> list[Any]:
292
- """Call batch event resolver for sync and async methods
293
-
294
- Parameters
295
- ----------
296
- event : list[dict]
297
- Batch event
298
- data_model : type[AppSyncResolverEvent]
299
- Data_model to decode AppSync event, by default AppSyncResolverEvent or a subclass
300
-
301
- Returns
302
- -------
303
- list[Any]
304
- Results of the resolver execution.
305
-
306
- Raises
307
- ------
308
- InconsistentPayloadError:
309
- When all events in the batch do not have the same fieldName.
310
-
311
- ResolverNotFoundError:
312
- When no resolver is found for the specified type and field.
313
- """
314
- logger.debug("Processing batch resolver event")
315
-
316
- self.current_batch_event = [data_model(e) for e in event]
317
- type_name, field_name = self.current_batch_event[0].type_name, self.current_batch_event[0].field_name
318
-
319
- resolver = self._batch_resolver_registry.find_resolver(type_name, field_name)
320
- async_resolver = self._async_batch_resolver_registry.find_resolver(type_name, field_name)
321
-
322
- if resolver and async_resolver:
323
- warnings.warn(
324
- f"Both synchronous and asynchronous resolvers found for the same event and field."
325
- f"The synchronous resolver takes precedence. Executing: {resolver['func'].__name__}",
326
- stacklevel=2,
327
- category=PowertoolsUserWarning,
328
- )
329
-
330
- if resolver:
331
- logger.debug(f"Found sync resolver. {resolver=}, {field_name=}")
332
- return self._call_sync_batch_resolver(
333
- resolver=resolver["func"],
334
- raise_on_error=resolver["raise_on_error"],
335
- aggregate=resolver["aggregate"],
336
- )
337
-
338
- if async_resolver:
339
- logger.debug(f"Found async resolver. {resolver=}, {field_name=}")
340
- return asyncio.run(
341
- self._call_async_batch_resolver(
342
- resolver=async_resolver["func"],
343
- raise_on_error=async_resolver["raise_on_error"],
344
- aggregate=async_resolver["aggregate"],
345
- ),
346
- )
347
-
348
- raise ResolverNotFoundError(f"No resolver found for '{type_name}.{field_name}'")
194
+ event: dict,
195
+ context: LambdaContext,
196
+ data_model: type[AppSyncResolverEvent] = AppSyncResolverEvent,
197
+ ) -> Any:
198
+ """Implicit lambda handler which internally calls `resolve`"""
199
+ return self.resolve(event, context, data_model)
349
200
 
350
201
  def include_router(self, router: Router) -> None:
351
202
  """Adds all resolvers defined in a router
@@ -355,112 +206,15 @@ class AppSyncResolver(Router):
355
206
  router : Router
356
207
  A router containing a dict of field resolvers
357
208
  """
358
-
359
209
  # Merge app and router context
360
- logger.debug("Merging router and app context")
361
210
  self.context.update(**router.context)
362
-
363
211
  # use pointer to allow context clearance after event is processed e.g., resolve(evt, ctx)
364
212
  router.context = self.context
365
213
 
366
- logger.debug("Merging router resolver registries")
367
- self._resolver_registry.merge(router._resolver_registry)
368
- self._batch_resolver_registry.merge(router._batch_resolver_registry)
369
- self._async_batch_resolver_registry.merge(router._async_batch_resolver_registry)
214
+ self._resolvers.update(router._resolvers)
370
215
 
371
- def resolver(self, type_name: str = "*", field_name: str | None = None) -> Callable:
372
- """Registers direct resolver function for GraphQL type and field name.
373
-
374
- Parameters
375
- ----------
376
- type_name : str, optional
377
- GraphQL type e.g., Query, Mutation, by default "*" meaning any
378
- field_name : Optional[str], optional
379
- GraphQL field e.g., getTodo, createTodo, by default None
380
216
 
381
- Returns
382
- -------
383
- Callable
384
- Registered resolver
385
-
386
- Example
387
- -------
388
-
389
- ```python
390
- from aws_lambda_powertools.event_handler import AppSyncResolver
391
-
392
- from typing import TypedDict
393
-
394
- app = AppSyncResolver()
395
-
396
- class Todo(TypedDict, total=False):
397
- id: str
398
- userId: str
399
- title: str
400
- completed: bool
401
-
402
- # resolve any GraphQL `getTodo` queries
403
- # arguments are injected as function arguments as-is
404
- @app.resolver(type_name="Query", field_name="getTodo")
405
- def get_todo(id: str = "", status: str = "open") -> Todo:
406
- todos: Response = requests.get(f"https://jsonplaceholder.typicode.com/todos/{id}")
407
- todos.raise_for_status()
408
-
409
- return todos.json()
410
-
411
- def lambda_handler(event, context):
412
- return app.resolve(event, context)
413
- ```
414
- """
415
- return self._resolver_registry.register(field_name=field_name, type_name=type_name)
416
-
417
- def batch_resolver(
418
- self,
419
- type_name: str = "*",
420
- field_name: str | None = None,
421
- raise_on_error: bool = False,
422
- aggregate: bool = True,
423
- ) -> Callable:
424
- """Registers batch resolver function for GraphQL type and field name.
425
-
426
- By default, we handle errors gracefully by returning `None`. If you want
427
- to short-circuit and fail the entire batch use `raise_on_error=True`.
428
-
429
- Parameters
430
- ----------
431
- type_name : str, optional
432
- GraphQL type e.g., Query, Mutation, by default "*" meaning any
433
- field_name : Optional[str], optional
434
- GraphQL field e.g., getTodo, createTodo, by default None
435
- raise_on_error : bool, optional
436
- Whether to fail entire batch upon error, or handle errors gracefully (None), by default False
437
- aggregate: bool
438
- A flag indicating whether the batch items should be processed at once or individually.
439
- If True (default), the batch resolver will process all items in the batch as a single event.
440
- If False, the batch resolver will process each item in the batch individually.
441
-
442
- Returns
443
- -------
444
- Callable
445
- Registered resolver
446
- """
447
- return self._batch_resolver_registry.register(
448
- field_name=field_name,
449
- type_name=type_name,
450
- raise_on_error=raise_on_error,
451
- aggregate=aggregate,
452
- )
453
-
454
- def async_batch_resolver(
455
- self,
456
- type_name: str = "*",
457
- field_name: str | None = None,
458
- raise_on_error: bool = False,
459
- aggregate: bool = True,
460
- ) -> Callable:
461
- return self._async_batch_resolver_registry.register(
462
- field_name=field_name,
463
- type_name=type_name,
464
- raise_on_error=raise_on_error,
465
- aggregate=aggregate,
466
- )
217
+ class Router(BaseRouter):
218
+ def __init__(self):
219
+ super().__init__()
220
+ self.context = {} # early init as customers might add context before event resolution