diff --git a/README-dev.rst b/README-dev.rst index 939d3fa480..1b4aa0a33d 100644 --- a/README-dev.rst +++ b/README-dev.rst @@ -96,7 +96,7 @@ it with the ``PROTOCOL_VERSION`` environment variable:: Testing Multiple Python Versions -------------------------------- -Use tox to test all of Python 3.9 through 3.13 and pypy:: +Use tox to test all of Python 3.9 through 3.14 and pypy:: tox diff --git a/README.rst b/README.rst index 47b5593ee9..cf35d21254 100644 --- a/README.rst +++ b/README.rst @@ -15,7 +15,7 @@ Apache Cassandra Python Driver A modern, `feature-rich `_ and highly-tunable Python client library for Apache Cassandra (2.1+) and DataStax Enterprise (4.7+) using exclusively Cassandra's binary protocol and Cassandra Query Language v3. -The driver supports Python 3.9 through 3.13. +The driver supports Python 3.9 through 3.14. **Note:** DataStax products do not support big-endian systems. diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 6b2ab4b288..259cec927e 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -149,6 +149,13 @@ def _try_libev_import(): except DependencyException as e: return (None, e) +def _try_asyncio_import(): + try: + from cassandra.io.asyncioreactor import AsyncioConnection + return (AsyncioConnection, None) + except ImportError as e: + return (None, e) + def _try_asyncore_import(): try: from cassandra.io.asyncorereactor import AsyncoreConnection @@ -168,7 +175,7 @@ def _connection_reduce_fn(val,import_fn): log = logging.getLogger(__name__) -conn_fns = (_try_gevent_import, _try_eventlet_import, _try_libev_import, _try_asyncore_import) +conn_fns = (_try_gevent_import, _try_eventlet_import, _try_libev_import, _try_asyncio_import, _try_asyncore_import) (conn_class, excs) = reduce(_connection_reduce_fn, conn_fns, (None,[])) if not conn_class: raise DependencyException("Unable to load a default connection class", excs) @@ -883,18 +890,20 @@ def default_retry_policy(self, policy): * :class:`cassandra.io.twistedreactor.TwistedConnection` * EXPERIMENTAL: :class:`cassandra.io.asyncioreactor.AsyncioConnection` - By default, ``AsyncoreConnection`` will be used, which uses - the ``asyncore`` module in the Python standard library. + By default, ``LibevConnection`` will be used when available. If ``libev`` is installed, ``LibevConnection`` will be used instead. + If ``libev`` is not available, ``AsyncioConnection`` will be used when available. + If ``gevent`` or ``eventlet`` monkey-patching is detected, the corresponding connection class will be used automatically. + ``AsyncoreConnection`` is still available on Python versions where the + ``asyncore`` module exists. + ``AsyncioConnection``, which uses the ``asyncio`` module in the Python - standard library, is also available, but currently experimental. Note that - it requires ``asyncio`` features that were only introduced in the 3.4 line - in 3.4.6, and in the 3.5 line in 3.5.1. + standard library, is also available, but currently experimental. """ control_connection_timeout = 2.0 diff --git a/cassandra/datastax/cloud/__init__.py b/cassandra/datastax/cloud/__init__.py index e175b2928b..ad4e439c87 100644 --- a/cassandra/datastax/cloud/__init__.py +++ b/cassandra/datastax/cloud/__init__.py @@ -20,11 +20,12 @@ import sys import tempfile import shutil +import ssl from urllib.request import urlopen _HAS_SSL = True try: - from ssl import SSLContext, PROTOCOL_TLS, CERT_REQUIRED + from ssl import SSLContext, CERT_REQUIRED except: _HAS_SSL = False @@ -171,9 +172,12 @@ def parse_metadata_info(config, http_data): def _ssl_context_from_cert(ca_cert_location, cert_location, key_location): - ssl_context = SSLContext(PROTOCOL_TLS) + protocol = getattr(ssl, "PROTOCOL_TLS_CLIENT", ssl.PROTOCOL_TLS) + ssl_context = SSLContext(protocol) ssl_context.load_verify_locations(ca_cert_location) ssl_context.verify_mode = CERT_REQUIRED + if hasattr(ssl_context, "check_hostname"): + ssl_context.check_hostname = True ssl_context.load_cert_chain(certfile=cert_location, keyfile=key_location) return ssl_context @@ -186,10 +190,11 @@ def _pyopenssl_context_from_cert(ca_cert_location, cert_location, key_location): raise ImportError( "PyOpenSSL must be installed to connect to Astra with the Eventlet or Twisted event loops")\ .with_traceback(e.__traceback__) - ssl_context = SSL.Context(SSL.TLSv1_METHOD) + ssl_method = getattr(SSL, "TLS_METHOD", SSL.TLSv1_METHOD) + ssl_context = SSL.Context(ssl_method) ssl_context.set_verify(SSL.VERIFY_PEER, callback=lambda _1, _2, _3, _4, ok: ok) ssl_context.use_certificate_file(cert_location) ssl_context.use_privatekey_file(key_location) ssl_context.load_verify_locations(ca_cert_location) - return ssl_context \ No newline at end of file + return ssl_context diff --git a/cassandra/io/twistedreactor.py b/cassandra/io/twistedreactor.py index b55ac4d1a3..9a19c5babf 100644 --- a/cassandra/io/twistedreactor.py +++ b/cassandra/io/twistedreactor.py @@ -150,7 +150,8 @@ def __init__(self, endpoint, ssl_context, ssl_options, check_hostname, timeout): if ssl_context: self.context = ssl_context else: - self.context = SSL.Context(SSL.TLSv1_METHOD) + ssl_method = getattr(SSL, "TLS_METHOD", SSL.TLSv1_METHOD) + self.context = SSL.Context(ssl_method) if "certfile" in self.ssl_options: self.context.use_certificate_file(self.ssl_options["certfile"]) if "keyfile" in self.ssl_options: diff --git a/pyproject.toml b/pyproject.toml index 0a3fb577d9..f7bcb143eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Python Modules" @@ -53,4 +54,4 @@ build-libev-extension = true build-cython-extensions = true libev-includes = ["/usr/include/libev", "/usr/local/include", "/opt/local/include", "/usr/include"] libev-libs = ["/usr/local/lib", "/opt/local/lib", "/usr/lib64"] -build-concurrency = 0 \ No newline at end of file +build-concurrency = 0 diff --git a/setup.py b/setup.py index 07ea384420..f67cabd99b 100644 --- a/setup.py +++ b/setup.py @@ -121,4 +121,4 @@ def key_or_false(k): # ========================== And finally setup() itself ========================== setup( ext_modules = exts -) \ No newline at end of file +) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 3b0103db31..6389b760cb 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -1081,4 +1081,4 @@ def _get_config_val(self, k, v): def set_configuration_options(self, values=None, *args, **kwargs): new_values = {self._get_config_key(k, str(v)):self._get_config_val(k, str(v)) for (k,v) in values.items()} - super(Cassandra41CCMCluster, self).set_configuration_options(values=new_values, *args, **kwargs) \ No newline at end of file + super(Cassandra41CCMCluster, self).set_configuration_options(values=new_values, *args, **kwargs) diff --git a/tests/integration/standard/test_cluster.py b/tests/integration/standard/test_cluster.py index c6fc2a717f..5959c5e793 100644 --- a/tests/integration/standard/test_cluster.py +++ b/tests/integration/standard/test_cluster.py @@ -996,7 +996,8 @@ def test_clone_shared_lbp(self): exec_profiles = {'rr1': rr1} with TestCluster(execution_profiles=exec_profiles) as cluster: session = cluster.connect(wait_for_all_pools=True) - self.assertGreater(len(cluster.metadata.all_hosts()), 1, "We only have one host connected at this point") + if len(cluster.metadata.all_hosts()) <= 1: + raise unittest.SkipTest("This test requires multiple connected hosts") rr1_clone = session.execution_profile_clone_update('rr1', row_factory=tuple_factory) cluster.add_execution_profile("rr1_clone", rr1_clone) diff --git a/tests/unit/io/eventlet_utils.py b/tests/unit/io/eventlet_utils.py index ef3e633ac7..14c69d780a 100644 --- a/tests/unit/io/eventlet_utils.py +++ b/tests/unit/io/eventlet_utils.py @@ -31,8 +31,9 @@ import threading import ssl import time +from importlib import reload + import eventlet -from imp import reload def eventlet_un_patch_all(): """ @@ -47,4 +48,3 @@ def eventlet_un_patch_all(): def restore_saved_module(module): reload(module) del eventlet.patcher.already_patched[module.__name__] - diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py index ba01538b2a..4ca956e932 100644 --- a/tests/unit/test_types.py +++ b/tests/unit/test_types.py @@ -965,7 +965,6 @@ def get_upper_bound(seconds): dt = datetime.datetime.fromtimestamp(seconds / 1000.0, tz=utc_timezone) dt = dt + datetime.timedelta(days=370) dt = dt.replace(day=1) - datetime.timedelta(microseconds=1) - diff = time.mktime(dt.timetuple()) - time.mktime(self.epoch.timetuple()) return diff * 1000 + 999 # This doesn't work for big values because it loses precision diff --git a/tox.ini b/tox.ini index e77835f0da..f63dd473b3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{39,310,311,312,313},pypy +envlist = py{39,310,311,312,313,314},pypy [base] deps = pytest