Skip to content

Commit

Permalink
feat: add support for reading google.api.api_version (#1999)
Browse files Browse the repository at this point in the history
Co-authored-by: Victor Chudnovsky <vchudnov@google.com>
  • Loading branch information
parthea and vchudnov-g authored May 8, 2024
1 parent 617222d commit b2486e5
Show file tree
Hide file tree
Showing 33 changed files with 501 additions and 140 deletions.
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,76 @@
{#
# 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.
#
# This file is a copy of `_shared_macros.j2` in standard templates located at
# `gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2`
# It is intended to be a symlink.
# See https://github.com/googleapis/gapic-generator-python/issues/2028
# which contains follow up work to convert it to a symlink.
# Do not diverge from the copy of `_shared_macros.j2` in standard templates.
#}

{% 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
@@ -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 @@ -181,7 +183,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 @@ -265,27 +268,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 %}
Loading

0 comments on commit b2486e5

Please sign in to comment.