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 if hasattr(mtls, "should_use_client_cert"):
304 use_client_cert = mtls.should_use_client_cert()
305 else:
306 # if unsupported, fallback to reading from env var
307 use_client_cert_str = os.getenv(
308 "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false"
309 ).lower()
310 if use_client_cert_str not in ("true", "false"):
311 raise ValueError(
312 "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be"
313 " either `true` or `false`"
314 )
315 use_client_cert = use_client_cert_str == "true"
316 client_cert_source_func = None
317 is_mtls = False
318 if use_client_cert:
319 if client_options.client_cert_source:
320 is_mtls = True
321 client_cert_source_func = client_options.client_cert_source
322 else:
323 is_mtls = mtls.has_default_client_cert_source()
324 if is_mtls:
325 client_cert_source_func = mtls.default_client_cert_source()
326 else:
327 client_cert_source_func = None
328
329 # Figure out which api endpoint to use.
330 if client_options.api_endpoint is not None:
331 api_endpoint = client_options.api_endpoint
332 else:
333 use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto")
334 if use_mtls_env == "never":
335 api_endpoint = self.DEFAULT_ENDPOINT
336 elif use_mtls_env == "always":
337 api_endpoint = self.DEFAULT_MTLS_ENDPOINT
338 elif use_mtls_env == "auto":
339 if is_mtls:
340 api_endpoint = self.DEFAULT_MTLS_ENDPOINT
341 else:
342 api_endpoint = self.DEFAULT_ENDPOINT
343 else:
344 raise MutualTLSChannelError(
345 "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted "
346 "values: never, auto, always"
347 )
348
349 # Save or instantiate the transport.
350 # Ordinarily, we provide the transport, but allowing a custom transport
351 # instance provides an extensibility point for unusual situations.
352 if isinstance(transport, OperationsTransport):
353 # transport is a OperationsTransport instance.
354 if credentials or client_options.credentials_file:
355 raise ValueError(
356 "When providing a transport instance, "
357 "provide its credentials directly."
358 )
359 if client_options.scopes:
360 raise ValueError(
361 "When providing a transport instance, provide its scopes "
362 "directly."
363 )
364 self._transport = transport
365 else:
366 Transport = type(self).get_transport_class(transport)
367 self._transport = Transport(
368 credentials=credentials,
369 credentials_file=client_options.credentials_file,
370 host=api_endpoint,
371 scopes=client_options.scopes,
372 client_cert_source_for_mtls=client_cert_source_func,
373 quota_project_id=client_options.quota_project_id,
374 client_info=client_info,
375 always_use_jwt_access=True,
376 )