diff --git a/.github/workflows/cloud_build_failure_reporter.yml b/.github/workflows/cloud_build_failure_reporter.yml index a07e3a676..493ddecd2 100644 --- a/.github/workflows/cloud_build_failure_reporter.yml +++ b/.github/workflows/cloud_build_failure_reporter.yml @@ -38,7 +38,7 @@ jobs: runs-on: 'ubuntu-latest' steps: - - uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' # v7 + - uses: 'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd' # v8 with: script: |- // parse test names diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2b01fa803..301fb0204 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@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9 + uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7 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@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9 + uses: github/codeql-action/autobuild@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9 + uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ecb188869..df109d873 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: "3.13" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 481cd39ef..f82d503a0 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@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: "3.13" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 62dd7a387..3e8c8687f 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@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9 + uses: github/codeql-action/upload-sarif@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7 with: sarif_file: resultsFiltered.sarif diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 63dc9a2be..b600742bf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -47,7 +47,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: ${{ matrix.python-version }} @@ -56,7 +56,7 @@ jobs: - id: auth name: Authenticate to Google Cloud - uses: google-github-actions/auth@b7593ed2efd1c1617e1b0254da33b86225adb2a5 # v2.1.12 + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 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@50ec04d56ddf2740b0bde82926cc742f90e06d2b # v2.2.4 + uses: google-github-actions/get-secretmanager-secrets@bc9c54b29fdffb8a47776820a7d26e77b379d262 # v3.0.0 with: secrets: |- MYSQL_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/MYSQL_CONNECTION_NAME @@ -156,7 +156,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: ${{ matrix.python-version }} @@ -167,7 +167,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@b7593ed2efd1c1617e1b0254da33b86225adb2a5 # v2.1.12 + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 with: workload_identity_provider: ${{ vars.PROVIDER_NAME }} service_account: ${{ vars.SERVICE_ACCOUNT }} diff --git a/.gitignore b/.gitignore index 9f449ce4a..06827b584 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ dist/ .idea .coverage sponge_log.xml +.envrc +*.iml diff --git a/CHANGELOG.md b/CHANGELOG.md index 677c7d5a2..1347a4f8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.18.5](https://github.com/GoogleCloudPlatform/cloud-sql-python-connector/compare/v1.18.4...v1.18.5) (2025-10-09) + + +### Bug Fixes + +* error on connect_async() event loop mismatch ([#1113](https://github.com/GoogleCloudPlatform/cloud-sql-python-connector/issues/1113)) ([3030b82](https://github.com/GoogleCloudPlatform/cloud-sql-python-connector/commit/3030b824c899c56861832c0254df3c87585d3fcb)) + ## [1.18.4](https://github.com/GoogleCloudPlatform/cloud-sql-python-connector/compare/v1.18.3...v1.18.4) (2025-08-12) diff --git a/README.md b/README.md index 1c5489e04..9bea3d6d6 100644 --- a/README.md +++ b/README.md @@ -566,7 +566,7 @@ 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() + connector = await create_async_connector() # creation function to generate asyncpg connections as the 'connect' arg async def getconn(instance_connection_name, **kwargs) -> asyncpg.Connection: diff --git a/build.sh b/build.sh new file mode 100755 index 000000000..fc85edff9 --- /dev/null +++ b/build.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash + +# Copyright 2025 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. + +# Set SCRIPT_DIR to the current directory of this file. +SCRIPT_DIR=$(cd -P "$(dirname "$0")" >/dev/null 2>&1 && pwd) +SCRIPT_FILE="${SCRIPT_DIR}/$(basename "$0")" + +## +## Local Development +## +## These functions should be used to run the local development process +## + +if [[ ! -d venv ]] ; then + echo "./venv not found. Setting up venv" + python3 -m venv "$PWD/venv" +fi +source "$PWD/venv/bin/activate" + +if which pip3 ; then + PIP_CMD=pip3 +elif which pip ; then + PIP_CMD=pip +else + echo "pip not found. Please add pip to your path." + exit 1 +fi +if ! which nox ; then + $PIP_CMD install nox +fi + + + +## clean - Cleans the build output +function clean() { + if [[ -d '.tools' ]] ; then + rm -rf .tools + fi +} + +## build - Builds the project without running tests. +function build() { + nox --sessions build +} + +## test - Runs local unit tests. +function test() { + nox --sessions unit --python=3.13 +} + +## e2e - Runs end-to-end integration tests. +function e2e() { + if [[ ! -f .envrc ]] ; then + write_e2e_env .envrc + fi + source .envrc + nox --sessions system --python=3.13 +} + +## fix - Fixes code format. +function fix() { + nox --sessions format +} + +## lint - runs the linters +function lint() { + # Check the commit includes a go.mod that is fully + # up to date. + nox --sessions lint +} + +## deps - updates project dependencies to latest +function deps() { + echo "Todo: deps" + exit 1 +} + +# write_e2e_env - Loads secrets from the gcloud project and writes +# them to target/e2e.env to run e2e tests. +function write_e2e_env(){ + # All secrets used by the e2e tests in the form = + secret_vars=( + MYSQL_CONNECTION_NAME=MYSQL_CONNECTION_NAME + MYSQL_USER=MYSQL_USER + MYSQL_USER_IAM=MYSQL_USER_IAM_GO + MYSQL_PASS=MYSQL_PASS + MYSQL_DB=MYSQL_DB + MYSQL_MCP_CONNECTION_NAME=MYSQL_MCP_CONNECTION_NAME + MYSQL_MCP_PASS=MYSQL_MCP_PASS + POSTGRES_CONNECTION_NAME=POSTGRES_CONNECTION_NAME + POSTGRES_USER=POSTGRES_USER + POSTGRES_USER_IAM=POSTGRES_USER_IAM_GO + POSTGRES_PASS=POSTGRES_PASS + POSTGRES_DB=POSTGRES_DB + POSTGRES_CAS_CONNECTION_NAME=POSTGRES_CAS_CONNECTION_NAME + POSTGRES_CAS_PASS=POSTGRES_CAS_PASS + POSTGRES_CUSTOMER_CAS_CONNECTION_NAME=POSTGRES_CUSTOMER_CAS_CONNECTION_NAME + POSTGRES_CUSTOMER_CAS_PASS=POSTGRES_CUSTOMER_CAS_PASS + POSTGRES_CUSTOMER_CAS_DOMAIN_NAME=POSTGRES_CUSTOMER_CAS_DOMAIN_NAME + POSTGRES_CUSTOMER_CAS_INVALID_DOMAIN_NAME=POSTGRES_CUSTOMER_CAS_INVALID_DOMAIN_NAME + POSTGRES_MCP_CONNECTION_NAME=POSTGRES_MCP_CONNECTION_NAME + POSTGRES_MCP_PASS=POSTGRES_MCP_PASS + SQLSERVER_CONNECTION_NAME=SQLSERVER_CONNECTION_NAME + SQLSERVER_USER=SQLSERVER_USER + SQLSERVER_PASS=SQLSERVER_PASS + SQLSERVER_DB=SQLSERVER_DB + QUOTA_PROJECT=QUOTA_PROJECT + ) + + if [[ -z "$TEST_PROJECT" ]] ; then + echo "Set TEST_PROJECT environment variable to the project containing" + echo "the e2e test suite secrets." + exit 1 + fi + + echo "Getting test secrets from $TEST_PROJECT into $1" + { + for env_name in "${secret_vars[@]}" ; do + env_var_name="${env_name%%=*}" + secret_name="${env_name##*=}" + set -x + val=$(gcloud secrets versions access latest --project "$TEST_PROJECT" --secret="$secret_name") + echo "export $env_var_name='$val'" + done + } > "$1" + +} + +## help - prints the help details +## +function help() { + # This will print the comments beginning with ## above each function + # in this file. + + echo "build.sh " + echo + echo "Commands to assist with local development and CI builds." + echo + echo "Commands:" + echo + grep -e '^##' "$SCRIPT_FILE" | sed -e 's/##/ /' +} + +set -euo pipefail + +# Check CLI Arguments +if [[ "$#" -lt 1 ]] ; then + help + exit 1 +fi + +cd "$SCRIPT_DIR" + +"$@" + diff --git a/google/cloud/sql/connector/client.py b/google/cloud/sql/connector/client.py index 2e1fbc20d..11508ce17 100644 --- a/google/cloud/sql/connector/client.py +++ b/google/cloud/sql/connector/client.py @@ -22,9 +22,9 @@ import aiohttp from cryptography.hazmat.backends import default_backend from cryptography.x509 import load_pem_x509_certificate - from google.auth.credentials import TokenState from google.auth.transport import requests + from google.cloud.sql.connector.connection_info import ConnectionInfo from google.cloud.sql.connector.connection_name import ConnectionName from google.cloud.sql.connector.exceptions import AutoIAMAuthNotSupported diff --git a/google/cloud/sql/connector/connector.py b/google/cloud/sql/connector/connector.py index 05eaa51df..0229b7283 100755 --- a/google/cloud/sql/connector/connector.py +++ b/google/cloud/sql/connector/connector.py @@ -28,11 +28,13 @@ import google.auth from google.auth.credentials import Credentials from google.auth.credentials import with_scopes_if_required + import google.cloud.sql.connector.asyncpg as asyncpg from google.cloud.sql.connector.client import CloudSQLClient from google.cloud.sql.connector.enums import DriverMapping from google.cloud.sql.connector.enums import IPTypes from google.cloud.sql.connector.enums import RefreshStrategy +from google.cloud.sql.connector.exceptions import ConnectorLoopError from google.cloud.sql.connector.instance import RefreshAheadCache from google.cloud.sql.connector.lazy import LazyRefreshCache from google.cloud.sql.connector.monitored_cache import MonitoredCache @@ -280,6 +282,15 @@ async def connect_async( KeyError: Unsupported database driver Must be one of pymysql, asyncpg, pg8000, and pytds. """ + # check if event loop is running in current thread + if self._loop != asyncio.get_running_loop(): + raise ConnectorLoopError( + "Running event loop does not match 'connector._loop'. " + "Connector.connect_async() must be called from the event loop " + "the Connector was initialized with. If you need to connect " + "across event loops, please use a new Connector object." + ) + if self._keys is None: self._keys = asyncio.create_task(generate_keys()) if self._client is None: diff --git a/google/cloud/sql/connector/refresh_utils.py b/google/cloud/sql/connector/refresh_utils.py index 0ef7fcbf7..898f0f7a9 100644 --- a/google/cloud/sql/connector/refresh_utils.py +++ b/google/cloud/sql/connector/refresh_utils.py @@ -24,7 +24,6 @@ from typing import Any, Callable import aiohttp - from google.auth.credentials import Credentials from google.auth.credentials import Scoped import google.auth.transport.requests diff --git a/google/cloud/sql/connector/version.py b/google/cloud/sql/connector/version.py index 776c409a2..c42d7c855 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.18.4" +__version__ = "1.18.5" diff --git a/noxfile.py b/noxfile.py index f04756241..b0220f474 100644 --- a/noxfile.py +++ b/noxfile.py @@ -69,6 +69,15 @@ def format(session): *LINT_PATHS, ) +@nox.session() +def build(session): + """ + Just run the default tools to install requirements. + """ + # Install all test dependencies, then install this package in-place. + session.install("-r", "requirements-test.txt") + session.install("-e", ".") + session.install("-r", "requirements.txt") def default(session, path): # Install all test dependencies, then install this package in-place. diff --git a/requirements-test.txt b/requirements-test.txt index 8f690bfc5..9cba5a851 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,6 +1,6 @@ pytest==8.4.1 mock==5.2.0 -pytest-cov==6.2.1 +pytest-cov==7.0.0 pytest-asyncio==1.1.0 SQLAlchemy[asyncio]==2.0.43 sqlalchemy-pytds==1.0.2 diff --git a/requirements.txt b/requirements.txt index 5892a9341..766d62dc5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ aiofiles==24.1.0 aiohttp==3.12.15 -cryptography==45.0.6 +cryptography==46.0.2 dnspython==2.7.0 Requests==2.32.4 google-auth==2.40.3 diff --git a/tests/unit/mocks.py b/tests/unit/mocks.py index 28c2fc307..66bf64a32 100644 --- a/tests/unit/mocks.py +++ b/tests/unit/mocks.py @@ -31,10 +31,10 @@ from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.x509.oid import NameOID - from google.auth import _helpers from google.auth.credentials import Credentials from google.auth.credentials import TokenState + from google.cloud.sql.connector.connector import _DEFAULT_UNIVERSE_DOMAIN from google.cloud.sql.connector.utils import generate_keys from google.cloud.sql.connector.utils import write_to_file diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 2ecefe220..cfe509470 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -17,10 +17,10 @@ from aiohttp import ClientResponseError from aioresponses import aioresponses +from google.auth.credentials import Credentials from mocks import FakeCredentials import pytest -from google.auth.credentials import Credentials from google.cloud.sql.connector.client import CloudSQLClient from google.cloud.sql.connector.utils import generate_keys from google.cloud.sql.connector.version import __version__ as version diff --git a/tests/unit/test_connector.py b/tests/unit/test_connector.py index 157697723..1bcb42616 100644 --- a/tests/unit/test_connector.py +++ b/tests/unit/test_connector.py @@ -16,19 +16,21 @@ import asyncio import os +from threading import Thread from typing import Union from aiohttp import ClientResponseError +from google.auth.credentials import Credentials from mock import patch import pytest # noqa F401 Needed to run the tests -from google.auth.credentials import Credentials from google.cloud.sql.connector import Connector from google.cloud.sql.connector import create_async_connector from google.cloud.sql.connector import IPTypes from google.cloud.sql.connector.client import CloudSQLClient from google.cloud.sql.connector.connection_name import ConnectionName from google.cloud.sql.connector.exceptions import CloudSQLIPTypeError +from google.cloud.sql.connector.exceptions import ConnectorLoopError from google.cloud.sql.connector.exceptions import IncompatibleDriverError from google.cloud.sql.connector.instance import RefreshAheadCache @@ -280,6 +282,38 @@ async def test_Connector_connect_async( assert connection is True +@pytest.mark.asyncio +async def test_Connector_connect_async_multiple_event_loops( + fake_credentials: Credentials, fake_client: CloudSQLClient +) -> None: + """Test that Connector.connect_async errors when run on wrong event loop.""" + + new_loop = asyncio.new_event_loop() + thread = Thread(target=new_loop.run_forever, daemon=True) + thread.start() + + async with Connector( + credentials=fake_credentials, loop=asyncio.get_running_loop() + ) as connector: + connector._client = fake_client + with pytest.raises(ConnectorLoopError) as exc_info: + future = asyncio.run_coroutine_threadsafe( + connector.connect_async( + "test-project:test-region:test-instance", "asyncpg" + ), + loop=new_loop, + ) + future.result() + assert ( + exc_info.value.args[0] == "Running event loop does not match " + "'connector._loop'. Connector.connect_async() must be called from " + "the event loop the Connector was initialized with. If you need to " + "connect across event loops, please use a new Connector object." + ) + new_loop.call_soon_threadsafe(new_loop.stop) + thread.join() + + @pytest.mark.asyncio async def test_create_async_connector(fake_credentials: Credentials) -> None: """Test that create_async_connector properly initializes connector diff --git a/tests/unit/test_refresh_utils.py b/tests/unit/test_refresh_utils.py index 2d4cdb853..119e92c7a 100644 --- a/tests/unit/test_refresh_utils.py +++ b/tests/unit/test_refresh_utils.py @@ -20,19 +20,19 @@ import datetime from conftest import SCOPES # type: ignore +import google.auth +from google.auth.credentials import Credentials +from google.auth.credentials import TokenState +import google.oauth2.credentials from mock import Mock from mock import patch import pytest # noqa F401 Needed to run the tests -import google.auth -from google.auth.credentials import Credentials -from google.auth.credentials import TokenState from google.cloud.sql.connector.refresh_utils import _downscope_credentials from google.cloud.sql.connector.refresh_utils import _exponential_backoff from google.cloud.sql.connector.refresh_utils import _is_valid from google.cloud.sql.connector.refresh_utils import _seconds_until_refresh from google.cloud.sql.connector.refresh_utils import retry_50x -import google.oauth2.credentials @pytest.fixture