aws-lambda-powertools 3.0.0.dev15__py3-none-any.whl → 3.0.1a1__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/api_gateway.py +227 -54
- aws_lambda_powertools/event_handler/appsync.py +343 -97
- aws_lambda_powertools/event_handler/bedrock_agent.py +121 -0
- aws_lambda_powertools/event_handler/exceptions.py +1 -1
- aws_lambda_powertools/event_handler/graphql_appsync/__init__.py +0 -0
- aws_lambda_powertools/event_handler/graphql_appsync/_registry.py +78 -0
- aws_lambda_powertools/event_handler/graphql_appsync/base.py +160 -0
- aws_lambda_powertools/event_handler/graphql_appsync/exceptions.py +10 -0
- aws_lambda_powertools/event_handler/graphql_appsync/router.py +62 -0
- aws_lambda_powertools/event_handler/middlewares/openapi_validation.py +1 -1
- aws_lambda_powertools/event_handler/openapi/encoders.py +94 -70
- aws_lambda_powertools/event_handler/openapi/exceptions.py +12 -0
- aws_lambda_powertools/event_handler/openapi/models.py +38 -5
- aws_lambda_powertools/event_handler/openapi/swagger_ui/html.py +1 -4
- aws_lambda_powertools/event_handler/util.py +55 -2
- aws_lambda_powertools/logging/utils.py +5 -5
- aws_lambda_powertools/metrics/provider/cloudwatch_emf/metric_properties.py +1 -0
- aws_lambda_powertools/shared/cookies.py +2 -2
- aws_lambda_powertools/shared/dynamodb_deserializer.py +13 -0
- aws_lambda_powertools/shared/version.py +1 -1
- aws_lambda_powertools/tracing/tracer.py +4 -2
- aws_lambda_powertools/utilities/batch/base.py +13 -4
- aws_lambda_powertools/utilities/data_classes/__init__.py +4 -0
- aws_lambda_powertools/utilities/data_classes/code_deploy_lifecycle_hook_event.py +13 -0
- aws_lambda_powertools/utilities/data_classes/cognito_user_pool_event.py +260 -1
- aws_lambda_powertools/utilities/data_classes/kafka_event.py +2 -2
- aws_lambda_powertools/utilities/data_classes/vpc_lattice.py +7 -0
- aws_lambda_powertools/utilities/data_masking/base.py +4 -4
- aws_lambda_powertools/utilities/feature_flags/appconfig.py +21 -2
- aws_lambda_powertools/utilities/idempotency/idempotency.py +22 -2
- aws_lambda_powertools/utilities/idempotency/persistence/base.py +4 -1
- aws_lambda_powertools/utilities/parameters/secrets.py +1 -1
- aws_lambda_powertools/utilities/parser/models/__init__.py +6 -0
- aws_lambda_powertools/utilities/parser/models/apigw.py +12 -1
- aws_lambda_powertools/utilities/parser/models/apigwv2.py +7 -1
- aws_lambda_powertools/utilities/validation/base.py +27 -2
- aws_lambda_powertools/utilities/validation/validator.py +49 -4
- {aws_lambda_powertools-3.0.0.dev15.dist-info → aws_lambda_powertools-3.0.1a1.dist-info}/METADATA +12 -2
- {aws_lambda_powertools-3.0.0.dev15.dist-info → aws_lambda_powertools-3.0.1a1.dist-info}/RECORD +41 -35
- {aws_lambda_powertools-3.0.0.dev15.dist-info → aws_lambda_powertools-3.0.1a1.dist-info}/LICENSE +0 -0
- {aws_lambda_powertools-3.0.0.dev15.dist-info → aws_lambda_powertools-3.0.1a1.dist-info}/WHEEL +0 -0
|
@@ -14,10 +14,12 @@ from http import HTTPStatus
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import TYPE_CHECKING, Any, Callable, Generic, Literal, Mapping, Match, Pattern, Sequence, TypeVar, cast
|
|
16
16
|
|
|
17
|
+
from typing_extensions import override
|
|
18
|
+
|
|
17
19
|
from aws_lambda_powertools.event_handler import content_types
|
|
18
20
|
from aws_lambda_powertools.event_handler.exceptions import NotFoundError, ServiceError
|
|
19
21
|
from aws_lambda_powertools.event_handler.openapi.constants import DEFAULT_API_VERSION, DEFAULT_OPENAPI_VERSION
|
|
20
|
-
from aws_lambda_powertools.event_handler.openapi.exceptions import RequestValidationError
|
|
22
|
+
from aws_lambda_powertools.event_handler.openapi.exceptions import RequestValidationError, SchemaValidationError
|
|
21
23
|
from aws_lambda_powertools.event_handler.openapi.types import (
|
|
22
24
|
COMPONENT_REF_PREFIX,
|
|
23
25
|
METHODS_WITH_BODY,
|
|
@@ -27,7 +29,13 @@ from aws_lambda_powertools.event_handler.openapi.types import (
|
|
|
27
29
|
validation_error_definition,
|
|
28
30
|
validation_error_response_definition,
|
|
29
31
|
)
|
|
30
|
-
from aws_lambda_powertools.event_handler.util import
|
|
32
|
+
from aws_lambda_powertools.event_handler.util import (
|
|
33
|
+
_FrozenDict,
|
|
34
|
+
_FrozenListDict,
|
|
35
|
+
_validate_openapi_security_parameters,
|
|
36
|
+
extract_origin_header,
|
|
37
|
+
)
|
|
38
|
+
from aws_lambda_powertools.shared.cookies import Cookie
|
|
31
39
|
from aws_lambda_powertools.shared.functions import powertools_dev_is_set
|
|
32
40
|
from aws_lambda_powertools.shared.json_encoder import Encoder
|
|
33
41
|
from aws_lambda_powertools.utilities.data_classes import (
|
|
@@ -96,20 +104,22 @@ class CORSConfig:
|
|
|
96
104
|
Examples
|
|
97
105
|
--------
|
|
98
106
|
|
|
99
|
-
Simple
|
|
107
|
+
Simple CORS example using the default permissive CORS, note that this should only be used during early prototyping.
|
|
100
108
|
|
|
101
109
|
```python
|
|
102
|
-
from aws_lambda_powertools.event_handler import
|
|
110
|
+
from aws_lambda_powertools.event_handler.api_gateway import (
|
|
111
|
+
APIGatewayRestResolver, CORSConfig
|
|
112
|
+
)
|
|
103
113
|
|
|
104
|
-
app = APIGatewayRestResolver()
|
|
114
|
+
app = APIGatewayRestResolver(cors=CORSConfig())
|
|
105
115
|
|
|
106
|
-
@app.get("/my/path"
|
|
116
|
+
@app.get("/my/path")
|
|
107
117
|
def with_cors():
|
|
108
118
|
return {"message": "Foo"}
|
|
109
119
|
```
|
|
110
120
|
|
|
111
121
|
Using a custom CORSConfig where `with_cors` used the custom provided CORSConfig and `without_cors`
|
|
112
|
-
do not include any
|
|
122
|
+
do not include any CORS headers.
|
|
113
123
|
|
|
114
124
|
```python
|
|
115
125
|
from aws_lambda_powertools.event_handler.api_gateway import (
|
|
@@ -166,9 +176,12 @@ class CORSConfig:
|
|
|
166
176
|
allow_credentials: bool
|
|
167
177
|
A boolean value that sets the value of `Access-Control-Allow-Credentials`
|
|
168
178
|
"""
|
|
179
|
+
|
|
169
180
|
self._allowed_origins = [allow_origin]
|
|
181
|
+
|
|
170
182
|
if extra_origins:
|
|
171
183
|
self._allowed_origins.extend(extra_origins)
|
|
184
|
+
|
|
172
185
|
self.allow_headers = set(self._REQUIRED_HEADERS + (allow_headers or []))
|
|
173
186
|
self.expose_headers = expose_headers or []
|
|
174
187
|
self.max_age = max_age
|
|
@@ -189,17 +202,42 @@ class CORSConfig:
|
|
|
189
202
|
# The origin matched an allowed origin, so return the CORS headers
|
|
190
203
|
headers = {
|
|
191
204
|
"Access-Control-Allow-Origin": origin,
|
|
192
|
-
"Access-Control-Allow-Headers":
|
|
205
|
+
"Access-Control-Allow-Headers": CORSConfig.build_allow_methods(self.allow_headers),
|
|
193
206
|
}
|
|
194
207
|
|
|
195
208
|
if self.expose_headers:
|
|
196
209
|
headers["Access-Control-Expose-Headers"] = ",".join(self.expose_headers)
|
|
197
210
|
if self.max_age is not None:
|
|
198
211
|
headers["Access-Control-Max-Age"] = str(self.max_age)
|
|
199
|
-
if self.allow_credentials is True:
|
|
212
|
+
if origin != "*" and self.allow_credentials is True:
|
|
200
213
|
headers["Access-Control-Allow-Credentials"] = "true"
|
|
201
214
|
return headers
|
|
202
215
|
|
|
216
|
+
def allowed_origin(self, extracted_origin: str) -> str | None:
|
|
217
|
+
if extracted_origin in self._allowed_origins:
|
|
218
|
+
return extracted_origin
|
|
219
|
+
if extracted_origin is not None and "*" in self._allowed_origins:
|
|
220
|
+
return "*"
|
|
221
|
+
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def build_allow_methods(methods: set[str]) -> str:
|
|
226
|
+
"""Build sorted comma delimited methods for Access-Control-Allow-Methods header
|
|
227
|
+
|
|
228
|
+
Parameters
|
|
229
|
+
----------
|
|
230
|
+
methods : set[str]
|
|
231
|
+
Set of HTTP Methods
|
|
232
|
+
|
|
233
|
+
Returns
|
|
234
|
+
-------
|
|
235
|
+
set[str]
|
|
236
|
+
Formatted string with all HTTP Methods allowed for CORS e.g., `GET, OPTIONS`
|
|
237
|
+
|
|
238
|
+
"""
|
|
239
|
+
return ",".join(sorted(methods))
|
|
240
|
+
|
|
203
241
|
|
|
204
242
|
class Response(Generic[ResponseT]):
|
|
205
243
|
"""Response data class that provides greater control over what is returned from the proxy event"""
|
|
@@ -260,16 +298,17 @@ class Route:
|
|
|
260
298
|
func: Callable,
|
|
261
299
|
cors: bool,
|
|
262
300
|
compress: bool,
|
|
263
|
-
cache_control: str | None,
|
|
264
|
-
summary: str | None,
|
|
265
|
-
description: str | None,
|
|
266
|
-
responses: dict[int, OpenAPIResponse] | None,
|
|
267
|
-
response_description: str | None,
|
|
268
|
-
tags: list[str] | None,
|
|
269
|
-
operation_id: str | None,
|
|
270
|
-
include_in_schema: bool,
|
|
271
|
-
security: list[dict[str, list[str]]] | None,
|
|
272
|
-
|
|
301
|
+
cache_control: str | None = None,
|
|
302
|
+
summary: str | None = None,
|
|
303
|
+
description: str | None = None,
|
|
304
|
+
responses: dict[int, OpenAPIResponse] | None = None,
|
|
305
|
+
response_description: str | None = None,
|
|
306
|
+
tags: list[str] | None = None,
|
|
307
|
+
operation_id: str | None = None,
|
|
308
|
+
include_in_schema: bool = True,
|
|
309
|
+
security: list[dict[str, list[str]]] | None = None,
|
|
310
|
+
openapi_extensions: dict[str, Any] | None = None,
|
|
311
|
+
middlewares: list[Callable[..., Response]] | None = None,
|
|
273
312
|
):
|
|
274
313
|
"""
|
|
275
314
|
|
|
@@ -306,6 +345,8 @@ class Route:
|
|
|
306
345
|
Whether or not to include this route in the OpenAPI schema
|
|
307
346
|
security: list[dict[str, list[str]]], optional
|
|
308
347
|
The OpenAPI security for this route
|
|
348
|
+
openapi_extensions: dict[str, Any], optional
|
|
349
|
+
Additional OpenAPI extensions as a dictionary.
|
|
309
350
|
middlewares: list[Callable[..., Response]] | None
|
|
310
351
|
The list of route middlewares to be called in order.
|
|
311
352
|
"""
|
|
@@ -329,6 +370,7 @@ class Route:
|
|
|
329
370
|
self.tags = tags or []
|
|
330
371
|
self.include_in_schema = include_in_schema
|
|
331
372
|
self.security = security
|
|
373
|
+
self.openapi_extensions = openapi_extensions
|
|
332
374
|
self.middlewares = middlewares or []
|
|
333
375
|
self.operation_id = operation_id or self._generate_operation_id()
|
|
334
376
|
|
|
@@ -480,6 +522,10 @@ class Route:
|
|
|
480
522
|
if self.security:
|
|
481
523
|
operation["security"] = self.security
|
|
482
524
|
|
|
525
|
+
# Add OpenAPI extensions if present
|
|
526
|
+
if self.openapi_extensions:
|
|
527
|
+
operation.update(self.openapi_extensions)
|
|
528
|
+
|
|
483
529
|
# Add the parameters to the OpenAPI operation
|
|
484
530
|
if parameters:
|
|
485
531
|
all_parameters = {(param["in"], param["name"]): param for param in parameters}
|
|
@@ -762,7 +808,10 @@ class ResponseBuilder(Generic[ResponseEventT]):
|
|
|
762
808
|
def _add_cors(self, event: ResponseEventT, cors: CORSConfig):
|
|
763
809
|
"""Update headers to include the configured Access-Control headers"""
|
|
764
810
|
extracted_origin_header = extract_origin_header(event.resolved_headers_field)
|
|
765
|
-
|
|
811
|
+
|
|
812
|
+
origin = cors.allowed_origin(extracted_origin_header)
|
|
813
|
+
if origin is not None:
|
|
814
|
+
self.response.headers.update(cors.to_dict(origin))
|
|
766
815
|
|
|
767
816
|
def _add_cache_control(self, cache_control: str):
|
|
768
817
|
"""Set the specified cache control headers for 200 http responses. For non-200 `no-cache` is used."""
|
|
@@ -873,6 +922,7 @@ class BaseRouter(ABC):
|
|
|
873
922
|
operation_id: str | None = None,
|
|
874
923
|
include_in_schema: bool = True,
|
|
875
924
|
security: list[dict[str, list[str]]] | None = None,
|
|
925
|
+
openapi_extensions: dict[str, Any] | None = None,
|
|
876
926
|
middlewares: list[Callable[..., Any]] | None = None,
|
|
877
927
|
):
|
|
878
928
|
raise NotImplementedError()
|
|
@@ -932,6 +982,7 @@ class BaseRouter(ABC):
|
|
|
932
982
|
operation_id: str | None = None,
|
|
933
983
|
include_in_schema: bool = True,
|
|
934
984
|
security: list[dict[str, list[str]]] | None = None,
|
|
985
|
+
openapi_extensions: dict[str, Any] | None = None,
|
|
935
986
|
middlewares: list[Callable[..., Any]] | None = None,
|
|
936
987
|
):
|
|
937
988
|
"""Get route decorator with GET `method`
|
|
@@ -970,6 +1021,7 @@ class BaseRouter(ABC):
|
|
|
970
1021
|
operation_id,
|
|
971
1022
|
include_in_schema,
|
|
972
1023
|
security,
|
|
1024
|
+
openapi_extensions,
|
|
973
1025
|
middlewares,
|
|
974
1026
|
)
|
|
975
1027
|
|
|
@@ -987,6 +1039,7 @@ class BaseRouter(ABC):
|
|
|
987
1039
|
operation_id: str | None = None,
|
|
988
1040
|
include_in_schema: bool = True,
|
|
989
1041
|
security: list[dict[str, list[str]]] | None = None,
|
|
1042
|
+
openapi_extensions: dict[str, Any] | None = None,
|
|
990
1043
|
middlewares: list[Callable[..., Any]] | None = None,
|
|
991
1044
|
):
|
|
992
1045
|
"""Post route decorator with POST `method`
|
|
@@ -1026,6 +1079,7 @@ class BaseRouter(ABC):
|
|
|
1026
1079
|
operation_id,
|
|
1027
1080
|
include_in_schema,
|
|
1028
1081
|
security,
|
|
1082
|
+
openapi_extensions,
|
|
1029
1083
|
middlewares,
|
|
1030
1084
|
)
|
|
1031
1085
|
|
|
@@ -1043,6 +1097,7 @@ class BaseRouter(ABC):
|
|
|
1043
1097
|
operation_id: str | None = None,
|
|
1044
1098
|
include_in_schema: bool = True,
|
|
1045
1099
|
security: list[dict[str, list[str]]] | None = None,
|
|
1100
|
+
openapi_extensions: dict[str, Any] | None = None,
|
|
1046
1101
|
middlewares: list[Callable[..., Any]] | None = None,
|
|
1047
1102
|
):
|
|
1048
1103
|
"""Put route decorator with PUT `method`
|
|
@@ -1082,6 +1137,7 @@ class BaseRouter(ABC):
|
|
|
1082
1137
|
operation_id,
|
|
1083
1138
|
include_in_schema,
|
|
1084
1139
|
security,
|
|
1140
|
+
openapi_extensions,
|
|
1085
1141
|
middlewares,
|
|
1086
1142
|
)
|
|
1087
1143
|
|
|
@@ -1099,6 +1155,7 @@ class BaseRouter(ABC):
|
|
|
1099
1155
|
operation_id: str | None = None,
|
|
1100
1156
|
include_in_schema: bool = True,
|
|
1101
1157
|
security: list[dict[str, list[str]]] | None = None,
|
|
1158
|
+
openapi_extensions: dict[str, Any] | None = None,
|
|
1102
1159
|
middlewares: list[Callable[..., Any]] | None = None,
|
|
1103
1160
|
):
|
|
1104
1161
|
"""Delete route decorator with DELETE `method`
|
|
@@ -1137,6 +1194,7 @@ class BaseRouter(ABC):
|
|
|
1137
1194
|
operation_id,
|
|
1138
1195
|
include_in_schema,
|
|
1139
1196
|
security,
|
|
1197
|
+
openapi_extensions,
|
|
1140
1198
|
middlewares,
|
|
1141
1199
|
)
|
|
1142
1200
|
|
|
@@ -1154,6 +1212,7 @@ class BaseRouter(ABC):
|
|
|
1154
1212
|
operation_id: str | None = None,
|
|
1155
1213
|
include_in_schema: bool = True,
|
|
1156
1214
|
security: list[dict[str, list[str]]] | None = None,
|
|
1215
|
+
openapi_extensions: dict[str, Any] | None = None,
|
|
1157
1216
|
middlewares: list[Callable] | None = None,
|
|
1158
1217
|
):
|
|
1159
1218
|
"""Patch route decorator with PATCH `method`
|
|
@@ -1195,6 +1254,7 @@ class BaseRouter(ABC):
|
|
|
1195
1254
|
operation_id,
|
|
1196
1255
|
include_in_schema,
|
|
1197
1256
|
security,
|
|
1257
|
+
openapi_extensions,
|
|
1198
1258
|
middlewares,
|
|
1199
1259
|
)
|
|
1200
1260
|
|
|
@@ -1212,6 +1272,7 @@ class BaseRouter(ABC):
|
|
|
1212
1272
|
operation_id: str | None = None,
|
|
1213
1273
|
include_in_schema: bool = True,
|
|
1214
1274
|
security: list[dict[str, list[str]]] | None = None,
|
|
1275
|
+
openapi_extensions: dict[str, Any] | None = None,
|
|
1215
1276
|
middlewares: list[Callable] | None = None,
|
|
1216
1277
|
):
|
|
1217
1278
|
"""Head route decorator with HEAD `method`
|
|
@@ -1252,6 +1313,7 @@ class BaseRouter(ABC):
|
|
|
1252
1313
|
operation_id,
|
|
1253
1314
|
include_in_schema,
|
|
1254
1315
|
security,
|
|
1316
|
+
openapi_extensions,
|
|
1255
1317
|
middlewares,
|
|
1256
1318
|
)
|
|
1257
1319
|
|
|
@@ -1372,7 +1434,6 @@ def _registered_api_adapter(app: ApiGatewayResolver, next_middleware: Callable[.
|
|
|
1372
1434
|
"""
|
|
1373
1435
|
route_args: dict = app.context.get("_route_args", {})
|
|
1374
1436
|
logger.debug(f"Calling API Route Handler: {route_args}")
|
|
1375
|
-
|
|
1376
1437
|
return app._to_response(next_middleware(**route_args))
|
|
1377
1438
|
|
|
1378
1439
|
|
|
@@ -1473,6 +1534,7 @@ class ApiGatewayResolver(BaseRouter):
|
|
|
1473
1534
|
license_info: License | None = None,
|
|
1474
1535
|
security_schemes: dict[str, SecurityScheme] | None = None,
|
|
1475
1536
|
security: list[dict[str, list[str]]] | None = None,
|
|
1537
|
+
openapi_extensions: dict[str, Any] | None = None,
|
|
1476
1538
|
) -> OpenAPI:
|
|
1477
1539
|
"""
|
|
1478
1540
|
Returns the OpenAPI schema as a pydantic model.
|
|
@@ -1503,6 +1565,8 @@ class ApiGatewayResolver(BaseRouter):
|
|
|
1503
1565
|
A declaration of the security schemes available to be used in the specification.
|
|
1504
1566
|
security: list[dict[str, list[str]]], optional
|
|
1505
1567
|
A declaration of which security mechanisms are applied globally across the API.
|
|
1568
|
+
openapi_extensions: Dict[str, Any], optional
|
|
1569
|
+
Additional OpenAPI extensions as a dictionary.
|
|
1506
1570
|
|
|
1507
1571
|
Returns
|
|
1508
1572
|
-------
|
|
@@ -1535,11 +1599,15 @@ class ApiGatewayResolver(BaseRouter):
|
|
|
1535
1599
|
|
|
1536
1600
|
info.update({field: value for field, value in optional_fields.items() if value})
|
|
1537
1601
|
|
|
1602
|
+
if not isinstance(openapi_extensions, dict):
|
|
1603
|
+
openapi_extensions = {}
|
|
1604
|
+
|
|
1538
1605
|
output: dict[str, Any] = {
|
|
1539
1606
|
"openapi": openapi_version,
|
|
1540
1607
|
"info": info,
|
|
1541
1608
|
"servers": self._get_openapi_servers(servers),
|
|
1542
1609
|
"security": self._get_openapi_security(security, security_schemes),
|
|
1610
|
+
**openapi_extensions,
|
|
1543
1611
|
}
|
|
1544
1612
|
|
|
1545
1613
|
components: dict[str, dict[str, Any]] = {}
|
|
@@ -1560,6 +1628,16 @@ class ApiGatewayResolver(BaseRouter):
|
|
|
1560
1628
|
|
|
1561
1629
|
# Add routes to the OpenAPI schema
|
|
1562
1630
|
for route in all_routes:
|
|
1631
|
+
|
|
1632
|
+
if route.security and not _validate_openapi_security_parameters(
|
|
1633
|
+
security=route.security,
|
|
1634
|
+
security_schemes=security_schemes,
|
|
1635
|
+
):
|
|
1636
|
+
raise SchemaValidationError(
|
|
1637
|
+
"Security configuration was not found in security_schemas or security_schema was not defined. "
|
|
1638
|
+
"See: https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/#security-schemes",
|
|
1639
|
+
)
|
|
1640
|
+
|
|
1563
1641
|
if not route.include_in_schema:
|
|
1564
1642
|
continue
|
|
1565
1643
|
|
|
@@ -1605,12 +1683,11 @@ class ApiGatewayResolver(BaseRouter):
|
|
|
1605
1683
|
if not security:
|
|
1606
1684
|
return None
|
|
1607
1685
|
|
|
1608
|
-
if not security_schemes:
|
|
1609
|
-
raise
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
raise ValueError("Some security schemes not found in security_schemes")
|
|
1686
|
+
if not _validate_openapi_security_parameters(security=security, security_schemes=security_schemes):
|
|
1687
|
+
raise SchemaValidationError(
|
|
1688
|
+
"Security configuration was not found in security_schemas or security_schema was not defined. "
|
|
1689
|
+
"See: https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/#security-schemes",
|
|
1690
|
+
)
|
|
1614
1691
|
|
|
1615
1692
|
return security
|
|
1616
1693
|
|
|
@@ -1641,6 +1718,7 @@ class ApiGatewayResolver(BaseRouter):
|
|
|
1641
1718
|
license_info: License | None = None,
|
|
1642
1719
|
security_schemes: dict[str, SecurityScheme] | None = None,
|
|
1643
1720
|
security: list[dict[str, list[str]]] | None = None,
|
|
1721
|
+
openapi_extensions: dict[str, Any] | None = None,
|
|
1644
1722
|
) -> str:
|
|
1645
1723
|
"""
|
|
1646
1724
|
Returns the OpenAPI schema as a JSON serializable dict
|
|
@@ -1671,6 +1749,8 @@ class ApiGatewayResolver(BaseRouter):
|
|
|
1671
1749
|
A declaration of the security schemes available to be used in the specification.
|
|
1672
1750
|
security: list[dict[str, list[str]]], optional
|
|
1673
1751
|
A declaration of which security mechanisms are applied globally across the API.
|
|
1752
|
+
openapi_extensions: Dict[str, Any], optional
|
|
1753
|
+
Additional OpenAPI extensions as a dictionary.
|
|
1674
1754
|
|
|
1675
1755
|
Returns
|
|
1676
1756
|
-------
|
|
@@ -1693,6 +1773,7 @@ class ApiGatewayResolver(BaseRouter):
|
|
|
1693
1773
|
license_info=license_info,
|
|
1694
1774
|
security_schemes=security_schemes,
|
|
1695
1775
|
security=security,
|
|
1776
|
+
openapi_extensions=openapi_extensions,
|
|
1696
1777
|
),
|
|
1697
1778
|
by_alias=True,
|
|
1698
1779
|
exclude_none=True,
|
|
@@ -1720,6 +1801,7 @@ class ApiGatewayResolver(BaseRouter):
|
|
|
1720
1801
|
security: list[dict[str, list[str]]] | None = None,
|
|
1721
1802
|
oauth2_config: OAuth2Config | None = None,
|
|
1722
1803
|
persist_authorization: bool = False,
|
|
1804
|
+
openapi_extensions: dict[str, Any] | None = None,
|
|
1723
1805
|
):
|
|
1724
1806
|
"""
|
|
1725
1807
|
Returns the OpenAPI schema as a JSON serializable dict
|
|
@@ -1762,6 +1844,8 @@ class ApiGatewayResolver(BaseRouter):
|
|
|
1762
1844
|
The OAuth2 configuration for the Swagger UI.
|
|
1763
1845
|
persist_authorization: bool, optional
|
|
1764
1846
|
Whether to persist authorization data on browser close/refresh.
|
|
1847
|
+
openapi_extensions: dict[str, Any], optional
|
|
1848
|
+
Additional OpenAPI extensions as a dictionary.
|
|
1765
1849
|
"""
|
|
1766
1850
|
from aws_lambda_powertools.event_handler.openapi.compat import model_json
|
|
1767
1851
|
from aws_lambda_powertools.event_handler.openapi.models import Server
|
|
@@ -1811,6 +1895,7 @@ class ApiGatewayResolver(BaseRouter):
|
|
|
1811
1895
|
license_info=license_info,
|
|
1812
1896
|
security_schemes=security_schemes,
|
|
1813
1897
|
security=security,
|
|
1898
|
+
openapi_extensions=openapi_extensions,
|
|
1814
1899
|
)
|
|
1815
1900
|
|
|
1816
1901
|
# The .replace('</', '<\\/') part is necessary to prevent a potential issue where the JSON string contains
|
|
@@ -1835,7 +1920,6 @@ class ApiGatewayResolver(BaseRouter):
|
|
|
1835
1920
|
|
|
1836
1921
|
body = generate_swagger_html(
|
|
1837
1922
|
escaped_spec,
|
|
1838
|
-
f"{base_path}{path}",
|
|
1839
1923
|
swagger_js,
|
|
1840
1924
|
swagger_css,
|
|
1841
1925
|
swagger_base_url,
|
|
@@ -1864,6 +1948,7 @@ class ApiGatewayResolver(BaseRouter):
|
|
|
1864
1948
|
operation_id: str | None = None,
|
|
1865
1949
|
include_in_schema: bool = True,
|
|
1866
1950
|
security: list[dict[str, list[str]]] | None = None,
|
|
1951
|
+
openapi_extensions: dict[str, Any] | None = None,
|
|
1867
1952
|
middlewares: list[Callable[..., Any]] | None = None,
|
|
1868
1953
|
):
|
|
1869
1954
|
"""Route decorator includes parameter `method`"""
|
|
@@ -1891,6 +1976,7 @@ class ApiGatewayResolver(BaseRouter):
|
|
|
1891
1976
|
operation_id,
|
|
1892
1977
|
include_in_schema,
|
|
1893
1978
|
security,
|
|
1979
|
+
openapi_extensions,
|
|
1894
1980
|
middlewares,
|
|
1895
1981
|
)
|
|
1896
1982
|
|
|
@@ -1916,6 +2002,36 @@ class ApiGatewayResolver(BaseRouter):
|
|
|
1916
2002
|
def resolve(self, event, context) -> dict[str, Any]:
|
|
1917
2003
|
"""Resolves the response based on the provide event and decorator routes
|
|
1918
2004
|
|
|
2005
|
+
## Internals
|
|
2006
|
+
|
|
2007
|
+
Request processing chain is triggered by a Route object being called _(`_call_route` -> `__call__`)_:
|
|
2008
|
+
|
|
2009
|
+
1. **When a route is matched**
|
|
2010
|
+
1.1. Exception handlers _(if any exception bubbled up and caught)_
|
|
2011
|
+
1.2. Global middlewares _(before, and after on the way back)_
|
|
2012
|
+
1.3. Path level middleware _(before, and after on the way back)_
|
|
2013
|
+
1.4. Middleware adapter to ensure Response is homogenous (_registered_api_adapter)
|
|
2014
|
+
1.5. Run actual route
|
|
2015
|
+
2. **When a route is NOT matched**
|
|
2016
|
+
2.1. Exception handlers _(if any exception bubbled up and caught)_
|
|
2017
|
+
2.2. Global middlewares _(before, and after on the way back)_
|
|
2018
|
+
2.3. Path level middleware _(before, and after on the way back)_
|
|
2019
|
+
2.4. Middleware adapter to ensure Response is homogenous (_registered_api_adapter)
|
|
2020
|
+
2.5. Run 404 route handler
|
|
2021
|
+
3. **When a route is a pre-flight CORS (often not matched)**
|
|
2022
|
+
3.1. Exception handlers _(if any exception bubbled up and caught)_
|
|
2023
|
+
3.2. Global middlewares _(before, and after on the way back)_
|
|
2024
|
+
3.3. Path level middleware _(before, and after on the way back)_
|
|
2025
|
+
3.4. Middleware adapter to ensure Response is homogenous (_registered_api_adapter)
|
|
2026
|
+
3.5. Return 204 with appropriate CORS headers
|
|
2027
|
+
4. **When a route is matched with Data Validation enabled**
|
|
2028
|
+
4.1. Exception handlers _(if any exception bubbled up and caught)_
|
|
2029
|
+
4.2. Data Validation middleware _(before, and after on the way back)_
|
|
2030
|
+
4.3. Global middlewares _(before, and after on the way back)_
|
|
2031
|
+
4.4. Path level middleware _(before, and after on the way back)_
|
|
2032
|
+
4.5. Middleware adapter to ensure Response is homogenous (_registered_api_adapter)
|
|
2033
|
+
4.6. Run actual route
|
|
2034
|
+
|
|
1919
2035
|
Parameters
|
|
1920
2036
|
----------
|
|
1921
2037
|
event: dict[str, Any]
|
|
@@ -2039,7 +2155,9 @@ class ApiGatewayResolver(BaseRouter):
|
|
|
2039
2155
|
method = self.current_event.http_method.upper()
|
|
2040
2156
|
path = self._remove_prefix(self.current_event.path)
|
|
2041
2157
|
|
|
2042
|
-
|
|
2158
|
+
registered_routes = self._static_routes + self._dynamic_routes
|
|
2159
|
+
|
|
2160
|
+
for route in registered_routes:
|
|
2043
2161
|
if method != route.method:
|
|
2044
2162
|
continue
|
|
2045
2163
|
match_results: Match | None = route.rule.match(path)
|
|
@@ -2051,8 +2169,7 @@ class ApiGatewayResolver(BaseRouter):
|
|
|
2051
2169
|
route_keys = self._convert_matches_into_route_keys(match_results)
|
|
2052
2170
|
return self._call_route(route, route_keys) # pass fn args
|
|
2053
2171
|
|
|
2054
|
-
|
|
2055
|
-
return self._not_found(method)
|
|
2172
|
+
return self._handle_not_found(method=method, path=path)
|
|
2056
2173
|
|
|
2057
2174
|
def _remove_prefix(self, path: str) -> str:
|
|
2058
2175
|
"""Remove the configured prefix from the path"""
|
|
@@ -2090,36 +2207,65 @@ class ApiGatewayResolver(BaseRouter):
|
|
|
2090
2207
|
|
|
2091
2208
|
return path.startswith(prefix + "/")
|
|
2092
2209
|
|
|
2093
|
-
def
|
|
2210
|
+
def _handle_not_found(self, method: str, path: str) -> ResponseBuilder:
|
|
2094
2211
|
"""Called when no matching route was found and includes support for the cors preflight response"""
|
|
2095
|
-
|
|
2096
|
-
if self._cors:
|
|
2097
|
-
logger.debug("CORS is enabled, updating headers.")
|
|
2098
|
-
extracted_origin_header = extract_origin_header(self.current_event.resolved_headers_field)
|
|
2099
|
-
headers.update(self._cors.to_dict(extracted_origin_header))
|
|
2100
|
-
|
|
2101
|
-
if method == "OPTIONS":
|
|
2102
|
-
logger.debug("Pre-flight request detected. Returning CORS with null response")
|
|
2103
|
-
headers["Access-Control-Allow-Methods"] = ",".join(sorted(self._cors_methods))
|
|
2104
|
-
return ResponseBuilder(
|
|
2105
|
-
response=Response(status_code=204, content_type=None, headers=headers, body=""),
|
|
2106
|
-
serializer=self._serializer,
|
|
2107
|
-
)
|
|
2212
|
+
logger.debug(f"No match found for path {path} and method {method}")
|
|
2108
2213
|
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2214
|
+
def not_found_handler():
|
|
2215
|
+
"""Route handler for 404s
|
|
2216
|
+
|
|
2217
|
+
It handles in the following order:
|
|
2218
|
+
|
|
2219
|
+
1. Pre-flight CORS requests (OPTIONS)
|
|
2220
|
+
2. Detects and calls custom HTTP 404 handler
|
|
2221
|
+
3. Returns standard 404 along with CORS headers
|
|
2222
|
+
|
|
2223
|
+
Returns
|
|
2224
|
+
-------
|
|
2225
|
+
Response
|
|
2226
|
+
HTTP 404 response
|
|
2227
|
+
"""
|
|
2228
|
+
_headers: dict[str, Any] = {}
|
|
2229
|
+
|
|
2230
|
+
# Pre-flight request? Return immediately to avoid browser error
|
|
2231
|
+
if self._cors and method == "OPTIONS":
|
|
2232
|
+
logger.debug("Pre-flight request detected. Returning CORS with empty response")
|
|
2233
|
+
_headers["Access-Control-Allow-Methods"] = CORSConfig.build_allow_methods(self._cors_methods)
|
|
2234
|
+
|
|
2235
|
+
return Response(status_code=204, content_type=None, headers=_headers, body="")
|
|
2236
|
+
|
|
2237
|
+
# Customer registered 404 route? Call it.
|
|
2238
|
+
custom_not_found_handler = self._lookup_exception_handler(NotFoundError)
|
|
2239
|
+
if custom_not_found_handler:
|
|
2240
|
+
return custom_not_found_handler(NotFoundError())
|
|
2112
2241
|
|
|
2113
|
-
|
|
2114
|
-
|
|
2242
|
+
# No CORS and no custom 404 fn? Default response
|
|
2243
|
+
return Response(
|
|
2115
2244
|
status_code=HTTPStatus.NOT_FOUND.value,
|
|
2116
2245
|
content_type=content_types.APPLICATION_JSON,
|
|
2117
|
-
headers=
|
|
2246
|
+
headers=_headers,
|
|
2118
2247
|
body={"statusCode": HTTPStatus.NOT_FOUND.value, "message": "Not found"},
|
|
2119
|
-
)
|
|
2120
|
-
|
|
2248
|
+
)
|
|
2249
|
+
|
|
2250
|
+
# We create a route to trigger entire request chain (middleware+exception handlers)
|
|
2251
|
+
route = Route(
|
|
2252
|
+
rule=self._compile_regex(r".*"),
|
|
2253
|
+
method=method,
|
|
2254
|
+
path=path,
|
|
2255
|
+
func=not_found_handler,
|
|
2256
|
+
cors=self._cors_enabled,
|
|
2257
|
+
compress=False,
|
|
2121
2258
|
)
|
|
2122
2259
|
|
|
2260
|
+
# Add matched Route reference into the Resolver context
|
|
2261
|
+
self.append_context(_route=route, _path=path)
|
|
2262
|
+
|
|
2263
|
+
# Kick-off request chain:
|
|
2264
|
+
# -> exception_handlers()
|
|
2265
|
+
# --> middlewares()
|
|
2266
|
+
# ---> not_found_route()
|
|
2267
|
+
return self._call_route(route=route, route_arguments={})
|
|
2268
|
+
|
|
2123
2269
|
def _call_route(self, route: Route, route_arguments: dict[str, str]) -> ResponseBuilder:
|
|
2124
2270
|
"""Actually call the matching route with any provided keyword arguments."""
|
|
2125
2271
|
try:
|
|
@@ -2344,6 +2490,7 @@ class Router(BaseRouter):
|
|
|
2344
2490
|
operation_id: str | None = None,
|
|
2345
2491
|
include_in_schema: bool = True,
|
|
2346
2492
|
security: list[dict[str, list[str]]] | None = None,
|
|
2493
|
+
openapi_extensions: dict[str, Any] | None = None,
|
|
2347
2494
|
middlewares: list[Callable[..., Any]] | None = None,
|
|
2348
2495
|
):
|
|
2349
2496
|
def register_route(func: Callable):
|
|
@@ -2351,6 +2498,8 @@ class Router(BaseRouter):
|
|
|
2351
2498
|
methods = (method,) if isinstance(method, str) else tuple(method)
|
|
2352
2499
|
frozen_responses = _FrozenDict(responses) if responses else None
|
|
2353
2500
|
frozen_tags = frozenset(tags) if tags else None
|
|
2501
|
+
frozen_security = _FrozenListDict(security) if security else None
|
|
2502
|
+
fronzen_openapi_extensions = _FrozenDict(openapi_extensions) if openapi_extensions else None
|
|
2354
2503
|
|
|
2355
2504
|
route_key = (
|
|
2356
2505
|
rule,
|
|
@@ -2365,7 +2514,8 @@ class Router(BaseRouter):
|
|
|
2365
2514
|
frozen_tags,
|
|
2366
2515
|
operation_id,
|
|
2367
2516
|
include_in_schema,
|
|
2368
|
-
|
|
2517
|
+
frozen_security,
|
|
2518
|
+
fronzen_openapi_extensions,
|
|
2369
2519
|
)
|
|
2370
2520
|
|
|
2371
2521
|
# Collate Middleware for routes
|
|
@@ -2446,6 +2596,7 @@ class APIGatewayRestResolver(ApiGatewayResolver):
|
|
|
2446
2596
|
operation_id: str | None = None,
|
|
2447
2597
|
include_in_schema: bool = True,
|
|
2448
2598
|
security: list[dict[str, list[str]]] | None = None,
|
|
2599
|
+
openapi_extensions: dict[str, Any] | None = None,
|
|
2449
2600
|
middlewares: list[Callable[..., Any]] | None = None,
|
|
2450
2601
|
):
|
|
2451
2602
|
# NOTE: see #1552 for more context.
|
|
@@ -2463,6 +2614,7 @@ class APIGatewayRestResolver(ApiGatewayResolver):
|
|
|
2463
2614
|
operation_id,
|
|
2464
2615
|
include_in_schema,
|
|
2465
2616
|
security,
|
|
2617
|
+
openapi_extensions,
|
|
2466
2618
|
middlewares,
|
|
2467
2619
|
)
|
|
2468
2620
|
|
|
@@ -2524,3 +2676,24 @@ class ALBResolver(ApiGatewayResolver):
|
|
|
2524
2676
|
def _get_base_path(self) -> str:
|
|
2525
2677
|
# ALB doesn't have a stage variable, so we just return an empty string
|
|
2526
2678
|
return ""
|
|
2679
|
+
|
|
2680
|
+
@override
|
|
2681
|
+
def _to_response(self, result: dict | tuple | Response) -> Response:
|
|
2682
|
+
"""Convert the route's result to a Response
|
|
2683
|
+
|
|
2684
|
+
ALB requires a non-null body otherwise it converts as HTTP 5xx
|
|
2685
|
+
|
|
2686
|
+
3 main result types are supported:
|
|
2687
|
+
|
|
2688
|
+
- Dict[str, Any]: Rest api response with just the Dict to json stringify and content-type is set to
|
|
2689
|
+
application/json
|
|
2690
|
+
- Tuple[dict, int]: Same dict handling as above but with the option of including a status code
|
|
2691
|
+
- Response: returned as is, and allows for more flexibility
|
|
2692
|
+
"""
|
|
2693
|
+
|
|
2694
|
+
# NOTE: Minor override for early return on Response with null body for ALB
|
|
2695
|
+
if isinstance(result, Response) and result.body is None:
|
|
2696
|
+
logger.debug("ALB doesn't allow None responses; converting to empty string")
|
|
2697
|
+
result.body = ""
|
|
2698
|
+
|
|
2699
|
+
return super()._to_response(result)
|