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 25 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
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ jobs:

cd ..
nox -s showcase_mtls_alternative_templates
# TODO(yon-mg): add compute unit tests
showcase-unit-3.6:
docker:
- image: python:3.6-slim
Expand Down
1 change: 1 addition & 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
23 changes: 23 additions & 0 deletions gapic/schema/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,29 @@ def http_opt(self) -> Optional[Dict[str, str]]:
# TODO(yon-mg): enums for http verbs?
return answer

@property
def path_params(self) -> Sequence[str]:
"""Return the path parameters found in the http annotation url"""
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: How about "the field paths encoded in the path parameters"?
s/"url"/"path template"/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well it doesn't do this yet so I'll update the comment when it does.

Copy link
Contributor

Choose a reason for hiding this comment

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

WDYM? You're returning the {foo} which is a field path, right?

# TODO(yon-mg): fully implement grpc transcoding (currently only handles basic case)
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'])]
software-dov marked this conversation as resolved.
Show resolved Hide resolved

@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!

"""Return query parameters for API call as determined by http annotation and grpc transcoding"""
vchudnov-g marked this conversation as resolved.
Show resolved Hide resolved
# TODO(yon-mg): fully implement grpc transcoding (currently only handles basic case)
if self.http_opt is None:
return set()
params = set(self.path_params)
body = self.http_opt.get('body')
if body:
params.add(body)

return set(self.input.fields) ^ params

# 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 @@ -133,30 +133,44 @@ class {{ service.name }}RestTransport({{ service.name }}Transport):
{%- endif %}
"""

{%- if 'body' in method.http_opt.keys() %}
{%- if 'body' in method.http_opt %}
# 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.

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

Copy link
Contributor

Choose a reason for hiding this comment

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

This is fine, but another approach is to potentialParams=["{k}={v}".format(k=k, v=v) for k, v in ...] here and then url += "?{}".format("&".join(potentialParams)) below

vchudnov-g marked this conversation as resolved.
Show resolved Hide resolved
for i, (param_name, param_value) in enumerate(potentialParams):
q = '?' if i == 0 else '&'
url += "{q}{name}={value}".format(q=q, name=param_name, value=param_value.replace(' ', '+'))
vchudnov-g marked this conversation as resolved.
Show resolved Hide resolved

{% 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 %},
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
16 changes: 16 additions & 0 deletions gapic/utils/case.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,19 @@ 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 any sane case system

Returns:
str: The string in lower camel case.
'''

items = re.split(r'[_-]', to_snake_case(s))
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 surprised by the hyphen -. It almost seems like that should be handled by to_snake_case, though that's outside the scope of this PR. Does it come up in practice, that an input to this function has hyphens/kebab-case?

Opinion, @software-dov ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It actually might handle it: I didn't check. As far as use, it doesn't get used but I wouldn't want a future user to attempt o try it and have it fail.

Copy link
Contributor

Choose a reason for hiding this comment

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

My vote: write a test for to_snake_case's behavior, add the functionality if the test fails, just split on '_'.

Copy link
Contributor

Choose a reason for hiding this comment

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

If to_snake_case does not handle it, could adding the functionality break existing clients? is there an easy way to check? (This is why I was suggesting "outside the scope of this PR")

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 doubt it would break anything since it's additive but this is why I didn't change to_snake_case's behavior

Copy link
Contributor

Choose a reason for hiding this comment

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

But the generator has already produced libraries that were published, so if one of those had a hyphen somewhere, it made it through to_snake_case and changing that implementation could lead to an incompatible surface.

return items[0].lower() + "".join(x.capitalize() for x in items[1:])
6 changes: 6 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ def unit(session):
)


# TODO(yon-mg): -add compute context manager that includes rest transport
# -add compute unit tests
# (to test against temporarily while rest transport is incomplete)
# (to be removed once all features are complete)
@contextmanager
def showcase_library(
session, templates="DEFAULT", other_opts: typing.Iterable[str] = ()
Expand Down Expand Up @@ -87,6 +91,8 @@ def showcase_library(

# Write out a client library for Showcase.
template_opt = f"python-gapic-templates={templates}"
# TODO(yon-mg): add "transports=grpc+rest" when all rest features required for
# Showcase are implemented i.e. (grpc transcoding, LROs, etc)
opts = "--python_gapic_opt="
opts += ",".join(other_opts + (f"{template_opt}",))
cmd_tup = (
Expand Down
51 changes: 51 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,57 @@ 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
16 changes: 16 additions & 0 deletions tests/unit/utils/test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,19 @@ def test_camel_to_snake():

def test_constant_to_snake():
assert case.to_snake_case('CONSTANT_CASE_THING') == 'constant_case_thing'


def test_pascal_to_camel():
assert case.to_camel_case('PascalCaseThing') == 'pascalCaseThing'


def test_snake_to_camel():
assert case.to_camel_case('snake_case_thing') == 'snakeCaseThing'


def test_constant_to_camel():
assert case.to_camel_case('CONSTANT_CASE_THING') == 'constantCaseThing'


def test_kebab_to_camel():
assert case.to_camel_case('kebab-case-thing') == 'kebabCaseThing'