1# Copyright 2017 Google LLC 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Support for bucket notification resources.""" 16 17import re 18 19from google.api_core.exceptions import NotFound 20 21from google.cloud.storage.constants import _DEFAULT_TIMEOUT 22from google.cloud.storage.retry import DEFAULT_RETRY 23 24 25OBJECT_FINALIZE_EVENT_TYPE = "OBJECT_FINALIZE" 26OBJECT_METADATA_UPDATE_EVENT_TYPE = "OBJECT_METADATA_UPDATE" 27OBJECT_DELETE_EVENT_TYPE = "OBJECT_DELETE" 28OBJECT_ARCHIVE_EVENT_TYPE = "OBJECT_ARCHIVE" 29 30JSON_API_V1_PAYLOAD_FORMAT = "JSON_API_V1" 31NONE_PAYLOAD_FORMAT = "NONE" 32 33_TOPIC_REF_FMT = "//pubsub.googleapis.com/projects/{}/topics/{}" 34_PROJECT_PATTERN = r"(?P<project>[a-z][a-z0-9-]{4,28}[a-z0-9])" 35_TOPIC_NAME_PATTERN = r"(?P<name>[A-Za-z](\w|[-_.~+%])+)" 36_TOPIC_REF_PATTERN = _TOPIC_REF_FMT.format(_PROJECT_PATTERN, _TOPIC_NAME_PATTERN) 37_TOPIC_REF_RE = re.compile(_TOPIC_REF_PATTERN) 38_BAD_TOPIC = ( 39 "Resource has invalid topic: {}; see " 40 "https://cloud.google.com/storage/docs/json_api/v1/" 41 "notifications/insert#topic" 42) 43 44 45class BucketNotification(object): 46 """Represent a single notification resource for a bucket. 47 48 See: https://cloud.google.com/storage/docs/json_api/v1/notifications 49 50 :type bucket: :class:`google.cloud.storage.bucket.Bucket` 51 :param bucket: Bucket to which the notification is bound. 52 53 :type topic_name: str 54 :param topic_name: 55 (Optional) Topic name to which notifications are published. 56 57 :type topic_project: str 58 :param topic_project: 59 (Optional) Project ID of topic to which notifications are published. 60 If not passed, uses the project ID of the bucket's client. 61 62 :type custom_attributes: dict 63 :param custom_attributes: 64 (Optional) Additional attributes passed with notification events. 65 66 :type event_types: list(str) 67 :param event_types: 68 (Optional) Event types for which notification events are published. 69 70 :type blob_name_prefix: str 71 :param blob_name_prefix: 72 (Optional) Prefix of blob names for which notification events are 73 published. 74 75 :type payload_format: str 76 :param payload_format: 77 (Optional) Format of payload for notification events. 78 79 :type notification_id: str 80 :param notification_id: 81 (Optional) The ID of the notification. 82 """ 83 84 def __init__( 85 self, 86 bucket, 87 topic_name=None, 88 topic_project=None, 89 custom_attributes=None, 90 event_types=None, 91 blob_name_prefix=None, 92 payload_format=NONE_PAYLOAD_FORMAT, 93 notification_id=None, 94 ): 95 self._bucket = bucket 96 self._topic_name = topic_name 97 98 if topic_project is None: 99 topic_project = bucket.client.project 100 101 if topic_project is None: 102 raise ValueError("Client project not set: pass an explicit topic_project.") 103 104 self._topic_project = topic_project 105 106 self._properties = {} 107 108 if custom_attributes is not None: 109 self._properties["custom_attributes"] = custom_attributes 110 111 if event_types is not None: 112 self._properties["event_types"] = event_types 113 114 if blob_name_prefix is not None: 115 self._properties["object_name_prefix"] = blob_name_prefix 116 117 if notification_id is not None: 118 self._properties["id"] = notification_id 119 120 self._properties["payload_format"] = payload_format 121 122 @classmethod 123 def from_api_repr(cls, resource, bucket): 124 """Construct an instance from the JSON repr returned by the server. 125 126 See: https://cloud.google.com/storage/docs/json_api/v1/notifications 127 128 :type resource: dict 129 :param resource: JSON repr of the notification 130 131 :type bucket: :class:`google.cloud.storage.bucket.Bucket` 132 :param bucket: Bucket to which the notification is bound. 133 134 :rtype: :class:`BucketNotification` 135 :returns: the new notification instance 136 """ 137 topic_path = resource.get("topic") 138 if topic_path is None: 139 raise ValueError("Resource has no topic") 140 141 name, project = _parse_topic_path(topic_path) 142 instance = cls(bucket, name, topic_project=project) 143 instance._properties = resource 144 145 return instance 146 147 @property 148 def bucket(self): 149 """Bucket to which the notification is bound.""" 150 return self._bucket 151 152 @property 153 def topic_name(self): 154 """Topic name to which notifications are published.""" 155 return self._topic_name 156 157 @property 158 def topic_project(self): 159 """Project ID of topic to which notifications are published. 160 """ 161 return self._topic_project 162 163 @property 164 def custom_attributes(self): 165 """Custom attributes passed with notification events. 166 """ 167 return self._properties.get("custom_attributes") 168 169 @property 170 def event_types(self): 171 """Event types for which notification events are published. 172 """ 173 return self._properties.get("event_types") 174 175 @property 176 def blob_name_prefix(self): 177 """Prefix of blob names for which notification events are published. 178 """ 179 return self._properties.get("object_name_prefix") 180 181 @property 182 def payload_format(self): 183 """Format of payload of notification events.""" 184 return self._properties.get("payload_format") 185 186 @property 187 def notification_id(self): 188 """Server-set ID of notification resource.""" 189 return self._properties.get("id") 190 191 @property 192 def etag(self): 193 """Server-set ETag of notification resource.""" 194 return self._properties.get("etag") 195 196 @property 197 def self_link(self): 198 """Server-set ETag of notification resource.""" 199 return self._properties.get("selfLink") 200 201 @property 202 def client(self): 203 """The client bound to this notfication.""" 204 return self.bucket.client 205 206 @property 207 def path(self): 208 """The URL path for this notification.""" 209 return "/b/{}/notificationConfigs/{}".format( 210 self.bucket.name, self.notification_id 211 ) 212 213 def _require_client(self, client): 214 """Check client or verify over-ride. 215 216 :type client: :class:`~google.cloud.storage.client.Client` or 217 ``NoneType`` 218 :param client: the client to use. 219 220 :rtype: :class:`google.cloud.storage.client.Client` 221 :returns: The client passed in or the bucket's client. 222 """ 223 if client is None: 224 client = self.client 225 return client 226 227 def _set_properties(self, response): 228 """Helper for :meth:`reload`. 229 230 :type response: dict 231 :param response: resource mapping from server 232 """ 233 self._properties.clear() 234 self._properties.update(response) 235 236 def create(self, client=None, timeout=_DEFAULT_TIMEOUT, retry=None): 237 """API wrapper: create the notification. 238 239 See: 240 https://cloud.google.com/storage/docs/json_api/v1/notifications/insert 241 242 If :attr:`user_project` is set on the bucket, bills the API request 243 to that project. 244 245 :type client: :class:`~google.cloud.storage.client.Client` 246 :param client: (Optional) The client to use. If not passed, falls back 247 to the ``client`` stored on the notification's bucket. 248 :type timeout: float or tuple 249 :param timeout: 250 (Optional) The amount of time, in seconds, to wait 251 for the server response. See: :ref:`configuring_timeouts` 252 253 :type retry: google.api_core.retry.Retry or google.cloud.storage.retry.ConditionalRetryPolicy 254 :param retry: 255 (Optional) How to retry the RPC. See: :ref:`configuring_retries` 256 257 :raises ValueError: if the notification already exists. 258 """ 259 if self.notification_id is not None: 260 raise ValueError( 261 "Notification already exists w/ id: {}".format(self.notification_id) 262 ) 263 264 client = self._require_client(client) 265 266 query_params = {} 267 if self.bucket.user_project is not None: 268 query_params["userProject"] = self.bucket.user_project 269 270 path = "/b/{}/notificationConfigs".format(self.bucket.name) 271 properties = self._properties.copy() 272 273 if self.topic_name is None: 274 properties["topic"] = _TOPIC_REF_FMT.format(self.topic_project, "") 275 else: 276 properties["topic"] = _TOPIC_REF_FMT.format( 277 self.topic_project, self.topic_name 278 ) 279 280 self._properties = client._post_resource( 281 path, properties, query_params=query_params, timeout=timeout, retry=retry, 282 ) 283 284 def exists(self, client=None, timeout=_DEFAULT_TIMEOUT, retry=DEFAULT_RETRY): 285 """Test whether this notification exists. 286 287 See: 288 https://cloud.google.com/storage/docs/json_api/v1/notifications/get 289 290 If :attr:`user_project` is set on the bucket, bills the API request 291 to that project. 292 293 :type client: :class:`~google.cloud.storage.client.Client` or 294 ``NoneType`` 295 :param client: (Optional) The client to use. If not passed, falls back 296 to the ``client`` stored on the current bucket. 297 :type timeout: float or tuple 298 :param timeout: 299 (Optional) The amount of time, in seconds, to wait 300 for the server response. See: :ref:`configuring_timeouts` 301 302 :type retry: google.api_core.retry.Retry or google.cloud.storage.retry.ConditionalRetryPolicy 303 :param retry: 304 (Optional) How to retry the RPC. See: :ref:`configuring_retries` 305 306 :rtype: bool 307 :returns: True, if the notification exists, else False. 308 :raises ValueError: if the notification has no ID. 309 """ 310 if self.notification_id is None: 311 raise ValueError("Notification not intialized by server") 312 313 client = self._require_client(client) 314 315 query_params = {} 316 if self.bucket.user_project is not None: 317 query_params["userProject"] = self.bucket.user_project 318 319 try: 320 client._get_resource( 321 self.path, query_params=query_params, timeout=timeout, retry=retry, 322 ) 323 except NotFound: 324 return False 325 else: 326 return True 327 328 def reload(self, client=None, timeout=_DEFAULT_TIMEOUT, retry=DEFAULT_RETRY): 329 """Update this notification from the server configuration. 330 331 See: 332 https://cloud.google.com/storage/docs/json_api/v1/notifications/get 333 334 If :attr:`user_project` is set on the bucket, bills the API request 335 to that project. 336 337 :type client: :class:`~google.cloud.storage.client.Client` or 338 ``NoneType`` 339 :param client: (Optional) The client to use. If not passed, falls back 340 to the ``client`` stored on the current bucket. 341 :type timeout: float or tuple 342 :param timeout: 343 (Optional) The amount of time, in seconds, to wait 344 for the server response. See: :ref:`configuring_timeouts` 345 346 :type retry: google.api_core.retry.Retry or google.cloud.storage.retry.ConditionalRetryPolicy 347 :param retry: 348 (Optional) How to retry the RPC. See: :ref:`configuring_retries` 349 350 351 :raises ValueError: if the notification has no ID. 352 """ 353 if self.notification_id is None: 354 raise ValueError("Notification not intialized by server") 355 356 client = self._require_client(client) 357 358 query_params = {} 359 if self.bucket.user_project is not None: 360 query_params["userProject"] = self.bucket.user_project 361 362 response = client._get_resource( 363 self.path, query_params=query_params, timeout=timeout, retry=retry, 364 ) 365 self._set_properties(response) 366 367 def delete(self, client=None, timeout=_DEFAULT_TIMEOUT, retry=DEFAULT_RETRY): 368 """Delete this notification. 369 370 See: 371 https://cloud.google.com/storage/docs/json_api/v1/notifications/delete 372 373 If :attr:`user_project` is set on the bucket, bills the API request 374 to that project. 375 376 :type client: :class:`~google.cloud.storage.client.Client` or 377 ``NoneType`` 378 :param client: (Optional) The client to use. If not passed, falls back 379 to the ``client`` stored on the current bucket. 380 :type timeout: float or tuple 381 :param timeout: 382 (Optional) The amount of time, in seconds, to wait 383 for the server response. See: :ref:`configuring_timeouts` 384 385 :type retry: google.api_core.retry.Retry or google.cloud.storage.retry.ConditionalRetryPolicy 386 :param retry: 387 (Optional) How to retry the RPC. See: :ref:`configuring_retries` 388 389 :raises: :class:`google.api_core.exceptions.NotFound`: 390 if the notification does not exist. 391 :raises ValueError: if the notification has no ID. 392 """ 393 if self.notification_id is None: 394 raise ValueError("Notification not intialized by server") 395 396 client = self._require_client(client) 397 398 query_params = {} 399 if self.bucket.user_project is not None: 400 query_params["userProject"] = self.bucket.user_project 401 402 client._delete_resource( 403 self.path, query_params=query_params, timeout=timeout, retry=retry, 404 ) 405 406 407def _parse_topic_path(topic_path): 408 """Verify that a topic path is in the correct format. 409 410 .. _resource manager docs: https://cloud.google.com/resource-manager/\ 411 reference/rest/v1beta1/projects#\ 412 Project.FIELDS.project_id 413 .. _topic spec: https://cloud.google.com/storage/docs/json_api/v1/\ 414 notifications/insert#topic 415 416 Expected to be of the form: 417 418 //pubsub.googleapis.com/projects/{project}/topics/{topic} 419 420 where the ``project`` value must be "6 to 30 lowercase letters, digits, 421 or hyphens. It must start with a letter. Trailing hyphens are prohibited." 422 (see `resource manager docs`_) and ``topic`` must have length at least two, 423 must start with a letter and may only contain alphanumeric characters or 424 ``-``, ``_``, ``.``, ``~``, ``+`` or ``%`` (i.e characters used for URL 425 encoding, see `topic spec`_). 426 427 Args: 428 topic_path (str): The topic path to be verified. 429 430 Returns: 431 Tuple[str, str]: The ``project`` and ``topic`` parsed from the 432 ``topic_path``. 433 434 Raises: 435 ValueError: If the topic path is invalid. 436 """ 437 match = _TOPIC_REF_RE.match(topic_path) 438 if match is None: 439 raise ValueError(_BAD_TOPIC.format(topic_path)) 440 441 return match.group("name"), match.group("project") 442