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