diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b9e89cf8..11df68a7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -24,9 +24,9 @@ jobs: uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.12' cache: 'pip' cache-dependency-path: 'setup.py' diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 9941d180..1d1dbbfc 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: os: ['ubuntu-latest'] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] cratedb-version: ['nightly'] sqla-version: ['latest'] pip-allow-prerelease: ['false'] @@ -24,7 +24,7 @@ jobs: # Another CI test matrix slot to test against prerelease versions of Python packages. include: - os: 'ubuntu-latest' - python-version: '3.11' + python-version: '3.12' cratedb-version: 'nightly' sqla-version: 'latest' pip-allow-prerelease: 'true' @@ -39,7 +39,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4d35c3c2..8a62e7df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,9 +12,9 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.11' cache: 'pip' cache-dependency-path: 'setup.py' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 58f086d1..51c3d71f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,12 +19,12 @@ jobs: strategy: matrix: os: ['ubuntu-latest', 'macos-latest'] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] - cratedb-version: ['5.2.2'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + cratedb-version: ['5.4.5'] sqla-version: ['<1.4', '<1.5', '<2.1'] pip-allow-prerelease: ['false'] - # To save resources, only use the most recent Python version on macOS. + # To save resources, only use the most recent Python versions on macOS. exclude: - os: 'macos-latest' python-version: '3.7' @@ -38,8 +38,8 @@ jobs: # Another CI test matrix slot to test against prerelease versions of Python packages. include: - os: 'ubuntu-latest' - python-version: '3.11' - cratedb-version: '5.2.2' + python-version: '3.12' + cratedb-version: '5.4.5' sqla-version: 'latest' pip-allow-prerelease: 'true' @@ -54,7 +54,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' diff --git a/CHANGES.txt b/CHANGES.txt index 3ccfd634..bafafc50 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -6,6 +6,14 @@ Unreleased ========== +2024/01/17 0.35.0 +================= + +- Permit ``urllib3.Timeout`` instances for defining timeout values. + This way, both ``connect`` and ``read`` socket timeout settings can be + configured. The unit is seconds. + + 2023/09/29 0.34.0 ================= diff --git a/buildout.cfg b/buildout.cfg index edd92a7f..55e94462 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -6,7 +6,7 @@ parts = crate [crate:linux] recipe = hexagonit.recipe.download -url = https://cdn.crate.io/downloads/releases/crate-${versions:crate_server}.tar.gz +url = https://cdn.crate.io/downloads/releases/cratedb/x64_linux/crate-${versions:crate_server}.tar.gz strip-top-level-dir = true [crate:macosx] diff --git a/devtools/setup_ci.sh b/devtools/setup_ci.sh index 5a02a479..30e7f2ea 100755 --- a/devtools/setup_ci.sh +++ b/devtools/setup_ci.sh @@ -12,7 +12,7 @@ function main() { # Replace CrateDB version. if [ ${CRATEDB_VERSION} = "nightly" ]; then - sed -ir "s/releases/releases\/nightly/g" buildout.cfg + sed -ir "s!releases/cratedb/x64_linux!releases/nightly!g" buildout.cfg sed -ir "s/crate_server.*/crate_server = latest/g" versions.cfg else sed -ir "s/crate_server.*/crate_server = ${CRATEDB_VERSION}/g" versions.cfg diff --git a/docs/by-example/client.rst b/docs/by-example/client.rst index e053d73f..6e8f08df 100644 --- a/docs/by-example/client.rst +++ b/docs/by-example/client.rst @@ -48,12 +48,25 @@ traceback if a server error occurs: >>> connection = client.connect([crate_host], error_trace=True) >>> connection.close() +Network Timeouts +---------------- + It's possible to define a default timeout value in seconds for all servers -using the optional parameter ``timeout``: +using the optional parameter ``timeout``. In this case, it will serve as a +total timeout (connect and read): >>> connection = client.connect([crate_host, invalid_host], timeout=5) >>> connection.close() +If you want to adjust the connect- vs. read-timeout values individually, +please use the ``urllib3.Timeout`` object like: + + >>> import urllib3 + >>> connection = client.connect( + ... [crate_host, invalid_host], + ... timeout=urllib3.Timeout(connect=5, read=None)) + >>> connection.close() + Authentication -------------- diff --git a/docs/by-example/http.rst b/docs/by-example/http.rst index 494e7b65..5ceed5ae 100644 --- a/docs/by-example/http.rst +++ b/docs/by-example/http.rst @@ -199,8 +199,8 @@ timeout exception: {...} >>> http_client.close() -It's possible to define a HTTP timeout in seconds on client instantiation, so -an exception is raised when the timeout is reached: +It is possible to define a HTTP timeout in seconds when creating a client +object, so an exception is raised when the timeout expires: >>> http_client = HttpClient(crate_host, timeout=0.01) >>> http_client.sql('select fib(32)') @@ -209,6 +209,17 @@ an exception is raised when the timeout is reached: crate.client.exceptions.ConnectionError: No more Servers available, exception from last server: ... >>> http_client.close() +In order to adjust the connect- vs. read-timeout values individually, +please use the ``urllib3.Timeout`` object like: + + >>> import urllib3 + >>> http_client = HttpClient(crate_host, timeout=urllib3.Timeout(connect=1.11, read=0.01)) + >>> http_client.sql('select fib(32)') + Traceback (most recent call last): + ... + crate.client.exceptions.ConnectionError: No more Servers available, exception from last server: ... + >>> http_client.close() + When connecting to non-CrateDB servers, the HttpClient will raise a ConnectionError like this: >>> http_client = HttpClient(["https://example.org/"]) diff --git a/docs/data-types.rst b/docs/data-types.rst index acad570c..2c55e7a7 100644 --- a/docs/data-types.rst +++ b/docs/data-types.rst @@ -47,7 +47,7 @@ CrateDB Python ============= =========== __ https://crate.io/docs/crate/reference/en/latest/general/ddl/data-types.html#boolean -__ https://docs.python.org/3/library/stdtypes.html#boolean-values +__ https://docs.python.org/3/library/stdtypes.html#boolean-type-bool __ https://crate.io/docs/crate/reference/en/latest/general/ddl/data-types.html#character-data __ https://docs.python.org/3/library/stdtypes.html#str __ https://crate.io/docs/crate/reference/en/latest/general/ddl/data-types.html#numeric-data diff --git a/docs/index.rst b/docs/index.rst index c166b513..27e4752e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -169,6 +169,7 @@ Examples - The :ref:`by-example` section enumerates concise examples demonstrating the different API interfaces of the CrateDB Python client library. Those are DB API, HTTP, and BLOB interfaces, and the SQLAlchemy dialect. +- Executable code examples are maintained within the `cratedb-examples repository`_. - The `sample application`_ and the corresponding `sample application documentation`_ demonstrate the use of the driver on behalf of an example "guestbook" application. @@ -223,6 +224,7 @@ The project is licensed under the terms of the Apache 2.0 license, like .. _CrateDB source: https://github.com/crate/crate .. _Create an issue: https://github.com/crate/crate-python/issues .. _development sandbox: https://github.com/crate/crate-python/blob/master/DEVELOP.rst +.. _cratedb-examples repository: https://github.com/crate/cratedb-examples/tree/main/by-language .. _FIWARE QuantumLeap data historian: https://github.com/orchestracities/ngsi-timeseries-api .. _GeoJSON: https://geojson.org/ .. _GeoJSON geometry objects: https://tools.ietf.org/html/rfc7946#section-3.1 diff --git a/examples/README.rst b/examples/README.rst new file mode 100644 index 00000000..558d412e --- /dev/null +++ b/examples/README.rst @@ -0,0 +1,9 @@ +############################## +CrateDB Python driver examples +############################## + + +Executable code examples are maintained within the `cratedb-examples repository`_. + + +.. _cratedb-examples repository: https://github.com/crate/cratedb-examples/tree/main/by-language diff --git a/setup.py b/setup.py index ca00d565..3c5c5332 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,10 @@ def read(path): 'crate = crate.client.sqlalchemy:CrateDialect' ] }, - install_requires=['urllib3<2.1'], + install_requires=[ + 'urllib3<2.2', + 'verlib2==0.2.0', + ], extras_require=dict( sqlalchemy=['sqlalchemy>=1.0,<2.1', 'geojson>=2.5.0,<4', @@ -71,8 +74,8 @@ def read(path): 'createcoverage>=1,<2', 'dask', 'stopit>=1.1.2,<2', - 'flake8>=4,<7', - 'pandas', + 'flake8>=4,<8', + 'pandas<2.1', 'pytz', ], doc=['sphinx>=3.5,<8', @@ -93,6 +96,7 @@ def read(path): 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Database' diff --git a/src/crate/client/__init__.py b/src/crate/client/__init__.py index 3d67a541..0d2e371d 100644 --- a/src/crate/client/__init__.py +++ b/src/crate/client/__init__.py @@ -29,7 +29,7 @@ # version string read from setup.py using a regex. Take care not to break the # regex! -__version__ = "0.34.0" +__version__ = "0.35.0" apilevel = "2.0" threadsafety = 2 diff --git a/src/crate/client/_pep440.py b/src/crate/client/_pep440.py deleted file mode 100644 index 83a61101..00000000 --- a/src/crate/client/_pep440.py +++ /dev/null @@ -1,501 +0,0 @@ -"""Utility to compare pep440 compatible version strings. - -The LooseVersion and StrictVersion classes that distutils provides don't -work; they don't recognize anything like alpha/beta/rc/dev versions. - -This specific file has been vendored from NumPy on 2023-02-10 [1]. -Its reference location is in `packaging` [2,3]. - -[1] https://github.com/numpy/numpy/blob/v1.25.0.dev0/numpy/compat/_pep440.py -[2] https://github.com/pypa/packaging/blob/23.0/src/packaging/_structures.py -[3] https://github.com/pypa/packaging/blob/23.0/src/packaging/version.py -""" - -# Copyright (c) Donald Stufft and individual contributors. -# All rights reserved. - -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: - -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. - -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. - -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -import collections -import itertools -import re - - -__all__ = [ - "parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN", -] - - -# BEGIN packaging/_structures.py - - -class Infinity: - def __repr__(self): - return "Infinity" - - def __hash__(self): - return hash(repr(self)) - - def __lt__(self, other): - return False - - def __le__(self, other): - return False - - def __eq__(self, other): - return isinstance(other, self.__class__) - - def __ne__(self, other): - return not isinstance(other, self.__class__) - - def __gt__(self, other): - return True - - def __ge__(self, other): - return True - - def __neg__(self): - return NegativeInfinity - - -Infinity = Infinity() - - -class NegativeInfinity: - def __repr__(self): - return "-Infinity" - - def __hash__(self): - return hash(repr(self)) - - def __lt__(self, other): - return True - - def __le__(self, other): - return True - - def __eq__(self, other): - return isinstance(other, self.__class__) - - def __ne__(self, other): - return not isinstance(other, self.__class__) - - def __gt__(self, other): - return False - - def __ge__(self, other): - return False - - def __neg__(self): - return Infinity - - -# BEGIN packaging/version.py - - -NegativeInfinity = NegativeInfinity() - -_Version = collections.namedtuple( - "_Version", - ["epoch", "release", "dev", "pre", "post", "local"], -) - - -def parse(version): - """ - Parse the given version string and return either a :class:`Version` object - or a :class:`LegacyVersion` object depending on if the given version is - a valid PEP 440 version or a legacy version. - """ - try: - return Version(version) - except InvalidVersion: - return LegacyVersion(version) - - -class InvalidVersion(ValueError): - """ - An invalid version was found, users should refer to PEP 440. - """ - - -class _BaseVersion: - - def __hash__(self): - return hash(self._key) - - def __lt__(self, other): - return self._compare(other, lambda s, o: s < o) - - def __le__(self, other): - return self._compare(other, lambda s, o: s <= o) - - def __eq__(self, other): - return self._compare(other, lambda s, o: s == o) - - def __ge__(self, other): - return self._compare(other, lambda s, o: s >= o) - - def __gt__(self, other): - return self._compare(other, lambda s, o: s > o) - - def __ne__(self, other): - return self._compare(other, lambda s, o: s != o) - - def _compare(self, other, method): - if not isinstance(other, _BaseVersion): - return NotImplemented - - return method(self._key, other._key) - - -class LegacyVersion(_BaseVersion): - - def __init__(self, version): - self._version = str(version) - self._key = _legacy_cmpkey(self._version) - - def __str__(self): - return self._version - - def __repr__(self): - return "".format(repr(str(self))) - - @property - def public(self): - return self._version - - @property - def base_version(self): - return self._version - - @property - def local(self): - return None - - @property - def is_prerelease(self): - return False - - @property - def is_postrelease(self): - return False - - -_legacy_version_component_re = re.compile( - r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE, -) - -_legacy_version_replacement_map = { - "pre": "c", "preview": "c", "-": "final-", "rc": "c", "dev": "@", -} - - -def _parse_version_parts(s): - for part in _legacy_version_component_re.split(s): - part = _legacy_version_replacement_map.get(part, part) - - if not part or part == ".": - continue - - if part[:1] in "0123456789": - # pad for numeric comparison - yield part.zfill(8) - else: - yield "*" + part - - # ensure that alpha/beta/candidate are before final - yield "*final" - - -def _legacy_cmpkey(version): - # We hardcode an epoch of -1 here. A PEP 440 version can only have an epoch - # greater than or equal to 0. This will effectively put the LegacyVersion, - # which uses the defacto standard originally implemented by setuptools, - # as before all PEP 440 versions. - epoch = -1 - - # This scheme is taken from pkg_resources.parse_version setuptools prior to - # its adoption of the packaging library. - parts = [] - for part in _parse_version_parts(version.lower()): - if part.startswith("*"): - # remove "-" before a prerelease tag - if part < "*final": - while parts and parts[-1] == "*final-": - parts.pop() - - # remove trailing zeros from each series of numeric parts - while parts and parts[-1] == "00000000": - parts.pop() - - parts.append(part) - parts = tuple(parts) - - return epoch, parts - - -# Deliberately not anchored to the start and end of the string, to make it -# easier for 3rd party code to reuse -VERSION_PATTERN = r""" - v? - (?: - (?:(?P[0-9]+)!)? # epoch - (?P[0-9]+(?:\.[0-9]+)*) # release segment - (?P
                                          # pre-release
-            [-_\.]?
-            (?P(a|b|c|rc|alpha|beta|pre|preview))
-            [-_\.]?
-            (?P[0-9]+)?
-        )?
-        (?P                                         # post release
-            (?:-(?P[0-9]+))
-            |
-            (?:
-                [-_\.]?
-                (?Ppost|rev|r)
-                [-_\.]?
-                (?P[0-9]+)?
-            )
-        )?
-        (?P                                          # dev release
-            [-_\.]?
-            (?Pdev)
-            [-_\.]?
-            (?P[0-9]+)?
-        )?
-    )
-    (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
-"""
-
-
-class Version(_BaseVersion):
-
-    _regex = re.compile(
-        r"^\s*" + VERSION_PATTERN + r"\s*$",
-        re.VERBOSE | re.IGNORECASE,
-    )
-
-    def __init__(self, version):
-        # Validate the version and parse it into pieces
-        match = self._regex.search(version)
-        if not match:
-            raise InvalidVersion("Invalid version: '{0}'".format(version))
-
-        # Store the parsed out pieces of the version
-        self._version = _Version(
-            epoch=int(match.group("epoch")) if match.group("epoch") else 0,
-            release=tuple(int(i) for i in match.group("release").split(".")),
-            pre=_parse_letter_version(
-                match.group("pre_l"),
-                match.group("pre_n"),
-            ),
-            post=_parse_letter_version(
-                match.group("post_l"),
-                match.group("post_n1") or match.group("post_n2"),
-            ),
-            dev=_parse_letter_version(
-                match.group("dev_l"),
-                match.group("dev_n"),
-            ),
-            local=_parse_local_version(match.group("local")),
-        )
-
-        # Generate a key which will be used for sorting
-        self._key = _cmpkey(
-            self._version.epoch,
-            self._version.release,
-            self._version.pre,
-            self._version.post,
-            self._version.dev,
-            self._version.local,
-        )
-
-    def __repr__(self):
-        return "".format(repr(str(self)))
-
-    def __str__(self):
-        parts = []
-
-        # Epoch
-        if self._version.epoch != 0:
-            parts.append("{0}!".format(self._version.epoch))
-
-        # Release segment
-        parts.append(".".join(str(x) for x in self._version.release))
-
-        # Pre-release
-        if self._version.pre is not None:
-            parts.append("".join(str(x) for x in self._version.pre))
-
-        # Post-release
-        if self._version.post is not None:
-            parts.append(".post{0}".format(self._version.post[1]))
-
-        # Development release
-        if self._version.dev is not None:
-            parts.append(".dev{0}".format(self._version.dev[1]))
-
-        # Local version segment
-        if self._version.local is not None:
-            parts.append(
-                "+{0}".format(".".join(str(x) for x in self._version.local))
-            )
-
-        return "".join(parts)
-
-    @property
-    def public(self):
-        return str(self).split("+", 1)[0]
-
-    @property
-    def base_version(self):
-        parts = []
-
-        # Epoch
-        if self._version.epoch != 0:
-            parts.append("{0}!".format(self._version.epoch))
-
-        # Release segment
-        parts.append(".".join(str(x) for x in self._version.release))
-
-        return "".join(parts)
-
-    @property
-    def local(self):
-        version_string = str(self)
-        if "+" in version_string:
-            return version_string.split("+", 1)[1]
-
-    @property
-    def is_prerelease(self):
-        return bool(self._version.dev or self._version.pre)
-
-    @property
-    def is_postrelease(self):
-        return bool(self._version.post)
-
-    @property
-    def version(self) -> tuple:
-        """
-        PATCH: Return version tuple for backward-compatibility.
-        """
-        return self._version.release
-
-
-def _parse_letter_version(letter, number):
-    if letter:
-        # We assume there is an implicit 0 in a pre-release if there is
-        # no numeral associated with it.
-        if number is None:
-            number = 0
-
-        # We normalize any letters to their lower-case form
-        letter = letter.lower()
-
-        # We consider some words to be alternate spellings of other words and
-        # in those cases we want to normalize the spellings to our preferred
-        # spelling.
-        if letter == "alpha":
-            letter = "a"
-        elif letter == "beta":
-            letter = "b"
-        elif letter in ["c", "pre", "preview"]:
-            letter = "rc"
-        elif letter in ["rev", "r"]:
-            letter = "post"
-
-        return letter, int(number)
-    if not letter and number:
-        # We assume that if we are given a number but not given a letter,
-        # then this is using the implicit post release syntax (e.g., 1.0-1)
-        letter = "post"
-
-        return letter, int(number)
-
-
-_local_version_seperators = re.compile(r"[\._-]")
-
-
-def _parse_local_version(local):
-    """
-    Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
-    """
-    if local is not None:
-        return tuple(
-            part.lower() if not part.isdigit() else int(part)
-            for part in _local_version_seperators.split(local)
-        )
-
-
-def _cmpkey(epoch, release, pre, post, dev, local):
-    # When we compare a release version, we want to compare it with all of the
-    # trailing zeros removed. So we'll use a reverse the list, drop all the now
-    # leading zeros until we come to something non-zero, then take the rest,
-    # re-reverse it back into the correct order, and make it a tuple and use
-    # that for our sorting key.
-    release = tuple(
-        reversed(list(
-            itertools.dropwhile(
-                lambda x: x == 0,
-                reversed(release),
-            )
-        ))
-    )
-
-    # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
-    # We'll do this by abusing the pre-segment, but we _only_ want to do this
-    # if there is no pre- or a post-segment. If we have one of those, then
-    # the normal sorting rules will handle this case correctly.
-    if pre is None and post is None and dev is not None:
-        pre = -Infinity
-    # Versions without a pre-release (except as noted above) should sort after
-    # those with one.
-    elif pre is None:
-        pre = Infinity
-
-    # Versions without a post-segment should sort before those with one.
-    if post is None:
-        post = -Infinity
-
-    # Versions without a development segment should sort after those with one.
-    if dev is None:
-        dev = Infinity
-
-    if local is None:
-        # Versions without a local segment should sort before those with one.
-        local = -Infinity
-    else:
-        # Versions with a local segment need that segment parsed to implement
-        # the sorting rules in PEP440.
-        # - Alphanumeric segments sort before numeric segments
-        # - Alphanumeric segments sort lexicographically
-        # - Numeric segments sort numerically
-        # - Shorter versions sort before longer versions when the prefixes
-        #   match exactly
-        local = tuple(
-            (i, "") if isinstance(i, int) else (-Infinity, i)
-            for i in local
-        )
-
-    return epoch, release, pre, post, dev, local
diff --git a/src/crate/client/connection.py b/src/crate/client/connection.py
index 03b5b444..9e72b2f7 100644
--- a/src/crate/client/connection.py
+++ b/src/crate/client/connection.py
@@ -23,7 +23,7 @@
 from .exceptions import ProgrammingError, ConnectionError
 from .http import Client
 from .blob import BlobContainer
-from ._pep440 import Version
+from verlib2 import Version
 
 
 class Connection(object):
diff --git a/src/crate/client/http.py b/src/crate/client/http.py
index 1318cca2..78e0e594 100644
--- a/src/crate/client/http.py
+++ b/src/crate/client/http.py
@@ -49,8 +49,8 @@
     SSLError,
 )
 from urllib3.util.retry import Retry
+from verlib2 import Version
 
-from crate.client._pep440 import Version
 from crate.client.exceptions import (
     ConnectionError,
     BlobLocationNotFoundException,
@@ -273,7 +273,9 @@ def _pool_kw_args(verify_ssl_cert, ca_cert, client_cert, client_key,
         'key_file': client_key,
     }
     if timeout is not None:
-        kw['timeout'] = float(timeout)
+        if isinstance(timeout, str):
+            timeout = float(timeout)
+        kw['timeout'] = timeout
     if pool_size is not None:
         kw['maxsize'] = int(pool_size)
     return kw
diff --git a/src/crate/client/sqlalchemy/sa_version.py b/src/crate/client/sqlalchemy/sa_version.py
index 972b568c..6b45f8b8 100644
--- a/src/crate/client/sqlalchemy/sa_version.py
+++ b/src/crate/client/sqlalchemy/sa_version.py
@@ -20,7 +20,7 @@
 # software solely pursuant to the terms of the relevant commercial agreement.
 
 import sqlalchemy as sa
-from crate.client._pep440 import Version
+from verlib2 import Version
 
 SA_VERSION = Version(sa.__version__)
 
diff --git a/src/crate/client/test_connection.py b/src/crate/client/test_connection.py
index 3b5c294c..93510864 100644
--- a/src/crate/client/test_connection.py
+++ b/src/crate/client/test_connection.py
@@ -1,5 +1,7 @@
 import datetime
 
+from urllib3 import Timeout
+
 from .connection import Connection
 from .http import Client
 from crate.client import connect
@@ -72,3 +74,25 @@ def test_with_timezone(self):
         cursor = connection.cursor()
         self.assertEqual(cursor.time_zone.tzname(None), "UTC")
         self.assertEqual(cursor.time_zone.utcoffset(None), datetime.timedelta(0))
+
+    def test_timeout_float(self):
+        """
+        Verify setting the timeout value as a scalar (float) works.
+        """
+        with connect('localhost:4200', timeout=2.42) as conn:
+            self.assertEqual(conn.client._pool_kw["timeout"], 2.42)
+
+    def test_timeout_string(self):
+        """
+        Verify setting the timeout value as a scalar (string) works.
+        """
+        with connect('localhost:4200', timeout="2.42") as conn:
+            self.assertEqual(conn.client._pool_kw["timeout"], 2.42)
+
+    def test_timeout_object(self):
+        """
+        Verify setting the timeout value as a Timeout object works.
+        """
+        timeout = Timeout(connect=2.42, read=0.01)
+        with connect('localhost:4200', timeout=timeout) as conn:
+            self.assertEqual(conn.client._pool_kw["timeout"], timeout)
diff --git a/src/crate/testing/test_layer.py b/src/crate/testing/test_layer.py
index f028e021..aaeca336 100644
--- a/src/crate/testing/test_layer.py
+++ b/src/crate/testing/test_layer.py
@@ -22,7 +22,7 @@
 import os
 import tempfile
 import urllib
-from crate.client._pep440 import Version
+from verlib2 import Version
 from unittest import TestCase, mock
 from io import BytesIO