Skip to content

Commit 8f6cb17

Browse files
Transforming List[Dict..List]]] in a hashed object
1 parent 418172d commit 8f6cb17

File tree

3 files changed

+64
-2
lines changed

3 files changed

+64
-2
lines changed

aws_lambda_powertools/event_handler/api_gateway.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
validation_error_definition,
4444
validation_error_response_definition,
4545
)
46-
from aws_lambda_powertools.event_handler.util import _FrozenDict, extract_origin_header
46+
from aws_lambda_powertools.event_handler.util import _FrozenDict, _FrozenListDict, extract_origin_header
4747
from aws_lambda_powertools.shared.cookies import Cookie
4848
from aws_lambda_powertools.shared.functions import powertools_dev_is_set
4949
from aws_lambda_powertools.shared.json_encoder import Encoder
@@ -2386,6 +2386,7 @@ def register_route(func: Callable):
23862386
methods = (method,) if isinstance(method, str) else tuple(method)
23872387
frozen_responses = _FrozenDict(responses) if responses else None
23882388
frozen_tags = frozenset(tags) if tags else None
2389+
frozen_security = _FrozenListDict(security) if security else None
23892390

23902391
route_key = (
23912392
rule,
@@ -2400,7 +2401,7 @@ def register_route(func: Callable):
24002401
frozen_tags,
24012402
operation_id,
24022403
include_in_schema,
2403-
security,
2404+
frozen_security,
24042405
)
24052406

24062407
# Collate Middleware for routes

aws_lambda_powertools/event_handler/util.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,27 @@ def __hash__(self):
1818
return hash(frozenset(self.keys()))
1919

2020

21+
class _FrozenListDict(list[dict[str, list[str]]]):
22+
"""
23+
Freezes a list of dictionaries containing lists of strings.
24+
25+
This function takes a list of dictionaries where the values are lists of strings and converts it into
26+
a frozen set of frozen sets of frozen dictionaries. This is done by iterating over the input list,
27+
converting each dictionary's values (lists of strings) into frozen sets of strings, and then
28+
converting the resulting dictionary into a frozen dictionary. Finally, all these frozen dictionaries
29+
are collected into a frozen set of frozen sets.
30+
31+
This operation is useful when you want to ensure the immutability of the data structure and make it
32+
hashable, which is required for certain operations like using it as a key in a dictionary or as an
33+
element in a set.
34+
35+
Example: [{"TestAuth": ["test", "test1"]}]
36+
"""
37+
38+
def __hash__(self):
39+
return hash(frozenset({_FrozenDict({key: frozenset(self) for key, self in item.items()}) for item in self}))
40+
41+
2142
def extract_origin_header(resolver_headers: Dict[str, Any]):
2243
"""
2344
Extracts the 'origin' or 'Origin' header from the provided resolver headers.

tests/functional/event_handler/test_openapi_security.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
11
import pytest
22

33
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
4+
from aws_lambda_powertools.event_handler.api_gateway import Router
45
from aws_lambda_powertools.event_handler.openapi.models import APIKey, APIKeyIn
56

67

78
def test_openapi_top_level_security():
9+
# GIVEN an APIGatewayRestResolver instance
810
app = APIGatewayRestResolver()
911

1012
@app.get("/")
1113
def handler():
1214
raise NotImplementedError()
1315

16+
# WHEN the get_openapi_schema method is called with a security scheme
1417
schema = app.get_openapi_schema(
1518
security_schemes={
1619
"apiKey": APIKey(name="X-API-KEY", description="API Key", in_=APIKeyIn.header),
1720
},
1821
security=[{"apiKey": []}],
1922
)
2023

24+
# THEN the resulting schema should have security defined at the top level
2125
security = schema.security
2226
assert security is not None
2327

@@ -26,31 +30,67 @@ def handler():
2630

2731

2832
def test_openapi_top_level_security_missing():
33+
# GIVEN an APIGatewayRestResolver instance
2934
app = APIGatewayRestResolver()
3035

3136
@app.get("/")
3237
def handler():
3338
raise NotImplementedError()
3439

40+
# WHEN the get_openapi_schema method is called with security defined without security schemes
41+
# THEN a ValueError should be raised
3542
with pytest.raises(ValueError):
3643
app.get_openapi_schema(
3744
security=[{"apiKey": []}],
3845
)
3946

4047

4148
def test_openapi_operation_security():
49+
# GIVEN an APIGatewayRestResolver instance
4250
app = APIGatewayRestResolver()
4351

4452
@app.get("/", security=[{"apiKey": []}])
4553
def handler():
4654
raise NotImplementedError()
4755

56+
# WHEN the get_openapi_schema method is called with security defined at the operation level
4857
schema = app.get_openapi_schema(
4958
security_schemes={
5059
"apiKey": APIKey(name="X-API-KEY", description="API Key", in_=APIKeyIn.header),
5160
},
5261
)
5362

63+
# THEN the resulting schema should have security defined at the operation level, not the top level
64+
security = schema.security
65+
assert security is None
66+
67+
operation = schema.paths["/"].get
68+
security = operation.security
69+
assert security is not None
70+
71+
assert len(security) == 1
72+
assert security[0] == {"apiKey": []}
73+
74+
75+
def test_openapi_operation_security_with_router():
76+
# GIVEN an APIGatewayRestResolver instance with a Router
77+
app = APIGatewayRestResolver()
78+
router = Router()
79+
80+
@router.get("/", security=[{"apiKey": []}])
81+
def handler():
82+
raise NotImplementedError()
83+
84+
app.include_router(router)
85+
86+
# WHEN the get_openapi_schema method is called with security defined at the operation level in the Router
87+
schema = app.get_openapi_schema(
88+
security_schemes={
89+
"apiKey": APIKey(name="X-API-KEY", description="API Key", in_=APIKeyIn.header),
90+
},
91+
)
92+
93+
# THEN the resulting schema should have security defined at the operation level
5494
security = schema.security
5595
assert security is None
5696

0 commit comments

Comments
 (0)