Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit a1c343c

Browse files
committed
feat: support quota project override via client options
1 parent b5e1b1e commit a1c343c

6 files changed

Lines changed: 80 additions & 18 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
214214
scopes=client_options.scopes,
215215
api_mtls_endpoint=client_options.api_endpoint,
216216
client_cert_source=client_options.client_cert_source,
217+
quota_project_id=client_options.quota_project_id,
217218
)
218219

219220
{% for method in service.methods.values() -%}

gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class {{ service.name }}Transport(abc.ABC):
3333
credentials: credentials.Credentials = None,
3434
credentials_file: typing.Optional[str] = None,
3535
scopes: typing.Optional[typing.Sequence[str]] = AUTH_SCOPES,
36+
quota_project_id: typing.Optional[str] = None,
3637
**kwargs,
3738
) -> None:
3839
"""Instantiate the transport.
@@ -49,6 +50,8 @@ class {{ service.name }}Transport(abc.ABC):
4950
be loaded with :func:`google.auth.load_credentials_from_file`.
5051
This argument is mutually exclusive with credentials.
5152
scope (Optional[Sequence[str]]): A list of scopes.
53+
quota_project_id (Optional[str]): An optional project to use for billing
54+
and quota.
5255
"""
5356
# Save the hostname. Default to port 443 (HTTPS) if none is specified.
5457
if ':' not in host:
@@ -61,9 +64,14 @@ class {{ service.name }}Transport(abc.ABC):
6164
raise exceptions.DuplicateCredentialArgs("'credentials_file' and 'credentials' are mutually exclusive")
6265

6366
if credentials_file is not None:
64-
credentials, _ = auth.load_credentials_from_file(credentials_file, scopes=scopes)
67+
credentials, _ = auth.load_credentials_from_file(
68+
credentials_file,
69+
scopes=scopes,
70+
quota_project_id=quota_project_id
71+
)
72+
6573
elif credentials is None:
66-
credentials, _ = auth.default(scopes=scopes)
74+
credentials, _ = auth.default(scopes=scopes, quota_project_id=quota_project_id)
6775

6876
# Save the credentials.
6977
self._credentials = credentials

gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
4444
scopes: Sequence[str] = None,
4545
channel: grpc.Channel = None,
4646
api_mtls_endpoint: str = None,
47-
client_cert_source: Callable[[], Tuple[bytes, bytes]] = None) -> None:
47+
client_cert_source: Callable[[], Tuple[bytes, bytes]] = None,
48+
quota_project_id: Optional[str] = None) -> None:
4849
"""Instantiate the transport.
4950

5051
Args:
@@ -71,6 +72,8 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
7172
callback to provide client SSL certificate bytes and private key
7273
bytes, both in PEM format. It is ignored if ``api_mtls_endpoint``
7374
is None.
75+
quota_project_id (Optional[str]): An optional project to use for billing
76+
and quota.
7477

7578
Raises:
7679
google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport
@@ -89,7 +92,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
8992
host = api_mtls_endpoint if ":" in api_mtls_endpoint else api_mtls_endpoint + ":443"
9093

9194
if credentials is None:
92-
credentials, _ = auth.default(scopes=self.AUTH_SCOPES)
95+
credentials, _ = auth.default(scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id)
9396

9497
# Create SSL credentials with client_cert_source or application
9598
# default SSL credentials.
@@ -108,14 +111,16 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
108111
credentials_file=credentials_file,
109112
ssl_credentials=ssl_credentials,
110113
scopes=scopes or self.AUTH_SCOPES,
114+
quota_project_id=quota_project_id,
111115
)
112116

113117
# Run the base constructor.
114118
super().__init__(
115119
host=host,
116120
credentials=credentials,
117121
credentials_file=credentials_file,
118-
scopes=scopes or self.AUTH_SCOPES
122+
scopes=scopes or self.AUTH_SCOPES,
123+
quota_project_id=quota_project_id,
119124
)
120125

121126
self._stubs = {} # type: Dict[str, Callable]
@@ -126,6 +131,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
126131
credentials: credentials.Credentials = None,
127132
credentials_file: str = None,
128133
scopes: Optional[Sequence[str]] = None,
134+
quota_project_id: Optional[str] = None,
129135
**kwargs) -> grpc.Channel:
130136
"""Create and return a gRPC channel object.
131137
Args:
@@ -141,6 +147,8 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
141147
scopes (Optional[Sequence[str]]): A optional list of scopes needed for this
142148
service. These are only used when credentials are not specified and
143149
are passed to :func:`google.auth.default`.
150+
quota_project_id (Optional[str]): An optional project to use for billing
151+
and quota.
144152
kwargs (Optional[dict]): Keyword arguments, which are passed to the
145153
channel creation.
146154
Returns:
@@ -156,6 +164,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
156164
credentials=credentials,
157165
credentials_file=credentials_file,
158166
scopes=scopes,
167+
quota_project_id=quota_project_id,
159168
**kwargs
160169
)
161170

gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
4545
credentials: credentials.Credentials = None,
4646
credentials_file: Optional[str] = None,
4747
scopes: Optional[Sequence[str]] = None,
48+
quota_project_id: Optional[str] = None,
4849
**kwargs) -> aio.Channel:
4950
"""Create and return a gRPC AsyncIO channel object.
5051
Args:
@@ -60,6 +61,8 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
6061
scopes (Optional[Sequence[str]]): A optional list of scopes needed for this
6162
service. These are only used when credentials are not specified and
6263
are passed to :func:`google.auth.default`.
64+
quota_project_id (Optional[str]): An optional project to use for billing
65+
and quota.
6366
kwargs (Optional[dict]): Keyword arguments, which are passed to the
6467
channel creation.
6568
Returns:
@@ -71,6 +74,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
7174
credentials=credentials,
7275
credentials_file=credentials_file,
7376
scopes=scopes,
77+
quota_project_id=quota_project_id,
7478
**kwargs
7579
)
7680

@@ -81,7 +85,9 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
8185
scopes: Optional[Sequence[str]] = None,
8286
channel: aio.Channel = None,
8387
api_mtls_endpoint: str = None,
84-
client_cert_source: Callable[[], Tuple[bytes, bytes]] = None) -> None:
88+
client_cert_source: Callable[[], Tuple[bytes, bytes]] = None,
89+
quota_project_id=None,
90+
) -> None:
8591
"""Instantiate the transport.
8692

8793
Args:
@@ -109,6 +115,8 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
109115
callback to provide client SSL certificate bytes and private key
110116
bytes, both in PEM format. It is ignored if ``api_mtls_endpoint``
111117
is None.
118+
quota_project_id (Optional[str]): An optional project to use for billing
119+
and quota.
112120

113121
Raises:
114122
google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport
@@ -143,14 +151,16 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
143151
credentials_file=credentials_file,
144152
ssl_credentials=ssl_credentials,
145153
scopes=scopes or self.AUTH_SCOPES,
154+
quota_project_id=quota_project_id,
146155
)
147156

148157
# Run the base constructor.
149158
super().__init__(
150159
host=host,
151160
credentials=credentials,
152161
credentials_file=credentials_file,
153-
scopes=scopes or self.AUTH_SCOPES
162+
scopes=scopes or self.AUTH_SCOPES,
163+
quota_project_id=quota_project_id,
154164
)
155165

156166
self._stubs = {}

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

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ def test_{{ service.client_name|snake_case }}_client_options(client_class, trans
108108
scopes=None,
109109
api_mtls_endpoint="squid.clam.whelk",
110110
client_cert_source=None,
111+
quota_project_id=None,
111112
)
112113

113114
# Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS is
@@ -123,6 +124,7 @@ def test_{{ service.client_name|snake_case }}_client_options(client_class, trans
123124
scopes=None,
124125
api_mtls_endpoint=client.DEFAULT_ENDPOINT,
125126
client_cert_source=None,
127+
quota_project_id=None,
126128
)
127129

128130
# Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS is
@@ -138,6 +140,7 @@ def test_{{ service.client_name|snake_case }}_client_options(client_class, trans
138140
scopes=None,
139141
api_mtls_endpoint=client.DEFAULT_MTLS_ENDPOINT,
140142
client_cert_source=None,
143+
quota_project_id=None,
141144
)
142145

143146
# Check the case api_endpoint is not provided, GOOGLE_API_USE_MTLS is
@@ -154,6 +157,7 @@ def test_{{ service.client_name|snake_case }}_client_options(client_class, trans
154157
scopes=None,
155158
api_mtls_endpoint=client.DEFAULT_MTLS_ENDPOINT,
156159
client_cert_source=client_cert_source_callback,
160+
quota_project_id=None,
157161

158162
)
159163

@@ -171,6 +175,7 @@ def test_{{ service.client_name|snake_case }}_client_options(client_class, trans
171175
scopes=None,
172176
api_mtls_endpoint=client.DEFAULT_MTLS_ENDPOINT,
173177
client_cert_source=None,
178+
quota_project_id=None,
174179
)
175180

176181
# Check the case api_endpoint is not provided, GOOGLE_API_USE_MTLS is
@@ -187,15 +192,29 @@ def test_{{ service.client_name|snake_case }}_client_options(client_class, trans
187192
scopes=None,
188193
api_mtls_endpoint=client.DEFAULT_ENDPOINT,
189194
client_cert_source=None,
195+
quota_project_id=None,
190196
)
191197

192198
# Check the case api_endpoint is not provided and GOOGLE_API_USE_MTLS has
193199
# unsupported value.
194-
os.environ["GOOGLE_API_USE_MTLS"] = "Unsupported"
195-
with pytest.raises(MutualTLSChannelError):
196-
client = client_class()
200+
with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS": "Unsupported"}):
201+
with pytest.raises(MutualTLSChannelError):
202+
client = client_class()
197203

198-
del os.environ["GOOGLE_API_USE_MTLS"]
204+
# Check the case quota_project_id is provided
205+
options = client_options.ClientOptions(quota_project_id="octopus")
206+
with mock.patch.object(transport_class, '__init__') as patched:
207+
patched.return_value = None
208+
client = client_class(client_options=options)
209+
patched.assert_called_once_with(
210+
credentials=None,
211+
credentials_file=None,
212+
host=client.DEFAULT_ENDPOINT,
213+
scopes=None,
214+
api_mtls_endpoint=client.DEFAULT_ENDPOINT,
215+
client_cert_source=None,
216+
quota_project_id="octopus",
217+
)
199218

200219

201220
@pytest.mark.parametrize("client_class,transport_class,transport_name", [
@@ -217,6 +236,7 @@ def test_{{ service.client_name|snake_case }}_client_options_scopes(client_class
217236
scopes=["1", "2"],
218237
api_mtls_endpoint="localhost:7469",
219238
client_cert_source=None,
239+
quota_project_id=None,
220240
)
221241

222242

@@ -239,6 +259,7 @@ def test_{{ service.client_name|snake_case }}_client_options_credentials_file(cl
239259
scopes=None,
240260
api_mtls_endpoint="localhost:7469",
241261
client_cert_source=None,
262+
quota_project_id=None,
242263
)
243264

244265

@@ -255,6 +276,7 @@ def test_{{ service.client_name|snake_case }}_client_options_from_dict():
255276
scopes=None,
256277
api_mtls_endpoint="squid.clam.whelk",
257278
client_cert_source=None,
279+
quota_project_id=None,
258280
)
259281

260282

@@ -950,12 +972,15 @@ def test_{{ service.name|snake_case }}_base_transport_with_credentials_file():
950972
load_creds.return_value = (credentials.AnonymousCredentials(), None)
951973
transport = transports.{{ service.name }}Transport(
952974
credentials_file="credentials.json",
975+
quota_project_id="octopus",
953976
)
954977
load_creds.assert_called_once_with("credentials.json", scopes=(
955978
{%- for scope in service.oauth_scopes %}
956979
'{{ scope }}',
957980
{%- endfor %}
958-
))
981+
),
982+
quota_project_id="octopus",
983+
)
959984

960985

961986
def test_{{ service.name|snake_case }}_auth_adc():
@@ -966,22 +991,23 @@ def test_{{ service.name|snake_case }}_auth_adc():
966991
adc.assert_called_once_with(scopes=(
967992
{%- for scope in service.oauth_scopes %}
968993
'{{ scope }}',
969-
{%- endfor %}
970-
))
994+
{%- endfor %}),
995+
quota_project_id=None,
996+
)
971997

972998

973999
def test_{{ service.name|snake_case }}_transport_auth_adc():
9741000
# If credentials and host are not provided, the transport class should use
9751001
# ADC credentials.
9761002
with mock.patch.object(auth, 'default') as adc:
9771003
adc.return_value = (credentials.AnonymousCredentials(), None)
978-
transports.{{ service.name }}GrpcTransport(host="squid.clam.whelk")
1004+
transports.{{ service.name }}GrpcTransport(host="squid.clam.whelk", quota_project_id="octopus")
9791005
adc.assert_called_once_with(scopes=(
9801006
{%- for scope in service.oauth_scopes %}
9811007
'{{ scope }}',
982-
{%- endfor %}
983-
))
984-
1008+
{%- endfor %}),
1009+
quota_project_id="octopus",
1010+
)
9851011

9861012
def test_{{ service.name|snake_case }}_host_no_port():
9871013
{% with host = (service.host|default('localhost', true)).split(':')[0] -%}
@@ -1071,6 +1097,7 @@ def test_{{ service.name|snake_case }}_grpc_transport_channel_mtls_with_client_c
10711097
{%- endfor %}
10721098
),
10731099
ssl_credentials=mock_ssl_cred,
1100+
quota_project_id=None,
10741101
)
10751102
assert transport.grpc_channel == mock_grpc_channel
10761103

@@ -1109,6 +1136,7 @@ def test_{{ service.name|snake_case }}_grpc_asyncio_transport_channel_mtls_with_
11091136
{%- endfor %}
11101137
),
11111138
ssl_credentials=mock_ssl_cred,
1139+
quota_project_id=None,
11121140
)
11131141
assert transport.grpc_channel == mock_grpc_channel
11141142

@@ -1149,6 +1177,7 @@ def test_{{ service.name|snake_case }}_grpc_transport_channel_mtls_with_adc(
11491177
{%- endfor %}
11501178
),
11511179
ssl_credentials=mock_ssl_cred,
1180+
quota_project_id=None,
11521181
)
11531182
assert transport.grpc_channel == mock_grpc_channel
11541183

@@ -1189,6 +1218,7 @@ def test_{{ service.name|snake_case }}_grpc_asyncio_transport_channel_mtls_with_
11891218
{%- endfor %}
11901219
),
11911220
ssl_credentials=mock_ssl_cred,
1221+
quota_project_id=None,
11921222
)
11931223
assert transport.grpc_channel == mock_grpc_channel
11941224

noxfile.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ def showcase_library(
9898
# Install the library.
9999
session.install(tmp_dir)
100100

101+
# REMOVE ME: temporarily install api-core and auth from branches
102+
session.install("--force-reinstall", "git+https://github.com/googleapis/python-api-core/@quota-project-override")
103+
session.install("--force-reinstall", "git+https://github.com/googleapis/google-auth-library-python.git@more-quota-project")
104+
101105
yield tmp_dir
102106

103107

0 commit comments

Comments
 (0)