Skip to content

Commit 8816dfe

Browse files
committed
feat(api-core,generator): centralize endpoint routing and universe domain resolution
1 parent d461da7 commit 8816dfe

32 files changed

Lines changed: 719 additions & 1438 deletions

File tree

packages/gapic-generator/gapic/generator/generator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def get_response(self, api_schema: api.API, opts: Options) -> CodeGeneratorRespo
120120
for template_name in client_templates:
121121
# Quick check: Skip "private" templates.
122122
filename = template_name.split("/")[-1]
123-
if filename.startswith("_") and filename != "__init__.py.j2":
123+
if filename.startswith("_") and filename not in ("__init__.py.j2", "_compat.py.j2"):
124124
continue
125125

126126
# Append to the output files dictionary.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# {% include '_license.j2' %}
2+
3+
import re
4+
from typing import Optional, Callable, Tuple, Union
5+
from google.auth.exceptions import MutualTLSChannelError
6+
7+
try:
8+
from google.api_core.gapic_v1.client_utils import (
9+
get_default_mtls_endpoint,
10+
get_api_endpoint,
11+
get_universe_domain,
12+
)
13+
except ImportError:
14+
# TODO: Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version.
15+
16+
def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]:
17+
"""Converts api endpoint to mTLS endpoint."""
18+
if not api_endpoint:
19+
return api_endpoint
20+
21+
mtls_endpoint_re = re.compile(
22+
r"(?P<name>[^.]+)(?P<mtls>\.mtls)?(?P<sandbox>\.sandbox)?(?P<googledomain>\.googleapis\.com)?"
23+
)
24+
25+
m = mtls_endpoint_re.match(api_endpoint)
26+
if m is None:
27+
# Could not parse api_endpoint; return as-is.
28+
return api_endpoint
29+
30+
name, mtls, sandbox, googledomain = m.groups()
31+
if mtls or not googledomain:
32+
return api_endpoint
33+
34+
if sandbox:
35+
return api_endpoint.replace(
36+
"sandbox.googleapis.com", "mtls.sandbox.googleapis.com"
37+
)
38+
39+
return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com")
40+
41+
def get_api_endpoint(
42+
api_override: Optional[str],
43+
client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]],
44+
universe_domain: str,
45+
use_mtls_endpoint: str,
46+
default_universe: str,
47+
default_mtls_endpoint: str,
48+
default_endpoint_template: str,
49+
) -> str:
50+
"""Return the API endpoint used by the client."""
51+
if api_override is not None:
52+
api_endpoint = api_override
53+
elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source):
54+
if universe_domain != default_universe:
55+
raise MutualTLSChannelError(
56+
f"mTLS is not supported in any universe other than {default_universe}."
57+
)
58+
api_endpoint = default_mtls_endpoint
59+
else:
60+
api_endpoint = default_endpoint_template.format(UNIVERSE_DOMAIN=universe_domain)
61+
return api_endpoint
62+
63+
def get_universe_domain(
64+
client_universe_domain: Optional[str],
65+
universe_domain_env: Optional[str],
66+
default_universe: str,
67+
) -> str:
68+
"""Return the universe domain used by the client."""
69+
universe_domain = default_universe
70+
if client_universe_domain is not None:
71+
universe_domain = client_universe_domain
72+
elif universe_domain_env is not None:
73+
universe_domain = universe_domain_env
74+
if len(universe_domain.strip()) == 0:
75+
raise ValueError("Universe Domain cannot be an empty string.")
76+
return universe_domain

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

Lines changed: 24 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ from google.api_core import exceptions as core_exceptions
3030
from google.api_core import extended_operation
3131
{% endif %}
3232
from google.api_core import gapic_v1
33+
from {{package_path}} import _compat as client_utils
3334
from google.api_core import retry as retries
3435
from google.auth import credentials as ga_credentials # type: ignore
3536
from google.auth.transport import mtls # type: ignore
@@ -143,44 +144,10 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
143144
"""{{ service.meta.doc|rst(width=72, indent=4) }}{% if service.version|length %}
144145
This class implements API version {{ service.version }}.{% endif %}"""
145146

146-
@staticmethod
147-
def _get_default_mtls_endpoint(api_endpoint) -> Optional[str]:
148-
"""Converts api endpoint to mTLS endpoint.
149-
150-
Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to
151-
"*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively.
152-
Args:
153-
api_endpoint (Optional[str]): the api endpoint to convert.
154-
Returns:
155-
Optional[str]: converted mTLS api endpoint.
156-
"""
157-
if not api_endpoint:
158-
return api_endpoint
159-
160-
mtls_endpoint_re = re.compile(
161-
r"(?P<name>[^.]+)(?P<mtls>\.mtls)?(?P<sandbox>\.sandbox)?(?P<googledomain>\.googleapis\.com)?"
162-
)
163-
164-
m = mtls_endpoint_re.match(api_endpoint)
165-
if m is None:
166-
# Could not parse api_endpoint; return as-is.
167-
return api_endpoint
168-
169-
name, mtls, sandbox, googledomain = m.groups()
170-
if mtls or not googledomain:
171-
return api_endpoint
172-
173-
if sandbox:
174-
return api_endpoint.replace(
175-
"sandbox.googleapis.com", "mtls.sandbox.googleapis.com"
176-
)
177-
178-
return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com")
179-
180147
# Note: DEFAULT_ENDPOINT is deprecated. Use _DEFAULT_ENDPOINT_TEMPLATE instead.
181148
DEFAULT_ENDPOINT = {% if service.host %}"{{ service.host }}"{% else %}None{% endif %}
182149

183-
DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore
150+
DEFAULT_MTLS_ENDPOINT = client_utils.get_default_mtls_endpoint(
184151
DEFAULT_ENDPOINT
185152
)
186153

@@ -392,30 +359,34 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
392359
return client_cert_source
393360

394361
@staticmethod
395-
def _get_api_endpoint(api_override, client_cert_source, universe_domain, use_mtls_endpoint) -> str:
362+
def _get_api_endpoint(
363+
api_override: Optional[str],
364+
client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]],
365+
universe_domain: str,
366+
use_mtls_endpoint: str,
367+
) -> str:
396368
"""Return the API endpoint used by the client.
397369

398370
Args:
399-
api_override (str): The API endpoint override. If specified, this is always
371+
api_override (Optional[str]): The API endpoint override. If specified, this is always
400372
the return value of this function and the other arguments are not used.
401-
client_cert_source (bytes): The client certificate source used by the client.
373+
client_cert_source (Union[Callable[[], Tuple[bytes, bytes]], None]): The client certificate source used by the client.
402374
universe_domain (str): The universe domain used by the client.
403375
use_mtls_endpoint (str): How to use the mTLS endpoint, which depends also on the other parameters.
404376
Possible values are "always", "auto", or "never".
405377

406378
Returns:
407379
str: The API endpoint to be used by the client.
408380
"""
409-
if api_override is not None:
410-
api_endpoint = api_override
411-
elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source):
412-
_default_universe = {{ service.client_name }}._DEFAULT_UNIVERSE
413-
if universe_domain != _default_universe:
414-
raise MutualTLSChannelError(f"mTLS is not supported in any universe other than {_default_universe}.")
415-
api_endpoint = {{ service.client_name }}.DEFAULT_MTLS_ENDPOINT
416-
else:
417-
api_endpoint = {{ service.client_name }}._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=universe_domain)
418-
return api_endpoint
381+
return client_utils.get_api_endpoint(
382+
api_override,
383+
client_cert_source,
384+
universe_domain,
385+
use_mtls_endpoint,
386+
{{ service.client_name }}._DEFAULT_UNIVERSE,
387+
{{ service.client_name }}.DEFAULT_MTLS_ENDPOINT,
388+
{{ service.client_name }}._DEFAULT_ENDPOINT_TEMPLATE,
389+
)
419390

420391
@staticmethod
421392
def _get_universe_domain(client_universe_domain: Optional[str], universe_domain_env: Optional[str]) -> str:
@@ -431,14 +402,11 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
431402
Raises:
432403
ValueError: If the universe domain is an empty string.
433404
"""
434-
universe_domain = {{ service.client_name }}._DEFAULT_UNIVERSE
435-
if client_universe_domain is not None:
436-
universe_domain = client_universe_domain
437-
elif universe_domain_env is not None:
438-
universe_domain = universe_domain_env
439-
if len(universe_domain.strip()) == 0:
440-
raise ValueError("Universe Domain cannot be an empty string.")
441-
return universe_domain
405+
return client_utils.get_universe_domain(
406+
client_universe_domain,
407+
universe_domain_env,
408+
default_universe={{ service.client_name }}._DEFAULT_UNIVERSE,
409+
)
442410

443411
def _validate_universe_domain(self):
444412
"""Validates client's and credentials' universe domains are consistent.

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

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -156,22 +156,6 @@ def set_event_loop():
156156
asyncio.set_event_loop(None)
157157

158158

159-
def test__get_default_mtls_endpoint():
160-
api_endpoint = "example.googleapis.com"
161-
api_mtls_endpoint = "example.mtls.googleapis.com"
162-
sandbox_endpoint = "example.sandbox.googleapis.com"
163-
sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com"
164-
non_googleapi = "api.example.com"
165-
custom_endpoint = ".custom"
166-
167-
assert {{ service.client_name }}._get_default_mtls_endpoint(None) is None
168-
assert {{ service.client_name }}._get_default_mtls_endpoint(api_endpoint) == api_mtls_endpoint
169-
assert {{ service.client_name }}._get_default_mtls_endpoint(api_mtls_endpoint) == api_mtls_endpoint
170-
assert {{ service.client_name }}._get_default_mtls_endpoint(sandbox_endpoint) == sandbox_mtls_endpoint
171-
assert {{ service.client_name }}._get_default_mtls_endpoint(sandbox_mtls_endpoint) == sandbox_mtls_endpoint
172-
assert {{ service.client_name }}._get_default_mtls_endpoint(non_googleapi) == non_googleapi
173-
assert {{ service.client_name }}._get_default_mtls_endpoint(custom_endpoint) == custom_endpoint
174-
175159
def test__read_environment_variables():
176160
assert {{ service.client_name }}._read_environment_variables() == (False, "auto", None)
177161

@@ -313,29 +297,7 @@ def test__get_client_cert_source():
313297
assert {{ service.client_name }}._get_client_cert_source(None, True) is mock_default_cert_source
314298
assert {{ service.client_name }}._get_client_cert_source(mock_provided_cert_source, "true") is mock_provided_cert_source
315299

316-
@mock.patch.object({{ service.client_name }}, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template({{ service.client_name }}))
317-
{% if 'grpc' in opts.transport %}
318-
@mock.patch.object({{ service.async_client_name }}, "_DEFAULT_ENDPOINT_TEMPLATE", modify_default_endpoint_template({{ service.async_client_name }}))
319-
{% endif %}
320-
def test__get_api_endpoint():
321-
api_override = "foo.com"
322-
mock_client_cert_source = mock.Mock()
323-
default_universe = {{ service.client_name }}._DEFAULT_UNIVERSE
324-
default_endpoint = {{ service.client_name }}._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=default_universe)
325-
mock_universe = "bar.com"
326-
mock_endpoint = {{ service.client_name }}._DEFAULT_ENDPOINT_TEMPLATE.format(UNIVERSE_DOMAIN=mock_universe)
327-
328-
assert {{ service.client_name }}._get_api_endpoint(api_override, mock_client_cert_source, default_universe, "always") == api_override
329-
assert {{ service.client_name }}._get_api_endpoint(None, mock_client_cert_source, default_universe, "auto") == {{ service.client_name }}.DEFAULT_MTLS_ENDPOINT
330-
assert {{ service.client_name }}._get_api_endpoint(None, None, default_universe, "auto") == default_endpoint
331-
assert {{ service.client_name }}._get_api_endpoint(None, None, default_universe, "always") == {{ service.client_name }}.DEFAULT_MTLS_ENDPOINT
332-
assert {{ service.client_name }}._get_api_endpoint(None, mock_client_cert_source, default_universe, "always") == {{ service.client_name }}.DEFAULT_MTLS_ENDPOINT
333-
assert {{ service.client_name }}._get_api_endpoint(None, None, mock_universe, "never") == mock_endpoint
334-
assert {{ service.client_name }}._get_api_endpoint(None, None, default_universe, "never") == default_endpoint
335300

336-
with pytest.raises(MutualTLSChannelError) as excinfo:
337-
{{ service.client_name }}._get_api_endpoint(None, mock_client_cert_source, mock_universe, "auto")
338-
assert str(excinfo.value) == "mTLS is not supported in any universe other than googleapis.com."
339301

340302
{% if service.version %}
341303
{% for method in service.methods.values() %}{% with method_name = method.name|snake_case %}
@@ -384,17 +346,7 @@ def test_{{ method_name }}_api_version_header(transport_name):
384346
{% endfor %}
385347
{% endif %}{# service.version #}
386348

387-
def test__get_universe_domain():
388-
client_universe_domain = "foo.com"
389-
universe_domain_env = "bar.com"
390-
391-
assert {{ service.client_name }}._get_universe_domain(client_universe_domain, universe_domain_env) == client_universe_domain
392-
assert {{ service.client_name }}._get_universe_domain(None, universe_domain_env) == universe_domain_env
393-
assert {{ service.client_name }}._get_universe_domain(None, None) == {{ service.client_name }}._DEFAULT_UNIVERSE
394349

395-
with pytest.raises(ValueError) as excinfo:
396-
{{ service.client_name }}._get_universe_domain("", None)
397-
assert str(excinfo.value) == "Universe Domain cannot be an empty string."
398350

399351
@pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [
400352
(401, CRED_INFO_JSON, True),

0 commit comments

Comments
 (0)