hotdata-runtime 0.1.1__tar.gz → 0.2.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 (33) hide show
  1. hotdata_runtime-0.2.1/.github/workflows/check-release.yml +26 -0
  2. hotdata_runtime-0.2.1/.github/workflows/ci.yml +33 -0
  3. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/.github/workflows/publish.yml +0 -1
  4. hotdata_runtime-0.2.1/.github/workflows/release.yml +54 -0
  5. hotdata_runtime-0.2.1/CHANGELOG.md +46 -0
  6. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/CONTRACT.md +4 -7
  7. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/PKG-INFO +2 -2
  8. hotdata_runtime-0.2.1/RELEASING.md +43 -0
  9. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/hotdata_runtime/__init__.py +0 -6
  10. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/hotdata_runtime/client.py +77 -36
  11. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/hotdata_runtime/databases.py +6 -36
  12. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/pyproject.toml +2 -2
  13. hotdata_runtime-0.2.1/scripts/check-release.py +62 -0
  14. hotdata_runtime-0.2.1/scripts/extract-changelog.py +36 -0
  15. hotdata_runtime-0.2.1/scripts/publish-workflow.sh +75 -0
  16. hotdata_runtime-0.2.1/scripts/release.sh +187 -0
  17. hotdata_runtime-0.2.1/scripts/update_changelog.py +71 -0
  18. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/tests/test_client.py +61 -0
  19. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/tests/test_contract.py +0 -3
  20. hotdata_runtime-0.2.1/tests/test_databases.py +242 -0
  21. hotdata_runtime-0.2.1/tests/test_update_changelog.py +48 -0
  22. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/uv.lock +5 -5
  23. hotdata_runtime-0.1.1/tests/test_databases.py +0 -209
  24. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/.gitignore +0 -0
  25. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/README.md +0 -0
  26. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/examples/basic_usage.py +0 -0
  27. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/hotdata_runtime/env.py +0 -0
  28. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/hotdata_runtime/health.py +0 -0
  29. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/hotdata_runtime/http.py +0 -0
  30. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/hotdata_runtime/result.py +0 -0
  31. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/tests/test_health.py +0 -0
  32. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/tests/test_result.py +0 -0
  33. {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.1}/tests/test_version.py +0 -0
@@ -0,0 +1,26 @@
1
+ name: Check release metadata
2
+
3
+ on:
4
+ pull_request:
5
+ paths:
6
+ - 'pyproject.toml'
7
+ - 'CHANGELOG.md'
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ check:
14
+ name: Verify changelog matches version bump
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
18
+ with:
19
+ fetch-depth: 0
20
+
21
+ - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
22
+ with:
23
+ python-version: '3.12'
24
+
25
+ - name: Check release metadata
26
+ run: python scripts/check-release.py
@@ -0,0 +1,33 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["main", "master"]
6
+ pull_request:
7
+
8
+ concurrency:
9
+ group: ci-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ test:
17
+ name: Test (Python 3.12)
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
21
+
22
+ - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v6
23
+ with:
24
+ enable-cache: true
25
+
26
+ - name: Set up Python
27
+ run: uv python install 3.12
28
+
29
+ - name: Install dependencies
30
+ run: uv sync --locked
31
+
32
+ - name: Test
33
+ run: uv run pytest -v
@@ -28,7 +28,6 @@ jobs:
28
28
 
29
29
  - name: Verify tag matches pyproject version
30
30
  run: |
31
- # Release tags must start with `v` followed by a PEP 440 version (e.g. v1.2.3, v1.2.3a1).
32
31
  if [[ ! "$GITHUB_REF_NAME" =~ ^v[0-9] ]]; then
33
32
  echo "Release tag '$GITHUB_REF_NAME' must start with 'v' followed by a digit (e.g. v1.0.0)" >&2
34
33
  exit 1
@@ -0,0 +1,54 @@
1
+ name: GitHub Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v[0-9]*'
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ release:
13
+ name: Create GitHub Release
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
17
+
18
+ - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
19
+ with:
20
+ python-version: '3.12'
21
+
22
+ - name: Read package metadata
23
+ id: meta
24
+ run: |
25
+ pkg_name=$(python -c "import tomllib,pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['name'])")
26
+ pkg_version="${GITHUB_REF_NAME#v}"
27
+ echo "name=${pkg_name}" >> "$GITHUB_OUTPUT"
28
+ echo "version=${pkg_version}" >> "$GITHUB_OUTPUT"
29
+
30
+ - name: Extract changelog notes
31
+ id: notes
32
+ run: |
33
+ set -euo pipefail
34
+ version="${GITHUB_REF_NAME#v}"
35
+ if [[ -f CHANGELOG.md ]]; then
36
+ body="$(python scripts/extract-changelog.py "$version")"
37
+ else
38
+ body="Release ${version}."
39
+ fi
40
+ delimiter="EOF_${RANDOM}_${RANDOM}"
41
+ {
42
+ echo "body<<${delimiter}"
43
+ echo "$body"
44
+ echo "${delimiter}"
45
+ } >> "$GITHUB_OUTPUT"
46
+
47
+ - name: Create GitHub Release
48
+ uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2
49
+ with:
50
+ tag_name: ${{ github.ref_name }}
51
+ name: ${{ steps.meta.outputs.name }} ${{ steps.meta.outputs.version }}
52
+ body: ${{ steps.notes.outputs.body }}
53
+ generate_release_notes: false
54
+ make_latest: true
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+
11
+ ## [0.2.1] - 2026-05-24
12
+
13
+ ### Added
14
+
15
+ - `execute_sql` accepts an optional `database` keyword argument. When provided, the database name is resolved to an ID and sent as the `X-Database-Id` header so SQL can reference managed database tables as `"default"."<schema>"."<table>"`. Behaviour is unchanged when `database` is omitted.
16
+
17
+ ## [0.2.0] - 2026-05-24
18
+
19
+ ### Changed
20
+
21
+ - Switch managed database operations from the connections API to the dedicated `/databases` API (`hotdata>=0.2.3` required).
22
+ - `create_managed_database` first parameter renamed from `name` to `description` (keyword-only).
23
+ - `ManagedDatabase` dataclass: replace `name`/`source_type` fields with `description`/`default_connection_id`.
24
+ - `resolve_managed_database` tries direct ID lookup first, then falls back to a description scan.
25
+ - `list_managed_databases` now fetches all databases regardless of source type.
26
+ - `list_managed_tables`, `load_managed_table`, and `delete_managed_table` use `default_connection_id` instead of database `id` for connection-scoped operations.
27
+
28
+ ### Added
29
+
30
+ - `create_managed_database` accepts an optional `expires_at` parameter.
31
+
32
+ ### Removed
33
+
34
+ - `MANAGED_SOURCE_TYPE`, `build_managed_config`, and `create_connection_request` removed from the public API.
35
+
36
+ ## [0.1.1] - 2026-05-19
37
+
38
+ ### Added
39
+
40
+ - Managed database helpers on `HotdataClient`.
41
+
42
+ ## [0.1.0] - 2026-05-06
43
+
44
+ ### Added
45
+
46
+ - Initial release.
@@ -33,10 +33,7 @@ The supported import surface is:
33
33
  - `ManagedDatabase`
34
34
  - `ManagedTable`
35
35
  - `LoadManagedTableResult`
36
- - `MANAGED_SOURCE_TYPE`
37
36
  - `DEFAULT_SCHEMA`
38
- - `build_managed_config`
39
- - `create_connection_request`
40
37
  - `is_parquet_path`
41
38
 
42
39
  Adapters should import from `hotdata_runtime` and treat this surface as the stable API.
@@ -58,10 +55,10 @@ Adapters should import from `hotdata_runtime` and treat this surface as the stab
58
55
  - `columns_for_qualified(qualified, connection_id=...)` resolves table columns, and
59
56
  adapters should pass `connection_id` when known.
60
57
  - `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.
58
+ - `list_managed_databases()` returns all databases via the `/databases` API.
59
+ - `resolve_managed_database(name_or_id)` resolves a database by id (direct lookup) or description (list scan).
60
+ - `create_managed_database(description=..., schema=..., tables=..., expires_at=...)` creates a database via the `/databases` API and optionally declares tables up front.
61
+ - `delete_managed_database(name_or_id)` deletes a database via the `/databases` API.
65
62
  - `list_managed_tables(database, schema=...)` lists tables in a managed database.
66
63
  - `upload_parquet(path)` uploads a local parquet file and returns an upload id.
67
64
  - `load_managed_table(database, table, schema=..., upload_id=..., file=...)` publishes parquet data into a declared managed table.
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hotdata-runtime
3
- Version: 0.1.1
3
+ Version: 0.2.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.2.0
7
+ Requires-Dist: hotdata>=0.2.3
8
8
  Requires-Dist: pandas>=2.0
9
9
  Description-Content-Type: text/markdown
10
10
 
@@ -0,0 +1,43 @@
1
+ # Releasing
2
+
3
+ Every release uses `./scripts/release.sh`. Do not bump versions, tag, or create GitHub Releases manually.
4
+
5
+ ## One-time setup
6
+
7
+ - Install [GitHub CLI](https://cli.github.com/) (`gh`) and authenticate.
8
+ - Ensure PyPI [trusted publishing](https://docs.pypi.org/trusted-publishers/) is configured for this repo (`publish.yml` uses the `pypi` GitHub environment).
9
+
10
+ ## Release steps
11
+
12
+ 1. Add user-facing notes under `## [Unreleased]` in `CHANGELOG.md`.
13
+ 2. Prepare the release PR:
14
+
15
+ ```bash
16
+ ./scripts/release.sh prepare patch # or minor | major | 1.2.3
17
+ ```
18
+
19
+ 3. Merge the PR after CI passes (including the changelog check).
20
+ 4. Publish from a clean default branch checkout:
21
+
22
+ ```bash
23
+ git checkout main # or master for hotdata-marimo
24
+ git pull
25
+ ./scripts/release.sh publish
26
+ ```
27
+
28
+ ## What happens automatically
29
+
30
+ Pushing a `vX.Y.Z` tag triggers two workflows:
31
+
32
+ | Workflow | Purpose |
33
+ |----------|---------|
34
+ | `publish.yml` | Build wheel/sdist and publish to PyPI |
35
+ | `release.yml` | Create the GitHub Release with notes from `CHANGELOG.md` |
36
+
37
+ ## Enforcement
38
+
39
+ - **PR check** (`check-release.yml`): if `pyproject.toml` version changes, `CHANGELOG.md` must contain a matching `## [X.Y.Z]` section.
40
+ - **Tag check** (`publish.yml`): the tag (without `v`) must match `[project].version` in `pyproject.toml`.
41
+ - **Publish guard** (`release.sh publish`): refuses to tag if the changelog section is missing.
42
+
43
+ Together, these make it hard to ship a version without changelog notes or a GitHub Release.
@@ -13,9 +13,6 @@ from hotdata_runtime.databases import (
13
13
  LoadManagedTableResult,
14
14
  ManagedDatabase,
15
15
  ManagedTable,
16
- MANAGED_SOURCE_TYPE,
17
- build_managed_config,
18
- create_connection_request,
19
16
  is_parquet_path,
20
17
  )
21
18
  from hotdata_runtime.env import (
@@ -42,12 +39,9 @@ __all__ = [
42
39
  "DEFAULT_SCHEMA",
43
40
  "HotdataClient",
44
41
  "LoadManagedTableResult",
45
- "MANAGED_SOURCE_TYPE",
46
42
  "ManagedDatabase",
47
43
  "ManagedTable",
48
44
  "QueryResult",
49
- "build_managed_config",
50
- "create_connection_request",
51
45
  "is_parquet_path",
52
46
  "workspace_health_lines",
53
47
  "default_api_key",
@@ -9,6 +9,7 @@ from urllib3.exceptions import ProtocolError
9
9
 
10
10
  from hotdata import ApiClient, Configuration
11
11
  from hotdata.api.connections_api import ConnectionsApi
12
+ from hotdata.api.databases_api import DatabasesApi
12
13
  from hotdata.api.information_schema_api import InformationSchemaApi
13
14
  from hotdata.api.query_api import QueryApi
14
15
  from hotdata.api.query_runs_api import QueryRunsApi
@@ -16,9 +17,12 @@ from hotdata.api.results_api import ResultsApi
16
17
  from hotdata.api.uploads_api import UploadsApi
17
18
  from hotdata.exceptions import ApiException
18
19
  from hotdata.models.async_query_response import AsyncQueryResponse
20
+ from hotdata.models.create_database_request import CreateDatabaseRequest
21
+ from hotdata.models.database_default_schema_decl import DatabaseDefaultSchemaDecl
22
+ from hotdata.models.database_default_table_decl import DatabaseDefaultTableDecl
23
+ from hotdata.models.load_managed_table_request import LoadManagedTableRequest
19
24
  from hotdata.models.query_request import QueryRequest
20
25
  from hotdata.models.query_response import QueryResponse
21
- from hotdata.models.load_managed_table_request import LoadManagedTableRequest
22
26
  from hotdata.models.table_info import TableInfo
23
27
 
24
28
  from hotdata_runtime.env import (
@@ -33,11 +37,9 @@ from hotdata_runtime.databases import (
33
37
  LoadManagedTableResult,
34
38
  ManagedDatabase,
35
39
  ManagedTable,
36
- MANAGED_SOURCE_TYPE,
37
40
  api_error_message,
38
- create_connection_request,
39
41
  is_parquet_path,
40
- managed_database_from_connection,
42
+ managed_database_from_detail,
41
43
  )
42
44
  from hotdata_runtime.http import default_http_retries
43
45
  from hotdata_runtime.result import QueryResult
@@ -130,6 +132,9 @@ class HotdataClient:
130
132
  def connections(self) -> ConnectionsApi:
131
133
  return ConnectionsApi(self._api)
132
134
 
135
+ def _databases_api(self) -> DatabasesApi:
136
+ return DatabasesApi(self._api)
137
+
133
138
  def _information_schema(self) -> InformationSchemaApi:
134
139
  return InformationSchemaApi(self._api)
135
140
 
@@ -152,47 +157,71 @@ class HotdataClient:
152
157
  return UploadsApi(self._api)
153
158
 
154
159
  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
- ]
160
+ listing = self._databases_api().list_databases()
161
+ result: list[ManagedDatabase] = []
162
+ for summary in listing.databases:
163
+ try:
164
+ detail = self._databases_api().get_database(summary.id)
165
+ result.append(managed_database_from_detail(detail))
166
+ except ApiException:
167
+ pass
168
+ return result
161
169
 
162
170
  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
171
+ # Try direct ID lookup first
172
+ try:
173
+ detail = self._databases_api().get_database(name_or_id)
174
+ return managed_database_from_detail(detail)
175
+ except ApiException as e:
176
+ if e.status != 404:
177
+ raise RuntimeError(api_error_message(e)) from e
178
+
179
+ # Fall back to description-based lookup
180
+ listing = self._databases_api().list_databases()
181
+ match_id: str | None = None
182
+ for db in listing.databases:
183
+ if db.description == name_or_id:
184
+ match_id = db.id
168
185
  break
169
- if match is None:
186
+ if match_id is None:
170
187
  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)
188
+ try:
189
+ detail = self._databases_api().get_database(match_id)
190
+ except ApiException as e:
191
+ raise RuntimeError(api_error_message(e)) from e
192
+ return managed_database_from_detail(detail)
177
193
 
178
194
  def create_managed_database(
179
195
  self,
180
- name: str,
196
+ description: str | None = None,
181
197
  *,
182
198
  schema: str = DEFAULT_SCHEMA,
183
199
  tables: list[str] | None = None,
200
+ expires_at: str | None = None,
184
201
  ) -> ManagedDatabase:
185
- request = create_connection_request(name, schema=schema, tables=tables)
202
+ schemas = None
203
+ if tables:
204
+ schemas = [
205
+ DatabaseDefaultSchemaDecl(
206
+ name=schema,
207
+ tables=[DatabaseDefaultTableDecl(name=t) for t in tables],
208
+ )
209
+ ]
210
+ request = CreateDatabaseRequest(
211
+ description=description,
212
+ schemas=schemas,
213
+ expires_at=expires_at,
214
+ )
186
215
  try:
187
- created = self.connections().create_connection(request)
216
+ created = self._databases_api().create_database(request)
188
217
  except ApiException as e:
189
218
  raise RuntimeError(api_error_message(e)) from e
190
- return managed_database_from_connection(created)
219
+ return managed_database_from_detail(created)
191
220
 
192
221
  def delete_managed_database(self, name_or_id: str) -> None:
193
222
  db = self.resolve_managed_database(name_or_id)
194
223
  try:
195
- self.connections().delete_connection(db.id)
224
+ self._databases_api().delete_database(db.id)
196
225
  except ApiException as e:
197
226
  raise RuntimeError(api_error_message(e)) from e
198
227
 
@@ -204,12 +233,12 @@ class HotdataClient:
204
233
  ) -> list[ManagedTable]:
205
234
  db = self.resolve_managed_database(database)
206
235
  rows: list[ManagedTable] = []
207
- for t in self.iter_tables(connection_id=db.id):
236
+ for t in self.iter_tables(connection_id=db.default_connection_id):
208
237
  if schema is not None and t.var_schema != schema:
209
238
  continue
210
239
  rows.append(
211
240
  ManagedTable(
212
- full_name=f"{db.name}.{t.var_schema}.{t.table}",
241
+ full_name=f"{db.id}.{t.var_schema}.{t.table}",
213
242
  schema=t.var_schema,
214
243
  table=t.table,
215
244
  synced=t.synced,
@@ -258,7 +287,7 @@ class HotdataClient:
258
287
  )
259
288
  try:
260
289
  loaded = self.connections().load_managed_table(
261
- db.id,
290
+ db.default_connection_id,
262
291
  schema,
263
292
  table,
264
293
  request,
@@ -270,7 +299,7 @@ class HotdataClient:
270
299
  schema_name=loaded.schema_name,
271
300
  table_name=loaded.table_name,
272
301
  row_count=loaded.row_count,
273
- full_name=f"{db.name}.{loaded.schema_name}.{loaded.table_name}",
302
+ full_name=f"{db.id}.{loaded.schema_name}.{loaded.table_name}",
274
303
  )
275
304
 
276
305
  def delete_managed_table(
@@ -282,7 +311,7 @@ class HotdataClient:
282
311
  ) -> None:
283
312
  db = self.resolve_managed_database(database)
284
313
  try:
285
- self.connections().delete_managed_table(db.id, schema, table)
314
+ self.connections().delete_managed_table(db.default_connection_id, schema, table)
286
315
  except ApiException as e:
287
316
  raise RuntimeError(api_error_message(e)) from e
288
317
 
@@ -446,11 +475,20 @@ class HotdataClient:
446
475
  f"(last status: {getattr(last, 'status', None)})"
447
476
  )
448
477
 
449
- def execute_sql(self, sql: str) -> QueryResult:
478
+ def execute_sql(self, sql: str, *, database: str | None = None) -> QueryResult:
479
+ """Execute SQL and return a :class:`QueryResult`.
480
+
481
+ Pass ``database`` to scope the query to a managed database. The name
482
+ is resolved to a database ID once before the retry loop, and the
483
+ ``X-Database-Id`` header is sent with every attempt. Inside a managed
484
+ database the built-in catalog is always ``"default"``, so table
485
+ references should use ``"default"."<schema>"."<table>"``.
486
+ """
487
+ database_id = self.resolve_managed_database(database).id if database else None
450
488
  last_err: BaseException | None = None
451
489
  for attempt in range(3):
452
490
  try:
453
- return self._execute_sql_once(sql)
491
+ return self._execute_sql_once(sql, database_id=database_id)
454
492
  except (ProtocolError, ConnectionResetError, Urllib3HTTPError) as e:
455
493
  last_err = e
456
494
  if attempt == 2:
@@ -458,10 +496,13 @@ class HotdataClient:
458
496
  time.sleep(0.2 * (2**attempt))
459
497
  raise last_err # pragma: no cover
460
498
 
461
- def _execute_sql_once(self, sql: str) -> QueryResult:
499
+ def _execute_sql_once(self, sql: str, *, database_id: str | None = None) -> QueryResult:
462
500
  q = self._query_api()
463
501
  try:
464
- raw = q.query(QueryRequest(sql=sql))
502
+ if database_id:
503
+ raw = q.query(QueryRequest(sql=sql), x_database_id=database_id)
504
+ else:
505
+ raw = q.query(QueryRequest(sql=sql))
465
506
  except ApiException as e:
466
507
  raise RuntimeError(e.reason or str(e)) from e
467
508
 
@@ -7,17 +7,15 @@ from pathlib import Path
7
7
  from typing import Any
8
8
 
9
9
  from hotdata.exceptions import ApiException
10
- from hotdata.models.create_connection_request import CreateConnectionRequest
11
10
 
12
- MANAGED_SOURCE_TYPE = "managed"
13
11
  DEFAULT_SCHEMA = "public"
14
12
 
15
13
 
16
14
  @dataclass(frozen=True)
17
15
  class ManagedDatabase:
18
16
  id: str
19
- name: str
20
- source_type: str
17
+ description: str | None
18
+ default_connection_id: str
21
19
 
22
20
  def to_dict(self) -> dict[str, Any]:
23
21
  return asdict(self)
@@ -51,39 +49,11 @@ def is_parquet_path(path: str) -> bool:
51
49
  return Path(path).suffix.lower() == ".parquet"
52
50
 
53
51
 
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:
52
+ def managed_database_from_detail(detail: Any) -> ManagedDatabase:
83
53
  return ManagedDatabase(
84
- id=str(conn.id),
85
- name=str(conn.name),
86
- source_type=str(conn.source_type),
54
+ id=str(detail.id),
55
+ description=detail.description,
56
+ default_connection_id=str(detail.default_connection_id),
87
57
  )
88
58
 
89
59
 
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hotdata-runtime"
7
- version = "0.1.1"
7
+ version = "0.2.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.2.0",
13
+ "hotdata>=0.2.3",
14
14
  "pandas>=2.0",
15
15
  ]
16
16
 
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env python3
2
+ """Fail CI when pyproject.toml version changes without a matching CHANGELOG entry."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import re
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+ def git_show(path: str, ref: str) -> str:
13
+ try:
14
+ return subprocess.check_output(["git", "show", f"{ref}:{path}"], text=True)
15
+ except subprocess.CalledProcessError:
16
+ return ""
17
+
18
+
19
+ def read_version(text: str) -> str:
20
+ match = re.search(r'(?m)^version = "([^"]+)"', text)
21
+ if not match:
22
+ raise SystemExit("could not read version from pyproject.toml")
23
+ return match.group(1)
24
+
25
+
26
+ def has_changelog_section(version: str) -> bool:
27
+ changelog = Path("CHANGELOG.md")
28
+ if not changelog.exists():
29
+ return False
30
+ return bool(re.search(rf"^## \[{re.escape(version)}\]", changelog.read_text(), re.M))
31
+
32
+
33
+ def main() -> None:
34
+ base = "origin/main"
35
+ for candidate in ("origin/main", "origin/master"):
36
+ if subprocess.call(["git", "rev-parse", "--verify", candidate], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0:
37
+ base = candidate
38
+ break
39
+
40
+ current = Path("pyproject.toml").read_text()
41
+ previous = git_show("pyproject.toml", base)
42
+ if not previous:
43
+ print("skip: no base pyproject.toml to compare")
44
+ return
45
+
46
+ old_version = read_version(previous)
47
+ new_version = read_version(current)
48
+ if old_version == new_version:
49
+ print(f"version unchanged ({new_version})")
50
+ return
51
+
52
+ if not has_changelog_section(new_version):
53
+ raise SystemExit(
54
+ f"pyproject.toml version bumped to {new_version} but CHANGELOG.md "
55
+ f"has no '## [{new_version}]' section"
56
+ )
57
+
58
+ print(f"release metadata ok for {new_version}")
59
+
60
+
61
+ if __name__ == "__main__":
62
+ main()
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env python3
2
+ """Print the Keep a Changelog section for a release version."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+
10
+
11
+ def extract(changelog: str, version: str) -> str:
12
+ pattern = rf"^## \[{re.escape(version)}\].*$"
13
+ match = re.search(pattern, changelog, re.M)
14
+ if not match:
15
+ raise SystemExit(f"no changelog section for {version}")
16
+
17
+ start = match.start()
18
+ rest = changelog[match.end() :]
19
+ next_heading = re.search(r"^## \[", rest, re.M)
20
+ end = match.end() + (next_heading.start() if next_heading else len(rest))
21
+ section = changelog[start:end].strip()
22
+ title, _, body = section.partition("\n")
23
+ return body.strip() or f"Release {version}."
24
+
25
+
26
+ def main() -> None:
27
+ if len(sys.argv) != 2:
28
+ raise SystemExit("usage: extract-changelog.py VERSION")
29
+
30
+ version = sys.argv[1]
31
+ changelog = Path("CHANGELOG.md").read_text()
32
+ print(extract(changelog, version))
33
+
34
+
35
+ if __name__ == "__main__":
36
+ main()