Skip to content

Commit ee7f830

Browse files
committed
feat(storage): add abstract BaseClient for Python storage clients
1 parent 4e91c54 commit ee7f830

File tree

3 files changed

+204
-150
lines changed

3 files changed

+204
-150
lines changed

google/cloud/storage/abstracts/__init__.py

Whitespace-only changes.
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""The abstract for python-storage Client."""
15+
16+
from google.cloud.storage._helpers import _get_api_endpoint_override
17+
from google.cloud.storage._helpers import _get_environ_project
18+
from google.cloud.storage._helpers import _get_storage_emulator_override
19+
from google.cloud.storage._helpers import _DEFAULT_SCHEME
20+
from google.cloud.storage._helpers import _STORAGE_HOST_TEMPLATE
21+
from google.auth.credentials import AnonymousCredentials
22+
from google.cloud.client import ClientWithProject
23+
from google.cloud._helpers import _LocalStack
24+
from google.auth.transport import mtls
25+
from abc import ABC
26+
27+
import os
28+
import google.api_core
29+
30+
marker = object()
31+
32+
class BaseClient(ClientWithProject, ABC):
33+
"""Abstract class for python-storage Client"""
34+
35+
SCOPE = (
36+
"https://www.googleapis.com/auth/devstorage.full_control",
37+
"https://www.googleapis.com/auth/devstorage.read_only",
38+
"https://www.googleapis.com/auth/devstorage.read_write",
39+
)
40+
"""The scopes required for authenticating as a Cloud Storage consumer."""
41+
42+
def __init__(
43+
self,
44+
project=marker,
45+
credentials=None,
46+
_http=None,
47+
client_info=None,
48+
client_options=None,
49+
use_auth_w_custom_endpoint=True,
50+
extra_headers={},
51+
*,
52+
api_key=None,
53+
):
54+
self._base_connection = None
55+
56+
if project is None:
57+
no_project = True
58+
project = "<none>"
59+
else:
60+
no_project = False
61+
62+
if project is marker:
63+
project = None
64+
65+
# Save the initial value of constructor arguments before they
66+
# are passed along, for use in __reduce__ defined elsewhere.
67+
self._initial_client_info = client_info
68+
self._initial_client_options = client_options
69+
self._extra_headers = extra_headers
70+
71+
connection_kw_args = {"client_info": client_info}
72+
73+
# api_key should set client_options.api_key. Set it here whether
74+
# client_options was specified as a dict, as a ClientOptions object, or
75+
# None.
76+
if api_key:
77+
if client_options and not isinstance(client_options, dict):
78+
client_options.api_key = api_key
79+
else:
80+
if not client_options:
81+
client_options = {}
82+
client_options["api_key"] = api_key
83+
84+
if client_options:
85+
if isinstance(client_options, dict):
86+
client_options = google.api_core.client_options.from_dict(
87+
client_options
88+
)
89+
90+
if client_options and client_options.universe_domain:
91+
self._universe_domain = client_options.universe_domain
92+
else:
93+
self._universe_domain = None
94+
95+
storage_emulator_override = _get_storage_emulator_override()
96+
api_endpoint_override = _get_api_endpoint_override()
97+
98+
# Determine the api endpoint. The rules are as follows:
99+
100+
# 1. If the `api_endpoint` is set in `client_options`, use that as the
101+
# endpoint.
102+
if client_options and client_options.api_endpoint:
103+
api_endpoint = client_options.api_endpoint
104+
105+
# 2. Elif the "STORAGE_EMULATOR_HOST" env var is set, then use that as the
106+
# endpoint.
107+
elif storage_emulator_override:
108+
api_endpoint = storage_emulator_override
109+
110+
# 3. Elif the "API_ENDPOINT_OVERRIDE" env var is set, then use that as the
111+
# endpoint.
112+
elif api_endpoint_override:
113+
api_endpoint = api_endpoint_override
114+
115+
# 4. Elif the `universe_domain` is set in `client_options`,
116+
# create the endpoint using that as the default.
117+
#
118+
# Mutual TLS is not compatible with a non-default universe domain
119+
# at this time. If such settings are enabled along with the
120+
# "GOOGLE_API_USE_CLIENT_CERTIFICATE" env variable, a ValueError will
121+
# be raised.
122+
123+
elif self._universe_domain:
124+
# The final decision of whether to use mTLS takes place in
125+
# google-auth-library-python. We peek at the environment variable
126+
# here only to issue an exception in case of a conflict.
127+
use_client_cert = False
128+
if hasattr(mtls, "should_use_client_cert"):
129+
use_client_cert = mtls.should_use_client_cert()
130+
else:
131+
use_client_cert = (
132+
os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE") == "true"
133+
)
134+
135+
if use_client_cert:
136+
raise ValueError(
137+
'The "GOOGLE_API_USE_CLIENT_CERTIFICATE" env variable is '
138+
'set to "true" and a non-default universe domain is '
139+
"configured. mTLS is not supported in any universe other than"
140+
"googleapis.com."
141+
)
142+
api_endpoint = _DEFAULT_SCHEME + _STORAGE_HOST_TEMPLATE.format(
143+
universe_domain=self._universe_domain
144+
)
145+
146+
# 5. Else, use the default, which is to use the default
147+
# universe domain of "googleapis.com" and create the endpoint
148+
# "storage.googleapis.com" from that.
149+
else:
150+
api_endpoint = None
151+
152+
connection_kw_args["api_endpoint"] = api_endpoint
153+
self._is_emulator_set = True if storage_emulator_override else False
154+
155+
# If a custom endpoint is set, the client checks for credentials
156+
# or finds the default credentials based on the current environment.
157+
# Authentication may be bypassed under certain conditions:
158+
# (1) STORAGE_EMULATOR_HOST is set (for backwards compatibility), OR
159+
# (2) use_auth_w_custom_endpoint is set to False.
160+
if connection_kw_args["api_endpoint"] is not None:
161+
if self._is_emulator_set or not use_auth_w_custom_endpoint:
162+
if credentials is None:
163+
credentials = AnonymousCredentials()
164+
if project is None:
165+
project = _get_environ_project()
166+
if project is None:
167+
no_project = True
168+
project = "<none>"
169+
170+
super(BaseClient, self).__init__(
171+
project=project,
172+
credentials=credentials,
173+
client_options=client_options,
174+
_http=_http,
175+
)
176+
177+
# Validate that the universe domain of the credentials matches the
178+
# universe domain of the client.
179+
if self._credentials.universe_domain != self.universe_domain:
180+
raise ValueError(
181+
"The configured universe domain ({client_ud}) does not match "
182+
"the universe domain found in the credentials ({cred_ud}). If "
183+
"you haven't configured the universe domain explicitly, "
184+
"`googleapis.com` is the default.".format(
185+
client_ud=self.universe_domain,
186+
cred_ud=self._credentials.universe_domain,
187+
)
188+
)
189+
190+
if no_project:
191+
self.project = None
192+
193+
self.connection_kw_args = connection_kw_args
194+
self._batch_stack = _LocalStack()

0 commit comments

Comments
 (0)