Skip to content

Commit e55b036

Browse files
committed
- Updated bytes usage
- Add more docstrings and corrections - Add gcs_uri support - Remove VisionEncoder
1 parent 976e8ed commit e55b036

9 files changed

Lines changed: 165 additions & 86 deletions

File tree

gcloud/vision/client.py

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,47 +14,48 @@
1414

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

17-
import json
18-
from json import JSONEncoder
1917

2018
from gcloud.client import JSONClient
2119
from gcloud.vision.connection import Connection
2220
from gcloud.vision.feature import Feature
2321
from gcloud.vision.image import Image
2422

2523

26-
class VisionJSONEncoder(JSONEncoder):
27-
def default(self, o):
28-
if 'as_dict' in dir(o):
29-
return o.as_dict()
30-
else:
31-
return o.__dict__
24+
class VisionRequest(object):
25+
"""Request container with image and features information to annotate.
3226
27+
:type features: list of :class:`gcoud.vision.feature.Feature`.
28+
:param features: The features that dictate which annotations to run.
3329
34-
class VisionRequest(object):
35-
def __init__(self, image, feature):
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):
3634
self._features = []
3735
self._image = image
3836

39-
if isinstance(feature, list):
40-
self._features.extend(feature)
41-
elif isinstance(feature, Feature):
42-
self._features.append(feature)
37+
if isinstance(features, list):
38+
self._features.extend(features)
39+
elif isinstance(features, Feature):
40+
self._features.append(features)
4341
else:
4442
raise TypeError('Feature or list of Feature classes are required.')
4543

4644
def as_dict(self):
45+
"""Dictionary representation of Image."""
4746
return {
48-
'image': self.image,
49-
'features': self.features
47+
'image': self.image.as_dict(),
48+
'features': [feature.as_dict() for feature in self.features]
5049
}
5150

5251
@property
5352
def features(self):
53+
"""List of Feature objects."""
5454
return self._features
5555

5656
@property
5757
def image(self):
58+
"""Image object containing image content."""
5859
return self._image
5960

6061

@@ -81,27 +82,28 @@ class Client(JSONClient):
8182

8283
_connection_class = Connection
8384

84-
def annotate(self, image, features=[]):
85+
def annotate(self, image, features):
8586
"""Annotate an image to discover it's attributes.
8687
8788
:type image: str
8889
:param image: A string which can be a URL, a Google Cloud Storage path,
8990
or a byte stream of the image.
9091
91-
:type features: list
92+
:type features: list of :class:`gcloud.vision.feature.Feature`
9293
:param features: The type of detection that the Vision API should
9394
use to determine image attributes. Pricing is
9495
based on the number of Feature Types.
9596
9697
See: https://cloud.google.com/vision/docs/pricing
98+
:rtype: dict
99+
:returns: List of annotations.
97100
"""
98-
data = {'requests': []}
99-
100101
img = Image(image, self)
101-
data['requests'].append(VisionRequest(img, features))
102+
request = VisionRequest(img, features)
102103

103-
data = json.dumps(data, cls=VisionJSONEncoder)
104+
data = {'requests': [request.as_dict()]}
104105
response = self.connection.api_request(method='POST',
105106
path='/images:annotate',
106107
data=data)
108+
107109
return response['responses'][0]

gcloud/vision/feature.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
"""Feature represenging various types of annotating."""
16+
1517

1618
class FeatureTypes(object):
1719
"""Feature Types to indication which annotations to perform.
@@ -31,7 +33,8 @@ class Feature(object):
3133
"""Feature object specifying the annotation type and maximum results.
3234
3335
:type feature_type: str
34-
:param feature_type: String representation of feature type.
36+
:param feature_type: String representation of
37+
:class:`gcloud.vision.feature.FeatureType`.
3538
3639
:type max_results: int
3740
:param max_results: Number of results to return for the specified
@@ -41,7 +44,10 @@ class Feature(object):
4144
https://cloud.google.com/vision/reference/rest/v1/images/annotate#Feature
4245
"""
4346
def __init__(self, feature_type, max_results=1):
44-
self._feature_type = getattr(FeatureTypes, feature_type)
47+
try:
48+
self._feature_type = getattr(FeatureTypes, feature_type)
49+
except AttributeError:
50+
raise AttributeError('Feature type passed in cannot be found.')
4551
self._max_results = int(max_results)
4652

4753
def as_dict(self):

gcloud/vision/image.py

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,41 +12,55 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
"""Image represented by either a URI or byte stream."""
16+
17+
1518
from base64 import b64encode
1619

20+
from gcloud._helpers import _to_bytes
21+
from gcloud._helpers import _bytes_to_unicode
22+
1723

1824
class Image(object):
19-
"""Image representation containing either the source or content of the
20-
image that will get annotated.
25+
"""Image representation containing information to be annotate.
2126
2227
:type image_source: str
23-
:param image_source: A string which can be a URL, a Google Cloud Storage
24-
path, or a byte stream/string of the image.
28+
:param image_source: A string which can a Google Cloud Storage URI, or
29+
a byte stream of the image.
2530
26-
See:
27-
https://cloud.google.com/vision/reference/rest/v1/images/annotate#Image
31+
:type client: :class:`Client`
32+
:param client: Instance of Vision client.
2833
"""
2934

3035
def __init__(self, image_source, client):
31-
"""Initialize Image class
32-
33-
:type image_source: str
34-
:param image_source: String or Bytes
35-
36-
:type client: :class:`Client`
37-
:param client: Instance of Vision client.
38-
"""
39-
4036
self.client = client
41-
self._content = b64encode(image_source)
37+
self._content = None
38+
self._source = None
39+
40+
if _bytes_to_unicode(image_source).startswith('gs://'):
41+
self._source = image_source
42+
else:
43+
self._content = b64encode(_to_bytes(image_source))
4244

4345
def as_dict(self):
4446
"""Generate dictionary structure for request"""
45-
return {
46-
'content': self.content
47-
}
47+
if self.content:
48+
return {
49+
'content': self.content
50+
}
51+
else:
52+
return {
53+
'source': {
54+
'gcs_image_uri': self.source
55+
}
56+
}
4857

4958
@property
5059
def content(self):
5160
"""Base64 encoded image content"""
5261
return self._content
62+
63+
@property
64+
def source(self):
65+
"""Google Cloud Storage URI"""
66+
return self._source

gcloud/vision/test_client.py

Lines changed: 14 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import unittest
15+
1616
import base64
17-
import json
17+
import unittest
18+
19+
from gcloud._helpers import _to_bytes
1820

1921

2022
class TestClient(unittest.TestCase):
2123
PROJECT = 'PROJECT'
2224
IMAGE_SOURCE = 'gs://some/image.jpg'
23-
IMAGE_CONTENT = '/9j/4QNURXhpZgAASUkq'
25+
IMAGE_CONTENT = _to_bytes('/9j/4QNURXhpZgAASUkq')
2426
B64_IMAGE_CONTENT = base64.b64encode(IMAGE_CONTENT)
2527

2628
def _getTargetClass(self):
@@ -38,7 +40,7 @@ def test_ctor(self):
3840

3941
def test_face_annotation(self):
4042

41-
from gcloud.vision.fixtures import FACE_DETECTION_RESPONSE as RETURNED
43+
from gcloud.vision._fixtures import FACE_DETECTION_RESPONSE as RETURNED
4244

4345
REQUEST = {
4446
"requests": [
@@ -66,13 +68,15 @@ def test_face_annotation(self):
6668

6769
response = client.annotate(self.IMAGE_CONTENT, features)
6870

69-
self.assertEqual(json.dumps(REQUEST),
71+
self.assertEqual(REQUEST,
7072
client.connection._requested[0]['data'])
7173

7274
self.assertTrue('faceAnnotations' in response)
7375

7476

7577
class TestVisionRequest(unittest.TestCase):
78+
_IMAGE_CONTENT = _to_bytes('/9j/4QNURXhpZgAASUkq')
79+
7680
def _getTargetClass(self):
7781
from gcloud.vision.client import VisionRequest
7882
return VisionRequest
@@ -81,49 +85,23 @@ def _makeOne(self, *args, **kw):
8185
return self._getTargetClass()(*args, **kw)
8286

8387
def test_make_vision_request(self):
84-
IMAGE_CONTENT = '/9j/4QNURXhpZgAASUkq'
8588
from gcloud.vision.feature import Feature, FeatureTypes
8689
feature = Feature(feature_type=FeatureTypes.FACE_DETECTION,
8790
max_results=3)
88-
vision_request = self._makeOne(IMAGE_CONTENT, feature)
91+
vision_request = self._makeOne(self._IMAGE_CONTENT, feature)
8992

90-
self.assertEqual(IMAGE_CONTENT, vision_request.image)
93+
self.assertEqual(self._IMAGE_CONTENT, vision_request.image)
9194
self.assertEqual(FeatureTypes.FACE_DETECTION,
9295
vision_request.features[0].feature_type)
9396

94-
vision_request = self._makeOne(IMAGE_CONTENT, [feature])
97+
vision_request = self._makeOne(self._IMAGE_CONTENT, [feature])
9598

96-
self.assertEqual(IMAGE_CONTENT, vision_request.image)
99+
self.assertEqual(self._IMAGE_CONTENT, vision_request.image)
97100
self.assertEqual(FeatureTypes.FACE_DETECTION,
98101
vision_request.features[0].feature_type)
99102

100103
with self.assertRaises(TypeError):
101-
self._makeOne(IMAGE_CONTENT, 'nonsensefeature')
102-
103-
104-
class VisionJSONEncoder(unittest.TestCase):
105-
def _getTargetClass(self):
106-
from gcloud.vision.client import VisionJSONEncoder
107-
return VisionJSONEncoder
108-
109-
def _makeOne(self, *args, **kw):
110-
return self._getTargetClass()(*args, **kw)
111-
112-
def test_vision_json_encoder(self):
113-
class CustomJSON(object):
114-
def as_dict(self):
115-
return {'custom': 'encoder'}
116-
custom_json_class = CustomJSON()
117-
118-
encoder = self._makeOne()
119-
self.assertEqual({'custom': 'encoder'},
120-
encoder.default(custom_json_class))
121-
122-
class StandardJSON(object):
123-
pass
124-
125-
standard_json_class = StandardJSON()
126-
self.assertEqual({}, encoder.default(standard_json_class))
104+
self._makeOne(self._IMAGE_CONTENT, 'nonsensefeature')
127105

128106

129107
class _Credentials(object):

gcloud/vision/test_feature.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,35 @@
1414

1515
import unittest
1616

17-
from gcloud.vision.feature import Feature
18-
from gcloud.vision.feature import FeatureTypes
19-
2017

2118
class TestFeature(unittest.TestCase):
19+
def _getTargetClass(self):
20+
from gcloud.vision.feature import Feature
21+
return Feature
22+
23+
def _makeOne(self, *args, **kw):
24+
return self._getTargetClass()(*args, **kw)
2225

2326
def test_construct_feature(self):
24-
feature = Feature(FeatureTypes.LABEL_DETECTION)
27+
from gcloud.vision.feature import FeatureTypes
28+
feature = self._makeOne(FeatureTypes.LABEL_DETECTION)
2529
self.assertEqual(1, feature.max_results)
2630
self.assertEqual('LABEL_DETECTION', feature.feature_type)
2731

28-
feature = Feature(FeatureTypes.FACE_DETECTION, 3)
32+
feature = self._makeOne(FeatureTypes.FACE_DETECTION, 3)
2933
self.assertEqual(3, feature.max_results)
3034
self.assertEqual('FACE_DETECTION', feature.feature_type)
3135

3236
def test_feature_as_dict(self):
33-
feature = Feature(FeatureTypes.FACE_DETECTION, max_results=5)
37+
from gcloud.vision.feature import FeatureTypes
38+
feature = self._makeOne(FeatureTypes.FACE_DETECTION, max_results=5)
3439
EXPECTED = {
3540
'type': 'FACE_DETECTION',
3641
'maxResults': 5
3742
}
3843
self.assertEqual(EXPECTED, feature.as_dict())
44+
45+
def test_bad_feature_type(self):
46+
with self.assertRaises(AttributeError):
47+
self._makeOne('something_not_feature_type',
48+
max_results=5)

0 commit comments

Comments
 (0)