Skip to content

Commit 5e9b208

Browse files
committed
Merge branch 'develop' into feature-702
* develop: feat(feature-flags): improve "IN/NOT_IN"; new rule actions (aws-powertools#710) feat(idempotency): makes customers unit testing easier (aws-powertools#719) feat(feature-flags): get_raw_configuration property in Store (aws-powertools#720) feat: boto3 sessions in batch, parameters & idempotency (aws-powertools#717) feat: add get_raw_configuration property in store; expose store fix(mypy): a few return types, type signatures, and untyped areas (aws-powertools#718) docs: Terraform reference for SAR Lambda Layer (aws-powertools#716) chore(deps-dev): bump flake8-bugbear from 21.9.1 to 21.9.2 (aws-powertools#712) chore(deps): bump boto3 from 1.18.49 to 1.18.51 (aws-powertools#713) fix(idempotency): sorting keys before hashing
2 parents 1233966 + d0bd984 commit 5e9b208

File tree

33 files changed

+758
-112
lines changed

33 files changed

+758
-112
lines changed

aws_lambda_powertools/logging/formatter.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class LambdaPowertoolsFormatter(BasePowertoolsFormatter):
5858
def __init__(
5959
self,
6060
json_serializer: Optional[Callable[[Dict], str]] = None,
61-
json_deserializer: Optional[Callable[[Dict], str]] = None,
61+
json_deserializer: Optional[Callable[[Union[Dict, str, bool, int, float]], str]] = None,
6262
json_default: Optional[Callable[[Any], Any]] = None,
6363
datefmt: Optional[str] = None,
6464
log_record_order: Optional[List[str]] = None,
@@ -106,7 +106,7 @@ def __init__(
106106
self.update_formatter = self.append_keys # alias to old method
107107

108108
if self.utc:
109-
self.converter = time.gmtime
109+
self.converter = time.gmtime # type: ignore
110110

111111
super(LambdaPowertoolsFormatter, self).__init__(datefmt=self.datefmt)
112112

@@ -128,7 +128,7 @@ def format(self, record: logging.LogRecord) -> str: # noqa: A003
128128
return self.serialize(log=formatted_log)
129129

130130
def formatTime(self, record: logging.LogRecord, datefmt: Optional[str] = None) -> str:
131-
record_ts = self.converter(record.created)
131+
record_ts = self.converter(record.created) # type: ignore
132132
if datefmt:
133133
return time.strftime(datefmt, record_ts)
134134

@@ -201,7 +201,7 @@ def _extract_log_exception(self, log_record: logging.LogRecord) -> Union[Tuple[s
201201
Log record with constant traceback info and exception name
202202
"""
203203
if log_record.exc_info:
204-
return self.formatException(log_record.exc_info), log_record.exc_info[0].__name__
204+
return self.formatException(log_record.exc_info), log_record.exc_info[0].__name__ # type: ignore
205205

206206
return None, None
207207

aws_lambda_powertools/logging/logger.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ def registered_handler(self) -> logging.Handler:
361361
return handlers[0]
362362

363363
@property
364-
def registered_formatter(self) -> Optional[PowertoolsFormatter]:
364+
def registered_formatter(self) -> PowertoolsFormatter:
365365
"""Convenience property to access logger formatter"""
366366
return self.registered_handler.formatter # type: ignore
367367

@@ -405,7 +405,9 @@ def get_correlation_id(self) -> Optional[str]:
405405
str, optional
406406
Value for the correlation id
407407
"""
408-
return self.registered_formatter.log_format.get("correlation_id")
408+
if isinstance(self.registered_formatter, LambdaPowertoolsFormatter):
409+
return self.registered_formatter.log_format.get("correlation_id")
410+
return None
409411

410412
@staticmethod
411413
def _get_log_level(level: Union[str, int, None]) -> Union[str, int]:

aws_lambda_powertools/metrics/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def __init__(
9090
self._metric_unit_options = list(MetricUnit.__members__)
9191
self.metadata_set = metadata_set if metadata_set is not None else {}
9292

93-
def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float):
93+
def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float) -> None:
9494
"""Adds given metric
9595
9696
Example
@@ -215,7 +215,7 @@ def serialize_metric_set(
215215
**metric_names_and_values, # "single_metric": 1.0
216216
}
217217

218-
def add_dimension(self, name: str, value: str):
218+
def add_dimension(self, name: str, value: str) -> None:
219219
"""Adds given dimension to all metrics
220220
221221
Example
@@ -241,7 +241,7 @@ def add_dimension(self, name: str, value: str):
241241
# checking before casting improves performance in most cases
242242
self.dimension_set[name] = value if isinstance(value, str) else str(value)
243243

244-
def add_metadata(self, key: str, value: Any):
244+
def add_metadata(self, key: str, value: Any) -> None:
245245
"""Adds high cardinal metadata for metrics object
246246
247247
This will not be available during metrics visualization.

aws_lambda_powertools/metrics/metric.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class SingleMetric(MetricManager):
4242
Inherits from `aws_lambda_powertools.metrics.base.MetricManager`
4343
"""
4444

45-
def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float):
45+
def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float) -> None:
4646
"""Method to prevent more than one metric being created
4747
4848
Parameters

aws_lambda_powertools/metrics/metrics.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
import json
33
import logging
44
import warnings
5-
from typing import Any, Callable, Dict, Optional
5+
from typing import Any, Callable, Dict, Optional, Union, cast
66

7+
from ..shared.types import AnyCallableT
78
from .base import MetricManager, MetricUnit
89
from .metric import single_metric
910

@@ -87,7 +88,7 @@ def __init__(self, service: Optional[str] = None, namespace: Optional[str] = Non
8788
service=self.service,
8889
)
8990

90-
def set_default_dimensions(self, **dimensions):
91+
def set_default_dimensions(self, **dimensions) -> None:
9192
"""Persist dimensions across Lambda invocations
9293
9394
Parameters
@@ -113,10 +114,10 @@ def lambda_handler():
113114

114115
self.default_dimensions.update(**dimensions)
115116

116-
def clear_default_dimensions(self):
117+
def clear_default_dimensions(self) -> None:
117118
self.default_dimensions.clear()
118119

119-
def clear_metrics(self):
120+
def clear_metrics(self) -> None:
120121
logger.debug("Clearing out existing metric set from memory")
121122
self.metric_set.clear()
122123
self.dimension_set.clear()
@@ -125,11 +126,11 @@ def clear_metrics(self):
125126

126127
def log_metrics(
127128
self,
128-
lambda_handler: Optional[Callable[[Any, Any], Any]] = None,
129+
lambda_handler: Union[Callable[[Dict, Any], Any], Optional[Callable[[Dict, Any, Optional[Dict]], Any]]] = None,
129130
capture_cold_start_metric: bool = False,
130131
raise_on_empty_metrics: bool = False,
131132
default_dimensions: Optional[Dict[str, str]] = None,
132-
):
133+
) -> AnyCallableT:
133134
"""Decorator to serialize and publish metrics at the end of a function execution.
134135
135136
Be aware that the log_metrics **does call* the decorated function (e.g. lambda_handler).
@@ -169,11 +170,14 @@ def handler(event, context):
169170
# Return a partial function with args filled
170171
if lambda_handler is None:
171172
logger.debug("Decorator called with parameters")
172-
return functools.partial(
173-
self.log_metrics,
174-
capture_cold_start_metric=capture_cold_start_metric,
175-
raise_on_empty_metrics=raise_on_empty_metrics,
176-
default_dimensions=default_dimensions,
173+
return cast(
174+
AnyCallableT,
175+
functools.partial(
176+
self.log_metrics,
177+
capture_cold_start_metric=capture_cold_start_metric,
178+
raise_on_empty_metrics=raise_on_empty_metrics,
179+
default_dimensions=default_dimensions,
180+
),
177181
)
178182

179183
@functools.wraps(lambda_handler)
@@ -194,9 +198,9 @@ def decorate(event, context):
194198

195199
return response
196200

197-
return decorate
201+
return cast(AnyCallableT, decorate)
198202

199-
def __add_cold_start_metric(self, context: Any):
203+
def __add_cold_start_metric(self, context: Any) -> None:
200204
"""Add cold start metric and function_name dimension
201205
202206
Parameters

aws_lambda_powertools/middleware_factory/factory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def final_decorator(func: Optional[Callable] = None, **kwargs):
118118
if not inspect.isfunction(func):
119119
# @custom_middleware(True) vs @custom_middleware(log_event=True)
120120
raise MiddlewareInvalidArgumentError(
121-
f"Only keyword arguments is supported for middlewares: {decorator.__qualname__} received {func}"
121+
f"Only keyword arguments is supported for middlewares: {decorator.__qualname__} received {func}" # type: ignore # noqa: E501
122122
)
123123

124124
@functools.wraps(func)

aws_lambda_powertools/shared/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@
2121

2222
XRAY_SDK_MODULE: str = "aws_xray_sdk"
2323
XRAY_SDK_CORE_MODULE: str = "aws_xray_sdk.core"
24+
25+
IDEMPOTENCY_DISABLED_ENV: str = "POWERTOOLS_IDEMPOTENCY_DISABLED"

aws_lambda_powertools/shared/jmespath_utils.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,23 @@
66

77
import jmespath
88
from jmespath.exceptions import LexerError
9+
from jmespath.functions import Functions, signature
910

1011
from aws_lambda_powertools.exceptions import InvalidEnvelopeExpressionError
1112

1213
logger = logging.getLogger(__name__)
1314

1415

15-
class PowertoolsFunctions(jmespath.functions.Functions):
16-
@jmespath.functions.signature({"types": ["string"]})
16+
class PowertoolsFunctions(Functions):
17+
@signature({"types": ["string"]})
1718
def _func_powertools_json(self, value):
1819
return json.loads(value)
1920

20-
@jmespath.functions.signature({"types": ["string"]})
21+
@signature({"types": ["string"]})
2122
def _func_powertools_base64(self, value):
2223
return base64.b64decode(value).decode()
2324

24-
@jmespath.functions.signature({"types": ["string"]})
25+
@signature({"types": ["string"]})
2526
def _func_powertools_base64_gzip(self, value):
2627
encoded = base64.b64decode(value)
2728
uncompressed = gzip.decompress(encoded)

aws_lambda_powertools/tracing/tracer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
logger = logging.getLogger(__name__)
1818

1919
aws_xray_sdk = LazyLoader(constants.XRAY_SDK_MODULE, globals(), constants.XRAY_SDK_MODULE)
20-
aws_xray_sdk.core = LazyLoader(constants.XRAY_SDK_CORE_MODULE, globals(), constants.XRAY_SDK_CORE_MODULE)
20+
aws_xray_sdk.core = LazyLoader(constants.XRAY_SDK_CORE_MODULE, globals(), constants.XRAY_SDK_CORE_MODULE) # type: ignore # noqa: E501
2121

2222

2323
class Tracer:

aws_lambda_powertools/utilities/batch/sqs.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class PartialSQSProcessor(BasePartialProcessor):
3131
botocore config object
3232
suppress_exception: bool, optional
3333
Supress exception raised if any messages fail processing, by default False
34+
boto3_session : boto3.session.Session, optional
35+
Boto3 session to use for AWS API communication
3436
3537
3638
Example
@@ -56,12 +58,18 @@ class PartialSQSProcessor(BasePartialProcessor):
5658
5759
"""
5860

59-
def __init__(self, config: Optional[Config] = None, suppress_exception: bool = False):
61+
def __init__(
62+
self,
63+
config: Optional[Config] = None,
64+
suppress_exception: bool = False,
65+
boto3_session: Optional[boto3.session.Session] = None,
66+
):
6067
"""
6168
Initializes sqs client.
6269
"""
6370
config = config or Config()
64-
self.client = boto3.client("sqs", config=config)
71+
session = boto3_session or boto3.session.Session()
72+
self.client = session.client("sqs", config=config)
6573
self.suppress_exception = suppress_exception
6674

6775
super().__init__()
@@ -142,6 +150,7 @@ def sqs_batch_processor(
142150
record_handler: Callable,
143151
config: Optional[Config] = None,
144152
suppress_exception: bool = False,
153+
boto3_session: Optional[boto3.session.Session] = None,
145154
):
146155
"""
147156
Middleware to handle SQS batch event processing
@@ -160,6 +169,8 @@ def sqs_batch_processor(
160169
botocore config object
161170
suppress_exception: bool, optional
162171
Supress exception raised if any messages fail processing, by default False
172+
boto3_session : boto3.session.Session, optional
173+
Boto3 session to use for AWS API communication
163174
164175
Examples
165176
--------
@@ -180,7 +191,9 @@ def sqs_batch_processor(
180191
181192
"""
182193
config = config or Config()
183-
processor = PartialSQSProcessor(config=config, suppress_exception=suppress_exception)
194+
session = boto3_session or boto3.session.Session()
195+
196+
processor = PartialSQSProcessor(config=config, suppress_exception=suppress_exception, boto3_session=session)
184197

185198
records = event["Records"]
186199

aws_lambda_powertools/utilities/data_classes/sqs_event.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@ def data_type(self) -> str:
7575

7676

7777
class SQSMessageAttributes(Dict[str, SQSMessageAttribute]):
78-
def __getitem__(self, key: str) -> Optional[SQSMessageAttribute]:
78+
def __getitem__(self, key: str) -> Optional[SQSMessageAttribute]: # type: ignore
7979
item = super(SQSMessageAttributes, self).get(key)
80-
return None if item is None else SQSMessageAttribute(item)
80+
return None if item is None else SQSMessageAttribute(item) # type: ignore
8181

8282

8383
class SQSRecord(DictWrapper):

aws_lambda_powertools/utilities/feature_flags/appconfig.py

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,7 @@ def __init__(
4747
Used to log messages. If None is supplied, one will be created.
4848
"""
4949
super().__init__()
50-
if logger == None:
51-
self.logger = logging.getLogger(__name__)
52-
else:
53-
self.logger = logger
50+
self.logger = logger or logging.getLogger(__name__)
5451
self.environment = environment
5552
self.application = application
5653
self.name = name
@@ -60,9 +57,31 @@ def __init__(
6057
self.jmespath_options = jmespath_options
6158
self._conf_store = AppConfigProvider(environment=environment, application=application, config=sdk_config)
6259

60+
@property
61+
def get_raw_configuration(self) -> Dict[str, Any]:
62+
"""Fetch feature schema configuration from AWS AppConfig"""
63+
try:
64+
# parse result conf as JSON, keep in cache for self.max_age seconds
65+
return cast(
66+
dict,
67+
self._conf_store.get(
68+
name=self.name,
69+
transform=TRANSFORM_TYPE,
70+
max_age=self.cache_seconds,
71+
),
72+
)
73+
except (GetParameterError, TransformParameterError) as exc:
74+
err_msg = traceback.format_exc()
75+
if "AccessDenied" in err_msg:
76+
raise StoreClientError(err_msg) from exc
77+
raise ConfigurationStoreError("Unable to get AWS AppConfig configuration file") from exc
78+
6379
def get_configuration(self) -> Dict[str, Any]:
6480
"""Fetch feature schema configuration from AWS AppConfig
6581
82+
If envelope is set, it'll extract and return feature flags from configuration,
83+
otherwise it'll return the entire configuration fetched from AWS AppConfig.
84+
6685
Raises
6786
------
6887
ConfigurationStoreError
@@ -73,25 +92,11 @@ def get_configuration(self) -> Dict[str, Any]:
7392
Dict[str, Any]
7493
parsed JSON dictionary
7594
"""
76-
try:
77-
# parse result conf as JSON, keep in cache for self.max_age seconds
78-
config = cast(
79-
dict,
80-
self._conf_store.get(
81-
name=self.name,
82-
transform=TRANSFORM_TYPE,
83-
max_age=self.cache_seconds,
84-
),
85-
)
95+
config = self.get_raw_configuration
8696

87-
if self.envelope:
88-
config = jmespath_utils.extract_data_from_envelope(
89-
data=config, envelope=self.envelope, jmespath_options=self.jmespath_options
90-
)
97+
if self.envelope:
98+
config = jmespath_utils.extract_data_from_envelope(
99+
data=config, envelope=self.envelope, jmespath_options=self.jmespath_options
100+
)
91101

92-
return config
93-
except (GetParameterError, TransformParameterError) as exc:
94-
err_msg = traceback.format_exc()
95-
if "AccessDenied" in err_msg:
96-
raise StoreClientError(err_msg) from exc
97-
raise ConfigurationStoreError("Unable to get AWS AppConfig configuration file") from exc
102+
return config

0 commit comments

Comments
 (0)