hotdata-framework 0.5.0__tar.gz → 0.6.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/CHANGELOG.md +17 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/PKG-INFO +2 -2
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/hotdata_framework/client.py +28 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/hotdata_framework/databases.py +6 -1
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/hotdata_framework/errors.py +5 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/hotdata_framework/managed_client.py +23 -7
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/pyproject.toml +2 -2
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/tests/test_client.py +29 -0
- hotdata_framework-0.6.1/tests/test_errors.py +49 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/tests/test_managed_client.py +74 -8
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/uv.lock +5 -5
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/.github/dependabot.yml +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/.github/workflows/check-release.yml +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/.github/workflows/ci.yml +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/.github/workflows/dependabot-automerge.yml +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/.github/workflows/publish.yml +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/.github/workflows/release.yml +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/.gitignore +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/CONTRACT.md +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/README.md +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/RELEASING.md +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/examples/basic_usage.py +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/hotdata_framework/__init__.py +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/hotdata_framework/env.py +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/hotdata_framework/health.py +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/hotdata_framework/http.py +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/hotdata_framework/py.typed +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/hotdata_framework/result.py +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/scripts/check-release.py +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/scripts/extract-changelog.py +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/scripts/publish-workflow.sh +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/scripts/release.sh +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/scripts/update_changelog.py +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/tests/test_contract.py +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/tests/test_databases.py +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/tests/test_health.py +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/tests/test_result.py +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/tests/test_update_changelog.py +0 -0
- {hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/tests/test_version.py +0 -0
|
@@ -8,6 +8,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
## [0.6.1] - 2026-07-08
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- `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).
|
|
16
|
+
- API error messages now include the response body (flattened, truncated to 500 chars). `400: Bad Request` alone hid the server's actual explanation.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- 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.
|
|
21
|
+
|
|
22
|
+
## [0.6.0] - 2026-06-30
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- `HotdataClient.add_managed_table(database, table, *, schema)` declares a new table on an existing managed database (wrapping the SDK `add_database_table` endpoint). This allows additive schema evolution without recreating the database.
|
|
27
|
+
|
|
11
28
|
## [0.5.0] - 2026-06-28
|
|
12
29
|
|
|
13
30
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hotdata-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: Python framework for building Hotdata integrations: workspace/session runtime, query execution, and managed databases
|
|
5
5
|
Project-URL: Homepage, https://www.hotdata.dev
|
|
6
6
|
Project-URL: Documentation, https://www.hotdata.dev/docs
|
|
@@ -21,7 +21,7 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Requires-Python: >=3.10
|
|
24
|
-
Requires-Dist: hotdata>=0.
|
|
24
|
+
Requires-Dist: hotdata>=0.6.0
|
|
25
25
|
Requires-Dist: pandas>=2.0
|
|
26
26
|
Requires-Dist: pyarrow>=14.0
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
@@ -14,6 +14,7 @@ from hotdata.api.query_runs_api import QueryRunsApi
|
|
|
14
14
|
from hotdata.api.results_api import ResultsApi
|
|
15
15
|
from hotdata.api.uploads_api import UploadsApi
|
|
16
16
|
from hotdata.exceptions import ApiException
|
|
17
|
+
from hotdata.models.add_managed_table_request import AddManagedTableRequest
|
|
17
18
|
from hotdata.models.async_query_response import AsyncQueryResponse
|
|
18
19
|
from hotdata.models.create_database_request import CreateDatabaseRequest
|
|
19
20
|
from hotdata.models.database_default_schema_decl import DatabaseDefaultSchemaDecl
|
|
@@ -300,6 +301,33 @@ class HotdataClient:
|
|
|
300
301
|
full_name=f"{db.id}.{loaded.schema_name}.{loaded.table_name}",
|
|
301
302
|
)
|
|
302
303
|
|
|
304
|
+
def add_managed_table(
|
|
305
|
+
self,
|
|
306
|
+
database: str,
|
|
307
|
+
table: str,
|
|
308
|
+
*,
|
|
309
|
+
schema: str = DEFAULT_SCHEMA,
|
|
310
|
+
) -> ManagedTable:
|
|
311
|
+
"""Declare a new table on an existing managed database.
|
|
312
|
+
|
|
313
|
+
The table is added empty (declared-but-unloaded); populate it with
|
|
314
|
+
:meth:`load_managed_table`. Use this to evolve a managed database's
|
|
315
|
+
schema after creation without recreating it.
|
|
316
|
+
"""
|
|
317
|
+
db = self.resolve_managed_database(database)
|
|
318
|
+
request = AddManagedTableRequest(name=table)
|
|
319
|
+
try:
|
|
320
|
+
self._databases_api().add_database_table(db.id, schema, request)
|
|
321
|
+
except ApiException as e:
|
|
322
|
+
raise RuntimeError(api_error_message(e)) from e
|
|
323
|
+
return ManagedTable(
|
|
324
|
+
full_name=f"{db.id}.{schema}.{table}",
|
|
325
|
+
schema=schema,
|
|
326
|
+
table=table,
|
|
327
|
+
synced=False,
|
|
328
|
+
last_sync=None,
|
|
329
|
+
)
|
|
330
|
+
|
|
303
331
|
def delete_managed_table(
|
|
304
332
|
self,
|
|
305
333
|
database: str,
|
|
@@ -58,4 +58,9 @@ def managed_database_from_detail(detail: Any) -> ManagedDatabase:
|
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
def api_error_message(exc: ApiException) -> str:
|
|
61
|
-
|
|
61
|
+
reason = exc.reason or str(exc)
|
|
62
|
+
# Keep the response body: it carries the API's actual explanation.
|
|
63
|
+
body = getattr(exc, "body", None)
|
|
64
|
+
if body:
|
|
65
|
+
return f"{reason}: {' '.join(str(body).split())[:500]}"
|
|
66
|
+
return reason
|
|
@@ -23,6 +23,11 @@ def classify_sdk_error(error: Exception) -> HotdataError:
|
|
|
23
23
|
if isinstance(error, ApiException):
|
|
24
24
|
status_code = int(error.status or 0)
|
|
25
25
|
message = f"{status_code}: {error.reason or 'unknown error'}"
|
|
26
|
+
# The response body is where the API explains itself (e.g. which
|
|
27
|
+
# header is missing) — without it "400: Bad Request" is undebuggable.
|
|
28
|
+
body = getattr(error, "body", None)
|
|
29
|
+
if body:
|
|
30
|
+
message = f"{message} — {' '.join(str(body).split())[:500]}"
|
|
26
31
|
if status_code in (408, 409, 425, 429):
|
|
27
32
|
return HotdataTransientError(message)
|
|
28
33
|
if 500 <= status_code <= 599:
|
|
@@ -110,10 +110,22 @@ class ManagedDatabaseClient:
|
|
|
110
110
|
result_id = self._query_database_scoped(sql, database_id=db.id)
|
|
111
111
|
if result_id is None:
|
|
112
112
|
return None
|
|
113
|
-
return
|
|
113
|
+
return self._fetch_result_arrow(result_id, database_id=db.id)
|
|
114
114
|
|
|
115
115
|
return self._request_with_retry(operation)
|
|
116
116
|
|
|
117
|
+
def _fetch_result_arrow(self, result_id: str, *, database_id: str) -> pa.Table:
|
|
118
|
+
"""Fetch a ready result as Arrow, carrying the database scope.
|
|
119
|
+
|
|
120
|
+
Results of database-scoped queries are themselves database-scoped —
|
|
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.
|
|
124
|
+
"""
|
|
125
|
+
return ArrowResultsApi(self._runtime.api).get_result_arrow(
|
|
126
|
+
result_id, x_database_id=database_id
|
|
127
|
+
)
|
|
128
|
+
|
|
117
129
|
def _poll(
|
|
118
130
|
self,
|
|
119
131
|
fetch: Callable[[], S],
|
|
@@ -146,26 +158,30 @@ class ManagedDatabaseClient:
|
|
|
146
158
|
# under ``result_id``; that result may be ``processing`` when the
|
|
147
159
|
# inline preview returns, so wait for ``ready`` before the caller
|
|
148
160
|
# fetches it as Arrow.
|
|
149
|
-
return self._wait_result_ready(raw.result_id)
|
|
161
|
+
return self._wait_result_ready(raw.result_id, database_id=database_id)
|
|
150
162
|
if isinstance(raw, AsyncQueryResponse):
|
|
151
|
-
|
|
163
|
+
run_result = self._await_query_run(raw.query_run_id, database_id=database_id)
|
|
164
|
+
return self._wait_result_ready(run_result, database_id=database_id)
|
|
152
165
|
return None
|
|
153
166
|
|
|
154
|
-
def _await_query_run(self, query_run_id: str) -> str | None:
|
|
167
|
+
def _await_query_run(self, query_run_id: str, *, database_id: str) -> str | None:
|
|
155
168
|
runs = QueryRunsApi(self._runtime.api)
|
|
156
169
|
run = self._poll(
|
|
157
|
-
|
|
170
|
+
# Runs (like results) of database-scoped queries are database-scoped.
|
|
171
|
+
lambda: runs.get_query_run(query_run_id, x_database_id=database_id),
|
|
158
172
|
is_ready=lambda r: r.status == "succeeded",
|
|
159
173
|
describe="Query",
|
|
160
174
|
)
|
|
161
175
|
return run.result_id
|
|
162
176
|
|
|
163
|
-
def _wait_result_ready(self, result_id: str | None) -> str | None:
|
|
177
|
+
def _wait_result_ready(self, result_id: str | None, *, database_id: str) -> str | None:
|
|
164
178
|
if result_id is None:
|
|
165
179
|
return None
|
|
166
180
|
results = ResultsApi(self._runtime.api)
|
|
167
181
|
self._poll(
|
|
168
|
-
|
|
182
|
+
# The stored result of a database-scoped query 400s without the
|
|
183
|
+
# database scope.
|
|
184
|
+
lambda: results.get_result(result_id, x_database_id=database_id),
|
|
169
185
|
is_ready=lambda r: r.status == "ready",
|
|
170
186
|
describe=f"Result {result_id}",
|
|
171
187
|
)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hotdata-framework"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.1"
|
|
8
8
|
description = "Python framework for building Hotdata integrations: workspace/session runtime, query execution, and managed databases"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -26,7 +26,7 @@ classifiers = [
|
|
|
26
26
|
"Typing :: Typed",
|
|
27
27
|
]
|
|
28
28
|
dependencies = [
|
|
29
|
-
"hotdata>=0.
|
|
29
|
+
"hotdata>=0.6.0",
|
|
30
30
|
"pandas>=2.0",
|
|
31
31
|
"pyarrow>=14.0",
|
|
32
32
|
]
|
|
@@ -178,6 +178,35 @@ def test_columns_for_qualified_prefers_explicit_connection_id():
|
|
|
178
178
|
assert fake_api.kwargs["connection_id"] == "conn_explicit"
|
|
179
179
|
|
|
180
180
|
|
|
181
|
+
def test_add_managed_table_declares_table_on_existing_database():
|
|
182
|
+
client = HotdataClient("k", "ws", host="https://api.hotdata.dev")
|
|
183
|
+
fake_db = SimpleNamespace(id="db_1", default_connection_id="conn")
|
|
184
|
+
|
|
185
|
+
class FakeDatabasesApi:
|
|
186
|
+
def __init__(self):
|
|
187
|
+
self.calls: list[tuple[str, str, str]] = []
|
|
188
|
+
|
|
189
|
+
def add_database_table(self, database_id, var_schema, request):
|
|
190
|
+
self.calls.append((database_id, var_schema, request.name))
|
|
191
|
+
return SimpleNamespace(
|
|
192
|
+
connection_id="conn", var_schema=var_schema, table=request.name
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
fake_api = FakeDatabasesApi()
|
|
196
|
+
with (
|
|
197
|
+
patch.object(client, "resolve_managed_database", return_value=fake_db),
|
|
198
|
+
patch.object(client, "_databases_api", return_value=fake_api),
|
|
199
|
+
):
|
|
200
|
+
result = client.add_managed_table("mydb", "orders", schema="public")
|
|
201
|
+
|
|
202
|
+
assert fake_api.calls == [("db_1", "public", "orders")]
|
|
203
|
+
assert result.full_name == "db_1.public.orders"
|
|
204
|
+
assert result.schema == "public"
|
|
205
|
+
assert result.table == "orders"
|
|
206
|
+
assert result.synced is False
|
|
207
|
+
assert result.last_sync is None
|
|
208
|
+
|
|
209
|
+
|
|
181
210
|
def test_list_recent_results_returns_normalized_summaries():
|
|
182
211
|
client = HotdataClient("k", "ws", host="https://api.hotdata.dev")
|
|
183
212
|
listing = SimpleNamespace(
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Error message construction: the API's response body must survive.
|
|
2
|
+
|
|
3
|
+
"400: Bad Request" alone is undebuggable; the body carries the server's
|
|
4
|
+
actual explanation (e.g. which header was missing). Regression for the
|
|
5
|
+
opaque load failures in dlthubworker#70.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from hotdata.rest import ApiException
|
|
11
|
+
|
|
12
|
+
from hotdata_framework.databases import api_error_message
|
|
13
|
+
from hotdata_framework.errors import (
|
|
14
|
+
HotdataTerminalError,
|
|
15
|
+
HotdataTransientError,
|
|
16
|
+
classify_sdk_error,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
BODY = '{"error":{"code":"BAD_REQUEST","message":"X-Database-Id header is required"}}'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_classify_sdk_error_includes_response_body() -> None:
|
|
23
|
+
err = classify_sdk_error(ApiException(status=400, reason="Bad Request", body=BODY))
|
|
24
|
+
assert isinstance(err, HotdataTerminalError)
|
|
25
|
+
assert "400: Bad Request" in str(err)
|
|
26
|
+
assert "X-Database-Id header is required" in str(err)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_classify_sdk_error_without_body_keeps_short_form() -> None:
|
|
30
|
+
err = classify_sdk_error(ApiException(status=409, reason="Conflict"))
|
|
31
|
+
assert isinstance(err, HotdataTransientError)
|
|
32
|
+
assert str(err) == "409: Conflict"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_classify_sdk_error_truncates_and_flattens_body() -> None:
|
|
36
|
+
noisy = "x\n" * 1000
|
|
37
|
+
err = classify_sdk_error(ApiException(status=500, reason="ISE", body=noisy))
|
|
38
|
+
assert "\n" not in str(err)
|
|
39
|
+
assert len(str(err)) < 600
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_api_error_message_includes_body() -> None:
|
|
43
|
+
msg = api_error_message(ApiException(status=400, reason="Bad Request", body=BODY))
|
|
44
|
+
assert msg.startswith("Bad Request: ")
|
|
45
|
+
assert "X-Database-Id header is required" in msg
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_api_error_message_without_body() -> None:
|
|
49
|
+
assert api_error_message(ApiException(status=404, reason="Not Found")) == "Not Found"
|
|
@@ -50,7 +50,7 @@ def test_fetch_table_waits_for_ready_before_arrow(monkeypatch: pytest.MonkeyPatc
|
|
|
50
50
|
def __init__(self, api: object) -> None:
|
|
51
51
|
pass
|
|
52
52
|
|
|
53
|
-
def get_result(self, result_id: str) -> Any:
|
|
53
|
+
def get_result(self, result_id: str, **kwargs: Any) -> Any:
|
|
54
54
|
status = next(statuses)
|
|
55
55
|
calls.append(f"get_result:{status}")
|
|
56
56
|
return SimpleNamespace(status=status, result_id=result_id, error_message=None)
|
|
@@ -59,7 +59,7 @@ def test_fetch_table_waits_for_ready_before_arrow(monkeypatch: pytest.MonkeyPatc
|
|
|
59
59
|
def __init__(self, api: object) -> None:
|
|
60
60
|
pass
|
|
61
61
|
|
|
62
|
-
def get_result_arrow(self, result_id: str) -> pa.Table:
|
|
62
|
+
def get_result_arrow(self, result_id: str, **kwargs: Any) -> pa.Table:
|
|
63
63
|
calls.append("arrow")
|
|
64
64
|
return pa.table({"id": [1, 2]})
|
|
65
65
|
|
|
@@ -75,7 +75,20 @@ def test_fetch_table_waits_for_ready_before_arrow(monkeypatch: pytest.MonkeyPatc
|
|
|
75
75
|
max_retries=1,
|
|
76
76
|
retry_backoff_seconds=0.0,
|
|
77
77
|
)
|
|
78
|
-
client._runtime =
|
|
78
|
+
client._runtime = _fake_runtime()
|
|
79
|
+
|
|
80
|
+
table = client.fetch_table(database="mydb", schema="public", table="orders")
|
|
81
|
+
|
|
82
|
+
assert table is not None
|
|
83
|
+
assert table.num_rows == 2
|
|
84
|
+
# The result was polled to readiness, and Arrow was fetched only afterwards.
|
|
85
|
+
assert "get_result:processing" in calls
|
|
86
|
+
assert "get_result:ready" in calls
|
|
87
|
+
assert calls.index("arrow") > calls.index("get_result:ready")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _fake_runtime() -> SimpleNamespace:
|
|
91
|
+
return SimpleNamespace(
|
|
79
92
|
api=object(),
|
|
80
93
|
resolve_managed_database=lambda name: SimpleNamespace(id="db1", default_connection_id="c"),
|
|
81
94
|
list_managed_tables=lambda database, schema=None: [
|
|
@@ -83,11 +96,64 @@ def test_fetch_table_waits_for_ready_before_arrow(monkeypatch: pytest.MonkeyPatc
|
|
|
83
96
|
],
|
|
84
97
|
)
|
|
85
98
|
|
|
99
|
+
|
|
100
|
+
def test_fetch_table_carries_database_scope_on_result_reads(
|
|
101
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Results (and runs) of a database-scoped query are database-scoped:
|
|
104
|
+
the results endpoints 400 with "X-Database-Id header is required" when
|
|
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.
|
|
108
|
+
|
|
109
|
+
Regression: reruns/append loads against an existing synced table failed
|
|
110
|
+
with an opaque ``400: Bad Request`` (dlthubworker#70) because both reads
|
|
111
|
+
omitted the scope.
|
|
112
|
+
"""
|
|
113
|
+
result_scopes: list[str | None] = []
|
|
114
|
+
arrow_scopes: list[str | None] = []
|
|
115
|
+
|
|
116
|
+
class FakeQueryApi:
|
|
117
|
+
def __init__(self, api: object) -> None:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
def query(self, request: object, *, x_database_id: str) -> QueryResponse:
|
|
121
|
+
assert x_database_id == "db1"
|
|
122
|
+
return _query_response("rslt1")
|
|
123
|
+
|
|
124
|
+
class FakeResultsApi:
|
|
125
|
+
def __init__(self, api: object) -> None:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
def get_result(self, result_id: str, *, x_database_id: str | None = None) -> Any:
|
|
129
|
+
result_scopes.append(x_database_id)
|
|
130
|
+
return SimpleNamespace(status="ready", result_id=result_id, error_message=None)
|
|
131
|
+
|
|
132
|
+
class FakeArrowResultsApi:
|
|
133
|
+
def __init__(self, api: object) -> None:
|
|
134
|
+
pass
|
|
135
|
+
|
|
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)
|
|
140
|
+
return pa.table({"id": [1]})
|
|
141
|
+
|
|
142
|
+
monkeypatch.setattr(mc, "QueryApi", FakeQueryApi)
|
|
143
|
+
monkeypatch.setattr(mc, "ResultsApi", FakeResultsApi)
|
|
144
|
+
monkeypatch.setattr(mc, "ArrowResultsApi", FakeArrowResultsApi)
|
|
145
|
+
|
|
146
|
+
client = mc.ManagedDatabaseClient(
|
|
147
|
+
api_key="k",
|
|
148
|
+
workspace_id="w",
|
|
149
|
+
api_base_url="https://example.test",
|
|
150
|
+
max_retries=1,
|
|
151
|
+
retry_backoff_seconds=0.0,
|
|
152
|
+
)
|
|
153
|
+
client._runtime = _fake_runtime()
|
|
154
|
+
|
|
86
155
|
table = client.fetch_table(database="mydb", schema="public", table="orders")
|
|
87
156
|
|
|
88
157
|
assert table is not None
|
|
89
|
-
assert
|
|
90
|
-
|
|
91
|
-
assert "get_result:processing" in calls
|
|
92
|
-
assert "get_result:ready" in calls
|
|
93
|
-
assert calls.index("arrow") > calls.index("get_result:ready")
|
|
158
|
+
assert result_scopes == ["db1"]
|
|
159
|
+
assert arrow_scopes == ["db1"]
|
|
@@ -86,7 +86,7 @@ wheels = [
|
|
|
86
86
|
|
|
87
87
|
[[package]]
|
|
88
88
|
name = "hotdata"
|
|
89
|
-
version = "0.
|
|
89
|
+
version = "0.6.0"
|
|
90
90
|
source = { registry = "https://pypi.org/simple" }
|
|
91
91
|
dependencies = [
|
|
92
92
|
{ name = "pydantic" },
|
|
@@ -94,14 +94,14 @@ dependencies = [
|
|
|
94
94
|
{ name = "typing-extensions" },
|
|
95
95
|
{ name = "urllib3" },
|
|
96
96
|
]
|
|
97
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
97
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d2/17/6d9e7cbe7e766cd52968653096ce4199d0cb1aa55b73fcfc2979fb9805ff/hotdata-0.6.0.tar.gz", hash = "sha256:c655fd9691a1d65dd4ed51b86f1c27bf8e73268b4c44696987a57a624da35143", size = 214162, upload-time = "2026-07-07T16:44:32.65Z" }
|
|
98
98
|
wheels = [
|
|
99
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
99
|
+
{ url = "https://files.pythonhosted.org/packages/dc/0a/4c9f23e1fa85a8d0336681f608c13100e4ab2cf4204846d90374b1bb141b/hotdata-0.6.0-py3-none-any.whl", hash = "sha256:3324758eb0aa37bbcc505ddac524566a8c10fbe0787e226932e8b04d565200c1", size = 312820, upload-time = "2026-07-07T16:44:31.121Z" },
|
|
100
100
|
]
|
|
101
101
|
|
|
102
102
|
[[package]]
|
|
103
103
|
name = "hotdata-framework"
|
|
104
|
-
version = "0.
|
|
104
|
+
version = "0.6.1"
|
|
105
105
|
source = { editable = "." }
|
|
106
106
|
dependencies = [
|
|
107
107
|
{ name = "hotdata" },
|
|
@@ -120,7 +120,7 @@ dev = [
|
|
|
120
120
|
|
|
121
121
|
[package.metadata]
|
|
122
122
|
requires-dist = [
|
|
123
|
-
{ name = "hotdata", specifier = ">=0.
|
|
123
|
+
{ name = "hotdata", specifier = ">=0.6.0" },
|
|
124
124
|
{ name = "pandas", specifier = ">=2.0" },
|
|
125
125
|
{ name = "pyarrow", specifier = ">=14.0" },
|
|
126
126
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hotdata_framework-0.5.0 → hotdata_framework-0.6.1}/.github/workflows/dependabot-automerge.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|