Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(438)

Side by Side Diff: third_party/google-endpoints/endpoints/endpointscfg.py

Issue 2666783008: Add google-endpoints to third_party/. (Closed)
Patch Set: Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 #!/usr/bin/python
2 # Copyright 2016 Google Inc. All Rights Reserved.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 r"""External script for generating Cloud Endpoints related files.
16
17 The gen_discovery_doc subcommand takes a list of fully qualified ProtoRPC
18 service names and calls a cloud service which generates a discovery document in
19 REST or RPC style.
20
21 Example:
22 endpointscfg.py gen_discovery_doc -o . -f rest postservice.GreetingsV1
23
24 The gen_client_lib subcommand takes a discovery document and calls a cloud
25 service to generate a client library for a target language (currently just Java)
26
27 Example:
28 endpointscfg.py gen_client_lib java -o . greetings-v0.1.discovery
29
30 The get_client_lib subcommand does both of the above commands at once.
31
32 Example:
33 endpointscfg.py get_client_lib java -o . postservice.GreetingsV1
34
35 The gen_api_config command outputs an .api configuration file for a service.
36
37 Example:
38 endpointscfg.py gen_api_config -o . -a /path/to/app \
39 --hostname myhost.appspot.com postservice.GreetingsV1
40 """
41
42 from __future__ import with_statement
43
44 import argparse
45 import collections
46 import contextlib
47 # Conditional import, pylint: disable=g-import-not-at-top
48 try:
49 import json
50 except ImportError:
51 # If we can't find json packaged with Python import simplejson, which is
52 # packaged with the SDK.
53 import simplejson as json
54 import os
55 import re
56 import sys
57 import urllib
58 import urllib2
59 import _endpointscfg_setup # pylint: disable=unused-import
60 import api_config
61 from protorpc import remote
62 import openapi_generator
63 import yaml
64
65 from google.appengine.ext import testbed
66
67 DISCOVERY_DOC_BASE = ('https://webapis-discovery.appspot.com/_ah/api/'
68 'discovery/v1/apis/generate/')
69 CLIENT_LIBRARY_BASE = 'https://google-api-client-libraries.appspot.com/generate'
70 _VISIBLE_COMMANDS = ('get_client_lib', 'get_discovery_doc', 'get_openapi_spec')
71
72
73 class ServerRequestException(Exception):
74 """Exception for problems with the request to a server."""
75
76 def __init__(self, http_error):
77 """Create a ServerRequestException from a given urllib2.HTTPError.
78
79 Args:
80 http_error: The HTTPError that the ServerRequestException will be
81 based on.
82 """
83 error_details = None
84 error_response = None
85 if http_error.fp:
86 try:
87 error_response = http_error.fp.read()
88 error_body = json.loads(error_response)
89 error_details = ['%s: %s' % (detail['message'], detail['debug_info'])
90 for detail in error_body['error']['errors']]
91 except (ValueError, TypeError, KeyError):
92 pass
93 if error_details:
94 error_details_str = ', '.join(error_details)
95 error_message = ('HTTP %s (%s) error when communicating with URL: %s. '
96 'Details: %s' % (http_error.code, http_error.reason,
97 http_error.filename, error_details_str))
98 else:
99 error_message = ('HTTP %s (%s) error when communicating with URL: %s. '
100 'Response: %s' % (http_error.code, http_error.reason,
101 http_error.filename,
102 error_response))
103 super(ServerRequestException, self).__init__(error_message)
104
105
106 class _EndpointsParser(argparse.ArgumentParser):
107 """Create a subclass of argparse.ArgumentParser for Endpoints."""
108
109 def error(self, message):
110 """Override superclass to support customized error message.
111
112 Error message needs to be rewritten in order to display visible commands
113 only, when invalid command is called by user. Otherwise, hidden commands
114 will be displayed in stderr, which is not expected.
115
116 Refer the following argparse python documentation for detailed method
117 information:
118 http://docs.python.org/2/library/argparse.html#exiting-methods
119
120 Args:
121 message: original error message that will be printed to stderr
122 """
123 # subcommands_quoted is the same as subcommands, except each value is
124 # surrounded with double quotes. This is done to match the standard
125 # output of the ArgumentParser, while hiding commands we don't want users
126 # to use, as they are no longer documented and only here for legacy use.
127 subcommands_quoted = ', '.join(
128 [repr(command) for command in _VISIBLE_COMMANDS])
129 subcommands = ', '.join(_VISIBLE_COMMANDS)
130 message = re.sub(
131 r'(argument {%s}: invalid choice: .*) \(choose from (.*)\)$'
132 % subcommands, r'\1 (choose from %s)' % subcommands_quoted, message)
133 super(_EndpointsParser, self).error(message)
134
135
136 def _WriteFile(output_path, name, content):
137 """Write given content to a file in a given directory.
138
139 Args:
140 output_path: The directory to store the file in.
141 name: The name of the file to store the content in.
142 content: The content to write to the file.close
143
144 Returns:
145 The full path to the written file.
146 """
147 path = os.path.join(output_path, name)
148 with open(path, 'wb') as f:
149 f.write(content)
150 return path
151
152
153 def GenApiConfig(service_class_names, config_string_generator=None,
154 hostname=None, application_path=None):
155 """Write an API configuration for endpoints annotated ProtoRPC services.
156
157 Args:
158 service_class_names: A list of fully qualified ProtoRPC service classes.
159 config_string_generator: A generator object that produces API config strings
160 using its pretty_print_config_to_json method.
161 hostname: A string hostname which will be used as the default version
162 hostname. If no hostname is specificied in the @endpoints.api decorator,
163 this value is the fallback.
164 application_path: A string with the path to the AppEngine application.
165
166 Raises:
167 TypeError: If any service classes don't inherit from remote.Service.
168 messages.DefinitionNotFoundError: If a service can't be found.
169
170 Returns:
171 A map from service names to a string containing the API configuration of the
172 service in JSON format.
173 """
174 # First, gather together all the different APIs implemented by these
175 # classes. There may be fewer APIs than service classes. Each API is
176 # uniquely identified by (name, version). Order needs to be preserved here,
177 # so APIs that were listed first are returned first.
178 api_service_map = collections.OrderedDict()
179 for service_class_name in service_class_names:
180 module_name, base_service_class_name = service_class_name.rsplit('.', 1)
181 module = __import__(module_name, fromlist=base_service_class_name)
182 service = getattr(module, base_service_class_name)
183 if not isinstance(service, type) or not issubclass(service, remote.Service):
184 raise TypeError('%s is not a ProtoRPC service' % service_class_name)
185
186 services = api_service_map.setdefault(
187 (service.api_info.name, service.api_info.version), [])
188 services.append(service)
189
190 # If hostname isn't specified in the API or on the command line, we'll
191 # try to build it from information in app.yaml.
192 app_yaml_hostname = _GetAppYamlHostname(application_path)
193
194 service_map = collections.OrderedDict()
195 config_string_generator = (
196 config_string_generator or api_config.ApiConfigGenerator())
197 for api_info, services in api_service_map.iteritems():
198 assert services, 'An API must have at least one ProtoRPC service'
199 # Only override hostname if None. Hostname will be the same for all
200 # services within an API, since it's stored in common info.
201 hostname = services[0].api_info.hostname or hostname or app_yaml_hostname
202
203 # Map each API by name-version.
204 service_map['%s-%s' % api_info] = (
205 config_string_generator.pretty_print_config_to_json(
206 services, hostname=hostname))
207
208 return service_map
209
210
211 def _GetAppYamlHostname(application_path, open_func=open):
212 """Build the hostname for this app based on the name in app.yaml.
213
214 Args:
215 application_path: A string with the path to the AppEngine application. This
216 should be the directory containing the app.yaml file.
217 open_func: Function to call to open a file. Used to override the default
218 open function in unit tests.
219
220 Returns:
221 A hostname, usually in the form of "myapp.appspot.com", based on the
222 application name in the app.yaml file. If the file can't be found or
223 there's a problem building the name, this will return None.
224 """
225 try:
226 app_yaml_file = open_func(os.path.join(application_path or '.', 'app.yaml'))
227 config = yaml.safe_load(app_yaml_file.read())
228 except IOError:
229 # Couldn't open/read app.yaml.
230 return None
231
232 application = config.get('application')
233 if not application:
234 return None
235
236 if ':' in application:
237 # Don't try to deal with alternate domains.
238 return None
239
240 # If there's a prefix ending in a '~', strip it.
241 tilde_index = application.rfind('~')
242 if tilde_index >= 0:
243 application = application[tilde_index + 1:]
244 if not application:
245 return None
246
247 return '%s.appspot.com' % application
248
249
250 def _FetchDiscoveryDoc(config, doc_format):
251 """Fetch discovery documents generated from a cloud service.
252
253 Args:
254 config: An API config.
255 doc_format: The requested format for the discovery doc. (rest|rpc)
256
257 Raises:
258 ServerRequestException: If fetching the generated discovery doc fails.
259
260 Returns:
261 A list of discovery doc strings.
262 """
263 body = json.dumps({'config': config}, indent=2, sort_keys=True)
264 request = urllib2.Request(DISCOVERY_DOC_BASE + doc_format, body)
265 request.add_header('content-type', 'application/json')
266
267 try:
268 with contextlib.closing(urllib2.urlopen(request)) as response:
269 return response.read()
270 except urllib2.HTTPError, error:
271 raise ServerRequestException(error)
272
273
274 def _GenDiscoveryDoc(service_class_names, doc_format,
275 output_path, hostname=None,
276 application_path=None):
277 """Write discovery documents generated from a cloud service to file.
278
279 Args:
280 service_class_names: A list of fully qualified ProtoRPC service names.
281 doc_format: The requested format for the discovery doc. (rest|rpc)
282 output_path: The directory to output the discovery docs to.
283 hostname: A string hostname which will be used as the default version
284 hostname. If no hostname is specificied in the @endpoints.api decorator,
285 this value is the fallback. Defaults to None.
286 application_path: A string containing the path to the AppEngine app.
287
288 Raises:
289 ServerRequestException: If fetching the generated discovery doc fails.
290
291 Returns:
292 A list of discovery doc filenames.
293 """
294 output_files = []
295 service_configs = GenApiConfig(service_class_names, hostname=hostname,
296 application_path=application_path)
297 for api_name_version, config in service_configs.iteritems():
298 discovery_doc = _FetchDiscoveryDoc(config, doc_format)
299 discovery_name = api_name_version + '.discovery'
300 output_files.append(_WriteFile(output_path, discovery_name, discovery_doc))
301
302 return output_files
303
304
305 def _GenOpenApiSpec(service_class_names, output_path, hostname=None,
306 application_path=None):
307 """Write discovery documents generated from a cloud service to file.
308
309 Args:
310 service_class_names: A list of fully qualified ProtoRPC service names.
311 output_path: The directory to which to output the OpenAPI specs.
312 hostname: A string hostname which will be used as the default version
313 hostname. If no hostname is specified in the @endpoints.api decorator,
314 this value is the fallback. Defaults to None.
315 application_path: A string containing the path to the AppEngine app.
316
317 Returns:
318 A list of OpenAPI spec filenames.
319 """
320 output_files = []
321 service_configs = GenApiConfig(
322 service_class_names, hostname=hostname,
323 config_string_generator=openapi_generator.OpenApiGenerator(),
324 application_path=application_path)
325 for api_name_version, config in service_configs.iteritems():
326 openapi_name = api_name_version.replace('-', '') + 'openapi.json'
327 output_files.append(_WriteFile(output_path, openapi_name, config))
328
329 return output_files
330
331
332 def _GenClientLib(discovery_path, language, output_path, build_system):
333 """Write a client library from a discovery doc, using a cloud service to file.
334
335 Args:
336 discovery_path: Path to the discovery doc used to generate the client
337 library.
338 language: The client library language to generate. (java)
339 output_path: The directory to output the client library zip to.
340 build_system: The target build system for the client library language.
341
342 Raises:
343 IOError: If reading the discovery doc fails.
344 ServerRequestException: If fetching the generated client library fails.
345
346 Returns:
347 The path to the zipped client library.
348 """
349 with open(discovery_path) as f:
350 discovery_doc = f.read()
351
352 client_name = re.sub(r'\.discovery$', '.zip',
353 os.path.basename(discovery_path))
354
355 return _GenClientLibFromContents(discovery_doc, language, output_path,
356 build_system, client_name)
357
358
359 def _GenClientLibFromContents(discovery_doc, language, output_path,
360 build_system, client_name):
361 """Write a client library from a discovery doc, using a cloud service to file.
362
363 Args:
364 discovery_doc: A string, the contents of the discovery doc used to
365 generate the client library.
366 language: A string, the client library language to generate. (java)
367 output_path: A string, the directory to output the client library zip to.
368 build_system: A string, the target build system for the client language.
369 client_name: A string, the filename used to save the client lib.
370
371 Raises:
372 IOError: If reading the discovery doc fails.
373 ServerRequestException: If fetching the generated client library fails.
374
375 Returns:
376 The path to the zipped client library.
377 """
378
379 body = urllib.urlencode({'lang': language, 'content': discovery_doc,
380 'layout': build_system})
381 request = urllib2.Request(CLIENT_LIBRARY_BASE, body)
382 try:
383 with contextlib.closing(urllib2.urlopen(request)) as response:
384 content = response.read()
385 return _WriteFile(output_path, client_name, content)
386 except urllib2.HTTPError, error:
387 raise ServerRequestException(error)
388
389
390 def _GetClientLib(service_class_names, language, output_path, build_system,
391 hostname=None, application_path=None):
392 """Fetch client libraries from a cloud service.
393
394 Args:
395 service_class_names: A list of fully qualified ProtoRPC service names.
396 language: The client library language to generate. (java)
397 output_path: The directory to output the discovery docs to.
398 build_system: The target build system for the client library language.
399 hostname: A string hostname which will be used as the default version
400 hostname. If no hostname is specificied in the @endpoints.api decorator,
401 this value is the fallback. Defaults to None.
402 application_path: A string containing the path to the AppEngine app.
403
404 Returns:
405 A list of paths to client libraries.
406 """
407 client_libs = []
408 service_configs = GenApiConfig(service_class_names, hostname=hostname,
409 application_path=application_path)
410 for api_name_version, config in service_configs.iteritems():
411 discovery_doc = _FetchDiscoveryDoc(config, 'rest')
412 client_name = api_name_version + '.zip'
413 client_libs.append(
414 _GenClientLibFromContents(discovery_doc, language, output_path,
415 build_system, client_name))
416 return client_libs
417
418
419 def _GenApiConfigCallback(args, api_func=GenApiConfig):
420 """Generate an api file.
421
422 Args:
423 args: An argparse.Namespace object to extract parameters from.
424 api_func: A function that generates and returns an API configuration
425 for a list of services.
426 """
427 service_configs = api_func(args.service,
428 hostname=args.hostname,
429 application_path=args.application)
430
431 for api_name_version, config in service_configs.iteritems():
432 _WriteFile(args.output, api_name_version + '.api', config)
433
434
435 def _GetClientLibCallback(args, client_func=_GetClientLib):
436 """Generate discovery docs and client libraries to files.
437
438 Args:
439 args: An argparse.Namespace object to extract parameters from.
440 client_func: A function that generates client libraries and stores them to
441 files, accepting a list of service names, a client library language,
442 an output directory, a build system for the client library language, and
443 a hostname.
444 """
445 client_paths = client_func(
446 args.service, args.language, args.output, args.build_system,
447 hostname=args.hostname, application_path=args.application)
448
449 for client_path in client_paths:
450 print 'API client library written to %s' % client_path
451
452
453 def _GenDiscoveryDocCallback(args, discovery_func=_GenDiscoveryDoc):
454 """Generate discovery docs to files.
455
456 Args:
457 args: An argparse.Namespace object to extract parameters from
458 discovery_func: A function that generates discovery docs and stores them to
459 files, accepting a list of service names, a discovery doc format, and an
460 output directory.
461 """
462 discovery_paths = discovery_func(args.service, args.format,
463 args.output, hostname=args.hostname,
464 application_path=args.application)
465 for discovery_path in discovery_paths:
466 print 'API discovery document written to %s' % discovery_path
467
468
469 def _GenOpenApiSpecCallback(args, openapi_func=_GenOpenApiSpec):
470 """Generate OpenAPI (Swagger) specs to files.
471
472 Args:
473 args: An argparse.Namespace object to extract parameters from
474 openapi_func: A function that generates OpenAPI specs and stores them to
475 files, accepting a list of service names and an output directory.
476 """
477 openapi_paths = openapi_func(args.service, args.output,
478 hostname=args.hostname,
479 application_path=args.application)
480 for openapi_path in openapi_paths:
481 print 'OpenAPI spec written to %s' % openapi_path
482
483
484 def _GenClientLibCallback(args, client_func=_GenClientLib):
485 """Generate a client library to file.
486
487 Args:
488 args: An argparse.Namespace object to extract parameters from
489 client_func: A function that generates client libraries and stores them to
490 files, accepting a path to a discovery doc, a client library language, an
491 output directory, and a build system for the client library language.
492 """
493 client_path = client_func(args.discovery_doc[0], args.language, args.output,
494 args.build_system)
495 print 'API client library written to %s' % client_path
496
497
498 def MakeParser(prog):
499 """Create an argument parser.
500
501 Args:
502 prog: The name of the program to use when outputting help text.
503
504 Returns:
505 An argparse.ArgumentParser built to specification.
506 """
507
508 def AddStandardOptions(parser, *args):
509 """Add common endpoints options to a parser.
510
511 Args:
512 parser: The parser to add options to.
513 *args: A list of option names to add. Possible names are: application,
514 format, output, language, service, and discovery_doc.
515 """
516 if 'application' in args:
517 parser.add_argument('-a', '--application', default='.',
518 help='The path to the Python App Engine App')
519 if 'format' in args:
520 parser.add_argument('-f', '--format', default='rest',
521 choices=['rest', 'rpc'],
522 help='The requested API protocol type')
523 if 'hostname' in args:
524 help_text = ('Default application hostname, if none is specified '
525 'for API service.')
526 parser.add_argument('--hostname', help=help_text)
527 if 'output' in args:
528 parser.add_argument('-o', '--output', default='.',
529 help='The directory to store output files')
530 if 'language' in args:
531 parser.add_argument('language',
532 help='The target output programming language')
533 if 'service' in args:
534 parser.add_argument('service', nargs='+',
535 help='Fully qualified service class name')
536 if 'discovery_doc' in args:
537 parser.add_argument('discovery_doc', nargs=1,
538 help='Path to the discovery document')
539 if 'build_system' in args:
540 parser.add_argument('-bs', '--build_system', default='default',
541 help='The target build system')
542
543 parser = _EndpointsParser(prog=prog)
544 subparsers = parser.add_subparsers(
545 title='subcommands', metavar='{%s}' % ', '.join(_VISIBLE_COMMANDS))
546
547 get_client_lib = subparsers.add_parser(
548 'get_client_lib', help=('Generates discovery documents and client '
549 'libraries from service classes'))
550 get_client_lib.set_defaults(callback=_GetClientLibCallback)
551 AddStandardOptions(get_client_lib, 'application', 'hostname', 'output',
552 'language', 'service', 'build_system')
553
554 get_discovery_doc = subparsers.add_parser(
555 'get_discovery_doc',
556 help='Generates discovery documents from service classes')
557 get_discovery_doc.set_defaults(callback=_GenDiscoveryDocCallback)
558 AddStandardOptions(get_discovery_doc, 'application', 'format', 'hostname',
559 'output', 'service')
560
561 get_openapi_spec = subparsers.add_parser(
562 'get_openapi_spec',
563 help='Generates OpenAPI (Swagger) specs from service classes')
564 get_openapi_spec.set_defaults(callback=_GenOpenApiSpecCallback)
565 AddStandardOptions(get_openapi_spec, 'application', 'hostname', 'output',
566 'service')
567
568 # Create an alias for get_openapi_spec called get_swagger_spec to support
569 # the old-style naming. This won't be a visible command, but it will still
570 # function to support legacy scripts.
571 get_swagger_spec = subparsers.add_parser(
572 'get_swagger_spec',
573 help='Generates OpenAPI (Swagger) specs from service classes')
574 get_swagger_spec.set_defaults(callback=_GenOpenApiSpecCallback)
575 AddStandardOptions(get_swagger_spec, 'application', 'hostname', 'output',
576 'service')
577
578 # By removing the help attribute, the following three actions won't be
579 # displayed in usage message
580 gen_api_config = subparsers.add_parser('gen_api_config')
581 gen_api_config.set_defaults(callback=_GenApiConfigCallback)
582 AddStandardOptions(gen_api_config, 'application', 'hostname', 'output',
583 'service')
584
585 gen_discovery_doc = subparsers.add_parser('gen_discovery_doc')
586 gen_discovery_doc.set_defaults(callback=_GenDiscoveryDocCallback)
587 AddStandardOptions(gen_discovery_doc, 'application', 'format', 'hostname',
588 'output', 'service')
589
590 gen_client_lib = subparsers.add_parser('gen_client_lib')
591 gen_client_lib.set_defaults(callback=_GenClientLibCallback)
592 AddStandardOptions(gen_client_lib, 'output', 'language', 'discovery_doc',
593 'build_system')
594
595 return parser
596
597
598 def _SetupStubs():
599 tb = testbed.Testbed()
600 tb.setup_env(CURRENT_VERSION_ID='1.0')
601 tb.activate()
602 for k, v in testbed.INIT_STUB_METHOD_NAMES.iteritems():
603 # The old stub initialization code didn't support the image service at all
604 # so we just ignore it here.
605 if k != 'images':
606 getattr(tb, v)()
607
608
609 def main(argv):
610 _SetupStubs()
611
612 parser = MakeParser(argv[0])
613 args = parser.parse_args(argv[1:])
614
615 # Handle the common "application" argument here, since most of the handlers
616 # use this.
617 application_path = getattr(args, 'application', None)
618 if application_path is not None:
619 sys.path.insert(0, os.path.abspath(application_path))
620
621 args.callback(args)
622
623
624 if __name__ == '__main__':
625 main(sys.argv)
OLDNEW
« no previous file with comments | « third_party/google-endpoints/endpoints/endpoints_dispatcher.py ('k') | third_party/google-endpoints/endpoints/errors.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698