Skip to content

Commit 4d91de5

Browse files
committed
feat(data-classes): adds support for S3 event notifications through EventBridge
1 parent 604f787 commit 4d91de5

File tree

4 files changed

+277
-1
lines changed

4 files changed

+277
-1
lines changed

aws_lambda_powertools/utilities/data_classes/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from .kinesis_firehose_event import KinesisFirehoseEvent
1717
from .kinesis_stream_event import KinesisStreamEvent
1818
from .lambda_function_url_event import LambdaFunctionUrlEvent
19-
from .s3_event import S3Event
19+
from .s3_event import S3Event, S3EventBridgeNotificationEvent
2020
from .ses_event import SESEvent
2121
from .sns_event import SNSEvent
2222
from .sqs_event import SQSEvent
@@ -37,6 +37,7 @@
3737
"KinesisStreamEvent",
3838
"LambdaFunctionUrlEvent",
3939
"S3Event",
40+
"S3EventBridgeNotificationEvent",
4041
"SESEvent",
4142
"SNSEvent",
4243
"SQSEvent",

aws_lambda_powertools/utilities/data_classes/s3_event.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
from urllib.parse import unquote_plus
33

44
from aws_lambda_powertools.utilities.data_classes.common import DictWrapper
5+
from aws_lambda_powertools.utilities.data_classes.event_bridge_event import (
6+
EventBridgeEvent,
7+
)
58

69

710
class S3Identity(DictWrapper):
@@ -16,6 +19,138 @@ def source_ip_address(self) -> str:
1619
return self["requestParameters"]["sourceIPAddress"]
1720

1821

22+
class S3EventNotificationEventBridgeBucket(DictWrapper):
23+
@property
24+
def name(self) -> str:
25+
return self["name"]
26+
27+
28+
class S3EventBridgeNotificationObject(DictWrapper):
29+
@property
30+
def key(self) -> str:
31+
"""Object key"""
32+
return unquote_plus(self["key"])
33+
34+
@property
35+
def size(self) -> str:
36+
"""Object size"""
37+
return self["size"]
38+
39+
@property
40+
def etag(self) -> str:
41+
"""Object etag"""
42+
return self["etag"]
43+
44+
@property
45+
def version_id(self) -> str:
46+
"""Object version ID"""
47+
return self["version-id"]
48+
49+
@property
50+
def sequencer(self) -> str:
51+
"""Object key"""
52+
return self["sequencer"]
53+
54+
55+
class S3EventBridgeNotificationDetail(DictWrapper):
56+
@property
57+
def version(self) -> str:
58+
"""Get the detail version"""
59+
return self["version"]
60+
61+
@property
62+
def bucket(self) -> S3EventNotificationEventBridgeBucket:
63+
"""Get the bucket name for the S3 notification"""
64+
return S3EventNotificationEventBridgeBucket(self["bucket"])
65+
66+
@property
67+
def object(self) -> S3EventBridgeNotificationObject: # noqa: A003
68+
"""Get the request-id for the S3 notification"""
69+
return S3EventBridgeNotificationObject(self["object"])
70+
71+
@property
72+
def request_id(self) -> str:
73+
"""Get the request-id for the S3 notification"""
74+
return self["request-id"]
75+
76+
@property
77+
def requester(self) -> str:
78+
"""Get the AWS account ID or AWS service principal of requester for the S3 notification"""
79+
return self["requester"]
80+
81+
@property
82+
def source_ip_address(self) -> Optional[str]:
83+
"""Get the source IP address of S3 request. Only present for events triggered by an S3 request."""
84+
return self.get("source-ip-address")
85+
86+
@property
87+
def reason(self) -> Optional[str]:
88+
"""Get the reason for the S3 notification.
89+
90+
For 'Object Created events', the S3 API used to create the object: `PutObject`, `POST Object`, `CopyObject`, or
91+
`CompleteMultipartUpload`. For 'Object Deleted' events, this is set to `DeleteObject` when an object is deleted
92+
by an S3 API call, or 'Lifecycle Expiration' when an object is deleted by an S3 Lifecycle expiration rule.
93+
"""
94+
return self.get("reason")
95+
96+
@property
97+
def deletion_type(self) -> Optional[str]:
98+
"""Get the deletion type for the S3 object in this notification.
99+
100+
For 'Object Deleted' events, when an unversioned object is deleted, or a versioned object is permanently deleted
101+
this is set to 'Permanently Deleted'. When a delete marker is created for a versioned object, this is set to
102+
'Delete Marker Created'.
103+
"""
104+
return self.get("deletion-type")
105+
106+
@property
107+
def restore_expiry_time(self) -> Optional[str]:
108+
"""Get the restore expiry time for the S3 object in this notification.
109+
110+
For 'Object Restore Completed' events, the time when the temporary copy of the object will be deleted from S3.
111+
"""
112+
return self.get("restore-expiry-time")
113+
114+
@property
115+
def source_storage_class(self) -> Optional[str]:
116+
"""Get the source storage class of the S3 object in this notification.
117+
118+
For 'Object Restore Initiated' and 'Object Restore Completed' events, the storage class of the object being
119+
restored.
120+
"""
121+
return self.get("source-storage-class")
122+
123+
@property
124+
def destination_storage_class(self) -> Optional[str]:
125+
"""Get the destination storage class of the S3 object in this notification.
126+
127+
For 'Object Storage Class Changed' events, the new storage class of the object.
128+
"""
129+
return self.get("destination-storage-class")
130+
131+
@property
132+
def destination_access_tier(self) -> Optional[str]:
133+
"""Get the destination access tier of the S3 object in this notification.
134+
135+
For 'Object Access Tier Changed' events, the new access tier of the object.
136+
"""
137+
return self.get("destination-access-tier")
138+
139+
140+
class S3EventBridgeNotificationEvent(EventBridgeEvent):
141+
"""Amazon S3EventBridge Event
142+
143+
Documentation:
144+
--------------
145+
- https://docs.aws.amazon.com/AmazonS3/latest/userguide/ev-events.html
146+
"""
147+
148+
@property
149+
def detail(self) -> S3EventBridgeNotificationDetail: # type: ignore[override]
150+
"""S3 notification details"""
151+
return S3EventBridgeNotificationDetail(self["detail"])
152+
153+
19154
class S3Bucket(DictWrapper):
20155
@property
21156
def name(self) -> str:

docs/utilities/data_classes.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ Same example as above, but using the `event_source` decorator
8282
| [Rabbit MQ](#rabbit-mq) | `RabbitMQEvent` |
8383
| [S3](#s3) | `S3Event` |
8484
| [S3 Object Lambda](#s3-object-lambda) | `S3ObjectLambdaEvent` |
85+
| [S3 EventBridge Notification](#s3-eventbridge-notification) | `S3EventBridgeNotificationEvent` |
8586
| [SES](#ses) | `SESEvent` |
8687
| [SNS](#sns) | `SNSEvent` |
8788
| [SQS](#sqs) | `SQSEvent` |
@@ -1043,6 +1044,19 @@ This example is based on the AWS Blog post [Introducing Amazon S3 Object Lambda
10431044
return {"status_code": 200}
10441045
```
10451046

1047+
### S3 EventBridge Notification
1048+
1049+
=== "app.py"
1050+
1051+
```python
1052+
from aws_lambda_powertools.utilities.data_classes import event_source, S3EventBridgeNotificationEvent
1053+
1054+
@event_source(data_class=S3EventBridgeNotificationEvent)
1055+
def lambda_handler(event: S3EventBridgeNotificationEvent, context):
1056+
bucket_name = event.detail.bucket.name
1057+
file_key = event.detail.object.key
1058+
```
1059+
10461060
### SES
10471061

10481062
=== "app.py"
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from aws_lambda_powertools.utilities.data_classes.s3_event import (
2+
S3EventBridgeNotificationDetail,
3+
S3EventBridgeNotificationEvent,
4+
S3EventBridgeNotificationObject,
5+
)
6+
from tests.functional.utils import load_event
7+
8+
9+
def test_s3_eventbridge_notification_detail_parsed_object_created():
10+
event = S3EventBridgeNotificationEvent(load_event("s3EventBridgeNotificationObjectCreatedEvent.json"))
11+
bucket_name = "example-bucket"
12+
deletion_type = None
13+
destination_access_tier = None
14+
destination_storage_class = None
15+
detail: S3EventBridgeNotificationDetail = event.detail
16+
_object: S3EventBridgeNotificationObject = detail.object
17+
reason = "PutObject"
18+
request_id = "57H08PA84AB1JZW0"
19+
requester = "123456789012"
20+
restore_expiry_time = None
21+
source_ip_address = "34.252.34.74"
22+
source_storage_class = None
23+
version = "0"
24+
25+
assert bucket_name == event.detail.bucket.name
26+
assert deletion_type == event.detail.deletion_type
27+
assert destination_access_tier == event.detail.destination_access_tier
28+
assert destination_storage_class == event.detail.destination_storage_class
29+
assert _object == event.detail.object
30+
assert reason == event.detail.reason
31+
assert request_id == event.detail.request_id
32+
assert requester == event.detail.requester
33+
assert restore_expiry_time == event.detail.restore_expiry_time
34+
assert source_ip_address == event.detail.source_ip_address
35+
assert source_storage_class == event.detail.source_storage_class
36+
assert version == event.version
37+
38+
39+
def test_s3_eventbridge_notification_detail_parsed_object_deleted():
40+
event = S3EventBridgeNotificationEvent(load_event("s3EventBridgeNotificationObjectDeletedEvent.json"))
41+
bucket_name = "example-bucket"
42+
deletion_type = "Delete Marker Created"
43+
destination_access_tier = None
44+
destination_storage_class = None
45+
detail: S3EventBridgeNotificationDetail = event.detail
46+
_object: S3EventBridgeNotificationObject = detail.object
47+
reason = "DeleteObject"
48+
request_id = "0BH729840619AG5K"
49+
requester = "123456789012"
50+
restore_expiry_time = None
51+
source_ip_address = "34.252.34.74"
52+
source_storage_class = None
53+
version = "0"
54+
55+
assert bucket_name == event.detail.bucket.name
56+
assert deletion_type == event.detail.deletion_type
57+
assert destination_access_tier == event.detail.destination_access_tier
58+
assert destination_storage_class == event.detail.destination_storage_class
59+
assert _object == event.detail.object
60+
assert reason == event.detail.reason
61+
assert request_id == event.detail.request_id
62+
assert requester == event.detail.requester
63+
assert restore_expiry_time == event.detail.restore_expiry_time
64+
assert source_ip_address == event.detail.source_ip_address
65+
assert source_storage_class == event.detail.source_storage_class
66+
assert version == event.version
67+
68+
69+
def test_s3_eventbridge_notification_detail_parsed_object_expired():
70+
event = S3EventBridgeNotificationEvent(load_event("s3EventBridgeNotificationObjectExpiredEvent.json"))
71+
bucket_name = "example-bucket"
72+
deletion_type = "Delete Marker Created"
73+
destination_access_tier = None
74+
destination_storage_class = None
75+
detail: S3EventBridgeNotificationDetail = event.detail
76+
_object: S3EventBridgeNotificationObject = detail.object
77+
reason = "Lifecycle Expiration"
78+
request_id = "20EB74C14654DC47"
79+
requester = "s3.amazonaws.com"
80+
restore_expiry_time = None
81+
source_ip_address = None
82+
source_storage_class = None
83+
version = "0"
84+
85+
assert bucket_name == event.detail.bucket.name
86+
assert deletion_type == event.detail.deletion_type
87+
assert destination_access_tier == event.detail.destination_access_tier
88+
assert destination_storage_class == event.detail.destination_storage_class
89+
assert _object == event.detail.object
90+
assert reason == event.detail.reason
91+
assert request_id == event.detail.request_id
92+
assert requester == event.detail.requester
93+
assert restore_expiry_time == event.detail.restore_expiry_time
94+
assert source_ip_address == event.detail.source_ip_address
95+
assert source_storage_class == event.detail.source_storage_class
96+
assert version == event.version
97+
98+
99+
def test_s3_eventbridge_notification_detail_parsed_object_restore_completed():
100+
event = S3EventBridgeNotificationEvent(load_event("s3EventBridgeNotificationObjectRestoreCompletedEvent.json"))
101+
bucket_name = "example-bucket"
102+
deletion_type = None
103+
destination_access_tier = None
104+
destination_storage_class = None
105+
detail: S3EventBridgeNotificationDetail = event.detail
106+
_object: S3EventBridgeNotificationObject = detail.object
107+
reason = None
108+
request_id = "189F19CB7FB1B6A4"
109+
requester = "s3.amazonaws.com"
110+
restore_expiry_time = "2021-11-13T00:00:00Z"
111+
source_ip_address = None
112+
source_storage_class = "GLACIER"
113+
version = "0"
114+
115+
assert bucket_name == event.detail.bucket.name
116+
assert deletion_type == event.detail.deletion_type
117+
assert destination_access_tier == event.detail.destination_access_tier
118+
assert destination_storage_class == event.detail.destination_storage_class
119+
assert _object == event.detail.object
120+
assert reason == event.detail.reason
121+
assert request_id == event.detail.request_id
122+
assert requester == event.detail.requester
123+
assert restore_expiry_time == event.detail.restore_expiry_time
124+
assert source_ip_address == event.detail.source_ip_address
125+
assert source_storage_class == event.detail.source_storage_class
126+
assert version == event.version

0 commit comments

Comments
 (0)