Skip to content

Commit 73d8158

Browse files
authored
fix(managed): adopt hotdata 0.6.0 native x_database_id result params (#36)
The 0.6.0 SDK (bumped in #34) added x_database_id to get_result, get_query_run, and get_result_arrow — and made it REQUIRED on get_result_arrow, so the header-based scope from #35 would raise TypeError at the Arrow fetch under the new SDK (the mocked tests could not see a generated-signature change). - _wait_result_ready / _await_query_run / _fetch_result_arrow pass x_database_id natively; the scoped default-header workaround is gone. - hotdata pin raised to >=0.6.0 (older framework code cannot run on 0.6.0 anyway, per the required param). - Test fakes mirror the 0.6.0 signatures, with x_database_id required on the Arrow fake so dropping the scope fails the suite again. Verified live against api.hotdata.dev: the state-table append path (fetch -> concat -> load) completes on SDK 0.6.0. Co-authored-by: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com>
1 parent 6c68988 commit 73d8158

5 files changed

Lines changed: 35 additions & 50 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- `ManagedDatabaseClient.fetch_table` now carries the `X-Database-Id` scope header on the result poll, the query-run poll, and the Arrow fetch — not only on the query submit. Results of database-scoped queries are themselves database-scoped, so every read against an existing synced table (merge/append loads, dlt state restore) failed with `400: Bad Request` once the table had data (dlthubworker#70).
1313
- API error messages now include the response body (flattened, truncated to 500 chars). `400: Bad Request` alone hid the server's actual explanation.
1414

15+
### Changed
16+
17+
- The `hotdata` SDK dependency is now `>=0.6.0`, and the scope above rides its native `x_database_id` parameters (`get_result`, `get_query_run`, `get_result_arrow`). Note 0.6.0 made `x_database_id` **required** on `get_result_arrow`, so older framework releases cannot run on it.
18+
1519
## [0.6.0] - 2026-06-30
1620

1721
### Added

hotdata_framework/managed_client.py

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -118,17 +118,13 @@ def _fetch_result_arrow(self, result_id: str, *, database_id: str) -> pa.Table:
118118
"""Fetch a ready result as Arrow, carrying the database scope.
119119
120120
Results of database-scoped queries are themselves database-scoped —
121-
the results endpoints reject requests without an ``X-Database-Id``
122-
header. The Arrow helper takes no per-call headers, so the header is
123-
set as a client default for the duration of the call and removed
124-
after (this client is not shared across threads).
121+
the results endpoints reject requests without the scope. The hotdata
122+
0.6.0 SDK exposes (and requires) ``x_database_id`` on the Arrow
123+
helper directly.
125124
"""
126-
api = self._runtime.api
127-
api.set_default_header("X-Database-Id", database_id)
128-
try:
129-
return ArrowResultsApi(api).get_result_arrow(result_id)
130-
finally:
131-
api.default_headers.pop("X-Database-Id", None)
125+
return ArrowResultsApi(self._runtime.api).get_result_arrow(
126+
result_id, x_database_id=database_id
127+
)
132128

133129
def _poll(
134130
self,
@@ -172,9 +168,7 @@ def _await_query_run(self, query_run_id: str, *, database_id: str) -> str | None
172168
runs = QueryRunsApi(self._runtime.api)
173169
run = self._poll(
174170
# Runs (like results) of database-scoped queries are database-scoped.
175-
lambda: runs.get_query_run(
176-
query_run_id, _headers={"X-Database-Id": database_id}
177-
),
171+
lambda: runs.get_query_run(query_run_id, x_database_id=database_id),
178172
is_ready=lambda r: r.status == "succeeded",
179173
describe="Query",
180174
)
@@ -186,10 +180,8 @@ def _wait_result_ready(self, result_id: str | None, *, database_id: str) -> str
186180
results = ResultsApi(self._runtime.api)
187181
self._poll(
188182
# The stored result of a database-scoped query 400s without the
189-
# database scope header.
190-
lambda: results.get_result(
191-
result_id, _headers={"X-Database-Id": database_id}
192-
),
183+
# database scope.
184+
lambda: results.get_result(result_id, x_database_id=database_id),
193185
is_ready=lambda r: r.status == "ready",
194186
describe=f"Result {result_id}",
195187
)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ classifiers = [
2626
"Typing :: Typed",
2727
]
2828
dependencies = [
29-
"hotdata>=0.5.0",
29+
"hotdata>=0.6.0",
3030
"pandas>=2.0",
3131
"pyarrow>=14.0",
3232
]

tests/test_managed_client.py

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class FakeArrowResultsApi:
5959
def __init__(self, api: object) -> None:
6060
pass
6161

62-
def get_result_arrow(self, result_id: str) -> pa.Table:
62+
def get_result_arrow(self, result_id: str, **kwargs: Any) -> pa.Table:
6363
calls.append("arrow")
6464
return pa.table({"id": [1, 2]})
6565

@@ -87,19 +87,9 @@ def get_result_arrow(self, result_id: str) -> pa.Table:
8787
assert calls.index("arrow") > calls.index("get_result:ready")
8888

8989

90-
class _FakeApiClient:
91-
"""Just enough of the generated ApiClient's default-header surface."""
92-
93-
def __init__(self) -> None:
94-
self.default_headers: dict[str, str] = {}
95-
96-
def set_default_header(self, name: str, value: str) -> None:
97-
self.default_headers[name] = value
98-
99-
100-
def _fake_runtime(api: object | None = None) -> SimpleNamespace:
90+
def _fake_runtime() -> SimpleNamespace:
10191
return SimpleNamespace(
102-
api=api if api is not None else _FakeApiClient(),
92+
api=object(),
10393
resolve_managed_database=lambda name: SimpleNamespace(id="db1", default_connection_id="c"),
10494
list_managed_tables=lambda database, schema=None: [
10595
SimpleNamespace(table="orders", synced=True)
@@ -112,16 +102,16 @@ def test_fetch_table_carries_database_scope_on_result_reads(
112102
) -> None:
113103
"""Results (and runs) of a database-scoped query are database-scoped:
114104
the results endpoints 400 with "X-Database-Id header is required" when
115-
the header is missing. ``fetch_table`` must carry the database id on the
116-
result poll and the Arrow fetch, not only on the query submit.
105+
the scope is missing. ``fetch_table`` must carry the database id on the
106+
result poll and the Arrow fetch, not only on the query submit — the
107+
hotdata 0.6.0 SDK exposes ``x_database_id`` on all three.
117108
118109
Regression: reruns/append loads against an existing synced table failed
119110
with an opaque ``400: Bad Request`` (dlthubworker#70) because both reads
120-
omitted the header.
111+
omitted the scope.
121112
"""
122-
seen_headers: list[dict[str, Any] | None] = []
123-
arrow_headers: list[str | None] = []
124-
fake_api = _FakeApiClient()
113+
result_scopes: list[str | None] = []
114+
arrow_scopes: list[str | None] = []
125115

126116
class FakeQueryApi:
127117
def __init__(self, api: object) -> None:
@@ -135,17 +125,18 @@ class FakeResultsApi:
135125
def __init__(self, api: object) -> None:
136126
pass
137127

138-
def get_result(self, result_id: str, *, _headers: dict[str, Any] | None = None) -> Any:
139-
seen_headers.append(_headers)
128+
def get_result(self, result_id: str, *, x_database_id: str | None = None) -> Any:
129+
result_scopes.append(x_database_id)
140130
return SimpleNamespace(status="ready", result_id=result_id, error_message=None)
141131

142132
class FakeArrowResultsApi:
143-
def __init__(self, api: _FakeApiClient) -> None:
144-
self._api = api
133+
def __init__(self, api: object) -> None:
134+
pass
145135

146-
def get_result_arrow(self, result_id: str) -> pa.Table:
147-
# The scoped default header must be present DURING the fetch.
148-
arrow_headers.append(self._api.default_headers.get("X-Database-Id"))
136+
# x_database_id is REQUIRED in the 0.6.0 SDK — mirroring that here
137+
# makes this test fail if a caller ever drops the scope again.
138+
def get_result_arrow(self, result_id: str, *, x_database_id: str) -> pa.Table:
139+
arrow_scopes.append(x_database_id)
149140
return pa.table({"id": [1]})
150141

151142
monkeypatch.setattr(mc, "QueryApi", FakeQueryApi)
@@ -159,12 +150,10 @@ def get_result_arrow(self, result_id: str) -> pa.Table:
159150
max_retries=1,
160151
retry_backoff_seconds=0.0,
161152
)
162-
client._runtime = _fake_runtime(fake_api)
153+
client._runtime = _fake_runtime()
163154

164155
table = client.fetch_table(database="mydb", schema="public", table="orders")
165156

166157
assert table is not None
167-
assert seen_headers == [{"X-Database-Id": "db1"}]
168-
assert arrow_headers == ["db1"]
169-
# The scoped header is removed once the fetch completes.
170-
assert "X-Database-Id" not in fake_api.default_headers
158+
assert result_scopes == ["db1"]
159+
assert arrow_scopes == ["db1"]

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)