Skip to content

Commit 11e3967

Browse files
authored
feat: Add support for reading ClientLibrarySettings from service configuration YAML (#2098)
1 parent c440807 commit 11e3967

File tree

2 files changed

+138
-21
lines changed

2 files changed

+138
-21
lines changed

gapic/schema/api.py

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ class MethodSettingsError(ValueError):
6868
pass
6969

7070

71+
class ClientLibrarySettingsError(ValueError):
72+
"""
73+
Raised when `google.api.client_pb2.ClientLibrarySettings` contains
74+
an invalid value.
75+
"""
76+
pass
77+
78+
7179
@dataclasses.dataclass(frozen=True)
7280
class Proto:
7381
"""A representation of a particular proto file within an API."""
@@ -574,7 +582,7 @@ def mixin_http_options(self):
574582
def all_methods(self) -> Mapping[str, MethodDescriptorProto]:
575583
"""Return a map of all methods for the API.
576584
577-
Return:
585+
Returns:
578586
Mapping[str, MethodDescriptorProto]: A mapping of MethodDescriptorProto
579587
values for the API.
580588
"""
@@ -607,21 +615,21 @@ def enforce_valid_method_settings(
607615
Args:
608616
service_method_settings (Sequence[client_pb2.MethodSettings]): Method
609617
settings to be used when generating API methods.
610-
Return:
618+
Returns:
611619
None
612620
Raises:
613621
MethodSettingsError: if fields in `method_settings.auto_populated_fields`
614622
cannot be automatically populated.
615623
"""
616624

617625
all_errors: dict = {}
618-
selectors_seen = []
626+
selectors_seen: set = set()
619627
for method_settings in service_method_settings:
620628
# Check if this selector is defind more than once
621629
if method_settings.selector in selectors_seen:
622630
all_errors[method_settings.selector] = ["Duplicate selector"]
623631
continue
624-
selectors_seen.append(method_settings.selector)
632+
selectors_seen.add(method_settings.selector)
625633

626634
method_descriptor = self.all_methods.get(method_settings.selector)
627635
# Check if this selector can be mapped to a method in the API.
@@ -670,13 +678,70 @@ def enforce_valid_method_settings(
670678
if all_errors:
671679
raise MethodSettingsError(yaml.dump(all_errors))
672680

681+
@cached_property
682+
def all_library_settings(
683+
self,
684+
) -> Mapping[str, Sequence[client_pb2.ClientLibrarySettings]]:
685+
"""Return a map of all `google.api.client.ClientLibrarySettings` to be used
686+
when generating client libraries.
687+
https://github.com/googleapis/googleapis/blob/master/google/api/client.proto#L130
688+
689+
Returns:
690+
Mapping[str, Sequence[client_pb2.ClientLibrarySettings]]: A mapping of all library
691+
settings read from the service YAML.
692+
693+
Raises:
694+
gapic.schema.api.ClientLibrarySettingsError: Raised when `google.api.client_pb2.ClientLibrarySettings`
695+
contains an invalid value.
696+
"""
697+
self.enforce_valid_library_settings(
698+
self.service_yaml_config.publishing.library_settings
699+
)
700+
701+
return {
702+
library_setting.version: client_pb2.ClientLibrarySettings(
703+
version=library_setting.version,
704+
python_settings=library_setting.python_settings,
705+
)
706+
for library_setting in self.service_yaml_config.publishing.library_settings
707+
}
708+
709+
def enforce_valid_library_settings(
710+
self, client_library_settings: Sequence[client_pb2.ClientLibrarySettings]
711+
) -> None:
712+
"""
713+
Checks each `google.api.client.ClientLibrarySettings` provided for validity and
714+
raises an exception if invalid values are found.
715+
716+
Args:
717+
client_library_settings (Sequence[client_pb2.ClientLibrarySettings]): Client
718+
library settings to be used when generating API methods.
719+
Returns:
720+
None
721+
Raises:
722+
ClientLibrarySettingsError: if fields in `client_library_settings.experimental_features`
723+
are not supported.
724+
"""
725+
726+
all_errors: dict = {}
727+
versions_seen: set = set()
728+
for library_settings in client_library_settings:
729+
# Check if this version is defind more than once
730+
if library_settings.version in versions_seen:
731+
all_errors[library_settings.version] = ["Duplicate version"]
732+
continue
733+
versions_seen.add(library_settings.version)
734+
735+
if all_errors:
736+
raise ClientLibrarySettingsError(yaml.dump(all_errors))
737+
673738
@cached_property
674739
def all_method_settings(self) -> Mapping[str, Sequence[client_pb2.MethodSettings]]:
675740
"""Return a map of all `google.api.client.MethodSettings` to be used
676741
when generating methods.
677742
https://github.com/googleapis/googleapis/blob/7dab3de7ec79098bb367b6b2ac3815512a49dd56/google/api/client.proto#L325
678743
679-
Return:
744+
Returns:
680745
Mapping[str, Sequence[client_pb2.MethodSettings]]: A mapping of all method
681746
settings read from the service YAML.
682747
@@ -953,7 +1018,7 @@ def _load_children(self,
9531018
used to correspond to documentation in
9541019
``SourceCodeInfo.Location`` in ``descriptor.proto``.
9551020
956-
Return:
1021+
Returns:
9571022
Mapping[str, Union[~.MessageType, ~.Service, ~.EnumType]]: A
9581023
sequence of the objects that were loaded.
9591024
"""

tests/unit/schema/test_api.py

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2608,7 +2608,7 @@ def test_has_iam_mixin():
26082608
assert api_schema.has_iam_mixin
26092609

26102610

2611-
def get_file_descriptor_proto_for_method_settings_tests(
2611+
def get_file_descriptor_proto_for_tests(
26122612
fields: Sequence[descriptor_pb2.FieldDescriptorProto] = None,
26132613
client_streaming: bool = False,
26142614
server_streaming: bool = False,
@@ -2621,7 +2621,7 @@ def get_file_descriptor_proto_for_method_settings_tests(
26212621
`descriptor_pb2.FileDescriptorProto` should use client streaming.
26222622
server_streaming (bool): Whether the methods in the return object
26232623
`descriptor_pb2.FileDescriptorProto` should use server streaming.
2624-
Return:
2624+
Returns:
26252625
descriptor_pb2.FileDescriptorProto: Returns an object describing the API.
26262626
"""
26272627

@@ -2686,7 +2686,7 @@ def test_api_all_methods():
26862686
Tests the `all_methods` method of `gapic.schema.api` method which returns a map of
26872687
all methods for the API.
26882688
"""
2689-
fd = get_file_descriptor_proto_for_method_settings_tests()
2689+
fd = get_file_descriptor_proto_for_tests()
26902690
api_schema = api.API.build(fd, "google.example.v1beta1")
26912691
assert len(api_schema.all_methods) == 2
26922692
assert list(api_schema.all_methods.keys()) == [
@@ -2695,6 +2695,58 @@ def test_api_all_methods():
26952695
]
26962696

26972697

2698+
def test_read_python_settings_from_service_yaml():
2699+
service_yaml_config = {
2700+
"apis": [
2701+
{"name": "google.example.v1beta1.ServiceOne.Example1"},
2702+
],
2703+
"publishing": {
2704+
"library_settings": [
2705+
{
2706+
"version": "google.example.v1beta1",
2707+
"python_settings": {
2708+
"experimental_features": {"rest_async_io_enabled": True},
2709+
},
2710+
}
2711+
]
2712+
},
2713+
}
2714+
cli_options = Options(service_yaml_config=service_yaml_config)
2715+
fd = get_file_descriptor_proto_for_tests(fields=[])
2716+
api_schema = api.API.build(fd, "google.example.v1beta1", opts=cli_options)
2717+
assert api_schema.all_library_settings == {
2718+
"google.example.v1beta1": client_pb2.ClientLibrarySettings(
2719+
version="google.example.v1beta1",
2720+
python_settings=client_pb2.PythonSettings(
2721+
experimental_features=client_pb2.PythonSettings.ExperimentalFeatures(
2722+
rest_async_io_enabled=True
2723+
)
2724+
),
2725+
)
2726+
}
2727+
2728+
2729+
def test_python_settings_duplicate_version_raises_error():
2730+
"""
2731+
Test that `ClientLibrarySettingsError` is raised when there are duplicate versions in
2732+
`client_pb2.ClientLibrarySettings`.
2733+
"""
2734+
fd = get_file_descriptor_proto_for_tests()
2735+
api_schema = api.API.build(fd, "google.example.v1beta1")
2736+
clientlibrarysettings = [
2737+
client_pb2.ClientLibrarySettings(
2738+
version="google.example.v1beta1",
2739+
),
2740+
client_pb2.ClientLibrarySettings(
2741+
version="google.example.v1beta1",
2742+
),
2743+
]
2744+
with pytest.raises(
2745+
api.ClientLibrarySettingsError, match="(?i)duplicate version"
2746+
):
2747+
api_schema.enforce_valid_library_settings(clientlibrarysettings)
2748+
2749+
26982750
def test_read_method_settings_from_service_yaml():
26992751
"""
27002752
Tests the `gapic.schema.api.all_method_settings` method which reads
@@ -2730,7 +2782,7 @@ def test_read_method_settings_from_service_yaml():
27302782
name="mollusc", type="TYPE_STRING", options=field_options, number=2
27312783
)
27322784
fields = [squid, mollusc]
2733-
fd = get_file_descriptor_proto_for_method_settings_tests(fields=fields)
2785+
fd = get_file_descriptor_proto_for_tests(fields=fields)
27342786
api_schema = api.API.build(fd, "google.example.v1beta1", opts=cli_options)
27352787
assert api_schema.all_method_settings == {
27362788
"google.example.v1beta1.ServiceOne.Example1": client_pb2.MethodSettings(
@@ -2746,7 +2798,7 @@ def test_method_settings_duplicate_selector_raises_error():
27462798
Test that `MethodSettingsError` is raised when there are duplicate selectors in
27472799
`client_pb2.MethodSettings`.
27482800
"""
2749-
fd = get_file_descriptor_proto_for_method_settings_tests()
2801+
fd = get_file_descriptor_proto_for_tests()
27502802
api_schema = api.API.build(fd, "google.example.v1beta1")
27512803
methodsettings = [
27522804
client_pb2.MethodSettings(
@@ -2770,7 +2822,7 @@ def test_method_settings_invalid_selector_raises_error():
27702822
method_example1 = "google.example.v1beta1.DoesNotExist.Example1"
27712823
method_example2 = "google.example.v1beta1.ServiceOne.DoesNotExist"
27722824

2773-
fd = get_file_descriptor_proto_for_method_settings_tests()
2825+
fd = get_file_descriptor_proto_for_tests()
27742826
api_schema = api.API.build(fd, "google.example.v1beta1")
27752827
methodsettings = [
27762828
client_pb2.MethodSettings(
@@ -2802,7 +2854,7 @@ def test_method_settings_unsupported_auto_populated_field_type_raises_error():
28022854
`client_pb2.MethodSettings.auto_populated_fields` is not of type string.
28032855
"""
28042856
squid = make_field_pb2(name="squid", type="TYPE_INT32", number=1)
2805-
fd = get_file_descriptor_proto_for_method_settings_tests(fields=[squid])
2857+
fd = get_file_descriptor_proto_for_tests(fields=[squid])
28062858
api_schema = api.API.build(fd, "google.example.v1beta1")
28072859
methodsettings = [
28082860
client_pb2.MethodSettings(
@@ -2820,7 +2872,7 @@ def test_method_settings_auto_populated_field_not_found_raises_error():
28202872
`client_pb2.MethodSettings.auto_populated_fields` is not found in the top-level
28212873
request message of the selector.
28222874
"""
2823-
fd = get_file_descriptor_proto_for_method_settings_tests()
2875+
fd = get_file_descriptor_proto_for_tests()
28242876
api_schema = api.API.build(fd, "google.example.v1beta1")
28252877
methodsettings = [
28262878
client_pb2.MethodSettings(
@@ -2846,7 +2898,7 @@ def test_method_settings_auto_populated_nested_field_raises_error():
28462898
type='TYPE_MESSAGE',
28472899
)
28482900

2849-
fd = get_file_descriptor_proto_for_method_settings_tests(
2901+
fd = get_file_descriptor_proto_for_tests(
28502902
fields=[octopus.field_pb]
28512903
)
28522904
api_schema = api.API.build(fd, "google.example.v1beta1")
@@ -2865,7 +2917,7 @@ def test_method_settings_auto_populated_field_client_streaming_rpc_raises_error(
28652917
Test that `MethodSettingsError` is raised when the selector in
28662918
`client_pb2.MethodSettings.selector` maps to a method which uses client streaming.
28672919
"""
2868-
fd = get_file_descriptor_proto_for_method_settings_tests(
2920+
fd = get_file_descriptor_proto_for_tests(
28692921
client_streaming=True
28702922
)
28712923
api_schema = api.API.build(fd, "google.example.v1beta1")
@@ -2886,7 +2938,7 @@ def test_method_settings_auto_populated_field_server_streaming_rpc_raises_error(
28862938
Test that `MethodSettingsError` is raised when the selector in
28872939
`client_pb2.MethodSettings.selector` maps to a method which uses server streaming.
28882940
"""
2889-
fd = get_file_descriptor_proto_for_method_settings_tests(
2941+
fd = get_file_descriptor_proto_for_tests(
28902942
server_streaming=True
28912943
)
28922944
api_schema = api.API.build(fd, "google.example.v1beta1")
@@ -2914,7 +2966,7 @@ def test_method_settings_unsupported_auto_populated_field_behavior_raises_error(
29142966
squid = make_field_pb2(
29152967
name="squid", type="TYPE_STRING", options=field_options, number=1
29162968
)
2917-
fd = get_file_descriptor_proto_for_method_settings_tests(fields=[squid])
2969+
fd = get_file_descriptor_proto_for_tests(fields=[squid])
29182970
api_schema = api.API.build(fd, "google.example.v1beta1")
29192971
methodsettings = [
29202972
client_pb2.MethodSettings(
@@ -2936,7 +2988,7 @@ def test_method_settings_auto_populated_field_field_info_format_not_specified_ra
29362988
the format of the field is not specified.
29372989
"""
29382990
squid = make_field_pb2(name="squid", type="TYPE_STRING", number=1)
2939-
fd = get_file_descriptor_proto_for_method_settings_tests(fields=[squid])
2991+
fd = get_file_descriptor_proto_for_tests(fields=[squid])
29402992
api_schema = api.API.build(fd, "google.example.v1beta1")
29412993
methodsettings = [
29422994
client_pb2.MethodSettings(
@@ -2962,7 +3014,7 @@ def test_method_settings_unsupported_auto_populated_field_field_info_format_rais
29623014
squid = make_field_pb2(
29633015
name="squid", type="TYPE_STRING", options=field_options, number=1
29643016
)
2965-
fd = get_file_descriptor_proto_for_method_settings_tests(fields=[squid])
3017+
fd = get_file_descriptor_proto_for_tests(fields=[squid])
29663018
api_schema = api.API.build(fd, "google.example.v1beta1")
29673019
methodsettings = [
29683020
client_pb2.MethodSettings(
@@ -3001,7 +3053,7 @@ def test_method_settings_invalid_multiple_issues():
30013053
# Field Octopus Errors
30023054
# - Not annotated with google.api.field_info.format = UUID4
30033055
octopus = make_field_pb2(name="octopus", type="TYPE_STRING", number=1)
3004-
fd = get_file_descriptor_proto_for_method_settings_tests(
3056+
fd = get_file_descriptor_proto_for_tests(
30053057
fields=[squid, octopus]
30063058
)
30073059
api_schema = api.API.build(fd, "google.example.v1beta1")

0 commit comments

Comments
 (0)