hotdata-runtime 0.1.0__tar.gz → 0.1.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.
Files changed (21) hide show
  1. {hotdata_runtime-0.1.0 → hotdata_runtime-0.1.1}/CONTRACT.md +17 -0
  2. {hotdata_runtime-0.1.0 → hotdata_runtime-0.1.1}/PKG-INFO +3 -2
  3. {hotdata_runtime-0.1.0 → hotdata_runtime-0.1.1}/README.md +1 -0
  4. {hotdata_runtime-0.1.0 → hotdata_runtime-0.1.1}/hotdata_runtime/__init__.py +18 -0
  5. {hotdata_runtime-0.1.0 → hotdata_runtime-0.1.1}/hotdata_runtime/client.py +151 -0
  6. hotdata_runtime-0.1.1/hotdata_runtime/databases.py +91 -0
  7. {hotdata_runtime-0.1.0 → hotdata_runtime-0.1.1}/pyproject.toml +2 -2
  8. {hotdata_runtime-0.1.0 → hotdata_runtime-0.1.1}/tests/test_contract.py +8 -0
  9. hotdata_runtime-0.1.1/tests/test_databases.py +209 -0
  10. {hotdata_runtime-0.1.0 → hotdata_runtime-0.1.1}/uv.lock +5 -5
  11. {hotdata_runtime-0.1.0 → hotdata_runtime-0.1.1}/.github/workflows/publish.yml +0 -0
  12. {hotdata_runtime-0.1.0 → hotdata_runtime-0.1.1}/.gitignore +0 -0
  13. {hotdata_runtime-0.1.0 → hotdata_runtime-0.1.1}/examples/basic_usage.py +0 -0
  14. {hotdata_runtime-0.1.0 → hotdata_runtime-0.1.1}/hotdata_runtime/env.py +0 -0
  15. {hotdata_runtime-0.1.0 → hotdata_runtime-0.1.1}/hotdata_runtime/health.py +0 -0
  16. {hotdata_runtime-0.1.0 → hotdata_runtime-0.1.1}/hotdata_runtime/http.py +0 -0
  17. {hotdata_runtime-0.1.0 → hotdata_runtime-0.1.1}/hotdata_runtime/result.py +0 -0
  18. {hotdata_runtime-0.1.0 → hotdata_runtime-0.1.1}/tests/test_client.py +0 -0
  19. {hotdata_runtime-0.1.0 → hotdata_runtime-0.1.1}/tests/test_health.py +0 -0
  20. {hotdata_runtime-0.1.0 → hotdata_runtime-0.1.1}/tests/test_result.py +0 -0
  21. {hotdata_runtime-0.1.0 → hotdata_runtime-0.1.1}/tests/test_version.py +0 -0
@@ -30,6 +30,14 @@ The supported import surface is:
30
30
  - `ResultSummary`
31
31
  - `RunHistoryItem`
32
32
  - `WorkspaceSelection`
33
+ - `ManagedDatabase`
34
+ - `ManagedTable`
35
+ - `LoadManagedTableResult`
36
+ - `MANAGED_SOURCE_TYPE`
37
+ - `DEFAULT_SCHEMA`
38
+ - `build_managed_config`
39
+ - `create_connection_request`
40
+ - `is_parquet_path`
33
41
 
34
42
  Adapters should import from `hotdata_runtime` and treat this surface as the stable API.
35
43
 
@@ -49,6 +57,15 @@ Adapters should import from `hotdata_runtime` and treat this surface as the stab
49
57
  - `list_qualified_table_names(...)` returns sorted fully qualified table names.
50
58
  - `columns_for_qualified(qualified, connection_id=...)` resolves table columns, and
51
59
  adapters should pass `connection_id` when known.
60
+ - `uploads()` returns the uploads API wrapper for parquet staging.
61
+ - `list_managed_databases()` returns managed-catalog connections (`source_type: managed`).
62
+ - `resolve_managed_database(name_or_id)` resolves a managed database by name or id.
63
+ - `create_managed_database(name, schema=..., tables=...)` creates a managed database and optionally declares tables up front.
64
+ - `delete_managed_database(name_or_id)` deletes a managed database connection.
65
+ - `list_managed_tables(database, schema=...)` lists tables in a managed database.
66
+ - `upload_parquet(path)` uploads a local parquet file and returns an upload id.
67
+ - `load_managed_table(database, table, schema=..., upload_id=..., file=...)` publishes parquet data into a declared managed table.
68
+ - `delete_managed_table(database, table, schema=...)` deletes a managed table.
52
69
 
53
70
  ### `QueryResult`
54
71
 
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hotdata-runtime
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Workspace/session runtime primitives for Hotdata integrations
5
5
  License: MIT
6
6
  Requires-Python: >=3.10
7
- Requires-Dist: hotdata>=0.1.0
7
+ Requires-Dist: hotdata>=0.2.0
8
8
  Requires-Dist: pandas>=2.0
9
9
  Description-Content-Type: text/markdown
10
10
 
@@ -23,6 +23,7 @@ Runtime boundary and guarantees are defined in `CONTRACT.md`.
23
23
  - **SQL execution helper** — run SQL through `POST /v1/query`, poll async query runs when needed, and return a `QueryResult`.
24
24
  - **Result utilities** — convert query results to records, pandas DataFrames, or metadata dictionaries for adapter display layers.
25
25
  - **History helpers** — list recent results and query run history with normalized dataclasses.
26
+ - **Managed databases** — create Hotdata-owned catalogs, declare tables, upload parquet, and load managed tables (mirrors `hotdata databases` in the CLI).
26
27
  - **Health helpers** — build compact API/workspace health summaries for UI integrations.
27
28
 
28
29
  Install:
@@ -13,6 +13,7 @@ Runtime boundary and guarantees are defined in `CONTRACT.md`.
13
13
  - **SQL execution helper** — run SQL through `POST /v1/query`, poll async query runs when needed, and return a `QueryResult`.
14
14
  - **Result utilities** — convert query results to records, pandas DataFrames, or metadata dictionaries for adapter display layers.
15
15
  - **History helpers** — list recent results and query run history with normalized dataclasses.
16
+ - **Managed databases** — create Hotdata-owned catalogs, declare tables, upload parquet, and load managed tables (mirrors `hotdata databases` in the CLI).
16
17
  - **Health helpers** — build compact API/workspace health summaries for UI integrations.
17
18
 
18
19
  Install:
@@ -8,6 +8,16 @@ from hotdata_runtime.client import (
8
8
  RunHistoryItem,
9
9
  from_env,
10
10
  )
11
+ from hotdata_runtime.databases import (
12
+ DEFAULT_SCHEMA,
13
+ LoadManagedTableResult,
14
+ ManagedDatabase,
15
+ ManagedTable,
16
+ MANAGED_SOURCE_TYPE,
17
+ build_managed_config,
18
+ create_connection_request,
19
+ is_parquet_path,
20
+ )
11
21
  from hotdata_runtime.env import (
12
22
  default_api_key,
13
23
  default_host,
@@ -29,8 +39,16 @@ except PackageNotFoundError:
29
39
 
30
40
  __all__ = [
31
41
  "__version__",
42
+ "DEFAULT_SCHEMA",
32
43
  "HotdataClient",
44
+ "LoadManagedTableResult",
45
+ "MANAGED_SOURCE_TYPE",
46
+ "ManagedDatabase",
47
+ "ManagedTable",
33
48
  "QueryResult",
49
+ "build_managed_config",
50
+ "create_connection_request",
51
+ "is_parquet_path",
34
52
  "workspace_health_lines",
35
53
  "default_api_key",
36
54
  "default_host",
@@ -13,10 +13,12 @@ from hotdata.api.information_schema_api import InformationSchemaApi
13
13
  from hotdata.api.query_api import QueryApi
14
14
  from hotdata.api.query_runs_api import QueryRunsApi
15
15
  from hotdata.api.results_api import ResultsApi
16
+ from hotdata.api.uploads_api import UploadsApi
16
17
  from hotdata.exceptions import ApiException
17
18
  from hotdata.models.async_query_response import AsyncQueryResponse
18
19
  from hotdata.models.query_request import QueryRequest
19
20
  from hotdata.models.query_response import QueryResponse
21
+ from hotdata.models.load_managed_table_request import LoadManagedTableRequest
20
22
  from hotdata.models.table_info import TableInfo
21
23
 
22
24
  from hotdata_runtime.env import (
@@ -26,6 +28,17 @@ from hotdata_runtime.env import (
26
28
  normalize_host,
27
29
  pick_workspace,
28
30
  )
31
+ from hotdata_runtime.databases import (
32
+ DEFAULT_SCHEMA,
33
+ LoadManagedTableResult,
34
+ ManagedDatabase,
35
+ ManagedTable,
36
+ MANAGED_SOURCE_TYPE,
37
+ api_error_message,
38
+ create_connection_request,
39
+ is_parquet_path,
40
+ managed_database_from_connection,
41
+ )
29
42
  from hotdata_runtime.http import default_http_retries
30
43
  from hotdata_runtime.result import QueryResult
31
44
 
@@ -135,6 +148,144 @@ class HotdataClient:
135
148
  def results(self) -> ResultsApi:
136
149
  return self._results_api()
137
150
 
151
+ def uploads(self) -> UploadsApi:
152
+ return UploadsApi(self._api)
153
+
154
+ def list_managed_databases(self) -> list[ManagedDatabase]:
155
+ listing = self.connections().list_connections()
156
+ return [
157
+ managed_database_from_connection(c)
158
+ for c in listing.connections
159
+ if c.source_type == MANAGED_SOURCE_TYPE
160
+ ]
161
+
162
+ def resolve_managed_database(self, name_or_id: str) -> ManagedDatabase:
163
+ listing = self.connections().list_connections()
164
+ match = None
165
+ for c in listing.connections:
166
+ if c.id == name_or_id or c.name == name_or_id:
167
+ match = c
168
+ break
169
+ if match is None:
170
+ raise KeyError(f"No database named or with id {name_or_id!r}")
171
+ if match.source_type != MANAGED_SOURCE_TYPE:
172
+ raise ValueError(
173
+ f"{match.name!r} is not a managed database "
174
+ f"(source_type: {match.source_type})"
175
+ )
176
+ return managed_database_from_connection(match)
177
+
178
+ def create_managed_database(
179
+ self,
180
+ name: str,
181
+ *,
182
+ schema: str = DEFAULT_SCHEMA,
183
+ tables: list[str] | None = None,
184
+ ) -> ManagedDatabase:
185
+ request = create_connection_request(name, schema=schema, tables=tables)
186
+ try:
187
+ created = self.connections().create_connection(request)
188
+ except ApiException as e:
189
+ raise RuntimeError(api_error_message(e)) from e
190
+ return managed_database_from_connection(created)
191
+
192
+ def delete_managed_database(self, name_or_id: str) -> None:
193
+ db = self.resolve_managed_database(name_or_id)
194
+ try:
195
+ self.connections().delete_connection(db.id)
196
+ except ApiException as e:
197
+ raise RuntimeError(api_error_message(e)) from e
198
+
199
+ def list_managed_tables(
200
+ self,
201
+ database: str,
202
+ *,
203
+ schema: str | None = None,
204
+ ) -> list[ManagedTable]:
205
+ db = self.resolve_managed_database(database)
206
+ rows: list[ManagedTable] = []
207
+ for t in self.iter_tables(connection_id=db.id):
208
+ if schema is not None and t.var_schema != schema:
209
+ continue
210
+ rows.append(
211
+ ManagedTable(
212
+ full_name=f"{db.name}.{t.var_schema}.{t.table}",
213
+ schema=t.var_schema,
214
+ table=t.table,
215
+ synced=t.synced,
216
+ last_sync=t.last_sync,
217
+ )
218
+ )
219
+ rows.sort(key=lambda row: (row.schema, row.table))
220
+ return rows
221
+
222
+ def upload_parquet(self, path: str) -> str:
223
+ if not is_parquet_path(path):
224
+ raise ValueError(
225
+ f"Managed table loads require a parquet file (got {path!r})"
226
+ )
227
+ with open(path, "rb") as f:
228
+ data = f.read()
229
+ try:
230
+ uploaded = self.uploads().upload_file(
231
+ data,
232
+ _content_type="application/octet-stream",
233
+ )
234
+ except ApiException as e:
235
+ raise RuntimeError(api_error_message(e)) from e
236
+ return uploaded.id
237
+
238
+ def load_managed_table(
239
+ self,
240
+ database: str,
241
+ table: str,
242
+ *,
243
+ schema: str = DEFAULT_SCHEMA,
244
+ upload_id: str | None = None,
245
+ file: str | None = None,
246
+ ) -> LoadManagedTableResult:
247
+ if (upload_id is None) == (file is None):
248
+ raise ValueError("Exactly one of upload_id or file is required")
249
+ db = self.resolve_managed_database(database)
250
+ if upload_id is not None:
251
+ resolved_upload_id = upload_id
252
+ else:
253
+ assert file is not None
254
+ resolved_upload_id = self.upload_parquet(file)
255
+ request = LoadManagedTableRequest(
256
+ mode="replace",
257
+ upload_id=resolved_upload_id,
258
+ )
259
+ try:
260
+ loaded = self.connections().load_managed_table(
261
+ db.id,
262
+ schema,
263
+ table,
264
+ request,
265
+ )
266
+ except ApiException as e:
267
+ raise RuntimeError(api_error_message(e)) from e
268
+ return LoadManagedTableResult(
269
+ connection_id=loaded.connection_id,
270
+ schema_name=loaded.schema_name,
271
+ table_name=loaded.table_name,
272
+ row_count=loaded.row_count,
273
+ full_name=f"{db.name}.{loaded.schema_name}.{loaded.table_name}",
274
+ )
275
+
276
+ def delete_managed_table(
277
+ self,
278
+ database: str,
279
+ table: str,
280
+ *,
281
+ schema: str = DEFAULT_SCHEMA,
282
+ ) -> None:
283
+ db = self.resolve_managed_database(database)
284
+ try:
285
+ self.connections().delete_managed_table(db.id, schema, table)
286
+ except ApiException as e:
287
+ raise RuntimeError(api_error_message(e)) from e
288
+
138
289
  def list_recent_results(
139
290
  self,
140
291
  *,
@@ -0,0 +1,91 @@
1
+ """Managed database helpers (Hotdata-owned catalogs with parquet table loads)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from hotdata.exceptions import ApiException
10
+ from hotdata.models.create_connection_request import CreateConnectionRequest
11
+
12
+ MANAGED_SOURCE_TYPE = "managed"
13
+ DEFAULT_SCHEMA = "public"
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class ManagedDatabase:
18
+ id: str
19
+ name: str
20
+ source_type: str
21
+
22
+ def to_dict(self) -> dict[str, Any]:
23
+ return asdict(self)
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class ManagedTable:
28
+ full_name: str
29
+ schema: str
30
+ table: str
31
+ synced: bool
32
+ last_sync: str | None
33
+
34
+ def to_dict(self) -> dict[str, Any]:
35
+ return asdict(self)
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class LoadManagedTableResult:
40
+ connection_id: str
41
+ schema_name: str
42
+ table_name: str
43
+ row_count: int
44
+ full_name: str
45
+
46
+ def to_dict(self) -> dict[str, Any]:
47
+ return asdict(self)
48
+
49
+
50
+ def is_parquet_path(path: str) -> bool:
51
+ return Path(path).suffix.lower() == ".parquet"
52
+
53
+
54
+ def build_managed_config(schema: str, tables: list[str]) -> dict[str, Any]:
55
+ if not tables:
56
+ return {}
57
+ return {
58
+ "schemas": [
59
+ {
60
+ "name": schema,
61
+ "tables": [{"name": table} for table in tables],
62
+ }
63
+ ]
64
+ }
65
+
66
+
67
+ def create_connection_request(
68
+ name: str,
69
+ *,
70
+ schema: str = DEFAULT_SCHEMA,
71
+ tables: list[str] | None = None,
72
+ ) -> CreateConnectionRequest:
73
+ table_list = tables or []
74
+ return CreateConnectionRequest(
75
+ name=name,
76
+ source_type=MANAGED_SOURCE_TYPE,
77
+ config=build_managed_config(schema, table_list),
78
+ skip_discovery=True,
79
+ )
80
+
81
+
82
+ def managed_database_from_connection(conn: Any) -> ManagedDatabase:
83
+ return ManagedDatabase(
84
+ id=str(conn.id),
85
+ name=str(conn.name),
86
+ source_type=str(conn.source_type),
87
+ )
88
+
89
+
90
+ def api_error_message(exc: ApiException) -> str:
91
+ return exc.reason or str(exc)
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hotdata-runtime"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "Workspace/session runtime primitives for Hotdata integrations"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = { text = "MIT" }
12
12
  dependencies = [
13
- "hotdata>=0.1.0",
13
+ "hotdata>=0.2.0",
14
14
  "pandas>=2.0",
15
15
  ]
16
16
 
@@ -11,8 +11,16 @@ from hotdata_runtime.result import QueryResult
11
11
  def test_public_exports_contract():
12
12
  assert hr.__all__ == [
13
13
  "__version__",
14
+ "DEFAULT_SCHEMA",
14
15
  "HotdataClient",
16
+ "LoadManagedTableResult",
17
+ "MANAGED_SOURCE_TYPE",
18
+ "ManagedDatabase",
19
+ "ManagedTable",
15
20
  "QueryResult",
21
+ "build_managed_config",
22
+ "create_connection_request",
23
+ "is_parquet_path",
16
24
  "workspace_health_lines",
17
25
  "default_api_key",
18
26
  "default_host",
@@ -0,0 +1,209 @@
1
+ from __future__ import annotations
2
+
3
+ from types import SimpleNamespace
4
+ from unittest.mock import mock_open, patch
5
+
6
+ import pytest
7
+
8
+ from hotdata.exceptions import ApiException
9
+ from hotdata_runtime.client import HotdataClient
10
+ from hotdata_runtime.databases import (
11
+ build_managed_config,
12
+ create_connection_request,
13
+ is_parquet_path,
14
+ )
15
+
16
+
17
+ def _client() -> HotdataClient:
18
+ return HotdataClient("k", "ws", host="https://api.hotdata.dev")
19
+
20
+
21
+ def test_build_managed_config_empty_without_tables():
22
+ assert build_managed_config("public", []) == {}
23
+
24
+
25
+ def test_build_managed_config_declares_tables():
26
+ cfg = build_managed_config("public", ["orders", "customers"])
27
+ assert cfg == {
28
+ "schemas": [
29
+ {
30
+ "name": "public",
31
+ "tables": [{"name": "orders"}, {"name": "customers"}],
32
+ }
33
+ ]
34
+ }
35
+
36
+
37
+ def test_create_connection_request_uses_managed_source_type():
38
+ req = create_connection_request("sales", schema="public", tables=["orders"])
39
+ assert req.name == "sales"
40
+ assert req.source_type == "managed"
41
+ assert req.skip_discovery is True
42
+ assert req.config["schemas"][0]["tables"][0]["name"] == "orders"
43
+
44
+
45
+ @pytest.mark.parametrize(
46
+ ("path", "expected"),
47
+ [
48
+ ("/data/orders.parquet", True),
49
+ ("/data/ORDERS.PARQUET", True),
50
+ ("/data/orders.csv", False),
51
+ ],
52
+ )
53
+ def test_is_parquet_path(path: str, expected: bool):
54
+ assert is_parquet_path(path) is expected
55
+
56
+
57
+ def test_list_managed_databases_filters_managed_only():
58
+ client = _client()
59
+ listing = SimpleNamespace(
60
+ connections=[
61
+ SimpleNamespace(id="c1", name="sales", source_type="managed"),
62
+ SimpleNamespace(id="c2", name="warehouse", source_type="postgres"),
63
+ ]
64
+ )
65
+ with patch.object(client, "connections") as connections:
66
+ connections.return_value.list_connections.return_value = listing
67
+ dbs = client.list_managed_databases()
68
+ assert [db.name for db in dbs] == ["sales"]
69
+
70
+
71
+ def test_resolve_managed_database_by_name_and_id():
72
+ client = _client()
73
+ listing = SimpleNamespace(
74
+ connections=[
75
+ SimpleNamespace(id="conn_abc", name="sales", source_type="managed"),
76
+ ]
77
+ )
78
+ with patch.object(client, "connections") as connections:
79
+ connections.return_value.list_connections.return_value = listing
80
+ by_name = client.resolve_managed_database("sales")
81
+ by_id = client.resolve_managed_database("conn_abc")
82
+ assert by_name.id == "conn_abc"
83
+ assert by_id.name == "sales"
84
+
85
+
86
+ def test_resolve_managed_database_rejects_non_managed():
87
+ client = _client()
88
+ listing = SimpleNamespace(
89
+ connections=[
90
+ SimpleNamespace(id="c1", name="warehouse", source_type="postgres"),
91
+ ]
92
+ )
93
+ with patch.object(client, "connections") as connections:
94
+ connections.return_value.list_connections.return_value = listing
95
+ with pytest.raises(ValueError, match="not a managed database"):
96
+ client.resolve_managed_database("warehouse")
97
+
98
+
99
+ def test_create_managed_database_returns_summary():
100
+ client = _client()
101
+ created = SimpleNamespace(id="conn_new", name="mydb", source_type="managed")
102
+ with patch.object(client, "connections") as connections:
103
+ connections.return_value.create_connection.return_value = created
104
+ db = client.create_managed_database("mydb", tables=["orders"])
105
+ assert db.id == "conn_new"
106
+ assert db.name == "mydb"
107
+ req = connections.return_value.create_connection.call_args.args[0]
108
+ assert req.config["schemas"][0]["tables"][0]["name"] == "orders"
109
+
110
+
111
+ def test_create_managed_database_wraps_api_errors():
112
+ client = _client()
113
+ with patch.object(client, "connections") as connections:
114
+ connections.return_value.create_connection.side_effect = ApiException(
115
+ status=400,
116
+ reason="bad request",
117
+ )
118
+ with pytest.raises(RuntimeError, match="bad request"):
119
+ client.create_managed_database("mydb")
120
+
121
+
122
+ def test_list_managed_tables_builds_full_names():
123
+ client = _client()
124
+ listing = SimpleNamespace(
125
+ connections=[
126
+ SimpleNamespace(id="conn1", name="sales", source_type="managed"),
127
+ ]
128
+ )
129
+ table = SimpleNamespace(
130
+ connection="sales",
131
+ var_schema="public",
132
+ table="orders",
133
+ synced=True,
134
+ last_sync="2026-05-19T00:00:00Z",
135
+ )
136
+ with patch.object(client, "connections") as connections, patch.object(
137
+ client, "iter_tables", return_value=[table]
138
+ ):
139
+ connections.return_value.list_connections.return_value = listing
140
+ rows = client.list_managed_tables("sales")
141
+ assert len(rows) == 1
142
+ assert rows[0].full_name == "sales.public.orders"
143
+ assert rows[0].synced is True
144
+
145
+
146
+ def test_upload_parquet_rejects_non_parquet():
147
+ client = _client()
148
+ with pytest.raises(ValueError, match="parquet"):
149
+ client.upload_parquet("/tmp/data.csv")
150
+
151
+
152
+ def test_upload_parquet_returns_upload_id():
153
+ client = _client()
154
+ uploaded = SimpleNamespace(id="upl_123")
155
+ with patch("builtins.open", mock_open(read_data=b"PAR1")), patch.object(
156
+ client, "uploads"
157
+ ) as uploads:
158
+ uploads.return_value.upload_file.return_value = uploaded
159
+ upload_id = client.upload_parquet("/tmp/data.parquet")
160
+ assert upload_id == "upl_123"
161
+
162
+
163
+ def test_load_managed_table_with_upload_id():
164
+ client = _client()
165
+ db = SimpleNamespace(id="conn1", name="sales", source_type="managed")
166
+ loaded = SimpleNamespace(
167
+ connection_id="conn1",
168
+ schema_name="public",
169
+ table_name="orders",
170
+ row_count=42,
171
+ )
172
+ with patch.object(client, "resolve_managed_database", return_value=db), patch.object(
173
+ client, "connections"
174
+ ) as connections:
175
+ connections.return_value.load_managed_table.return_value = loaded
176
+ result = client.load_managed_table(
177
+ "sales",
178
+ "orders",
179
+ upload_id="upl_123",
180
+ )
181
+ assert result.row_count == 42
182
+ assert result.full_name == "sales.public.orders"
183
+
184
+
185
+ def test_load_managed_table_requires_exactly_one_source():
186
+ client = _client()
187
+ with pytest.raises(ValueError, match="Exactly one"):
188
+ client.load_managed_table("sales", "orders")
189
+ with pytest.raises(ValueError, match="Exactly one"):
190
+ client.load_managed_table(
191
+ "sales",
192
+ "orders",
193
+ upload_id="upl_1",
194
+ file="/tmp/x.parquet",
195
+ )
196
+
197
+
198
+ def test_delete_managed_table_calls_sdk():
199
+ client = _client()
200
+ db = SimpleNamespace(id="conn1", name="sales", source_type="managed")
201
+ with patch.object(client, "resolve_managed_database", return_value=db), patch.object(
202
+ client, "connections"
203
+ ) as connections:
204
+ client.delete_managed_table("sales", "orders")
205
+ connections.return_value.delete_managed_table.assert_called_once_with(
206
+ "conn1",
207
+ "public",
208
+ "orders",
209
+ )
@@ -43,7 +43,7 @@ wheels = [
43
43
 
44
44
  [[package]]
45
45
  name = "hotdata"
46
- version = "0.1.0"
46
+ version = "0.2.0"
47
47
  source = { registry = "https://pypi.org/simple" }
48
48
  dependencies = [
49
49
  { name = "pydantic" },
@@ -51,14 +51,14 @@ dependencies = [
51
51
  { name = "typing-extensions" },
52
52
  { name = "urllib3" },
53
53
  ]
54
- sdist = { url = "https://files.pythonhosted.org/packages/63/a2/7e997581dc23fca35330c355cd433135c4d18cc5506fb77fb35fd0180e97/hotdata-0.1.0.tar.gz", hash = "sha256:6795ff7381fb8f2f258ee3f0c31f9b1ba2f5908728c51fa399840fdf603acc46", size = 97691, upload-time = "2026-04-25T17:57:00.102Z" }
54
+ sdist = { url = "https://files.pythonhosted.org/packages/ce/0f/1e9e024aa13f8d4bf8f9fb1bce777da6ca19da05b8435f2ba5cd5f87ec80/hotdata-0.2.0.tar.gz", hash = "sha256:e1131c05ed34d2f39ddee84930eb6694ed46971d7a442df5932689b28a6c9b4f", size = 108780, upload-time = "2026-05-19T04:01:38.345Z" }
55
55
  wheels = [
56
- { url = "https://files.pythonhosted.org/packages/3f/21/e04ca377e7e3db50215bf207867ef02a56af11f61022390b7689e6ff2db3/hotdata-0.1.0-py3-none-any.whl", hash = "sha256:304f46d7c7ed5b586a9102684ef42e45972955dfb66a492c5e0b016e8bc545fa", size = 242376, upload-time = "2026-04-25T17:56:58.126Z" },
56
+ { url = "https://files.pythonhosted.org/packages/9a/e7/63b4820963ec475fe16403d363e5ddec237cfe01a39c2d7aff6a6d48d720/hotdata-0.2.0-py3-none-any.whl", hash = "sha256:d3d644a3b607f4891a784b8d5afa30a00bd9e437db013fd0581bf8bca501ac0d", size = 256603, upload-time = "2026-05-19T04:01:36.253Z" },
57
57
  ]
58
58
 
59
59
  [[package]]
60
60
  name = "hotdata-runtime"
61
- version = "0.1.0"
61
+ version = "0.1.1"
62
62
  source = { editable = "." }
63
63
  dependencies = [
64
64
  { name = "hotdata" },
@@ -74,7 +74,7 @@ dev = [
74
74
 
75
75
  [package.metadata]
76
76
  requires-dist = [
77
- { name = "hotdata", specifier = ">=0.1.0" },
77
+ { name = "hotdata", specifier = ">=0.2.0" },
78
78
  { name = "pandas", specifier = ">=2.0" },
79
79
  ]
80
80