Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add rest transport generation for clients with optional flag #688

Merged
merged 16 commits into from
Nov 14, 2020

Conversation

yon-mg
Copy link
Contributor

@yon-mg yon-mg commented Nov 2, 2020

This pr adds traditional rest/json transport to GAPIC clients. As of now, this does not yet handle all cases and there may be bugs in the generated client. As such, there is now a --transport flag set to 'grpc' by default so any new additions won't be reflected in generated clients.

@google-cla google-cla bot added the cla: yes This human has signed the Contributor License Agreement. label Nov 2, 2020
@yon-mg yon-mg marked this pull request as ready for review November 2, 2020 18:09
@yon-mg yon-mg requested a review from a team as a code owner November 2, 2020 18:09
@yon-mg yon-mg marked this pull request as draft November 2, 2020 18:17
Copy link
Contributor

@software-dov software-dov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First pass, some broad outlines and some nitpicking.

I'm more in favor of naming the new transport and its module "Http" instead of Rest; it is possible to do protobuf over http, and it's more immediately obvious to someone swimming in the code what it means.



{%- for method in service.methods.values() %}
{# TODO(yonmg): consider using enums #}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for using enums.

Comment on lines 143 to 157
{%- if ns.http.get != '' %}
{%- set ns.httpMethod = {'method':'get','url':ns.http.get} %}
{%- elif ns.http.put != '' %}
{%- set ns.httpMethod = {'method':'put','url':ns.http.put} %}
{%- elif ns.http.post != '' %}
{%- set ns.httpMethod = {'method':'post','url':ns.http.post} %}
{%- elif ns.http.delete != '' %}
{%- set ns.httpMethod = {'method':'delete','url':ns.http.delete} %}
{%- elif ns.http.patch != '' %}
{%- set ns.httpMethod = {'method':'patch','url':ns.http.patch} %}
{%- elif ns.http.custom != '' %}
{%- set ns.httpMethod = {'method':'custom','url':ns.http.custom} %}
{%- else %}
{%- set ns.httpMethod = None %}
{%- endif %}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm leaning towards anything that involves state modification living in the generator datatypes. It's easier to test and easier to change if we need to.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand what you mean by generator datatypes. Could you clarify?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gapic/schema/wrappers.py:{MessageType,Service,Method} classes. The auxiliary datastructures that the generate creates before iterating over the templates.

@yon-mg yon-mg changed the title feat: add rest transport generation for clients feat WIP: add rest transport generation for clients Nov 2, 2020
@busunkim96
Copy link
Contributor

Is the REST transport going to be added in one PR? If not, can we maintain a long-running branch and merge to it instead?

The microgenerator is now relied on by Cloud clients at GA stability so I'd prefer not to to surface this until it is somewhat stable.

@yon-mg
Copy link
Contributor Author

yon-mg commented Nov 2, 2020

Is the REST transport going to be added in one PR? If not, can we maintain a long-running branch and merge to it instead?

The microgenerator is now relied on by Cloud clients at GA stability so I'd prefer not to to surface this until it is somewhat stable.

This sounds reasonable to me. I'll defer to the other reviewers on what the best practice should be. @software-dov @vam-google @vchudnov-g

@yon-mg yon-mg changed the title feat WIP: add rest transport generation for clients feat[WIP]: add rest transport generation for clients Nov 2, 2020
@software-dov
Copy link
Contributor

If it's not too much work, I think I'd be more in favor of feature toggles and small PRs instead of having a long-running branch. It's easier to do merging. If that seems like too much work, I'm okay with a long-running branch.

@yon-mg
Copy link
Contributor Author

yon-mg commented Nov 6, 2020

I'm more in favor of naming the new transport and its module "Http" instead of Rest; it is possible to do protobuf over http, and it's more immediately obvious to someone swimming in the code what it means.

I agree with this somewhat but for consistency across other languages, flag arguments and regapic design specs, I think it's more appropriate to name it Rest. In addition, it's because it's possible to do protobuf over http that naming it such may be ambiguous. Rest seems to me more clearly defined.

Copy link
Contributor

@software-dov software-dov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just a few things.

@@ -40,6 +40,8 @@ class Options:
lazy_import: bool = False
old_naming: bool = False
add_iam_methods: bool = False
# TODO(yonmg): should there be an enum for transport type?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for using an enum

gapic/utils/options.py Show resolved Hide resolved
gapic/generator/generator.py Outdated Show resolved Hide resolved
gapic/generator/generator.py Outdated Show resolved Hide resolved
gapic/schema/wrappers.py Outdated Show resolved Hide resolved
@yon-mg yon-mg changed the title feat[WIP]: add rest transport generation for clients feat(WIP): add rest transport generation for clients Nov 11, 2020
@codecov
Copy link

codecov bot commented Nov 11, 2020

Codecov Report

Merging #688 (b5c6d06) into master (b85cbd4) will not change coverage.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff            @@
##            master      #688   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           26        26           
  Lines         1560      1578   +18     
  Branches       316       320    +4     
=========================================
+ Hits          1560      1578   +18     
Impacted Files Coverage Δ
gapic/generator/generator.py 100.00% <100.00%> (ø)
gapic/schema/wrappers.py 100.00% <100.00%> (ø)
gapic/utils/options.py 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update b85cbd4...b5c6d06. Read the comment docs.

@yon-mg yon-mg changed the title feat(WIP): add rest transport generation for clients feat: add rest transport generation for clients Nov 11, 2020
@yon-mg yon-mg marked this pull request as ready for review November 11, 2020 19:44
Copy link
Contributor

@software-dov software-dov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just two minor nits.

]
with mock.patch.object(jinja2.Environment, "get_template") as gt:
gt.return_value = jinja2.Template("Service: {{ service.name }}")
cgr = g.get_response(api_schema=make_api(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting nit: putting this on a new line makes it slightly easier to read the entire thing.

Comment on lines 278 to 279
'transport' in template_name
and not self._is_desired_transport(template_name, opts)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you put parentheses around this chunk, just to make it clear that it's a cohesive unit?

@@ -14,7 +14,7 @@

from collections import defaultdict
from os import path
from typing import Any, DefaultDict, Dict, FrozenSet, List, Optional, Tuple
from typing import Any, DefaultDict, Dict, FrozenSet, List, Optional, Tuple, Set
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is Set used somewhere?

Copy link
Contributor

@vam-google vam-google left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM
All the comments are minor. Please address them and then push the changes (please use squash and merge button). Thanks @software-dov for sharing your expertise in Python review!

@@ -20,6 +20,7 @@ _lazy_type_to_package_map = {
'{{ enum.name }}': '{{ enum.ident.package|join('.') }}.types.{{enum.ident.module }}',
{%- endfor %}

{# TODO(yonmg): add rest transport service once I know what this is #}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use your github id for TODOs (here and in the other places)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@@ -786,6 +808,7 @@ def grpc_stub_type(self) -> str:
server='stream' if self.server_streaming else 'unary',
)

# TODO(yonmg): figure out why idempotent is reliant on http annotation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

http GET requests are automatically considered idempotent because they do not change data on the server. Post requests are usually non-idempotent, because it is usually unsafe retry write operation if it failed, because it may lead for example into writing the same information twice on the server.

Copy link
Contributor Author

@yon-mg yon-mg Nov 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get that GET is idempotent but couldn't there be methods without an http annotation that are also idempotent? are grpc only methods inherently not so?

@@ -220,13 +223,13 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
client.
"""
# Sanity check: Only create a new client if we do not already have one.
if 'operations_client' not in self.__dict__:
self.__dict__['operations_client'] = operations_v1.OperationsClient(
if self._operations_client is None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

won't if not self._operation_client work? I thought it was more idiomatic for python.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little bit complicated. Some values can be falsy even if they are non-null, e.g. empty lists and strings.
In this particular case, it's slightly more idiomatic to do if self._operations_client, but I personally am fine with explicit checks in cases like this.

@@ -172,7 +175,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
**kwargs) -> grpc.Channel:
"""Create and return a gRPC channel object.
Args:
address (Optionsl[str]): The host for the channel to use.
address (Optional[str]): The host for the channel to use.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this changed (looks unrelated to your changes)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I approve of fixing typos on the fly!

@@ -225,13 +228,13 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
client.
"""
# Sanity check: Only create a new client if we do not already have one.
if 'operations_client' not in self.__dict__:
self.__dict__['operations_client'] = operations_v1.OperationsAsyncClient(
if self._operations_client is None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you changing this code (looks unrelated to http transport stuff)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#688 (comment)
I decided to update the other templates as well for consistency.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for consistency

__all__ = (
'{{ service.name }}RestTransport',
)
{% endblock %}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add new line

Copy link
Contributor

@vchudnov-g vchudnov-g left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Only minor comments. Nice job! Very excited to have this in!

@@ -20,6 +20,7 @@ _lazy_type_to_package_map = {
'{{ enum.name }}': '{{ enum.ident.package|join('.') }}.types.{{enum.ident.module }}',
{%- endfor %}

{# TODO(yonmg): add rest transport service once I know what this is #}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@@ -42,7 +42,6 @@ def generate(

# Pull apart arguments in the request.
opts = Options.build(req.parameter)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I like having the blank line here for readability.

@@ -719,6 +719,7 @@ def _client_output(self, enable_asyncio: bool):
# Return the usual output.
return self.output

# TODO(yonmg): remove or rewrite. don't think it performs as intended
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Suggest adding just a tiny bit more detail. It often happens that the person working on the TODO is not the person who wrote it, so adding some context is a helpful practice.

}
if len(http) > 1:
http_opt = http[1]
answer[http_opt[0].name] = http_opt[1]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For clarity I would suggest

body_spec = http[1]
answer[body_spec[0].name] = body_spec[1]


http_method = http[0]
answer: Dict[str, str] = {
'method': http_method[0].name,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on our offline discussion, let's s/'method'/'verb'/, since "method" is overloaded (service method, object method, HTTP method)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this be madness, there be method to it! 💀

@property
def http_opt(self) -> Optional[Dict[str, str]]:
"""Return the http option for this method."""
http = self.options.Extensions[annotations_pb2.http].ListFields()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think a type declaration or a comment saying this is a List[Tuple[FieldDescriptor, str]] would be helpful.

gapic/utils/options.py Outdated Show resolved Hide resolved
@@ -172,7 +175,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
**kwargs) -> grpc.Channel:
"""Create and return a gRPC channel object.
Args:
address (Optionsl[str]): The host for the channel to use.
address (Optional[str]): The host for the channel to use.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I approve of fixing typos on the fly!

gapic/schema/wrappers.py Outdated Show resolved Hide resolved
@yon-mg yon-mg changed the title feat: add rest transport generation for clients feat: add rest transport generation for clients with optional flag Nov 14, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes This human has signed the Contributor License Agreement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Re-add HTTP transport and make it the fallback
5 participants