Skip to content

Commit 4b90837

Browse files
committed
Merge pull request #1173 from tseaver/search-document
Add document/field/value support.
2 parents bf69bee + 89f1a1e commit 4b90837

File tree

2 files changed

+962
-0
lines changed

2 files changed

+962
-0
lines changed

gcloud/search/document.py

Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
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 Document."""
16+
17+
import datetime
18+
19+
import six
20+
21+
from gcloud._helpers import UTC
22+
from gcloud._helpers import _RFC3339_MICROS
23+
from gcloud.exceptions import NotFound
24+
25+
26+
class StringValue(object):
27+
"""StringValues hold individual text values for a given field
28+
29+
See:
30+
https://cloud.google.com/search/reference/rest/google/cloudsearch/v1/FieldValue
31+
32+
:type string_value: string
33+
:param string_value: the actual value.
34+
35+
:type string_format: string
36+
:param string_format: how the value should be indexed: one of
37+
'ATOM', 'TEXT', 'HTML' (leave as ``None`` to
38+
use the server-supplied default).
39+
40+
:type language: string
41+
:param language: Human language of the text. Should be an ISO 639-1
42+
language code.
43+
"""
44+
45+
value_type = 'string'
46+
47+
def __init__(self, string_value, string_format=None, language=None):
48+
self.string_value = string_value
49+
self.string_format = string_format
50+
self.language = language
51+
52+
53+
class NumberValue(object):
54+
"""NumberValues hold individual numeric values for a given field
55+
56+
See:
57+
https://cloud.google.com/search/reference/rest/google/cloudsearch/v1/FieldValue
58+
59+
:type number_value: integer, float (long on Python2)
60+
:param number_value: the actual value.
61+
"""
62+
63+
value_type = 'number'
64+
65+
def __init__(self, number_value):
66+
self.number_value = number_value
67+
68+
69+
class TimestampValue(object):
70+
"""TimestampValues hold individual datetime values for a given field
71+
See:
72+
https://cloud.google.com/search/reference/rest/google/cloudsearch/v1/FieldValue
73+
74+
:type timestamp_value: class:``datetime.datetime``
75+
:param timestamp_value: the actual value.
76+
"""
77+
78+
value_type = 'timestamp'
79+
80+
def __init__(self, timestamp_value):
81+
self.timestamp_value = timestamp_value
82+
83+
84+
class GeoValue(object):
85+
"""GeoValues hold individual latitude/longitude values for a given field
86+
See:
87+
https://cloud.google.com/search/reference/rest/google/cloudsearch/v1/FieldValue
88+
89+
:type geo_value: tuple, (float, float)
90+
:param geo_value: latitude, longitude
91+
"""
92+
93+
value_type = 'geo'
94+
95+
def __init__(self, geo_value):
96+
self.geo_value = geo_value
97+
98+
99+
class Field(object):
100+
"""Fields hold values for a given document
101+
102+
See:
103+
https://cloud.google.com/search/reference/rest/google/cloudsearch/v1/FieldValueList
104+
105+
:type name: string
106+
:param name: field name
107+
"""
108+
109+
def __init__(self, name):
110+
self.name = name
111+
self.values = []
112+
113+
def add_value(self, value, **kw):
114+
"""Add a value to the field.
115+
116+
Selects type of value instance based on type of ``value``.
117+
118+
:type value: string, integer, float, datetime, or tuple (float, float)
119+
:param value: the field value to add.
120+
121+
:param kw: extra keyword arguments to be passed to the value instance
122+
constructor. Currently, only :class:`StringValue`
123+
expects / honors additional parameters.
124+
125+
:raises: ValueError if unable to match the type of ``value``.
126+
"""
127+
if isinstance(value, six.string_types):
128+
self.values.append(StringValue(value, **kw))
129+
elif isinstance(value, (six.integer_types, float)):
130+
self.values.append(NumberValue(value, **kw))
131+
elif isinstance(value, datetime.datetime):
132+
self.values.append(TimestampValue(value, **kw))
133+
elif isinstance(value, tuple):
134+
self.values.append(GeoValue(value, **kw))
135+
else:
136+
raise ValueError("Couldn't determine value type: %s" % (value,))
137+
138+
139+
class Document(object):
140+
"""Documents hold values for search within indexes.
141+
142+
See:
143+
https://cloud.google.com/search/reference/rest/v1/projects/indexes/documents
144+
145+
:type name: string
146+
:param name: the name of the document
147+
148+
:type index: :class:`gcloud.search.index.Index`
149+
:param index: the index to which the document belongs.
150+
151+
:type rank: positive integer
152+
:param rank: override the server-generated rank for ordering the document
153+
within in queries. If not passed, the server generates a
154+
timestamp-based value. See the ``rank`` entry on the
155+
page above for details.
156+
"""
157+
def __init__(self, name, index, rank=None):
158+
self.name = name
159+
self.index = index
160+
self.rank = rank
161+
self.fields = {}
162+
163+
@classmethod
164+
def from_api_repr(cls, resource, index):
165+
"""Factory: construct a document given its API representation
166+
167+
:type resource: dict
168+
:param resource: document resource representation returned from the API
169+
170+
:type index: :class:`gcloud.search.index.Index`
171+
:param index: Index holding the document.
172+
173+
:rtype: :class:`gcloud.search.document.Document`
174+
:returns: Document parsed from ``resource``.
175+
"""
176+
name = resource.get('docId')
177+
if name is None:
178+
raise KeyError(
179+
'Resource lacks required identity information: ["docId"]')
180+
rank = resource.get('rank')
181+
document = cls(name, index, rank)
182+
document._parse_fields_resource(resource)
183+
return document
184+
185+
def _parse_value_resource(self, resource):
186+
"""Helper for _parse_fields_resource"""
187+
if 'stringValue' in resource:
188+
string_format = resource.get('stringFormat')
189+
language = resource.get('lang')
190+
value = resource['stringValue']
191+
return StringValue(value, string_format, language)
192+
if 'numberValue' in resource:
193+
value = resource['numberValue']
194+
if isinstance(value, six.string_types):
195+
if '.' in value:
196+
value = float(value)
197+
else:
198+
value = int(value)
199+
return NumberValue(value)
200+
if 'timestampValue' in resource:
201+
stamp = resource['timestampValue']
202+
value = datetime.datetime.strptime(stamp, _RFC3339_MICROS)
203+
value = value.replace(tzinfo=UTC)
204+
return TimestampValue(value)
205+
if 'geoValue' in resource:
206+
lat_long = resource['geoValue']
207+
lat, long = [float(coord.strip()) for coord in lat_long.split(',')]
208+
return GeoValue((lat, long))
209+
raise ValueError("Unknown value type")
210+
211+
def _parse_fields_resource(self, resource):
212+
"""Helper for from_api_repr, create, reload"""
213+
self.fields.clear()
214+
for field_name, val_obj in resource.get('fields', {}).items():
215+
field = self.field(field_name)
216+
for value in val_obj['values']:
217+
field.values.append(self._parse_value_resource(value))
218+
219+
@property
220+
def path(self):
221+
"""URL path for the document's APIs"""
222+
return '%s/documents/%s' % (self.index.path, self.name)
223+
224+
def field(self, name):
225+
"""Construct a Field instance.
226+
227+
:type name: string
228+
:param name: field's name
229+
"""
230+
field = self.fields[name] = Field(name)
231+
return field
232+
233+
def _require_client(self, client):
234+
"""Check client or verify over-ride.
235+
236+
:type client: :class:`gcloud.search.client.Client` or ``NoneType``
237+
:param client: the client to use. If not passed, falls back to the
238+
``client`` stored on the index of the
239+
current document.
240+
241+
:rtype: :class:`gcloud.search.client.Client`
242+
:returns: The client passed in or the currently bound client.
243+
"""
244+
if client is None:
245+
client = self.index._client
246+
return client
247+
248+
def _build_value_resource(self, value):
249+
"""Helper for _build_fields_resource"""
250+
result = {}
251+
if value.value_type == 'string':
252+
result['stringValue'] = value.string_value
253+
if value.string_format is not None:
254+
result['stringFormat'] = value.string_format
255+
if value.language is not None:
256+
result['lang'] = value.language
257+
elif value.value_type == 'number':
258+
result['numberValue'] = value.number_value
259+
elif value.value_type == 'timestamp':
260+
stamp = value.timestamp_value.strftime(_RFC3339_MICROS)
261+
result['timestampValue'] = stamp
262+
elif value.value_type == 'geo':
263+
result['geoValue'] = '%s, %s' % value.geo_value
264+
else:
265+
raise ValueError('Unknown value_type: %s' % value.value_type)
266+
return result
267+
268+
def _build_fields_resource(self):
269+
"""Helper for create"""
270+
fields = {}
271+
for field_name, field in self.fields.items():
272+
if field.values:
273+
values = []
274+
fields[field_name] = {'values': values}
275+
for value in field.values:
276+
values.append(self._build_value_resource(value))
277+
return fields
278+
279+
def _set_properties(self, api_response):
280+
"""Helper for create, reload"""
281+
self.rank = api_response.get('rank')
282+
self._parse_fields_resource(api_response)
283+
284+
def create(self, client=None):
285+
"""API call: create the document via a PUT request
286+
287+
See:
288+
https://cloud.google.com/search/reference/rest/v1/projects/indexes/documents/create
289+
290+
:type client: :class:`gcloud.search.client.Client` or ``NoneType``
291+
:param client: the client to use. If not passed, falls back to the
292+
``client`` stored on the current document's index.
293+
"""
294+
data = {'docId': self.name}
295+
296+
if self.rank is not None:
297+
data['rank'] = self.rank
298+
299+
fields = self._build_fields_resource()
300+
if fields:
301+
data['fields'] = fields
302+
303+
client = self._require_client(client)
304+
api_response = client.connection.api_request(
305+
method='PUT', path=self.path, data=data)
306+
307+
self._set_properties(api_response)
308+
309+
def exists(self, client=None):
310+
"""API call: test existence of the document via a GET request
311+
312+
See
313+
https://cloud.google.com/search/reference/rest/v1/projects/indexes/documents/get
314+
315+
:type client: :class:`gcloud.search.client.Client` or ``NoneType``
316+
:param client: the client to use. If not passed, falls back to the
317+
``client`` stored on the current document's index.
318+
"""
319+
client = self._require_client(client)
320+
try:
321+
client.connection.api_request(method='GET', path=self.path)
322+
except NotFound:
323+
return False
324+
else:
325+
return True
326+
327+
def reload(self, client=None):
328+
"""API call: sync local document configuration via a GET request
329+
330+
See
331+
https://cloud.google.com/search/reference/rest/v1/projects/indexes/documents/get
332+
333+
:type client: :class:`gcloud.search.client.Client` or ``NoneType``
334+
:param client: the client to use. If not passed, falls back to the
335+
``client`` stored on the current document's index.
336+
"""
337+
client = self._require_client(client)
338+
api_response = client.connection.api_request(
339+
method='GET', path=self.path)
340+
self._set_properties(api_response)
341+
342+
def delete(self, client=None):
343+
"""API call: delete the document via a DELETE request.
344+
345+
See:
346+
https://cloud.google.com/search/reference/rest/v1/projects/indexes/documents/delete
347+
348+
:type client: :class:`gcloud.search.client.Client` or ``NoneType``
349+
:param client: the client to use. If not passed, falls back to the
350+
``client`` stored on the current document's index.
351+
"""
352+
client = self._require_client(client)
353+
client.connection.api_request(method='DELETE', path=self.path)

0 commit comments

Comments
 (0)