1#!/usr/bin/env python
2#
3# Copyright 2015 Google Inc.
4#
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
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
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.
16
17"""Command-line interface to gen_client."""
18
19import argparse
20import contextlib
21import io
22import json
23import logging
24import os
25import pkgutil
26import sys
27
28from apitools.base.py import exceptions
29from apitools.gen import gen_client_lib
30from apitools.gen import util
31
32
33def _CopyLocalFile(filename):
34    with contextlib.closing(io.open(filename, 'w')) as out:
35        src_data = pkgutil.get_data(
36            'apitools.base.py', filename)
37        if src_data is None:
38            raise exceptions.GeneratedClientError(
39                'Could not find file %s' % filename)
40        out.write(src_data)
41
42
43def _GetDiscoveryDocFromFlags(args):
44    """Get the discovery doc from flags."""
45    if args.discovery_url:
46        try:
47            return util.FetchDiscoveryDoc(args.discovery_url)
48        except exceptions.CommunicationError:
49            raise exceptions.GeneratedClientError(
50                'Could not fetch discovery doc')
51
52    infile = os.path.expanduser(args.infile) or '/dev/stdin'
53    with io.open(infile, encoding='utf8') as f:
54        return json.loads(util.ReplaceHomoglyphs(f.read()))
55
56
57def _GetCodegenFromFlags(args):
58    """Create a codegen object from flags."""
59    discovery_doc = _GetDiscoveryDocFromFlags(args)
60    names = util.Names(
61        args.strip_prefix,
62        args.experimental_name_convention,
63        args.experimental_capitalize_enums)
64
65    if args.client_json:
66        try:
67            with io.open(args.client_json, encoding='utf8') as client_json:
68                f = json.loads(util.ReplaceHomoglyphs(client_json.read()))
69                web = f.get('installed', f.get('web', {}))
70                client_id = web.get('client_id')
71                client_secret = web.get('client_secret')
72        except IOError:
73            raise exceptions.NotFoundError(
74                'Failed to open client json file: %s' % args.client_json)
75    else:
76        client_id = args.client_id
77        client_secret = args.client_secret
78
79    if not client_id:
80        logging.warning('No client ID supplied')
81        client_id = ''
82
83    if not client_secret:
84        logging.warning('No client secret supplied')
85        client_secret = ''
86
87    client_info = util.ClientInfo.Create(
88        discovery_doc, args.scope, client_id, client_secret,
89        args.user_agent, names, args.api_key)
90    outdir = os.path.expanduser(args.outdir) or client_info.default_directory
91    if os.path.exists(outdir) and not args.overwrite:
92        raise exceptions.ConfigurationValueError(
93            'Output directory exists, pass --overwrite to replace '
94            'the existing files.')
95    if not os.path.exists(outdir):
96        os.makedirs(outdir)
97
98    return gen_client_lib.DescriptorGenerator(
99        discovery_doc, client_info, names, args.root_package, outdir,
100        base_package=args.base_package,
101        protorpc_package=args.protorpc_package,
102        init_wildcards_file=(args.init_file == 'wildcards'),
103        use_proto2=args.experimental_proto2_output,
104        unelidable_request_methods=args.unelidable_request_methods,
105        apitools_version=args.apitools_version)
106
107
108# TODO(user): Delete this if we don't need this functionality.
109def _WriteBaseFiles(codegen):
110    with util.Chdir(codegen.outdir):
111        _CopyLocalFile('base_api.py')
112        _CopyLocalFile('credentials_lib.py')
113        _CopyLocalFile('exceptions.py')
114
115
116def _WriteIntermediateInit(codegen):
117    with io.open('__init__.py', 'w') as out:
118        codegen.WriteIntermediateInit(out)
119
120
121def _WriteProtoFiles(codegen):
122    with util.Chdir(codegen.outdir):
123        with io.open(codegen.client_info.messages_proto_file_name, 'w') as out:
124            codegen.WriteMessagesProtoFile(out)
125        with io.open(codegen.client_info.services_proto_file_name, 'w') as out:
126            codegen.WriteServicesProtoFile(out)
127
128
129def _WriteGeneratedFiles(args, codegen):
130    if codegen.use_proto2:
131        _WriteProtoFiles(codegen)
132    with util.Chdir(codegen.outdir):
133        with io.open(codegen.client_info.messages_file_name, 'w') as out:
134            codegen.WriteMessagesFile(out)
135        with io.open(codegen.client_info.client_file_name, 'w') as out:
136            codegen.WriteClientLibrary(out)
137
138
139def _WriteInit(codegen):
140    with util.Chdir(codegen.outdir):
141        with io.open('__init__.py', 'w') as out:
142            codegen.WriteInit(out)
143
144
145def _WriteSetupPy(codegen):
146    with io.open('setup.py', 'w') as out:
147        codegen.WriteSetupPy(out)
148
149
150def GenerateClient(args):
151
152    """Driver for client code generation."""
153
154    codegen = _GetCodegenFromFlags(args)
155    if codegen is None:
156        logging.error('Failed to create codegen, exiting.')
157        return 128
158    _WriteGeneratedFiles(args, codegen)
159    if args.init_file != 'none':
160        _WriteInit(codegen)
161
162
163def GeneratePipPackage(args):
164
165    """Generate a client as a pip-installable tarball."""
166
167    discovery_doc = _GetDiscoveryDocFromFlags(args)
168    package = discovery_doc['name']
169    original_outdir = os.path.expanduser(args.outdir)
170    args.outdir = os.path.join(
171        args.outdir, 'apitools/clients/%s' % package)
172    args.root_package = 'apitools.clients.%s' % package
173    codegen = _GetCodegenFromFlags(args)
174    if codegen is None:
175        logging.error('Failed to create codegen, exiting.')
176        return 1
177    _WriteGeneratedFiles(args, codegen)
178    _WriteInit(codegen)
179    with util.Chdir(original_outdir):
180        _WriteSetupPy(codegen)
181        with util.Chdir('apitools'):
182            _WriteIntermediateInit(codegen)
183            with util.Chdir('clients'):
184                _WriteIntermediateInit(codegen)
185
186
187def GenerateProto(args):
188    """Generate just the two proto files for a given API."""
189
190    codegen = _GetCodegenFromFlags(args)
191    _WriteProtoFiles(codegen)
192
193
194class _SplitCommaSeparatedList(argparse.Action):
195
196    def __call__(self, parser, namespace, values, option_string=None):
197        setattr(namespace, self.dest, values.split(','))
198
199
200def main(argv=None):
201    if argv is None:
202        argv = sys.argv
203    parser = argparse.ArgumentParser(
204        description='Apitools Client Code Generator')
205
206    discovery_group = parser.add_mutually_exclusive_group()
207    discovery_group.add_argument(
208        '--infile',
209        help=('Filename for the discovery document. Mutually exclusive with '
210              '--discovery_url'))
211
212    discovery_group.add_argument(
213        '--discovery_url',
214        help=('URL (or "name.version") of the discovery document to use. '
215              'Mutually exclusive with --infile.'))
216
217    parser.add_argument(
218        '--base_package',
219        default='apitools.base.py',
220        help='Base package path of apitools (defaults to apitools.base.py')
221
222    parser.add_argument(
223        '--protorpc_package',
224        default='apitools.base.protorpclite',
225        help=('Base package path of protorpc '
226              '(defaults to apitools.base.protorpclite'))
227
228    parser.add_argument(
229        '--outdir',
230        default='',
231        help='Directory name for output files. (Defaults to the API name.)')
232
233    parser.add_argument(
234        '--overwrite',
235        default=False, action='store_true',
236        help='Only overwrite the output directory if this flag is specified.')
237
238    parser.add_argument(
239        '--root_package',
240        default='',
241        help=('Python import path for where these modules '
242              'should be imported from.'))
243
244    parser.add_argument(
245        '--strip_prefix', nargs='*',
246        default=[],
247        help=('Prefix to strip from type names in the discovery document. '
248              '(May be specified multiple times.)'))
249
250    parser.add_argument(
251        '--api_key',
252        help=('API key to use for API access.'))
253
254    parser.add_argument(
255        '--client_json',
256        help=('Use the given file downloaded from the dev. console for '
257              'client_id and client_secret.'))
258
259    parser.add_argument(
260        '--client_id',
261        default='1042881264118.apps.googleusercontent.com',
262        help='Client ID to use for the generated client.')
263
264    parser.add_argument(
265        '--client_secret',
266        default='x_Tw5K8nnjoRAqULM9PFAC2b',
267        help='Client secret for the generated client.')
268
269    parser.add_argument(
270        '--scope', nargs='*',
271        default=[],
272        help=('Scopes to request in the generated client. '
273              'May be specified more than once.'))
274
275    parser.add_argument(
276        '--user_agent',
277        default='x_Tw5K8nnjoRAqULM9PFAC2b',
278        help=('User agent for the generated client. '
279              'Defaults to <api>-generated/0.1.'))
280
281    parser.add_argument(
282        '--generate_cli', dest='generate_cli', action='store_true',
283        help='Ignored.')
284    parser.add_argument(
285        '--nogenerate_cli', dest='generate_cli', action='store_false',
286        help='Ignored.')
287
288    parser.add_argument(
289        '--init-file',
290        choices=['none', 'empty', 'wildcards'],
291        type=lambda s: s.lower(),
292        default='wildcards',
293        help='Controls whether and how to generate package __init__.py file.')
294
295    parser.add_argument(
296        '--unelidable_request_methods',
297        action=_SplitCommaSeparatedList,
298        default=[],
299        help=('Full method IDs of methods for which we should NOT try to '
300              'elide the request type. (Should be a comma-separated list.'))
301
302    parser.add_argument(
303        '--apitools_version',
304        default='', dest='apitools_version',
305        help=('Apitools version used as a requirement in generated clients. '
306              'Defaults to version of apitools used to generate the clients.'))
307
308    parser.add_argument(
309        '--experimental_capitalize_enums',
310        default=False, action='store_true',
311        help='Dangerous: attempt to rewrite enum values to be uppercase.')
312
313    parser.add_argument(
314        '--experimental_name_convention',
315        choices=util.Names.NAME_CONVENTIONS,
316        default=util.Names.DEFAULT_NAME_CONVENTION,
317        help='Dangerous: use a particular style for generated names.')
318
319    parser.add_argument(
320        '--experimental_proto2_output',
321        default=False, action='store_true',
322        help='Dangerous: also output a proto2 message file.')
323
324    subparsers = parser.add_subparsers(help='Type of generated code')
325
326    client_parser = subparsers.add_parser(
327        'client', help='Generate apitools client in destination folder')
328    client_parser.set_defaults(func=GenerateClient)
329
330    pip_package_parser = subparsers.add_parser(
331        'pip_package', help='Generate apitools client pip package')
332    pip_package_parser.set_defaults(func=GeneratePipPackage)
333
334    proto_parser = subparsers.add_parser(
335        'proto', help='Generate apitools client protos')
336    proto_parser.set_defaults(func=GenerateProto)
337
338    args = parser.parse_args(argv[1:])
339    return args.func(args) or 0
340
341
342if __name__ == '__main__':
343    sys.exit(main())
344