diff --git a/core/google/cloud/iam.py b/core/google/cloud/iam.py new file mode 100644 index 000000000000..4747b39bbf07 --- /dev/null +++ b/core/google/cloud/iam.py @@ -0,0 +1,242 @@ +# Copyright 2017 Google Inc. +# +# 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. +"""Non-API-specific IAM policy definitions + +For allowed roles / permissions, see: +https://cloud.google.com/iam/docs/understanding-roles +""" + +import collections +import warnings + +# 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.""" + +_ASSIGNMENT_DEPRECATED_MSG = """\ +Assigning to '{}' is deprecated. Replace with 'policy[{}] = members.""" + + +class Policy(collections.MutableMapping): + """IAM Policy + + See: + https://cloud.google.com/iam/reference/rest/v1/Policy + + :type etag: str + :param etag: ETag used to identify a unique of the policy + + :type version: int + :param version: unique version of the policy + """ + _OWNER_ROLES = (OWNER_ROLE,) + """Roles mapped onto our ``owners`` attribute.""" + + _EDITOR_ROLES = (EDITOR_ROLE,) + """Roles mapped onto our ``editors`` attribute.""" + + _VIEWER_ROLES = (VIEWER_ROLE,) + """Roles mapped onto our ``viewers`` attribute.""" + + def __init__(self, etag=None, version=None): + self.etag = etag + self.version = version + self._bindings = {} + + def __iter__(self): + return iter(self._bindings) + + def __len__(self): + return len(self._bindings) + + def __getitem__(self, key): + return self._bindings[key] + + def __setitem__(self, key, value): + self._bindings[key] = frozenset(value) + + def __delitem__(self, key): + del self._bindings[key] + + @property + def owners(self): + """Legacy access to owner role.""" + result = set() + for role in self._OWNER_ROLES: + for member in self._bindings.get(role, ()): + result.add(member) + return frozenset(result) + + @owners.setter + def owners(self, value): + """Update owners.""" + warnings.warn( + _ASSIGNMENT_DEPRECATED_MSG.format('owners', OWNER_ROLE), + DeprecationWarning) + self._bindings[OWNER_ROLE] = list(value) + + @property + def editors(self): + """Legacy access to editor role.""" + result = set() + for role in self._EDITOR_ROLES: + for member in self._bindings.get(role, ()): + result.add(member) + return frozenset(result) + + @editors.setter + def editors(self, value): + """Update editors.""" + warnings.warn( + _ASSIGNMENT_DEPRECATED_MSG.format('editors', EDITOR_ROLE), + DeprecationWarning) + self._bindings[EDITOR_ROLE] = list(value) + + @property + def viewers(self): + """Legacy access to viewer role.""" + result = set() + for role in self._VIEWER_ROLES: + for member in self._bindings.get(role, ()): + result.add(member) + return frozenset(result) + + @viewers.setter + def viewers(self, value): + """Update viewers.""" + warnings.warn( + _ASSIGNMENT_DEPRECATED_MSG.format('viewers', VIEWER_ROLE), + DeprecationWarning) + self._bindings[VIEWER_ROLE] = list(value) + + @staticmethod + def user(email): + """Factory method for a user member. + + :type email: str + :param email: E-mail for this particular user. + + :rtype: str + :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: str + :param email: E-mail for this particular service account. + + :rtype: str + :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: str + :param email: An id or e-mail for this particular group. + + :rtype: str + :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: str + :param domain: The domain for this member. + + :rtype: str + :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: str + :returns: A member string representing all users. + """ + return 'allUsers' + + @staticmethod + def authenticated_users(): + """Factory method for a member representing all authenticated users. + + :rtype: str + :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 = sorted(binding['members']) + policy._bindings[role] = members + 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 + + if len(self._bindings) > 0: + bindings = resource['bindings'] = [] + for role, members in sorted(self._bindings.items()): + if len(members) > 0: + bindings.append( + {'role': role, 'members': sorted(set(members))}) + + if len(bindings) == 0: + del resource['bindings'] + + return resource + + +collections.MutableMapping.register(Policy) diff --git a/core/tests/unit/test_iam.py b/core/tests/unit/test_iam.py new file mode 100644 index 000000000000..9f1bd9b3904f --- /dev/null +++ b/core/tests/unit/test_iam.py @@ -0,0 +1,281 @@ +# Copyright 2017 Google Inc. +# +# 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 unittest + + +class TestPolicy(unittest.TestCase): + + @staticmethod + def _get_target_class(): + from google.cloud.iam import Policy + + return Policy + + def _make_one(self, *args, **kw): + return self._get_target_class()(*args, **kw) + + def test_ctor_defaults(self): + policy = self._make_one() + self.assertIsNone(policy.etag) + self.assertIsNone(policy.version) + self.assertIsInstance(policy.owners, frozenset) + self.assertEqual(list(policy.owners), []) + self.assertIsInstance(policy.editors, frozenset) + self.assertEqual(list(policy.editors), []) + self.assertIsInstance(policy.viewers, frozenset) + self.assertEqual(list(policy.viewers), []) + self.assertEqual(len(policy), 0) + self.assertEqual(dict(policy), {}) + + def test_ctor_explicit(self): + VERSION = 17 + ETAG = 'ETAG' + policy = self._make_one(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(len(policy), 0) + self.assertEqual(dict(policy), {}) + + def test___getitem___miss(self): + policy = self._make_one() + with self.assertRaises(KeyError): + policy['nonesuch'] + + def test___setitem__(self): + USER = 'user:phred@example.com' + PRINCIPALS = frozenset([USER]) + policy = self._make_one() + policy['rolename'] = [USER] + self.assertEqual(policy['rolename'], PRINCIPALS) + self.assertEqual(len(policy), 1) + self.assertEqual(dict(policy), {'rolename': PRINCIPALS}) + + def test___delitem___hit(self): + policy = self._make_one() + policy._bindings['rolename'] = ['phred@example.com'] + del policy['rolename'] + self.assertEqual(len(policy), 0) + self.assertEqual(dict(policy), {}) + + def test___delitem___miss(self): + policy = self._make_one() + with self.assertRaises(KeyError): + del policy['nonesuch'] + + def test_owners_getter(self): + from google.cloud.iam import OWNER_ROLE + MEMBER = 'user:phred@example.com' + policy = self._make_one() + policy[OWNER_ROLE] = [MEMBER] + self.assertIsInstance(policy.owners, frozenset) + self.assertEqual(list(policy.owners), [MEMBER]) + + def test_owners_setter(self): + import warnings + from google.cloud.iam import OWNER_ROLE + MEMBER = 'user:phred@example.com' + policy = self._make_one() + with warnings.catch_warnings(): + policy.owners = [MEMBER] + self.assertEqual(list(policy[OWNER_ROLE]), [MEMBER]) + + def test_editors_getter(self): + from google.cloud.iam import EDITOR_ROLE + MEMBER = 'user:phred@example.com' + policy = self._make_one() + policy[EDITOR_ROLE] = [MEMBER] + self.assertIsInstance(policy.editors, frozenset) + self.assertEqual(list(policy.editors), [MEMBER]) + + def test_editors_setter(self): + import warnings + from google.cloud.iam import EDITOR_ROLE + MEMBER = 'user:phred@example.com' + policy = self._make_one() + with warnings.catch_warnings(): + policy.editors = [MEMBER] + self.assertEqual(list(policy[EDITOR_ROLE]), [MEMBER]) + + def test_viewers_getter(self): + from google.cloud.iam import VIEWER_ROLE + MEMBER = 'user:phred@example.com' + policy = self._make_one() + policy[VIEWER_ROLE] = [MEMBER] + self.assertIsInstance(policy.viewers, frozenset) + self.assertEqual(list(policy.viewers), [MEMBER]) + + def test_viewers_setter(self): + import warnings + from google.cloud.iam import VIEWER_ROLE + MEMBER = 'user:phred@example.com' + policy = self._make_one() + with warnings.catch_warnings(): + warnings.simplefilter('always') + policy.viewers = [MEMBER] + self.assertEqual(list(policy[VIEWER_ROLE]), [MEMBER]) + + def test_user(self): + EMAIL = 'phred@example.com' + MEMBER = 'user:%s' % (EMAIL,) + policy = self._make_one() + self.assertEqual(policy.user(EMAIL), MEMBER) + + def test_service_account(self): + EMAIL = 'phred@example.com' + MEMBER = 'serviceAccount:%s' % (EMAIL,) + policy = self._make_one() + self.assertEqual(policy.service_account(EMAIL), MEMBER) + + def test_group(self): + EMAIL = 'phred@example.com' + MEMBER = 'group:%s' % (EMAIL,) + policy = self._make_one() + self.assertEqual(policy.group(EMAIL), MEMBER) + + def test_domain(self): + DOMAIN = 'example.com' + MEMBER = 'domain:%s' % (DOMAIN,) + policy = self._make_one() + self.assertEqual(policy.domain(DOMAIN), MEMBER) + + def test_all_users(self): + policy = self._make_one() + self.assertEqual(policy.all_users(), 'allUsers') + + def test_authenticated_users(self): + policy = self._make_one() + self.assertEqual(policy.authenticated_users(), 'allAuthenticatedUsers') + + def test_from_api_repr_only_etag(self): + RESOURCE = { + 'etag': 'ACAB', + } + klass = self._get_target_class() + policy = klass.from_api_repr(RESOURCE) + self.assertEqual(policy.etag, 'ACAB') + self.assertIsNone(policy.version) + self.assertEqual(list(policy.owners), []) + self.assertEqual(list(policy.editors), []) + self.assertEqual(list(policy.viewers), []) + self.assertEqual(dict(policy), {}) + + def test_from_api_repr_complete(self): + from google.cloud.iam import ( + OWNER_ROLE, + EDITOR_ROLE, + VIEWER_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' + RESOURCE = { + 'etag': 'DEADBEEF', + 'version': 17, + 'bindings': [ + {'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]}, + {'role': EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]}, + {'role': VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]}, + ], + } + klass = self._get_target_class() + policy = klass.from_api_repr(RESOURCE) + self.assertEqual(policy.etag, 'DEADBEEF') + self.assertEqual(policy.version, 17) + self.assertEqual(sorted(policy.owners), [OWNER1, OWNER2]) + self.assertEqual(sorted(policy.editors), [EDITOR1, EDITOR2]) + self.assertEqual(sorted(policy.viewers), [VIEWER1, VIEWER2]) + self.assertEqual( + dict(policy), { + OWNER_ROLE: [OWNER1, OWNER2], + EDITOR_ROLE: [EDITOR1, EDITOR2], + VIEWER_ROLE: [VIEWER1, VIEWER2], + }) + + def test_from_api_repr_unknown_role(self): + USER = 'user:phred@example.com' + GROUP = 'group:cloud-logs@google.com' + RESOURCE = { + 'etag': 'DEADBEEF', + 'version': 17, + 'bindings': [ + {'role': 'unknown', 'members': [USER, GROUP]}, + ], + } + klass = self._get_target_class() + policy = klass.from_api_repr(RESOURCE) + self.assertEqual(policy.etag, 'DEADBEEF') + self.assertEqual(policy.version, 17) + self.assertEqual(dict(policy), {'unknown': [GROUP, USER]}) + + def test_to_api_repr_defaults(self): + policy = self._make_one() + self.assertEqual(policy.to_api_repr(), {}) + + def test_to_api_repr_only_etag(self): + policy = self._make_one('DEADBEEF') + self.assertEqual(policy.to_api_repr(), {'etag': 'DEADBEEF'}) + + def test_to_api_repr_binding_wo_members(self): + policy = self._make_one() + policy['empty'] = [] + self.assertEqual(policy.to_api_repr(), {}) + + def test_to_api_repr_binding_w_duplicates(self): + from google.cloud.iam import OWNER_ROLE + + OWNER = 'group:cloud-logs@google.com' + policy = self._make_one() + policy.owners = [OWNER, OWNER] + self.assertEqual( + policy.to_api_repr(), { + 'bindings': [{'role': OWNER_ROLE, 'members': [OWNER]}], + }) + + def test_to_api_repr_full(self): + import operator + from google.cloud.iam import ( + OWNER_ROLE, + EDITOR_ROLE, + VIEWER_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' + BINDINGS = [ + {'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]}, + {'role': EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]}, + {'role': VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]}, + ] + policy = self._make_one('DEADBEEF', 17) + policy.owners = [OWNER1, OWNER2] + policy.editors = [EDITOR1, EDITOR2] + policy.viewers = [VIEWER1, VIEWER2] + resource = policy.to_api_repr() + self.assertEqual(resource['etag'], 'DEADBEEF') + self.assertEqual(resource['version'], 17) + key = operator.itemgetter('role') + self.assertEqual( + sorted(resource['bindings'], key=key), sorted(BINDINGS, key=key)) diff --git a/docs/google-cloud-api.rst b/docs/google-cloud-api.rst index bb2fd2842e9f..195a79c5abb2 100644 --- a/docs/google-cloud-api.rst +++ b/docs/google-cloud-api.rst @@ -29,3 +29,10 @@ Environment Variables .. automodule:: google.cloud.environment_vars :members: :show-inheritance: + +IAM Support +~~~~~~~~~~~ + +.. automodule:: google.cloud.iam + :members: + :show-inheritance: diff --git a/docs/pubsub_snippets.py b/docs/pubsub_snippets.py index 7255694d15f1..8584dedcd34d 100644 --- a/docs/pubsub_snippets.py +++ b/docs/pubsub_snippets.py @@ -124,9 +124,9 @@ def topic_iam_policy(client, to_delete): # [START topic_set_iam_policy] ALL_USERS = policy.all_users() - policy.viewers.add(ALL_USERS) + policy.viewers = [ALL_USERS] LOGS_GROUP = policy.group('cloud-logs@google.com') - policy.editors.add(LOGS_GROUP) + policy.editors = [LOGS_GROUP] new_policy = topic.set_iam_policy(policy) # API request # [END topic_set_iam_policy] @@ -395,9 +395,9 @@ def subscription_iam_policy(client, to_delete): # [START subscription_set_iam_policy] ALL_USERS = policy.all_users() - policy.viewers.add(ALL_USERS) + policy.viewers = [ALL_USERS] LOGS_GROUP = policy.group('cloud-logs@google.com') - policy.editors.add(LOGS_GROUP) + policy.editors = [LOGS_GROUP] new_policy = subscription.set_iam_policy(policy) # API request # [END subscription_set_iam_policy] diff --git a/docs/speech-usage.rst b/docs/speech-usage.rst index 27c1309beda1..f1feb20e7979 100644 --- a/docs/speech-usage.rst +++ b/docs/speech-usage.rst @@ -5,11 +5,6 @@ The `Google Speech`_ API enables developers to convert audio to text. The API recognizes over 80 languages and variants, to support your global user base. -.. warning:: - - This is a Beta release of Google Speech API. This - API is not intended for real-time usage in critical applications. - .. _Google Speech: https://cloud.google.com/speech/docs/getting-started Client @@ -39,11 +34,6 @@ data to the Speech API and initiates a Long Running Operation. Using this operation, you can periodically poll for recognition results. Use asynchronous requests for audio data of any duration up to 80 minutes. -.. note:: - - Only the :attr:`Encoding.LINEAR16` encoding type is supported by - asynchronous recognition. - See: `Speech Asynchronous Recognize`_ diff --git a/error_reporting/google/cloud/error_reporting/client.py b/error_reporting/google/cloud/error_reporting/client.py index 8be6627ada28..6e768a0534fb 100644 --- a/error_reporting/google/cloud/error_reporting/client.py +++ b/error_reporting/google/cloud/error_reporting/client.py @@ -224,11 +224,10 @@ def _build_error_report(self, if http_context: http_context_dict = http_context.__dict__ # strip out None values - payload['context']['httpContext'] = { + payload['context']['httpRequest'] = { key: value for key, value in six.iteritems(http_context_dict) if value is not None } - if user: payload['context']['user'] = user return payload diff --git a/error_reporting/tests/unit/test_client.py b/error_reporting/tests/unit/test_client.py index 5bdc6c5ed60d..5cef939a1da3 100644 --- a/error_reporting/tests/unit/test_client.py +++ b/error_reporting/tests/unit/test_client.py @@ -149,9 +149,9 @@ def test_report_exception_with_service_version_in_constructor( payload['message']) self.assertIn('test_client.py', payload['message']) self.assertEqual( - payload['context']['httpContext']['responseStatusCode'], 500) + payload['context']['httpRequest']['responseStatusCode'], 500) self.assertEqual( - payload['context']['httpContext']['method'], 'GET') + payload['context']['httpRequest']['method'], 'GET') self.assertEqual(payload['context']['user'], user) @mock.patch('google.cloud.error_reporting.client.make_report_error_api') diff --git a/language/google/cloud/language/_http.py b/language/google/cloud/language/_http.py index af0ad516677f..1d466a47edf9 100644 --- a/language/google/cloud/language/_http.py +++ b/language/google/cloud/language/_http.py @@ -41,3 +41,14 @@ class Connection(_http.JSONConnection): _EXTRA_HEADERS = { _http.CLIENT_INFO_HEADER: _CLIENT_INFO, } + + +class V1Beta2Connection(Connection): + """A connection to Google Cloud Natural Language JSON 1.1 REST API. + + :type client: :class:`~google.cloud.language.client.Client` + :param client: The client that owns the current connection. + """ + + API_VERSION = 'v1beta2' + """The version of the API, used in building the API call's URL.""" diff --git a/language/google/cloud/language/client.py b/language/google/cloud/language/client.py index 39221316e428..da6ea90c156b 100644 --- a/language/google/cloud/language/client.py +++ b/language/google/cloud/language/client.py @@ -20,6 +20,7 @@ from google.cloud import client as client_module from google.cloud.language._http import Connection +from google.cloud.language._http import V1Beta2Connection from google.cloud.language.document import Document @@ -45,10 +46,16 @@ class Client(client_module.Client): SCOPE = ('https://www.googleapis.com/auth/cloud-platform',) """The scopes required for authenticating as an API consumer.""" - def __init__(self, credentials=None, _http=None): + _CONNECTION_CLASSES = { + 'v1': Connection, + 'v1beta2': V1Beta2Connection, + } + + def __init__(self, credentials=None, api_version='v1', _http=None): super(Client, self).__init__( credentials=credentials, _http=_http) - self._connection = Connection(self) + ConnectionClass = self._CONNECTION_CLASSES[api_version] + self._connection = ConnectionClass(self) def document_from_text(self, content, **kwargs): """Create a plain text document bound to this client. diff --git a/language/google/cloud/language/document.py b/language/google/cloud/language/document.py index 96691a4c9be9..f350fcb6c63b 100644 --- a/language/google/cloud/language/document.py +++ b/language/google/cloud/language/document.py @@ -188,6 +188,32 @@ def analyze_entities(self): method='POST', path='analyzeEntities', data=data) return api_responses.EntityResponse.from_api_repr(api_response) + def analyze_entity_sentiment(self): + """Analyze the entity sentiment. + + Finds entities, similar to `AnalyzeEntities` in the text and + analyzes sentiment associated with each entity and its mentions. + + :rtype: :class:`~language.entity.EntitySentimentResponse` + :returns: A representation of the entity sentiment response. + """ + # Sanity check: Not available on v1. + if self.client._connection.API_VERSION == 'v1': + raise NotImplementedError( + 'The `analyze_entity_sentiment` method is only available ' + 'on the Natural Language 1.1 beta. Use version="v1beta2" ' + 'as a keyword argument to the constructor.', + ) + + # Perform the API request. + data = { + 'document': self._to_dict(), + 'encodingType': self.encoding, + } + api_response = self.client._connection.api_request( + method='POST', path='analyzeEntitySentiment', data=data) + return api_responses.EntityResponse.from_api_repr(api_response) + def analyze_sentiment(self): """Analyze the sentiment in the current document. diff --git a/language/google/cloud/language/entity.py b/language/google/cloud/language/entity.py index 4bf3ed950ba8..991232f19461 100644 --- a/language/google/cloud/language/entity.py +++ b/language/google/cloud/language/entity.py @@ -17,6 +17,8 @@ An entity is used to describe a proper name extracted from text. """ +from google.cloud.language.sentiment import Sentiment + class EntityType(object): """List of possible entity types.""" @@ -152,14 +154,20 @@ class Entity(object): :type mentions: list :param mentions: List of strings that mention the entity. + + :type sentiment: :class:`~.language.sentiment.Sentiment` + :params sentiment: The sentiment; sent only on `analyze_entity_sentiment` + calls. """ - def __init__(self, name, entity_type, metadata, salience, mentions): + def __init__(self, name, entity_type, metadata, salience, mentions, + sentiment): self.name = name self.entity_type = entity_type self.metadata = metadata self.salience = salience self.mentions = mentions + self.sentiment = sentiment @classmethod def from_api_repr(cls, payload): @@ -176,4 +184,7 @@ def from_api_repr(cls, payload): metadata = payload['metadata'] salience = payload['salience'] mentions = [Mention.from_api_repr(val) for val in payload['mentions']] - return cls(name, entity_type, metadata, salience, mentions) + sentiment = None + if payload.get('sentiment'): + sentiment = Sentiment.from_api_repr(payload['sentiment']) + return cls(name, entity_type, metadata, salience, mentions, sentiment) diff --git a/language/google/cloud/language/sentiment.py b/language/google/cloud/language/sentiment.py index e1e5da7edc6f..2c56f9d1ee87 100644 --- a/language/google/cloud/language/sentiment.py +++ b/language/google/cloud/language/sentiment.py @@ -52,6 +52,6 @@ def from_api_repr(cls, payload): :rtype: :class:`Sentiment` :returns: The sentiment parsed from the API representation. """ - score = payload['score'] + score = payload.get('score', None) magnitude = payload['magnitude'] return cls(score, magnitude) diff --git a/language/setup.py b/language/setup.py index a269668892ce..bbc36afe3e83 100644 --- a/language/setup.py +++ b/language/setup.py @@ -56,7 +56,7 @@ setup( name='google-cloud-language', - version='0.24.0', + version='0.24.1', description='Python Client for Google Cloud Natural Language', long_description=README, namespace_packages=[ diff --git a/language/tests/unit/test_document.py b/language/tests/unit/test_document.py index 19f747ed491b..c30d13b6f15e 100644 --- a/language/tests/unit/test_document.py +++ b/language/tests/unit/test_document.py @@ -96,12 +96,12 @@ def _get_entities(include_entities): return entities -def make_mock_client(response): +def make_mock_client(response, api_version='v1'): import mock - from google.cloud.language._http import Connection from google.cloud.language.client import Client - connection = mock.Mock(spec=Connection) + connection = mock.Mock(spec=Client._CONNECTION_CLASSES[api_version]) + connection.API_VERSION = api_version connection.api_request.return_value = response return mock.Mock(_connection=connection, spec=Client) @@ -205,7 +205,8 @@ def test__to_dict_with_no_content(self): 'type': klass.PLAIN_TEXT, }) - def _verify_entity(self, entity, name, entity_type, wiki_url, salience): + def _verify_entity(self, entity, name, entity_type, wiki_url, salience, + sentiment=None): from google.cloud.language.entity import Entity self.assertIsInstance(entity, Entity) @@ -218,6 +219,10 @@ def _verify_entity(self, entity, name, entity_type, wiki_url, salience): self.assertEqual(entity.salience, salience) self.assertEqual(len(entity.mentions), 1) self.assertEqual(entity.mentions[0].text.content, name) + if sentiment: + self.assertEqual(entity.sentiment.score, sentiment.score) + self.assertAlmostEqual(entity.sentiment.magnitude, + sentiment.magnitude) @staticmethod def _expected_data(content, encoding_type=None, @@ -308,6 +313,85 @@ def test_analyze_entities(self): client._connection.api_request.assert_called_once_with( path='analyzeEntities', method='POST', data=expected) + def test_analyze_entity_sentiment_v1_error(self): + client = make_mock_client({}) + document = self._make_one(client, 'foo bar baz') + with self.assertRaises(NotImplementedError): + entity_response = document.analyze_entity_sentiment() + + def test_analyze_entity_sentiment(self): + from google.cloud.language.document import Encoding + from google.cloud.language.entity import EntityType + from google.cloud.language.sentiment import Sentiment + + name1 = 'R-O-C-K' + name2 = 'USA' + content = name1 + ' in the ' + name2 + wiki2 = 'http://en.wikipedia.org/wiki/United_States' + salience1 = 0.91391456 + salience2 = 0.086085409 + sentiment = Sentiment(score=0.15, magnitude=42) + response = { + 'entities': [ + { + 'name': name1, + 'type': EntityType.OTHER, + 'metadata': {}, + 'salience': salience1, + 'mentions': [ + { + 'text': { + 'content': name1, + 'beginOffset': -1 + }, + 'type': 'TYPE_UNKNOWN', + } + ], + 'sentiment': { + 'score': 0.15, + 'magnitude': 42, + } + }, + { + 'name': name2, + 'type': EntityType.LOCATION, + 'metadata': {'wikipedia_url': wiki2}, + 'salience': salience2, + 'mentions': [ + { + 'text': { + 'content': name2, + 'beginOffset': -1, + }, + 'type': 'PROPER', + }, + ], + 'sentiment': { + 'score': 0.15, + 'magnitude': 42, + } + }, + ], + 'language': 'en-US', + } + client = make_mock_client(response, api_version='v1beta2') + document = self._make_one(client, content) + + entity_response = document.analyze_entity_sentiment() + self.assertEqual(len(entity_response.entities), 2) + entity1 = entity_response.entities[0] + self._verify_entity(entity1, name1, EntityType.OTHER, + None, salience1, sentiment) + entity2 = entity_response.entities[1] + self._verify_entity(entity2, name2, EntityType.LOCATION, + wiki2, salience2, sentiment) + + # Verify the request. + expected = self._expected_data( + content, encoding_type=Encoding.get_default()) + client._connection.api_request.assert_called_once_with( + path='analyzeEntitySentiment', method='POST', data=expected) + def _verify_sentiment(self, sentiment, score, magnitude): from google.cloud.language.sentiment import Sentiment diff --git a/language/tests/unit/test_entity.py b/language/tests/unit/test_entity.py index d8ff94094831..45c7b5dec3b4 100644 --- a/language/tests/unit/test_entity.py +++ b/language/tests/unit/test_entity.py @@ -43,8 +43,9 @@ def test_constructor_defaults(self): mention_type=MentionType.PROPER, text=TextSpan(content='Italian', begin_offset=0), )] + sentiment = None entity = self._make_one(name, entity_type, metadata, - salience, mentions) + salience, mentions, sentiment) self.assertEqual(entity.name, name) self.assertEqual(entity.entity_type, entity_type) self.assertEqual(entity.metadata, metadata) @@ -55,6 +56,7 @@ def test_from_api_repr(self): from google.cloud.language.entity import EntityType from google.cloud.language.entity import Mention from google.cloud.language.entity import MentionType + from google.cloud.language.sentiment import Sentiment klass = self._get_target_class() name = 'Italy' @@ -101,6 +103,20 @@ def test_from_api_repr(self): for mention in entity.mentions: self.assertEqual(mention.mention_type, MentionType.PROPER) + # Assert that there is no sentiment. + self.assertIs(entity.sentiment, None) + + # Assert that there *is* sentiment if it is provided. + payload_with_sentiment = dict(payload, sentiment={ + 'magnitude': 42, + 'score': 0.15, + }) + entity_with_sentiment = klass.from_api_repr(payload_with_sentiment) + self.assertIsInstance(entity_with_sentiment.sentiment, Sentiment) + sentiment = entity_with_sentiment.sentiment + self.assertEqual(sentiment.magnitude, 42) + self.assertAlmostEqual(sentiment.score, 0.15) + class TestMention(unittest.TestCase): PAYLOAD = { diff --git a/pubsub/google/cloud/pubsub/iam.py b/pubsub/google/cloud/pubsub/iam.py index 53c0f36579f3..e92f2151dc05 100644 --- a/pubsub/google/cloud/pubsub/iam.py +++ b/pubsub/google/cloud/pubsub/iam.py @@ -17,16 +17,15 @@ https://cloud.google.com/pubsub/access_control#permissions """ -# Generic IAM roles +import warnings -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.""" +# pylint: disable=unused-import +from google.cloud.iam import OWNER_ROLE # noqa - backward compat +from google.cloud.iam import EDITOR_ROLE # noqa - backward compat +from google.cloud.iam import VIEWER_ROLE # noqa - backward compat +# pylint: enable=unused-import +from google.cloud.iam import Policy as _BasePolicy +from google.cloud.iam import _ASSIGNMENT_DEPRECATED_MSG # Pubsub-specific IAM roles @@ -94,166 +93,46 @@ """Permission: update subscriptions.""" -class Policy(object): - """Combined IAM Policy / Bindings. +class Policy(_BasePolicy): + """IAM Policy / Bindings. See: https://cloud.google.com/pubsub/docs/reference/rest/Shared.Types/Policy https://cloud.google.com/pubsub/docs/reference/rest/Shared.Types/Binding - - :type etag: str - :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: str - :param email: E-mail for this particular user. - - :rtype: str - :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: str - :param email: E-mail for this particular service account. - - :rtype: str - :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: str - :param email: An id or e-mail for this particular group. - - :rtype: str - :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: str - :param domain: The domain for this member. - - :rtype: str - :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: str - :returns: A member string representing all users. - """ - return 'allUsers' - - @staticmethod - def authenticated_users(): - """Factory method for a member representing all authenticated users. - - :rtype: str - :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 + _OWNER_ROLES = (OWNER_ROLE, PUBSUB_ADMIN_ROLE) + """Roles mapped onto our ``owners`` attribute.""" + + _EDITOR_ROLES = (EDITOR_ROLE, PUBSUB_EDITOR_ROLE) + """Roles mapped onto our ``editors`` attribute.""" + + _VIEWER_ROLES = (VIEWER_ROLE, PUBSUB_VIEWER_ROLE) + """Roles mapped onto our ``viewers`` attribute.""" + + @property + def publishers(self): + """Legacy access to owner role.""" + return frozenset(self._bindings.get(PUBSUB_PUBLISHER_ROLE, ())) + + @publishers.setter + def publishers(self, value): + """Update publishers.""" + warnings.warn( + _ASSIGNMENT_DEPRECATED_MSG.format( + 'publishers', PUBSUB_PUBLISHER_ROLE), + DeprecationWarning) + self._bindings[PUBSUB_PUBLISHER_ROLE] = list(value) + + @property + def subscribers(self): + """Legacy access to owner role.""" + return frozenset(self._bindings.get(PUBSUB_SUBSCRIBER_ROLE, ())) + + @subscribers.setter + def subscribers(self, value): + """Update subscribers.""" + warnings.warn( + _ASSIGNMENT_DEPRECATED_MSG.format( + 'subscribers', PUBSUB_SUBSCRIBER_ROLE), + DeprecationWarning) + self._bindings[PUBSUB_SUBSCRIBER_ROLE] = list(value) diff --git a/pubsub/tests/system.py b/pubsub/tests/system.py index ea88b0478a8c..090a38ef4b58 100644 --- a/pubsub/tests/system.py +++ b/pubsub/tests/system.py @@ -264,7 +264,9 @@ def test_topic_iam_policy(self): if topic.check_iam_permissions([PUBSUB_TOPICS_GET_IAM_POLICY]): policy = topic.get_iam_policy() - policy.viewers.add(policy.user('jjg@google.com')) + viewers = set(policy.viewers) + viewers.add(policy.user('jjg@google.com')) + policy.viewers = viewers new_policy = topic.set_iam_policy(policy) self.assertEqual(new_policy.viewers, policy.viewers) @@ -292,7 +294,9 @@ def test_subscription_iam_policy(self): if subscription.check_iam_permissions( [PUBSUB_SUBSCRIPTIONS_GET_IAM_POLICY]): policy = subscription.get_iam_policy() - policy.viewers.add(policy.user('jjg@google.com')) + viewers = set(policy.viewers) + viewers.add(policy.user('jjg@google.com')) + policy.viewers = viewers new_policy = subscription.set_iam_policy(policy) self.assertEqual(new_policy.viewers, policy.viewers) diff --git a/pubsub/tests/unit/test_iam.py b/pubsub/tests/unit/test_iam.py index 1d73277c270d..3bf4aaa922f0 100644 --- a/pubsub/tests/unit/test_iam.py +++ b/pubsub/tests/unit/test_iam.py @@ -30,10 +30,15 @@ def test_ctor_defaults(self): policy = self._make_one() self.assertIsNone(policy.etag) self.assertIsNone(policy.version) + self.assertIsInstance(policy.owners, frozenset) self.assertEqual(list(policy.owners), []) + self.assertIsInstance(policy.editors, frozenset) self.assertEqual(list(policy.editors), []) + self.assertIsInstance(policy.viewers, frozenset) self.assertEqual(list(policy.viewers), []) + self.assertIsInstance(policy.publishers, frozenset) self.assertEqual(list(policy.publishers), []) + self.assertIsInstance(policy.subscribers, frozenset) self.assertEqual(list(policy.subscribers), []) def test_ctor_explicit(self): @@ -48,145 +53,30 @@ def test_ctor_explicit(self): self.assertEqual(list(policy.publishers), []) self.assertEqual(list(policy.subscribers), []) - def test_user(self): - EMAIL = 'phred@example.com' - MEMBER = 'user:%s' % (EMAIL,) - policy = self._make_one() - self.assertEqual(policy.user(EMAIL), MEMBER) - - def test_service_account(self): - EMAIL = 'phred@example.com' - MEMBER = 'serviceAccount:%s' % (EMAIL,) - policy = self._make_one() - self.assertEqual(policy.service_account(EMAIL), MEMBER) - - def test_group(self): - EMAIL = 'phred@example.com' - MEMBER = 'group:%s' % (EMAIL,) - policy = self._make_one() - self.assertEqual(policy.group(EMAIL), MEMBER) - - def test_domain(self): - DOMAIN = 'example.com' - MEMBER = 'domain:%s' % (DOMAIN,) - policy = self._make_one() - self.assertEqual(policy.domain(DOMAIN), MEMBER) - - def test_all_users(self): - policy = self._make_one() - self.assertEqual(policy.all_users(), 'allUsers') - - def test_authenticated_users(self): - policy = self._make_one() - self.assertEqual(policy.authenticated_users(), 'allAuthenticatedUsers') - - def test_from_api_repr_only_etag(self): - RESOURCE = { - 'etag': 'ACAB', - } - klass = self._get_target_class() - policy = klass.from_api_repr(RESOURCE) - self.assertEqual(policy.etag, 'ACAB') - self.assertIsNone(policy.version) - self.assertEqual(list(policy.owners), []) - self.assertEqual(list(policy.editors), []) - self.assertEqual(list(policy.viewers), []) - - def test_from_api_repr_complete(self): + def test_publishers_setter(self): + import warnings from google.cloud.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._get_target_class() - 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._get_target_class() - with self.assertRaises(ValueError): - klass.from_api_repr(RESOURCE) - - def test_to_api_repr_defaults(self): policy = self._make_one() - self.assertEqual(policy.to_api_repr(), {}) + with warnings.catch_warnings(): + policy.publishers = [PUBLISHER] - def test_to_api_repr_only_etag(self): - policy = self._make_one('DEADBEEF') - self.assertEqual(policy.to_api_repr(), {'etag': 'DEADBEEF'}) + self.assertEqual(sorted(policy.publishers), [PUBLISHER]) + self.assertEqual( + dict(policy), {PUBSUB_PUBLISHER_ROLE: [PUBLISHER]}) - def test_to_api_repr_full(self): + def test_subscribers_setter(self): + import warnings from google.cloud.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._make_one('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) + policy = self._make_one() + with warnings.catch_warnings(): + policy.subscribers = [SUBSCRIBER] + + self.assertEqual(sorted(policy.subscribers), [SUBSCRIBER]) + self.assertEqual( + dict(policy), {PUBSUB_SUBSCRIBER_ROLE: [SUBSCRIBER]}) diff --git a/pubsub/tests/unit/test_subscription.py b/pubsub/tests/unit/test_subscription.py index 89b6bb8d9d94..d15665bccd24 100644 --- a/pubsub/tests/unit/test_subscription.py +++ b/pubsub/tests/unit/test_subscription.py @@ -606,11 +606,12 @@ def test_get_iam_policy_w_alternate_client(self): self.assertEqual(api._got_iam_policy, self.SUB_PATH) def test_set_iam_policy_w_bound_client(self): + import operator from google.cloud.pubsub.iam import Policy from google.cloud.pubsub.iam import ( - PUBSUB_ADMIN_ROLE, - PUBSUB_EDITOR_ROLE, - PUBSUB_VIEWER_ROLE, + OWNER_ROLE, + EDITOR_ROLE, + VIEWER_ROLE, PUBSUB_PUBLISHER_ROLE, PUBSUB_SUBSCRIBER_ROLE, ) @@ -627,9 +628,9 @@ def test_set_iam_policy_w_bound_client(self): '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': 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]}, ], @@ -643,14 +644,11 @@ def test_set_iam_policy_w_bound_client(self): topic = _Topic(self.TOPIC_NAME, client=client) subscription = self._make_one(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) + policy.owners = [OWNER1, OWNER2] + policy.editors = [EDITOR1, EDITOR2] + policy.viewers = [VIEWER1, VIEWER2] + policy.publishers = [PUBLISHER] + policy.subscribers = [SUBSCRIBER] new_policy = subscription.set_iam_policy(policy) @@ -661,7 +659,15 @@ def test_set_iam_policy_w_bound_client(self): 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)) + self.assertEqual(len(api._set_iam_policy), 2) + self.assertEqual(api._set_iam_policy[0], self.SUB_PATH) + resource = api._set_iam_policy[1] + self.assertEqual(resource['etag'], POLICY['etag']) + self.assertEqual(resource['version'], POLICY['version']) + key = operator.itemgetter('role') + self.assertEqual( + sorted(resource['bindings'], key=key), + sorted(POLICY['bindings'], key=key)) def test_set_iam_policy_w_alternate_client(self): from google.cloud.pubsub.iam import Policy diff --git a/pubsub/tests/unit/test_topic.py b/pubsub/tests/unit/test_topic.py index 01864fa24fdd..2c90432195c2 100644 --- a/pubsub/tests/unit/test_topic.py +++ b/pubsub/tests/unit/test_topic.py @@ -509,11 +509,12 @@ def test_get_iam_policy_w_alternate_client(self): self.assertEqual(api._got_iam_policy, self.TOPIC_PATH) def test_set_iam_policy_w_bound_client(self): + import operator from google.cloud.pubsub.iam import Policy from google.cloud.pubsub.iam import ( - PUBSUB_ADMIN_ROLE, - PUBSUB_EDITOR_ROLE, - PUBSUB_VIEWER_ROLE, + OWNER_ROLE, + EDITOR_ROLE, + VIEWER_ROLE, PUBSUB_PUBLISHER_ROLE, PUBSUB_SUBSCRIBER_ROLE, ) @@ -530,11 +531,11 @@ def test_set_iam_policy_w_bound_client(self): 'etag': 'DEADBEEF', 'version': 17, 'bindings': [ - {'role': PUBSUB_ADMIN_ROLE, + {'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]}, - {'role': PUBSUB_EDITOR_ROLE, + {'role': EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]}, - {'role': PUBSUB_VIEWER_ROLE, + {'role': VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]}, {'role': PUBSUB_PUBLISHER_ROLE, 'members': [PUBLISHER]}, @@ -551,14 +552,11 @@ def test_set_iam_policy_w_bound_client(self): api._set_iam_policy_response = RESPONSE topic = self._make_one(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) + policy.owners = [OWNER1, OWNER2] + policy.editors = [EDITOR1, EDITOR2] + policy.viewers = [VIEWER1, VIEWER2] + policy.publishers = [PUBLISHER] + policy.subscribers = [SUBSCRIBER] new_policy = topic.set_iam_policy(policy) @@ -569,7 +567,15 @@ def test_set_iam_policy_w_bound_client(self): 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)) + self.assertEqual(len(api._set_iam_policy), 2) + self.assertEqual(api._set_iam_policy[0], self.TOPIC_PATH) + resource = api._set_iam_policy[1] + self.assertEqual(resource['etag'], POLICY['etag']) + self.assertEqual(resource['version'], POLICY['version']) + key = operator.itemgetter('role') + self.assertEqual( + sorted(resource['bindings'], key=key), + sorted(POLICY['bindings'], key=key)) def test_set_iam_policy_w_alternate_client(self): from google.cloud.pubsub.iam import Policy diff --git a/speech/google/cloud/speech/encoding.py b/speech/google/cloud/speech/encoding.py index 9519ecfce4e4..529f8e45e889 100644 --- a/speech/google/cloud/speech/encoding.py +++ b/speech/google/cloud/speech/encoding.py @@ -36,3 +36,9 @@ class Encoding(object): AMR_WB = 'AMR_WB' """AMR_WB encoding type.""" + + OGG_OPUS = 'OGG_OPUS' + """OGG_OPUS encoding type.""" + + SPEEX_WITH_HEADER_BYTE = 'SPEEX_WITH_HEADER_BYTE' + """SPEEX_WITH_HEADER_BYTE encoding type.""" diff --git a/speech/google/cloud/speech/sample.py b/speech/google/cloud/speech/sample.py index 673e6ab4969c..0380fac12586 100644 --- a/speech/google/cloud/speech/sample.py +++ b/speech/google/cloud/speech/sample.py @@ -66,11 +66,6 @@ def __init__(self, content=None, source_uri=None, stream=None, self._content = content self._source_uri = source_uri self._stream = stream - - if (sample_rate_hertz is not None and - not 8000 <= sample_rate_hertz <= 48000): - raise ValueError('The value of sample_rate_hertz must be between ' - '8000 and 48000.') self._sample_rate_hertz = sample_rate_hertz if encoding is not None and getattr(Encoding, encoding, False): @@ -170,9 +165,6 @@ def long_running_recognize(self, language_code, max_alternatives=None, :rtype: :class:`~google.cloud.speech.operation.Operation` :returns: Operation for asynchronous request to Google Speech API. """ - if self.encoding is not Encoding.LINEAR16: - raise ValueError('Only LINEAR16 encoding is supported by ' - 'long-running speech requests.') api = self._client.speech_api return api.long_running_recognize( self, language_code, max_alternatives, profanity_filter, diff --git a/speech/setup.py b/speech/setup.py index f8357ccf8eab..2ec636c8d1c0 100644 --- a/speech/setup.py +++ b/speech/setup.py @@ -57,7 +57,7 @@ setup( name='google-cloud-speech', - version='0.25.0', + version='0.25.1', description='Python Client for Google Cloud Speech', long_description=README, namespace_packages=[ diff --git a/speech/tests/unit/test_sample.py b/speech/tests/unit/test_sample.py index 73b40dd0041c..313bf5843473 100644 --- a/speech/tests/unit/test_sample.py +++ b/speech/tests/unit/test_sample.py @@ -84,13 +84,6 @@ def test_bytes_converts_to_file_like_object(self): def test_sample_rates(self): from google.cloud.speech.encoding import Encoding - with self.assertRaises(ValueError): - self._make_one( - source_uri=self.AUDIO_SOURCE_URI, sample_rate_hertz=7999) - with self.assertRaises(ValueError): - self._make_one( - source_uri=self.AUDIO_SOURCE_URI, sample_rate_hertz=48001) - sample = self._make_one( encoding=Encoding.FLAC, sample_rate_hertz=self.SAMPLE_RATE, @@ -119,14 +112,3 @@ def test_encoding(self): source_uri=self.AUDIO_SOURCE_URI, ) self.assertEqual(sample.encoding, Encoding.FLAC) - - def test_async_linear16_only(self): - from google.cloud.speech.encoding import Encoding - - sample = self._make_one( - encoding=Encoding.FLAC, - sample_rate_hertz=self.SAMPLE_RATE, - source_uri=self.AUDIO_SOURCE_URI, - ) - with self.assertRaises(ValueError): - sample.long_running_recognize(language_code='en-US')