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 (#2098)
  • Loading branch information
parthea authored Aug 29, 2024
1 parent c440807 commit 11e3967
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 21 deletions.
77 changes: 71 additions & 6 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 @@ -574,7 +582,7 @@ def mixin_http_options(self):
def all_methods(self) -> Mapping[str, MethodDescriptorProto]:
"""Return a map of all methods for the API.
Return:
Returns:
Mapping[str, MethodDescriptorProto]: A mapping of MethodDescriptorProto
values for the API.
"""
Expand Down Expand Up @@ -607,21 +615,21 @@ def enforce_valid_method_settings(
Args:
service_method_settings (Sequence[client_pb2.MethodSettings]): Method
settings to be used when generating API methods.
Return:
Returns:
None
Raises:
MethodSettingsError: if fields in `method_settings.auto_populated_fields`
cannot be automatically populated.
"""

all_errors: dict = {}
selectors_seen = []
selectors_seen: set = set()
for method_settings in service_method_settings:
# Check if this selector is defind more than once
if method_settings.selector in selectors_seen:
all_errors[method_settings.selector] = ["Duplicate selector"]
continue
selectors_seen.append(method_settings.selector)
selectors_seen.add(method_settings.selector)

method_descriptor = self.all_methods.get(method_settings.selector)
# Check if this selector can be mapped to a method in the API.
Expand Down Expand Up @@ -670,13 +678,70 @@ 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
Returns:
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.
Returns:
None
Raises:
ClientLibrarySettingsError: if fields in `client_library_settings.experimental_features`
are not supported.
"""

all_errors: dict = {}
versions_seen: set = set()
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.add(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
when generating methods.
https://github.com/googleapis/googleapis/blob/7dab3de7ec79098bb367b6b2ac3815512a49dd56/google/api/client.proto#L325
Return:
Returns:
Mapping[str, Sequence[client_pb2.MethodSettings]]: A mapping of all method
settings read from the service YAML.
Expand Down Expand Up @@ -953,7 +1018,7 @@ def _load_children(self,
used to correspond to documentation in
``SourceCodeInfo.Location`` in ``descriptor.proto``.
Return:
Returns:
Mapping[str, Union[~.MessageType, ~.Service, ~.EnumType]]: A
sequence of the objects that were loaded.
"""
Expand Down
82 changes: 67 additions & 15 deletions tests/unit/schema/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2608,7 +2608,7 @@ def test_has_iam_mixin():
assert api_schema.has_iam_mixin


def get_file_descriptor_proto_for_method_settings_tests(
def get_file_descriptor_proto_for_tests(
fields: Sequence[descriptor_pb2.FieldDescriptorProto] = None,
client_streaming: bool = False,
server_streaming: bool = False,
Expand All @@ -2621,7 +2621,7 @@ def get_file_descriptor_proto_for_method_settings_tests(
`descriptor_pb2.FileDescriptorProto` should use client streaming.
server_streaming (bool): Whether the methods in the return object
`descriptor_pb2.FileDescriptorProto` should use server streaming.
Return:
Returns:
descriptor_pb2.FileDescriptorProto: Returns an object describing the API.
"""

Expand Down Expand Up @@ -2686,7 +2686,7 @@ def test_api_all_methods():
Tests the `all_methods` method of `gapic.schema.api` method which returns a map of
all methods for the API.
"""
fd = get_file_descriptor_proto_for_method_settings_tests()
fd = get_file_descriptor_proto_for_tests()
api_schema = api.API.build(fd, "google.example.v1beta1")
assert len(api_schema.all_methods) == 2
assert list(api_schema.all_methods.keys()) == [
Expand All @@ -2695,6 +2695,58 @@ def test_api_all_methods():
]


def test_read_python_settings_from_service_yaml():
service_yaml_config = {
"apis": [
{"name": "google.example.v1beta1.ServiceOne.Example1"},
],
"publishing": {
"library_settings": [
{
"version": "google.example.v1beta1",
"python_settings": {
"experimental_features": {"rest_async_io_enabled": True},
},
}
]
},
}
cli_options = Options(service_yaml_config=service_yaml_config)
fd = get_file_descriptor_proto_for_tests(fields=[])
api_schema = api.API.build(fd, "google.example.v1beta1", opts=cli_options)
assert api_schema.all_library_settings == {
"google.example.v1beta1": client_pb2.ClientLibrarySettings(
version="google.example.v1beta1",
python_settings=client_pb2.PythonSettings(
experimental_features=client_pb2.PythonSettings.ExperimentalFeatures(
rest_async_io_enabled=True
)
),
)
}


def test_python_settings_duplicate_version_raises_error():
"""
Test that `ClientLibrarySettingsError` is raised when there are duplicate versions in
`client_pb2.ClientLibrarySettings`.
"""
fd = get_file_descriptor_proto_for_tests()
api_schema = api.API.build(fd, "google.example.v1beta1")
clientlibrarysettings = [
client_pb2.ClientLibrarySettings(
version="google.example.v1beta1",
),
client_pb2.ClientLibrarySettings(
version="google.example.v1beta1",
),
]
with pytest.raises(
api.ClientLibrarySettingsError, match="(?i)duplicate version"
):
api_schema.enforce_valid_library_settings(clientlibrarysettings)


def test_read_method_settings_from_service_yaml():
"""
Tests the `gapic.schema.api.all_method_settings` method which reads
Expand Down Expand Up @@ -2730,7 +2782,7 @@ def test_read_method_settings_from_service_yaml():
name="mollusc", type="TYPE_STRING", options=field_options, number=2
)
fields = [squid, mollusc]
fd = get_file_descriptor_proto_for_method_settings_tests(fields=fields)
fd = get_file_descriptor_proto_for_tests(fields=fields)
api_schema = api.API.build(fd, "google.example.v1beta1", opts=cli_options)
assert api_schema.all_method_settings == {
"google.example.v1beta1.ServiceOne.Example1": client_pb2.MethodSettings(
Expand All @@ -2746,7 +2798,7 @@ def test_method_settings_duplicate_selector_raises_error():
Test that `MethodSettingsError` is raised when there are duplicate selectors in
`client_pb2.MethodSettings`.
"""
fd = get_file_descriptor_proto_for_method_settings_tests()
fd = get_file_descriptor_proto_for_tests()
api_schema = api.API.build(fd, "google.example.v1beta1")
methodsettings = [
client_pb2.MethodSettings(
Expand All @@ -2770,7 +2822,7 @@ def test_method_settings_invalid_selector_raises_error():
method_example1 = "google.example.v1beta1.DoesNotExist.Example1"
method_example2 = "google.example.v1beta1.ServiceOne.DoesNotExist"

fd = get_file_descriptor_proto_for_method_settings_tests()
fd = get_file_descriptor_proto_for_tests()
api_schema = api.API.build(fd, "google.example.v1beta1")
methodsettings = [
client_pb2.MethodSettings(
Expand Down Expand Up @@ -2802,7 +2854,7 @@ def test_method_settings_unsupported_auto_populated_field_type_raises_error():
`client_pb2.MethodSettings.auto_populated_fields` is not of type string.
"""
squid = make_field_pb2(name="squid", type="TYPE_INT32", number=1)
fd = get_file_descriptor_proto_for_method_settings_tests(fields=[squid])
fd = get_file_descriptor_proto_for_tests(fields=[squid])
api_schema = api.API.build(fd, "google.example.v1beta1")
methodsettings = [
client_pb2.MethodSettings(
Expand All @@ -2820,7 +2872,7 @@ def test_method_settings_auto_populated_field_not_found_raises_error():
`client_pb2.MethodSettings.auto_populated_fields` is not found in the top-level
request message of the selector.
"""
fd = get_file_descriptor_proto_for_method_settings_tests()
fd = get_file_descriptor_proto_for_tests()
api_schema = api.API.build(fd, "google.example.v1beta1")
methodsettings = [
client_pb2.MethodSettings(
Expand All @@ -2846,7 +2898,7 @@ def test_method_settings_auto_populated_nested_field_raises_error():
type='TYPE_MESSAGE',
)

fd = get_file_descriptor_proto_for_method_settings_tests(
fd = get_file_descriptor_proto_for_tests(
fields=[octopus.field_pb]
)
api_schema = api.API.build(fd, "google.example.v1beta1")
Expand All @@ -2865,7 +2917,7 @@ def test_method_settings_auto_populated_field_client_streaming_rpc_raises_error(
Test that `MethodSettingsError` is raised when the selector in
`client_pb2.MethodSettings.selector` maps to a method which uses client streaming.
"""
fd = get_file_descriptor_proto_for_method_settings_tests(
fd = get_file_descriptor_proto_for_tests(
client_streaming=True
)
api_schema = api.API.build(fd, "google.example.v1beta1")
Expand All @@ -2886,7 +2938,7 @@ def test_method_settings_auto_populated_field_server_streaming_rpc_raises_error(
Test that `MethodSettingsError` is raised when the selector in
`client_pb2.MethodSettings.selector` maps to a method which uses server streaming.
"""
fd = get_file_descriptor_proto_for_method_settings_tests(
fd = get_file_descriptor_proto_for_tests(
server_streaming=True
)
api_schema = api.API.build(fd, "google.example.v1beta1")
Expand Down Expand Up @@ -2914,7 +2966,7 @@ def test_method_settings_unsupported_auto_populated_field_behavior_raises_error(
squid = make_field_pb2(
name="squid", type="TYPE_STRING", options=field_options, number=1
)
fd = get_file_descriptor_proto_for_method_settings_tests(fields=[squid])
fd = get_file_descriptor_proto_for_tests(fields=[squid])
api_schema = api.API.build(fd, "google.example.v1beta1")
methodsettings = [
client_pb2.MethodSettings(
Expand All @@ -2936,7 +2988,7 @@ def test_method_settings_auto_populated_field_field_info_format_not_specified_ra
the format of the field is not specified.
"""
squid = make_field_pb2(name="squid", type="TYPE_STRING", number=1)
fd = get_file_descriptor_proto_for_method_settings_tests(fields=[squid])
fd = get_file_descriptor_proto_for_tests(fields=[squid])
api_schema = api.API.build(fd, "google.example.v1beta1")
methodsettings = [
client_pb2.MethodSettings(
Expand All @@ -2962,7 +3014,7 @@ def test_method_settings_unsupported_auto_populated_field_field_info_format_rais
squid = make_field_pb2(
name="squid", type="TYPE_STRING", options=field_options, number=1
)
fd = get_file_descriptor_proto_for_method_settings_tests(fields=[squid])
fd = get_file_descriptor_proto_for_tests(fields=[squid])
api_schema = api.API.build(fd, "google.example.v1beta1")
methodsettings = [
client_pb2.MethodSettings(
Expand Down Expand Up @@ -3001,7 +3053,7 @@ def test_method_settings_invalid_multiple_issues():
# Field Octopus Errors
# - Not annotated with google.api.field_info.format = UUID4
octopus = make_field_pb2(name="octopus", type="TYPE_STRING", number=1)
fd = get_file_descriptor_proto_for_method_settings_tests(
fd = get_file_descriptor_proto_for_tests(
fields=[squid, octopus]
)
api_schema = api.API.build(fd, "google.example.v1beta1")
Expand Down

0 comments on commit 11e3967

Please sign in to comment.