From e545b0b4721f5e8f553fa88c6a6c48c60ac78b0d Mon Sep 17 00:00:00 2001 From: Kenneth Bandes Date: Tue, 14 Sep 2021 17:46:39 +0000 Subject: [PATCH 1/8] Support alternative http bindings in the gapic schema. --- gapic/schema/wrappers.py | 89 +++++++++++++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 19 deletions(-) diff --git a/gapic/schema/wrappers.py b/gapic/schema/wrappers.py index 2af844d0a1..5a0951c0ed 100644 --- a/gapic/schema/wrappers.py +++ b/gapic/schema/wrappers.py @@ -843,6 +843,57 @@ def field_headers(self) -> Sequence[str]: return next((tuple(pattern.findall(verb)) for verb in potential_verbs if verb), ()) + def http_rule_to_tuple(self, http_rule: http_pb2.HttpRule) -> Tuple[Optional[str], Optional[str], Optional[str]]: + """Represent salient info in an http rule as a tuple. + + Args: + http_rule: the http option message to examine. + + Returns: + A tuple of (method, uri pattern, body or None), + or None if no method is specified. + """ + + http_dict: Mapping[str, str] + + method = http_rule.WhichOneof('pattern') + if method is None or method == 'custom': + return (None, None, None) + + uri = getattr(http_rule, method) + if not uri: + return (None, None, None) + body = http_rule.body if http_rule.body else None + return (method, uri, body) + + @property + def http_options(self) -> List[Dict[str, str]]: + """Return a list of the http options for this method. + + e.g. [{'method': 'post' + 'uri': '/some/path' + 'body': '*'},] + + """ + http = self.options.Extensions[annotations_pb2.http] + # shallow copy is fine here (elements are not modified) + http_options = list(http.additional_bindings) + # Main pattern comes first + http_options.insert(0, http) + answers : List[Dict[str, str]] = [] + + for http_rule in http_options: + method, uri, body = self.http_rule_to_tuple(http_rule) + if method is None: + continue + answer : Dict[str, str] = {} + answer['method'] = method + answer['uri'] = uri + if body is not None: + answer['body'] = body + answers.append(answer) + return answers + @property def http_options(self) -> List[HttpRule]: """Return a list of the http bindings for this method.""" @@ -860,25 +911,25 @@ def http_opt(self) -> Optional[Dict[str, str]]: 'url': '/some/path' 'body': '*'} - """ - http: List[Tuple[descriptor_pb2.FieldDescriptorProto, str]] - http = self.options.Extensions[annotations_pb2.http].ListFields() - - if len(http) < 1: - return None - - http_method = http[0] - answer: Dict[str, str] = { - 'verb': http_method[0].name, - 'url': http_method[1], - } - if len(http) > 1: - body_spec = http[1] - answer[body_spec[0].name] = body_spec[1] - - # TODO(yon-mg): handle nested fields & fields past body i.e. 'additional bindings' - # TODO(yon-mg): enums for http verbs? - return answer + """ + http: List[Tuple[descriptor_pb2.FieldDescriptorProto, str]] + http = self.options.Extensions[annotations_pb2.http].ListFields() + + if len(http) < 1: + return None + + http_method = http[0] + answer: Dict[str, str] = { + 'verb': http_method[0].name, + 'url': http_method[1], + } + if len(http) > 1: + body_spec = http[1] + answer[body_spec[0].name] = body_spec[1] + + # TODO(yon-mg): handle nested fields & fields past body i.e. 'additional bindings' + # TODO(yon-mg): enums for http verbs? + return answer @property def path_params(self) -> Sequence[str]: From b3d76b0e53740940a303d28bd705e258fff74d36 Mon Sep 17 00:00:00 2001 From: Kenneth Bandes Date: Tue, 14 Sep 2021 19:40:58 +0000 Subject: [PATCH 2/8] Fixed some style and pytype check failures. --- gapic/schema/wrappers.py | 90 +++++++++++------------ tests/unit/schema/wrappers/test_method.py | 35 +++++---- 2 files changed, 62 insertions(+), 63 deletions(-) diff --git a/gapic/schema/wrappers.py b/gapic/schema/wrappers.py index 5a0951c0ed..d0226f8c7f 100644 --- a/gapic/schema/wrappers.py +++ b/gapic/schema/wrappers.py @@ -844,27 +844,27 @@ def field_headers(self) -> Sequence[str]: return next((tuple(pattern.findall(verb)) for verb in potential_verbs if verb), ()) def http_rule_to_tuple(self, http_rule: http_pb2.HttpRule) -> Tuple[Optional[str], Optional[str], Optional[str]]: - """Represent salient info in an http rule as a tuple. + """Represent salient info in an http rule as a tuple. - Args: - http_rule: the http option message to examine. + Args: + http_rule: the http option message to examine. - Returns: - A tuple of (method, uri pattern, body or None), - or None if no method is specified. - """ + Returns: + A tuple of (method, uri pattern, body or None), + or None if no method is specified. + """ - http_dict: Mapping[str, str] + http_dict: Mapping[str, str] - method = http_rule.WhichOneof('pattern') - if method is None or method == 'custom': - return (None, None, None) + method = http_rule.WhichOneof('pattern') + if method is None or method == 'custom': + return (None, None, None) - uri = getattr(http_rule, method) - if not uri: - return (None, None, None) - body = http_rule.body if http_rule.body else None - return (method, uri, body) + uri = getattr(http_rule, method) + if not uri: + return (None, None, None) + body = http_rule.body if http_rule.body else None + return (method, uri, body) @property def http_options(self) -> List[Dict[str, str]]: @@ -880,18 +880,18 @@ def http_options(self) -> List[Dict[str, str]]: http_options = list(http.additional_bindings) # Main pattern comes first http_options.insert(0, http) - answers : List[Dict[str, str]] = [] + answers: List[Dict[str, str]] = [] for http_rule in http_options: - method, uri, body = self.http_rule_to_tuple(http_rule) - if method is None: - continue - answer : Dict[str, str] = {} - answer['method'] = method - answer['uri'] = uri - if body is not None: - answer['body'] = body - answers.append(answer) + method, uri, body = self.http_rule_to_tuple(http_rule) + if method is None or uri is None: + continue + answer: Dict[str, str] = {} + answer['method'] = method + answer['uri'] = uri + if body is not None: + answer['body'] = body + answers.append(answer) return answers @property @@ -911,25 +911,25 @@ def http_opt(self) -> Optional[Dict[str, str]]: 'url': '/some/path' 'body': '*'} - """ - http: List[Tuple[descriptor_pb2.FieldDescriptorProto, str]] - http = self.options.Extensions[annotations_pb2.http].ListFields() - - if len(http) < 1: - return None - - http_method = http[0] - answer: Dict[str, str] = { - 'verb': http_method[0].name, - 'url': http_method[1], - } - if len(http) > 1: - body_spec = http[1] - answer[body_spec[0].name] = body_spec[1] - - # TODO(yon-mg): handle nested fields & fields past body i.e. 'additional bindings' - # TODO(yon-mg): enums for http verbs? - return answer + """ + http: List[Tuple[descriptor_pb2.FieldDescriptorProto, str]] + http = self.options.Extensions[annotations_pb2.http].ListFields() + + if len(http) < 1: + return None + + http_method = http[0] + answer: Dict[str, str] = { + 'verb': http_method[0].name, + 'url': http_method[1], + } + if len(http) > 1: + body_spec = http[1] + answer[body_spec[0].name] = body_spec[1] + + # TODO(yon-mg): handle nested fields & fields past body i.e. 'additional bindings' + # TODO(yon-mg): enums for http verbs? + return answer @property def path_params(self) -> Sequence[str]: diff --git a/tests/unit/schema/wrappers/test_method.py b/tests/unit/schema/wrappers/test_method.py index 00ade8aefb..74a315e096 100644 --- a/tests/unit/schema/wrappers/test_method.py +++ b/tests/unit/schema/wrappers/test_method.py @@ -342,8 +342,7 @@ def test_method_http_options(): method = make_method('DoSomething', http_rule=http_rule) assert [dataclasses.asdict(http) for http in method.http_options] == [{ 'method': v, - 'uri': '/v1/{parent=projects/*}/topics', - 'body': None + 'uri': '/v1/{parent=projects/*}/topics' }] @@ -391,22 +390,22 @@ def test_method_http_options_additional_bindings(): ] ) method = make_method('DoSomething', http_rule=http_rule) - assert [dataclasses.asdict(http) for http in method.http_options] == [ - { - 'method': 'post', - 'uri': '/v1/{parent=projects/*}/topics', - 'body': '*' - }, - { - 'method': 'post', - 'uri': '/v1/{parent=projects/*/regions/*}/topics', - 'body': '*' - }, - { - 'method': 'post', - 'uri': '/v1/projects/p1/topics', - 'body': 'body_field' - }] + assert len(method.http_options) == 3 + assert { + 'method': 'post', + 'uri': '/v1/{parent=projects/*}/topics', + 'body': '*' + } in method.http_options + assert { + 'method': 'post', + 'uri': '/v1/{parent=projects/*/regions/*}/topics', + 'body': '*' + } in method.http_options + assert { + 'method': 'post', + 'uri': '/v1/projects/p1/topics', + 'body': 'body_field' + } in method.http_options def test_method_query_params(): From 9f5d7dbc8fde58eef19a6954a41eb541ed59fe9d Mon Sep 17 00:00:00 2001 From: Kenneth Bandes Date: Tue, 14 Sep 2021 20:37:41 +0000 Subject: [PATCH 3/8] fix: corrected code coverage issue. --- gapic/schema/wrappers.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gapic/schema/wrappers.py b/gapic/schema/wrappers.py index d0226f8c7f..a23d543ddd 100644 --- a/gapic/schema/wrappers.py +++ b/gapic/schema/wrappers.py @@ -843,7 +843,7 @@ def field_headers(self) -> Sequence[str]: return next((tuple(pattern.findall(verb)) for verb in potential_verbs if verb), ()) - def http_rule_to_tuple(self, http_rule: http_pb2.HttpRule) -> Tuple[Optional[str], Optional[str], Optional[str]]: + def http_rule_to_tuple(self, http_rule: http_pb2.HttpRule) -> Tuple[str, str, str]: """Represent salient info in an http rule as a tuple. Args: @@ -858,11 +858,11 @@ def http_rule_to_tuple(self, http_rule: http_pb2.HttpRule) -> Tuple[Optional[str method = http_rule.WhichOneof('pattern') if method is None or method == 'custom': - return (None, None, None) + return ('', '', '') uri = getattr(http_rule, method) if not uri: - return (None, None, None) + return ('', '', '') body = http_rule.body if http_rule.body else None return (method, uri, body) @@ -884,12 +884,12 @@ def http_options(self) -> List[Dict[str, str]]: for http_rule in http_options: method, uri, body = self.http_rule_to_tuple(http_rule) - if method is None or uri is None: + if not method: continue answer: Dict[str, str] = {} answer['method'] = method answer['uri'] = uri - if body is not None: + if body: answer['body'] = body answers.append(answer) return answers From 3dfa56a074227fc835b5363aabc4b771de64eb49 Mon Sep 17 00:00:00 2001 From: Kenneth Bandes Date: Wed, 15 Sep 2021 02:35:43 +0000 Subject: [PATCH 4/8] refactor: made HttpRule a dataclass. --- gapic/schema/wrappers.py | 54 +++-------------------- tests/unit/schema/wrappers/test_method.py | 35 ++++++++------- 2 files changed, 24 insertions(+), 65 deletions(-) diff --git a/gapic/schema/wrappers.py b/gapic/schema/wrappers.py index a23d543ddd..8957e8d85e 100644 --- a/gapic/schema/wrappers.py +++ b/gapic/schema/wrappers.py @@ -843,56 +843,14 @@ def field_headers(self) -> Sequence[str]: return next((tuple(pattern.findall(verb)) for verb in potential_verbs if verb), ()) - def http_rule_to_tuple(self, http_rule: http_pb2.HttpRule) -> Tuple[str, str, str]: - """Represent salient info in an http rule as a tuple. - - Args: - http_rule: the http option message to examine. - - Returns: - A tuple of (method, uri pattern, body or None), - or None if no method is specified. - """ - - http_dict: Mapping[str, str] - - method = http_rule.WhichOneof('pattern') - if method is None or method == 'custom': - return ('', '', '') - - uri = getattr(http_rule, method) - if not uri: - return ('', '', '') - body = http_rule.body if http_rule.body else None - return (method, uri, body) - @property - def http_options(self) -> List[Dict[str, str]]: - """Return a list of the http options for this method. - - e.g. [{'method': 'post' - 'uri': '/some/path' - 'body': '*'},] - - """ + def http_options(self) -> List[HttpRule]: + """Return a list of the http bindings for this method.""" http = self.options.Extensions[annotations_pb2.http] - # shallow copy is fine here (elements are not modified) - http_options = list(http.additional_bindings) - # Main pattern comes first - http_options.insert(0, http) - answers: List[Dict[str, str]] = [] - - for http_rule in http_options: - method, uri, body = self.http_rule_to_tuple(http_rule) - if not method: - continue - answer: Dict[str, str] = {} - answer['method'] = method - answer['uri'] = uri - if body: - answer['body'] = body - answers.append(answer) - return answers + http_options = [http] + list(http.additional_bindings) + opt_gen = (HttpRule.try_parse_http_rule(http_rule) + for http_rule in http_options) + return [rule for rule in opt_gen if rule] @property def http_options(self) -> List[HttpRule]: diff --git a/tests/unit/schema/wrappers/test_method.py b/tests/unit/schema/wrappers/test_method.py index 74a315e096..00ade8aefb 100644 --- a/tests/unit/schema/wrappers/test_method.py +++ b/tests/unit/schema/wrappers/test_method.py @@ -342,7 +342,8 @@ def test_method_http_options(): method = make_method('DoSomething', http_rule=http_rule) assert [dataclasses.asdict(http) for http in method.http_options] == [{ 'method': v, - 'uri': '/v1/{parent=projects/*}/topics' + 'uri': '/v1/{parent=projects/*}/topics', + 'body': None }] @@ -390,22 +391,22 @@ def test_method_http_options_additional_bindings(): ] ) method = make_method('DoSomething', http_rule=http_rule) - assert len(method.http_options) == 3 - assert { - 'method': 'post', - 'uri': '/v1/{parent=projects/*}/topics', - 'body': '*' - } in method.http_options - assert { - 'method': 'post', - 'uri': '/v1/{parent=projects/*/regions/*}/topics', - 'body': '*' - } in method.http_options - assert { - 'method': 'post', - 'uri': '/v1/projects/p1/topics', - 'body': 'body_field' - } in method.http_options + assert [dataclasses.asdict(http) for http in method.http_options] == [ + { + 'method': 'post', + 'uri': '/v1/{parent=projects/*}/topics', + 'body': '*' + }, + { + 'method': 'post', + 'uri': '/v1/{parent=projects/*/regions/*}/topics', + 'body': '*' + }, + { + 'method': 'post', + 'uri': '/v1/projects/p1/topics', + 'body': 'body_field' + }] def test_method_query_params(): From 40a749e7c2ed8587e2565496a0c46f9bdcbaa426 Mon Sep 17 00:00:00 2001 From: Kenneth Bandes Date: Sat, 25 Sep 2021 20:40:00 +0000 Subject: [PATCH 5/8] FIX: Correct errors in generated unit tests for REGAPIC. --- gapic/schema/wrappers.py | 14 + .../services/%service/transports/rest.py.j2 | 205 ++++++----- .../%name_%version/%sub/test_%service.py.j2 | 329 ++++++++++-------- gapic/utils/__init__.py | 2 + gapic/utils/uri_sample.py | 76 ++++ tests/unit/schema/wrappers/test_method.py | 11 + 6 files changed, 414 insertions(+), 223 deletions(-) create mode 100644 gapic/utils/uri_sample.py diff --git a/gapic/schema/wrappers.py b/gapic/schema/wrappers.py index 8957e8d85e..ca19a2c221 100644 --- a/gapic/schema/wrappers.py +++ b/gapic/schema/wrappers.py @@ -29,6 +29,7 @@ import collections import dataclasses +import json import re from itertools import chain from typing import (Any, cast, Dict, FrozenSet, Iterable, List, Mapping, @@ -39,6 +40,7 @@ from google.api import http_pb2 from google.api import resource_pb2 from google.api_core import exceptions # type: ignore +from google.api_core import path_template # type: ignore from google.protobuf import descriptor_pb2 # type: ignore from google.protobuf.json_format import MessageToDict # type: ignore @@ -714,6 +716,18 @@ class HttpRule: uri: str body: Optional[str] + @property + def path_fields(self) -> List[Tuple[str, str]]: + """return list of (name, template) tuples extracted from uri.""" + return [(match.group("name"), match.group("template")) + for match in path_template._VARIABLE_RE.finditer(self.uri)] + + @property + def sample_request(self) -> str: + """return json dict for sample request matching the uri template.""" + sample = utils.sample_from_path_fields(self.path_fields) + return json.dumps(sample) + @classmethod def try_parse_http_rule(cls, http_rule) -> Optional['HttpRule']: method = http_rule.WhichOneof("pattern") diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 index 2308ea8c26..43fbaa31f5 100644 --- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 +++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 @@ -1,29 +1,31 @@ +from google.auth.transport.requests import AuthorizedSession +import json # type: ignore +import grpc # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore +from google.auth import credentials as ga_credentials # type: ignore +from google.api_core import exceptions as core_exceptions # type: ignore +from google.api_core import retry as retries # type: ignore +from google.api_core import rest_helpers # type: ignore +from google.api_core import path_template # type: ignore +from google.api_core import gapic_v1 # type: ignore +from google.api_core import operations_v1 +from requests import __version__ as requests_version +from typing import Callable, Dict, Optional, Sequence, Tuple +import warnings {% extends '_base.py.j2' %} {% block content %} -import warnings -from typing import Callable, Dict, Optional, Sequence, Tuple -from requests import __version__ as requests_version {% if service.has_lro %} -from google.api_core import operations_v1 {% endif %} -from google.api_core import gapic_v1 # type: ignore -from google.api_core import retry as retries # type: ignore -from google.api_core import exceptions as core_exceptions # type: ignore -from google.auth import credentials as ga_credentials # type: ignore -from google.auth.transport.grpc import SslCredentials # type: ignore - -import grpc # type: ignore -from google.auth.transport.requests import AuthorizedSession {# TODO(yon-mg): re-add python_import/ python_modules from removed diff/current grpc template code #} {% filter sort_lines %} {% for method in service.methods.values() %} -{{ method.input.ident.python_import }} -{{ method.output.ident.python_import }} +{{method.input.ident.python_import}} +{{method.output.ident.python_import}} {% endfor %} {% if opts.add_iam_methods %} from google.iam.v1 import iam_policy_pb2 # type: ignore @@ -31,7 +33,7 @@ from google.iam.v1 import policy_pb2 # type: ignore {% endif %} {% endfilter %} -from .base import {{ service.name }}Transport, DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO +from .base import {{service.name}}Transport, DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( @@ -40,7 +42,7 @@ DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( rest_version=requests_version, ) -class {{ service.name }}RestTransport({{ service.name }}Transport): +class {{service.name}}RestTransport({{service.name}}Transport): """REST backend transport for {{ service.name }}. {{ service.meta.doc|rst(width=72, indent=4) }} @@ -54,13 +56,15 @@ class {{ service.name }}RestTransport({{ service.name }}Transport): {# TODO(yon-mg): handle mtls stuff if that's relevant for rest transport #} def __init__(self, *, host: str{% if service.host %} = '{{ service.host }}'{% endif %}, - credentials: ga_credentials.Credentials = None, - credentials_file: str = None, - scopes: Sequence[str] = None, - client_cert_source_for_mtls: Callable[[], Tuple[bytes, bytes]] = None, - quota_project_id: Optional[str] = None, - client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, - always_use_jwt_access: Optional[bool] = False, + credentials: ga_credentials.Credentials=None, + credentials_file: str=None, + scopes: Sequence[str]=None, + client_cert_source_for_mtls: Callable[[ + ], Tuple[bytes, bytes]]=None, + quota_project_id: Optional[str]=None, + client_info: gapic_v1.client_info.ClientInfo=DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool]=False, + url_scheme: str='https', ) -> None: """Instantiate the transport. @@ -88,6 +92,11 @@ class {{ service.name }}RestTransport({{ service.name }}Transport): API requests. If ``None``, then default info will be used. Generally, you only need to set this if you're developing your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + url_scheme: the protocol scheme for the API endpoint. Normally + "https", but for testing or local servers, + "http" can be specified. """ # Run the base constructor # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. @@ -99,7 +108,8 @@ class {{ service.name }}RestTransport({{ service.name }}Transport): client_info=client_info, always_use_jwt_access=always_use_jwt_access, ) - self._session = AuthorizedSession(self._credentials, default_host=self.DEFAULT_HOST) + self._session = AuthorizedSession( + self._credentials, default_host=self.DEFAULT_HOST) {% if service.has_lro %} self._operations_client = None {% endif %} @@ -109,7 +119,7 @@ class {{ service.name }}RestTransport({{ service.name }}Transport): {% if service.has_lro %} - @property + @ property def operations_client(self) -> operations_v1.OperationsClient: """Create the client designed to process long-running operations. @@ -136,16 +146,17 @@ class {{ service.name }}RestTransport({{ service.name }}Transport): # Return the client from cache. return self._operations_client + + {% endif %} {% for method in service.methods.values() %} - {% if method.http_opt %} - - def {{ method.name|snake_case }}(self, - request: {{ method.input.ident }}, *, - retry: retries.Retry = gapic_v1.method.DEFAULT, - timeout: float = None, - metadata: Sequence[Tuple[str, str]] = (), - ) -> {{ method.output.ident }}: + {%- if method.http_options and not method.lro and not (method.server_streaming or method.client_streaming) %} + def _{{method.name | snake_case}}(self, + request: {{method.input.ident}}, *, + retry: retries.Retry=gapic_v1.method.DEFAULT, + timeout: float=None, + metadata: Sequence[Tuple[str, str]]=(), + ) -> {{method.output.ident}}: r"""Call the {{- ' ' -}} {{ (method.name|snake_case).replace('_',' ')|wrap( width=70, offset=45, indent=8) }} @@ -168,62 +179,58 @@ class {{ service.name }}RestTransport({{ service.name }}Transport): {% endif %} """ - {# 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 %} + http_options = [ + {%- for rule in method.http_options %}{ + 'method': '{{ rule.method }}', + 'uri': '{{ rule.uri }}', + {%- if rule.body %} + + 'body': '{{ rule.body }}', + {%- endif %} + }, + {%- endfor %}] + + request_kwargs = {{method.input.ident}}.to_dict(request) + transcoded_request = path_template.transcode( + http_options, **request_kwargs) + + {% set body_spec = method.http_options[0].body %} + {%- if body_spec %} + # 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'] }}, + body = {% if body_spec == '*' -%} + {{method.input.ident}}.to_json( + {{method.input.ident}}(transcoded_request['body']), + {%- else -%} + {{method.input.fields[body_spec].type.ident}}.to_json( + {{method.input.fields[body_spec].type.ident}}( + transcoded_request['body']), + {%- endif %} + including_default_value_fields=False, use_integers_for_enums=False ) - {% else %} - body = {{ method.input.ident }}.to_json( - request, - use_integers_for_enums=False - ) - {% endif %} - {% endif %} + {%- endif %}{# body_spec #} - {# 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 basic case of grpc transcoding - url = 'https://{host}{{ method.http_opt['url'] }}'.format( - host=self._host, - {% for field in method.path_params %} - {{ field }}=request.{{ method.input.get_field(field).name }}, - {% endfor %} - ) + uri=transcoded_request['uri'] + method=transcoded_request['method'] - {# 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 correctly rather than using only top level fields - # not required for GCE - query_params = {} - {% for field in method.query_params | sort%} - {% if method.input.fields[field].proto3_optional %} - if {{ method.input.ident }}.{{ field }} in request: - query_params['{{ field|camel_case }}'] = request.{{ field }} - {% else %} - query_params['{{ field|camel_case }}'] = request.{{ field }} - {% endif %} - {% endfor %} + # Jsonify the query params + query_params=json.loads({{method.input.ident}}.to_json( + {{method.input.ident}}(transcoded_request['query_params']), + including_default_value_fields=False, + use_integers_for_enums=False + )) # Send the request - headers = dict(metadata) - headers['Content-Type'] = 'application/json' - response = self._session.{{ method.http_opt['verb'] }}( - url, + headers=dict(metadata) + headers['Content-Type']='application/json' + response=getattr(self._session, method)( + uri, timeout=timeout, headers=headers, - params=query_params, - {% if 'body' in method.http_opt %} + params=rest_helpers.flatten_query_params(query_params), + {% if body_spec %} data=body, {% endif %} ) @@ -235,16 +242,50 @@ class {{ service.name }}RestTransport({{ service.name }}Transport): {% if not method.void %} # Return the response - return {{ method.output.ident }}.from_json( + return {{method.output.ident}}.from_json( response.content, ignore_unknown_fields=True ) {% endif %} - {% endif %} + {% else %} + + def _{{method.name | snake_case}}(self, + request: {{method.input.ident}}, *, + metadata: Sequence[Tuple[str, str]]=(), + ) -> {{method.output.ident}}: + r"""Placeholder: Unable to implement over REST + """ + {%- if not method.http_options %} + + raise RuntimeError( + "Cannot define a method without a valid 'google.api.http' annotation.") + {%- elif method.lro %} + + raise NotImplementedError( + "LRO over REST is not yet defined for python client.") + {%- elif method.server_streaming or method.client_streaming %} + + raise NotImplementedError( + "Streaming over REST is not yet defined for python client") + {%- else %} + + raise NotImplementedError() + {%- endif %} + {%- endif %} + + {% endfor %} + {%- for method in service.methods.values() %} + + @ property + def {{method.name | snake_case}}(self) -> Callable[ + [{{method.input.ident}}], + {{method.output.ident}}]: + return self._{{method.name | snake_case}} + {%- endfor %} -__all__ = ( +__all__=( '{{ service.name }}RestTransport', ) {% endblock %} diff --git a/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index 61618036d4..098a3b94cc 100644 --- a/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -33,6 +33,7 @@ from google.api_core import client_options from google.api_core import exceptions as core_exceptions from google.api_core import grpc_helpers from google.api_core import grpc_helpers_async +from google.api_core import path_template {% if service.has_lro %} from google.api_core import future from google.api_core import operations_v1 @@ -977,6 +978,7 @@ def test_{{ method.name|snake_case }}_pager(): )), ) {% endif %} + pager = client.{{ method.name|snake_case }}(request={}) assert pager._metadata == metadata @@ -1126,16 +1128,18 @@ def test_{{ method.name|snake_case }}_raw_page_lro(): {% endfor %} {# method in methods for grpc #} -{% for method in service.methods.values() if 'rest' in opts.transport %} +{% for method in service.methods.values() if 'rest' in opts.transport and + method.http_options %} +{# TODO(kbandes): remove this if condition when lro and streaming are supported. #} +{% if not method.lro and not (method.server_streaming or method.client_streaming) %} def test_{{ method.name|snake_case }}_rest(transport: str = 'rest', request_type={{ method.input.ident }}): client = {{ service.client_name }}( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) - # Everything is optional in proto3 as far as the runtime is concerned, - # and we are mocking out the actual API, so just send an empty request. - request = request_type() + # send a request that will satisfy transcoding + request = request_type({{ method.http_options[0].sample_request}}) {% if method.client_streaming %} requests = [request] {% endif %} @@ -1151,16 +1155,27 @@ def test_{{ method.name|snake_case }}_rest(transport: str = 'rest', request_type return_value = iter([{{ method.output.ident }}()]) {% else %} return_value = {{ method.output.ident }}( - {% for field in method.output.fields.values() %} - {{ field.name }}={{ field.mock_value }}, - {% endfor %} + {% for field in method.output.fields.values() | rejectattr('message')%} + {% if not field.oneof or field.proto3_optional %} + {{ field.name }}={{ field.mock_value }}, + {% endif %}{% endfor %} + {# This is a hack to only pick one field #} + {% for oneof_fields in method.output.oneof_fields().values() %} + {% with field = oneof_fields[0] %} + {{ field.name }}={{ field.mock_value }}, + {% endwith %} + {% endfor %} ) {% endif %} # Wrap the value into a proper Response obj - json_return_value = {{ method.output.ident }}.to_json(return_value) response_value = Response() response_value.status_code = 200 + {% if method.void %} + json_return_value = '' + {% else %} + json_return_value = {{ method.output.ident }}.to_json(return_value) + {% endif %} response_value._content = json_return_value.encode('UTF-8') req.return_value = response_value {% if method.client_streaming %} @@ -1179,7 +1194,8 @@ def test_{{ method.name|snake_case }}_rest(transport: str = 'rest', request_type assert response is None {% else %} assert isinstance(response, {{ method.client_output.ident }}) - {% for field in method.output.fields.values() %} + {% for field in method.output.fields.values() | rejectattr('message') %} + {% if not field.oneof or field.proto3_optional %} {% if field.field_pb.type in [1, 2] %}{# Use approx eq for floats #} assert math.isclose(response.{{ field.name }}, {{ field.mock_value }}, rel_tol=1e-6) {% elif field.field_pb.type == 8 %}{# Use 'is' for bools #} @@ -1187,6 +1203,7 @@ def test_{{ method.name|snake_case }}_rest(transport: str = 'rest', request_type {% else %} assert response.{{ field.name }} == {{ field.mock_value }} {% endif %} + {% endif %}{# end oneof/optional #} {% endfor %} {% endif %} @@ -1195,6 +1212,7 @@ def test_{{ method.name|snake_case }}_rest_from_dict(): test_{{ method.name|snake_case }}_rest(request_type=dict) +{% if method.flattened_fields %} def test_{{ method.name|snake_case }}_rest_flattened(): client = {{ service.client_name }}( credentials=ga_credentials.AnonymousCredentials(), @@ -1214,46 +1232,40 @@ def test_{{ method.name|snake_case }}_rest_flattened(): {% endif %} # Wrap the value into a proper Response obj - json_return_value = {{ method.output.ident }}.to_json(return_value) response_value = Response() response_value.status_code = 200 + {% if method.void %} + json_return_value = '' + {% else %} + json_return_value = {{ method.output.ident }}.to_json(return_value) + {% endif %} + response_value._content = json_return_value.encode('UTF-8') req.return_value = response_value - # Call the method with a truthy value for each flattened field, - # using the keyword arguments to the method. - {% for field in method.flattened_fields.values() if field.field_pb is msg_field_pb %} - {{ field.name }} = {{ field.mock_value }} - {% endfor %} - client.{{ method.name|snake_case }}( + # get arguments that satisfy an http rule for this method + sample_request = {{ method.http_options[0].sample_request }} + + # get truthy value for each flattened field + mock_args = dict( {% for field in method.flattened_fields.values() %} - {% if field.field_pb is msg_field_pb %} - {{ field.name }}={{ field.name }}, - {% else %} + {% if not field.oneof or field.proto3_optional %} + {# ignore oneof fields that might conflict with sample_request #} {{ field.name }}={{ field.mock_value }}, {% endif %} {% endfor %} ) + mock_args.update(sample_request) + client.{{ method.name|snake_case }}(**mock_args) # Establish that the underlying call was made with the expected # request object values. assert len(req.mock_calls) == 1 - _, http_call, http_params = req.mock_calls[0] - body = http_params.get('data') - params = http_params.get('params') - {% for key, field in method.flattened_fields.items() %} - {% if not field.oneof or field.proto3_optional %} - {% if field.ident|string() == 'timestamp_pb2.Timestamp' %} - assert TimestampRule().to_proto(http_call[0].{{ key }}) == {{ field.mock_value }} - {% elif field.ident|string() == 'duration_pb2.Duration' %} - assert DurationRule().to_proto(http_call[0].{{ key }}) == {{ field.mock_value }} - {% else %} - assert {% if field.field_pb is msg_field_pb %}{{ field.ident }}.to_json({{ field.name }}, including_default_value_fields=False, use_integers_for_enums=False) - {%- elif field.field_pb is str_field_pb %}{{ field.mock_value }} - {%- else %}str({{ field.mock_value }}) - {%- endif %} in http_call[1] + str(body) + str(params) - {% endif %} - {% endif %}{% endfor %} + _, args, _ = req.mock_calls[0] + {% with uri = method.http_options[0].uri %} + assert path_template.validate("{{ uri }}", args[1]) + {% endwith %} + {# TODO(kbandes) - reverse-transcode request args to check all request fields #} def test_{{ method.name|snake_case }}_rest_flattened_error(): @@ -1270,128 +1282,143 @@ def test_{{ method.name|snake_case }}_rest_flattened_error(): {{ field.name }}={{ field.mock_value }}, {% endfor %} ) +{% endif %}{# flattened fields #} {% if method.paged_result_field %} -def test_{{ method.name|snake_case }}_pager(): +def test_{{ method.name|snake_case }}_rest_pager(): client = {{ service.client_name }}( credentials=ga_credentials.AnonymousCredentials(), ) # Mock the http request call within the method and fake a response. with mock.patch.object(Session, 'request') as req: - # Set the response as a series of pages - {% if method.paged_result_field.map%} - response = ( - {{ method.output.ident }}( - {{ method.paged_result_field.name }}={ - 'a':{{ method.paged_result_field.type.fields.get('value').ident }}(), - 'b':{{ method.paged_result_field.type.fields.get('value').ident }}(), - 'c':{{ method.paged_result_field.type.fields.get('value').ident }}(), - }, - next_page_token='abc', - ), - {{ method.output.ident }}( - {{ method.paged_result_field.name }}={}, - next_page_token='def', - ), - {{ method.output.ident }}( - {{ method.paged_result_field.name }}={ - 'g':{{ method.paged_result_field.type.fields.get('value').ident }}(), - }, - next_page_token='ghi', - ), - {{ method.output.ident }}( - {{ method.paged_result_field.name }}={ - 'h':{{ method.paged_result_field.type.fields.get('value').ident }}(), - 'i':{{ method.paged_result_field.type.fields.get('value').ident }}(), - }, - ), - ) - {% else %} - response = ( - {{ method.output.ident }}( - {{ method.paged_result_field.name }}=[ - {{ method.paged_result_field.type.ident }}(), - {{ method.paged_result_field.type.ident }}(), - {{ method.paged_result_field.type.ident }}(), - ], - next_page_token='abc', - ), - {{ method.output.ident }}( - {{ method.paged_result_field.name }}=[], - next_page_token='def', - ), - {{ method.output.ident }}( - {{ method.paged_result_field.name }}=[ - {{ method.paged_result_field.type.ident }}(), - ], - next_page_token='ghi', - ), - {{ method.output.ident }}( - {{ method.paged_result_field.name }}=[ - {{ method.paged_result_field.type.ident }}(), - {{ method.paged_result_field.type.ident }}(), - ], - ), - ) - {% endif %} - # Two responses for two calls - response = response + response + # TODO(kbandes): remove this mock unless there's a good reason for it. + #with mock.patch.object(path_template, 'transcode') as transcode: + # Set the response as a series of pages + {% if method.paged_result_field.map%} + response = ( + {{ method.output.ident }}( + {{ method.paged_result_field.name }}={ + 'a':{{ method.paged_result_field.type.fields.get('value').ident }}(), + 'b':{{ method.paged_result_field.type.fields.get('value').ident }}(), + 'c':{{ method.paged_result_field.type.fields.get('value').ident }}(), + }, + next_page_token='abc', + ), + {{ method.output.ident }}( + {{ method.paged_result_field.name }}={}, + next_page_token='def', + ), + {{ method.output.ident }}( + {{ method.paged_result_field.name }}={ + 'g':{{ method.paged_result_field.type.fields.get('value').ident }}(), + }, + next_page_token='ghi', + ), + {{ method.output.ident }}( + {{ method.paged_result_field.name }}={ + 'h':{{ method.paged_result_field.type.fields.get('value').ident }}(), + 'i':{{ method.paged_result_field.type.fields.get('value').ident }}(), + }, + ), + ) + {% else %} + response = ( + {{ method.output.ident }}( + {{ method.paged_result_field.name }}=[ + {{ method.paged_result_field.type.ident }}(), + {{ method.paged_result_field.type.ident }}(), + {{ method.paged_result_field.type.ident }}(), + ], + next_page_token='abc', + ), + {{ method.output.ident }}( + {{ method.paged_result_field.name }}=[], + next_page_token='def', + ), + {{ method.output.ident }}( + {{ method.paged_result_field.name }}=[ + {{ method.paged_result_field.type.ident }}(), + ], + next_page_token='ghi', + ), + {{ method.output.ident }}( + {{ method.paged_result_field.name }}=[ + {{ method.paged_result_field.type.ident }}(), + {{ method.paged_result_field.type.ident }}(), + ], + ), + ) + {% endif %} + # Two responses for two calls + response = response + response + + # Wrap the values into proper Response objs + response = tuple({{ method.output.ident }}.to_json(x) for x in response) + return_values = tuple(Response() for i in response) + for return_val, response_val in zip(return_values, response): + return_val._content = response_val.encode('UTF-8') + return_val.status_code = 200 + req.side_effect = return_values + + sample_request = {{ method.http_options[0].sample_request }} + pager = client.{{ method.name|snake_case }}(request=sample_request) + + {% if method.paged_result_field.map %} + assert isinstance(pager.get('a'), {{ method.paged_result_field.type.fields.get('value').ident }}) + assert pager.get('h') is None + {% endif %} - # Wrap the values into proper Response objs - response = tuple({{ method.output.ident }}.to_json(x) for x in response) - return_values = tuple(Response() for i in response) - for return_val, response_val in zip(return_values, response): - return_val._content = response_val.encode('UTF-8') - return_val.status_code = 200 - req.side_effect = return_values + results = list(pager) + assert len(results) == 6 + {% if method.paged_result_field.map %} + assert all( + isinstance(i, tuple) + for i in results) + for result in results: + assert isinstance(result, tuple) + assert tuple(type(t) for t in result) == (str, {{ method.paged_result_field.type.fields.get('value').ident }}) + + assert pager.get('a') is None + assert isinstance(pager.get('h'), {{ method.paged_result_field.type.fields.get('value').ident }}) + {% else %} + assert all(isinstance(i, {{ method.paged_result_field.type.ident }}) + for i in results) + {% endif %} - metadata = () - {% if method.field_headers %} - metadata = tuple(metadata) + ( - gapic_v1.routing_header.to_grpc_metadata(( - {% for field_header in method.field_headers %} - {% if not method.client_streaming %} - ('{{ field_header }}', ''), - {% endif %} - {% endfor %} - )), - ) - {% endif %} - pager = client.{{ method.name|snake_case }}(request={}) + pages = list(client.{{ method.name|snake_case }}(request=sample_request).pages) + for page_, token in zip(pages, ['abc','def','ghi', '']): + assert page_.raw_page.next_page_token == token - assert pager._metadata == metadata - {% if method.paged_result_field.map %} - assert isinstance(pager.get('a'), {{ method.paged_result_field.type.fields.get('value').ident }}) - assert pager.get('h') is None - {% endif %} +{% endif %} {# paged methods #} +{%- else %} - results = list(pager) - assert len(results) == 6 - {% if method.paged_result_field.map %} - assert all( - isinstance(i, tuple) - for i in results) - for result in results: - assert isinstance(result, tuple) - assert tuple(type(t) for t in result) == (str, {{ method.paged_result_field.type.fields.get('value').ident }}) - - assert pager.get('a') is None - assert isinstance(pager.get('h'), {{ method.paged_result_field.type.fields.get('value').ident }}) - {% else %} - assert all(isinstance(i, {{ method.paged_result_field.type.ident }}) - for i in results) - {% endif %} +def test_{{ method.name|snake_case }}_rest_error(): + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + transport='rest' + ) + {%- if not method.http_options %} + # Since a `google.api.http` annotation is required for using a rest transport + # method, this should error. + with pytest.raises(RuntimeError) as runtime_error: + client.{{ method.name|snake_case }}({}) + assert ('Cannot define a method without a valid `google.api.http` annotation.' + in str(runtime_error.value)) + {%- else %} + + # TODO(yon-mg): Remove when this method has a working implementation + # or testing straegy + with pytest.raises(NotImplementedError): + client.{{ method.name|snake_case }}({}) - pages = list(client.{{ method.name|snake_case }}(request={}).pages) - for page_, token in zip(pages, ['abc','def','ghi', '']): - assert page_.raw_page.next_page_token == token + {%- endif %} +{% endif %} -{% endif %}{# paged methods #} -{% endfor %}{# method in methods for rest #} +{% endfor -%} {#- method in methods for rest #} def test_credentials_transport_error(): # It is an error to provide credentials and a transport instance. transport = transports.{{ service.name }}{{ opts.transport[0].capitalize() }}Transport( @@ -1736,7 +1763,26 @@ def test_{{ service.name|snake_case }}_http_transport_client_cert_source_for_mtl client_cert_source_for_mtls=client_cert_source_callback ) mock_configure_mtls_channel.assert_called_once_with(client_cert_source_callback) -{% endif %} + + +{# TODO(kbandes): re-enable this code when LRO is implmented for rest #} +{% if False and service.has_lro -%} +def test_{{ service.name|snake_case }}_rest_lro_client(): + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + transport='rest', + ) + transport = client.transport + + # Ensure that we have a api-core operations client. + assert isinstance( + transport.operations_client, + operations_v1.OperationsClient, + ) + + # Ensure that subsequent calls to the property send the exact same object. + assert transport.operations_client is transport.operations_client +{%- endif %} def test_{{ service.name|snake_case }}_host_no_port(): {% with host = (service.host|default('localhost', true)).split(':')[0] %} @@ -1756,6 +1802,7 @@ def test_{{ service.name|snake_case }}_host_with_port(): ) assert client.transport._host == '{{ host }}:8000' {% endwith %} +{% endif %} {# rest #} {% if 'grpc' in opts.transport %} def test_{{ service.name|snake_case }}_grpc_transport_channel(): diff --git a/gapic/utils/__init__.py b/gapic/utils/__init__.py index 98d31c283f..447b0df837 100644 --- a/gapic/utils/__init__.py +++ b/gapic/utils/__init__.py @@ -28,6 +28,7 @@ from gapic.utils.options import Options from gapic.utils.reserved_names import RESERVED_NAMES from gapic.utils.rst import rst +from gapic.utils.uri_sample import sample_from_path_fields __all__ = ( @@ -41,6 +42,7 @@ 'partition', 'RESERVED_NAMES', 'rst', + 'sample_from_path_fields', 'sort_lines', 'to_snake_case', 'to_camel_case', diff --git a/gapic/utils/uri_sample.py b/gapic/utils/uri_sample.py new file mode 100644 index 0000000000..5560e6481e --- /dev/null +++ b/gapic/utils/uri_sample.py @@ -0,0 +1,76 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, Iterable, Dict, List, Tuple +import re + + +def _sample_names() -> Iterable[str]: + sample_num: int = 0 + while True: + sample_num += 1 + yield "sample{}".format(sample_num) + + +def add_field(obj, path, value): + """Insert a field into a nested dict and return the (outer) dict. + Keys and sub-dicts are inserted if necessary to create the path. + e.g. if obj, as passed in, is {}, path is "a.b.c", and value is + "hello", obj will be updated to: + {'a': + {'b': + { + 'c': 'hello' + } + } + } + + Args: + obj: a (possibly) nested dict (parsed json) + path: a segmented field name, e.g. "a.b.c" + where each part is a dict key. + value: the value of the new key. + Returns: + obj, possibly modified + Raises: + AttributeError if the path references a key that is + not a dict.: e.g. path='a.b', obj = {'a':'abc'} + """ + segments = path.split('.') + leaf = segments.pop() + subfield = obj + for segment in segments: + subfield = subfield.setdefault(segment, {}) + subfield[leaf] = value + return obj + + +def sample_from_path_fields(paths: List[Tuple[str, str]]) -> Dict[Any, Any]: + """Construct a dict for a sample request object from a list of fields + and template patterns. + + Args: + paths: a list of tuples, each with a (segmented) name and a pattern. + Returns: + A new nested dict with the templates instantiated. + """ + + request = {} + sample_names = _sample_names() + + for path, template in paths: + sample_value = re.sub( + r"(\*\*|\*)", lambda n: next(sample_names), template) + add_field(request, path, sample_value) + return request diff --git a/tests/unit/schema/wrappers/test_method.py b/tests/unit/schema/wrappers/test_method.py index 00ade8aefb..c6a81d9128 100644 --- a/tests/unit/schema/wrappers/test_method.py +++ b/tests/unit/schema/wrappers/test_method.py @@ -14,6 +14,7 @@ import collections import dataclasses +import json from typing import Sequence from google.api import field_behavior_pb2 @@ -409,6 +410,16 @@ def test_method_http_options_additional_bindings(): }] +def test_method_http_options_generate_sample(): + http_rule = http_pb2.HttpRule( + get='/v1/{resource.id=projects/*/regions/*/id/**}/stuff', + ) + method = make_method('DoSomething', http_rule=http_rule) + sample = method.http_options[0].sample_request + assert json.loads(sample) == {'resource': { + 'id': 'projects/sample1/regions/sample2/id/sample3'}} + + def test_method_query_params(): # tests only the basic case of grpc transcoding http_rule = http_pb2.HttpRule( From a61a43e5028fee635fe627d782f4a5e9b7b4c0c4 Mon Sep 17 00:00:00 2001 From: Kenneth Bandes Date: Mon, 27 Sep 2021 16:45:03 +0000 Subject: [PATCH 6/8] fix: corrected mypy and coverage errors. --- gapic/schema/wrappers.py | 9 --------- gapic/utils/uri_sample.py | 6 +++--- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/gapic/schema/wrappers.py b/gapic/schema/wrappers.py index ca19a2c221..b9a3876133 100644 --- a/gapic/schema/wrappers.py +++ b/gapic/schema/wrappers.py @@ -866,15 +866,6 @@ def http_options(self) -> List[HttpRule]: for http_rule in http_options) return [rule for rule in opt_gen if rule] - @property - def http_options(self) -> List[HttpRule]: - """Return a list of the http bindings for this method.""" - http = self.options.Extensions[annotations_pb2.http] - http_options = [http] + list(http.additional_bindings) - opt_gen = (HttpRule.try_parse_http_rule(http_rule) - for http_rule in http_options) - return [rule for rule in opt_gen if rule] - @property def http_opt(self) -> Optional[Dict[str, str]]: """Return the (main) http option for this method. diff --git a/gapic/utils/uri_sample.py b/gapic/utils/uri_sample.py index 5560e6481e..43b8865abf 100644 --- a/gapic/utils/uri_sample.py +++ b/gapic/utils/uri_sample.py @@ -12,11 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any, Iterable, Dict, List, Tuple +from typing import Any, Generator, Dict, List, Tuple import re -def _sample_names() -> Iterable[str]: +def _sample_names() -> Generator[str, None, None]: sample_num: int = 0 while True: sample_num += 1 @@ -66,7 +66,7 @@ def sample_from_path_fields(paths: List[Tuple[str, str]]) -> Dict[Any, Any]: A new nested dict with the templates instantiated. """ - request = {} + request: Dict[str, Any] = {} sample_names = _sample_names() for path, template in paths: From 74668a990bfd5b16ab539ab5581882ea8ae2376f Mon Sep 17 00:00:00 2001 From: Kenneth Bandes Date: Mon, 27 Sep 2021 19:36:43 +0000 Subject: [PATCH 7/8] fix: fixed failing integration tests. --- .../tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 | 3 +-- .../asset/tests/unit/gapic/asset_v1/test_asset_service.py | 1 + .../tests/unit/gapic/credentials_v1/test_iam_credentials.py | 1 + .../tests/unit/gapic/logging_v2/test_config_service_v2.py | 1 + .../tests/unit/gapic/logging_v2/test_logging_service_v2.py | 1 + .../tests/unit/gapic/logging_v2/test_metrics_service_v2.py | 1 + .../redis/tests/unit/gapic/redis_v1/test_cloud_redis.py | 1 + 7 files changed, 7 insertions(+), 2 deletions(-) diff --git a/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index 098a3b94cc..d91f0ba134 100644 --- a/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -978,7 +978,6 @@ def test_{{ method.name|snake_case }}_pager(): )), ) {% endif %} - pager = client.{{ method.name|snake_case }}(request={}) assert pager._metadata == metadata @@ -1783,6 +1782,7 @@ def test_{{ service.name|snake_case }}_rest_lro_client(): # Ensure that subsequent calls to the property send the exact same object. assert transport.operations_client is transport.operations_client {%- endif %} +{% endif %} {# rest #} def test_{{ service.name|snake_case }}_host_no_port(): {% with host = (service.host|default('localhost', true)).split(':')[0] %} @@ -1802,7 +1802,6 @@ def test_{{ service.name|snake_case }}_host_with_port(): ) assert client.transport._host == '{{ host }}:8000' {% endwith %} -{% endif %} {# rest #} {% if 'grpc' in opts.transport %} def test_{{ service.name|snake_case }}_grpc_transport_channel(): diff --git a/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py b/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py index 87c110ebcb..21d14df35c 100644 --- a/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py +++ b/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py @@ -32,6 +32,7 @@ from google.api_core import grpc_helpers_async from google.api_core import operation_async # type: ignore from google.api_core import operations_v1 +from google.api_core import path_template from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError from google.cloud.asset_v1.services.asset_service import AssetServiceAsyncClient diff --git a/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py b/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py index 74dacb0923..6945e557a0 100644 --- a/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py +++ b/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py @@ -29,6 +29,7 @@ from google.api_core import gapic_v1 from google.api_core import grpc_helpers from google.api_core import grpc_helpers_async +from google.api_core import path_template from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError from google.iam.credentials_v1.services.iam_credentials import IAMCredentialsAsyncClient diff --git a/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py b/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py index 979cbd3605..288323a4bc 100644 --- a/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -29,6 +29,7 @@ from google.api_core import gapic_v1 from google.api_core import grpc_helpers from google.api_core import grpc_helpers_async +from google.api_core import path_template from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError from google.cloud.logging_v2.services.config_service_v2 import ConfigServiceV2AsyncClient diff --git a/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py index b952814609..31f15bca04 100644 --- a/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -30,6 +30,7 @@ from google.api_core import gapic_v1 from google.api_core import grpc_helpers from google.api_core import grpc_helpers_async +from google.api_core import path_template from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError from google.cloud.logging_v2.services.logging_service_v2 import LoggingServiceV2AsyncClient diff --git a/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index 5ce85b4284..e3eb2f3aca 100644 --- a/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -33,6 +33,7 @@ from google.api_core import gapic_v1 from google.api_core import grpc_helpers from google.api_core import grpc_helpers_async +from google.api_core import path_template from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError from google.cloud.logging_v2.services.metrics_service_v2 import MetricsServiceV2AsyncClient diff --git a/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py b/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py index a1c9cc2e64..1b71940694 100644 --- a/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -32,6 +32,7 @@ from google.api_core import grpc_helpers_async from google.api_core import operation_async # type: ignore from google.api_core import operations_v1 +from google.api_core import path_template from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError from google.cloud.redis_v1.services.cloud_redis import CloudRedisAsyncClient From 8024b387a69c2cbcfc4cac1453343043bfe4fb74 Mon Sep 17 00:00:00 2001 From: Kenneth Bandes Date: Wed, 29 Sep 2021 01:15:04 +0000 Subject: [PATCH 8/8] fix: addressed review comments (formatting) --- .../%sub/services/%service/transports/rest.py.j2 | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 index 43fbaa31f5..87eef7d497 100644 --- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 +++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 @@ -119,7 +119,7 @@ class {{service.name}}RestTransport({{service.name}}Transport): {% if service.has_lro %} - @ property + @property def operations_client(self) -> operations_v1.OperationsClient: """Create the client designed to process long-running operations. @@ -184,7 +184,6 @@ class {{service.name}}RestTransport({{service.name}}Transport): 'method': '{{ rule.method }}', 'uri': '{{ rule.uri }}', {%- if rule.body %} - 'body': '{{ rule.body }}', {%- endif %} }, @@ -212,19 +211,19 @@ class {{service.name}}RestTransport({{service.name}}Transport): ) {%- endif %}{# body_spec #} - uri=transcoded_request['uri'] - method=transcoded_request['method'] + uri = transcoded_request['uri'] + method = transcoded_request['method'] # Jsonify the query params - query_params=json.loads({{method.input.ident}}.to_json( + query_params = json.loads({{method.input.ident}}.to_json( {{method.input.ident}}(transcoded_request['query_params']), including_default_value_fields=False, use_integers_for_enums=False )) # Send the request - headers=dict(metadata) - headers['Content-Type']='application/json' + headers = dict(metadata) + headers['Content-Type'] = 'application/json' response=getattr(self._session, method)( uri, timeout=timeout,