1# -*- coding: utf-8 -*-
2# Copyright 2024 Google LLC
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16from collections import OrderedDict
17import os
18import re
19from typing import Dict, Optional, Type, Union
20
21from google.api_core import client_options as client_options_lib # type: ignore
22from google.api_core import gapic_v1 # type: ignore
23from google.api_core.operations_v1.transports.base import (
24 DEFAULT_CLIENT_INFO,
25 OperationsTransport,
26)
27from google.api_core.operations_v1.transports.rest import OperationsRestTransport
28
29try:
30 from google.api_core.operations_v1.transports.rest_asyncio import (
31 AsyncOperationsRestTransport,
32 )
33
34 HAS_ASYNC_REST_DEPENDENCIES = True
35except ImportError as e:
36 HAS_ASYNC_REST_DEPENDENCIES = False
37 ASYNC_REST_EXCEPTION = e
38
39from google.auth import credentials as ga_credentials # type: ignore
40from google.auth.exceptions import MutualTLSChannelError # type: ignore
41from google.auth.transport import mtls # type: ignore
42
43
44class AbstractOperationsBaseClientMeta(type):
45 """Metaclass for the Operations Base client.
46
47 This provides base class-level methods for building and retrieving
48 support objects (e.g. transport) without polluting the client instance
49 objects.
50 """
51
52 _transport_registry = OrderedDict() # type: Dict[str, Type[OperationsTransport]]
53 _transport_registry["rest"] = OperationsRestTransport
54 if HAS_ASYNC_REST_DEPENDENCIES:
55 _transport_registry["rest_asyncio"] = AsyncOperationsRestTransport
56
57 def get_transport_class(
58 cls,
59 label: Optional[str] = None,
60 ) -> Type[OperationsTransport]:
61 """Returns an appropriate transport class.
62
63 Args:
64 label: The name of the desired transport. If none is
65 provided, then the first transport in the registry is used.
66
67 Returns:
68 The transport class to use.
69 """
70 # If a specific transport is requested, return that one.
71 if (
72 label == "rest_asyncio" and not HAS_ASYNC_REST_DEPENDENCIES
73 ): # pragma: NO COVER
74 raise ASYNC_REST_EXCEPTION
75
76 if label:
77 return cls._transport_registry[label]
78
79 # No transport is requested; return the default (that is, the first one
80 # in the dictionary).
81 return next(iter(cls._transport_registry.values()))
82
83
84class AbstractOperationsBaseClient(metaclass=AbstractOperationsBaseClientMeta):
85 """Manages long-running operations with an API service.
86
87 When an API method normally takes long time to complete, it can be
88 designed to return [Operation][google.api_core.operations_v1.Operation] to the
89 client, and the client can use this interface to receive the real
90 response asynchronously by polling the operation resource, or pass
91 the operation resource to another API (such as Google Cloud Pub/Sub
92 API) to receive the response. Any API service that returns
93 long-running operations should implement the ``Operations``
94 interface so developers can have a consistent client experience.
95 """
96
97 @staticmethod
98 def _get_default_mtls_endpoint(api_endpoint):
99 """Converts api endpoint to mTLS endpoint.
100
101 Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to
102 "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively.
103 Args:
104 api_endpoint (Optional[str]): the api endpoint to convert.
105 Returns:
106 str: converted mTLS api endpoint.
107 """
108 if not api_endpoint:
109 return api_endpoint
110
111 mtls_endpoint_re = re.compile(
112 r"(?P<name>[^.]+)(?P<mtls>\.mtls)?(?P<sandbox>\.sandbox)?(?P<googledomain>\.googleapis\.com)?"
113 )
114
115 m = mtls_endpoint_re.match(api_endpoint)
116 name, mtls, sandbox, googledomain = m.groups()
117 if mtls or not googledomain:
118 return api_endpoint
119
120 if sandbox:
121 return api_endpoint.replace(
122 "sandbox.googleapis.com", "mtls.sandbox.googleapis.com"
123 )
124
125 return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com")
126
127 DEFAULT_ENDPOINT = "longrunning.googleapis.com"
128 DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore
129 DEFAULT_ENDPOINT
130 )
131
132 @classmethod
133 def from_service_account_info(cls, info: dict, *args, **kwargs):
134 """
135 This class method should be overridden by the subclasses.
136
137 Args:
138 info (dict): The service account private key info.
139 args: Additional arguments to pass to the constructor.
140 kwargs: Additional arguments to pass to the constructor.
141
142 Raises:
143 NotImplementedError: If the method is called on the base class.
144 """
145 raise NotImplementedError("`from_service_account_info` is not implemented.")
146
147 @classmethod
148 def from_service_account_file(cls, filename: str, *args, **kwargs):
149 """
150 This class method should be overridden by the subclasses.
151
152 Args:
153 filename (str): The path to the service account private key json
154 file.
155 args: Additional arguments to pass to the constructor.
156 kwargs: Additional arguments to pass to the constructor.
157
158 Raises:
159 NotImplementedError: If the method is called on the base class.
160 """
161 raise NotImplementedError("`from_service_account_file` is not implemented.")
162
163 from_service_account_json = from_service_account_file
164
165 @property
166 def transport(self) -> OperationsTransport:
167 """Returns the transport used by the client instance.
168
169 Returns:
170 OperationsTransport: The transport used by the client
171 instance.
172 """
173 return self._transport
174
175 @staticmethod
176 def common_billing_account_path(
177 billing_account: str,
178 ) -> str:
179 """Returns a fully-qualified billing_account string."""
180 return "billingAccounts/{billing_account}".format(
181 billing_account=billing_account,
182 )
183
184 @staticmethod
185 def parse_common_billing_account_path(path: str) -> Dict[str, str]:
186 """Parse a billing_account path into its component segments."""
187 m = re.match(r"^billingAccounts/(?P<billing_account>.+?)$", path)
188 return m.groupdict() if m else {}
189
190 @staticmethod
191 def common_folder_path(
192 folder: str,
193 ) -> str:
194 """Returns a fully-qualified folder string."""
195 return "folders/{folder}".format(
196 folder=folder,
197 )
198
199 @staticmethod
200 def parse_common_folder_path(path: str) -> Dict[str, str]:
201 """Parse a folder path into its component segments."""
202 m = re.match(r"^folders/(?P<folder>.+?)$", path)
203 return m.groupdict() if m else {}
204
205 @staticmethod
206 def common_organization_path(
207 organization: str,
208 ) -> str:
209 """Returns a fully-qualified organization string."""
210 return "organizations/{organization}".format(
211 organization=organization,
212 )
213
214 @staticmethod
215 def parse_common_organization_path(path: str) -> Dict[str, str]:
216 """Parse a organization path into its component segments."""
217 m = re.match(r"^organizations/(?P<organization>.+?)$", path)
218 return m.groupdict() if m else {}
219
220 @staticmethod
221 def common_project_path(
222 project: str,
223 ) -> str:
224 """Returns a fully-qualified project string."""
225 return "projects/{project}".format(
226 project=project,
227 )
228
229 @staticmethod
230 def parse_common_project_path(path: str) -> Dict[str, str]:
231 """Parse a project path into its component segments."""
232 m = re.match(r"^projects/(?P<project>.+?)$", path)
233 return m.groupdict() if m else {}
234
235 @staticmethod
236 def common_location_path(
237 project: str,
238 location: str,
239 ) -> str:
240 """Returns a fully-qualified location string."""
241 return "projects/{project}/locations/{location}".format(
242 project=project,
243 location=location,
244 )
245
246 @staticmethod
247 def parse_common_location_path(path: str) -> Dict[str, str]:
248 """Parse a location path into its component segments."""
249 m = re.match(r"^projects/(?P<project>.+?)/locations/(?P<location>.+?)$", path)
250 return m.groupdict() if m else {}
251
252 def __init__(
253 self,
254 *,
255 credentials: Optional[ga_credentials.Credentials] = None,
256 transport: Union[str, OperationsTransport, None] = None,
257 client_options: Optional[client_options_lib.ClientOptions] = None,
258 client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO,
259 ) -> None:
260 """Instantiates the operations client.
261
262 Args:
263 credentials (Optional[google.auth.credentials.Credentials]): The
264 authorization credentials to attach to requests. These
265 credentials identify the application to the service; if none
266 are specified, the client will attempt to ascertain the
267 credentials from the environment.
268 transport (Union[str, OperationsTransport]): The
269 transport to use. If set to None, a transport is chosen
270 automatically.
271 client_options (google.api_core.client_options.ClientOptions): Custom options for the
272 client. It won't take effect if a ``transport`` instance is provided.
273 (1) The ``api_endpoint`` property can be used to override the
274 default endpoint provided by the client. GOOGLE_API_USE_MTLS_ENDPOINT
275 environment variable can also be used to override the endpoint:
276 "always" (always use the default mTLS endpoint), "never" (always
277 use the default regular endpoint) and "auto" (auto switch to the
278 default mTLS endpoint if client certificate is present, this is
279 the default value). However, the ``api_endpoint`` property takes
280 precedence if provided.
281 (2) If GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable
282 is "true", then the ``client_cert_source`` property can be used
283 to provide client certificate for mutual TLS transport. If
284 not provided, the default SSL client certificate will be used if
285 present. If GOOGLE_API_USE_CLIENT_CERTIFICATE is "false" or not
286 set, no client certificate will be used.
287 client_info (google.api_core.gapic_v1.client_info.ClientInfo):
288 The client info used to send a user-agent string along with
289 API requests. If ``None``, then default info will be used.
290 Generally, you only need to set this if you're developing
291 your own client library.
292
293 Raises:
294 google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport
295 creation failed for any reason.
296 """
297 if isinstance(client_options, dict):
298 client_options = client_options_lib.from_dict(client_options)
299 if client_options is None:
300 client_options = client_options_lib.ClientOptions()
301
302 # Create SSL credentials for mutual TLS if needed.
303 use_client_cert = os.getenv(
304 "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false"
305 ).lower()
306 if use_client_cert not in ("true", "false"):
307 raise ValueError(
308 "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`"
309 )
310 client_cert_source_func = None
311 is_mtls = False
312 if use_client_cert == "true":
313 if client_options.client_cert_source:
314 is_mtls = True
315 client_cert_source_func = client_options.client_cert_source
316 else:
317 is_mtls = mtls.has_default_client_cert_source()
318 if is_mtls:
319 client_cert_source_func = mtls.default_client_cert_source()
320 else:
321 client_cert_source_func = None
322
323 # Figure out which api endpoint to use.
324 if client_options.api_endpoint is not None:
325 api_endpoint = client_options.api_endpoint
326 else:
327 use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto")
328 if use_mtls_env == "never":
329 api_endpoint = self.DEFAULT_ENDPOINT
330 elif use_mtls_env == "always":
331 api_endpoint = self.DEFAULT_MTLS_ENDPOINT
332 elif use_mtls_env == "auto":
333 if is_mtls:
334 api_endpoint = self.DEFAULT_MTLS_ENDPOINT
335 else:
336 api_endpoint = self.DEFAULT_ENDPOINT
337 else:
338 raise MutualTLSChannelError(
339 "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted "
340 "values: never, auto, always"
341 )
342
343 # Save or instantiate the transport.
344 # Ordinarily, we provide the transport, but allowing a custom transport
345 # instance provides an extensibility point for unusual situations.
346 if isinstance(transport, OperationsTransport):
347 # transport is a OperationsTransport instance.
348 if credentials or client_options.credentials_file:
349 raise ValueError(
350 "When providing a transport instance, "
351 "provide its credentials directly."
352 )
353 if client_options.scopes:
354 raise ValueError(
355 "When providing a transport instance, provide its scopes "
356 "directly."
357 )
358 self._transport = transport
359 else:
360 Transport = type(self).get_transport_class(transport)
361 self._transport = Transport(
362 credentials=credentials,
363 credentials_file=client_options.credentials_file,
364 host=api_endpoint,
365 scopes=client_options.scopes,
366 client_cert_source_for_mtls=client_cert_source_func,
367 quota_project_id=client_options.quota_project_id,
368 client_info=client_info,
369 always_use_jwt_access=True,
370 )