Skip to content

Commit fb6f7f8

Browse files
committed
Separate HTTP client.
1 parent badbd9d commit fb6f7f8

File tree

5 files changed

+170
-90
lines changed

5 files changed

+170
-90
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright 2016 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""HTTP Client for interacting with the Google Cloud Vision API."""
16+
17+
from google.cloud.vision.feature import Feature
18+
19+
20+
class _HTTPVisionAPI(object):
21+
"""Vision API for interacting with the JSON/HTTP version of Vision
22+
23+
:type client: :class:`~google.cloud.core.client.Client`
24+
:param client: Instance of ``Client`` object.
25+
"""
26+
27+
def __init__(self, client):
28+
self._client = client
29+
self._connection = client._connection
30+
31+
def annotate(self, image, features):
32+
"""Annotate an image to discover it's attributes.
33+
34+
:type image: :class:`~google.cloud.vision.image.Image`
35+
:param image: A instance of ``Image``.
36+
37+
:type features: list of :class:`~google.cloud.vision.feature.Feature`
38+
:param features: The type of detection that the Vision API should
39+
use to determine image attributes. Pricing is
40+
based on the number of Feature Types.
41+
42+
See: https://cloud.google.com/vision/docs/pricing
43+
:rtype: dict
44+
:returns: List of annotations.
45+
"""
46+
request = _make_request(image, features)
47+
48+
data = {'requests': [request]}
49+
api_response = self._connection.api_request(
50+
method='POST', path='/images:annotate', data=data)
51+
responses = api_response.get('responses')
52+
return responses[0]
53+
54+
55+
def _make_request(image, features):
56+
"""Prepare request object to send to Vision API.
57+
58+
:type image: :class:`~google.cloud.vision.image.Image`
59+
:param image: Instance of ``Image``.
60+
61+
:type features: list of :class:`~google.cloud.vision.feature.Feature`
62+
:param features: Either a list of ``Feature`` instances or a single
63+
instance of ``Feature``.
64+
65+
:rtype: dict
66+
:returns: Dictionary prepared to send to the Vision API.
67+
"""
68+
if isinstance(features, Feature):
69+
features = [features]
70+
71+
feature_check = (isinstance(feature, Feature) for feature in features)
72+
if not any(feature_check):
73+
raise TypeError('Feature or list of Feature classes are required.')
74+
75+
return {
76+
'image': image.as_dict(),
77+
'features': [feature.as_dict() for feature in features],
78+
}

vision/google/cloud/vision/client.py

Lines changed: 13 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -14,49 +14,10 @@
1414

1515
"""Client for interacting with the Google Cloud Vision API."""
1616

17-
1817
from google.cloud.client import JSONClient
1918
from google.cloud.vision.connection import Connection
20-
from google.cloud.vision.feature import Feature
2119
from google.cloud.vision.image import Image
22-
23-
24-
class VisionRequest(object):
25-
"""Request container with image and features information to annotate.
26-
27-
:type features: list of :class:`~gcoud.vision.feature.Feature`.
28-
:param features: The features that dictate which annotations to run.
29-
30-
:type image: bytes
31-
:param image: Either Google Cloud Storage URI or raw byte stream of image.
32-
"""
33-
def __init__(self, image, features):
34-
self._features = []
35-
self._image = image
36-
37-
if isinstance(features, list):
38-
self._features.extend(features)
39-
elif isinstance(features, Feature):
40-
self._features.append(features)
41-
else:
42-
raise TypeError('Feature or list of Feature classes are required.')
43-
44-
def as_dict(self):
45-
"""Dictionary representation of Image."""
46-
return {
47-
'image': self.image.as_dict(),
48-
'features': [feature.as_dict() for feature in self.features]
49-
}
50-
51-
@property
52-
def features(self):
53-
"""List of Feature objects."""
54-
return self._features
55-
56-
@property
57-
def image(self):
58-
"""Image object containing image content."""
59-
return self._image
20+
from google.cloud.vision._http import _HTTPVisionAPI
6021

6122

6223
class Client(JSONClient):
@@ -81,30 +42,7 @@ class Client(JSONClient):
8142
"""
8243

8344
_connection_class = Connection
84-
85-
def annotate(self, image, features):
86-
"""Annotate an image to discover it's attributes.
87-
88-
:type image: str
89-
:param image: A string which can be a URL, a Google Cloud Storage path,
90-
or a byte stream of the image.
91-
92-
:type features: list of :class:`~google.cloud.vision.feature.Feature`
93-
:param features: The type of detection that the Vision API should
94-
use to determine image attributes. Pricing is
95-
based on the number of Feature Types.
96-
97-
See: https://cloud.google.com/vision/docs/pricing
98-
:rtype: dict
99-
:returns: List of annotations.
100-
"""
101-
request = VisionRequest(image, features)
102-
103-
data = {'requests': [request.as_dict()]}
104-
response = self._connection.api_request(
105-
method='POST', path='/images:annotate', data=data)
106-
107-
return response['responses'][0]
45+
_vision_api_internal = None
10846

10947
def image(self, content=None, filename=None, source_uri=None):
11048
"""Get instance of Image using current client.
@@ -123,3 +61,14 @@ def image(self, content=None, filename=None, source_uri=None):
12361
"""
12462
return Image(client=self, content=content, filename=filename,
12563
source_uri=source_uri)
64+
65+
@property
66+
def _vision_api(self):
67+
"""Proxy method that handles which transport call Vision Annotate.
68+
69+
:rtype: :class:`~google.cloud.vision._rest._HTTPVisionAPI`
70+
:returns: Instance of ``_HTTPVisionAPI`` used to make requests.
71+
"""
72+
if self._vision_api_internal is None:
73+
self._vision_api_internal = _HTTPVisionAPI(self)
74+
return self._vision_api_internal

vision/google/cloud/vision/image.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def _detect_annotation(self, features):
109109
:class:`~google.cloud.vision.color.ImagePropertiesAnnotation`,
110110
:class:`~google.cloud.vision.sage.SafeSearchAnnotation`,
111111
"""
112-
results = self.client.annotate(self, features)
112+
results = self.client._vision_api.annotate(self, features)
113113
return Annotations.from_api_repr(results)
114114

115115
def detect(self, features):

vision/unit_tests/test__http.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Copyright 2016 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import base64
16+
import unittest
17+
18+
19+
IMAGE_CONTENT = b'/9j/4QNURXhpZgAASUkq'
20+
PROJECT = 'PROJECT'
21+
B64_IMAGE_CONTENT = base64.b64encode(IMAGE_CONTENT).decode('ascii')
22+
23+
24+
class TestVisionRequest(unittest.TestCase):
25+
@staticmethod
26+
def _get_target_function():
27+
from google.cloud.vision._http import _make_request
28+
return _make_request
29+
30+
def _call_fut(self, *args, **kw):
31+
return self._get_target_function()(*args, **kw)
32+
33+
def test_call_vision_request(self):
34+
from google.cloud.vision.feature import Feature
35+
from google.cloud.vision.feature import FeatureTypes
36+
from google.cloud.vision.image import Image
37+
38+
client = object()
39+
image = Image(client, content=IMAGE_CONTENT)
40+
feature = Feature(feature_type=FeatureTypes.FACE_DETECTION,
41+
max_results=3)
42+
request = self._call_fut(image, feature)
43+
self.assertEqual(request['image'].get('content'), B64_IMAGE_CONTENT)
44+
features = request['features']
45+
self.assertEqual(len(features), 1)
46+
feature = features[0]
47+
print(feature)
48+
self.assertEqual(feature['type'], FeatureTypes.FACE_DETECTION)
49+
self.assertEqual(feature['maxResults'], 3)
50+
51+
def test_call_vision_request_with_not_feature(self):
52+
from google.cloud.vision.image import Image
53+
54+
client = object()
55+
image = Image(client, content=IMAGE_CONTENT)
56+
with self.assertRaises(TypeError):
57+
self._call_fut(image, 'nonsensefeature')
58+
59+
def test_call_vision_request_with_list_bad_features(self):
60+
from google.cloud.vision.image import Image
61+
62+
client = object()
63+
image = Image(client, content=IMAGE_CONTENT)
64+
with self.assertRaises(TypeError):
65+
self._call_fut(image, ['nonsensefeature'])

vision/unit_tests/test_client.py

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ def test_ctor(self):
4343
client = self._make_one(project=PROJECT, credentials=creds)
4444
self.assertEqual(client.project, PROJECT)
4545

46+
def test_annotate_with_preset_api(self):
47+
credentials = _make_credentials()
48+
client = self._make_one(project=PROJECT, credentials=credentials)
49+
client._connection = _Connection()
50+
51+
api = mock.Mock()
52+
api.annotate.return_value = mock.sentinel.annotated
53+
54+
client._vision_api_internal = api
55+
client._vision_api.annotate()
56+
api.annotate.assert_called_once_with()
57+
4658
def test_face_annotation(self):
4759
from google.cloud.vision.feature import Feature, FeatureTypes
4860
from unit_tests._fixtures import FACE_DETECTION_RESPONSE
@@ -70,7 +82,7 @@ def test_face_annotation(self):
7082
features = [Feature(feature_type=FeatureTypes.FACE_DETECTION,
7183
max_results=3)]
7284
image = client.image(content=IMAGE_CONTENT)
73-
response = client.annotate(image, features)
85+
response = client._vision_api.annotate(image, features)
7486

7587
self.assertEqual(REQUEST,
7688
client._connection._requested[0]['data'])
@@ -433,30 +445,6 @@ def test_image_properties_no_results(self):
433445
self.assertEqual(len(image_properties), 0)
434446

435447

436-
class TestVisionRequest(unittest.TestCase):
437-
@staticmethod
438-
def _get_target_class():
439-
from google.cloud.vision.client import VisionRequest
440-
return VisionRequest
441-
442-
def _make_one(self, *args, **kw):
443-
return self._get_target_class()(*args, **kw)
444-
445-
def test_make_vision_request(self):
446-
from google.cloud.vision.feature import Feature, FeatureTypes
447-
448-
feature = Feature(feature_type=FeatureTypes.FACE_DETECTION,
449-
max_results=3)
450-
vision_request = self._make_one(IMAGE_CONTENT, feature)
451-
self.assertEqual(IMAGE_CONTENT, vision_request.image)
452-
self.assertEqual(FeatureTypes.FACE_DETECTION,
453-
vision_request.features[0].feature_type)
454-
455-
def test_make_vision_request_with_bad_feature(self):
456-
with self.assertRaises(TypeError):
457-
self._make_one(IMAGE_CONTENT, 'nonsensefeature')
458-
459-
460448
class _Connection(object):
461449

462450
def __init__(self, *responses):

0 commit comments

Comments
 (0)