Skip to content

Commit b5097d1

Browse files
committed
Merge branch 'develop' into docs/versioning
* develop: fix(idempotent): Correctly raise IdempotencyKeyError (#378) feat(event-handler): Add AppSync handler decorator (#363) feat(parameter): add dynamodb_endpoint_url for local_testing (#376) fix(parser): S3Model support empty keys (#375) fix(data-classes): Add missing operationName (#373) fix: perf tests for Logger and fail str msgs feat(parser): Add S3 Object Lambda Event (#362) build(pre-commit): Add pre-commit to make pr (#368) fix(tracer): Correct type hint for MyPy (#365) fix(metrics): AttributeError raised by MediaManager and Typing and docs (#357) Signed-off-by: heitorlessa <lessa@amazon.co.uk>
2 parents 24bd872 + b558b18 commit b5097d1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+827
-388
lines changed

Diff for: Makefile

+4-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ test:
2323
coverage-html:
2424
poetry run pytest -m "not perf" --cov=aws_lambda_powertools --cov-report=html
2525

26-
pr: lint test security-baseline complexity-baseline
26+
pre-commit:
27+
pre-commit run --show-diff-on-failure
28+
29+
pr: lint pre-commit test security-baseline complexity-baseline
2730

2831
build: pr
2932
poetry build

Diff for: aws_lambda_powertools/event_handler/__init__.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""
2+
Event handler decorators for common Lambda events
3+
"""
4+
5+
from .appsync import AppSyncResolver
6+
7+
__all__ = ["AppSyncResolver"]

Diff for: aws_lambda_powertools/event_handler/appsync.py

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import logging
2+
from typing import Any, Callable
3+
4+
from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent
5+
from aws_lambda_powertools.utilities.typing import LambdaContext
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
class AppSyncResolver:
11+
"""
12+
AppSync resolver decorator
13+
14+
Example
15+
-------
16+
17+
**Sample usage**
18+
19+
from aws_lambda_powertools.event_handler import AppSyncResolver
20+
21+
app = AppSyncResolver()
22+
23+
@app.resolver(type_name="Query", field_name="listLocations")
24+
def list_locations(page: int = 0, size: int = 10) -> list:
25+
# Your logic to fetch locations with arguments passed in
26+
return [{"id": 100, "name": "Smooth Grooves"}]
27+
28+
@app.resolver(type_name="Merchant", field_name="extraInfo")
29+
def get_extra_info() -> dict:
30+
# Can use "app.current_event.source" to filter within the parent context
31+
account_type = app.current_event.source["accountType"]
32+
method = "BTC" if account_type == "NEW" else "USD"
33+
return {"preferredPaymentMethod": method}
34+
35+
@app.resolver(field_name="commonField")
36+
def common_field() -> str:
37+
# Would match all fieldNames matching 'commonField'
38+
return str(uuid.uuid4())
39+
"""
40+
41+
current_event: AppSyncResolverEvent
42+
lambda_context: LambdaContext
43+
44+
def __init__(self):
45+
self._resolvers: dict = {}
46+
47+
def resolver(self, type_name: str = "*", field_name: str = None):
48+
"""Registers the resolver for field_name
49+
50+
Parameters
51+
----------
52+
type_name : str
53+
Type name
54+
field_name : str
55+
Field name
56+
"""
57+
58+
def register_resolver(func):
59+
logger.debug(f"Adding resolver `{func.__name__}` for field `{type_name}.{field_name}`")
60+
self._resolvers[f"{type_name}.{field_name}"] = {"func": func}
61+
return func
62+
63+
return register_resolver
64+
65+
def resolve(self, event: dict, context: LambdaContext) -> Any:
66+
"""Resolve field_name
67+
68+
Parameters
69+
----------
70+
event : dict
71+
Lambda event
72+
context : LambdaContext
73+
Lambda context
74+
75+
Returns
76+
-------
77+
Any
78+
Returns the result of the resolver
79+
80+
Raises
81+
-------
82+
ValueError
83+
If we could not find a field resolver
84+
"""
85+
self.current_event = AppSyncResolverEvent(event)
86+
self.lambda_context = context
87+
resolver = self._get_resolver(self.current_event.type_name, self.current_event.field_name)
88+
return resolver(**self.current_event.arguments)
89+
90+
def _get_resolver(self, type_name: str, field_name: str) -> Callable:
91+
"""Get resolver for field_name
92+
93+
Parameters
94+
----------
95+
type_name : str
96+
Type name
97+
field_name : str
98+
Field name
99+
100+
Returns
101+
-------
102+
Callable
103+
callable function and configuration
104+
"""
105+
full_name = f"{type_name}.{field_name}"
106+
resolver = self._resolvers.get(full_name, self._resolvers.get(f"*.{field_name}"))
107+
if not resolver:
108+
raise ValueError(f"No resolver found for '{full_name}'")
109+
return resolver["func"]
110+
111+
def __call__(self, event, context) -> Any:
112+
"""Implicit lambda handler which internally calls `resolve`"""
113+
return self.resolve(event, context)

Diff for: aws_lambda_powertools/logging/lambda_context.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from typing import Any
2+
3+
14
class LambdaContextModel:
25
"""A handful of Lambda Runtime Context fields
36
@@ -31,7 +34,7 @@ def __init__(
3134
self.function_request_id = function_request_id
3235

3336

34-
def build_lambda_context_model(context: object) -> LambdaContextModel:
37+
def build_lambda_context_model(context: Any) -> LambdaContextModel:
3538
"""Captures Lambda function runtime info to be used across all log statements
3639
3740
Parameters

Diff for: aws_lambda_powertools/logging/logger.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import os
55
import random
66
import sys
7-
from typing import Any, Callable, Dict, Union
7+
from typing import Any, Callable, Dict, Optional, Union
88

99
import jmespath
1010

@@ -318,12 +318,12 @@ def set_correlation_id(self, value: str):
318318
self.structure_logs(append=True, correlation_id=value)
319319

320320
@staticmethod
321-
def _get_log_level(level: Union[str, int]) -> Union[str, int]:
321+
def _get_log_level(level: Union[str, int, None]) -> Union[str, int]:
322322
""" Returns preferred log level set by the customer in upper case """
323323
if isinstance(level, int):
324324
return level
325325

326-
log_level: str = level or os.getenv("LOG_LEVEL")
326+
log_level: Optional[str] = level or os.getenv("LOG_LEVEL")
327327
if log_level is None:
328328
return logging.INFO
329329

Diff for: aws_lambda_powertools/metrics/base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def __init__(
8888
self.service = resolve_env_var_choice(choice=service, env=os.getenv(constants.SERVICE_NAME_ENV))
8989
self._metric_units = [unit.value for unit in MetricUnit]
9090
self._metric_unit_options = list(MetricUnit.__members__)
91-
self.metadata_set = self.metadata_set if metadata_set is not None else {}
91+
self.metadata_set = metadata_set if metadata_set is not None else {}
9292

9393
def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float):
9494
"""Adds given metric

Diff for: aws_lambda_powertools/metrics/metric.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
import logging
33
from contextlib import contextmanager
4-
from typing import Dict
4+
from typing import Dict, Optional, Union
55

66
from .base import MetricManager, MetricUnit
77

@@ -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: MetricUnit, value: float):
45+
def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float):
4646
"""Method to prevent more than one metric being created
4747
4848
Parameters
@@ -109,11 +109,11 @@ def single_metric(name: str, unit: MetricUnit, value: float, namespace: str = No
109109
SchemaValidationError
110110
When metric object fails EMF schema validation
111111
"""
112-
metric_set = None
112+
metric_set: Optional[Dict] = None
113113
try:
114114
metric: SingleMetric = SingleMetric(namespace=namespace)
115115
metric.add_metric(name=name, unit=unit, value=value)
116116
yield metric
117-
metric_set: Dict = metric.serialize_metric_set()
117+
metric_set = metric.serialize_metric_set()
118118
finally:
119119
print(json.dumps(metric_set, separators=(",", ":")))

Diff for: aws_lambda_powertools/metrics/metrics.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import json
33
import logging
44
import warnings
5-
from typing import Any, Callable
5+
from typing import Any, Callable, Dict, Optional
66

77
from .base import MetricManager, MetricUnit
88
from .metric import single_metric
@@ -71,15 +71,15 @@ def do_something():
7171
When metric object fails EMF schema validation
7272
"""
7373

74-
_metrics = {}
75-
_dimensions = {}
76-
_metadata = {}
74+
_metrics: Dict[str, Any] = {}
75+
_dimensions: Dict[str, str] = {}
76+
_metadata: Dict[str, Any] = {}
7777

7878
def __init__(self, service: str = None, namespace: str = None):
7979
self.metric_set = self._metrics
8080
self.dimension_set = self._dimensions
8181
self.service = service
82-
self.namespace = namespace
82+
self.namespace: Optional[str] = namespace
8383
self.metadata_set = self._metadata
8484

8585
super().__init__(

Diff for: aws_lambda_powertools/shared/functions.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from distutils.util import strtobool
2-
from typing import Any, Union
2+
from typing import Any, Optional, Union
33

44

55
def resolve_truthy_env_var_choice(env: Any, choice: bool = None) -> bool:
@@ -22,7 +22,7 @@ def resolve_truthy_env_var_choice(env: Any, choice: bool = None) -> bool:
2222
return choice if choice is not None else strtobool(env)
2323

2424

25-
def resolve_env_var_choice(env: Any, choice: bool = None) -> Union[bool, Any]:
25+
def resolve_env_var_choice(env: Any, choice: Optional[Any] = None) -> Union[bool, Any]:
2626
"""Pick explicit choice over env, if available, otherwise return env value received
2727
2828
NOTE: Environment variable should be resolved by the caller.

Diff for: aws_lambda_powertools/tracing/tracer.py

+12-12
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ def patch(self, modules: Tuple[str] = None):
244244

245245
def capture_lambda_handler(
246246
self,
247-
lambda_handler: Callable[[Dict, Any, Optional[Dict]], Any] = None,
247+
lambda_handler: Union[Callable[[Dict, Any], Any], Callable[[Dict, Any, Optional[Dict]], Any]] = None,
248248
capture_response: Optional[bool] = None,
249249
capture_error: Optional[bool] = None,
250250
):
@@ -517,7 +517,7 @@ async def async_tasks():
517517

518518
def _decorate_async_function(
519519
self,
520-
method: Callable = None,
520+
method: Callable,
521521
capture_response: Optional[Union[bool, str]] = None,
522522
capture_error: Optional[Union[bool, str]] = None,
523523
method_name: str = None,
@@ -544,7 +544,7 @@ async def decorate(*args, **kwargs):
544544

545545
def _decorate_generator_function(
546546
self,
547-
method: Callable = None,
547+
method: Callable,
548548
capture_response: Optional[Union[bool, str]] = None,
549549
capture_error: Optional[Union[bool, str]] = None,
550550
method_name: str = None,
@@ -571,7 +571,7 @@ def decorate(*args, **kwargs):
571571

572572
def _decorate_generator_function_with_context_manager(
573573
self,
574-
method: Callable = None,
574+
method: Callable,
575575
capture_response: Optional[Union[bool, str]] = None,
576576
capture_error: Optional[Union[bool, str]] = None,
577577
method_name: str = None,
@@ -599,7 +599,7 @@ def decorate(*args, **kwargs):
599599

600600
def _decorate_sync_function(
601601
self,
602-
method: Callable = None,
602+
method: Callable,
603603
capture_response: Optional[Union[bool, str]] = None,
604604
capture_error: Optional[Union[bool, str]] = None,
605605
method_name: str = None,
@@ -654,20 +654,20 @@ def _add_response_as_metadata(
654654

655655
def _add_full_exception_as_metadata(
656656
self,
657-
method_name: str = None,
658-
error: Exception = None,
659-
subsegment: BaseSegment = None,
657+
method_name: str,
658+
error: Exception,
659+
subsegment: BaseSegment,
660660
capture_error: Optional[bool] = None,
661661
):
662662
"""Add full exception object as metadata for given subsegment
663663
664664
Parameters
665665
----------
666-
method_name : str, optional
666+
method_name : str
667667
method name to add as metadata key, by default None
668-
error : Exception, optional
668+
error : Exception
669669
error to add as subsegment metadata, by default None
670-
subsegment : BaseSegment, optional
670+
subsegment : BaseSegment
671671
existing subsegment to add metadata on, by default None
672672
capture_error : bool, optional
673673
Do not include error as metadata, by default True
@@ -717,7 +717,7 @@ def __build_config(
717717
service: str = None,
718718
disabled: bool = None,
719719
auto_patch: bool = None,
720-
patch_modules: List = None,
720+
patch_modules: Union[List, Tuple] = None,
721721
provider: BaseProvider = None,
722722
):
723723
""" Populates Tracer config for new and existing initializations """

Diff for: aws_lambda_powertools/utilities/batch/base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def failure_handler(self, record: Any, exception: Tuple):
104104

105105
@lambda_handler_decorator
106106
def batch_processor(
107-
handler: Callable, event: Dict, context: Dict, record_handler: Callable, processor: BasePartialProcessor = None
107+
handler: Callable, event: Dict, context: Dict, record_handler: Callable, processor: BasePartialProcessor
108108
):
109109
"""
110110
Middleware to handle batch event processing

Diff for: aws_lambda_powertools/utilities/batch/sqs.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def _get_queue_url(self) -> Optional[str]:
7171
Format QueueUrl from first records entry
7272
"""
7373
if not getattr(self, "records", None):
74-
return
74+
return None
7575

7676
*_, account_id, queue_name = self.records[0]["eventSourceARN"].split(":")
7777
return f"{self.client._endpoint.host}/{account_id}/{queue_name}"

Diff for: aws_lambda_powertools/utilities/data_classes/__init__.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import AppSyncResolverEvent
1+
"""
2+
Event Source Data Classes utility provides classes self-describing Lambda event sources.
3+
"""
24

35
from .alb_event import ALBEvent
46
from .api_gateway_proxy_event import APIGatewayProxyEvent, APIGatewayProxyEventV2
7+
from .appsync_resolver_event import AppSyncResolverEvent
58
from .cloud_watch_logs_event import CloudWatchLogsEvent
69
from .connect_contact_flow_event import ConnectContactFlowEvent
710
from .dynamo_db_stream_event import DynamoDBStreamEvent

Diff for: aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py

+5
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,11 @@ def route_key(self) -> Optional[str]:
195195
"""The selected route key."""
196196
return self["requestContext"].get("routeKey")
197197

198+
@property
199+
def operation_name(self) -> Optional[str]:
200+
"""The name of the operation being performed"""
201+
return self["requestContext"].get("operationName")
202+
198203

199204
class APIGatewayProxyEvent(BaseProxyEvent):
200205
"""AWS Lambda proxy V1

0 commit comments

Comments
 (0)