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"""Common code for converting proto to other formats, such as JSON.""" 18 19import base64 20import collections 21import datetime 22import json 23import os 24import sys 25 26import six 27 28from apitools.base.protorpclite import message_types 29from apitools.base.protorpclite import messages 30from apitools.base.protorpclite import protojson 31from apitools.base.py import exceptions 32 33__all__ = [ 34 'CopyProtoMessage', 35 'JsonToMessage', 36 'MessageToJson', 37 'DictToMessage', 38 'MessageToDict', 39 'PyValueToMessage', 40 'MessageToPyValue', 41 'MessageToRepr', 42 'GetCustomJsonFieldMapping', 43 'AddCustomJsonFieldMapping', 44 'GetCustomJsonEnumMapping', 45 'AddCustomJsonEnumMapping', 46] 47 48 49_Codec = collections.namedtuple('_Codec', ['encoder', 'decoder']) 50CodecResult = collections.namedtuple('CodecResult', ['value', 'complete']) 51 52 53# TODO(craigcitro): Make these non-global. 54_UNRECOGNIZED_FIELD_MAPPINGS = {} 55_CUSTOM_MESSAGE_CODECS = {} 56_CUSTOM_FIELD_CODECS = {} 57_FIELD_TYPE_CODECS = {} 58 59 60def MapUnrecognizedFields(field_name): 61 """Register field_name as a container for unrecognized fields.""" 62 def Register(cls): 63 _UNRECOGNIZED_FIELD_MAPPINGS[cls] = field_name 64 return cls 65 return Register 66 67 68def RegisterCustomMessageCodec(encoder, decoder): 69 """Register a custom encoder/decoder for this message class.""" 70 def Register(cls): 71 _CUSTOM_MESSAGE_CODECS[cls] = _Codec(encoder=encoder, decoder=decoder) 72 return cls 73 return Register 74 75 76def RegisterCustomFieldCodec(encoder, decoder): 77 """Register a custom encoder/decoder for this field.""" 78 def Register(field): 79 _CUSTOM_FIELD_CODECS[field] = _Codec(encoder=encoder, decoder=decoder) 80 return field 81 return Register 82 83 84def RegisterFieldTypeCodec(encoder, decoder): 85 """Register a custom encoder/decoder for all fields of this type.""" 86 def Register(field_type): 87 _FIELD_TYPE_CODECS[field_type] = _Codec( 88 encoder=encoder, decoder=decoder) 89 return field_type 90 return Register 91 92 93# TODO(craigcitro): Delete this function with the switch to proto2. 94def CopyProtoMessage(message): 95 codec = protojson.ProtoJson() 96 return codec.decode_message(type(message), codec.encode_message(message)) 97 98 99def MessageToJson(message, include_fields=None): 100 """Convert the given message to JSON.""" 101 result = _ProtoJsonApiTools.Get().encode_message(message) 102 return _IncludeFields(result, message, include_fields) 103 104 105def JsonToMessage(message_type, message): 106 """Convert the given JSON to a message of type message_type.""" 107 return _ProtoJsonApiTools.Get().decode_message(message_type, message) 108 109 110# TODO(craigcitro): Do this directly, instead of via JSON. 111def DictToMessage(d, message_type): 112 """Convert the given dictionary to a message of type message_type.""" 113 return JsonToMessage(message_type, json.dumps(d)) 114 115 116def MessageToDict(message): 117 """Convert the given message to a dictionary.""" 118 return json.loads(MessageToJson(message)) 119 120 121def DictToProtoMap(properties, additional_property_type, sort_items=False): 122 """Convert the given dictionary to an AdditionalProperty message.""" 123 items = properties.items() 124 if sort_items: 125 items = sorted(items) 126 map_ = [] 127 for key, value in items: 128 map_.append(additional_property_type.AdditionalProperty( 129 key=key, value=value)) 130 return additional_property_type(additional_properties=map_) 131 132 133def PyValueToMessage(message_type, value): 134 """Convert the given python value to a message of type message_type.""" 135 return JsonToMessage(message_type, json.dumps(value)) 136 137 138def MessageToPyValue(message): 139 """Convert the given message to a python value.""" 140 return json.loads(MessageToJson(message)) 141 142 143def MessageToRepr(msg, multiline=False, **kwargs): 144 """Return a repr-style string for a protorpc message. 145 146 protorpc.Message.__repr__ does not return anything that could be considered 147 python code. Adding this function lets us print a protorpc message in such 148 a way that it could be pasted into code later, and used to compare against 149 other things. 150 151 Args: 152 msg: protorpc.Message, the message to be repr'd. 153 multiline: bool, True if the returned string should have each field 154 assignment on its own line. 155 **kwargs: {str:str}, Additional flags for how to format the string. 156 157 Known **kwargs: 158 shortstrings: bool, True if all string values should be 159 truncated at 100 characters, since when mocking the contents 160 typically don't matter except for IDs, and IDs are usually 161 less than 100 characters. 162 no_modules: bool, True if the long module name should not be printed with 163 each type. 164 165 Returns: 166 str, A string of valid python (assuming the right imports have been made) 167 that recreates the message passed into this function. 168 169 """ 170 171 # TODO(jasmuth): craigcitro suggests a pretty-printer from apitools/gen. 172 173 indent = kwargs.get('indent', 0) 174 175 def IndentKwargs(kwargs): 176 kwargs = dict(kwargs) 177 kwargs['indent'] = kwargs.get('indent', 0) + 4 178 return kwargs 179 180 if isinstance(msg, list): 181 s = '[' 182 for item in msg: 183 if multiline: 184 s += '\n' + ' ' * (indent + 4) 185 s += MessageToRepr( 186 item, multiline=multiline, **IndentKwargs(kwargs)) + ',' 187 if multiline: 188 s += '\n' + ' ' * indent 189 s += ']' 190 return s 191 192 if isinstance(msg, messages.Message): 193 s = type(msg).__name__ + '(' 194 if not kwargs.get('no_modules'): 195 s = msg.__module__ + '.' + s 196 names = sorted([field.name for field in msg.all_fields()]) 197 for name in names: 198 field = msg.field_by_name(name) 199 if multiline: 200 s += '\n' + ' ' * (indent + 4) 201 value = getattr(msg, field.name) 202 s += field.name + '=' + MessageToRepr( 203 value, multiline=multiline, **IndentKwargs(kwargs)) + ',' 204 if multiline: 205 s += '\n' + ' ' * indent 206 s += ')' 207 return s 208 209 if isinstance(msg, six.string_types): 210 if kwargs.get('shortstrings') and len(msg) > 100: 211 msg = msg[:100] 212 213 if isinstance(msg, datetime.datetime): 214 215 class SpecialTZInfo(datetime.tzinfo): 216 217 def __init__(self, offset): 218 super(SpecialTZInfo, self).__init__() 219 self.offset = offset 220 221 def __repr__(self): 222 s = 'TimeZoneOffset(' + repr(self.offset) + ')' 223 if not kwargs.get('no_modules'): 224 s = 'apitools.base.protorpclite.util.' + s 225 return s 226 227 msg = datetime.datetime( 228 msg.year, msg.month, msg.day, msg.hour, msg.minute, msg.second, 229 msg.microsecond, SpecialTZInfo(msg.tzinfo.utcoffset(0))) 230 231 return repr(msg) 232 233 234def _GetField(message, field_path): 235 for field in field_path: 236 if field not in dir(message): 237 raise KeyError('no field "%s"' % field) 238 message = getattr(message, field) 239 return message 240 241 242def _SetField(dictblob, field_path, value): 243 for field in field_path[:-1]: 244 dictblob = dictblob.setdefault(field, {}) 245 dictblob[field_path[-1]] = value 246 247 248def _IncludeFields(encoded_message, message, include_fields): 249 """Add the requested fields to the encoded message.""" 250 if include_fields is None: 251 return encoded_message 252 result = json.loads(encoded_message) 253 for field_name in include_fields: 254 try: 255 value = _GetField(message, field_name.split('.')) 256 nullvalue = None 257 if isinstance(value, list): 258 nullvalue = [] 259 except KeyError: 260 raise exceptions.InvalidDataError( 261 'No field named %s in message of type %s' % ( 262 field_name, type(message))) 263 _SetField(result, field_name.split('.'), nullvalue) 264 return json.dumps(result) 265 266 267def _GetFieldCodecs(field, attr): 268 result = [ 269 getattr(_CUSTOM_FIELD_CODECS.get(field), attr, None), 270 getattr(_FIELD_TYPE_CODECS.get(type(field)), attr, None), 271 ] 272 return [x for x in result if x is not None] 273 274 275class _ProtoJsonApiTools(protojson.ProtoJson): 276 277 """JSON encoder used by apitools clients.""" 278 _INSTANCE = None 279 280 @classmethod 281 def Get(cls): 282 if cls._INSTANCE is None: 283 cls._INSTANCE = cls() 284 return cls._INSTANCE 285 286 def decode_message(self, message_type, encoded_message): 287 if message_type in _CUSTOM_MESSAGE_CODECS: 288 return _CUSTOM_MESSAGE_CODECS[ 289 message_type].decoder(encoded_message) 290 result = _DecodeCustomFieldNames(message_type, encoded_message) 291 result = super(_ProtoJsonApiTools, self).decode_message( 292 message_type, result) 293 result = _ProcessUnknownEnums(result, encoded_message) 294 result = _ProcessUnknownMessages(result, encoded_message) 295 return _DecodeUnknownFields(result, encoded_message) 296 297 def decode_field(self, field, value): 298 """Decode the given JSON value. 299 300 Args: 301 field: a messages.Field for the field we're decoding. 302 value: a python value we'd like to decode. 303 304 Returns: 305 A value suitable for assignment to field. 306 """ 307 for decoder in _GetFieldCodecs(field, 'decoder'): 308 result = decoder(field, value) 309 value = result.value 310 if result.complete: 311 return value 312 if isinstance(field, messages.MessageField): 313 field_value = self.decode_message( 314 field.message_type, json.dumps(value)) 315 elif isinstance(field, messages.EnumField): 316 value = GetCustomJsonEnumMapping( 317 field.type, json_name=value) or value 318 try: 319 field_value = super( 320 _ProtoJsonApiTools, self).decode_field(field, value) 321 except messages.DecodeError: 322 if not isinstance(value, six.string_types): 323 raise 324 field_value = None 325 else: 326 field_value = super( 327 _ProtoJsonApiTools, self).decode_field(field, value) 328 return field_value 329 330 def encode_message(self, message): 331 if isinstance(message, messages.FieldList): 332 return '[%s]' % (', '.join(self.encode_message(x) 333 for x in message)) 334 335 # pylint: disable=unidiomatic-typecheck 336 if type(message) in _CUSTOM_MESSAGE_CODECS: 337 return _CUSTOM_MESSAGE_CODECS[type(message)].encoder(message) 338 339 message = _EncodeUnknownFields(message) 340 result = super(_ProtoJsonApiTools, self).encode_message(message) 341 result = _EncodeCustomFieldNames(message, result) 342 return json.dumps(json.loads(result), sort_keys=True) 343 344 def encode_field(self, field, value): 345 """Encode the given value as JSON. 346 347 Args: 348 field: a messages.Field for the field we're encoding. 349 value: a value for field. 350 351 Returns: 352 A python value suitable for json.dumps. 353 """ 354 for encoder in _GetFieldCodecs(field, 'encoder'): 355 result = encoder(field, value) 356 value = result.value 357 if result.complete: 358 return value 359 if isinstance(field, messages.EnumField): 360 if field.repeated: 361 remapped_value = [GetCustomJsonEnumMapping( 362 field.type, python_name=e.name) or e.name for e in value] 363 else: 364 remapped_value = GetCustomJsonEnumMapping( 365 field.type, python_name=value.name) 366 if remapped_value: 367 return remapped_value 368 if (isinstance(field, messages.MessageField) and 369 not isinstance(field, message_types.DateTimeField)): 370 value = json.loads(self.encode_message(value)) 371 return super(_ProtoJsonApiTools, self).encode_field(field, value) 372 373 374# TODO(craigcitro): Fold this and _IncludeFields in as codecs. 375def _DecodeUnknownFields(message, encoded_message): 376 """Rewrite unknown fields in message into message.destination.""" 377 destination = _UNRECOGNIZED_FIELD_MAPPINGS.get(type(message)) 378 if destination is None: 379 return message 380 pair_field = message.field_by_name(destination) 381 if not isinstance(pair_field, messages.MessageField): 382 raise exceptions.InvalidDataFromServerError( 383 'Unrecognized fields must be mapped to a compound ' 384 'message type.') 385 pair_type = pair_field.message_type 386 # TODO(craigcitro): Add more error checking around the pair 387 # type being exactly what we suspect (field names, etc). 388 if isinstance(pair_type.value, messages.MessageField): 389 new_values = _DecodeUnknownMessages( 390 message, json.loads(encoded_message), pair_type) 391 else: 392 new_values = _DecodeUnrecognizedFields(message, pair_type) 393 setattr(message, destination, new_values) 394 # We could probably get away with not setting this, but 395 # why not clear it? 396 setattr(message, '_Message__unrecognized_fields', {}) 397 return message 398 399 400def _DecodeUnknownMessages(message, encoded_message, pair_type): 401 """Process unknown fields in encoded_message of a message type.""" 402 field_type = pair_type.value.type 403 new_values = [] 404 all_field_names = [x.name for x in message.all_fields()] 405 for name, value_dict in six.iteritems(encoded_message): 406 if name in all_field_names: 407 continue 408 value = PyValueToMessage(field_type, value_dict) 409 if pair_type.value.repeated: 410 value = _AsMessageList(value) 411 new_pair = pair_type(key=name, value=value) 412 new_values.append(new_pair) 413 return new_values 414 415 416def _DecodeUnrecognizedFields(message, pair_type): 417 """Process unrecognized fields in message.""" 418 new_values = [] 419 for unknown_field in message.all_unrecognized_fields(): 420 # TODO(craigcitro): Consider validating the variant if 421 # the assignment below doesn't take care of it. It may 422 # also be necessary to check it in the case that the 423 # type has multiple encodings. 424 value, _ = message.get_unrecognized_field_info(unknown_field) 425 value_type = pair_type.field_by_name('value') 426 if isinstance(value_type, messages.MessageField): 427 decoded_value = DictToMessage(value, pair_type.value.message_type) 428 else: 429 decoded_value = protojson.ProtoJson().decode_field( 430 pair_type.value, value) 431 new_pair = pair_type(key=str(unknown_field), value=decoded_value) 432 new_values.append(new_pair) 433 return new_values 434 435 436def _EncodeUnknownFields(message): 437 """Remap unknown fields in message out of message.source.""" 438 source = _UNRECOGNIZED_FIELD_MAPPINGS.get(type(message)) 439 if source is None: 440 return message 441 result = CopyProtoMessage(message) 442 pairs_field = message.field_by_name(source) 443 if not isinstance(pairs_field, messages.MessageField): 444 raise exceptions.InvalidUserInputError( 445 'Invalid pairs field %s' % pairs_field) 446 pairs_type = pairs_field.message_type 447 value_variant = pairs_type.field_by_name('value').variant 448 pairs = getattr(message, source) 449 for pair in pairs: 450 if value_variant == messages.Variant.MESSAGE: 451 encoded_value = MessageToDict(pair.value) 452 else: 453 encoded_value = pair.value 454 result.set_unrecognized_field(pair.key, encoded_value, value_variant) 455 setattr(result, source, []) 456 return result 457 458 459def _SafeEncodeBytes(field, value): 460 """Encode the bytes in value as urlsafe base64.""" 461 try: 462 if field.repeated: 463 result = [base64.urlsafe_b64encode(byte) for byte in value] 464 else: 465 result = base64.urlsafe_b64encode(value) 466 complete = True 467 except TypeError: 468 result = value 469 complete = False 470 return CodecResult(value=result, complete=complete) 471 472 473def _SafeDecodeBytes(unused_field, value): 474 """Decode the urlsafe base64 value into bytes.""" 475 try: 476 result = base64.urlsafe_b64decode(str(value)) 477 complete = True 478 except TypeError: 479 result = value 480 complete = False 481 return CodecResult(value=result, complete=complete) 482 483 484def _ProcessUnknownEnums(message, encoded_message): 485 """Add unknown enum values from encoded_message as unknown fields. 486 487 ProtoRPC diverges from the usual protocol buffer behavior here and 488 doesn't allow unknown fields. Throwing on unknown fields makes it 489 impossible to let servers add new enum values and stay compatible 490 with older clients, which isn't reasonable for us. We simply store 491 unrecognized enum values as unknown fields, and all is well. 492 493 Args: 494 message: Proto message we've decoded thus far. 495 encoded_message: JSON string we're decoding. 496 497 Returns: 498 message, with any unknown enums stored as unrecognized fields. 499 """ 500 if not encoded_message: 501 return message 502 decoded_message = json.loads(encoded_message) 503 for field in message.all_fields(): 504 if (isinstance(field, messages.EnumField) and 505 field.name in decoded_message and 506 message.get_assigned_value(field.name) is None): 507 message.set_unrecognized_field( 508 field.name, decoded_message[field.name], messages.Variant.ENUM) 509 return message 510 511 512def _ProcessUnknownMessages(message, encoded_message): 513 """Store any remaining unknown fields as strings. 514 515 ProtoRPC currently ignores unknown values for which no type can be 516 determined (and logs a "No variant found" message). For the purposes 517 of reserializing, this is quite harmful (since it throws away 518 information). Here we simply add those as unknown fields of type 519 string (so that they can easily be reserialized). 520 521 Args: 522 message: Proto message we've decoded thus far. 523 encoded_message: JSON string we're decoding. 524 525 Returns: 526 message, with any remaining unrecognized fields saved. 527 """ 528 if not encoded_message: 529 return message 530 decoded_message = json.loads(encoded_message) 531 message_fields = [x.name for x in message.all_fields()] + list( 532 message.all_unrecognized_fields()) 533 missing_fields = [x for x in decoded_message.keys() 534 if x not in message_fields] 535 for field_name in missing_fields: 536 message.set_unrecognized_field(field_name, decoded_message[field_name], 537 messages.Variant.STRING) 538 return message 539 540 541RegisterFieldTypeCodec(_SafeEncodeBytes, _SafeDecodeBytes)(messages.BytesField) 542 543 544# Note that these could share a dictionary, since they're keyed by 545# distinct types, but it's not really worth it. 546_JSON_ENUM_MAPPINGS = {} 547_JSON_FIELD_MAPPINGS = {} 548 549 550def _GetTypeKey(message_type, package): 551 """Get the prefix for this message type in mapping dicts.""" 552 key = message_type.definition_name() 553 if package and key.startswith(package + '.'): 554 module_name = message_type.__module__ 555 # We normalize '__main__' to something unique, if possible. 556 if module_name == '__main__': 557 try: 558 file_name = sys.modules[module_name].__file__ 559 except (AttributeError, KeyError): 560 pass 561 else: 562 base_name = os.path.basename(file_name) 563 split_name = os.path.splitext(base_name) 564 if len(split_name) == 1: 565 module_name = unicode(base_name) 566 else: 567 module_name = u'.'.join(split_name[:-1]) 568 key = module_name + '.' + key.partition('.')[2] 569 return key 570 571 572def AddCustomJsonEnumMapping(enum_type, python_name, json_name, 573 package=''): 574 """Add a custom wire encoding for a given enum value. 575 576 This is primarily used in generated code, to handle enum values 577 which happen to be Python keywords. 578 579 Args: 580 enum_type: (messages.Enum) An enum type 581 python_name: (basestring) Python name for this value. 582 json_name: (basestring) JSON name to be used on the wire. 583 package: (basestring, optional) Package prefix for this enum, if 584 present. We strip this off the enum name in order to generate 585 unique keys. 586 """ 587 if not issubclass(enum_type, messages.Enum): 588 raise exceptions.TypecheckError( 589 'Cannot set JSON enum mapping for non-enum "%s"' % enum_type) 590 enum_name = _GetTypeKey(enum_type, package) 591 if python_name not in enum_type.names(): 592 raise exceptions.InvalidDataError( 593 'Enum value %s not a value for type %s' % (python_name, enum_type)) 594 field_mappings = _JSON_ENUM_MAPPINGS.setdefault(enum_name, {}) 595 _CheckForExistingMappings('enum', enum_type, python_name, json_name) 596 field_mappings[python_name] = json_name 597 598 599def AddCustomJsonFieldMapping(message_type, python_name, json_name, 600 package=''): 601 """Add a custom wire encoding for a given message field. 602 603 This is primarily used in generated code, to handle enum values 604 which happen to be Python keywords. 605 606 Args: 607 message_type: (messages.Message) A message type 608 python_name: (basestring) Python name for this value. 609 json_name: (basestring) JSON name to be used on the wire. 610 package: (basestring, optional) Package prefix for this message, if 611 present. We strip this off the message name in order to generate 612 unique keys. 613 """ 614 if not issubclass(message_type, messages.Message): 615 raise exceptions.TypecheckError( 616 'Cannot set JSON field mapping for ' 617 'non-message "%s"' % message_type) 618 message_name = _GetTypeKey(message_type, package) 619 try: 620 _ = message_type.field_by_name(python_name) 621 except KeyError: 622 raise exceptions.InvalidDataError( 623 'Field %s not recognized for type %s' % ( 624 python_name, message_type)) 625 field_mappings = _JSON_FIELD_MAPPINGS.setdefault(message_name, {}) 626 _CheckForExistingMappings('field', message_type, python_name, json_name) 627 field_mappings[python_name] = json_name 628 629 630def GetCustomJsonEnumMapping(enum_type, python_name=None, json_name=None): 631 """Return the appropriate remapping for the given enum, or None.""" 632 return _FetchRemapping(enum_type.definition_name(), 'enum', 633 python_name=python_name, json_name=json_name, 634 mappings=_JSON_ENUM_MAPPINGS) 635 636 637def GetCustomJsonFieldMapping(message_type, python_name=None, json_name=None): 638 """Return the appropriate remapping for the given field, or None.""" 639 return _FetchRemapping(message_type.definition_name(), 'field', 640 python_name=python_name, json_name=json_name, 641 mappings=_JSON_FIELD_MAPPINGS) 642 643 644def _FetchRemapping(type_name, mapping_type, python_name=None, json_name=None, 645 mappings=None): 646 """Common code for fetching a key or value from a remapping dict.""" 647 if python_name and json_name: 648 raise exceptions.InvalidDataError( 649 'Cannot specify both python_name and json_name ' 650 'for %s remapping' % mapping_type) 651 if not (python_name or json_name): 652 raise exceptions.InvalidDataError( 653 'Must specify either python_name or json_name for %s remapping' % ( 654 mapping_type,)) 655 field_remappings = mappings.get(type_name, {}) 656 if field_remappings: 657 if python_name: 658 return field_remappings.get(python_name) 659 elif json_name: 660 if json_name in list(field_remappings.values()): 661 return [k for k in field_remappings 662 if field_remappings[k] == json_name][0] 663 return None 664 665 666def _CheckForExistingMappings(mapping_type, message_type, 667 python_name, json_name): 668 """Validate that no mappings exist for the given values.""" 669 if mapping_type == 'field': 670 getter = GetCustomJsonFieldMapping 671 elif mapping_type == 'enum': 672 getter = GetCustomJsonEnumMapping 673 remapping = getter(message_type, python_name=python_name) 674 if remapping is not None and remapping != json_name: 675 raise exceptions.InvalidDataError( 676 'Cannot add mapping for %s "%s", already mapped to "%s"' % ( 677 mapping_type, python_name, remapping)) 678 remapping = getter(message_type, json_name=json_name) 679 if remapping is not None and remapping != python_name: 680 raise exceptions.InvalidDataError( 681 'Cannot add mapping for %s "%s", already mapped to "%s"' % ( 682 mapping_type, json_name, remapping)) 683 684 685def _EncodeCustomFieldNames(message, encoded_value): 686 message_name = type(message).definition_name() 687 field_remappings = list(_JSON_FIELD_MAPPINGS.get(message_name, {}).items()) 688 if field_remappings: 689 decoded_value = json.loads(encoded_value) 690 for python_name, json_name in field_remappings: 691 if python_name in encoded_value: 692 decoded_value[json_name] = decoded_value.pop(python_name) 693 encoded_value = json.dumps(decoded_value) 694 return encoded_value 695 696 697def _DecodeCustomFieldNames(message_type, encoded_message): 698 message_name = message_type.definition_name() 699 field_remappings = _JSON_FIELD_MAPPINGS.get(message_name, {}) 700 if field_remappings: 701 decoded_message = json.loads(encoded_message) 702 for python_name, json_name in list(field_remappings.items()): 703 if json_name in decoded_message: 704 decoded_message[python_name] = decoded_message.pop(json_name) 705 encoded_message = json.dumps(decoded_message) 706 return encoded_message 707 708 709def _AsMessageList(msg): 710 """Convert the provided list-as-JsonValue to a list.""" 711 # This really needs to live in extra_types, but extra_types needs 712 # to import this file to be able to register codecs. 713 # TODO(craigcitro): Split out a codecs module and fix this ugly 714 # import. 715 from apitools.base.py import extra_types 716 717 def _IsRepeatedJsonValue(msg): 718 """Return True if msg is a repeated value as a JsonValue.""" 719 if isinstance(msg, extra_types.JsonArray): 720 return True 721 if isinstance(msg, extra_types.JsonValue) and msg.array_value: 722 return True 723 return False 724 725 if not _IsRepeatedJsonValue(msg): 726 raise ValueError('invalid argument to _AsMessageList') 727 if isinstance(msg, extra_types.JsonValue): 728 msg = msg.array_value 729 if isinstance(msg, extra_types.JsonArray): 730 msg = msg.entries 731 return msg 732