|
| 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