| Index: third_party/google-endpoints/endpoints/discovery_generator.py
|
| diff --git a/third_party/google-endpoints/endpoints/discovery_generator.py b/third_party/google-endpoints/endpoints/discovery_generator.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..a1b5516592b1c3e2bbb843090775e9934a0409b0
|
| --- /dev/null
|
| +++ b/third_party/google-endpoints/endpoints/discovery_generator.py
|
| @@ -0,0 +1,1010 @@
|
| +# Copyright 2016 Google Inc. All Rights Reserved.
|
| +#
|
| +# 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.
|
| +
|
| +"""A library for converting service configs to discovery docs."""
|
| +
|
| +import collections
|
| +import json
|
| +import logging
|
| +import re
|
| +
|
| +import api_exceptions
|
| +import message_parser
|
| +from protorpc import message_types
|
| +from protorpc import messages
|
| +from protorpc import remote
|
| +import resource_container
|
| +import util
|
| +
|
| +
|
| +_PATH_VARIABLE_PATTERN = r'{([a-zA-Z_][a-zA-Z_.\d]*)}'
|
| +
|
| +_MULTICLASS_MISMATCH_ERROR_TEMPLATE = (
|
| + 'Attempting to implement service %s, version %s, with multiple '
|
| + 'classes that are not compatible. See docstring for api() for '
|
| + 'examples how to implement a multi-class API.')
|
| +
|
| +_INVALID_AUTH_ISSUER = 'No auth issuer named %s defined in this Endpoints API.'
|
| +
|
| +_API_KEY = 'api_key'
|
| +_API_KEY_PARAM = 'key'
|
| +
|
| +CUSTOM_VARIANT_MAP = {
|
| + messages.Variant.DOUBLE: ('number', 'double'),
|
| + messages.Variant.FLOAT: ('number', 'float'),
|
| + messages.Variant.INT64: ('string', 'int64'),
|
| + messages.Variant.SINT64: ('string', 'int64'),
|
| + messages.Variant.UINT64: ('string', 'uint64'),
|
| + messages.Variant.INT32: ('integer', 'int32'),
|
| + messages.Variant.SINT32: ('integer', 'int32'),
|
| + messages.Variant.UINT32: ('integer', 'uint32'),
|
| + messages.Variant.BOOL: ('boolean', None),
|
| + messages.Variant.STRING: ('string', None),
|
| + messages.Variant.BYTES: ('string', 'byte'),
|
| + messages.Variant.ENUM: ('string', None),
|
| +}
|
| +
|
| +
|
| +
|
| +class DiscoveryGenerator(object):
|
| + """Generates a discovery doc from a ProtoRPC service.
|
| +
|
| + Example:
|
| +
|
| + class HelloRequest(messages.Message):
|
| + my_name = messages.StringField(1, required=True)
|
| +
|
| + class HelloResponse(messages.Message):
|
| + hello = messages.StringField(1, required=True)
|
| +
|
| + class HelloService(remote.Service):
|
| +
|
| + @remote.method(HelloRequest, HelloResponse)
|
| + def hello(self, request):
|
| + return HelloResponse(hello='Hello there, %s!' %
|
| + request.my_name)
|
| +
|
| + api_config = DiscoveryGenerator().pretty_print_config_to_json(HelloService)
|
| +
|
| + The resulting api_config will be a JSON discovery document describing the API
|
| + implemented by HelloService.
|
| + """
|
| +
|
| + # Constants for categorizing a request method.
|
| + # __NO_BODY - Request without a request body, such as GET and DELETE methods.
|
| + # __HAS_BODY - Request (such as POST/PUT/PATCH) with info in the request body.
|
| + __NO_BODY = 1 # pylint: disable=invalid-name
|
| + __HAS_BODY = 2 # pylint: disable=invalid-name
|
| +
|
| + def __init__(self):
|
| + self.__parser = message_parser.MessageTypeToJsonSchema()
|
| +
|
| + # Maps method id to the request schema id.
|
| + self.__request_schema = {}
|
| +
|
| + # Maps method id to the response schema id.
|
| + self.__response_schema = {}
|
| +
|
| + def _get_resource_path(self, method_id):
|
| + """Return the resource path for a method or an empty array if none."""
|
| + return method_id.split('.')[1:-1]
|
| +
|
| + def _get_canonical_method_id(self, method_id):
|
| + return method_id.split('.')[-1]
|
| +
|
| + def __get_request_kind(self, method_info):
|
| + """Categorize the type of the request.
|
| +
|
| + Args:
|
| + method_info: _MethodInfo, method information.
|
| +
|
| + Returns:
|
| + The kind of request.
|
| + """
|
| + if method_info.http_method in ('GET', 'DELETE'):
|
| + return self.__NO_BODY
|
| + else:
|
| + return self.__HAS_BODY
|
| +
|
| + def __field_to_subfields(self, field):
|
| + """Fully describes data represented by field, including the nested case.
|
| +
|
| + In the case that the field is not a message field, we have no fields nested
|
| + within a message definition, so we can simply return that field. However, in
|
| + the nested case, we can't simply describe the data with one field or even
|
| + with one chain of fields.
|
| +
|
| + For example, if we have a message field
|
| +
|
| + m_field = messages.MessageField(RefClass, 1)
|
| +
|
| + which references a class with two fields:
|
| +
|
| + class RefClass(messages.Message):
|
| + one = messages.StringField(1)
|
| + two = messages.IntegerField(2)
|
| +
|
| + then we would need to include both one and two to represent all the
|
| + data contained.
|
| +
|
| + Calling __field_to_subfields(m_field) would return:
|
| + [
|
| + [<MessageField "m_field">, <StringField "one">],
|
| + [<MessageField "m_field">, <StringField "two">],
|
| + ]
|
| +
|
| + If the second field was instead a message field
|
| +
|
| + class RefClass(messages.Message):
|
| + one = messages.StringField(1)
|
| + two = messages.MessageField(OtherRefClass, 2)
|
| +
|
| + referencing another class with two fields
|
| +
|
| + class OtherRefClass(messages.Message):
|
| + three = messages.BooleanField(1)
|
| + four = messages.FloatField(2)
|
| +
|
| + then we would need to recurse one level deeper for two.
|
| +
|
| + With this change, calling __field_to_subfields(m_field) would return:
|
| + [
|
| + [<MessageField "m_field">, <StringField "one">],
|
| + [<MessageField "m_field">, <StringField "two">, <StringField "three">],
|
| + [<MessageField "m_field">, <StringField "two">, <StringField "four">],
|
| + ]
|
| +
|
| + Args:
|
| + field: An instance of a subclass of messages.Field.
|
| +
|
| + Returns:
|
| + A list of lists, where each sublist is a list of fields.
|
| + """
|
| + # Termination condition
|
| + if not isinstance(field, messages.MessageField):
|
| + return [[field]]
|
| +
|
| + result = []
|
| + for subfield in sorted(field.message_type.all_fields(),
|
| + key=lambda f: f.number):
|
| + subfield_results = self.__field_to_subfields(subfield)
|
| + for subfields_list in subfield_results:
|
| + subfields_list.insert(0, field)
|
| + result.append(subfields_list)
|
| + return result
|
| +
|
| + def __field_to_parameter_type_and_format(self, field):
|
| + """Converts the field variant type into a tuple describing the parameter.
|
| +
|
| + Args:
|
| + field: An instance of a subclass of messages.Field.
|
| +
|
| + Returns:
|
| + A tuple with the type and format of the field, respectively.
|
| +
|
| + Raises:
|
| + TypeError: if the field variant is a message variant.
|
| + """
|
| + # We use lowercase values for types (e.g. 'string' instead of 'STRING').
|
| + variant = field.variant
|
| + if variant == messages.Variant.MESSAGE:
|
| + raise TypeError('A message variant cannot be used in a parameter.')
|
| +
|
| + # Note that the 64-bit integers are marked as strings -- this is to
|
| + # accommodate JavaScript, which would otherwise demote them to 32-bit
|
| + # integers.
|
| +
|
| + return CUSTOM_VARIANT_MAP.get(variant) or (variant.name.lower(), None)
|
| +
|
| + def __get_path_parameters(self, path):
|
| + """Parses path paremeters from a URI path and organizes them by parameter.
|
| +
|
| + Some of the parameters may correspond to message fields, and so will be
|
| + represented as segments corresponding to each subfield; e.g. first.second if
|
| + the field "second" in the message field "first" is pulled from the path.
|
| +
|
| + The resulting dictionary uses the first segments as keys and each key has as
|
| + value the list of full parameter values with first segment equal to the key.
|
| +
|
| + If the match path parameter is null, that part of the path template is
|
| + ignored; this occurs if '{}' is used in a template.
|
| +
|
| + Args:
|
| + path: String; a URI path, potentially with some parameters.
|
| +
|
| + Returns:
|
| + A dictionary with strings as keys and list of strings as values.
|
| + """
|
| + path_parameters_by_segment = {}
|
| + for format_var_name in re.findall(_PATH_VARIABLE_PATTERN, path):
|
| + first_segment = format_var_name.split('.', 1)[0]
|
| + matches = path_parameters_by_segment.setdefault(first_segment, [])
|
| + matches.append(format_var_name)
|
| +
|
| + return path_parameters_by_segment
|
| +
|
| + def __validate_simple_subfield(self, parameter, field, segment_list,
|
| + segment_index=0):
|
| + """Verifies that a proposed subfield actually exists and is a simple field.
|
| +
|
| + Here, simple means it is not a MessageField (nested).
|
| +
|
| + Args:
|
| + parameter: String; the '.' delimited name of the current field being
|
| + considered. This is relative to some root.
|
| + field: An instance of a subclass of messages.Field. Corresponds to the
|
| + previous segment in the path (previous relative to _segment_index),
|
| + since this field should be a message field with the current segment
|
| + as a field in the message class.
|
| + segment_list: The full list of segments from the '.' delimited subfield
|
| + being validated.
|
| + segment_index: Integer; used to hold the position of current segment so
|
| + that segment_list can be passed as a reference instead of having to
|
| + copy using segment_list[1:] at each step.
|
| +
|
| + Raises:
|
| + TypeError: If the final subfield (indicated by _segment_index relative
|
| + to the length of segment_list) is a MessageField.
|
| + TypeError: If at any stage the lookup at a segment fails, e.g if a.b
|
| + exists but a.b.c does not exist. This can happen either if a.b is not
|
| + a message field or if a.b.c is not a property on the message class from
|
| + a.b.
|
| + """
|
| + if segment_index >= len(segment_list):
|
| + # In this case, the field is the final one, so should be simple type
|
| + if isinstance(field, messages.MessageField):
|
| + field_class = field.__class__.__name__
|
| + raise TypeError('Can\'t use messages in path. Subfield %r was '
|
| + 'included but is a %s.' % (parameter, field_class))
|
| + return
|
| +
|
| + segment = segment_list[segment_index]
|
| + parameter += '.' + segment
|
| + try:
|
| + field = field.type.field_by_name(segment)
|
| + except (AttributeError, KeyError):
|
| + raise TypeError('Subfield %r from path does not exist.' % (parameter,))
|
| +
|
| + self.__validate_simple_subfield(parameter, field, segment_list,
|
| + segment_index=segment_index + 1)
|
| +
|
| + def __validate_path_parameters(self, field, path_parameters):
|
| + """Verifies that all path parameters correspond to an existing subfield.
|
| +
|
| + Args:
|
| + field: An instance of a subclass of messages.Field. Should be the root
|
| + level property name in each path parameter in path_parameters. For
|
| + example, if the field is called 'foo', then each path parameter should
|
| + begin with 'foo.'.
|
| + path_parameters: A list of Strings representing URI parameter variables.
|
| +
|
| + Raises:
|
| + TypeError: If one of the path parameters does not start with field.name.
|
| + """
|
| + for param in path_parameters:
|
| + segment_list = param.split('.')
|
| + if segment_list[0] != field.name:
|
| + raise TypeError('Subfield %r can\'t come from field %r.'
|
| + % (param, field.name))
|
| + self.__validate_simple_subfield(field.name, field, segment_list[1:])
|
| +
|
| + def __parameter_default(self, field):
|
| + """Returns default value of field if it has one.
|
| +
|
| + Args:
|
| + field: A simple field.
|
| +
|
| + Returns:
|
| + The default value of the field, if any exists, with the exception of an
|
| + enum field, which will have its value cast to a string.
|
| + """
|
| + if field.default:
|
| + if isinstance(field, messages.EnumField):
|
| + return field.default.name
|
| + else:
|
| + return field.default
|
| +
|
| + def __parameter_enum(self, param):
|
| + """Returns enum descriptor of a parameter if it is an enum.
|
| +
|
| + An enum descriptor is a list of keys.
|
| +
|
| + Args:
|
| + param: A simple field.
|
| +
|
| + Returns:
|
| + The enum descriptor for the field, if it's an enum descriptor, else
|
| + returns None.
|
| + """
|
| + if isinstance(param, messages.EnumField):
|
| + return [enum_entry[0] for enum_entry in sorted(
|
| + param.type.to_dict().items(), key=lambda v: v[1])]
|
| +
|
| + def __parameter_descriptor(self, param):
|
| + """Creates descriptor for a parameter.
|
| +
|
| + Args:
|
| + param: The parameter to be described.
|
| +
|
| + Returns:
|
| + Dictionary containing a descriptor for the parameter.
|
| + """
|
| + descriptor = {}
|
| +
|
| + param_type, param_format = self.__field_to_parameter_type_and_format(param)
|
| +
|
| + # Required
|
| + if param.required:
|
| + descriptor['required'] = True
|
| +
|
| + # Type
|
| + descriptor['type'] = param_type
|
| +
|
| + # Format (optional)
|
| + if param_format:
|
| + descriptor['format'] = param_format
|
| +
|
| + # Default
|
| + default = self.__parameter_default(param)
|
| + if default is not None:
|
| + descriptor['default'] = default
|
| +
|
| + # Repeated
|
| + if param.repeated:
|
| + descriptor['repeated'] = True
|
| +
|
| + # Enum
|
| + # Note that enumDescriptions are not currently supported using the
|
| + # framework's annotations, so just insert blank strings.
|
| + enum_descriptor = self.__parameter_enum(param)
|
| + if enum_descriptor is not None:
|
| + descriptor['enum'] = enum_descriptor
|
| + descriptor['enumDescriptions'] = [''] * len(enum_descriptor)
|
| +
|
| + return descriptor
|
| +
|
| + def __add_parameter(self, param, path_parameters, params):
|
| + """Adds all parameters in a field to a method parameters descriptor.
|
| +
|
| + Simple fields will only have one parameter, but a message field 'x' that
|
| + corresponds to a message class with fields 'y' and 'z' will result in
|
| + parameters 'x.y' and 'x.z', for example. The mapping from field to
|
| + parameters is mostly handled by __field_to_subfields.
|
| +
|
| + Args:
|
| + param: Parameter to be added to the descriptor.
|
| + path_parameters: A list of parameters matched from a path for this field.
|
| + For example for the hypothetical 'x' from above if the path was
|
| + '/a/{x.z}/b/{other}' then this list would contain only the element
|
| + 'x.z' since 'other' does not match to this field.
|
| + params: List of parameters. Each parameter in the field.
|
| + """
|
| + # If this is a simple field, just build the descriptor and append it.
|
| + # Otherwise, build a schema and assign it to this descriptor
|
| + descriptor = None
|
| + if not isinstance(param, messages.MessageField):
|
| + name = param.name
|
| + descriptor = self.__parameter_descriptor(param)
|
| + descriptor['location'] = 'path' if name in path_parameters else 'query'
|
| +
|
| + if descriptor:
|
| + params[name] = descriptor
|
| + else:
|
| + for subfield_list in self.__field_to_subfields(param):
|
| + name = '.'.join(subfield.name for subfield in subfield_list)
|
| + descriptor = self.__parameter_descriptor(subfield_list[-1])
|
| + if name in path_parameters:
|
| + descriptor['required'] = True
|
| + descriptor['location'] = 'path'
|
| + else:
|
| + descriptor.pop('required', None)
|
| + descriptor['location'] = 'query'
|
| +
|
| + if descriptor:
|
| + params[name] = descriptor
|
| +
|
| +
|
| + def __params_descriptor_without_container(self, message_type,
|
| + request_kind, path):
|
| + """Describe parameters of a method which does not use a ResourceContainer.
|
| +
|
| + Makes sure that the path parameters are included in the message definition
|
| + and adds any required fields and URL query parameters.
|
| +
|
| + This method is to preserve backwards compatibility and will be removed in
|
| + a future release.
|
| +
|
| + Args:
|
| + message_type: messages.Message class, Message with parameters to describe.
|
| + request_kind: The type of request being made.
|
| + path: string, HTTP path to method.
|
| +
|
| + Returns:
|
| + A list of dicts: Descriptors of the parameters
|
| + """
|
| + params = {}
|
| +
|
| + path_parameter_dict = self.__get_path_parameters(path)
|
| + for field in sorted(message_type.all_fields(), key=lambda f: f.number):
|
| + matched_path_parameters = path_parameter_dict.get(field.name, [])
|
| + self.__validate_path_parameters(field, matched_path_parameters)
|
| + if matched_path_parameters or request_kind == self.__NO_BODY:
|
| + self.__add_parameter(field, matched_path_parameters, params)
|
| +
|
| + return params
|
| +
|
| + def __params_descriptor(self, message_type, request_kind, path, method_id,
|
| + request_params_class):
|
| + """Describe the parameters of a method.
|
| +
|
| + If the message_type is not a ResourceContainer, will fall back to
|
| + __params_descriptor_without_container (which will eventually be deprecated).
|
| +
|
| + If the message type is a ResourceContainer, then all path/query parameters
|
| + will come from the ResourceContainer. This method will also make sure all
|
| + path parameters are covered by the message fields.
|
| +
|
| + Args:
|
| + message_type: messages.Message or ResourceContainer class, Message with
|
| + parameters to describe.
|
| + request_kind: The type of request being made.
|
| + path: string, HTTP path to method.
|
| + method_id: string, Unique method identifier (e.g. 'myapi.items.method')
|
| + request_params_class: messages.Message, the original params message when
|
| + using a ResourceContainer. Otherwise, this should be null.
|
| +
|
| + Returns:
|
| + A tuple (dict, list of string): Descriptor of the parameters, Order of the
|
| + parameters.
|
| + """
|
| + path_parameter_dict = self.__get_path_parameters(path)
|
| +
|
| + if request_params_class is None:
|
| + if path_parameter_dict:
|
| + logging.warning('Method %s specifies path parameters but you are not '
|
| + 'using a ResourceContainer. This will fail in future '
|
| + 'releases; please switch to using ResourceContainer as '
|
| + 'soon as possible.', method_id)
|
| + return self.__params_descriptor_without_container(
|
| + message_type, request_kind, path)
|
| +
|
| + # From here, we can assume message_type is from a ResourceContainer.
|
| + message_type = request_params_class
|
| +
|
| + params = {}
|
| +
|
| + # Make sure all path parameters are covered.
|
| + for field_name, matched_path_parameters in path_parameter_dict.iteritems():
|
| + field = message_type.field_by_name(field_name)
|
| + self.__validate_path_parameters(field, matched_path_parameters)
|
| +
|
| + # Add all fields, sort by field.number since we have parameterOrder.
|
| + for field in sorted(message_type.all_fields(), key=lambda f: f.number):
|
| + matched_path_parameters = path_parameter_dict.get(field.name, [])
|
| + self.__add_parameter(field, matched_path_parameters, params)
|
| +
|
| + return params
|
| +
|
| + def __params_order_descriptor(self, message_type, path):
|
| + """Describe the order of path parameters.
|
| +
|
| + Args:
|
| + message_type: messages.Message class, Message with parameters to describe.
|
| + path: string, HTTP path to method.
|
| +
|
| + Returns:
|
| + Descriptor list for the parameter order.
|
| + """
|
| + descriptor = []
|
| + path_parameter_dict = self.__get_path_parameters(path)
|
| +
|
| + for field in sorted(message_type.all_fields(), key=lambda f: f.number):
|
| + matched_path_parameters = path_parameter_dict.get(field.name, [])
|
| + if not isinstance(field, messages.MessageField):
|
| + name = field.name
|
| + if name in matched_path_parameters:
|
| + descriptor.append(name)
|
| + else:
|
| + for subfield_list in self.__field_to_subfields(field):
|
| + name = '.'.join(subfield.name for subfield in subfield_list)
|
| + if name in matched_path_parameters:
|
| + descriptor.append(name)
|
| +
|
| + return descriptor
|
| +
|
| + def __schemas_descriptor(self):
|
| + """Describes the schemas section of the discovery document.
|
| +
|
| + Returns:
|
| + Dictionary describing the schemas of the document.
|
| + """
|
| + # Filter out any keys that aren't 'properties', 'type', or 'id'
|
| + result = {}
|
| + for schema_key, schema_value in self.__parser.schemas().iteritems():
|
| + field_keys = schema_value.keys()
|
| + key_result = {}
|
| +
|
| + # Some special processing for the properties value
|
| + if 'properties' in field_keys:
|
| + key_result['properties'] = schema_value['properties'].copy()
|
| + # Add in enumDescriptions for any enum properties and strip out
|
| + # the required tag for consistency with Java framework
|
| + for prop_key, prop_value in schema_value['properties'].iteritems():
|
| + if 'enum' in prop_value:
|
| + num_enums = len(prop_value['enum'])
|
| + key_result['properties'][prop_key]['enumDescriptions'] = (
|
| + [''] * num_enums)
|
| + key_result['properties'][prop_key].pop('required', None)
|
| +
|
| + for key in ('type', 'id', 'description'):
|
| + if key in field_keys:
|
| + key_result[key] = schema_value[key]
|
| +
|
| + if key_result:
|
| + result[schema_key] = key_result
|
| +
|
| + # Add 'type': 'object' to all object properties
|
| + for schema_value in result.itervalues():
|
| + for field_value in schema_value.itervalues():
|
| + if isinstance(field_value, dict):
|
| + if '$ref' in field_value:
|
| + field_value['type'] = 'object'
|
| +
|
| + return result
|
| +
|
| + def __request_message_descriptor(self, request_kind, message_type, method_id,
|
| + request_body_class):
|
| + """Describes the parameters and body of the request.
|
| +
|
| + Args:
|
| + request_kind: The type of request being made.
|
| + message_type: messages.Message or ResourceContainer class. The message to
|
| + describe.
|
| + method_id: string, Unique method identifier (e.g. 'myapi.items.method')
|
| + request_body_class: messages.Message of the original body when using
|
| + a ResourceContainer. Otherwise, this should be null.
|
| +
|
| + Returns:
|
| + Dictionary describing the request.
|
| +
|
| + Raises:
|
| + ValueError: if the method path and request required fields do not match
|
| + """
|
| + if request_body_class:
|
| + message_type = request_body_class
|
| +
|
| + if (request_kind != self.__NO_BODY and
|
| + message_type != message_types.VoidMessage()):
|
| + self.__request_schema[method_id] = self.__parser.add_message(
|
| + message_type.__class__)
|
| + return {
|
| + '$ref': self.__request_schema[method_id],
|
| + 'parameterName': 'resource',
|
| + }
|
| +
|
| + def __response_message_descriptor(self, message_type, method_id):
|
| + """Describes the response.
|
| +
|
| + Args:
|
| + message_type: messages.Message class, The message to describe.
|
| + method_id: string, Unique method identifier (e.g. 'myapi.items.method')
|
| +
|
| + Returns:
|
| + Dictionary describing the response.
|
| + """
|
| + if message_type != message_types.VoidMessage():
|
| + self.__parser.add_message(message_type.__class__)
|
| + self.__response_schema[method_id] = self.__parser.ref_for_message_type(
|
| + message_type.__class__)
|
| + return {'$ref': self.__response_schema[method_id]}
|
| + else:
|
| + return None
|
| +
|
| + def __method_descriptor(self, service, method_info,
|
| + protorpc_method_info):
|
| + """Describes a method.
|
| +
|
| + Args:
|
| + service: endpoints.Service, Implementation of the API as a service.
|
| + method_info: _MethodInfo, Configuration for the method.
|
| + protorpc_method_info: protorpc.remote._RemoteMethodInfo, ProtoRPC
|
| + description of the method.
|
| +
|
| + Returns:
|
| + Dictionary describing the method.
|
| + """
|
| + descriptor = {}
|
| +
|
| + request_message_type = (resource_container.ResourceContainer.
|
| + get_request_message(protorpc_method_info.remote))
|
| + request_kind = self.__get_request_kind(method_info)
|
| + remote_method = protorpc_method_info.remote
|
| +
|
| + method_id = method_info.method_id(service.api_info)
|
| +
|
| + path = method_info.get_path(service.api_info)
|
| +
|
| + description = protorpc_method_info.remote.method.__doc__
|
| +
|
| + descriptor['id'] = method_id
|
| + descriptor['path'] = path
|
| + descriptor['httpMethod'] = method_info.http_method
|
| +
|
| + if description:
|
| + descriptor['description'] = description
|
| +
|
| + descriptor['scopes'] = [
|
| + 'https://www.googleapis.com/auth/userinfo.email'
|
| + ]
|
| +
|
| + parameters = self.__params_descriptor(
|
| + request_message_type, request_kind, path, method_id,
|
| + method_info.request_params_class)
|
| + if parameters:
|
| + descriptor['parameters'] = parameters
|
| +
|
| + if method_info.request_params_class:
|
| + parameter_order = self.__params_order_descriptor(
|
| + method_info.request_params_class, path)
|
| + else:
|
| + parameter_order = self.__params_order_descriptor(
|
| + request_message_type, path)
|
| + if parameter_order:
|
| + descriptor['parameterOrder'] = parameter_order
|
| +
|
| + request_descriptor = self.__request_message_descriptor(
|
| + request_kind, request_message_type, method_id,
|
| + method_info.request_body_class)
|
| + if request_descriptor is not None:
|
| + descriptor['request'] = request_descriptor
|
| +
|
| + response_descriptor = self.__response_message_descriptor(
|
| + remote_method.response_type(), method_info.method_id(service.api_info))
|
| + if response_descriptor is not None:
|
| + descriptor['response'] = response_descriptor
|
| +
|
| + return descriptor
|
| +
|
| + def __resource_descriptor(self, resource_path, methods):
|
| + """Describes a resource.
|
| +
|
| + Args:
|
| + resource_path: string, the path of the resource (e.g., 'entries.items')
|
| + methods: list of tuples of type
|
| + (endpoints.Service, protorpc.remote._RemoteMethodInfo), the methods
|
| + that serve this resource.
|
| +
|
| + Returns:
|
| + Dictionary describing the resource.
|
| + """
|
| + descriptor = {}
|
| + method_map = {}
|
| + sub_resource_index = collections.defaultdict(list)
|
| + sub_resource_map = {}
|
| +
|
| + resource_path_tokens = resource_path.split('.')
|
| + for service, protorpc_meth_info in methods:
|
| + method_info = getattr(protorpc_meth_info, 'method_info', None)
|
| + path = method_info.get_path(service.api_info)
|
| + method_id = method_info.method_id(service.api_info)
|
| + canonical_method_id = self._get_canonical_method_id(method_id)
|
| +
|
| + current_resource_path = self._get_resource_path(method_id)
|
| +
|
| + # Sanity-check that this method belongs to the resource path
|
| + if (current_resource_path[:len(resource_path_tokens)] !=
|
| + resource_path_tokens):
|
| + raise api_exceptions.ToolError(
|
| + 'Internal consistency error in resource path {0}'.format(
|
| + current_resource_path))
|
| +
|
| + # Remove the portion of the current method's resource path that's already
|
| + # part of the resource path at this level.
|
| + effective_resource_path = current_resource_path[
|
| + len(resource_path_tokens):]
|
| +
|
| + # If this method is part of a sub-resource, note it and skip it for now
|
| + if effective_resource_path:
|
| + sub_resource_name = effective_resource_path[0]
|
| + new_resource_path = '.'.join([resource_path, sub_resource_name])
|
| + sub_resource_index[new_resource_path].append(
|
| + (service, protorpc_meth_info))
|
| + else:
|
| + method_map[canonical_method_id] = self.__method_descriptor(
|
| + service, method_info, protorpc_meth_info)
|
| +
|
| + # Process any sub-resources
|
| + for sub_resource, sub_resource_methods in sub_resource_index.items():
|
| + sub_resource_name = sub_resource.split('.')[-1]
|
| + sub_resource_map[sub_resource_name] = self.__resource_descriptor(
|
| + sub_resource, sub_resource_methods)
|
| +
|
| + if method_map:
|
| + descriptor['methods'] = method_map
|
| +
|
| + if sub_resource_map:
|
| + descriptor['resources'] = sub_resource_map
|
| +
|
| + return descriptor
|
| +
|
| + def __standard_parameters_descriptor(self):
|
| + return {
|
| + 'alt': {
|
| + 'type': 'string',
|
| + 'description': 'Data format for the response.',
|
| + 'default': 'json',
|
| + 'enum': ['json'],
|
| + 'enumDescriptions': [
|
| + 'Responses with Content-Type of application/json'
|
| + ],
|
| + 'location': 'query',
|
| + },
|
| + 'fields': {
|
| + 'type': 'string',
|
| + 'description': 'Selector specifying which fields to include in a '
|
| + 'partial response.',
|
| + 'location': 'query',
|
| + },
|
| + 'key': {
|
| + 'type': 'string',
|
| + 'description': 'API key. Your API key identifies your project and '
|
| + 'provides you with API access, quota, and reports. '
|
| + 'Required unless you provide an OAuth 2.0 token.',
|
| + 'location': 'query',
|
| + },
|
| + 'oauth_token': {
|
| + 'type': 'string',
|
| + 'description': 'OAuth 2.0 token for the current user.',
|
| + 'location': 'query',
|
| + },
|
| + 'prettyPrint': {
|
| + 'type': 'boolean',
|
| + 'description': 'Returns response with indentations and line '
|
| + 'breaks.',
|
| + 'default': 'true',
|
| + 'location': 'query',
|
| + },
|
| + 'quotaUser': {
|
| + 'type': 'string',
|
| + 'description': 'Available to use for quota purposes for '
|
| + 'server-side applications. Can be any arbitrary '
|
| + 'string assigned to a user, but should not exceed '
|
| + '40 characters. Overrides userIp if both are '
|
| + 'provided.',
|
| + 'location': 'query',
|
| + },
|
| + 'userIp': {
|
| + 'type': 'string',
|
| + 'description': 'IP address of the site where the request '
|
| + 'originates. Use this if you want to enforce '
|
| + 'per-user limits.',
|
| + 'location': 'query',
|
| + },
|
| + }
|
| +
|
| + def __standard_auth_descriptor(self):
|
| + return {
|
| + 'oauth2': {
|
| + 'scopes': {
|
| + 'https://www.googleapis.com/auth/userinfo.email': {
|
| + 'description': 'View your email address'
|
| + }
|
| + }
|
| + }
|
| + }
|
| +
|
| + def __get_merged_api_info(self, services):
|
| + """Builds a description of an API.
|
| +
|
| + Args:
|
| + services: List of protorpc.remote.Service instances implementing an
|
| + api/version.
|
| +
|
| + Returns:
|
| + The _ApiInfo object to use for the API that the given services implement.
|
| +
|
| + Raises:
|
| + ApiConfigurationError: If there's something wrong with the API
|
| + configuration, such as a multiclass API decorated with different API
|
| + descriptors (see the docstring for api()).
|
| + """
|
| + merged_api_info = services[0].api_info
|
| +
|
| + # Verify that, if there are multiple classes here, they're allowed to
|
| + # implement the same API.
|
| + for service in services[1:]:
|
| + if not merged_api_info.is_same_api(service.api_info):
|
| + raise api_exceptions.ApiConfigurationError(
|
| + _MULTICLASS_MISMATCH_ERROR_TEMPLATE % (service.api_info.name,
|
| + service.api_info.version))
|
| +
|
| + return merged_api_info
|
| +
|
| + def __discovery_doc_descriptor(self, services, hostname=None):
|
| + """Builds a discovery doc for an API.
|
| +
|
| + Args:
|
| + services: List of protorpc.remote.Service instances implementing an
|
| + api/version.
|
| + hostname: string, Hostname of the API, to override the value set on the
|
| + current service. Defaults to None.
|
| +
|
| + Returns:
|
| + A dictionary that can be deserialized into JSON in discovery doc format.
|
| +
|
| + Raises:
|
| + ApiConfigurationError: If there's something wrong with the API
|
| + configuration, such as a multiclass API decorated with different API
|
| + descriptors (see the docstring for api()), or a repeated method
|
| + signature.
|
| + """
|
| + merged_api_info = self.__get_merged_api_info(services)
|
| + descriptor = self.get_descriptor_defaults(merged_api_info,
|
| + hostname=hostname)
|
| +
|
| + description = merged_api_info.description
|
| + if not description and len(services) == 1:
|
| + description = services[0].__doc__
|
| + if description:
|
| + descriptor['description'] = description
|
| +
|
| + descriptor['parameters'] = self.__standard_parameters_descriptor()
|
| + descriptor['auth'] = self.__standard_auth_descriptor()
|
| +
|
| + method_map = {}
|
| + method_collision_tracker = {}
|
| + rest_collision_tracker = {}
|
| +
|
| + resource_index = collections.defaultdict(list)
|
| + resource_map = {}
|
| +
|
| + # For the first pass, only process top-level methods (that is, those methods
|
| + # that are unattached to a resource).
|
| + for service in services:
|
| + remote_methods = service.all_remote_methods()
|
| +
|
| + for protorpc_meth_name, protorpc_meth_info in remote_methods.iteritems():
|
| + method_info = getattr(protorpc_meth_info, 'method_info', None)
|
| + # Skip methods that are not decorated with @method
|
| + if method_info is None:
|
| + continue
|
| + path = method_info.get_path(service.api_info)
|
| + method_id = method_info.method_id(service.api_info)
|
| + canonical_method_id = self._get_canonical_method_id(method_id)
|
| + resource_path = self._get_resource_path(method_id)
|
| +
|
| + # Make sure the same method name isn't repeated.
|
| + if method_id in method_collision_tracker:
|
| + raise api_exceptions.ApiConfigurationError(
|
| + 'Method %s used multiple times, in classes %s and %s' %
|
| + (method_id, method_collision_tracker[method_id],
|
| + service.__name__))
|
| + else:
|
| + method_collision_tracker[method_id] = service.__name__
|
| +
|
| + # Make sure the same HTTP method & path aren't repeated.
|
| + rest_identifier = (method_info.http_method, path)
|
| + if rest_identifier in rest_collision_tracker:
|
| + raise api_exceptions.ApiConfigurationError(
|
| + '%s path "%s" used multiple times, in classes %s and %s' %
|
| + (method_info.http_method, path,
|
| + rest_collision_tracker[rest_identifier],
|
| + service.__name__))
|
| + else:
|
| + rest_collision_tracker[rest_identifier] = service.__name__
|
| +
|
| + # If this method is part of a resource, note it and skip it for now
|
| + if resource_path:
|
| + resource_index[resource_path[0]].append((service, protorpc_meth_info))
|
| + else:
|
| + method_map[canonical_method_id] = self.__method_descriptor(
|
| + service, method_info, protorpc_meth_info)
|
| +
|
| + # Do another pass for methods attached to resources
|
| + for resource, resource_methods in resource_index.items():
|
| + resource_map[resource] = self.__resource_descriptor(resource,
|
| + resource_methods)
|
| +
|
| + if method_map:
|
| + descriptor['methods'] = method_map
|
| +
|
| + if resource_map:
|
| + descriptor['resources'] = resource_map
|
| +
|
| + # Add schemas, if any
|
| + schemas = self.__schemas_descriptor()
|
| + if schemas:
|
| + descriptor['schemas'] = schemas
|
| +
|
| + return descriptor
|
| +
|
| + def get_descriptor_defaults(self, api_info, hostname=None):
|
| + """Gets a default configuration for a service.
|
| +
|
| + Args:
|
| + api_info: _ApiInfo object for this service.
|
| + hostname: string, Hostname of the API, to override the value set on the
|
| + current service. Defaults to None.
|
| +
|
| + Returns:
|
| + A dictionary with the default configuration.
|
| + """
|
| + hostname = (hostname or util.get_app_hostname() or
|
| + api_info.hostname)
|
| + protocol = 'http' if ((hostname and hostname.startswith('localhost')) or
|
| + util.is_running_on_devserver()) else 'https'
|
| + full_base_path = '{0}{1}/{2}/'.format(api_info.base_path,
|
| + api_info.name,
|
| + api_info.version)
|
| + base_url = '{0}://{1}{2}'.format(protocol, hostname, full_base_path)
|
| + root_url = '{0}://{1}{2}'.format(protocol, hostname, api_info.base_path)
|
| + defaults = {
|
| + 'kind': 'discovery#restDescription',
|
| + 'discoveryVersion': 'v1',
|
| + 'id': '{0}:{1}'.format(api_info.name, api_info.version),
|
| + 'name': api_info.name,
|
| + 'version': api_info.version,
|
| + 'icons': {
|
| + 'x16': 'http://www.google.com/images/icons/product/search-16.gif',
|
| + 'x32': 'http://www.google.com/images/icons/product/search-32.gif'
|
| + },
|
| + 'protocol': 'rest',
|
| + 'servicePath': '{0}/{1}/'.format(api_info.name, api_info.version),
|
| + 'batchPath': 'batch',
|
| + 'basePath': full_base_path,
|
| + 'rootUrl': root_url,
|
| + 'baseUrl': base_url,
|
| + }
|
| +
|
| + return defaults
|
| +
|
| + def get_discovery_doc(self, services, hostname=None):
|
| + """JSON dict description of a protorpc.remote.Service in discovery format.
|
| +
|
| + Args:
|
| + services: Either a single protorpc.remote.Service or a list of them
|
| + that implements an api/version.
|
| + hostname: string, Hostname of the API, to override the value set on the
|
| + current service. Defaults to None.
|
| +
|
| + Returns:
|
| + dict, The discovery document as a JSON dict.
|
| + """
|
| +
|
| + if not isinstance(services, (tuple, list)):
|
| + services = [services]
|
| +
|
| + # The type of a class that inherits from remote.Service is actually
|
| + # remote._ServiceClass, thanks to metaclass strangeness.
|
| + # pylint: disable=protected-access
|
| + util.check_list_type(services, remote._ServiceClass, 'services',
|
| + allow_none=False)
|
| +
|
| + return self.__discovery_doc_descriptor(services, hostname=hostname)
|
| +
|
| + def pretty_print_config_to_json(self, services, hostname=None):
|
| + """JSON string description of a protorpc.remote.Service in a discovery doc.
|
| +
|
| + Args:
|
| + services: Either a single protorpc.remote.Service or a list of them
|
| + that implements an api/version.
|
| + hostname: string, Hostname of the API, to override the value set on the
|
| + current service. Defaults to None.
|
| +
|
| + Returns:
|
| + string, The discovery doc descriptor document as a JSON string.
|
| + """
|
| + descriptor = self.get_discovery_doc(services, hostname)
|
| + return json.dumps(descriptor, sort_keys=True, indent=2,
|
| + separators=(',', ': '))
|
|
|