Home | History | Annotate | Download | only in py
      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 
     19 import base64
     20 import collections
     21 import datetime
     22 import json
     23 import os
     24 import sys
     25 
     26 import six
     27 
     28 from apitools.base.protorpclite import message_types
     29 from apitools.base.protorpclite import messages
     30 from apitools.base.protorpclite import protojson
     31 from 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'])
     50 CodecResult = 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 
     60 def 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 
     68 def 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 
     76 def 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 
     84 def 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.
     94 def CopyProtoMessage(message):
     95     codec = protojson.ProtoJson()
     96     return codec.decode_message(type(message), codec.encode_message(message))
     97 
     98 
     99 def 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 
    105 def 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.
    111 def 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 
    116 def MessageToDict(message):
    117     """Convert the given message to a dictionary."""
    118     return json.loads(MessageToJson(message))
    119 
    120 
    121 def 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 
    133 def 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 
    138 def MessageToPyValue(message):
    139     """Convert the given message to a python value."""
    140     return json.loads(MessageToJson(message))
    141 
    142 
    143 def 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 
    234 def _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 
    242 def _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 
    248 def _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 
    267 def _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 
    275 class _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.
    375 def _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 
    400 def _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 
    416 def _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 
    436 def _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 
    459 def _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 
    473 def _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 
    484 def _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 
    512 def _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 
    541 RegisterFieldTypeCodec(_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 
    550 def _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 
    572 def 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 
    599 def 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 
    630 def 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 
    637 def 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 
    644 def _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 
    666 def _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 
    685 def _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 
    697 def _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 
    709 def _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