Skip to content

Commit 46e417e

Browse files
Language: Expose an api_version parameter on the Client. (googleapis#3283)
Sending api_version='v1beta2' causes the `analyze_entity_sentiment` method to be available and work.
1 parent 0d61119 commit 46e417e

File tree

6 files changed

+164
-9
lines changed

6 files changed

+164
-9
lines changed

language/google/cloud/language/_http.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,14 @@ class Connection(_http.JSONConnection):
4141
_EXTRA_HEADERS = {
4242
_http.CLIENT_INFO_HEADER: _CLIENT_INFO,
4343
}
44+
45+
46+
class V1Beta2Connection(Connection):
47+
"""A connection to Google Cloud Natural Language JSON 1.1 REST API.
48+
49+
:type client: :class:`~google.cloud.language.client.Client`
50+
:param client: The client that owns the current connection.
51+
"""
52+
53+
API_VERSION = 'v1beta2'
54+
"""The version of the API, used in building the API call's URL."""

language/google/cloud/language/client.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from google.cloud import client as client_module
2121

2222
from google.cloud.language._http import Connection
23+
from google.cloud.language._http import V1Beta2Connection
2324
from google.cloud.language.document import Document
2425

2526

@@ -45,10 +46,16 @@ class Client(client_module.Client):
4546
SCOPE = ('https://www.googleapis.com/auth/cloud-platform',)
4647
"""The scopes required for authenticating as an API consumer."""
4748

48-
def __init__(self, credentials=None, _http=None):
49+
_CONNECTION_CLASSES = {
50+
'v1': Connection,
51+
'v1beta2': V1Beta2Connection,
52+
}
53+
54+
def __init__(self, credentials=None, api_version='v1', _http=None):
4955
super(Client, self).__init__(
5056
credentials=credentials, _http=_http)
51-
self._connection = Connection(self)
57+
ConnectionClass = self._CONNECTION_CLASSES[api_version]
58+
self._connection = ConnectionClass(self)
5259

5360
def document_from_text(self, content, **kwargs):
5461
"""Create a plain text document bound to this client.

language/google/cloud/language/document.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,32 @@ def analyze_entities(self):
188188
method='POST', path='analyzeEntities', data=data)
189189
return api_responses.EntityResponse.from_api_repr(api_response)
190190

191+
def analyze_entity_sentiment(self):
192+
"""Analyze the entity sentiment.
193+
194+
Finds entities, similar to `AnalyzeEntities` in the text and
195+
analyzes sentiment associated with each entity and its mentions.
196+
197+
:rtype: :class:`~language.entity.EntitySentimentResponse`
198+
:returns: A representation of the entity sentiment response.
199+
"""
200+
# Sanity check: Not available on v1.
201+
if self.client._connection.API_VERSION == 'v1':
202+
raise NotImplementedError(
203+
'The `analyze_entity_sentiment` method is only available '
204+
'on the Natural Language 1.1 beta. Use version="v1beta2" '
205+
'as a keyword argument to the constructor.',
206+
)
207+
208+
# Perform the API request.
209+
data = {
210+
'document': self._to_dict(),
211+
'encodingType': self.encoding,
212+
}
213+
api_response = self.client._connection.api_request(
214+
method='POST', path='analyzeEntitySentiment', data=data)
215+
return api_responses.EntityResponse.from_api_repr(api_response)
216+
191217
def analyze_sentiment(self):
192218
"""Analyze the sentiment in the current document.
193219

language/google/cloud/language/entity.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
An entity is used to describe a proper name extracted from text.
1818
"""
1919

20+
from google.cloud.language.sentiment import Sentiment
21+
2022

2123
class EntityType(object):
2224
"""List of possible entity types."""
@@ -152,14 +154,20 @@ class Entity(object):
152154
153155
:type mentions: list
154156
:param mentions: List of strings that mention the entity.
157+
158+
:type sentiment: :class:`~.language.sentiment.Sentiment`
159+
:params sentiment: The sentiment; sent only on `analyze_entity_sentiment`
160+
calls.
155161
"""
156162

157-
def __init__(self, name, entity_type, metadata, salience, mentions):
163+
def __init__(self, name, entity_type, metadata, salience, mentions,
164+
sentiment):
158165
self.name = name
159166
self.entity_type = entity_type
160167
self.metadata = metadata
161168
self.salience = salience
162169
self.mentions = mentions
170+
self.sentiment = sentiment
163171

164172
@classmethod
165173
def from_api_repr(cls, payload):
@@ -176,4 +184,7 @@ def from_api_repr(cls, payload):
176184
metadata = payload['metadata']
177185
salience = payload['salience']
178186
mentions = [Mention.from_api_repr(val) for val in payload['mentions']]
179-
return cls(name, entity_type, metadata, salience, mentions)
187+
sentiment = None
188+
if payload.get('sentiment'):
189+
sentiment = Sentiment.from_api_repr(payload['sentiment'])
190+
return cls(name, entity_type, metadata, salience, mentions, sentiment)

language/tests/unit/test_document.py

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,12 @@ def _get_entities(include_entities):
9696
return entities
9797

9898

99-
def make_mock_client(response):
99+
def make_mock_client(response, api_version='v1'):
100100
import mock
101-
from google.cloud.language._http import Connection
102101
from google.cloud.language.client import Client
103102

104-
connection = mock.Mock(spec=Connection)
103+
connection = mock.Mock(spec=Client._CONNECTION_CLASSES[api_version])
104+
connection.API_VERSION = api_version
105105
connection.api_request.return_value = response
106106
return mock.Mock(_connection=connection, spec=Client)
107107

@@ -205,7 +205,8 @@ def test__to_dict_with_no_content(self):
205205
'type': klass.PLAIN_TEXT,
206206
})
207207

208-
def _verify_entity(self, entity, name, entity_type, wiki_url, salience):
208+
def _verify_entity(self, entity, name, entity_type, wiki_url, salience,
209+
sentiment=None):
209210
from google.cloud.language.entity import Entity
210211

211212
self.assertIsInstance(entity, Entity)
@@ -218,6 +219,10 @@ def _verify_entity(self, entity, name, entity_type, wiki_url, salience):
218219
self.assertEqual(entity.salience, salience)
219220
self.assertEqual(len(entity.mentions), 1)
220221
self.assertEqual(entity.mentions[0].text.content, name)
222+
if sentiment:
223+
self.assertEqual(entity.sentiment.score, sentiment.score)
224+
self.assertAlmostEqual(entity.sentiment.magnitude,
225+
sentiment.magnitude)
221226

222227
@staticmethod
223228
def _expected_data(content, encoding_type=None,
@@ -308,6 +313,85 @@ def test_analyze_entities(self):
308313
client._connection.api_request.assert_called_once_with(
309314
path='analyzeEntities', method='POST', data=expected)
310315

316+
def test_analyze_entity_sentiment_v1_error(self):
317+
client = make_mock_client({})
318+
document = self._make_one(client, 'foo bar baz')
319+
with self.assertRaises(NotImplementedError):
320+
entity_response = document.analyze_entity_sentiment()
321+
322+
def test_analyze_entity_sentiment(self):
323+
from google.cloud.language.document import Encoding
324+
from google.cloud.language.entity import EntityType
325+
from google.cloud.language.sentiment import Sentiment
326+
327+
name1 = 'R-O-C-K'
328+
name2 = 'USA'
329+
content = name1 + ' in the ' + name2
330+
wiki2 = 'http://en.wikipedia.org/wiki/United_States'
331+
salience1 = 0.91391456
332+
salience2 = 0.086085409
333+
sentiment = Sentiment(score=0.15, magnitude=42)
334+
response = {
335+
'entities': [
336+
{
337+
'name': name1,
338+
'type': EntityType.OTHER,
339+
'metadata': {},
340+
'salience': salience1,
341+
'mentions': [
342+
{
343+
'text': {
344+
'content': name1,
345+
'beginOffset': -1
346+
},
347+
'type': 'TYPE_UNKNOWN',
348+
}
349+
],
350+
'sentiment': {
351+
'score': 0.15,
352+
'magnitude': 42,
353+
}
354+
},
355+
{
356+
'name': name2,
357+
'type': EntityType.LOCATION,
358+
'metadata': {'wikipedia_url': wiki2},
359+
'salience': salience2,
360+
'mentions': [
361+
{
362+
'text': {
363+
'content': name2,
364+
'beginOffset': -1,
365+
},
366+
'type': 'PROPER',
367+
},
368+
],
369+
'sentiment': {
370+
'score': 0.15,
371+
'magnitude': 42,
372+
}
373+
},
374+
],
375+
'language': 'en-US',
376+
}
377+
client = make_mock_client(response, api_version='v1beta2')
378+
document = self._make_one(client, content)
379+
380+
entity_response = document.analyze_entity_sentiment()
381+
self.assertEqual(len(entity_response.entities), 2)
382+
entity1 = entity_response.entities[0]
383+
self._verify_entity(entity1, name1, EntityType.OTHER,
384+
None, salience1, sentiment)
385+
entity2 = entity_response.entities[1]
386+
self._verify_entity(entity2, name2, EntityType.LOCATION,
387+
wiki2, salience2, sentiment)
388+
389+
# Verify the request.
390+
expected = self._expected_data(
391+
content, encoding_type=Encoding.get_default())
392+
client._connection.api_request.assert_called_once_with(
393+
path='analyzeEntitySentiment', method='POST', data=expected)
394+
311395
def _verify_sentiment(self, sentiment, score, magnitude):
312396
from google.cloud.language.sentiment import Sentiment
313397

language/tests/unit/test_entity.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ def test_constructor_defaults(self):
4343
mention_type=MentionType.PROPER,
4444
text=TextSpan(content='Italian', begin_offset=0),
4545
)]
46+
sentiment = None
4647
entity = self._make_one(name, entity_type, metadata,
47-
salience, mentions)
48+
salience, mentions, sentiment)
4849
self.assertEqual(entity.name, name)
4950
self.assertEqual(entity.entity_type, entity_type)
5051
self.assertEqual(entity.metadata, metadata)
@@ -55,6 +56,7 @@ def test_from_api_repr(self):
5556
from google.cloud.language.entity import EntityType
5657
from google.cloud.language.entity import Mention
5758
from google.cloud.language.entity import MentionType
59+
from google.cloud.language.sentiment import Sentiment
5860

5961
klass = self._get_target_class()
6062
name = 'Italy'
@@ -101,6 +103,20 @@ def test_from_api_repr(self):
101103
for mention in entity.mentions:
102104
self.assertEqual(mention.mention_type, MentionType.PROPER)
103105

106+
# Assert that there is no sentiment.
107+
self.assertIs(entity.sentiment, None)
108+
109+
# Assert that there *is* sentiment if it is provided.
110+
payload_with_sentiment = dict(payload, sentiment={
111+
'magnitude': 42,
112+
'score': 0.15,
113+
})
114+
entity_with_sentiment = klass.from_api_repr(payload_with_sentiment)
115+
self.assertIsInstance(entity_with_sentiment.sentiment, Sentiment)
116+
sentiment = entity_with_sentiment.sentiment
117+
self.assertEqual(sentiment.magnitude, 42)
118+
self.assertAlmostEqual(sentiment.score, 0.15)
119+
104120

105121
class TestMention(unittest.TestCase):
106122
PAYLOAD = {

0 commit comments

Comments
 (0)