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 grpc transcoding + tests #259

Merged
merged 20 commits into from Sep 15, 2021
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
57 changes: 56 additions & 1 deletion google/api_core/path_template.py
Expand Up @@ -64,7 +64,7 @@ def _expand_variable_match(positional_vars, named_vars, match):
"""Expand a matched variable with its value.

Args:
positional_vars (list): A list of positonal variables. This list will
positional_vars (list): A list of positional variables. This list will
be modified.
named_vars (dict): A dictionary of named variables.
match (re.Match): A regular expression match.
Expand Down Expand Up @@ -193,3 +193,58 @@ def validate(tmpl, path):
"""
pattern = _generate_pattern_for_template(tmpl) + "$"
return True if re.match(pattern, path) is not None else False

def transcode(http_options, **request_kwargs):
"""Transcodes a grpc request pattern into a proper HTTP request following the rules outlined here,
https://github.com/googleapis/googleapis/blob/master/google/api/http.proto#L44-L312
yihjenku marked this conversation as resolved.
Show resolved Hide resolved

Args:
http_options (list(dict)): A list of dicts which consist of these keys,
yihjenku marked this conversation as resolved.
Show resolved Hide resolved
'method' (str): The http method
'uri' (str): The path template
'body' (str): The body field name (optional)
(This is a simplified representation of the proto option `google.api.http`)
tseaver marked this conversation as resolved.
Show resolved Hide resolved
request_kwargs (dict) : A dict representing the request object

Returns:
dict: The transcoded request with these keys,
yihjenku marked this conversation as resolved.
Show resolved Hide resolved
'method' (str) : The http method
'uri' (str) : The expanded uri
'body' (dict) : A dict representing the body (optional)
'query_params' (dict) : A dict mapping query parameter variables and values

Raises:
ValueError: If the request does not match the given template.
"""
request = {}
yihjenku marked this conversation as resolved.
Show resolved Hide resolved
for http_option in http_options:
yihjenku marked this conversation as resolved.
Show resolved Hide resolved

# Assign path
uri_template = http_option['uri']
path_fields = [match.group('name') for match in _VARIABLE_RE.finditer(uri_template)]
path_args = {field: request_kwargs.get(field) for field in path_fields}
yihjenku marked this conversation as resolved.
Show resolved Hide resolved
leftovers = {k: v for k,v in request_kwargs.items() if k not in path_args}
yihjenku marked this conversation as resolved.
Show resolved Hide resolved
request['uri'] = expand(uri_template, **path_args)

if not validate(uri_template, request['uri']) or not all(path_args.values()):
continue

# Assign body and query params
body = http_option.get('body')

if body:
if body == '*':
request['body'] = leftovers
request['query_params'] = {}
else:
try:
request['body'] = leftovers.pop(body)
except KeyError:
continue
request['query_params'] = leftovers
else:
request['query_params'] = leftovers
request['method'] = http_option['method']
return request

raise ValueError("Request obj does not match any template")
137 changes: 137 additions & 0 deletions tests/unit/test_path_template.py
Expand Up @@ -113,3 +113,140 @@ def test__replace_variable_with_pattern():
match.group.return_value = None
with pytest.raises(ValueError, match="Unknown"):
path_template._replace_variable_with_pattern(match)


@pytest.mark.parametrize(
'http_options, request_kwargs, expected_result',
[
[[['get','/v1/no/template','']],
{'foo':'bar'},
['get','/v1/no/template',{},{'foo':'bar'}]],

# Single templates
[[['get','/v1/{field}','']],
{'field':'parent'},
['get','/v1/parent',{},{}]],

[[['get','/v1/{field.sub}','']],
{'field.sub':'parent', 'foo':'bar'},
['get','/v1/parent',{},{'foo':'bar'}]],
]
)
def test_transcode_base_case(http_options, request_kwargs, expected_result):
http_options, expected_result = helper_test_transcode(http_options, expected_result)
result = path_template.transcode(http_options, **request_kwargs)
assert result == expected_result


@pytest.mark.parametrize(
'http_options, request_kwargs, expected_result',
[
# Single segment wildcard
[[['get','/v1/{field=*}','']],
{'field':'parent'},
['get','/v1/parent',{},{}]],

[[['get','/v1/{field=a/*/b/*}','']],
{'field':'a/parent/b/child','foo':'bar'},
['get','/v1/a/parent/b/child',{},{'foo':'bar'}]],

# Double segment wildcard
[[['get','/v1/{field=**}','']],
{'field':'parent/p1'},
['get','/v1/parent/p1',{},{}]],

[[['get','/v1/{field=a/**/b/**}','']],
{'field':'a/parent/p1/b/child/c1', 'foo':'bar'},
['get','/v1/a/parent/p1/b/child/c1',{},{'foo':'bar'}]],

# Combined single and double segment wildcard
[[['get','/v1/{field=a/*/b/**}','']],
{'field':'a/parent/b/child/c1'},
['get','/v1/a/parent/b/child/c1',{},{}]],

[[['get','/v1/{field=a/**/b/*}/v2/{name}','']],
{'field':'a/parent/p1/b/child', 'name':'first', 'foo':'bar'},
['get','/v1/a/parent/p1/b/child/v2/first',{},{'foo':'bar'}]],
]
)
def test_transcode_with_wildcard(http_options, request_kwargs, expected_result):
http_options, expected_result = helper_test_transcode(http_options, expected_result)
result = path_template.transcode(http_options, **request_kwargs)
assert result == expected_result


@pytest.mark.parametrize(
'http_options, request_kwargs, expected_result',
[
# Single field body
[[['post','/v1/no/template','data']],
{'data':{'id':1, 'info':'some info'},'foo':'bar'},
['post','/v1/no/template',{'id':1, 'info':'some info'},{'foo':'bar'}]],

[[['post','/v1/{field=a/*}/b/{name=**}','data']],
{'field':'a/parent','name':'first/last','data':{'id':1, 'info':'some info'},'foo':'bar'},
['post','/v1/a/parent/b/first/last',{'id':1, 'info':'some info'},{'foo':'bar'}]],

# Wildcard body
[[['post','/v1/{field=a/*}/b/{name=**}','*']],
{'field':'a/parent','name':'first/last','data':{'id':1, 'info':'some info'},'foo':'bar'},
['post','/v1/a/parent/b/first/last',{'data':{'id':1, 'info':'some info'},'foo':'bar'},{}]],
]
)
def test_transcode_with_body(http_options, request_kwargs, expected_result):
http_options, expected_result = helper_test_transcode(http_options, expected_result)
result = path_template.transcode(http_options, **request_kwargs)
assert result == expected_result
yihjenku marked this conversation as resolved.
Show resolved Hide resolved


@pytest.mark.parametrize(
'http_options, request_kwargs, expected_result',
[
# Additional bindings
[[['post','/v1/{field=a/*}/b/{name=**}','extra_data'], ['post','/v1/{field=a/*}/b/{name=**}','*']],
{'field':'a/parent','name':'first/last','data':{'id':1, 'info':'some info'},'foo':'bar'},
['post','/v1/a/parent/b/first/last',{'data':{'id':1, 'info':'some info'},'foo':'bar'},{}]],

[[['get','/v1/{field=a/*}/b/{name=**}',''],['get','/v1/{field=a/*}/b/first/last','']],
{'field':'a/parent','foo':'bar'},
['get','/v1/a/parent/b/first/last',{},{'foo':'bar'}]],
]
)
def test_transcode_with_additional_bindings(http_options, request_kwargs, expected_result):
http_options, expected_result = helper_test_transcode(http_options, expected_result)
result = path_template.transcode(http_options, **request_kwargs)
assert result == expected_result


@pytest.mark.parametrize(
'http_options, request_kwargs',
[
[[['get','/v1/{name}','']], {'foo':'bar'}],
[[['get','/v1/{name}','']], {'name':'first/last'}],
[[['get','/v1/{name=mr/*/*}','']], {'name':'first/last'}],
[[['post','/v1/{name}','data']], {'name':'first/last'}],
]
)
def test_transcode_fails(http_options, request_kwargs):
http_options, _ = helper_test_transcode(http_options, range(4))
with pytest.raises(ValueError):
path_template.transcode(http_options, **request_kwargs)


def helper_test_transcode(http_options_list, expected_result_list):
http_options = []
for opt_list in http_options_list:
http_option = {'method':opt_list[0], 'uri':opt_list[1]}
if opt_list[2]:
http_option['body'] = opt_list[2]
http_options.append(http_option)

expected_result = {
'method':expected_result_list[0],
'uri':expected_result_list[1],
'query_params':expected_result_list[3]
}
if (expected_result_list[2]):
expected_result['body'] = expected_result_list[2]

return(http_options, expected_result)