Merge branch 'main' into avm99963-monorail

Merged commit 34d8229ae2b51fb1a15bd208e6fe6185c94f6266

GitOrigin-RevId: 7ee0917f93a577e475f8e09526dd144d245593f4
diff --git a/third_party/endpoints/api_config.py b/third_party/endpoints/api_config.py
new file mode 100644
index 0000000..e24cd57
--- /dev/null
+++ b/third_party/endpoints/api_config.py
@@ -0,0 +1,2257 @@
+# 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.
+
+"""Library for generating an API configuration document for a ProtoRPC backend.
+
+The protorpc.remote.Service is inspected and a JSON document describing
+the API is returned.
+
+    class MyResponse(messages.Message):
+      bool_value = messages.BooleanField(1)
+      int32_value = messages.IntegerField(2)
+
+    class MyService(remote.Service):
+
+      @remote.method(message_types.VoidMessage, MyResponse)
+      def entries_get(self, request):
+        pass
+
+    api = ApiConfigGenerator().pretty_print_config_to_json(MyService)
+"""
+
+# pylint: disable=g-bad-name
+
+# pylint: disable=g-statement-before-imports,g-import-not-at-top
+from __future__ import absolute_import
+
+import json
+import logging
+import re
+import six
+
+from google.appengine.api import app_identity
+
+import attr
+from protorpc import util
+
+from . import api_exceptions
+from . import constants
+from . import message_parser
+from . import message_types
+from . import messages
+from . import remote
+from . import resource_container
+from . import types as endpoints_types
+# originally in this module
+from .types import Issuer, LimitDefinition, Namespace
+from . import users_id_token
+from . import util as endpoints_util
+
+_logger = logging.getLogger(__name__)
+package = 'google.appengine.endpoints'
+
+
+__all__ = [
+    'ApiAuth',
+    'ApiConfigGenerator',
+    'ApiFrontEndLimitRule',
+    'ApiFrontEndLimits',
+    'EMAIL_SCOPE',
+    'Issuer',
+    'LimitDefinition',
+    'Namespace',
+    'api',
+    'method',
+    'AUTH_LEVEL',
+    'package',
+]
+
+
+EMAIL_SCOPE = 'https://www.googleapis.com/auth/userinfo.email'
+_EMAIL_SCOPE_DESCRIPTION = 'View your email address'
+_EMAIL_SCOPE_OBJ = endpoints_types.OAuth2Scope(
+    scope=EMAIL_SCOPE, description=_EMAIL_SCOPE_DESCRIPTION)
+_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 aren\'t compatible. See docstring for api() for '
+    'examples how to implement a multi-class API.')
+
+_INVALID_NAMESPACE_ERROR_TEMPLATE = (
+    'Invalid namespace configuration. If a namespace is set, make sure to set '
+    '%s. package_path is optional.')
+
+
+_VALID_PART_RE = re.compile('^{[^{}]+}$')
+_VALID_LAST_PART_RE = re.compile('^{[^{}]+}(:)?(?(1)[^{}]+)$')
+
+
+
+def _Enum(docstring, *names):
+  """Utility to generate enum classes used by annotations.
+
+  Args:
+    docstring: Docstring for the generated enum class.
+    *names: Enum names.
+
+  Returns:
+    A class that contains enum names as attributes.
+  """
+  enums = dict(zip(names, range(len(names))))
+  reverse = dict((value, key) for key, value in enums.items())
+  enums['reverse_mapping'] = reverse
+  enums['__doc__'] = docstring
+  return type('Enum', (object,), enums)
+
+_AUTH_LEVEL_DOCSTRING = """
+  Define the enums used by the auth_level annotation to specify frontend
+  authentication requirement.
+
+  Frontend authentication is handled by a Google API server prior to the
+  request reaching backends. An early return before hitting the backend can
+  happen if the request does not fulfil the requirement specified by the
+  auth_level.
+
+  Valid values of auth_level and their meanings are:
+
+  AUTH_LEVEL.REQUIRED: Valid authentication credentials are required. Backend
+    will be called only if authentication credentials are present and valid.
+
+  AUTH_LEVEL.OPTIONAL: Authentication is optional. If authentication credentials
+    are supplied they must be valid. Backend will be called if the request
+    contains valid authentication credentials or no authentication credentials.
+
+  AUTH_LEVEL.OPTIONAL_CONTINUE: Authentication is optional and will be attempted
+    if authentication credentials are supplied. Invalid authentication
+    credentials will be removed but the request can always reach backend.
+
+  AUTH_LEVEL.NONE: Frontend authentication will be skipped. If authentication is
+   desired, it will need to be performed by the backend.
+  """
+
+AUTH_LEVEL = _Enum(_AUTH_LEVEL_DOCSTRING, 'REQUIRED', 'OPTIONAL',
+                   'OPTIONAL_CONTINUE', 'NONE')
+_AUTH_LEVEL_WARNING = ("Due to a design error, auth_level has never actually been functional. "
+                       "It will likely be removed and replaced by a functioning alternative "
+                       "in a future version of the framework. Please stop using auth_level now.")
+
+
+def _GetFieldAttributes(field):
+  """Decomposes field into the needed arguments to pass to the constructor.
+
+  This can be used to create copies of the field or to compare if two fields
+  are "equal" (since __eq__ is not implemented on messages.Field).
+
+  Args:
+    field: A ProtoRPC message field (potentially to be copied).
+
+  Raises:
+    TypeError: If the field is not an instance of messages.Field.
+
+  Returns:
+    A pair of relevant arguments to be passed to the constructor for the field
+      type. The first element is a list of positional arguments for the
+      constructor and the second is a dictionary of keyword arguments.
+  """
+  if not isinstance(field, messages.Field):
+    raise TypeError('Field %r to be copied not a ProtoRPC field.' % (field,))
+
+  positional_args = []
+  kwargs = {
+      'required': field.required,
+      'repeated': field.repeated,
+      'variant': field.variant,
+      'default': field._Field__default,  # pylint: disable=protected-access
+  }
+
+  if isinstance(field, messages.MessageField):
+    # Message fields can't have a default
+    kwargs.pop('default')
+    if not isinstance(field, message_types.DateTimeField):
+      positional_args.insert(0, field.message_type)
+  elif isinstance(field, messages.EnumField):
+    positional_args.insert(0, field.type)
+
+  return positional_args, kwargs
+
+
+def _CheckType(value, check_type, name, allow_none=True):
+  """Check that the type of an object is acceptable.
+
+  Args:
+    value: The object whose type is to be checked.
+    check_type: The type that the object must be an instance of.
+    name: Name of the object, to be placed in any error messages.
+    allow_none: True if value can be None, false if not.
+
+  Raises:
+    TypeError: If value is not an acceptable type.
+  """
+  if value is None and allow_none:
+    return
+  if not isinstance(value, check_type):
+    raise TypeError('%s type doesn\'t match %s.' % (name, check_type))
+
+
+def _CheckEnum(value, check_type, name):
+  if value is None:
+    return
+  if value not in check_type.reverse_mapping:
+    raise TypeError('%s is not a valid value for %s' % (value, name))
+
+
+def _CheckNamespace(namespace):
+  _CheckType(namespace, Namespace, 'namespace')
+  if namespace:
+    if not namespace.owner_domain:
+      raise api_exceptions.InvalidNamespaceException(
+          _INVALID_NAMESPACE_ERROR_TEMPLATE % 'owner_domain')
+    if not namespace.owner_name:
+      raise api_exceptions.InvalidNamespaceException(
+          _INVALID_NAMESPACE_ERROR_TEMPLATE % 'owner_name')
+
+    _CheckType(namespace.owner_domain, six.string_types, 'namespace.owner_domain')
+    _CheckType(namespace.owner_name, six.string_types, 'namespace.owner_name')
+    _CheckType(namespace.package_path, six.string_types, 'namespace.package_path')
+
+
+def _CheckAudiences(audiences):
+  # Audiences can either be a list of audiences using the google_id_token
+  # or a dict mapping auth issuer name to the list of audiences.
+  if audiences is None or isinstance(audiences, dict):
+    return
+  else:
+    endpoints_util.check_list_type(audiences, six.string_types, 'audiences')
+
+
+def _CheckLimitDefinitions(limit_definitions):
+  _CheckType(limit_definitions, list, 'limit_definitions')
+  if limit_definitions:
+    for ld in limit_definitions:
+      if not ld.metric_name:
+        raise api_exceptions.InvalidLimitDefinitionException(
+          "Metric name must be set in all limit definitions.")
+      if not ld.display_name:
+        raise api_exceptions.InvalidLimitDefinitionException(
+          "Display name must be set in all limit definitions.")
+
+      _CheckType(ld.metric_name, six.string_types, 'limit_definition.metric_name')
+      _CheckType(ld.display_name, six.string_types, 'limit_definition.display_name')
+      _CheckType(ld.default_limit, int, 'limit_definition.default_limit')
+
+
+# pylint: disable=g-bad-name
+class _ApiInfo(object):
+  """Configurable attributes of an API.
+
+  A structured data object used to store API information associated with each
+  remote.Service-derived class that implements an API.  This stores properties
+  that could be different for each class (such as the path or
+  collection/resource name), as well as properties common to all classes in
+  the API (such as API name and version).
+  """
+
+  @util.positional(2)
+  def __init__(self, common_info, resource_name=None, path=None, audiences=None,
+               scopes=None, allowed_client_ids=None, auth_level=None,
+               api_key_required=None):
+    """Constructor for _ApiInfo.
+
+    Args:
+      common_info: _ApiDecorator.__ApiCommonInfo, Information that's common for
+        all classes that implement an API.
+      resource_name: string, The collection that the annotated class will
+        implement in the API. (Default: None)
+      path: string, Base request path for all methods in this API.
+        (Default: None)
+      audiences: list of strings, Acceptable audiences for authentication.
+        (Default: None)
+      scopes: list of strings, Acceptable scopes for authentication.
+        (Default: None)
+      allowed_client_ids: list of strings, Acceptable client IDs for auth.
+        (Default: None)
+      auth_level: enum from AUTH_LEVEL, Frontend authentication level.
+        (Default: None)
+      api_key_required: bool, whether a key is required to call this API.
+    """
+    _CheckType(resource_name, six.string_types, 'resource_name')
+    _CheckType(path, six.string_types, 'path')
+    endpoints_util.check_list_type(audiences, six.string_types, 'audiences')
+    endpoints_util.check_list_type(scopes, six.string_types, 'scopes')
+    endpoints_util.check_list_type(allowed_client_ids, six.string_types,
+                                   'allowed_client_ids')
+    _CheckEnum(auth_level, AUTH_LEVEL, 'auth_level')
+    _CheckType(api_key_required, bool, 'api_key_required')
+
+    self.__common_info = common_info
+    self.__resource_name = resource_name
+    self.__path = path
+    self.__audiences = audiences
+    self.__scopes = endpoints_types.OAuth2Scope.convert_list(scopes)
+    self.__allowed_client_ids = allowed_client_ids
+    self.__auth_level = auth_level
+    self.__api_key_required = api_key_required
+
+  def is_same_api(self, other):
+    """Check if this implements the same API as another _ApiInfo instance."""
+    if not isinstance(other, _ApiInfo):
+      return False
+    # pylint: disable=protected-access
+    return self.__common_info is other.__common_info
+
+  @property
+  def name(self):
+    """Name of the API."""
+    return self.__common_info.name
+
+  @property
+  def api_version(self):
+    """Version of the API."""
+    return self.__common_info.api_version
+
+  @property
+  def path_version(self):
+    """Version of the API for putting in the path."""
+    return self.__common_info.path_version
+
+  @property
+  def description(self):
+    """Description of the API."""
+    return self.__common_info.description
+
+  @property
+  def hostname(self):
+    """Hostname for the API."""
+    return self.__common_info.hostname
+
+  @property
+  def audiences(self):
+    """List of audiences accepted for the API, overriding the defaults."""
+    if self.__audiences is not None:
+      return self.__audiences
+    return self.__common_info.audiences
+
+  @property
+  def scope_objs(self):
+    """List of scopes (as OAuth2Scopes) accepted for the API, overriding the defaults."""
+    if self.__scopes is not None:
+      return self.__scopes
+    return self.__common_info.scope_objs
+
+  @property
+  def scopes(self):
+    """List of scopes (as strings) accepted for the API, overriding the defaults."""
+    if self.scope_objs is not None:
+      return [_s.scope for _s in self.scope_objs]
+
+  @property
+  def allowed_client_ids(self):
+    """List of client IDs accepted for the API, overriding the defaults."""
+    if self.__allowed_client_ids is not None:
+      return self.__allowed_client_ids
+    return self.__common_info.allowed_client_ids
+
+  @property
+  def issuers(self):
+    """Dict mapping auth issuer names to auth issuers for the API."""
+    return self.__common_info.issuers
+
+  @property
+  def namespace(self):
+    """Namespace for the API."""
+    return self.__common_info.namespace
+
+  @property
+  def auth_level(self):
+    """Enum from AUTH_LEVEL specifying the frontend authentication level."""
+    if self.__auth_level is not None:
+      return self.__auth_level
+    return self.__common_info.auth_level
+
+  @property
+  def api_key_required(self):
+    """bool specifying whether a key is required to call into this API."""
+    if self.__api_key_required is not None:
+      return self.__api_key_required
+    return self.__common_info.api_key_required
+
+  @property
+  def canonical_name(self):
+    """Canonical name for the API."""
+    return self.__common_info.canonical_name
+
+  @property
+  def auth(self):
+    """Authentication configuration information for this API."""
+    return self.__common_info.auth
+
+  @property
+  def owner_domain(self):
+    """Domain of the owner of this API."""
+    return self.__common_info.owner_domain
+
+  @property
+  def owner_name(self):
+    """Name of the owner of this API."""
+    return self.__common_info.owner_name
+
+  @property
+  def package_path(self):
+    """Package this API belongs to, '/' delimited.  Used by client libs."""
+    return self.__common_info.package_path
+
+  @property
+  def frontend_limits(self):
+    """Optional query limits for unregistered developers."""
+    return self.__common_info.frontend_limits
+
+  @property
+  def title(self):
+    """Human readable name of this API."""
+    return self.__common_info.title
+
+  @property
+  def documentation(self):
+    """Link to the documentation for this version of the API."""
+    return self.__common_info.documentation
+
+  @property
+  def resource_name(self):
+    """Resource name for the class this decorates."""
+    return self.__resource_name
+
+  @property
+  def path(self):
+    """Base path prepended to any method paths in the class this decorates."""
+    return self.__path
+
+  @property
+  def base_path(self):
+    """Base path for the entire API prepended before the path property."""
+    return self.__common_info.base_path
+
+  @property
+  def limit_definitions(self):
+    """Rate limiting metric definitions for this API."""
+    return self.__common_info.limit_definitions
+
+  @property
+  def use_request_uri(self):
+    """Match request paths based on the REQUEST_URI instead of PATH_INFO."""
+    return self.__common_info.use_request_uri
+
+
+class _ApiDecorator(object):
+  """Decorator for single- or multi-class APIs.
+
+  An instance of this class can be used directly as a decorator for a
+  single-class API.  Or call the api_class() method to decorate a multi-class
+  API.
+  """
+
+  @util.positional(3)
+  def __init__(self, name, version, description=None, hostname=None,
+               audiences=None, scopes=None, allowed_client_ids=None,
+               canonical_name=None, auth=None, owner_domain=None,
+               owner_name=None, package_path=None, frontend_limits=None,
+               title=None, documentation=None, auth_level=None, issuers=None,
+               namespace=None, api_key_required=None, base_path=None,
+               limit_definitions=None, use_request_uri=None):
+    """Constructor for _ApiDecorator.
+
+    Args:
+      name: string, Name of the API.
+      version: string, Version of the API.
+      description: string, Short description of the API (Default: None)
+      hostname: string, Hostname of the API (Default: app engine default host)
+      audiences: list of strings, Acceptable audiences for authentication.
+      scopes: list of strings, Acceptable scopes for authentication.
+      allowed_client_ids: list of strings, Acceptable client IDs for auth.
+      canonical_name: string, the canonical name for the API, a more human
+        readable version of the name.
+      auth: ApiAuth instance, the authentication configuration information
+        for this API.
+      owner_domain: string, the domain of the person or company that owns
+        this API.  Along with owner_name, this provides hints to properly
+        name client libraries for this API.
+      owner_name: string, the name of the owner of this API.  Along with
+        owner_domain, this provides hints to properly name client libraries
+        for this API.
+      package_path: string, the "package" this API belongs to.  This '/'
+        delimited value specifies logical groupings of APIs.  This is used by
+        client libraries of this API.
+      frontend_limits: ApiFrontEndLimits, optional query limits for unregistered
+        developers.
+      title: string, the human readable title of your API. It is exposed in the
+        discovery service.
+      documentation: string, a URL where users can find documentation about this
+        version of the API. This will be surfaced in the API Explorer and GPE
+        plugin to allow users to learn about your service.
+      auth_level: enum from AUTH_LEVEL, Frontend authentication level.
+      issuers: dict, mapping auth issuer names to endpoints.Issuer objects.
+      namespace: endpoints.Namespace, the namespace for the API.
+      api_key_required: bool, whether a key is required to call this API.
+      base_path: string, the base path for all endpoints in this API.
+      limit_definitions: list of LimitDefinition tuples used in this API.
+      use_request_uri: if true, match requests against REQUEST_URI instead of PATH_INFO
+    """
+    self.__common_info = self.__ApiCommonInfo(
+        name, version, description=description, hostname=hostname,
+        audiences=audiences, scopes=scopes,
+        allowed_client_ids=allowed_client_ids,
+        canonical_name=canonical_name, auth=auth, owner_domain=owner_domain,
+        owner_name=owner_name, package_path=package_path,
+        frontend_limits=frontend_limits, title=title,
+        documentation=documentation, auth_level=auth_level, issuers=issuers,
+        namespace=namespace, api_key_required=api_key_required,
+        base_path=base_path, limit_definitions=limit_definitions,
+        use_request_uri=use_request_uri)
+    self.__classes = []
+
+  class __ApiCommonInfo(object):
+    """API information that's common among all classes that implement an API.
+
+    When a remote.Service-derived class implements part of an API, there is
+    some common information that remains constant across all such classes
+    that implement the same API.  This includes things like name, version,
+    hostname, and so on.  __ApiComminInfo stores that common information, and
+    a single __ApiCommonInfo instance is shared among all classes that
+    implement the same API, guaranteeing that they share the same common
+    information.
+
+    Some of these values can be overridden (such as audiences and scopes),
+    while some can't and remain the same for all classes that implement
+    the API (such as name and version).
+    """
+
+    @util.positional(3)
+    def __init__(self, name, version, description=None, hostname=None,
+                 audiences=None, scopes=None, allowed_client_ids=None,
+                 canonical_name=None, auth=None, owner_domain=None,
+                 owner_name=None, package_path=None, frontend_limits=None,
+                 title=None, documentation=None, auth_level=None, issuers=None,
+                 namespace=None, api_key_required=None, base_path=None,
+                 limit_definitions=None, use_request_uri=None):
+      """Constructor for _ApiCommonInfo.
+
+      Args:
+        name: string, Name of the API.
+        version: string, Version of the API.
+        description: string, Short description of the API (Default: None)
+        hostname: string, Hostname of the API (Default: app engine default host)
+        audiences: list of strings, Acceptable audiences for authentication.
+        scopes: list of strings, Acceptable scopes for authentication.
+        allowed_client_ids: list of strings, Acceptable client IDs for auth.
+        canonical_name: string, the canonical name for the API, a more human
+          readable version of the name.
+        auth: ApiAuth instance, the authentication configuration information
+          for this API.
+        owner_domain: string, the domain of the person or company that owns
+          this API.  Along with owner_name, this provides hints to properly
+          name client libraries for this API.
+        owner_name: string, the name of the owner of this API.  Along with
+          owner_domain, this provides hints to properly name client libraries
+          for this API.
+        package_path: string, the "package" this API belongs to.  This '/'
+          delimited value specifies logical groupings of APIs.  This is used by
+          client libraries of this API.
+        frontend_limits: ApiFrontEndLimits, optional query limits for
+          unregistered developers.
+        title: string, the human readable title of your API. It is exposed in
+          the discovery service.
+        documentation: string, a URL where users can find documentation about
+          this version of the API. This will be surfaced in the API Explorer and
+          GPE plugin to allow users to learn about your service.
+        auth_level: enum from AUTH_LEVEL, Frontend authentication level.
+        issuers: dict, mapping auth issuer names to endpoints.Issuer objects.
+        namespace: endpoints.Namespace, the namespace for the API.
+        api_key_required: bool, whether a key is required to call into this API.
+        base_path: string, the base path for all endpoints in this API.
+        limit_definitions: list of LimitDefinition tuples used in this API.
+        use_request_uri: if true, match requests against REQUEST_URI instead of PATH_INFO
+      """
+      _CheckType(name, six.string_types, 'name', allow_none=False)
+      _CheckType(version, six.string_types, 'version', allow_none=False)
+      _CheckType(description, six.string_types, 'description')
+      _CheckType(hostname, six.string_types, 'hostname')
+      endpoints_util.check_list_type(scopes, (six.string_types, endpoints_types.OAuth2Scope), 'scopes')
+      endpoints_util.check_list_type(allowed_client_ids, six.string_types,
+                                     'allowed_client_ids')
+      _CheckType(canonical_name, six.string_types, 'canonical_name')
+      _CheckType(auth, ApiAuth, 'auth')
+      _CheckType(owner_domain, six.string_types, 'owner_domain')
+      _CheckType(owner_name, six.string_types, 'owner_name')
+      _CheckType(package_path, six.string_types, 'package_path')
+      _CheckType(frontend_limits, ApiFrontEndLimits, 'frontend_limits')
+      _CheckType(title, six.string_types, 'title')
+      _CheckType(documentation, six.string_types, 'documentation')
+      _CheckEnum(auth_level, AUTH_LEVEL, 'auth_level')
+      _CheckType(api_key_required, bool, 'api_key_required')
+      _CheckType(base_path, six.string_types, 'base_path')
+
+      _CheckType(issuers, dict, 'issuers')
+      if issuers:
+        for issuer_name, issuer_value in issuers.items():
+          _CheckType(issuer_name, six.string_types, 'issuer %s' % issuer_name)
+          _CheckType(issuer_value, Issuer, 'issuer value for %s' % issuer_name)
+
+      _CheckNamespace(namespace)
+
+      _CheckAudiences(audiences)
+
+      _CheckLimitDefinitions(limit_definitions)
+      _CheckType(use_request_uri, bool, 'use_request_uri')
+
+      if hostname is None:
+        hostname = app_identity.get_default_version_hostname()
+      if scopes is None:
+        scopes = [_EMAIL_SCOPE_OBJ]
+      else:
+        scopes = endpoints_types.OAuth2Scope.convert_list(scopes)
+      if allowed_client_ids is None:
+        allowed_client_ids = [constants.API_EXPLORER_CLIENT_ID]
+      if auth_level is None:
+        auth_level = AUTH_LEVEL.NONE
+      if api_key_required is None:
+        api_key_required = False
+      if base_path is None:
+        base_path = '/_ah/api/'
+      if use_request_uri is None:
+        use_request_uri = False
+
+      self.__name = name
+      self.__api_version = version
+      self.__path_version = version
+      self.__description = description
+      self.__hostname = hostname
+      self.__audiences = audiences
+      self.__scopes = scopes
+      self.__allowed_client_ids = allowed_client_ids
+      self.__canonical_name = canonical_name
+      self.__auth = auth
+      self.__owner_domain = owner_domain
+      self.__owner_name = owner_name
+      self.__package_path = package_path
+      self.__frontend_limits = frontend_limits
+      self.__title = title
+      self.__documentation = documentation
+      self.__auth_level = auth_level
+      self.__issuers = issuers
+      self.__namespace = namespace
+      self.__api_key_required = api_key_required
+      self.__base_path = base_path
+      self.__limit_definitions = limit_definitions
+      self.__use_request_uri = use_request_uri
+
+    @property
+    def name(self):
+      """Name of the API."""
+      return self.__name
+
+    @property
+    def api_version(self):
+      """Version of the API."""
+      return self.__api_version
+
+    @property
+    def path_version(self):
+      """Version of the API for putting in the path."""
+      return self.__path_version
+
+    @property
+    def description(self):
+      """Description of the API."""
+      return self.__description
+
+    @property
+    def hostname(self):
+      """Hostname for the API."""
+      return self.__hostname
+
+    @property
+    def audiences(self):
+      """List of audiences accepted by default for the API."""
+      return self.__audiences
+
+    @property
+    def scope_objs(self):
+      """List of scopes (as OAuth2Scopes) accepted by default for the API."""
+      return self.__scopes
+
+    @property
+    def scopes(self):
+      """List of scopes (as strings) accepted by default for the API."""
+      if self.scope_objs is not None:
+        return [_s.scope for _s in self.scope_objs]
+
+    @property
+    def allowed_client_ids(self):
+      """List of client IDs accepted by default for the API."""
+      return self.__allowed_client_ids
+
+    @property
+    def issuers(self):
+      """List of auth issuers for the API."""
+      return self.__issuers
+
+    @property
+    def namespace(self):
+      """Namespace of the API."""
+      return self.__namespace
+
+    @property
+    def auth_level(self):
+      """Enum from AUTH_LEVEL specifying default frontend auth level."""
+      return self.__auth_level
+
+    @property
+    def canonical_name(self):
+      """Canonical name for the API."""
+      return self.__canonical_name
+
+    @property
+    def auth(self):
+      """Authentication configuration for this API."""
+      return self.__auth
+
+    @property
+    def api_key_required(self):
+      """Whether a key is required to call into this API."""
+      return self.__api_key_required
+
+    @property
+    def owner_domain(self):
+      """Domain of the owner of this API."""
+      return self.__owner_domain
+
+    @property
+    def owner_name(self):
+      """Name of the owner of this API."""
+      return self.__owner_name
+
+    @property
+    def package_path(self):
+      """Package this API belongs to, '/' delimited.  Used by client libs."""
+      return self.__package_path
+
+    @property
+    def frontend_limits(self):
+      """Optional query limits for unregistered developers."""
+      return self.__frontend_limits
+
+    @property
+    def title(self):
+      """Human readable name of this API."""
+      return self.__title
+
+    @property
+    def documentation(self):
+      """Link to the documentation for this version of the API."""
+      return self.__documentation
+
+    @property
+    def base_path(self):
+      """The base path for all endpoints in this API."""
+      return self.__base_path
+
+    @property
+    def limit_definitions(self):
+      """Rate limiting metric definitions for this API."""
+      return self.__limit_definitions
+
+    @property
+    def use_request_uri(self):
+      """Match request paths based on the REQUEST_URI instead of PATH_INFO."""
+      return self.__use_request_uri
+
+  def __call__(self, service_class):
+    """Decorator for ProtoRPC class that configures Google's API server.
+
+    Args:
+      service_class: remote.Service class, ProtoRPC service class being wrapped.
+
+    Returns:
+      Same class with API attributes assigned in api_info.
+    """
+    return self.api_class()(service_class)
+
+  def api_class(self, resource_name=None, path=None, audiences=None,
+                scopes=None, allowed_client_ids=None, auth_level=None,
+                api_key_required=None):
+    """Get a decorator for a class that implements an API.
+
+    This can be used for single-class or multi-class implementations.  It's
+    used implicitly in simple single-class APIs that only use @api directly.
+
+    Args:
+      resource_name: string, Resource name for the class this decorates.
+        (Default: None)
+      path: string, Base path prepended to any method paths in the class this
+        decorates. (Default: None)
+      audiences: list of strings, Acceptable audiences for authentication.
+        (Default: None)
+      scopes: list of strings, Acceptable scopes for authentication.
+        (Default: None)
+      allowed_client_ids: list of strings, Acceptable client IDs for auth.
+        (Default: None)
+      auth_level: enum from AUTH_LEVEL, Frontend authentication level.
+        (Default: None)
+      api_key_required: bool, Whether a key is required to call into this API.
+        (Default: None)
+
+    Returns:
+      A decorator function to decorate a class that implements an API.
+    """
+    if auth_level is not None:
+      _logger.warn(_AUTH_LEVEL_WARNING)
+
+    def apiserving_api_decorator(api_class):
+      """Decorator for ProtoRPC class that configures Google's API server.
+
+      Args:
+        api_class: remote.Service class, ProtoRPC service class being wrapped.
+
+      Returns:
+        Same class with API attributes assigned in api_info.
+      """
+      self.__classes.append(api_class)
+      api_class.api_info = _ApiInfo(
+          self.__common_info, resource_name=resource_name,
+          path=path, audiences=audiences, scopes=scopes,
+          allowed_client_ids=allowed_client_ids, auth_level=auth_level,
+          api_key_required=api_key_required)
+      return api_class
+
+    return apiserving_api_decorator
+
+  def get_api_classes(self):
+    """Get the list of remote.Service classes that implement this API."""
+    return self.__classes
+
+
+class ApiAuth(object):
+  """Optional authorization configuration information for an API."""
+
+  def __init__(self, allow_cookie_auth=None, blocked_regions=None):
+    """Constructor for ApiAuth, authentication information for an API.
+
+    Args:
+      allow_cookie_auth: boolean, whether cooking auth is allowed. By
+        default, API methods do not allow cookie authentication, and
+        require the use of OAuth2 or ID tokens. Setting this field to
+        True will allow cookies to be used to access the API, with
+        potentially dangerous results. Please be very cautious in enabling
+        this setting, and make sure to require appropriate XSRF tokens to
+        protect your API.
+      blocked_regions: list of Strings, a list of 2-letter ISO region codes
+        to block.
+    """
+    _CheckType(allow_cookie_auth, bool, 'allow_cookie_auth')
+    endpoints_util.check_list_type(blocked_regions, six.string_types,
+                                   'blocked_regions')
+
+    self.__allow_cookie_auth = allow_cookie_auth
+    self.__blocked_regions = blocked_regions
+
+  @property
+  def allow_cookie_auth(self):
+    """Whether cookie authentication is allowed for this API."""
+    return self.__allow_cookie_auth
+
+  @property
+  def blocked_regions(self):
+    """List of 2-letter ISO region codes to block."""
+    return self.__blocked_regions
+
+
+class ApiFrontEndLimitRule(object):
+  """Custom rule to limit unregistered traffic."""
+
+  def __init__(self, match=None, qps=None, user_qps=None, daily=None,
+               analytics_id=None):
+    """Constructor for ApiFrontEndLimitRule.
+
+    Args:
+      match: string, the matching rule that defines this traffic segment.
+      qps: int, the aggregate QPS for this segment.
+      user_qps: int, the per-end-user QPS for this segment.
+      daily: int, the aggregate daily maximum for this segment.
+      analytics_id: string, the project ID under which traffic for this segment
+        will be logged.
+    """
+    _CheckType(match, six.string_types, 'match')
+    _CheckType(qps, int, 'qps')
+    _CheckType(user_qps, int, 'user_qps')
+    _CheckType(daily, int, 'daily')
+    _CheckType(analytics_id, six.string_types, 'analytics_id')
+
+    self.__match = match
+    self.__qps = qps
+    self.__user_qps = user_qps
+    self.__daily = daily
+    self.__analytics_id = analytics_id
+
+  @property
+  def match(self):
+    """The matching rule that defines this traffic segment."""
+    return self.__match
+
+  @property
+  def qps(self):
+    """The aggregate QPS for this segment."""
+    return self.__qps
+
+  @property
+  def user_qps(self):
+    """The per-end-user QPS for this segment."""
+    return self.__user_qps
+
+  @property
+  def daily(self):
+    """The aggregate daily maximum for this segment."""
+    return self.__daily
+
+  @property
+  def analytics_id(self):
+    """Project ID under which traffic for this segment will be logged."""
+    return self.__analytics_id
+
+
+class ApiFrontEndLimits(object):
+  """Optional front end limit information for an API."""
+
+  def __init__(self, unregistered_user_qps=None, unregistered_qps=None,
+               unregistered_daily=None, rules=None):
+    """Constructor for ApiFrontEndLimits, front end limit info for an API.
+
+    Args:
+      unregistered_user_qps: int, the per-end-user QPS.  Users are identified
+        by their IP address. A value of 0 will block unregistered requests.
+      unregistered_qps: int, an aggregate QPS upper-bound for all unregistered
+        traffic. A value of 0 currently means unlimited, though it might change
+        in the future. To block unregistered requests, use unregistered_user_qps
+        or unregistered_daily instead.
+      unregistered_daily: int, an aggregate daily upper-bound for all
+        unregistered traffic. A value of 0 will block unregistered requests.
+      rules: A list or tuple of ApiFrontEndLimitRule instances: custom rules
+        used to apply limits to unregistered traffic.
+    """
+    _CheckType(unregistered_user_qps, int, 'unregistered_user_qps')
+    _CheckType(unregistered_qps, int, 'unregistered_qps')
+    _CheckType(unregistered_daily, int, 'unregistered_daily')
+    endpoints_util.check_list_type(rules, ApiFrontEndLimitRule, 'rules')
+
+    self.__unregistered_user_qps = unregistered_user_qps
+    self.__unregistered_qps = unregistered_qps
+    self.__unregistered_daily = unregistered_daily
+    self.__rules = rules
+
+  @property
+  def unregistered_user_qps(self):
+    """Per-end-user QPS limit."""
+    return self.__unregistered_user_qps
+
+  @property
+  def unregistered_qps(self):
+    """Aggregate QPS upper-bound for all unregistered traffic."""
+    return self.__unregistered_qps
+
+  @property
+  def unregistered_daily(self):
+    """Aggregate daily upper-bound for all unregistered traffic."""
+    return self.__unregistered_daily
+
+  @property
+  def rules(self):
+    """Custom rules used to apply limits to unregistered traffic."""
+    return self.__rules
+
+
+@util.positional(2)
+def api(name, version, description=None, hostname=None, audiences=None,
+        scopes=None, allowed_client_ids=None, canonical_name=None,
+        auth=None, owner_domain=None, owner_name=None, package_path=None,
+        frontend_limits=None, title=None, documentation=None, auth_level=None,
+        issuers=None, namespace=None, api_key_required=None, base_path=None,
+        limit_definitions=None, use_request_uri=None):
+  """Decorate a ProtoRPC Service class for use by the framework above.
+
+  This decorator can be used to specify an API name, version, description, and
+  hostname for your API.
+
+  Sample usage (python 2.7):
+    @endpoints.api(name='guestbook', version='v0.2',
+                   description='Guestbook API')
+    class PostService(remote.Service):
+      ...
+
+  Sample usage (python 2.5):
+    class PostService(remote.Service):
+      ...
+    endpoints.api(name='guestbook', version='v0.2',
+                  description='Guestbook API')(PostService)
+
+  Sample usage if multiple classes implement one API:
+    api_root = endpoints.api(name='library', version='v1.0')
+
+    @api_root.api_class(resource_name='shelves')
+    class Shelves(remote.Service):
+      ...
+
+    @api_root.api_class(resource_name='books', path='books')
+    class Books(remote.Service):
+      ...
+
+  Args:
+    name: string, Name of the API.
+    version: string, Version of the API.
+    description: string, Short description of the API (Default: None)
+    hostname: string, Hostname of the API (Default: app engine default host)
+    audiences: list of strings, Acceptable audiences for authentication.
+    scopes: list of strings, Acceptable scopes for authentication.
+    allowed_client_ids: list of strings, Acceptable client IDs for auth.
+    canonical_name: string, the canonical name for the API, a more human
+      readable version of the name.
+    auth: ApiAuth instance, the authentication configuration information
+      for this API.
+    owner_domain: string, the domain of the person or company that owns
+      this API.  Along with owner_name, this provides hints to properly
+      name client libraries for this API.
+    owner_name: string, the name of the owner of this API.  Along with
+      owner_domain, this provides hints to properly name client libraries
+      for this API.
+    package_path: string, the "package" this API belongs to.  This '/'
+      delimited value specifies logical groupings of APIs.  This is used by
+      client libraries of this API.
+    frontend_limits: ApiFrontEndLimits, optional query limits for unregistered
+      developers.
+    title: string, the human readable title of your API. It is exposed in the
+      discovery service.
+    documentation: string, a URL where users can find documentation about this
+      version of the API. This will be surfaced in the API Explorer and GPE
+      plugin to allow users to learn about your service.
+    auth_level: enum from AUTH_LEVEL, frontend authentication level.
+    issuers: dict, mapping auth issuer names to endpoints.Issuer objects.
+    namespace: endpoints.Namespace, the namespace for the API.
+    api_key_required: bool, whether a key is required to call into this API.
+    base_path: string, the base path for all endpoints in this API.
+    limit_definitions: list of endpoints.LimitDefinition objects, quota metric
+      definitions for this API.
+    use_request_uri: if true, match requests against REQUEST_URI instead of PATH_INFO
+
+
+  Returns:
+    Class decorated with api_info attribute, an instance of ApiInfo.
+  """
+  if auth_level is not None:
+    _logger.warn(_AUTH_LEVEL_WARNING)
+
+  return _ApiDecorator(name, version, description=description,
+                       hostname=hostname, audiences=audiences, scopes=scopes,
+                       allowed_client_ids=allowed_client_ids,
+                       canonical_name=canonical_name, auth=auth,
+                       owner_domain=owner_domain, owner_name=owner_name,
+                       package_path=package_path,
+                       frontend_limits=frontend_limits, title=title,
+                       documentation=documentation, auth_level=auth_level,
+                       issuers=issuers, namespace=namespace,
+                       api_key_required=api_key_required, base_path=base_path,
+                       limit_definitions=limit_definitions,
+                       use_request_uri=use_request_uri)
+
+
+class _MethodInfo(object):
+  """Configurable attributes of an API method.
+
+  Consolidates settings from @method decorator and/or any settings that were
+  calculating from the ProtoRPC method name, so they only need to be calculated
+  once.
+  """
+
+  @util.positional(1)
+  def __init__(self, name=None, path=None, http_method=None,
+               scopes=None, audiences=None, allowed_client_ids=None,
+               auth_level=None, api_key_required=None, request_body_class=None,
+               request_params_class=None, metric_costs=None, use_request_uri=None):
+    """Constructor.
+
+    Args:
+      name: string, Name of the method, prepended with <apiname>. to make it
+        unique.
+      path: string, Path portion of the URL to the method, for RESTful methods.
+      http_method: string, HTTP method supported by the method.
+      scopes: list of string, OAuth2 token must contain one of these scopes.
+      audiences: list of string, IdToken must contain one of these audiences.
+      allowed_client_ids: list of string, Client IDs allowed to call the method.
+      auth_level: enum from AUTH_LEVEL, Frontend auth level for the method.
+      api_key_required: bool, whether a key is required to call the method.
+      request_body_class: The type for the request body when using a
+        ResourceContainer. Otherwise, null.
+      request_params_class: The type for the request parameters when using a
+        ResourceContainer. Otherwise, null.
+      metric_costs: dict with keys matching an API limit metric and values
+        representing the cost for each successful call against that metric.
+      use_request_uri: if true, match requests against REQUEST_URI instead of PATH_INFO
+    """
+    self.__name = name
+    self.__path = path
+    self.__http_method = http_method
+    self.__scopes = endpoints_types.OAuth2Scope.convert_list(scopes)
+    self.__audiences = audiences
+    self.__allowed_client_ids = allowed_client_ids
+    self.__auth_level = auth_level
+    self.__api_key_required = api_key_required
+    self.__request_body_class = request_body_class
+    self.__request_params_class = request_params_class
+    self.__metric_costs = metric_costs
+    self.__use_request_uri = use_request_uri
+
+  def __safe_name(self, method_name):
+    """Restrict method name to a-zA-Z0-9_, first char lowercase."""
+    # Endpoints backend restricts what chars are allowed in a method name.
+    safe_name = re.sub(r'[^\.a-zA-Z0-9_]', '', method_name)
+
+    # Strip any number of leading underscores.
+    safe_name = safe_name.lstrip('_')
+
+    # Ensure the first character is lowercase.
+    # Slice from 0:1 rather than indexing [0] in case safe_name is length 0.
+    return safe_name[0:1].lower() + safe_name[1:]
+
+  @property
+  def name(self):
+    """Method name as specified in decorator or derived."""
+    return self.__name
+
+  def get_path(self, api_info):
+    """Get the path portion of the URL to the method (for RESTful methods).
+
+    Request path can be specified in the method, and it could have a base
+    path prepended to it.
+
+    Args:
+      api_info: API information for this API, possibly including a base path.
+        This is the api_info property on the class that's been annotated for
+        this API.
+
+    Returns:
+      This method's request path (not including the http://.../{base_path}
+      prefix).
+
+    Raises:
+      ApiConfigurationError: If the path isn't properly formatted.
+    """
+    path = self.__path or ''
+    if path and path[0] == '/':
+      # Absolute path, ignoring any prefixes.  Just strip off the leading /.
+      path = path[1:]
+    else:
+      # Relative path.
+      if api_info.path:
+        path = '%s%s%s' % (api_info.path, '/' if path else '', path)
+
+    # Verify that the path seems valid.
+    parts = path.split('/')
+    for n, part in enumerate(parts):
+      r = _VALID_PART_RE if n < len(parts) - 1 else _VALID_LAST_PART_RE
+      if part and '{' in part and '}' in part:
+        if not r.match(part):
+          raise api_exceptions.ApiConfigurationError(
+              'Invalid path segment: %s (part of %s)' % (part, path))
+    return path
+
+  @property
+  def http_method(self):
+    """HTTP method supported by the method (e.g. GET, POST)."""
+    return self.__http_method
+
+  @property
+  def scope_objs(self):
+    """List of scopes (as OAuth2Scopes) accepted for the API method."""
+    return self.__scopes
+
+  @property
+  def scopes(self):
+    """List of scopes (as strings) accepted for the API method."""
+    if self.scope_objs is not None:
+      return [_s.scope for _s in self.scope_objs]
+
+  @property
+  def audiences(self):
+    """List of audiences for the API method."""
+    return self.__audiences
+
+  @property
+  def allowed_client_ids(self):
+    """List of allowed client IDs for the API method."""
+    return self.__allowed_client_ids
+
+  @property
+  def auth_level(self):
+    """Enum from AUTH_LEVEL specifying default frontend auth level."""
+    return self.__auth_level
+
+  @property
+  def api_key_required(self):
+    """bool whether a key is required to call the API method."""
+    return self.__api_key_required
+
+  @property
+  def metric_costs(self):
+    """Dict mapping API limit metric names to costs against that metric."""
+    return self.__metric_costs
+
+  @property
+  def request_body_class(self):
+    """Type of request body when using a ResourceContainer."""
+    return self.__request_body_class
+
+  @property
+  def request_params_class(self):
+    """Type of request parameter message when using a ResourceContainer."""
+    return self.__request_params_class
+
+  def is_api_key_required(self, api_info):
+    if self.api_key_required is not None:
+      return self.api_key_required
+    else:
+      return api_info.api_key_required
+
+  def use_request_uri(self, api_info):
+    if self.__use_request_uri is not None:
+      return self.__use_request_uri
+    else:
+      return api_info.use_request_uri
+
+  def method_id(self, api_info):
+    """Computed method name."""
+    # This is done here for now because at __init__ time, the method is known
+    # but not the api, and thus not the api name.  Later, in
+    # ApiConfigGenerator.__method_descriptor, the api name is known.
+    if api_info.resource_name:
+      resource_part = '.%s' % self.__safe_name(api_info.resource_name)
+    else:
+      resource_part = ''
+    return '%s%s.%s' % (self.__safe_name(api_info.name), resource_part,
+                        self.__safe_name(self.name))
+
+
+@util.positional(2)
+def method(request_message=message_types.VoidMessage,
+           response_message=message_types.VoidMessage,
+           name=None,
+           path=None,
+           http_method='POST',
+           scopes=None,
+           audiences=None,
+           allowed_client_ids=None,
+           auth_level=None,
+           api_key_required=None,
+           metric_costs=None,
+           use_request_uri=None):
+  """Decorate a ProtoRPC Method for use by the framework above.
+
+  This decorator can be used to specify a method name, path, http method,
+  scopes, audiences, client ids and auth_level.
+
+  Sample usage:
+    @api_config.method(RequestMessage, ResponseMessage,
+                       name='insert', http_method='PUT')
+    def greeting_insert(request):
+      ...
+      return response
+
+  Args:
+    request_message: Message type of expected request.
+    response_message: Message type of expected response.
+    name: string, Name of the method, prepended with <apiname>. to make it
+      unique. (Default: python method name)
+    path: string, Path portion of the URL to the method, for RESTful methods.
+    http_method: string, HTTP method supported by the method. (Default: POST)
+    scopes: list of string, OAuth2 token must contain one of these scopes.
+    audiences: list of string, IdToken must contain one of these audiences.
+    allowed_client_ids: list of string, Client IDs allowed to call the method.
+      If None and auth_level is REQUIRED, no calls will be allowed.
+    auth_level: enum from AUTH_LEVEL, Frontend auth level for the method.
+    api_key_required: bool, whether a key is required to call the method
+    metric_costs: dict with keys matching an API limit metric and values
+      representing the cost for each successful call against that metric.
+    use_request_uri: if true, match requests against REQUEST_URI instead of PATH_INFO
+
+  Returns:
+    'apiserving_method_wrapper' function.
+
+  Raises:
+    TypeError: if the request_type or response_type parameters are not
+      proper subclasses of messages.Message.
+  """
+  if auth_level is not None:
+    _logger.warn(_AUTH_LEVEL_WARNING)
+
+  # Default HTTP method if one is not specified.
+  DEFAULT_HTTP_METHOD = 'POST'
+
+  def apiserving_method_decorator(api_method):
+    """Decorator for ProtoRPC method that configures Google's API server.
+
+    Args:
+      api_method: Original method being wrapped.
+
+    Returns:
+      Function responsible for actual invocation.
+      Assigns the following attributes to invocation function:
+        remote: Instance of RemoteInfo, contains remote method information.
+        remote.request_type: Expected request type for remote method.
+        remote.response_type: Response type returned from remote method.
+        method_info: Instance of _MethodInfo, api method configuration.
+      It is also assigned attributes corresponding to the aforementioned kwargs.
+
+    Raises:
+      TypeError: if the request_type or response_type parameters are not
+        proper subclasses of messages.Message.
+      KeyError: if the request_message is a ResourceContainer and the newly
+          created remote method has been reference by the container before. This
+          should never occur because a remote method is created once.
+    """
+    request_body_class = None
+    request_params_class = None
+    if isinstance(request_message, resource_container.ResourceContainer):
+      remote_decorator = remote.method(request_message.combined_message_class,
+                                       response_message)
+      request_body_class = request_message.body_message_class()
+      request_params_class = request_message.parameters_message_class()
+    else:
+      remote_decorator = remote.method(request_message, response_message)
+    remote_method = remote_decorator(api_method)
+
+    def invoke_remote(service_instance, request):
+      # If the server didn't specify any auth information, build it now.
+      # pylint: disable=protected-access
+      users_id_token._maybe_set_current_user_vars(
+          invoke_remote, api_info=getattr(service_instance, 'api_info', None),
+          request=request)
+      # pylint: enable=protected-access
+      return remote_method(service_instance, request)
+
+    invoke_remote.remote = remote_method.remote
+    if isinstance(request_message, resource_container.ResourceContainer):
+      resource_container.ResourceContainer.add_to_cache(
+          invoke_remote.remote, request_message)
+
+    invoke_remote.method_info = _MethodInfo(
+        name=name or api_method.__name__, path=path or api_method.__name__,
+        http_method=http_method or DEFAULT_HTTP_METHOD,
+        scopes=scopes, audiences=audiences,
+        allowed_client_ids=allowed_client_ids, auth_level=auth_level,
+        api_key_required=api_key_required, metric_costs=metric_costs,
+        use_request_uri=use_request_uri,
+        request_body_class=request_body_class,
+        request_params_class=request_params_class)
+    invoke_remote.__name__ = invoke_remote.method_info.name
+    return invoke_remote
+
+  endpoints_util.check_list_type(scopes, (six.string_types, endpoints_types.OAuth2Scope), 'scopes')
+  endpoints_util.check_list_type(allowed_client_ids, six.string_types,
+                                 'allowed_client_ids')
+  _CheckEnum(auth_level, AUTH_LEVEL, 'auth_level')
+
+  _CheckAudiences(audiences)
+
+  _CheckType(metric_costs, dict, 'metric_costs')
+
+  return apiserving_method_decorator
+
+
+class ApiConfigGenerator(object):
+  """Generates an API configuration 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 = ApiConfigGenerator().pretty_print_config_to_json(HelloService)
+
+  The resulting api_config will be a JSON 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
+  __HAS_BODY = 2
+
+  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 = {}
+
+    # Maps from ProtoRPC name to method id.
+    self.__id_from_name = {}
+
+  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
+
+  # TODO(dhermes): Support all the parameter types
+  # Currently missing DATE and ETAG
+  def __field_to_parameter_type(self, field):
+    """Converts the field variant type into a string describing the parameter.
+
+    Args:
+      field: An instance of a subclass of messages.Field.
+
+    Returns:
+      A string corresponding to the variant enum of the field, with a few
+        exceptions. In the case of signed ints, the 's' is dropped; for the BOOL
+        variant, 'boolean' is used; and for the ENUM variant, 'string' is used.
+
+    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 can\'t be used in a parameter.')
+
+    custom_variant_map = {
+        messages.Variant.SINT32: 'int32',
+        messages.Variant.SINT64: 'int64',
+        messages.Variant.BOOL: 'boolean',
+        messages.Variant.ENUM: 'string',
+    }
+    return custom_variant_map.get(variant) or variant.name.lower()
+
+  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, final_subfield):
+    """Returns default value of final subfield if it has one.
+
+    If this subfield comes from a field list returned from __field_to_subfields,
+    none of the fields in the subfield list can have a default except the final
+    one since they all must be message fields.
+
+    Args:
+      final_subfield: A simple field from the end of a subfield list.
+
+    Returns:
+      The default value of the subfield, if any exists, with the exception of an
+          enum field, which will have its value cast to a string.
+    """
+    if final_subfield.default:
+      if isinstance(final_subfield, messages.EnumField):
+        return final_subfield.default.name
+      else:
+        return final_subfield.default
+
+  def __parameter_enum(self, final_subfield):
+    """Returns enum descriptor of final subfield if it is an enum.
+
+    An enum descriptor is a dictionary with keys as the names from the enum and
+    each value is a dictionary with a single key "backendValue" and value equal
+    to the same enum name used to stored it in the descriptor.
+
+    The key "description" can also be used next to "backendValue", but protorpc
+    Enum classes have no way of supporting a description for each value.
+
+    Args:
+      final_subfield: A simple field from the end of a subfield list.
+
+    Returns:
+      The enum descriptor for the field, if it's an enum descriptor, else
+          returns None.
+    """
+    if isinstance(final_subfield, messages.EnumField):
+      enum_descriptor = {}
+      for enum_value in final_subfield.type.to_dict().keys():
+        enum_descriptor[enum_value] = {'backendValue': enum_value}
+      return enum_descriptor
+
+  def __parameter_descriptor(self, subfield_list):
+    """Creates descriptor for a parameter using the subfields that define it.
+
+    Each parameter is defined by a list of fields, with all but the last being
+    a message field and the final being a simple (non-message) field.
+
+    Many of the fields in the descriptor are determined solely by the simple
+    field at the end, though some (such as repeated and required) take the whole
+    chain of fields into consideration.
+
+    Args:
+      subfield_list: List of fields describing the parameter.
+
+    Returns:
+      Dictionary containing a descriptor for the parameter described by the list
+          of fields.
+    """
+    descriptor = {}
+    final_subfield = subfield_list[-1]
+
+    # Required
+    if all(subfield.required for subfield in subfield_list):
+      descriptor['required'] = True
+
+    # Type
+    descriptor['type'] = self.__field_to_parameter_type(final_subfield)
+
+    # Default
+    default = self.__parameter_default(final_subfield)
+    if default is not None:
+      descriptor['default'] = default
+
+    # Repeated
+    if any(subfield.repeated for subfield in subfield_list):
+      descriptor['repeated'] = True
+
+    # Enum
+    enum_descriptor = self.__parameter_enum(final_subfield)
+    if enum_descriptor is not None:
+      descriptor['enum'] = enum_descriptor
+
+    return descriptor
+
+  def __add_parameters_from_field(self, field, path_parameters,
+                                  params, param_order):
+    """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:
+      field: Field from which parameters will be added to the method 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: Dictionary with parameter names as keys and parameter descriptors
+          as values. This will be updated for each parameter in the field.
+      param_order: List of required parameter names to give them an order in the
+          descriptor. All required parameters in the field will be added to this
+          list.
+    """
+    for subfield_list in self.__field_to_subfields(field):
+      descriptor = self.__parameter_descriptor(subfield_list)
+
+      qualified_name = '.'.join(subfield.name for subfield in subfield_list)
+      in_path = qualified_name in path_parameters
+      if descriptor.get('required', in_path):
+        descriptor['required'] = True
+        param_order.append(qualified_name)
+
+      params[qualified_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 tuple (dict, list of string): Descriptor of the parameters, Order of the
+        parameters.
+    """
+    params = {}
+    param_order = []
+
+    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_parameters_from_field(field, matched_path_parameters,
+                                         params, param_order)
+
+    return params, param_order
+
+  # TODO(user): request_kind is only used by
+  #              __params_descriptor_without_container so can be removed
+  #              once that method is fully deprecated.
+  def __params_descriptor(self, message_type, request_kind, path, method_id):
+    """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')
+
+    Returns:
+      A tuple (dict, list of string): Descriptor of the parameters, Order of the
+        parameters.
+    """
+    path_parameter_dict = self.__get_path_parameters(path)
+
+    if not isinstance(message_type, resource_container.ResourceContainer):
+      if path_parameter_dict:
+        _logger.warning('Method %s specifies path parameters but you are not '
+                        'using a ResourceContainer; instead, you are using %r. '
+                        'This will fail in future releases; please switch to '
+                        'using ResourceContainer as soon as possible.',
+                        method_id, type(message_type))
+      return self.__params_descriptor_without_container(
+          message_type, request_kind, path)
+
+    # From here, we can assume message_type is a ResourceContainer
+    message_type = message_type.parameters_message_class()
+
+    params = {}
+    param_order = []
+
+    # Make sure all path parameters are covered.
+    for field_name, matched_path_parameters in path_parameter_dict.items():
+      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_parameters_from_field(field, matched_path_parameters,
+                                       params, param_order)
+
+    return params, param_order
+
+  def __request_message_descriptor(self, request_kind, message_type, method_id,
+                                   path):
+    """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')
+      path: string, HTTP path to method.
+
+    Returns:
+      Dictionary describing the request.
+
+    Raises:
+      ValueError: if the method path and request required fields do not match
+    """
+    descriptor = {}
+
+    params, param_order = self.__params_descriptor(message_type, request_kind,
+                                                   path, method_id)
+
+    if isinstance(message_type, resource_container.ResourceContainer):
+      message_type = message_type.body_message_class()
+
+    if (request_kind == self.__NO_BODY or
+        message_type == message_types.VoidMessage()):
+      descriptor['body'] = 'empty'
+    else:
+      descriptor['body'] = 'autoTemplate(backendRequest)'
+      descriptor['bodyName'] = 'resource'
+      self.__request_schema[method_id] = self.__parser.add_message(
+          message_type.__class__)
+
+    if params:
+      descriptor['parameters'] = params
+
+    if param_order:
+      descriptor['parameterOrder'] = param_order
+
+    return descriptor
+
+  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.
+    """
+    descriptor = {}
+
+    self.__parser.add_message(message_type.__class__)
+    if message_type == message_types.VoidMessage():
+      descriptor['body'] = 'empty'
+    else:
+      descriptor['body'] = 'autoTemplate(backendResponse)'
+      descriptor['bodyName'] = 'resource'
+      self.__response_schema[method_id] = self.__parser.ref_for_message_type(
+          message_type.__class__)
+
+    return descriptor
+
+  def __method_descriptor(self, service, method_info,
+                          rosy_method, protorpc_method_info):
+    """Describes a method.
+
+    Args:
+      service: endpoints.Service, Implementation of the API as a service.
+      method_info: _MethodInfo, Configuration for the method.
+      rosy_method: string, ProtoRPC method name prefixed with the
+        name of the service.
+      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
+
+    descriptor['path'] = method_info.get_path(service.api_info)
+    descriptor['httpMethod'] = method_info.http_method
+    descriptor['rosyMethod'] = rosy_method
+    descriptor['request'] = self.__request_message_descriptor(
+        request_kind, request_message_type,
+        method_info.method_id(service.api_info),
+        descriptor['path'])
+    descriptor['response'] = self.__response_message_descriptor(
+        remote_method.response_type(), method_info.method_id(service.api_info))
+
+    # Audiences, scopes, allowed_client_ids and auth_level could be set at
+    # either the method level or the API level.  Allow an empty list at the
+    # method level to override the setting at the API level.
+    scopes = (method_info.scopes
+              if method_info.scopes is not None
+              else service.api_info.scopes)
+    if scopes:
+      descriptor['scopes'] = scopes
+    audiences = (method_info.audiences
+                 if method_info.audiences is not None
+                 else service.api_info.audiences)
+    if audiences:
+      descriptor['audiences'] = audiences
+    allowed_client_ids = (method_info.allowed_client_ids
+                          if method_info.allowed_client_ids is not None
+                          else service.api_info.allowed_client_ids)
+    if allowed_client_ids:
+      descriptor['clientIds'] = allowed_client_ids
+
+    if remote_method.method.__doc__:
+      descriptor['description'] = remote_method.method.__doc__
+
+    auth_level = (method_info.auth_level
+                  if method_info.auth_level is not None
+                  else service.api_info.auth_level)
+    if auth_level is not None:
+      descriptor['authLevel'] = AUTH_LEVEL.reverse_mapping[auth_level]
+
+    descriptor['useRequestUri'] = method_info.use_request_uri(service.api_info)
+
+    return descriptor
+
+  def __schema_descriptor(self, services):
+    """Descriptor for the all the JSON Schema used.
+
+    Args:
+      services: List of protorpc.remote.Service instances implementing an
+        api/version.
+
+    Returns:
+      Dictionary containing all the JSON Schema used in the service.
+    """
+    methods_desc = {}
+
+    for service in services:
+      protorpc_methods = service.all_remote_methods()
+      for protorpc_method_name in protorpc_methods.keys():
+        rosy_method = '%s.%s' % (service.__name__, protorpc_method_name)
+        method_id = self.__id_from_name[rosy_method]
+
+        request_response = {}
+
+        request_schema_id = self.__request_schema.get(method_id)
+        if request_schema_id:
+          request_response['request'] = {
+              '$ref': request_schema_id
+              }
+
+        response_schema_id = self.__response_schema.get(method_id)
+        if response_schema_id:
+          request_response['response'] = {
+              '$ref': response_schema_id
+              }
+
+        methods_desc[rosy_method] = request_response
+
+    descriptor = {
+        'methods': methods_desc,
+        'schemas': self.__parser.schemas(),
+        }
+
+    return descriptor
+
+  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.api_version))
+
+    return merged_api_info
+
+  def __auth_descriptor(self, api_info):
+    """Builds an auth descriptor from API info.
+
+    Args:
+      api_info: An _ApiInfo object.
+
+    Returns:
+      A dictionary with 'allowCookieAuth' and/or 'blockedRegions' keys.
+    """
+    if api_info.auth is None:
+      return None
+
+    auth_descriptor = {}
+    if api_info.auth.allow_cookie_auth is not None:
+      auth_descriptor['allowCookieAuth'] = api_info.auth.allow_cookie_auth
+    if api_info.auth.blocked_regions:
+      auth_descriptor['blockedRegions'] = api_info.auth.blocked_regions
+
+    return auth_descriptor
+
+  def __frontend_limit_descriptor(self, api_info):
+    """Builds a frontend limit descriptor from API info.
+
+    Args:
+      api_info: An _ApiInfo object.
+
+    Returns:
+      A dictionary with frontend limit information.
+    """
+    if api_info.frontend_limits is None:
+      return None
+
+    descriptor = {}
+    for propname, descname in (('unregistered_user_qps', 'unregisteredUserQps'),
+                               ('unregistered_qps', 'unregisteredQps'),
+                               ('unregistered_daily', 'unregisteredDaily')):
+      if getattr(api_info.frontend_limits, propname) is not None:
+        descriptor[descname] = getattr(api_info.frontend_limits, propname)
+
+    rules = self.__frontend_limit_rules_descriptor(api_info)
+    if rules:
+      descriptor['rules'] = rules
+
+    return descriptor
+
+  def __frontend_limit_rules_descriptor(self, api_info):
+    """Builds a frontend limit rules descriptor from API info.
+
+    Args:
+      api_info: An _ApiInfo object.
+
+    Returns:
+      A list of dictionaries with frontend limit rules information.
+    """
+    if not api_info.frontend_limits.rules:
+      return None
+
+    rules = []
+    for rule in api_info.frontend_limits.rules:
+      descriptor = {}
+      for propname, descname in (('match', 'match'),
+                                 ('qps', 'qps'),
+                                 ('user_qps', 'userQps'),
+                                 ('daily', 'daily'),
+                                 ('analytics_id', 'analyticsId')):
+        if getattr(rule, propname) is not None:
+          descriptor[descname] = getattr(rule, propname)
+      if descriptor:
+        rules.append(descriptor)
+
+    return rules
+
+  def __api_descriptor(self, services, hostname=None):
+    """Builds a description of 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 and stored as an API
+      description document.
+
+    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
+
+    auth_descriptor = self.__auth_descriptor(merged_api_info)
+    if auth_descriptor:
+      descriptor['auth'] = auth_descriptor
+
+    frontend_limit_descriptor = self.__frontend_limit_descriptor(
+        merged_api_info)
+    if frontend_limit_descriptor:
+      descriptor['frontendLimits'] = frontend_limit_descriptor
+
+    method_map = {}
+    method_collision_tracker = {}
+    rest_collision_tracker = {}
+
+    for service in services:
+      remote_methods = service.all_remote_methods()
+      for protorpc_meth_name, protorpc_meth_info in remote_methods.items():
+        method_info = getattr(protorpc_meth_info, 'method_info', None)
+        # Skip methods that are not decorated with @method
+        if method_info is None:
+          continue
+        method_id = method_info.method_id(service.api_info)
+        rosy_method = '%s.%s' % (service.__name__, protorpc_meth_name)
+        self.__id_from_name[rosy_method] = method_id
+        method_map[method_id] = self.__method_descriptor(
+            service, method_info, rosy_method, protorpc_meth_info)
+
+        # 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,
+                           method_info.get_path(service.api_info))
+        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, method_info.get_path(service.api_info),
+               rest_collision_tracker[rest_identifier],
+               service.__name__))
+        else:
+          rest_collision_tracker[rest_identifier] = service.__name__
+
+    if method_map:
+      descriptor['methods'] = method_map
+      descriptor['descriptor'] = self.__schema_descriptor(services)
+
+    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 endpoints_util.get_app_hostname() or
+                api_info.hostname)
+    protocol = 'http' if ((hostname and hostname.startswith('localhost')) or
+                          endpoints_util.is_running_on_devserver()) else 'https'
+    base_path = api_info.base_path.strip('/')
+    defaults = {
+        'extends': 'thirdParty.api',
+        'root': '{0}://{1}/{2}'.format(protocol, hostname, base_path),
+        'name': api_info.name,
+        'version': api_info.api_version,
+        'api_version': api_info.api_version,
+        'path_version': api_info.path_version,
+        'defaultVersion': True,
+        'abstract': False,
+        'adapter': {
+            'bns': '{0}://{1}/{2}'.format(protocol, hostname, base_path),
+            'type': 'lily',
+            'deadline': 10.0
+        }
+    }
+    if api_info.canonical_name:
+      defaults['canonicalName'] = api_info.canonical_name
+    if api_info.owner_domain:
+      defaults['ownerDomain'] = api_info.owner_domain
+    if api_info.owner_name:
+      defaults['ownerName'] = api_info.owner_name
+    if api_info.package_path:
+      defaults['packagePath'] = api_info.package_path
+    if api_info.title:
+      defaults['title'] = api_info.title
+    if api_info.documentation:
+      defaults['documentation'] = api_info.documentation
+    return defaults
+
+  def get_config_dict(self, services, hostname=None):
+    """JSON dict description of a protorpc.remote.Service in API 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 API descriptor 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
+    endpoints_util.check_list_type(services, remote._ServiceClass, 'services',
+                                   allow_none=False)
+
+    return self.__api_descriptor(services, hostname=hostname)
+
+  def pretty_print_config_to_json(self, services, hostname=None):
+    """JSON string description of a protorpc.remote.Service in API 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:
+      string, The API descriptor document as a JSON string.
+    """
+    descriptor = self.get_config_dict(services, hostname)
+    return json.dumps(descriptor, sort_keys=True, indent=2,
+                      separators=(',', ': '))