blob: 6b8598d56c1d4ec1b6fe6ee69b525b8c4d26616d [file] [log] [blame]
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -07001#!/usr/bin/env python
Matteo Scandolo11d074c2017-08-29 13:29:37 -07002
Zsolt Harasztiaccad4a2017-01-03 21:56:48 -08003# Copyright 2017 the original author or authors.
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -07004#
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#
Zsolt Harasztidca6fa12016-11-03 16:56:17 -070017
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070018import sys
19
20from google.protobuf.compiler import plugin_pb2 as plugin
Zsolt Haraszti46c72002016-10-10 09:55:30 -070021from google.protobuf.descriptor_pb2 import ServiceDescriptorProto, \
22 MethodOptions
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070023from jinja2 import Template
24from simplejson import dumps
25
Daniele Moro538eba72020-02-12 22:07:24 -080026from google.api import annotations_pb2, http_pb2
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070027
28template = Template("""
29# Generated file; please do not edit
30
31from simplejson import dumps, load
32from structlog import get_logger
Zsolt Harasztic8cfdf32016-11-28 14:28:39 -080033from google.protobuf.json_format import MessageToDict, ParseDict
Zsolt Haraszti2aac6232016-11-23 11:18:23 -080034from twisted.internet.defer import inlineCallbacks, returnValue
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070035
36{% set package = file_name.replace('.proto', '') %}
Zsolt Haraszticba96de2016-11-06 14:04:55 -080037
38{% for pypackage, module in includes %}
39{% if pypackage %}
40from {{ pypackage }} import {{ module }}
Scott Baker80a36b72018-03-12 18:55:53 -070041try:
42 from {{ pypackage }} import {{ module }}_grpc
43except ImportError:
44 pass
Zsolt Haraszticba96de2016-11-06 14:04:55 -080045{% else %}
46import {{ module }}
Scott Baker80a36b72018-03-12 18:55:53 -070047try:
48 import {{ module }}_grpc
49except ImportError:
50 pass
Zsolt Haraszticba96de2016-11-06 14:04:55 -080051{% endif %}
52{% endfor %}
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070053
54log = get_logger()
55
56def add_routes(app, grpc_client):
57
Zsolt Harasztiabae5912016-10-16 19:30:34 -070058 pass # so that if no endpoints are defined, Python is still happy
59
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070060 {% for method in methods %}
Zsolt Haraszticba96de2016-11-06 14:04:55 -080061 {% set method_name = method['service'].rpartition('.')[2] + '_' + method['method'] %}
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070062 {% set path = method['path'].replace('{', '<string:').replace('}', '>') %}
63 @app.route('{{ path }}', methods=['{{ method['verb'].upper() }}'])
Zsolt Haraszti2aac6232016-11-23 11:18:23 -080064 @inlineCallbacks
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070065 def {{ method_name }}(server, request, **kw):
66 log.debug('{{ method_name }}', request=request, server=server, **kw)
67 {% if method['body'] == '*' %}
68 data = load(request.content)
69 data.update(kw)
70 {% elif method['body'] == '' %}
71 data = kw
72 {% else %}
Zsolt Harasztic8cfdf32016-11-28 14:28:39 -080073 raise NotImplementedError('cannot handle specific body field list')
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070074 {% endif %}
Zsolt Harasztidca6fa12016-11-03 16:56:17 -070075 try:
Zsolt Harasztic8cfdf32016-11-28 14:28:39 -080076 req = ParseDict(data, {{ type_map[method['input_type']] }}())
Zsolt Harasztidca6fa12016-11-03 16:56:17 -070077 except Exception, e:
78 log.error('cannot-convert-to-protobuf', e=e, data=data)
79 raise
Zsolt Harasztic8cfdf32016-11-28 14:28:39 -080080 res, metadata = yield grpc_client.invoke(
Scott Baker80a36b72018-03-12 18:55:53 -070081 {{ stub_map[method['service']] }}Stub,
Zsolt Harasztic8cfdf32016-11-28 14:28:39 -080082 '{{ method['method'] }}', req, request.getAllHeaders().items())
Zsolt Harasztidca6fa12016-11-03 16:56:17 -070083 try:
Zsolt Haraszti2aac6232016-11-23 11:18:23 -080084 out_data = MessageToDict(res, True, True)
Zsolt Harasztidca6fa12016-11-03 16:56:17 -070085 except AttributeError, e:
86 filename = '/tmp/chameleon_failed_to_convert_data.pbd'
87 with file(filename, 'w') as f:
88 f.write(res.SerializeToString())
89 log.error('cannot-convert-from-protobuf', outdata_saved=filename)
90 raise
Zsolt Harasztic8cfdf32016-11-28 14:28:39 -080091 for key, value in metadata:
92 request.setHeader(key, value)
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070093 request.setHeader('Content-Type', 'application/json')
94 log.debug('{{ method_name }}', **out_data)
Zsolt Haraszti2aac6232016-11-23 11:18:23 -080095 returnValue(dumps(out_data))
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -070096
97 {% endfor %}
98
99""", trim_blocks=True, lstrip_blocks=True)
100
101
102def traverse_methods(proto_file):
103
104 package = proto_file.name
105 for service in proto_file.service:
106 assert isinstance(service, ServiceDescriptorProto)
107
108 for method in service.method:
109 options = method.options
110 assert isinstance(options, MethodOptions)
111 for fd, http in options.ListFields():
Daniele Moro538eba72020-02-12 22:07:24 -0800112 if fd.full_name == 'google.api.http':
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -0700113 assert fd.name == 'http'
114 assert isinstance(http, http_pb2.HttpRule)
115
116 input_type = method.input_type
117 if input_type.startswith('.'):
118 input_type = input_type[1:]
119
120 output_type = method.output_type
121 if output_type.startswith('.'):
122 output_type = output_type[1:]
123
124 if http.delete:
125 verb = 'delete'
126 path = http.delete
127 elif http.get:
128 verb = 'get'
129 path = http.get
130 elif http.patch:
131 verb = 'patch'
132 path = http.patch
133 elif http.post:
134 verb = 'post'
135 path = http.post
136 elif http.put:
137 verb = 'put'
138 path = http.put
139 else:
140 raise AttributeError('No valid verb in method %s' %
141 method.name)
142
143 body = http.body
144
145 data = {
146 'package': package,
147 'filename': proto_file.name,
Zsolt Haraszticba96de2016-11-06 14:04:55 -0800148 'service': proto_file.package + '.' + service.name,
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -0700149 'method': method.name,
150 'input_type': input_type,
151 'output_type': output_type,
152 'path': path,
153 'verb': verb,
154 'body': body
155 }
156
157 yield data
158
159
Scott Baker80a36b72018-03-12 18:55:53 -0700160def generate_gw_code(file_name, methods, type_map, stub_map, includes):
Zsolt Haraszticba96de2016-11-06 14:04:55 -0800161 return template.render(file_name=file_name, methods=methods,
Scott Baker80a36b72018-03-12 18:55:53 -0700162 type_map=type_map, stub_map=stub_map, includes=includes)
Zsolt Haraszticba96de2016-11-06 14:04:55 -0800163
164
165class IncludeManager(object):
166 # need to keep track of what files define what message types and
167 # under what package name. Later, when we analyze the methods, we
168 # need to be able to derive the list of files we need to load and we
169 # also need to replce the <proto-package-name>.<artifact-name> in the
170 # templates with <python-package-name>.<artifact-name> so Python can
171 # resolve these.
172 def __init__(self):
173 self.package_to_localname = {}
174 self.fullname_to_filename = {}
175 self.prefix_table = [] # sorted table of top-level symbols in protos
176 self.type_map = {} # full name as used in .proto -> python name
Scott Baker80a36b72018-03-12 18:55:53 -0700177 self.stub_map = {}
Zsolt Haraszticba96de2016-11-06 14:04:55 -0800178 self.includes_needed = set() # names of files needed to be included
179 self.filename_to_module = {} # filename -> (package, module)
180
181 def extend_symbol_tables(self, proto_file):
182 # keep track of what file adds what top-level symbol to what abstract
183 # package name
184 package_name = proto_file.package
185 file_name = proto_file.name
186 self._add_filename(file_name)
187 all_defs = list(proto_file.message_type)
188 all_defs.extend(list(proto_file.enum_type))
189 all_defs.extend(list(proto_file.service))
190 for typedef in all_defs:
191 name = typedef.name
192 fullname = package_name + '.' + name
193 self.fullname_to_filename[fullname] = file_name
194 self.package_to_localname.setdefault(package_name, []).append(name)
195 self._update_prefix_table()
196
197 def _add_filename(self, filename):
198 if filename not in self.filename_to_module:
199 python_path = filename.replace('.proto', '_pb2').replace('/', '.')
200 package_name, _, module_name = python_path.rpartition('.')
201 self.filename_to_module[filename] = (package_name, module_name)
202
203 def _update_prefix_table(self):
204 # make a sorted list symbol prefixes needed to resolv for potential use
205 # of nested symbols
206 self.prefix_table = sorted(self.fullname_to_filename.iterkeys(),
207 reverse=True)
208
209 def _find_matching_prefix(self, fullname):
210 for prefix in self.prefix_table:
211 if fullname.startswith(prefix):
212 return prefix
213 # This should never happen
214 raise Exception('No match for type name "{}"'.format(fullname))
215
216 def add_needed_symbol(self, fullname):
217 if fullname in self.type_map:
218 return
219 top_level_symbol = self._find_matching_prefix(fullname)
220 name = top_level_symbol.rpartition('.')[2]
221 nested_name = fullname[len(top_level_symbol):] # may be empty
222 file_name = self.fullname_to_filename[top_level_symbol]
223 self.includes_needed.add(file_name)
224 module_name = self.filename_to_module[file_name][1]
225 python_name = module_name + '.' + name + nested_name
226 self.type_map[fullname] = python_name
Scott Baker80a36b72018-03-12 18:55:53 -0700227 python_name = module_name + '_grpc.' + name + nested_name
228 self.stub_map[fullname] = python_name
Zsolt Haraszticba96de2016-11-06 14:04:55 -0800229
230 def get_type_map(self):
231 return self.type_map
232
Scott Baker80a36b72018-03-12 18:55:53 -0700233 def get_stub_map(self):
234 return self.stub_map
235
Zsolt Haraszticba96de2016-11-06 14:04:55 -0800236 def get_includes(self):
237 return sorted(
238 self.filename_to_module[fn] for fn in self.includes_needed)
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -0700239
240
241def generate_code(request, response):
242
243 assert isinstance(request, plugin.CodeGeneratorRequest)
Zsolt Haraszticba96de2016-11-06 14:04:55 -0800244
245 include_manager = IncludeManager()
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -0700246 for proto_file in request.proto_file:
Zsolt Haraszticba96de2016-11-06 14:04:55 -0800247
248 include_manager.extend_symbol_tables(proto_file)
249
250 methods = []
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -0700251
252 for data in traverse_methods(proto_file):
Zsolt Haraszticba96de2016-11-06 14:04:55 -0800253 methods.append(data)
254 include_manager.add_needed_symbol(data['input_type'])
255 include_manager.add_needed_symbol(data['output_type'])
256 include_manager.add_needed_symbol(data['service'])
257
258 type_map = include_manager.get_type_map()
Scott Baker80a36b72018-03-12 18:55:53 -0700259 stub_map = include_manager.get_stub_map()
Zsolt Haraszticba96de2016-11-06 14:04:55 -0800260 includes = include_manager.get_includes()
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -0700261
262 # as a nice side-effect, generate a json file capturing the essence
263 # of the RPC method entries
264 f = response.file.add()
265 f.name = proto_file.name + '.json'
Zsolt Haraszticba96de2016-11-06 14:04:55 -0800266 f.content = dumps(dict(
Scott Baker80a36b72018-03-12 18:55:53 -0700267 type_rename_map=type_map, # TODO: is stub_map needed here?
Zsolt Haraszticba96de2016-11-06 14:04:55 -0800268 includes=includes,
269 methods=methods), indent=4)
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -0700270
271 # generate the real Python code file
272 f = response.file.add()
273 assert proto_file.name.endswith('.proto')
274 f.name = proto_file.name.replace('.proto', '_gw.py')
Zsolt Haraszticba96de2016-11-06 14:04:55 -0800275 f.content = generate_gw_code(proto_file.name,
Scott Baker80a36b72018-03-12 18:55:53 -0700276 methods, type_map, stub_map, includes)
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -0700277
278
279if __name__ == '__main__':
280
281 if len(sys.argv) >= 2:
282 # read input from file, to allow troubleshooting
283 with open(sys.argv[1], 'r') as f:
284 data = f.read()
285 else:
286 # read input from stdin
287 data = sys.stdin.read()
Zsolt Haraszticba96de2016-11-06 14:04:55 -0800288 # with file('/tmp/buf', 'wb') as f:
289 # f.write(data)
Zsolt Haraszti3d55ffc2016-10-03 22:26:41 -0700290
291 # parse request
292 request = plugin.CodeGeneratorRequest()
293 request.ParseFromString(data)
294
295 # create response object
296 response = plugin.CodeGeneratorResponse()
297
298 # generate the output and the response
299 generate_code(request, response)
300
301 # serialize the response
302 output = response.SerializeToString()
303
304 # write response to stdout
305 sys.stdout.write(output)