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=(',', ': '))