diff --git a/CHANGELOG.md b/CHANGELOG.md index cb08dbc..7c06d17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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..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,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, @@ -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, @@ -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, }, @@ -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, }, @@ -750,6 +762,7 @@ 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, }, @@ -757,6 +770,7 @@ def test_list_tables_regex_like(httpserver: HTTPServer, srv: str): "connection": TPCH_CONN, "schema": TPCH_SF1, "table": "lineitem", + **_REQUIRED_TABLE_FIELDS, "synced": True, "columns": None, }, @@ -764,6 +778,7 @@ def test_list_tables_regex_like(httpserver: HTTPServer, srv: str): "connection": TPCH_CONN, "schema": TPCH_SF1, "table": "nation", + **_REQUIRED_TABLE_FIELDS, "synced": True, "columns": None, }, @@ -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: @@ -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, @@ -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" + ) 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" },