cloud-sql-python-connector 1.17.0__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.
@@ -0,0 +1,154 @@
1
+ """
2
+ Copyright 2021 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
+ https://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
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import asyncio
20
+ import copy
21
+ import datetime
22
+ import logging
23
+ import random
24
+ from typing import Any, Callable
25
+
26
+ import aiohttp
27
+ from google.auth.credentials import Credentials
28
+ from google.auth.credentials import Scoped
29
+ import google.auth.transport.requests
30
+
31
+ logger = logging.getLogger(name=__name__)
32
+
33
+ # _refresh_buffer is the amount of time before a refresh's result expires
34
+ # that a new refresh operation begins.
35
+ _refresh_buffer: int = 4 * 60 # 4 minutes
36
+
37
+
38
+ def _seconds_until_refresh(
39
+ expiration: datetime.datetime,
40
+ ) -> int:
41
+ """
42
+ Calculates the duration to wait before starting the next refresh.
43
+
44
+ Usually the duration will be half of the time until certificate
45
+ expiration.
46
+
47
+ :rtype: int
48
+ :returns: Time in seconds to wait before performing next refresh.
49
+ """
50
+
51
+ duration = int(
52
+ (expiration - datetime.datetime.now(datetime.timezone.utc)).total_seconds()
53
+ )
54
+
55
+ # if certificate duration is less than 1 hour
56
+ if duration < 3600:
57
+ # something is wrong with certificate, refresh now
58
+ if duration < _refresh_buffer:
59
+ return 0
60
+ # otherwise wait until 4 minutes before expiration for next refresh
61
+ return duration - _refresh_buffer
62
+
63
+ return duration // 2
64
+
65
+
66
+ async def _is_valid(task: asyncio.Task) -> bool:
67
+ try:
68
+ metadata = await task
69
+ # only valid if now is before the cert expires
70
+ if datetime.datetime.now(datetime.timezone.utc) < metadata.expiration:
71
+ return True
72
+ except Exception:
73
+ # supress any errors from task
74
+ logger.debug("Current instance metadata is invalid.")
75
+ return False
76
+
77
+
78
+ def _downscope_credentials(
79
+ credentials: Credentials,
80
+ scopes: list[str] = ["https://www.googleapis.com/auth/sqlservice.login"],
81
+ ) -> Credentials:
82
+ """Generate a down-scoped credential.
83
+
84
+ :type credentials: google.auth.credentials.Credentials
85
+ :param credentials
86
+ Credentials object used to generate down-scoped credentials.
87
+
88
+ :type scopes: list[str]
89
+ :param scopes
90
+ List of Google scopes to include in down-scoped credentials object.
91
+
92
+ :rtype: google.auth.credentials.Credentials
93
+ :returns: Down-scoped credentials object.
94
+ """
95
+ # credentials sourced from a service account or metadata are children of
96
+ # Scoped class and are capable of being re-scoped
97
+ if isinstance(credentials, Scoped):
98
+ scoped_creds = credentials.with_scopes(scopes=scopes)
99
+ # authenticated user credentials can not be re-scoped
100
+ else:
101
+ # create shallow copy to not overwrite scopes on default credentials
102
+ scoped_creds = copy.copy(credentials)
103
+ # overwrite '_scopes' to down-scope user credentials
104
+ # Cloud SDK reference: https://github.com/google-cloud-sdk-unofficial/google-cloud-sdk/blob/93920ccb6d2cce0fe6d1ce841e9e33410551d66b/lib/googlecloudsdk/command_lib/sql/generate_login_token_util.py#L116
105
+ scoped_creds._scopes = scopes
106
+ # down-scoped credentials require refresh, are invalid after being re-scoped
107
+ request = google.auth.transport.requests.Request()
108
+ scoped_creds.refresh(request)
109
+ return scoped_creds
110
+
111
+
112
+ def _exponential_backoff(attempt: int) -> float:
113
+ """Calculates a duration to backoff in milliseconds based on the attempt i.
114
+
115
+ The formula is:
116
+
117
+ base * multi^(attempt + 1 + random)
118
+
119
+ With base = 200ms and multi = 1.618, and random = [0.0, 1.0),
120
+ the backoff values would fall between the following low and high ends:
121
+
122
+ Attempt Low (ms) High (ms)
123
+
124
+ 0 324 524
125
+ 1 524 847
126
+ 2 847 1371
127
+ 3 1371 2218
128
+ 4 2218 3588
129
+
130
+ The theoretical worst case scenario would have a client wait 8.5s in total
131
+ for an API request to complete (with the first four attempts failing, and
132
+ the fifth succeeding).
133
+ """
134
+ base = 200
135
+ multi = 1.618
136
+ exp = attempt + 1 + random.random()
137
+ return base * pow(multi, exp)
138
+
139
+
140
+ async def retry_50x(
141
+ request_coro: Callable, *args: Any, **kwargs: Any
142
+ ) -> aiohttp.ClientResponse:
143
+ """Retry any 50x HTTP response up to X number of times."""
144
+ max_retries = 5
145
+ for i in range(max_retries):
146
+ resp = await request_coro(*args, **kwargs)
147
+ # backoff for any 50X errors
148
+ if resp.status >= 500 and i < max_retries:
149
+ # calculate backoff time
150
+ backoff = _exponential_backoff(i)
151
+ await asyncio.sleep(backoff / 1000)
152
+ else:
153
+ break
154
+ return resp
@@ -0,0 +1,70 @@
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
+ import dns.asyncresolver
16
+
17
+ from google.cloud.sql.connector.connection_name import (
18
+ _parse_connection_name_with_domain_name,
19
+ )
20
+ from google.cloud.sql.connector.connection_name import _parse_connection_name
21
+ from google.cloud.sql.connector.connection_name import ConnectionName
22
+ from google.cloud.sql.connector.exceptions import DnsResolutionError
23
+
24
+
25
+ class DefaultResolver:
26
+ """DefaultResolver simply validates and parses instance connection name."""
27
+
28
+ async def resolve(self, connection_name: str) -> ConnectionName:
29
+ return _parse_connection_name(connection_name)
30
+
31
+
32
+ class DnsResolver(dns.asyncresolver.Resolver):
33
+ """
34
+ DnsResolver resolves domain names into instance connection names using
35
+ TXT records in DNS.
36
+ """
37
+
38
+ async def resolve(self, dns: str) -> ConnectionName: # type: ignore
39
+ try:
40
+ conn_name = _parse_connection_name(dns)
41
+ except ValueError:
42
+ # The connection name was not project:region:instance format.
43
+ # Attempt to query a TXT record to get connection name.
44
+ conn_name = await self.query_dns(dns)
45
+ return conn_name
46
+
47
+ async def query_dns(self, dns: str) -> ConnectionName:
48
+ try:
49
+ # Attempt to query the TXT records.
50
+ records = await super().resolve(dns, "TXT", raise_on_no_answer=True)
51
+ # Sort the TXT record values alphabetically, strip quotes as record
52
+ # values can be returned as raw strings
53
+ rdata = [record.to_text().strip('"') for record in records]
54
+ rdata.sort()
55
+ # Attempt to parse records, returning the first valid record.
56
+ for record in rdata:
57
+ try:
58
+ conn_name = _parse_connection_name_with_domain_name(record, dns)
59
+ return conn_name
60
+ except Exception:
61
+ continue
62
+ # If all records failed to parse, throw error
63
+ raise DnsResolutionError(
64
+ f"Unable to parse TXT record for `{dns}` -> `{rdata[0]}`"
65
+ )
66
+ # Don't override above DnsResolutionError
67
+ except DnsResolutionError:
68
+ raise
69
+ except Exception as e:
70
+ raise DnsResolutionError(f"Unable to resolve TXT record for `{dns}`") from e
@@ -0,0 +1,103 @@
1
+ """
2
+ Copyright 2019 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
+ https://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
+ """
16
+
17
+ import aiofiles
18
+ from cryptography.hazmat.backends import default_backend
19
+ from cryptography.hazmat.primitives import serialization
20
+ from cryptography.hazmat.primitives.asymmetric import rsa
21
+
22
+
23
+ async def generate_keys() -> tuple[bytes, str]:
24
+ """A helper function to generate the private and public keys.
25
+
26
+ backend - The value specified is default_backend(). This is because the
27
+ cryptography library used to support different backends, but now only uses
28
+ the default_backend().
29
+
30
+ public_exponent - The public exponent is one of the variables used in the
31
+ generation of the keys. 65537 is recommended due to being a good balance
32
+ between speed and security.
33
+
34
+ key_size - The cryptography documentation recommended a key_size
35
+ of at least 2048.
36
+
37
+ """
38
+ private_key_obj = rsa.generate_private_key(
39
+ backend=default_backend(), public_exponent=65537, key_size=2048
40
+ )
41
+
42
+ pub_key = (
43
+ private_key_obj.public_key()
44
+ .public_bytes(
45
+ encoding=serialization.Encoding.PEM,
46
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
47
+ )
48
+ .decode("UTF-8")
49
+ )
50
+
51
+ priv_key = private_key_obj.private_bytes(
52
+ encoding=serialization.Encoding.PEM,
53
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
54
+ encryption_algorithm=serialization.NoEncryption(),
55
+ )
56
+
57
+ return priv_key, pub_key
58
+
59
+
60
+ async def write_to_file(
61
+ dir_path: str, serverCaCert: str, ephemeralCert: str, priv_key: bytes
62
+ ) -> tuple[str, str, str]:
63
+ """
64
+ Helper function to write the serverCaCert, ephemeral certificate and
65
+ private key to .pem files in a given directory
66
+ """
67
+ ca_filename = f"{dir_path}/ca.pem"
68
+ cert_filename = f"{dir_path}/cert.pem"
69
+ key_filename = f"{dir_path}/priv.pem"
70
+
71
+ async with aiofiles.open(ca_filename, "w+") as ca_out:
72
+ await ca_out.write(serverCaCert)
73
+ async with aiofiles.open(cert_filename, "w+") as ephemeral_out:
74
+ await ephemeral_out.write(ephemeralCert)
75
+ async with aiofiles.open(key_filename, "wb") as priv_out:
76
+ await priv_out.write(priv_key)
77
+
78
+ return (ca_filename, cert_filename, key_filename)
79
+
80
+
81
+ def format_database_user(database_version: str, user: str) -> str:
82
+ """
83
+ Format database `user` param for Cloud SQL automatic IAM authentication.
84
+
85
+ :type database_version: str
86
+ :param database_version
87
+ Cloud SQL database version. (i.e. POSTGRES_14, MYSQL8_0, etc.)
88
+
89
+ :type user: str
90
+ :param user
91
+ Database username to connect to Cloud SQL database with.
92
+ """
93
+ # remove suffix for Postgres service accounts
94
+ if database_version.startswith("POSTGRES"):
95
+ suffix = ".gserviceaccount.com"
96
+ user = user[: -len(suffix)] if user.endswith(suffix) else user
97
+ return user
98
+
99
+ # remove everything after and including the @ for MySQL
100
+ if database_version.startswith("MYSQL") and "@" in user:
101
+ return user.split("@")[0]
102
+
103
+ return user
@@ -0,0 +1,15 @@
1
+ # Copyright 2021 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
+ __version__ = "1.17.0"