• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3# Copyright 2015 Google Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Extra types understood by apitools."""
18
19import datetime
20import json
21import numbers
22
23import six
24
25from apitools.base.protorpclite import message_types
26from apitools.base.protorpclite import messages
27from apitools.base.protorpclite import protojson
28from apitools.base.py import encoding_helper as encoding
29from apitools.base.py import exceptions
30from apitools.base.py import util
31
32if six.PY3:
33    from collections.abc import Iterable
34else:
35    from collections import Iterable
36
37__all__ = [
38    'DateField',
39    'DateTimeMessage',
40    'JsonArray',
41    'JsonObject',
42    'JsonValue',
43    'JsonProtoEncoder',
44    'JsonProtoDecoder',
45]
46
47# pylint:disable=invalid-name
48DateTimeMessage = message_types.DateTimeMessage
49# pylint:enable=invalid-name
50
51
52# We insert our own metaclass here to avoid letting ProtoRPC
53# register this as the default field type for strings.
54#  * since ProtoRPC does this via metaclasses, we don't have any
55#    choice but to use one ourselves
56#  * since a subclass's metaclass must inherit from its superclass's
57#    metaclass, we're forced to have this hard-to-read inheritance.
58#
59# pylint: disable=protected-access
60class _FieldMeta(messages._FieldMeta):
61
62    def __init__(cls, name, bases, dct):  # pylint: disable=no-self-argument
63        # pylint: disable=super-init-not-called,non-parent-init-called
64        type.__init__(cls, name, bases, dct)
65# pylint: enable=protected-access
66
67
68class DateField(six.with_metaclass(_FieldMeta, messages.Field)):
69
70    """Field definition for Date values."""
71
72    VARIANTS = frozenset([messages.Variant.STRING])
73    DEFAULT_VARIANT = messages.Variant.STRING
74    type = datetime.date
75
76
77def _ValidateJsonValue(json_value):
78    entries = [(f, json_value.get_assigned_value(f.name))
79               for f in json_value.all_fields()]
80    assigned_entries = [(f, value)
81                        for f, value in entries if value is not None]
82    if len(assigned_entries) != 1:
83        raise exceptions.InvalidDataError(
84            'Malformed JsonValue: %s' % json_value)
85
86
87def _JsonValueToPythonValue(json_value):
88    """Convert the given JsonValue to a json string."""
89    util.Typecheck(json_value, JsonValue)
90    _ValidateJsonValue(json_value)
91    if json_value.is_null:
92        return None
93    entries = [(f, json_value.get_assigned_value(f.name))
94               for f in json_value.all_fields()]
95    assigned_entries = [(f, value)
96                        for f, value in entries if value is not None]
97    field, value = assigned_entries[0]
98    if not isinstance(field, messages.MessageField):
99        return value
100    elif field.message_type is JsonObject:
101        return _JsonObjectToPythonValue(value)
102    elif field.message_type is JsonArray:
103        return _JsonArrayToPythonValue(value)
104
105
106def _JsonObjectToPythonValue(json_value):
107    util.Typecheck(json_value, JsonObject)
108    return dict([(prop.key, _JsonValueToPythonValue(prop.value)) for prop
109                 in json_value.properties])
110
111
112def _JsonArrayToPythonValue(json_value):
113    util.Typecheck(json_value, JsonArray)
114    return [_JsonValueToPythonValue(e) for e in json_value.entries]
115
116
117_MAXINT64 = 2 << 63 - 1
118_MININT64 = -(2 << 63)
119
120
121def _PythonValueToJsonValue(py_value):
122    """Convert the given python value to a JsonValue."""
123    if py_value is None:
124        return JsonValue(is_null=True)
125    if isinstance(py_value, bool):
126        return JsonValue(boolean_value=py_value)
127    if isinstance(py_value, six.string_types):
128        return JsonValue(string_value=py_value)
129    if isinstance(py_value, numbers.Number):
130        if isinstance(py_value, six.integer_types):
131            if _MININT64 < py_value < _MAXINT64:
132                return JsonValue(integer_value=py_value)
133        return JsonValue(double_value=float(py_value))
134    if isinstance(py_value, dict):
135        return JsonValue(object_value=_PythonValueToJsonObject(py_value))
136    if isinstance(py_value, Iterable):
137        return JsonValue(array_value=_PythonValueToJsonArray(py_value))
138    raise exceptions.InvalidDataError(
139        'Cannot convert "%s" to JsonValue' % py_value)
140
141
142def _PythonValueToJsonObject(py_value):
143    util.Typecheck(py_value, dict)
144    return JsonObject(
145        properties=[
146            JsonObject.Property(key=key, value=_PythonValueToJsonValue(value))
147            for key, value in py_value.items()])
148
149
150def _PythonValueToJsonArray(py_value):
151    return JsonArray(entries=list(map(_PythonValueToJsonValue, py_value)))
152
153
154class JsonValue(messages.Message):
155
156    """Any valid JSON value."""
157    # Is this JSON object `null`?
158    is_null = messages.BooleanField(1, default=False)
159
160    # Exactly one of the following is provided if is_null is False; none
161    # should be provided if is_null is True.
162    boolean_value = messages.BooleanField(2)
163    string_value = messages.StringField(3)
164    # We keep two numeric fields to keep int64 round-trips exact.
165    double_value = messages.FloatField(4, variant=messages.Variant.DOUBLE)
166    integer_value = messages.IntegerField(5, variant=messages.Variant.INT64)
167    # Compound types
168    object_value = messages.MessageField('JsonObject', 6)
169    array_value = messages.MessageField('JsonArray', 7)
170
171
172class JsonObject(messages.Message):
173
174    """A JSON object value.
175
176    Messages:
177      Property: A property of a JsonObject.
178
179    Fields:
180      properties: A list of properties of a JsonObject.
181    """
182
183    class Property(messages.Message):
184
185        """A property of a JSON object.
186
187        Fields:
188          key: Name of the property.
189          value: A JsonValue attribute.
190        """
191        key = messages.StringField(1)
192        value = messages.MessageField(JsonValue, 2)
193
194    properties = messages.MessageField(Property, 1, repeated=True)
195
196
197class JsonArray(messages.Message):
198
199    """A JSON array value."""
200    entries = messages.MessageField(JsonValue, 1, repeated=True)
201
202
203_JSON_PROTO_TO_PYTHON_MAP = {
204    JsonArray: _JsonArrayToPythonValue,
205    JsonObject: _JsonObjectToPythonValue,
206    JsonValue: _JsonValueToPythonValue,
207}
208_JSON_PROTO_TYPES = tuple(_JSON_PROTO_TO_PYTHON_MAP.keys())
209
210
211def _JsonProtoToPythonValue(json_proto):
212    util.Typecheck(json_proto, _JSON_PROTO_TYPES)
213    return _JSON_PROTO_TO_PYTHON_MAP[type(json_proto)](json_proto)
214
215
216def _PythonValueToJsonProto(py_value):
217    if isinstance(py_value, dict):
218        return _PythonValueToJsonObject(py_value)
219    if (isinstance(py_value, Iterable) and
220            not isinstance(py_value, six.string_types)):
221        return _PythonValueToJsonArray(py_value)
222    return _PythonValueToJsonValue(py_value)
223
224
225def _JsonProtoToJson(json_proto, unused_encoder=None):
226    return json.dumps(_JsonProtoToPythonValue(json_proto))
227
228
229def _JsonToJsonProto(json_data, unused_decoder=None):
230    return _PythonValueToJsonProto(json.loads(json_data))
231
232
233def _JsonToJsonValue(json_data, unused_decoder=None):
234    result = _PythonValueToJsonProto(json.loads(json_data))
235    if isinstance(result, JsonValue):
236        return result
237    elif isinstance(result, JsonObject):
238        return JsonValue(object_value=result)
239    elif isinstance(result, JsonArray):
240        return JsonValue(array_value=result)
241    else:
242        raise exceptions.InvalidDataError(
243            'Malformed JsonValue: %s' % json_data)
244
245
246# pylint:disable=invalid-name
247JsonProtoEncoder = _JsonProtoToJson
248JsonProtoDecoder = _JsonToJsonProto
249# pylint:enable=invalid-name
250encoding.RegisterCustomMessageCodec(
251    encoder=JsonProtoEncoder, decoder=_JsonToJsonValue)(JsonValue)
252encoding.RegisterCustomMessageCodec(
253    encoder=JsonProtoEncoder, decoder=JsonProtoDecoder)(JsonObject)
254encoding.RegisterCustomMessageCodec(
255    encoder=JsonProtoEncoder, decoder=JsonProtoDecoder)(JsonArray)
256
257
258def _EncodeDateTimeField(field, value):
259    result = protojson.ProtoJson().encode_field(field, value)
260    return encoding.CodecResult(value=result, complete=True)
261
262
263def _DecodeDateTimeField(unused_field, value):
264    result = protojson.ProtoJson().decode_field(
265        message_types.DateTimeField(1), value)
266    return encoding.CodecResult(value=result, complete=True)
267
268
269encoding.RegisterFieldTypeCodec(_EncodeDateTimeField, _DecodeDateTimeField)(
270    message_types.DateTimeField)
271
272
273def _EncodeInt64Field(field, value):
274    """Handle the special case of int64 as a string."""
275    capabilities = [
276        messages.Variant.INT64,
277        messages.Variant.UINT64,
278    ]
279    if field.variant not in capabilities:
280        return encoding.CodecResult(value=value, complete=False)
281
282    if field.repeated:
283        result = [str(x) for x in value]
284    else:
285        result = str(value)
286    return encoding.CodecResult(value=result, complete=True)
287
288
289def _DecodeInt64Field(unused_field, value):
290    # Don't need to do anything special, they're decoded just fine
291    return encoding.CodecResult(value=value, complete=False)
292
293
294encoding.RegisterFieldTypeCodec(_EncodeInt64Field, _DecodeInt64Field)(
295    messages.IntegerField)
296
297
298def _EncodeDateField(field, value):
299    """Encoder for datetime.date objects."""
300    if field.repeated:
301        result = [d.isoformat() for d in value]
302    else:
303        result = value.isoformat()
304    return encoding.CodecResult(value=result, complete=True)
305
306
307def _DecodeDateField(unused_field, value):
308    date = datetime.datetime.strptime(value, '%Y-%m-%d').date()
309    return encoding.CodecResult(value=date, complete=True)
310
311
312encoding.RegisterFieldTypeCodec(_EncodeDateField, _DecodeDateField)(DateField)
313