App Engine Java SDK version 1.9.14
[gae.git] / python / google / appengine / tools / endpointscfg.py
blobade4fcfc8a1432de9a5854a35bcd896ee1df1e23
1 #!/usr/bin/env python
3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
18 """External script for generating Cloud Endpoints related files.
20 The gen_discovery_doc subcommand takes a list of fully qualified ProtoRPC
21 service names and calls a cloud service which generates a discovery document in
22 REST or RPC style.
24 Example:
25 endpointscfg.py gen_discovery_doc -o . -f rest postservice.GreetingsV1
27 The gen_client_lib subcommand takes a discovery document and calls a cloud
28 service to generate a client library for a target language (currently just Java)
30 Example:
31 endpointscfg.py gen_client_lib java -o . greetings-v0.1.discovery
33 The get_client_lib subcommand does both of the above commands at once.
35 Example:
36 endpointscfg.py get_client_lib java -o . postservice.GreetingsV1
38 The gen_api_config command outputs an .api configuration file for a service.
40 Example:
41 endpointscfg.py gen_api_config -o . -a /path/to/app \
42 --hostname myhost.appspot.com postservice.GreetingsV1
43 """
45 from __future__ import with_statement
48 import argparse
49 import collections
50 import contextlib
52 try:
53 import json
54 except ImportError:
57 import simplejson as json
58 import os
59 import re
60 import sys
61 import urllib
62 import urllib2
64 from endpoints import api_config
65 from protorpc import remote
66 import yaml
68 from google.appengine.tools.devappserver2 import api_server
72 DISCOVERY_DOC_BASE = ('https://webapis-discovery.appspot.com/_ah/api/'
73 'discovery/v1/apis/generate/')
74 CLIENT_LIBRARY_BASE = 'https://google-api-client-libraries.appspot.com/generate'
75 _VISIBLE_COMMANDS = ('get_client_lib', 'get_discovery_doc')
78 class ServerRequestException(Exception):
79 """Exception for problems with the request to a server."""
81 def __init__(self, http_error):
82 """Create a ServerRequestException from a given urllib2.HTTPError.
84 Args:
85 http_error: The HTTPError that the ServerRequestException will be
86 based on.
87 """
88 error_details = None
89 error_response = None
90 if http_error.fp:
91 try:
92 error_response = http_error.fp.read()
93 error_body = json.loads(error_response)
94 error_details = ['%s: %s' % (detail['message'], detail['debug_info'])
95 for detail in error_body['error']['errors']]
96 except (ValueError, TypeError, KeyError):
97 pass
98 if error_details:
99 error_details_str = ', '.join(error_details)
100 error_message = ('HTTP %s (%s) error when communicating with URL: %s. '
101 'Details: %s' % (http_error.code, http_error.reason,
102 http_error.filename, error_details_str))
103 else:
104 error_message = ('HTTP %s (%s) error when communicating with URL: %s. '
105 'Response: %s' % (http_error.code, http_error.reason,
106 http_error.filename,
107 error_response))
108 super(ServerRequestException, self).__init__(error_message)
111 class _EndpointsParser(argparse.ArgumentParser):
112 """Create a subclass of argparse.ArgumentParser for Endpoints."""
114 def error(self, message):
115 """Override superclass to support customized error message.
117 Error message needs to be rewritten in order to display visible commands
118 only, when invalid command is called by user. Otherwise, hidden commands
119 will be displayed in stderr, which is not expected.
121 Refer the following argparse python documentation for detailed method
122 information:
123 http://docs.python.org/2/library/argparse.html#exiting-methods
125 Args:
126 message: original error message that will be printed to stderr
132 subcommands_quoted = ', '.join(
133 [repr(command) for command in _VISIBLE_COMMANDS])
134 subcommands = ', '.join(_VISIBLE_COMMANDS)
135 message = re.sub(
136 r'(argument {%s}: invalid choice: .*) \(choose from (.*)\)$'
137 % subcommands, r'\1 (choose from %s)' % subcommands_quoted, message)
138 super(_EndpointsParser, self).error(message)
141 def _WriteFile(output_path, name, content):
142 """Write given content to a file in a given directory.
144 Args:
145 output_path: The directory to store the file in.
146 name: The name of the file to store the content in.
147 content: The content to write to the file.close
149 Returns:
150 The full path to the written file.
152 path = os.path.join(output_path, name)
153 with open(path, 'wb') as f:
154 f.write(content)
155 return path
158 def GenApiConfig(service_class_names, config_string_generator=None,
159 hostname=None, application_path=None):
160 """Write an API configuration for endpoints annotated ProtoRPC services.
162 Args:
163 service_class_names: A list of fully qualified ProtoRPC service classes.
164 config_string_generator: A generator object that produces API config strings
165 using its pretty_print_config_to_json method.
166 hostname: A string hostname which will be used as the default version
167 hostname. If no hostname is specificied in the @endpoints.api decorator,
168 this value is the fallback.
169 application_path: A string with the path to the AppEngine application.
171 Raises:
172 TypeError: If any service classes don't inherit from remote.Service.
173 messages.DefinitionNotFoundError: If a service can't be found.
175 Returns:
176 A map from service names to a string containing the API configuration of the
177 service in JSON format.
183 api_service_map = collections.OrderedDict()
184 for service_class_name in service_class_names:
185 module_name, base_service_class_name = service_class_name.rsplit('.', 1)
186 module = __import__(module_name, fromlist=base_service_class_name)
187 service = getattr(module, base_service_class_name)
188 if not isinstance(service, type) or not issubclass(service, remote.Service):
189 raise TypeError('%s is not a ProtoRPC service' % service_class_name)
191 services = api_service_map.setdefault(
192 (service.api_info.name, service.api_info.version), [])
193 services.append(service)
197 app_yaml_hostname = _GetAppYamlHostname(application_path)
199 service_map = collections.OrderedDict()
200 config_string_generator = (
201 config_string_generator or api_config.ApiConfigGenerator())
202 for api_info, services in api_service_map.iteritems():
203 assert len(services) > 0, 'An API must have at least one ProtoRPC service'
206 hostname = services[0].api_info.hostname or hostname or app_yaml_hostname
209 service_map['%s-%s' % api_info] = (
210 config_string_generator.pretty_print_config_to_json(
211 services, hostname=hostname))
213 return service_map
216 def _GetAppYamlHostname(application_path, open_func=open):
217 """Build the hostname for this app based on the name in app.yaml.
219 Args:
220 application_path: A string with the path to the AppEngine application. This
221 should be the directory containing the app.yaml file.
222 open_func: Function to call to open a file. Used to override the default
223 open function in unit tests.
225 Returns:
226 A hostname, usually in the form of "myapp.appspot.com", based on the
227 application name in the app.yaml file. If the file can't be found or
228 there's a problem building the name, this will return None.
230 try:
231 app_yaml_file = open_func(os.path.join(application_path or '.', 'app.yaml'))
232 config = yaml.safe_load(app_yaml_file.read())
233 except IOError:
235 return None
237 application = config.get('application')
238 if not application:
239 return None
241 if ':' in application:
243 return None
246 tilde_index = application.rfind('~')
247 if tilde_index >= 0:
248 application = application[tilde_index + 1:]
249 if not application:
250 return None
252 return '%s.appspot.com' % application
255 def _FetchDiscoveryDoc(config, doc_format):
256 """Fetch discovery documents generated from a cloud service.
258 Args:
259 config: An API config.
260 doc_format: The requested format for the discovery doc. (rest|rpc)
262 Raises:
263 ServerRequestException: If fetching the generated discovery doc fails.
265 Returns:
266 A list of discovery doc strings.
268 body = json.dumps({'config': config}, indent=2, sort_keys=True)
269 request = urllib2.Request(DISCOVERY_DOC_BASE + doc_format, body)
270 request.add_header('content-type', 'application/json')
272 try:
273 with contextlib.closing(urllib2.urlopen(request)) as response:
274 return response.read()
275 except urllib2.HTTPError, error:
276 raise ServerRequestException(error)
279 def _GenDiscoveryDoc(service_class_names, doc_format,
280 output_path, hostname=None,
281 application_path=None):
282 """Write discovery documents generated from a cloud service to file.
284 Args:
285 service_class_names: A list of fully qualified ProtoRPC service names.
286 doc_format: The requested format for the discovery doc. (rest|rpc)
287 output_path: The directory to output the discovery docs to.
288 hostname: A string hostname which will be used as the default version
289 hostname. If no hostname is specificied in the @endpoints.api decorator,
290 this value is the fallback. Defaults to None.
291 application_path: A string containing the path to the AppEngine app.
293 Raises:
294 ServerRequestException: If fetching the generated discovery doc fails.
296 Returns:
297 A list of discovery doc filenames.
299 output_files = []
300 service_configs = GenApiConfig(service_class_names, hostname=hostname,
301 application_path=application_path)
302 for api_name_version, config in service_configs.iteritems():
303 discovery_doc = _FetchDiscoveryDoc(config, doc_format)
304 discovery_name = api_name_version + '.discovery'
305 output_files.append(_WriteFile(output_path, discovery_name, discovery_doc))
307 return output_files
310 def _GenClientLib(discovery_path, language, output_path, build_system):
311 """Write a client library from a discovery doc, using a cloud service to file.
313 Args:
314 discovery_path: Path to the discovery doc used to generate the client
315 library.
316 language: The client library language to generate. (java)
317 output_path: The directory to output the client library zip to.
318 build_system: The target build system for the client library language.
320 Raises:
321 IOError: If reading the discovery doc fails.
322 ServerRequestException: If fetching the generated client library fails.
324 Returns:
325 The path to the zipped client library.
327 with open(discovery_path) as f:
328 discovery_doc = f.read()
330 client_name = re.sub(r'\.discovery$', '.zip',
331 os.path.basename(discovery_path))
333 return _GenClientLibFromContents(discovery_doc, language, output_path,
334 build_system, client_name)
337 def _GenClientLibFromContents(discovery_doc, language, output_path,
338 build_system, client_name):
339 """Write a client library from a discovery doc, using a cloud service to file.
341 Args:
342 discovery_doc: A string, the contents of the discovery doc used to
343 generate the client library.
344 language: A string, the client library language to generate. (java)
345 output_path: A string, the directory to output the client library zip to.
346 build_system: A string, the target build system for the client language.
347 client_name: A string, the filename used to save the client lib.
349 Raises:
350 IOError: If reading the discovery doc fails.
351 ServerRequestException: If fetching the generated client library fails.
353 Returns:
354 The path to the zipped client library.
357 body = urllib.urlencode({'lang': language, 'content': discovery_doc,
358 'layout': build_system})
359 request = urllib2.Request(CLIENT_LIBRARY_BASE, body)
360 try:
361 with contextlib.closing(urllib2.urlopen(request)) as response:
362 content = response.read()
363 return _WriteFile(output_path, client_name, content)
364 except urllib2.HTTPError, error:
365 raise ServerRequestException(error)
368 def _GetClientLib(service_class_names, language, output_path, build_system,
369 hostname=None, application_path=None):
370 """Fetch client libraries from a cloud service.
372 Args:
373 service_class_names: A list of fully qualified ProtoRPC service names.
374 language: The client library language to generate. (java)
375 output_path: The directory to output the discovery docs to.
376 build_system: The target build system for the client library language.
377 hostname: A string hostname which will be used as the default version
378 hostname. If no hostname is specificied in the @endpoints.api decorator,
379 this value is the fallback. Defaults to None.
380 application_path: A string containing the path to the AppEngine app.
382 Returns:
383 A list of paths to client libraries.
385 client_libs = []
386 service_configs = GenApiConfig(service_class_names, hostname=hostname,
387 application_path=application_path)
388 for api_name_version, config in service_configs.iteritems():
389 discovery_doc = _FetchDiscoveryDoc(config, 'rest')
390 client_name = api_name_version + '.zip'
391 client_libs.append(
392 _GenClientLibFromContents(discovery_doc, language, output_path,
393 build_system, client_name))
394 return client_libs
397 def _GenApiConfigCallback(args, api_func=GenApiConfig):
398 """Generate an api file.
400 Args:
401 args: An argparse.Namespace object to extract parameters from.
402 api_func: A function that generates and returns an API configuration
403 for a list of services.
405 service_configs = api_func(args.service,
406 hostname=args.hostname,
407 application_path=args.application)
409 for api_name_version, config in service_configs.iteritems():
410 _WriteFile(args.output, api_name_version + '.api', config)
413 def _GetClientLibCallback(args, client_func=_GetClientLib):
414 """Generate discovery docs and client libraries to files.
416 Args:
417 args: An argparse.Namespace object to extract parameters from.
418 client_func: A function that generates client libraries and stores them to
419 files, accepting a list of service names, a client library language,
420 an output directory, a build system for the client library language, and
421 a hostname.
423 client_paths = client_func(
424 args.service, args.language, args.output, args.build_system,
425 hostname=args.hostname, application_path=args.application)
427 for client_path in client_paths:
428 print 'API client library written to %s' % client_path
431 def _GenDiscoveryDocCallback(args, discovery_func=_GenDiscoveryDoc):
432 """Generate discovery docs to files.
434 Args:
435 args: An argparse.Namespace object to extract parameters from
436 discovery_func: A function that generates discovery docs and stores them to
437 files, accepting a list of service names, a discovery doc format, and an
438 output directory.
440 discovery_paths = discovery_func(args.service, args.format,
441 args.output, hostname=args.hostname,
442 application_path=args.application)
443 for discovery_path in discovery_paths:
444 print 'API discovery document written to %s' % discovery_path
447 def _GenClientLibCallback(args, client_func=_GenClientLib):
448 """Generate a client library to file.
450 Args:
451 args: An argparse.Namespace object to extract parameters from
452 client_func: A function that generates client libraries and stores them to
453 files, accepting a path to a discovery doc, a client library language, an
454 output directory, and a build system for the client library language.
456 client_path = client_func(args.discovery_doc[0], args.language, args.output,
457 args.build_system)
458 print 'API client library written to %s' % client_path
461 def MakeParser(prog):
462 """Create an argument parser.
464 Args:
465 prog: The name of the program to use when outputting help text.
467 Returns:
468 An argparse.ArgumentParser built to specification.
471 def AddStandardOptions(parser, *args):
472 """Add common endpoints options to a parser.
474 Args:
475 parser: The parser to add options to.
476 *args: A list of option names to add. Possible names are: application,
477 format, output, language, service, and discovery_doc.
479 if 'application' in args:
480 parser.add_argument('-a', '--application', default='.',
481 help='The path to the Python App Engine App')
482 if 'format' in args:
483 parser.add_argument('-f', '--format', default='rest',
484 choices=['rest', 'rpc'],
485 help='The requested API protocol type')
486 if 'hostname' in args:
487 help_text = ('Default application hostname, if none is specified '
488 'for API service.')
489 parser.add_argument('--hostname', help=help_text)
490 if 'output' in args:
491 parser.add_argument('-o', '--output', default='.',
492 help='The directory to store output files')
493 if 'language' in args:
494 parser.add_argument('language',
495 help='The target output programming language')
496 if 'service' in args:
497 parser.add_argument('service', nargs='+',
498 help='Fully qualified service class name')
499 if 'discovery_doc' in args:
500 parser.add_argument('discovery_doc', nargs=1,
501 help='Path to the discovery document')
502 if 'build_system' in args:
503 parser.add_argument('-bs', '--build_system', default='default',
504 help='The target build system')
506 parser = _EndpointsParser(prog=prog)
507 subparsers = parser.add_subparsers(
508 title='subcommands', metavar='{%s}' % ', '.join(_VISIBLE_COMMANDS))
510 get_client_lib = subparsers.add_parser(
511 'get_client_lib', help=('Generates discovery documents and client '
512 'libraries from service classes'))
513 get_client_lib.set_defaults(callback=_GetClientLibCallback)
514 AddStandardOptions(get_client_lib, 'application', 'hostname', 'output',
515 'language', 'service', 'build_system')
517 get_discovery_doc = subparsers.add_parser(
518 'get_discovery_doc',
519 help='Generates discovery documents from service classes')
520 get_discovery_doc.set_defaults(callback=_GenDiscoveryDocCallback)
521 AddStandardOptions(get_discovery_doc, 'application', 'format', 'hostname',
522 'output', 'service')
526 gen_api_config = subparsers.add_parser('gen_api_config')
527 gen_api_config.set_defaults(callback=_GenApiConfigCallback)
528 AddStandardOptions(gen_api_config, 'application', 'hostname', 'output',
529 'service')
531 gen_discovery_doc = subparsers.add_parser('gen_discovery_doc')
532 gen_discovery_doc.set_defaults(callback=_GenDiscoveryDocCallback)
533 AddStandardOptions(gen_discovery_doc, 'application', 'format', 'hostname',
534 'output', 'service')
536 gen_client_lib = subparsers.add_parser('gen_client_lib')
537 gen_client_lib.set_defaults(callback=_GenClientLibCallback)
538 AddStandardOptions(gen_client_lib, 'output', 'language', 'discovery_doc',
539 'build_system')
541 return parser
544 def main(argv):
545 api_server.test_setup_stubs(app_id='_')
547 parser = MakeParser(argv[0])
548 args = parser.parse_args(argv[1:])
552 application_path = getattr(args, 'application', None)
553 if application_path is not None:
554 sys.path.insert(0, os.path.abspath(application_path))
556 args.callback(args)
559 if __name__ == '__main__':
560 main(sys.argv)