From bf555d968b3b20849040e93107a680c96d4dd868 Mon Sep 17 00:00:00 2001 From: Anoop Narang Date: Tue, 11 Aug 2026 22:24:30 +0530 Subject: [PATCH 1/2] feat!: remove session/sandbox support, require hotdata >=0.9 connect() and the hotdata:// URL no longer accept session_id, the --session example flag and HOTDATA_SESSION_ID are gone, and no X-Session-Id header is sent. Forced by the SDK. hotdata 0.9.0 removes the SessionId security scheme, so Configuration(session_id=...) raises TypeError rather than being ignored, and http.py passed it unconditionally -- every connection would have failed. Proven by sabotage: reviving it from the environment (no signature change at all) takes the suite to 37 failed / 72 passed, because the client cannot even construct. WHY THIS PACKAGE IS ON THE CRITICAL PATH. Its hotdata cap was <0.9, and hotdata-dlt-destination pulls it in through its [ibis] extra -- so this cap was what stopped the destination adopting hotdata 0.9 and hotdata-framework 0.12, which in turn blocked dlthubworker from using the SDK's new table-layout API instead of hand-built HTTP. Four repos deep, and this is the bottom of it. The X-Session-Id test is inverted rather than deleted: it now asserts the header is ABSENT, at the HTTP layer, because that is the only place a revival shows up. It also asserts connect(session_id=...) raises, which is what catches a signature revival -- sabotaged both directions. 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 information_schema call rather than for that field. Nine tests failed on exactly this before the fixtures were updated -- the same total-rather-than-partial failure mode flagged in review of sdk-python#160. 109 passed. --- CHANGELOG.md | 25 +++++++++++++++++++ README.md | 1 - examples/_helpers.py | 10 -------- pyproject.toml | 2 +- src/ibis_hotdata/backend.py | 9 ++----- src/ibis_hotdata/http.py | 5 +--- tests/test_hotdata_backend.py | 46 ++++++++++++++++++++++++++++++++--- uv.lock | 8 +++--- 8 files changed, 76 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb08dbc..9e48293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,31 @@ 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()` and the + `hotdata://` URL no longer accept `session_id`, the `--session` example flag and + its `HOTDATA_SESSION_ID` env var are removed, and no `X-Session-Id` header is + sent. + + 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 diff --git a/README.md b/README.md index 34cd371..a90919f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/examples/_helpers.py b/examples/_helpers.py index e0113d8..a918953 100644 --- a/examples/_helpers.py +++ b/examples/_helpers.py @@ -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", @@ -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: @@ -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: diff --git a/pyproject.toml b/pyproject.toml index 8d99aa3..755cdb2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/src/ibis_hotdata/backend.py b/src/ibis_hotdata/backend.py index 08c49e8..501d347 100644 --- a/src/ibis_hotdata/backend.py +++ b/src/ibis_hotdata/backend.py @@ -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. """ @@ -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), @@ -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, @@ -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 @@ -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, ) diff --git a/src/ibis_hotdata/http.py b/src/ibis_hotdata/http.py index b4c544f..5a7a595 100644 --- a/src/ibis_hotdata/http.py +++ b/src/ibis_hotdata/http.py @@ -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): diff --git a/tests/test_hotdata_backend.py b/tests/test_hotdata_backend.py index 4f8c31f..0d58a26 100644 --- a/tests/test_hotdata_backend.py +++ b/tests/test_hotdata_backend.py @@ -86,6 +86,10 @@ def information_schema_response( "connection": connection, "schema": schema_name, "table": table_name, + # Required on TableInfo since hotdata 0.9.0; the API always sends + # them, and an omission fails validation for the whole listing. + "partition_by": [], + "sorted_by": [], "synced": True, "last_sync": None, "columns": columns, @@ -650,6 +654,10 @@ def test_information_schema_discovery(httpserver: HTTPServer, srv: str): "connection": TPCH_CONN, "schema": TPCH_SF1, "table": "customer", + # Required on TableInfo since hotdata 0.9.0; the API always sends + # them, and an omission fails validation for the whole listing. + "partition_by": [], + "sorted_by": [], "synced": True, "last_sync": None, "columns": TPCH_CUSTOMER_COLS, @@ -676,6 +684,10 @@ def test_information_schema_pagination_merges_pages(httpserver: HTTPServer, srv: "connection": TPCH_CONN, "schema": TPCH_SF1, "table": "customer", + # Required on TableInfo since hotdata 0.9.0; the API always sends + # them, and an omission fails validation for the whole listing. + "partition_by": [], + "sorted_by": [], "synced": True, "columns": None, }, @@ -685,6 +697,10 @@ def test_information_schema_pagination_merges_pages(httpserver: HTTPServer, srv: "connection": TPCH_CONN, "schema": TPCH_SF1, "table": "lineitem", + # Required on TableInfo since hotdata 0.9.0; the API always sends + # them, and an omission fails validation for the whole listing. + "partition_by": [], + "sorted_by": [], "synced": True, "columns": None, }, @@ -750,6 +766,10 @@ def test_list_tables_regex_like(httpserver: HTTPServer, srv: str): "connection": TPCH_CONN, "schema": TPCH_SF1, "table": "customer", + # Required on TableInfo since hotdata 0.9.0; the API always sends + # them, and an omission fails validation for the whole listing. + "partition_by": [], + "sorted_by": [], "synced": True, "columns": None, }, @@ -757,6 +777,10 @@ def test_list_tables_regex_like(httpserver: HTTPServer, srv: str): "connection": TPCH_CONN, "schema": TPCH_SF1, "table": "lineitem", + # Required on TableInfo since hotdata 0.9.0; the API always sends + # them, and an omission fails validation for the whole listing. + "partition_by": [], + "sorted_by": [], "synced": True, "columns": None, }, @@ -764,6 +788,10 @@ def test_list_tables_regex_like(httpserver: HTTPServer, srv: str): "connection": TPCH_CONN, "schema": TPCH_SF1, "table": "nation", + # Required on TableInfo since hotdata 0.9.0; the API always sends + # them, and an omission fails validation for the whole listing. + "partition_by": [], + "sorted_by": [], "synced": True, "columns": None, }, @@ -799,7 +827,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: @@ -839,7 +874,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, @@ -848,4 +882,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" + ) diff --git a/uv.lock b/uv.lock index 0da7ae2..f10e820 100644 --- a/uv.lock +++ b/uv.lock @@ -127,7 +127,7 @@ wheels = [ [[package]] name = "hotdata" -version = "0.8.0" +version = "0.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, @@ -135,9 +135,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8a/38/30ed3d1d99413e7684672fa424baef85a347db465b90c774443320cf1cea/hotdata-0.8.0.tar.gz", hash = "sha256:cdac515ffa193ed028491e4b7abcd1b6404d4e3f96986c9c0a2b2f407db42eb6", size = 216848, upload-time = "2026-07-20T06:30:37.659Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/db/91d0e8f8a8bfc9a76b588c8dc2ecb054d3e70ebf2ee7a70cd6e4a99a7dec/hotdata-0.9.0.tar.gz", hash = "sha256:35e4a569b7223e025c26b0d3299c8b4cd493276f32826682de6df9e583cea4ef", size = 217368, upload-time = "2026-08-11T14:09:08.166Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/0f/f2ccaeb0f910c3f8cd84a911d934704277f0d6e5a20f8b49cf0eedb2a8aa/hotdata-0.8.0-py3-none-any.whl", hash = "sha256:5d64bfc185a0e7e2bf34ebdcea50787d3418fb250dacc53e00dc74d64e99f753", size = 316923, upload-time = "2026-07-20T06:30:35.778Z" }, + { url = "https://files.pythonhosted.org/packages/ec/48/9b2f5cbbcc92e6dc06a6980fcf4413b83484af302a78263381a22236c308/hotdata-0.9.0-py3-none-any.whl", hash = "sha256:9d942a0d78979552ad641419e5706c9703d5165db0cf3497ae49344cd032e635", size = 316070, upload-time = "2026-08-11T14:09:06.589Z" }, ] [[package]] @@ -165,7 +165,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "hotdata", specifier = ">=0.7,<0.9" }, + { name = "hotdata", specifier = ">=0.9.0,<0.10" }, { name = "ibis-framework", specifier = ">=12,<13" }, { name = "pandas", specifier = ">=2" }, { name = "pyarrow", specifier = ">=16" }, From 38a673255e7c951fcf2d64265cff374f84940ed3 Mon Sep 17 00:00:00 2001 From: Anoop Narang Date: Tue, 11 Aug 2026 22:34:28 +0530 Subject: [PATCH 2/2] docs+test: say what the URL does, and state the fixture reason once The changelog claimed the hotdata:// URL "no longer accepts" session_id. Verified otherwise: _from_url builds its arguments from named lookups and discards whatever else is in the query string, so session_id there is IGNORED, not rejected -- an existing URL still connects, minus the header. Only the keyword argument raises. Now says so, and says the asymmetry is deliberate. The fixture explanation was pasted verbatim above all seven TableInfo dicts. Replaced with a spread constant so the reason is stated once and the next required field is a one-line change rather than seven. Kept as a spread rather than a full helper because the seven literals differ in which other keys they set, and rewriting them risked changing what each fixture means. 109 passed. --- CHANGELOG.md | 14 ++++++++---- tests/test_hotdata_backend.py | 43 ++++++++++++----------------------- 2 files changed, 25 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e48293..7c06d17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed -- **Breaking:** session/sandbox support is gone. `ibis.hotdata.connect()` and the - `hotdata://` URL no longer accept `session_id`, the `--session` example flag and - its `HOTDATA_SESSION_ID` env var are removed, and no `X-Session-Id` header is - sent. +- **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 — diff --git a/tests/test_hotdata_backend.py b/tests/test_hotdata_backend.py index 0d58a26..5865857 100644 --- a/tests/test_hotdata_backend.py +++ b/tests/test_hotdata_backend.py @@ -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: @@ -86,10 +94,7 @@ def information_schema_response( "connection": connection, "schema": schema_name, "table": table_name, - # Required on TableInfo since hotdata 0.9.0; the API always sends - # them, and an omission fails validation for the whole listing. - "partition_by": [], - "sorted_by": [], + **_REQUIRED_TABLE_FIELDS, "synced": True, "last_sync": None, "columns": columns, @@ -654,10 +659,7 @@ def test_information_schema_discovery(httpserver: HTTPServer, srv: str): "connection": TPCH_CONN, "schema": TPCH_SF1, "table": "customer", - # Required on TableInfo since hotdata 0.9.0; the API always sends - # them, and an omission fails validation for the whole listing. - "partition_by": [], - "sorted_by": [], + **_REQUIRED_TABLE_FIELDS, "synced": True, "last_sync": None, "columns": TPCH_CUSTOMER_COLS, @@ -684,10 +686,7 @@ def test_information_schema_pagination_merges_pages(httpserver: HTTPServer, srv: "connection": TPCH_CONN, "schema": TPCH_SF1, "table": "customer", - # Required on TableInfo since hotdata 0.9.0; the API always sends - # them, and an omission fails validation for the whole listing. - "partition_by": [], - "sorted_by": [], + **_REQUIRED_TABLE_FIELDS, "synced": True, "columns": None, }, @@ -697,10 +696,7 @@ def test_information_schema_pagination_merges_pages(httpserver: HTTPServer, srv: "connection": TPCH_CONN, "schema": TPCH_SF1, "table": "lineitem", - # Required on TableInfo since hotdata 0.9.0; the API always sends - # them, and an omission fails validation for the whole listing. - "partition_by": [], - "sorted_by": [], + **_REQUIRED_TABLE_FIELDS, "synced": True, "columns": None, }, @@ -766,10 +762,7 @@ def test_list_tables_regex_like(httpserver: HTTPServer, srv: str): "connection": TPCH_CONN, "schema": TPCH_SF1, "table": "customer", - # Required on TableInfo since hotdata 0.9.0; the API always sends - # them, and an omission fails validation for the whole listing. - "partition_by": [], - "sorted_by": [], + **_REQUIRED_TABLE_FIELDS, "synced": True, "columns": None, }, @@ -777,10 +770,7 @@ def test_list_tables_regex_like(httpserver: HTTPServer, srv: str): "connection": TPCH_CONN, "schema": TPCH_SF1, "table": "lineitem", - # Required on TableInfo since hotdata 0.9.0; the API always sends - # them, and an omission fails validation for the whole listing. - "partition_by": [], - "sorted_by": [], + **_REQUIRED_TABLE_FIELDS, "synced": True, "columns": None, }, @@ -788,10 +778,7 @@ def test_list_tables_regex_like(httpserver: HTTPServer, srv: str): "connection": TPCH_CONN, "schema": TPCH_SF1, "table": "nation", - # Required on TableInfo since hotdata 0.9.0; the API always sends - # them, and an omission fails validation for the whole listing. - "partition_by": [], - "sorted_by": [], + **_REQUIRED_TABLE_FIELDS, "synced": True, "columns": None, },