From 4f74dc649d4d6317d858afe18d3db41cfe63fdfe Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Thu, 1 Oct 2015 12:49:42 -0400 Subject: [PATCH 1/2] Add search indexes. --- gcloud/search/client.py | 62 +++++++++++++++ gcloud/search/index.py | 150 +++++++++++++++++++++++++++++++++++ gcloud/search/test_client.py | 114 +++++++++++++++++++++++++- gcloud/search/test_index.py | 113 ++++++++++++++++++++++++++ 4 files changed, 437 insertions(+), 2 deletions(-) create mode 100644 gcloud/search/index.py create mode 100644 gcloud/search/test_index.py diff --git a/gcloud/search/client.py b/gcloud/search/client.py index 30a598d3c5a6..138d62385e25 100644 --- a/gcloud/search/client.py +++ b/gcloud/search/client.py @@ -17,6 +17,7 @@ from gcloud.client import JSONClient from gcloud.search.connection import Connection +from gcloud.search.index import Index class Client(JSONClient): @@ -41,3 +42,64 @@ class Client(JSONClient): """ _connection_class = Connection + + def list_indexes(self, max_results=None, page_token=None, + view=None, prefix=None): + """List zones for the project associated with this client. + + See: + https://cloud.google.com/search/reference/rest/v1/indexes/list + + :type max_results: int + :param max_results: maximum number of zones to return, If not + passed, defaults to a value set by the API. + + :type page_token: string + :param page_token: opaque marker for the next "page" of zones. If + not passed, the API will return the first page of + zones. + + :type view: string + :param view: One of 'ID_ONLY' (return only the index ID; the default) + or 'FULL' (return information on indexed fields). + + :type prefix: string + :param prefix: return only indexes whose ID starts with ``prefix``. + + :rtype: tuple, (list, str) + :returns: list of :class:`gcloud.dns.index.Index`, plus a + "next page token" string: if the token is not None, + indicates that more zones can be retrieved with another + call (pass that value as ``page_token``). + """ + params = {} + + if max_results is not None: + params['pageSize'] = max_results + + if page_token is not None: + params['pageToken'] = page_token + + if view is not None: + params['view'] = view + + if prefix is not None: + params['indexNamePrefix'] = prefix + + path = '/projects/%s/indexes' % (self.project,) + resp = self.connection.api_request(method='GET', path=path, + query_params=params) + zones = [Index.from_api_repr(resource, self) + for resource in resp['indexes']] + return zones, resp.get('nextPageToken') + + def index(self, name): + """Construct an index bound to this client. + + :type name: string + :param name: Name of the zone. + + :rtype: :class:`gcloud.search.index.Index` + :returns: a new ``Index`` instance + """ + return Index(name, client=self) diff --git a/gcloud/search/index.py b/gcloud/search/index.py new file mode 100644 index 000000000000..6d97092fed1a --- /dev/null +++ b/gcloud/search/index.py @@ -0,0 +1,150 @@ +# 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 Indexes.""" + + +class Index(object): + """Indexes are containers for documents. + + See: + https://cloud.google.com/search/reference/rest/v1/indexes + + :type name: string + :param name: the name of the index + + :type client: :class:`gcloud.dns.client.Client` + :param client: A client which holds credentials and project configuration + for the index (which requires a project). + """ + + def __init__(self, name, client): + self.name = name + self._client = client + self._properties = {} + + @classmethod + def from_api_repr(cls, resource, client): + """Factory: construct an index given its API representation + + :type resource: dict + :param resource: index resource representation returned from the API + + :type client: :class:`gcloud.dns.client.Client` + :param client: Client which holds credentials and project + configuration for the index. + + :rtype: :class:`gcloud.dns.index.Index` + :returns: Index parsed from ``resource``. + """ + name = resource.get('indexId') + if name is None is None: + raise KeyError( + 'Resource lacks required identity information: ["indexId"]') + index = cls(name, client=client) + index._set_properties(resource) + return index + + @property + def project(self): + """Project bound to the index. + + :rtype: string + :returns: the project (derived from the client). + """ + return self._client.project + + @property + def path(self): + """URL path for the index's APIs. + + :rtype: string + :returns: the path based on project and dataste name. + """ + return '/projects/%s/indexes/%s' % (self.project, self.name) + + def _list_field_names(self, field_type): + """Helper for 'text_fields', etc. + """ + fields = self._properties.get('indexedField', {}) + return fields.get(field_type) + + @property + def text_fields(self): + """Names of text fields in the index. + + :rtype: list of string, or None + :returns: names of text fields in the index, or None if no + resource information is available. + """ + return self._list_field_names('textFields') + + @property + def atom_fields(self): + """Names of atom fields in the index. + + :rtype: list of string, or None + :returns: names of atom fields in the index, or None if no + resource information is available. + """ + return self._list_field_names('atomFields') + + @property + def html_fields(self): + """Names of html fields in the index. + + :rtype: list of string, or None + :returns: names of html fields in the index, or None if no + resource information is available. + """ + return self._list_field_names('htmlFields') + + @property + def date_fields(self): + """Names of date fields in the index. + + :rtype: list of string, or None + :returns: names of date fields in the index, or None if no + resource information is available. + """ + return self._list_field_names('dateFields') + + @property + def number_fields(self): + """Names of number fields in the index. + + :rtype: list of string, or None + :returns: names of number fields in the index, or None if no + resource information is available. + """ + return self._list_field_names('numberFields') + + @property + def geo_fields(self): + """Names of geo fields in the index. + + :rtype: list of string, or None + :returns: names of geo fields in the index, or None if no + resource information is available. + """ + return self._list_field_names('geoFields') + + def _set_properties(self, api_response): + """Update properties from resource in body of ``api_response`` + + :type api_response: httplib2.Response + :param api_response: response returned from an API call + """ + self._properties.clear() + self._properties.update(api_response) diff --git a/gcloud/search/test_client.py b/gcloud/search/test_client.py index e128eb128f75..1692be82b7b3 100644 --- a/gcloud/search/test_client.py +++ b/gcloud/search/test_client.py @@ -16,6 +16,7 @@ class TestClient(unittest2.TestCase): + PROJECT = 'PROJECT' def _getTargetClass(self): from gcloud.search.client import Client @@ -26,14 +27,111 @@ def _makeOne(self, *args, **kw): def test_ctor(self): from gcloud.search.connection import Connection - PROJECT = 'PROJECT' creds = _Credentials() http = object() - client = self._makeOne(project=PROJECT, credentials=creds, http=http) + client = self._makeOne( + project=self.PROJECT, credentials=creds, http=http) self.assertTrue(isinstance(client.connection, Connection)) self.assertTrue(client.connection.credentials is creds) self.assertTrue(client.connection.http is http) + def test_list_indexes_defaults(self): + from gcloud.search.index import Index + INDEX_1 = 'index-one' + INDEX_2 = 'index-two' + PATH = 'projects/%s/indexes' % self.PROJECT + TOKEN = 'TOKEN' + DATA = { + 'nextPageToken': TOKEN, + 'indexes': [ + {'project': self.PROJECT, + 'indexId': INDEX_1}, + {'project': self.PROJECT, + 'indexId': INDEX_2}, + ] + } + creds = _Credentials() + client = self._makeOne(self.PROJECT, creds) + conn = client.connection = _Connection(DATA) + + zones, token = client.list_indexes() + + self.assertEqual(len(zones), len(DATA['indexes'])) + for found, expected in zip(zones, DATA['indexes']): + self.assertTrue(isinstance(found, Index)) + self.assertEqual(found.name, expected['indexId']) + self.assertEqual(found.text_fields, None) + self.assertEqual(found.atom_fields, None) + self.assertEqual(found.html_fields, None) + self.assertEqual(found.date_fields, None) + self.assertEqual(found.number_fields, None) + self.assertEqual(found.geo_fields, None) + self.assertEqual(token, TOKEN) + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + + def test_list_indexes_explicit(self): + from gcloud.search.index import Index + INDEX_1 = 'index-one' + INDEX_2 = 'index-two' + PATH = 'projects/%s/indexes' % self.PROJECT + TOKEN = 'TOKEN' + DATA = { + 'indexes': [ + {'project': self.PROJECT, + 'indexId': INDEX_1, + 'indexedField': {'textFields': ['text-1']}}, + {'project': self.PROJECT, + 'indexId': INDEX_2, + 'indexedField': {'htmlFields': ['html-1']}}, + ] + } + creds = _Credentials() + client = self._makeOne(self.PROJECT, creds) + conn = client.connection = _Connection(DATA) + + zones, token = client.list_indexes( + max_results=3, page_token=TOKEN, prefix='index', view='FULL') + + self.assertEqual(len(zones), len(DATA['indexes'])) + for found, expected in zip(zones, DATA['indexes']): + self.assertTrue(isinstance(found, Index)) + self.assertEqual(found.name, expected['indexId']) + field_info = expected['indexedField'] + self.assertEqual(found.text_fields, field_info.get('textFields')) + self.assertEqual(found.atom_fields, field_info.get('atomFields')) + self.assertEqual(found.html_fields, field_info.get('htmlFields')) + self.assertEqual(found.date_fields, field_info.get('dateFields')) + self.assertEqual(found.number_fields, + field_info.get('numberFields')) + self.assertEqual(found.geo_fields, field_info.get('geoFields')) + self.assertEqual(token, None) + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + self.assertEqual(req['query_params'], + {'indexNamePrefix': 'index', + 'pageSize': 3, + 'pageToken': TOKEN, + 'view': 'FULL'}) + + def test_index(self): + from gcloud.search.index import Index + INDEX_ID = 'index-id' + creds = _Credentials() + http = object() + client = self._makeOne( + project=self.PROJECT, credentials=creds, http=http) + index = client.index(INDEX_ID) + self.assertTrue(isinstance(index, Index)) + self.assertEqual(index.name, INDEX_ID) + self.assertTrue(index._client is client) + class _Credentials(object): @@ -46,3 +144,15 @@ def create_scoped_required(): def create_scoped(self, scope): self._scopes = scope return self + + +class _Connection(object): + + def __init__(self, *responses): + self._responses = responses + self._requested = [] + + def api_request(self, **kw): + self._requested.append(kw) + response, self._responses = self._responses[0], self._responses[1:] + return response diff --git a/gcloud/search/test_index.py b/gcloud/search/test_index.py new file mode 100644 index 000000000000..26f36879d710 --- /dev/null +++ b/gcloud/search/test_index.py @@ -0,0 +1,113 @@ +# 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 TestIndex(unittest2.TestCase): + PROJECT = 'project' + INDEX_ID = 'index-id' + + def _getTargetClass(self): + from gcloud.search.index import Index + return Index + + def _makeOne(self, *args, **kw): + return self._getTargetClass()(*args, **kw) + + def _setUpConstants(self): + import datetime + from gcloud._helpers import UTC + + self.WHEN_TS = 1437767599.006 + self.WHEN = datetime.datetime.utcfromtimestamp(self.WHEN_TS).replace( + tzinfo=UTC) + self.ZONE_ID = 12345 + + def _makeResource(self): + self._setUpConstants() + return { + 'projectId': self.PROJECT, + 'indexId': self.INDEX_ID, + 'indexedField': { + 'textFields': ['text-1', 'text-2'], + 'htmlFields': ['html-1', 'html-2'], + 'atomFields': ['atom-1', 'atom-2'], + 'dateFields': ['date-1', 'date-2'], + 'numberFields': ['number-1', 'number-2'], + 'geoFields': ['geo-1', 'geo-2'], + } + } + + def _verifyResourceProperties(self, index, resource): + + self.assertEqual(index.name, resource.get('indexId')) + field_info = resource.get('indexedField', {}) + self.assertEqual(index.text_fields, field_info.get('textFields')) + self.assertEqual(index.html_fields, field_info.get('htmlFields')) + self.assertEqual(index.atom_fields, field_info.get('atomFields')) + self.assertEqual(index.date_fields, field_info.get('dateFields')) + self.assertEqual(index.number_fields, field_info.get('numberFields')) + self.assertEqual(index.geo_fields, field_info.get('geoFields')) + + def test_ctor(self): + client = _Client(self.PROJECT) + index = self._makeOne(self.INDEX_ID, client) + self.assertEqual(index.name, self.INDEX_ID) + self.assertTrue(index._client is client) + self.assertEqual(index.project, client.project) + self.assertEqual( + index.path, + '/projects/%s/indexes/%s' % (self.PROJECT, self.INDEX_ID)) + self.assertEqual(index.text_fields, None) + self.assertEqual(index.html_fields, None) + self.assertEqual(index.atom_fields, None) + self.assertEqual(index.date_fields, None) + self.assertEqual(index.number_fields, None) + self.assertEqual(index.geo_fields, None) + + def test_from_api_repr_missing_identity(self): + self._setUpConstants() + client = _Client(self.PROJECT) + RESOURCE = {} + klass = self._getTargetClass() + with self.assertRaises(KeyError): + klass.from_api_repr(RESOURCE, client=client) + + def test_from_api_repr_bare(self): + self._setUpConstants() + client = _Client(self.PROJECT) + RESOURCE = { + 'indexId': self.INDEX_ID, + } + klass = self._getTargetClass() + index = klass.from_api_repr(RESOURCE, client=client) + self.assertTrue(index._client is client) + self._verifyResourceProperties(index, RESOURCE) + + def test_from_api_repr_w_properties(self): + self._setUpConstants() + client = _Client(self.PROJECT) + RESOURCE = self._makeResource() + klass = self._getTargetClass() + index = klass.from_api_repr(RESOURCE, client=client) + self.assertTrue(index._client is client) + self._verifyResourceProperties(index, RESOURCE) + + +class _Client(object): + + def __init__(self, project='project', connection=None): + self.project = project + self.connection = connection From 3b8b31c53fa3d01e96cc5fb998fe2fbcd1937218 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 6 Oct 2015 15:20:50 -0400 Subject: [PATCH 2/2] Remove typo. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1165/files#r41306251 --- gcloud/search/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcloud/search/index.py b/gcloud/search/index.py index 6d97092fed1a..31c2950b7105 100644 --- a/gcloud/search/index.py +++ b/gcloud/search/index.py @@ -49,7 +49,7 @@ def from_api_repr(cls, resource, client): :returns: Index parsed from ``resource``. """ name = resource.get('indexId') - if name is None is None: + if name is None: raise KeyError( 'Resource lacks required identity information: ["indexId"]') index = cls(name, client=client)