blob: f229b8d64112158b378c761ba51f5c0a6e88c7c2 [file]
#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""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)
"""
try:
import json
except ImportError:
import simplejson as json
import logging
import re
from protorpc import message_types
from protorpc import messages
from protorpc import remote
from protorpc import util
from google.appengine.api import app_identity
from google.appengine.ext.endpoints import message_parser
from google.appengine.ext.endpoints import users_id_token
__all__ = [
'API_EXPLORER_CLIENT_ID',
'ApiConfigGenerator',
'CacheControl',
'EMAIL_SCOPE',
'api',
'method',
]
API_EXPLORER_CLIENT_ID = '292824132082.apps.googleusercontent.com'
EMAIL_SCOPE = 'https://www.googleapis.com/auth/userinfo.email'
def _CheckListType(settings, allowed_type, name, allow_none=True):
"""Verify that settings in list are of the allowed type or raise TypeError.
Args:
settings: The list of settings to check.
allowed_type: The allowed type of items in 'settings'.
name: Name of the setting, added to the exception.
allow_none: If set, None is also allowed.
Raises:
TypeError: if setting is not of the allowed type.
Returns:
The list of settings, for convenient use in assignment.
"""
if settings is None:
if not allow_none:
raise TypeError('%s is None, which is not allowed.' % name)
return settings
if not isinstance(settings, (tuple, list)):
raise TypeError('%s is not a list.' % name)
if not all(isinstance(i, allowed_type) for i in settings):
type_list = list(set(type(setting) for setting in settings))
raise TypeError('%s contains types that don\'t match %s: %s' %
(name, allowed_type.__name__, type_list))
return settings
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))
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):
"""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)
"""
_CheckType(resource_name, basestring, 'resource_name')
_CheckType(path, basestring, 'path')
_CheckListType(audiences, basestring, 'audiences')
_CheckListType(scopes, basestring, 'scopes')
_CheckListType(allowed_client_ids, basestring, 'allowed_client_ids')
self.__common_info = common_info
self.__resource_name = resource_name
self.__path = path
self.__audiences = audiences
self.__scopes = scopes
self.__allowed_client_ids = allowed_client_ids
@property
def name(self):
"""Name of the API."""
return self.__common_info.name
@property
def version(self):
"""Version of the API."""
return self.__common_info.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 scopes(self):
"""List of scopes accepted for the API, overriding the defaults."""
if self.__scopes is not None:
return self.__scopes
return self.__common_info.scopes
@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 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
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 collection() 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):
"""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.
"""
self.__common_info = self.__ApiCommonInfo(
name, version, description=description, hostname=hostname,
audiences=audiences, scopes=scopes,
allowed_client_ids=allowed_client_ids)
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):
"""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.
"""
_CheckType(name, basestring, 'name', allow_none=False)
_CheckType(version, basestring, 'version', allow_none=False)
_CheckType(description, basestring, 'description')
_CheckType(hostname, basestring, 'hostname')
_CheckListType(audiences, basestring, 'audiences')
_CheckListType(scopes, basestring, 'scopes')
_CheckListType(allowed_client_ids, basestring, 'allowed_client_ids')
if hostname is None:
hostname = app_identity.get_default_version_hostname()
if audiences is None and hostname is not None:
audiences = [hostname]
if scopes is None:
scopes = [EMAIL_SCOPE]
if allowed_client_ids is None:
allowed_client_ids = [API_EXPLORER_CLIENT_ID]
self.__name = name
self.__version = version
self.__description = description
self.__hostname = hostname
self.__audiences = audiences
self.__scopes = scopes
self.__allowed_client_ids = allowed_client_ids
@property
def name(self):
"""Name of the API."""
return self.__name
@property
def version(self):
"""Version of the API."""
return self.__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 scopes(self):
"""List of scopes accepted by default for the API."""
return self.__scopes
@property
def allowed_client_ids(self):
"""List of client IDs accepted by default for the API."""
return self.__allowed_client_ids
def __call__(self, 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.
"""
return self.collection()(api_class)
def collection(self, resource_name=None, path=None, audiences=None,
scopes=None, allowed_client_ids=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)
Returns:
A decorator function to decorate a class that implements an API.
"""
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.
"""
api_class.api_info = _ApiInfo(
self.__common_info, resource_name=resource_name,
path=path, audiences=audiences, scopes=scopes,
allowed_client_ids=allowed_client_ids)
return api_class
return apiserving_api_decorator
@util.positional(2)
def api(name, version, description=None, hostname=None, audiences=None,
scopes=None, allowed_client_ids=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):
pass
Sample usage (python 2.5):
class PostService(remote.Service):
pass
endpoints.api(name='guestbook', version='v0.2',
description='Guestbook API')(PostService)
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.
Returns:
Class decorated with api_info attribute, an instance of ApiInfo.
"""
return _ApiDecorator(name, version, description=description,
hostname=hostname, audiences=audiences, scopes=scopes,
allowed_client_ids=allowed_client_ids)
class CacheControl(object):
"""Cache control settings for an API method.
Setting is composed of a directive and maximum cache age.
Available types:
PUBLIC - Allows clients and proxies to cache responses.
PRIVATE - Allows only clients to cache responses.
NO_CACHE - Allows none to cache responses.
"""
PUBLIC = 'public'
PRIVATE = 'private'
NO_CACHE = 'no-cache'
VALID_VALUES = (PUBLIC, PRIVATE, NO_CACHE)
def __init__(self, directive=NO_CACHE, max_age_seconds=0):
"""Constructor.
Args:
directive: string, Cache control directive, as above. (Default: NO_CACHE)
max_age_seconds: int, Maximum age of cache responses. (Default: 0)
"""
if directive not in self.VALID_VALUES:
directive = self.NO_CACHE
self.__directive = directive
self.__max_age_seconds = max_age_seconds
@property
def directive(self):
"""The cache setting for this method, PUBLIC, PRIVATE, or NO_CACHE."""
return self.__directive
@property
def max_age_seconds(self):
"""The maximum age of cache responses for this method, in seconds."""
return self.__max_age_seconds
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,
cache_control=None, scopes=None, audiences=None,
allowed_client_ids=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.
cache_control: CacheControl, Cache settings for the API 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.
"""
self.__name = name
self.__path = path
self.__http_method = http_method
self.__cache_control = cache_control
self.__scopes = scopes
self.__audiences = audiences
self.__allowed_client_ids = allowed_client_ids
def __safe_name(self, method_name):
"""Restrict method name to a-zA-Z0-9, first char lowercase."""
safe_name = re.sub('[^\.a-zA-Z0-9]', '', method_name)
return safe_name[0:1].lower() + safe_name[1:]
@property
def name(self):
"""Method name as specified in decorator or derived."""
return self.__name
@property
def path(self):
"""Deprecated: path portion of the URL to the method."""
logging.warning('_MethodInfopath property is deprecated. '
'Use the get_path method instead.')
return self.__path
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://.../_ah/api/ prefix).
"""
path = self.__path or ''
if path and path[0] == '/':
path = path[1:]
else:
if api_info.path:
path = '%s%s%s' % (api_info.path, '/' if path else '', path)
return path
@property
def http_method(self):
"""HTTP method supported by the method (e.g. GET, POST)."""
return self.__http_method
@property
def cache_control(self):
"""Cache control setting for the API method."""
return self.__cache_control
@property
def scopes(self):
"""List of scopes for the API method."""
return self.__scopes
@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
def method_id(self, api_info):
"""Computed method name."""
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',
cache_control=None,
scopes=None,
audiences=None,
allowed_client_ids=None):
"""Decorate a ProtoRPC Method for use by the framework above.
This decorator can be used to specify a method name, path, http method,
cache control, scopes, audiences, and client ids
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)
cache_control: CacheControl, Cache settings for the API 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.
Currently limited to 5. If None, no calls will be allowed.
Returns:
'apiserving_method_wrapper' function.
Raises:
ValueError: if more than 5 allowed_client_ids are specified.
TypeError: if the request_type or response_type parameters are not
proper subclasses of messages.Message.
"""
DEFAULT_HTTP_METHOD = 'POST'
def check_type(setting, allowed_type, name, allow_none=True):
"""Verify that the setting is of the allowed type or raise TypeError.
Args:
setting: The setting to check.
allowed_type: The allowed type.
name: Name of the setting, added to the exception.
allow_none: If set, None is also allowed.
Raises:
TypeError: if setting is not of the allowed type.
Returns:
The setting, for convenient use in assignment.
"""
if (setting is None and allow_none or
isinstance(setting, allowed_type)):
return setting
raise TypeError('%s is not of type %s' % (name, allowed_type.__name__))
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.
"""
remote_decorator = remote.method(request_message, response_message)
remote_method = remote_decorator(api_method)
def invoke_remote(service_instance, request):
users_id_token._maybe_set_current_user_vars(
invoke_remote, api_info=getattr(service_instance, 'api_info', None))
return remote_method(service_instance, request)
invoke_remote.remote = remote_method.remote
invoke_remote.method_info = _MethodInfo(
name=name or api_method.__name__, path=path or '',
http_method=http_method or DEFAULT_HTTP_METHOD,
cache_control=cache_control, scopes=scopes, audiences=audiences,
allowed_client_ids=allowed_client_ids)
invoke_remote.__name__ = invoke_remote.method_info.name
return invoke_remote
check_type(cache_control, CacheControl, 'cache_control')
_CheckListType(scopes, basestring, 'scopes')
_CheckListType(audiences, basestring, 'audiences')
_CheckListType(allowed_client_ids, basestring, 'allowed_client_ids')
if allowed_client_ids is not None and len(allowed_client_ids) > 5:
raise ValueError('allowed_client_ids must have 5 or fewer entries.')
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.
"""
__NO_BODY = 1
__BODY_ONLY = 2
__FIELD_TO_PARAM_TYPE_MAP = {
messages.IntegerField: 'int64',
messages.FloatField: 'double',
messages.BooleanField: 'boolean',
messages.BytesField: 'string',
messages.StringField: 'string',
messages.MessageField: 'object',
messages.EnumField: 'string',
}
__DEFAULT_PARAM_TYPE = 'string'
def __init__(self):
self.__parser = message_parser.MessageTypeToJsonSchema()
self.__request_schema = {}
self.__response_schema = {}
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.__BODY_ONLY
def __params_descriptor(self, message_type, request_kind, path):
"""Describe the parameters of a method.
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
Raises:
ValueError: if the method path and request required fields do not match
"""
params = {}
param_order = []
for field in sorted(message_type.all_fields(), key=lambda f: f.number):
descriptor = {}
required = field.required
if '{%s}' % field.name in path:
required = True
elif request_kind != self.__NO_BODY:
continue
if required:
param_order.append(field.name)
descriptor['required'] = True
param_type = self.__FIELD_TO_PARAM_TYPE_MAP.get(
type(field), self.__DEFAULT_PARAM_TYPE)
descriptor['type'] = param_type
if field.default:
if isinstance(field, messages.EnumField):
descriptor['default'] = str(field.default)
else:
descriptor['default'] = field.default
if field.repeated:
descriptor['repeated'] = True
if isinstance(field, messages.EnumField):
enum_descriptor = {}
for enum_value in field.type.to_dict().keys():
enum_descriptor[enum_value] = {'backendValue': enum_value}
descriptor['enum'] = enum_descriptor
params[field.name] = descriptor
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 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)
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,
cache_control):
"""Describes the response.
Args:
message_type: messages.Message class, The message to describe.
method_id: string, Unique method identifier (e.g. 'myapi.items.method')
cache_control: CacheControl, Cache settings for the API 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__)
if cache_control is not None:
descriptor['cacheControl'] = {
'type': cache_control.directive,
'maxAge': cache_control.max_age_seconds,
}
return descriptor
def __method_descriptor(self, service, service_name, method_info,
protorpc_method_name, protorpc_method_info):
"""Describes a method.
Args:
service: endpoints.Service, Implementation of the API as a service.
service_name: string, Name of the service.
method_info: _MethodInfo, Configuration for the method.
protorpc_method_name: string, Name of the method as given in the
ProtoRPC implementation.
protorpc_method_info: protorpc.remote._RemoteMethodInfo, ProtoRPC
description of the method.
Returns:
Dictionary describing the method.
Raises:
ValueError: if the method path and request required fields do not match
"""
descriptor = {}
request_message_type = protorpc_method_info.remote.request_type()
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'] = '%s.%s' % (service_name, protorpc_method_name)
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),
method_info.cache_control)
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__
return descriptor
def __schema_descriptor(self, service_name, protorpc_methods):
"""Descriptor for the all the JSON Schema used.
Args:
service_name: string, Name of the service.
protorpc_methods: dict, Map of protorpc_method_name to
protorpc.remote._RemoteMethodInfo.
Returns:
Dictionary containing all the JSON Schema used in the service.
"""
methods_desc = {}
for protorpc_method_name in protorpc_methods.iterkeys():
method_id = self.__id_from_name[protorpc_method_name]
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
}
rosy_method = '%s.%s' % (service_name, protorpc_method_name)
methods_desc[rosy_method] = request_response
descriptor = {
'methods': methods_desc,
'schemas': self.__parser.schemas(),
}
return descriptor
def __api_descriptor(self, service, hostname=None):
"""Builds a description of an API.
Args:
service: protorpc.remote.Service, Implementation of the API as a service.
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:
ValueError: if any method path and request required fields do not match
"""
descriptor = self.get_descriptor_defaults(service, hostname=hostname)
description = service.api_info.description or service.__doc__
if description:
descriptor['description'] = description
method_map = {}
protorpc_methods = service.all_remote_methods()
for protorpc_meth_name, protorpc_meth_info in protorpc_methods.iteritems():
method_info = getattr(protorpc_meth_info, 'method_info', None)
if method_info is None:
continue
method_id = method_info.method_id(service.api_info)
self.__id_from_name[protorpc_meth_name] = method_id
method_map[method_id] = self.__method_descriptor(
service, service.__name__, method_info,
protorpc_meth_name, protorpc_meth_info)
if method_map:
descriptor['methods'] = method_map
descriptor['descriptor'] = self.__schema_descriptor(
service.__name__, protorpc_methods)
return descriptor
def get_descriptor_defaults(self, service, hostname=None):
"""Gets a default configuration for a service.
Args:
service: protorpc.remote.Service, implementation of the API as a 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 service.api_info.hostname
return {
'extends': 'thirdParty.api',
'root': 'https://%s/_ah/api' % hostname,
'name': service.api_info.name,
'version': service.api_info.version,
'defaultVersion': True,
'abstract': False,
'adapter': {
'bns': 'https://%s/_ah/spi' % hostname,
'type': 'lily'
}
}
def pretty_print_config_to_json(self, service, hostname=None):
"""Description of a protorpc.remote.Service in API format.
Args:
service: protorpc.remote.Service, Implementation of the API as a service.
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 JSON.
Raises:
ValueError: if any method path and request required fields do not match
"""
descriptor = self.__api_descriptor(service, hostname=hostname)
return json.dumps(descriptor, sort_keys=True, indent=2)