| Index: tools/telemetry/third_party/gsutilz/third_party/protorpc/protorpc/protourlencode.py
|
| diff --git a/tools/telemetry/third_party/gsutilz/third_party/protorpc/protorpc/protourlencode.py b/tools/telemetry/third_party/gsutilz/third_party/protorpc/protorpc/protourlencode.py
|
| new file mode 100755
|
| index 0000000000000000000000000000000000000000..9f6059e090178f6770a9c84f2a2b7af0a1171585
|
| --- /dev/null
|
| +++ b/tools/telemetry/third_party/gsutilz/third_party/protorpc/protorpc/protourlencode.py
|
| @@ -0,0 +1,563 @@
|
| +#!/usr/bin/env python
|
| +#
|
| +# Copyright 2010 Google Inc.
|
| +#
|
| +# Licensed under the Apache License, Version 2.0 (the "License");
|
| +# you may not use this file except in compliance with the License.
|
| +# You may obtain a copy of the License at
|
| +#
|
| +# http://www.apache.org/licenses/LICENSE-2.0
|
| +#
|
| +# Unless required by applicable law or agreed to in writing, software
|
| +# distributed under the License is distributed on an "AS IS" BASIS,
|
| +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| +# See the License for the specific language governing permissions and
|
| +# limitations under the License.
|
| +#
|
| +
|
| +"""URL encoding support for messages types.
|
| +
|
| +Protocol support for URL encoded form parameters.
|
| +
|
| +Nested Fields:
|
| + Nested fields are repesented by dot separated names. For example, consider
|
| + the following messages:
|
| +
|
| + class WebPage(Message):
|
| +
|
| + title = StringField(1)
|
| + tags = StringField(2, repeated=True)
|
| +
|
| + class WebSite(Message):
|
| +
|
| + name = StringField(1)
|
| + home = MessageField(WebPage, 2)
|
| + pages = MessageField(WebPage, 3, repeated=True)
|
| +
|
| + And consider the object:
|
| +
|
| + page = WebPage()
|
| + page.title = 'Welcome to NewSite 2010'
|
| +
|
| + site = WebSite()
|
| + site.name = 'NewSite 2010'
|
| + site.home = page
|
| +
|
| + The URL encoded representation of this constellation of objects is.
|
| +
|
| + name=NewSite+2010&home.title=Welcome+to+NewSite+2010
|
| +
|
| + An object that exists but does not have any state can be represented with
|
| + a reference to its name alone with no value assigned to it. For example:
|
| +
|
| + page = WebSite()
|
| + page.name = 'My Empty Site'
|
| + page.home = WebPage()
|
| +
|
| + is represented as:
|
| +
|
| + name=My+Empty+Site&home=
|
| +
|
| + This represents a site with an empty uninitialized home page.
|
| +
|
| +Repeated Fields:
|
| + Repeated fields are represented by the name of and the index of each value
|
| + separated by a dash. For example, consider the following message:
|
| +
|
| + home = Page()
|
| + home.title = 'Nome'
|
| +
|
| + news = Page()
|
| + news.title = 'News'
|
| + news.tags = ['news', 'articles']
|
| +
|
| + instance = WebSite()
|
| + instance.name = 'Super fun site'
|
| + instance.pages = [home, news, preferences]
|
| +
|
| + An instance of this message can be represented as:
|
| +
|
| + name=Super+fun+site&page-0.title=Home&pages-1.title=News&...
|
| + pages-1.tags-0=new&pages-1.tags-1=articles
|
| +
|
| +Helper classes:
|
| +
|
| + URLEncodedRequestBuilder: Used for encapsulating the logic used for building
|
| + a request message from a URL encoded RPC.
|
| +"""
|
| +import six
|
| +
|
| +__author__ = 'rafek@google.com (Rafe Kaplan)'
|
| +
|
| +import cgi
|
| +import re
|
| +import urllib
|
| +
|
| +from . import message_types
|
| +from . import messages
|
| +from . import util
|
| +
|
| +__all__ = ['CONTENT_TYPE',
|
| + 'URLEncodedRequestBuilder',
|
| + 'encode_message',
|
| + 'decode_message',
|
| + ]
|
| +
|
| +CONTENT_TYPE = 'application/x-www-form-urlencoded'
|
| +
|
| +_FIELD_NAME_REGEX = re.compile(r'^([a-zA-Z_][a-zA-Z_0-9]*)(?:-([0-9]+))?$')
|
| +
|
| +
|
| +class URLEncodedRequestBuilder(object):
|
| + """Helper that encapsulates the logic used for building URL encoded messages.
|
| +
|
| + This helper is used to map query parameters from a URL encoded RPC to a
|
| + message instance.
|
| + """
|
| +
|
| + @util.positional(2)
|
| + def __init__(self, message, prefix=''):
|
| + """Constructor.
|
| +
|
| + Args:
|
| + message: Message instance to build from parameters.
|
| + prefix: Prefix expected at the start of valid parameters.
|
| + """
|
| + self.__parameter_prefix = prefix
|
| +
|
| + # The empty tuple indicates the root message, which has no path.
|
| + # __messages is a full cache that makes it very easy to look up message
|
| + # instances by their paths. See make_path for details about what a path
|
| + # is.
|
| + self.__messages = {(): message}
|
| +
|
| + # This is a cache that stores paths which have been checked for
|
| + # correctness. Correctness means that an index is present for repeated
|
| + # fields on the path and absent for non-repeated fields. The cache is
|
| + # also used to check that indexes are added in the right order so that
|
| + # dicontiguous ranges of indexes are ignored.
|
| + self.__checked_indexes = set([()])
|
| +
|
| + def make_path(self, parameter_name):
|
| + """Parse a parameter name and build a full path to a message value.
|
| +
|
| + The path of a method is a tuple of 2-tuples describing the names and
|
| + indexes within repeated fields from the root message (the message being
|
| + constructed by the builder) to an arbitrarily nested message within it.
|
| +
|
| + Each 2-tuple node of a path (name, index) is:
|
| + name: The name of the field that refers to the message instance.
|
| + index: The index within a repeated field that refers to the message
|
| + instance, None if not a repeated field.
|
| +
|
| + For example, consider:
|
| +
|
| + class VeryInner(messages.Message):
|
| + ...
|
| +
|
| + class Inner(messages.Message):
|
| +
|
| + very_inner = messages.MessageField(VeryInner, 1, repeated=True)
|
| +
|
| + class Outer(messages.Message):
|
| +
|
| + inner = messages.MessageField(Inner, 1)
|
| +
|
| + If this builder is building an instance of Outer, that instance is
|
| + referred to in the URL encoded parameters without a path. Therefore
|
| + its path is ().
|
| +
|
| + The child 'inner' is referred to by its path (('inner', None)).
|
| +
|
| + The first child of repeated field 'very_inner' on the Inner instance
|
| + is referred to by (('inner', None), ('very_inner', 0)).
|
| +
|
| + Examples:
|
| + # Correct reference to model where nation is a Message, district is
|
| + # repeated Message and county is any not repeated field type.
|
| + >>> make_path('nation.district-2.county')
|
| + (('nation', None), ('district', 2), ('county', None))
|
| +
|
| + # Field is not part of model.
|
| + >>> make_path('nation.made_up_field')
|
| + None
|
| +
|
| + # nation field is not repeated and index provided.
|
| + >>> make_path('nation-1')
|
| + None
|
| +
|
| + # district field is repeated and no index provided.
|
| + >>> make_path('nation.district')
|
| + None
|
| +
|
| + Args:
|
| + parameter_name: Name of query parameter as passed in from the request.
|
| + in order to make a path, this parameter_name must point to a valid
|
| + field within the message structure. Nodes of the path that refer to
|
| + repeated fields must be indexed with a number, non repeated nodes must
|
| + not have an index.
|
| +
|
| + Returns:
|
| + Parsed version of the parameter_name as a tuple of tuples:
|
| + attribute: Name of attribute associated with path.
|
| + index: Postitive integer index when it is a repeated field, else None.
|
| + Will return None if the parameter_name does not have the right prefix,
|
| + does not point to a field within the message structure, does not have
|
| + an index if it is a repeated field or has an index but is not a repeated
|
| + field.
|
| + """
|
| + if parameter_name.startswith(self.__parameter_prefix):
|
| + parameter_name = parameter_name[len(self.__parameter_prefix):]
|
| + else:
|
| + return None
|
| +
|
| + path = []
|
| + name = []
|
| + message_type = type(self.__messages[()]) # Get root message.
|
| +
|
| + for item in parameter_name.split('.'):
|
| + # This will catch sub_message.real_message_field.not_real_field
|
| + if not message_type:
|
| + return None
|
| +
|
| + item_match = _FIELD_NAME_REGEX.match(item)
|
| + if not item_match:
|
| + return None
|
| + attribute = item_match.group(1)
|
| + index = item_match.group(2)
|
| + if index:
|
| + index = int(index)
|
| +
|
| + try:
|
| + field = message_type.field_by_name(attribute)
|
| + except KeyError:
|
| + return None
|
| +
|
| + if field.repeated != (index is not None):
|
| + return None
|
| +
|
| + if isinstance(field, messages.MessageField):
|
| + message_type = field.message_type
|
| + else:
|
| + message_type = None
|
| +
|
| + # Path is valid so far. Append node and continue.
|
| + path.append((attribute, index))
|
| +
|
| + return tuple(path)
|
| +
|
| + def __check_index(self, parent_path, name, index):
|
| + """Check correct index use and value relative to a given path.
|
| +
|
| + Check that for a given path the index is present for repeated fields
|
| + and that it is in range for the existing list that it will be inserted
|
| + in to or appended to.
|
| +
|
| + Args:
|
| + parent_path: Path to check against name and index.
|
| + name: Name of field to check for existance.
|
| + index: Index to check. If field is repeated, should be a number within
|
| + range of the length of the field, or point to the next item for
|
| + appending.
|
| + """
|
| + # Don't worry about non-repeated fields.
|
| + # It's also ok if index is 0 because that means next insert will append.
|
| + if not index:
|
| + return True
|
| +
|
| + parent = self.__messages.get(parent_path, None)
|
| + value_list = getattr(parent, name, None)
|
| + # If the list does not exist then the index should be 0. Since it is
|
| + # not, path is not valid.
|
| + if not value_list:
|
| + return False
|
| +
|
| + # The index must either point to an element of the list or to the tail.
|
| + return len(value_list) >= index
|
| +
|
| + def __check_indexes(self, path):
|
| + """Check that all indexes are valid and in the right order.
|
| +
|
| + This method must iterate over the path and check that all references
|
| + to indexes point to an existing message or to the end of the list, meaning
|
| + the next value should be appended to the repeated field.
|
| +
|
| + Args:
|
| + path: Path to check indexes for. Tuple of 2-tuples (name, index). See
|
| + make_path for more information.
|
| +
|
| + Returns:
|
| + True if all the indexes of the path are within range, else False.
|
| + """
|
| + if path in self.__checked_indexes:
|
| + return True
|
| +
|
| + # Start with the root message.
|
| + parent_path = ()
|
| +
|
| + for name, index in path:
|
| + next_path = parent_path + ((name, index),)
|
| + # First look in the checked indexes cache.
|
| + if next_path not in self.__checked_indexes:
|
| + if not self.__check_index(parent_path, name, index):
|
| + return False
|
| + self.__checked_indexes.add(next_path)
|
| +
|
| + parent_path = next_path
|
| +
|
| + return True
|
| +
|
| + def __get_or_create_path(self, path):
|
| + """Get a message from the messages cache or create it and add it.
|
| +
|
| + This method will also create any parent messages based on the path.
|
| +
|
| + When a new instance of a given message is created, it is stored in
|
| + __message by its path.
|
| +
|
| + Args:
|
| + path: Path of message to get. Path must be valid, in other words
|
| + __check_index(path) returns true. Tuple of 2-tuples (name, index).
|
| + See make_path for more information.
|
| +
|
| + Returns:
|
| + Message instance if the field being pointed to by the path is a
|
| + message, else will return None for non-message fields.
|
| + """
|
| + message = self.__messages.get(path, None)
|
| + if message:
|
| + return message
|
| +
|
| + parent_path = ()
|
| + parent = self.__messages[()] # Get the root object
|
| +
|
| + for name, index in path:
|
| + field = parent.field_by_name(name)
|
| + next_path = parent_path + ((name, index),)
|
| + next_message = self.__messages.get(next_path, None)
|
| + if next_message is None:
|
| + next_message = field.message_type()
|
| + self.__messages[next_path] = next_message
|
| + if not field.repeated:
|
| + setattr(parent, field.name, next_message)
|
| + else:
|
| + list_value = getattr(parent, field.name, None)
|
| + if list_value is None:
|
| + setattr(parent, field.name, [next_message])
|
| + else:
|
| + list_value.append(next_message)
|
| +
|
| + parent_path = next_path
|
| + parent = next_message
|
| +
|
| + return parent
|
| +
|
| + def add_parameter(self, parameter, values):
|
| + """Add a single parameter.
|
| +
|
| + Adds a single parameter and its value to the request message.
|
| +
|
| + Args:
|
| + parameter: Query string parameter to map to request.
|
| + values: List of values to assign to request message.
|
| +
|
| + Returns:
|
| + True if parameter was valid and added to the message, else False.
|
| +
|
| + Raises:
|
| + DecodeError if the parameter refers to a valid field, and the values
|
| + parameter does not have one and only one value. Non-valid query
|
| + parameters may have multiple values and should not cause an error.
|
| + """
|
| + path = self.make_path(parameter)
|
| +
|
| + if not path:
|
| + return False
|
| +
|
| + # Must check that all indexes of all items in the path are correct before
|
| + # instantiating any of them. For example, consider:
|
| + #
|
| + # class Repeated(object):
|
| + # ...
|
| + #
|
| + # class Inner(object):
|
| + #
|
| + # repeated = messages.MessageField(Repeated, 1, repeated=True)
|
| + #
|
| + # class Outer(object):
|
| + #
|
| + # inner = messages.MessageField(Inner, 1)
|
| + #
|
| + # instance = Outer()
|
| + # builder = URLEncodedRequestBuilder(instance)
|
| + # builder.add_parameter('inner.repeated')
|
| + #
|
| + # assert not hasattr(instance, 'inner')
|
| + #
|
| + # The check is done relative to the instance of Outer pass in to the
|
| + # constructor of the builder. This instance is not referred to at all
|
| + # because all names are assumed to be relative to it.
|
| + #
|
| + # The 'repeated' part of the path is not correct because it is missing an
|
| + # index. Because it is missing an index, it should not create an instance
|
| + # of Repeated. In this case add_parameter will return False and have no
|
| + # side effects.
|
| + #
|
| + # A correct path that would cause a new Inner instance to be inserted at
|
| + # instance.inner and a new Repeated instance to be appended to the
|
| + # instance.inner.repeated list would be 'inner.repeated-0'.
|
| + if not self.__check_indexes(path):
|
| + return False
|
| +
|
| + # Ok to build objects.
|
| + parent_path = path[:-1]
|
| + parent = self.__get_or_create_path(parent_path)
|
| + name, index = path[-1]
|
| + field = parent.field_by_name(name)
|
| +
|
| + if len(values) != 1:
|
| + raise messages.DecodeError(
|
| + 'Found repeated values for field %s.' % field.name)
|
| +
|
| + value = values[0]
|
| +
|
| + if isinstance(field, messages.IntegerField):
|
| + converted_value = int(value)
|
| + elif isinstance(field, message_types.DateTimeField):
|
| + try:
|
| + converted_value = util.decode_datetime(value)
|
| + except ValueError as e:
|
| + raise messages.DecodeError(e)
|
| + elif isinstance(field, messages.MessageField):
|
| + # Just make sure it's instantiated. Assignment to field or
|
| + # appending to list is done in __get_or_create_path.
|
| + self.__get_or_create_path(path)
|
| + return True
|
| + elif isinstance(field, messages.StringField):
|
| + converted_value = value.decode('utf-8')
|
| + elif isinstance(field, messages.BooleanField):
|
| + converted_value = value.lower() == 'true' and True or False
|
| + else:
|
| + try:
|
| + converted_value = field.type(value)
|
| + except TypeError:
|
| + raise messages.DecodeError('Invalid enum value "%s"' % value)
|
| +
|
| + if field.repeated:
|
| + value_list = getattr(parent, field.name, None)
|
| + if value_list is None:
|
| + setattr(parent, field.name, [converted_value])
|
| + else:
|
| + if index == len(value_list):
|
| + value_list.append(converted_value)
|
| + else:
|
| + # Index should never be above len(value_list) because it was
|
| + # verified during the index check above.
|
| + value_list[index] = converted_value
|
| + else:
|
| + setattr(parent, field.name, converted_value)
|
| +
|
| + return True
|
| +
|
| +
|
| +@util.positional(1)
|
| +def encode_message(message, prefix=''):
|
| + """Encode Message instance to url-encoded string.
|
| +
|
| + Args:
|
| + message: Message instance to encode in to url-encoded string.
|
| + prefix: Prefix to append to field names of contained values.
|
| +
|
| + Returns:
|
| + String encoding of Message in URL encoded format.
|
| +
|
| + Raises:
|
| + messages.ValidationError if message is not initialized.
|
| + """
|
| + message.check_initialized()
|
| +
|
| + parameters = []
|
| + def build_message(parent, prefix):
|
| + """Recursively build parameter list for URL response.
|
| +
|
| + Args:
|
| + parent: Message to build parameters for.
|
| + prefix: Prefix to append to field names of contained values.
|
| +
|
| + Returns:
|
| + True if some value of parent was added to the parameters list,
|
| + else False, meaning the object contained no values.
|
| + """
|
| + has_any_values = False
|
| + for field in sorted(parent.all_fields(), key=lambda f: f.number):
|
| + next_value = parent.get_assigned_value(field.name)
|
| + if next_value is None:
|
| + continue
|
| +
|
| + # Found a value. Ultimate return value should be True.
|
| + has_any_values = True
|
| +
|
| + # Normalize all values in to a list.
|
| + if not field.repeated:
|
| + next_value = [next_value]
|
| +
|
| + for index, item in enumerate(next_value):
|
| + # Create a name with an index if it is a repeated field.
|
| + if field.repeated:
|
| + field_name = '%s%s-%s' % (prefix, field.name, index)
|
| + else:
|
| + field_name = prefix + field.name
|
| +
|
| + if isinstance(field, message_types.DateTimeField):
|
| + # DateTimeField stores its data as a RFC 3339 compliant string.
|
| + parameters.append((field_name, item.isoformat()))
|
| + elif isinstance(field, messages.MessageField):
|
| + # Message fields must be recursed in to in order to construct
|
| + # their component parameter values.
|
| + if not build_message(item, field_name + '.'):
|
| + # The nested message is empty. Append an empty value to
|
| + # represent it.
|
| + parameters.append((field_name, ''))
|
| + elif isinstance(field, messages.BooleanField):
|
| + parameters.append((field_name, item and 'true' or 'false'))
|
| + else:
|
| + if isinstance(item, six.text_type):
|
| + item = item.encode('utf-8')
|
| + parameters.append((field_name, str(item)))
|
| +
|
| + return has_any_values
|
| +
|
| + build_message(message, prefix)
|
| +
|
| + # Also add any unrecognized values from the decoded string.
|
| + for key in message.all_unrecognized_fields():
|
| + values, _ = message.get_unrecognized_field_info(key)
|
| + if not isinstance(values, (list, tuple)):
|
| + values = (values,)
|
| + for value in values:
|
| + parameters.append((key, value))
|
| +
|
| + return urllib.urlencode(parameters)
|
| +
|
| +
|
| +def decode_message(message_type, encoded_message, **kwargs):
|
| + """Decode urlencoded content to message.
|
| +
|
| + Args:
|
| + message_type: Message instance to merge URL encoded content into.
|
| + encoded_message: URL encoded message.
|
| + prefix: Prefix to append to field names of contained values.
|
| +
|
| + Returns:
|
| + Decoded instance of message_type.
|
| + """
|
| + message = message_type()
|
| + builder = URLEncodedRequestBuilder(message, **kwargs)
|
| + arguments = cgi.parse_qs(encoded_message, keep_blank_values=True)
|
| + for argument, values in sorted(six.iteritems(arguments)):
|
| + added = builder.add_parameter(argument, values)
|
| + # Save off any unknown values, so they're still accessible.
|
| + if not added:
|
| + message.set_unrecognized_field(argument, values, messages.Variant.STRING)
|
| + message.check_initialized()
|
| + return message
|
|
|