Skip to content

Commit 1f56604

Browse files
partheavchudnov-g
andauthored
feat: add support for reading google.api.api_version (#1999)
Co-authored-by: Victor Chudnovsky <vchudnov@google.com>
1 parent 8d81ae0 commit 1f56604

33 files changed

Lines changed: 501 additions & 140 deletions

File tree

packages/gapic-generator/.github/workflows/tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ concurrency:
1414
cancel-in-progress: true
1515

1616
env:
17-
SHOWCASE_VERSION: 0.32.0
17+
SHOWCASE_VERSION: 0.35.0
1818
PROTOC_VERSION: 3.20.2
1919

2020
jobs:
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
{#
2+
# Copyright (C) 2024 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
# This file is a copy of `_shared_macros.j2` in standard templates located at
17+
# `gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2`
18+
# It is intended to be a symlink.
19+
# See https://github.com/googleapis/gapic-generator-python/issues/2028
20+
# which contains follow up work to convert it to a symlink.
21+
# Do not diverge from the copy of `_shared_macros.j2` in standard templates.
22+
#}
23+
24+
{% macro auto_populate_uuid4_fields(api, method) %}
25+
{#
26+
Automatically populate UUID4 fields according to
27+
https://google.aip.dev/client-libraries/4235 when the
28+
field satisfies either of:
29+
- The field supports explicit presence and has not been set by the user.
30+
- The field doesn't support explicit presence, and its value is the empty
31+
string (i.e. the default value).
32+
When using this macro, ensure the calling template generates a line `import uuid`
33+
#}
34+
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
35+
{% if method_settings is not none %}
36+
{% for auto_populated_field in method_settings.auto_populated_fields %}
37+
{% if method.input.fields[auto_populated_field].proto3_optional %}
38+
if '{{ auto_populated_field }}' not in request:
39+
{% else %}
40+
if not request.{{ auto_populated_field }}:
41+
{% endif %}
42+
request.{{ auto_populated_field }} = str(uuid.uuid4())
43+
{% endfor %}
44+
{% endif %}{# if method_settings is not none #}
45+
{% endwith %}{# method_settings #}
46+
{% endmacro %}
47+
48+
{% macro add_google_api_core_version_header_import(service_version) %}
49+
{#
50+
The `version_header` module was added to `google-api-core`
51+
in version 2.19.0.
52+
https://github.com/googleapis/python-api-core/releases/tag/v2.19.0
53+
The `try/except` below can be removed once the minimum version of
54+
`google-api-core` is 2.19.0 or newer.
55+
#}
56+
{% if service_version %}
57+
try:
58+
from google.api_core import version_header
59+
HAS_GOOGLE_API_CORE_VERSION_HEADER = True # pragma: NO COVER
60+
except ImportError: # pragma: NO COVER
61+
HAS_GOOGLE_API_CORE_VERSION_HEADER = False
62+
{% endif %}{# service_version #}
63+
{% endmacro %}
64+
{% macro add_api_version_header_to_metadata(service_version) %}
65+
{#
66+
Add API Version to metadata as per https://github.com/aip-dev/google.aip.dev/pull/1331.
67+
When using this macro, ensure the calling template also calls macro
68+
`add_google_api_core_version_header_import` to add the necessary import statements.
69+
#}
70+
{% if service_version %}
71+
if HAS_GOOGLE_API_CORE_VERSION_HEADER: # pragma: NO COVER
72+
metadata = tuple(metadata) + (
73+
version_header.to_api_version_header("{{ service_version }}"),
74+
)
75+
{% endif %}{# service_version #}
76+
{% endmacro %}

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

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{% extends '_base.py.j2' %}
22

33
{% block content %}
4+
{% import "%namespace/%name/%version/%sub/services/%service/_shared_macros.j2" as shared_macros %}
45

56
from collections import OrderedDict
67
import os
@@ -23,6 +24,7 @@ from google.auth.transport.grpc import SslCredentials # type: ignore
2324
from google.auth.exceptions import MutualTLSChannelError # type: ignore
2425
from google.oauth2 import service_account # type: ignore
2526

27+
{{ shared_macros.add_google_api_core_version_header_import(service.version) }}
2628
{% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %}
2729
from {{package_path}} import gapic_version as package_version
2830

@@ -94,7 +96,8 @@ class {{ service.client_name }}Meta(type):
9496

9597

9698
class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
97-
"""{{ service.meta.doc|rst(width=72, indent=4) }}"""
99+
"""{{ service.meta.doc|rst(width=72, indent=4) }}{% if service.version|length %}
100+
This class implements API version {{ service.version }}.{% endif %}"""
98101

99102
@staticmethod
100103
def _get_default_mtls_endpoint(api_endpoint):
@@ -475,27 +478,8 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
475478
)),
476479
)
477480
{% endif %}
478-
479-
{#
480-
Automatically populate UUID4 fields according to
481-
https://google.aip.dev/client-libraries/4235 when the
482-
field satisfies either of:
483-
- The field supports explicit presence and has not been set by the user.
484-
- The field doesn't support explicit presence, and its value is the empty
485-
string (i.e. the default value).
486-
#}
487-
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
488-
{% if method_settings is not none %}
489-
{% for auto_populated_field in method_settings.auto_populated_fields %}
490-
{% if method.input.fields[auto_populated_field].proto3_optional %}
491-
if '{{ auto_populated_field }}' not in request:
492-
{% else %}
493-
if not request.{{ auto_populated_field }}:
494-
{% endif %}
495-
request.{{ auto_populated_field }} = str(uuid.uuid4())
496-
{% endfor %}
497-
{% endif %}{# if method_settings is not none #}
498-
{% endwith %}{# method_settings #}
481+
{{ shared_macros.add_api_version_header_to_metadata(service.version) }}
482+
{{ shared_macros.auto_populate_uuid4_fields(api, method) }}
499483

500484
# Send the request.
501485
{%+ if not method.void %}response = {% endif %}rpc(

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

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{% extends "_base.py.j2" %}
22

33
{% block content %}
4+
{% import "%namespace/%name/%version/%sub/services/%service/_shared_macros.j2" as shared_macros %}
45

56
import os
67
{% if api.all_method_settings.values()|map(attribute="auto_populated_fields", default=[])|list %}
@@ -39,6 +40,7 @@ from google.oauth2 import service_account
3940
from {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }} import {{ service.client_name }}
4041
from {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }} import transports
4142

43+
from google.api_core import api_core_version
4244
from google.api_core import client_options
4345
from google.api_core import exceptions as core_exceptions
4446
from google.api_core import grpc_helpers
@@ -69,6 +71,8 @@ from google.iam.v1 import options_pb2 # type: ignore
6971
from google.iam.v1 import policy_pb2 # type: ignore
7072
{% endif %}
7173
{% endfilter %}
74+
{{ shared_macros.add_google_api_core_version_header_import(service.version) }}
75+
7276

7377
{% with uuid4_re = "[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}" %}
7478

@@ -636,6 +640,35 @@ def test_{{ method_name }}(request_type, transport: str = 'grpc'):
636640
{% endwith %}{# auto_populated_field_sample_value #}
637641

638642

643+
{% if service.version %}
644+
@pytest.mark.parametrize("transport_name", [
645+
{% if 'grpc' in opts.transport %}
646+
("grpc"),
647+
{% endif %}
648+
{% if 'rest' in opts.transport %}
649+
("rest"),
650+
{% endif %}
651+
])
652+
def test_{{ method_name }}_api_version_header(transport_name):
653+
# TODO: Make this test unconditional once the minimum supported version of
654+
# google-api-core becomes 2.19.0 or higher.
655+
api_core_major, api_core_minor = [int(part) for part in api_core_version.__version__.split(".")[0:2]]
656+
if api_core_major > 2 or (api_core_major == 2 and api_core_minor >= 19):
657+
client = {{ service.client_name }}(credentials=ga_credentials.AnonymousCredentials(), transport=transport_name)
658+
# Mock the actual call within the gRPC stub, and fake the request.
659+
with mock.patch.object(
660+
type(client.transport.{{ method.transport_safe_name|snake_case }}),
661+
'__call__'
662+
) as call:
663+
client.{{ method_name }}()
664+
665+
# Establish that the api version header was sent.
666+
_, _, kw = call.mock_calls[0]
667+
assert kw['metadata'][0] == (version_header.API_VERSION_METADATA_KEY, "{{ service.version }}")
668+
else:
669+
pytest.skip("google-api-core>=2.19.0 is required for `google.api_core.version_header`")
670+
{% endif %}{# service.version #}
671+
639672
{% if not method.client_streaming %}
640673
def test_{{ method_name }}_empty_call():
641674
# This test is a coverage failsafe to make sure that totally empty calls,
@@ -904,9 +937,9 @@ def test_{{ method_name }}_pager(transport_name: str = "grpc"):
904937
RuntimeError,
905938
)
906939

907-
metadata = ()
940+
expected_metadata = ()
908941
{% if method.field_headers %}
909-
metadata = tuple(metadata) + (
942+
expected_metadata = tuple(expected_metadata) + (
910943
gapic_v1.routing_header.to_grpc_metadata((
911944
{% for field_header in method.field_headers %}
912945
{% if not method.client_streaming %}
@@ -918,7 +951,13 @@ def test_{{ method_name }}_pager(transport_name: str = "grpc"):
918951
{% endif %}
919952
pager = client.{{ method_name }}(request={})
920953

921-
assert pager._metadata == metadata
954+
{% if service.version %}
955+
if HAS_GOOGLE_API_CORE_VERSION_HEADER:
956+
expected_metadata = tuple(expected_metadata) + (
957+
version_header.to_api_version_header("{{ service.version }}"),
958+
)
959+
{% endif %}
960+
assert pager._metadata == expected_metadata
922961

923962
results = list(pager)
924963
assert len(results) == 6

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1734,6 +1734,17 @@ def host(self) -> str:
17341734
return self.options.Extensions[client_pb2.default_host]
17351735
return ''
17361736

1737+
@property
1738+
def version(self) -> str:
1739+
"""Return the API version for this service, if specified.
1740+
1741+
Returns:
1742+
str: The API version for this service.
1743+
"""
1744+
if self.options.Extensions[client_pb2.api_version]:
1745+
return self.options.Extensions[client_pb2.api_version]
1746+
return ''
1747+
17371748
@property
17381749
def shortname(self) -> str:
17391750
"""Return the API short name. DRIFT uses this to identify

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

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
# limitations under the License.
1515
#}
1616

17+
{% import "%namespace/%name_%version/%sub/services/%service/_shared_macros.j2" as shared_macros %}
18+
1719
{% macro client_method(method, name, snippet_index, api, service, full_extended_lro=False) %}
1820
def {{ name }}(self,
1921
{% if not method.client_streaming %}
@@ -181,7 +183,8 @@
181183
)
182184
{% endif %} {# method.explicit_routing #}
183185

184-
{{ auto_populate_uuid4_fields(api, method) }}
186+
{{ shared_macros.add_api_version_header_to_metadata(service.version) }}
187+
{{ shared_macros.auto_populate_uuid4_fields(api, method) }}
185188

186189
# Validate the universe domain.
187190
self._validate_universe_domain()
@@ -265,27 +268,3 @@
265268

266269
{% macro define_extended_operation_subclass(extended_operation) %}
267270
{% endmacro %}
268-
269-
{% macro auto_populate_uuid4_fields(api, method) %}
270-
{#
271-
Automatically populate UUID4 fields according to
272-
https://google.aip.dev/client-libraries/4235 when the
273-
field satisfies either of:
274-
- The field supports explicit presence and has not been set by the user.
275-
- The field doesn't support explicit presence, and its value is the empty
276-
string (i.e. the default value).
277-
When using this macro, ensure the calling template generates a line `import uuid`
278-
#}
279-
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
280-
{% if method_settings is not none %}
281-
{% for auto_populated_field in method_settings.auto_populated_fields %}
282-
{% if method.input.fields[auto_populated_field].proto3_optional %}
283-
if '{{ auto_populated_field }}' not in request:
284-
{% else %}
285-
if not request.{{ auto_populated_field }}:
286-
{% endif %}
287-
request.{{ auto_populated_field }} = str(uuid.uuid4())
288-
{% endfor %}
289-
{% endif %}{# if method_settings is not none #}
290-
{% endwith %}{# method_settings #}
291-
{% endmacro %}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{#
2+
# Copyright (C) 2024 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#}
16+
17+
{% macro auto_populate_uuid4_fields(api, method) %}
18+
{#
19+
Automatically populate UUID4 fields according to
20+
https://google.aip.dev/client-libraries/4235 when the
21+
field satisfies either of:
22+
- The field supports explicit presence and has not been set by the user.
23+
- The field doesn't support explicit presence, and its value is the empty
24+
string (i.e. the default value).
25+
When using this macro, ensure the calling template generates a line `import uuid`
26+
#}
27+
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
28+
{% if method_settings is not none %}
29+
{% for auto_populated_field in method_settings.auto_populated_fields %}
30+
{% if method.input.fields[auto_populated_field].proto3_optional %}
31+
if '{{ auto_populated_field }}' not in request:
32+
{% else %}
33+
if not request.{{ auto_populated_field }}:
34+
{% endif %}
35+
request.{{ auto_populated_field }} = str(uuid.uuid4())
36+
{% endfor %}
37+
{% endif %}{# if method_settings is not none #}
38+
{% endwith %}{# method_settings #}
39+
{% endmacro %}
40+
41+
{% macro add_google_api_core_version_header_import(service_version) %}
42+
{#
43+
The `version_header` module was added to `google-api-core`
44+
in version 2.19.0.
45+
https://github.com/googleapis/python-api-core/releases/tag/v2.19.0
46+
The `try/except` below can be removed once the minimum version of
47+
`google-api-core` is 2.19.0 or newer.
48+
#}
49+
{% if service_version %}
50+
try:
51+
from google.api_core import version_header
52+
HAS_GOOGLE_API_CORE_VERSION_HEADER = True # pragma: NO COVER
53+
except ImportError: # pragma: NO COVER
54+
HAS_GOOGLE_API_CORE_VERSION_HEADER = False
55+
{% endif %}{# service_version #}
56+
{% endmacro %}
57+
58+
{% macro add_api_version_header_to_metadata(service_version) %}
59+
{#
60+
Add API Version to metadata as per https://github.com/aip-dev/google.aip.dev/pull/1331.
61+
When using this macro, ensure the calling template also calls macro
62+
`add_google_api_core_version_header_import` to add the necessary import statements.
63+
#}
64+
{% if service_version %}
65+
if HAS_GOOGLE_API_CORE_VERSION_HEADER: # pragma: NO COVER
66+
metadata = tuple(metadata) + (
67+
version_header.to_api_version_header("{{ service_version }}"),
68+
)
69+
{% endif %}{# service_version #}
70+
{% endmacro %}

0 commit comments

Comments
 (0)