Skip to content

Commit 06c3250

Browse files
committed
* 'develop' of https://github.com/awslabs/aws-lambda-powertools-python: docs(event-handler): document catch-all routes (aws-powertools#705) chore: add python 3.9 support docs: add team behind it and email ISSUE-693: Use ExpressionAttributeNames in _put_record (aws-powertools#697) feat(validator): include missing data elements from a validation error (aws-powertools#686) chore(deps-dev): bump mkdocs-material from 7.2.8 to 7.3.0 (aws-powertools#695) chore(deps-dev): bump mkdocs-material from 7.2.6 to 7.2.8 (aws-powertools#682) chore(deps-dev): bump flake8-bugbear from 21.4.3 to 21.9.1 (aws-powertools#676) chore(deps): bump boto3 from 1.18.38 to 1.18.41 (aws-powertools#677) chore(deps-dev): bump radon from 4.5.2 to 5.1.0 (aws-powertools#673) chore(deps): bump boto3 from 1.18.32 to 1.18.38 (aws-powertools#671) refactor(data-classes): clean up internal logic for APIGatewayAuthorizerResponse (aws-powertools#643) fix(data-classes): use correct asdict funciton (aws-powertools#666) chore(deps-dev): bump xenon from 0.7.3 to 0.8.0 (aws-powertools#669) chore: bump to 1.20.2 fix: Fix issue with strip_prefixes (aws-powertools#647) chore(deps-dev): bump mkdocs-material from 7.2.4 to 7.2.6 (aws-powertools#665) chore(deps): bump boto3 from 1.18.26 to 1.18.32 (aws-powertools#663) chore(deps-dev): bump pytest from 6.2.4 to 6.2.5 (aws-powertools#662) chore(license): Add THIRD-PARTY-LICENSES (aws-powertools#641)
2 parents b9fa07a + 977162a commit 06c3250

File tree

16 files changed

+345
-102
lines changed

16 files changed

+345
-102
lines changed

.pylintrc

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[MESSAGES CONTROL]
2+
disable=
3+
too-many-arguments,
4+
too-many-instance-attributes,
5+
too-few-public-methods,
6+
anomalous-backslash-in-string,
7+
missing-class-docstring,
8+
missing-module-docstring,
9+
missing-function-docstring,
10+
11+
[FORMAT]
12+
max-line-length=120

CHANGELOG.md

+14
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ This project follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) fo
77

88
## [Unreleased]
99

10+
## 1.20.2 - 2021-09-02
11+
12+
### Bug Fixes
13+
14+
* **event-handler:** fix issue with strip_prefixes and root level resolvers ([#646](https://github.com/awslabs/aws-lambda-powertools-python/issues/646))
15+
16+
### Maintenance
17+
18+
* **deps:** bump boto3 from 1.18.26 to 1.18.32 ([#663](https://github.com/awslabs/aws-lambda-powertools-python/issues/663))
19+
* **deps-dev:** bump mkdocs-material from 7.2.4 to 7.2.6 ([#665](https://github.com/awslabs/aws-lambda-powertools-python/issues/665))
20+
* **deps-dev:** bump pytest from 6.2.4 to 6.2.5 ([#662](https://github.com/awslabs/aws-lambda-powertools-python/issues/662))
21+
* **deps-dev:** bump mike from 0.6.0 to 1.0.1 ([#453](https://github.com/awslabs/aws-lambda-powertools-python/issues/453))
22+
* **license:** add third party license to pyproject.toml ([#641](https://github.com/awslabs/aws-lambda-powertools-python/issues/641))
23+
1024
## 1.20.1 - 2021-08-22
1125

1226
### Bug Fixes

README.md

+10-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
![Build](https://github.com/awslabs/aws-lambda-powertools/workflows/Powertools%20Python/badge.svg?branch=master)
44
[![codecov.io](https://codecov.io/github/awslabs/aws-lambda-powertools-python/branch/develop/graphs/badge.svg)](https://app.codecov.io/gh/awslabs/aws-lambda-powertools-python)
5-
![PythonSupport](https://img.shields.io/static/v1?label=python&message=3.6%20|%203.7|%203.8&color=blue?style=flat-square&logo=python) ![PyPI version](https://badge.fury.io/py/aws-lambda-powertools.svg) ![PyPi monthly downloads](https://img.shields.io/pypi/dm/aws-lambda-powertools)
5+
![PythonSupport](https://img.shields.io/static/v1?label=python&message=3.6%20|%203.7|%203.8|%203.9&color=blue?style=flat-square&logo=python) ![PyPI version](https://badge.fury.io/py/aws-lambda-powertools.svg) ![PyPi monthly downloads](https://img.shields.io/pypi/dm/aws-lambda-powertools)
66

77
A suite of Python utilities for AWS Lambda functions to ease adopting best practices such as tracing, structured logging, custom metrics, and more. ([AWS Lambda Powertools Java](https://github.com/awslabs/aws-lambda-powertools-java) is also available).
88

9+
10+
911
**[📜Documentation](https://awslabs.github.io/aws-lambda-powertools-python/)** | **[🐍PyPi](https://pypi.org/project/aws-lambda-powertools/)** | **[Roadmap](https://github.com/awslabs/aws-lambda-powertools-roadmap/projects/1)** | **[Quick hello world example](https://github.com/aws-samples/cookiecutter-aws-sam-python)** | **[Detailed blog post](https://aws.amazon.com/blogs/opensource/simplifying-serverless-best-practices-with-lambda-powertools/)**
1012

11-
> **Join us on the AWS Developers Slack at `#lambda-powertools`** - **[Invite, if you don't have an account](https://join.slack.com/t/awsdevelopers/shared_invite/zt-gu30gquv-EhwIYq3kHhhysaZ2aIX7ew)**
13+
> **An AWS Developer Acceleration (DevAx) initiative by Specialist Solution Architects | aws-devax-open-source@amazon.com**
1214
1315
## Features
1416

@@ -42,6 +44,12 @@ With [pip](https://pip.pypa.io/en/latest/index.html) installed, run: ``pip insta
4244
* Structured logging initial implementation from [aws-lambda-logging](https://gitlab.com/hadrien/aws_lambda_logging)
4345
* Powertools idea [DAZN Powertools](https://github.com/getndazn/dazn-lambda-powertools/)
4446

47+
48+
## Connect
49+
50+
* **AWS Developers Slack**: `#lambda-powertools`** - **[Invite, if you don't have an account](https://join.slack.com/t/awsdevelopers/shared_invite/zt-gu30gquv-EhwIYq3kHhhysaZ2aIX7ew)**
51+
* **Email**: aws-lambda-powertools-feedback@amazon.com
52+
4553
## License
4654

4755
This library is licensed under the MIT-0 License. See the LICENSE file.

aws_lambda_powertools/event_handler/api_gateway.py

+2
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,8 @@ def _remove_prefix(self, path: str) -> str:
546546
return path
547547

548548
for prefix in self._strip_prefixes:
549+
if path == prefix:
550+
return "/"
549551
if self._path_starts_with(path, prefix):
550552
return path[len(prefix) :]
551553

aws_lambda_powertools/utilities/data_classes/api_gateway_authorizer_event.py

+80-29
Original file line numberDiff line numberDiff line change
@@ -234,10 +234,12 @@ def raw_query_string(self) -> str:
234234

235235
@property
236236
def cookies(self) -> List[str]:
237+
"""Cookies"""
237238
return self["cookies"]
238239

239240
@property
240241
def headers(self) -> Dict[str, str]:
242+
"""Http headers"""
241243
return self["headers"]
242244

243245
@property
@@ -314,6 +316,8 @@ def asdict(self) -> dict:
314316

315317

316318
class HttpVerb(enum.Enum):
319+
"""Enum of http methods / verbs"""
320+
317321
GET = "GET"
318322
POST = "POST"
319323
PUT = "PUT"
@@ -324,15 +328,32 @@ class HttpVerb(enum.Enum):
324328
ALL = "*"
325329

326330

331+
DENY_ALL_RESPONSE = {
332+
"principalId": "deny-all-user",
333+
"policyDocument": {
334+
"Version": "2012-10-17",
335+
"Statement": [
336+
{
337+
"Action": "execute-api:Invoke",
338+
"Effect": "Deny",
339+
"Resource": ["*"],
340+
}
341+
],
342+
},
343+
}
344+
345+
327346
class APIGatewayAuthorizerResponse:
328-
"""Api Gateway HTTP API V1 payload or Rest api authorizer response helper
347+
"""The IAM Policy Response required for API Gateway REST APIs and HTTP APIs.
329348
330349
Based on: - https://github.com/awslabs/aws-apigateway-lambda-authorizer-blueprints/blob/\
331350
master/blueprints/python/api-gateway-authorizer-python.py
332-
"""
333351
334-
version = "2012-10-17"
335-
"""The policy version used for the evaluation. This should always be '2012-10-17'"""
352+
Documentation:
353+
-------------
354+
- https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-lambda-authorizer.html
355+
- https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html
356+
"""
336357

337358
path_regex = r"^[/.a-zA-Z0-9-\*]+$"
338359
"""The regular expression used to validate resource paths for the policy"""
@@ -345,6 +366,7 @@ def __init__(
345366
api_id: str,
346367
stage: str,
347368
context: Optional[Dict] = None,
369+
usage_identifier_key: Optional[str] = None,
348370
):
349371
"""
350372
Parameters
@@ -373,32 +395,57 @@ def __init__(
373395
context : Dict, optional
374396
Optional, context.
375397
Note: only names of type string and values of type int, string or boolean are supported
398+
usage_identifier_key: str, optional
399+
If the API uses a usage plan (the apiKeySource is set to `AUTHORIZER`), the Lambda authorizer function
400+
must return one of the usage plan's API keys as the usageIdentifierKey property value.
401+
> **Note:** This only applies for REST APIs.
376402
"""
377403
self.principal_id = principal_id
378404
self.region = region
379405
self.aws_account_id = aws_account_id
380406
self.api_id = api_id
381407
self.stage = stage
382408
self.context = context
409+
self.usage_identifier_key = usage_identifier_key
383410
self._allow_routes: List[Dict] = []
384411
self._deny_routes: List[Dict] = []
412+
self._resource_pattern = re.compile(self.path_regex)
385413

386-
def _add_route(self, effect: str, verb: str, resource: str, conditions: List[Dict]):
414+
@staticmethod
415+
def from_route_arn(
416+
arn: str,
417+
principal_id: str,
418+
context: Optional[Dict] = None,
419+
usage_identifier_key: Optional[str] = None,
420+
) -> "APIGatewayAuthorizerResponse":
421+
parsed_arn = parse_api_gateway_arn(arn)
422+
return APIGatewayAuthorizerResponse(
423+
principal_id,
424+
parsed_arn.region,
425+
parsed_arn.aws_account_id,
426+
parsed_arn.api_id,
427+
parsed_arn.stage,
428+
context,
429+
usage_identifier_key,
430+
)
431+
432+
def _add_route(self, effect: str, http_method: str, resource: str, conditions: Optional[List[Dict]] = None):
387433
"""Adds a route to the internal lists of allowed or denied routes. Each object in
388434
the internal list contains a resource ARN and a condition statement. The condition
389435
statement can be null."""
390-
if verb != "*" and verb not in HttpVerb.__members__:
436+
if http_method != "*" and http_method not in HttpVerb.__members__:
391437
allowed_values = [verb.value for verb in HttpVerb]
392-
raise ValueError(f"Invalid HTTP verb: '{verb}'. Use either '{allowed_values}'")
438+
raise ValueError(f"Invalid HTTP verb: '{http_method}'. Use either '{allowed_values}'")
393439

394-
resource_pattern = re.compile(self.path_regex)
395-
if not resource_pattern.match(resource):
440+
if not self._resource_pattern.match(resource):
396441
raise ValueError(f"Invalid resource path: {resource}. Path should match {self.path_regex}")
397442

398443
if resource[:1] == "/":
399444
resource = resource[1:]
400445

401-
resource_arn = APIGatewayRouteArn(self.region, self.aws_account_id, self.api_id, self.stage, verb, resource).arn
446+
resource_arn = APIGatewayRouteArn(
447+
self.region, self.aws_account_id, self.api_id, self.stage, http_method, resource
448+
).arn
402449

403450
route = {"resourceArn": resource_arn, "conditions": conditions}
404451

@@ -412,24 +459,27 @@ def _get_empty_statement(effect: str) -> Dict[str, Any]:
412459
"""Returns an empty statement object prepopulated with the correct action and the desired effect."""
413460
return {"Action": "execute-api:Invoke", "Effect": effect.capitalize(), "Resource": []}
414461

415-
def _get_statement_for_effect(self, effect: str, methods: List) -> List:
416-
"""This function loops over an array of objects containing a resourceArn and
417-
conditions statement and generates the array of statements for the policy."""
418-
if len(methods) == 0:
462+
def _get_statement_for_effect(self, effect: str, routes: List[Dict]) -> List[Dict]:
463+
"""This function loops over an array of objects containing a `resourceArn` and
464+
`conditions` statement and generates the array of statements for the policy."""
465+
if not routes:
419466
return []
420467

421-
statements = []
422-
468+
statements: List[Dict] = []
423469
statement = self._get_empty_statement(effect)
424-
for method in methods:
425-
if method["conditions"] is None or len(method["conditions"]) == 0:
426-
statement["Resource"].append(method["resourceArn"])
427-
else:
470+
471+
for route in routes:
472+
resource_arn = route["resourceArn"]
473+
conditions = route.get("conditions")
474+
if conditions is not None and len(conditions) > 0:
428475
conditional_statement = self._get_empty_statement(effect)
429-
conditional_statement["Resource"].append(method["resourceArn"])
430-
conditional_statement["Condition"] = method["conditions"]
476+
conditional_statement["Resource"].append(resource_arn)
477+
conditional_statement["Condition"] = conditions
431478
statements.append(conditional_statement)
432479

480+
else:
481+
statement["Resource"].append(resource_arn)
482+
433483
if len(statement["Resource"]) > 0:
434484
statements.append(statement)
435485

@@ -442,7 +492,7 @@ def allow_all_routes(self, http_method: str = HttpVerb.ALL.value):
442492
----------
443493
http_method: str
444494
"""
445-
self._add_route(effect="Allow", verb=http_method, resource="*", conditions=[])
495+
self._add_route(effect="Allow", http_method=http_method, resource="*")
446496

447497
def deny_all_routes(self, http_method: str = HttpVerb.ALL.value):
448498
"""Adds a '*' allow to the policy to deny access to all methods of an API
@@ -452,25 +502,23 @@ def deny_all_routes(self, http_method: str = HttpVerb.ALL.value):
452502
http_method: str
453503
"""
454504

455-
self._add_route(effect="Deny", verb=http_method, resource="*", conditions=[])
505+
self._add_route(effect="Deny", http_method=http_method, resource="*")
456506

457507
def allow_route(self, http_method: str, resource: str, conditions: Optional[List[Dict]] = None):
458508
"""Adds an API Gateway method (Http verb + Resource path) to the list of allowed
459509
methods for the policy.
460510
461511
Optionally includes a condition for the policy statement. More on AWS policy
462512
conditions here: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition"""
463-
conditions = conditions or []
464-
self._add_route(effect="Allow", verb=http_method, resource=resource, conditions=conditions)
513+
self._add_route(effect="Allow", http_method=http_method, resource=resource, conditions=conditions)
465514

466515
def deny_route(self, http_method: str, resource: str, conditions: Optional[List[Dict]] = None):
467516
"""Adds an API Gateway method (Http verb + Resource path) to the list of denied
468517
methods for the policy.
469518
470519
Optionally includes a condition for the policy statement. More on AWS policy
471520
conditions here: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Condition"""
472-
conditions = conditions or []
473-
self._add_route(effect="Deny", verb=http_method, resource=resource, conditions=conditions)
521+
self._add_route(effect="Deny", http_method=http_method, resource=resource, conditions=conditions)
474522

475523
def asdict(self) -> Dict[str, Any]:
476524
"""Generates the policy document based on the internal lists of allowed and denied
@@ -482,12 +530,15 @@ def asdict(self) -> Dict[str, Any]:
482530

483531
response: Dict[str, Any] = {
484532
"principalId": self.principal_id,
485-
"policyDocument": {"Version": self.version, "Statement": []},
533+
"policyDocument": {"Version": "2012-10-17", "Statement": []},
486534
}
487535

488536
response["policyDocument"]["Statement"].extend(self._get_statement_for_effect("Allow", self._allow_routes))
489537
response["policyDocument"]["Statement"].extend(self._get_statement_for_effect("Deny", self._deny_routes))
490538

539+
if self.usage_identifier_key:
540+
response["usageIdentifierKey"] = self.usage_identifier_key
541+
491542
if self.context:
492543
response["context"] = self.context
493544

aws_lambda_powertools/utilities/idempotency/persistence/dynamodb.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ def _put_record(self, data_record: DataRecord) -> None:
121121
logger.debug(f"Putting record for idempotency key: {data_record.idempotency_key}")
122122
self.table.put_item(
123123
Item=item,
124-
ConditionExpression=f"attribute_not_exists({self.key_attr}) OR {self.expiry_attr} < :now",
124+
ConditionExpression="attribute_not_exists(#id) OR #now < :now",
125+
ExpressionAttributeNames={"#id": self.key_attr, "#now": self.expiry_attr},
125126
ExpressionAttributeValues={":now": int(now.timestamp())},
126127
)
127128
except self._ddb_resource.meta.client.exceptions.ConditionalCheckFailedException:

aws_lambda_powertools/utilities/validation/base.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ def validate_data_against_schema(data: Union[Dict, str], schema: Dict, formats:
3232
fastjsonschema.validate(definition=schema, data=data, formats=formats)
3333
except (TypeError, AttributeError, fastjsonschema.JsonSchemaDefinitionException) as e:
3434
raise InvalidSchemaFormatError(f"Schema received: {schema}, Formats: {formats}. Error: {e}")
35-
except fastjsonschema.JsonSchemaException as e:
36-
message = f"Failed schema validation. Error: {e.message}, Path: {e.path}, Data: {e.value}" # noqa: B306, E501
37-
raise SchemaValidationError(message)
35+
except fastjsonschema.JsonSchemaValueException as e:
36+
message = f"Failed schema validation. Error: {e.message}, Path: {e.path}, Data: {e.value}"
37+
raise SchemaValidationError(
38+
message,
39+
validation_message=e.message,
40+
name=e.name,
41+
path=e.path,
42+
value=e.value,
43+
definition=e.definition,
44+
rule=e.rule,
45+
rule_definition=e.rule_definition,
46+
)

aws_lambda_powertools/utilities/validation/exceptions.py

+50
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,59 @@
1+
from typing import Any, List, Optional
2+
13
from ...exceptions import InvalidEnvelopeExpressionError
24

35

46
class SchemaValidationError(Exception):
57
"""When serialization fail schema validation"""
68

9+
def __init__(
10+
self,
11+
message: str,
12+
validation_message: Optional[str] = None,
13+
name: Optional[str] = None,
14+
path: Optional[List] = None,
15+
value: Optional[Any] = None,
16+
definition: Optional[Any] = None,
17+
rule: Optional[str] = None,
18+
rule_definition: Optional[Any] = None,
19+
):
20+
"""
21+
22+
Parameters
23+
----------
24+
message : str
25+
Powertools formatted error message
26+
validation_message : str, optional
27+
Containing human-readable information what is wrong
28+
(e.g. `data.property[index] must be smaller than or equal to 42`)
29+
name : str, optional
30+
name of a path in the data structure
31+
(e.g. `data.property[index]`)
32+
path: List, optional
33+
`path` as an array in the data structure
34+
(e.g. `['data', 'property', 'index']`),
35+
value : Any, optional
36+
The invalid value
37+
definition : Any, optional
38+
The full rule `definition`
39+
(e.g. `42`)
40+
rule : str, optional
41+
`rule` which the `data` is breaking
42+
(e.g. `maximum`)
43+
rule_definition : Any, optional
44+
The specific rule `definition`
45+
(e.g. `42`)
46+
"""
47+
super().__init__(message)
48+
self.message = message
49+
self.validation_message = validation_message
50+
self.name = name
51+
self.path = path
52+
self.value = value
53+
self.definition = definition
54+
self.rule = rule
55+
self.rule_definition = rule_definition
56+
757

858
class InvalidSchemaFormatError(Exception):
959
"""When JSON Schema is in invalid format"""

0 commit comments

Comments
 (0)