cloud-sql-python-connector 1.9.2__py2.py3-none-any.whl → 1.11.0__py2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloud-sql-python-connector
3
- Version: 1.9.2
3
+ Version: 1.11.0
4
4
  Summary: The Cloud SQL Python Connector is a library that can be used alongside a database driver to allow users with sufficient permissions to connect to a Cloud SQL database without having to manually allowlist IPs or manage SSL certificates.
5
5
  Home-page: https://github.com/GoogleCloudPlatform/cloud-sql-python-connector
6
6
  Author: Google LLC
@@ -207,9 +207,10 @@ To close the `Connector` object's background resources, call its `close()` metho
207
207
  connector.close()
208
208
  ```
209
209
 
210
- **Note**: For more examples of using SQLAlchemy to manage connection pooling with the connector, please see [Cloud SQL SQLAlchemy Samples](https://cloud.google.com/sql/docs/postgres/connect-connectors#python_1).
211
-
212
- **Note for SQL Server users**: If your SQL Server instance requires SSL, you need to download the CA certificate for your instance and include `cafile={path to downloaded certificate}` and `validate_host=False`. This is a workaround for a [known issue](https://issuetracker.google.com/184867147).
210
+ > [!NOTE]
211
+ >
212
+ > For more examples of using SQLAlchemy to manage connection pooling with the connector,
213
+ > please see [Cloud SQL SQLAlchemy Samples](https://cloud.google.com/sql/docs/postgres/connect-connectors#python_1).
213
214
 
214
215
  ### Configuring the Connector
215
216
 
@@ -218,14 +219,15 @@ defaults for each connection to make, you can initialize a
218
219
  `Connector` object as follows:
219
220
 
220
221
  ```python
221
- from google.cloud.sql.connector import Connector, IPTypes
222
+ from google.cloud.sql.connector import Connector
222
223
 
223
224
  # Note: all parameters below are optional
224
225
  connector = Connector(
225
226
  ip_type="public", # can also be "private" or "psc"
226
227
  enable_iam_auth=False,
227
228
  timeout=30,
228
- credentials=custom_creds # google.auth.credentials.Credentials
229
+ credentials=custom_creds, # google.auth.credentials.Credentials
230
+ refresh_strategy="lazy", # can be "lazy" or "background"
229
231
  )
230
232
  ```
231
233
 
@@ -287,6 +289,21 @@ with Connector() as connector:
287
289
  print(row)
288
290
  ```
289
291
 
292
+ ### Configuring a Lazy Refresh (Cloud Run, Cloud Functions etc.)
293
+
294
+ The Connector's `refresh_strategy` argument can be set to `"lazy"` to configure
295
+ the Python Connector to retrieve connection info lazily and as-needed.
296
+ Otherwise, a background refresh cycle runs to retrive the connection info
297
+ periodically. This setting is useful in environments where the CPU may be
298
+ throttled outside of a request context, e.g., Cloud Run, Cloud Functions, etc.
299
+
300
+ To set the refresh strategy, set the `refresh_strategy` keyword argument when
301
+ initializing a `Connector`:
302
+
303
+ ```python
304
+ connector = Connector(refresh_strategy="lazy")
305
+ ```
306
+
290
307
  ### Specifying IP Address Type
291
308
 
292
309
  The Cloud SQL Python Connector can be used to connect to Cloud SQL instances
@@ -309,9 +326,11 @@ conn = connector.connect(
309
326
  )
310
327
  ```
311
328
 
312
- Note: If specifying Private IP or Private Service Connect, your application must be
313
- attached to the proper VPC network to connect to your Cloud SQL instance. For most
314
- applications this will require the use of a [VPC Connector][vpc-connector].
329
+ > [!IMPORTANT]
330
+ >
331
+ > If specifying Private IP or Private Service Connect (PSC), your application must be
332
+ > attached to the proper VPC network to connect to your Cloud SQL instance. For most
333
+ > applications this will require the use of a [VPC Connector][vpc-connector].
315
334
 
316
335
  [psc]: https://cloud.google.com/vpc/docs/private-service-connect
317
336
  [vpc-connector]: https://cloud.google.com/vpc/docs/configure-serverless-vpc-access#create-connector
@@ -340,9 +359,20 @@ conn = connector.connect(
340
359
  )
341
360
  ```
342
361
 
343
- ### SQL Server Active Directory Authentication
362
+ ### SQL Server (MSSQL)
363
+
364
+ > [!IMPORTANT]
365
+ >
366
+ > If your SQL Server instance is set to [enforce SSL connections](https://cloud.google.com/sql/docs/sqlserver/configure-ssl-instance#enforcing-ssl),
367
+ > you need to download the CA certificate for your instance and include `cafile={path to downloaded certificate}`
368
+ > and `validate_host=False`. This is a workaround for a [known issue](https://issuetracker.google.com/184867147).
369
+
370
+ #### Active Directory Authentication
344
371
 
345
- Active Directory authentication for SQL Server instances is currently only supported on Windows. First, make sure to follow [these steps](https://cloud.google.com/blog/topics/developers-practitioners/creating-sql-server-instance-integrated-active-directory-using-google-cloud-sql) to set up a Managed AD domain and join your Cloud SQL instance to the domain. [See here for more info on Cloud SQL Active Directory integration](https://cloud.google.com/sql/docs/sqlserver/ad).
372
+ Active Directory authentication for SQL Server instances is currently only supported on Windows.
373
+ First, make sure to follow [these steps](https://cloud.google.com/blog/topics/developers-practitioners/creating-sql-server-instance-integrated-active-directory-using-google-cloud-sql)
374
+ to set up a Managed AD domain and join your Cloud SQL instance to the domain.
375
+ [See here for more info on Cloud SQL Active Directory integration](https://cloud.google.com/sql/docs/sqlserver/ad).
346
376
 
347
377
  Once you have followed the steps linked above, you can run the following code to return a connection object:
348
378
 
@@ -375,6 +405,14 @@ The Python Connector can be used alongside popular Python web frameworks such
375
405
  as Flask, FastAPI, etc, to integrate Cloud SQL databases within your
376
406
  web applications.
377
407
 
408
+ > [!NOTE]
409
+ >
410
+ > For serverless environments such as Cloud Functions, Cloud Run, etc, it may be
411
+ > beneficial to initialize the `Connector` with the lazy refresh strategy.
412
+ > i.e. `Connector(refresh_strategy="lazy")`
413
+ >
414
+ > See [Configuring a Lazy Refresh](#configuring-a-lazy-refresh-cloud-run-cloud-functions-etc)
415
+
378
416
  #### Flask-SQLAlchemy
379
417
 
380
418
  [Flask-SQLAlchemy](https://flask-sqlalchemy.palletsprojects.com/en/2.x/)
@@ -555,8 +593,10 @@ An alternative to using the `create_async_connector` function is initializing
555
593
  a `Connector` as an async context manager, removing the need for explicit
556
594
  calls to `connector.close_async()` to cleanup resources.
557
595
 
558
- **Note:** This alternative requires that the running event loop be
559
- passed in as the `loop` argument to `Connector()`.
596
+ > [!NOTE]
597
+ >
598
+ > This alternative requires that the running event loop be
599
+ > passed in as the `loop` argument to `Connector()`.
560
600
 
561
601
  ```python
562
602
  import asyncio
@@ -603,6 +643,28 @@ async def main():
603
643
  await pool.dispose()
604
644
  ```
605
645
 
646
+ ### Debug Logging
647
+
648
+ The Cloud SQL Python Connector uses the standard [Python logging module][python-logging]
649
+ for debug logging support.
650
+
651
+ Add the below code to your application to enable debug logging with the Cloud SQL
652
+ Python Connector:
653
+
654
+ ```python
655
+ import logging
656
+
657
+ logging.basicConfig(format="%(asctime)s [%(levelname)s]: %(message)s")
658
+ logger = logging.getLogger(name="google.cloud.sql.connector")
659
+ logger.setLevel(logging.DEBUG)
660
+ ```
661
+
662
+ For more details on configuring logging, please refer to the
663
+ [Python logging docs][configure-logging].
664
+
665
+ [python-logging]: https://docs.python.org/3/library/logging.html
666
+ [configure-logging]: https://docs.python.org/3/howto/logging.html#configuring-logging
667
+
606
668
  ## Support policy
607
669
 
608
670
  ### Major version lifecycle
@@ -0,0 +1,22 @@
1
+ google/cloud/sql/connector/__init__.py,sha256=x8ui7u3gMlMI-LLJkKTtUU9yvzZvQkTJ-yzr2FH4VTU,983
2
+ google/cloud/sql/connector/asyncpg.py,sha256=c4lAlvEgGf_eBq0G8MVNPRAJQQvo6DcwybiXDW36qkU,1838
3
+ google/cloud/sql/connector/client.py,sha256=wGO6ESxWWYT5qZ-Utg_qh-Rtl_4k5tIjoD-ILcP4REQ,11481
4
+ google/cloud/sql/connector/connection_info.py,sha256=3nFuPtGPLUEZRy3Zx9Z2DvTdh6TJqmhOOoE6yKmSGBQ,4308
5
+ google/cloud/sql/connector/connector.py,sha256=NTubTmsT8rPpGYuhxXgKDW3MX7KUi4WSalKCHJkHFY8,21417
6
+ google/cloud/sql/connector/enums.py,sha256=44tfMm8D_t0ls6GfKwiQ6YkslWM21PZR40RhJPWntkc,2882
7
+ google/cloud/sql/connector/exceptions.py,sha256=OVenqBkkn98xFeytHuOXFTWa9R5bCmemBtbSIm8caoM,1665
8
+ google/cloud/sql/connector/instance.py,sha256=EySszNNHMFjAFskf6-G6Upikvb-tYjEwXx9A_OJBl14,10136
9
+ google/cloud/sql/connector/lazy.py,sha256=og4UPiJxeuLgN7tyqrFAnQF3895Yqdf6kkd4HZSzVJA,5093
10
+ google/cloud/sql/connector/pg8000.py,sha256=EZuO7XgCkG_Y79NQlh3Vv8Ff2w85BFJyFVO237hx1GM,1814
11
+ google/cloud/sql/connector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ google/cloud/sql/connector/pymysql.py,sha256=wldUgwDPlPa8Nj5F3JaipuP8hJyvzlgOAJbRnOoaB1U,2105
13
+ google/cloud/sql/connector/pytds.py,sha256=vKJoE7E2P0GrhZojg8bGZkqCMLc8dgKAcXE4ee7vEVc,2452
14
+ google/cloud/sql/connector/rate_limiter.py,sha256=bPTupfM4f5QRSCmCAWMqjQylEB7bUM-50plVBU3W2CA,2993
15
+ google/cloud/sql/connector/refresh_utils.py,sha256=CajL-MOcQBBRUkywowdHB3GNOr0WpQn37DXG7dRpkMk,5069
16
+ google/cloud/sql/connector/utils.py,sha256=FQs7RgoViJLIFgJIynVT-_DwAVWnpyXIKu5WZ4m10Ts,3448
17
+ google/cloud/sql/connector/version.py,sha256=l5iWT7w_8JLh0wHTJOCrcxkaD1xxBwPGJDzeSMQLXTI,598
18
+ cloud_sql_python_connector-1.11.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
19
+ cloud_sql_python_connector-1.11.0.dist-info/METADATA,sha256=4NukfHGYVymtt0NbBrH6bFW3BM544K072H-_f8gx6KM,25925
20
+ cloud_sql_python_connector-1.11.0.dist-info/WHEEL,sha256=bb2Ot9scclHKMOLDEHY6B2sicWOgugjFKaJsT7vwMQo,110
21
+ cloud_sql_python_connector-1.11.0.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
22
+ cloud_sql_python_connector-1.11.0.dist-info/RECORD,,
@@ -16,7 +16,14 @@ limitations under the License.
16
16
 
17
17
  from google.cloud.sql.connector.connector import Connector
18
18
  from google.cloud.sql.connector.connector import create_async_connector
19
- from google.cloud.sql.connector.instance import IPTypes
19
+ from google.cloud.sql.connector.enums import IPTypes
20
+ from google.cloud.sql.connector.enums import RefreshStrategy
20
21
  from google.cloud.sql.connector.version import __version__
21
22
 
22
- __all__ = ["__version__", "create_async_connector", "Connector", "IPTypes"]
23
+ __all__ = [
24
+ "__version__",
25
+ "create_async_connector",
26
+ "Connector",
27
+ "IPTypes",
28
+ "RefreshStrategy",
29
+ ]
@@ -14,6 +14,7 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import asyncio
17
18
  import datetime
18
19
  import logging
19
20
  from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING
@@ -21,9 +22,13 @@ from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING
21
22
  import aiohttp
22
23
  from cryptography.hazmat.backends import default_backend
23
24
  from cryptography.x509 import load_pem_x509_certificate
24
- import google.auth.transport.requests
25
+ from google.auth.credentials import TokenState
26
+ from google.auth.transport import requests
25
27
 
28
+ from google.cloud.sql.connector.connection_info import ConnectionInfo
29
+ from google.cloud.sql.connector.exceptions import AutoIAMAuthNotSupported
26
30
  from google.cloud.sql.connector.refresh_utils import _downscope_credentials
31
+ from google.cloud.sql.connector.refresh_utils import retry_50x
27
32
  from google.cloud.sql.connector.version import __version__ as version
28
33
 
29
34
  if TYPE_CHECKING:
@@ -113,9 +118,6 @@ class CloudSQLClient:
113
118
  addresses and their type and a string representing the
114
119
  certificate authority.
115
120
  """
116
- if not self._credentials.valid:
117
- request = google.auth.transport.requests.Request()
118
- self._credentials.refresh(request)
119
121
 
120
122
  headers = {
121
123
  "Authorization": f"Bearer {self._credentials.token}",
@@ -123,9 +125,10 @@ class CloudSQLClient:
123
125
 
124
126
  url = f"{self._sqladmin_api_endpoint}/sql/{API_VERSION}/projects/{project}/instances/{instance}/connectSettings"
125
127
 
126
- logger.debug(f"['{instance}']: Requesting metadata")
127
-
128
- resp = await self._client.get(url, headers=headers, raise_for_status=True)
128
+ resp = await self._client.get(url, headers=headers)
129
+ if resp.status >= 500:
130
+ resp = await retry_50x(self._client.get, url, headers=headers)
131
+ resp.raise_for_status()
129
132
  ret_dict = await resp.json()
130
133
 
131
134
  if ret_dict["region"] != region:
@@ -138,8 +141,10 @@ class CloudSQLClient:
138
141
  if "ipAddresses" in ret_dict
139
142
  else {}
140
143
  )
141
- if "dnsName" in ret_dict:
142
- ip_addresses["PSC"] = ret_dict["dnsName"]
144
+ # Remove trailing period from PSC DNS name.
145
+ psc_dns = ret_dict.get("dnsName")
146
+ if psc_dns:
147
+ ip_addresses["PSC"] = psc_dns.rstrip(".")
143
148
 
144
149
  return {
145
150
  "ip_addresses": ip_addresses,
@@ -174,13 +179,6 @@ class CloudSQLClient:
174
179
  :returns: An ephemeral certificate from the Cloud SQL instance that allows
175
180
  authorized connections to the instance.
176
181
  """
177
-
178
- logger.debug(f"['{instance}']: Requesting ephemeral certificate")
179
-
180
- if not self._credentials.valid:
181
- request = google.auth.transport.requests.Request()
182
- self._credentials.refresh(request)
183
-
184
182
  headers = {
185
183
  "Authorization": f"Bearer {self._credentials.token}",
186
184
  }
@@ -194,10 +192,10 @@ class CloudSQLClient:
194
192
  login_creds = _downscope_credentials(self._credentials)
195
193
  data["access_token"] = login_creds.token
196
194
 
197
- resp = await self._client.post(
198
- url, headers=headers, json=data, raise_for_status=True
199
- )
200
-
195
+ resp = await self._client.post(url, headers=headers, json=data)
196
+ if resp.status >= 500:
197
+ resp = await retry_50x(self._client.post, url, headers=headers, json=data)
198
+ resp.raise_for_status()
201
199
  ret_dict = await resp.json()
202
200
 
203
201
  ephemeral_cert: str = ret_dict["ephemeralCert"]["cert"]
@@ -220,6 +218,84 @@ class CloudSQLClient:
220
218
  expiration = token_expiration
221
219
  return ephemeral_cert, expiration
222
220
 
221
+ async def get_connection_info(
222
+ self,
223
+ project: str,
224
+ region: str,
225
+ instance: str,
226
+ keys: asyncio.Future,
227
+ enable_iam_auth: bool,
228
+ ) -> ConnectionInfo:
229
+ """Immediately performs a full refresh operation using the Cloud SQL
230
+ Admin API.
231
+
232
+ Args:
233
+ project (str): The name of the project the Cloud SQL instance is
234
+ located in.
235
+ region (str): The region the Cloud SQL instance is located in.
236
+ instance (str): Name of the Cloud SQL instance.
237
+ keys (asyncio.Future): A future to the client's public-private key
238
+ pair.
239
+ enable_iam_auth (bool): Whether an automatic IAM database
240
+ authentication connection is being requested (Postgres and MySQL).
241
+
242
+ Returns:
243
+ ConnectionInfo: All the information required to connect securely to
244
+ the Cloud SQL instance.
245
+ Raises:
246
+ AutoIAMAuthNotSupported: Database engine does not support automatic
247
+ IAM authentication.
248
+ """
249
+ priv_key, pub_key = await keys
250
+ # before making Cloud SQL Admin API calls, refresh creds if required
251
+ if not self._credentials.token_state == TokenState.FRESH:
252
+ self._credentials.refresh(requests.Request())
253
+
254
+ metadata_task = asyncio.create_task(
255
+ self._get_metadata(
256
+ project,
257
+ region,
258
+ instance,
259
+ )
260
+ )
261
+
262
+ ephemeral_task = asyncio.create_task(
263
+ self._get_ephemeral(
264
+ project,
265
+ instance,
266
+ pub_key,
267
+ enable_iam_auth,
268
+ )
269
+ )
270
+ try:
271
+ metadata = await metadata_task
272
+ # check if automatic IAM database authn is supported for database engine
273
+ if enable_iam_auth and not metadata["database_version"].startswith(
274
+ ("POSTGRES", "MYSQL")
275
+ ):
276
+ raise AutoIAMAuthNotSupported(
277
+ f"'{metadata['database_version']}' does not support "
278
+ "automatic IAM authentication. It is only supported with "
279
+ "Cloud SQL Postgres or MySQL instances."
280
+ )
281
+ except Exception:
282
+ # cancel ephemeral cert task if exception occurs before it is awaited
283
+ ephemeral_task.cancel()
284
+ raise
285
+
286
+ ephemeral_cert, expiration = await ephemeral_task
287
+
288
+ return ConnectionInfo(
289
+ ephemeral_cert,
290
+ metadata["server_ca_cert"],
291
+ priv_key,
292
+ metadata["ip_addresses"],
293
+ metadata["database_version"],
294
+ expiration,
295
+ )
296
+
223
297
  async def close(self) -> None:
224
298
  """Close CloudSQLClient gracefully."""
299
+ logger.debug("Waiting for Connector's http client to close")
225
300
  await self._client.close()
301
+ logger.debug("Closed Connector's http client")
@@ -0,0 +1,105 @@
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
+ from dataclasses import dataclass
18
+ import logging
19
+ import ssl
20
+ from tempfile import TemporaryDirectory
21
+ from typing import Any, Dict, Optional, TYPE_CHECKING
22
+
23
+ from google.cloud.sql.connector.exceptions import CloudSQLIPTypeError
24
+ from google.cloud.sql.connector.exceptions import TLSVersionError
25
+ from google.cloud.sql.connector.utils import write_to_file
26
+
27
+ if TYPE_CHECKING:
28
+ import datetime
29
+
30
+ from google.cloud.sql.connector.enums import IPTypes
31
+
32
+ logger = logging.getLogger(name=__name__)
33
+
34
+
35
+ @dataclass
36
+ class ConnectionInfo:
37
+ """Contains all necessary information to connect securely to the
38
+ server-side Proxy running on a Cloud SQL instance."""
39
+
40
+ client_cert: str
41
+ server_ca_cert: str
42
+ private_key: bytes
43
+ ip_addrs: Dict[str, Any]
44
+ database_version: str
45
+ expiration: datetime.datetime
46
+ context: Optional[ssl.SSLContext] = None
47
+
48
+ def create_ssl_context(self, enable_iam_auth: bool = False) -> ssl.SSLContext:
49
+ """Constructs a SSL/TLS context for the given connection info.
50
+
51
+ Cache the SSL context to ensure we don't read from disk repeatedly when
52
+ configuring a secure connection.
53
+ """
54
+ # if SSL context is cached, use it
55
+ if self.context is not None:
56
+ return self.context
57
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
58
+
59
+ # update ssl.PROTOCOL_TLS_CLIENT default
60
+ context.check_hostname = False
61
+
62
+ # TODO: remove if/else when Python 3.10 is min version. PEP 644 has been
63
+ # implemented. The ssl module requires OpenSSL 1.1.1 or newer.
64
+ # verify OpenSSL version supports TLSv1.3
65
+ if ssl.HAS_TLSv1_3:
66
+ # force TLSv1.3 if supported by client
67
+ context.minimum_version = ssl.TLSVersion.TLSv1_3
68
+ # fallback to TLSv1.2 for older versions of OpenSSL
69
+ else:
70
+ if enable_iam_auth:
71
+ raise TLSVersionError(
72
+ f"Your current version of OpenSSL ({ssl.OPENSSL_VERSION}) does not "
73
+ "support TLSv1.3, which is required to use IAM Authentication.\n"
74
+ "Upgrade your OpenSSL version to 1.1.1 for TLSv1.3 support."
75
+ )
76
+ logger.warning(
77
+ "TLSv1.3 is not supported with your version of OpenSSL "
78
+ f"({ssl.OPENSSL_VERSION}), falling back to TLSv1.2\n"
79
+ "Upgrade your OpenSSL version to 1.1.1 for TLSv1.3 support."
80
+ )
81
+ context.minimum_version = ssl.TLSVersion.TLSv1_2
82
+
83
+ # tmpdir and its contents are automatically deleted after the CA cert
84
+ # and ephemeral cert are loaded into the SSLcontext. The values
85
+ # need to be written to files in order to be loaded by the SSLContext
86
+ with TemporaryDirectory() as tmpdir:
87
+ ca_filename, cert_filename, key_filename = write_to_file(
88
+ tmpdir, self.server_ca_cert, self.client_cert, self.private_key
89
+ )
90
+ context.load_cert_chain(cert_filename, keyfile=key_filename)
91
+ context.load_verify_locations(cafile=ca_filename)
92
+ # set class attribute to cache context for subsequent calls
93
+ self.context = context
94
+ return context
95
+
96
+ def get_preferred_ip(self, ip_type: IPTypes) -> str:
97
+ """Returns the first IP address for the instance, according to the preference
98
+ supplied by ip_type. If no IP addressess with the given preference are found,
99
+ an error is raised."""
100
+ if ip_type.value in self.ip_addrs:
101
+ return self.ip_addrs[ip_type.value]
102
+ raise CloudSQLIPTypeError(
103
+ "Cloud SQL instance does not have any IP addresses matching "
104
+ f"preference: {ip_type.value}"
105
+ )