Skip to content
Open
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
35 changes: 35 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,41 @@ list](https://github.com/trinodb/trino-python-client/tags), the
[README](https://github.com/trinodb/trino-python-client/blob/master/README.md)
and the [PyPI page](https://pypi.org/project/trino/).

## Release 0.340.0

* **Breaking:** Replace the `requests` HTTP stack with
[httpx2](https://github.com/pydantic/httpx2). HTTP/2 is negotiated via ALPN
on TLS connections when the server supports it, with transparent HTTP/1.1
fallback. Note the following interface changes:
* The `http_session` parameter of `trino.dbapi.Connection` and
`trino.client.TrinoRequest` now takes an `httpx2.Client` instead of a
`requests.Session`. TLS options (`verify`, `cert`) and `trust_env` can
only be configured when the client is constructed, so combining a custom
`http_session` with an authentication that needs them (for example
`CertificateAuthentication`) now raises `TrinoConnectionError` instead of
silently mutating the session.
* The `trino.auth.Authentication` interface changed: `set_http_session` was
replaced by `get_http_auth()` (returning an `httpx2.Auth`) and
`get_client_arguments()` (returning `verify`/`cert`/`trust_env`
constructor arguments). Third-party implementations must be updated.
* Transport errors are now `httpx2` exceptions (`httpx2.HTTPError` and
subclasses) instead of `requests.exceptions.*`.
* `trino.client.PROXIES` uses the httpx mounts shape
(`{"all://": "socks5://host:port"}`) and is applied when the HTTP client
is constructed, not per request. SOCKS support requires the new
`trino[socks]` extra.
* **Breaking:** Kerberos and GSSAPI authentication implement the SPNEGO token
exchange in the client on top of `python-gssapi` instead of
`requests_kerberos`/`requests_gssapi`. The public constructor signatures of
`KerberosAuthentication` and `GSSAPIAuthentication` are unchanged and both
the `trino[kerberos]` and `trino[gssapi]` extras now install the same
dependencies. A GSSAPI implementation such as MIT Kerberos is required;
Windows SSPI is not supported.
* Add `trino.aio`, an asynchronous DBAPI-like interface built on
`httpx2.AsyncClient` with support for all authentication mechanisms and the
spooled protocol. Transactions are not supported by the asynchronous client
yet.

## Release 0.339.0

* Add support for returning column comments from SQLAlchemy `get_columns`.
Expand Down
11 changes: 7 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ tox

### Package layout (`trino/`)

- **`client.py`** - Core HTTP protocol implementation. `TrinoRequest` manages HTTP requests to the coordinator, `TrinoQuery` manages query lifecycle (submit -> poll -> fetch results), `ClientSession` holds connection state (headers, session properties, transaction ID). Implements the protocol described at https://github.com/trinodb/trino/wiki/HTTP-Protocol.
- **`client.py`** - Core synchronous HTTP protocol implementation on `httpx2.Client`. `TrinoRequest` manages HTTP requests to the coordinator, `TrinoQuery` manages query lifecycle (submit -> poll -> fetch results), `ClientSession` holds connection state (headers, session properties, transaction ID). Implements the protocol described at https://github.com/trinodb/trino/wiki/HTTP-Protocol.
- **`_protocol.py`** - Transport-agnostic protocol logic shared by the sync and async clients: header construction/parsing, response processing, retry decisions, session state, segment decoding.
- **`aio/`** - Asynchronous client on `httpx2.AsyncClient`: `AsyncTrinoRequest`/`AsyncTrinoQuery` in `aio/client.py` and a DBAPI-like async `Connection`/`Cursor` in `aio/dbapi.py` (entry point `trino.aio.connect()`). Not PEP 249; transactions unsupported.
- **`_spnego.py`** - In-repo SPNEGO (Kerberos/GSSAPI) `httpx2.Auth` flow on python-gssapi, backing `KerberosAuthentication` and `GSSAPIAuthentication`.
- **`dbapi.py`** - PEP 249 DBAPI 2.0 interface. `Connection` and `Cursor` classes that wrap `client.py`. Entry point is `trino.dbapi.connect()`.
- **`sqlalchemy/`** - SQLAlchemy dialect (`TrinoDialect`), compiler, and type mapping. Registered as `trino://` via `sqlalchemy.dialects` entry point. Compatible with SQLAlchemy 1.3.x, 1.4.x, and 2.0.x.
- **`auth.py`** - Authentication implementations (Basic, JWT, OAuth2, Kerberos/GSSAPI, Certificate).
Expand All @@ -46,14 +49,14 @@ tox

### Tests (`tests/`)

- `tests/unit/` - No external dependencies needed. Uses `httpretty` for HTTP mocking.
- `tests/unit/` - No external dependencies needed. HTTP is mocked with the hand-written recording router in `tests/unit/mock_http.py` (`MockTrinoServer`, served through `httpx2.MockTransport`; works for both sync and async clients). Async tests live in `tests/unit/aio/` and run under pytest-asyncio (`asyncio_mode = auto`).
- `tests/integration/` - Requires Docker. Tests automatically pull `trinodb/trino` image and start a container (or reuse one on port 8080).
- Env vars: `TRINO_VERSION` (image tag, default `latest`), `TRINO_RUNNING_HOST`/`TRINO_RUNNING_PORT` (use an existing server instead of starting one).
- When `TRINO_VERSION` is `latest` or >= 466, a LocalStack container with an S3 `spooling` bucket is also started to test the spooled client protocol; older versions use the `etc/*-pre-466*` configs.

### Key dependencies

- `requests` - HTTP transport
- `httpx2` - HTTP transport (sync and async, HTTP/2 via ALPN with HTTP/1.1 fallback)
- `orjson` (CPython) / `json` (PyPy) - JSON parsing, selected at import time in `client.py`
- `lz4`, `zstandard` - Response decompression
- `python-dateutil`, `pytz`, `tzlocal` - Timezone/datetime handling
Expand All @@ -63,5 +66,5 @@ tox
- **Max line length**: 120 characters (flake8)
- **Import ordering**: managed by `reorder-python-imports` (Python 3.9+ style)
- **Type checking**: mypy with strict settings, though `tests/*`, `trino/client`, `trino/dbapi`, and `trino/sqlalchemy.*` have `ignore_errors = true`
- **No mocking libraries**: write mocks by hand instead of using `unittest.mock` or similar. The project uses `httpretty` for HTTP-level stubbing only.
- **No mocking libraries**: write mocks by hand instead of using `unittest.mock` or similar. HTTP-level stubbing goes through `tests/unit/mock_http.py` (`httpx2.MockTransport`-based).
- **Pre-commit hooks**: flake8, mypy, reorder-python-imports, trailing whitespace, EOF newlines, YAML syntax, case-conflict checks
51 changes: 43 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,38 @@ the query's current stats dictionary (the same dictionary returned by
`Cursor.stats`), so mutating it has no effect on the client. Any exception
raised by the callback propagates to the caller of `execute()`/`fetch()`.

### Asynchronous client

`trino.aio` provides an asynchronous, DBAPI-like interface built on
`httpx2.AsyncClient`. It mirrors `trino.dbapi` with coroutine methods and
accepts the same connection arguments (it is not a PEP 249 implementation, as
PEP 249 is a synchronous specification):

```python
import asyncio
import trino.aio


async def main():
async with trino.aio.connect(
host="<host>",
port=<port>,
user="<username>",
catalog="<catalog>",
schema="<schema>",
) as conn:
cur = conn.cursor()
await cur.execute("SELECT * FROM system.runtime.nodes")
rows = await cur.fetchall()
# or: async for row in cur: ...


asyncio.run(main())
```

All authentication mechanisms and the spooled protocol are supported.
Transactions are not supported by the asynchronous client yet.

### SQLAlchemy

**Prerequisite**
Expand Down Expand Up @@ -357,8 +389,10 @@ Make sure that the GSSAPI support is installed using `pip install trino[gssapi]`
The `GSSAPIAuthentication` class can be used to connect to a Trino cluster configured with
the [`Kerberos` authentication type](https://trino.io/docs/current/security/kerberos.html):

It follows the interface for `KerberosAuthentication`, but is using
[requests-gssapi](https://github.com/pythongssapi/requests-gssapi), instead of [requests-kerberos](https://github.com/requests/requests-kerberos) under the hood.
It follows the interface for `KerberosAuthentication`. Both classes implement
the SPNEGO token exchange directly on top of
[python-gssapi](https://github.com/pythongssapi/python-gssapi), which requires
a GSSAPI implementation such as MIT Kerberos (Windows SSPI is not supported).

- DBAPI

Expand Down Expand Up @@ -479,7 +513,7 @@ conn = connect(
### Self-signed certificates

To use self-signed certificates, specify a path to the certificate in `verify` parameter.
More details can be found in [the Python requests library documentation](https://requests.readthedocs.io/en/latest/user/advanced/#ssl-cert-verification).
More details can be found in [the httpx SSL documentation](https://www.python-httpx.org/advanced/ssl/).

```python
from trino.dbapi import connect
Expand Down Expand Up @@ -568,23 +602,24 @@ The transaction is created when the first SQL statement is executed.
exits the *with* context and the queries succeed, otherwise
`trino.dbapi.Connection.rollback()` will be called.

## Custom requests Session
## Custom HTTP client

You can create a custom [requests.Session object](https://requests.readthedocs.io/en/latest/user/advanced/#session-objects) and pass it to the `http_session` parameter. This can be used for things like setting additional HTTP headers, client certificates, etc.
You can create a custom [httpx2.Client](https://www.python-httpx.org/advanced/clients/) and pass it to the `http_session` parameter. This can be used for things like setting additional HTTP headers, client certificates, proxies, etc. Note that TLS options (`verify`, `cert`) and `trust_env` can only be set when the client is constructed.

```python
import requests
import httpx2
from trino.dbapi import connect

s = requests.Session()
s.cert = '/path/client.cert'
s = httpx2.Client(cert='/path/client.cert', http2=True)

conn = connect(
http_session=s,
...
)
```

The asynchronous client accepts an `httpx2.AsyncClient` the same way.

## Legacy Primitive types

By default, the client will convert the results of the query to the
Expand Down
6 changes: 5 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,9 @@ no_implicit_optional = true
warn_unused_ignores = true
disable_error_code = import-untyped

[mypy-tests.*,trino.client,trino.dbapi,trino.sqlalchemy.*]
[mypy-tests.*,trino.client,trino.dbapi,trino.aio.*,trino.sqlalchemy.*]
ignore_errors = true

[tool:pytest]
asyncio_mode = auto
asyncio_default_fixture_loop_scope = function
22 changes: 11 additions & 11 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,22 @@
with open(os.path.join(here, "README.md"), "r", "utf-8") as f:
readme = f.read()

kerberos_require = ["requests_kerberos"]
gssapi_require = [""
"requests_gssapi",
# PyPy compatibility issue https://github.com/jborean93/pykrb5/issues/49
"krb5 == 0.5.1"]
# Kerberos and GSSAPI authentication use the in-repo SPNEGO flow (trino/_spnego.py)
# built on python-gssapi; both extras pull the same dependencies.
kerberos_require = ["gssapi",
# PyPy compatibility issue https://github.com/jborean93/pykrb5/issues/49
"krb5 == 0.5.1"]
gssapi_require = kerberos_require
sqlalchemy_require = ["sqlalchemy >= 1.3"]
socks_require = ["httpx2[socks]"]
external_authentication_token_cache_require = ["keyring"]

# We don't add localstorage_require to all_require as users must explicitly opt in to use keyring.
all_require = kerberos_require + sqlalchemy_require
all_require = sqlalchemy_require

tests_require = all_require + [
# httpretty >= 1.1 duplicates requests in `httpretty.latest_requests`
# https://github.com/gabrielfalcao/HTTPretty/issues/425
"httpretty < 1.1",
"pytest",
"pytest-asyncio",
"pytest-runner",
"pre-commit",
"black",
Expand Down Expand Up @@ -83,12 +83,11 @@
],
python_requires=">=3.9",
install_requires=[
"httpx2[http2]",
"lz4",
"orjson >= 3.11.0 ; platform_python_implementation != 'PyPy'",
"python-dateutil",
"pytz",
# CVE-2024-47081
"requests>=2.32.4",
"tzlocal",
"zstandard",
],
Expand All @@ -97,6 +96,7 @@
"kerberos": kerberos_require,
"gssapi": gssapi_require,
"sqlalchemy": sqlalchemy_require,
"socks": socks_require,
"tests": tests_require,
"external-authentication-token-cache": external_authentication_token_cache_require,
},
Expand Down
19 changes: 10 additions & 9 deletions tests/integration/test_dbapi_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
from typing import Tuple
from zoneinfo import ZoneInfo

import httpx2
import pytest
import requests
from tzlocal import get_localzone_name # type: ignore

import trino
Expand Down Expand Up @@ -1580,18 +1580,19 @@ def retrieve_client_tags_from_query(run_trino, client_tags):
api_url = "http://" + trino_connection.host + ":" + str(trino_connection.port)

if trino_version() >= 483:
session = requests.Session()
resp = session.post(api_url + "/ui/auth/login", json={
"username": "admin", "password": ""
})
assert resp.ok, f"POST request to /ui/auth/login failed: {resp.status_code} {resp.reason}"
query_info = session.get(api_url + "/ui/api/query/" + cur._query.query_id).json()
with httpx2.Client() as session:
resp = session.post(api_url + "/ui/auth/login", json={
"username": "admin", "password": ""
})
assert resp.is_success, \
f"POST request to /ui/auth/login failed: {resp.status_code} {resp.reason_phrase}"
query_info = session.get(api_url + "/ui/api/query/" + cur._query.query_id).json()
else:
query_info = requests.post(api_url + "/ui/login", data={
query_info = httpx2.post(api_url + "/ui/login", data={
"username": "admin",
"password": "",
"redirectPath": api_url + '/ui/api/query/' + cur._query.query_id
}).json()
}, follow_redirects=True).json()

query_client_tags = query_info['session']['clientTags']
return query_client_tags
Expand Down
Empty file added tests/unit/aio/__init__.py
Empty file.
Loading
Loading