32
32
33
33
from nitric .application import Nitric
34
34
from nitric .bidi import AsyncNotifierList
35
- from nitric .context import BucketNotificationContext , BucketNotificationHandler , BucketNotifyRequest , FunctionServer
35
+ from nitric .context import FunctionServer , Handler , Middleware
36
36
from nitric .exception import InvalidArgumentException , exception_from_grpc_error
37
37
from nitric .proto .resources .v1 import Action , ResourceDeclareRequest , ResourceIdentifier , ResourceType
38
38
from nitric .proto .storage .v1 import (
39
+ BlobEventRequest ,
39
40
BlobEventResponse ,
40
41
BlobEventType ,
41
42
ClientMessage ,
54
55
from nitric .utils import new_default_channel
55
56
56
57
58
+ class BucketNotifyRequest :
59
+ """Represents a translated Event, from a subscribed bucket notification, forwarded from the Nitric Membrane."""
60
+
61
+ bucket_name : str
62
+ key : str
63
+ notification_type : BlobEventType
64
+ bucket : BucketRef
65
+ file : FileRef
66
+
67
+ def __init__ (self , bucket_name : str , key : str , notification_type : BlobEventType ):
68
+ """Construct a new BucketNotifyRequest."""
69
+ self .bucket_name = bucket_name
70
+ self .key = key
71
+ self .notification_type = notification_type
72
+ self .bucket = BucketRef (bucket_name )
73
+ self .file = self .bucket .file (key )
74
+
75
+
76
+ class BucketNotifyResponse :
77
+ """Represents the response to a trigger from a Bucket."""
78
+
79
+ def __init__ (self , success : bool = True ):
80
+ """Construct a new BucketNotificationResponse."""
81
+ self .success = success
82
+
83
+
84
+ class BucketNotificationContext :
85
+ """Represents the full request/response context for a bucket notification trigger."""
86
+
87
+ def __init__ (self , request : BucketNotifyRequest , response : Optional [BucketNotifyResponse ] = None ):
88
+ """Construct a new BucketNotificationContext."""
89
+ self .req = request
90
+ self .res = response if response else BucketNotifyResponse ()
91
+
92
+
93
+ class FileNotifyRequest (BucketNotifyRequest ):
94
+ """Represents a translated Event, from a subscribed bucket notification, forwarded from the Nitric Membrane."""
95
+
96
+ def __init__ (
97
+ self ,
98
+ bucket_name : str ,
99
+ bucket_ref : BucketRef ,
100
+ key : str ,
101
+ notification_type : BlobEventType ,
102
+ ):
103
+ """Construct a new FileNotificationRequest."""
104
+ super ().__init__ (bucket_name = bucket_name , key = key , notification_type = notification_type )
105
+ self .file = bucket_ref .file (key )
106
+
107
+
108
+ class FileNotificationContext (BucketNotificationContext ):
109
+ """Represents the full request/response context for a bucket notification trigger."""
110
+
111
+ def __init__ (self , request : FileNotifyRequest , response : Optional [BucketNotifyResponse ] = None ):
112
+ """Construct a new FileNotificationContext."""
113
+ super ().__init__ (request = request , response = response )
114
+ self .req = request
115
+
116
+ @staticmethod
117
+ def _from_client_message_with_bucket (msg : BlobEventRequest , bucket_ref ) -> FileNotificationContext :
118
+ """Construct a new FileNotificationTrigger from a Bucket Notification trigger from the Nitric Membrane."""
119
+ return FileNotificationContext (
120
+ request = FileNotifyRequest (
121
+ bucket_name = msg .bucket_name ,
122
+ key = msg .blob_event .key ,
123
+ bucket_ref = bucket_ref ,
124
+ notification_type = msg .blob_event .type ,
125
+ )
126
+ )
127
+
128
+
129
+ BucketNotificationMiddleware = Middleware [BucketNotificationContext ]
130
+ BucketNotificationHandler = Handler [BucketNotificationContext ]
131
+
132
+ FileNotificationMiddleware = Middleware [FileNotificationContext ]
133
+ FileNotificationHandler = Handler [FileNotificationContext ]
134
+
135
+
57
136
class BucketRef (object ):
58
137
"""A reference to a deployed storage bucket, used to interact with the bucket at runtime."""
59
138
@@ -90,6 +169,21 @@ async def exists(self, key: str) -> bool:
90
169
)
91
170
return resp .exists
92
171
172
+ def on (
173
+ self , notification_type : str , notification_prefix_filter : str
174
+ ) -> Callable [[BucketNotificationHandler ], None ]:
175
+ """Create and return a bucket notification decorator for this bucket."""
176
+
177
+ def decorator (func : BucketNotificationHandler ) -> None :
178
+ Listener (
179
+ bucket_name = self .name ,
180
+ notification_type = notification_type ,
181
+ notification_prefix_filter = notification_prefix_filter ,
182
+ handler = func ,
183
+ )
184
+
185
+ return decorator
186
+
93
187
94
188
class FileMode (Enum ):
95
189
"""Definition of available operation modes for file signed URLs."""
@@ -121,7 +215,7 @@ async def write(self, body: bytes):
121
215
Will create the file if it doesn't already exist.
122
216
"""
123
217
try :
124
- await self ._bucket ._storage_stub .write ( # type: ignore pylint: disable=protected-access
218
+ await self ._bucket ._storage_stub .write (
125
219
storage_write_request = StorageWriteRequest (bucket_name = self ._bucket .name , key = self .key , body = body )
126
220
)
127
221
except GRPCError as grpc_err :
@@ -130,7 +224,7 @@ async def write(self, body: bytes):
130
224
async def read (self ) -> bytes :
131
225
"""Read this files contents from the bucket."""
132
226
try :
133
- response = await self ._bucket ._storage_stub .read ( # type: ignore pylint: disable=protected-access
227
+ response = await self ._bucket ._storage_stub .read (
134
228
storage_read_request = StorageReadRequest (bucket_name = self ._bucket .name , key = self .key )
135
229
)
136
230
return response .body
@@ -140,7 +234,7 @@ async def read(self) -> bytes:
140
234
async def delete (self ):
141
235
"""Delete this file from the bucket."""
142
236
try :
143
- await self ._bucket ._storage_stub .delete ( # type: ignore pylint: disable=protected-access
237
+ await self ._bucket ._storage_stub .delete (
144
238
storage_delete_request = StorageDeleteRequest (bucket_name = self ._bucket .name , key = self .key )
145
239
)
146
240
except GRPCError as grpc_err :
@@ -150,27 +244,33 @@ async def upload_url(self, expiry: Optional[Union[timedelta, int]] = None):
150
244
"""
151
245
Get a temporary writable URL to this file.
152
246
153
- Parameters:
154
-
155
- expiry (timedelta or int, optional): The expiry time for the signed URL.
156
- If an integer is provided, it is treated as seconds. Default is 600 seconds.
247
+ Parameters
248
+ ----------
249
+ expiry : int, timedelta, optional
250
+ The expiry time for the signed URL.
251
+ If an integer is provided, it is treated as seconds. Default is 600 seconds.
157
252
158
- Returns:
253
+ Returns
254
+ -------
159
255
str: The signed URL.
256
+
160
257
"""
161
258
return await self ._sign_url (mode = FileMode .WRITE , expiry = expiry )
162
259
163
260
async def download_url (self , expiry : Optional [Union [timedelta , int ]] = None ):
164
261
"""
165
262
Get a temporary readable URL to this file.
166
263
167
- Parameters:
168
-
169
- expiry (timedelta or int, optional): The expiry time for the signed URL.
170
- If an integer is provided, it is treated as seconds. Default is 600 seconds.
264
+ Parameters
265
+ ----------
266
+ expiry : int, timedelta, optional
267
+ The expiry time for the signed URL.
268
+ If an integer is provided, it is treated as seconds. Default is 600 seconds.
171
269
172
- Returns:
270
+ Returns
271
+ -------
173
272
str: The signed URL.
273
+
174
274
"""
175
275
return await self ._sign_url (mode = FileMode .READ , expiry = expiry )
176
276
@@ -182,7 +282,7 @@ async def _sign_url(self, mode: FileMode = FileMode.READ, expiry: Optional[Union
182
282
expiry = timedelta (seconds = expiry )
183
283
184
284
try :
185
- response = await self ._bucket ._storage_stub .pre_sign_url ( # type: ignore pylint: disable=protected-access
285
+ response = await self ._bucket ._storage_stub .pre_sign_url (
186
286
storage_pre_sign_url_request = StoragePreSignUrlRequest (
187
287
bucket_name = self ._bucket .name , key = self .key , operation = mode .to_request_operation (), expiry = expiry
188
288
)
@@ -257,7 +357,7 @@ def _perms_to_actions(self, *args: BucketPermission) -> List[Action]:
257
357
return [action for perm in args for action in permission_actions_map [perm ]]
258
358
259
359
def _to_resource_id (self ) -> ResourceIdentifier :
260
- return ResourceIdentifier (name = self .name , type = ResourceType .Bucket ) # type:ignore
360
+ return ResourceIdentifier (name = self .name , type = ResourceType .Bucket )
261
361
262
362
def allow (
263
363
self ,
@@ -316,6 +416,7 @@ def __init__(
316
416
key_prefix_filter = notification_prefix_filter ,
317
417
)
318
418
419
+ # noinspection PyProtectedMember
319
420
Nitric ._register_worker (self )
320
421
321
422
async def _listener_request_iterator (self ):
@@ -359,9 +460,12 @@ async def start(self) -> None:
359
460
print (f"Stream terminated: { e .message } " )
360
461
except grpclib .exceptions .StreamTerminatedError :
361
462
print ("Stream from membrane closed." )
463
+ except KeyboardInterrupt :
464
+ print ("Keyboard interrupt" )
362
465
finally :
363
466
print ("Closing client stream" )
364
467
channel .close ()
468
+ print ("Listener stopped" )
365
469
366
470
367
471
def bucket (name : str ) -> Bucket :
@@ -370,4 +474,4 @@ def bucket(name: str) -> Bucket:
370
474
371
475
If a bucket has already been registered with the same name, the original reference will be reused.
372
476
"""
373
- return Nitric ._create_resource (Bucket , name ) # type: ignore pylint: disable=protected-access
477
+ return Nitric ._create_resource (Bucket , name )
0 commit comments