Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ per-file-ignores =
proton_driver/varint.pyx: E225, E226, E227, E999
# ignore example print warning.
example/*: T201, T001
# standalone smoke script reports its result via print.
tests/smoke_no_compression.py: T201
exclude = venv,.conda,build
51 changes: 41 additions & 10 deletions .github/workflows/actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ jobs:
build_wheels:
name: Build wheels on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
permissions:
# The GitHub release is created with the workflow's own token; the
# GH_ACCESS_TOKEN PAT this step used through v0.2.13 no longer exists.
contents: write
strategy:
matrix:
os: [ ubuntu-20.04 , windows-2019, macos-12 ]
os: [ ubuntu-22.04 , windows-2022, macos-13 ]
steps:
- uses: actions/checkout@v4
- name: Get proton-python-driver tag
Expand Down Expand Up @@ -36,29 +40,53 @@ jobs:
image: tonistiigi/binfmt:latest
platforms: all
- name: Build wheels
uses: pypa/cibuildwheel@v2.21.3
# v3.4.1 is the first stable line with CPython 3.14 (incl. cp314t) wheel support.
# v2.21.3 predates 3.14 and would silently skip cp314/cp314t targets.
uses: pypa/cibuildwheel@v3.4.1
with:
package-dir: .
output-dir: wheelhouse
config-file: pyproject.toml
- name: Store the distribution packages
# Upload before the GitHub-release step: if release creation fails,
# the wheels still reach publish-to-pypi (and remain downloadable).
uses: actions/upload-artifact@v4
with:
# v4 requires unique artifact names per job; matrix.os keeps them distinct.
name: python-package-distributions-${{ matrix.os }}
path: wheelhouse/*.whl
- name: Release wheels
uses: softprops/action-gh-release@v2
with:
files: wheelhouse/*.whl
generate_release_notes: true
tag_name: ${{ join(steps.*.outputs.tag_name, '') }}
env:
GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Store the distribution packages
uses: actions/upload-artifact@v3
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

build_sdist:
# Interpreters without a published wheel (e.g. EOL PyPy releases that
# modern cibuildwheel cannot target) install from this sdist. It bundles
# the pre-generated Cython C, so building it needs no Cython.
name: Build source distribution
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Build sdist
run: pipx run build --sdist
- name: Store the source distribution
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: wheelhouse/*.whl

# Matches the python-package-distributions-* download pattern in
# publish-to-pypi alongside the per-OS wheel artifacts.
name: python-package-distributions-sdist
path: dist/*.tar.gz

publish-to-pypi:
name: Publish Python distribution to PyPI
needs:
- build_wheels
- build_sdist
runs-on: ubuntu-latest
environment:
name: pypi
Expand All @@ -67,9 +95,12 @@ jobs:
id-token: write # IMPORTANT: mandatory for trusted publishing
steps:
- name: Download all the dists
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: python-package-distributions
# v4 download without a name arg fetches every matching artifact; the pattern
# joins the per-OS uploads from build_wheels back into a single dist/ tree.
pattern: python-package-distributions-*
merge-multiple: true
path: dist/
- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
Expand Down
125 changes: 118 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ name: test
jobs:
tests:
if: ${{ github.event.action != 'closed' || github.event.pull_request.merged == true }}
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
strategy:
# Surface every broken matrix entry in one run instead of
# cancelling the rest on the first failure.
fail-fast: false
matrix:
use-numpy:
- 0
Expand All @@ -23,6 +26,7 @@ jobs:
- "3.11"
- "3.12"
- "3.13"
- "3.14"
- "pypy-3.9"
- "pypy-3.10"
proton-version:
Expand Down Expand Up @@ -64,36 +68,115 @@ jobs:
sed -i 's/^host=localhost$/host=proton-server/' setup.cfg
# Make host think that proton-server is localhost
echo '127.0.0.1 proton-server' | sudo tee /etc/hosts > /dev/null
- name: Wait for Proton server
run: |
# --host proton-server: the proton-client proxy docker-execs
# into the client container, where localhost is NOT the server.
probe() { proton-client --host proton-server --port 8463 --query "SELECT 1"; }
for i in $(seq 1 150); do
if probe >/dev/null 2>&1; then
echo "server is up (attempt $i)"; exit 0
fi
if [ $((i % 15)) -eq 0 ]; then
echo "still waiting (attempt $i); container: $(docker ps -a --filter name=test-proton-server --format '{{.Status}}')"
fi
sleep 2
done
echo "server failed to come up"
echo "--- container status:"
docker ps -a --filter name=test-proton
echo "--- docker logs (tail):"
docker logs test-proton-server 2>&1 | tail -50
echo "--- server error log (tail):"
docker exec test-proton-server tail -100 /var/log/timeplusd-server/timeplusd-server.err.log || true
echo "--- last client error:"
probe || true
exit 1
env:
TZ: UTC
- name: Build cython extensions with tracing
run: CYTHON_TRACE=1 python setup.py build_ext --define CYTHON_TRACE
if: ${{ !contains(matrix.python-version, 'pypy') }}
- name: Install requirements
run: |
# Newer coveralls do not work with github actions.
pip install 'coveralls<3.0.0'
# Modern coveralls works with GitHub Actions and pulls a
# coverage release that ships C-tracer wheels — the Cython
# coverage plugin needs that tracer.
pip install coveralls
pip install cython
python testsrequire.py
python setup.py develop
# pip resolves Requires-Python metadata; the legacy
# `setup.py develop` easy_install path ignored it and pulled
# tzlocal 5.x (zoneinfo) onto Python 3.8.
pip install -e . --no-build-isolation
# Limit each test time execution.
pip install pytest-timeout
env:
USE_NUMPY: ${{ matrix.use-numpy }}
- name: Run tests
run: coverage run -m pytest --timeout=10 -v
# coverage's C tracer doesn't exist on pypy and the Cython
# coverage plugin refuses to load without it — run plain pytest
# there (no compiled extensions to trace on pypy anyway).
run: |
if [[ "${{ matrix.python-version }}" == pypy* ]]; then
python -m pytest --timeout=10 -v
else
coverage run -m pytest --timeout=10 -v
fi
timeout-minutes: 5
env:
# Set initial TZ for docker exec -e "`env | grep ^TZ`"
TZ: UTC
- name: Upload coverage
run: coveralls
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_PARALLEL: true
COVERALLS_FLAG_NAME: ${{ matrix.python-version }} CH=${{ matrix.proton-version }} NUMPY=${{ matrix.use-numpy }}

# Free-threaded 3.14 (cp314t) smoke. Locks in that (a) the wheel builds on
# 3.14t, (b) every compiled extension imports with the stdlib compression
# modules (zlib / bz2 / compression.zstd / lzma) artificially blocked —
# the invariant CYTHON_COMPRESS_STRINGS=0 in setup.py guarantees — and
# (c) the GIL stays disabled after import: all four extensions declare
# freethreading_compatible, so a single regression here re-enables the
# GIL and fails the assert inside tests/smoke_no_compression.py.
smoke-ft:
name: 3.14t FT build + import smoke
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up free-threaded Python 3.14
uses: actions/setup-python@v5
with:
python-version: "3.14t"
- name: Install driver (pulls pytz/tzlocal and compiles extensions under 3.14t)
run: |
python -m pip install --upgrade pip
python -m pip install cython setuptools wheel
# Install into site-packages so the smoke script resolves proton_driver
# unambiguously regardless of cwd. A plain `build_ext --inplace` would
# leave the driver package importable only when cwd==project root.
python -m pip install .
- name: Report FT runtime state
run: |
python -c "import sys; print('python', sys.version); print('GIL enabled at startup:', getattr(sys, '_is_gil_enabled', lambda: True)())"
- name: Block-import smoke (zlib / bz2 / compression.zstd / lzma)
run: python tests/smoke_no_compression.py
- name: Threaded extension stress (GIL must stay off)
# pytest puts the repo checkout first on sys.path (tests/ is a
# package), shadowing the pip-installed driver — so the source
# tree needs its own compiled extensions.
run: |
python setup.py build_ext --inplace
python -m pip install pytest
python -m pytest tests/test_freethreading.py::ExtensionConcurrencyTestCase -v

coveralls-finished:
name: Indicate completion to coveralls.io
needs: tests
continue-on-error: true
runs-on: ubuntu-latest
steps:
- name: Finished
Expand All @@ -105,7 +188,7 @@ jobs:
valgrind:
name: Valgrind check
needs: tests
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Python
Expand Down Expand Up @@ -136,10 +219,38 @@ jobs:
sed -i 's/^host=localhost$/host=proton-server/' setup.cfg
# Make host think that proton-server is localhost
echo '127.0.0.1 proton-server' | sudo tee /etc/hosts > /dev/null
- name: Wait for Proton server
run: |
# --host proton-server: the proton-client proxy docker-execs
# into the client container, where localhost is NOT the server.
probe() { proton-client --host proton-server --port 8463 --query "SELECT 1"; }
for i in $(seq 1 150); do
if probe >/dev/null 2>&1; then
echo "server is up (attempt $i)"; exit 0
fi
if [ $((i % 15)) -eq 0 ]; then
echo "still waiting (attempt $i); container: $(docker ps -a --filter name=test-proton-server --format '{{.Status}}')"
fi
sleep 2
done
echo "server failed to come up"
echo "--- container status:"
docker ps -a --filter name=test-proton
echo "--- docker logs (tail):"
docker logs test-proton-server 2>&1 | tail -50
echo "--- server error log (tail):"
docker exec test-proton-server tail -100 /var/log/timeplusd-server/timeplusd-server.err.log || true
echo "--- last client error:"
probe || true
exit 1
env:
TZ: UTC
- name: Install requirements
run: |
# --no-build-isolation needs setuptools/wheel preinstalled.
pip install --upgrade pip setuptools wheel
python testsrequire.py
python setup.py develop
pip install -e . --no-build-isolation
env:
USE_NUMPY: 1
- name: Run tests under valgrind
Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,31 @@

## Unreleased

## [0.3.0] - 2026-06-11
### Added
- CPython 3.14 support: `cp314` and free-threaded `cp314t` wheels (Cython
regenerated with 3.2.4, cibuildwheel 3.4.1).
- True free-threading support: all compiled extensions declare
`freethreading_compatible`, so importing the driver on a 3.14t
interpreter keeps the GIL disabled. CI asserts this on every wheel.
- Threaded stress tests (extension-level and parallel-clients).

### Changed
- Compiled extensions no longer import stdlib compression modules at
init (`CYTHON_COMPRESS_STRINGS=0`), keeping minimal/embedded CPython
builds working.
- Building from `.pyx` sources now requires Cython >= 3.1; older
releases silently ignore the `freethreading_compatible` directive.
Standard pip installs compile the bundled generated C and are
unaffected.
- PyPy wheels are now published for PyPy 3.11 only; the PyPy 3.8–3.10
wheels shipped by 0.2.13 target EOL interpreters that the modern
build toolchain no longer supports (the sdist still installs there).
- The release workflow publishes an sdist again — no release since
0.2.10 had shipped one, so interpreters without a prebuilt wheel had
no installable artifact. The sdist bundles the pre-generated Cython C
and builds without Cython.

## [0.2.3] - 2022-02-07
### Added
- `tzlocal`>=4.0 support. Pull request [#263](https://github.com/mymarilyn/clickhouse-driver/pull/263) by [azat](https://github.com/azat).
Expand Down
19 changes: 18 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,24 @@ This project provides python driver to interact with Timeplus Proton or Timeplus

Installation
------------
Timeplus Python Driver currently supports the following versions of Python: 3.8, 3.9, 3.10, 3.11, 3.12 and 3.13.
Timeplus Python Driver currently supports the following versions of Python: 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, and 3.14.

The driver fully supports the free-threaded ``3.14t`` interpreter: all
compiled extensions declare free-threading compatibility, so the GIL stays
disabled when the driver is imported. ``cp314t`` wheels are published
alongside the regular ``cp314`` wheels under the same package name — ``pip``
on a free-threaded interpreter picks the right wheel automatically.

One caveat: the optional compression extras (``proton-driver[lz4]`` /
``[zstd]``) are not fully free-threaded yet. ``lz4`` 4.4.5+ already declares
free-threading support, but both extras also require ``clickhouse-cityhash``,
which has no free-threaded (``cp314t``) wheels and whose sdist does not
compile on ``3.14t`` — a free-threaded fork is maintained at
`timeplus-io/proton-cityhash <https://github.com/timeplus-io/proton-cityhash>`_
and will replace it in a follow-up release. ``zstd`` ships ``cp314t`` wheels
but does not declare free-threading support, so importing it re-enables the
GIL process-wide (everything still works, just without free-threaded
parallelism). Uncompressed connections — the default — are unaffected.

Installing with pip
We recommend creating a virtual environment when installing Python dependencies. For more information on setting up a virtual environment, see the `Python documentation <https://docs.python.org/3.9/tutorial/venv.html>`_.
Expand Down
2 changes: 1 addition & 1 deletion proton_driver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .dbapi import connect


VERSION = (0, 2, 13)
VERSION = (0, 3, 0)
__version__ = '.'.join(str(x) for x in VERSION)

__all__ = ['Client', 'connect']
Loading
Loading