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