Skip to content

Commit d0c1906

Browse files
committed
Merge branch 'main' into feat/gapic-generator-centralization-routing
2 parents b7ca762 + 88efc9d commit d0c1906

58 files changed

Lines changed: 994 additions & 355 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
permissions:
2+
contents: read
3+
4+
on:
5+
pull_request:
6+
paths:
7+
- 'packages/sqlalchemy-spanner/**'
8+
- '.github/workflows/sqlalchemy-spanner-compliance-tests-against-emulator.yml'
9+
push:
10+
branches:
11+
- main
12+
paths:
13+
- 'packages/sqlalchemy-spanner/**'
14+
- '.github/workflows/sqlalchemy-spanner-compliance-tests-against-emulator.yml'
15+
16+
defaults:
17+
run:
18+
working-directory: packages/sqlalchemy-spanner
19+
20+
name: Run SQLAlchemy Spanner compliance tests against emulator
21+
jobs:
22+
check_changes:
23+
runs-on: ubuntu-latest
24+
outputs:
25+
run_sqlalchemy_spanner: ${{ steps.filter.outputs.sqlalchemy_spanner }}
26+
steps:
27+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
28+
with:
29+
persist-credentials: false
30+
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
31+
id: filter
32+
with:
33+
filters: |
34+
sqlalchemy_spanner:
35+
- 'packages/sqlalchemy-spanner/**'
36+
- '.github/workflows/sqlalchemy-spanner-compliance-tests-against-emulator.yml'
37+
38+
compliance-tests:
39+
needs: check_changes
40+
if: ${{ needs.check_changes.outputs.run_sqlalchemy_spanner == 'true' }}
41+
runs-on: ubuntu-latest
42+
43+
services:
44+
emulator:
45+
image: gcr.io/cloud-spanner-emulator/emulator:latest # zizmor: ignore[unpinned-images]
46+
ports:
47+
- 9010:9010
48+
- 9020:9020
49+
50+
steps:
51+
- name: Checkout code
52+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
53+
with:
54+
persist-credentials: false
55+
- name: Set up Python 3.14
56+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
57+
with:
58+
python-version: "3.14"
59+
- name: Install nox
60+
run: python -m pip install nox
61+
- name: Run nox compliance_test_20
62+
run: nox -s compliance_test_20
63+
env:
64+
SPANNER_EMULATOR_HOST: localhost:9010
65+
GOOGLE_CLOUD_PROJECT: emulator-test-project
66+
GOOGLE_CLOUD_TESTS_CREATE_SPANNER_INSTANCE: true

.librarian/generator-input/client-post-processing/remove-unused-imports.yaml

Lines changed: 0 additions & 33 deletions
This file was deleted.

librarian.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,6 @@ libraries:
660660
apis:
661661
- path: google/bigtable/v2
662662
- path: google/bigtable/admin/v2
663-
skip_generate: true
664663
python:
665664
library_type: GAPIC_COMBO
666665
opt_args_by_api:

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ from google.longrunning import operations_pb2 # type: ignore
5757
{% endif %}
5858
from google.api_core import gapic_v1
5959
{% for method in service.methods.values() %}
60-
{% for ref_type in method.ref_types
60+
{% for ref_type in method.flat_ref_types
6161
if not ((ref_type.ident.python_import.package == ('google', 'api_core') and ref_type.ident.python_import.module == 'operation')
6262
or ref_type.ident.python_import.package == ('google', 'protobuf') and ref_type.ident.python_import.module == 'empty_pb2') %}
6363
{{ ref_type.ident.python_import }}

packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
{% block content %}
44

55
"""A compatibility module for older versions of google-api-core."""
6+
{% set has_auto_populated_fields = api.all_method_settings.values()|map(attribute="auto_populated_fields", default=[])|select|list %}
67
{# TODO(https://github.com/googleapis/google-cloud-python/issues/17813):
78
Clean up this file/functions when the minimum supported version of
89
google-api-core has the functions in `_compat.py.j2`. #}
@@ -13,10 +14,13 @@ falling back to the local implementation if not present. #}
1314
Backfill compatibility functions being removed from the client layer. #}
1415
"""A compatibility module for older versions of google-api-core."""
1516

16-
from typing import Optional
17+
import uuid
18+
19+
from typing import Optional, Union
1720
from urllib.parse import urlparse, urlunparse
1821

1922
from google.auth.exceptions import MutualTLSChannelError
23+
import google.protobuf.message
2024

2125

2226
def get_default_mtls_endpoint(api_endpoint: Optional[str]) -> Optional[str]:
@@ -155,4 +159,52 @@ def determine_domain(
155159
default_universe=DEFAULT_UNIVERSE,
156160
)
157161

158-
{% endblock %}
162+
{% endblock %}
163+
{% if has_auto_populated_fields %}
164+
165+
166+
def setup_request_id(
167+
request: Union[google.protobuf.message.Message, dict, None],
168+
field_name: str,
169+
is_proto3_optional: bool,
170+
) -> None:
171+
"""Populate a UUID4 field in the request if it is not already set.
172+
173+
This helper is used to ensure request idempotency by automatically
174+
generating a unique identifier (such as `request_id`) for requests
175+
that support it. If a request is retried, the same identifier can be
176+
sent on subsequent retries, allowing the server to recognize the retried
177+
request and prevent duplicate processing (e.g., creating duplicate
178+
resources).
179+
180+
Args:
181+
request (Union[google.protobuf.message.Message, dict]): The
182+
request object.
183+
field_name (str): The name of the field to populate.
184+
is_proto3_optional (bool): Whether the field is proto3 optional.
185+
"""
186+
if request is None:
187+
return
188+
189+
if isinstance(request, dict):
190+
if is_proto3_optional:
191+
if field_name not in request or request[field_name] is None:
192+
request[field_name] = str(uuid.uuid4())
193+
elif not request.get(field_name):
194+
request[field_name] = str(uuid.uuid4())
195+
return
196+
197+
if is_proto3_optional:
198+
try:
199+
# Pure protobuf messages
200+
if not request.HasField(field_name):
201+
setattr(request, field_name, str(uuid.uuid4()))
202+
except (AttributeError, ValueError):
203+
# Proto-plus messages or other objects
204+
if not getattr(request, field_name, None):
205+
setattr(request, field_name, str(uuid.uuid4()))
206+
else:
207+
if not getattr(request, field_name, None):
208+
setattr(request, field_name, str(uuid.uuid4()))
209+
{% endif %}
210+
{% endblock %}

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,7 @@
2828
{% if method_settings is not none %}
2929
{% for auto_populated_field in method_settings.auto_populated_fields %}
3030
{% set is_proto3_optional = method.input.fields[auto_populated_field].proto3_optional %}
31-
{% if is_async %}
32-
self._client._setup_request_id(request, '{{ auto_populated_field }}', {{ is_proto3_optional }})
33-
{% else %}
34-
self._setup_request_id(request, '{{ auto_populated_field }}', {{ is_proto3_optional }})
35-
{% endif %}
31+
setup_request_id(request, '{{ auto_populated_field }}', {{ is_proto3_optional }})
3632
{% endfor %}
3733
{% endif %}{# if method_settings is not none #}
3834
{% endwith %}{# method_settings #}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ from {{package_path}} import gapic_version as package_version
2121
from google.api_core.client_options import ClientOptions
2222
from google.api_core import exceptions as core_exceptions
2323
from google.api_core import gapic_v1
24+
{% if has_auto_populated_fields %}
25+
from {{package_path}}._compat import setup_request_id
26+
{% endif %}
2427
from google.api_core import retry_async as retries
2528
from google.auth import credentials as ga_credentials # type: ignore
2629
from google.oauth2 import service_account # type: ignore

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

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ from google.api_core import extended_operation
3131
{% endif %}
3232
from google.api_core import gapic_v1
3333
from {{package_path}}._compat import determine_domain, get_api_endpoint, get_default_mtls_endpoint
34+
{% if has_auto_populated_fields %}
35+
from {{package_path}}._compat import setup_request_id
36+
{% endif %}
3437
from google.api_core import retry as retries
3538
from google.auth import credentials as ga_credentials # type: ignore
3639
from google.auth.transport import mtls # type: ignore
@@ -372,39 +375,6 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
372375
# NOTE (b/349488459): universe validation is disabled until further notice.
373376
return True
374377

375-
{% if has_auto_populated_fields %}
376-
377-
@staticmethod
378-
def _setup_request_id(request, field_name: str, is_proto3_optional: bool):
379-
"""Populate a UUID4 field in the request if it is not already set.
380-
381-
Args:
382-
request (Union[google.protobuf.message.Message, dict]): The request object.
383-
field_name (str): The name of the field to populate.
384-
is_proto3_optional (bool): Whether the field is proto3 optional.
385-
"""
386-
if isinstance(request, dict):
387-
if is_proto3_optional:
388-
if field_name not in request:
389-
request[field_name] = str(uuid.uuid4())
390-
elif not request.get(field_name):
391-
request[field_name] = str(uuid.uuid4())
392-
return
393-
394-
if is_proto3_optional:
395-
try:
396-
# Pure protobuf messages
397-
if not request.HasField(field_name):
398-
setattr(request, field_name, str(uuid.uuid4()))
399-
except (AttributeError, ValueError):
400-
# Proto-plus messages or other objects
401-
if field_name not in request:
402-
setattr(request, field_name, str(uuid.uuid4()))
403-
else:
404-
if not getattr(request, field_name):
405-
setattr(request, field_name, str(uuid.uuid4()))
406-
{% endif %}
407-
408378
def _add_cred_info_for_auth_errors(
409379
self,
410380
error: core_exceptions.GoogleAPICallError

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

Lines changed: 1 addition & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ from google.longrunning import operations_pb2 # type: ignore
8686
{% endif %}
8787
from google.api_core import gapic_v1
8888
{% for method in service.methods.values() %}
89-
{% for ref_type in method.ref_types
89+
{% for ref_type in method.flat_ref_types
9090
if not (ref_type.ident.python_import.package == ('google', 'api_core') and ref_type.ident.python_import.module == 'operation') %}
9191
{{ ref_type.ident.python_import }}
9292
{% endfor %}
@@ -386,84 +386,6 @@ def test__add_cred_info_for_auth_errors_no_get_cred_info(error_code):
386386
client._add_cred_info_for_auth_errors(error)
387387
assert error.details == []
388388

389-
{% if has_auto_populated_fields %}
390-
def test__setup_request_id():
391-
class MockRequest:
392-
def __init__(self, **kwargs):
393-
for k, v in kwargs.items():
394-
setattr(self, k, v)
395-
def __contains__(self, key):
396-
return hasattr(self, key)
397-
398-
class MockProtoRequest:
399-
def __init__(self, **kwargs):
400-
for k, v in kwargs.items():
401-
setattr(self, k, v)
402-
def HasField(self, key):
403-
return hasattr(self, key)
404-
405-
# Test with proto3 optional field not in request
406-
request = MockRequest()
407-
{{ service.client_name }}._setup_request_id(request, "request_id", True)
408-
assert re.match(r"{{ test_macros.get_uuid4_re() }}", request.request_id)
409-
410-
# Test with proto3 optional field already in request
411-
request = MockRequest(request_id="already_set")
412-
{{ service.client_name }}._setup_request_id(request, "request_id", True)
413-
assert request.request_id == "already_set"
414-
415-
# Test with non-proto3 optional field empty
416-
request = MockRequest(request_id="")
417-
{{ service.client_name }}._setup_request_id(request, "request_id", False)
418-
assert re.match(r"{{ test_macros.get_uuid4_re() }}", request.request_id)
419-
420-
# Test with non-proto3 optional field already set
421-
request = MockRequest(request_id="already_set")
422-
{{ service.client_name }}._setup_request_id(request, "request_id", False)
423-
assert request.request_id == "already_set"
424-
425-
# Test with proto3 optional field not in request (MockProtoRequest)
426-
request = MockProtoRequest()
427-
{{ service.client_name }}._setup_request_id(request, "request_id", True)
428-
assert re.match(r"{{ test_macros.get_uuid4_re() }}", request.request_id)
429-
430-
# Test with proto3 optional field already in request (MockProtoRequest)
431-
request = MockProtoRequest(request_id="already_set")
432-
{{ service.client_name }}._setup_request_id(request, "request_id", True)
433-
assert request.request_id == "already_set"
434-
435-
# Test with ValueError
436-
class MockValueErrorRequest:
437-
def HasField(self, key):
438-
raise ValueError("Mismatched field")
439-
def __contains__(self, key):
440-
return hasattr(self, key)
441-
442-
request = MockValueErrorRequest()
443-
{{ service.client_name }}._setup_request_id(request, "request_id", True)
444-
assert re.match(r"{{ test_macros.get_uuid4_re() }}", request.request_id)
445-
446-
# Test with dict and proto3 optional field not in request
447-
request = {}
448-
{{ service.client_name }}._setup_request_id(request, "request_id", True)
449-
assert re.match(r"{{ test_macros.get_uuid4_re() }}", request["request_id"])
450-
451-
# Test with dict and proto3 optional field already in request
452-
request = {"request_id": "already_set"}
453-
{{ service.client_name }}._setup_request_id(request, "request_id", True)
454-
assert request["request_id"] == "already_set"
455-
456-
# Test with dict and non-proto3 optional field empty
457-
request = {"request_id": ""}
458-
{{ service.client_name }}._setup_request_id(request, "request_id", False)
459-
assert re.match(r"{{ test_macros.get_uuid4_re() }}", request["request_id"])
460-
461-
# Test with dict and non-proto3 optional field already set
462-
request = {"request_id": "already_set"}
463-
{{ service.client_name }}._setup_request_id(request, "request_id", False)
464-
assert request["request_id"] == "already_set"
465-
466-
{% endif %}
467389
@pytest.mark.parametrize("client_class,transport_name", [
468390
{% if 'grpc' in opts.transport %}
469391
({{ service.client_name }}, "grpc"),

0 commit comments

Comments
 (0)