Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
79f3668
feat(api-core): move request-id auto-population logic to gapic_v1 pub…
hebaalazzeh Jul 16, 2026
62fbcc8
refactor(api-core): rename gapic_v1.method_helpers to gapic_v1.reques…
hebaalazzeh Jul 16, 2026
20adea6
chore: address PR review comments for gapic centralization request ID
hebaalazzeh Jul 16, 2026
f03ac7c
feat(api-core): move request-id auto-population logic to gapic_v1 pub…
hebaalazzeh Jul 16, 2026
2cc27b5
feat: delegate request-id setup to public method_helpers
hebaalazzeh Jul 16, 2026
9c2121c
refactor: use request module instead of method_helpers in gapic-gener…
hebaalazzeh Jul 16, 2026
dbc7307
refactor(api-core): rename gapic_v1.method_helpers to gapic_v1.reques…
hebaalazzeh Jul 16, 2026
5f1b721
chore: address PR review comments for gapic centralization request ID
hebaalazzeh Jul 16, 2026
99cee8e
chore: remove private _setup_request_id and call setup_request_id hel…
hebaalazzeh Jul 16, 2026
4ff1910
test(api-core): update UUID regex in test_request.py
hebaalazzeh Jul 16, 2026
77f97c8
refactor(api-core): rename request.py to requests.py for consistency
hebaalazzeh Jul 17, 2026
7fdf94e
Merge branch 'feat/gapic-centralization-api-core-request-id' into fea…
hebaalazzeh Jul 17, 2026
37ad06f
refactor(generator): update templates and goldens to use requests ins…
hebaalazzeh Jul 17, 2026
b9e2c62
fix(api-core): handle dictionary requests with None value in setup_re…
hebaalazzeh Jul 17, 2026
6bb65bc
merge branch 'feat/gapic-centralization-api-core-request-id' into fea…
hebaalazzeh Jul 17, 2026
0482ae6
fix(api-core): remove redundant request.py and test_request.py, sort …
hebaalazzeh Jul 17, 2026
31ae5ec
Merge remote-tracking branch 'origin/main' into feat/gapic-generator-…
hebaalazzeh Jul 20, 2026
2001c79
feat(generator): add _compat.py fallback for setup_request_id
hebaalazzeh Jul 20, 2026
2d1d2a5
fix(generator): add type ignore to requests import in compat template…
hebaalazzeh Jul 21, 2026
9e169f8
fix(generator): add coverage pragma comments for fallback path in com…
hebaalazzeh Jul 21, 2026
5af5222
Merge remote-tracking branch 'origin/main' into feat/gapic-generator-…
hebaalazzeh Jul 21, 2026
6ef6dd4
Merge remote-tracking branch 'origin/main' into feat/gapic-generator-…
hebaalazzeh Jul 21, 2026
61aaabb
chore(generator): update golden client.py files with setup_request_id…
hebaalazzeh Jul 21, 2026
0b2ceb2
fix(generator): add utf-8 coding header to _compat.py.j2 template
hebaalazzeh Jul 21, 2026
3204cea
chore(generator): update storagebatchoperations client.py and async_c…
hebaalazzeh Jul 21, 2026
3e6b78c
refactor(generator): optimize setup_request_id uuid creation and link…
hebaalazzeh Jul 21, 2026
1e6b56e
docs(generator): update TODO issue link to #17813 in _compat.py
hebaalazzeh Jul 21, 2026
ea8d263
chore: trigger Kokoro system tests build rerun
hebaalazzeh Jul 21, 2026
8e9f505
fix(generator): add pragma no cover to setup_request_id definition in…
hebaalazzeh Jul 21, 2026
142d337
fix(generator): add module level pragma no cover to _compat.py to pre…
hebaalazzeh Jul 21, 2026
e5f8e13
fix(generator): only generate _compat.py if API schema contains auto_…
hebaalazzeh Jul 21, 2026
905d2b1
fix(generator): fix proto-plus field check in setup_request_id fallback
hebaalazzeh Jul 21, 2026
7b7668c
fix(generator): revert goldens without auto_populated_fields and fix …
hebaalazzeh Jul 21, 2026
536a20e
fix(generator): remove encoding and pragma no cover from _compat.py h…
hebaalazzeh Jul 21, 2026
a8105ff
fix(generator): remove unused get_uuid4_re macro
hebaalazzeh Jul 21, 2026
e243bbd
fix(generator): update fallback proto-plus check to exactly match api…
hebaalazzeh Jul 21, 2026
926eb3c
fix(generator): rename requests import to request to match api-core
hebaalazzeh Jul 21, 2026
22d569d
fix(generator): fix broken diff in client.py and duplicate wrapper in…
hebaalazzeh Jul 21, 2026
c4eb2d9
test(generator): add tests for _compat.py fallback
hebaalazzeh Jul 21, 2026
ebc6bd9
fix(generator): update storagebatchoperations goldens to reflect setu…
hebaalazzeh Jul 21, 2026
eb08c77
fix(generator): add storagebatchoperations goldens missing files
hebaalazzeh Jul 21, 2026
e052b61
fix(generator): address remaining PR review comments
hebaalazzeh Jul 21, 2026
44e43ce
fix(generator): always generate _compat.py and remove downstream unit…
hebaalazzeh Jul 21, 2026
6f07478
fix(generator): use unified _compat.py.j2
hebaalazzeh Jul 21, 2026
82aa10c
fix(generator): install local google-api-core in nox test sessions
hebaalazzeh Jul 21, 2026
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
5 changes: 4 additions & 1 deletion packages/gapic-generator/gapic/generator/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def get_response(self, api_schema: api.API, opts: Options) -> CodeGeneratorRespo
for template_name in client_templates:
# Quick check: Skip "private" templates.
filename = template_name.split("/")[-1]
if filename.startswith("_") and filename != "__init__.py.j2":
if filename.startswith("_") and filename not in ("__init__.py.j2", "_compat.py.j2"):
continue

# Append to the output files dictionary.
Expand Down Expand Up @@ -266,6 +266,9 @@ def _render_template(
if not opts.metadata and template_name.endswith("gapic_metadata.json.j2"):
return answer




# Disables generation of an unversioned Python package for this client
# library. This means that the module names will need to be versioned in
# import statements. For example `import google.cloud.library_v2` instead
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
# {% include '_license.j2' %}

"""A compatibility module for older versions of google-api-core."""

import functools
import json
import operator
import os
import re
import uuid
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
from google.auth.exceptions import MutualTLSChannelError
import google.protobuf.message


try:
from google.api_core.universe import (
get_default_mtls_endpoint,
get_api_endpoint,
get_universe_domain,
)
except ImportError:
# TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version.
def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]:
"""Converts api endpoint to mTLS endpoint."""
if not api_endpoint:
return api_endpoint

mtls_endpoint_re = re.compile(
r"(?P<name>[^.]+)(?P<mtls>\.mtls)?(?P<sandbox>\.sandbox)?(?P<googledomain>\.googleapis\.com)?"
)

m = mtls_endpoint_re.match(api_endpoint)
if m is None:
# Could not parse api_endpoint; return as-is.
return api_endpoint

name, mtls, sandbox, googledomain = m.groups()
if mtls or not googledomain:
return api_endpoint

if sandbox:
return api_endpoint.replace(
"sandbox.googleapis.com", "mtls.sandbox.googleapis.com"
)

return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com")

def get_api_endpoint(
api_override: Optional[str],
client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]],
universe_domain: str,
use_mtls_endpoint: str,
default_universe: str,
default_mtls_endpoint: Optional[str],
default_endpoint_template: str,
) -> Optional[str]:
"""Return the API endpoint used by the client."""
if api_override is not None:
api_endpoint = api_override
elif use_mtls_endpoint == "always" or (use_mtls_endpoint == "auto" and client_cert_source):
if universe_domain != default_universe:
raise MutualTLSChannelError(
f"mTLS is not supported in any universe other than {default_universe}."
)
api_endpoint = default_mtls_endpoint
else:
api_endpoint = default_endpoint_template.format(UNIVERSE_DOMAIN=universe_domain)
return api_endpoint

def get_universe_domain(
client_universe_domain: Optional[str],
universe_domain_env: Optional[str],
default_universe: str,
) -> str:
"""Return the universe domain used by the client."""
universe_domain = default_universe
if client_universe_domain is not None:
universe_domain = client_universe_domain
elif universe_domain_env is not None:
universe_domain = universe_domain_env
if len(universe_domain.strip()) == 0:
raise ValueError("Universe Domain cannot be an empty string.")
return universe_domain


try:
from google.api_core.gapic_v1.config import (
use_client_cert_effective,
get_client_cert_source,
read_environment_variables,
)
except ImportError:
from google.auth.transport import mtls # type: ignore

# TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version.

def use_client_cert_effective() -> bool:
"""Returns whether client certificate should be used for mTLS."""
if hasattr(mtls, "should_use_client_cert"):
return mtls.should_use_client_cert()
else:
use_client_cert_str = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false").lower()
if use_client_cert_str not in ("true", "false"):
raise ValueError(
"Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be"
" either `true` or `false`"
)
return use_client_cert_str == "true"

def get_client_cert_source(
provided_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]],
use_cert_flag: bool,
) -> Optional[Callable[[], Tuple[bytes, bytes]]]:
"""Return the client cert source to be used by the client."""
client_cert_source = None
if use_cert_flag:
if provided_cert_source:
client_cert_source = provided_cert_source
elif (
hasattr(mtls, "has_default_client_cert_source")
and mtls.has_default_client_cert_source()
):
client_cert_source = mtls.default_client_cert_source()
else:
raise ValueError(
"Client certificate is required for mTLS, but no client certificate source was provided or found."
)
return client_cert_source

def read_environment_variables() -> Tuple[bool, str, Optional[str]]:
"""Returns the environment variables used by the client."""
use_client_cert = use_client_cert_effective()
use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower()
universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN")
if use_mtls_endpoint not in ("auto", "never", "always"):
raise MutualTLSChannelError(
"Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` "
"must be `never`, `auto` or `always`"
)
return use_client_cert, use_mtls_endpoint, universe_domain_env


try:
from google.api_core.gapic_v1.request import setup_request_id # type: ignore
except ImportError:
# TODO(https://github.com/googleapis/google-cloud-python/issues/17813): Remove this fallback when google-api-core >= 2.26.0 is the minimum required version.
def setup_request_id(request, field_name: str, is_proto3_optional: bool):
"""Populate a UUID4 field in the request if it is not already set.

Args:
request (Union[google.protobuf.message.Message, dict]): The request object.
field_name (str): The name of the field to populate.
is_proto3_optional (bool): Whether the field is proto3 optional.
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

do we need the check below (similar to what we have in api-core)

if request is None:
    return

request_id_val = str(uuid.uuid4())
if request is None:
return

if isinstance(request, dict):
if is_proto3_optional:
if field_name not in request or request[field_name] is None:
request[field_name] = request_id_val
elif not request.get(field_name):
request[field_name] = request_id_val
return

if is_proto3_optional:
try:
# Pure protobuf messages
if not request.HasField(field_name):
setattr(request, field_name, request_id_val)
except (AttributeError, ValueError):
# Proto-plus messages or other objects
if getattr(request, field_name, None) is None:
setattr(request, field_name, request_id_val)
else:
if not getattr(request, field_name, None):
setattr(request, field_name, request_id_val)


try:
from google.api_core.rest_helpers import (
flatten_query_params,
transcode_request,
)
except ImportError: # pragma: NO COVER
# TODO: Remove these fallbacks when google-api-core >= 2.18.0 is the minimum required version.
from google.protobuf import json_format # type: ignore
from google.api_core import path_template # type: ignore

def flatten_query_params(obj, strict=False): # pragma: NO COVER
if obj is not None and not isinstance(obj, dict):
raise TypeError("flatten_query_params must be called with dict object")
return _flatten(obj, key_path=[], strict=strict)

def _flatten(obj, key_path, strict=False): # pragma: NO COVER
if obj is None:
return []
if isinstance(obj, dict):
return _flatten_dict(obj, key_path=key_path, strict=strict)
if isinstance(obj, list):
return _flatten_list(obj, key_path=key_path, strict=strict)
return _flatten_value(obj, key_path=key_path, strict=strict)

def _is_primitive_value(obj): # pragma: NO COVER
if obj is None:
return False
if isinstance(obj, (list, dict)):
raise ValueError("query params may not contain repeated dicts or lists")
return True

def _flatten_value(obj, key_path, strict=False): # pragma: NO COVER
return [(".".join(key_path), _canonicalize(obj, strict=strict))]

def _flatten_dict(obj, key_path, strict=False): # pragma: NO COVER
items = (
_flatten(value, key_path=key_path + [key], strict=strict)
for key, value in obj.items()
)
return functools.reduce(operator.concat, items, [])

def _flatten_list(elems, key_path, strict=False): # pragma: NO COVER
items = (
_flatten_value(elem, key_path=key_path, strict=strict)
for elem in elems
if _is_primitive_value(elem)
)
return functools.reduce(operator.concat, items, [])

def _canonicalize(obj, strict=False): # pragma: NO COVER
if strict:
value = str(obj)
if isinstance(obj, bool):
value = value.lower()
return value
return obj

def transcode_request( # pragma: NO COVER
http_options: List[Dict[str, str]],
request: Any,
required_fields_default_values: Optional[Dict[str, Any]] = None,
rest_numeric_enums: bool = False,
) -> Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]:
pb_request = getattr(request, "_pb", request)
transcoded_request = path_template.transcode(http_options, pb_request)

body_json = None
if transcoded_request.get("body") is not None:
body_json = json_format.MessageToJson(
transcoded_request["body"],
use_integers_for_enums=rest_numeric_enums,
)

query_params_json = {}
if transcoded_request.get("query_params") is not None:
query_params_json = json.loads(json_format.MessageToJson(
transcoded_request["query_params"],
use_integers_for_enums=rest_numeric_enums,
))

if required_fields_default_values:
for k, v in required_fields_default_values.items():
if k not in query_params_json:
query_params_json[k] = v

if rest_numeric_enums:
query_params_json["$alt"] = "json;enum-encoding=int"

return transcoded_request, body_json, query_params_json
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
{% set is_proto3_optional = method.input.fields[auto_populated_field].proto3_optional %}
{% if is_async %}
self._client._setup_request_id(request, '{{ auto_populated_field }}', {{ is_proto3_optional }})
{% else %}
self._setup_request_id(request, '{{ auto_populated_field }}', {{ is_proto3_optional }})
{% endif %}
setup_request_id(request, '{{ auto_populated_field }}', {{ is_proto3_optional }})
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
{% 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 %}

{% set has_auto_populated_fields = namespace(value=false) %}
{% for method in service.methods.values() %}
{% set method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings and method_settings.auto_populated_fields %}
{% set has_auto_populated_fields.value = true %}
{% endif %}
{% endfor %}

import logging as std_logging
from collections import OrderedDict
import re
from typing import Dict, Callable, Mapping, MutableMapping, MutableSequence, Optional, {% if service.any_server_streaming %}AsyncIterable, Awaitable, {% endif %}{% if service.any_client_streaming %}AsyncIterator, {% endif %}Sequence, Tuple, Type, Union
{% if api.all_method_settings.values()|map(attribute="auto_populated_fields", default=[])|select|list %}
import uuid
{% endif %}
{% if service.any_deprecated %}
import warnings
{% endif %}
Expand All @@ -21,6 +26,9 @@ from {{package_path}} import gapic_version as package_version
from google.api_core.client_options import ClientOptions
from google.api_core import exceptions as core_exceptions
from google.api_core import gapic_v1
{% if has_auto_populated_fields.value %}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do we only generate this file when has_auto_populated_fields is true?

from {{package_path}}._compat import setup_request_id
{% endif %}
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
Expand Down
Loading
Loading