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