From c5394f5951b6437bc91f6eec9150b77b3935312f Mon Sep 17 00:00:00 2001
From: Adam Tankanow <adam.tankanow@gmail.com>
Date: Mon, 12 Sep 2022 13:21:37 -0400
Subject: [PATCH 1/2] ISSUE-1503: ISSUE-1503: ISSUE-1503: fix: add Mapping abc
 and missing methods to DictWrapper

ISSUE-1503: Add DictWrapper Mapping abc tests

ISSUE-1503: add StreamRecord tests
---
 .../utilities/data_classes/common.py          | 10 ++++--
 .../data_classes/dynamo_db_stream_event.py    |  4 ++-
 tests/functional/test_data_classes.py         | 31 +++++++++++++++++++
 3 files changed, 42 insertions(+), 3 deletions(-)

diff --git a/aws_lambda_powertools/utilities/data_classes/common.py b/aws_lambda_powertools/utilities/data_classes/common.py
index 2109ee3dd3e..83c5c87b216 100644
--- a/aws_lambda_powertools/utilities/data_classes/common.py
+++ b/aws_lambda_powertools/utilities/data_classes/common.py
@@ -1,9 +1,9 @@
 import base64
 import json
-from typing import Any, Dict, Optional
+from typing import Any, Dict, Iterator, Mapping, Optional
 
 
-class DictWrapper:
+class DictWrapper(Mapping):
     """Provides a single read only access to a wrapper dict"""
 
     def __init__(self, data: Dict[str, Any]):
@@ -19,6 +19,12 @@ def __eq__(self, other: Any) -> bool:
 
         return self._data == other._data
 
+    def __iter__(self) -> Iterator:
+        return iter(self._data)
+
+    def __len__(self) -> int:
+        return len(self._data)
+
     def get(self, key: str, default: Optional[Any] = None) -> Optional[Any]:
         return self._data.get(key, default)
 
diff --git a/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py b/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py
index 7e209fab3e2..28bbffdb510 100644
--- a/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py
+++ b/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py
@@ -182,8 +182,10 @@ def approximate_creation_date_time(self) -> Optional[int]:
         item = self.get("ApproximateCreationDateTime")
         return None if item is None else int(item)
 
+    # This override breaks the Mapping protocol of DictWrapper, it's left here for backwards compatibility with
+    # a 'type: ignore' comment. This is currently the only subclass of DictWrapper that breaks this protocol.
     @property
-    def keys(self) -> Optional[Dict[str, AttributeValue]]:
+    def keys(self) -> Optional[Dict[str, AttributeValue]]:  # type: ignore
         """The primary key attribute(s) for the DynamoDB item that was modified."""
         return _attribute_value_dict(self._data, "Keys")
 
diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py
index dbef57162e2..1ff0e32e7c4 100644
--- a/tests/functional/test_data_classes.py
+++ b/tests/functional/test_data_classes.py
@@ -74,6 +74,7 @@
     AttributeValueType,
     DynamoDBRecordEventName,
     DynamoDBStreamEvent,
+    StreamRecord,
     StreamViewType,
 )
 from aws_lambda_powertools.utilities.data_classes.event_source import event_source
@@ -101,6 +102,19 @@ def message(self) -> str:
     assert DataClassSample(data1).raw_event is data1
 
 
+def test_dict_wrapper_imlements_mapping():
+    class DataClassSample(DictWrapper):
+        pass
+
+    data = {"message": "foo1"}
+    dcs = DataClassSample(data)
+    assert len(dcs) == len(data)
+    assert list(dcs) == list(data)
+    assert dcs.keys() == data.keys()
+    assert list(dcs.values()) == list(data.values())
+    assert dcs.items() == data.items()
+
+
 def test_cloud_watch_dashboard_event():
     event = CloudWatchDashboardCustomWidgetEvent(load_event("cloudWatchDashboardEvent.json"))
     assert event.describe is False
@@ -617,6 +631,23 @@ def test_dynamo_attribute_value_type_error():
         print(attribute_value.get_type)
 
 
+def test_stream_record_keys_with_valid_keys():
+    attribute_value = {"Foo": "Bar"}
+    sr = StreamRecord({"Keys": {"Key1": attribute_value}})
+    assert sr.keys == {"Key1": AttributeValue(attribute_value)}
+
+
+def test_stream_record_keys_with_no_keys():
+    sr = StreamRecord({})
+    assert sr.keys is None
+
+
+def test_stream_record_keys_overrides_dict_wrapper_keys():
+    data = {"Keys": {"key1": {"attr1": "value1"}}}
+    sr = StreamRecord(data)
+    assert sr.keys != data.keys()
+
+
 def test_event_bridge_event():
     event = EventBridgeEvent(load_event("eventBridgeEvent.json"))
 

From 9875e224b20fd4486f5e94928775918c891a3053 Mon Sep 17 00:00:00 2001
From: Adam Tankanow <adam.tankanow@gmail.com>
Date: Wed, 28 Sep 2022 09:34:02 -0400
Subject: [PATCH 2/2] Apply suggestions from code review

Co-authored-by: Heitor Lessa <lessa@amazon.nl>
---
 .../utilities/data_classes/common.py          |  3 ++-
 .../data_classes/dynamo_db_stream_event.py    |  6 ++---
 tests/functional/test_data_classes.py         | 26 +++++++++----------
 3 files changed, 18 insertions(+), 17 deletions(-)

diff --git a/aws_lambda_powertools/utilities/data_classes/common.py b/aws_lambda_powertools/utilities/data_classes/common.py
index 83c5c87b216..1b671489cdd 100644
--- a/aws_lambda_powertools/utilities/data_classes/common.py
+++ b/aws_lambda_powertools/utilities/data_classes/common.py
@@ -1,6 +1,7 @@
 import base64
 import json
-from typing import Any, Dict, Iterator, Mapping, Optional
+from collections.abc import Mapping
+from typing import Any, Dict, Iterator, Optional
 
 
 class DictWrapper(Mapping):
diff --git a/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py b/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py
index 28bbffdb510..eb674c86b60 100644
--- a/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py
+++ b/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py
@@ -182,10 +182,10 @@ def approximate_creation_date_time(self) -> Optional[int]:
         item = self.get("ApproximateCreationDateTime")
         return None if item is None else int(item)
 
-    # This override breaks the Mapping protocol of DictWrapper, it's left here for backwards compatibility with
-    # a 'type: ignore' comment. This is currently the only subclass of DictWrapper that breaks this protocol.
+    # NOTE: This override breaks the Mapping protocol of DictWrapper, it's left here for backwards compatibility with
+    # a 'type: ignore' comment. See #1516 for discussion
     @property
-    def keys(self) -> Optional[Dict[str, AttributeValue]]:  # type: ignore
+    def keys(self) -> Optional[Dict[str, AttributeValue]]:  # type: ignore[override]
         """The primary key attribute(s) for the DynamoDB item that was modified."""
         return _attribute_value_dict(self._data, "Keys")
 
diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py
index 1ff0e32e7c4..f0ac4af0af0 100644
--- a/tests/functional/test_data_classes.py
+++ b/tests/functional/test_data_classes.py
@@ -102,17 +102,17 @@ def message(self) -> str:
     assert DataClassSample(data1).raw_event is data1
 
 
-def test_dict_wrapper_imlements_mapping():
+def test_dict_wrapper_implements_mapping():
     class DataClassSample(DictWrapper):
         pass
 
     data = {"message": "foo1"}
-    dcs = DataClassSample(data)
-    assert len(dcs) == len(data)
-    assert list(dcs) == list(data)
-    assert dcs.keys() == data.keys()
-    assert list(dcs.values()) == list(data.values())
-    assert dcs.items() == data.items()
+    event_source = DataClassSample(data)
+    assert len(event_source) == len(data)
+    assert list(event_source) == list(data)
+    assert event_source.keys() == data.keys()
+    assert list(event_source.values()) == list(data.values())
+    assert event_source.items() == data.items()
 
 
 def test_cloud_watch_dashboard_event():
@@ -633,19 +633,19 @@ def test_dynamo_attribute_value_type_error():
 
 def test_stream_record_keys_with_valid_keys():
     attribute_value = {"Foo": "Bar"}
-    sr = StreamRecord({"Keys": {"Key1": attribute_value}})
-    assert sr.keys == {"Key1": AttributeValue(attribute_value)}
+    record = StreamRecord({"Keys": {"Key1": attribute_value}})
+    assert record.keys == {"Key1": AttributeValue(attribute_value)}
 
 
 def test_stream_record_keys_with_no_keys():
-    sr = StreamRecord({})
-    assert sr.keys is None
+    record = StreamRecord({})
+    assert record.keys is None
 
 
 def test_stream_record_keys_overrides_dict_wrapper_keys():
     data = {"Keys": {"key1": {"attr1": "value1"}}}
-    sr = StreamRecord(data)
-    assert sr.keys != data.keys()
+    record = StreamRecord(data)
+    assert record.keys != data.keys()
 
 
 def test_event_bridge_event():