Skip to content

Commit eb30c53

Browse files
committed
Merge pull request #1165 from tseaver/search-index
Add search indexes
2 parents 8d36b33 + 3b8b31c commit eb30c53

File tree

4 files changed

+437
-2
lines changed

4 files changed

+437
-2
lines changed

gcloud/search/client.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
from gcloud.client import JSONClient
1919
from gcloud.search.connection import Connection
20+
from gcloud.search.index import Index
2021

2122

2223
class Client(JSONClient):
@@ -41,3 +42,64 @@ class Client(JSONClient):
4142
"""
4243

4344
_connection_class = Connection
45+
46+
def list_indexes(self, max_results=None, page_token=None,
47+
view=None, prefix=None):
48+
"""List zones for the project associated with this client.
49+
50+
See:
51+
https://cloud.google.com/search/reference/rest/v1/indexes/list
52+
53+
:type max_results: int
54+
:param max_results: maximum number of zones to return, If not
55+
passed, defaults to a value set by the API.
56+
57+
:type page_token: string
58+
:param page_token: opaque marker for the next "page" of zones. If
59+
not passed, the API will return the first page of
60+
zones.
61+
62+
:type view: string
63+
:param view: One of 'ID_ONLY' (return only the index ID; the default)
64+
or 'FULL' (return information on indexed fields).
65+
66+
:type prefix: string
67+
:param prefix: return only indexes whose ID starts with ``prefix``.
68+
69+
:rtype: tuple, (list, str)
70+
:returns: list of :class:`gcloud.dns.index.Index`, plus a
71+
"next page token" string: if the token is not None,
72+
indicates that more zones can be retrieved with another
73+
call (pass that value as ``page_token``).
74+
"""
75+
params = {}
76+
77+
if max_results is not None:
78+
params['pageSize'] = max_results
79+
80+
if page_token is not None:
81+
params['pageToken'] = page_token
82+
83+
if view is not None:
84+
params['view'] = view
85+
86+
if prefix is not None:
87+
params['indexNamePrefix'] = prefix
88+
89+
path = '/projects/%s/indexes' % (self.project,)
90+
resp = self.connection.api_request(method='GET', path=path,
91+
query_params=params)
92+
zones = [Index.from_api_repr(resource, self)
93+
for resource in resp['indexes']]
94+
return zones, resp.get('nextPageToken')
95+
96+
def index(self, name):
97+
"""Construct an index bound to this client.
98+
99+
:type name: string
100+
:param name: Name of the zone.
101+
102+
:rtype: :class:`gcloud.search.index.Index`
103+
:returns: a new ``Index`` instance
104+
"""
105+
return Index(name, client=self)

gcloud/search/index.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Copyright 2015 Google Inc. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Define API Indexes."""
16+
17+
18+
class Index(object):
19+
"""Indexes are containers for documents.
20+
21+
See:
22+
https://cloud.google.com/search/reference/rest/v1/indexes
23+
24+
:type name: string
25+
:param name: the name of the index
26+
27+
:type client: :class:`gcloud.dns.client.Client`
28+
:param client: A client which holds credentials and project configuration
29+
for the index (which requires a project).
30+
"""
31+
32+
def __init__(self, name, client):
33+
self.name = name
34+
self._client = client
35+
self._properties = {}
36+
37+
@classmethod
38+
def from_api_repr(cls, resource, client):
39+
"""Factory: construct an index given its API representation
40+
41+
:type resource: dict
42+
:param resource: index resource representation returned from the API
43+
44+
:type client: :class:`gcloud.dns.client.Client`
45+
:param client: Client which holds credentials and project
46+
configuration for the index.
47+
48+
:rtype: :class:`gcloud.dns.index.Index`
49+
:returns: Index parsed from ``resource``.
50+
"""
51+
name = resource.get('indexId')
52+
if name is None:
53+
raise KeyError(
54+
'Resource lacks required identity information: ["indexId"]')
55+
index = cls(name, client=client)
56+
index._set_properties(resource)
57+
return index
58+
59+
@property
60+
def project(self):
61+
"""Project bound to the index.
62+
63+
:rtype: string
64+
:returns: the project (derived from the client).
65+
"""
66+
return self._client.project
67+
68+
@property
69+
def path(self):
70+
"""URL path for the index's APIs.
71+
72+
:rtype: string
73+
:returns: the path based on project and dataste name.
74+
"""
75+
return '/projects/%s/indexes/%s' % (self.project, self.name)
76+
77+
def _list_field_names(self, field_type):
78+
"""Helper for 'text_fields', etc.
79+
"""
80+
fields = self._properties.get('indexedField', {})
81+
return fields.get(field_type)
82+
83+
@property
84+
def text_fields(self):
85+
"""Names of text fields in the index.
86+
87+
:rtype: list of string, or None
88+
:returns: names of text fields in the index, or None if no
89+
resource information is available.
90+
"""
91+
return self._list_field_names('textFields')
92+
93+
@property
94+
def atom_fields(self):
95+
"""Names of atom fields in the index.
96+
97+
:rtype: list of string, or None
98+
:returns: names of atom fields in the index, or None if no
99+
resource information is available.
100+
"""
101+
return self._list_field_names('atomFields')
102+
103+
@property
104+
def html_fields(self):
105+
"""Names of html fields in the index.
106+
107+
:rtype: list of string, or None
108+
:returns: names of html fields in the index, or None if no
109+
resource information is available.
110+
"""
111+
return self._list_field_names('htmlFields')
112+
113+
@property
114+
def date_fields(self):
115+
"""Names of date fields in the index.
116+
117+
:rtype: list of string, or None
118+
:returns: names of date fields in the index, or None if no
119+
resource information is available.
120+
"""
121+
return self._list_field_names('dateFields')
122+
123+
@property
124+
def number_fields(self):
125+
"""Names of number fields in the index.
126+
127+
:rtype: list of string, or None
128+
:returns: names of number fields in the index, or None if no
129+
resource information is available.
130+
"""
131+
return self._list_field_names('numberFields')
132+
133+
@property
134+
def geo_fields(self):
135+
"""Names of geo fields in the index.
136+
137+
:rtype: list of string, or None
138+
:returns: names of geo fields in the index, or None if no
139+
resource information is available.
140+
"""
141+
return self._list_field_names('geoFields')
142+
143+
def _set_properties(self, api_response):
144+
"""Update properties from resource in body of ``api_response``
145+
146+
:type api_response: httplib2.Response
147+
:param api_response: response returned from an API call
148+
"""
149+
self._properties.clear()
150+
self._properties.update(api_response)

gcloud/search/test_client.py

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717

1818
class TestClient(unittest2.TestCase):
19+
PROJECT = 'PROJECT'
1920

2021
def _getTargetClass(self):
2122
from gcloud.search.client import Client
@@ -26,14 +27,111 @@ def _makeOne(self, *args, **kw):
2627

2728
def test_ctor(self):
2829
from gcloud.search.connection import Connection
29-
PROJECT = 'PROJECT'
3030
creds = _Credentials()
3131
http = object()
32-
client = self._makeOne(project=PROJECT, credentials=creds, http=http)
32+
client = self._makeOne(
33+
project=self.PROJECT, credentials=creds, http=http)
3334
self.assertTrue(isinstance(client.connection, Connection))
3435
self.assertTrue(client.connection.credentials is creds)
3536
self.assertTrue(client.connection.http is http)
3637

38+
def test_list_indexes_defaults(self):
39+
from gcloud.search.index import Index
40+
INDEX_1 = 'index-one'
41+
INDEX_2 = 'index-two'
42+
PATH = 'projects/%s/indexes' % self.PROJECT
43+
TOKEN = 'TOKEN'
44+
DATA = {
45+
'nextPageToken': TOKEN,
46+
'indexes': [
47+
{'project': self.PROJECT,
48+
'indexId': INDEX_1},
49+
{'project': self.PROJECT,
50+
'indexId': INDEX_2},
51+
]
52+
}
53+
creds = _Credentials()
54+
client = self._makeOne(self.PROJECT, creds)
55+
conn = client.connection = _Connection(DATA)
56+
57+
zones, token = client.list_indexes()
58+
59+
self.assertEqual(len(zones), len(DATA['indexes']))
60+
for found, expected in zip(zones, DATA['indexes']):
61+
self.assertTrue(isinstance(found, Index))
62+
self.assertEqual(found.name, expected['indexId'])
63+
self.assertEqual(found.text_fields, None)
64+
self.assertEqual(found.atom_fields, None)
65+
self.assertEqual(found.html_fields, None)
66+
self.assertEqual(found.date_fields, None)
67+
self.assertEqual(found.number_fields, None)
68+
self.assertEqual(found.geo_fields, None)
69+
self.assertEqual(token, TOKEN)
70+
71+
self.assertEqual(len(conn._requested), 1)
72+
req = conn._requested[0]
73+
self.assertEqual(req['method'], 'GET')
74+
self.assertEqual(req['path'], '/%s' % PATH)
75+
76+
def test_list_indexes_explicit(self):
77+
from gcloud.search.index import Index
78+
INDEX_1 = 'index-one'
79+
INDEX_2 = 'index-two'
80+
PATH = 'projects/%s/indexes' % self.PROJECT
81+
TOKEN = 'TOKEN'
82+
DATA = {
83+
'indexes': [
84+
{'project': self.PROJECT,
85+
'indexId': INDEX_1,
86+
'indexedField': {'textFields': ['text-1']}},
87+
{'project': self.PROJECT,
88+
'indexId': INDEX_2,
89+
'indexedField': {'htmlFields': ['html-1']}},
90+
]
91+
}
92+
creds = _Credentials()
93+
client = self._makeOne(self.PROJECT, creds)
94+
conn = client.connection = _Connection(DATA)
95+
96+
zones, token = client.list_indexes(
97+
max_results=3, page_token=TOKEN, prefix='index', view='FULL')
98+
99+
self.assertEqual(len(zones), len(DATA['indexes']))
100+
for found, expected in zip(zones, DATA['indexes']):
101+
self.assertTrue(isinstance(found, Index))
102+
self.assertEqual(found.name, expected['indexId'])
103+
field_info = expected['indexedField']
104+
self.assertEqual(found.text_fields, field_info.get('textFields'))
105+
self.assertEqual(found.atom_fields, field_info.get('atomFields'))
106+
self.assertEqual(found.html_fields, field_info.get('htmlFields'))
107+
self.assertEqual(found.date_fields, field_info.get('dateFields'))
108+
self.assertEqual(found.number_fields,
109+
field_info.get('numberFields'))
110+
self.assertEqual(found.geo_fields, field_info.get('geoFields'))
111+
self.assertEqual(token, None)
112+
113+
self.assertEqual(len(conn._requested), 1)
114+
req = conn._requested[0]
115+
self.assertEqual(req['method'], 'GET')
116+
self.assertEqual(req['path'], '/%s' % PATH)
117+
self.assertEqual(req['query_params'],
118+
{'indexNamePrefix': 'index',
119+
'pageSize': 3,
120+
'pageToken': TOKEN,
121+
'view': 'FULL'})
122+
123+
def test_index(self):
124+
from gcloud.search.index import Index
125+
INDEX_ID = 'index-id'
126+
creds = _Credentials()
127+
http = object()
128+
client = self._makeOne(
129+
project=self.PROJECT, credentials=creds, http=http)
130+
index = client.index(INDEX_ID)
131+
self.assertTrue(isinstance(index, Index))
132+
self.assertEqual(index.name, INDEX_ID)
133+
self.assertTrue(index._client is client)
134+
37135

38136
class _Credentials(object):
39137

@@ -46,3 +144,15 @@ def create_scoped_required():
46144
def create_scoped(self, scope):
47145
self._scopes = scope
48146
return self
147+
148+
149+
class _Connection(object):
150+
151+
def __init__(self, *responses):
152+
self._responses = responses
153+
self._requested = []
154+
155+
def api_request(self, **kw):
156+
self._requested.append(kw)
157+
response, self._responses = self._responses[0], self._responses[1:]
158+
return response

0 commit comments

Comments
 (0)