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 all 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
25 changes: 25 additions & 0 deletions gapic/schema/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,31 @@ 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 path template"""
# TODO(yon-mg): fully implement grpc transcoding (currently only handles basic case)
if self.http_opt is None:
return []

pattern = r'\{(\w+)\}'
return 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!

"""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)
# TODO(yon-mg): remove this method and move logic to generated client
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,31 +133,59 @@ class {{ service.name }}RestTransport({{ service.name }}Transport):
{%- endif %}
"""

{%- if 'body' in method.http_opt.keys() %}
# Jsonify the input
data = {{ method.output.ident }}.to_json(
{%- if method.http_opt['body'] == '*' %}
{# TODO(yon-mg): refactor when implementing grpc transcoding
- parse request pb & assign body, path params
- shove leftovers into query params
- make sure dotted nested fields preserved
- format url and send the request
#}
{%- if 'body' in method.http_opt %}
# Jsonify the request body
{%- if method.http_opt['body'] != '*' %}
body = {{ method.input.fields[method.http_opt['body']].type.ident }}.to_json(
request.{{ method.http_opt['body'] }},
including_default_value_fields=False
)
{%- else %}
body = {{ method.input.ident }}.to_json(
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
# Send the request
# current impl assumes basic case of grpc transcoding
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 %}
)

{# TODO(yon-mg): move all query param logic out of wrappers into here to handle
nested fields correctly (can't just use set of top level fields
#}
# TODO(yon-mg): handle nested fields corerctly rather than using only top level fields
# not required for GCE
query_params = {
{%- for field in method.query_params %}
'{{ field|camel_case }}': request.{{ field }},
vchudnov-g marked this conversation as resolved.
Show resolved Hide resolved
{%- endfor %}
}
# TODO(yon-mg): further discussion needed whether 'python truthiness' is appropriate here
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for the TODO on truthiness!

# discards default values
# TODO(yon-mg): add test for proper url encoded strings
query_params = ((k, v) for k, v in query_params.items() if v)
for i, (param_name, param_value) in enumerate(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.

nit: Do consider the string.Join suggestion from earlier (but it's certainly not a blocker)

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

# Send the request
{% if not method.void %}response = {% endif %}self._session.{{ method.http_opt['verb'] }}(
url,
{%- if 'body' in method.http_opt.keys() %}
json=data,
url
{%- if 'body' in method.http_opt %},
json=body,
{%- endif %}
)
{%- if not method.void %}
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
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@
"protobuf >= 3.12.0",
"pypandoc >= 1.4",
"PyYAML >= 5.1.1",
"dataclasses<0.8; python_version < '3.7'"
"dataclasses < 0.8; python_version < '3.7'"
),
extras_require={':python_version<"3.7"': ("dataclasses >= 0.4",),},
extras_require={':python_version<"3.7"': ("dataclasses >= 0.4, < 0.8",),},
tests_require=("pyfakefs >= 3.6",),
python_requires=">=3.6",
classifiers=(
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'