hotdata-runtime 0.1.0__tar.gz → 0.2.0__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_runtime-0.2.0/.github/workflows/check-release.yml +26 -0
- hotdata_runtime-0.2.0/.github/workflows/ci.yml +33 -0
- {hotdata_runtime-0.1.0 → hotdata_runtime-0.2.0}/.github/workflows/publish.yml +0 -1
- hotdata_runtime-0.2.0/.github/workflows/release.yml +54 -0
- hotdata_runtime-0.2.0/CHANGELOG.md +39 -0
- {hotdata_runtime-0.1.0 → hotdata_runtime-0.2.0}/CONTRACT.md +14 -0
- {hotdata_runtime-0.1.0 → hotdata_runtime-0.2.0}/PKG-INFO +3 -2
- {hotdata_runtime-0.1.0 → hotdata_runtime-0.2.0}/README.md +1 -0
- hotdata_runtime-0.2.0/RELEASING.md +43 -0
- {hotdata_runtime-0.1.0 → hotdata_runtime-0.2.0}/hotdata_runtime/__init__.py +12 -0
- {hotdata_runtime-0.1.0 → hotdata_runtime-0.2.0}/hotdata_runtime/client.py +180 -0
- hotdata_runtime-0.2.0/hotdata_runtime/databases.py +61 -0
- {hotdata_runtime-0.1.0 → hotdata_runtime-0.2.0}/pyproject.toml +2 -2
- hotdata_runtime-0.2.0/scripts/check-release.py +62 -0
- hotdata_runtime-0.2.0/scripts/extract-changelog.py +36 -0
- hotdata_runtime-0.2.0/scripts/publish-workflow.sh +75 -0
- hotdata_runtime-0.2.0/scripts/release.sh +187 -0
- hotdata_runtime-0.2.0/scripts/update_changelog.py +71 -0
- {hotdata_runtime-0.1.0 → hotdata_runtime-0.2.0}/tests/test_contract.py +5 -0
- hotdata_runtime-0.2.0/tests/test_databases.py +242 -0
- hotdata_runtime-0.2.0/tests/test_update_changelog.py +48 -0
- {hotdata_runtime-0.1.0 → hotdata_runtime-0.2.0}/uv.lock +5 -5
- {hotdata_runtime-0.1.0 → hotdata_runtime-0.2.0}/.gitignore +0 -0
- {hotdata_runtime-0.1.0 → hotdata_runtime-0.2.0}/examples/basic_usage.py +0 -0
- {hotdata_runtime-0.1.0 → hotdata_runtime-0.2.0}/hotdata_runtime/env.py +0 -0
- {hotdata_runtime-0.1.0 → hotdata_runtime-0.2.0}/hotdata_runtime/health.py +0 -0
- {hotdata_runtime-0.1.0 → hotdata_runtime-0.2.0}/hotdata_runtime/http.py +0 -0
- {hotdata_runtime-0.1.0 → hotdata_runtime-0.2.0}/hotdata_runtime/result.py +0 -0
- {hotdata_runtime-0.1.0 → hotdata_runtime-0.2.0}/tests/test_client.py +0 -0
- {hotdata_runtime-0.1.0 → hotdata_runtime-0.2.0}/tests/test_health.py +0 -0
- {hotdata_runtime-0.1.0 → hotdata_runtime-0.2.0}/tests/test_result.py +0 -0
- {hotdata_runtime-0.1.0 → hotdata_runtime-0.2.0}/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@1e812e8210a4a8a0b23075e5795f2a4e2b2a0b7 # 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,39 @@
|
|
|
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
|
+
## [0.2.0] - 2026-05-24
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Switch managed database operations from the connections API to the dedicated `/databases` API (`hotdata>=0.2.3` required).
|
|
15
|
+
- `create_managed_database` first parameter renamed from `name` to `description` (keyword-only).
|
|
16
|
+
- `ManagedDatabase` dataclass: replace `name`/`source_type` fields with `description`/`default_connection_id`.
|
|
17
|
+
- `resolve_managed_database` tries direct ID lookup first, then falls back to a description scan.
|
|
18
|
+
- `list_managed_databases` now fetches all databases regardless of source type.
|
|
19
|
+
- `list_managed_tables`, `load_managed_table`, and `delete_managed_table` use `default_connection_id` instead of database `id` for connection-scoped operations.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `create_managed_database` accepts an optional `expires_at` parameter.
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
|
|
27
|
+
- `MANAGED_SOURCE_TYPE`, `build_managed_config`, and `create_connection_request` removed from the public API.
|
|
28
|
+
|
|
29
|
+
## [0.1.1] - 2026-05-19
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- Managed database helpers on `HotdataClient`.
|
|
34
|
+
|
|
35
|
+
## [0.1.0] - 2026-05-06
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- Initial release.
|
|
@@ -30,6 +30,11 @@ The supported import surface is:
|
|
|
30
30
|
- `ResultSummary`
|
|
31
31
|
- `RunHistoryItem`
|
|
32
32
|
- `WorkspaceSelection`
|
|
33
|
+
- `ManagedDatabase`
|
|
34
|
+
- `ManagedTable`
|
|
35
|
+
- `LoadManagedTableResult`
|
|
36
|
+
- `DEFAULT_SCHEMA`
|
|
37
|
+
- `is_parquet_path`
|
|
33
38
|
|
|
34
39
|
Adapters should import from `hotdata_runtime` and treat this surface as the stable API.
|
|
35
40
|
|
|
@@ -49,6 +54,15 @@ Adapters should import from `hotdata_runtime` and treat this surface as the stab
|
|
|
49
54
|
- `list_qualified_table_names(...)` returns sorted fully qualified table names.
|
|
50
55
|
- `columns_for_qualified(qualified, connection_id=...)` resolves table columns, and
|
|
51
56
|
adapters should pass `connection_id` when known.
|
|
57
|
+
- `uploads()` returns the uploads API wrapper for parquet staging.
|
|
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.
|
|
62
|
+
- `list_managed_tables(database, schema=...)` lists tables in a managed database.
|
|
63
|
+
- `upload_parquet(path)` uploads a local parquet file and returns an upload id.
|
|
64
|
+
- `load_managed_table(database, table, schema=..., upload_id=..., file=...)` publishes parquet data into a declared managed table.
|
|
65
|
+
- `delete_managed_table(database, table, schema=...)` deletes a managed table.
|
|
52
66
|
|
|
53
67
|
### `QueryResult`
|
|
54
68
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hotdata-runtime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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.
|
|
7
|
+
Requires-Dist: hotdata>=0.2.3
|
|
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:
|
|
@@ -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.
|
|
@@ -8,6 +8,13 @@ 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
|
+
is_parquet_path,
|
|
17
|
+
)
|
|
11
18
|
from hotdata_runtime.env import (
|
|
12
19
|
default_api_key,
|
|
13
20
|
default_host,
|
|
@@ -29,8 +36,13 @@ except PackageNotFoundError:
|
|
|
29
36
|
|
|
30
37
|
__all__ = [
|
|
31
38
|
"__version__",
|
|
39
|
+
"DEFAULT_SCHEMA",
|
|
32
40
|
"HotdataClient",
|
|
41
|
+
"LoadManagedTableResult",
|
|
42
|
+
"ManagedDatabase",
|
|
43
|
+
"ManagedTable",
|
|
33
44
|
"QueryResult",
|
|
45
|
+
"is_parquet_path",
|
|
34
46
|
"workspace_health_lines",
|
|
35
47
|
"default_api_key",
|
|
36
48
|
"default_host",
|
|
@@ -9,12 +9,18 @@ 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
|
|
15
16
|
from hotdata.api.results_api import ResultsApi
|
|
17
|
+
from hotdata.api.uploads_api import UploadsApi
|
|
16
18
|
from hotdata.exceptions import ApiException
|
|
17
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
|
|
18
24
|
from hotdata.models.query_request import QueryRequest
|
|
19
25
|
from hotdata.models.query_response import QueryResponse
|
|
20
26
|
from hotdata.models.table_info import TableInfo
|
|
@@ -26,6 +32,15 @@ from hotdata_runtime.env import (
|
|
|
26
32
|
normalize_host,
|
|
27
33
|
pick_workspace,
|
|
28
34
|
)
|
|
35
|
+
from hotdata_runtime.databases import (
|
|
36
|
+
DEFAULT_SCHEMA,
|
|
37
|
+
LoadManagedTableResult,
|
|
38
|
+
ManagedDatabase,
|
|
39
|
+
ManagedTable,
|
|
40
|
+
api_error_message,
|
|
41
|
+
is_parquet_path,
|
|
42
|
+
managed_database_from_detail,
|
|
43
|
+
)
|
|
29
44
|
from hotdata_runtime.http import default_http_retries
|
|
30
45
|
from hotdata_runtime.result import QueryResult
|
|
31
46
|
|
|
@@ -117,6 +132,9 @@ class HotdataClient:
|
|
|
117
132
|
def connections(self) -> ConnectionsApi:
|
|
118
133
|
return ConnectionsApi(self._api)
|
|
119
134
|
|
|
135
|
+
def _databases_api(self) -> DatabasesApi:
|
|
136
|
+
return DatabasesApi(self._api)
|
|
137
|
+
|
|
120
138
|
def _information_schema(self) -> InformationSchemaApi:
|
|
121
139
|
return InformationSchemaApi(self._api)
|
|
122
140
|
|
|
@@ -135,6 +153,168 @@ class HotdataClient:
|
|
|
135
153
|
def results(self) -> ResultsApi:
|
|
136
154
|
return self._results_api()
|
|
137
155
|
|
|
156
|
+
def uploads(self) -> UploadsApi:
|
|
157
|
+
return UploadsApi(self._api)
|
|
158
|
+
|
|
159
|
+
def list_managed_databases(self) -> list[ManagedDatabase]:
|
|
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
|
|
169
|
+
|
|
170
|
+
def resolve_managed_database(self, name_or_id: str) -> ManagedDatabase:
|
|
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
|
|
185
|
+
break
|
|
186
|
+
if match_id is None:
|
|
187
|
+
raise KeyError(f"No database named or with id {name_or_id!r}")
|
|
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)
|
|
193
|
+
|
|
194
|
+
def create_managed_database(
|
|
195
|
+
self,
|
|
196
|
+
description: str | None = None,
|
|
197
|
+
*,
|
|
198
|
+
schema: str = DEFAULT_SCHEMA,
|
|
199
|
+
tables: list[str] | None = None,
|
|
200
|
+
expires_at: str | None = None,
|
|
201
|
+
) -> ManagedDatabase:
|
|
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
|
+
)
|
|
215
|
+
try:
|
|
216
|
+
created = self._databases_api().create_database(request)
|
|
217
|
+
except ApiException as e:
|
|
218
|
+
raise RuntimeError(api_error_message(e)) from e
|
|
219
|
+
return managed_database_from_detail(created)
|
|
220
|
+
|
|
221
|
+
def delete_managed_database(self, name_or_id: str) -> None:
|
|
222
|
+
db = self.resolve_managed_database(name_or_id)
|
|
223
|
+
try:
|
|
224
|
+
self._databases_api().delete_database(db.id)
|
|
225
|
+
except ApiException as e:
|
|
226
|
+
raise RuntimeError(api_error_message(e)) from e
|
|
227
|
+
|
|
228
|
+
def list_managed_tables(
|
|
229
|
+
self,
|
|
230
|
+
database: str,
|
|
231
|
+
*,
|
|
232
|
+
schema: str | None = None,
|
|
233
|
+
) -> list[ManagedTable]:
|
|
234
|
+
db = self.resolve_managed_database(database)
|
|
235
|
+
rows: list[ManagedTable] = []
|
|
236
|
+
for t in self.iter_tables(connection_id=db.default_connection_id):
|
|
237
|
+
if schema is not None and t.var_schema != schema:
|
|
238
|
+
continue
|
|
239
|
+
rows.append(
|
|
240
|
+
ManagedTable(
|
|
241
|
+
full_name=f"{db.id}.{t.var_schema}.{t.table}",
|
|
242
|
+
schema=t.var_schema,
|
|
243
|
+
table=t.table,
|
|
244
|
+
synced=t.synced,
|
|
245
|
+
last_sync=t.last_sync,
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
rows.sort(key=lambda row: (row.schema, row.table))
|
|
249
|
+
return rows
|
|
250
|
+
|
|
251
|
+
def upload_parquet(self, path: str) -> str:
|
|
252
|
+
if not is_parquet_path(path):
|
|
253
|
+
raise ValueError(
|
|
254
|
+
f"Managed table loads require a parquet file (got {path!r})"
|
|
255
|
+
)
|
|
256
|
+
with open(path, "rb") as f:
|
|
257
|
+
data = f.read()
|
|
258
|
+
try:
|
|
259
|
+
uploaded = self.uploads().upload_file(
|
|
260
|
+
data,
|
|
261
|
+
_content_type="application/octet-stream",
|
|
262
|
+
)
|
|
263
|
+
except ApiException as e:
|
|
264
|
+
raise RuntimeError(api_error_message(e)) from e
|
|
265
|
+
return uploaded.id
|
|
266
|
+
|
|
267
|
+
def load_managed_table(
|
|
268
|
+
self,
|
|
269
|
+
database: str,
|
|
270
|
+
table: str,
|
|
271
|
+
*,
|
|
272
|
+
schema: str = DEFAULT_SCHEMA,
|
|
273
|
+
upload_id: str | None = None,
|
|
274
|
+
file: str | None = None,
|
|
275
|
+
) -> LoadManagedTableResult:
|
|
276
|
+
if (upload_id is None) == (file is None):
|
|
277
|
+
raise ValueError("Exactly one of upload_id or file is required")
|
|
278
|
+
db = self.resolve_managed_database(database)
|
|
279
|
+
if upload_id is not None:
|
|
280
|
+
resolved_upload_id = upload_id
|
|
281
|
+
else:
|
|
282
|
+
assert file is not None
|
|
283
|
+
resolved_upload_id = self.upload_parquet(file)
|
|
284
|
+
request = LoadManagedTableRequest(
|
|
285
|
+
mode="replace",
|
|
286
|
+
upload_id=resolved_upload_id,
|
|
287
|
+
)
|
|
288
|
+
try:
|
|
289
|
+
loaded = self.connections().load_managed_table(
|
|
290
|
+
db.default_connection_id,
|
|
291
|
+
schema,
|
|
292
|
+
table,
|
|
293
|
+
request,
|
|
294
|
+
)
|
|
295
|
+
except ApiException as e:
|
|
296
|
+
raise RuntimeError(api_error_message(e)) from e
|
|
297
|
+
return LoadManagedTableResult(
|
|
298
|
+
connection_id=loaded.connection_id,
|
|
299
|
+
schema_name=loaded.schema_name,
|
|
300
|
+
table_name=loaded.table_name,
|
|
301
|
+
row_count=loaded.row_count,
|
|
302
|
+
full_name=f"{db.id}.{loaded.schema_name}.{loaded.table_name}",
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def delete_managed_table(
|
|
306
|
+
self,
|
|
307
|
+
database: str,
|
|
308
|
+
table: str,
|
|
309
|
+
*,
|
|
310
|
+
schema: str = DEFAULT_SCHEMA,
|
|
311
|
+
) -> None:
|
|
312
|
+
db = self.resolve_managed_database(database)
|
|
313
|
+
try:
|
|
314
|
+
self.connections().delete_managed_table(db.default_connection_id, schema, table)
|
|
315
|
+
except ApiException as e:
|
|
316
|
+
raise RuntimeError(api_error_message(e)) from e
|
|
317
|
+
|
|
138
318
|
def list_recent_results(
|
|
139
319
|
self,
|
|
140
320
|
*,
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
|
|
11
|
+
DEFAULT_SCHEMA = "public"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ManagedDatabase:
|
|
16
|
+
id: str
|
|
17
|
+
description: str | None
|
|
18
|
+
default_connection_id: str
|
|
19
|
+
|
|
20
|
+
def to_dict(self) -> dict[str, Any]:
|
|
21
|
+
return asdict(self)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class ManagedTable:
|
|
26
|
+
full_name: str
|
|
27
|
+
schema: str
|
|
28
|
+
table: str
|
|
29
|
+
synced: bool
|
|
30
|
+
last_sync: str | None
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict[str, Any]:
|
|
33
|
+
return asdict(self)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class LoadManagedTableResult:
|
|
38
|
+
connection_id: str
|
|
39
|
+
schema_name: str
|
|
40
|
+
table_name: str
|
|
41
|
+
row_count: int
|
|
42
|
+
full_name: str
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> dict[str, Any]:
|
|
45
|
+
return asdict(self)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_parquet_path(path: str) -> bool:
|
|
49
|
+
return Path(path).suffix.lower() == ".parquet"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def managed_database_from_detail(detail: Any) -> ManagedDatabase:
|
|
53
|
+
return ManagedDatabase(
|
|
54
|
+
id=str(detail.id),
|
|
55
|
+
description=detail.description,
|
|
56
|
+
default_connection_id=str(detail.default_connection_id),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def api_error_message(exc: ApiException) -> str:
|
|
61
|
+
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.
|
|
7
|
+
version = "0.2.0"
|
|
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.
|
|
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()
|