aws-lambda-powertools 3.0.1.dev2__py3-none-any.whl → 3.0.2.dev1__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 (39) hide show
  1. aws_lambda_powertools/event_handler/api_gateway.py +227 -54
  2. aws_lambda_powertools/event_handler/appsync.py +343 -97
  3. aws_lambda_powertools/event_handler/bedrock_agent.py +121 -0
  4. aws_lambda_powertools/event_handler/exceptions.py +1 -1
  5. aws_lambda_powertools/event_handler/graphql_appsync/__init__.py +0 -0
  6. aws_lambda_powertools/event_handler/graphql_appsync/_registry.py +78 -0
  7. aws_lambda_powertools/event_handler/graphql_appsync/base.py +160 -0
  8. aws_lambda_powertools/event_handler/graphql_appsync/exceptions.py +10 -0
  9. aws_lambda_powertools/event_handler/graphql_appsync/router.py +62 -0
  10. aws_lambda_powertools/event_handler/middlewares/openapi_validation.py +1 -1
  11. aws_lambda_powertools/event_handler/openapi/encoders.py +94 -70
  12. aws_lambda_powertools/event_handler/openapi/exceptions.py +12 -0
  13. aws_lambda_powertools/event_handler/openapi/models.py +38 -5
  14. aws_lambda_powertools/event_handler/openapi/swagger_ui/html.py +1 -4
  15. aws_lambda_powertools/event_handler/util.py +55 -2
  16. aws_lambda_powertools/logging/utils.py +5 -5
  17. aws_lambda_powertools/metrics/provider/cloudwatch_emf/metric_properties.py +1 -0
  18. aws_lambda_powertools/shared/cookies.py +2 -2
  19. aws_lambda_powertools/shared/dynamodb_deserializer.py +13 -0
  20. aws_lambda_powertools/shared/version.py +1 -1
  21. aws_lambda_powertools/tracing/tracer.py +4 -2
  22. aws_lambda_powertools/utilities/batch/base.py +13 -4
  23. aws_lambda_powertools/utilities/data_classes/cognito_user_pool_event.py +260 -1
  24. aws_lambda_powertools/utilities/data_classes/kafka_event.py +2 -2
  25. aws_lambda_powertools/utilities/data_classes/vpc_lattice.py +7 -0
  26. aws_lambda_powertools/utilities/data_masking/base.py +4 -4
  27. aws_lambda_powertools/utilities/feature_flags/appconfig.py +21 -2
  28. aws_lambda_powertools/utilities/idempotency/idempotency.py +22 -2
  29. aws_lambda_powertools/utilities/idempotency/persistence/base.py +4 -1
  30. aws_lambda_powertools/utilities/parameters/secrets.py +1 -1
  31. aws_lambda_powertools/utilities/parser/models/__init__.py +6 -0
  32. aws_lambda_powertools/utilities/parser/models/apigw.py +12 -1
  33. aws_lambda_powertools/utilities/parser/models/apigwv2.py +7 -1
  34. aws_lambda_powertools/utilities/validation/base.py +27 -2
  35. aws_lambda_powertools/utilities/validation/validator.py +49 -4
  36. {aws_lambda_powertools-3.0.1.dev2.dist-info → aws_lambda_powertools-3.0.2.dev1.dist-info}/METADATA +12 -2
  37. {aws_lambda_powertools-3.0.1.dev2.dist-info → aws_lambda_powertools-3.0.2.dev1.dist-info}/RECORD +39 -34
  38. {aws_lambda_powertools-3.0.1.dev2.dist-info → aws_lambda_powertools-3.0.2.dev1.dist-info}/LICENSE +0 -0
  39. {aws_lambda_powertools-3.0.1.dev2.dist-info → aws_lambda_powertools-3.0.2.dev1.dist-info}/WHEEL +0 -0
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  from typing import TYPE_CHECKING, Any, Callable
4
5
 
5
6
  from typing_extensions import override
@@ -10,10 +11,12 @@ from aws_lambda_powertools.event_handler.api_gateway import (
10
11
  ProxyEventType,
11
12
  ResponseBuilder,
12
13
  )
14
+ from aws_lambda_powertools.event_handler.openapi.constants import DEFAULT_API_VERSION, DEFAULT_OPENAPI_VERSION
13
15
 
14
16
  if TYPE_CHECKING:
15
17
  from re import Match
16
18
 
19
+ from aws_lambda_powertools.event_handler.openapi.models import Contact, License, SecurityScheme, Server, Tag
17
20
  from aws_lambda_powertools.event_handler.openapi.types import OpenAPIResponse
18
21
  from aws_lambda_powertools.utilities.data_classes import BedrockAgentEvent
19
22
 
@@ -107,6 +110,8 @@ class BedrockAgentResolver(ApiGatewayResolver):
107
110
  include_in_schema: bool = True,
108
111
  middlewares: list[Callable[..., Any]] | None = None,
109
112
  ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
113
+
114
+ openapi_extensions = None
110
115
  security = None
111
116
 
112
117
  return super().get(
@@ -122,6 +127,7 @@ class BedrockAgentResolver(ApiGatewayResolver):
122
127
  operation_id,
123
128
  include_in_schema,
124
129
  security,
130
+ openapi_extensions,
125
131
  middlewares,
126
132
  )
127
133
 
@@ -142,6 +148,7 @@ class BedrockAgentResolver(ApiGatewayResolver):
142
148
  include_in_schema: bool = True,
143
149
  middlewares: list[Callable[..., Any]] | None = None,
144
150
  ):
151
+ openapi_extensions = None
145
152
  security = None
146
153
 
147
154
  return super().post(
@@ -157,6 +164,7 @@ class BedrockAgentResolver(ApiGatewayResolver):
157
164
  operation_id,
158
165
  include_in_schema,
159
166
  security,
167
+ openapi_extensions,
160
168
  middlewares,
161
169
  )
162
170
 
@@ -177,6 +185,7 @@ class BedrockAgentResolver(ApiGatewayResolver):
177
185
  include_in_schema: bool = True,
178
186
  middlewares: list[Callable[..., Any]] | None = None,
179
187
  ):
188
+ openapi_extensions = None
180
189
  security = None
181
190
 
182
191
  return super().put(
@@ -192,6 +201,7 @@ class BedrockAgentResolver(ApiGatewayResolver):
192
201
  operation_id,
193
202
  include_in_schema,
194
203
  security,
204
+ openapi_extensions,
195
205
  middlewares,
196
206
  )
197
207
 
@@ -212,6 +222,7 @@ class BedrockAgentResolver(ApiGatewayResolver):
212
222
  include_in_schema: bool = True,
213
223
  middlewares: list[Callable] | None = None,
214
224
  ):
225
+ openapi_extensions = None
215
226
  security = None
216
227
 
217
228
  return super().patch(
@@ -227,6 +238,7 @@ class BedrockAgentResolver(ApiGatewayResolver):
227
238
  operation_id,
228
239
  include_in_schema,
229
240
  security,
241
+ openapi_extensions,
230
242
  middlewares,
231
243
  )
232
244
 
@@ -247,6 +259,7 @@ class BedrockAgentResolver(ApiGatewayResolver):
247
259
  include_in_schema: bool = True,
248
260
  middlewares: list[Callable[..., Any]] | None = None,
249
261
  ):
262
+ openapi_extensions = None
250
263
  security = None
251
264
 
252
265
  return super().delete(
@@ -262,6 +275,7 @@ class BedrockAgentResolver(ApiGatewayResolver):
262
275
  operation_id,
263
276
  include_in_schema,
264
277
  security,
278
+ openapi_extensions,
265
279
  middlewares,
266
280
  )
267
281
 
@@ -273,3 +287,110 @@ class BedrockAgentResolver(ApiGatewayResolver):
273
287
  if match.groupdict() and self.current_event.parameters:
274
288
  parameters = {parameter["name"]: parameter["value"] for parameter in self.current_event.parameters}
275
289
  return parameters
290
+
291
+ @override
292
+ def get_openapi_json_schema( # type: ignore[override]
293
+ self,
294
+ *,
295
+ title: str = "Powertools API",
296
+ version: str = DEFAULT_API_VERSION,
297
+ openapi_version: str = DEFAULT_OPENAPI_VERSION,
298
+ summary: str | None = None,
299
+ description: str | None = None,
300
+ tags: list[Tag | str] | None = None,
301
+ servers: list[Server] | None = None,
302
+ terms_of_service: str | None = None,
303
+ contact: Contact | None = None,
304
+ license_info: License | None = None,
305
+ security_schemes: dict[str, SecurityScheme] | None = None,
306
+ security: list[dict[str, list[str]]] | None = None,
307
+ ) -> str:
308
+ """
309
+ Returns the OpenAPI schema as a JSON serializable dict.
310
+ Since Bedrock Agents only support OpenAPI 3.0.0, we convert OpenAPI 3.1.0 schemas
311
+ and enforce 3.0.0 compatibility for seamless integration.
312
+
313
+ Parameters
314
+ ----------
315
+ title: str
316
+ The title of the application.
317
+ version: str
318
+ The version of the OpenAPI document (which is distinct from the OpenAPI Specification version or the API
319
+ openapi_version: str, default = "3.0.0"
320
+ The version of the OpenAPI Specification (which the document uses).
321
+ summary: str, optional
322
+ A short summary of what the application does.
323
+ description: str, optional
324
+ A verbose explanation of the application behavior.
325
+ tags: list[Tag, str], optional
326
+ A list of tags used by the specification with additional metadata.
327
+ servers: list[Server], optional
328
+ An array of Server Objects, which provide connectivity information to a target server.
329
+ terms_of_service: str, optional
330
+ A URL to the Terms of Service for the API. MUST be in the format of a URL.
331
+ contact: Contact, optional
332
+ The contact information for the exposed API.
333
+ license_info: License, optional
334
+ The license information for the exposed API.
335
+ security_schemes: dict[str, SecurityScheme]], optional
336
+ A declaration of the security schemes available to be used in the specification.
337
+ security: list[dict[str, list[str]]], optional
338
+ A declaration of which security mechanisms are applied globally across the API.
339
+
340
+ Returns
341
+ -------
342
+ str
343
+ The OpenAPI schema as a JSON serializable dict.
344
+ """
345
+ from aws_lambda_powertools.event_handler.openapi.compat import model_json
346
+
347
+ openapi_extensions = None
348
+
349
+ schema = super().get_openapi_schema(
350
+ title=title,
351
+ version=version,
352
+ openapi_version=openapi_version,
353
+ summary=summary,
354
+ description=description,
355
+ tags=tags,
356
+ servers=servers,
357
+ terms_of_service=terms_of_service,
358
+ contact=contact,
359
+ license_info=license_info,
360
+ security_schemes=security_schemes,
361
+ security=security,
362
+ openapi_extensions=openapi_extensions,
363
+ )
364
+ schema.openapi = "3.0.3"
365
+
366
+ # Transform OpenAPI 3.1 into 3.0
367
+ def inner(yaml_dict):
368
+ if isinstance(yaml_dict, dict):
369
+ if "anyOf" in yaml_dict and isinstance((anyOf := yaml_dict["anyOf"]), list):
370
+ for i, item in enumerate(anyOf):
371
+ if isinstance(item, dict) and item.get("type") == "null":
372
+ anyOf.pop(i)
373
+ yaml_dict["nullable"] = True
374
+ if "examples" in yaml_dict:
375
+ examples = yaml_dict["examples"]
376
+ del yaml_dict["examples"]
377
+ if isinstance(examples, list) and len(examples):
378
+ yaml_dict["example"] = examples[0]
379
+ for value in yaml_dict.values():
380
+ inner(value)
381
+ elif isinstance(yaml_dict, list):
382
+ for item in yaml_dict:
383
+ inner(item)
384
+
385
+ model = json.loads(
386
+ model_json(
387
+ schema,
388
+ by_alias=True,
389
+ exclude_none=True,
390
+ indent=2,
391
+ ),
392
+ )
393
+
394
+ inner(model)
395
+
396
+ return json.dumps(model)
@@ -39,7 +39,7 @@ class NotFoundError(ServiceError):
39
39
 
40
40
 
41
41
  class InternalServerError(ServiceError):
42
- """API Gateway and ALB Not Found Internal Server Error (500)"""
42
+ """API Gateway and ALB Internal Server Error (500)"""
43
43
 
44
44
  def __init__(self, message: str):
45
45
  super().__init__(HTTPStatus.INTERNAL_SERVER_ERROR, message)
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Any, Callable
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class ResolverRegistry:
10
+ def __init__(self):
11
+ self.resolvers: dict[str, dict[str, Any]] = {}
12
+
13
+ def register(
14
+ self,
15
+ type_name: str = "*",
16
+ field_name: str | None = None,
17
+ raise_on_error: bool = False,
18
+ aggregate: bool = True,
19
+ ) -> Callable:
20
+ """Registers the resolver for field_name
21
+
22
+ Parameters
23
+ ----------
24
+ type_name : str
25
+ Type name
26
+ field_name : str
27
+ Field name
28
+ raise_on_error: bool
29
+ A flag indicating whether to raise an error when processing batches
30
+ with failed items. Defaults to False, which means errors are handled without raising exceptions.
31
+ aggregate: bool
32
+ A flag indicating whether the batch items should be processed at once or individually.
33
+ If True (default), the batch resolver will process all items in the batch as a single event.
34
+ If False, the batch resolver will process each item in the batch individually.
35
+
36
+ Return
37
+ ----------
38
+ Callable
39
+ A Callable
40
+ """
41
+
42
+ def _register(func) -> Callable:
43
+ logger.debug(f"Adding resolver `{func.__name__}` for field `{type_name}.{field_name}`")
44
+ self.resolvers[f"{type_name}.{field_name}"] = {
45
+ "func": func,
46
+ "raise_on_error": raise_on_error,
47
+ "aggregate": aggregate,
48
+ }
49
+ return func
50
+
51
+ return _register
52
+
53
+ def find_resolver(self, type_name: str, field_name: str) -> dict | None:
54
+ """Find resolver based on type_name and field_name
55
+
56
+ Parameters
57
+ ----------
58
+ type_name : str
59
+ Type name
60
+ field_name : str
61
+ Field name
62
+ Return
63
+ ----------
64
+ Optional[Dict]
65
+ A dictionary with the resolver and if raise exception on error
66
+ """
67
+ logger.debug(f"Looking for resolver for type={type_name}, field={field_name}.")
68
+ return self.resolvers.get(f"{type_name}.{field_name}", self.resolvers.get(f"*.{field_name}"))
69
+
70
+ def merge(self, other_registry: ResolverRegistry):
71
+ """Update current registry with incoming registry
72
+
73
+ Parameters
74
+ ----------
75
+ other_registry : ResolverRegistry
76
+ Registry to merge from
77
+ """
78
+ self.resolvers.update(**other_registry.resolvers)
@@ -0,0 +1,160 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Callable
5
+
6
+
7
+ class BaseRouter(ABC):
8
+ """Abstract base class for Router (resolvers)"""
9
+
10
+ @abstractmethod
11
+ def resolver(self, type_name: str = "*", field_name: str | None = None) -> Callable:
12
+ """
13
+ Retrieve a resolver function for a specific type and field.
14
+
15
+ Parameters
16
+ -----------
17
+ type_name: str
18
+ The name of the type.
19
+ field_name: str, optional
20
+ The name of the field (default is None).
21
+
22
+ Examples
23
+ --------
24
+ ```python
25
+ from typing import Optional
26
+
27
+ from aws_lambda_powertools.event_handler import AppSyncResolver
28
+ from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
29
+ from aws_lambda_powertools.utilities.typing import LambdaContext
30
+
31
+ app = AppSyncResolver()
32
+
33
+ @app.resolver(type_name="Query", field_name="getPost")
34
+ def related_posts(event: AppSyncResolverEvent) -> Optional[list]:
35
+ return {"success": "ok"}
36
+
37
+ def lambda_handler(event, context: LambdaContext) -> dict:
38
+ return app.resolve(event, context)
39
+ ```
40
+
41
+ Returns
42
+ -------
43
+ Callable
44
+ The resolver function.
45
+ """
46
+ raise NotImplementedError
47
+
48
+ @abstractmethod
49
+ def batch_resolver(
50
+ self,
51
+ type_name: str = "*",
52
+ field_name: str | None = None,
53
+ raise_on_error: bool = False,
54
+ aggregate: bool = True,
55
+ ) -> Callable:
56
+ """
57
+ Retrieve a batch resolver function for a specific type and field.
58
+
59
+ Parameters
60
+ -----------
61
+ type_name: str
62
+ The name of the type.
63
+ field_name: str, optional
64
+ The name of the field (default is None).
65
+ raise_on_error: bool
66
+ A flag indicating whether to raise an error when processing batches
67
+ with failed items. Defaults to False, which means errors are handled without raising exceptions.
68
+ aggregate: bool
69
+ A flag indicating whether the batch items should be processed at once or individually.
70
+ If True (default), the batch resolver will process all items in the batch as a single event.
71
+ If False, the batch resolver will process each item in the batch individually.
72
+
73
+ Examples
74
+ --------
75
+ ```python
76
+ from typing import Optional
77
+
78
+ from aws_lambda_powertools.event_handler import AppSyncResolver
79
+ from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
80
+ from aws_lambda_powertools.utilities.typing import LambdaContext
81
+
82
+ app = AppSyncResolver()
83
+
84
+ @app.batch_resolver(type_name="Query", field_name="getPost")
85
+ def related_posts(event: AppSyncResolverEvent, id) -> Optional[list]:
86
+ return {"post_id": id}
87
+
88
+ def lambda_handler(event, context: LambdaContext) -> dict:
89
+ return app.resolve(event, context)
90
+ ```
91
+
92
+ Returns
93
+ -------
94
+ Callable
95
+ The batch resolver function.
96
+ """
97
+ raise NotImplementedError
98
+
99
+ @abstractmethod
100
+ def async_batch_resolver(
101
+ self,
102
+ type_name: str = "*",
103
+ field_name: str | None = None,
104
+ raise_on_error: bool = False,
105
+ aggregate: bool = True,
106
+ ) -> Callable:
107
+ """
108
+ Retrieve a batch resolver function for a specific type and field and runs async.
109
+
110
+ Parameters
111
+ -----------
112
+ type_name: str
113
+ The name of the type.
114
+ field_name: str, optional
115
+ The name of the field (default is None).
116
+ raise_on_error: bool
117
+ A flag indicating whether to raise an error when processing batches
118
+ with failed items. Defaults to False, which means errors are handled without raising exceptions.
119
+ aggregate: bool
120
+ A flag indicating whether the batch items should be processed at once or individually.
121
+ If True (default), the batch resolver will process all items in the batch as a single event.
122
+ If False, the batch resolver will process each item in the batch individually.
123
+
124
+ Examples
125
+ --------
126
+ ```python
127
+ from typing import Optional
128
+
129
+ from aws_lambda_powertools.event_handler import AppSyncResolver
130
+ from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
131
+ from aws_lambda_powertools.utilities.typing import LambdaContext
132
+
133
+ app = AppSyncResolver()
134
+
135
+ @app.async_batch_resolver(type_name="Query", field_name="getPost")
136
+ async def related_posts(event: AppSyncResolverEvent, id) -> Optional[list]:
137
+ return {"post_id": id}
138
+
139
+ def lambda_handler(event, context: LambdaContext) -> dict:
140
+ return app.resolve(event, context)
141
+ ```
142
+
143
+ Returns
144
+ -------
145
+ Callable
146
+ The batch resolver function.
147
+ """
148
+ raise NotImplementedError
149
+
150
+ @abstractmethod
151
+ def append_context(self, **additional_context) -> None:
152
+ """
153
+ Appends context information available under any route.
154
+
155
+ Parameters
156
+ -----------
157
+ **additional_context: dict
158
+ Additional context key-value pairs to append.
159
+ """
160
+ raise NotImplementedError
@@ -0,0 +1,10 @@
1
+ class ResolverNotFoundError(Exception):
2
+ """
3
+ When a resolver is not found during a lookup.
4
+ """
5
+
6
+
7
+ class InvalidBatchResponse(Exception):
8
+ """
9
+ When a batch response something different from a List
10
+ """
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Callable
4
+
5
+ from aws_lambda_powertools.event_handler.graphql_appsync._registry import ResolverRegistry
6
+ from aws_lambda_powertools.event_handler.graphql_appsync.base import BaseRouter
7
+
8
+ if TYPE_CHECKING:
9
+ from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import AppSyncResolverEvent
10
+ from aws_lambda_powertools.utilities.typing.lambda_context import LambdaContext
11
+
12
+
13
+ class Router(BaseRouter):
14
+ context: dict
15
+ current_batch_event: list[AppSyncResolverEvent] = []
16
+ current_event: AppSyncResolverEvent | None = None
17
+ lambda_context: LambdaContext | None = None
18
+
19
+ def __init__(self):
20
+ self.context = {} # early init as customers might add context before event resolution
21
+ self._resolver_registry = ResolverRegistry()
22
+ self._batch_resolver_registry = ResolverRegistry()
23
+ self._async_batch_resolver_registry = ResolverRegistry()
24
+
25
+ def resolver(self, type_name: str = "*", field_name: str | None = None) -> Callable:
26
+ return self._resolver_registry.register(field_name=field_name, type_name=type_name)
27
+
28
+ def batch_resolver(
29
+ self,
30
+ type_name: str = "*",
31
+ field_name: str | None = None,
32
+ raise_on_error: bool = False,
33
+ aggregate: bool = True,
34
+ ) -> Callable:
35
+ return self._batch_resolver_registry.register(
36
+ field_name=field_name,
37
+ type_name=type_name,
38
+ raise_on_error=raise_on_error,
39
+ aggregate=aggregate,
40
+ )
41
+
42
+ def async_batch_resolver(
43
+ self,
44
+ type_name: str = "*",
45
+ field_name: str | None = None,
46
+ raise_on_error: bool = False,
47
+ aggregate: bool = True,
48
+ ) -> Callable:
49
+ return self._async_batch_resolver_registry.register(
50
+ field_name=field_name,
51
+ type_name=type_name,
52
+ raise_on_error=raise_on_error,
53
+ aggregate=aggregate,
54
+ )
55
+
56
+ def append_context(self, **additional_context):
57
+ """Append key=value data as routing context"""
58
+ self.context.update(**additional_context)
59
+
60
+ def clear_context(self):
61
+ """Resets routing context"""
62
+ self.context.clear()
@@ -232,7 +232,7 @@ class OpenAPIValidationMiddleware(BaseMiddlewareHandler):
232
232
  for k, v in res.items()
233
233
  }
234
234
  elif dataclasses.is_dataclass(res):
235
- return dataclasses.asdict(res)
235
+ return dataclasses.asdict(res) # type: ignore[call-overload]
236
236
  return res
237
237
 
238
238
  def _get_body(self, app: EventHandlerInstance) -> dict[str, Any]: