Repository: GoogleCloudPlatform/cloud-sql-python-connector (python)

Issue:
Update datetimes to aware and remove naive datetimes
```
refresh_utils.py:214: CryptographyDeprecationWarning: Properties that return a naïve datetime object have been deprecated. Please switch to not_valid_after_utc.
```

Pull request:
chore: update datetime objects to be datetime aware
Overall, datetime aware datetimes `datetime.now(timezone)` are recommended over datetime naive datetimes `datetime.utcnow()` etc and Python is beginning to slowly deprecate datetime naive as it leads to ambiguity overall. Switching over our repo to datetime aware.

Context: https://blog.miguelgrinberg.com/post/it-s-time-for-a-change-datetime-utcnow-is-now-deprecated

As well in cryptography library (v42.0.0) there is a new `not_valid_after_utc` datetime attribute that is datetime aware and is recommended over `not_valid_after` which is naive. A deprecation warning will be removed with this update.

Need to update `google.auth` credentials expiry to be timezone aware as [auth library strips the timezone info](https://github.com/googleapis/google-auth-library-python/blob/main/google/auth/_helpers.py#L93-L99) in order to be backwards compatible with python 2. 

Fixes #982 

Patch:
diff --git a/google/cloud/sql/connector/refresh_utils.py b/google/cloud/sql/connector/refresh_utils.py
index c46600b..18ce222 100644
--- a/google/cloud/sql/connector/refresh_utils.py
+++ b/google/cloud/sql/connector/refresh_utils.py
@@ -190,11 +190,15 @@ async def _get_ephemeral(
 
     # decode cert to read expiration
     x509 = load_pem_x509_certificate(ephemeral_cert.encode("UTF-8"), default_backend())
-    expiration = x509.not_valid_after
+    expiration = x509.not_valid_after_utc
     # for IAM authentication OAuth2 token is embedded in cert so it
     # must still be valid for successful connection
     if enable_iam_auth:
         token_expiration: datetime.datetime = login_creds.expiry
+        # google.auth library strips timezone info for backwards compatibality
+        # reasons with Python 2. Add it back to allow timezone aware datetimes.
+        # Ref: https://github.com/googleapis/google-auth-library-python/blob/49a5ff7411a2ae4d32a7d11700f9f961c55406a9/google/auth/_helpers.py#L93-L99
+        token_expiration = token_expiration.replace(tzinfo=datetime.timezone.utc)
         if expiration > token_expiration:
             expiration = token_expiration
     return ephemeral_cert, expiration
@@ -213,7 +217,9 @@ def _seconds_until_refresh(
     :returns: Time in seconds to wait before performing next refresh.
     """
 
-    duration = int((expiration - datetime.datetime.utcnow()).total_seconds())
+    duration = int(
+        (expiration - datetime.datetime.now(datetime.timezone.utc)).total_seconds()
+    )
 
     # if certificate duration is less than 1 hour
     if duration < 3600:
@@ -230,7 +236,7 @@ async def _is_valid(task: asyncio.Task) -> bool:
     try:
         metadata = await task
         # only valid if now is before the cert expires
-        if datetime.datetime.utcnow() < metadata.expiration:
+        if datetime.datetime.now(datetime.timezone.utc) < metadata.expiration:
             return True
     except Exception:
         # supress any errors from task
diff --git a/setup.py b/setup.py
index 41cb56f..fb6407f 100644
--- a/setup.py
+++ b/setup.py
@@ -26,7 +26,7 @@ description = (
 release_status = "Development Status :: 5 - Production/Stable"
 dependencies = [
     "aiohttp",
-    "cryptography>=38.0.3",
+    "cryptography>=42.0.0",
     "Requests",
     "google-auth",
 ]


Test patch:
diff --git a/tests/unit/mocks.py b/tests/unit/mocks.py
index d56e63f..57d4642 100644
--- a/tests/unit/mocks.py
+++ b/tests/unit/mocks.py
@@ -50,7 +50,9 @@ class FakeCredentials:
     def refresh(self, request: Callable) -> None:
         """Refreshes the access token."""
         self.token = "12345"
-        self.expiry = datetime.datetime.utcnow() + datetime.timedelta(minutes=60)
+        self.expiry = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(
+            minutes=60
+        )
 
     @property
     def expired(self) -> bool:
@@ -62,7 +64,9 @@ class FakeCredentials:
         """
         if self.expiry is None:
             return False
-        return False if self.expiry > datetime.datetime.utcnow() else True
+        if self.expiry > datetime.datetime.now(datetime.timezone.utc):
+            return False
+        return True
 
     @property
     def valid(self) -> bool:
@@ -108,11 +112,15 @@ class MockMetadata(ConnectionInfo):
 
 
 async def instance_metadata_success(*args: Any, **kwargs: Any) -> MockMetadata:
-    return MockMetadata(datetime.datetime.utcnow() + datetime.timedelta(minutes=10))
+    return MockMetadata(
+        datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=10)
+    )
 
 
 async def instance_metadata_expired(*args: Any, **kwargs: Any) -> MockMetadata:
-    return MockMetadata(datetime.datetime.utcnow() - datetime.timedelta(minutes=10))
+    return MockMetadata(
+        datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(minutes=10)
+    )
 
 
 async def instance_metadata_error(*args: Any, **kwargs: Any) -> None:
@@ -145,10 +153,10 @@ def generate_cert(
         .issuer_name(issuer)
         .public_key(key.public_key())
         .serial_number(x509.random_serial_number())
-        .not_valid_before(datetime.datetime.utcnow())
+        .not_valid_before(datetime.datetime.now(datetime.timezone.utc))
         .not_valid_after(
             # cert valid for 10 mins
-            datetime.datetime.utcnow()
+            datetime.datetime.now(datetime.timezone.utc)
             + datetime.timedelta(minutes=60)
         )
     )
@@ -189,7 +197,7 @@ def client_key_signed_cert(
         .issuer_name(issuer)
         .public_key(client_key)
         .serial_number(x509.random_serial_number())
-        .not_valid_before(datetime.datetime.utcnow())
+        .not_valid_before(datetime.datetime.now(datetime.timezone.utc))
         .not_valid_after(cert._not_valid_after)  # type: ignore
     )
     return (
@@ -258,7 +266,8 @@ class FakeCSQLInstance:
                     "cert": server_ca_cert,
                     "instance": self.name,
                     "expirationTime": str(
-                        datetime.datetime.utcnow() + datetime.timedelta(minutes=10)
+                        datetime.datetime.now(datetime.timezone.utc)
+                        + datetime.timedelta(minutes=10)
                     ),
                 },
                 "dnsName": "abcde.12345.us-central1.sql.goog",
@@ -284,7 +293,8 @@ class FakeCSQLInstance:
                     "kind": "sql#sslCert",
                     "cert": ephemeral_cert,
                     "expirationTime": str(
-                        datetime.datetime.utcnow() + datetime.timedelta(minutes=10)
+                        datetime.datetime.now(datetime.timezone.utc)
+                        + datetime.timedelta(minutes=10)
                     ),
                 }
             }
diff --git a/tests/unit/test_instance.py b/tests/unit/test_instance.py
index b8948aa..8731554 100644
--- a/tests/unit/test_instance.py
+++ b/tests/unit/test_instance.py
@@ -257,7 +257,9 @@ async def test_perform_refresh(
     assert isinstance(instance_metadata, ConnectionInfo)
     # verify instance metadata expiration
     assert (
-        mock_instance.cert._not_valid_after.replace(microsecond=0)  # type: ignore
+        mock_instance.cert._not_valid_after.replace(
+            tzinfo=datetime.timezone.utc, microsecond=0  # type: ignore
+        )
         == instance_metadata.expiration
     )
 
@@ -273,7 +275,9 @@ async def test_perform_refresh_expiration(
     credentials expiration should be used.
     """
     # set credentials expiration to 1 minute from now
-    expiration = datetime.datetime.utcnow() + datetime.timedelta(minutes=1)
+    expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(
+        minutes=1
+    )
     credentials = mocks.FakeCredentials(token="my-token", expiry=expiration)
     setattr(instance, "_enable_iam_auth", True)
     # set downscoped credential to mock
diff --git a/tests/unit/test_refresh_utils.py b/tests/unit/test_refresh_utils.py
index 18d38d7..d798fa2 100644
--- a/tests/unit/test_refresh_utils.py
+++ b/tests/unit/test_refresh_utils.py
@@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 """
 import asyncio
-from datetime import datetime
-from datetime import timedelta
+import datetime
 from typing import Any, no_type_check
 
 import aiohttp
@@ -226,7 +225,11 @@ def test_seconds_until_refresh_over_1_hour() -> None:
     # using pytest.approx since sometimes can be off by a second
     assert (
         pytest.approx(
-            _seconds_until_refresh(datetime.utcnow() + timedelta(minutes=62)), 1
+            _seconds_until_refresh(
+                datetime.datetime.now(datetime.timezone.utc)
+                + datetime.timedelta(minutes=62)
+            ),
+            1,
         )
         == 31 * 60
     )
@@ -242,7 +245,11 @@ def test_seconds_until_refresh_under_1_hour_over_4_mins() -> None:
     # using pytest.approx since sometimes can be off by a second
     assert (
         pytest.approx(
-            _seconds_until_refresh(datetime.utcnow() + timedelta(minutes=5)), 1
+            _seconds_until_refresh(
+                datetime.datetime.now(datetime.timezone.utc)
+                + datetime.timedelta(minutes=5)
+            ),
+            1,
         )
         == 60
     )
@@ -254,4 +261,9 @@ def test_seconds_until_refresh_under_4_mins() -> None:
 
     If expiration is under 4 minutes, should return 0.
     """
-    assert _seconds_until_refresh(datetime.utcnow() + timedelta(minutes=3)) == 0
+    assert (
+        _seconds_until_refresh(
+            datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=3)
+        )
+        == 0
+    )