Updated DB_Helper by adding firebase methods.

This commit is contained in:
Batuhan Berk Başoğlu 2020-10-05 16:53:40 -04:00
parent 485cc3bbba
commit c82121d036
1810 changed files with 537281 additions and 1 deletions

View file

@ -0,0 +1,32 @@
# 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.
"""Google Cloud Pubsub API wrapper.
The main concepts with this API are:
- :class:`gcloud.pubsub.topic.Topic` represents an endpoint to which messages
can be published using the Cloud Storage Pubsub API.
- :class:`gcloud.pubsub.subscription.Subscription` represents a named
subscription (either pull or push) to a topic.
"""
from gcloud.pubsub.client import Client
from gcloud.pubsub.connection import Connection
from gcloud.pubsub.subscription import Subscription
from gcloud.pubsub.topic import Topic
SCOPE = Connection.SCOPE

View file

@ -0,0 +1,502 @@
# Copyright 2016 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.
"""GAX wrapper for Pubsub API requests."""
# pylint: disable=import-error
from google.gax import CallOptions
from google.gax import INITIAL_PAGE
from google.gax.errors import GaxError
from google.gax.grpc import exc_to_code
from google.pubsub.v1.pubsub_pb2 import PubsubMessage
from google.pubsub.v1.pubsub_pb2 import PushConfig
from grpc.beta.interfaces import StatusCode
# pylint: enable=import-error
from gcloud.exceptions import Conflict
from gcloud.exceptions import NotFound
from gcloud._helpers import _to_bytes
def _build_paging_options(page_token=None):
"""Helper for :meth:'_PublisherAPI.list_topics' et aliae."""
if page_token is None:
page_token = INITIAL_PAGE
options = {'page_token': page_token}
return CallOptions(**options)
class _PublisherAPI(object):
"""Helper mapping publisher-related APIs.
:type gax_api: :class:`google.pubsub.v1.publisher_api.PublisherApi`
:param gax_api: API object used to make GAX requests.
"""
def __init__(self, gax_api):
self._gax_api = gax_api
def list_topics(self, project, page_size=0, page_token=None):
"""List topics for the project associated with this API.
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/list
:type project: string
:param project: project ID
: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.
:rtype: tuple, (list, str)
:returns: list of ``Topic`` resource dicts, 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``).
"""
options = _build_paging_options(page_token)
path = 'projects/%s' % (project,)
page_iter = self._gax_api.list_topics(
path, page_size=page_size, options=options)
topics = [{'name': topic_pb.name} for topic_pb in page_iter.next()]
token = page_iter.page_token or None
return topics, token
def topic_create(self, topic_path):
"""API call: create a topic
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/create
:type topic_path: string
:param topic_path: fully-qualified path of the new topic, in format
``projects/<PROJECT>/topics/<TOPIC_NAME>``.
:rtype: dict
:returns: ``Topic`` resource returned from the API.
:raises: :exc:`gcloud.exceptions.Conflict` if the topic already
exists
"""
try:
topic_pb = self._gax_api.create_topic(topic_path)
except GaxError as exc:
if exc_to_code(exc.cause) == StatusCode.FAILED_PRECONDITION:
raise Conflict(topic_path)
raise
return {'name': topic_pb.name}
def topic_get(self, topic_path):
"""API call: retrieve a topic
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/get
:type topic_path: string
:param topic_path: fully-qualified path of the topic, in format
``projects/<PROJECT>/topics/<TOPIC_NAME>``.
:rtype: dict
:returns: ``Topic`` resource returned from the API.
:raises: :exc:`gcloud.exceptions.NotFound` if the topic does not
exist
"""
try:
topic_pb = self._gax_api.get_topic(topic_path)
except GaxError as exc:
if exc_to_code(exc.cause) == StatusCode.NOT_FOUND:
raise NotFound(topic_path)
raise
return {'name': topic_pb.name}
def topic_delete(self, topic_path):
"""API call: delete a topic
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/create
:type topic_path: string
:param topic_path: fully-qualified path of the new topic, in format
``projects/<PROJECT>/topics/<TOPIC_NAME>``.
:rtype: dict
:returns: ``Topic`` resource returned from the API.
"""
try:
self._gax_api.delete_topic(topic_path)
except GaxError as exc:
if exc_to_code(exc.cause) == StatusCode.NOT_FOUND:
raise NotFound(topic_path)
raise
def topic_publish(self, topic_path, messages):
"""API call: publish one or more messages to a topic
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/publish
:type topic_path: string
:param topic_path: fully-qualified path of the topic, in format
``projects/<PROJECT>/topics/<TOPIC_NAME>``.
:type messages: list of dict
:param messages: messages to be published.
:rtype: list of string
:returns: list of opaque IDs for published messages.
:raises: :exc:`gcloud.exceptions.NotFound` if the topic does not
exist
"""
options = CallOptions(is_bundling=False)
message_pbs = [_message_pb_from_dict(message)
for message in messages]
try:
result = self._gax_api.publish(topic_path, message_pbs,
options=options)
except GaxError as exc:
if exc_to_code(exc.cause) == StatusCode.NOT_FOUND:
raise NotFound(topic_path)
raise
return result.message_ids
def topic_list_subscriptions(self, topic_path, page_size=0,
page_token=None):
"""API call: list subscriptions bound to a topic
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics.subscriptions/list
:type topic_path: string
:param topic_path: fully-qualified path of the topic, in format
``projects/<PROJECT>/topics/<TOPIC_NAME>``.
:type page_size: int
:param page_size: maximum number of subscriptions 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 subscriptions.
If not passed, the API will return the first page
of subscriptions.
:rtype: list of strings
:returns: fully-qualified names of subscriptions for the supplied
topic.
:raises: :exc:`gcloud.exceptions.NotFound` if the topic does not
exist
"""
options = _build_paging_options(page_token)
try:
page_iter = self._gax_api.list_topic_subscriptions(
topic_path, page_size=page_size, options=options)
except GaxError as exc:
if exc_to_code(exc.cause) == StatusCode.NOT_FOUND:
raise NotFound(topic_path)
raise
subs = page_iter.next()
token = page_iter.page_token or None
return subs, token
class _SubscriberAPI(object):
"""Helper mapping subscriber-related APIs.
:type gax_api: :class:`google.pubsub.v1.publisher_api.SubscriberApi`
:param gax_api: API object used to make GAX requests.
"""
def __init__(self, gax_api):
self._gax_api = gax_api
def list_subscriptions(self, project, page_size=0, page_token=None):
"""List subscriptions for the project associated with this API.
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/list
:type project: string
:param project: project ID
:type page_size: int
:param page_size: maximum number of subscriptions 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 subscriptions.
If not passed, the API will return the first page
of subscriptions.
:rtype: tuple, (list, str)
:returns: list of ``Subscription`` resource dicts, 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``).
"""
options = _build_paging_options(page_token)
path = 'projects/%s' % (project,)
page_iter = self._gax_api.list_subscriptions(
path, page_size=page_size, options=options)
subscriptions = [_subscription_pb_to_mapping(sub_pb)
for sub_pb in page_iter.next()]
token = page_iter.page_token or None
return subscriptions, token
def subscription_create(self, subscription_path, topic_path,
ack_deadline=None, push_endpoint=None):
"""API call: create a subscription
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/create
:type subscription_path: string
:param subscription_path: the fully-qualified path of the new
subscription, in format
``projects/<PROJECT>/subscriptions/<SUB_NAME>``.
:type topic_path: string
:param topic_path: the fully-qualified path of the topic being
subscribed, in format
``projects/<PROJECT>/topics/<TOPIC_NAME>``.
:type ack_deadline: int, or ``NoneType``
:param ack_deadline: the deadline (in seconds) by which messages pulled
from the back-end must be acknowledged.
:type push_endpoint: string, or ``NoneType``
:param push_endpoint: URL to which messages will be pushed by the
back-end. If not set, the application must pull
messages.
:rtype: dict
:returns: ``Subscription`` resource returned from the API.
"""
if push_endpoint is not None:
push_config = PushConfig(push_endpoint=push_endpoint)
else:
push_config = None
if ack_deadline is None:
ack_deadline = 0
try:
sub_pb = self._gax_api.create_subscription(
subscription_path, topic_path, push_config, ack_deadline)
except GaxError as exc:
if exc_to_code(exc.cause) == StatusCode.FAILED_PRECONDITION:
raise Conflict(topic_path)
raise
return _subscription_pb_to_mapping(sub_pb)
def subscription_get(self, subscription_path):
"""API call: retrieve a subscription
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/get
:type subscription_path: string
:param subscription_path: the fully-qualified path of the subscription,
in format
``projects/<PROJECT>/subscriptions/<SUB_NAME>``.
:rtype: dict
:returns: ``Subscription`` resource returned from the API.
"""
try:
sub_pb = self._gax_api.get_subscription(subscription_path)
except GaxError as exc:
if exc_to_code(exc.cause) == StatusCode.NOT_FOUND:
raise NotFound(subscription_path)
raise
return _subscription_pb_to_mapping(sub_pb)
def subscription_delete(self, subscription_path):
"""API call: delete a subscription
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/delete
:type subscription_path: string
:param subscription_path: the fully-qualified path of the subscription,
in format
``projects/<PROJECT>/subscriptions/<SUB_NAME>``.
"""
try:
self._gax_api.delete_subscription(subscription_path)
except GaxError as exc:
if exc_to_code(exc.cause) == StatusCode.NOT_FOUND:
raise NotFound(subscription_path)
raise
def subscription_modify_push_config(self, subscription_path,
push_endpoint):
"""API call: update push config of a subscription
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/modifyPushConfig
:type subscription_path: string
:param subscription_path: the fully-qualified path of the new
subscription, in format
``projects/<PROJECT>/subscriptions/<SUB_NAME>``.
:type push_endpoint: string, or ``NoneType``
:param push_endpoint: URL to which messages will be pushed by the
back-end. If not set, the application must pull
messages.
"""
push_config = PushConfig(push_endpoint=push_endpoint)
try:
self._gax_api.modify_push_config(subscription_path, push_config)
except GaxError as exc:
if exc_to_code(exc.cause) == StatusCode.NOT_FOUND:
raise NotFound(subscription_path)
raise
def subscription_pull(self, subscription_path, return_immediately=False,
max_messages=1):
"""API call: retrieve messages for a subscription
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/modifyPushConfig
:type subscription_path: string
:param subscription_path: the fully-qualified path of the new
subscription, in format
``projects/<PROJECT>/subscriptions/<SUB_NAME>``.
:type return_immediately: boolean
:param return_immediately: if True, the back-end returns even if no
messages are available; if False, the API
call blocks until one or more messages are
available.
:type max_messages: int
:param max_messages: the maximum number of messages to return.
:rtype: list of dict
:returns: the ``receivedMessages`` element of the response.
"""
try:
response_pb = self._gax_api.pull(
subscription_path, max_messages, return_immediately)
except GaxError as exc:
if exc_to_code(exc.cause) == StatusCode.NOT_FOUND:
raise NotFound(subscription_path)
raise
return [_received_message_pb_to_mapping(rmpb)
for rmpb in response_pb.received_messages]
def subscription_acknowledge(self, subscription_path, ack_ids):
"""API call: acknowledge retrieved messages
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/modifyPushConfig
:type subscription_path: string
:param subscription_path: the fully-qualified path of the new
subscription, in format
``projects/<PROJECT>/subscriptions/<SUB_NAME>``.
:type ack_ids: list of string
:param ack_ids: ack IDs of messages being acknowledged
"""
try:
self._gax_api.acknowledge(subscription_path, ack_ids)
except GaxError as exc:
if exc_to_code(exc.cause) == StatusCode.NOT_FOUND:
raise NotFound(subscription_path)
raise
def subscription_modify_ack_deadline(self, subscription_path, ack_ids,
ack_deadline):
"""API call: update ack deadline for retrieved messages
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/modifyAckDeadline
:type subscription_path: string
:param subscription_path: the fully-qualified path of the new
subscription, in format
``projects/<PROJECT>/subscriptions/<SUB_NAME>``.
:type ack_ids: list of string
:param ack_ids: ack IDs of messages being acknowledged
:type ack_deadline: int
:param ack_deadline: the deadline (in seconds) by which messages pulled
from the back-end must be acknowledged.
"""
try:
self._gax_api.modify_ack_deadline(
subscription_path, ack_ids, ack_deadline)
except GaxError as exc:
if exc_to_code(exc.cause) == StatusCode.NOT_FOUND:
raise NotFound(subscription_path)
raise
def _message_pb_from_dict(message):
"""Helper for :meth:`_PublisherAPI.topic_publish`."""
return PubsubMessage(data=_to_bytes(message['data']),
attributes=message['attributes'])
def _subscription_pb_to_mapping(sub_pb):
"""Helper for :meth:`list_subscriptions`, et aliae
Ideally, would use a function from :mod:`protobuf.json_format`, but
the right one isn't public. See:
https://github.com/google/protobuf/issues/1351
"""
mapping = {
'name': sub_pb.name,
'topic': sub_pb.topic,
'ackDeadlineSeconds': sub_pb.ack_deadline_seconds,
}
if sub_pb.push_config.push_endpoint != '':
mapping['pushConfig'] = {
'pushEndpoint': sub_pb.push_config.push_endpoint,
}
return mapping
def _message_pb_to_mapping(message_pb):
"""Helper for :meth:`pull`, et aliae
Ideally, would use a function from :mod:`protobuf.json_format`, but
the right one isn't public. See:
https://github.com/google/protobuf/issues/1351
"""
return {
'messageId': message_pb.message_id,
'data': message_pb.data,
'attributes': message_pb.attributes,
}
def _received_message_pb_to_mapping(received_message_pb):
"""Helper for :meth:`pull`, et aliae
Ideally, would use a function from :mod:`protobuf.json_format`, but
the right one isn't public. See:
https://github.com/google/protobuf/issues/1351
"""
return {
'ackId': received_message_pb.ack_id,
'message': _message_pb_to_mapping(
received_message_pb.message),
}

View file

@ -0,0 +1,73 @@
# 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.
"""Helper functions for shared behavior."""
import re
from gcloud._helpers import _name_from_project_path
_TOPIC_TEMPLATE = re.compile(r"""
projects/ # static prefix
(?P<project>[^/]+) # initial letter, wordchars + hyphen
/topics/ # static midfix
(?P<name>[^/]+) # initial letter, wordchars + allowed punc
""", re.VERBOSE)
_SUBSCRIPTION_TEMPLATE = re.compile(r"""
projects/ # static prefix
(?P<project>[^/]+) # initial letter, wordchars + hyphen
/subscriptions/ # static midfix
(?P<name>[^/]+) # initial letter, wordchars + allowed punc
""", re.VERBOSE)
def topic_name_from_path(path, project):
"""Validate a topic URI path and get the topic name.
:type path: string
:param path: URI path for a topic API request.
:type project: string
:param project: The project associated with the request. It is
included for validation purposes.
:rtype: string
:returns: Topic name parsed from ``path``.
:raises: :class:`ValueError` if the ``path`` is ill-formed or if
the project from the ``path`` does not agree with the
``project`` passed in.
"""
return _name_from_project_path(path, project, _TOPIC_TEMPLATE)
def subscription_name_from_path(path, project):
"""Validate a subscription URI path and get the subscription name.
:type path: string
:param path: URI path for a subscription API request.
:type project: string
:param project: The project associated with the request. It is
included for validation purposes.
:rtype: string
:returns: subscription name parsed from ``path``.
:raises: :class:`ValueError` if the ``path`` is ill-formed or if
the project from the ``path`` does not agree with the
``project`` passed in.
"""
return _name_from_project_path(path, project, _SUBSCRIPTION_TEMPLATE)

View file

@ -0,0 +1,186 @@
# 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.
"""Client for interacting with the Google Cloud Pub/Sub API."""
import os
from gcloud.client import JSONClient
from gcloud.pubsub.connection import Connection
from gcloud.pubsub.connection import _PublisherAPI as JSONPublisherAPI
from gcloud.pubsub.connection import _SubscriberAPI as JSONSubscriberAPI
from gcloud.pubsub.connection import _IAMPolicyAPI
from gcloud.pubsub.subscription import Subscription
from gcloud.pubsub.topic import Topic
try:
from google.pubsub.v1.publisher_api import (
PublisherApi as GeneratedPublisherAPI)
from google.pubsub.v1.subscriber_api import (
SubscriberApi as GeneratedSubscriberAPI)
from gcloud.pubsub._gax import _PublisherAPI as GAXPublisherAPI
from gcloud.pubsub._gax import _SubscriberAPI as GAXSubscriberAPI
except ImportError: # pragma: NO COVER
_HAVE_GAX = False
GeneratedPublisherAPI = GAXPublisherAPI = None
GeneratedSubscriberAPI = GAXSubscriberAPI = None
else:
_HAVE_GAX = True
_USE_GAX = _HAVE_GAX and (os.environ.get('GCLOUD_ENABLE_GAX') is not None)
class Client(JSONClient):
"""Client to bundle configuration needed for API requests.
:type project: string
:param project: the project which the client acts on behalf of. Will be
passed when creating a topic. If not passed,
falls back to the default inferred from the environment.
:type credentials: :class:`oauth2client.client.OAuth2Credentials` or
:class:`NoneType`
:param credentials: The OAuth2 Credentials to use for the connection
owned by this client. If not passed (and if no ``http``
object is passed), falls back to the default inferred
from the environment.
:type http: :class:`httplib2.Http` or class that defines ``request()``.
:param http: An optional HTTP object to make requests. If not passed, an
``http`` object is created that is bound to the
``credentials`` for the current object.
"""
_connection_class = Connection
_publisher_api = _subscriber_api = _iam_policy_api = None
@property
def publisher_api(self):
"""Helper for publisher-related API calls."""
if self._publisher_api is None:
if _USE_GAX:
generated = GeneratedPublisherAPI()
self._publisher_api = GAXPublisherAPI(generated)
else:
self._publisher_api = JSONPublisherAPI(self.connection)
return self._publisher_api
@property
def subscriber_api(self):
"""Helper for subscriber-related API calls."""
if self._subscriber_api is None:
if _USE_GAX:
generated = GeneratedSubscriberAPI()
self._subscriber_api = GAXSubscriberAPI(generated)
else:
self._subscriber_api = JSONSubscriberAPI(self.connection)
return self._subscriber_api
@property
def iam_policy_api(self):
"""Helper for IAM policy-related API calls."""
if self._iam_policy_api is None:
self._iam_policy_api = _IAMPolicyAPI(self.connection)
return self._iam_policy_api
def list_topics(self, page_size=None, page_token=None):
"""List topics for the project associated with this client.
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/list
Example:
.. literalinclude:: pubsub_snippets.py
:start-after: [START client_list_topics]
:end-before: [END client_list_topics]
: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.
:rtype: tuple, (list, str)
:returns: list of :class:`gcloud.pubsub.topic.Topic`, 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``).
"""
api = self.publisher_api
resources, next_token = api.list_topics(
self.project, page_size, page_token)
topics = [Topic.from_api_repr(resource, self)
for resource in resources]
return topics, next_token
def list_subscriptions(self, page_size=None, page_token=None):
"""List subscriptions for the project associated with this client.
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/list
Example:
.. literalinclude:: pubsub_snippets.py
:start-after: [START client_list_subscriptions]
:end-before: [END client_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.
: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``).
"""
api = self.subscriber_api
resources, next_token = api.list_subscriptions(
self.project, page_size, page_token)
topics = {}
subscriptions = [Subscription.from_api_repr(resource, self,
topics=topics)
for resource in resources]
return subscriptions, next_token
def topic(self, name, timestamp_messages=False):
"""Creates a topic bound to the current client.
Example:
.. literalinclude:: pubsub_snippets.py
:start-after: [START client_topic]
:end-before: [END client_topic]
:type name: string
:param name: the name of the topic to be constructed.
:type timestamp_messages: boolean
:param timestamp_messages: To be passed to ``Topic`` constructor.
:rtype: :class:`gcloud.pubsub.topic.Topic`
:returns: Topic created with the current client.
"""
return Topic(name, client=self, timestamp_messages=timestamp_messages)

View file

@ -0,0 +1,539 @@
# 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.
"""Create / interact with gcloud pubsub connections."""
import os
from gcloud import connection as base_connection
from gcloud.environment_vars import PUBSUB_EMULATOR
class Connection(base_connection.JSONConnection):
"""A connection to Google Cloud Pubsub via the JSON REST API.
:type credentials: :class:`oauth2client.client.OAuth2Credentials`
:param credentials: (Optional) The OAuth2 Credentials to use for this
connection.
:type http: :class:`httplib2.Http` or class that defines ``request()``.
:param http: (Optional) HTTP object to make requests.
:type api_base_url: string
:param api_base_url: The base of the API call URL. Defaults to the value
:attr:`Connection.API_BASE_URL`.
"""
API_BASE_URL = 'https://pubsub.googleapis.com'
"""The base of the API call URL."""
API_VERSION = 'v1'
"""The version of the API, used in building the API call's URL."""
API_URL_TEMPLATE = '{api_base_url}/{api_version}{path}'
"""A template for the URL of a particular API call."""
SCOPE = ('https://www.googleapis.com/auth/pubsub',
'https://www.googleapis.com/auth/cloud-platform')
"""The scopes required for authenticating as a Cloud Pub/Sub consumer."""
def __init__(self, credentials=None, http=None, api_base_url=None):
super(Connection, self).__init__(credentials=credentials, http=http)
if api_base_url is None:
emulator_host = os.getenv(PUBSUB_EMULATOR)
if emulator_host is None:
api_base_url = self.__class__.API_BASE_URL
else:
api_base_url = 'http://' + emulator_host
self.api_base_url = api_base_url
def build_api_url(self, path, query_params=None,
api_base_url=None, api_version=None):
"""Construct an API url given a few components, some optional.
Typically, you shouldn't need to use this method.
:type path: string
:param path: The path to the resource.
:type query_params: dict or list
:param query_params: A dictionary of keys and values (or list of
key-value pairs) to insert into the query
string of the URL.
:type api_base_url: string
:param api_base_url: The base URL for the API endpoint.
Typically you won't have to provide this.
:type api_version: string
:param api_version: The version of the API to call.
Typically you shouldn't provide this and instead
use the default for the library.
:rtype: string
:returns: The URL assembled from the pieces provided.
"""
if api_base_url is None:
api_base_url = self.api_base_url
return super(Connection, self.__class__).build_api_url(
path, query_params=query_params,
api_base_url=api_base_url, api_version=api_version)
class _PublisherAPI(object):
"""Helper mapping publisher-related APIs.
:type connection: :class:`Connection`
:param connection: the connection used to make API requests.
"""
def __init__(self, connection):
self._connection = connection
def list_topics(self, project, page_size=None, page_token=None):
"""API call: list topics for a given project
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/list
:type project: string
:param project: project ID
: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.
:rtype: tuple, (list, str)
:returns: list of ``Topic`` resource dicts, 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``).
"""
conn = self._connection
params = {}
if page_size is not None:
params['pageSize'] = page_size
if page_token is not None:
params['pageToken'] = page_token
path = '/projects/%s/topics' % (project,)
resp = conn.api_request(method='GET', path=path, query_params=params)
return resp.get('topics', ()), resp.get('nextPageToken')
def topic_create(self, topic_path):
"""API call: create a topic
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/create
:type topic_path: string
:param topic_path: the fully-qualified path of the new topic, in format
``projects/<PROJECT>/topics/<TOPIC_NAME>``.
:rtype: dict
:returns: ``Topic`` resource returned from the API.
"""
conn = self._connection
return conn.api_request(method='PUT', path='/%s' % (topic_path,))
def topic_get(self, topic_path):
"""API call: retrieve a topic
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/get
:type topic_path: string
:param topic_path: the fully-qualified path of the topic, in format
``projects/<PROJECT>/topics/<TOPIC_NAME>``.
:rtype: dict
:returns: ``Topic`` resource returned from the API.
"""
conn = self._connection
return conn.api_request(method='GET', path='/%s' % (topic_path,))
def topic_delete(self, topic_path):
"""API call: delete a topic
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/delete
:type topic_path: string
:param topic_path: the fully-qualified path of the topic, in format
``projects/<PROJECT>/topics/<TOPIC_NAME>``.
"""
conn = self._connection
conn.api_request(method='DELETE', path='/%s' % (topic_path,))
def topic_publish(self, topic_path, messages):
"""API call: publish one or more messages to a topic
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/publish
:type topic_path: string
:param topic_path: the fully-qualified path of the topic, in format
``projects/<PROJECT>/topics/<TOPIC_NAME>``.
:type messages: list of dict
:param messages: messages to be published.
:rtype: list of string
:returns: list of opaque IDs for published messages.
"""
conn = self._connection
data = {'messages': messages}
response = conn.api_request(
method='POST', path='/%s:publish' % (topic_path,), data=data)
return response['messageIds']
def topic_list_subscriptions(self, topic_path, page_size=None,
page_token=None):
"""API call: list subscriptions bound to a topic
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics.subscriptions/list
:type topic_path: string
:param topic_path: the fully-qualified path of the topic, in format
``projects/<PROJECT>/topics/<TOPIC_NAME>``.
:type page_size: int
:param page_size: maximum number of subscriptions 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.
:rtype: list of strings
:returns: fully-qualified names of subscriptions for the supplied
topic.
"""
conn = self._connection
params = {}
if page_size is not None:
params['pageSize'] = page_size
if page_token is not None:
params['pageToken'] = page_token
path = '/%s/subscriptions' % (topic_path,)
resp = conn.api_request(method='GET', path=path, query_params=params)
return resp.get('subscriptions', ()), resp.get('nextPageToken')
class _SubscriberAPI(object):
"""Helper mapping subscriber-related APIs.
:type connection: :class:`Connection`
:param connection: the connection used to make API requests.
"""
def __init__(self, connection):
self._connection = connection
def list_subscriptions(self, project, page_size=None, page_token=None):
"""API call: list subscriptions for a given project
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/list
:type project: string
:param project: project ID
:type page_size: int
:param page_size: maximum number of subscriptions 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 subscriptions.
If not passed, the API will return the first page
of subscriptions.
:rtype: tuple, (list, str)
:returns: list of ``Subscription`` resource dicts, plus a
"next page token" string: if not None, indicates that
more subscriptions can be retrieved with another call (pass
that value as ``page_token``).
"""
conn = self._connection
params = {}
if page_size is not None:
params['pageSize'] = page_size
if page_token is not None:
params['pageToken'] = page_token
path = '/projects/%s/subscriptions' % (project,)
resp = conn.api_request(method='GET', path=path, query_params=params)
return resp.get('subscriptions', ()), resp.get('nextPageToken')
def subscription_create(self, subscription_path, topic_path,
ack_deadline=None, push_endpoint=None):
"""API call: create a subscription
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/create
:type subscription_path: string
:param subscription_path: the fully-qualified path of the new
subscription, in format
``projects/<PROJECT>/subscriptions/<SUB_NAME>``.
:type topic_path: string
:param topic_path: the fully-qualified path of the topic being
subscribed, in format
``projects/<PROJECT>/topics/<TOPIC_NAME>``.
:type ack_deadline: int, or ``NoneType``
:param ack_deadline: the deadline (in seconds) by which messages pulled
from the back-end must be acknowledged.
:type push_endpoint: string, or ``NoneType``
:param push_endpoint: URL to which messages will be pushed by the
back-end. If not set, the application must pull
messages.
:rtype: dict
:returns: ``Subscription`` resource returned from the API.
"""
conn = self._connection
path = '/%s' % (subscription_path,)
resource = {'topic': topic_path}
if ack_deadline is not None:
resource['ackDeadlineSeconds'] = ack_deadline
if push_endpoint is not None:
resource['pushConfig'] = {'pushEndpoint': push_endpoint}
return conn.api_request(method='PUT', path=path, data=resource)
def subscription_get(self, subscription_path):
"""API call: retrieve a subscription
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/get
:type subscription_path: string
:param subscription_path: the fully-qualified path of the subscription,
in format
``projects/<PROJECT>/subscriptions/<SUB_NAME>``.
:rtype: dict
:returns: ``Subscription`` resource returned from the API.
"""
conn = self._connection
path = '/%s' % (subscription_path,)
return conn.api_request(method='GET', path=path)
def subscription_delete(self, subscription_path):
"""API call: delete a subscription
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/delete
:type subscription_path: string
:param subscription_path: the fully-qualified path of the subscription,
in format
``projects/<PROJECT>/subscriptions/<SUB_NAME>``.
"""
conn = self._connection
path = '/%s' % (subscription_path,)
conn.api_request(method='DELETE', path=path)
def subscription_modify_push_config(self, subscription_path,
push_endpoint):
"""API call: update push config of a subscription
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/modifyPushConfig
:type subscription_path: string
:param subscription_path: the fully-qualified path of the new
subscription, in format
``projects/<PROJECT>/subscriptions/<SUB_NAME>``.
:type push_endpoint: string, or ``NoneType``
:param push_endpoint: URL to which messages will be pushed by the
back-end. If not set, the application must pull
messages.
"""
conn = self._connection
path = '/%s:modifyPushConfig' % (subscription_path,)
resource = {'pushConfig': {'pushEndpoint': push_endpoint}}
conn.api_request(method='POST', path=path, data=resource)
def subscription_pull(self, subscription_path, return_immediately=False,
max_messages=1):
"""API call: retrieve messages for a subscription
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/modifyPushConfig
:type subscription_path: string
:param subscription_path: the fully-qualified path of the new
subscription, in format
``projects/<PROJECT>/subscriptions/<SUB_NAME>``.
:type return_immediately: boolean
:param return_immediately: if True, the back-end returns even if no
messages are available; if False, the API
call blocks until one or more messages are
available.
:type max_messages: int
:param max_messages: the maximum number of messages to return.
:rtype: list of dict
:returns: the ``receivedMessages`` element of the response.
"""
conn = self._connection
path = '/%s:pull' % (subscription_path,)
data = {
'returnImmediately': return_immediately,
'maxMessages': max_messages,
}
response = conn.api_request(method='POST', path=path, data=data)
return response.get('receivedMessages', ())
def subscription_acknowledge(self, subscription_path, ack_ids):
"""API call: acknowledge retrieved messages
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/modifyPushConfig
:type subscription_path: string
:param subscription_path: the fully-qualified path of the new
subscription, in format
``projects/<PROJECT>/subscriptions/<SUB_NAME>``.
:type ack_ids: list of string
:param ack_ids: ack IDs of messages being acknowledged
"""
conn = self._connection
path = '/%s:acknowledge' % (subscription_path,)
data = {
'ackIds': ack_ids,
}
conn.api_request(method='POST', path=path, data=data)
def subscription_modify_ack_deadline(self, subscription_path, ack_ids,
ack_deadline):
"""API call: update ack deadline for retrieved messages
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/modifyAckDeadline
:type subscription_path: string
:param subscription_path: the fully-qualified path of the new
subscription, in format
``projects/<PROJECT>/subscriptions/<SUB_NAME>``.
:type ack_ids: list of string
:param ack_ids: ack IDs of messages being acknowledged
:type ack_deadline: int
:param ack_deadline: the deadline (in seconds) by which messages pulled
from the back-end must be acknowledged.
"""
conn = self._connection
path = '/%s:modifyAckDeadline' % (subscription_path,)
data = {
'ackIds': ack_ids,
'ackDeadlineSeconds': ack_deadline,
}
conn.api_request(method='POST', path=path, data=data)
class _IAMPolicyAPI(object):
"""Helper mapping IAM policy-related APIs.
:type connection: :class:`Connection`
:param connection: the connection used to make API requests.
"""
def __init__(self, connection):
self._connection = connection
def get_iam_policy(self, target_path):
"""API call: fetch the IAM policy for the target
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/getIamPolicy
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/getIamPolicy
:type target_path: string
:param target_path: the path of the target object.
:rtype: dict
:returns: the resource returned by the ``getIamPolicy`` API request.
"""
conn = self._connection
path = '/%s:getIamPolicy' % (target_path,)
return conn.api_request(method='GET', path=path)
def set_iam_policy(self, target_path, policy):
"""API call: update the IAM policy for the target
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/setIamPolicy
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/setIamPolicy
:type target_path: string
:param target_path: the path of the target object.
:type policy: dict
:param policy: the new policy resource.
:rtype: dict
:returns: the resource returned by the ``setIamPolicy`` API request.
"""
conn = self._connection
wrapped = {'policy': policy}
path = '/%s:setIamPolicy' % (target_path,)
return conn.api_request(method='POST', path=path, data=wrapped)
def test_iam_permissions(self, target_path, permissions):
"""API call: test permissions
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.topics/testIamPermissions
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/testIamPermissions
:type target_path: string
:param target_path: the path of the target object.
:type permissions: list of string
:param permissions: the permissions to check
:rtype: dict
:returns: the resource returned by the ``getIamPolicy`` API request.
"""
conn = self._connection
wrapped = {'permissions': permissions}
path = '/%s:testIamPermissions' % (target_path,)
resp = conn.api_request(method='POST', path=path, data=wrapped)
return resp.get('permissions', [])

View file

@ -0,0 +1,259 @@
# Copyright 2016 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.
"""PubSub API IAM policy definitions
For allowed roles / permissions, see:
https://cloud.google.com/pubsub/access_control#permissions
"""
# Generic IAM roles
OWNER_ROLE = 'roles/owner'
"""Generic role implying all rights to an object."""
EDITOR_ROLE = 'roles/editor'
"""Generic role implying rights to modify an object."""
VIEWER_ROLE = 'roles/viewer'
"""Generic role implying rights to access an object."""
# Pubsub-specific IAM roles
PUBSUB_ADMIN_ROLE = 'roles/pubsub.admin'
"""Role implying all rights to an object."""
PUBSUB_EDITOR_ROLE = 'roles/pubsub.editor'
"""Role implying rights to modify an object."""
PUBSUB_VIEWER_ROLE = 'roles/pubsub.viewer'
"""Role implying rights to access an object."""
PUBSUB_PUBLISHER_ROLE = 'roles/pubsub.publisher'
"""Role implying rights to publish to a topic."""
PUBSUB_SUBSCRIBER_ROLE = 'roles/pubsub.subscriber'
"""Role implying rights to subscribe to a topic."""
# Pubsub-specific permissions
PUBSUB_TOPICS_CONSUME = 'pubsub.topics.consume'
"""Permission: consume events from a subscription."""
PUBSUB_TOPICS_CREATE = 'pubsub.topics.create'
"""Permission: create topics."""
PUBSUB_TOPICS_DELETE = 'pubsub.topics.delete'
"""Permission: delete topics."""
PUBSUB_TOPICS_GET = 'pubsub.topics.get'
"""Permission: retrieve topics."""
PUBSUB_TOPICS_GET_IAM_POLICY = 'pubsub.topics.getIamPolicy'
"""Permission: retrieve subscription IAM policies."""
PUBSUB_TOPICS_LIST = 'pubsub.topics.list'
"""Permission: list topics."""
PUBSUB_TOPICS_SET_IAM_POLICY = 'pubsub.topics.setIamPolicy'
"""Permission: update subscription IAM policies."""
PUBSUB_SUBSCRIPTIONS_CONSUME = 'pubsub.subscriptions.consume'
"""Permission: consume events from a subscription."""
PUBSUB_SUBSCRIPTIONS_CREATE = 'pubsub.subscriptions.create'
"""Permission: create subscriptions."""
PUBSUB_SUBSCRIPTIONS_DELETE = 'pubsub.subscriptions.delete'
"""Permission: delete subscriptions."""
PUBSUB_SUBSCRIPTIONS_GET = 'pubsub.subscriptions.get'
"""Permission: retrieve subscriptions."""
PUBSUB_SUBSCRIPTIONS_GET_IAM_POLICY = 'pubsub.subscriptions.getIamPolicy'
"""Permission: retrieve subscription IAM policies."""
PUBSUB_SUBSCRIPTIONS_LIST = 'pubsub.subscriptions.list'
"""Permission: list subscriptions."""
PUBSUB_SUBSCRIPTIONS_SET_IAM_POLICY = 'pubsub.subscriptions.setIamPolicy'
"""Permission: update subscription IAM policies."""
PUBSUB_SUBSCRIPTIONS_UPDATE = 'pubsub.subscriptions.update'
"""Permission: update subscriptions."""
class Policy(object):
"""Combined IAM Policy / Bindings.
See:
https://cloud.google.com/pubsub/reference/rest/Shared.Types/Policy
https://cloud.google.com/pubsub/reference/rest/Shared.Types/Binding
:type etag: string
:param etag: ETag used to identify a unique of the policy
:type version: int
:param version: unique version of the policy
"""
def __init__(self, etag=None, version=None):
self.etag = etag
self.version = version
self.owners = set()
self.editors = set()
self.viewers = set()
self.publishers = set()
self.subscribers = set()
@staticmethod
def user(email):
"""Factory method for a user member.
:type email: string
:param email: E-mail for this particular user.
:rtype: string
:returns: A member string corresponding to the given user.
"""
return 'user:%s' % (email,)
@staticmethod
def service_account(email):
"""Factory method for a service account member.
:type email: string
:param email: E-mail for this particular service account.
:rtype: string
:returns: A member string corresponding to the given service account.
"""
return 'serviceAccount:%s' % (email,)
@staticmethod
def group(email):
"""Factory method for a group member.
:type email: string
:param email: An id or e-mail for this particular group.
:rtype: string
:returns: A member string corresponding to the given group.
"""
return 'group:%s' % (email,)
@staticmethod
def domain(domain):
"""Factory method for a domain member.
:type domain: string
:param domain: The domain for this member.
:rtype: string
:returns: A member string corresponding to the given domain.
"""
return 'domain:%s' % (domain,)
@staticmethod
def all_users():
"""Factory method for a member representing all users.
:rtype: string
:returns: A member string representing all users.
"""
return 'allUsers'
@staticmethod
def authenticated_users():
"""Factory method for a member representing all authenticated users.
:rtype: string
:returns: A member string representing all authenticated users.
"""
return 'allAuthenticatedUsers'
@classmethod
def from_api_repr(cls, resource):
"""Create a policy from the resource returned from the API.
:type resource: dict
:param resource: resource returned from the ``getIamPolicy`` API.
:rtype: :class:`Policy`
:returns: the parsed policy
"""
version = resource.get('version')
etag = resource.get('etag')
policy = cls(etag, version)
for binding in resource.get('bindings', ()):
role = binding['role']
members = set(binding['members'])
if role in (OWNER_ROLE, PUBSUB_ADMIN_ROLE):
policy.owners |= members
elif role in (EDITOR_ROLE, PUBSUB_EDITOR_ROLE):
policy.editors |= members
elif role in (VIEWER_ROLE, PUBSUB_VIEWER_ROLE):
policy.viewers |= members
elif role == PUBSUB_PUBLISHER_ROLE:
policy.publishers |= members
elif role == PUBSUB_SUBSCRIBER_ROLE:
policy.subscribers |= members
else:
raise ValueError('Unknown role: %s' % (role,))
return policy
def to_api_repr(self):
"""Construct a Policy resource.
:rtype: dict
:returns: a resource to be passed to the ``setIamPolicy`` API.
"""
resource = {}
if self.etag is not None:
resource['etag'] = self.etag
if self.version is not None:
resource['version'] = self.version
bindings = []
if self.owners:
bindings.append(
{'role': PUBSUB_ADMIN_ROLE,
'members': sorted(self.owners)})
if self.editors:
bindings.append(
{'role': PUBSUB_EDITOR_ROLE,
'members': sorted(self.editors)})
if self.viewers:
bindings.append(
{'role': PUBSUB_VIEWER_ROLE,
'members': sorted(self.viewers)})
if self.publishers:
bindings.append(
{'role': PUBSUB_PUBLISHER_ROLE,
'members': sorted(self.publishers)})
if self.subscribers:
bindings.append(
{'role': PUBSUB_SUBSCRIBER_ROLE,
'members': sorted(self.subscribers)})
if bindings:
resource['bindings'] = bindings
return resource

View file

@ -0,0 +1,90 @@
# 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 _rfc3339_to_datetime
class Message(object):
"""Messages can be published to a topic and received by subscribers.
See:
https://cloud.google.com/pubsub/reference/rest/v1/PubsubMessage
:type data: bytes
:param data: the payload of the message
:type message_id: string
:param message_id: An ID assigned to the message by the API.
:type attributes: dict or None
:param attributes: Extra metadata associated by the publisher with the
message.
"""
_service_timestamp = None
def __init__(self, data, message_id, attributes=None):
self.data = data
self.message_id = message_id
self._attributes = attributes
@property
def attributes(self):
"""Lazily-constructed attribute dictionary"""
if self._attributes is None:
self._attributes = {}
return self._attributes
@property
def timestamp(self):
"""Return sortable timestamp from attributes, if passed.
Allows sorting messages in publication order (assuming consistent
clocks across all publishers).
:rtype: :class:`datetime.datetime`
:returns: timestamp (in UTC timezone) parsed from RFC 3339 timestamp
:raises: ValueError if timestamp not in ``attributes``, or if it does
not match the RFC 3339 format.
"""
stamp = self.attributes.get('timestamp')
if stamp is None:
raise ValueError('No timestamp')
return _rfc3339_to_datetime(stamp)
@property
def service_timestamp(self):
"""Return server-set timestamp.
:rtype: string
:returns: timestamp (in UTC timezone) in RFC 3339 format
"""
return self._service_timestamp
@classmethod
def from_api_repr(cls, api_repr):
"""Factory: construct message from API representation.
:type api_repr: dict or None
:param api_repr: The API representation of the message
"""
data = base64.b64decode(api_repr.get('data', b''))
instance = cls(
data=data, message_id=api_repr['messageId'],
attributes=api_repr.get('attributes'))
instance._service_timestamp = api_repr.get('publishTimestamp')
return instance

View file

@ -0,0 +1,422 @@
# 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 Subscriptions."""
from gcloud.exceptions import NotFound
from gcloud.pubsub._helpers import topic_name_from_path
from gcloud.pubsub.iam import Policy
from gcloud.pubsub.message import Message
class Subscription(object):
"""Subscriptions receive messages published to their topics.
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions
:type name: string
:param name: the name of the subscription
:type topic: :class:`gcloud.pubsub.topic.Topic` or ``NoneType``
:param topic: the topic to which the subscription belongs; if ``None``,
the subscription's topic has been deleted.
: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.
: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 topic.
"""
_DELETED_TOPIC_PATH = '_deleted-topic_'
"""Value of ``projects.subscriptions.topic`` when topic has been deleted.
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions#Subscription.FIELDS.topic
"""
def __init__(self, name, topic=None, ack_deadline=None, push_endpoint=None,
client=None):
if client is None and topic is None:
raise TypeError("Pass only one of 'topic' or 'client'.")
if client is not None and topic is not None:
raise TypeError("Pass only one of 'topic' or 'client'.")
self.name = name
self.topic = topic
self._client = client or topic._client
self._project = self._client.project
self.ack_deadline = ack_deadline
self.push_endpoint = push_endpoint
@classmethod
def from_api_repr(cls, resource, client, topics=None):
"""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 a topic.
:type topics: dict or None
:param topics: A mapping of topic names -> topics. If not passed,
the subscription will have a newly-created topic.
:rtype: :class:`gcloud.pubsub.subscription.Subscription`
:returns: Subscription parsed from ``resource``.
"""
if topics is None:
topics = {}
topic_path = resource['topic']
if topic_path == cls._DELETED_TOPIC_PATH:
topic = None
else:
topic = topics.get(topic_path)
if topic is None:
# NOTE: This duplicates behavior from Topic.from_api_repr to
# avoid an import cycle.
topic_name = topic_name_from_path(topic_path, client.project)
topic = topics[topic_path] = client.topic(topic_name)
_, _, _, name = resource['name'].split('/')
ack_deadline = resource.get('ackDeadlineSeconds')
push_config = resource.get('pushConfig', {})
push_endpoint = push_config.get('pushEndpoint')
if topic is None:
return cls(name, ack_deadline=ack_deadline,
push_endpoint=push_endpoint, client=client)
return cls(name, topic, ack_deadline, push_endpoint)
@property
def project(self):
"""Project bound to the subscription."""
return self._client.project
@property
def full_name(self):
"""Fully-qualified name used in subscription APIs"""
return 'projects/%s/subscriptions/%s' % (self.project, self.name)
@property
def path(self):
"""URL path for the subscription's APIs"""
return '/%s' % (self.full_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 topic of the
current subscription.
: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 subscription via a PUT request
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/create
Example:
.. literalinclude:: pubsub_snippets.py
:start-after: [START subscription_create]
:end-before: [END subscription_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 subscription's topic.
"""
client = self._require_client(client)
api = client.subscriber_api
api.subscription_create(
self.full_name, self.topic.full_name, self.ack_deadline,
self.push_endpoint)
def exists(self, client=None):
"""API call: test existence of the subscription via a GET request
See
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/get
Example:
.. literalinclude:: pubsub_snippets.py
:start-after: [START subscription_exists]
:end-before: [END subscription_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 subscription's topic.
"""
client = self._require_client(client)
api = client.subscriber_api
try:
api.subscription_get(self.full_name)
except NotFound:
return False
else:
return True
def reload(self, client=None):
"""API call: sync local subscription configuration via a GET request
See
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/get
Example:
.. literalinclude:: pubsub_snippets.py
:start-after: [START subscription_reload]
:end-before: [END subscription_reload]
: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 subscription's topic.
"""
client = self._require_client(client)
api = client.subscriber_api
data = api.subscription_get(self.full_name)
self.ack_deadline = data.get('ackDeadlineSeconds')
push_config = data.get('pushConfig', {})
self.push_endpoint = push_config.get('pushEndpoint')
def delete(self, client=None):
"""API call: delete the subscription via a DELETE request.
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/delete
Example:
.. literalinclude:: pubsub_snippets.py
:start-after: [START subscription_delete]
:end-before: [END subscription_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 subscription's topic.
"""
client = self._require_client(client)
api = client.subscriber_api
api.subscription_delete(self.full_name)
def modify_push_configuration(self, push_endpoint, client=None):
"""API call: update the push endpoint for the subscription.
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/modifyPushConfig
Example:
.. literalinclude:: pubsub_snippets.py
:start-after: [START subscription_push_pull]
:end-before: [END subscription_push_pull]
.. literalinclude:: pubsub_snippets.py
:start-after: [START subscription_pull_push]
:end-before: [END subscription_pull_push]
:type push_endpoint: string
:param push_endpoint: URL to which messages will be pushed by the
back-end. If None, the application must pull
messages.
: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 subscription's topic.
"""
client = self._require_client(client)
api = client.subscriber_api
api.subscription_modify_push_config(self.full_name, push_endpoint)
self.push_endpoint = push_endpoint
def pull(self, return_immediately=False, max_messages=1, client=None):
"""API call: retrieve messages for the subscription.
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/pull
Example:
.. literalinclude:: pubsub_snippets.py
:start-after: [START subscription_pull]
:end-before: [END subscription_pull]
:type return_immediately: boolean
:param return_immediately: if True, the back-end returns even if no
messages are available; if False, the API
call blocks until one or more messages are
available.
:type max_messages: int
:param max_messages: the maximum number of messages to return.
: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 subscription's topic.
:rtype: list of (ack_id, message) tuples
:returns: sequence of tuples: ``ack_id`` is the ID to be used in a
subsequent call to :meth:`acknowledge`, and ``message``
is an instance of :class:`gcloud.pubsub.message.Message`.
"""
client = self._require_client(client)
api = client.subscriber_api
response = api.subscription_pull(
self.full_name, return_immediately, max_messages)
return [(info['ackId'], Message.from_api_repr(info['message']))
for info in response]
def acknowledge(self, ack_ids, client=None):
"""API call: acknowledge retrieved messages for the subscription.
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/acknowledge
Example:
.. literalinclude:: pubsub_snippets.py
:start-after: [START subscription_acknowledge]
:end-before: [END subscription_acknowledge]
:type ack_ids: list of string
:param ack_ids: ack IDs of messages being acknowledged
: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 subscription's topic.
"""
client = self._require_client(client)
api = client.subscriber_api
api.subscription_acknowledge(self.full_name, ack_ids)
def modify_ack_deadline(self, ack_ids, ack_deadline, client=None):
"""API call: update acknowledgement deadline for a retrieved message.
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/modifyAckDeadline
:type ack_ids: list of string
:param ack_ids: ack IDs of messages being updated
:type ack_deadline: int
:param ack_deadline: new deadline for the message, in seconds
: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 subscription's topic.
"""
client = self._require_client(client)
api = client.subscriber_api
api.subscription_modify_ack_deadline(
self.full_name, ack_ids, ack_deadline)
def get_iam_policy(self, client=None):
"""Fetch the IAM policy for the subscription.
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/getIamPolicy
Example:
.. literalinclude:: pubsub_snippets.py
:start-after: [START subscription_get_iam_policy]
:end-before: [END subscription_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 subscription's topic.
: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 subscription.
See:
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/setIamPolicy
Example:
.. literalinclude:: pubsub_snippets.py
:start-after: [START subscription_set_iam_policy]
:end-before: [END subscription_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 subscription's topic.
: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.subscriptions/testIamPermissions
Example:
.. literalinclude:: pubsub_snippets.py
:start-after: [START subscription_check_iam_permissions]
:end-before: [END subscription_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 subscription's topic.
: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))

View file

@ -0,0 +1,944 @@
# Copyright 2016 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.
import unittest2
try:
# pylint: disable=unused-import
import gcloud.pubsub._gax
# pylint: enable=unused-import
except ImportError: # pragma: NO COVER
_HAVE_GAX = False
else:
_HAVE_GAX = True
class _Base(object):
PROJECT = 'PROJECT'
PROJECT_PATH = 'projects/%s' % (PROJECT,)
LIST_TOPICS_PATH = '%s/topics' % (PROJECT_PATH,)
TOPIC_NAME = 'topic_name'
TOPIC_PATH = 'projects/%s/topics/%s' % (PROJECT, TOPIC_NAME)
LIST_TOPIC_SUBSCRIPTIONS_PATH = '%s/subscriptions' % (TOPIC_PATH,)
SUB_NAME = 'sub_name'
SUB_PATH = '%s/subscriptions/%s' % (TOPIC_PATH, SUB_NAME)
def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw)
@unittest2.skipUnless(_HAVE_GAX, 'No gax-python')
class Test_PublisherAPI(_Base, unittest2.TestCase):
def _getTargetClass(self):
from gcloud.pubsub._gax import _PublisherAPI
return _PublisherAPI
def test_ctor(self):
gax_api = _GAXPublisherAPI()
api = self._makeOne(gax_api)
self.assertTrue(api._gax_api is gax_api)
def test_list_topics_no_paging(self):
from google.gax import INITIAL_PAGE
from gcloud._testing import _GAXPageIterator
TOKEN = 'TOKEN'
response = _GAXPageIterator([_TopicPB(self.TOPIC_PATH)], TOKEN)
gax_api = _GAXPublisherAPI(_list_topics_response=response)
api = self._makeOne(gax_api)
topics, next_token = api.list_topics(self.PROJECT)
self.assertEqual(len(topics), 1)
topic = topics[0]
self.assertIsInstance(topic, dict)
self.assertEqual(topic['name'], self.TOPIC_PATH)
self.assertEqual(next_token, TOKEN)
name, page_size, options = gax_api._list_topics_called_with
self.assertEqual(name, self.PROJECT_PATH)
self.assertEqual(page_size, 0)
self.assertTrue(options.page_token is INITIAL_PAGE)
def test_list_topics_with_paging(self):
from gcloud._testing import _GAXPageIterator
SIZE = 23
TOKEN = 'TOKEN'
NEW_TOKEN = 'NEW_TOKEN'
response = _GAXPageIterator(
[_TopicPB(self.TOPIC_PATH)], NEW_TOKEN)
gax_api = _GAXPublisherAPI(_list_topics_response=response)
api = self._makeOne(gax_api)
topics, next_token = api.list_topics(
self.PROJECT, page_size=SIZE, page_token=TOKEN)
self.assertEqual(len(topics), 1)
topic = topics[0]
self.assertIsInstance(topic, dict)
self.assertEqual(topic['name'], self.TOPIC_PATH)
self.assertEqual(next_token, NEW_TOKEN)
name, page_size, options = gax_api._list_topics_called_with
self.assertEqual(name, self.PROJECT_PATH)
self.assertEqual(page_size, SIZE)
self.assertEqual(options.page_token, TOKEN)
def test_topic_create(self):
topic_pb = _TopicPB(self.TOPIC_PATH)
gax_api = _GAXPublisherAPI(_create_topic_response=topic_pb)
api = self._makeOne(gax_api)
resource = api.topic_create(self.TOPIC_PATH)
self.assertEqual(resource, {'name': self.TOPIC_PATH})
topic_path, options = gax_api._create_topic_called_with
self.assertEqual(topic_path, self.TOPIC_PATH)
self.assertEqual(options, None)
def test_topic_create_already_exists(self):
from gcloud.exceptions import Conflict
gax_api = _GAXPublisherAPI(_create_topic_conflict=True)
api = self._makeOne(gax_api)
with self.assertRaises(Conflict):
api.topic_create(self.TOPIC_PATH)
topic_path, options = gax_api._create_topic_called_with
self.assertEqual(topic_path, self.TOPIC_PATH)
self.assertEqual(options, None)
def test_topic_create_error(self):
from google.gax.errors import GaxError
gax_api = _GAXPublisherAPI(_random_gax_error=True)
api = self._makeOne(gax_api)
with self.assertRaises(GaxError):
api.topic_create(self.TOPIC_PATH)
topic_path, options = gax_api._create_topic_called_with
self.assertEqual(topic_path, self.TOPIC_PATH)
self.assertEqual(options, None)
def test_topic_get_hit(self):
topic_pb = _TopicPB(self.TOPIC_PATH)
gax_api = _GAXPublisherAPI(_get_topic_response=topic_pb)
api = self._makeOne(gax_api)
resource = api.topic_get(self.TOPIC_PATH)
self.assertEqual(resource, {'name': self.TOPIC_PATH})
topic_path, options = gax_api._get_topic_called_with
self.assertEqual(topic_path, self.TOPIC_PATH)
self.assertEqual(options, None)
def test_topic_get_miss(self):
from gcloud.exceptions import NotFound
gax_api = _GAXPublisherAPI()
api = self._makeOne(gax_api)
with self.assertRaises(NotFound):
api.topic_get(self.TOPIC_PATH)
topic_path, options = gax_api._get_topic_called_with
self.assertEqual(topic_path, self.TOPIC_PATH)
self.assertEqual(options, None)
def test_topic_get_error(self):
from google.gax.errors import GaxError
gax_api = _GAXPublisherAPI(_random_gax_error=True)
api = self._makeOne(gax_api)
with self.assertRaises(GaxError):
api.topic_get(self.TOPIC_PATH)
topic_path, options = gax_api._get_topic_called_with
self.assertEqual(topic_path, self.TOPIC_PATH)
self.assertEqual(options, None)
def test_topic_delete_hit(self):
gax_api = _GAXPublisherAPI(_delete_topic_ok=True)
api = self._makeOne(gax_api)
api.topic_delete(self.TOPIC_PATH)
topic_path, options = gax_api._delete_topic_called_with
self.assertEqual(topic_path, self.TOPIC_PATH)
self.assertEqual(options, None)
def test_topic_delete_miss(self):
from gcloud.exceptions import NotFound
gax_api = _GAXPublisherAPI(_delete_topic_ok=False)
api = self._makeOne(gax_api)
with self.assertRaises(NotFound):
api.topic_delete(self.TOPIC_PATH)
topic_path, options = gax_api._delete_topic_called_with
self.assertEqual(topic_path, self.TOPIC_PATH)
self.assertEqual(options, None)
def test_topic_delete_error(self):
from google.gax.errors import GaxError
gax_api = _GAXPublisherAPI(_random_gax_error=True)
api = self._makeOne(gax_api)
with self.assertRaises(GaxError):
api.topic_delete(self.TOPIC_PATH)
topic_path, options = gax_api._delete_topic_called_with
self.assertEqual(topic_path, self.TOPIC_PATH)
self.assertEqual(options, None)
def test_topic_publish_hit(self):
import base64
PAYLOAD = b'This is the message text'
B64 = base64.b64encode(PAYLOAD).decode('ascii')
MSGID = 'DEADBEEF'
MESSAGE = {'data': B64, 'attributes': {}}
response = _PublishResponsePB([MSGID])
gax_api = _GAXPublisherAPI(_publish_response=response)
api = self._makeOne(gax_api)
resource = api.topic_publish(self.TOPIC_PATH, [MESSAGE])
self.assertEqual(resource, [MSGID])
topic_path, message_pbs, options = gax_api._publish_called_with
self.assertEqual(topic_path, self.TOPIC_PATH)
message_pb, = message_pbs
self.assertEqual(message_pb.data, B64)
self.assertEqual(message_pb.attributes, {})
self.assertEqual(options.is_bundling, False)
def test_topic_publish_miss_w_attrs_w_bytes_payload(self):
import base64
from gcloud.exceptions import NotFound
PAYLOAD = u'This is the message text'
B64 = base64.b64encode(PAYLOAD)
MESSAGE = {'data': B64, 'attributes': {'foo': 'bar'}}
gax_api = _GAXPublisherAPI()
api = self._makeOne(gax_api)
with self.assertRaises(NotFound):
api.topic_publish(self.TOPIC_PATH, [MESSAGE])
topic_path, message_pbs, options = gax_api._publish_called_with
self.assertEqual(topic_path, self.TOPIC_PATH)
message_pb, = message_pbs
self.assertEqual(message_pb.data, B64)
self.assertEqual(message_pb.attributes, {'foo': 'bar'})
self.assertEqual(options.is_bundling, False)
def test_topic_publish_error(self):
import base64
from google.gax.errors import GaxError
PAYLOAD = b'This is the message text'
B64 = base64.b64encode(PAYLOAD).decode('ascii')
MESSAGE = {'data': B64, 'attributes': {}}
gax_api = _GAXPublisherAPI(_random_gax_error=True)
api = self._makeOne(gax_api)
with self.assertRaises(GaxError):
api.topic_publish(self.TOPIC_PATH, [MESSAGE])
topic_path, message_pbs, options = gax_api._publish_called_with
self.assertEqual(topic_path, self.TOPIC_PATH)
message_pb, = message_pbs
self.assertEqual(message_pb.data, B64)
self.assertEqual(message_pb.attributes, {})
self.assertEqual(options.is_bundling, False)
def test_topic_list_subscriptions_no_paging(self):
from google.gax import INITIAL_PAGE
from gcloud._testing import _GAXPageIterator
response = _GAXPageIterator([
{'name': self.SUB_PATH, 'topic': self.TOPIC_PATH}], None)
gax_api = _GAXPublisherAPI(_list_topic_subscriptions_response=response)
api = self._makeOne(gax_api)
subscriptions, next_token = api.topic_list_subscriptions(
self.TOPIC_PATH)
self.assertEqual(len(subscriptions), 1)
subscription = subscriptions[0]
self.assertIsInstance(subscription, dict)
self.assertEqual(subscription['name'], self.SUB_PATH)
self.assertEqual(subscription['topic'], self.TOPIC_PATH)
self.assertEqual(next_token, None)
topic_path, page_size, options = (
gax_api._list_topic_subscriptions_called_with)
self.assertEqual(topic_path, self.TOPIC_PATH)
self.assertEqual(page_size, 0)
self.assertTrue(options.page_token is INITIAL_PAGE)
def test_topic_list_subscriptions_with_paging(self):
from gcloud._testing import _GAXPageIterator
SIZE = 23
TOKEN = 'TOKEN'
NEW_TOKEN = 'NEW_TOKEN'
response = _GAXPageIterator([
{'name': self.SUB_PATH, 'topic': self.TOPIC_PATH}], NEW_TOKEN)
gax_api = _GAXPublisherAPI(_list_topic_subscriptions_response=response)
api = self._makeOne(gax_api)
subscriptions, next_token = api.topic_list_subscriptions(
self.TOPIC_PATH, page_size=SIZE, page_token=TOKEN)
self.assertEqual(len(subscriptions), 1)
subscription = subscriptions[0]
self.assertIsInstance(subscription, dict)
self.assertEqual(subscription['name'], self.SUB_PATH)
self.assertEqual(subscription['topic'], self.TOPIC_PATH)
self.assertEqual(next_token, NEW_TOKEN)
name, page_size, options = (
gax_api._list_topic_subscriptions_called_with)
self.assertEqual(name, self.TOPIC_PATH)
self.assertEqual(page_size, SIZE)
self.assertEqual(options.page_token, TOKEN)
def test_topic_list_subscriptions_miss(self):
from google.gax import INITIAL_PAGE
from gcloud.exceptions import NotFound
gax_api = _GAXPublisherAPI()
api = self._makeOne(gax_api)
with self.assertRaises(NotFound):
api.topic_list_subscriptions(self.TOPIC_PATH)
topic_path, page_size, options = (
gax_api._list_topic_subscriptions_called_with)
self.assertEqual(topic_path, self.TOPIC_PATH)
self.assertEqual(page_size, 0)
self.assertTrue(options.page_token is INITIAL_PAGE)
def test_topic_list_subscriptions_error(self):
from google.gax import INITIAL_PAGE
from google.gax.errors import GaxError
gax_api = _GAXPublisherAPI(_random_gax_error=True)
api = self._makeOne(gax_api)
with self.assertRaises(GaxError):
api.topic_list_subscriptions(self.TOPIC_PATH)
topic_path, page_size, options = (
gax_api._list_topic_subscriptions_called_with)
self.assertEqual(topic_path, self.TOPIC_PATH)
self.assertEqual(page_size, 0)
self.assertTrue(options.page_token is INITIAL_PAGE)
@unittest2.skipUnless(_HAVE_GAX, 'No gax-python')
class Test_SubscriberAPI(_Base, unittest2.TestCase):
PUSH_ENDPOINT = 'https://api.example.com/push'
def _getTargetClass(self):
from gcloud.pubsub._gax import _SubscriberAPI
return _SubscriberAPI
def test_ctor(self):
gax_api = _GAXSubscriberAPI()
api = self._makeOne(gax_api)
self.assertTrue(api._gax_api is gax_api)
def test_list_subscriptions_no_paging(self):
from google.gax import INITIAL_PAGE
from gcloud._testing import _GAXPageIterator
response = _GAXPageIterator([_SubscriptionPB(
self.SUB_PATH, self.TOPIC_PATH, self.PUSH_ENDPOINT, 0)], None)
gax_api = _GAXSubscriberAPI(_list_subscriptions_response=response)
api = self._makeOne(gax_api)
subscriptions, next_token = api.list_subscriptions(self.PROJECT)
self.assertEqual(len(subscriptions), 1)
subscription = subscriptions[0]
self.assertIsInstance(subscription, dict)
self.assertEqual(subscription['name'], self.SUB_PATH)
self.assertEqual(subscription['topic'], self.TOPIC_PATH)
self.assertEqual(subscription['pushConfig'],
{'pushEndpoint': self.PUSH_ENDPOINT})
self.assertEqual(subscription['ackDeadlineSeconds'], 0)
self.assertEqual(next_token, None)
name, page_size, options = gax_api._list_subscriptions_called_with
self.assertEqual(name, self.PROJECT_PATH)
self.assertEqual(page_size, 0)
self.assertTrue(options.page_token is INITIAL_PAGE)
def test_list_subscriptions_with_paging(self):
from gcloud._testing import _GAXPageIterator
SIZE = 23
TOKEN = 'TOKEN'
NEW_TOKEN = 'NEW_TOKEN'
response = _GAXPageIterator([_SubscriptionPB(
self.SUB_PATH, self.TOPIC_PATH, self.PUSH_ENDPOINT, 0)], NEW_TOKEN)
gax_api = _GAXSubscriberAPI(_list_subscriptions_response=response)
api = self._makeOne(gax_api)
subscriptions, next_token = api.list_subscriptions(
self.PROJECT, page_size=SIZE, page_token=TOKEN)
self.assertEqual(len(subscriptions), 1)
subscription = subscriptions[0]
self.assertIsInstance(subscription, dict)
self.assertEqual(subscription['name'], self.SUB_PATH)
self.assertEqual(subscription['topic'], self.TOPIC_PATH)
self.assertEqual(subscription['pushConfig'],
{'pushEndpoint': self.PUSH_ENDPOINT})
self.assertEqual(subscription['ackDeadlineSeconds'], 0)
self.assertEqual(next_token, NEW_TOKEN)
name, page_size, options = gax_api._list_subscriptions_called_with
self.assertEqual(name, self.PROJECT_PATH)
self.assertEqual(page_size, 23)
self.assertEqual(options.page_token, TOKEN)
def test_subscription_create(self):
sub_pb = _SubscriptionPB(self.SUB_PATH, self.TOPIC_PATH, '', 0)
gax_api = _GAXSubscriberAPI(_create_subscription_response=sub_pb)
api = self._makeOne(gax_api)
resource = api.subscription_create(self.SUB_PATH, self.TOPIC_PATH)
expected = {
'name': self.SUB_PATH,
'topic': self.TOPIC_PATH,
'ackDeadlineSeconds': 0,
}
self.assertEqual(resource, expected)
name, topic, push_config, ack_deadline, options = (
gax_api._create_subscription_called_with)
self.assertEqual(name, self.SUB_PATH)
self.assertEqual(topic, self.TOPIC_PATH)
self.assertEqual(push_config, None)
self.assertEqual(ack_deadline, 0)
self.assertEqual(options, None)
def test_subscription_create_already_exists(self):
from gcloud.exceptions import Conflict
DEADLINE = 600
gax_api = _GAXSubscriberAPI(_create_subscription_conflict=True)
api = self._makeOne(gax_api)
with self.assertRaises(Conflict):
api.subscription_create(
self.SUB_PATH, self.TOPIC_PATH, DEADLINE, self.PUSH_ENDPOINT)
name, topic, push_config, ack_deadline, options = (
gax_api._create_subscription_called_with)
self.assertEqual(name, self.SUB_PATH)
self.assertEqual(topic, self.TOPIC_PATH)
self.assertEqual(push_config.push_endpoint, self.PUSH_ENDPOINT)
self.assertEqual(ack_deadline, DEADLINE)
self.assertEqual(options, None)
def test_subscription_create_error(self):
from google.gax.errors import GaxError
gax_api = _GAXSubscriberAPI(_random_gax_error=True)
api = self._makeOne(gax_api)
with self.assertRaises(GaxError):
api.subscription_create(self.SUB_PATH, self.TOPIC_PATH)
name, topic, push_config, ack_deadline, options = (
gax_api._create_subscription_called_with)
self.assertEqual(name, self.SUB_PATH)
self.assertEqual(topic, self.TOPIC_PATH)
self.assertEqual(push_config, None)
self.assertEqual(ack_deadline, 0)
self.assertEqual(options, None)
def test_subscription_get_hit(self):
sub_pb = _SubscriptionPB(
self.SUB_PATH, self.TOPIC_PATH, self.PUSH_ENDPOINT, 0)
gax_api = _GAXSubscriberAPI(_get_subscription_response=sub_pb)
api = self._makeOne(gax_api)
resource = api.subscription_get(self.SUB_PATH)
expected = {
'name': self.SUB_PATH,
'topic': self.TOPIC_PATH,
'ackDeadlineSeconds': 0,
'pushConfig': {
'pushEndpoint': self.PUSH_ENDPOINT,
},
}
self.assertEqual(resource, expected)
sub_path, options = gax_api._get_subscription_called_with
self.assertEqual(sub_path, self.SUB_PATH)
self.assertEqual(options, None)
def test_subscription_get_miss(self):
from gcloud.exceptions import NotFound
gax_api = _GAXSubscriberAPI()
api = self._makeOne(gax_api)
with self.assertRaises(NotFound):
api.subscription_get(self.SUB_PATH)
sub_path, options = gax_api._get_subscription_called_with
self.assertEqual(sub_path, self.SUB_PATH)
self.assertEqual(options, None)
def test_subscription_get_error(self):
from google.gax.errors import GaxError
gax_api = _GAXSubscriberAPI(_random_gax_error=True)
api = self._makeOne(gax_api)
with self.assertRaises(GaxError):
api.subscription_get(self.SUB_PATH)
sub_path, options = gax_api._get_subscription_called_with
self.assertEqual(sub_path, self.SUB_PATH)
self.assertEqual(options, None)
def test_subscription_delete_hit(self):
gax_api = _GAXSubscriberAPI(_delete_subscription_ok=True)
api = self._makeOne(gax_api)
api.subscription_delete(self.TOPIC_PATH)
sub_path, options = gax_api._delete_subscription_called_with
self.assertEqual(sub_path, self.TOPIC_PATH)
self.assertEqual(options, None)
def test_subscription_delete_miss(self):
from gcloud.exceptions import NotFound
gax_api = _GAXSubscriberAPI(_delete_subscription_ok=False)
api = self._makeOne(gax_api)
with self.assertRaises(NotFound):
api.subscription_delete(self.TOPIC_PATH)
sub_path, options = gax_api._delete_subscription_called_with
self.assertEqual(sub_path, self.TOPIC_PATH)
self.assertEqual(options, None)
def test_subscription_delete_error(self):
from google.gax.errors import GaxError
gax_api = _GAXSubscriberAPI(_random_gax_error=True)
api = self._makeOne(gax_api)
with self.assertRaises(GaxError):
api.subscription_delete(self.TOPIC_PATH)
sub_path, options = gax_api._delete_subscription_called_with
self.assertEqual(sub_path, self.TOPIC_PATH)
self.assertEqual(options, None)
def test_subscription_modify_push_config_hit(self):
gax_api = _GAXSubscriberAPI(_modify_push_config_ok=True)
api = self._makeOne(gax_api)
api.subscription_modify_push_config(self.SUB_PATH, self.PUSH_ENDPOINT)
sub_path, config, options = gax_api._modify_push_config_called_with
self.assertEqual(sub_path, self.SUB_PATH)
self.assertEqual(config.push_endpoint, self.PUSH_ENDPOINT)
self.assertEqual(options, None)
def test_subscription_modify_push_config_miss(self):
from gcloud.exceptions import NotFound
gax_api = _GAXSubscriberAPI()
api = self._makeOne(gax_api)
with self.assertRaises(NotFound):
api.subscription_modify_push_config(
self.SUB_PATH, self.PUSH_ENDPOINT)
sub_path, config, options = gax_api._modify_push_config_called_with
self.assertEqual(sub_path, self.SUB_PATH)
self.assertEqual(config.push_endpoint, self.PUSH_ENDPOINT)
self.assertEqual(options, None)
def test_subscription_modify_push_config_error(self):
from google.gax.errors import GaxError
gax_api = _GAXSubscriberAPI(_random_gax_error=True)
api = self._makeOne(gax_api)
with self.assertRaises(GaxError):
api.subscription_modify_push_config(
self.SUB_PATH, self.PUSH_ENDPOINT)
sub_path, config, options = gax_api._modify_push_config_called_with
self.assertEqual(sub_path, self.SUB_PATH)
self.assertEqual(config.push_endpoint, self.PUSH_ENDPOINT)
self.assertEqual(options, None)
def test_subscription_pull_explicit(self):
import base64
PAYLOAD = b'This is the message text'
B64 = base64.b64encode(PAYLOAD).decode('ascii')
ACK_ID = 'DEADBEEF'
MSG_ID = 'BEADCAFE'
MESSAGE = {'messageId': MSG_ID, 'data': B64, 'attributes': {'a': 'b'}}
RECEIVED = [{'ackId': ACK_ID, 'message': MESSAGE}]
message_pb = _PubsubMessagePB(MSG_ID, B64, {'a': 'b'})
response_pb = _PullResponsePB([_ReceivedMessagePB(ACK_ID, message_pb)])
gax_api = _GAXSubscriberAPI(_pull_response=response_pb)
api = self._makeOne(gax_api)
MAX_MESSAGES = 10
received = api.subscription_pull(
self.SUB_PATH, return_immediately=True, max_messages=MAX_MESSAGES)
self.assertEqual(received, RECEIVED)
sub_path, max_messages, return_immediately, options = (
gax_api._pull_called_with)
self.assertEqual(sub_path, self.SUB_PATH)
self.assertEqual(max_messages, MAX_MESSAGES)
self.assertTrue(return_immediately)
self.assertEqual(options, None)
def test_subscription_pull_defaults_miss(self):
from gcloud.exceptions import NotFound
gax_api = _GAXSubscriberAPI()
api = self._makeOne(gax_api)
with self.assertRaises(NotFound):
api.subscription_pull(self.SUB_PATH)
sub_path, max_messages, return_immediately, options = (
gax_api._pull_called_with)
self.assertEqual(sub_path, self.SUB_PATH)
self.assertEqual(max_messages, 1)
self.assertFalse(return_immediately)
self.assertEqual(options, None)
def test_subscription_pull_defaults_error(self):
from google.gax.errors import GaxError
gax_api = _GAXSubscriberAPI(_random_gax_error=True)
api = self._makeOne(gax_api)
with self.assertRaises(GaxError):
api.subscription_pull(self.SUB_PATH)
sub_path, max_messages, return_immediately, options = (
gax_api._pull_called_with)
self.assertEqual(sub_path, self.SUB_PATH)
self.assertEqual(max_messages, 1)
self.assertFalse(return_immediately)
self.assertEqual(options, None)
def test_subscription_acknowledge_hit(self):
ACK_ID1 = 'DEADBEEF'
ACK_ID2 = 'BEADCAFE'
gax_api = _GAXSubscriberAPI(_acknowledge_ok=True)
api = self._makeOne(gax_api)
api.subscription_acknowledge(self.SUB_PATH, [ACK_ID1, ACK_ID2])
sub_path, ack_ids, options = gax_api._acknowledge_called_with
self.assertEqual(sub_path, self.SUB_PATH)
self.assertEqual(ack_ids, [ACK_ID1, ACK_ID2])
self.assertEqual(options, None)
def test_subscription_acknowledge_miss(self):
from gcloud.exceptions import NotFound
ACK_ID1 = 'DEADBEEF'
ACK_ID2 = 'BEADCAFE'
gax_api = _GAXSubscriberAPI()
api = self._makeOne(gax_api)
with self.assertRaises(NotFound):
api.subscription_acknowledge(self.SUB_PATH, [ACK_ID1, ACK_ID2])
sub_path, ack_ids, options = gax_api._acknowledge_called_with
self.assertEqual(sub_path, self.SUB_PATH)
self.assertEqual(ack_ids, [ACK_ID1, ACK_ID2])
self.assertEqual(options, None)
def test_subscription_acknowledge_error(self):
from google.gax.errors import GaxError
ACK_ID1 = 'DEADBEEF'
ACK_ID2 = 'BEADCAFE'
gax_api = _GAXSubscriberAPI(_random_gax_error=True)
api = self._makeOne(gax_api)
with self.assertRaises(GaxError):
api.subscription_acknowledge(self.SUB_PATH, [ACK_ID1, ACK_ID2])
sub_path, ack_ids, options = gax_api._acknowledge_called_with
self.assertEqual(sub_path, self.SUB_PATH)
self.assertEqual(ack_ids, [ACK_ID1, ACK_ID2])
self.assertEqual(options, None)
def test_subscription_modify_ack_deadline_hit(self):
ACK_ID1 = 'DEADBEEF'
ACK_ID2 = 'BEADCAFE'
NEW_DEADLINE = 90
gax_api = _GAXSubscriberAPI(_modify_ack_deadline_ok=True)
api = self._makeOne(gax_api)
api.subscription_modify_ack_deadline(
self.SUB_PATH, [ACK_ID1, ACK_ID2], NEW_DEADLINE)
sub_path, ack_ids, deadline, options = (
gax_api._modify_ack_deadline_called_with)
self.assertEqual(sub_path, self.SUB_PATH)
self.assertEqual(ack_ids, [ACK_ID1, ACK_ID2])
self.assertEqual(deadline, NEW_DEADLINE)
self.assertEqual(options, None)
def test_subscription_modify_ack_deadline_miss(self):
from gcloud.exceptions import NotFound
ACK_ID1 = 'DEADBEEF'
ACK_ID2 = 'BEADCAFE'
NEW_DEADLINE = 90
gax_api = _GAXSubscriberAPI()
api = self._makeOne(gax_api)
with self.assertRaises(NotFound):
api.subscription_modify_ack_deadline(
self.SUB_PATH, [ACK_ID1, ACK_ID2], NEW_DEADLINE)
sub_path, ack_ids, deadline, options = (
gax_api._modify_ack_deadline_called_with)
self.assertEqual(sub_path, self.SUB_PATH)
self.assertEqual(ack_ids, [ACK_ID1, ACK_ID2])
self.assertEqual(deadline, NEW_DEADLINE)
self.assertEqual(options, None)
def test_subscription_modify_ack_deadline_error(self):
from google.gax.errors import GaxError
ACK_ID1 = 'DEADBEEF'
ACK_ID2 = 'BEADCAFE'
NEW_DEADLINE = 90
gax_api = _GAXSubscriberAPI(_random_gax_error=True)
api = self._makeOne(gax_api)
with self.assertRaises(GaxError):
api.subscription_modify_ack_deadline(
self.SUB_PATH, [ACK_ID1, ACK_ID2], NEW_DEADLINE)
sub_path, ack_ids, deadline, options = (
gax_api._modify_ack_deadline_called_with)
self.assertEqual(sub_path, self.SUB_PATH)
self.assertEqual(ack_ids, [ACK_ID1, ACK_ID2])
self.assertEqual(deadline, NEW_DEADLINE)
self.assertEqual(options, None)
class _GaxAPIBase(object):
_random_gax_error = False
def __init__(self, **kw):
self.__dict__.update(kw)
def _make_grpc_error(self, status_code):
from grpc.framework.interfaces.face.face import AbortionError
class _DummyException(AbortionError):
code = status_code
def __init__(self):
pass
return _DummyException()
def _make_grpc_not_found(self):
from grpc.beta.interfaces import StatusCode
return self._make_grpc_error(StatusCode.NOT_FOUND)
def _make_grpc_failed_precondition(self):
from grpc.beta.interfaces import StatusCode
return self._make_grpc_error(StatusCode.FAILED_PRECONDITION)
class _GAXPublisherAPI(_GaxAPIBase):
_create_topic_conflict = False
def list_topics(self, name, page_size, options):
self._list_topics_called_with = name, page_size, options
return self._list_topics_response
def create_topic(self, name, options=None):
from google.gax.errors import GaxError
self._create_topic_called_with = name, options
if self._random_gax_error:
raise GaxError('error')
if self._create_topic_conflict:
raise GaxError('conflict', self._make_grpc_failed_precondition())
return self._create_topic_response
def get_topic(self, name, options=None):
from google.gax.errors import GaxError
self._get_topic_called_with = name, options
if self._random_gax_error:
raise GaxError('error')
try:
return self._get_topic_response
except AttributeError:
raise GaxError('miss', self._make_grpc_not_found())
def delete_topic(self, name, options=None):
from google.gax.errors import GaxError
self._delete_topic_called_with = name, options
if self._random_gax_error:
raise GaxError('error')
if not self._delete_topic_ok:
raise GaxError('miss', self._make_grpc_not_found())
def publish(self, topic, messages, options=None):
from google.gax.errors import GaxError
self._publish_called_with = topic, messages, options
if self._random_gax_error:
raise GaxError('error')
try:
return self._publish_response
except AttributeError:
raise GaxError('miss', self._make_grpc_not_found())
def list_topic_subscriptions(self, topic, page_size, options=None):
from google.gax.errors import GaxError
self._list_topic_subscriptions_called_with = topic, page_size, options
if self._random_gax_error:
raise GaxError('error')
try:
return self._list_topic_subscriptions_response
except AttributeError:
raise GaxError('miss', self._make_grpc_not_found())
class _GAXSubscriberAPI(_GaxAPIBase):
_create_subscription_conflict = False
_modify_push_config_ok = False
_acknowledge_ok = False
_modify_ack_deadline_ok = False
def list_subscriptions(self, project, page_size, options=None):
self._list_subscriptions_called_with = (project, page_size, options)
return self._list_subscriptions_response
def create_subscription(self, name, topic,
push_config, ack_deadline_seconds,
options=None):
from google.gax.errors import GaxError
self._create_subscription_called_with = (
name, topic, push_config, ack_deadline_seconds, options)
if self._random_gax_error:
raise GaxError('error')
if self._create_subscription_conflict:
raise GaxError('conflict', self._make_grpc_failed_precondition())
return self._create_subscription_response
def get_subscription(self, name, options=None):
from google.gax.errors import GaxError
self._get_subscription_called_with = name, options
if self._random_gax_error:
raise GaxError('error')
try:
return self._get_subscription_response
except AttributeError:
raise GaxError('miss', self._make_grpc_not_found())
def delete_subscription(self, name, options=None):
from google.gax.errors import GaxError
self._delete_subscription_called_with = name, options
if self._random_gax_error:
raise GaxError('error')
if not self._delete_subscription_ok:
raise GaxError('miss', self._make_grpc_not_found())
def modify_push_config(self, name, push_config, options=None):
from google.gax.errors import GaxError
self._modify_push_config_called_with = name, push_config, options
if self._random_gax_error:
raise GaxError('error')
if not self._modify_push_config_ok:
raise GaxError('miss', self._make_grpc_not_found())
def pull(self, name, max_messages, return_immediately, options=None):
from google.gax.errors import GaxError
self._pull_called_with = (
name, max_messages, return_immediately, options)
if self._random_gax_error:
raise GaxError('error')
try:
return self._pull_response
except AttributeError:
raise GaxError('miss', self._make_grpc_not_found())
def acknowledge(self, name, ack_ids, options=None):
from google.gax.errors import GaxError
self._acknowledge_called_with = name, ack_ids, options
if self._random_gax_error:
raise GaxError('error')
if not self._acknowledge_ok:
raise GaxError('miss', self._make_grpc_not_found())
def modify_ack_deadline(self, name, ack_ids, deadline, options=None):
from google.gax.errors import GaxError
self._modify_ack_deadline_called_with = (
name, ack_ids, deadline, options)
if self._random_gax_error:
raise GaxError('error')
if not self._modify_ack_deadline_ok:
raise GaxError('miss', self._make_grpc_not_found())
class _TopicPB(object):
def __init__(self, name):
self.name = name
class _PublishResponsePB(object):
def __init__(self, message_ids):
self.message_ids = message_ids
class _PushConfigPB(object):
def __init__(self, push_endpoint):
self.push_endpoint = push_endpoint
class _PubsubMessagePB(object):
def __init__(self, message_id, data, attributes):
self.message_id = message_id
self.data = data
self.attributes = attributes
class _ReceivedMessagePB(object):
def __init__(self, ack_id, message):
self.ack_id = ack_id
self.message = message
class _PullResponsePB(object):
def __init__(self, received_messages):
self.received_messages = received_messages
class _SubscriptionPB(object):
def __init__(self, name, topic, push_endpoint, ack_deadline_seconds):
self.name = name
self.topic = topic
self.push_config = _PushConfigPB(push_endpoint)
self.ack_deadline_seconds = ack_deadline_seconds

View file

@ -0,0 +1,57 @@
# 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.
import unittest2
class Test_topic_name_from_path(unittest2.TestCase):
def _callFUT(self, path, project):
from gcloud.pubsub._helpers import topic_name_from_path
return topic_name_from_path(path, project)
def test_w_simple_name(self):
TOPIC_NAME = 'TOPIC_NAME'
PROJECT = 'my-project-1234'
PATH = 'projects/%s/topics/%s' % (PROJECT, TOPIC_NAME)
topic_name = self._callFUT(PATH, PROJECT)
self.assertEqual(topic_name, TOPIC_NAME)
def test_w_name_w_all_extras(self):
TOPIC_NAME = 'TOPIC_NAME-part.one~part.two%part-three'
PROJECT = 'my-project-1234'
PATH = 'projects/%s/topics/%s' % (PROJECT, TOPIC_NAME)
topic_name = self._callFUT(PATH, PROJECT)
self.assertEqual(topic_name, TOPIC_NAME)
class Test_subscription_name_from_path(unittest2.TestCase):
def _callFUT(self, path, project):
from gcloud.pubsub._helpers import subscription_name_from_path
return subscription_name_from_path(path, project)
def test_w_simple_name(self):
SUBSCRIPTION_NAME = 'SUBSCRIPTION_NAME'
PROJECT = 'my-project-1234'
PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUBSCRIPTION_NAME)
subscription_name = self._callFUT(PATH, PROJECT)
self.assertEqual(subscription_name, SUBSCRIPTION_NAME)
def test_w_name_w_all_extras(self):
SUBSCRIPTION_NAME = 'SUBSCRIPTION_NAME-part.one~part.two%part-three'
PROJECT = 'my-project-1234'
PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUBSCRIPTION_NAME)
topic_name = self._callFUT(PATH, PROJECT)
self.assertEqual(topic_name, SUBSCRIPTION_NAME)

View file

@ -0,0 +1,299 @@
# 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.
import unittest2
class TestClient(unittest2.TestCase):
PROJECT = 'PROJECT'
TOPIC_NAME = 'topic_name'
TOPIC_PATH = 'projects/%s/topics/%s' % (PROJECT, TOPIC_NAME)
SUB_NAME = 'subscription_name'
SUB_PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME)
def _getTargetClass(self):
from gcloud.pubsub.client import Client
return Client
def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw)
def test_publisher_api_wo_gax(self):
from gcloud.pubsub.connection import _PublisherAPI
from gcloud.pubsub import client as MUT
from gcloud._testing import _Monkey
creds = _Credentials()
client = self._makeOne(project=self.PROJECT, credentials=creds)
conn = client.connection = object()
with _Monkey(MUT, _USE_GAX=False):
api = client.publisher_api
self.assertIsInstance(api, _PublisherAPI)
self.assertTrue(api._connection is conn)
# API instance is cached
again = client.publisher_api
self.assertTrue(again is api)
def test_publisher_api_w_gax(self):
from gcloud.pubsub import client as MUT
from gcloud._testing import _Monkey
wrapped = object()
_called_with = []
def _generated_api(*args, **kw):
_called_with.append((args, kw))
return wrapped
class _GaxPublisherAPI(object):
def __init__(self, _wrapped):
self._wrapped = _wrapped
creds = _Credentials()
client = self._makeOne(project=self.PROJECT, credentials=creds)
with _Monkey(MUT,
_USE_GAX=True,
GeneratedPublisherAPI=_generated_api,
GAXPublisherAPI=_GaxPublisherAPI):
api = client.publisher_api
self.assertIsInstance(api, _GaxPublisherAPI)
self.assertTrue(api._wrapped is wrapped)
# API instance is cached
again = client.publisher_api
self.assertTrue(again is api)
def test_subscriber_api_wo_gax(self):
from gcloud.pubsub.connection import _SubscriberAPI
from gcloud.pubsub import client as MUT
from gcloud._testing import _Monkey
creds = _Credentials()
client = self._makeOne(project=self.PROJECT, credentials=creds)
conn = client.connection = object()
with _Monkey(MUT, _USE_GAX=False):
api = client.subscriber_api
self.assertIsInstance(api, _SubscriberAPI)
self.assertTrue(api._connection is conn)
# API instance is cached
again = client.subscriber_api
self.assertTrue(again is api)
def test_subscriber_api_w_gax(self):
from gcloud.pubsub import client as MUT
from gcloud._testing import _Monkey
wrapped = object()
_called_with = []
def _generated_api(*args, **kw):
_called_with.append((args, kw))
return wrapped
class _GaxSubscriberAPI(object):
def __init__(self, _wrapped):
self._wrapped = _wrapped
creds = _Credentials()
client = self._makeOne(project=self.PROJECT, credentials=creds)
with _Monkey(MUT,
_USE_GAX=True,
GeneratedSubscriberAPI=_generated_api,
GAXSubscriberAPI=_GaxSubscriberAPI):
api = client.subscriber_api
self.assertIsInstance(api, _GaxSubscriberAPI)
self.assertTrue(api._wrapped is wrapped)
# API instance is cached
again = client.subscriber_api
self.assertTrue(again is api)
def test_iam_policy_api(self):
from gcloud.pubsub.connection import _IAMPolicyAPI
creds = _Credentials()
client = self._makeOne(project=self.PROJECT, credentials=creds)
conn = client.connection = object()
api = client.iam_policy_api
self.assertIsInstance(api, _IAMPolicyAPI)
self.assertTrue(api._connection is conn)
# API instance is cached
again = client.iam_policy_api
self.assertTrue(again is api)
def test_list_topics_no_paging(self):
from gcloud.pubsub.topic import Topic
creds = _Credentials()
client = self._makeOne(project=self.PROJECT, credentials=creds)
client.connection = object()
api = client._publisher_api = _FauxPublisherAPI()
api._list_topics_response = [{'name': self.TOPIC_PATH}], None
topics, next_page_token = client.list_topics()
self.assertEqual(len(topics), 1)
self.assertIsInstance(topics[0], Topic)
self.assertEqual(topics[0].name, self.TOPIC_NAME)
self.assertEqual(next_page_token, None)
self.assertEqual(api._listed_topics, (self.PROJECT, None, None))
def test_list_topics_with_paging(self):
from gcloud.pubsub.topic import Topic
TOKEN1 = 'TOKEN1'
TOKEN2 = 'TOKEN2'
SIZE = 1
creds = _Credentials()
client = self._makeOne(project=self.PROJECT, credentials=creds)
client.connection = object()
api = client._publisher_api = _FauxPublisherAPI()
api._list_topics_response = [{'name': self.TOPIC_PATH}], TOKEN2
topics, next_page_token = client.list_topics(SIZE, TOKEN1)
self.assertEqual(len(topics), 1)
self.assertIsInstance(topics[0], Topic)
self.assertEqual(topics[0].name, self.TOPIC_NAME)
self.assertEqual(next_page_token, TOKEN2)
self.assertEqual(api._listed_topics, (self.PROJECT, 1, TOKEN1))
def test_list_topics_missing_key(self):
creds = _Credentials()
client = self._makeOne(project=self.PROJECT, credentials=creds)
client.connection = object()
api = client._publisher_api = _FauxPublisherAPI()
api._list_topics_response = (), None
topics, next_page_token = client.list_topics()
self.assertEqual(len(topics), 0)
self.assertEqual(next_page_token, None)
self.assertEqual(api._listed_topics, (self.PROJECT, None, None))
def test_list_subscriptions_no_paging(self):
from gcloud.pubsub.subscription import Subscription
SUB_INFO = {'name': self.SUB_PATH, 'topic': self.TOPIC_PATH}
creds = _Credentials()
client = self._makeOne(project=self.PROJECT, credentials=creds)
client.connection = object()
api = client._subscriber_api = _FauxSubscriberAPI()
api._list_subscriptions_response = [SUB_INFO], None
subscriptions, next_page_token = client.list_subscriptions()
self.assertEqual(len(subscriptions), 1)
self.assertIsInstance(subscriptions[0], Subscription)
self.assertEqual(subscriptions[0].name, self.SUB_NAME)
self.assertEqual(subscriptions[0].topic.name, self.TOPIC_NAME)
self.assertEqual(next_page_token, None)
self.assertEqual(api._listed_subscriptions,
(self.PROJECT, None, None))
def test_list_subscriptions_with_paging(self):
from gcloud.pubsub.subscription import Subscription
SUB_INFO = {'name': self.SUB_PATH, 'topic': self.TOPIC_PATH}
creds = _Credentials()
client = self._makeOne(project=self.PROJECT, credentials=creds)
ACK_DEADLINE = 42
PUSH_ENDPOINT = 'https://push.example.com/endpoint'
SUB_INFO = {'name': self.SUB_PATH,
'topic': self.TOPIC_PATH,
'ackDeadlineSeconds': ACK_DEADLINE,
'pushConfig': {'pushEndpoint': PUSH_ENDPOINT}}
TOKEN1 = 'TOKEN1'
TOKEN2 = 'TOKEN2'
SIZE = 1
client.connection = object()
api = client._subscriber_api = _FauxSubscriberAPI()
api._list_subscriptions_response = [SUB_INFO], TOKEN2
subscriptions, next_page_token = client.list_subscriptions(
SIZE, TOKEN1)
self.assertEqual(len(subscriptions), 1)
self.assertIsInstance(subscriptions[0], Subscription)
self.assertEqual(subscriptions[0].name, self.SUB_NAME)
self.assertEqual(subscriptions[0].topic.name, self.TOPIC_NAME)
self.assertEqual(subscriptions[0].ack_deadline, ACK_DEADLINE)
self.assertEqual(subscriptions[0].push_endpoint, PUSH_ENDPOINT)
self.assertEqual(next_page_token, TOKEN2)
self.assertEqual(api._listed_subscriptions,
(self.PROJECT, SIZE, TOKEN1))
def test_list_subscriptions_w_missing_key(self):
PROJECT = 'PROJECT'
creds = _Credentials()
client = self._makeOne(project=PROJECT, credentials=creds)
client.connection = object()
api = client._subscriber_api = _FauxSubscriberAPI()
api._list_subscriptions_response = (), None
subscriptions, next_page_token = client.list_subscriptions()
self.assertEqual(len(subscriptions), 0)
self.assertEqual(next_page_token, None)
self.assertEqual(api._listed_subscriptions,
(self.PROJECT, None, None))
def test_topic(self):
PROJECT = 'PROJECT'
TOPIC_NAME = 'TOPIC_NAME'
creds = _Credentials()
client_obj = self._makeOne(project=PROJECT, credentials=creds)
new_topic = client_obj.topic(TOPIC_NAME)
self.assertEqual(new_topic.name, TOPIC_NAME)
self.assertTrue(new_topic._client is client_obj)
self.assertEqual(new_topic.project, PROJECT)
self.assertEqual(new_topic.full_name,
'projects/%s/topics/%s' % (PROJECT, TOPIC_NAME))
self.assertFalse(new_topic.timestamp_messages)
class _Credentials(object):
_scopes = None
@staticmethod
def create_scoped_required():
return True
def create_scoped(self, scope):
self._scopes = scope
return self
class _FauxPublisherAPI(object):
def list_topics(self, project, page_size, page_token):
self._listed_topics = (project, page_size, page_token)
return self._list_topics_response
class _FauxSubscriberAPI(object):
def list_subscriptions(self, project, page_size, page_token):
self._listed_subscriptions = (project, page_size, page_token)
return self._list_subscriptions_response

View file

@ -0,0 +1,749 @@
# 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.
import unittest2
class _Base(unittest2.TestCase):
PROJECT = 'PROJECT'
LIST_TOPICS_PATH = 'projects/%s/topics' % (PROJECT,)
LIST_SUBSCRIPTIONS_PATH = 'projects/%s/subscriptions' % (PROJECT,)
TOPIC_NAME = 'topic_name'
TOPIC_PATH = 'projects/%s/topics/%s' % (PROJECT, TOPIC_NAME)
LIST_TOPIC_SUBSCRIPTIONS_PATH = '%s/subscriptions' % (TOPIC_PATH,)
SUB_NAME = 'subscription_name'
SUB_PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME)
def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw)
class TestConnection(_Base):
def _getTargetClass(self):
from gcloud.pubsub.connection import Connection
return Connection
def test_default_url(self):
conn = self._makeOne()
klass = self._getTargetClass()
self.assertEqual(conn.api_base_url, klass.API_BASE_URL)
def test_custom_url_from_env(self):
import os
from gcloud._testing import _Monkey
from gcloud.environment_vars import PUBSUB_EMULATOR
HOST = 'localhost:8187'
fake_environ = {PUBSUB_EMULATOR: HOST}
with _Monkey(os, getenv=fake_environ.get):
conn = self._makeOne()
klass = self._getTargetClass()
self.assertNotEqual(conn.api_base_url, klass.API_BASE_URL)
self.assertEqual(conn.api_base_url, 'http://' + HOST)
def test_custom_url_from_constructor(self):
HOST = object()
conn = self._makeOne(api_base_url=HOST)
klass = self._getTargetClass()
self.assertNotEqual(conn.api_base_url, klass.API_BASE_URL)
self.assertEqual(conn.api_base_url, HOST)
def test_custom_url_constructor_and_env(self):
import os
from gcloud._testing import _Monkey
from gcloud.environment_vars import PUBSUB_EMULATOR
HOST1 = object()
HOST2 = object()
fake_environ = {PUBSUB_EMULATOR: HOST1}
with _Monkey(os, getenv=fake_environ.get):
conn = self._makeOne(api_base_url=HOST2)
klass = self._getTargetClass()
self.assertNotEqual(conn.api_base_url, klass.API_BASE_URL)
self.assertNotEqual(conn.api_base_url, HOST1)
self.assertEqual(conn.api_base_url, HOST2)
def test_build_api_url_no_extra_query_params(self):
conn = self._makeOne()
URI = '/'.join([
conn.API_BASE_URL,
conn.API_VERSION,
'foo',
])
self.assertEqual(conn.build_api_url('/foo'), URI)
def test_build_api_url_w_extra_query_params(self):
from six.moves.urllib.parse import parse_qsl
from six.moves.urllib.parse import urlsplit
conn = self._makeOne()
uri = conn.build_api_url('/foo', {'bar': 'baz'})
scheme, netloc, path, qs, _ = urlsplit(uri)
self.assertEqual('%s://%s' % (scheme, netloc), conn.API_BASE_URL)
self.assertEqual(path,
'/'.join(['', conn.API_VERSION, 'foo']))
parms = dict(parse_qsl(qs))
self.assertEqual(parms['bar'], 'baz')
def test_build_api_url_w_base_url_override(self):
base_url1 = 'api-base-url1'
base_url2 = 'api-base-url2'
conn = self._makeOne(api_base_url=base_url1)
URI = '/'.join([
base_url2,
conn.API_VERSION,
'foo',
])
self.assertEqual(conn.build_api_url('/foo', api_base_url=base_url2),
URI)
class Test_PublisherAPI(_Base):
def _getTargetClass(self):
from gcloud.pubsub.connection import _PublisherAPI
return _PublisherAPI
def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw)
def test_ctor(self):
connection = _Connection()
api = self._makeOne(connection)
self.assertTrue(api._connection is connection)
def test_list_topics_no_paging(self):
RETURNED = {'topics': [{'name': self.TOPIC_PATH}]}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
topics, next_token = api.list_topics(self.PROJECT)
self.assertEqual(len(topics), 1)
topic = topics[0]
self.assertIsInstance(topic, dict)
self.assertEqual(topic['name'], self.TOPIC_PATH)
self.assertEqual(next_token, None)
self.assertEqual(connection._called_with['method'], 'GET')
path = '/%s' % (self.LIST_TOPICS_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['query_params'], {})
def test_list_topics_with_paging(self):
TOKEN1 = 'TOKEN1'
TOKEN2 = 'TOKEN2'
SIZE = 1
RETURNED = {
'topics': [{'name': self.TOPIC_PATH}],
'nextPageToken': 'TOKEN2',
}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
topics, next_token = api.list_topics(
self.PROJECT, page_token=TOKEN1, page_size=SIZE)
self.assertEqual(len(topics), 1)
topic = topics[0]
self.assertIsInstance(topic, dict)
self.assertEqual(topic['name'], self.TOPIC_PATH)
self.assertEqual(next_token, TOKEN2)
self.assertEqual(connection._called_with['method'], 'GET')
path = '/%s' % (self.LIST_TOPICS_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['query_params'],
{'pageToken': TOKEN1, 'pageSize': SIZE})
def test_list_topics_missing_key(self):
RETURNED = {}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
topics, next_token = api.list_topics(self.PROJECT)
self.assertEqual(len(topics), 0)
self.assertEqual(next_token, None)
self.assertEqual(connection._called_with['method'], 'GET')
path = '/%s' % (self.LIST_TOPICS_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['query_params'], {})
def test_topic_create(self):
RETURNED = {'name': self.TOPIC_PATH}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
resource = api.topic_create(self.TOPIC_PATH)
self.assertEqual(resource, RETURNED)
self.assertEqual(connection._called_with['method'], 'PUT')
path = '/%s' % (self.TOPIC_PATH,)
self.assertEqual(connection._called_with['path'], path)
def test_topic_create_already_exists(self):
from gcloud.exceptions import Conflict
connection = _Connection()
connection._no_response_error = Conflict
api = self._makeOne(connection)
with self.assertRaises(Conflict):
api.topic_create(self.TOPIC_PATH)
self.assertEqual(connection._called_with['method'], 'PUT')
path = '/%s' % (self.TOPIC_PATH,)
self.assertEqual(connection._called_with['path'], path)
def test_topic_get_hit(self):
RETURNED = {'name': self.TOPIC_PATH}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
resource = api.topic_get(self.TOPIC_PATH)
self.assertEqual(resource, RETURNED)
self.assertEqual(connection._called_with['method'], 'GET')
path = '/%s' % (self.TOPIC_PATH,)
self.assertEqual(connection._called_with['path'], path)
def test_topic_get_miss(self):
from gcloud.exceptions import NotFound
connection = _Connection()
api = self._makeOne(connection)
with self.assertRaises(NotFound):
api.topic_get(self.TOPIC_PATH)
self.assertEqual(connection._called_with['method'], 'GET')
path = '/%s' % (self.TOPIC_PATH,)
self.assertEqual(connection._called_with['path'], path)
def test_topic_delete_hit(self):
RETURNED = {}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
api.topic_delete(self.TOPIC_PATH)
self.assertEqual(connection._called_with['method'], 'DELETE')
path = '/%s' % (self.TOPIC_PATH,)
self.assertEqual(connection._called_with['path'], path)
def test_topic_delete_miss(self):
from gcloud.exceptions import NotFound
connection = _Connection()
api = self._makeOne(connection)
with self.assertRaises(NotFound):
api.topic_delete(self.TOPIC_PATH)
self.assertEqual(connection._called_with['method'], 'DELETE')
path = '/%s' % (self.TOPIC_PATH,)
self.assertEqual(connection._called_with['path'], path)
def test_topic_publish_hit(self):
import base64
PAYLOAD = b'This is the message text'
B64 = base64.b64encode(PAYLOAD).decode('ascii')
MSGID = 'DEADBEEF'
MESSAGE = {'data': B64, 'attributes': {}}
RETURNED = {'messageIds': [MSGID]}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
resource = api.topic_publish(self.TOPIC_PATH, [MESSAGE])
self.assertEqual(resource, [MSGID])
self.assertEqual(connection._called_with['method'], 'POST')
path = '/%s:publish' % (self.TOPIC_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['data'],
{'messages': [MESSAGE]})
def test_topic_publish_miss(self):
import base64
from gcloud.exceptions import NotFound
PAYLOAD = b'This is the message text'
B64 = base64.b64encode(PAYLOAD).decode('ascii')
MESSAGE = {'data': B64, 'attributes': {}}
connection = _Connection()
api = self._makeOne(connection)
with self.assertRaises(NotFound):
api.topic_publish(self.TOPIC_PATH, [MESSAGE])
self.assertEqual(connection._called_with['method'], 'POST')
path = '/%s:publish' % (self.TOPIC_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['data'],
{'messages': [MESSAGE]})
def test_topic_list_subscriptions_no_paging(self):
SUB_INFO = {'name': self.SUB_PATH, 'topic': self.TOPIC_PATH}
RETURNED = {'subscriptions': [SUB_INFO]}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
subscriptions, next_token = api.topic_list_subscriptions(
self.TOPIC_PATH)
self.assertEqual(len(subscriptions), 1)
subscription = subscriptions[0]
self.assertIsInstance(subscription, dict)
self.assertEqual(subscription['name'], self.SUB_PATH)
self.assertEqual(subscription['topic'], self.TOPIC_PATH)
self.assertEqual(next_token, None)
self.assertEqual(connection._called_with['method'], 'GET')
path = '/%s' % (self.LIST_TOPIC_SUBSCRIPTIONS_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['query_params'], {})
def test_topic_list_subscriptions_with_paging(self):
TOKEN1 = 'TOKEN1'
TOKEN2 = 'TOKEN2'
SIZE = 1
SUB_INFO = {'name': self.SUB_PATH, 'topic': self.TOPIC_PATH}
RETURNED = {
'subscriptions': [SUB_INFO],
'nextPageToken': 'TOKEN2',
}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
subscriptions, next_token = api.topic_list_subscriptions(
self.TOPIC_PATH, page_token=TOKEN1, page_size=SIZE)
self.assertEqual(len(subscriptions), 1)
subscription = subscriptions[0]
self.assertIsInstance(subscription, dict)
self.assertEqual(subscription['name'], self.SUB_PATH)
self.assertEqual(subscription['topic'], self.TOPIC_PATH)
self.assertEqual(next_token, TOKEN2)
self.assertEqual(connection._called_with['method'], 'GET')
path = '/%s' % (self.LIST_TOPIC_SUBSCRIPTIONS_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['query_params'],
{'pageToken': TOKEN1, 'pageSize': SIZE})
def test_topic_list_subscriptions_missing_key(self):
RETURNED = {}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
subscriptions, next_token = api.topic_list_subscriptions(
self.TOPIC_PATH)
self.assertEqual(len(subscriptions), 0)
self.assertEqual(next_token, None)
self.assertEqual(connection._called_with['method'], 'GET')
path = '/%s' % (self.LIST_TOPIC_SUBSCRIPTIONS_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['query_params'], {})
def test_topic_list_subscriptions_miss(self):
from gcloud.exceptions import NotFound
connection = _Connection()
api = self._makeOne(connection)
with self.assertRaises(NotFound):
api.topic_list_subscriptions(self.TOPIC_PATH)
self.assertEqual(connection._called_with['method'], 'GET')
path = '/%s' % (self.LIST_TOPIC_SUBSCRIPTIONS_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['query_params'], {})
class Test_SubscriberAPI(_Base):
def _getTargetClass(self):
from gcloud.pubsub.connection import _SubscriberAPI
return _SubscriberAPI
def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw)
def test_ctor(self):
connection = _Connection()
api = self._makeOne(connection)
self.assertTrue(api._connection is connection)
def test_list_subscriptions_no_paging(self):
SUB_INFO = {'name': self.SUB_PATH, 'topic': self.TOPIC_PATH}
RETURNED = {'subscriptions': [SUB_INFO]}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
subscriptions, next_token = api.list_subscriptions(self.PROJECT)
self.assertEqual(len(subscriptions), 1)
subscription = subscriptions[0]
self.assertIsInstance(subscription, dict)
self.assertEqual(subscription['name'], self.SUB_PATH)
self.assertEqual(subscription['topic'], self.TOPIC_PATH)
self.assertEqual(next_token, None)
self.assertEqual(connection._called_with['method'], 'GET')
path = '/%s' % (self.LIST_SUBSCRIPTIONS_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['query_params'], {})
def test_list_subscriptions_with_paging(self):
TOKEN1 = 'TOKEN1'
TOKEN2 = 'TOKEN2'
SIZE = 1
SUB_INFO = {'name': self.SUB_PATH, 'topic': self.TOPIC_PATH}
RETURNED = {
'subscriptions': [SUB_INFO],
'nextPageToken': 'TOKEN2',
}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
subscriptions, next_token = api.list_subscriptions(
self.PROJECT, page_token=TOKEN1, page_size=SIZE)
self.assertEqual(len(subscriptions), 1)
subscription = subscriptions[0]
self.assertIsInstance(subscription, dict)
self.assertEqual(subscription['name'], self.SUB_PATH)
self.assertEqual(subscription['topic'], self.TOPIC_PATH)
self.assertEqual(next_token, TOKEN2)
self.assertEqual(connection._called_with['method'], 'GET')
path = '/%s' % (self.LIST_SUBSCRIPTIONS_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['query_params'],
{'pageToken': TOKEN1, 'pageSize': SIZE})
def test_list_subscriptions_missing_key(self):
RETURNED = {}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
subscriptions, next_token = api.list_subscriptions(self.PROJECT)
self.assertEqual(len(subscriptions), 0)
self.assertEqual(next_token, None)
self.assertEqual(connection._called_with['method'], 'GET')
path = '/%s' % (self.LIST_SUBSCRIPTIONS_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['query_params'], {})
def test_subscription_create_defaults(self):
RESOURCE = {'topic': self.TOPIC_PATH}
RETURNED = RESOURCE.copy()
RETURNED['name'] = self.SUB_PATH
connection = _Connection(RETURNED)
api = self._makeOne(connection)
resource = api.subscription_create(self.SUB_PATH, self.TOPIC_PATH)
self.assertEqual(resource, RETURNED)
self.assertEqual(connection._called_with['method'], 'PUT')
path = '/%s' % (self.SUB_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['data'], RESOURCE)
def test_subscription_create_explicit(self):
ACK_DEADLINE = 90
PUSH_ENDPOINT = 'https://api.example.com/push'
RESOURCE = {
'topic': self.TOPIC_PATH,
'ackDeadlineSeconds': ACK_DEADLINE,
'pushConfig': {
'pushEndpoint': PUSH_ENDPOINT,
},
}
RETURNED = RESOURCE.copy()
RETURNED['name'] = self.SUB_PATH
connection = _Connection(RETURNED)
api = self._makeOne(connection)
resource = api.subscription_create(
self.SUB_PATH, self.TOPIC_PATH,
ack_deadline=ACK_DEADLINE, push_endpoint=PUSH_ENDPOINT)
self.assertEqual(resource, RETURNED)
self.assertEqual(connection._called_with['method'], 'PUT')
path = '/%s' % (self.SUB_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['data'], RESOURCE)
def test_subscription_get(self):
ACK_DEADLINE = 90
PUSH_ENDPOINT = 'https://api.example.com/push'
RETURNED = {
'topic': self.TOPIC_PATH,
'name': self.SUB_PATH,
'ackDeadlineSeconds': ACK_DEADLINE,
'pushConfig': {'pushEndpoint': PUSH_ENDPOINT},
}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
resource = api.subscription_get(self.SUB_PATH)
self.assertEqual(resource, RETURNED)
self.assertEqual(connection._called_with['method'], 'GET')
path = '/%s' % (self.SUB_PATH,)
self.assertEqual(connection._called_with['path'], path)
def test_subscription_delete(self):
RETURNED = {}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
api.subscription_delete(self.SUB_PATH)
self.assertEqual(connection._called_with['method'], 'DELETE')
path = '/%s' % (self.SUB_PATH,)
self.assertEqual(connection._called_with['path'], path)
def test_subscription_modify_push_config(self):
PUSH_ENDPOINT = 'https://api.example.com/push'
BODY = {
'pushConfig': {'pushEndpoint': PUSH_ENDPOINT},
}
RETURNED = {}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
api.subscription_modify_push_config(self.SUB_PATH, PUSH_ENDPOINT)
self.assertEqual(connection._called_with['method'], 'POST')
path = '/%s:modifyPushConfig' % (self.SUB_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['data'], BODY)
def test_subscription_pull_defaults(self):
import base64
PAYLOAD = b'This is the message text'
B64 = base64.b64encode(PAYLOAD).decode('ascii')
ACK_ID = 'DEADBEEF'
MSG_ID = 'BEADCAFE'
MESSAGE = {'messageId': MSG_ID, 'data': B64, 'attributes': {'a': 'b'}}
RETURNED = {
'receivedMessages': [{'ackId': ACK_ID, 'message': MESSAGE}],
}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
BODY = {
'returnImmediately': False,
'maxMessages': 1,
}
received = api.subscription_pull(self.SUB_PATH)
self.assertEqual(received, RETURNED['receivedMessages'])
self.assertEqual(connection._called_with['method'], 'POST')
path = '/%s:pull' % (self.SUB_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['data'], BODY)
def test_subscription_pull_explicit(self):
import base64
PAYLOAD = b'This is the message text'
B64 = base64.b64encode(PAYLOAD).decode('ascii')
ACK_ID = 'DEADBEEF'
MSG_ID = 'BEADCAFE'
MESSAGE = {'messageId': MSG_ID, 'data': B64, 'attributes': {'a': 'b'}}
RETURNED = {
'receivedMessages': [{'ackId': ACK_ID, 'message': MESSAGE}],
}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
MAX_MESSAGES = 10
BODY = {
'returnImmediately': True,
'maxMessages': MAX_MESSAGES,
}
received = api.subscription_pull(
self.SUB_PATH, return_immediately=True, max_messages=MAX_MESSAGES)
self.assertEqual(received, RETURNED['receivedMessages'])
self.assertEqual(connection._called_with['method'], 'POST')
path = '/%s:pull' % (self.SUB_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['data'], BODY)
def test_subscription_acknowledge(self):
ACK_ID1 = 'DEADBEEF'
ACK_ID2 = 'BEADCAFE'
BODY = {
'ackIds': [ACK_ID1, ACK_ID2],
}
RETURNED = {}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
api.subscription_acknowledge(self.SUB_PATH, [ACK_ID1, ACK_ID2])
self.assertEqual(connection._called_with['method'], 'POST')
path = '/%s:acknowledge' % (self.SUB_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['data'], BODY)
def test_subscription_modify_ack_deadline(self):
ACK_ID1 = 'DEADBEEF'
ACK_ID2 = 'BEADCAFE'
NEW_DEADLINE = 90
BODY = {
'ackIds': [ACK_ID1, ACK_ID2],
'ackDeadlineSeconds': NEW_DEADLINE,
}
RETURNED = {}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
api.subscription_modify_ack_deadline(
self.SUB_PATH, [ACK_ID1, ACK_ID2], NEW_DEADLINE)
self.assertEqual(connection._called_with['method'], 'POST')
path = '/%s:modifyAckDeadline' % (self.SUB_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['data'], BODY)
class Test_IAMPolicyAPI(_Base):
def _getTargetClass(self):
from gcloud.pubsub.connection import _IAMPolicyAPI
return _IAMPolicyAPI
def test_ctor(self):
connection = _Connection()
api = self._makeOne(connection)
self.assertTrue(api._connection is connection)
def test_get_iam_policy(self):
from gcloud.pubsub.iam import OWNER_ROLE, EDITOR_ROLE, VIEWER_ROLE
OWNER1 = 'user:phred@example.com'
OWNER2 = 'group:cloud-logs@google.com'
EDITOR1 = 'domain:google.com'
EDITOR2 = 'user:phred@example.com'
VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com'
VIEWER2 = 'user:phred@example.com'
RETURNED = {
'etag': 'DEADBEEF',
'version': 17,
'bindings': [
{'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]},
{'role': EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
{'role': VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
],
}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
policy = api.get_iam_policy(self.TOPIC_PATH)
self.assertEqual(policy, RETURNED)
self.assertEqual(connection._called_with['method'], 'GET')
path = '/%s:getIamPolicy' % (self.TOPIC_PATH,)
self.assertEqual(connection._called_with['path'], path)
def test_set_iam_policy(self):
from gcloud.pubsub.iam import OWNER_ROLE, EDITOR_ROLE, VIEWER_ROLE
OWNER1 = 'user:phred@example.com'
OWNER2 = 'group:cloud-logs@google.com'
EDITOR1 = 'domain:google.com'
EDITOR2 = 'user:phred@example.com'
VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com'
VIEWER2 = 'user:phred@example.com'
POLICY = {
'etag': 'DEADBEEF',
'version': 17,
'bindings': [
{'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]},
{'role': EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
{'role': VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
],
}
RETURNED = POLICY.copy()
connection = _Connection(RETURNED)
api = self._makeOne(connection)
policy = api.set_iam_policy(self.TOPIC_PATH, POLICY)
self.assertEqual(policy, RETURNED)
self.assertEqual(connection._called_with['method'], 'POST')
path = '/%s:setIamPolicy' % (self.TOPIC_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['data'],
{'policy': POLICY})
def test_test_iam_permissions(self):
from gcloud.pubsub.iam import OWNER_ROLE, EDITOR_ROLE, VIEWER_ROLE
ALL_ROLES = [OWNER_ROLE, EDITOR_ROLE, VIEWER_ROLE]
ALLOWED = ALL_ROLES[1:]
RETURNED = {'permissions': ALLOWED}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
allowed = api.test_iam_permissions(self.TOPIC_PATH, ALL_ROLES)
self.assertEqual(allowed, ALLOWED)
self.assertEqual(connection._called_with['method'], 'POST')
path = '/%s:testIamPermissions' % (self.TOPIC_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['data'],
{'permissions': ALL_ROLES})
def test_test_iam_permissions_missing_key(self):
from gcloud.pubsub.iam import OWNER_ROLE, EDITOR_ROLE, VIEWER_ROLE
ALL_ROLES = [OWNER_ROLE, EDITOR_ROLE, VIEWER_ROLE]
RETURNED = {}
connection = _Connection(RETURNED)
api = self._makeOne(connection)
allowed = api.test_iam_permissions(self.TOPIC_PATH, ALL_ROLES)
self.assertEqual(allowed, [])
self.assertEqual(connection._called_with['method'], 'POST')
path = '/%s:testIamPermissions' % (self.TOPIC_PATH,)
self.assertEqual(connection._called_with['path'], path)
self.assertEqual(connection._called_with['data'],
{'permissions': ALL_ROLES})
class _Connection(object):
_called_with = None
_no_response_error = None
def __init__(self, *responses):
self._responses = responses
def api_request(self, **kw):
from gcloud.exceptions import NotFound
self._called_with = kw
try:
response, self._responses = self._responses[0], self._responses[1:]
except IndexError:
err_class = self._no_response_error or NotFound
raise err_class('miss')
return response

View file

@ -0,0 +1,188 @@
# Copyright 2016 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.
import unittest2
class TestPolicy(unittest2.TestCase):
def _getTargetClass(self):
from gcloud.pubsub.iam import Policy
return Policy
def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw)
def test_ctor_defaults(self):
policy = self._makeOne()
self.assertEqual(policy.etag, None)
self.assertEqual(policy.version, None)
self.assertEqual(list(policy.owners), [])
self.assertEqual(list(policy.editors), [])
self.assertEqual(list(policy.viewers), [])
self.assertEqual(list(policy.publishers), [])
self.assertEqual(list(policy.subscribers), [])
def test_ctor_explicit(self):
VERSION = 17
ETAG = 'ETAG'
policy = self._makeOne(ETAG, VERSION)
self.assertEqual(policy.etag, ETAG)
self.assertEqual(policy.version, VERSION)
self.assertEqual(list(policy.owners), [])
self.assertEqual(list(policy.editors), [])
self.assertEqual(list(policy.viewers), [])
self.assertEqual(list(policy.publishers), [])
self.assertEqual(list(policy.subscribers), [])
def test_user(self):
EMAIL = 'phred@example.com'
MEMBER = 'user:%s' % (EMAIL,)
policy = self._makeOne()
self.assertEqual(policy.user(EMAIL), MEMBER)
def test_service_account(self):
EMAIL = 'phred@example.com'
MEMBER = 'serviceAccount:%s' % (EMAIL,)
policy = self._makeOne()
self.assertEqual(policy.service_account(EMAIL), MEMBER)
def test_group(self):
EMAIL = 'phred@example.com'
MEMBER = 'group:%s' % (EMAIL,)
policy = self._makeOne()
self.assertEqual(policy.group(EMAIL), MEMBER)
def test_domain(self):
DOMAIN = 'example.com'
MEMBER = 'domain:%s' % (DOMAIN,)
policy = self._makeOne()
self.assertEqual(policy.domain(DOMAIN), MEMBER)
def test_all_users(self):
policy = self._makeOne()
self.assertEqual(policy.all_users(), 'allUsers')
def test_authenticated_users(self):
policy = self._makeOne()
self.assertEqual(policy.authenticated_users(), 'allAuthenticatedUsers')
def test_from_api_repr_only_etag(self):
RESOURCE = {
'etag': 'ACAB',
}
klass = self._getTargetClass()
policy = klass.from_api_repr(RESOURCE)
self.assertEqual(policy.etag, 'ACAB')
self.assertEqual(policy.version, None)
self.assertEqual(list(policy.owners), [])
self.assertEqual(list(policy.editors), [])
self.assertEqual(list(policy.viewers), [])
def test_from_api_repr_complete(self):
from gcloud.pubsub.iam import (
OWNER_ROLE,
EDITOR_ROLE,
VIEWER_ROLE,
PUBSUB_PUBLISHER_ROLE,
PUBSUB_SUBSCRIBER_ROLE,
)
OWNER1 = 'user:phred@example.com'
OWNER2 = 'group:cloud-logs@google.com'
EDITOR1 = 'domain:google.com'
EDITOR2 = 'user:phred@example.com'
VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com'
VIEWER2 = 'user:phred@example.com'
PUBLISHER = 'user:phred@example.com'
SUBSCRIBER = 'serviceAccount:1234-abcdef@service.example.com'
RESOURCE = {
'etag': 'DEADBEEF',
'version': 17,
'bindings': [
{'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]},
{'role': EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
{'role': VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
{'role': PUBSUB_PUBLISHER_ROLE, 'members': [PUBLISHER]},
{'role': PUBSUB_SUBSCRIBER_ROLE, 'members': [SUBSCRIBER]},
],
}
klass = self._getTargetClass()
policy = klass.from_api_repr(RESOURCE)
self.assertEqual(policy.etag, 'DEADBEEF')
self.assertEqual(policy.version, 17)
self.assertEqual(sorted(policy.owners), [OWNER2, OWNER1])
self.assertEqual(sorted(policy.editors), [EDITOR1, EDITOR2])
self.assertEqual(sorted(policy.viewers), [VIEWER1, VIEWER2])
self.assertEqual(sorted(policy.publishers), [PUBLISHER])
self.assertEqual(sorted(policy.subscribers), [SUBSCRIBER])
def test_from_api_repr_bad_role(self):
BOGUS1 = 'user:phred@example.com'
BOGUS2 = 'group:cloud-logs@google.com'
RESOURCE = {
'etag': 'DEADBEEF',
'version': 17,
'bindings': [
{'role': 'nonesuch', 'members': [BOGUS1, BOGUS2]},
],
}
klass = self._getTargetClass()
with self.assertRaises(ValueError):
klass.from_api_repr(RESOURCE)
def test_to_api_repr_defaults(self):
policy = self._makeOne()
self.assertEqual(policy.to_api_repr(), {})
def test_to_api_repr_only_etag(self):
policy = self._makeOne('DEADBEEF')
self.assertEqual(policy.to_api_repr(), {'etag': 'DEADBEEF'})
def test_to_api_repr_full(self):
from gcloud.pubsub.iam import (
PUBSUB_ADMIN_ROLE,
PUBSUB_EDITOR_ROLE,
PUBSUB_VIEWER_ROLE,
PUBSUB_PUBLISHER_ROLE,
PUBSUB_SUBSCRIBER_ROLE,
)
OWNER1 = 'group:cloud-logs@google.com'
OWNER2 = 'user:phred@example.com'
EDITOR1 = 'domain:google.com'
EDITOR2 = 'user:phred@example.com'
VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com'
VIEWER2 = 'user:phred@example.com'
PUBLISHER = 'user:phred@example.com'
SUBSCRIBER = 'serviceAccount:1234-abcdef@service.example.com'
EXPECTED = {
'etag': 'DEADBEEF',
'version': 17,
'bindings': [
{'role': PUBSUB_ADMIN_ROLE, 'members': [OWNER1, OWNER2]},
{'role': PUBSUB_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
{'role': PUBSUB_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
{'role': PUBSUB_PUBLISHER_ROLE, 'members': [PUBLISHER]},
{'role': PUBSUB_SUBSCRIBER_ROLE, 'members': [SUBSCRIBER]},
],
}
policy = self._makeOne('DEADBEEF', 17)
policy.owners.add(OWNER1)
policy.owners.add(OWNER2)
policy.editors.add(EDITOR1)
policy.editors.add(EDITOR2)
policy.viewers.add(VIEWER1)
policy.viewers.add(VIEWER2)
policy.publishers.add(PUBLISHER)
policy.subscribers.add(SUBSCRIBER)
self.assertEqual(policy.to_api_repr(), EXPECTED)

View file

@ -0,0 +1,126 @@
# 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.
import unittest2
class TestMessage(unittest2.TestCase):
def _getTargetClass(self):
from gcloud.pubsub.message import Message
return Message
def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw)
def test_ctor_no_attributes(self):
DATA = b'DEADBEEF'
MESSAGE_ID = b'12345'
message = self._makeOne(data=DATA, message_id=MESSAGE_ID)
self.assertEqual(message.data, DATA)
self.assertEqual(message.message_id, MESSAGE_ID)
self.assertEqual(message.attributes, {})
self.assertEqual(message.service_timestamp, None)
def test_ctor_w_attributes(self):
DATA = b'DEADBEEF'
MESSAGE_ID = b'12345'
ATTRS = {'a': 'b'}
message = self._makeOne(data=DATA, message_id=MESSAGE_ID,
attributes=ATTRS)
self.assertEqual(message.data, DATA)
self.assertEqual(message.message_id, MESSAGE_ID)
self.assertEqual(message.attributes, ATTRS)
self.assertEqual(message.service_timestamp, None)
def test_timestamp_no_attributes(self):
DATA = b'DEADBEEF'
MESSAGE_ID = b'12345'
message = self._makeOne(data=DATA, message_id=MESSAGE_ID)
def _to_fail():
return message.timestamp
self.assertRaises(ValueError, _to_fail)
def test_timestamp_wo_timestamp_in_attributes(self):
DATA = b'DEADBEEF'
MESSAGE_ID = b'12345'
ATTRS = {'a': 'b'}
message = self._makeOne(data=DATA, message_id=MESSAGE_ID,
attributes=ATTRS)
def _to_fail():
return message.timestamp
self.assertRaises(ValueError, _to_fail)
def test_timestamp_w_timestamp_in_attributes(self):
from datetime import datetime
from gcloud._helpers import _RFC3339_MICROS
from gcloud._helpers import UTC
DATA = b'DEADBEEF'
MESSAGE_ID = b'12345'
TIMESTAMP = '2015-04-10T18:42:27.131956Z'
naive = datetime.strptime(TIMESTAMP, _RFC3339_MICROS)
timestamp = naive.replace(tzinfo=UTC)
ATTRS = {'timestamp': TIMESTAMP}
message = self._makeOne(data=DATA, message_id=MESSAGE_ID,
attributes=ATTRS)
self.assertEqual(message.timestamp, timestamp)
def test_from_api_repr_missing_data(self):
MESSAGE_ID = '12345'
api_repr = {'messageId': MESSAGE_ID}
message = self._getTargetClass().from_api_repr(api_repr)
self.assertEqual(message.data, b'')
self.assertEqual(message.message_id, MESSAGE_ID)
self.assertEqual(message.attributes, {})
self.assertEqual(message.service_timestamp, None)
def test_from_api_repr_no_attributes(self):
from base64 import b64encode as b64
DATA = b'DEADBEEF'
B64_DATA = b64(DATA)
MESSAGE_ID = '12345'
TIMESTAMP = '2016-03-18-19:38:22.001393427Z'
api_repr = {
'data': B64_DATA,
'messageId': MESSAGE_ID,
'publishTimestamp': TIMESTAMP,
}
message = self._getTargetClass().from_api_repr(api_repr)
self.assertEqual(message.data, DATA)
self.assertEqual(message.message_id, MESSAGE_ID)
self.assertEqual(message.attributes, {})
self.assertEqual(message.service_timestamp, TIMESTAMP)
def test_from_api_repr_w_attributes(self):
from base64 import b64encode as b64
DATA = b'DEADBEEF'
B64_DATA = b64(DATA)
MESSAGE_ID = '12345'
ATTRS = {'a': 'b'}
TIMESTAMP = '2016-03-18-19:38:22.001393427Z'
api_repr = {
'data': B64_DATA,
'messageId': MESSAGE_ID,
'publishTimestamp': TIMESTAMP,
'attributes': ATTRS,
}
message = self._getTargetClass().from_api_repr(api_repr)
self.assertEqual(message.data, DATA)
self.assertEqual(message.message_id, MESSAGE_ID)
self.assertEqual(message.service_timestamp, TIMESTAMP)
self.assertEqual(message.attributes, ATTRS)

View file

@ -0,0 +1,679 @@
# 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.
import unittest2
class TestSubscription(unittest2.TestCase):
PROJECT = 'PROJECT'
TOPIC_NAME = 'topic_name'
TOPIC_PATH = 'projects/%s/topics/%s' % (PROJECT, TOPIC_NAME)
SUB_NAME = 'sub_name'
SUB_PATH = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME)
DEADLINE = 42
ENDPOINT = 'https://api.example.com/push'
def _getTargetClass(self):
from gcloud.pubsub.subscription import Subscription
return Subscription
def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw)
def test_ctor_defaults(self):
client = _Client(project=self.PROJECT)
topic = _Topic(self.TOPIC_NAME, client=client)
subscription = self._makeOne(self.SUB_NAME, topic)
self.assertEqual(subscription.name, self.SUB_NAME)
self.assertTrue(subscription.topic is topic)
self.assertEqual(subscription.ack_deadline, None)
self.assertEqual(subscription.push_endpoint, None)
def test_ctor_explicit(self):
client = _Client(project=self.PROJECT)
topic = _Topic(self.TOPIC_NAME, client=client)
subscription = self._makeOne(self.SUB_NAME, topic,
self.DEADLINE, self.ENDPOINT)
self.assertEqual(subscription.name, self.SUB_NAME)
self.assertTrue(subscription.topic is topic)
self.assertEqual(subscription.ack_deadline, self.DEADLINE)
self.assertEqual(subscription.push_endpoint, self.ENDPOINT)
def test_ctor_w_client_wo_topic(self):
client = _Client(project=self.PROJECT)
subscription = self._makeOne(self.SUB_NAME, client=client)
self.assertEqual(subscription.name, self.SUB_NAME)
self.assertTrue(subscription.topic is None)
def test_ctor_w_both_topic_and_client(self):
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
topic = _Topic(self.TOPIC_NAME, client=client1)
with self.assertRaises(TypeError):
self._makeOne(self.SUB_NAME, topic, client=client2)
def test_ctor_w_neither_topic_nor_client(self):
with self.assertRaises(TypeError):
self._makeOne(self.SUB_NAME)
def test_from_api_repr_no_topics(self):
from gcloud.pubsub.topic import Topic
resource = {'topic': self.TOPIC_PATH,
'name': self.SUB_PATH,
'ackDeadlineSeconds': self.DEADLINE,
'pushConfig': {'pushEndpoint': self.ENDPOINT}}
klass = self._getTargetClass()
client = _Client(project=self.PROJECT)
subscription = klass.from_api_repr(resource, client)
self.assertEqual(subscription.name, self.SUB_NAME)
topic = subscription.topic
self.assertIsInstance(topic, Topic)
self.assertEqual(topic.name, self.TOPIC_NAME)
self.assertEqual(topic.project, self.PROJECT)
self.assertEqual(subscription.ack_deadline, self.DEADLINE)
self.assertEqual(subscription.push_endpoint, self.ENDPOINT)
def test_from_api_repr_w_deleted_topic(self):
klass = self._getTargetClass()
resource = {'topic': klass._DELETED_TOPIC_PATH,
'name': self.SUB_PATH,
'ackDeadlineSeconds': self.DEADLINE,
'pushConfig': {'pushEndpoint': self.ENDPOINT}}
klass = self._getTargetClass()
client = _Client(project=self.PROJECT)
subscription = klass.from_api_repr(resource, client)
self.assertEqual(subscription.name, self.SUB_NAME)
self.assertTrue(subscription.topic is None)
self.assertEqual(subscription.ack_deadline, self.DEADLINE)
self.assertEqual(subscription.push_endpoint, self.ENDPOINT)
def test_from_api_repr_w_topics_no_topic_match(self):
from gcloud.pubsub.topic import Topic
resource = {'topic': self.TOPIC_PATH,
'name': self.SUB_PATH,
'ackDeadlineSeconds': self.DEADLINE,
'pushConfig': {'pushEndpoint': self.ENDPOINT}}
topics = {}
klass = self._getTargetClass()
client = _Client(project=self.PROJECT)
subscription = klass.from_api_repr(resource, client, topics=topics)
self.assertEqual(subscription.name, self.SUB_NAME)
topic = subscription.topic
self.assertIsInstance(topic, Topic)
self.assertTrue(topic is topics[self.TOPIC_PATH])
self.assertEqual(topic.name, self.TOPIC_NAME)
self.assertEqual(topic.project, self.PROJECT)
self.assertEqual(subscription.ack_deadline, self.DEADLINE)
self.assertEqual(subscription.push_endpoint, self.ENDPOINT)
def test_from_api_repr_w_topics_w_topic_match(self):
resource = {'topic': self.TOPIC_PATH,
'name': self.SUB_PATH,
'ackDeadlineSeconds': self.DEADLINE,
'pushConfig': {'pushEndpoint': self.ENDPOINT}}
client = _Client(project=self.PROJECT)
topic = _Topic(self.TOPIC_NAME, client=client)
topics = {self.TOPIC_PATH: topic}
klass = self._getTargetClass()
subscription = klass.from_api_repr(resource, client, topics=topics)
self.assertEqual(subscription.name, self.SUB_NAME)
self.assertTrue(subscription.topic is topic)
self.assertEqual(subscription.ack_deadline, self.DEADLINE)
self.assertEqual(subscription.push_endpoint, self.ENDPOINT)
def test_full_name_and_path(self):
PROJECT = 'PROJECT'
SUB_NAME = 'sub_name'
SUB_FULL = 'projects/%s/subscriptions/%s' % (PROJECT, SUB_NAME)
SUB_PATH = '/%s' % (SUB_FULL,)
TOPIC_NAME = 'topic_name'
CLIENT = _Client(project=PROJECT)
topic = _Topic(TOPIC_NAME, client=CLIENT)
subscription = self._makeOne(SUB_NAME, topic)
self.assertEqual(subscription.full_name, SUB_FULL)
self.assertEqual(subscription.path, SUB_PATH)
def test_create_pull_wo_ack_deadline_w_bound_client(self):
RESPONSE = {
'topic': self.TOPIC_PATH,
'name': self.SUB_PATH,
}
client = _Client(project=self.PROJECT)
api = client.subscriber_api = _FauxSubscribererAPI()
api._subscription_create_response = RESPONSE
topic = _Topic(self.TOPIC_NAME, client=client)
subscription = self._makeOne(self.SUB_NAME, topic)
subscription.create()
self.assertEqual(api._subscription_created,
(self.SUB_PATH, self.TOPIC_PATH, None, None))
def test_create_push_w_ack_deadline_w_alternate_client(self):
RESPONSE = {
'topic': self.TOPIC_PATH,
'name': self.SUB_PATH,
'ackDeadlineSeconds': self.DEADLINE,
'pushConfig': {'pushEndpoint': self.ENDPOINT}
}
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
api = client2.subscriber_api = _FauxSubscribererAPI()
api._subscription_create_response = RESPONSE
topic = _Topic(self.TOPIC_NAME, client=client1)
subscription = self._makeOne(self.SUB_NAME, topic,
self.DEADLINE, self.ENDPOINT)
subscription.create(client=client2)
self.assertEqual(
api._subscription_created,
(self.SUB_PATH, self.TOPIC_PATH, self.DEADLINE, self.ENDPOINT))
def test_exists_miss_w_bound_client(self):
client = _Client(project=self.PROJECT)
api = client.subscriber_api = _FauxSubscribererAPI()
topic = _Topic(self.TOPIC_NAME, client=client)
subscription = self._makeOne(self.SUB_NAME, topic)
self.assertFalse(subscription.exists())
self.assertEqual(api._subscription_got, self.SUB_PATH)
def test_exists_hit_w_alternate_client(self):
RESPONSE = {'name': self.SUB_PATH, 'topic': self.TOPIC_PATH}
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
api = client2.subscriber_api = _FauxSubscribererAPI()
api._subscription_get_response = RESPONSE
topic = _Topic(self.TOPIC_NAME, client=client1)
subscription = self._makeOne(self.SUB_NAME, topic)
self.assertTrue(subscription.exists(client=client2))
self.assertEqual(api._subscription_got, self.SUB_PATH)
def test_reload_w_bound_client(self):
RESPONSE = {
'name': self.SUB_PATH,
'topic': self.TOPIC_PATH,
'ackDeadlineSeconds': self.DEADLINE,
'pushConfig': {'pushEndpoint': self.ENDPOINT},
}
client = _Client(project=self.PROJECT)
api = client.subscriber_api = _FauxSubscribererAPI()
api._subscription_get_response = RESPONSE
topic = _Topic(self.TOPIC_NAME, client=client)
subscription = self._makeOne(self.SUB_NAME, topic)
subscription.reload()
self.assertEqual(subscription.ack_deadline, self.DEADLINE)
self.assertEqual(subscription.push_endpoint, self.ENDPOINT)
self.assertEqual(api._subscription_got, self.SUB_PATH)
def test_reload_w_alternate_client(self):
RESPONSE = {
'name': self.SUB_PATH,
'topic': self.TOPIC_PATH,
}
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
api = client2.subscriber_api = _FauxSubscribererAPI()
api._subscription_get_response = RESPONSE
topic = _Topic(self.TOPIC_NAME, client=client1)
subscription = self._makeOne(self.SUB_NAME, topic,
self.DEADLINE, self.ENDPOINT)
subscription.reload(client=client2)
self.assertEqual(subscription.ack_deadline, None)
self.assertEqual(subscription.push_endpoint, None)
self.assertEqual(api._subscription_got, self.SUB_PATH)
def test_delete_w_bound_client(self):
RESPONSE = {}
client = _Client(project=self.PROJECT)
api = client.subscriber_api = _FauxSubscribererAPI()
api._subscription_delete_response = RESPONSE
topic = _Topic(self.TOPIC_NAME, client=client)
subscription = self._makeOne(self.SUB_NAME, topic)
subscription.delete()
self.assertEqual(api._subscription_deleted, self.SUB_PATH)
def test_delete_w_alternate_client(self):
RESPONSE = {}
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
api = client2.subscriber_api = _FauxSubscribererAPI()
api._subscription_delete_response = RESPONSE
topic = _Topic(self.TOPIC_NAME, client=client1)
subscription = self._makeOne(self.SUB_NAME, topic,
self.DEADLINE, self.ENDPOINT)
subscription.delete(client=client2)
self.assertEqual(api._subscription_deleted, self.SUB_PATH)
def test_modify_push_config_w_endpoint_w_bound_client(self):
client = _Client(project=self.PROJECT)
api = client.subscriber_api = _FauxSubscribererAPI()
api._subscription_modify_push_config_response = {}
topic = _Topic(self.TOPIC_NAME, client=client)
subscription = self._makeOne(self.SUB_NAME, topic)
subscription.modify_push_configuration(push_endpoint=self.ENDPOINT)
self.assertEqual(subscription.push_endpoint, self.ENDPOINT)
self.assertEqual(api._subscription_modified_push_config,
(self.SUB_PATH, self.ENDPOINT))
def test_modify_push_config_wo_endpoint_w_alternate_client(self):
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
api = client2.subscriber_api = _FauxSubscribererAPI()
api._subscription_modify_push_config_response = {}
topic = _Topic(self.TOPIC_NAME, client=client1)
subscription = self._makeOne(self.SUB_NAME, topic,
push_endpoint=self.ENDPOINT)
subscription.modify_push_configuration(push_endpoint=None,
client=client2)
self.assertEqual(subscription.push_endpoint, None)
self.assertEqual(api._subscription_modified_push_config,
(self.SUB_PATH, None))
def test_pull_wo_return_immediately_max_messages_w_bound_client(self):
import base64
from gcloud.pubsub.message import Message
ACK_ID = 'DEADBEEF'
MSG_ID = 'BEADCAFE'
PAYLOAD = b'This is the message text'
B64 = base64.b64encode(PAYLOAD)
MESSAGE = {'messageId': MSG_ID, 'data': B64}
REC_MESSAGE = {'ackId': ACK_ID, 'message': MESSAGE}
client = _Client(project=self.PROJECT)
api = client.subscriber_api = _FauxSubscribererAPI()
api._subscription_pull_response = [REC_MESSAGE]
topic = _Topic(self.TOPIC_NAME, client=client)
subscription = self._makeOne(self.SUB_NAME, topic)
pulled = subscription.pull()
self.assertEqual(len(pulled), 1)
ack_id, message = pulled[0]
self.assertEqual(ack_id, ACK_ID)
self.assertIsInstance(message, Message)
self.assertEqual(message.data, PAYLOAD)
self.assertEqual(message.message_id, MSG_ID)
self.assertEqual(message.attributes, {})
self.assertEqual(api._subscription_pulled,
(self.SUB_PATH, False, 1))
def test_pull_w_return_immediately_w_max_messages_w_alt_client(self):
import base64
from gcloud.pubsub.message import Message
ACK_ID = 'DEADBEEF'
MSG_ID = 'BEADCAFE'
PAYLOAD = b'This is the message text'
B64 = base64.b64encode(PAYLOAD)
MESSAGE = {'messageId': MSG_ID, 'data': B64, 'attributes': {'a': 'b'}}
REC_MESSAGE = {'ackId': ACK_ID, 'message': MESSAGE}
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
api = client2.subscriber_api = _FauxSubscribererAPI()
api._subscription_pull_response = [REC_MESSAGE]
topic = _Topic(self.TOPIC_NAME, client=client1)
subscription = self._makeOne(self.SUB_NAME, topic)
pulled = subscription.pull(return_immediately=True, max_messages=3,
client=client2)
self.assertEqual(len(pulled), 1)
ack_id, message = pulled[0]
self.assertEqual(ack_id, ACK_ID)
self.assertIsInstance(message, Message)
self.assertEqual(message.data, PAYLOAD)
self.assertEqual(message.message_id, MSG_ID)
self.assertEqual(message.attributes, {'a': 'b'})
self.assertEqual(api._subscription_pulled,
(self.SUB_PATH, True, 3))
def test_pull_wo_receivedMessages(self):
client = _Client(project=self.PROJECT)
api = client.subscriber_api = _FauxSubscribererAPI()
api._subscription_pull_response = {}
topic = _Topic(self.TOPIC_NAME, client=client)
subscription = self._makeOne(self.SUB_NAME, topic)
pulled = subscription.pull(return_immediately=False)
self.assertEqual(len(pulled), 0)
self.assertEqual(api._subscription_pulled,
(self.SUB_PATH, False, 1))
def test_acknowledge_w_bound_client(self):
ACK_ID1 = 'DEADBEEF'
ACK_ID2 = 'BEADCAFE'
client = _Client(project=self.PROJECT)
api = client.subscriber_api = _FauxSubscribererAPI()
api._subscription_acknowlege_response = {}
topic = _Topic(self.TOPIC_NAME, client=client)
subscription = self._makeOne(self.SUB_NAME, topic)
subscription.acknowledge([ACK_ID1, ACK_ID2])
self.assertEqual(api._subscription_acked,
(self.SUB_PATH, [ACK_ID1, ACK_ID2]))
def test_acknowledge_w_alternate_client(self):
ACK_ID1 = 'DEADBEEF'
ACK_ID2 = 'BEADCAFE'
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
api = client2.subscriber_api = _FauxSubscribererAPI()
api._subscription_acknowlege_response = {}
topic = _Topic(self.TOPIC_NAME, client=client1)
subscription = self._makeOne(self.SUB_NAME, topic)
subscription.acknowledge([ACK_ID1, ACK_ID2], client=client2)
self.assertEqual(api._subscription_acked,
(self.SUB_PATH, [ACK_ID1, ACK_ID2]))
def test_modify_ack_deadline_w_bound_client(self):
ACK_ID1 = 'DEADBEEF'
ACK_ID2 = 'BEADCAFE'
client = _Client(project=self.PROJECT)
api = client.subscriber_api = _FauxSubscribererAPI()
api._subscription_modify_ack_deadline_response = {}
topic = _Topic(self.TOPIC_NAME, client=client)
subscription = self._makeOne(self.SUB_NAME, topic)
subscription.modify_ack_deadline([ACK_ID1, ACK_ID2], self.DEADLINE)
self.assertEqual(api._subscription_modified_ack_deadline,
(self.SUB_PATH, [ACK_ID1, ACK_ID2], self.DEADLINE))
def test_modify_ack_deadline_w_alternate_client(self):
ACK_ID1 = 'DEADBEEF'
ACK_ID2 = 'BEADCAFE'
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
api = client2.subscriber_api = _FauxSubscribererAPI()
api._subscription_modify_ack_deadline_response = {}
topic = _Topic(self.TOPIC_NAME, client=client1)
subscription = self._makeOne(self.SUB_NAME, topic)
subscription.modify_ack_deadline(
[ACK_ID1, ACK_ID2], self.DEADLINE, client=client2)
self.assertEqual(api._subscription_modified_ack_deadline,
(self.SUB_PATH, [ACK_ID1, ACK_ID2], self.DEADLINE))
def test_get_iam_policy_w_bound_client(self):
from gcloud.pubsub.iam import (
PUBSUB_ADMIN_ROLE,
PUBSUB_EDITOR_ROLE,
PUBSUB_VIEWER_ROLE,
PUBSUB_PUBLISHER_ROLE,
PUBSUB_SUBSCRIBER_ROLE,
)
OWNER1 = 'user:phred@example.com'
OWNER2 = 'group:cloud-logs@google.com'
EDITOR1 = 'domain:google.com'
EDITOR2 = 'user:phred@example.com'
VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com'
VIEWER2 = 'user:phred@example.com'
PUBLISHER = 'user:phred@example.com'
SUBSCRIBER = 'serviceAccount:1234-abcdef@service.example.com'
POLICY = {
'etag': 'DEADBEEF',
'version': 17,
'bindings': [
{'role': PUBSUB_ADMIN_ROLE, 'members': [OWNER1, OWNER2]},
{'role': PUBSUB_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
{'role': PUBSUB_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
{'role': PUBSUB_PUBLISHER_ROLE, 'members': [PUBLISHER]},
{'role': PUBSUB_SUBSCRIBER_ROLE, 'members': [SUBSCRIBER]},
],
}
client = _Client(project=self.PROJECT)
api = client.iam_policy_api = _FauxIAMPolicy()
api._get_iam_policy_response = POLICY
topic = _Topic(self.TOPIC_NAME, client=client)
subscription = self._makeOne(self.SUB_NAME, topic)
policy = subscription.get_iam_policy()
self.assertEqual(policy.etag, 'DEADBEEF')
self.assertEqual(policy.version, 17)
self.assertEqual(sorted(policy.owners), [OWNER2, OWNER1])
self.assertEqual(sorted(policy.editors), [EDITOR1, EDITOR2])
self.assertEqual(sorted(policy.viewers), [VIEWER1, VIEWER2])
self.assertEqual(sorted(policy.publishers), [PUBLISHER])
self.assertEqual(sorted(policy.subscribers), [SUBSCRIBER])
self.assertEqual(api._got_iam_policy, self.SUB_PATH)
def test_get_iam_policy_w_alternate_client(self):
POLICY = {
'etag': 'ACAB',
}
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
api = client2.iam_policy_api = _FauxIAMPolicy()
api._get_iam_policy_response = POLICY
topic = _Topic(self.TOPIC_NAME, client=client1)
subscription = self._makeOne(self.SUB_NAME, topic)
policy = subscription.get_iam_policy(client=client2)
self.assertEqual(policy.etag, 'ACAB')
self.assertEqual(policy.version, None)
self.assertEqual(sorted(policy.owners), [])
self.assertEqual(sorted(policy.editors), [])
self.assertEqual(sorted(policy.viewers), [])
self.assertEqual(api._got_iam_policy, self.SUB_PATH)
def test_set_iam_policy_w_bound_client(self):
from gcloud.pubsub.iam import Policy
from gcloud.pubsub.iam import (
PUBSUB_ADMIN_ROLE,
PUBSUB_EDITOR_ROLE,
PUBSUB_VIEWER_ROLE,
PUBSUB_PUBLISHER_ROLE,
PUBSUB_SUBSCRIBER_ROLE,
)
OWNER1 = 'group:cloud-logs@google.com'
OWNER2 = 'user:phred@example.com'
EDITOR1 = 'domain:google.com'
EDITOR2 = 'user:phred@example.com'
VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com'
VIEWER2 = 'user:phred@example.com'
PUBLISHER = 'user:phred@example.com'
SUBSCRIBER = 'serviceAccount:1234-abcdef@service.example.com'
POLICY = {
'etag': 'DEADBEEF',
'version': 17,
'bindings': [
{'role': PUBSUB_ADMIN_ROLE, 'members': [OWNER1, OWNER2]},
{'role': PUBSUB_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
{'role': PUBSUB_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
{'role': PUBSUB_PUBLISHER_ROLE, 'members': [PUBLISHER]},
{'role': PUBSUB_SUBSCRIBER_ROLE, 'members': [SUBSCRIBER]},
],
}
RESPONSE = POLICY.copy()
RESPONSE['etag'] = 'ABACABAF'
RESPONSE['version'] = 18
client = _Client(project=self.PROJECT)
api = client.iam_policy_api = _FauxIAMPolicy()
api._set_iam_policy_response = RESPONSE
topic = _Topic(self.TOPIC_NAME, client=client)
subscription = self._makeOne(self.SUB_NAME, topic)
policy = Policy('DEADBEEF', 17)
policy.owners.add(OWNER1)
policy.owners.add(OWNER2)
policy.editors.add(EDITOR1)
policy.editors.add(EDITOR2)
policy.viewers.add(VIEWER1)
policy.viewers.add(VIEWER2)
policy.publishers.add(PUBLISHER)
policy.subscribers.add(SUBSCRIBER)
new_policy = subscription.set_iam_policy(policy)
self.assertEqual(new_policy.etag, 'ABACABAF')
self.assertEqual(new_policy.version, 18)
self.assertEqual(sorted(new_policy.owners), [OWNER1, OWNER2])
self.assertEqual(sorted(new_policy.editors), [EDITOR1, EDITOR2])
self.assertEqual(sorted(new_policy.viewers), [VIEWER1, VIEWER2])
self.assertEqual(sorted(new_policy.publishers), [PUBLISHER])
self.assertEqual(sorted(new_policy.subscribers), [SUBSCRIBER])
self.assertEqual(api._set_iam_policy, (self.SUB_PATH, POLICY))
def test_set_iam_policy_w_alternate_client(self):
from gcloud.pubsub.iam import Policy
RESPONSE = {'etag': 'ACAB'}
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
api = client2.iam_policy_api = _FauxIAMPolicy()
api._set_iam_policy_response = RESPONSE
topic = _Topic(self.TOPIC_NAME, client=client1)
subscription = self._makeOne(self.SUB_NAME, topic)
policy = Policy()
new_policy = subscription.set_iam_policy(policy, client=client2)
self.assertEqual(new_policy.etag, 'ACAB')
self.assertEqual(new_policy.version, None)
self.assertEqual(sorted(new_policy.owners), [])
self.assertEqual(sorted(new_policy.editors), [])
self.assertEqual(sorted(new_policy.viewers), [])
self.assertEqual(api._set_iam_policy, (self.SUB_PATH, {}))
def test_check_iam_permissions_w_bound_client(self):
from gcloud.pubsub.iam import OWNER_ROLE, EDITOR_ROLE, VIEWER_ROLE
ROLES = [VIEWER_ROLE, EDITOR_ROLE, OWNER_ROLE]
client = _Client(project=self.PROJECT)
api = client.iam_policy_api = _FauxIAMPolicy()
api._test_iam_permissions_response = ROLES[:-1]
topic = _Topic(self.TOPIC_NAME, client=client)
subscription = self._makeOne(self.SUB_NAME, topic)
allowed = subscription.check_iam_permissions(ROLES)
self.assertEqual(allowed, ROLES[:-1])
self.assertEqual(api._tested_iam_permissions,
(self.SUB_PATH, ROLES))
def test_check_iam_permissions_w_alternate_client(self):
from gcloud.pubsub.iam import OWNER_ROLE, EDITOR_ROLE, VIEWER_ROLE
ROLES = [VIEWER_ROLE, EDITOR_ROLE, OWNER_ROLE]
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
api = client2.iam_policy_api = _FauxIAMPolicy()
api._test_iam_permissions_response = []
topic = _Topic(self.TOPIC_NAME, client=client1)
subscription = self._makeOne(self.SUB_NAME, topic)
allowed = subscription.check_iam_permissions(ROLES, client=client2)
self.assertEqual(len(allowed), 0)
self.assertEqual(api._tested_iam_permissions,
(self.SUB_PATH, ROLES))
class _FauxSubscribererAPI(object):
def subscription_create(self, subscription_path, topic_path,
ack_deadline=None, push_endpoint=None):
self._subscription_created = (
subscription_path, topic_path, ack_deadline, push_endpoint)
return self._subscription_create_response
def subscription_get(self, subscription_path):
from gcloud.exceptions import NotFound
self._subscription_got = subscription_path
try:
return self._subscription_get_response
except AttributeError:
raise NotFound(subscription_path)
def subscription_delete(self, subscription_path):
self._subscription_deleted = subscription_path
return self._subscription_delete_response
def subscription_modify_push_config(
self, subscription_path, push_endpoint):
self._subscription_modified_push_config = (
subscription_path, push_endpoint)
return self._subscription_modify_push_config_response
def subscription_pull(self, subscription_path, return_immediately,
max_messages):
self._subscription_pulled = (
subscription_path, return_immediately, max_messages)
return self._subscription_pull_response
def subscription_acknowledge(self, subscription_path, ack_ids):
self._subscription_acked = (subscription_path, ack_ids)
return self._subscription_acknowlege_response
def subscription_modify_ack_deadline(self, subscription_path, ack_ids,
ack_deadline):
self._subscription_modified_ack_deadline = (
subscription_path, ack_ids, ack_deadline)
return self._subscription_modify_ack_deadline_response
class _FauxIAMPolicy(object):
def get_iam_policy(self, target_path):
self._got_iam_policy = target_path
return self._get_iam_policy_response
def set_iam_policy(self, target_path, policy):
self._set_iam_policy = target_path, policy
return self._set_iam_policy_response
def test_iam_permissions(self, target_path, permissions):
self._tested_iam_permissions = target_path, permissions
return self._test_iam_permissions_response
class _Topic(object):
def __init__(self, name, client):
self.name = name
self._client = client
self.project = client.project
self.full_name = 'projects/%s/topics/%s' % (client.project, name)
self.path = '/projects/%s/topics/%s' % (client.project, name)
class _Client(object):
connection = None
def __init__(self, project):
self.project = project
def topic(self, name, timestamp_messages=False):
from gcloud.pubsub.topic import Topic
return Topic(name, client=self, timestamp_messages=timestamp_messages)

View file

@ -0,0 +1,783 @@
# 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.
import unittest2
class TestTopic(unittest2.TestCase):
PROJECT = 'PROJECT'
TOPIC_NAME = 'topic_name'
TOPIC_PATH = 'projects/%s/topics/%s' % (PROJECT, TOPIC_NAME)
def _getTargetClass(self):
from gcloud.pubsub.topic import Topic
return Topic
def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw)
def test_ctor_w_explicit_timestamp(self):
client = _Client(project=self.PROJECT)
topic = self._makeOne(self.TOPIC_NAME,
client=client,
timestamp_messages=True)
self.assertEqual(topic.name, self.TOPIC_NAME)
self.assertEqual(topic.project, self.PROJECT)
self.assertEqual(topic.full_name, self.TOPIC_PATH)
self.assertTrue(topic.timestamp_messages)
def test_from_api_repr(self):
client = _Client(project=self.PROJECT)
resource = {'name': self.TOPIC_PATH}
klass = self._getTargetClass()
topic = klass.from_api_repr(resource, client=client)
self.assertEqual(topic.name, self.TOPIC_NAME)
self.assertTrue(topic._client is client)
self.assertEqual(topic.project, self.PROJECT)
self.assertEqual(topic.full_name, self.TOPIC_PATH)
def test_from_api_repr_with_bad_client(self):
PROJECT1 = 'PROJECT1'
PROJECT2 = 'PROJECT2'
client = _Client(project=PROJECT1)
PATH = 'projects/%s/topics/%s' % (PROJECT2, self.TOPIC_NAME)
resource = {'name': PATH}
klass = self._getTargetClass()
self.assertRaises(ValueError, klass.from_api_repr,
resource, client=client)
def test_create_w_bound_client(self):
client = _Client(project=self.PROJECT)
api = client.publisher_api = _FauxPublisherAPI()
api._topic_create_response = {'name': self.TOPIC_PATH}
topic = self._makeOne(self.TOPIC_NAME, client=client)
topic.create()
self.assertEqual(api._topic_created, self.TOPIC_PATH)
def test_create_w_alternate_client(self):
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
api = client2.publisher_api = _FauxPublisherAPI()
api._topic_create_response = {'name': self.TOPIC_PATH}
topic = self._makeOne(self.TOPIC_NAME, client=client1)
topic.create(client=client2)
self.assertEqual(api._topic_created, self.TOPIC_PATH)
def test_exists_miss_w_bound_client(self):
client = _Client(project=self.PROJECT)
api = client.publisher_api = _FauxPublisherAPI()
topic = self._makeOne(self.TOPIC_NAME, client=client)
self.assertFalse(topic.exists())
self.assertEqual(api._topic_got, self.TOPIC_PATH)
def test_exists_hit_w_alternate_client(self):
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
api = client2.publisher_api = _FauxPublisherAPI()
api._topic_get_response = {'name': self.TOPIC_PATH}
topic = self._makeOne(self.TOPIC_NAME, client=client1)
self.assertTrue(topic.exists(client=client2))
self.assertEqual(api._topic_got, self.TOPIC_PATH)
def test_delete_w_bound_client(self):
client = _Client(project=self.PROJECT)
api = client.publisher_api = _FauxPublisherAPI()
api._topic_delete_response = {}
topic = self._makeOne(self.TOPIC_NAME, client=client)
topic.delete()
self.assertEqual(api._topic_deleted, self.TOPIC_PATH)
def test_delete_w_alternate_client(self):
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
api = client2.publisher_api = _FauxPublisherAPI()
api._topic_delete_response = {}
topic = self._makeOne(self.TOPIC_NAME, client=client1)
topic.delete(client=client2)
self.assertEqual(api._topic_deleted, self.TOPIC_PATH)
def test_publish_single_bytes_wo_attrs_w_bound_client(self):
import base64
PAYLOAD = b'This is the message text'
B64 = base64.b64encode(PAYLOAD).decode('ascii')
MSGID = 'DEADBEEF'
MESSAGE = {'data': B64, 'attributes': {}}
client = _Client(project=self.PROJECT)
api = client.publisher_api = _FauxPublisherAPI()
api._topic_publish_response = [MSGID]
topic = self._makeOne(self.TOPIC_NAME, client=client)
msgid = topic.publish(PAYLOAD)
self.assertEqual(msgid, MSGID)
self.assertEqual(api._topic_published, (self.TOPIC_PATH, [MESSAGE]))
def test_publish_single_bytes_wo_attrs_w_add_timestamp_alt_client(self):
import base64
import datetime
from gcloud.pubsub import topic as MUT
from gcloud._helpers import _RFC3339_MICROS
from gcloud._testing import _Monkey
NOW = datetime.datetime.utcnow()
def _utcnow():
return NOW
PAYLOAD = b'This is the message text'
B64 = base64.b64encode(PAYLOAD).decode('ascii')
MSGID = 'DEADBEEF'
MESSAGE = {
'data': B64,
'attributes': {'timestamp': NOW.strftime(_RFC3339_MICROS)},
}
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
api = client2.publisher_api = _FauxPublisherAPI()
api._topic_publish_response = [MSGID]
topic = self._makeOne(self.TOPIC_NAME, client=client1,
timestamp_messages=True)
with _Monkey(MUT, _NOW=_utcnow):
msgid = topic.publish(PAYLOAD, client=client2)
self.assertEqual(msgid, MSGID)
self.assertEqual(api._topic_published, (self.TOPIC_PATH, [MESSAGE]))
def test_publish_single_bytes_w_add_timestamp_w_ts_in_attrs(self):
import base64
PAYLOAD = b'This is the message text'
B64 = base64.b64encode(PAYLOAD).decode('ascii')
MSGID = 'DEADBEEF'
OVERRIDE = '2015-04-10T16:46:22.868399Z'
MESSAGE = {'data': B64,
'attributes': {'timestamp': OVERRIDE}}
client = _Client(project=self.PROJECT)
api = client.publisher_api = _FauxPublisherAPI()
api._topic_publish_response = [MSGID]
topic = self._makeOne(self.TOPIC_NAME, client=client,
timestamp_messages=True)
msgid = topic.publish(PAYLOAD, timestamp=OVERRIDE)
self.assertEqual(msgid, MSGID)
self.assertEqual(api._topic_published, (self.TOPIC_PATH, [MESSAGE]))
def test_publish_single_w_attrs(self):
import base64
PAYLOAD = b'This is the message text'
B64 = base64.b64encode(PAYLOAD).decode('ascii')
MSGID = 'DEADBEEF'
MESSAGE = {'data': B64,
'attributes': {'attr1': 'value1', 'attr2': 'value2'}}
client = _Client(project=self.PROJECT)
api = client.publisher_api = _FauxPublisherAPI()
api._topic_publish_response = [MSGID]
topic = self._makeOne(self.TOPIC_NAME, client=client)
msgid = topic.publish(PAYLOAD, attr1='value1', attr2='value2')
self.assertEqual(msgid, MSGID)
self.assertEqual(api._topic_published, (self.TOPIC_PATH, [MESSAGE]))
def test_publish_multiple_w_bound_client(self):
import base64
PAYLOAD1 = b'This is the first message text'
PAYLOAD2 = b'This is the second message text'
B64_1 = base64.b64encode(PAYLOAD1)
B64_2 = base64.b64encode(PAYLOAD2)
MSGID1 = 'DEADBEEF'
MSGID2 = 'BEADCAFE'
MESSAGE1 = {'data': B64_1.decode('ascii'),
'attributes': {}}
MESSAGE2 = {'data': B64_2.decode('ascii'),
'attributes': {'attr1': 'value1', 'attr2': 'value2'}}
client = _Client(project=self.PROJECT)
api = client.publisher_api = _FauxPublisherAPI()
api._topic_publish_response = [MSGID1, MSGID2]
topic = self._makeOne(self.TOPIC_NAME, client=client)
with topic.batch() as batch:
batch.publish(PAYLOAD1)
batch.publish(PAYLOAD2, attr1='value1', attr2='value2')
self.assertEqual(list(batch), [MSGID1, MSGID2])
self.assertEqual(list(batch.messages), [])
self.assertEqual(api._topic_published,
(self.TOPIC_PATH, [MESSAGE1, MESSAGE2]))
def test_publish_multiple_w_alternate_client(self):
import base64
PAYLOAD1 = b'This is the first message text'
PAYLOAD2 = b'This is the second message text'
B64_1 = base64.b64encode(PAYLOAD1)
B64_2 = base64.b64encode(PAYLOAD2)
MSGID1 = 'DEADBEEF'
MSGID2 = 'BEADCAFE'
MESSAGE1 = {'data': B64_1.decode('ascii'), 'attributes': {}}
MESSAGE2 = {
'data': B64_2.decode('ascii'),
'attributes': {'attr1': 'value1', 'attr2': 'value2'},
}
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
api = client2.publisher_api = _FauxPublisherAPI()
api._topic_publish_response = [MSGID1, MSGID2]
topic = self._makeOne(self.TOPIC_NAME, client=client1)
with topic.batch(client=client2) as batch:
batch.publish(PAYLOAD1)
batch.publish(PAYLOAD2, attr1='value1', attr2='value2')
self.assertEqual(list(batch), [MSGID1, MSGID2])
self.assertEqual(list(batch.messages), [])
self.assertEqual(api._topic_published,
(self.TOPIC_PATH, [MESSAGE1, MESSAGE2]))
def test_publish_multiple_error(self):
PAYLOAD1 = b'This is the first message text'
PAYLOAD2 = b'This is the second message text'
client = _Client(project=self.PROJECT)
api = client.publisher_api = _FauxPublisherAPI()
topic = self._makeOne(self.TOPIC_NAME, client=client)
try:
with topic.batch() as batch:
batch.publish(PAYLOAD1)
batch.publish(PAYLOAD2, attr1='value1', attr2='value2')
raise _Bugout()
except _Bugout:
pass
self.assertEqual(list(batch), [])
self.assertEqual(getattr(api, '_topic_published', self), self)
def test_subscription(self):
from gcloud.pubsub.subscription import Subscription
client = _Client(project=self.PROJECT)
topic = self._makeOne(self.TOPIC_NAME, client=client)
SUBSCRIPTION_NAME = 'subscription_name'
subscription = topic.subscription(SUBSCRIPTION_NAME)
self.assertIsInstance(subscription, Subscription)
self.assertEqual(subscription.name, SUBSCRIPTION_NAME)
self.assertTrue(subscription.topic is topic)
def test_list_subscriptions_no_paging(self):
from gcloud.pubsub.subscription import Subscription
SUB_NAME_1 = 'subscription_1'
SUB_PATH_1 = 'projects/%s/subscriptions/%s' % (
self.PROJECT, SUB_NAME_1)
SUB_NAME_2 = 'subscription_2'
SUB_PATH_2 = 'projects/%s/subscriptions/%s' % (
self.PROJECT, SUB_NAME_2)
SUBS_LIST = [SUB_PATH_1, SUB_PATH_2]
TOKEN = 'TOKEN'
client = _Client(project=self.PROJECT)
api = client.publisher_api = _FauxPublisherAPI()
api._topic_list_subscriptions_response = SUBS_LIST, TOKEN
topic = self._makeOne(self.TOPIC_NAME, client=client)
subscriptions, next_page_token = topic.list_subscriptions()
self.assertEqual(len(subscriptions), 2)
subscription = subscriptions[0]
self.assertIsInstance(subscription, Subscription)
self.assertEqual(subscriptions[0].name, SUB_NAME_1)
self.assertTrue(subscription.topic is topic)
subscription = subscriptions[1]
self.assertIsInstance(subscription, Subscription)
self.assertEqual(subscriptions[1].name, SUB_NAME_2)
self.assertTrue(subscription.topic is topic)
self.assertEqual(next_page_token, TOKEN)
self.assertEqual(api._topic_listed,
(self.TOPIC_PATH, None, None))
def test_list_subscriptions_with_paging(self):
from gcloud.pubsub.subscription import Subscription
SUB_NAME_1 = 'subscription_1'
SUB_PATH_1 = 'projects/%s/subscriptions/%s' % (
self.PROJECT, SUB_NAME_1)
SUB_NAME_2 = 'subscription_2'
SUB_PATH_2 = 'projects/%s/subscriptions/%s' % (
self.PROJECT, SUB_NAME_2)
SUBS_LIST = [SUB_PATH_1, SUB_PATH_2]
PAGE_SIZE = 10
TOKEN = 'TOKEN'
client = _Client(project=self.PROJECT)
api = client.publisher_api = _FauxPublisherAPI()
api._topic_list_subscriptions_response = SUBS_LIST, None
topic = self._makeOne(self.TOPIC_NAME, client=client)
subscriptions, next_page_token = topic.list_subscriptions(
page_size=PAGE_SIZE, page_token=TOKEN)
self.assertEqual(len(subscriptions), 2)
subscription = subscriptions[0]
self.assertIsInstance(subscription, Subscription)
self.assertEqual(subscriptions[0].name, SUB_NAME_1)
self.assertTrue(subscription.topic is topic)
subscription = subscriptions[1]
self.assertIsInstance(subscription, Subscription)
self.assertEqual(subscriptions[1].name, SUB_NAME_2)
self.assertTrue(subscription.topic is topic)
self.assertEqual(next_page_token, None)
self.assertEqual(api._topic_listed,
(self.TOPIC_PATH, PAGE_SIZE, TOKEN))
def test_list_subscriptions_missing_key(self):
client = _Client(project=self.PROJECT)
api = client.publisher_api = _FauxPublisherAPI()
api._topic_list_subscriptions_response = (), None
topic = self._makeOne(self.TOPIC_NAME, client=client)
subscriptions, next_page_token = topic.list_subscriptions()
self.assertEqual(len(subscriptions), 0)
self.assertEqual(next_page_token, None)
self.assertEqual(api._topic_listed,
(self.TOPIC_PATH, None, None))
def test_get_iam_policy_w_bound_client(self):
from gcloud.pubsub.iam import (
PUBSUB_ADMIN_ROLE,
PUBSUB_EDITOR_ROLE,
PUBSUB_VIEWER_ROLE,
PUBSUB_PUBLISHER_ROLE,
PUBSUB_SUBSCRIBER_ROLE,
)
OWNER1 = 'user:phred@example.com'
OWNER2 = 'group:cloud-logs@google.com'
EDITOR1 = 'domain:google.com'
EDITOR2 = 'user:phred@example.com'
VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com'
VIEWER2 = 'user:phred@example.com'
PUBLISHER = 'user:phred@example.com'
SUBSCRIBER = 'serviceAccount:1234-abcdef@service.example.com'
POLICY = {
'etag': 'DEADBEEF',
'version': 17,
'bindings': [
{'role': PUBSUB_ADMIN_ROLE, 'members': [OWNER1, OWNER2]},
{'role': PUBSUB_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
{'role': PUBSUB_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
{'role': PUBSUB_PUBLISHER_ROLE, 'members': [PUBLISHER]},
{'role': PUBSUB_SUBSCRIBER_ROLE, 'members': [SUBSCRIBER]},
],
}
client = _Client(project=self.PROJECT)
api = client.iam_policy_api = _FauxIAMPolicy()
api._get_iam_policy_response = POLICY
topic = self._makeOne(self.TOPIC_NAME, client=client)
policy = topic.get_iam_policy()
self.assertEqual(policy.etag, 'DEADBEEF')
self.assertEqual(policy.version, 17)
self.assertEqual(sorted(policy.owners), [OWNER2, OWNER1])
self.assertEqual(sorted(policy.editors), [EDITOR1, EDITOR2])
self.assertEqual(sorted(policy.viewers), [VIEWER1, VIEWER2])
self.assertEqual(sorted(policy.publishers), [PUBLISHER])
self.assertEqual(sorted(policy.subscribers), [SUBSCRIBER])
self.assertEqual(api._got_iam_policy, self.TOPIC_PATH)
def test_get_iam_policy_w_alternate_client(self):
POLICY = {
'etag': 'ACAB',
}
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
api = client2.iam_policy_api = _FauxIAMPolicy()
api._get_iam_policy_response = POLICY
topic = self._makeOne(self.TOPIC_NAME, client=client1)
policy = topic.get_iam_policy(client=client2)
self.assertEqual(policy.etag, 'ACAB')
self.assertEqual(policy.version, None)
self.assertEqual(sorted(policy.owners), [])
self.assertEqual(sorted(policy.editors), [])
self.assertEqual(sorted(policy.viewers), [])
self.assertEqual(api._got_iam_policy, self.TOPIC_PATH)
def test_set_iam_policy_w_bound_client(self):
from gcloud.pubsub.iam import Policy
from gcloud.pubsub.iam import (
PUBSUB_ADMIN_ROLE,
PUBSUB_EDITOR_ROLE,
PUBSUB_VIEWER_ROLE,
PUBSUB_PUBLISHER_ROLE,
PUBSUB_SUBSCRIBER_ROLE,
)
OWNER1 = 'group:cloud-logs@google.com'
OWNER2 = 'user:phred@example.com'
EDITOR1 = 'domain:google.com'
EDITOR2 = 'user:phred@example.com'
VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com'
VIEWER2 = 'user:phred@example.com'
PUBLISHER = 'user:phred@example.com'
SUBSCRIBER = 'serviceAccount:1234-abcdef@service.example.com'
POLICY = {
'etag': 'DEADBEEF',
'version': 17,
'bindings': [
{'role': PUBSUB_ADMIN_ROLE,
'members': [OWNER1, OWNER2]},
{'role': PUBSUB_EDITOR_ROLE,
'members': [EDITOR1, EDITOR2]},
{'role': PUBSUB_VIEWER_ROLE,
'members': [VIEWER1, VIEWER2]},
{'role': PUBSUB_PUBLISHER_ROLE,
'members': [PUBLISHER]},
{'role': PUBSUB_SUBSCRIBER_ROLE,
'members': [SUBSCRIBER]},
],
}
RESPONSE = POLICY.copy()
RESPONSE['etag'] = 'ABACABAF'
RESPONSE['version'] = 18
client = _Client(project=self.PROJECT)
api = client.iam_policy_api = _FauxIAMPolicy()
api._set_iam_policy_response = RESPONSE
topic = self._makeOne(self.TOPIC_NAME, client=client)
policy = Policy('DEADBEEF', 17)
policy.owners.add(OWNER1)
policy.owners.add(OWNER2)
policy.editors.add(EDITOR1)
policy.editors.add(EDITOR2)
policy.viewers.add(VIEWER1)
policy.viewers.add(VIEWER2)
policy.publishers.add(PUBLISHER)
policy.subscribers.add(SUBSCRIBER)
new_policy = topic.set_iam_policy(policy)
self.assertEqual(new_policy.etag, 'ABACABAF')
self.assertEqual(new_policy.version, 18)
self.assertEqual(sorted(new_policy.owners), [OWNER1, OWNER2])
self.assertEqual(sorted(new_policy.editors), [EDITOR1, EDITOR2])
self.assertEqual(sorted(new_policy.viewers), [VIEWER1, VIEWER2])
self.assertEqual(sorted(new_policy.publishers), [PUBLISHER])
self.assertEqual(sorted(new_policy.subscribers), [SUBSCRIBER])
self.assertEqual(api._set_iam_policy, (self.TOPIC_PATH, POLICY))
def test_set_iam_policy_w_alternate_client(self):
from gcloud.pubsub.iam import Policy
RESPONSE = {'etag': 'ACAB'}
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
api = client2.iam_policy_api = _FauxIAMPolicy()
api._set_iam_policy_response = RESPONSE
topic = self._makeOne(self.TOPIC_NAME, client=client1)
policy = Policy()
new_policy = topic.set_iam_policy(policy, client=client2)
self.assertEqual(new_policy.etag, 'ACAB')
self.assertEqual(new_policy.version, None)
self.assertEqual(sorted(new_policy.owners), [])
self.assertEqual(sorted(new_policy.editors), [])
self.assertEqual(sorted(new_policy.viewers), [])
self.assertEqual(api._set_iam_policy, (self.TOPIC_PATH, {}))
def test_check_iam_permissions_w_bound_client(self):
from gcloud.pubsub.iam import OWNER_ROLE, EDITOR_ROLE, VIEWER_ROLE
ROLES = [VIEWER_ROLE, EDITOR_ROLE, OWNER_ROLE]
client = _Client(project=self.PROJECT)
api = client.iam_policy_api = _FauxIAMPolicy()
api._test_iam_permissions_response = ROLES[:-1]
topic = self._makeOne(self.TOPIC_NAME, client=client)
allowed = topic.check_iam_permissions(ROLES)
self.assertEqual(allowed, ROLES[:-1])
self.assertEqual(api._tested_iam_permissions,
(self.TOPIC_PATH, ROLES))
def test_check_iam_permissions_w_alternate_client(self):
from gcloud.pubsub.iam import OWNER_ROLE, EDITOR_ROLE, VIEWER_ROLE
ROLES = [VIEWER_ROLE, EDITOR_ROLE, OWNER_ROLE]
client1 = _Client(project=self.PROJECT)
client2 = _Client(project=self.PROJECT)
api = client2.iam_policy_api = _FauxIAMPolicy()
api._test_iam_permissions_response = []
topic = self._makeOne(self.TOPIC_NAME, client=client1)
allowed = topic.check_iam_permissions(ROLES, client=client2)
self.assertEqual(len(allowed), 0)
self.assertEqual(api._tested_iam_permissions,
(self.TOPIC_PATH, ROLES))
class TestBatch(unittest2.TestCase):
PROJECT = 'PROJECT'
def _getTargetClass(self):
from gcloud.pubsub.topic import Batch
return Batch
def _makeOne(self, *args, **kwargs):
return self._getTargetClass()(*args, **kwargs)
def test_ctor_defaults(self):
topic = _Topic()
client = _Client(project=self.PROJECT)
batch = self._makeOne(topic, client)
self.assertTrue(batch.topic is topic)
self.assertTrue(batch.client is client)
self.assertEqual(len(batch.messages), 0)
self.assertEqual(len(batch.message_ids), 0)
def test___iter___empty(self):
topic = _Topic()
client = object()
batch = self._makeOne(topic, client)
self.assertEqual(list(batch), [])
def test___iter___non_empty(self):
topic = _Topic()
client = object()
batch = self._makeOne(topic, client)
batch.message_ids[:] = ['ONE', 'TWO', 'THREE']
self.assertEqual(list(batch), ['ONE', 'TWO', 'THREE'])
def test_publish_bytes_wo_attrs(self):
import base64
PAYLOAD = b'This is the message text'
B64 = base64.b64encode(PAYLOAD).decode('ascii')
MESSAGE = {'data': B64,
'attributes': {}}
client = _Client(project=self.PROJECT)
topic = _Topic()
batch = self._makeOne(topic, client=client)
batch.publish(PAYLOAD)
self.assertEqual(batch.messages, [MESSAGE])
def test_publish_bytes_w_add_timestamp(self):
import base64
PAYLOAD = b'This is the message text'
B64 = base64.b64encode(PAYLOAD).decode('ascii')
MESSAGE = {'data': B64,
'attributes': {'timestamp': 'TIMESTAMP'}}
client = _Client(project=self.PROJECT)
topic = _Topic(timestamp_messages=True)
batch = self._makeOne(topic, client=client)
batch.publish(PAYLOAD)
self.assertEqual(batch.messages, [MESSAGE])
def test_commit_w_bound_client(self):
import base64
PAYLOAD1 = b'This is the first message text'
PAYLOAD2 = b'This is the second message text'
B64_1 = base64.b64encode(PAYLOAD1)
B64_2 = base64.b64encode(PAYLOAD2)
MSGID1 = 'DEADBEEF'
MSGID2 = 'BEADCAFE'
MESSAGE1 = {'data': B64_1.decode('ascii'),
'attributes': {}}
MESSAGE2 = {'data': B64_2.decode('ascii'),
'attributes': {'attr1': 'value1', 'attr2': 'value2'}}
client = _Client(project='PROJECT')
api = client.publisher_api = _FauxPublisherAPI()
api._topic_publish_response = [MSGID1, MSGID2]
topic = _Topic()
batch = self._makeOne(topic, client=client)
batch.publish(PAYLOAD1)
batch.publish(PAYLOAD2, attr1='value1', attr2='value2')
batch.commit()
self.assertEqual(list(batch), [MSGID1, MSGID2])
self.assertEqual(list(batch.messages), [])
self.assertEqual(api._topic_published,
(topic.full_name, [MESSAGE1, MESSAGE2]))
def test_commit_w_alternate_client(self):
import base64
PAYLOAD1 = b'This is the first message text'
PAYLOAD2 = b'This is the second message text'
B64_1 = base64.b64encode(PAYLOAD1)
B64_2 = base64.b64encode(PAYLOAD2)
MSGID1 = 'DEADBEEF'
MSGID2 = 'BEADCAFE'
MESSAGE1 = {'data': B64_1.decode('ascii'),
'attributes': {}}
MESSAGE2 = {'data': B64_2.decode('ascii'),
'attributes': {'attr1': 'value1', 'attr2': 'value2'}}
client1 = _Client(project='PROJECT')
client2 = _Client(project='PROJECT')
api = client2.publisher_api = _FauxPublisherAPI()
api._topic_publish_response = [MSGID1, MSGID2]
topic = _Topic()
batch = self._makeOne(topic, client=client1)
batch.publish(PAYLOAD1)
batch.publish(PAYLOAD2, attr1='value1', attr2='value2')
batch.commit(client=client2)
self.assertEqual(list(batch), [MSGID1, MSGID2])
self.assertEqual(list(batch.messages), [])
self.assertEqual(api._topic_published,
(topic.full_name, [MESSAGE1, MESSAGE2]))
def test_context_mgr_success(self):
import base64
PAYLOAD1 = b'This is the first message text'
PAYLOAD2 = b'This is the second message text'
B64_1 = base64.b64encode(PAYLOAD1)
B64_2 = base64.b64encode(PAYLOAD2)
MSGID1 = 'DEADBEEF'
MSGID2 = 'BEADCAFE'
MESSAGE1 = {'data': B64_1.decode('ascii'),
'attributes': {}}
MESSAGE2 = {'data': B64_2.decode('ascii'),
'attributes': {'attr1': 'value1', 'attr2': 'value2'}}
client = _Client(project='PROJECT')
api = client.publisher_api = _FauxPublisherAPI()
api._topic_publish_response = [MSGID1, MSGID2]
topic = _Topic()
batch = self._makeOne(topic, client=client)
with batch as other:
batch.publish(PAYLOAD1)
batch.publish(PAYLOAD2, attr1='value1', attr2='value2')
self.assertTrue(other is batch)
self.assertEqual(list(batch), [MSGID1, MSGID2])
self.assertEqual(list(batch.messages), [])
self.assertEqual(api._topic_published,
(topic.full_name, [MESSAGE1, MESSAGE2]))
def test_context_mgr_failure(self):
import base64
PAYLOAD1 = b'This is the first message text'
PAYLOAD2 = b'This is the second message text'
B64_1 = base64.b64encode(PAYLOAD1)
B64_2 = base64.b64encode(PAYLOAD2)
MESSAGE1 = {'data': B64_1.decode('ascii'),
'attributes': {}}
MESSAGE2 = {'data': B64_2.decode('ascii'),
'attributes': {'attr1': 'value1', 'attr2': 'value2'}}
client = _Client(project='PROJECT')
api = client.publisher_api = _FauxPublisherAPI()
topic = _Topic()
batch = self._makeOne(topic, client=client)
try:
with batch as other:
batch.publish(PAYLOAD1)
batch.publish(PAYLOAD2, attr1='value1', attr2='value2')
raise _Bugout()
except _Bugout:
pass
self.assertTrue(other is batch)
self.assertEqual(list(batch), [])
self.assertEqual(list(batch.messages), [MESSAGE1, MESSAGE2])
self.assertEqual(getattr(api, '_topic_published', self), self)
class _FauxPublisherAPI(object):
def topic_create(self, topic_path):
self._topic_created = topic_path
return self._topic_create_response
def topic_get(self, topic_path):
from gcloud.exceptions import NotFound
self._topic_got = topic_path
try:
return self._topic_get_response
except AttributeError:
raise NotFound(topic_path)
def topic_delete(self, topic_path):
self._topic_deleted = topic_path
return self._topic_delete_response
def topic_publish(self, topic_path, messages):
self._topic_published = topic_path, messages
return self._topic_publish_response
def topic_list_subscriptions(self, topic_path, page_size=None,
page_token=None):
self._topic_listed = topic_path, page_size, page_token
return self._topic_list_subscriptions_response
class _FauxIAMPolicy(object):
def get_iam_policy(self, target_path):
self._got_iam_policy = target_path
return self._get_iam_policy_response
def set_iam_policy(self, target_path, policy):
self._set_iam_policy = target_path, policy
return self._set_iam_policy_response
def test_iam_permissions(self, target_path, permissions):
self._tested_iam_permissions = target_path, permissions
return self._test_iam_permissions_response
class _Topic(object):
def __init__(self, name="NAME", project="PROJECT",
timestamp_messages=False):
self.full_name = 'projects/%s/topics/%s' % (project, name)
self.path = '/%s' % (self.full_name,)
self.timestamp_messages = timestamp_messages
def _timestamp_message(self, attrs):
if self.timestamp_messages:
attrs['timestamp'] = 'TIMESTAMP'
class _Client(object):
connection = None
def __init__(self, project):
self.project = project
class _Bugout(Exception):
pass

View file

@ -0,0 +1,451 @@
# 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[:]