Skip to content

Commit

Permalink
feat: Add support for reading ClientLibrarySettings from service conf…
Browse files Browse the repository at this point in the history
…iguration YAML
  • Loading branch information
parthea committed Aug 27, 2024
1 parent f773c8a commit ffb23fa
Show file tree
Hide file tree
Showing 40 changed files with 6,662 additions and 3,390 deletions.
4 changes: 3 additions & 1 deletion gapic/generator/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ def _render_template(
# TODO(yon-mg) - remove when rest async implementation resolved
# temporarily stop async client gen while rest async is unkown
('async' in template_name and 'grpc' not in opts.transport)
or
('rest_base' in template_name and 'rest' not in opts.transport)
):
continue

Expand All @@ -319,7 +321,7 @@ def _render_template(

def _is_desired_transport(self, template_name: str, opts: Options) -> bool:
"""Returns true if template name contains a desired transport"""
desired_transports = ['__init__', 'base'] + opts.transport
desired_transports = ['__init__', 'base', 'README'] + opts.transport
return any(transport in template_name for transport in desired_transports)

def _get_file(
Expand Down
65 changes: 65 additions & 0 deletions gapic/schema/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ class MethodSettingsError(ValueError):
pass


class ClientLibrarySettingsError(ValueError):
"""
Raised when `google.api.client_pb2.ClientLibrarySettings` contains
an invalid value.
"""
pass


@dataclasses.dataclass(frozen=True)
class Proto:
"""A representation of a particular proto file within an API."""
Expand Down Expand Up @@ -670,6 +678,63 @@ def enforce_valid_method_settings(
if all_errors:
raise MethodSettingsError(yaml.dump(all_errors))

@cached_property
def all_library_settings(
self,
) -> Mapping[str, Sequence[client_pb2.ClientLibrarySettings]]:
"""Return a map of all `google.api.client.ClientLibrarySettings` to be used
when generating client libraries.
https://github.com/googleapis/googleapis/blob/master/google/api/client.proto#L130
Return:
Mapping[str, Sequence[client_pb2.ClientLibrarySettings]]: A mapping of all library
settings read from the service YAML.
Raises:
gapic.schema.api.ClientLibrarySettingsError: Raised when `google.api.client_pb2.ClientLibrarySettings`
contains an invalid value.
"""
self.enforce_valid_library_settings(
self.service_yaml_config.publishing.library_settings
)

return {
library_setting.version: client_pb2.ClientLibrarySettings(
version=library_setting.version,
python_settings=library_setting.python_settings,
)
for library_setting in self.service_yaml_config.publishing.library_settings
}

def enforce_valid_library_settings(
self, client_library_settings: Sequence[client_pb2.ClientLibrarySettings]
) -> None:
"""
Checks each `google.api.client.ClientLibrarySettings` provided for validity and
raises an exception if invalid values are found.
Args:
client_library_settings (Sequence[client_pb2.ClientLibrarySettings]): Client
library settings to be used when generating API methods.
Return:
None
Raises:
ClientLibrarySettingsError: if fields in `client_library_settings.experimental_features`
are not supported.
"""

all_errors: dict = {}
versions_seen = []
for library_settings in client_library_settings:
# Check if this version is defind more than once
if library_settings.version in versions_seen:
all_errors[library_settings.version] = ["Duplicate version"]
continue
versions_seen.append(library_settings.version)

if all_errors:
raise ClientLibrarySettingsError(yaml.dump(all_errors))

@cached_property
def all_method_settings(self) -> Mapping[str, Sequence[client_pb2.MethodSettings]]:
"""Return a map of all `google.api.client.MethodSettings` to be used
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,94 @@ except ImportError: # pragma: NO COVER
)
{% endif %}{# service_version #}
{% endmacro %}

{% macro operations_mixin_imports(api, service, opts) %}
{% if import_ns is not defined %}
{% set import_ns = namespace(has_operations_mixin=false) %}
{% endif %}{# import_ns is not defined #}
{% set import_ns.has_operations_mixin = api.has_operations_mixin %}

{% filter sort_lines %}
{% for method in service.methods.values() %}
{{method.input.ident.python_import}}
{% if method.output.ident|string() == "operations_pb2.Operation" %}
{% set import_ns.has_operations_mixin = True %}
{% else %}
{{method.output.ident.python_import}}
{% endif %}
{% endfor %}
{% if opts.add_iam_methods %}
from google.iam.v1 import iam_policy_pb2 # type: ignore
from google.iam.v1 import policy_pb2 # type: ignore
{% endif %}{# opts.add_iam_methods #}
{% endfilter %}
{% if import_ns.has_operations_mixin %}
from google.longrunning import operations_pb2 # type: ignore
{% endif %}{# import_ns.has_operations_mixin #}
{% endmacro %}

{% macro http_options_method(rules) %}
@staticmethod
def _get_http_options():
http_options: List[Dict[str, str]] = [
{%- for rule in rules %}{
'method': '{{ rule.method }}',
'uri': '{{ rule.uri }}',
{% if rule.body %}
'body': '{{ rule.body }}',
{% endif %}{# rule.body #}
},
{% endfor %}{# rule in rules #}
]
return http_options
{% endmacro %}

{% macro response_method(body_spec) %}
@staticmethod
def _get_response(
host,
metadata,
query_params,
session,
timeout,
transcoded_request,
body=None):

uri = transcoded_request['uri']
method = transcoded_request['method']
headers = dict(metadata)
headers['Content-Type'] = 'application/json'
response = getattr(session, method)(
"{host}{uri}".format(host=host, uri=uri),
timeout=timeout,
headers=headers,
params=rest_helpers.flatten_query_params(query_params, strict=True),
{% if body_spec %}
data=body,
{% endif %}
)
return response
{% endmacro %}

{% macro rest_call_method_common(body_spec, method_name, service_name) %}

http_options = _Base{{ service_name }}RestTransport._Base{{method_name}}._get_http_options()
request, metadata = self._interceptor.pre_{{ method_name|snake_case }}(request, metadata)
transcoded_request = _Base{{ service_name }}RestTransport._Base{{method_name}}._get_transcoded_request(http_options, request)

{% if body_spec %}
body = _Base{{ service_name }}RestTransport._Base{{method_name}}._get_request_body_json(transcoded_request)
{% endif %} {# body_spec #}

# Jsonify the query params
query_params = _Base{{ service_name }}RestTransport._Base{{method_name}}._get_query_params_json(transcoded_request)

# Send the request
response = {{ service_name }}RestTransport._{{method_name}}._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request{% if body_spec %}, body{% endif %})

# In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception
# subclass.
if response.status_code >= 400:
raise core_exceptions.from_http_response(response)

{% endmacro %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

transport inheritance structure
_______________________________

`{{ service.name }}Transport` is the ABC for all transports.
- public child `{{ service.name }}GrpcTransport` for sync gRPC transport (defined in `grpc.py`).
- public child `{{ service.name }}GrpcAsyncIOTransport` for async gRPC transport (defined in `grpc_asyncio.py`).
- private child `_Base{{ service.name }}RestTransport` for base REST transport with inner classes `_BaseMETHOD` (defined in `rest_base.py`).
- public child `{{ service.name }}RestTransport` for sync REST transport with inner classes `METHOD` derived from the parent's corresponding `_BaseMETHOD` classes (defined in `rest.py`).
{# Since the service mixins have a similar structure, we factor out shared code into `_shared_macros.j2` to avoid duplication. #}
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
{#
# Copyright (C) 2024 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
#
# http://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.
#}

{% import "%namespace/%name_%version/%sub/services/%service/_shared_macros.j2" as shared_macros %}

{% if "grpc" in opts.transport %}

{% if api.has_operations_mixin %}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
{#
# Copyright (C) 2024 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
#
# http://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.
#}

{% import "%namespace/%name_%version/%sub/services/%service/_shared_macros.j2" as shared_macros %}

{% if "rest" in opts.transport %}

{% for name, sig in api.mixin_api_signatures.items() %}
@property
def {{ name|snake_case }}(self):
return self._{{ name }}(self._session, self._host, self._interceptor) # type: ignore

class _{{ name }}({{service.name}}RestStub):
class _{{ name }}(_Base{{ service.name }}RestTransport._Base{{name}}, {{service.name}}RestStub):
{% set body_spec = api.mixin_http_options["{}".format(name)][0].body %}
{{ shared_macros.response_method(body_spec)|indent(8) }}

def __call__(self,
request: {{ sig.request_type }}, *,
retry: OptionalRetry=gapic_v1.method.DEFAULT,
Expand All @@ -32,52 +53,7 @@
{{ sig.response_type }}: Response from {{ name }} method.
{% endif %}
"""

http_options: List[Dict[str, str]] = [
{%- for rule in api.mixin_http_options["{}".format(name)] %}{
'method': '{{ rule.method }}',
'uri': '{{ rule.uri }}',
{% if rule.body %}
'body': '{{ rule.body }}',
{% endif %}{# rule.body #}
},
{% endfor %}
]

request, metadata = self._interceptor.pre_{{ name|snake_case }}(request, metadata)
request_kwargs = json_format.MessageToDict(request)
transcoded_request = path_template.transcode(
http_options, **request_kwargs)

{% set body_spec = api.mixin_http_options["{}".format(name)][0].body %}
{%- if body_spec %}
body = json.dumps(transcoded_request['body'])
{%- endif %}

uri = transcoded_request['uri']
method = transcoded_request['method']

# Jsonify the query params
query_params = json.loads(json.dumps(transcoded_request['query_params']))

# Send the request
headers = dict(metadata)
headers['Content-Type'] = 'application/json'

response = getattr(self._session, method)(
"{host}{uri}".format(host=self._host, uri=uri),
timeout=timeout,
headers=headers,
params=rest_helpers.flatten_query_params(query_params),
{% if body_spec %}
data=body,
{% endif %}
)

# In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception
# subclass.
if response.status_code >= 400:
raise core_exceptions.from_http_response(response)
{{ shared_macros.rest_call_method_common(body_spec, name, service.name)|indent(8) }}

{% if sig.response_type == "None" %}
return self._interceptor.post_{{ name|snake_case }}(None)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{#
# Copyright (C) 2024 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
#
# http://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.
#}

{% import "%namespace/%name_%version/%sub/services/%service/_shared_macros.j2" as shared_macros %}

{% if "rest" in opts.transport %}

{% for name, sig in api.mixin_api_signatures.items() %}
class _Base{{ name }}:

{{ shared_macros.http_options_method(api.mixin_http_options["{}".format(name)])|indent(8)}}

@staticmethod
def _get_transcoded_request(http_options, request):
request_kwargs = json_format.MessageToDict(request)
transcoded_request = path_template.transcode(
http_options, **request_kwargs)
return transcoded_request

{% set body_spec = api.mixin_http_options["{}".format(name)][0].body %}
{%- if body_spec %}

@staticmethod
def _get_request_body_json(transcoded_request):
body = json.dumps(transcoded_request['body'])
return body

{%- endif %} {# body_spec #}

@staticmethod
def _get_query_params_json(transcoded_request):
query_params = json.loads(json.dumps(transcoded_request['query_params']))
return query_params

{% endfor %}
{% endif %} {# rest in opts.transport #}
Loading

0 comments on commit ffb23fa

Please sign in to comment.