google-cloud-alloydb-connector 1.8.1.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. google/api/field_behavior_pb2.py +52 -0
  2. google/cloud/alloydb/connector/__init__.py +26 -0
  3. google/cloud/alloydb/connector/async_connector.py +245 -0
  4. google/cloud/alloydb/connector/asyncpg.py +63 -0
  5. google/cloud/alloydb/connector/client.py +279 -0
  6. google/cloud/alloydb/connector/connection_info.py +87 -0
  7. google/cloud/alloydb/connector/connector.py +399 -0
  8. google/cloud/alloydb/connector/enums.py +50 -0
  9. google/cloud/alloydb/connector/exceptions.py +25 -0
  10. google/cloud/alloydb/connector/instance.py +242 -0
  11. google/cloud/alloydb/connector/lazy.py +127 -0
  12. google/cloud/alloydb/connector/pg8000.py +49 -0
  13. google/cloud/alloydb/connector/py.typed +0 -0
  14. google/cloud/alloydb/connector/rate_limiter.py +77 -0
  15. google/cloud/alloydb/connector/refresh_utils.py +62 -0
  16. google/cloud/alloydb/connector/static.py +102 -0
  17. google/cloud/alloydb/connector/types.py +23 -0
  18. google/cloud/alloydb/connector/utils.py +72 -0
  19. google/cloud/alloydb/connector/version.py +15 -0
  20. google/cloud/alloydb_connectors_v1/proto/__init__.py +13 -0
  21. google/cloud/alloydb_connectors_v1/proto/resources_pb2.py +61 -0
  22. google/cloud/alloydb_connectors_v1/proto/resources_pb2.pyi +66 -0
  23. google_cloud_alloydb_connector-1.8.1.dev0.dist-info/METADATA +572 -0
  24. google_cloud_alloydb_connector-1.8.1.dev0.dist-info/RECORD +27 -0
  25. google_cloud_alloydb_connector-1.8.1.dev0.dist-info/WHEEL +5 -0
  26. google_cloud_alloydb_connector-1.8.1.dev0.dist-info/licenses/LICENSE +201 -0
  27. google_cloud_alloydb_connector-1.8.1.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,52 @@
1
+ # Copyright 2024 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
+
15
+ # type: ignore
16
+ # -*- coding: utf-8 -*-
17
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
18
+ # source: google/api/field_behavior.proto
19
+ # isort: skip_file
20
+ """Generated protocol buffer code."""
21
+ from google.protobuf import descriptor as _descriptor
22
+ from google.protobuf import descriptor_pool as _descriptor_pool
23
+ from google.protobuf import symbol_database as _symbol_database
24
+ from google.protobuf.internal import builder as _builder
25
+
26
+ # @@protoc_insertion_point(imports)
27
+
28
+ _sym_db = _symbol_database.Default()
29
+
30
+
31
+ from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2
32
+
33
+
34
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
35
+ b"\n\x1fgoogle/api/field_behavior.proto\x12\ngoogle.api\x1a google/protobuf/descriptor.proto*\xb6\x01\n\rFieldBehavior\x12\x1e\n\x1a\x46IELD_BEHAVIOR_UNSPECIFIED\x10\x00\x12\x0c\n\x08OPTIONAL\x10\x01\x12\x0c\n\x08REQUIRED\x10\x02\x12\x0f\n\x0bOUTPUT_ONLY\x10\x03\x12\x0e\n\nINPUT_ONLY\x10\x04\x12\r\n\tIMMUTABLE\x10\x05\x12\x12\n\x0eUNORDERED_LIST\x10\x06\x12\x15\n\x11NON_EMPTY_DEFAULT\x10\x07\x12\x0e\n\nIDENTIFIER\x10\x08:Q\n\x0e\x66ield_behavior\x12\x1d.google.protobuf.FieldOptions\x18\x9c\x08 \x03(\x0e\x32\x19.google.api.FieldBehaviorBp\n\x0e\x63om.google.apiB\x12\x46ieldBehaviorProtoP\x01ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\xa2\x02\x04GAPIb\x06proto3"
36
+ )
37
+
38
+ _globals = globals()
39
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
40
+ _builder.BuildTopDescriptorsAndMessages(
41
+ DESCRIPTOR, "google.api.field_behavior_pb2", _globals
42
+ )
43
+ if _descriptor._USE_C_DESCRIPTORS == False:
44
+ google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(
45
+ field_behavior
46
+ )
47
+
48
+ DESCRIPTOR._options = None
49
+ DESCRIPTOR._serialized_options = b"\n\016com.google.apiB\022FieldBehaviorProtoP\001ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\242\002\004GAPI"
50
+ _globals["_FIELDBEHAVIOR"]._serialized_start = 82
51
+ _globals["_FIELDBEHAVIOR"]._serialized_end = 264
52
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,26 @@
1
+ # Copyright 2023 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
+ from google.cloud.alloydb.connector.async_connector import AsyncConnector
15
+ from google.cloud.alloydb.connector.connector import Connector
16
+ from google.cloud.alloydb.connector.enums import IPTypes
17
+ from google.cloud.alloydb.connector.enums import RefreshStrategy
18
+ from google.cloud.alloydb.connector.version import __version__
19
+
20
+ __all__ = [
21
+ "__version__",
22
+ "Connector",
23
+ "AsyncConnector",
24
+ "IPTypes",
25
+ "RefreshStrategy",
26
+ ]
@@ -0,0 +1,245 @@
1
+ # Copyright 2024 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
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import logging
19
+ from types import TracebackType
20
+ from typing import Any, Optional, TYPE_CHECKING
21
+
22
+ import google.auth
23
+ from google.auth.credentials import with_scopes_if_required
24
+ import google.auth.transport.requests
25
+
26
+ import google.cloud.alloydb.connector.asyncpg as asyncpg
27
+ from google.cloud.alloydb.connector.client import AlloyDBClient
28
+ from google.cloud.alloydb.connector.enums import IPTypes
29
+ from google.cloud.alloydb.connector.enums import RefreshStrategy
30
+ from google.cloud.alloydb.connector.exceptions import ClosedConnectorError
31
+ from google.cloud.alloydb.connector.instance import RefreshAheadCache
32
+ from google.cloud.alloydb.connector.lazy import LazyRefreshCache
33
+ from google.cloud.alloydb.connector.types import CacheTypes
34
+ from google.cloud.alloydb.connector.utils import generate_keys
35
+ from google.cloud.alloydb.connector.utils import strip_http_prefix
36
+
37
+ if TYPE_CHECKING:
38
+ from google.auth.credentials import Credentials
39
+
40
+ logger = logging.getLogger(name=__name__)
41
+
42
+
43
+ class AsyncConnector:
44
+ """A class to configure and create connections to Cloud SQL instances
45
+ asynchronously.
46
+
47
+ Args:
48
+ credentials (google.auth.credentials.Credentials):
49
+ A credentials object created from the google-auth Python library.
50
+ If not specified, Application Default Credentials are used.
51
+ quota_project (str): The Project ID for an existing Google Cloud
52
+ project. The project specified is used for quota and
53
+ billing purposes.
54
+ Defaults to None, picking up project from environment.
55
+ alloydb_api_endpoint (str): Base URL to use when calling
56
+ the AlloyDB API endpoint. Defaults to "alloydb.googleapis.com".
57
+ enable_iam_auth (bool): Enables automatic IAM database authentication.
58
+ ip_type (str | IPTypes): Default IP type for all AlloyDB connections.
59
+ Defaults to IPTypes.PRIVATE ("PRIVATE") for private IP connections.
60
+ refresh_strategy (str | RefreshStrategy): The default refresh strategy
61
+ used to refresh SSL/TLS cert and instance metadata. Can be one
62
+ of the following: RefreshStrategy.LAZY ("LAZY") or
63
+ RefreshStrategy.BACKGROUND ("BACKGROUND").
64
+ Default: RefreshStrategy.BACKGROUND
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ credentials: Optional[Credentials] = None,
70
+ quota_project: Optional[str] = None,
71
+ alloydb_api_endpoint: str = "alloydb.googleapis.com",
72
+ enable_iam_auth: bool = False,
73
+ ip_type: str | IPTypes = IPTypes.PRIVATE,
74
+ user_agent: Optional[str] = None,
75
+ refresh_strategy: str | RefreshStrategy = RefreshStrategy.BACKGROUND,
76
+ ) -> None:
77
+ self._cache: dict[str, CacheTypes] = {}
78
+ # initialize default params
79
+ self._quota_project = quota_project
80
+ self._alloydb_api_endpoint = strip_http_prefix(alloydb_api_endpoint)
81
+ self._enable_iam_auth = enable_iam_auth
82
+ # if ip_type is str, convert to IPTypes enum
83
+ if isinstance(ip_type, str):
84
+ ip_type = IPTypes(ip_type.upper())
85
+ self._ip_type = ip_type
86
+ # if refresh_strategy is str, convert to RefreshStrategy enum
87
+ if isinstance(refresh_strategy, str):
88
+ refresh_strategy = RefreshStrategy(refresh_strategy.upper())
89
+ self._refresh_strategy = refresh_strategy
90
+ self._user_agent = user_agent
91
+ # initialize credentials
92
+ scopes = ["https://www.googleapis.com/auth/cloud-platform"]
93
+ if credentials:
94
+ self._credentials = with_scopes_if_required(credentials, scopes=scopes)
95
+ # otherwise use application default credentials
96
+ else:
97
+ self._credentials, _ = google.auth.default(scopes=scopes)
98
+
99
+ # check if AsyncConnector is being initialized with event loop running
100
+ # Otherwise we will lazy init keys
101
+ try:
102
+ self._keys: Optional[asyncio.Task] = asyncio.create_task(generate_keys())
103
+ except RuntimeError:
104
+ self._keys = None
105
+ self._client: Optional[AlloyDBClient] = None
106
+ self._closed = False
107
+
108
+ async def connect(
109
+ self,
110
+ instance_uri: str,
111
+ driver: str,
112
+ **kwargs: Any,
113
+ ) -> Any:
114
+ """
115
+ Asynchronously prepares and returns a database connection object.
116
+
117
+ Starts tasks to refresh the certificates and get
118
+ AlloyDB instance IP address. Creates a secure TLS connection
119
+ to establish connection to AlloyDB instance.
120
+
121
+ Args:
122
+ instance_uri (str): The instance URI of the AlloyDB instance.
123
+ ex. projects/<PROJECT>/locations/<REGION>/clusters/<CLUSTER>/instances/<INSTANCE>
124
+ driver (str): A string representing the database driver to connect
125
+ with. Supported drivers are asyncpg.
126
+ **kwargs: Pass in any database driver-specific arguments needed
127
+ to fine tune connection.
128
+
129
+ Returns:
130
+ connection: A DBAPI connection to the specified AlloyDB instance.
131
+ """
132
+ if self._closed:
133
+ raise ClosedConnectorError(
134
+ "Connection attempt failed because the connector has already been closed."
135
+ )
136
+ if self._keys is None:
137
+ self._keys = asyncio.create_task(generate_keys())
138
+ if self._client is None:
139
+ # lazy init client as it has to be initialized in async context
140
+ self._client = AlloyDBClient(
141
+ self._alloydb_api_endpoint,
142
+ self._quota_project,
143
+ self._credentials,
144
+ user_agent=self._user_agent,
145
+ driver=driver,
146
+ )
147
+
148
+ enable_iam_auth = kwargs.pop("enable_iam_auth", self._enable_iam_auth)
149
+
150
+ # use existing connection info if possible
151
+ if instance_uri in self._cache:
152
+ cache = self._cache[instance_uri]
153
+ else:
154
+ if self._refresh_strategy == RefreshStrategy.LAZY:
155
+ logger.debug(
156
+ f"['{instance_uri}']: Refresh strategy is set to lazy refresh"
157
+ )
158
+ cache = LazyRefreshCache(instance_uri, self._client, self._keys)
159
+ else:
160
+ logger.debug(
161
+ f"['{instance_uri}']: Refresh strategy is set to background"
162
+ " refresh"
163
+ )
164
+ cache = RefreshAheadCache(instance_uri, self._client, self._keys)
165
+ self._cache[instance_uri] = cache
166
+ logger.debug(f"['{instance_uri}']: Connection info added to cache")
167
+
168
+ connect_func = {
169
+ "asyncpg": asyncpg.connect,
170
+ }
171
+ # only accept supported database drivers
172
+ try:
173
+ connector = connect_func[driver]
174
+ except KeyError:
175
+ raise ValueError(f"Driver '{driver}' is not a supported database driver.")
176
+
177
+ # Host and ssl options come from the certificates and instance IP
178
+ # address so we don't want the user to specify them.
179
+ kwargs.pop("host", None)
180
+ kwargs.pop("ssl", None)
181
+ kwargs.pop("port", None)
182
+
183
+ # get connection info for AlloyDB instance
184
+ ip_type: str | IPTypes = kwargs.pop("ip_type", self._ip_type)
185
+ # if ip_type is str, convert to IPTypes enum
186
+ if isinstance(ip_type, str):
187
+ ip_type = IPTypes(ip_type.upper())
188
+ try:
189
+ conn_info = await cache.connect_info()
190
+ ip_address = conn_info.get_preferred_ip(ip_type)
191
+ except Exception:
192
+ # with an error from AlloyDB API call or IP type, invalidate the
193
+ # cache and re-raise the error
194
+ await self._remove_cached(instance_uri)
195
+ raise
196
+ logger.debug(f"['{instance_uri}']: Connecting to {ip_address}:5433")
197
+
198
+ # callable to be used for auto IAM authn
199
+ def get_authentication_token() -> str:
200
+ """Get OAuth2 access token to be used for IAM database authentication"""
201
+ # refresh credentials if expired
202
+ if not self._credentials.valid:
203
+ request = google.auth.transport.requests.Request()
204
+ self._credentials.refresh(request)
205
+ return self._credentials.token
206
+
207
+ # if enable_iam_auth is set, use auth token as database password
208
+ if enable_iam_auth:
209
+ kwargs["password"] = get_authentication_token
210
+ try:
211
+ return await connector(
212
+ ip_address, await conn_info.create_ssl_context(), **kwargs
213
+ )
214
+ except Exception:
215
+ # we attempt a force refresh, then throw the error
216
+ await cache.force_refresh()
217
+ raise
218
+
219
+ async def _remove_cached(self, instance_uri: str) -> None:
220
+ """Stops all background refreshes and deletes the connection
221
+ info cache from the map of caches.
222
+ """
223
+ logger.debug(f"['{instance_uri}']: Removing connection info from cache")
224
+ # remove cache from stored caches and close it
225
+ cache = self._cache.pop(instance_uri)
226
+ await cache.close()
227
+
228
+ async def __aenter__(self) -> Any:
229
+ """Enter async context manager by returning Connector object"""
230
+ return self
231
+
232
+ async def __aexit__(
233
+ self,
234
+ exc_type: Optional[type[BaseException]],
235
+ exc_val: Optional[BaseException],
236
+ exc_tb: Optional[TracebackType],
237
+ ) -> None:
238
+ """Exit async context manager by closing Connector"""
239
+ await self.close()
240
+
241
+ async def close(self) -> None:
242
+ """Helper function to cancel RefreshAheadCaches' tasks
243
+ and close client."""
244
+ await asyncio.gather(*[cache.close() for cache in self._cache.values()])
245
+ self._closed = True
@@ -0,0 +1,63 @@
1
+ # Copyright 2024 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
+ # https://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
+
15
+ import ssl
16
+ from typing import Any, TYPE_CHECKING
17
+
18
+ SERVER_PROXY_PORT = 5433
19
+
20
+ if TYPE_CHECKING:
21
+ import asyncpg
22
+
23
+
24
+ async def connect(
25
+ ip_address: str, ctx: ssl.SSLContext, **kwargs: Any
26
+ ) -> "asyncpg.Connection":
27
+ """Helper function to create an asyncpg DB-API connection object.
28
+
29
+ :type ip_address: str
30
+ :param ip_address: A string containing an IP address for the AlloyDB
31
+ instance.
32
+
33
+ :type ctx: ssl.SSLContext
34
+ :param ctx: An SSLContext object created from the AlloyDB server CA
35
+ cert and ephemeral cert.
36
+
37
+ :type kwargs: Any
38
+ :param kwargs: Keyword arguments for establishing asyncpg connection
39
+ object to AlloyDB instance.
40
+
41
+ :rtype: asyncpg.Connection
42
+ :returns: An asyncpg.Connection object to an AlloyDB instance.
43
+ """
44
+ try:
45
+ import asyncpg
46
+ except ImportError:
47
+ raise ImportError(
48
+ 'Unable to import module "asyncpg." Please install and try again.'
49
+ )
50
+ user = kwargs.pop("user")
51
+ db = kwargs.pop("db")
52
+ passwd = kwargs.pop("password")
53
+
54
+ return await asyncpg.connect(
55
+ user=user,
56
+ database=db,
57
+ password=passwd,
58
+ host=ip_address,
59
+ port=SERVER_PROXY_PORT,
60
+ ssl=ctx,
61
+ direct_tls=True,
62
+ **kwargs,
63
+ )
@@ -0,0 +1,279 @@
1
+ # Copyright 2023 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
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import logging
19
+ from typing import Optional, TYPE_CHECKING
20
+
21
+ from cryptography import x509
22
+ from google.api_core.client_options import ClientOptions
23
+ from google.api_core.gapic_v1.client_info import ClientInfo
24
+ from google.auth.credentials import TokenState
25
+ from google.auth.transport import requests
26
+ import google.cloud.alloydb_v1beta as v1beta
27
+ from google.protobuf import duration_pb2
28
+
29
+ from google.cloud.alloydb.connector.connection_info import ConnectionInfo
30
+ from google.cloud.alloydb.connector.version import __version__ as version
31
+
32
+ if TYPE_CHECKING:
33
+ from google.auth.credentials import Credentials
34
+
35
+ USER_AGENT: str = f"alloydb-python-connector/{version}"
36
+ API_VERSION: str = "v1beta"
37
+
38
+ logger = logging.getLogger(name=__name__)
39
+
40
+
41
+ def _format_user_agent(
42
+ driver: Optional[str],
43
+ custom_user_agent: Optional[str],
44
+ ) -> str:
45
+ """
46
+ Appends user-defined user agents to the base default agent.
47
+ """
48
+ agent = f"{USER_AGENT}+{driver}" if driver else USER_AGENT
49
+ if custom_user_agent and isinstance(custom_user_agent, str):
50
+ agent = f"{agent} {custom_user_agent}"
51
+
52
+ return agent
53
+
54
+
55
+ class AlloyDBClient:
56
+ def __init__(
57
+ self,
58
+ alloydb_api_endpoint: str,
59
+ quota_project: Optional[str],
60
+ credentials: Credentials,
61
+ client: Optional[v1beta.AlloyDBAdminAsyncClient] = None,
62
+ driver: Optional[str] = None,
63
+ user_agent: Optional[str] = None,
64
+ ) -> None:
65
+ """
66
+ Establish the client to be used for AlloyDB API requests.
67
+
68
+ Args:
69
+ alloydb_api_endpoint (str): Base URL to use when calling
70
+ the AlloyDB API endpoint.
71
+ quota_project (str): The Project ID for an existing Google Cloud
72
+ project. The project specified is used for quota and
73
+ billing purposes.
74
+ credentials (google.auth.credentials.Credentials):
75
+ A credentials object created from the google-auth Python library.
76
+ Must have the AlloyDB Admin scopes. For more info check out
77
+ https://google-auth.readthedocs.io/en/latest/.
78
+ client (v1beta.AlloyDBAdminAsyncClient): Async client used to make
79
+ requests to AlloyDB APIs.
80
+ Optional, defaults to None and creates new client.
81
+ driver (str): Database driver to be used by the client.
82
+ user_agent (str): The custom user-agent string to use in the HTTP
83
+ header when making requests to AlloyDB APIs.
84
+ Optional, defaults to None and uses a pre-defined one.
85
+ """
86
+ user_agent = _format_user_agent(driver, user_agent)
87
+
88
+ # TODO(rhatgadkar-goog): Rollback the PR of deciding between creating
89
+ # AlloyDBAdminClient or AlloyDBAdminAsyncClient when either
90
+ # https://github.com/grpc/grpc/issues/25364 is resolved or an async REST
91
+ # transport for AlloyDBAdminAsyncClient gets introduced.
92
+ # The issue is that the async gRPC transport does not work with multiple
93
+ # event loops in the same process. So all calls to the AlloyDB Admin
94
+ # API, even from multiple threads, need to be made to a single-event
95
+ # loop. See https://github.com/GoogleCloudPlatform/alloydb-python-connector/issues/435
96
+ # for more details.
97
+ self._is_sync = False
98
+ if client:
99
+ self._client = client
100
+ elif driver == "pg8000":
101
+ self._client = v1beta.AlloyDBAdminClient(
102
+ credentials=credentials,
103
+ transport="grpc",
104
+ client_options=ClientOptions(
105
+ api_endpoint=alloydb_api_endpoint,
106
+ quota_project_id=quota_project,
107
+ ),
108
+ client_info=ClientInfo(
109
+ user_agent=user_agent,
110
+ ),
111
+ )
112
+ self._is_sync = True
113
+ else:
114
+ self._client = v1beta.AlloyDBAdminAsyncClient(
115
+ credentials=credentials,
116
+ transport="grpc_asyncio",
117
+ client_options=ClientOptions(
118
+ api_endpoint=alloydb_api_endpoint,
119
+ quota_project_id=quota_project,
120
+ ),
121
+ client_info=ClientInfo(
122
+ user_agent=user_agent,
123
+ ),
124
+ )
125
+
126
+ self._credentials = credentials
127
+ # asyncpg does not currently support using metadata exchange
128
+ # only use metadata exchange for pg8000 driver
129
+ self._use_metadata = True if driver == "pg8000" else False
130
+ self._user_agent = user_agent
131
+
132
+ async def _get_metadata(
133
+ self,
134
+ project: str,
135
+ region: str,
136
+ cluster: str,
137
+ name: str,
138
+ ) -> dict[str, Optional[str]]:
139
+ """
140
+ Fetch the metadata for a given AlloyDB instance.
141
+
142
+ Call the AlloyDB APIs connectInfo method to retrieve the
143
+ information about an AlloyDB instance that is used to create secure
144
+ connections.
145
+
146
+ Args:
147
+ project (str): Google Cloud project ID that the AlloyDB instance
148
+ resides in.
149
+ region (str): Google Cloud region of the AlloyDB instance.
150
+ cluster (str): The name of the AlloyDB cluster.
151
+ name (str): The name of the AlloyDB instance.
152
+
153
+ Returns:
154
+ dict: IP addresses of the AlloyDB instance.
155
+ """
156
+ parent = (
157
+ f"projects/{project}/locations/{region}/clusters/{cluster}/instances/{name}"
158
+ )
159
+
160
+ req = v1beta.GetConnectionInfoRequest(parent=parent)
161
+ if self._is_sync:
162
+ resp = self._client.get_connection_info(request=req)
163
+ else:
164
+ resp = await self._client.get_connection_info(request=req)
165
+
166
+ # Remove trailing period from PSC DNS name.
167
+ psc_dns = resp.psc_dns_name
168
+ if psc_dns:
169
+ psc_dns = psc_dns.rstrip(".")
170
+
171
+ return {
172
+ "PRIVATE": resp.ip_address,
173
+ "PUBLIC": resp.public_ip_address,
174
+ "PSC": psc_dns,
175
+ }
176
+
177
+ async def _get_client_certificate(
178
+ self,
179
+ project: str,
180
+ region: str,
181
+ cluster: str,
182
+ pub_key: str,
183
+ ) -> tuple[str, list[str]]:
184
+ """
185
+ Fetch a client certificate for the given AlloyDB cluster.
186
+
187
+ Call the AlloyDB API's generateClientCertificate
188
+ method to create a signed TLS certificate that is authorized to connect via the
189
+ AlloyDB instance's serverside proxy. The cert is valid for twenty-four hours.
190
+
191
+ Args:
192
+ project (str): Google Cloud project ID that the AlloyDB instance
193
+ resides in.
194
+ region (str): Google Cloud region of the AlloyDB instance.
195
+ cluster (str): The name of the AlloyDB cluster.
196
+ pub_key (str): PEM-encoded client public key.
197
+
198
+ Returns:
199
+ tuple[str, list[str]]: tuple containing the CA certificate
200
+ and certificate chain for the AlloyDB instance.
201
+ """
202
+ parent = f"projects/{project}/locations/{region}/clusters/{cluster}"
203
+ dur = duration_pb2.Duration()
204
+ dur.seconds = 3600
205
+ req = v1beta.GenerateClientCertificateRequest(
206
+ parent=parent,
207
+ cert_duration=dur,
208
+ public_key=pub_key,
209
+ use_metadata_exchange=self._use_metadata,
210
+ )
211
+ if self._is_sync:
212
+ resp = self._client.generate_client_certificate(request=req)
213
+ else:
214
+ resp = await self._client.generate_client_certificate(request=req)
215
+ return (resp.ca_cert, resp.pem_certificate_chain)
216
+
217
+ async def get_connection_info(
218
+ self,
219
+ project: str,
220
+ region: str,
221
+ cluster: str,
222
+ name: str,
223
+ keys: asyncio.Future,
224
+ ) -> ConnectionInfo:
225
+ """Immediately performs a full refresh operation using the AlloyDB API.
226
+
227
+ Args:
228
+ project (str): The name of the project the AlloyDB instance is
229
+ located in.
230
+ region (str): The region the AlloyDB instance is located in.
231
+ cluster (str): The cluster the AlloyDB instance is located in.
232
+ name (str): Name of the AlloyDB instance.
233
+ keys (asyncio.Future): A future to the client's public-private key
234
+ pair.
235
+
236
+ Returns:
237
+ ConnectionInfo: All the information required to connect securely to
238
+ the AlloyDB instance.
239
+ """
240
+ priv_key, pub_key = await keys
241
+
242
+ # before making AlloyDB API calls, refresh creds if required
243
+ if not self._credentials.token_state == TokenState.FRESH:
244
+ self._credentials.refresh(requests.Request())
245
+
246
+ # fetch metadata
247
+ metadata_task = asyncio.create_task(
248
+ self._get_metadata(
249
+ project,
250
+ region,
251
+ cluster,
252
+ name,
253
+ )
254
+ )
255
+ # generate client and CA certs
256
+ certs_task = asyncio.create_task(
257
+ self._get_client_certificate(
258
+ project,
259
+ region,
260
+ cluster,
261
+ pub_key,
262
+ )
263
+ )
264
+
265
+ ip_addrs, certs = await asyncio.gather(metadata_task, certs_task)
266
+
267
+ # unpack certs
268
+ ca_cert, cert_chain = certs
269
+ # get expiration from client certificate
270
+ cert_obj = x509.load_pem_x509_certificate(cert_chain[0].encode("UTF-8"))
271
+ expiration = cert_obj.not_valid_after_utc
272
+
273
+ return ConnectionInfo(
274
+ cert_chain,
275
+ ca_cert,
276
+ priv_key,
277
+ ip_addrs,
278
+ expiration,
279
+ )