From eb95a9da35553387408e425721449660cc83196d Mon Sep 17 00:00:00 2001 From: Jack Wotherspoon Date: Fri, 25 Oct 2024 15:38:49 -0400 Subject: [PATCH 01/12] feat: support native asyncpg connection pools (#1182) As of asyncpg v0.30.0, asyncpg native connection pools now support a creation function (callable) via its connect argument, similar to SQLAlchemy's async_creator argument to generate connections. Adding integration test and usage samples to README. Also bumping asyncpg min supported version in setup.py to 0.30.0 to force supported version for this feature. --- README.md | 92 ++++++++++++++++++--- setup.py | 2 +- tests/system/test_asyncpg_connection.py | 101 +++++++++++++++++++++++- 3 files changed, 180 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c3f4c47ae..28553f972 100644 --- a/README.md +++ b/README.md @@ -502,6 +502,42 @@ The `create_async_connector` allows all the same input arguments as the Once a `Connector` object is returned by `create_async_connector` you can call its `connect_async` method, just as you would the `connect` method: +#### Asyncpg Connection Pool + +```python +import asyncpg +from google.cloud.sql.connector import Connector, create_async_connector + +async def main(): + # initialize Connector object for connections to Cloud SQL + connector = create_async_connector() + + # creation function to generate asyncpg connections as the 'connect' arg + async def getconn(instance_connection_name, **kwargs) -> asyncpg.Connection: + return await connector.connect_async( + instance_connection_name, + "asyncpg", + user="my-user", + password="my-password", + db="my-db", + **kwargs, # ... additional asyncpg args + ) + + # initialize connection pool + pool = await asyncpg.create_pool( + "my-project:my-region:my-instance", connect=getconn + ) + + # acquire connection and query Cloud SQL database + async with pool.acquire() as conn: + res = await conn.fetch("SELECT NOW()") + + # close Connector + await connector.close_async() +``` + +#### SQLAlchemy Async Engine + ```python import asyncpg @@ -511,7 +547,7 @@ from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine from google.cloud.sql.connector import Connector, create_async_connector async def init_connection_pool(connector: Connector) -> AsyncEngine: - # initialize Connector object for connections to Cloud SQL + # creation function to generate asyncpg connections as 'async_creator' arg async def getconn() -> asyncpg.Connection: conn: asyncpg.Connection = await connector.connect_async( "project:region:instance", # Cloud SQL instance connection name @@ -564,6 +600,40 @@ calls to `connector.close_async()` to cleanup resources. > This alternative requires that the running event loop be > passed in as the `loop` argument to `Connector()`. +#### Asyncpg Connection Pool + +```python +import asyncpg +from google.cloud.sql.connector import Connector, create_async_connector + +async def main(): + # initialize Connector object for connections to Cloud SQL + loop = asyncio.get_running_loop() + async with Connector(loop=loop) as connector: + + # creation function to generate asyncpg connections as the 'connect' arg + async def getconn(instance_connection_name, **kwargs) -> asyncpg.Connection: + return await connector.connect_async( + instance_connection_name, + "asyncpg", + user="my-user", + password="my-password", + db="my-db", + **kwargs, # ... additional asyncpg args + ) + + # create connection pool + pool = await asyncpg.create_pool( + "my-project:my-region:my-instance", connect=getconn + ) + + # acquire connection and query Cloud SQL database + async with pool.acquire() as conn: + res = await conn.fetch("SELECT NOW()") +``` + +#### SQLAlchemy Async Engine + ```python import asyncio import asyncpg @@ -574,17 +644,17 @@ from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine from google.cloud.sql.connector import Connector async def init_connection_pool(connector: Connector) -> AsyncEngine: - # initialize Connector object for connections to Cloud SQL + # creation function to generate asyncpg connections as 'async_creator' arg async def getconn() -> asyncpg.Connection: - conn: asyncpg.Connection = await connector.connect_async( - "project:region:instance", # Cloud SQL instance connection name - "asyncpg", - user="my-user", - password="my-password", - db="my-db-name" - # ... additional database driver args - ) - return conn + conn: asyncpg.Connection = await connector.connect_async( + "project:region:instance", # Cloud SQL instance connection name + "asyncpg", + user="my-user", + password="my-password", + db="my-db-name" + # ... additional database driver args + ) + return conn # The Cloud SQL Python Connector can be used along with SQLAlchemy using the # 'async_creator' argument to 'create_async_engine' diff --git a/setup.py b/setup.py index bdf7a27c4..bb70449a5 100644 --- a/setup.py +++ b/setup.py @@ -80,7 +80,7 @@ "pymysql": ["PyMySQL>=1.1.0"], "pg8000": ["pg8000>=1.31.1"], "pytds": ["python-tds>=1.15.0"], - "asyncpg": ["asyncpg>=0.29.0"], + "asyncpg": ["asyncpg>=0.30.0"], }, python_requires=">=3.9", include_package_data=True, diff --git a/tests/system/test_asyncpg_connection.py b/tests/system/test_asyncpg_connection.py index 20715c650..98a86a1a8 100644 --- a/tests/system/test_asyncpg_connection.py +++ b/tests/system/test_asyncpg_connection.py @@ -16,7 +16,7 @@ import asyncio import os -from typing import Tuple +from typing import Any, Tuple import asyncpg import sqlalchemy @@ -88,7 +88,68 @@ async def getconn() -> asyncpg.Connection: return engine, connector -async def test_connection_with_asyncpg() -> None: +async def create_asyncpg_pool( + instance_connection_name: str, + user: str, + password: str, + db: str, + refresh_strategy: str = "background", +) -> Tuple[asyncpg.Pool, Connector]: + """Creates a native asyncpg connection pool for a Cloud SQL instance and + returns the pool and the connector. Callers are responsible for closing the + pool and the connector. + + A sample invocation looks like: + + pool, connector = await create_asyncpg_pool( + inst_conn_name, + user, + password, + db, + ) + async with pool.acquire() as conn: + hello = await conn.fetch("SELECT 'Hello World!'") + # do something with query result + await connector.close_async() + + Args: + instance_connection_name (str): + The instance connection name specifies the instance relative to the + project and region. For example: "my-project:my-region:my-instance" + user (str): + The database user name, e.g., postgres + password (str): + The database user's password, e.g., secret-password + db (str): + The name of the database, e.g., mydb + refresh_strategy (Optional[str]): + Refresh strategy for the Cloud SQL Connector. Can be one of "lazy" + or "background". For serverless environments use "lazy" to avoid + errors resulting from CPU being throttled. + """ + loop = asyncio.get_running_loop() + connector = Connector(loop=loop, refresh_strategy=refresh_strategy) + + async def getconn( + instance_connection_name: str, **kwargs: Any + ) -> asyncpg.Connection: + conn: asyncpg.Connection = await connector.connect_async( + instance_connection_name, + "asyncpg", + user=user, + password=password, + db=db, + ip_type="public", # can also be "private" or "psc", + **kwargs + ) + return conn + + # create native asyncpg pool (requires asyncpg version >=0.30.0) + pool = await asyncpg.create_pool(instance_connection_name, connect=getconn) + return pool, connector + + +async def test_sqlalchemy_connection_with_asyncpg() -> None: """Basic test to get time from database.""" inst_conn_name = os.environ["POSTGRES_CONNECTION_NAME"] user = os.environ["POSTGRES_USER"] @@ -104,7 +165,7 @@ async def test_connection_with_asyncpg() -> None: await connector.close_async() -async def test_lazy_connection_with_asyncpg() -> None: +async def test_lazy_sqlalchemy_connection_with_asyncpg() -> None: """Basic test to get time from database.""" inst_conn_name = os.environ["POSTGRES_CONNECTION_NAME"] user = os.environ["POSTGRES_USER"] @@ -120,3 +181,37 @@ async def test_lazy_connection_with_asyncpg() -> None: assert res[0] == 1 await connector.close_async() + + +async def test_connection_with_asyncpg() -> None: + """Basic test to get time from database.""" + inst_conn_name = os.environ["POSTGRES_CONNECTION_NAME"] + user = os.environ["POSTGRES_USER"] + password = os.environ["POSTGRES_PASS"] + db = os.environ["POSTGRES_DB"] + + pool, connector = await create_asyncpg_pool(inst_conn_name, user, password, db) + + async with pool.acquire() as conn: + res = await conn.fetch("SELECT 1") + assert res[0][0] == 1 + + await connector.close_async() + + +async def test_lazy_connection_with_asyncpg() -> None: + """Basic test to get time from database.""" + inst_conn_name = os.environ["POSTGRES_CONNECTION_NAME"] + user = os.environ["POSTGRES_USER"] + password = os.environ["POSTGRES_PASS"] + db = os.environ["POSTGRES_DB"] + + pool, connector = await create_asyncpg_pool( + inst_conn_name, user, password, db, "lazy" + ) + + async with pool.acquire() as conn: + res = await conn.fetch("SELECT 1") + assert res[0][0] == 1 + + await connector.close_async() From 5b245c64ffc9ecb4e1565f9320c54d7023cc758e Mon Sep 17 00:00:00 2001 From: Jack Wotherspoon Date: Fri, 25 Oct 2024 16:22:23 -0400 Subject: [PATCH 02/12] chore: update type hints to standard collections (#1183) --- google/cloud/sql/connector/asyncpg.py | 1 + google/cloud/sql/connector/client.py | 8 ++++---- google/cloud/sql/connector/connection_info.py | 4 ++-- google/cloud/sql/connector/connector.py | 6 +++--- google/cloud/sql/connector/instance.py | 3 +-- google/cloud/sql/connector/refresh_utils.py | 6 +++--- google/cloud/sql/connector/utils.py | 6 ++---- tests/system/test_asyncpg_connection.py | 6 +++--- tests/system/test_asyncpg_iam_auth.py | 3 +-- tests/system/test_ip_types.py | 1 + tests/system/test_pg8000_connection.py | 3 +-- tests/system/test_pg8000_iam_auth.py | 3 +-- tests/system/test_pymysql_connection.py | 3 +-- tests/system/test_pymysql_iam_auth.py | 3 +-- tests/system/test_pytds_connection.py | 3 +-- tests/unit/mocks.py | 6 +++--- tests/unit/test_asyncpg.py | 1 + 17 files changed, 30 insertions(+), 36 deletions(-) diff --git a/google/cloud/sql/connector/asyncpg.py b/google/cloud/sql/connector/asyncpg.py index 0e03c0a65..47d34895f 100644 --- a/google/cloud/sql/connector/asyncpg.py +++ b/google/cloud/sql/connector/asyncpg.py @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. """ + import ssl from typing import Any, TYPE_CHECKING diff --git a/google/cloud/sql/connector/client.py b/google/cloud/sql/connector/client.py index 1c805814e..ed305ec53 100644 --- a/google/cloud/sql/connector/client.py +++ b/google/cloud/sql/connector/client.py @@ -17,7 +17,7 @@ import asyncio import datetime import logging -from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING +from typing import Any, Optional, TYPE_CHECKING import aiohttp from cryptography.hazmat.backends import default_backend @@ -98,7 +98,7 @@ async def _get_metadata( project: str, region: str, instance: str, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Requests metadata from the Cloud SQL Instance and returns a dictionary containing the IP addresses and certificate authority of the Cloud SQL Instance. @@ -113,7 +113,7 @@ async def _get_metadata( :type instance: str :param instance: A string representing the name of the instance. - :rtype: Dict[str: Union[Dict, str]] + :rtype: dict[str: Union[dict, str]] :returns: Returns a dictionary containing a dictionary of all IP addresses and their type and a string representing the certificate authority. @@ -161,7 +161,7 @@ async def _get_ephemeral( instance: str, pub_key: str, enable_iam_auth: bool = False, - ) -> Tuple[str, datetime.datetime]: + ) -> tuple[str, datetime.datetime]: """Asynchronously requests an ephemeral certificate from the Cloud SQL Instance. :type project: str diff --git a/google/cloud/sql/connector/connection_info.py b/google/cloud/sql/connector/connection_info.py index 7181134db..b738063c2 100644 --- a/google/cloud/sql/connector/connection_info.py +++ b/google/cloud/sql/connector/connection_info.py @@ -17,7 +17,7 @@ from dataclasses import dataclass import logging import ssl -from typing import Any, Dict, Optional, TYPE_CHECKING +from typing import Any, Optional, TYPE_CHECKING from aiofiles.tempfile import TemporaryDirectory @@ -41,7 +41,7 @@ class ConnectionInfo: client_cert: str server_ca_cert: str private_key: bytes - ip_addrs: Dict[str, Any] + ip_addrs: dict[str, Any] database_version: str expiration: datetime.datetime context: Optional[ssl.SSLContext] = None diff --git a/google/cloud/sql/connector/connector.py b/google/cloud/sql/connector/connector.py index 51107f2cc..7a89d7194 100755 --- a/google/cloud/sql/connector/connector.py +++ b/google/cloud/sql/connector/connector.py @@ -21,7 +21,7 @@ import logging from threading import Thread from types import TracebackType -from typing import Any, Dict, Optional, Tuple, Type, Union +from typing import Any, Optional, Type, Union import google.auth from google.auth.credentials import Credentials @@ -133,8 +133,8 @@ def __init__( ) # initialize dict to store caches, key is a tuple consisting of instance # connection name string and enable_iam_auth boolean flag - self._cache: Dict[ - Tuple[str, bool], Union[RefreshAheadCache, LazyRefreshCache] + self._cache: dict[ + tuple[str, bool], Union[RefreshAheadCache, LazyRefreshCache] ] = {} self._client: Optional[CloudSQLClient] = None diff --git a/google/cloud/sql/connector/instance.py b/google/cloud/sql/connector/instance.py index ab3c29de2..818d5eb11 100644 --- a/google/cloud/sql/connector/instance.py +++ b/google/cloud/sql/connector/instance.py @@ -22,7 +22,6 @@ from datetime import timezone import logging import re -from typing import Tuple import aiohttp @@ -43,7 +42,7 @@ CONN_NAME_REGEX = re.compile(("([^:]+(:[^:]+)?):([^:]+):([^:]+)")) -def _parse_instance_connection_name(connection_name: str) -> Tuple[str, str, str]: +def _parse_instance_connection_name(connection_name: str) -> tuple[str, str, str]: if CONN_NAME_REGEX.fullmatch(connection_name) is None: raise ValueError( "Arg `instance_connection_string` must have " diff --git a/google/cloud/sql/connector/refresh_utils.py b/google/cloud/sql/connector/refresh_utils.py index bc3711079..173f0d2ee 100644 --- a/google/cloud/sql/connector/refresh_utils.py +++ b/google/cloud/sql/connector/refresh_utils.py @@ -21,7 +21,7 @@ import datetime import logging import random -from typing import Any, Callable, List +from typing import Any, Callable import aiohttp from google.auth.credentials import Credentials @@ -77,7 +77,7 @@ async def _is_valid(task: asyncio.Task) -> bool: def _downscope_credentials( credentials: Credentials, - scopes: List[str] = ["https://www.googleapis.com/auth/sqlservice.login"], + scopes: list[str] = ["https://www.googleapis.com/auth/sqlservice.login"], ) -> Credentials: """Generate a down-scoped credential. @@ -85,7 +85,7 @@ def _downscope_credentials( :param credentials Credentials object used to generate down-scoped credentials. - :type scopes: List[str] + :type scopes: list[str] :param scopes List of Google scopes to include in down-scoped credentials object. diff --git a/google/cloud/sql/connector/utils.py b/google/cloud/sql/connector/utils.py index 47a318fb8..8caa73af6 100755 --- a/google/cloud/sql/connector/utils.py +++ b/google/cloud/sql/connector/utils.py @@ -14,15 +14,13 @@ limitations under the License. """ -from typing import Tuple - import aiofiles from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa -async def generate_keys() -> Tuple[bytes, str]: +async def generate_keys() -> tuple[bytes, str]: """A helper function to generate the private and public keys. backend - The value specified is default_backend(). This is because the @@ -61,7 +59,7 @@ async def generate_keys() -> Tuple[bytes, str]: async def write_to_file( dir_path: str, serverCaCert: str, ephemeralCert: str, priv_key: bytes -) -> Tuple[str, str, str]: +) -> tuple[str, str, str]: """ Helper function to write the serverCaCert, ephemeral certificate and private key to .pem files in a given directory diff --git a/tests/system/test_asyncpg_connection.py b/tests/system/test_asyncpg_connection.py index 98a86a1a8..eec9662e1 100644 --- a/tests/system/test_asyncpg_connection.py +++ b/tests/system/test_asyncpg_connection.py @@ -16,7 +16,7 @@ import asyncio import os -from typing import Any, Tuple +from typing import Any import asyncpg import sqlalchemy @@ -31,7 +31,7 @@ async def create_sqlalchemy_engine( password: str, db: str, refresh_strategy: str = "background", -) -> Tuple[sqlalchemy.ext.asyncio.engine.AsyncEngine, Connector]: +) -> tuple[sqlalchemy.ext.asyncio.engine.AsyncEngine, Connector]: """Creates a connection pool for a Cloud SQL instance and returns the pool and the connector. Callers are responsible for closing the pool and the connector. @@ -94,7 +94,7 @@ async def create_asyncpg_pool( password: str, db: str, refresh_strategy: str = "background", -) -> Tuple[asyncpg.Pool, Connector]: +) -> tuple[asyncpg.Pool, Connector]: """Creates a native asyncpg connection pool for a Cloud SQL instance and returns the pool and the connector. Callers are responsible for closing the pool and the connector. diff --git a/tests/system/test_asyncpg_iam_auth.py b/tests/system/test_asyncpg_iam_auth.py index 79c6e9c12..103efd1af 100644 --- a/tests/system/test_asyncpg_iam_auth.py +++ b/tests/system/test_asyncpg_iam_auth.py @@ -16,7 +16,6 @@ import asyncio import os -from typing import Tuple import asyncpg import sqlalchemy @@ -30,7 +29,7 @@ async def create_sqlalchemy_engine( user: str, db: str, refresh_strategy: str = "background", -) -> Tuple[sqlalchemy.ext.asyncio.engine.AsyncEngine, Connector]: +) -> tuple[sqlalchemy.ext.asyncio.engine.AsyncEngine, Connector]: """Creates a connection pool for a Cloud SQL instance and returns the pool and the connector. Callers are responsible for closing the pool and the connector. diff --git a/tests/system/test_ip_types.py b/tests/system/test_ip_types.py index 4ebcb467d..2df3b1df5 100644 --- a/tests/system/test_ip_types.py +++ b/tests/system/test_ip_types.py @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. """ + import os import uuid diff --git a/tests/system/test_pg8000_connection.py b/tests/system/test_pg8000_connection.py index b42565a7f..ea5b2ee69 100644 --- a/tests/system/test_pg8000_connection.py +++ b/tests/system/test_pg8000_connection.py @@ -16,7 +16,6 @@ from datetime import datetime import os -from typing import Tuple # [START cloud_sql_connector_postgres_pg8000] import pg8000 @@ -31,7 +30,7 @@ def create_sqlalchemy_engine( password: str, db: str, refresh_strategy: str = "background", -) -> Tuple[sqlalchemy.engine.Engine, Connector]: +) -> tuple[sqlalchemy.engine.Engine, Connector]: """Creates a connection pool for a Cloud SQL instance and returns the pool and the connector. Callers are responsible for closing the pool and the connector. diff --git a/tests/system/test_pg8000_iam_auth.py b/tests/system/test_pg8000_iam_auth.py index 1251d10d8..60d2974f0 100644 --- a/tests/system/test_pg8000_iam_auth.py +++ b/tests/system/test_pg8000_iam_auth.py @@ -16,7 +16,6 @@ from datetime import datetime import os -from typing import Tuple import pg8000 import sqlalchemy @@ -29,7 +28,7 @@ def create_sqlalchemy_engine( user: str, db: str, refresh_strategy: str = "background", -) -> Tuple[sqlalchemy.engine.Engine, Connector]: +) -> tuple[sqlalchemy.engine.Engine, Connector]: """Creates a connection pool for a Cloud SQL instance and returns the pool and the connector. Callers are responsible for closing the pool and the connector. diff --git a/tests/system/test_pymysql_connection.py b/tests/system/test_pymysql_connection.py index 1cf0c00ce..490b1fab4 100644 --- a/tests/system/test_pymysql_connection.py +++ b/tests/system/test_pymysql_connection.py @@ -16,7 +16,6 @@ from datetime import datetime import os -from typing import Tuple # [START cloud_sql_connector_mysql_pymysql] import pymysql @@ -31,7 +30,7 @@ def create_sqlalchemy_engine( password: str, db: str, refresh_strategy: str = "background", -) -> Tuple[sqlalchemy.engine.Engine, Connector]: +) -> tuple[sqlalchemy.engine.Engine, Connector]: """Creates a connection pool for a Cloud SQL instance and returns the pool and the connector. Callers are responsible for closing the pool and the connector. diff --git a/tests/system/test_pymysql_iam_auth.py b/tests/system/test_pymysql_iam_auth.py index a571ac630..80a10a134 100644 --- a/tests/system/test_pymysql_iam_auth.py +++ b/tests/system/test_pymysql_iam_auth.py @@ -16,7 +16,6 @@ from datetime import datetime import os -from typing import Tuple import pymysql import sqlalchemy @@ -29,7 +28,7 @@ def create_sqlalchemy_engine( user: str, db: str, refresh_strategy: str = "background", -) -> Tuple[sqlalchemy.engine.Engine, Connector]: +) -> tuple[sqlalchemy.engine.Engine, Connector]: """Creates a connection pool for a Cloud SQL instance and returns the pool and the connector. Callers are responsible for closing the pool and the connector. diff --git a/tests/system/test_pytds_connection.py b/tests/system/test_pytds_connection.py index 8aadad4e7..d848abc18 100644 --- a/tests/system/test_pytds_connection.py +++ b/tests/system/test_pytds_connection.py @@ -15,7 +15,6 @@ """ import os -from typing import Tuple # [START cloud_sql_connector_mysql_pytds] import pytds @@ -30,7 +29,7 @@ def create_sqlalchemy_engine( password: str, db: str, refresh_strategy: str = "background", -) -> Tuple[sqlalchemy.engine.Engine, Connector]: +) -> tuple[sqlalchemy.engine.Engine, Connector]: """Creates a connection pool for a Cloud SQL instance and returns the pool and the connector. Callers are responsible for closing the pool and the connector. diff --git a/tests/unit/mocks.py b/tests/unit/mocks.py index 0f25f1c14..5d863677b 100644 --- a/tests/unit/mocks.py +++ b/tests/unit/mocks.py @@ -19,7 +19,7 @@ import datetime import json import ssl -from typing import Any, Callable, Dict, Literal, Optional, Tuple +from typing import Any, Callable, Literal, Optional from aiofiles.tempfile import TemporaryDirectory from aiohttp import web @@ -113,7 +113,7 @@ def generate_cert( cert_before: datetime.datetime = datetime.datetime.now(datetime.timezone.utc), cert_after: datetime.datetime = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=1), -) -> Tuple[x509.CertificateBuilder, rsa.RSAPrivateKey]: +) -> tuple[x509.CertificateBuilder, rsa.RSAPrivateKey]: """ Generate a private key and cert object to be used in testing. """ @@ -221,7 +221,7 @@ def __init__( region: str = "test-region", name: str = "test-instance", db_version: str = "POSTGRES_15", - ip_addrs: Dict = { + ip_addrs: dict = { "PRIMARY": "127.0.0.1", "PRIVATE": "10.0.0.1", }, diff --git a/tests/unit/test_asyncpg.py b/tests/unit/test_asyncpg.py index 3076a6415..b29df8841 100644 --- a/tests/unit/test_asyncpg.py +++ b/tests/unit/test_asyncpg.py @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. """ + import ssl from typing import Any From 3b24c100d9e4b4cc158e6c35327ab9e9cbf28a73 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 30 Oct 2024 14:15:41 +0100 Subject: [PATCH 03/12] chore(deps): update dependency pytest-cov to v6 (#1185) --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 5eeb3b820..e18c63928 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,6 +1,6 @@ pytest==8.3.3 mock==5.1.0 -pytest-cov==5.0.0 +pytest-cov==6.0.0 pytest-asyncio==0.24.0 SQLAlchemy[asyncio]==2.0.36 sqlalchemy-pytds==1.0.2 From ef7d8fe242a41e51c7da5d9280754ad98b2daaab Mon Sep 17 00:00:00 2001 From: Jack Wotherspoon Date: Fri, 1 Nov 2024 11:54:42 -0400 Subject: [PATCH 04/12] refactor: add `ConnectionName` class (#1186) This PR refactors all instance connection name related code into its own file connection_name.py It introduces the ConnectionName class which will make tracking if a DNS name was given to the Connector easier in the future. --- google/cloud/sql/connector/connection_name.py | 51 +++++++++++++++++ google/cloud/sql/connector/instance.py | 50 ++++++----------- google/cloud/sql/connector/lazy.py | 22 ++++---- google/cloud/sql/connector/pg8000.py | 1 + google/cloud/sql/connector/pymysql.py | 1 + google/cloud/sql/connector/pytds.py | 1 + noxfile.py | 2 + tests/conftest.py | 1 + tests/unit/test_connection_name.py | 56 +++++++++++++++++++ tests/unit/test_instance.py | 30 ---------- tests/unit/test_pg8000.py | 1 + tests/unit/test_pymysql.py | 1 + tests/unit/test_pytds.py | 1 + tests/unit/test_rate_limiter.py | 1 + tests/unit/test_utils.py | 1 + 15 files changed, 147 insertions(+), 73 deletions(-) create mode 100644 google/cloud/sql/connector/connection_name.py create mode 100644 tests/unit/test_connection_name.py diff --git a/google/cloud/sql/connector/connection_name.py b/google/cloud/sql/connector/connection_name.py new file mode 100644 index 000000000..d240fb565 --- /dev/null +++ b/google/cloud/sql/connector/connection_name.py @@ -0,0 +1,51 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass +import re + +# Instance connection name is the format :: +# Additionally, we have to support legacy "domain-scoped" projects +# (e.g. "google.com:PROJECT") +CONN_NAME_REGEX = re.compile(("([^:]+(:[^:]+)?):([^:]+):([^:]+)")) + + +@dataclass +class ConnectionName: + """ConnectionName represents a Cloud SQL instance's "instance connection name". + + Takes the format "::". + """ + + project: str + region: str + instance_name: str + + def __str__(self) -> str: + return f"{self.project}:{self.region}:{self.instance_name}" + + +def _parse_instance_connection_name(connection_name: str) -> ConnectionName: + if CONN_NAME_REGEX.fullmatch(connection_name) is None: + raise ValueError( + "Arg `instance_connection_string` must have " + "format: PROJECT:REGION:INSTANCE, " + f"got {connection_name}." + ) + connection_name_split = CONN_NAME_REGEX.split(connection_name) + return ConnectionName( + connection_name_split[1], + connection_name_split[3], + connection_name_split[4], + ) diff --git a/google/cloud/sql/connector/instance.py b/google/cloud/sql/connector/instance.py index 818d5eb11..f244b8cf3 100644 --- a/google/cloud/sql/connector/instance.py +++ b/google/cloud/sql/connector/instance.py @@ -21,12 +21,12 @@ from datetime import timedelta from datetime import timezone import logging -import re import aiohttp from google.cloud.sql.connector.client import CloudSQLClient from google.cloud.sql.connector.connection_info import ConnectionInfo +from google.cloud.sql.connector.connection_name import _parse_instance_connection_name from google.cloud.sql.connector.exceptions import RefreshNotValidError from google.cloud.sql.connector.rate_limiter import AsyncRateLimiter from google.cloud.sql.connector.refresh_utils import _is_valid @@ -36,22 +36,6 @@ APPLICATION_NAME = "cloud-sql-python-connector" -# Instance connection name is the format :: -# Additionally, we have to support legacy "domain-scoped" projects -# (e.g. "google.com:PROJECT") -CONN_NAME_REGEX = re.compile(("([^:]+(:[^:]+)?):([^:]+):([^:]+)")) - - -def _parse_instance_connection_name(connection_name: str) -> tuple[str, str, str]: - if CONN_NAME_REGEX.fullmatch(connection_name) is None: - raise ValueError( - "Arg `instance_connection_string` must have " - "format: PROJECT:REGION:INSTANCE, " - f"got {connection_name}." - ) - connection_name_split = CONN_NAME_REGEX.split(connection_name) - return connection_name_split[1], connection_name_split[3], connection_name_split[4] - class RefreshAheadCache: """Cache that refreshes connection info in the background prior to expiration. @@ -81,10 +65,13 @@ def __init__( connections. """ # validate and parse instance connection name - self._project, self._region, self._instance = _parse_instance_connection_name( - instance_connection_string + conn_name = _parse_instance_connection_name(instance_connection_string) + self._project, self._region, self._instance = ( + conn_name.project, + conn_name.region, + conn_name.instance_name, ) - self._instance_connection_string = instance_connection_string + self._conn_name = conn_name self._enable_iam_auth = enable_iam_auth self._keys = keys @@ -121,8 +108,7 @@ async def _perform_refresh(self) -> ConnectionInfo: """ self._refresh_in_progress.set() logger.debug( - f"['{self._instance_connection_string}']: Connection info refresh " - "operation started" + f"['{self._conn_name}']: Connection info refresh " "operation started" ) try: @@ -135,17 +121,16 @@ async def _perform_refresh(self) -> ConnectionInfo: self._enable_iam_auth, ) logger.debug( - f"['{self._instance_connection_string}']: Connection info " - "refresh operation complete" + f"['{self._conn_name}']: Connection info " "refresh operation complete" ) logger.debug( - f"['{self._instance_connection_string}']: Current certificate " + f"['{self._conn_name}']: Current certificate " f"expiration = {connection_info.expiration.isoformat()}" ) except aiohttp.ClientResponseError as e: logger.debug( - f"['{self._instance_connection_string}']: Connection info " + f"['{self._conn_name}']: Connection info " f"refresh operation failed: {str(e)}" ) if e.status == 403: @@ -154,7 +139,7 @@ async def _perform_refresh(self) -> ConnectionInfo: except Exception as e: logger.debug( - f"['{self._instance_connection_string}']: Connection info " + f"['{self._conn_name}']: Connection info " f"refresh operation failed: {str(e)}" ) raise @@ -188,18 +173,17 @@ async def _refresh_task(self: RefreshAheadCache, delay: int) -> ConnectionInfo: # check that refresh is valid if not await _is_valid(refresh_task): raise RefreshNotValidError( - f"['{self._instance_connection_string}']: Invalid refresh operation. Certficate appears to be expired." + f"['{self._conn_name}']: Invalid refresh operation. Certficate appears to be expired." ) except asyncio.CancelledError: logger.debug( - f"['{self._instance_connection_string}']: Scheduled refresh" - " operation cancelled" + f"['{self._conn_name}']: Scheduled refresh" " operation cancelled" ) raise # bad refresh attempt except Exception as e: logger.exception( - f"['{self._instance_connection_string}']: " + f"['{self._conn_name}']: " "An error occurred while performing refresh. " "Scheduling another refresh attempt immediately", exc_info=e, @@ -216,7 +200,7 @@ async def _refresh_task(self: RefreshAheadCache, delay: int) -> ConnectionInfo: # calculate refresh delay based on certificate expiration delay = _seconds_until_refresh(refresh_data.expiration) logger.debug( - f"['{self._instance_connection_string}']: Connection info refresh" + f"['{self._conn_name}']: Connection info refresh" " operation scheduled for " f"{(datetime.now(timezone.utc) + timedelta(seconds=delay)).isoformat(timespec='seconds')} " f"(now + {timedelta(seconds=delay)})" @@ -240,7 +224,7 @@ async def close(self) -> None: graceful exit. """ logger.debug( - f"['{self._instance_connection_string}']: Canceling connection info " + f"['{self._conn_name}']: Canceling connection info " "refresh operation tasks" ) self._current.cancel() diff --git a/google/cloud/sql/connector/lazy.py b/google/cloud/sql/connector/lazy.py index 9b8cfa24d..672f989e8 100644 --- a/google/cloud/sql/connector/lazy.py +++ b/google/cloud/sql/connector/lazy.py @@ -21,7 +21,7 @@ from google.cloud.sql.connector.client import CloudSQLClient from google.cloud.sql.connector.connection_info import ConnectionInfo -from google.cloud.sql.connector.instance import _parse_instance_connection_name +from google.cloud.sql.connector.connection_name import _parse_instance_connection_name from google.cloud.sql.connector.refresh_utils import _refresh_buffer logger = logging.getLogger(name=__name__) @@ -56,10 +56,13 @@ def __init__( connections. """ # validate and parse instance connection name - self._project, self._region, self._instance = _parse_instance_connection_name( - instance_connection_string + conn_name = _parse_instance_connection_name(instance_connection_string) + self._project, self._region, self._instance = ( + conn_name.project, + conn_name.region, + conn_name.instance_name, ) - self._instance_connection_string = instance_connection_string + self._conn_name = conn_name self._enable_iam_auth = enable_iam_auth self._keys = keys @@ -91,13 +94,12 @@ async def connect_info(self) -> ConnectionInfo: < (self._cached.expiration - timedelta(seconds=_refresh_buffer)) ): logger.debug( - f"['{self._instance_connection_string}']: Connection info " + f"['{self._conn_name}']: Connection info " "is still valid, using cached info" ) return self._cached logger.debug( - f"['{self._instance_connection_string}']: Connection info " - "refresh operation started" + f"['{self._conn_name}']: Connection info " "refresh operation started" ) try: conn_info = await self._client.get_connection_info( @@ -109,16 +111,16 @@ async def connect_info(self) -> ConnectionInfo: ) except Exception as e: logger.debug( - f"['{self._instance_connection_string}']: Connection info " + f"['{self._conn_name}']: Connection info " f"refresh operation failed: {str(e)}" ) raise logger.debug( - f"['{self._instance_connection_string}']: Connection info " + f"['{self._conn_name}']: Connection info " "refresh operation completed successfully" ) logger.debug( - f"['{self._instance_connection_string}']: Current certificate " + f"['{self._conn_name}']: Current certificate " f"expiration = {str(conn_info.expiration)}" ) self._cached = conn_info diff --git a/google/cloud/sql/connector/pg8000.py b/google/cloud/sql/connector/pg8000.py index 623738f85..1f66dde2a 100644 --- a/google/cloud/sql/connector/pg8000.py +++ b/google/cloud/sql/connector/pg8000.py @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. """ + import socket import ssl from typing import Any, TYPE_CHECKING diff --git a/google/cloud/sql/connector/pymysql.py b/google/cloud/sql/connector/pymysql.py index 8971ff9b2..a16584367 100644 --- a/google/cloud/sql/connector/pymysql.py +++ b/google/cloud/sql/connector/pymysql.py @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. """ + import socket import ssl from typing import Any, TYPE_CHECKING diff --git a/google/cloud/sql/connector/pytds.py b/google/cloud/sql/connector/pytds.py index 5c78fd3fc..243d90fd5 100644 --- a/google/cloud/sql/connector/pytds.py +++ b/google/cloud/sql/connector/pytds.py @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. """ + import platform import socket import ssl diff --git a/noxfile.py b/noxfile.py index 528642c1a..8329b2de8 100644 --- a/noxfile.py +++ b/noxfile.py @@ -51,6 +51,7 @@ def lint(session): "--check-only", "--diff", "--profile=google", + "-w=88", *LINT_PATHS, ) session.run("black", "--check", "--diff", *LINT_PATHS) @@ -85,6 +86,7 @@ def format(session): "isort", "--fss", "--profile=google", + "-w=88", *LINT_PATHS, ) session.run( diff --git a/tests/conftest.py b/tests/conftest.py index dd5c3952d..470fe19f4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. """ + import asyncio import os import socket diff --git a/tests/unit/test_connection_name.py b/tests/unit/test_connection_name.py new file mode 100644 index 000000000..1e3730424 --- /dev/null +++ b/tests/unit/test_connection_name.py @@ -0,0 +1,56 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest # noqa F401 Needed to run the tests + +from google.cloud.sql.connector.connection_name import _parse_instance_connection_name +from google.cloud.sql.connector.connection_name import ConnectionName + + +def test_ConnectionName() -> None: + conn_name = ConnectionName("project", "region", "instance") + # test class attributes are set properly + assert conn_name.project == "project" + assert conn_name.region == "region" + assert conn_name.instance_name == "instance" + # test ConnectionName str() method prints instance connection name + assert str(conn_name) == "project:region:instance" + + +@pytest.mark.parametrize( + "connection_name, expected", + [ + ("project:region:instance", ConnectionName("project", "region", "instance")), + ( + "domain-prefix:project:region:instance", + ConnectionName("domain-prefix:project", "region", "instance"), + ), + ], +) +def test_parse_instance_connection_name( + connection_name: str, expected: ConnectionName +) -> None: + """ + Test that _parse_instance_connection_name works correctly on + normal instance connection names and domain-scoped projects. + """ + assert expected == _parse_instance_connection_name(connection_name) + + +def test_parse_instance_connection_name_bad_conn_name() -> None: + """ + Tests that ValueError is thrown for bad instance connection names. + """ + with pytest.raises(ValueError): + _parse_instance_connection_name("project:instance") # missing region diff --git a/tests/unit/test_instance.py b/tests/unit/test_instance.py index 5dcf1f5aa..5b0887aa2 100644 --- a/tests/unit/test_instance.py +++ b/tests/unit/test_instance.py @@ -16,7 +16,6 @@ import asyncio import datetime -from typing import Tuple from aiohttp import ClientResponseError from aiohttp import RequestInfo @@ -31,7 +30,6 @@ from google.cloud.sql.connector.connection_info import ConnectionInfo from google.cloud.sql.connector.exceptions import AutoIAMAuthNotSupported from google.cloud.sql.connector.exceptions import CloudSQLIPTypeError -from google.cloud.sql.connector.instance import _parse_instance_connection_name from google.cloud.sql.connector.instance import RefreshAheadCache from google.cloud.sql.connector.rate_limiter import AsyncRateLimiter from google.cloud.sql.connector.refresh_utils import _is_valid @@ -43,34 +41,6 @@ def test_rate_limiter() -> AsyncRateLimiter: return AsyncRateLimiter(max_capacity=1, rate=1 / 2) -@pytest.mark.parametrize( - "connection_name, expected", - [ - ("project:region:instance", ("project", "region", "instance")), - ( - "domain-prefix:project:region:instance", - ("domain-prefix:project", "region", "instance"), - ), - ], -) -def test_parse_instance_connection_name( - connection_name: str, expected: Tuple[str, str, str] -) -> None: - """ - Test that _parse_instance_connection_name works correctly on - normal instance connection names and domain-scoped projects. - """ - assert expected == _parse_instance_connection_name(connection_name) - - -def test_parse_instance_connection_name_bad_conn_name() -> None: - """ - Tests that ValueError is thrown for bad instance connection names. - """ - with pytest.raises(ValueError): - _parse_instance_connection_name("project:instance") # missing region - - @pytest.mark.asyncio async def test_Instance_init( cache: RefreshAheadCache, diff --git a/tests/unit/test_pg8000.py b/tests/unit/test_pg8000.py index 26a4bfeab..1b2adbb65 100644 --- a/tests/unit/test_pg8000.py +++ b/tests/unit/test_pg8000.py @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. """ + from functools import partial from typing import Any diff --git a/tests/unit/test_pymysql.py b/tests/unit/test_pymysql.py index 3afefb2a4..69d2aba8f 100644 --- a/tests/unit/test_pymysql.py +++ b/tests/unit/test_pymysql.py @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. """ + from functools import partial import ssl from typing import Any diff --git a/tests/unit/test_pytds.py b/tests/unit/test_pytds.py index 5bfa62419..633aab74a 100644 --- a/tests/unit/test_pytds.py +++ b/tests/unit/test_pytds.py @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. """ + from functools import partial import platform from typing import Any diff --git a/tests/unit/test_rate_limiter.py b/tests/unit/test_rate_limiter.py index 587e76809..5e187b81d 100644 --- a/tests/unit/test_rate_limiter.py +++ b/tests/unit/test_rate_limiter.py @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. """ + import asyncio import pytest # noqa F401 Needed to run the tests diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index fe190ceba..6545bc7a8 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. """ + import pytest # noqa F401 Needed to run the tests from google.cloud.sql.connector import utils From b591dde87959a6fcad7349e82d23a8d4f0f8a4c1 Mon Sep 17 00:00:00 2001 From: Jack Wotherspoon Date: Wed, 13 Nov 2024 12:31:00 -0500 Subject: [PATCH 05/12] build: use multiScm for Kokoro release builds (#1190) --- .github/release-trigger.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/release-trigger.yml b/.github/release-trigger.yml index 48b8f75e2..d75912b7f 100644 --- a/.github/release-trigger.yml +++ b/.github/release-trigger.yml @@ -13,3 +13,4 @@ # limitations under the License. enabled: true +multiScmName: cloud-sql-python-connector From 38d8c38c30e96b86085ecc0d4c89d1abc540e715 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 13 Nov 2024 18:35:47 +0100 Subject: [PATCH 06/12] chore(deps): Update dependencies for github (#1181) --- .github/workflows/codeql.yml | 8 ++++---- .github/workflows/coverage.yml | 6 +++--- .github/workflows/labels.yaml | 2 +- .github/workflows/lint.yml | 4 ++-- .github/workflows/scorecard.yml | 4 ++-- .github/workflows/tests.yml | 14 +++++++------- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 753e68624..51c86daf9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -42,20 +42,20 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + uses: github/codeql-action/init@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3 with: languages: ${{ matrix.language }} # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually - name: Autobuild - uses: github/codeql-action/autobuild@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + uses: github/codeql-action/autobuild@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + uses: github/codeql-action/analyze@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 7f06e8ae5..ae2ad3a33 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -24,14 +24,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup Python - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: "3.10" - run: pip install nox coverage - name: Checkout base branch - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.base_ref }} @@ -44,7 +44,7 @@ jobs: coverage erase - name: Checkout PR branch - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} diff --git a/.github/workflows/labels.yaml b/.github/workflows/labels.yaml index 9cf539a2d..7d57e9a42 100644 --- a/.github/workflows/labels.yaml +++ b/.github/workflows/labels.yaml @@ -28,7 +28,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: micnncim/action-label-syncer@3abd5ab72fda571e69fffd97bd4e0033dd5f495c # v1.3.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index aae776675..ba9782639 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup Python - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: "3.12" @@ -33,7 +33,7 @@ jobs: run: pip install nox - name: Checkout code - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Run nox lint session run: nox --sessions lint diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index b9ada1176..15d906797 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -35,7 +35,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false @@ -65,6 +65,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + uses: github/codeql-action/upload-sarif@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3 with: sarif_file: resultsFiltered.sarif diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ca86da1c3..b4af63dd9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,10 +44,10 @@ jobs: pull-requests: write steps: - name: Checkout code - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: ${{ matrix.python-version }} @@ -56,7 +56,7 @@ jobs: - id: auth name: Authenticate to Google Cloud - uses: google-github-actions/auth@8254fb75a33b976a221574d287e93919e6a36f70 # v2.1.6 + uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7 with: workload_identity_provider: ${{ vars.PROVIDER_NAME }} service_account: ${{ vars.SERVICE_ACCOUNT }} @@ -64,7 +64,7 @@ jobs: - id: secrets name: Get secrets - uses: google-github-actions/get-secretmanager-secrets@95a0b09b8348ef3d02c68c6ba5662a037e78d713 # v2.1.4 + uses: google-github-actions/get-secretmanager-secrets@e5bb06c2ca53b244f978d33348d18317a7f263ce # v2.2.2 with: secrets: |- MYSQL_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/MYSQL_CONNECTION_NAME @@ -143,10 +143,10 @@ jobs: pull-requests: write steps: - name: Checkout code - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: ${{ matrix.python-version }} @@ -157,7 +157,7 @@ jobs: name: Authenticate to Google Cloud # only needed for Flakybot on periodic (schedule) and continuous (push) events if: ${{ github.event_name == 'schedule' || github.event_name == 'push' }} - uses: google-github-actions/auth@8254fb75a33b976a221574d287e93919e6a36f70 # v2.1.6 + uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7 with: workload_identity_provider: ${{ vars.PROVIDER_NAME }} service_account: ${{ vars.SERVICE_ACCOUNT }} From b1c2f86dd2eef736a51fcd0dc0619593db1af302 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 19 Nov 2024 18:34:24 +0100 Subject: [PATCH 07/12] chore(deps): update python-nonmajor (#1187) --- requirements-test.txt | 4 ++-- requirements.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index e18c63928..4aeecede7 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -8,6 +8,6 @@ sqlalchemy-stubs==0.4 PyMySQL==1.1.1 pg8000==1.31.2 asyncpg==0.30.0 -python-tds==1.15.0 -aioresponses==0.7.6 +python-tds==1.16.0 +aioresponses==0.7.7 pytest-aiohttp==1.0.5 diff --git a/requirements.txt b/requirements.txt index de63f9163..faf24f3e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ aiofiles==24.1.0 -aiohttp==3.10.10 +aiohttp==3.11.2 cryptography==43.0.3 Requests==2.32.3 -google-auth==2.35.0 +google-auth==2.36.0 From e2bba73ad17857dd3d7eae3b314c504a12a97676 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 19 Nov 2024 18:57:29 +0100 Subject: [PATCH 08/12] chore(deps): update dependency aiohttp to v3.11.5 (#1195) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index faf24f3e2..05efb94d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ aiofiles==24.1.0 -aiohttp==3.11.2 +aiohttp==3.11.5 cryptography==43.0.3 Requests==2.32.3 google-auth==2.36.0 From aaa1ef39eb41555741e055f734df895df1a15461 Mon Sep 17 00:00:00 2001 From: Jack Wotherspoon Date: Tue, 19 Nov 2024 12:58:57 -0500 Subject: [PATCH 09/12] test: round backoff in exponential backoff tests (#1191) --- tests/unit/test_refresh_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_refresh_utils.py b/tests/unit/test_refresh_utils.py index 476c46863..119e92c7a 100644 --- a/tests/unit/test_refresh_utils.py +++ b/tests/unit/test_refresh_utils.py @@ -168,7 +168,7 @@ def test_exponential_backoff(attempt: int, low: int, high: int) -> None: """ Test _exponential_backoff produces times (in ms) in the proper range. """ - backoff = _exponential_backoff(attempt) + backoff = round(_exponential_backoff(attempt)) assert backoff >= low assert backoff <= high From eb4be8275be97a06c61f7a07756206188db8f300 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Tue, 19 Nov 2024 19:05:54 +0100 Subject: [PATCH 10/12] chore(deps): Update github/codeql-action action to v3.27.4 (#1192) --- .github/workflows/codeql.yml | 6 +++--- .github/workflows/scorecard.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 51c86daf9..ba6439848 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,16 +46,16 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3 + uses: github/codeql-action/init@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 with: languages: ${{ matrix.language }} # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually - name: Autobuild - uses: github/codeql-action/autobuild@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3 + uses: github/codeql-action/autobuild@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3 + uses: github/codeql-action/analyze@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 15d906797..37c9428dd 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -65,6 +65,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@396bb3e45325a47dd9ef434068033c6d5bb0d11a # v3.27.3 + uses: github/codeql-action/upload-sarif@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 with: sarif_file: resultsFiltered.sarif From 5396642975ec41105ac39cb4b5750c242f08e949 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 20 Nov 2024 16:18:24 +0100 Subject: [PATCH 11/12] chore(deps): update dependency aiohttp to v3.11.6 (#1196) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 05efb94d4..3085b3e94 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ aiofiles==24.1.0 -aiohttp==3.11.5 +aiohttp==3.11.6 cryptography==43.0.3 Requests==2.32.3 google-auth==2.36.0 From d622575cab34c0dc85763076f7c404e7265c3f26 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:22:49 -0500 Subject: [PATCH 12/12] chore(main): release 1.14.0 (#1184) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ google/cloud/sql/connector/version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b970692a..2e97c4830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.14.0](https://github.com/GoogleCloudPlatform/cloud-sql-python-connector/compare/v1.13.0...v1.14.0) (2024-11-20) + + +### Features + +* support native asyncpg connection pools ([#1182](https://github.com/GoogleCloudPlatform/cloud-sql-python-connector/issues/1182)) ([eb95a9d](https://github.com/GoogleCloudPlatform/cloud-sql-python-connector/commit/eb95a9da35553387408e425721449660cc83196d)) + ## [1.13.0](https://github.com/GoogleCloudPlatform/cloud-sql-python-connector/compare/v1.12.1...v1.13.0) (2024-10-22) diff --git a/google/cloud/sql/connector/version.py b/google/cloud/sql/connector/version.py index 2213c3d98..7f2e3d950 100644 --- a/google/cloud/sql/connector/version.py +++ b/google/cloud/sql/connector/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "1.13.0" +__version__ = "1.14.0"