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 proper handling of query/path/body parameters for rest transport #702

Merged
merged 29 commits into from
Dec 8, 2020
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c1a6f89
feat: add rest transport generation for clients
yon-mg Nov 2, 2020
bd7c32d
feat: add rest transport generation for clients
yon-mg Nov 2, 2020
cd3a7f1
Merge branch 'master' of github.com:yon-mg/gapic-generator-python
yon-mg Nov 2, 2020
b41db31
feat: add transport flag
yon-mg Nov 6, 2020
ce69e7d
refactor: moved template logic outside
yon-mg Nov 6, 2020
89fa08b
fix: small fixes in transport option logic
yon-mg Nov 9, 2020
86f3a9c
test: added unit test for transport flag
yon-mg Nov 9, 2020
234cc09
test: add unit test for http option method
yon-mg Nov 11, 2020
35ac276
test: add unit test for http option method branch
yon-mg Nov 11, 2020
c8155a5
fix: fix import paths
yon-mg Nov 11, 2020
55a815c
fix: style check issues
yon-mg Nov 11, 2020
ac0bdef
fix: more style check issues
yon-mg Nov 11, 2020
e79e8c7
fix: addressing pr reviews
yon-mg Nov 13, 2020
824ad11
fix: typo in test_method
yon-mg Nov 13, 2020
2d11e19
fix: style check fixes
yon-mg Nov 13, 2020
b5c6d06
Merge branch 'master' into master
yon-mg Nov 14, 2020
221cd44
feat: add proper handling of query/path/body parameters for rest tran…
yon-mg Nov 20, 2020
475f903
Merge branch 'master' of github.com:yon-mg/gapic-generator-python
yon-mg Nov 20, 2020
f08ca32
Merge remote-tracking branch 'upstream/master'
yon-mg Nov 20, 2020
efa0ed6
fix: typing errors
yon-mg Nov 20, 2020
d3e08c0
Update case.py
software-dov Nov 20, 2020
df88ef9
fix: minor changes adding a test, refactor and style check
yon-mg Nov 30, 2020
67c6bd1
Merge branch 'master' of github.com:yon-mg/gapic-generator-python
yon-mg Nov 30, 2020
37e3d28
fix: camel_case bug with constant case
yon-mg Nov 30, 2020
c964fba
fix: to_camel_case to produce lower camel case instead of PascalCase …
yon-mg Dec 1, 2020
85be88d
fix: addressing pr comments
yon-mg Dec 3, 2020
5d1c6a3
fix: adding appropriate todos, addressing comments
yon-mg Dec 4, 2020
f6c64cc
fix: dataclass dependency issue
yon-mg Dec 8, 2020
a4f34e7
Update wrappers.py
software-dov Dec 8, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions gapic/generator/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def __init__(self, opts: Options) -> None:
# Add filters which templates require.
self._env.filters["rst"] = utils.rst
self._env.filters["snake_case"] = utils.to_snake_case
self._env.filters["camel_case"] = utils.to_camel_case
self._env.filters["sort_lines"] = utils.sort_lines
self._env.filters["wrap"] = utils.wrap
self._env.filters["coerce_response_name"] = coerce_response_name
Expand Down Expand Up @@ -288,6 +289,8 @@ def _render_template(
opts=opts,
)
)
#for method in service.methods.values():
#breakpoint()
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a reminder to clean this up.

return answer

# This file is not iterating over anything else; return back
Expand Down
17 changes: 17 additions & 0 deletions gapic/schema/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,23 @@ def http_opt(self) -> Optional[Dict[str, str]]:
# TODO(yon-mg): enums for http verbs?
return answer

@property
def path_params(self) -> Sequence[str]:
if self.http_opt is None:
return []
pattern = r'\{\w+\}'
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 a comment that this handles grpc encoding in its simples case

return [x[1:-1] for x in re.findall(pattern, self.http_opt['url'])]

@property
def query_params(self) -> Set[str]:
Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious: why can't query_params and path_params return the same type, rather than a Set and a Sequence?

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 suppose they could. Just thought it's more appropriate since ordering seems important for path_params but not for query_params.

Copy link
Contributor

Choose a reason for hiding this comment

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

As discussed. That seems reasonable, but a complication is that repeated fields map to repeated query params. In that case, a sequence may be best. (If order doesn't matter, a multiset might be enough, but we can't assume that order doesn't matter for repeated fields)

Copy link
Contributor Author

@yon-mg yon-mg Dec 3, 2020

Choose a reason for hiding this comment

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

You are right set is not appropriate even if order doesn't matter but I should mention though that once query param logic is moved out, this won't matter anymore.

Copy link
Contributor

Choose a reason for hiding this comment

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

True!

if self.http_opt is None:
return set()
body = []
if 'body' in self.http_opt.keys():
body.append(self.http_opt['body'])
all_params = self.input.fields.keys()
software-dov marked this conversation as resolved.
Show resolved Hide resolved
return set(all_params) ^ set(body + list(self.path_params))
software-dov marked this conversation as resolved.
Show resolved Hide resolved
software-dov marked this conversation as resolved.
Show resolved Hide resolved

# TODO(yon-mg): refactor as there may be more than one method signature
@utils.cached_property
def flattened_fields(self) -> Mapping[str, Field]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,28 +135,42 @@ class {{ service.name }}RestTransport({{ service.name }}Transport):

{%- if 'body' in method.http_opt.keys() %}
# Jsonify the input
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: for clarity, maybe s/input/request/

data = {{ method.output.ident }}.to_json(
{%- if method.http_opt['body'] == '*' %}
{%- if method.http_opt['body'] != '*' %}
data = {{ method.input.fields[method.http_opt['body']].type.ident }}.to_json(
Copy link
Contributor

Choose a reason for hiding this comment

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

!! This will handle dot-notation nested fields, correct? If not, add a TODO.

Context: while https://github.com/googleapis/googleapis/blob/master/google/api/http.proto#L350 suggests that's not allowed, some APIs do have it (see gRPC transcoding doc), eg. https://github.com/googleapis/googleapis/blob/836f0eaf5f21f300f63ac635e5ef263d183e0cdd/google/cloud/dialogflow/cx/v3beta1/session.proto#L95

Copy link
Contributor Author

@yon-mg yon-mg Dec 1, 2020

Choose a reason for hiding this comment

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

It does not handle it but I have added a todo in the appropriate place in wrappers.Method. Pretty much anything dealing with full handling of grpc transcoding (or special cases outside grpc transcoding) is not yet taken care of in this PR as per the PR desc.

Copy link
Contributor

Choose a reason for hiding this comment

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

OK. As discussed, we should err on the side of duplicate TODOs rather than too few. If we address one, we're likely to see the others and address/delete them.

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: maybe have the generated code call this variable body rather than data so the code is more self-explanatory.

request.{{ method.http_opt['body'] }},
including_default_value_fields=False
)
{%- else %}
data = {{ method.input.ident }}.to_json(
Copy link
Contributor

Choose a reason for hiding this comment

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

Does request already have the body and query params stripped out at this point, so they don't get sent in two places?

Copy link
Contributor

Choose a reason for hiding this comment

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

As we clarified yesterday: params in the path may be (but don't have to be) repeated in the body if we're using "*", though we should never have a body: "foo" if foo is a path param. And query params are whatever's left over from what's not included in the path and body. So we just need to ensure no body params wind up as query params.

request
{%- else %}
request.body
{%- endif %}
)
{%- endif %}
{%- endif %}

{# TODO(yon-mg): Write helper method for handling grpc transcoding url #}
# TODO(yon-mg): need to handle grpc transcoding and parse url correctly
# current impl assumes simpler version of grpc transcoding
# current impl assumes basic case of grpc transcoding
# Send the request
url = 'https://{host}{{ method.http_opt['url'] }}'.format(
host=self._host,
{%- for field in method.input.fields.keys() %}
{%- for field in method.path_params %}
{{ field }}=request.{{ field }},
{%- endfor %}
)

potentialParams = {
Copy link
Contributor

Choose a reason for hiding this comment

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

It would probably be clearer to s/potentialParams/queryParams/g

{%- for field in method.query_params %}
'{{ field|camel_case }}': request.{{ field }},
vchudnov-g marked this conversation as resolved.
Show resolved Hide resolved
{%- endfor %}
}
potentialParams = {k: v for k, v in potentialParams.items() if v}
Copy link
Contributor

Choose a reason for hiding this comment

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

Very minor premature optimization nit: since we're immediately iterating over and then discarding this dictionary, we can turn it into a generator instead and prevent looping over the same data multiple times.

potential_params = ((k, v) for k, v in potentialParams.items() if v) # The parentheses make this a generator expression.
for i, (param_name, param_value) in enumerate(potentialParams):

This is a good rundown on generators and generator expressions. Dave Beazley also has a really fun youtube talk on generators.

for i, (param_name, param_value) in enumerate(potentialParams.items()):
q = '?' if i == 0 else '&'
url += q + param_name + '=' + str(param_value).replace(' ', '+')
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: prefer format strings to concatenating using +. It's easier to eyeball parse for longer or more convoluted strings.

url += "{q}{name}={value}".format(q=q, name=param_name, value=param_value.replace(' ', '+'))


{% if not method.void %}response = {% endif %}self._session.{{ method.http_opt['verb'] }}(
url,
{%- if 'body' in method.http_opt.keys() %}
url
{%- if 'body' in method.http_opt.keys() %},
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to use keys here, if 'body' in method.http_opt is more idiomatic.

json=data,
{%- endif %}
)
Expand Down
2 changes: 2 additions & 0 deletions gapic/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from gapic.utils.cache import cached_property
from gapic.utils.case import to_snake_case
from gapic.utils.case import to_camel_case
from gapic.utils.code import empty
from gapic.utils.code import nth
from gapic.utils.code import partition
Expand All @@ -38,6 +39,7 @@
'rst',
'sort_lines',
'to_snake_case',
'to_camel_case',
'to_valid_filename',
'to_valid_module_name',
'wrap',
Expand Down
14 changes: 14 additions & 0 deletions gapic/utils/case.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,17 @@ def to_snake_case(s: str) -> str:

# Done; return the camel-cased string.
return s.lower()

def to_camel_case(s: str) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

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

Are there any built-in/standard python functions which do the same? Please prefer using standard ones to custom, if there are any.

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 fairly certain there is no standard library function for to camel-case.

'''Convert any string to camel case.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: In this and the previous, pre-existing function, we're not touching spaces, right? Might be worth mentioning that.


This is provided to templates as the ``camel_case`` filter.

Args:
s (str): The input string, provided in snake case.

Returns:
str: The string in camel case with the first letter unchanged.
'''
items = s.split('_')
return items[0] + "".join([x.capitalize() for x in items[1:]])
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: no need to make a list as the argument to join. We could replace the square brackets with parentheses to make it a generator expression, but there's a minor syntactic optimization where if a generator expression is the sole function argument you can remove the parens.

join(x.capitalize() for x in items[1:])

49 changes: 49 additions & 0 deletions tests/unit/schema/wrappers/test_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,55 @@ def test_method_http_opt_no_http_rule():
assert method.http_opt == None


def test_method_path_params():
# tests only the basic case of grpc transcoding
http_rule = http_pb2.HttpRule(post='/v1/{project}/topics')
method = make_method('DoSomething', http_rule=http_rule)
assert method.path_params == ['project']


def test_method_path_params_no_http_rule():
method = make_method('DoSomething')
assert method.path_params == []


def test_method_query_params():
# tests only the basic case of grpc transcoding
http_rule = http_pb2.HttpRule(
post='/v1/{project}/topics',
body='address'
)
input_message = make_message(
'MethodInput',
fields=(
make_field('region'),
make_field('project'),
make_field('address')
)
)
method = make_method('DoSomething', http_rule=http_rule, input_message=input_message)
assert method.query_params == {'region'}


def test_method_query_params_no_body():
# tests only the basic case of grpc transcoding
http_rule = http_pb2.HttpRule(post='/v1/{project}/topics')
input_message = make_message(
'MethodInput',
fields=(
make_field('region'),
make_field('project'),
)
)
method = make_method('DoSomething', http_rule=http_rule, input_message=input_message)
assert method.query_params == {'region'}


def test_method_query_params_no_http_rule():
method = make_method('DoSomething')
assert method.query_params == set()


def test_method_idempotent_yes():
http_rule = http_pb2.HttpRule(get='/v1/{parent=projects/*}/topics')
method = make_method('DoSomething', http_rule=http_rule)
Expand Down