Skip to content

Commit 29639a2

Browse files
authored
feat: add support for common resource paths (#622)
Google Cloud defines a small set of common resources that do not belong to specific APIs or message types. All generated service clients now contain helper methods that allow construction and parsing of these paths. See https://github.com/googleapis/googleapis/blob/master/google/cloud/common_resources.proto for the list of common resources for Google Cloud.
1 parent 1983e52 commit 29639a2

File tree

7 files changed

+171
-7
lines changed

7 files changed

+171
-7
lines changed

packages/gapic-generator/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/client.py.j2

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,19 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
132132
m = re.match(r"{{ message.path_regex_str }}", path)
133133
return m.groupdict() if m else {}
134134
{% endfor %}
135+
{% for resource_msg in service.common_resources|sort(attribute="type_name") -%}
136+
@staticmethod
137+
def common_{{ resource_msg.message_type.resource_type|snake_case }}_path({% for arg in resource_msg.message_type.resource_path_args %}{{ arg }}: str, {%endfor %}) -> str:
138+
"""Return a fully-qualified {{ resource_msg.message_type.resource_type|snake_case }} string."""
139+
return "{{ resource_msg.message_type.resource_path }}".format({% for arg in resource_msg.message_type.resource_path_args %}{{ arg }}={{ arg }}, {% endfor %})
140+
141+
@staticmethod
142+
def parse_common_{{ resource_msg.message_type.resource_type|snake_case }}_path(path: str) -> Dict[str,str]:
143+
"""Parse a {{ resource_msg.message_type.resource_type|snake_case }} path into its component segments."""
144+
m = re.match(r"{{ resource_msg.message_type.path_regex_str }}", path)
145+
return m.groupdict() if m else {}
146+
147+
{% endfor %} {# common resources #}
135148

136149
def __init__(self, *,
137150
credentials: Optional[credentials.Credentials] = None,

packages/gapic-generator/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -714,8 +714,8 @@ def test_{{ service.name|snake_case }}_grpc_lro_client():
714714

715715
{% endif -%}
716716

717-
{% for message in service.resource_messages|sort(attribute="resource_type") -%}
718717
{% with molluscs = cycler("squid", "clam", "whelk", "octopus", "oyster", "nudibranch", "cuttlefish", "mussel", "winkle", "nautilus", "scallop", "abalone") -%}
718+
{% for message in service.resource_messages|sort(attribute="resource_type") -%}
719719
def test_{{ message.resource_type|snake_case }}_path():
720720
{% for arg in message.resource_path_args -%}
721721
{{ arg }} = "{{ molluscs.next() }}"
@@ -737,8 +737,31 @@ def test_parse_{{ message.resource_type|snake_case }}_path():
737737
actual = {{ service.client_name }}.parse_{{ message.resource_type|snake_case }}_path(path)
738738
assert expected == actual
739739

740-
{% endwith -%}
741740
{% endfor -%}
741+
{% for resource_msg in service.common_resources -%}
742+
def test_common_{{ resource_msg.message_type.resource_type|snake_case }}_path():
743+
{% for arg in resource_msg.message_type.resource_path_args -%}
744+
{{ arg }} = "{{ molluscs.next() }}"
745+
{% endfor %}
746+
expected = "{{ resource_msg.message_type.resource_path }}".format({% for arg in resource_msg.message_type.resource_path_args %}{{ arg }}={{ arg }}, {% endfor %})
747+
actual = {{ service.client_name }}.common_{{ resource_msg.message_type.resource_type|snake_case }}_path({{ resource_msg.message_type.resource_path_args|join(", ") }})
748+
assert expected == actual
749+
750+
751+
def test_parse_common_{{ resource_msg.message_type.resource_type|snake_case }}_path():
752+
expected = {
753+
{% for arg in resource_msg.message_type.resource_path_args -%}
754+
"{{ arg }}": "{{ molluscs.next() }}",
755+
{% endfor %}
756+
}
757+
path = {{ service.client_name }}.common_{{ resource_msg.message_type.resource_type|snake_case }}_path(**expected)
758+
759+
# Check that the path construction is reversible.
760+
actual = {{ service.client_name }}.parse_common_{{ resource_msg.message_type.resource_type|snake_case }}_path(path)
761+
assert expected == actual
762+
763+
{% endfor -%} {# common resources#}
764+
{% endwith -%} {# cycler #}
742765

743766
def test_client_withDEFAULT_CLIENT_INFO():
744767
client_info = gapic_v1.client_info.ClientInfo()

packages/gapic-generator/gapic/schema/wrappers.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
import dataclasses
3232
import re
3333
from itertools import chain
34-
from typing import (cast, Dict, FrozenSet, Iterable, List, Mapping, Optional,
35-
Sequence, Set, Tuple, Union)
34+
from typing import (cast, Dict, FrozenSet, Iterable, List, Mapping,
35+
ClassVar, Optional, Sequence, Set, Tuple, Union)
3636

3737
from google.api import annotations_pb2 # type: ignore
3838
from google.api import client_pb2
@@ -855,6 +855,26 @@ def with_context(self, *, collisions: FrozenSet[str]) -> 'Method':
855855
)
856856

857857

858+
@dataclasses.dataclass(frozen=True)
859+
class CommonResource:
860+
type_name: str
861+
pattern: str
862+
863+
@utils.cached_property
864+
def message_type(self):
865+
message_pb = descriptor_pb2.DescriptorProto()
866+
res_pb = message_pb.options.Extensions[resource_pb2.resource]
867+
res_pb.type = self.type_name
868+
res_pb.pattern.append(self.pattern)
869+
870+
return MessageType(
871+
message_pb=message_pb,
872+
fields={},
873+
nested_enums={},
874+
nested_messages={},
875+
)
876+
877+
858878
@dataclasses.dataclass(frozen=True)
859879
class Service:
860880
"""Description of a service (defined with the ``service`` keyword)."""
@@ -864,6 +884,33 @@ class Service:
864884
default_factory=metadata.Metadata,
865885
)
866886

887+
common_resources: ClassVar[Sequence[CommonResource]] = dataclasses.field(
888+
default=(
889+
CommonResource(
890+
"cloudresourcemanager.googleapis.com/Project",
891+
"projects/{project}",
892+
),
893+
CommonResource(
894+
"cloudresourcemanager.googleapis.com/Organization",
895+
"organizations/{organization}",
896+
),
897+
CommonResource(
898+
"cloudresourcemanager.googleapis.com/Folder",
899+
"folders/{folder}",
900+
),
901+
CommonResource(
902+
"cloudbilling.googleapis.com/BillingAccount",
903+
"billingAccounts/{billing_account}",
904+
),
905+
CommonResource(
906+
"locations.googleapis.com/Location",
907+
"projects/{project}/locations/{location}",
908+
),
909+
),
910+
init=False,
911+
compare=False,
912+
)
913+
867914
def __getattr__(self, name):
868915
return getattr(self.service_pb, name)
869916

packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ class {{ service.async_client_name }}:
4242
{{ message.resource_type|snake_case }}_path = staticmethod({{ service.client_name }}.{{ message.resource_type|snake_case }}_path)
4343
parse_{{ message.resource_type|snake_case}}_path = staticmethod({{ service.client_name }}.parse_{{ message.resource_type|snake_case }}_path)
4444
{% endfor %}
45+
{% for resource_msg in service.common_resources %}
46+
common_{{ resource_msg.message_type.resource_type|snake_case }}_path = staticmethod({{ service.client_name }}.common_{{ resource_msg.message_type.resource_type|snake_case }}_path)
47+
parse_common_{{ resource_msg.message_type.resource_type|snake_case }}_path = staticmethod({{ service.client_name }}.parse_common_{{ resource_msg.message_type.resource_type|snake_case }}_path)
48+
{% endfor %}
4549

4650
from_service_account_file = {{ service.client_name }}.from_service_account_file
4751
from_service_account_json = from_service_account_file

packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,20 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
137137
"""Parse a {{ message.resource_type|snake_case }} path into its component segments."""
138138
m = re.match(r"{{ message.path_regex_str }}", path)
139139
return m.groupdict() if m else {}
140-
{% endfor %}
140+
{% endfor %} {# resources #}
141+
{% for resource_msg in service.common_resources|sort(attribute="type_name") -%}
142+
@staticmethod
143+
def common_{{ resource_msg.message_type.resource_type|snake_case }}_path({% for arg in resource_msg.message_type.resource_path_args %}{{ arg }}: str, {%endfor %}) -> str:
144+
"""Return a fully-qualified {{ resource_msg.message_type.resource_type|snake_case }} string."""
145+
return "{{ resource_msg.message_type.resource_path }}".format({% for arg in resource_msg.message_type.resource_path_args %}{{ arg }}={{ arg }}, {% endfor %})
146+
147+
@staticmethod
148+
def parse_common_{{ resource_msg.message_type.resource_type|snake_case }}_path(path: str) -> Dict[str,str]:
149+
"""Parse a {{ resource_msg.message_type.resource_type|snake_case }} path into its component segments."""
150+
m = re.match(r"{{ resource_msg.message_type.path_regex_str }}", path)
151+
return m.groupdict() if m else {}
152+
153+
{% endfor %} {# common resources #}
141154

142155
def __init__(self, *,
143156
credentials: Optional[credentials.Credentials] = None,

packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,8 +1304,8 @@ def test_{{ service.name|snake_case }}_grpc_lro_async_client():
13041304

13051305
{% endif -%}
13061306

1307-
{% for message in service.resource_messages|sort(attribute="resource_type") -%}
13081307
{% with molluscs = cycler("squid", "clam", "whelk", "octopus", "oyster", "nudibranch", "cuttlefish", "mussel", "winkle", "nautilus", "scallop", "abalone") -%}
1308+
{% for message in service.resource_messages|sort(attribute="resource_type") -%}
13091309
def test_{{ message.resource_type|snake_case }}_path():
13101310
{% for arg in message.resource_path_args -%}
13111311
{{ arg }} = "{{ molluscs.next() }}"
@@ -1327,8 +1327,31 @@ def test_parse_{{ message.resource_type|snake_case }}_path():
13271327
actual = {{ service.client_name }}.parse_{{ message.resource_type|snake_case }}_path(path)
13281328
assert expected == actual
13291329

1330-
{% endwith -%}
13311330
{% endfor -%}
1331+
{% for resource_msg in service.common_resources -%}
1332+
def test_common_{{ resource_msg.message_type.resource_type|snake_case }}_path():
1333+
{% for arg in resource_msg.message_type.resource_path_args -%}
1334+
{{ arg }} = "{{ molluscs.next() }}"
1335+
{% endfor %}
1336+
expected = "{{ resource_msg.message_type.resource_path }}".format({% for arg in resource_msg.message_type.resource_path_args %}{{ arg }}={{ arg }}, {% endfor %})
1337+
actual = {{ service.client_name }}.common_{{ resource_msg.message_type.resource_type|snake_case }}_path({{ resource_msg.message_type.resource_path_args|join(", ") }})
1338+
assert expected == actual
1339+
1340+
1341+
def test_parse_common_{{ resource_msg.message_type.resource_type|snake_case }}_path():
1342+
expected = {
1343+
{% for arg in resource_msg.message_type.resource_path_args -%}
1344+
"{{ arg }}": "{{ molluscs.next() }}",
1345+
{% endfor %}
1346+
}
1347+
path = {{ service.client_name }}.common_{{ resource_msg.message_type.resource_type|snake_case }}_path(**expected)
1348+
1349+
# Check that the path construction is reversible.
1350+
actual = {{ service.client_name }}.parse_common_{{ resource_msg.message_type.resource_type|snake_case }}_path(path)
1351+
assert expected == actual
1352+
1353+
{% endfor -%} {# common resources#}
1354+
{% endwith -%} {# cycler #}
13321355

13331356

13341357
def test_client_withDEFAULT_CLIENT_INFO():

packages/gapic-generator/tests/unit/schema/wrappers/test_service.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from google.protobuf import descriptor_pb2
2121

2222
from gapic.schema import imp
23+
from gapic.schema.wrappers import CommonResource
2324

2425
from test_utils.test_utils import (
2526
get_method,
@@ -295,3 +296,43 @@ def test_has_pagers():
295296
),
296297
)
297298
assert not other_service.has_pagers
299+
300+
301+
def test_default_common_resources():
302+
service = make_service(name="MolluscMaker")
303+
304+
assert service.common_resources == (
305+
CommonResource(
306+
"cloudresourcemanager.googleapis.com/Project",
307+
"projects/{project}",
308+
),
309+
CommonResource(
310+
"cloudresourcemanager.googleapis.com/Organization",
311+
"organizations/{organization}",
312+
),
313+
CommonResource(
314+
"cloudresourcemanager.googleapis.com/Folder",
315+
"folders/{folder}",
316+
),
317+
CommonResource(
318+
"cloudbilling.googleapis.com/BillingAccount",
319+
"billingAccounts/{billing_account}",
320+
),
321+
CommonResource(
322+
"locations.googleapis.com/Location",
323+
"projects/{project}/locations/{location}",
324+
),
325+
)
326+
327+
328+
def test_common_resource_patterns():
329+
species = CommonResource(
330+
"nomenclature.linnaen.com/Species",
331+
"families/{family}/genera/{genus}/species/{species}",
332+
)
333+
species_msg = species.message_type
334+
335+
assert species_msg.resource_path == "families/{family}/genera/{genus}/species/{species}"
336+
assert species_msg.resource_type == "Species"
337+
assert species_msg.resource_path_args == ["family", "genus", "species"]
338+
assert species_msg.path_regex_str == '^families/(?P<family>.+?)/genera/(?P<genus>.+?)/species/(?P<species>.+?)$'

0 commit comments

Comments
 (0)