Skip to content

Commit 92b1c7a

Browse files
ivica-kheitorlessa
andauthored
feat(event_sources): support for S3 Event Notifications through EventBridge (aws-powertools#2024)
Co-authored-by: heitorlessa <[email protected]>
1 parent 32b6638 commit 92b1c7a

File tree

8 files changed

+465
-276
lines changed

8 files changed

+465
-276
lines changed

.flake8

+2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ exclude = docs, .eggs, setup.py, example, .aws-sam, .git, dist, *.md, *.yaml, ex
33
ignore = E203, E266, W503, BLK100, W291, I004
44
max-line-length = 120
55
max-complexity = 15
6+
; flake8-builtins isn't honouring inline ignore (A003)
67
per-file-ignores =
78
tests/e2e/utils/data_builder/__init__.py:F401
89
tests/e2e/utils/data_fetcher/__init__.py:F401
10+
aws_lambda_powertools/utilities/data_classes/s3_event.py:A003
911

1012
[isort]
1113
multi_line_output = 3

aws_lambda_powertools/utilities/data_classes/__init__.py

+2-1
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

+135
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 # ignore shadowing built-in grammar
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

+14
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"

0 commit comments

Comments
 (0)