451 lines
16 KiB
Python
451 lines
16 KiB
Python
# Copyright 2015 Google Inc. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Define API Topics."""
|
|
|
|
import base64
|
|
|
|
from gcloud._helpers import _datetime_to_rfc3339
|
|
from gcloud._helpers import _NOW
|
|
from gcloud.exceptions import NotFound
|
|
from gcloud.pubsub._helpers import subscription_name_from_path
|
|
from gcloud.pubsub._helpers import topic_name_from_path
|
|
from gcloud.pubsub.iam import Policy
|
|
from gcloud.pubsub.subscription import Subscription
|
|
|
|
|
|
class Topic(object):
|
|
"""Topics are targets to which messages can be published.
|
|
|
|
Subscribers then receive those messages.
|
|
|
|
See:
|
|
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics
|
|
|
|
:type name: string
|
|
:param name: the name of the topic
|
|
|
|
:type client: :class:`gcloud.pubsub.client.Client`
|
|
:param client: A client which holds credentials and project configuration
|
|
for the topic (which requires a project).
|
|
|
|
:type timestamp_messages: boolean
|
|
:param timestamp_messages: If true, the topic will add a ``timestamp`` key
|
|
to the attributes of each published message:
|
|
the value will be an RFC 3339 timestamp.
|
|
"""
|
|
def __init__(self, name, client, timestamp_messages=False):
|
|
self.name = name
|
|
self._client = client
|
|
self.timestamp_messages = timestamp_messages
|
|
|
|
def subscription(self, name, ack_deadline=None, push_endpoint=None):
|
|
"""Creates a subscription bound to the current topic.
|
|
|
|
Example: pull-mode subcription, default paramter values
|
|
|
|
.. literalinclude:: pubsub_snippets.py
|
|
:start-after: [START topic_subscription_defaults]
|
|
:end-before: [END topic_subscription_defaults]
|
|
|
|
Example: pull-mode subcription, override ``ack_deadline`` default
|
|
|
|
.. literalinclude:: pubsub_snippets.py
|
|
:start-after: [START topic_subscription_ack90]
|
|
:end-before: [END topic_subscription_ack90]
|
|
|
|
Example: push-mode subcription
|
|
|
|
.. literalinclude:: pubsub_snippets.py
|
|
:start-after: [START topic_subscription_push]
|
|
:end-before: [END topic_subscription_push]
|
|
|
|
:type name: string
|
|
:param name: the name of the subscription
|
|
|
|
:type ack_deadline: int
|
|
:param ack_deadline: the deadline (in seconds) by which messages pulled
|
|
from the back-end must be acknowledged.
|
|
|
|
:type push_endpoint: string
|
|
:param push_endpoint: URL to which messages will be pushed by the
|
|
back-end. If not set, the application must pull
|
|
messages.
|
|
"""
|
|
return Subscription(name, self, ack_deadline=ack_deadline,
|
|
push_endpoint=push_endpoint)
|
|
|
|
@classmethod
|
|
def from_api_repr(cls, resource, client):
|
|
"""Factory: construct a topic given its API representation
|
|
|
|
:type resource: dict
|
|
:param resource: topic resource representation returned from the API
|
|
|
|
:type client: :class:`gcloud.pubsub.client.Client`
|
|
:param client: Client which holds credentials and project
|
|
configuration for the topic.
|
|
|
|
:rtype: :class:`gcloud.pubsub.topic.Topic`
|
|
:returns: Topic parsed from ``resource``.
|
|
:raises: :class:`ValueError` if ``client`` is not ``None`` and the
|
|
project from the resource does not agree with the project
|
|
from the client.
|
|
"""
|
|
topic_name = topic_name_from_path(resource['name'], client.project)
|
|
return cls(topic_name, client=client)
|
|
|
|
@property
|
|
def project(self):
|
|
"""Project bound to the topic."""
|
|
return self._client.project
|
|
|
|
@property
|
|
def full_name(self):
|
|
"""Fully-qualified name used in topic / subscription APIs"""
|
|
return 'projects/%s/topics/%s' % (self.project, self.name)
|
|
|
|
def _require_client(self, client):
|
|
"""Check client or verify over-ride.
|
|
|
|
:type client: :class:`gcloud.pubsub.client.Client` or ``NoneType``
|
|
:param client: the client to use. If not passed, falls back to the
|
|
``client`` stored on the current topic.
|
|
|
|
:rtype: :class:`gcloud.pubsub.client.Client`
|
|
:returns: The client passed in or the currently bound client.
|
|
"""
|
|
if client is None:
|
|
client = self._client
|
|
return client
|
|
|
|
def create(self, client=None):
|
|
"""API call: create the topic via a PUT request
|
|
|
|
See:
|
|
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/create
|
|
|
|
Example:
|
|
|
|
.. literalinclude:: pubsub_snippets.py
|
|
:start-after: [START topic_create]
|
|
:end-before: [END topic_create]
|
|
|
|
:type client: :class:`gcloud.pubsub.client.Client` or ``NoneType``
|
|
:param client: the client to use. If not passed, falls back to the
|
|
``client`` stored on the current topic.
|
|
"""
|
|
client = self._require_client(client)
|
|
api = client.publisher_api
|
|
api.topic_create(topic_path=self.full_name)
|
|
|
|
def exists(self, client=None):
|
|
"""API call: test for the existence of the topic via a GET request
|
|
|
|
See
|
|
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/get
|
|
|
|
Example:
|
|
|
|
.. literalinclude:: pubsub_snippets.py
|
|
:start-after: [START topic_exists]
|
|
:end-before: [END topic_exists]
|
|
|
|
:type client: :class:`gcloud.pubsub.client.Client` or ``NoneType``
|
|
:param client: the client to use. If not passed, falls back to the
|
|
``client`` stored on the current topic.
|
|
"""
|
|
client = self._require_client(client)
|
|
api = client.publisher_api
|
|
|
|
try:
|
|
api.topic_get(topic_path=self.full_name)
|
|
except NotFound:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
def delete(self, client=None):
|
|
"""API call: delete the topic via a DELETE request
|
|
|
|
See:
|
|
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/delete
|
|
|
|
Example:
|
|
|
|
.. literalinclude:: pubsub_snippets.py
|
|
:start-after: [START topic_delete]
|
|
:end-before: [END topic_delete]
|
|
|
|
:type client: :class:`gcloud.pubsub.client.Client` or ``NoneType``
|
|
:param client: the client to use. If not passed, falls back to the
|
|
``client`` stored on the current topic.
|
|
"""
|
|
client = self._require_client(client)
|
|
api = client.publisher_api
|
|
api.topic_delete(topic_path=self.full_name)
|
|
|
|
def _timestamp_message(self, attrs):
|
|
"""Add a timestamp to ``attrs``, if the topic is so configured.
|
|
|
|
If ``attrs`` already has the key, do nothing.
|
|
|
|
Helper method for ``publish``/``Batch.publish``.
|
|
"""
|
|
if self.timestamp_messages and 'timestamp' not in attrs:
|
|
attrs['timestamp'] = _datetime_to_rfc3339(_NOW())
|
|
|
|
def publish(self, message, client=None, **attrs):
|
|
"""API call: publish a message to a topic via a POST request
|
|
|
|
See:
|
|
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/publish
|
|
|
|
Example without message attributes:
|
|
|
|
.. literalinclude:: pubsub_snippets.py
|
|
:start-after: [START topic_publish_simple_message]
|
|
:end-before: [END topic_publish_simple_message]
|
|
|
|
With message attributes:
|
|
|
|
.. literalinclude:: pubsub_snippets.py
|
|
:start-after: [START topic_publish_message_with_attrs]
|
|
:end-before: [END topic_publish_message_with_attrs]
|
|
|
|
:type message: bytes
|
|
:param message: the message payload
|
|
|
|
:type client: :class:`gcloud.pubsub.client.Client` or ``NoneType``
|
|
:param client: the client to use. If not passed, falls back to the
|
|
``client`` stored on the current topic.
|
|
|
|
:type attrs: dict (string -> string)
|
|
:param attrs: key-value pairs to send as message attributes
|
|
|
|
:rtype: str
|
|
:returns: message ID assigned by the server to the published message
|
|
"""
|
|
client = self._require_client(client)
|
|
api = client.publisher_api
|
|
|
|
self._timestamp_message(attrs)
|
|
message_b = base64.b64encode(message).decode('ascii')
|
|
message_data = {'data': message_b, 'attributes': attrs}
|
|
message_ids = api.topic_publish(self.full_name, [message_data])
|
|
return message_ids[0]
|
|
|
|
def batch(self, client=None):
|
|
"""Return a batch to use as a context manager.
|
|
|
|
Example:
|
|
|
|
.. literalinclude:: pubsub_snippets.py
|
|
:start-after: [START topic_batch]
|
|
:end-before: [END topic_batch]
|
|
|
|
.. note::
|
|
|
|
The only API request happens during the ``__exit__()`` of the topic
|
|
used as a context manager, and only if the block exits without
|
|
raising an exception.
|
|
|
|
:type client: :class:`gcloud.pubsub.client.Client` or ``NoneType``
|
|
:param client: the client to use. If not passed, falls back to the
|
|
``client`` stored on the current topic.
|
|
|
|
:rtype: :class:`Batch`
|
|
:returns: A batch to use as a context manager.
|
|
"""
|
|
client = self._require_client(client)
|
|
return Batch(self, client)
|
|
|
|
def list_subscriptions(self, page_size=None, page_token=None, client=None):
|
|
"""List subscriptions for the project associated with this client.
|
|
|
|
See:
|
|
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics.subscriptions/list
|
|
|
|
Example:
|
|
|
|
.. literalinclude:: pubsub_snippets.py
|
|
:start-after: [START topic_list_subscriptions]
|
|
:end-before: [END topic_list_subscriptions]
|
|
|
|
:type page_size: int
|
|
:param page_size: maximum number of topics to return, If not passed,
|
|
defaults to a value set by the API.
|
|
|
|
:type page_token: string
|
|
:param page_token: opaque marker for the next "page" of topics. If not
|
|
passed, the API will return the first page of
|
|
topics.
|
|
|
|
:type client: :class:`gcloud.pubsub.client.Client` or ``NoneType``
|
|
:param client: the client to use. If not passed, falls back to the
|
|
``client`` stored on the current topic.
|
|
|
|
:rtype: tuple, (list, str)
|
|
:returns: list of :class:`gcloud.pubsub.subscription.Subscription`,
|
|
plus a "next page token" string: if not None, indicates that
|
|
more topics can be retrieved with another call (pass that
|
|
value as ``page_token``).
|
|
"""
|
|
client = self._require_client(client)
|
|
api = client.publisher_api
|
|
sub_paths, next_token = api.topic_list_subscriptions(
|
|
self.full_name, page_size, page_token)
|
|
subscriptions = []
|
|
for sub_path in sub_paths:
|
|
sub_name = subscription_name_from_path(sub_path, self.project)
|
|
subscriptions.append(Subscription(sub_name, self))
|
|
return subscriptions, next_token
|
|
|
|
def get_iam_policy(self, client=None):
|
|
"""Fetch the IAM policy for the topic.
|
|
|
|
See:
|
|
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/getIamPolicy
|
|
|
|
Example:
|
|
|
|
.. literalinclude:: pubsub_snippets.py
|
|
:start-after: [START topic_get_iam_policy]
|
|
:end-before: [END topic_get_iam_policy]
|
|
|
|
:type client: :class:`gcloud.pubsub.client.Client` or ``NoneType``
|
|
:param client: the client to use. If not passed, falls back to the
|
|
``client`` stored on the current batch.
|
|
|
|
:rtype: :class:`gcloud.pubsub.iam.Policy`
|
|
:returns: policy created from the resource returned by the
|
|
``getIamPolicy`` API request.
|
|
"""
|
|
client = self._require_client(client)
|
|
api = client.iam_policy_api
|
|
resp = api.get_iam_policy(self.full_name)
|
|
return Policy.from_api_repr(resp)
|
|
|
|
def set_iam_policy(self, policy, client=None):
|
|
"""Update the IAM policy for the topic.
|
|
|
|
See:
|
|
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/setIamPolicy
|
|
|
|
Example:
|
|
|
|
.. literalinclude:: pubsub_snippets.py
|
|
:start-after: [START topic_set_iam_policy]
|
|
:end-before: [END topic_set_iam_policy]
|
|
|
|
:type policy: :class:`gcloud.pubsub.iam.Policy`
|
|
:param policy: the new policy, typically fetched via
|
|
:meth:`get_iam_policy` and updated in place.
|
|
|
|
:type client: :class:`gcloud.pubsub.client.Client` or ``NoneType``
|
|
:param client: the client to use. If not passed, falls back to the
|
|
``client`` stored on the current batch.
|
|
|
|
:rtype: :class:`gcloud.pubsub.iam.Policy`
|
|
:returns: updated policy created from the resource returned by the
|
|
``setIamPolicy`` API request.
|
|
"""
|
|
client = self._require_client(client)
|
|
api = client.iam_policy_api
|
|
resource = policy.to_api_repr()
|
|
resp = api.set_iam_policy(self.full_name, resource)
|
|
return Policy.from_api_repr(resp)
|
|
|
|
def check_iam_permissions(self, permissions, client=None):
|
|
"""Verify permissions allowed for the current user.
|
|
|
|
See:
|
|
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/testIamPermissions
|
|
|
|
Example:
|
|
|
|
.. literalinclude:: pubsub_snippets.py
|
|
:start-after: [START topic_check_iam_permissions]
|
|
:end-before: [END topic_check_iam_permissions]
|
|
|
|
:type permissions: list of string
|
|
:param permissions: list of permissions to be tested
|
|
|
|
:type client: :class:`gcloud.pubsub.client.Client` or ``NoneType``
|
|
:param client: the client to use. If not passed, falls back to the
|
|
``client`` stored on the current batch.
|
|
|
|
:rtype: sequence of string
|
|
:returns: subset of ``permissions`` allowed by current IAM policy.
|
|
"""
|
|
client = self._require_client(client)
|
|
api = client.iam_policy_api
|
|
return api.test_iam_permissions(
|
|
self.full_name, list(permissions))
|
|
|
|
|
|
class Batch(object):
|
|
"""Context manager: collect messages to publish via a single API call.
|
|
|
|
Helper returned by :meth:Topic.batch
|
|
|
|
:type topic: :class:`gcloud.pubsub.topic.Topic`
|
|
:param topic: the topic being published
|
|
|
|
:type client: :class:`gcloud.pubsub.client.Client`
|
|
:param client: The client to use.
|
|
"""
|
|
def __init__(self, topic, client):
|
|
self.topic = topic
|
|
self.messages = []
|
|
self.message_ids = []
|
|
self.client = client
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
if exc_type is None:
|
|
self.commit()
|
|
|
|
def __iter__(self):
|
|
return iter(self.message_ids)
|
|
|
|
def publish(self, message, **attrs):
|
|
"""Emulate publishing a message, but save it.
|
|
|
|
:type message: bytes
|
|
:param message: the message payload
|
|
|
|
:type attrs: dict (string -> string)
|
|
:param attrs: key-value pairs to send as message attributes
|
|
"""
|
|
self.topic._timestamp_message(attrs)
|
|
self.messages.append(
|
|
{'data': base64.b64encode(message).decode('ascii'),
|
|
'attributes': attrs})
|
|
|
|
def commit(self, client=None):
|
|
"""Send saved messages as a single API call.
|
|
|
|
:type client: :class:`gcloud.pubsub.client.Client` or ``NoneType``
|
|
:param client: the client to use. If not passed, falls back to the
|
|
``client`` stored on the current batch.
|
|
"""
|
|
if client is None:
|
|
client = self.client
|
|
api = client.publisher_api
|
|
message_ids = api.topic_publish(self.topic.full_name, self.messages[:])
|
|
self.message_ids.extend(message_ids)
|
|
del self.messages[:]
|