Skip to content

feat: add support for reading google.api.api_version #1999

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 37 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4e3138c
feat: add support for reading google.api.api_version
parthea Apr 10, 2024
009fc71
add showcase tests; clean up code
parthea Apr 29, 2024
b574ed1
remove noise in diff
parthea Apr 29, 2024
dd96aa4
remove noise in diff
parthea Apr 29, 2024
de4ba22
remove noise in diff
parthea Apr 29, 2024
28e6991
use gapic-showcase 0.35.0
parthea Apr 29, 2024
fca5b55
update comment
parthea Apr 29, 2024
8cd648a
add constraints to ads templates
parthea Apr 30, 2024
35755b7
add new line
parthea Apr 30, 2024
ea5666f
update google-api-core and googleapis-common-protos setup.py to match…
parthea Apr 30, 2024
79f1767
update grpc in setup.py to match the one in googleads/google-ads-python
parthea Apr 30, 2024
7eb5c92
style
parthea Apr 30, 2024
c0b7810
coverage
parthea Apr 30, 2024
d52e78e
fix test
parthea Apr 30, 2024
5129f61
mypy
parthea Apr 30, 2024
ab18eea
coverage
parthea Apr 30, 2024
6946304
revert
parthea Apr 30, 2024
6dfa53c
remove test code
parthea May 1, 2024
2e666ea
update copyright year
parthea May 2, 2024
1e6e96e
address review feedback
parthea May 2, 2024
6416902
fix comment
parthea May 2, 2024
68b4010
revert constraints files for ads
parthea May 2, 2024
178c964
remove test code
parthea May 2, 2024
a5d6109
address review feedback
parthea May 4, 2024
109edd4
address review feedback
parthea May 4, 2024
12ff101
revert WORKSPACE
parthea May 4, 2024
ca95a6b
clean up
parthea May 4, 2024
0e669ab
Update gapic/templates/%namespace/%name_%version/%sub/services/%servi…
parthea May 7, 2024
7957ded
convert to symlink
parthea May 7, 2024
542e4e1
Revert "convert to symlink"
parthea May 7, 2024
0f68169
Update gapic/templates/%namespace/%name_%version/%sub/services/%servi…
parthea May 7, 2024
921f20b
address review feedback
parthea May 7, 2024
16402e1
Revert "Update gapic/templates/%namespace/%name_%version/%sub/service…
parthea May 7, 2024
555cff7
fix build
parthea May 7, 2024
f150e67
Address review feedback
parthea May 7, 2024
220f873
Address review feedback
parthea May 7, 2024
a62b313
address review feedback
parthea May 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ concurrency:
cancel-in-progress: true

env:
SHOWCASE_VERSION: 0.32.0
SHOWCASE_VERSION: 0.35.0
PROTOC_VERSION: 3.20.2

jobs:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{#
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a regular file, not a symlink:

git ls-files -s  gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/_shared_macros.j2
100644 7094d3039af53ead4b844ba9952d7084f0526bd2 0       gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/_shared_macros.j2

See the meaning of Git modes. (Credit: SO answer)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch and thanks for the link! Fixed in 7957ded

(py39) partheniou@partheniou-vm-3:~/git/gapic-generator-python$ git ls-files -s  gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/_shared_macros.j2
120000 6884f2def889f3f4f2a5b19b54e76a52c2826174 0       gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/_shared_macros.j2

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reverted this change in 542e4e1 and filed #2028 to investigate if symlinks can be used.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the symlinks don't work (weird, they should), can you specify a template outside the current directory (something like {% include ../templates/foo/bar %}

If that also doesn't work, I would suggest keeping the duplicate files but adding a prominent header to the ads one saying that this is a copy of the one in the standard templates, intended to be a symlink. My rationale: we have duplicate code either way, and the macro file9s) makes them more organized....and it paves the way for the symlink once we can figure it out.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see. You reverted back to the duplicate file, like I said in that last paragraph. SG. Could you add a note that this is intended to be a symlink to the other file, and for now is a duplicate?

# 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.
#}

{% macro auto_populate_uuid4_fields(api, method) %}
{#
Automatically populate UUID4 fields according to
https://google.aip.dev/client-libraries/4235 when the
field satisfies either of:
- The field supports explicit presence and has not been set by the user.
- The field doesn't support explicit presence, and its value is the empty
string (i.e. the default value).
When using this macro, ensure the calling template generates a line `import uuid`
#}
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
{% if method.input.fields[auto_populated_field].proto3_optional %}
if '{{ auto_populated_field }}' not in request:
{% else %}
if not request.{{ auto_populated_field }}:
{% endif %}
request.{{ auto_populated_field }} = str(uuid.uuid4())
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
{% endmacro %}

{% macro add_google_api_core_version_header_import(service_version) %}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a note at the top that this is intended to be a symlink to the other file, and for now is a duplicate (link to the issue you filed)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a62b313

{#
The `version_header` module was added to `google-api-core`
in version 2.19.0.
https://github.com/googleapis/python-api-core/releases/tag/v2.19.0
The `try/except` below can be removed once the minimum version of
`google-api-core` is 2.19.0 or newer.
#}
{% if service_version %}
try:
from google.api_core import version_header
HAS_GOOGLE_API_CORE_VERSION_HEADER = True # pragma: NO COVER
except ImportError: # pragma: NO COVER
HAS_GOOGLE_API_CORE_VERSION_HEADER = False
{% endif %}{# service_version #}
{% endmacro %}
{% macro add_api_version_header_to_metadata(service_version) %}
{#
Add API Version to metadata as per https://github.com/aip-dev/google.aip.dev/pull/1331.
When using this macro, ensure the calling template also calls macro
`add_google_api_core_version_header_import` to add the necessary import statements.
#}
{% if service_version %}
if HAS_GOOGLE_API_CORE_VERSION_HEADER: # pragma: NO COVER
metadata = tuple(metadata) + (
version_header.to_api_version_header("{{ service_version }}"),
)
{% endif %}{# service_version #}
{% endmacro %}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{% extends '_base.py.j2' %}

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

from collections import OrderedDict
import os
Expand All @@ -23,6 +24,7 @@ from google.auth.transport.grpc import SslCredentials # type: ignore
from google.auth.exceptions import MutualTLSChannelError # type: ignore
from google.oauth2 import service_account # type: ignore

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

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


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

@staticmethod
def _get_default_mtls_endpoint(api_endpoint):
Expand Down Expand Up @@ -475,27 +478,8 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
)),
)
{% endif %}

{#
Automatically populate UUID4 fields according to
https://google.aip.dev/client-libraries/4235 when the
field satisfies either of:
- The field supports explicit presence and has not been set by the user.
- The field doesn't support explicit presence, and its value is the empty
string (i.e. the default value).
#}
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
{% if method.input.fields[auto_populated_field].proto3_optional %}
if '{{ auto_populated_field }}' not in request:
{% else %}
if not request.{{ auto_populated_field }}:
{% endif %}
request.{{ auto_populated_field }} = str(uuid.uuid4())
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
{{ shared_macros.add_api_version_header_to_metadata(service.version) }}
{{ shared_macros.auto_populate_uuid4_fields(api, method) }}

# Send the request.
{%+ if not method.void %}response = {% endif %}rpc(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{% extends "_base.py.j2" %}

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

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

from google.api_core import api_core_version
from google.api_core import client_options
from google.api_core import exceptions as core_exceptions
from google.api_core import grpc_helpers
Expand Down Expand Up @@ -69,6 +71,8 @@ from google.iam.v1 import options_pb2 # type: ignore
from google.iam.v1 import policy_pb2 # type: ignore
{% endif %}
{% endfilter %}
{{ shared_macros.add_google_api_core_version_header_import(service.version) }}


{% 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}" %}

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


{% if service.version %}
@pytest.mark.parametrize("transport_name", [
{% if 'grpc' in opts.transport %}
("grpc"),
{% endif %}
{% if 'rest' in opts.transport %}
("rest"),
{% endif %}
])
def test_{{ method_name }}_api_version_header(transport_name):
# TODO: Make this test unconditional once the minimum supported version of
# google-api-core becomes 2.19.0 or higher.
api_core_major, api_core_minor = [int(part) for part in api_core_version.__version__.split(".")[0:2]]
if api_core_major > 2 or (api_core_major == 2 and api_core_minor >= 19):
client = {{ service.client_name }}(credentials=ga_credentials.AnonymousCredentials(), transport=transport_name)
# Mock the actual call within the gRPC stub, and fake the request.
with mock.patch.object(
type(client.transport.{{ method.transport_safe_name|snake_case }}),
'__call__'
) as call:
client.{{ method_name }}()

# Establish that the api version header was sent.
_, _, kw = call.mock_calls[0]
assert kw['metadata'][0] == (version_header.API_VERSION_METADATA_KEY, "{{ service.version }}")
else:
pytest.skip("google-api-core>=2.19.0 is required for `google.api_core.version_header`")
{% endif %}{# service.version #}

{% if not method.client_streaming %}
def test_{{ method_name }}_empty_call():
# This test is a coverage failsafe to make sure that totally empty calls,
Expand Down Expand Up @@ -904,9 +937,9 @@ def test_{{ method_name }}_pager(transport_name: str = "grpc"):
RuntimeError,
)

metadata = ()
expected_metadata = ()
{% if method.field_headers %}
metadata = tuple(metadata) + (
expected_metadata = tuple(expected_metadata) + (
gapic_v1.routing_header.to_grpc_metadata((
{% for field_header in method.field_headers %}
{% if not method.client_streaming %}
Expand All @@ -918,7 +951,13 @@ def test_{{ method_name }}_pager(transport_name: str = "grpc"):
{% endif %}
pager = client.{{ method_name }}(request={})

assert pager._metadata == metadata
{% if service.version %}
if HAS_GOOGLE_API_CORE_VERSION_HEADER:
expected_metadata = tuple(expected_metadata) + (
version_header.to_api_version_header("{{ service.version }}"),
)
{% endif %}
assert pager._metadata == expected_metadata

results = list(pager)
assert len(results) == 6
Expand Down
11 changes: 11 additions & 0 deletions gapic/schema/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1734,6 +1734,17 @@ def host(self) -> str:
return self.options.Extensions[client_pb2.default_host]
return ''

@property
def version(self) -> str:
"""Return the API version for this service, if specified.

Returns:
str: The API version for this service.
"""
if self.options.Extensions[client_pb2.api_version]:
return self.options.Extensions[client_pb2.api_version]
return ''

@property
def shortname(self) -> str:
"""Return the API short name. DRIFT uses this to identify
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
# limitations under the License.
#}

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

{% macro client_method(method, name, snippet_index, api, service, full_extended_lro=False) %}
def {{ name }}(self,
{% if not method.client_streaming %}
Expand Down Expand Up @@ -183,7 +185,8 @@
)
{% endif %} {# method.explicit_routing #}

{{ auto_populate_uuid4_fields(api, method) }}
{{ shared_macros.add_api_version_header_to_metadata(service.version) }}
{{ shared_macros.auto_populate_uuid4_fields(api, method) }}

# Validate the universe domain.
self._validate_universe_domain()
Expand Down Expand Up @@ -267,27 +270,3 @@

{% macro define_extended_operation_subclass(extended_operation) %}
{% endmacro %}

{% macro auto_populate_uuid4_fields(api, method) %}
{#
Automatically populate UUID4 fields according to
https://google.aip.dev/client-libraries/4235 when the
field satisfies either of:
- The field supports explicit presence and has not been set by the user.
- The field doesn't support explicit presence, and its value is the empty
string (i.e. the default value).
When using this macro, ensure the calling template generates a line `import uuid`
#}
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
{% if method.input.fields[auto_populated_field].proto3_optional %}
if '{{ auto_populated_field }}' not in request:
{% else %}
if not request.{{ auto_populated_field }}:
{% endif %}
request.{{ auto_populated_field }} = str(uuid.uuid4())
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
{% endmacro %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{#
# 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.
#}

{% macro auto_populate_uuid4_fields(api, method) %}
{#
Automatically populate UUID4 fields according to
https://google.aip.dev/client-libraries/4235 when the
field satisfies either of:
- The field supports explicit presence and has not been set by the user.
- The field doesn't support explicit presence, and its value is the empty
string (i.e. the default value).
When using this macro, ensure the calling template generates a line `import uuid`
#}
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
{% if method.input.fields[auto_populated_field].proto3_optional %}
if '{{ auto_populated_field }}' not in request:
{% else %}
if not request.{{ auto_populated_field }}:
{% endif %}
request.{{ auto_populated_field }} = str(uuid.uuid4())
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
{% endmacro %}

{% macro add_google_api_core_version_header_import(service_version) %}
{#
The `version_header` module was added to `google-api-core`
in version 2.19.0.
https://github.com/googleapis/python-api-core/releases/tag/v2.19.0
The `try/except` below can be removed once the minimum version of
`google-api-core` is 2.19.0 or newer.
#}
{% if service_version %}
try:
from google.api_core import version_header
HAS_GOOGLE_API_CORE_VERSION_HEADER = True # pragma: NO COVER
except ImportError: # pragma: NO COVER
HAS_GOOGLE_API_CORE_VERSION_HEADER = False
{% endif %}{# service_version #}
{% endmacro %}

{% macro add_api_version_header_to_metadata(service_version) %}
{#
Add API Version to metadata as per https://github.com/aip-dev/google.aip.dev/pull/1331.
When using this macro, ensure the calling template also calls macro
`add_google_api_core_version_header_import` to add the necessary import statements.
#}
{% if service_version %}
if HAS_GOOGLE_API_CORE_VERSION_HEADER: # pragma: NO COVER
metadata = tuple(metadata) + (
version_header.to_api_version_header("{{ service_version }}"),
)
{% endif %}{# service_version #}
{% endmacro %}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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

from collections import OrderedDict
import functools
Expand All @@ -24,6 +25,7 @@ from google.api_core import retry_async as retries
from google.auth import credentials as ga_credentials # type: ignore
from google.oauth2 import service_account # type: ignore

{{ shared_macros.add_google_api_core_version_header_import(service.version) }}
try:
OptionalRetry = Union[retries.AsyncRetry, gapic_v1.method._MethodDefault, None]
except AttributeError: # pragma: NO COVER
Expand Down Expand Up @@ -54,7 +56,8 @@ from .client import {{ service.client_name }}

{# TODO(yon-mg): handle rest transport async client interaction #}
class {{ service.async_client_name }}:
"""{{ service.meta.doc|rst(width=72, indent=4) }}"""
"""{{ service.meta.doc|rst(width=72, indent=4) }}{% if service.version|length %}
This class implements API version {{ service.version }}.{% endif %}"""

_client: {{ service.client_name }}

Expand Down Expand Up @@ -388,7 +391,8 @@ class {{ service.async_client_name }}:
)
{% endif %}

{{ macros.auto_populate_uuid4_fields(api, method) }}
{{ shared_macros.add_api_version_header_to_metadata(service.version) }}
{{ shared_macros.auto_populate_uuid4_fields(api, method) }}

# Validate the universe domain.
self._client._validate_universe_domain()
Expand Down
Loading