diff --git a/gcloud/search/document.py b/gcloud/search/document.py
new file mode 100644
index 000000000000..5e7164c7995b
--- /dev/null
+++ b/gcloud/search/document.py
@@ -0,0 +1,353 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+"""Define API Document."""
+
+import datetime
+
+import six
+
+from gcloud._helpers import UTC
+from gcloud._helpers import _RFC3339_MICROS
+from gcloud.exceptions import NotFound
+
+
+class StringValue(object):
+ """StringValues hold individual text values for a given field
+
+ See:
+ https://cloud.google.com/search/reference/rest/google/cloudsearch/v1/FieldValue
+
+ :type string_value: string
+ :param string_value: the actual value.
+
+ :type string_format: string
+ :param string_format: how the value should be indexed: one of
+ 'ATOM', 'TEXT', 'HTML' (leave as ``None`` to
+ use the server-supplied default).
+
+ :type language: string
+ :param language: Human language of the text. Should be an ISO 639-1
+ language code.
+ """
+
+ value_type = 'string'
+
+ def __init__(self, string_value, string_format=None, language=None):
+ self.string_value = string_value
+ self.string_format = string_format
+ self.language = language
+
+
+class NumberValue(object):
+ """NumberValues hold individual numeric values for a given field
+
+ See:
+ https://cloud.google.com/search/reference/rest/google/cloudsearch/v1/FieldValue
+
+ :type number_value: integer, float (long on Python2)
+ :param number_value: the actual value.
+ """
+
+ value_type = 'number'
+
+ def __init__(self, number_value):
+ self.number_value = number_value
+
+
+class TimestampValue(object):
+ """TimestampValues hold individual datetime values for a given field
+ See:
+ https://cloud.google.com/search/reference/rest/google/cloudsearch/v1/FieldValue
+
+ :type timestamp_value: class:``datetime.datetime``
+ :param timestamp_value: the actual value.
+ """
+
+ value_type = 'timestamp'
+
+ def __init__(self, timestamp_value):
+ self.timestamp_value = timestamp_value
+
+
+class GeoValue(object):
+ """GeoValues hold individual latitude/longitude values for a given field
+ See:
+ https://cloud.google.com/search/reference/rest/google/cloudsearch/v1/FieldValue
+
+ :type geo_value: tuple, (float, float)
+ :param geo_value: latitude, longitude
+ """
+
+ value_type = 'geo'
+
+ def __init__(self, geo_value):
+ self.geo_value = geo_value
+
+
+class Field(object):
+ """Fields hold values for a given document
+
+ See:
+ https://cloud.google.com/search/reference/rest/google/cloudsearch/v1/FieldValueList
+
+ :type name: string
+ :param name: field name
+ """
+
+ def __init__(self, name):
+ self.name = name
+ self.values = []
+
+ def add_value(self, value, **kw):
+ """Add a value to the field.
+
+ Selects type of value instance based on type of ``value``.
+
+ :type value: string, integer, float, datetime, or tuple (float, float)
+ :param value: the field value to add.
+
+ :param kw: extra keyword arguments to be passed to the value instance
+ constructor. Currently, only :class:`StringValue`
+ expects / honors additional parameters.
+
+ :raises: ValueError if unable to match the type of ``value``.
+ """
+ if isinstance(value, six.string_types):
+ self.values.append(StringValue(value, **kw))
+ elif isinstance(value, (six.integer_types, float)):
+ self.values.append(NumberValue(value, **kw))
+ elif isinstance(value, datetime.datetime):
+ self.values.append(TimestampValue(value, **kw))
+ elif isinstance(value, tuple):
+ self.values.append(GeoValue(value, **kw))
+ else:
+ raise ValueError("Couldn't determine value type: %s" % (value,))
+
+
+class Document(object):
+ """Documents hold values for search within indexes.
+
+ See:
+ https://cloud.google.com/search/reference/rest/v1/projects/indexes/documents
+
+ :type name: string
+ :param name: the name of the document
+
+ :type index: :class:`gcloud.search.index.Index`
+ :param index: the index to which the document belongs.
+
+ :type rank: positive integer
+ :param rank: override the server-generated rank for ordering the document
+ within in queries. If not passed, the server generates a
+ timestamp-based value. See the ``rank`` entry on the
+ page above for details.
+ """
+ def __init__(self, name, index, rank=None):
+ self.name = name
+ self.index = index
+ self.rank = rank
+ self.fields = {}
+
+ @classmethod
+ def from_api_repr(cls, resource, index):
+ """Factory: construct a document given its API representation
+
+ :type resource: dict
+ :param resource: document resource representation returned from the API
+
+ :type index: :class:`gcloud.search.index.Index`
+ :param index: Index holding the document.
+
+ :rtype: :class:`gcloud.search.document.Document`
+ :returns: Document parsed from ``resource``.
+ """
+ name = resource.get('docId')
+ if name is None:
+ raise KeyError(
+ 'Resource lacks required identity information: ["docId"]')
+ rank = resource.get('rank')
+ document = cls(name, index, rank)
+ document._parse_fields_resource(resource)
+ return document
+
+ def _parse_value_resource(self, resource):
+ """Helper for _parse_fields_resource"""
+ if 'stringValue' in resource:
+ string_format = resource.get('stringFormat')
+ language = resource.get('lang')
+ value = resource['stringValue']
+ return StringValue(value, string_format, language)
+ if 'numberValue' in resource:
+ value = resource['numberValue']
+ if isinstance(value, six.string_types):
+ if '.' in value:
+ value = float(value)
+ else:
+ value = int(value)
+ return NumberValue(value)
+ if 'timestampValue' in resource:
+ stamp = resource['timestampValue']
+ value = datetime.datetime.strptime(stamp, _RFC3339_MICROS)
+ value = value.replace(tzinfo=UTC)
+ return TimestampValue(value)
+ if 'geoValue' in resource:
+ lat_long = resource['geoValue']
+ lat, long = [float(coord.strip()) for coord in lat_long.split(',')]
+ return GeoValue((lat, long))
+ raise ValueError("Unknown value type")
+
+ def _parse_fields_resource(self, resource):
+ """Helper for from_api_repr, create, reload"""
+ self.fields.clear()
+ for field_name, val_obj in resource.get('fields', {}).items():
+ field = self.field(field_name)
+ for value in val_obj['values']:
+ field.values.append(self._parse_value_resource(value))
+
+ @property
+ def path(self):
+ """URL path for the document's APIs"""
+ return '%s/documents/%s' % (self.index.path, self.name)
+
+ def field(self, name):
+ """Construct a Field instance.
+
+ :type name: string
+ :param name: field's name
+ """
+ field = self.fields[name] = Field(name)
+ return field
+
+ def _require_client(self, client):
+ """Check client or verify over-ride.
+
+ :type client: :class:`gcloud.search.client.Client` or ``NoneType``
+ :param client: the client to use. If not passed, falls back to the
+ ``client`` stored on the index of the
+ current document.
+
+ :rtype: :class:`gcloud.search.client.Client`
+ :returns: The client passed in or the currently bound client.
+ """
+ if client is None:
+ client = self.index._client
+ return client
+
+ def _build_value_resource(self, value):
+ """Helper for _build_fields_resource"""
+ result = {}
+ if value.value_type == 'string':
+ result['stringValue'] = value.string_value
+ if value.string_format is not None:
+ result['stringFormat'] = value.string_format
+ if value.language is not None:
+ result['lang'] = value.language
+ elif value.value_type == 'number':
+ result['numberValue'] = value.number_value
+ elif value.value_type == 'timestamp':
+ stamp = value.timestamp_value.strftime(_RFC3339_MICROS)
+ result['timestampValue'] = stamp
+ elif value.value_type == 'geo':
+ result['geoValue'] = '%s, %s' % value.geo_value
+ else:
+ raise ValueError('Unknown value_type: %s' % value.value_type)
+ return result
+
+ def _build_fields_resource(self):
+ """Helper for create"""
+ fields = {}
+ for field_name, field in self.fields.items():
+ if field.values:
+ values = []
+ fields[field_name] = {'values': values}
+ for value in field.values:
+ values.append(self._build_value_resource(value))
+ return fields
+
+ def _set_properties(self, api_response):
+ """Helper for create, reload"""
+ self.rank = api_response.get('rank')
+ self._parse_fields_resource(api_response)
+
+ def create(self, client=None):
+ """API call: create the document via a PUT request
+
+ See:
+ https://cloud.google.com/search/reference/rest/v1/projects/indexes/documents/create
+
+ :type client: :class:`gcloud.search.client.Client` or ``NoneType``
+ :param client: the client to use. If not passed, falls back to the
+ ``client`` stored on the current document's index.
+ """
+ data = {'docId': self.name}
+
+ if self.rank is not None:
+ data['rank'] = self.rank
+
+ fields = self._build_fields_resource()
+ if fields:
+ data['fields'] = fields
+
+ client = self._require_client(client)
+ api_response = client.connection.api_request(
+ method='PUT', path=self.path, data=data)
+
+ self._set_properties(api_response)
+
+ def exists(self, client=None):
+ """API call: test existence of the document via a GET request
+
+ See
+ https://cloud.google.com/search/reference/rest/v1/projects/indexes/documents/get
+
+ :type client: :class:`gcloud.search.client.Client` or ``NoneType``
+ :param client: the client to use. If not passed, falls back to the
+ ``client`` stored on the current document's index.
+ """
+ client = self._require_client(client)
+ try:
+ client.connection.api_request(method='GET', path=self.path)
+ except NotFound:
+ return False
+ else:
+ return True
+
+ def reload(self, client=None):
+ """API call: sync local document configuration via a GET request
+
+ See
+ https://cloud.google.com/search/reference/rest/v1/projects/indexes/documents/get
+
+ :type client: :class:`gcloud.search.client.Client` or ``NoneType``
+ :param client: the client to use. If not passed, falls back to the
+ ``client`` stored on the current document's index.
+ """
+ client = self._require_client(client)
+ api_response = client.connection.api_request(
+ method='GET', path=self.path)
+ self._set_properties(api_response)
+
+ def delete(self, client=None):
+ """API call: delete the document via a DELETE request.
+
+ See:
+ https://cloud.google.com/search/reference/rest/v1/projects/indexes/documents/delete
+
+ :type client: :class:`gcloud.search.client.Client` or ``NoneType``
+ :param client: the client to use. If not passed, falls back to the
+ ``client`` stored on the current document's index.
+ """
+ client = self._require_client(client)
+ client.connection.api_request(method='DELETE', path=self.path)
diff --git a/gcloud/search/test_document.py b/gcloud/search/test_document.py
new file mode 100644
index 000000000000..688fefd4520f
--- /dev/null
+++ b/gcloud/search/test_document.py
@@ -0,0 +1,609 @@
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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 unittest2
+
+
+class TestStringValue(unittest2.TestCase):
+
+ def _getTargetClass(self):
+ from gcloud.search.document import StringValue
+ return StringValue
+
+ def _makeOne(self, *args, **kw):
+ return self._getTargetClass()(*args, **kw)
+
+ def test_ctor_defaults(self):
+ sv = self._makeOne('abcde')
+ self.assertEqual(sv.string_value, 'abcde')
+ self.assertEqual(sv.string_format, None)
+ self.assertEqual(sv.language, None)
+
+ def test_ctor_explicit(self):
+ sv = self._makeOne('abcde', 'text', 'en')
+ self.assertEqual(sv.string_value, 'abcde')
+ self.assertEqual(sv.string_format, 'text')
+ self.assertEqual(sv.language, 'en')
+
+
+class TestNumberValue(unittest2.TestCase):
+
+ def _getTargetClass(self):
+ from gcloud.search.document import NumberValue
+ return NumberValue
+
+ def _makeOne(self, *args, **kw):
+ return self._getTargetClass()(*args, **kw)
+
+ def test_ctor(self):
+ nv = self._makeOne(42)
+ self.assertEqual(nv.number_value, 42)
+
+
+class TestTimestampValue(unittest2.TestCase):
+
+ def _getTargetClass(self):
+ from gcloud.search.document import TimestampValue
+ return TimestampValue
+
+ def _makeOne(self, *args, **kw):
+ return self._getTargetClass()(*args, **kw)
+
+ def test_ctor(self):
+ import datetime
+ from gcloud._helpers import UTC
+ NOW = datetime.datetime.utcnow().replace(tzinfo=UTC)
+ tv = self._makeOne(NOW)
+ self.assertEqual(tv.timestamp_value, NOW)
+
+
+class TestGeoValue(unittest2.TestCase):
+
+ LATITUDE, LONGITUDE = 38.301931, -77.458722
+
+ def _getTargetClass(self):
+ from gcloud.search.document import GeoValue
+ return GeoValue
+
+ def _makeOne(self, *args, **kw):
+ return self._getTargetClass()(*args, **kw)
+
+ def test_ctor(self):
+ gv = self._makeOne((self.LATITUDE, self.LONGITUDE))
+ self.assertEqual(gv.geo_value, (self.LATITUDE, self.LONGITUDE))
+
+
+class TestField(unittest2.TestCase):
+
+ def _getTargetClass(self):
+ from gcloud.search.document import Field
+ return Field
+
+ def _makeOne(self, *args, **kw):
+ return self._getTargetClass()(*args, **kw)
+
+ def test_ctor(self):
+ field = self._makeOne('field_name')
+ self.assertEqual(field.name, 'field_name')
+ self.assertEqual(len(field.values), 0)
+
+ def test_add_value_unknown(self):
+ field = self._makeOne('field_name')
+ with self.assertRaises(ValueError):
+ field.add_value(object())
+
+ def test_add_value_string_defaults(self):
+ from gcloud.search.document import StringValue
+ field = self._makeOne('field_name')
+ field.add_value('this is a string')
+ self.assertEqual(len(field.values), 1)
+ value = field.values[0]
+ self.assertTrue(isinstance(value, StringValue))
+ self.assertEqual(value.string_value, 'this is a string')
+ self.assertEqual(value.string_format, None)
+ self.assertEqual(value.language, None)
+
+ def test_add_value_string_explicit(self):
+ from gcloud.search.document import StringValue
+ field = self._makeOne('field_name')
+ field.add_value('this is a string',
+ string_format='text', language='en')
+ self.assertEqual(len(field.values), 1)
+ value = field.values[0]
+ self.assertTrue(isinstance(value, StringValue))
+ self.assertEqual(value.string_value, 'this is a string')
+ self.assertEqual(value.string_format, 'text')
+ self.assertEqual(value.language, 'en')
+
+ def test_add_value_integer(self):
+ from gcloud.search.document import NumberValue
+ field = self._makeOne('field_name')
+ field.add_value(42)
+ self.assertEqual(len(field.values), 1)
+ value = field.values[0]
+ self.assertTrue(isinstance(value, NumberValue))
+ self.assertEqual(value.number_value, 42)
+
+ def test_add_value_datetime(self):
+ import datetime
+ from gcloud._helpers import UTC
+ from gcloud.search.document import TimestampValue
+ NOW = datetime.datetime.utcnow().replace(tzinfo=UTC)
+ field = self._makeOne('field_name')
+ field.add_value(NOW)
+ self.assertEqual(len(field.values), 1)
+ value = field.values[0]
+ self.assertTrue(isinstance(value, TimestampValue))
+ self.assertEqual(value.timestamp_value, NOW)
+
+ def test_add_value_geo(self):
+ from gcloud.search.document import GeoValue
+ LATITUDE, LONGITUDE = 38.301931, -77.458722
+ field = self._makeOne('field_name')
+ field.add_value((LATITUDE, LONGITUDE))
+ self.assertEqual(len(field.values), 1)
+ value = field.values[0]
+ self.assertTrue(isinstance(value, GeoValue))
+ self.assertEqual(value.geo_value, (LATITUDE, LONGITUDE))
+
+
+class TestDocument(unittest2.TestCase):
+
+ PROJECT = 'PROJECT'
+ DOC_NAME = 'doc_name'
+ INDEX_NAME = 'index_name'
+ DOC_PATH = 'projects/%s/indexes/%s/documents/%s' % (
+ PROJECT, INDEX_NAME, DOC_NAME)
+ RANK = 42
+
+ def _getTargetClass(self):
+ from gcloud.search.document import Document
+ return Document
+
+ def _makeOne(self, *args, **kw):
+ return self._getTargetClass()(*args, **kw)
+
+ def test_ctor_defaults(self):
+ index = object()
+ document = self._makeOne(self.DOC_NAME, index)
+ self.assertEqual(document.name, self.DOC_NAME)
+ self.assertTrue(document.index is index)
+ self.assertEqual(document.rank, None)
+ self.assertEqual(document.fields, {})
+
+ def test_ctor_explicit(self):
+ index = object()
+ document = self._makeOne(self.DOC_NAME, index, self.RANK)
+ self.assertEqual(document.name, self.DOC_NAME)
+ self.assertTrue(document.index is index)
+ self.assertEqual(document.rank, self.RANK)
+ self.assertEqual(document.fields, {})
+
+ def test_from_api_repr_invalid(self):
+ klass = self._getTargetClass()
+ index = object()
+ with self.assertRaises(KeyError):
+ klass.from_api_repr({}, index)
+
+ def test_from_api_repr(self):
+ import datetime
+ from gcloud._helpers import UTC, _RFC3339_MICROS
+ VALUE = 'The quick brown fox'
+ HTML_VALUE = 'jumped over the lazy fence.'
+ NOW = datetime.datetime.utcnow().replace(tzinfo=UTC)
+ NOW_STR = NOW.strftime(_RFC3339_MICROS)
+ LATITUDE, LONGITUDE = 38.301931, -77.458722
+ resource = {
+ 'docId': self.DOC_NAME,
+ 'rank': self.RANK,
+ 'fields': {
+ 'title': {
+ 'values': [
+ {'stringFormat': 'text',
+ 'lang': 'en',
+ 'stringValue': VALUE},
+ {'stringFormat': 'html',
+ 'lang': 'en',
+ 'stringValue': HTML_VALUE},
+ {'numberValue': 42},
+ {'numberValue': '42'},
+ {'numberValue': '3.1415926'},
+ {'timestampValue': NOW_STR},
+ {'geoValue': '%s, %s' % (LATITUDE, LONGITUDE)},
+ ],
+ }
+ }
+ }
+ klass = self._getTargetClass()
+ index = object()
+
+ document = klass.from_api_repr(resource, index)
+
+ self.assertEqual(document.name, self.DOC_NAME)
+ self.assertTrue(document.index is index)
+ self.assertEqual(document.rank, self.RANK)
+
+ self.assertEqual(list(document.fields), ['title'])
+ field = document.fields['title']
+ self.assertEqual(field.name, 'title')
+ self.assertEqual(len(field.values), 7)
+
+ value = field.values[0]
+ self.assertEqual(value.value_type, 'string')
+ self.assertEqual(value.language, 'en')
+ self.assertEqual(value.string_format, 'text')
+ self.assertEqual(value.string_value, VALUE)
+
+ value = field.values[1]
+ self.assertEqual(value.value_type, 'string')
+ self.assertEqual(value.language, 'en')
+ self.assertEqual(value.string_format, 'html')
+ self.assertEqual(value.string_value,
+ 'jumped over the lazy fence.')
+
+ value = field.values[2]
+ self.assertEqual(value.value_type, 'number')
+ self.assertEqual(value.number_value, 42)
+
+ value = field.values[3]
+ self.assertEqual(value.value_type, 'number')
+ self.assertEqual(value.number_value, 42)
+
+ value = field.values[4]
+ self.assertEqual(value.value_type, 'number')
+ self.assertEqual(value.number_value, 3.1415926)
+
+ value = field.values[5]
+ self.assertEqual(value.value_type, 'timestamp')
+ self.assertEqual(value.timestamp_value, NOW)
+
+ value = field.values[6]
+ self.assertEqual(value.value_type, 'geo')
+ self.assertEqual(value.geo_value, (LATITUDE, LONGITUDE))
+
+ def test__parse_value_resource_invalid(self):
+ conn = _Connection()
+ client = _Client(project=self.PROJECT, connection=conn)
+ index = _Index(self.INDEX_NAME, client=client)
+ document = self._makeOne(self.DOC_NAME, index)
+ with self.assertRaises(ValueError):
+ document._parse_value_resource({})
+
+ def test__build_value_resource_invalid(self):
+ class _UnknownValue(object):
+ value_type = 'nonesuch'
+ conn = _Connection()
+ client = _Client(project=self.PROJECT, connection=conn)
+ index = _Index(self.INDEX_NAME, client=client)
+ document = self._makeOne(self.DOC_NAME, index)
+ with self.assertRaises(ValueError):
+ document._build_value_resource(_UnknownValue())
+
+ def test__build_field_resources_field_wo_values(self):
+ conn = _Connection()
+ client = _Client(project=self.PROJECT, connection=conn)
+ index = _Index(self.INDEX_NAME, client=client)
+ document = self._makeOne(self.DOC_NAME, index)
+ _ = document.field('testing') # no values
+ self.assertEqual(document._build_fields_resource(), {})
+
+ def test_create_wo_fields(self):
+ import copy
+ BODY = {'docId': self.DOC_NAME}
+ RESPONSE = copy.deepcopy(BODY)
+ RESPONSE['rank'] = self.RANK
+ conn = _Connection(RESPONSE)
+ client = _Client(project=self.PROJECT, connection=conn)
+ index = _Index(self.INDEX_NAME, client=client)
+ document = self._makeOne(self.DOC_NAME, index)
+
+ document.create()
+
+ self.assertEqual(list(document.fields), [])
+
+ self.assertEqual(len(conn._requested), 1)
+ req = conn._requested[0]
+ self.assertEqual(req['method'], 'PUT')
+ self.assertEqual(req['path'], '/%s' % self.DOC_PATH)
+ self.assertEqual(req['data'], BODY)
+
+ def test_create_wo_rank_w_bound_client(self):
+ import copy
+ VALUE = 'The quick brown fox'
+ BODY = {
+ 'docId': self.DOC_NAME,
+ 'fields': {
+ 'testing': {
+ 'values': [
+ {'stringValue': VALUE},
+ ],
+ }
+ }
+ }
+ RESPONSE = copy.deepcopy(BODY)
+ RESPONSE['rank'] = self.RANK
+ response_value = RESPONSE['fields']['testing']['values'][0]
+ response_value['stringFormat'] = 'auto'
+ conn = _Connection(RESPONSE)
+ client = _Client(project=self.PROJECT, connection=conn)
+ index = _Index(self.INDEX_NAME, client=client)
+ document = self._makeOne(self.DOC_NAME, index)
+ field = document.field('testing')
+ field.add_value(VALUE)
+
+ document.create()
+
+ self.assertEqual(list(document.fields), ['testing'])
+ field = document.fields['testing']
+ self.assertEqual(len(field.values), 1)
+
+ value = field.values[0]
+ self.assertEqual(value.value_type, 'string')
+ self.assertEqual(value.string_format, 'auto')
+ self.assertEqual(value.string_value, VALUE)
+ self.assertEqual(value.language, None)
+
+ self.assertEqual(len(conn._requested), 1)
+ req = conn._requested[0]
+ self.assertEqual(req['method'], 'PUT')
+ self.assertEqual(req['path'], '/%s' % self.DOC_PATH)
+ self.assertEqual(req['data'], BODY)
+
+ def test_create_w_rank_w_alternate_client(self):
+ import datetime
+ from gcloud._helpers import UTC, _RFC3339_MICROS
+ VALUE = 'The quick brown fox'
+ NOW = datetime.datetime.utcnow().replace(tzinfo=UTC)
+ NOW_STR = NOW.strftime(_RFC3339_MICROS)
+ LATITUDE, LONGITUDE = 38.301931, -77.458722
+ BODY = {
+ 'docId': self.DOC_NAME,
+ 'rank': self.RANK,
+ 'fields': {
+ 'title': {
+ 'values': [
+ {'stringValue': VALUE,
+ 'stringFormat': 'text',
+ 'lang': 'en'},
+ {'numberValue': 17.5},
+ {'timestampValue': NOW_STR},
+ {'geoValue': '%s, %s' % (LATITUDE, LONGITUDE)},
+ ],
+ }
+ }
+ }
+ RESPONSE = BODY.copy()
+ RESPONSE['rank'] = self.RANK
+ conn1 = _Connection()
+ client1 = _Client(project=self.PROJECT, connection=conn1)
+ conn2 = _Connection(BODY)
+ client2 = _Client(project=self.PROJECT, connection=conn2)
+ index = _Index(self.INDEX_NAME, client=client1)
+ document = self._makeOne(self.DOC_NAME, index, rank=self.RANK)
+ field = document.field('title')
+ field.add_value(VALUE, string_format='text', language='en')
+ field.add_value(17.5)
+ field.add_value(NOW)
+ field.add_value((LATITUDE, LONGITUDE))
+
+ document.create(client=client2)
+
+ self.assertEqual(list(document.fields), ['title'])
+ field = document.fields['title']
+ self.assertEqual(len(field.values), 4)
+
+ value = field.values[0]
+ self.assertEqual(value.value_type, 'string')
+ self.assertEqual(value.string_format, 'text')
+ self.assertEqual(value.string_value, VALUE)
+ self.assertEqual(value.language, 'en')
+
+ value = field.values[1]
+ self.assertEqual(value.value_type, 'number')
+ self.assertEqual(value.number_value, 17.5)
+
+ value = field.values[2]
+ self.assertEqual(value.value_type, 'timestamp')
+ self.assertEqual(value.timestamp_value, NOW)
+
+ value = field.values[3]
+ self.assertEqual(value.value_type, 'geo')
+ self.assertEqual(value.geo_value, (LATITUDE, LONGITUDE))
+
+ self.assertEqual(len(conn1._requested), 0)
+ self.assertEqual(len(conn2._requested), 1)
+
+ req = conn2._requested[0]
+ self.assertEqual(req['method'], 'PUT')
+ self.assertEqual(req['path'], '/%s' % self.DOC_PATH)
+ self.assertEqual(req['data'], BODY)
+
+ def test_exists_miss_w_bound_client(self):
+ conn = _Connection()
+ client = _Client(project=self.PROJECT, connection=conn)
+ index = _Index(self.INDEX_NAME, client=client)
+ document = self._makeOne(self.DOC_NAME, index)
+
+ self.assertFalse(document.exists())
+
+ self.assertEqual(len(conn._requested), 1)
+ req = conn._requested[0]
+ self.assertEqual(req['method'], 'GET')
+ self.assertEqual(req['path'], '/%s' % self.DOC_PATH)
+ self.assertEqual(req.get('query_params'), None)
+
+ def test_exists_hit_w_alternate_client(self):
+ BODY = {'docId': self.DOC_NAME, 'rank': self.RANK}
+ conn1 = _Connection()
+ client1 = _Client(project=self.PROJECT, connection=conn1)
+ conn2 = _Connection(BODY)
+ client2 = _Client(project=self.PROJECT, connection=conn2)
+ index = _Index(self.INDEX_NAME, client=client1)
+ document = self._makeOne(self.DOC_NAME, index)
+
+ self.assertTrue(document.exists(client=client2))
+
+ self.assertEqual(len(conn1._requested), 0)
+ self.assertEqual(len(conn2._requested), 1)
+ req = conn2._requested[0]
+ self.assertEqual(req['method'], 'GET')
+ self.assertEqual(req['path'], '/%s' % self.DOC_PATH)
+ self.assertEqual(req.get('query_params'), None)
+
+ def test_reload_w_bound_client(self):
+ VALUE = 'The quick brown fox'
+ BODY = {
+ 'docId': self.DOC_NAME,
+ 'rank': self.RANK,
+ 'fields': {
+ 'title': {
+ 'values': [
+ {'stringFormat': 'text',
+ 'lang': 'en',
+ 'stringValue': VALUE},
+ ],
+ }
+ }
+ }
+ conn = _Connection(BODY)
+ client = _Client(project=self.PROJECT, connection=conn)
+ index = _Index(self.INDEX_NAME, client=client)
+ document = self._makeOne(self.DOC_NAME, index)
+
+ document.reload()
+
+ self.assertEqual(document.rank, self.RANK)
+
+ self.assertEqual(list(document.fields), ['title'])
+ field = document.fields['title']
+ self.assertEqual(len(field.values), 1)
+ self.assertEqual(field.name, 'title')
+ self.assertEqual(len(field.values), 1)
+
+ value = field.values[0]
+ self.assertEqual(value.value_type, 'string')
+ self.assertEqual(value.language, 'en')
+ self.assertEqual(value.string_format, 'text')
+ self.assertEqual(value.string_value, VALUE)
+
+ self.assertEqual(len(conn._requested), 1)
+ req = conn._requested[0]
+ self.assertEqual(req['method'], 'GET')
+ self.assertEqual(req['path'], '/%s' % self.DOC_PATH)
+
+ def test_reload_w_alternate_client(self):
+ VALUE = 'The quick brown fox'
+ BODY = {
+ 'docId': self.DOC_NAME,
+ 'rank': self.RANK,
+ 'fields': {
+ 'title': {
+ 'values': [
+ {'stringFormat': 'text',
+ 'lang': 'en',
+ 'stringValue': VALUE},
+ ],
+ }
+ }
+ }
+ conn1 = _Connection()
+ client1 = _Client(project=self.PROJECT, connection=conn1)
+ conn2 = _Connection(BODY)
+ client2 = _Client(project=self.PROJECT, connection=conn2)
+ index = _Index(self.INDEX_NAME, client=client1)
+ document = self._makeOne(self.DOC_NAME, index)
+
+ document.reload(client=client2)
+
+ self.assertEqual(document.rank, self.RANK)
+
+ self.assertEqual(list(document.fields), ['title'])
+ field = document.fields['title']
+ self.assertEqual(field.name, 'title')
+ self.assertEqual(len(field.values), 1)
+
+ value = field.values[0]
+ self.assertEqual(value.value_type, 'string')
+ self.assertEqual(value.language, 'en')
+ self.assertEqual(value.string_format, 'text')
+ self.assertEqual(value.string_value, VALUE)
+
+ self.assertEqual(len(conn1._requested), 0)
+ self.assertEqual(len(conn2._requested), 1)
+ req = conn2._requested[0]
+ self.assertEqual(req['method'], 'GET')
+ self.assertEqual(req['path'], '/%s' % self.DOC_PATH)
+
+ def test_delete_w_bound_client(self):
+ conn = _Connection({})
+ client = _Client(project=self.PROJECT, connection=conn)
+ index = _Index(self.INDEX_NAME, client=client)
+ document = self._makeOne(self.DOC_NAME, index)
+
+ document.delete()
+
+ self.assertEqual(len(conn._requested), 1)
+ req = conn._requested[0]
+ self.assertEqual(req['method'], 'DELETE')
+ self.assertEqual(req['path'], '/%s' % self.DOC_PATH)
+
+ def test_delete_w_alternate_client(self):
+ conn1 = _Connection({})
+ client1 = _Client(project=self.PROJECT, connection=conn1)
+ conn2 = _Connection({})
+ client2 = _Client(project=self.PROJECT, connection=conn2)
+ index = _Index(self.INDEX_NAME, client=client1)
+ document = self._makeOne(self.DOC_NAME, index)
+
+ document.delete(client=client2)
+
+ self.assertEqual(len(conn1._requested), 0)
+ self.assertEqual(len(conn2._requested), 1)
+ req = conn2._requested[0]
+ self.assertEqual(req['method'], 'DELETE')
+ self.assertEqual(req['path'], '/%s' % self.DOC_PATH)
+
+
+class _Connection(object):
+
+ def __init__(self, *responses):
+ self._responses = responses
+ self._requested = []
+
+ def api_request(self, **kw):
+ from gcloud.exceptions import NotFound
+ self._requested.append(kw)
+
+ try:
+ response, self._responses = self._responses[0], self._responses[1:]
+ except:
+ raise NotFound('miss')
+ else:
+ return response
+
+
+class _Index(object):
+
+ def __init__(self, name, client):
+ self.name = name
+ self._client = client
+ self.project = client.project
+ self.path = '/projects/%s/indexes/%s' % (client.project, name)
+
+
+class _Client(object):
+
+ def __init__(self, project, connection=None):
+ self.project = project
+ self.connection = connection