blob: 3a00283cbfb7a72efe4e38cc54c6cfbedac43737 [file] [log] [blame]
#!/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.
#
"""External script for generating Cloud Endpoints related files.
The gen_discovery_doc subcommand takes a list of fully qualified ProtoRPC
service names and calls a cloud service which generates a discovery document in
REST or RPC style.
Example:
endpointscfg.py gen_discovery_doc -o . -f rest postservice.GreetingsV1
The gen_client_lib subcommand takes a discovery document and calls a cloud
service to generate a client library for a target language (currently just Java)
Example:
endpointscfg.py gen_client_lib java -o . greetings-v0.1.discovery
The get_client_lib subcommand does both of the above commands at once.
Example:
endpointscfg.py get_client_lib java -o . -f rest postservice.GreetingsV1
The gen_api_config command outputs an .api configuration file for a service.
Example:
endpointscfg.py gen_api_config -o . -a /path/to/app \
--hostname myhost.appspot.com postservice.GreetingsV1
"""
from __future__ import with_statement
import collections
import contextlib
try:
import json
except ImportError:
import simplejson as json
import os
import re
import sys
import urllib
import urllib2
from endpoints import api_config
from protorpc import remote
from google.appengine.tools.devappserver2 import api_server
DISCOVERY_DOC_BASE = ('https://webapis-discovery.appspot.com/_ah/api/'
'discovery/v1/apis/generate/')
CLIENT_LIBRARY_BASE = 'https://google-api-client-libraries.appspot.com/generate'
class ServerRequestException(Exception):
"""Exception for problems with the request to a server."""
def __init__(self, http_error):
"""Create a ServerRequestException from a given urllib2.HTTPError.
Args:
http_error: The HTTPError that the ServerRequestException will be
based on.
"""
error_details = None
if http_error.fp:
try:
error_body = json.load(http_error.fp)
error_details = ['%s: %s' % (detail['message'], detail['debug_info'])
for detail in error_body['error']['errors']]
except (ValueError, TypeError, KeyError):
pass
if error_details:
error_message = ('HTTP %s (%s) error when communicating with URL: %s. '
'Details: %s' % (http_error.code, http_error.reason,
http_error.filename, error_details))
else:
error_message = ('HTTP %s (%s) error when communicating with URL: %s.' %
(http_error.code, http_error.reason,
http_error.filename))
super(ServerRequestException, self).__init__(error_message)
def _WriteFile(output_path, name, content):
"""Write given content to a file in a given directory.
Args:
output_path: The directory to store the file in.
name: The name of the file to store the content in.
content: The content to write to the file.close
Returns:
The full path to the written file.
"""
path = os.path.join(output_path, name)
with open(path, 'wb') as f:
f.write(content)
return path
def GenApiConfig(service_class_names, generator=None, hostname=None):
"""Write an API configuration for endpoints annotated ProtoRPC services.
Args:
service_class_names: A list of fully qualified ProtoRPC service classes.
generator: An generator object that produces API config strings using its
pretty_print_config_to_json method.
hostname: A string hostname which will be used as the default version
hostname. If no hostname is specificied in the @endpoints.api decorator,
this value is the fallback. Defaults to None.
Raises:
TypeError: If any service classes don't inherit from remote.Service.
messages.DefinitionNotFoundError: If a service can't be found.
Returns:
A map from service names to a string containing the API configuration of the
service in JSON format.
"""
api_service_map = collections.OrderedDict()
for service_class_name in service_class_names:
module_name, base_service_class_name = service_class_name.rsplit('.', 1)
module = __import__(module_name, fromlist=base_service_class_name)
service = getattr(module, base_service_class_name)
if not (isinstance(service, type) and issubclass(service, remote.Service)):
raise TypeError('%s is not a ProtoRPC service' % service_class_name)
services = api_service_map.setdefault((service.api_info.name,
service.api_info.version),
[])
services.append(service)
service_map = collections.OrderedDict()
generator = generator or api_config.ApiConfigGenerator()
for api_info, services in api_service_map.iteritems():
hostname = services[0].api_info.hostname or hostname
service_map['%s-%s' % api_info] = generator.pretty_print_config_to_json(
services, hostname=hostname)
return service_map
def GenDiscoveryDoc(service_class_names, doc_format,
output_path, hostname=None):
"""Write discovery documents generated from a cloud service to file.
Args:
service_class_names: A list of fully qualified ProtoRPC service names.
doc_format: The requested format for the discovery doc. (rest|rpc)
output_path: The directory to output the discovery docs to.
hostname: A string hostname which will be used as the default version
hostname. If no hostname is specificied in the @endpoints.api decorator,
this value is the fallback. Defaults to None.
Raises:
ServerRequestException: If fetching the generated discovery doc fails.
Returns:
A list of discovery doc filenames.
"""
output_files = []
service_configs = GenApiConfig(service_class_names, hostname=hostname)
for api_name_version, config in service_configs.iteritems():
body = json.dumps({'config': config}, indent=2, sort_keys=True)
request = urllib2.Request(DISCOVERY_DOC_BASE + doc_format, body)
request.add_header('content-type', 'application/json')
try:
with contextlib.closing(urllib2.urlopen(request)) as response:
content = response.read()
discovery_name = api_name_version + '.discovery'
output_files.append(_WriteFile(output_path, discovery_name, content))
except urllib2.HTTPError, error:
raise ServerRequestException(error)
return output_files
def GenClientLib(discovery_path, language, output_path, build_system):
"""Write a client library from a discovery doc, using a cloud service to file.
Args:
discovery_path: Path to the discovery doc used to generate the client
library.
language: The client library language to generate. (java)
output_path: The directory to output the client library zip to.
build_system: The target build system for the client library language.
Raises:
IOError: If reading the discovery doc fails.
ServerRequestException: If fetching the generated client library fails.
Returns:
The path to the zipped client library.
"""
with open(discovery_path) as f:
discovery_doc = f.read()
client_name = re.sub(r'\.discovery$', '.zip',
os.path.basename(discovery_path))
_GenClientLibFromContents(discovery_doc, language, output_path, build_system,
client_name)
def _GenClientLibFromContents(discovery_doc, language, output_path,
build_system, client_name):
"""Write a client library from a discovery doc, using a cloud service to file.
Args:
discovery_doc: A string, the contents of the discovery doc used to
generate the client library.
language: A string, the client library language to generate. (java)
output_path: A string, the directory to output the client library zip to.
build_system: A string, the target build system for the client language.
client_name: A string, the filename used to save the client lib.
Raises:
IOError: If reading the discovery doc fails.
ServerRequestException: If fetching the generated client library fails.
Returns:
The path to the zipped client library.
"""
body = urllib.urlencode({'lang': language, 'content': discovery_doc,
'layout': build_system})
request = urllib2.Request(CLIENT_LIBRARY_BASE, body)
try:
with contextlib.closing(urllib2.urlopen(request)) as response:
content = response.read()
return _WriteFile(output_path, client_name, content)
except urllib2.HTTPError, error:
raise ServerRequestException(error)
def GetClientLib(service_class_names, doc_format, language,
output_path, build_system, hostname=None):
"""Fetch discovery documents and client libraries from a cloud service.
Args:
service_class_names: A list of fully qualified ProtoRPC service names.
doc_format: The requested format for the discovery doc. (rest|rpc)
language: The client library language to generate. (java)
output_path: The directory to output the discovery docs to.
build_system: The target build system for the client library language.
hostname: A string hostname which will be used as the default version
hostname. If no hostname is specificied in the @endpoints.api decorator,
this value is the fallback. Defaults to None.
Returns:
A tuple (discovery_files, client_libs):
discovery_files: A list of paths to discovery documents.
client_libs: A list of paths to client libraries.
"""
discovery_files = GenDiscoveryDoc(service_class_names, doc_format,
output_path, hostname=hostname)
client_libs = []
for discovery_path in discovery_files:
client_libs.append(
GenClientLib(discovery_path, language, output_path, build_system))
return discovery_files, client_libs
def _GenApiConfigCallback(args, api_func=GenApiConfig):
"""Generate an api file.
Args:
args: An argparse.Namespace object to extract parameters from.
api_func: A function that generates and returns an API configuration
for a list of services.
"""
service_class_names, output_path, hostname = (
args.service, args.output, args.hostname)
service_configs = api_func(service_class_names, hostname=hostname)
for api_name_version, config in service_configs.iteritems():
api_name = api_name_version + '.api'
_WriteFile(output_path, api_name, config)
def _GetClientLibCallback(args,
client_func=GetClientLib):
"""Generate discovery docs and client libraries to files.
Args:
args: An argparse.Namespace object to extract parameters from.
client_func: A function that generates client libraries and stores them to
files, accepting a list of service names, a discovery doc format, a client
library language, an output directory, a build system for the client
library language, and a hostname.
"""
service_class_names = args.service
doc_format = args.format
language = args.language
output_path = args.output
hostname = args.hostname
build_system = args.build_system
discovery_paths, client_paths = client_func(
service_class_names, doc_format, language, output_path, build_system,
hostname=hostname)
for discovery_path in discovery_paths:
print 'API discovery document written to %s' % discovery_path
for client_path in client_paths:
print 'API client library written to %s' % client_path
def _GenDiscoveryDocCallback(args, discovery_func=GenDiscoveryDoc):
"""Generate discovery docs to files.
Args:
args: An argparse.Namespace object to extract parameters from
discovery_func: A function that generates discovery docs and stores them to
files, accepting a list of service names, a discovery doc format, and an
output directory.
"""
services, doc_format, output_path, hostname = (
args.service, args.format, args.output, args.hostname)
discovery_paths = discovery_func(services, doc_format,
output_path, hostname=hostname)
for discovery_path in discovery_paths:
print 'API discovery document written to %s' % discovery_path
def _GenClientLibCallback(args, client_func=GenClientLib):
"""Generate a client library to file.
Args:
args: An argparse.Namespace object to extract parameters from
client_func: A function that generates client libraries and stores them to
files, accepting a path to a discovery doc, a client library language, an
output directory, and a build system for the client library language.
"""
discovery_path, language, output_path, build_system = (
args.discovery_doc[0], args.language, args.output, args.build_system)
client_path = client_func(discovery_path, language, output_path, build_system)
print 'API client library written to %s' % client_path
def MakeParser(prog):
"""Create an argument parser.
Args:
prog: The name of the program to use when outputting help text.
Returns:
An argparse.ArgumentParser built to specification.
"""
import argparse
def AddStandardOptions(parser, *args):
"""Add common endpoints options to a parser.
Args:
parser: The parser to add options to.
*args: A list of option names to add. Possible names are: application,
format, output, language, service, and discovery_doc.
"""
if 'application' in args:
parser.add_argument('-a', '--application', default='.',
help='The path to the Python App Engine App')
if 'format' in args:
parser.add_argument('-f', '--format', default='rest',
choices=['rest', 'rpc'],
help='The requested API protocol type')
if 'hostname' in args:
help_text = ('Default application hostname, if none is specified '
'for API service.')
parser.add_argument('--hostname', help=help_text)
if 'output' in args:
parser.add_argument('-o', '--output', default='.',
help='The directory to store output files')
if 'language' in args:
parser.add_argument('language', choices=['java'],
help='The target output programming language')
if 'service' in args:
parser.add_argument('service', nargs='+',
help='Fully qualified service class name')
if 'discovery_doc' in args:
parser.add_argument('discovery_doc', nargs=1,
help='Path to the discovery document')
if 'build_system' in args:
parser.add_argument('-bs', '--build_system', default='default',
help='The target build system')
parser = argparse.ArgumentParser(prog=prog)
subparsers = parser.add_subparsers(
title='subcommands', metavar='{get_client_lib, get_discovery_doc}')
get_client_lib = subparsers.add_parser(
'get_client_lib', help=('Generates discovery documents and client '
'libraries from service classes'))
get_client_lib.set_defaults(callback=_GetClientLibCallback)
AddStandardOptions(get_client_lib, 'application', 'format', 'hostname',
'output', 'language', 'service', 'build_system')
get_discovery_doc = subparsers.add_parser(
'get_discovery_doc',
help='Generates discovery documents from service classes')
get_discovery_doc.set_defaults(callback=_GenDiscoveryDocCallback)
AddStandardOptions(get_discovery_doc, 'application', 'format', 'hostname',
'output', 'service')
gen_api_config = subparsers.add_parser('gen_api_config')
gen_api_config.set_defaults(callback=_GenApiConfigCallback)
AddStandardOptions(gen_api_config, 'application', 'hostname', 'output',
'service')
gen_discovery_doc = subparsers.add_parser('gen_discovery_doc')
gen_discovery_doc.set_defaults(callback=_GenDiscoveryDocCallback)
AddStandardOptions(gen_discovery_doc, 'application', 'format', 'hostname',
'output', 'service')
gen_client_lib = subparsers.add_parser('gen_client_lib')
gen_client_lib.set_defaults(callback=_GenClientLibCallback)
AddStandardOptions(gen_client_lib, 'output', 'language', 'discovery_doc',
'build_system')
return parser
def main(argv):
api_server.test_setup_stubs(app_id='_')
parser = MakeParser(argv[0])
args = parser.parse_args(argv[1:])
application_path = getattr(args, 'application', None)
if application_path is not None:
sys.path.insert(0, os.path.abspath(application_path))
args.callback(args)
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv))