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
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Removed

- **Breaking:** session/sandbox support is gone. `ibis.hotdata.connect()` no
longer accepts `session_id` — passing it raises `TypeError` — the `--session`
example flag and its `HOTDATA_SESSION_ID` env var are removed, and no
`X-Session-Id` header is sent.

The two surfaces differ, deliberately: `session_id` in a `hotdata://` URL is
**ignored rather than rejected**, because `_from_url` builds its arguments from
named lookups and discards the rest, as it does for any unrecognised query
parameter. So an existing URL keeps connecting, minus the header. Only the
keyword argument errors.

Forced by the SDK: `hotdata` 0.9.0 removes the `SessionId` security scheme, so
`Configuration(session_id=...)` raises `TypeError` rather than being ignored —
this backend passed it unconditionally, so every connection would have failed.
The server stopped enforcing session scoping before that release, so requests
behave the same without it.

Drop `session_id=` from `connect()` calls and from `hotdata://` query strings.

### Changed

- Require `hotdata>=0.9.0,<0.10` (was `>=0.7,<0.9`). This package's cap was the
reason `hotdata-dlt-destination` could not adopt `hotdata` 0.9 or
`hotdata-framework` 0.12, which in turn blocked their consumers.

Test fixtures gained `partition_by` / `sorted_by` on every `TableInfo` dict:
0.9.0 makes both required, so a listing response omitting them fails validation
for the whole call rather than that field. The API always sends them.


## [0.4.0] - 2026-07-22

Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ con = ibis.hotdata.connect(
token="YOUR_API_KEY",
workspace_id="ws_...",
# optional
session_id=None, # sandbox id (X-Session-Id header)
timeout=120.0, # per-request HTTP timeout in seconds
verify_ssl=True, # False to skip TLS verification, or path to CA bundle
default_connection=None, # default catalog (connection id); auto-detected if only one exists
Expand Down
10 changes: 0 additions & 10 deletions examples/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,6 @@ def parser(description: str) -> argparse.ArgumentParser:
default=os.environ.get("HOTDATA_WORKSPACE", ""),
help="Workspace public id (env HOTDATA_WORKSPACE)",
)
p.add_argument(
"--session",
dest="session_id",
default=os.environ.get("HOTDATA_SESSION_ID") or None,
help="Sandbox id for X-Session-Id (env HOTDATA_SESSION_ID, optional)",
)
p.add_argument(
"--insecure",
action="store_true",
Expand Down Expand Up @@ -209,8 +203,6 @@ def connect_kwargs(ns: argparse.Namespace, **extras) -> dict:
"timeout": ns.timeout,
"verify_ssl": not getattr(ns, "insecure", False),
}
if ns.session_id:
kwargs["session_id"] = ns.session_id
if dc:
kwargs["default_connection"] = dc
if ds:
Expand All @@ -237,8 +229,6 @@ def hotdata_connect_uri(ns: argparse.Namespace) -> str:
"workspace_id": ns.workspace_id.strip(),
"verify_ssl": "true" if verify_ssl else "false",
}
if ns.session_id:
qs["session_id"] = ns.session_id
dc = getattr(ns, "default_connection", None)
ds = getattr(ns, "default_schema", None)
if dc:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ classifiers = [
]
dependencies = [
"ibis-framework>=12,<13",
"hotdata>=0.7,<0.9",
"hotdata>=0.9.0,<0.10",
"pyarrow>=16",
"pyarrow-hotfix>=0.6",
"pandas>=2",
Expand Down
9 changes: 2 additions & 7 deletions src/ibis_hotdata/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ def _from_url(self, url: ParseResult, **kwarg_overrides: Any):
"""Connect using ``hotdata://host`` or ``hotdata://host/path`` URLs.

* Base URL defaults to ``https://{host}`` plus optional leading ``path``.
* Query string may include ``token``, ``workspace_id``, ``session_id``,
``timeout``, ``verify_ssl`` (``true`` / ``false``), ``default_connection``,
* Query string may include ``token``, ``workspace_id``, ``timeout``,
``verify_ssl`` (``true`` / ``false``), ``default_connection``,
``default_schema``, ``poll_interval_s``, ``poll_timeout_s``.
* If ``token`` is omitted, ``urlparse`` password (`user:TOKEN@`) is accepted.
"""
Expand Down Expand Up @@ -115,7 +115,6 @@ def _from_url(self, url: ParseResult, **kwarg_overrides: Any):
"api_url": api_url,
"token": token,
"workspace_id": workspace_id,
"session_id": q.pop("session_id", None),
"timeout": timeout,
"verify_ssl": verify_ssl,
"default_connection": q.pop("default_connection", None),
Comment thread
anoop-narang marked this conversation as resolved.
Expand All @@ -138,7 +137,6 @@ def do_connect(
api_url: str,
token: str,
workspace_id: str,
session_id: str | None = None,
timeout: float = 120.0,
verify_ssl: bool | str = True,
default_connection: str | None = None,
Expand All @@ -160,8 +158,6 @@ def do_connect(
API bearer token (``Authorization`` header).
workspace_id
Workspace public id (``X-Workspace-Id`` header).
session_id
Optional sandbox id (``X-Session-Id`` header).
timeout
HTTP timeout in seconds (per request).
verify_ssl
Expand Down Expand Up @@ -196,7 +192,6 @@ def do_connect(
api_url=api_url,
token=token,
workspace_id=workspace_id,
session_id=session_id,
timeout=timeout,
verify_ssl=verify_ssl,
)
Expand Down
5 changes: 1 addition & 4 deletions src/ibis_hotdata/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,11 @@ def __init__(
api_url: str,
token: str,
workspace_id: str,
session_id: str | None = None,
timeout: float = 120.0,
verify_ssl: bool | str = True,
) -> None:
host = api_url.rstrip("/")
conf = Configuration(
host=host, api_key=token, workspace_id=workspace_id, session_id=session_id
)
conf = Configuration(host=host, api_key=token, workspace_id=workspace_id)
if verify_ssl is False:
conf.verify_ssl = False
elif isinstance(verify_ssl, str):
Expand Down
33 changes: 30 additions & 3 deletions tests/test_hotdata_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@
]


# TableInfo requires these from hotdata 0.9.0 on. The API always sends them, and
# an omission fails validation for the WHOLE information_schema call rather than
# for that field — nine tests here failed exactly that way before it was added.
# Spread into each fixture rather than repeated, so the next required field is a
# one-line change here.
_REQUIRED_TABLE_FIELDS = {"partition_by": [], "sorted_by": []}


def arrow_stream(table: pa.Table) -> bytes:
sink = io.BytesIO()
with ipc.new_stream(sink, table.schema) as writer:
Expand Down Expand Up @@ -86,6 +94,7 @@ def information_schema_response(
"connection": connection,
"schema": schema_name,
"table": table_name,
**_REQUIRED_TABLE_FIELDS,
"synced": True,
"last_sync": None,
"columns": columns,
Expand Down Expand Up @@ -650,6 +659,7 @@ def test_information_schema_discovery(httpserver: HTTPServer, srv: str):
"connection": TPCH_CONN,
"schema": TPCH_SF1,
"table": "customer",
**_REQUIRED_TABLE_FIELDS,
"synced": True,
"last_sync": None,
"columns": TPCH_CUSTOMER_COLS,
Expand All @@ -676,6 +686,7 @@ def test_information_schema_pagination_merges_pages(httpserver: HTTPServer, srv:
"connection": TPCH_CONN,
"schema": TPCH_SF1,
"table": "customer",
**_REQUIRED_TABLE_FIELDS,
"synced": True,
"columns": None,
},
Expand All @@ -685,6 +696,7 @@ def test_information_schema_pagination_merges_pages(httpserver: HTTPServer, srv:
"connection": TPCH_CONN,
"schema": TPCH_SF1,
"table": "lineitem",
**_REQUIRED_TABLE_FIELDS,
"synced": True,
"columns": None,
},
Expand Down Expand Up @@ -750,20 +762,23 @@ def test_list_tables_regex_like(httpserver: HTTPServer, srv: str):
"connection": TPCH_CONN,
"schema": TPCH_SF1,
"table": "customer",
**_REQUIRED_TABLE_FIELDS,
"synced": True,
"columns": None,
},
{
"connection": TPCH_CONN,
"schema": TPCH_SF1,
"table": "lineitem",
**_REQUIRED_TABLE_FIELDS,
"synced": True,
"columns": None,
},
{
"connection": TPCH_CONN,
"schema": TPCH_SF1,
"table": "nation",
**_REQUIRED_TABLE_FIELDS,
"synced": True,
"columns": None,
},
Expand Down Expand Up @@ -799,7 +814,14 @@ def test_ambiguous_default_connection(httpserver: HTTPServer, srv: str):
_ = con.current_catalog


def test_x_session_header_on_query(httpserver: HTTPServer, srv: str):
def test_no_session_header_on_query(httpserver: HTTPServer, srv: str):
"""The inverse of what this asserted before: no X-Session-Id on the wire.

Asserted at the HTTP layer rather than on the signature, because that is the
only place a revival shows up. Reading the value from the environment and
passing it into Configuration would restore the header with no signature
change at all, and every other check here would still pass.
"""
seen: list[str | None] = []

def on_post(req: Request) -> Response:
Expand Down Expand Up @@ -839,7 +861,6 @@ def on_post(req: Request) -> Response:
api_url=srv,
token="tok",
workspace_id="ws",
session_id="sb_xyz",
verify_ssl=False,
default_connection=TPCH_CONN,
default_schema=TPCH_SF1,
Expand All @@ -848,4 +869,10 @@ def on_post(req: Request) -> Response:
pdf = con.execute(ibis.literal(0).name("n"))
assert pdf == 0
assert len(seen) >= 1
assert all(h == "sb_xyz" for h in seen)
assert all(h is None for h in seen), f"X-Session-Id was sent: {seen}"

# And the argument that produced it is gone from the public signature.
with pytest.raises(TypeError, match="session_id"):
ibis.hotdata.connect(
api_url=srv, token="tok", workspace_id="ws", session_id="sb_xyz"
)
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading