Skip to content

Commit b3d7e99

Browse files
authored
cleanup: refactor rest transport class in gapics (#2099)
1 parent c33f0ef commit b3d7e99

File tree

24 files changed

+5541
-2572
lines changed

24 files changed

+5541
-2572
lines changed

gapic/generator/generator.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,8 @@ def _render_template(
297297
# TODO(yon-mg) - remove when rest async implementation resolved
298298
# temporarily stop async client gen while rest async is unkown
299299
('async' in template_name and 'grpc' not in opts.transport)
300+
or
301+
('rest_base' in template_name and 'rest' not in opts.transport)
300302
):
301303
continue
302304

@@ -319,7 +321,7 @@ def _render_template(
319321

320322
def _is_desired_transport(self, template_name: str, opts: Options) -> bool:
321323
"""Returns true if template name contains a desired transport"""
322-
desired_transports = ['__init__', 'base'] + opts.transport
324+
desired_transports = ['__init__', 'base', 'README'] + opts.transport
323325
return any(transport in template_name for transport in desired_transports)
324326

325327
def _get_file(

gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,94 @@ except ImportError: # pragma: NO COVER
6868
)
6969
{% endif %}{# service_version #}
7070
{% endmacro %}
71+
72+
{% macro operations_mixin_imports(api, service, opts) %}
73+
{% if import_ns is not defined %}
74+
{% set import_ns = namespace(has_operations_mixin=false) %}
75+
{% endif %}{# import_ns is not defined #}
76+
{% set import_ns.has_operations_mixin = api.has_operations_mixin %}
77+
78+
{% filter sort_lines %}
79+
{% for method in service.methods.values() %}
80+
{{method.input.ident.python_import}}
81+
{% if method.output.ident|string() == "operations_pb2.Operation" %}
82+
{% set import_ns.has_operations_mixin = True %}
83+
{% else %}
84+
{{method.output.ident.python_import}}
85+
{% endif %}
86+
{% endfor %}
87+
{% if opts.add_iam_methods %}
88+
from google.iam.v1 import iam_policy_pb2 # type: ignore
89+
from google.iam.v1 import policy_pb2 # type: ignore
90+
{% endif %}{# opts.add_iam_methods #}
91+
{% endfilter %}
92+
{% if import_ns.has_operations_mixin %}
93+
from google.longrunning import operations_pb2 # type: ignore
94+
{% endif %}{# import_ns.has_operations_mixin #}
95+
{% endmacro %}
96+
97+
{% macro http_options_method(rules) %}
98+
@staticmethod
99+
def _get_http_options():
100+
http_options: List[Dict[str, str]] = [
101+
{%- for rule in rules %}{
102+
'method': '{{ rule.method }}',
103+
'uri': '{{ rule.uri }}',
104+
{% if rule.body %}
105+
'body': '{{ rule.body }}',
106+
{% endif %}{# rule.body #}
107+
},
108+
{% endfor %}{# rule in rules #}
109+
]
110+
return http_options
111+
{% endmacro %}
112+
113+
{% macro response_method(body_spec) %}
114+
@staticmethod
115+
def _get_response(
116+
host,
117+
metadata,
118+
query_params,
119+
session,
120+
timeout,
121+
transcoded_request,
122+
body=None):
123+
124+
uri = transcoded_request['uri']
125+
method = transcoded_request['method']
126+
headers = dict(metadata)
127+
headers['Content-Type'] = 'application/json'
128+
response = getattr(session, method)(
129+
"{host}{uri}".format(host=host, uri=uri),
130+
timeout=timeout,
131+
headers=headers,
132+
params=rest_helpers.flatten_query_params(query_params, strict=True),
133+
{% if body_spec %}
134+
data=body,
135+
{% endif %}
136+
)
137+
return response
138+
{% endmacro %}
139+
140+
{% macro rest_call_method_common(body_spec, method_name, service_name) %}
141+
142+
http_options = _Base{{ service_name }}RestTransport._Base{{method_name}}._get_http_options()
143+
request, metadata = self._interceptor.pre_{{ method_name|snake_case }}(request, metadata)
144+
transcoded_request = _Base{{ service_name }}RestTransport._Base{{method_name}}._get_transcoded_request(http_options, request)
145+
146+
{% if body_spec %}
147+
body = _Base{{ service_name }}RestTransport._Base{{method_name}}._get_request_body_json(transcoded_request)
148+
{% endif %} {# body_spec #}
149+
150+
# Jsonify the query params
151+
query_params = _Base{{ service_name }}RestTransport._Base{{method_name}}._get_query_params_json(transcoded_request)
152+
153+
# Send the request
154+
response = {{ service_name }}RestTransport._{{method_name}}._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request{% if body_spec %}, body{% endif %})
155+
156+
# In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception
157+
# subclass.
158+
if response.status_code >= 400:
159+
raise core_exceptions.from_http_response(response)
160+
161+
{% endmacro %}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
transport inheritance structure
3+
_______________________________
4+
5+
`{{ service.name }}Transport` is the ABC for all transports.
6+
- public child `{{ service.name }}GrpcTransport` for sync gRPC transport (defined in `grpc.py`).
7+
- public child `{{ service.name }}GrpcAsyncIOTransport` for async gRPC transport (defined in `grpc_asyncio.py`).
8+
- private child `_Base{{ service.name }}RestTransport` for base REST transport with inner classes `_BaseMETHOD` (defined in `rest_base.py`).
9+
- public child `{{ service.name }}RestTransport` for sync REST transport with inner classes `METHOD` derived from the parent's corresponding `_BaseMETHOD` classes (defined in `rest.py`).
10+
{# Since the service mixins have a similar structure, we factor out shared code into `_shared_macros.j2` to avoid duplication. #}

gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/_mixins.py.j2

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
{#
2+
# Copyright (C) 2024 Google LLC
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+
#}
16+
17+
{% import "%namespace/%name_%version/%sub/services/%service/_shared_macros.j2" as shared_macros %}
18+
119
{% if "grpc" in opts.transport %}
220

321
{% if api.has_operations_mixin %}

gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/_rest_mixins.py.j2

Lines changed: 23 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,32 @@
1+
{#
2+
# Copyright (C) 2024 Google LLC
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+
#}
16+
17+
{% import "%namespace/%name_%version/%sub/services/%service/_shared_macros.j2" as shared_macros %}
18+
119
{% if "rest" in opts.transport %}
220

321
{% for name, sig in api.mixin_api_signatures.items() %}
422
@property
523
def {{ name|snake_case }}(self):
624
return self._{{ name }}(self._session, self._host, self._interceptor) # type: ignore
725

8-
class _{{ name }}({{service.name}}RestStub):
26+
class _{{ name }}(_Base{{ service.name }}RestTransport._Base{{name}}, {{service.name}}RestStub):
27+
{% set body_spec = api.mixin_http_options["{}".format(name)][0].body %}
28+
{{ shared_macros.response_method(body_spec)|indent(8) }}
29+
930
def __call__(self,
1031
request: {{ sig.request_type }}, *,
1132
retry: OptionalRetry=gapic_v1.method.DEFAULT,
@@ -32,52 +53,7 @@
3253
{{ sig.response_type }}: Response from {{ name }} method.
3354
{% endif %}
3455
"""
35-
36-
http_options: List[Dict[str, str]] = [
37-
{%- for rule in api.mixin_http_options["{}".format(name)] %}{
38-
'method': '{{ rule.method }}',
39-
'uri': '{{ rule.uri }}',
40-
{% if rule.body %}
41-
'body': '{{ rule.body }}',
42-
{% endif %}{# rule.body #}
43-
},
44-
{% endfor %}
45-
]
46-
47-
request, metadata = self._interceptor.pre_{{ name|snake_case }}(request, metadata)
48-
request_kwargs = json_format.MessageToDict(request)
49-
transcoded_request = path_template.transcode(
50-
http_options, **request_kwargs)
51-
52-
{% set body_spec = api.mixin_http_options["{}".format(name)][0].body %}
53-
{%- if body_spec %}
54-
body = json.dumps(transcoded_request['body'])
55-
{%- endif %}
56-
57-
uri = transcoded_request['uri']
58-
method = transcoded_request['method']
59-
60-
# Jsonify the query params
61-
query_params = json.loads(json.dumps(transcoded_request['query_params']))
62-
63-
# Send the request
64-
headers = dict(metadata)
65-
headers['Content-Type'] = 'application/json'
66-
67-
response = getattr(self._session, method)(
68-
"{host}{uri}".format(host=self._host, uri=uri),
69-
timeout=timeout,
70-
headers=headers,
71-
params=rest_helpers.flatten_query_params(query_params),
72-
{% if body_spec %}
73-
data=body,
74-
{% endif %}
75-
)
76-
77-
# In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception
78-
# subclass.
79-
if response.status_code >= 400:
80-
raise core_exceptions.from_http_response(response)
56+
{{ shared_macros.rest_call_method_common(body_spec, name, service.name)|indent(8) }}
8157

8258
{% if sig.response_type == "None" %}
8359
return self._interceptor.post_{{ name|snake_case }}(None)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{#
2+
# Copyright (C) 2024 Google LLC
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+
#}
16+
17+
{% import "%namespace/%name_%version/%sub/services/%service/_shared_macros.j2" as shared_macros %}
18+
19+
{% if "rest" in opts.transport %}
20+
21+
{% for name, sig in api.mixin_api_signatures.items() %}
22+
class _Base{{ name }}:
23+
24+
{{ shared_macros.http_options_method(api.mixin_http_options["{}".format(name)])|indent(8)}}
25+
26+
@staticmethod
27+
def _get_transcoded_request(http_options, request):
28+
request_kwargs = json_format.MessageToDict(request)
29+
transcoded_request = path_template.transcode(
30+
http_options, **request_kwargs)
31+
return transcoded_request
32+
33+
{% set body_spec = api.mixin_http_options["{}".format(name)][0].body %}
34+
{%- if body_spec %}
35+
36+
@staticmethod
37+
def _get_request_body_json(transcoded_request):
38+
body = json.dumps(transcoded_request['body'])
39+
return body
40+
41+
{%- endif %} {# body_spec #}
42+
43+
@staticmethod
44+
def _get_query_params_json(transcoded_request):
45+
query_params = json.loads(json.dumps(transcoded_request['query_params']))
46+
return query_params
47+
48+
{% endfor %}
49+
{% endif %} {# rest in opts.transport #}

0 commit comments

Comments
 (0)