-
Notifications
You must be signed in to change notification settings - Fork 69
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good so far, just took an early readthrough.
potentialParams = {k: v for k, v in potentialParams.items() if v} | ||
for i, (param_name, param_value) in enumerate(potentialParams.items()): | ||
q = '?' if i == 0 else '&' | ||
url += q + param_name + '=' + str(param_value).replace(' ', '+') |
There was a problem hiding this comment.
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(' ', '+'))
url, | ||
{%- if 'body' in method.http_opt.keys() %} | ||
url | ||
{%- if 'body' in method.http_opt.keys() %}, |
There was a problem hiding this comment.
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.
'{{ field|camel_case }}': request.{{ field }}, | ||
{%- endfor %} | ||
} | ||
potentialParams = {k: v for k, v in potentialParams.items() if v} |
There was a problem hiding this comment.
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.
gapic/utils/case.py
Outdated
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:]]) |
There was a problem hiding this comment.
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:])
gapic/generator/generator.py
Outdated
#for method in service.methods.values(): | ||
#breakpoint() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall looks good, please try to get LGTM when Dov comes back and push this
@@ -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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
gapic/schema/wrappers.py
Outdated
def path_params(self) -> Sequence[str]: | ||
if self.http_opt is None: | ||
return [] | ||
pattern = r'\{\w+\}' |
There was a problem hiding this comment.
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
Codecov Report
@@ Coverage Diff @@
## master #702 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 26 26
Lines 1578 1598 +20
Branches 320 324 +4
=========================================
+ Hits 1578 1598 +20
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I flagged my more important comments with !!
It looks like this generator does not have golden files or baselines, correct? Too bad; it would be good to see a sample of the generated GAPIC showing off these features.
@@ -135,28 +135,42 @@ class {{ service.name }}RestTransport({{ service.name }}Transport): | |||
|
|||
{%- if 'body' in method.http_opt.keys() %} | |||
# Jsonify the input |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
including_default_value_fields=False | ||
) | ||
{%- else %} | ||
data = {{ method.input.ident }}.to_json( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
'{{ field|camel_case }}': request.{{ field }}, | ||
{%- endfor %} | ||
} | ||
potentialParams = ((k, v) for k, v in potentialParams.items() if v) |
There was a problem hiding this comment.
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
gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2
Outdated
Show resolved
Hide resolved
|
||
|
||
def to_camel_case(s: str) -> str: | ||
'''Convert any string to camel case. |
There was a problem hiding this comment.
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.
str: The string in lower camel case. | ||
''' | ||
|
||
items = re.split(r'[_-]', to_snake_case(s)) |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 '_'.
There was a problem hiding this comment.
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")
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 [x[1:-1] for x in re.findall(pattern, self.http_opt['url'])] | ||
|
||
@property | ||
def query_params(self) -> Set[str]: |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good. Two of my previous !!
comments are still open. The truthiness one we should mark with a TODO, since we can't fully resolve it right now. The URL-encoding one, if it's not trivial to address now, we should mark with a TODO and address in a follow-up PR.
gapic/schema/wrappers.py
Outdated
@@ -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""" |
There was a problem hiding this comment.
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?
gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2
Show resolved
Hide resolved
gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! Thanks for doing this!
For clarity when looking back at this commit, maybe you can expand in the merge commit description what the "basic case" is: path params in simple form, no body, no query param.
return [x[1:-1] for x in re.findall(pattern, self.http_opt['url'])] | ||
|
||
@property | ||
def query_params(self) -> Set[str]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True!
gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2
Show resolved
Hide resolved
{%- for field in method.query_params %} | ||
'{{ field|camel_case }}': request.{{ field }}, | ||
{%- endfor %} | ||
} | ||
potentialParams = ((k, v) for k, v in potentialParams.items() if v) | ||
for i, (param_name, param_value) in enumerate(potentialParams): | ||
# TODO(yon-mg): further discussion needed whether 'python truthiness' is appropriate here |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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)
🤖 I have created a release \*beep\* \*boop\* --- ## [0.37.0](https://www.github.com/googleapis/gapic-generator-python/compare/v0.36.0...v0.37.0) (2020-12-08) ### Features * add proper handling of query/path/body parameters for rest transport ([#702](https://www.github.com/googleapis/gapic-generator-python/issues/702)) ([6b2de5d](https://www.github.com/googleapis/gapic-generator-python/commit/6b2de5dd9fbf15e6b0a42b428b01eb03f1a3820a)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please).
Implementation and tests assume basic case of grpc transcoding for handling of all query/path parameters and body field.
These changes are dependent upon a PR in googleapis/proto-plus-python repo:
googleapis/proto-plus-python#164