From 598ca883dd689e58cf0e86131089d66af08255ad Mon Sep 17 00:00:00 2001 From: Yonatan Getahun Date: Tue, 9 Feb 2021 01:37:34 +0000 Subject: [PATCH 01/13] feat: add grpc transcoding + tests --- google/api_core/path_template.py | 57 +++++++++++++++- tests/unit/test_path_template.py | 107 +++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 1 deletion(-) diff --git a/google/api_core/path_template.py b/google/api_core/path_template.py index c5969c14..4019a28f 100644 --- a/google/api_core/path_template.py +++ b/google/api_core/path_template.py @@ -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. @@ -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 + + Args: + http_options (list(dict)): A list of dicts which consist of these keys, + '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`) + request_kwargs (dict) : A dict representing the request object + + Returns: + dict: The transcoded request with these keys, + '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. + """ + answer = {} + for http_option in http_options: + + # 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, None) for field in path_fields} + leftovers = {k:v for k,v in request_kwargs.items() if k not in path_args} + answer['uri'] = expand(uri_template, **path_args) + + if not validate(uri_template, answer['uri']) or not all(path_args.values()): + continue + + # Assign body and query params + body = http_option.get('body') + + if body: + if body == '*': + answer['body'] = leftovers + answer['query_params'] = {} + else: + try: + answer['body'] = leftovers.pop(body) + except KeyError: + continue + answer['query_params'] = leftovers + else: + answer['query_params'] = leftovers + answer['method'] = http_option['method'] + return answer + + raise ValueError("Request obj does not match any template") diff --git a/tests/unit/test_path_template.py b/tests/unit/test_path_template.py index 4c8a7c5e..6b2a74e0 100644 --- a/tests/unit/test_path_template.py +++ b/tests/unit/test_path_template.py @@ -113,3 +113,110 @@ 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'}]], + + # 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'}]], + + # 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'},{}]], + + # 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(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) From a956abb7de3fd0c5a72eb21145e03b118a3b3718 Mon Sep 17 00:00:00 2001 From: Owl Bot Date: Wed, 25 Aug 2021 17:55:49 +0000 Subject: [PATCH 02/13] =?UTF-8?q?=F0=9F=A6=89=20Updates=20from=20OwlBot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --- google/api_core/path_template.py | 33 +++--- tests/unit/test_path_template.py | 195 +++++++++++++++++++------------ 2 files changed, 141 insertions(+), 87 deletions(-) diff --git a/google/api_core/path_template.py b/google/api_core/path_template.py index 4019a28f..3b55da64 100644 --- a/google/api_core/path_template.py +++ b/google/api_core/path_template.py @@ -194,6 +194,7 @@ 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 @@ -220,31 +221,33 @@ def transcode(http_options, **request_kwargs): for http_option in http_options: # 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, None) for field in path_fields} - leftovers = {k:v for k,v in request_kwargs.items() if k not in path_args} - answer['uri'] = expand(uri_template, **path_args) - - if not validate(uri_template, answer['uri']) or not all(path_args.values()): + 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, None) for field in path_fields} + leftovers = {k: v for k, v in request_kwargs.items() if k not in path_args} + answer["uri"] = expand(uri_template, **path_args) + + if not validate(uri_template, answer["uri"]) or not all(path_args.values()): continue # Assign body and query params - body = http_option.get('body') + body = http_option.get("body") if body: - if body == '*': - answer['body'] = leftovers - answer['query_params'] = {} + if body == "*": + answer["body"] = leftovers + answer["query_params"] = {} else: try: - answer['body'] = leftovers.pop(body) + answer["body"] = leftovers.pop(body) except KeyError: continue - answer['query_params'] = leftovers + answer["query_params"] = leftovers else: - answer['query_params'] = leftovers - answer['method'] = http_option['method'] + answer["query_params"] = leftovers + answer["method"] = http_option["method"] return answer raise ValueError("Request obj does not match any template") diff --git a/tests/unit/test_path_template.py b/tests/unit/test_path_template.py index 6b2a74e0..0816feb2 100644 --- a/tests/unit/test_path_template.py +++ b/tests/unit/test_path_template.py @@ -116,86 +116,137 @@ def test__replace_variable_with_pattern(): @pytest.mark.parametrize( - 'http_options, request_kwargs, expected_result', + "http_options, request_kwargs, expected_result", [ - [[['get','/v1/no/template','']], - {'foo':'bar'}, - ['get','/v1/no/template',{},{'foo':'bar'}]], - + [ + [["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'}]], - + [ + [["get", "/v1/{field}", ""]], + {"field": "parent"}, + ["get", "/v1/parent", {}, {}], + ], + [ + [["get", "/v1/{field.sub}", ""]], + {"field.sub": "parent", "foo": "bar"}, + ["get", "/v1/parent", {}, {"foo": "bar"}], + ], # 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'}]], - + [ + [["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'}]], - + [ + [["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'}]], - + [ + [["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"}], + ], # 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'}]], - + [ + [["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'},{}]], - + [ + [["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"}, + {}, + ], + ], # 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'}]], - ] + [ + [ + ["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(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', + "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'}], - ] + [[["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)) @@ -206,17 +257,17 @@ def test_transcode_fails(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]} + http_option = {"method": opt_list[0], "uri": opt_list[1]} if opt_list[2]: - http_option['body'] = 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] + "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] + if expected_result_list[2]: + expected_result["body"] = expected_result_list[2] - return(http_options, expected_result) + return (http_options, expected_result) From 1ca321af855838eed454faa85e6bbb2ab3cc0c42 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 25 Aug 2021 14:12:38 -0400 Subject: [PATCH 03/13] chore: tweak for clarity / idiomatic usage --- google/api_core/path_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/api_core/path_template.py b/google/api_core/path_template.py index 3b55da64..f55f1be4 100644 --- a/google/api_core/path_template.py +++ b/google/api_core/path_template.py @@ -225,7 +225,7 @@ def transcode(http_options, **request_kwargs): path_fields = [ match.group("name") for match in _VARIABLE_RE.finditer(uri_template) ] - path_args = {field: request_kwargs.get(field, None) for field in path_fields} + path_args = {field: request_kwargs.get(field) for field in path_fields} leftovers = {k: v for k, v in request_kwargs.items() if k not in path_args} answer["uri"] = expand(uri_template, **path_args) From f8725d98dbd0c04a20bea2223c1e9ba9af160c59 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 25 Aug 2021 14:45:43 -0400 Subject: [PATCH 04/13] chore: attempt to appease Sphinx --- google/api_core/path_template.py | 1 + 1 file changed, 1 insertion(+) diff --git a/google/api_core/path_template.py b/google/api_core/path_template.py index f55f1be4..0f6319c9 100644 --- a/google/api_core/path_template.py +++ b/google/api_core/path_template.py @@ -205,6 +205,7 @@ def transcode(http_options, **request_kwargs): 'uri' (str): The path template 'body' (str): The body field name (optional) (This is a simplified representation of the proto option `google.api.http`) + request_kwargs (dict) : A dict representing the request object Returns: From 79f561f6d586e470b86b41f9b5cbf1d2a545552d Mon Sep 17 00:00:00 2001 From: Yonatan Getahun Date: Tue, 9 Feb 2021 01:37:34 +0000 Subject: [PATCH 05/13] feat: add grpc transcoding + tests --- google/api_core/path_template.py | 57 ++++++++++++- tests/unit/test_path_template.py | 137 +++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 1 deletion(-) diff --git a/google/api_core/path_template.py b/google/api_core/path_template.py index c5969c14..a0af0cee 100644 --- a/google/api_core/path_template.py +++ b/google/api_core/path_template.py @@ -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. @@ -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 + + Args: + http_options (list(dict)): A list of dicts which consist of these keys, + '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`) + request_kwargs (dict) : A dict representing the request object + + Returns: + dict: The transcoded request with these keys, + '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 = {} + for http_option in http_options: + + # 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, None) for field in path_fields} + leftovers = {k:v for k,v in request_kwargs.items() if k not in path_args} + 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") diff --git a/tests/unit/test_path_template.py b/tests/unit/test_path_template.py index 4c8a7c5e..9a647295 100644 --- a/tests/unit/test_path_template.py +++ b/tests/unit/test_path_template.py @@ -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 + + +@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) From 51661d3a942e58b082e354eedc0ba64134071f22 Mon Sep 17 00:00:00 2001 From: Yih-Jen Ku Date: Thu, 9 Sep 2021 17:40:48 +0000 Subject: [PATCH 06/13] Add functions to properly handle subfields --- google/api_core/path_template.py | 40 ++++++++++++++++++++++++++++++-- tests/unit/test_path_template.py | 26 +++++++++++++++++++-- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/google/api_core/path_template.py b/google/api_core/path_template.py index e7a58bb0..ab063b73 100644 --- a/google/api_core/path_template.py +++ b/google/api_core/path_template.py @@ -25,6 +25,8 @@ from __future__ import unicode_literals +from collections import deque +import copy import functools import re @@ -170,6 +172,37 @@ def _generate_pattern_for_template(tmpl): return _VARIABLE_RE.sub(_replace_variable_with_pattern, tmpl) +def get_field(request, field): + try: + parts = field.split('.') + value = request + for part in parts: + if not isinstance(value, dict): + return + value = value[part] + if isinstance(value, dict): + return + return value + except KeyError: + return + + +def delete_field(request, field): + try: + parts = deque(field.split('.')) + while len(parts) > 1: + if not isinstance(request, dict): + return + part = parts.popleft() + request = request[part] + part = parts.popleft() + if not isinstance(request, dict): + return + del request[part] + except KeyError: + return + + def validate(tmpl, path): """Validate a path against the path template. @@ -222,8 +255,11 @@ def transcode(http_options, **request_kwargs): # 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} - leftovers = {k: v for k,v in request_kwargs.items() if k not in path_args} + path_args = {field: get_field(request_kwargs, field) for field in path_fields} + leftovers = copy.deepcopy(request_kwargs) + for path_field in path_fields: + delete_field(leftovers, path_field) + # leftovers = {k: v for k,v in request_kwargs.items() if k not in path_args} request['uri'] = expand(uri_template, **path_args) if not validate(uri_template, request['uri']) or not all(path_args.values()): diff --git a/tests/unit/test_path_template.py b/tests/unit/test_path_template.py index 9a647295..23b10825 100644 --- a/tests/unit/test_path_template.py +++ b/tests/unit/test_path_template.py @@ -128,8 +128,8 @@ def test__replace_variable_with_pattern(): ['get','/v1/parent',{},{}]], [[['get','/v1/{field.sub}','']], - {'field.sub':'parent', 'foo':'bar'}, - ['get','/v1/parent',{},{'foo':'bar'}]], + {'field': {'sub': 'parent'}, 'foo':'bar'}, + ['get','/v1/parent',{},{'field': {}, 'foo':'bar'}]], ] ) def test_transcode_base_case(http_options, request_kwargs, expected_result): @@ -138,6 +138,28 @@ def test_transcode_base_case(http_options, request_kwargs, expected_result): assert result == expected_result +@pytest.mark.parametrize( + 'http_options, request_kwargs, expected_result', + [ + [[['get','/v1/{field.subfield}','']], + {'field': {'subfield': 'parent'}, 'foo':'bar'}, + ['get','/v1/parent',{},{'field': {}, 'foo':'bar'}]], + + [[['get','/v1/{field.subfield.subsubfield}','']], + {'field': {'subfield': {'subsubfield': 'parent'}}, 'foo':'bar'}, + ['get','/v1/parent',{},{'field': {'subfield': {}}, 'foo':'bar'}]], + + [[['get','/v1/{field.subfield1}/{field.subfield2}','']], + {'field': {'subfield1': 'parent', 'subfield2': 'child'}, 'foo':'bar'}, + ['get','/v1/parent/child',{},{'field': {}, 'foo':'bar'}]], + ] +) +def test_transcode_subfields(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', [ From 12a4436339bc1cca5cbfaa4cb6a3c831e2ae23e3 Mon Sep 17 00:00:00 2001 From: Yih-Jen Ku Date: Thu, 9 Sep 2021 20:48:38 +0000 Subject: [PATCH 07/13] Add unit tests for get_field and delete_field. --- google/api_core/path_template.py | 9 +++--- tests/unit/test_path_template.py | 51 ++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/google/api_core/path_template.py b/google/api_core/path_template.py index ab063b73..08f24570 100644 --- a/google/api_core/path_template.py +++ b/google/api_core/path_template.py @@ -249,18 +249,19 @@ def transcode(http_options, **request_kwargs): Raises: ValueError: If the request does not match the given template. """ - request = {} for http_option in http_options: - + request = {} + # Assign path uri_template = http_option['uri'] path_fields = [match.group('name') for match in _VARIABLE_RE.finditer(uri_template)] path_args = {field: get_field(request_kwargs, field) for field in path_fields} + request['uri'] = expand(uri_template, **path_args) + + # Remove fields used in uri path from request leftovers = copy.deepcopy(request_kwargs) for path_field in path_fields: delete_field(leftovers, path_field) - # leftovers = {k: v for k,v in request_kwargs.items() if k not in path_args} - request['uri'] = expand(uri_template, **path_args) if not validate(uri_template, request['uri']) or not all(path_args.values()): continue diff --git a/tests/unit/test_path_template.py b/tests/unit/test_path_template.py index 23b10825..d43efea9 100644 --- a/tests/unit/test_path_template.py +++ b/tests/unit/test_path_template.py @@ -84,6 +84,57 @@ def test_expanded_failure(tmpl, args, kwargs, exc_match): path_template.expand(tmpl, *args, **kwargs) +@pytest.mark.parametrize( + 'request_obj, field, expected_result', + [ + [{'field': 'stringValue'}, 'field', 'stringValue'], + [{'field': 'stringValue'}, 'nosuchfield', None], + [{'field': 'stringValue'}, 'field.subfield', None], + [{'field': {'subfield': 'stringValue'}}, 'field.subfield', 'stringValue'], + [{'field': {'subfield': [1,2,3]}}, 'field.subfield', [1,2,3]], + [{'field': {'subfield': 'stringValue'}}, 'field', None], + [{'field': {'subfield': 'stringValue'}}, 'field.nosuchfield', None], + [ + {'field': {'subfield': {'subsubfield': 'stringValue'}}}, + 'field.subfield.subsubfield', + 'stringValue' + ], + ], +) +def test_get_field(request_obj, field, expected_result): + result = path_template.get_field(request_obj, field) + assert result == expected_result + + +@pytest.mark.parametrize( + 'request_obj, field, expected_result', + [ + [{'field': 'stringValue'}, 'field', {}], + [{'field': 'stringValue'}, 'nosuchfield', {'field': 'stringValue'}], + [{'field': 'stringValue'}, 'field.subfield', {'field': 'stringValue'}], + [{'field': {'subfield': 'stringValue'}}, 'field.subfield', {'field': {}}], + [ + {'field': {'subfield': 'stringValue', 'q': 'w'}, 'e': 'f'}, + 'field.subfield', + {'field': {'q': 'w'}, 'e': 'f'} + ], + [ + {'field': {'subfield': 'stringValue'}}, + 'field.nosuchfield', + {'field': {'subfield': 'stringValue'}} + ], + [ + {'field': {'subfield': {'subsubfield': 'stringValue', 'q': 'w'}}}, + 'field.subfield.subsubfield', + {'field': {'subfield': {'q': 'w'}}} + ], + ], +) +def test_get_field(request_obj, field, expected_result): + path_template.delete_field(request_obj, field) + assert request_obj == expected_result + + @pytest.mark.parametrize( "tmpl, path", [ From 28f587dfd28bed7753f21315761df433410faf62 Mon Sep 17 00:00:00 2001 From: Yih-Jen Ku Date: Tue, 14 Sep 2021 18:45:25 +0000 Subject: [PATCH 08/13] Add function docstrings and incorporate correct native dict functions. --- google/api_core/path_template.py | 49 +++++++++++++++++++------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/google/api_core/path_template.py b/google/api_core/path_template.py index 08f24570..6e94085c 100644 --- a/google/api_core/path_template.py +++ b/google/api_core/path_template.py @@ -173,34 +173,43 @@ def _generate_pattern_for_template(tmpl): def get_field(request, field): - try: - parts = field.split('.') - value = request - for part in parts: - if not isinstance(value, dict): - return - value = value[part] - if isinstance(value, dict): + """Get the value of a field from a given dictionary. + + Args: + request (dict): A dictionary object. + field (str): The key to the request in dot notation. + + Returns: + The value of the field. + """ + parts = field.split('.') + value = request + for part in parts: + if not isinstance(value, dict): return - return value - except KeyError: + value = value.get(part) + if isinstance(value, dict): return + return value def delete_field(request, field): - try: - parts = deque(field.split('.')) - while len(parts) > 1: - if not isinstance(request, dict): - return - part = parts.popleft() - request = request[part] - part = parts.popleft() + """Delete the value of a field from a given dictionary. + + Args: + request (dict): A dictionary object. + field (str): The key to the request in dot notation. + """ + parts = deque(field.split('.')) + while len(parts) > 1: if not isinstance(request, dict): return - del request[part] - except KeyError: + part = parts.popleft() + request = request.get(part) + part = parts.popleft() + if not isinstance(request, dict): return + request.pop(part, None) def validate(tmpl, path): From 928e3fb1f1cf194004d64d08d6fde1f169b0e530 Mon Sep 17 00:00:00 2001 From: Yih-Jen Ku Date: Tue, 14 Sep 2021 18:45:25 +0000 Subject: [PATCH 09/13] Add function docstrings and incorporate correct native dict functions. --- google/api_core/path_template.py | 50 +++++++++++++++++++------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/google/api_core/path_template.py b/google/api_core/path_template.py index 08f24570..b4926b12 100644 --- a/google/api_core/path_template.py +++ b/google/api_core/path_template.py @@ -173,34 +173,43 @@ def _generate_pattern_for_template(tmpl): def get_field(request, field): - try: - parts = field.split('.') - value = request - for part in parts: - if not isinstance(value, dict): - return - value = value[part] - if isinstance(value, dict): + """Get the value of a field from a given dictionary. + + Args: + request (dict): A dictionary object. + field (str): The key to the request in dot notation. + + Returns: + The value of the field. + """ + parts = field.split('.') + value = request + for part in parts: + if not isinstance(value, dict): return - return value - except KeyError: + value = value.get(part) + if isinstance(value, dict): return + return value def delete_field(request, field): - try: - parts = deque(field.split('.')) - while len(parts) > 1: - if not isinstance(request, dict): - return - part = parts.popleft() - request = request[part] - part = parts.popleft() + """Delete the value of a field from a given dictionary. + + Args: + request (dict): A dictionary object. + field (str): The key to the request in dot notation. + """ + parts = deque(field.split('.')) + while len(parts) > 1: if not isinstance(request, dict): return - del request[part] - except KeyError: + part = parts.popleft() + request = request.get(part) + part = parts.popleft() + if not isinstance(request, dict): return + request.pop(part, None) def validate(tmpl, path): @@ -237,6 +246,7 @@ def transcode(http_options, **request_kwargs): 'uri' (str): The path template 'body' (str): The body field name (optional) (This is a simplified representation of the proto option `google.api.http`) + request_kwargs (dict) : A dict representing the request object Returns: From 97c56979b88ebd58e426d2a782f263530075e072 Mon Sep 17 00:00:00 2001 From: Yih-Jen Ku Date: Wed, 15 Sep 2021 18:20:29 +0000 Subject: [PATCH 10/13] Increase code coverage --- tests/unit/test_path_template.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/test_path_template.py b/tests/unit/test_path_template.py index d43efea9..b6c15797 100644 --- a/tests/unit/test_path_template.py +++ b/tests/unit/test_path_template.py @@ -90,6 +90,7 @@ def test_expanded_failure(tmpl, args, kwargs, exc_match): [{'field': 'stringValue'}, 'field', 'stringValue'], [{'field': 'stringValue'}, 'nosuchfield', None], [{'field': 'stringValue'}, 'field.subfield', None], + [{'field': {'subfield': 'stringValue'}}, 'field', None], [{'field': {'subfield': 'stringValue'}}, 'field.subfield', 'stringValue'], [{'field': {'subfield': [1,2,3]}}, 'field.subfield', [1,2,3]], [{'field': {'subfield': 'stringValue'}}, 'field', None], @@ -99,6 +100,7 @@ def test_expanded_failure(tmpl, args, kwargs, exc_match): 'field.subfield.subsubfield', 'stringValue' ], + ['string', 'field', None], ], ) def test_get_field(request_obj, field, expected_result): @@ -128,6 +130,7 @@ def test_get_field(request_obj, field, expected_result): 'field.subfield.subsubfield', {'field': {'subfield': {'q': 'w'}}} ], + ['string', 'field,', 'string'], ], ) def test_get_field(request_obj, field, expected_result): From 1450b91dc0a47bad739b9f722839ad45819d11d4 Mon Sep 17 00:00:00 2001 From: Yih-Jen Ku Date: Wed, 15 Sep 2021 19:08:18 +0000 Subject: [PATCH 11/13] Increase code coverage --- tests/unit/test_path_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_path_template.py b/tests/unit/test_path_template.py index b6c15797..77eef996 100644 --- a/tests/unit/test_path_template.py +++ b/tests/unit/test_path_template.py @@ -133,7 +133,7 @@ def test_get_field(request_obj, field, expected_result): ['string', 'field,', 'string'], ], ) -def test_get_field(request_obj, field, expected_result): +def test_delete_field(request_obj, field, expected_result): path_template.delete_field(request_obj, field) assert request_obj == expected_result From 4c157a903ef77f13431fa13e81e16a520544a95a Mon Sep 17 00:00:00 2001 From: Yih-Jen Ku Date: Wed, 15 Sep 2021 19:28:02 +0000 Subject: [PATCH 12/13] Increase code coverage --- tests/unit/test_path_template.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_path_template.py b/tests/unit/test_path_template.py index 77eef996..fbfc6307 100644 --- a/tests/unit/test_path_template.py +++ b/tests/unit/test_path_template.py @@ -130,7 +130,8 @@ def test_get_field(request_obj, field, expected_result): 'field.subfield.subsubfield', {'field': {'subfield': {'q': 'w'}}} ], - ['string', 'field,', 'string'], + ['string', 'field', 'string'], + ['string', 'field.subfield', 'string'], ], ) def test_delete_field(request_obj, field, expected_result): From c99e7bbeeadd5e1b410c77923052d6ed73e4603f Mon Sep 17 00:00:00 2001 From: Yih-Jen Ku Date: Wed, 15 Sep 2021 19:46:52 +0000 Subject: [PATCH 13/13] Reformat files --- google/api_core/path_template.py | 35 ++-- tests/unit/test_path_template.py | 296 +++++++++++++++++++------------ 2 files changed, 197 insertions(+), 134 deletions(-) diff --git a/google/api_core/path_template.py b/google/api_core/path_template.py index b4926b12..41fbd4fe 100644 --- a/google/api_core/path_template.py +++ b/google/api_core/path_template.py @@ -182,7 +182,7 @@ def get_field(request, field): Returns: The value of the field. """ - parts = field.split('.') + parts = field.split(".") value = request for part in parts: if not isinstance(value, dict): @@ -200,7 +200,7 @@ def delete_field(request, field): request (dict): A dictionary object. field (str): The key to the request in dot notation. """ - parts = deque(field.split('.')) + parts = deque(field.split(".")) while len(parts) > 1: if not isinstance(request, dict): return @@ -210,7 +210,7 @@ def delete_field(request, field): if not isinstance(request, dict): return request.pop(part, None) - + def validate(tmpl, path): """Validate a path against the path template. @@ -236,6 +236,7 @@ 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 @@ -261,37 +262,39 @@ def transcode(http_options, **request_kwargs): """ for http_option in http_options: request = {} - + # Assign path - uri_template = http_option['uri'] - path_fields = [match.group('name') for match in _VARIABLE_RE.finditer(uri_template)] + uri_template = http_option["uri"] + path_fields = [ + match.group("name") for match in _VARIABLE_RE.finditer(uri_template) + ] path_args = {field: get_field(request_kwargs, field) for field in path_fields} - request['uri'] = expand(uri_template, **path_args) + request["uri"] = expand(uri_template, **path_args) # Remove fields used in uri path from request leftovers = copy.deepcopy(request_kwargs) for path_field in path_fields: delete_field(leftovers, path_field) - if not validate(uri_template, request['uri']) or not all(path_args.values()): + if not validate(uri_template, request["uri"]) or not all(path_args.values()): continue # Assign body and query params - body = http_option.get('body') + body = http_option.get("body") if body: - if body == '*': - request['body'] = leftovers - request['query_params'] = {} + if body == "*": + request["body"] = leftovers + request["query_params"] = {} else: try: - request['body'] = leftovers.pop(body) + request["body"] = leftovers.pop(body) except KeyError: continue - request['query_params'] = leftovers + request["query_params"] = leftovers else: - request['query_params'] = leftovers - request['method'] = http_option['method'] + request["query_params"] = leftovers + request["method"] = http_option["method"] return request raise ValueError("Request obj does not match any template") diff --git a/tests/unit/test_path_template.py b/tests/unit/test_path_template.py index fbfc6307..2c5216e0 100644 --- a/tests/unit/test_path_template.py +++ b/tests/unit/test_path_template.py @@ -85,22 +85,22 @@ def test_expanded_failure(tmpl, args, kwargs, exc_match): @pytest.mark.parametrize( - 'request_obj, field, expected_result', + "request_obj, field, expected_result", [ - [{'field': 'stringValue'}, 'field', 'stringValue'], - [{'field': 'stringValue'}, 'nosuchfield', None], - [{'field': 'stringValue'}, 'field.subfield', None], - [{'field': {'subfield': 'stringValue'}}, 'field', None], - [{'field': {'subfield': 'stringValue'}}, 'field.subfield', 'stringValue'], - [{'field': {'subfield': [1,2,3]}}, 'field.subfield', [1,2,3]], - [{'field': {'subfield': 'stringValue'}}, 'field', None], - [{'field': {'subfield': 'stringValue'}}, 'field.nosuchfield', None], + [{"field": "stringValue"}, "field", "stringValue"], + [{"field": "stringValue"}, "nosuchfield", None], + [{"field": "stringValue"}, "field.subfield", None], + [{"field": {"subfield": "stringValue"}}, "field", None], + [{"field": {"subfield": "stringValue"}}, "field.subfield", "stringValue"], + [{"field": {"subfield": [1, 2, 3]}}, "field.subfield", [1, 2, 3]], + [{"field": {"subfield": "stringValue"}}, "field", None], + [{"field": {"subfield": "stringValue"}}, "field.nosuchfield", None], [ - {'field': {'subfield': {'subsubfield': 'stringValue'}}}, - 'field.subfield.subsubfield', - 'stringValue' + {"field": {"subfield": {"subsubfield": "stringValue"}}}, + "field.subfield.subsubfield", + "stringValue", ], - ['string', 'field', None], + ["string", "field", None], ], ) def test_get_field(request_obj, field, expected_result): @@ -109,29 +109,29 @@ def test_get_field(request_obj, field, expected_result): @pytest.mark.parametrize( - 'request_obj, field, expected_result', + "request_obj, field, expected_result", [ - [{'field': 'stringValue'}, 'field', {}], - [{'field': 'stringValue'}, 'nosuchfield', {'field': 'stringValue'}], - [{'field': 'stringValue'}, 'field.subfield', {'field': 'stringValue'}], - [{'field': {'subfield': 'stringValue'}}, 'field.subfield', {'field': {}}], + [{"field": "stringValue"}, "field", {}], + [{"field": "stringValue"}, "nosuchfield", {"field": "stringValue"}], + [{"field": "stringValue"}, "field.subfield", {"field": "stringValue"}], + [{"field": {"subfield": "stringValue"}}, "field.subfield", {"field": {}}], [ - {'field': {'subfield': 'stringValue', 'q': 'w'}, 'e': 'f'}, - 'field.subfield', - {'field': {'q': 'w'}, 'e': 'f'} + {"field": {"subfield": "stringValue", "q": "w"}, "e": "f"}, + "field.subfield", + {"field": {"q": "w"}, "e": "f"}, ], [ - {'field': {'subfield': 'stringValue'}}, - 'field.nosuchfield', - {'field': {'subfield': 'stringValue'}} + {"field": {"subfield": "stringValue"}}, + "field.nosuchfield", + {"field": {"subfield": "stringValue"}}, ], [ - {'field': {'subfield': {'subsubfield': 'stringValue', 'q': 'w'}}}, - 'field.subfield.subsubfield', - {'field': {'subfield': {'q': 'w'}}} + {"field": {"subfield": {"subsubfield": "stringValue", "q": "w"}}}, + "field.subfield.subsubfield", + {"field": {"subfield": {"q": "w"}}}, ], - ['string', 'field', 'string'], - ['string', 'field.subfield', 'string'], + ["string", "field", "string"], + ["string", "field.subfield", "string"], ], ) def test_delete_field(request_obj, field, expected_result): @@ -171,21 +171,25 @@ def test__replace_variable_with_pattern(): @pytest.mark.parametrize( - 'http_options, request_kwargs, expected_result', + "http_options, request_kwargs, expected_result", [ - [[['get','/v1/no/template','']], - {'foo':'bar'}, - ['get','/v1/no/template',{},{'foo':'bar'}]], - + [ + [["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',{},{'field': {}, 'foo':'bar'}]], - ] + [ + [["get", "/v1/{field}", ""]], + {"field": "parent"}, + ["get", "/v1/parent", {}, {}], + ], + [ + [["get", "/v1/{field.sub}", ""]], + {"field": {"sub": "parent"}, "foo": "bar"}, + ["get", "/v1/parent", {}, {"field": {}, "foo": "bar"}], + ], + ], ) def test_transcode_base_case(http_options, request_kwargs, expected_result): http_options, expected_result = helper_test_transcode(http_options, expected_result) @@ -194,20 +198,24 @@ def test_transcode_base_case(http_options, request_kwargs, expected_result): @pytest.mark.parametrize( - 'http_options, request_kwargs, expected_result', + "http_options, request_kwargs, expected_result", [ - [[['get','/v1/{field.subfield}','']], - {'field': {'subfield': 'parent'}, 'foo':'bar'}, - ['get','/v1/parent',{},{'field': {}, 'foo':'bar'}]], - - [[['get','/v1/{field.subfield.subsubfield}','']], - {'field': {'subfield': {'subsubfield': 'parent'}}, 'foo':'bar'}, - ['get','/v1/parent',{},{'field': {'subfield': {}}, 'foo':'bar'}]], - - [[['get','/v1/{field.subfield1}/{field.subfield2}','']], - {'field': {'subfield1': 'parent', 'subfield2': 'child'}, 'foo':'bar'}, - ['get','/v1/parent/child',{},{'field': {}, 'foo':'bar'}]], - ] + [ + [["get", "/v1/{field.subfield}", ""]], + {"field": {"subfield": "parent"}, "foo": "bar"}, + ["get", "/v1/parent", {}, {"field": {}, "foo": "bar"}], + ], + [ + [["get", "/v1/{field.subfield.subsubfield}", ""]], + {"field": {"subfield": {"subsubfield": "parent"}}, "foo": "bar"}, + ["get", "/v1/parent", {}, {"field": {"subfield": {}}, "foo": "bar"}], + ], + [ + [["get", "/v1/{field.subfield1}/{field.subfield2}", ""]], + {"field": {"subfield1": "parent", "subfield2": "child"}, "foo": "bar"}, + ["get", "/v1/parent/child", {}, {"field": {}, "foo": "bar"}], + ], + ], ) def test_transcode_subfields(http_options, request_kwargs, expected_result): http_options, expected_result = helper_test_transcode(http_options, expected_result) @@ -216,35 +224,42 @@ def test_transcode_subfields(http_options, request_kwargs, expected_result): @pytest.mark.parametrize( - 'http_options, request_kwargs, expected_result', + "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'}]], - + [ + [["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'}]], - + [ + [["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'}]], - ] + [ + [["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) @@ -253,22 +268,46 @@ def test_transcode_with_wildcard(http_options, request_kwargs, expected_result): @pytest.mark.parametrize( - 'http_options, request_kwargs, expected_result', + "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'}]], - + [ + [["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'},{}]], - ] + [ + [["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) @@ -277,32 +316,53 @@ def test_transcode_with_body(http_options, request_kwargs, expected_result): @pytest.mark.parametrize( - 'http_options, request_kwargs, expected_result', + "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'}]], - ] + [ + [ + ["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): +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', + "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'}], - ] + [[["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)) @@ -313,17 +373,17 @@ def test_transcode_fails(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]} + http_option = {"method": opt_list[0], "uri": opt_list[1]} if opt_list[2]: - http_option['body'] = 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] + "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] + if expected_result_list[2]: + expected_result["body"] = expected_result_list[2] - return(http_options, expected_result) + return (http_options, expected_result)