Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(276)

Unified Diff: tools/telemetry/third_party/gsutilz/third_party/apitools/apitools/base/py/encoding.py

Issue 1264873003: Add gsutil/third_party to telemetry/third_party/gsutilz/third_party. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Remove httplib2 Created 5 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: tools/telemetry/third_party/gsutilz/third_party/apitools/apitools/base/py/encoding.py
diff --git a/tools/telemetry/third_party/gsutilz/third_party/apitools/apitools/base/py/encoding.py b/tools/telemetry/third_party/gsutilz/third_party/apitools/apitools/base/py/encoding.py
new file mode 100755
index 0000000000000000000000000000000000000000..c35e4cf3bff6cebcaf793d6eecdc049426a7076e
--- /dev/null
+++ b/tools/telemetry/third_party/gsutilz/third_party/apitools/apitools/base/py/encoding.py
@@ -0,0 +1,649 @@
+#!/usr/bin/env python
+"""Common code for converting proto to other formats, such as JSON."""
+
+import base64
+import collections
+import datetime
+import json
+import logging
+
+
+from protorpc import message_types
+from protorpc import messages
+from protorpc import protojson
+import six
+
+from apitools.base.py import exceptions
+
+__all__ = [
+ 'CopyProtoMessage',
+ 'JsonToMessage',
+ 'MessageToJson',
+ 'DictToMessage',
+ 'MessageToDict',
+ 'PyValueToMessage',
+ 'MessageToPyValue',
+ 'MessageToRepr',
+ 'GetCustomJsonFieldMapping',
+ 'AddCustomJsonFieldMapping',
+ 'GetCustomJsonEnumMapping',
+ 'AddCustomJsonEnumMapping',
+]
+
+
+_Codec = collections.namedtuple('_Codec', ['encoder', 'decoder'])
+CodecResult = collections.namedtuple('CodecResult', ['value', 'complete'])
+
+
+# TODO(craigcitro): Make these non-global.
+_UNRECOGNIZED_FIELD_MAPPINGS = {}
+_CUSTOM_MESSAGE_CODECS = {}
+_CUSTOM_FIELD_CODECS = {}
+_FIELD_TYPE_CODECS = {}
+
+
+def MapUnrecognizedFields(field_name):
+ """Register field_name as a container for unrecognized fields."""
+ def Register(cls):
+ _UNRECOGNIZED_FIELD_MAPPINGS[cls] = field_name
+ return cls
+ return Register
+
+
+def RegisterCustomMessageCodec(encoder, decoder):
+ """Register a custom encoder/decoder for this message class."""
+ def Register(cls):
+ _CUSTOM_MESSAGE_CODECS[cls] = _Codec(encoder=encoder, decoder=decoder)
+ return cls
+ return Register
+
+
+def RegisterCustomFieldCodec(encoder, decoder):
+ """Register a custom encoder/decoder for this field."""
+ def Register(field):
+ _CUSTOM_FIELD_CODECS[field] = _Codec(encoder=encoder, decoder=decoder)
+ return field
+ return Register
+
+
+def RegisterFieldTypeCodec(encoder, decoder):
+ """Register a custom encoder/decoder for all fields of this type."""
+ def Register(field_type):
+ _FIELD_TYPE_CODECS[field_type] = _Codec(
+ encoder=encoder, decoder=decoder)
+ return field_type
+ return Register
+
+
+# TODO(craigcitro): Delete this function with the switch to proto2.
+def CopyProtoMessage(message):
+ codec = protojson.ProtoJson()
+ return codec.decode_message(type(message), codec.encode_message(message))
+
+
+def MessageToJson(message, include_fields=None):
+ """Convert the given message to JSON."""
+ result = _ProtoJsonApiTools.Get().encode_message(message)
+ return _IncludeFields(result, message, include_fields)
+
+
+def JsonToMessage(message_type, message):
+ """Convert the given JSON to a message of type message_type."""
+ return _ProtoJsonApiTools.Get().decode_message(message_type, message)
+
+
+# TODO(craigcitro): Do this directly, instead of via JSON.
+def DictToMessage(d, message_type):
+ """Convert the given dictionary to a message of type message_type."""
+ return JsonToMessage(message_type, json.dumps(d))
+
+
+def MessageToDict(message):
+ """Convert the given message to a dictionary."""
+ return json.loads(MessageToJson(message))
+
+
+def PyValueToMessage(message_type, value):
+ """Convert the given python value to a message of type message_type."""
+ return JsonToMessage(message_type, json.dumps(value))
+
+
+def MessageToPyValue(message):
+ """Convert the given message to a python value."""
+ return json.loads(MessageToJson(message))
+
+
+def MessageToRepr(msg, multiline=False, **kwargs):
+ """Return a repr-style string for a protorpc message.
+
+ protorpc.Message.__repr__ does not return anything that could be considered
+ python code. Adding this function lets us print a protorpc message in such
+ a way that it could be pasted into code later, and used to compare against
+ other things.
+
+ Args:
+ msg: protorpc.Message, the message to be repr'd.
+ multiline: bool, True if the returned string should have each field
+ assignment on its own line.
+ **kwargs: {str:str}, Additional flags for how to format the string.
+
+ Known **kwargs:
+ shortstrings: bool, True if all string values should be
+ truncated at 100 characters, since when mocking the contents
+ typically don't matter except for IDs, and IDs are usually
+ less than 100 characters.
+ no_modules: bool, True if the long module name should not be printed with
+ each type.
+
+ Returns:
+ str, A string of valid python (assuming the right imports have been made)
+ that recreates the message passed into this function.
+
+ """
+
+ # TODO(user): craigcitro suggests a pretty-printer from apitools/gen.
+
+ indent = kwargs.get('indent', 0)
+
+ def IndentKwargs(kwargs):
+ kwargs = dict(kwargs)
+ kwargs['indent'] = kwargs.get('indent', 0) + 4
+ return kwargs
+
+ if isinstance(msg, list):
+ s = '['
+ for item in msg:
+ if multiline:
+ s += '\n' + ' ' * (indent + 4)
+ s += MessageToRepr(
+ item, multiline=multiline, **IndentKwargs(kwargs)) + ','
+ if multiline:
+ s += '\n' + ' ' * indent
+ s += ']'
+ return s
+
+ if isinstance(msg, messages.Message):
+ s = type(msg).__name__ + '('
+ if not kwargs.get('no_modules'):
+ s = msg.__module__ + '.' + s
+ names = sorted([field.name for field in msg.all_fields()])
+ for name in names:
+ field = msg.field_by_name(name)
+ if multiline:
+ s += '\n' + ' ' * (indent + 4)
+ value = getattr(msg, field.name)
+ s += field.name + '=' + MessageToRepr(
+ value, multiline=multiline, **IndentKwargs(kwargs)) + ','
+ if multiline:
+ s += '\n' + ' ' * indent
+ s += ')'
+ return s
+
+ if isinstance(msg, six.string_types):
+ if kwargs.get('shortstrings') and len(msg) > 100:
+ msg = msg[:100]
+
+ if isinstance(msg, datetime.datetime):
+
+ class SpecialTZInfo(datetime.tzinfo):
+
+ def __init__(self, offset):
+ super(SpecialTZInfo, self).__init__()
+ self.offset = offset
+
+ def __repr__(self):
+ s = 'TimeZoneOffset(' + repr(self.offset) + ')'
+ if not kwargs.get('no_modules'):
+ s = 'protorpc.util.' + s
+ return s
+
+ msg = datetime.datetime(
+ msg.year, msg.month, msg.day, msg.hour, msg.minute, msg.second,
+ msg.microsecond, SpecialTZInfo(msg.tzinfo.utcoffset(0)))
+
+ return repr(msg)
+
+
+def _GetField(message, field_path):
+ for field in field_path:
+ if field not in dir(message):
+ raise KeyError('no field "%s"' % field)
+ message = getattr(message, field)
+ return message
+
+
+def _SetField(dictblob, field_path, value):
+ for field in field_path[:-1]:
+ dictblob[field] = {}
+ dictblob = dictblob[field]
+ dictblob[field_path[-1]] = value
+
+
+def _IncludeFields(encoded_message, message, include_fields):
+ """Add the requested fields to the encoded message."""
+ if include_fields is None:
+ return encoded_message
+ result = json.loads(encoded_message)
+ for field_name in include_fields:
+ try:
+ value = _GetField(message, field_name.split('.'))
+ nullvalue = None
+ if isinstance(value, list):
+ nullvalue = []
+ except KeyError:
+ raise exceptions.InvalidDataError(
+ 'No field named %s in message of type %s' % (
+ field_name, type(message)))
+ _SetField(result, field_name.split('.'), nullvalue)
+ return json.dumps(result)
+
+
+def _GetFieldCodecs(field, attr):
+ result = [
+ getattr(_CUSTOM_FIELD_CODECS.get(field), attr, None),
+ getattr(_FIELD_TYPE_CODECS.get(type(field)), attr, None),
+ ]
+ return [x for x in result if x is not None]
+
+
+class _ProtoJsonApiTools(protojson.ProtoJson):
+
+ """JSON encoder used by apitools clients."""
+ _INSTANCE = None
+
+ @classmethod
+ def Get(cls):
+ if cls._INSTANCE is None:
+ cls._INSTANCE = cls()
+ return cls._INSTANCE
+
+ def decode_message(self, message_type, encoded_message):
+ if message_type in _CUSTOM_MESSAGE_CODECS:
+ return _CUSTOM_MESSAGE_CODECS[
+ message_type].decoder(encoded_message)
+ # We turn off the default logging in protorpc. We may want to
+ # remove this later.
+ old_level = logging.getLogger().level
+ logging.getLogger().setLevel(logging.ERROR)
+ result = _DecodeCustomFieldNames(message_type, encoded_message)
+ result = super(_ProtoJsonApiTools, self).decode_message(
+ message_type, result)
+ logging.getLogger().setLevel(old_level)
+ result = _ProcessUnknownEnums(result, encoded_message)
+ result = _ProcessUnknownMessages(result, encoded_message)
+ return _DecodeUnknownFields(result, encoded_message)
+
+ def decode_field(self, field, value):
+ """Decode the given JSON value.
+
+ Args:
+ field: a messages.Field for the field we're decoding.
+ value: a python value we'd like to decode.
+
+ Returns:
+ A value suitable for assignment to field.
+ """
+ for decoder in _GetFieldCodecs(field, 'decoder'):
+ result = decoder(field, value)
+ value = result.value
+ if result.complete:
+ return value
+ if isinstance(field, messages.MessageField):
+ field_value = self.decode_message(
+ field.message_type, json.dumps(value))
+ elif isinstance(field, messages.EnumField):
+ value = GetCustomJsonEnumMapping(
+ field.type, json_name=value) or value
+ try:
+ field_value = super(
+ _ProtoJsonApiTools, self).decode_field(field, value)
+ except messages.DecodeError:
+ if not isinstance(value, six.string_types):
+ raise
+ field_value = None
+ else:
+ field_value = super(
+ _ProtoJsonApiTools, self).decode_field(field, value)
+ return field_value
+
+ def encode_message(self, message):
+ if isinstance(message, messages.FieldList):
+ return '[%s]' % (', '.join(self.encode_message(x)
+ for x in message))
+ message_type = type(message)
+ if message_type in _CUSTOM_MESSAGE_CODECS:
+ return _CUSTOM_MESSAGE_CODECS[type(message)].encoder(message)
+ message = _EncodeUnknownFields(message)
+ result = super(_ProtoJsonApiTools, self).encode_message(message)
+ return _EncodeCustomFieldNames(message, result)
+
+ def encode_field(self, field, value):
+ """Encode the given value as JSON.
+
+ Args:
+ field: a messages.Field for the field we're encoding.
+ value: a value for field.
+
+ Returns:
+ A python value suitable for json.dumps.
+ """
+ for encoder in _GetFieldCodecs(field, 'encoder'):
+ result = encoder(field, value)
+ value = result.value
+ if result.complete:
+ return value
+ if isinstance(field, messages.EnumField):
+ if field.repeated:
+ remapped_value = [GetCustomJsonEnumMapping(
+ field.type, python_name=e.name) or e.name for e in value]
+ else:
+ remapped_value = GetCustomJsonEnumMapping(
+ field.type, python_name=value.name)
+ if remapped_value:
+ return remapped_value
+ if (isinstance(field, messages.MessageField) and
+ not isinstance(field, message_types.DateTimeField)):
+ value = json.loads(self.encode_message(value))
+ return super(_ProtoJsonApiTools, self).encode_field(field, value)
+
+
+# TODO(craigcitro): Fold this and _IncludeFields in as codecs.
+def _DecodeUnknownFields(message, encoded_message):
+ """Rewrite unknown fields in message into message.destination."""
+ destination = _UNRECOGNIZED_FIELD_MAPPINGS.get(type(message))
+ if destination is None:
+ return message
+ pair_field = message.field_by_name(destination)
+ if not isinstance(pair_field, messages.MessageField):
+ raise exceptions.InvalidDataFromServerError(
+ 'Unrecognized fields must be mapped to a compound '
+ 'message type.')
+ pair_type = pair_field.message_type
+ # TODO(craigcitro): Add more error checking around the pair
+ # type being exactly what we suspect (field names, etc).
+ if isinstance(pair_type.value, messages.MessageField):
+ new_values = _DecodeUnknownMessages(
+ message, json.loads(encoded_message), pair_type)
+ else:
+ new_values = _DecodeUnrecognizedFields(message, pair_type)
+ setattr(message, destination, new_values)
+ # We could probably get away with not setting this, but
+ # why not clear it?
+ setattr(message, '_Message__unrecognized_fields', {})
+ return message
+
+
+def _DecodeUnknownMessages(message, encoded_message, pair_type):
+ """Process unknown fields in encoded_message of a message type."""
+ field_type = pair_type.value.type
+ new_values = []
+ all_field_names = [x.name for x in message.all_fields()]
+ for name, value_dict in encoded_message.items():
+ if name in all_field_names:
+ continue
+ value = PyValueToMessage(field_type, value_dict)
+ new_pair = pair_type(key=name, value=value)
+ new_values.append(new_pair)
+ return new_values
+
+
+def _DecodeUnrecognizedFields(message, pair_type):
+ """Process unrecognized fields in message."""
+ new_values = []
+ for unknown_field in message.all_unrecognized_fields():
+ # TODO(craigcitro): Consider validating the variant if
+ # the assignment below doesn't take care of it. It may
+ # also be necessary to check it in the case that the
+ # type has multiple encodings.
+ value, _ = message.get_unrecognized_field_info(unknown_field)
+ value_type = pair_type.field_by_name('value')
+ if isinstance(value_type, messages.MessageField):
+ decoded_value = DictToMessage(value, pair_type.value.message_type)
+ else:
+ decoded_value = value
+ new_pair = pair_type(key=str(unknown_field), value=decoded_value)
+ new_values.append(new_pair)
+ return new_values
+
+
+def _EncodeUnknownFields(message):
+ """Remap unknown fields in message out of message.source."""
+ source = _UNRECOGNIZED_FIELD_MAPPINGS.get(type(message))
+ if source is None:
+ return message
+ result = CopyProtoMessage(message)
+ pairs_field = message.field_by_name(source)
+ if not isinstance(pairs_field, messages.MessageField):
+ raise exceptions.InvalidUserInputError(
+ 'Invalid pairs field %s' % pairs_field)
+ pairs_type = pairs_field.message_type
+ value_variant = pairs_type.field_by_name('value').variant
+ pairs = getattr(message, source)
+ for pair in pairs:
+ if value_variant == messages.Variant.MESSAGE:
+ encoded_value = MessageToDict(pair.value)
+ else:
+ encoded_value = pair.value
+ result.set_unrecognized_field(pair.key, encoded_value, value_variant)
+ setattr(result, source, [])
+ return result
+
+
+def _SafeEncodeBytes(field, value):
+ """Encode the bytes in value as urlsafe base64."""
+ try:
+ if field.repeated:
+ result = [base64.urlsafe_b64encode(byte) for byte in value]
+ else:
+ result = base64.urlsafe_b64encode(value)
+ complete = True
+ except TypeError:
+ result = value
+ complete = False
+ return CodecResult(value=result, complete=complete)
+
+
+def _SafeDecodeBytes(unused_field, value):
+ """Decode the urlsafe base64 value into bytes."""
+ try:
+ result = base64.urlsafe_b64decode(str(value))
+ complete = True
+ except TypeError:
+ result = value
+ complete = False
+ return CodecResult(value=result, complete=complete)
+
+
+def _ProcessUnknownEnums(message, encoded_message):
+ """Add unknown enum values from encoded_message as unknown fields.
+
+ ProtoRPC diverges from the usual protocol buffer behavior here and
+ doesn't allow unknown fields. Throwing on unknown fields makes it
+ impossible to let servers add new enum values and stay compatible
+ with older clients, which isn't reasonable for us. We simply store
+ unrecognized enum values as unknown fields, and all is well.
+
+ Args:
+ message: Proto message we've decoded thus far.
+ encoded_message: JSON string we're decoding.
+
+ Returns:
+ message, with any unknown enums stored as unrecognized fields.
+ """
+ if not encoded_message:
+ return message
+ decoded_message = json.loads(encoded_message)
+ for field in message.all_fields():
+ if (isinstance(field, messages.EnumField) and
+ field.name in decoded_message and
+ message.get_assigned_value(field.name) is None):
+ message.set_unrecognized_field(
+ field.name, decoded_message[field.name], messages.Variant.ENUM)
+ return message
+
+
+def _ProcessUnknownMessages(message, encoded_message):
+ """Store any remaining unknown fields as strings.
+
+ ProtoRPC currently ignores unknown values for which no type can be
+ determined (and logs a "No variant found" message). For the purposes
+ of reserializing, this is quite harmful (since it throws away
+ information). Here we simply add those as unknown fields of type
+ string (so that they can easily be reserialized).
+
+ Args:
+ message: Proto message we've decoded thus far.
+ encoded_message: JSON string we're decoding.
+
+ Returns:
+ message, with any remaining unrecognized fields saved.
+ """
+ if not encoded_message:
+ return message
+ decoded_message = json.loads(encoded_message)
+ message_fields = [x.name for x in message.all_fields()] + list(
+ message.all_unrecognized_fields())
+ missing_fields = [x for x in decoded_message.keys()
+ if x not in message_fields]
+ for field_name in missing_fields:
+ message.set_unrecognized_field(field_name, decoded_message[field_name],
+ messages.Variant.STRING)
+ return message
+
+
+RegisterFieldTypeCodec(_SafeEncodeBytes, _SafeDecodeBytes)(messages.BytesField)
+
+
+# Note that these could share a dictionary, since they're keyed by
+# distinct types, but it's not really worth it.
+_JSON_ENUM_MAPPINGS = {}
+_JSON_FIELD_MAPPINGS = {}
+
+
+def AddCustomJsonEnumMapping(enum_type, python_name, json_name):
+ """Add a custom wire encoding for a given enum value.
+
+ This is primarily used in generated code, to handle enum values
+ which happen to be Python keywords.
+
+ Args:
+ enum_type: (messages.Enum) An enum type
+ python_name: (string) Python name for this value.
+ json_name: (string) JSON name to be used on the wire.
+ """
+ if not issubclass(enum_type, messages.Enum):
+ raise exceptions.TypecheckError(
+ 'Cannot set JSON enum mapping for non-enum "%s"' % enum_type)
+ enum_name = enum_type.definition_name()
+ if python_name not in enum_type.names():
+ raise exceptions.InvalidDataError(
+ 'Enum value %s not a value for type %s' % (python_name, enum_type))
+ field_mappings = _JSON_ENUM_MAPPINGS.setdefault(enum_name, {})
+ _CheckForExistingMappings('enum', enum_type, python_name, json_name)
+ field_mappings[python_name] = json_name
+
+
+def AddCustomJsonFieldMapping(message_type, python_name, json_name):
+ """Add a custom wire encoding for a given message field.
+
+ This is primarily used in generated code, to handle enum values
+ which happen to be Python keywords.
+
+ Args:
+ message_type: (messages.Message) A message type
+ python_name: (string) Python name for this value.
+ json_name: (string) JSON name to be used on the wire.
+ """
+ if not issubclass(message_type, messages.Message):
+ raise exceptions.TypecheckError(
+ 'Cannot set JSON field mapping for '
+ 'non-message "%s"' % message_type)
+ message_name = message_type.definition_name()
+ try:
+ _ = message_type.field_by_name(python_name)
+ except KeyError:
+ raise exceptions.InvalidDataError(
+ 'Field %s not recognized for type %s' % (
+ python_name, message_type))
+ field_mappings = _JSON_FIELD_MAPPINGS.setdefault(message_name, {})
+ _CheckForExistingMappings('field', message_type, python_name, json_name)
+ field_mappings[python_name] = json_name
+
+
+def GetCustomJsonEnumMapping(enum_type, python_name=None, json_name=None):
+ """Return the appropriate remapping for the given enum, or None."""
+ return _FetchRemapping(enum_type.definition_name(), 'enum',
+ python_name=python_name, json_name=json_name,
+ mappings=_JSON_ENUM_MAPPINGS)
+
+
+def GetCustomJsonFieldMapping(message_type, python_name=None, json_name=None):
+ """Return the appropriate remapping for the given field, or None."""
+ return _FetchRemapping(message_type.definition_name(), 'field',
+ python_name=python_name, json_name=json_name,
+ mappings=_JSON_FIELD_MAPPINGS)
+
+
+def _FetchRemapping(type_name, mapping_type, python_name=None, json_name=None,
+ mappings=None):
+ """Common code for fetching a key or value from a remapping dict."""
+ if python_name and json_name:
+ raise exceptions.InvalidDataError(
+ 'Cannot specify both python_name and json_name '
+ 'for %s remapping' % mapping_type)
+ if not (python_name or json_name):
+ raise exceptions.InvalidDataError(
+ 'Must specify either python_name or json_name for %s remapping' % (
+ mapping_type,))
+ field_remappings = mappings.get(type_name, {})
+ if field_remappings:
+ if python_name:
+ return field_remappings.get(python_name)
+ elif json_name:
+ if json_name in list(field_remappings.values()):
+ return [k for k in field_remappings
+ if field_remappings[k] == json_name][0]
+ return None
+
+
+def _CheckForExistingMappings(mapping_type, message_type,
+ python_name, json_name):
+ """Validate that no mappings exist for the given values."""
+ if mapping_type == 'field':
+ getter = GetCustomJsonFieldMapping
+ elif mapping_type == 'enum':
+ getter = GetCustomJsonEnumMapping
+ remapping = getter(message_type, python_name=python_name)
+ if remapping is not None:
+ raise exceptions.InvalidDataError(
+ 'Cannot add mapping for %s "%s", already mapped to "%s"' % (
+ mapping_type, python_name, remapping))
+ remapping = getter(message_type, json_name=json_name)
+ if remapping is not None:
+ raise exceptions.InvalidDataError(
+ 'Cannot add mapping for %s "%s", already mapped to "%s"' % (
+ mapping_type, json_name, remapping))
+
+
+def _EncodeCustomFieldNames(message, encoded_value):
+ message_name = type(message).definition_name()
+ field_remappings = list(_JSON_FIELD_MAPPINGS.get(message_name, {}).items())
+ if field_remappings:
+ decoded_value = json.loads(encoded_value)
+ for python_name, json_name in field_remappings:
+ if python_name in encoded_value:
+ decoded_value[json_name] = decoded_value.pop(python_name)
+ encoded_value = json.dumps(decoded_value)
+ return encoded_value
+
+
+def _DecodeCustomFieldNames(message_type, encoded_message):
+ message_name = message_type.definition_name()
+ field_remappings = _JSON_FIELD_MAPPINGS.get(message_name, {})
+ if field_remappings:
+ decoded_message = json.loads(encoded_message)
+ for python_name, json_name in list(field_remappings.items()):
+ if json_name in decoded_message:
+ decoded_message[python_name] = decoded_message.pop(json_name)
+ encoded_message = json.dumps(decoded_message)
+ return encoded_message

Powered by Google App Engine
This is Rietveld 408576698