Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions vision/google/cloud/vision/_http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Copyright 2016 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.

"""HTTP Client for interacting with the Google Cloud Vision API."""

from google.cloud.vision.feature import Feature


class _HTTPVisionAPI(object):
"""Vision API for interacting with the JSON/HTTP version of Vision

:type client: :class:`~google.cloud.core.client.Client`
:param client: Instance of ``Client`` object.
"""

def __init__(self, client):
self._client = client
self._connection = client._connection

def annotate(self, image, features):
"""Annotate an image to discover it's attributes.

:type image: :class:`~google.cloud.vision.image.Image`
:param image: A instance of ``Image``.

:type features: list of :class:`~google.cloud.vision.feature.Feature`
:param features: The type of detection that the Vision API should
use to determine image attributes. Pricing is
based on the number of Feature Types.

See: https://cloud.google.com/vision/docs/pricing
:rtype: dict
:returns: List of annotations.
"""
request = _make_request(image, features)

data = {'requests': [request]}
api_response = self._connection.api_request(
method='POST', path='/images:annotate', data=data)
responses = api_response.get('responses')
return responses[0]


def _make_request(image, features):
"""Prepare request object to send to Vision API.

:type image: :class:`~google.cloud.vision.image.Image`
:param image: Instance of ``Image``.

:type features: list of :class:`~google.cloud.vision.feature.Feature`
:param features: Either a list of ``Feature`` instances or a single
instance of ``Feature``.

:rtype: dict
:returns: Dictionary prepared to send to the Vision API.
"""
if isinstance(features, Feature):
features = [features]

feature_check = (isinstance(feature, Feature) for feature in features)
if not any(feature_check):
raise TypeError('Feature or list of Feature classes are required.')

return {
'image': image.as_dict(),
'features': [feature.as_dict() for feature in features],
}
77 changes: 13 additions & 64 deletions vision/google/cloud/vision/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,49 +14,10 @@

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


from google.cloud.client import JSONClient
from google.cloud.vision.connection import Connection
from google.cloud.vision.feature import Feature
from google.cloud.vision.image import Image


class VisionRequest(object):
"""Request container with image and features information to annotate.

:type features: list of :class:`~gcoud.vision.feature.Feature`.
:param features: The features that dictate which annotations to run.

:type image: bytes
:param image: Either Google Cloud Storage URI or raw byte stream of image.
"""
def __init__(self, image, features):
self._features = []
self._image = image

if isinstance(features, list):
self._features.extend(features)
elif isinstance(features, Feature):
self._features.append(features)
else:
raise TypeError('Feature or list of Feature classes are required.')

def as_dict(self):
"""Dictionary representation of Image."""
return {
'image': self.image.as_dict(),
'features': [feature.as_dict() for feature in self.features]
}

@property
def features(self):
"""List of Feature objects."""
return self._features

@property
def image(self):
"""Image object containing image content."""
return self._image
from google.cloud.vision._http import _HTTPVisionAPI


class Client(JSONClient):
Expand All @@ -80,37 +41,14 @@ class Client(JSONClient):
``http`` object is created that is bound to the
``credentials`` for the current object.
"""
_vision_api_internal = None

def __init__(self, project=None, credentials=None, http=None):
super(Client, self).__init__(
project=project, credentials=credentials, http=http)
self._connection = Connection(
credentials=self._credentials, http=self._http)

def annotate(self, image, features):
"""Annotate an image to discover it's attributes.

:type image: str
:param image: A string which can be a URL, a Google Cloud Storage path,
or a byte stream of the image.

:type features: list of :class:`~google.cloud.vision.feature.Feature`
:param features: The type of detection that the Vision API should
use to determine image attributes. Pricing is
based on the number of Feature Types.

See: https://cloud.google.com/vision/docs/pricing
:rtype: dict
:returns: List of annotations.
"""
request = VisionRequest(image, features)

data = {'requests': [request.as_dict()]}
response = self._connection.api_request(
method='POST', path='/images:annotate', data=data)

return response['responses'][0]

def image(self, content=None, filename=None, source_uri=None):
"""Get instance of Image using current client.

Expand All @@ -128,3 +66,14 @@ def image(self, content=None, filename=None, source_uri=None):
"""
return Image(client=self, content=content, filename=filename,
source_uri=source_uri)

@property
def _vision_api(self):
"""Proxy method that handles which transport call Vision Annotate.

:rtype: :class:`~google.cloud.vision._rest._HTTPVisionAPI`
:returns: Instance of ``_HTTPVisionAPI`` used to make requests.
"""
if self._vision_api_internal is None:
self._vision_api_internal = _HTTPVisionAPI(self)
return self._vision_api_internal
2 changes: 1 addition & 1 deletion vision/google/cloud/vision/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def _detect_annotation(self, features):
:class:`~google.cloud.vision.color.ImagePropertiesAnnotation`,
:class:`~google.cloud.vision.sage.SafeSearchAnnotation`,
"""
results = self.client.annotate(self, features)
results = self.client._vision_api.annotate(self, features)
return Annotations.from_api_repr(results)

def detect(self, features):
Expand Down
65 changes: 65 additions & 0 deletions vision/unit_tests/test__http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Copyright 2016 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 base64
import unittest


IMAGE_CONTENT = b'/9j/4QNURXhpZgAASUkq'
PROJECT = 'PROJECT'
B64_IMAGE_CONTENT = base64.b64encode(IMAGE_CONTENT).decode('ascii')


class TestVisionRequest(unittest.TestCase):
@staticmethod
def _get_target_function():
from google.cloud.vision._http import _make_request
return _make_request

def _call_fut(self, *args, **kw):
return self._get_target_function()(*args, **kw)

def test_call_vision_request(self):
from google.cloud.vision.feature import Feature
from google.cloud.vision.feature import FeatureTypes
from google.cloud.vision.image import Image

client = object()
image = Image(client, content=IMAGE_CONTENT)
feature = Feature(feature_type=FeatureTypes.FACE_DETECTION,
max_results=3)
request = self._call_fut(image, feature)
self.assertEqual(request['image'].get('content'), B64_IMAGE_CONTENT)
features = request['features']
self.assertEqual(len(features), 1)
feature = features[0]
print(feature)
self.assertEqual(feature['type'], FeatureTypes.FACE_DETECTION)
self.assertEqual(feature['maxResults'], 3)

def test_call_vision_request_with_not_feature(self):
from google.cloud.vision.image import Image

client = object()
image = Image(client, content=IMAGE_CONTENT)
with self.assertRaises(TypeError):
self._call_fut(image, 'nonsensefeature')

def test_call_vision_request_with_list_bad_features(self):
from google.cloud.vision.image import Image

client = object()
image = Image(client, content=IMAGE_CONTENT)
with self.assertRaises(TypeError):
self._call_fut(image, ['nonsensefeature'])
38 changes: 13 additions & 25 deletions vision/unit_tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ def test_ctor(self):
client = self._make_one(project=PROJECT, credentials=creds)
self.assertEqual(client.project, PROJECT)

def test_annotate_with_preset_api(self):
credentials = _make_credentials()
client = self._make_one(project=PROJECT, credentials=credentials)
client._connection = _Connection()

api = mock.Mock()
api.annotate.return_value = mock.sentinel.annotated

client._vision_api_internal = api
client._vision_api.annotate()
api.annotate.assert_called_once_with()

def test_face_annotation(self):
from google.cloud.vision.feature import Feature, FeatureTypes
from unit_tests._fixtures import FACE_DETECTION_RESPONSE
Expand Down Expand Up @@ -70,7 +82,7 @@ def test_face_annotation(self):
features = [Feature(feature_type=FeatureTypes.FACE_DETECTION,
max_results=3)]
image = client.image(content=IMAGE_CONTENT)
response = client.annotate(image, features)
response = client._vision_api.annotate(image, features)

self.assertEqual(REQUEST,
client._connection._requested[0]['data'])
Expand Down Expand Up @@ -433,30 +445,6 @@ def test_image_properties_no_results(self):
self.assertEqual(len(image_properties), 0)


class TestVisionRequest(unittest.TestCase):
@staticmethod
def _get_target_class():
from google.cloud.vision.client import VisionRequest
return VisionRequest

def _make_one(self, *args, **kw):
return self._get_target_class()(*args, **kw)

def test_make_vision_request(self):
from google.cloud.vision.feature import Feature, FeatureTypes

feature = Feature(feature_type=FeatureTypes.FACE_DETECTION,
max_results=3)
vision_request = self._make_one(IMAGE_CONTENT, feature)
self.assertEqual(IMAGE_CONTENT, vision_request.image)
self.assertEqual(FeatureTypes.FACE_DETECTION,
vision_request.features[0].feature_type)

def test_make_vision_request_with_bad_feature(self):
with self.assertRaises(TypeError):
self._make_one(IMAGE_CONTENT, 'nonsensefeature')


class _Connection(object):

def __init__(self, *responses):
Expand Down