hotdata-runtime 0.1.1__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.1 → 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.1 → hotdata_runtime-0.2.0}/CONTRACT.md +4 -7
- {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.0}/PKG-INFO +2 -2
- hotdata_runtime-0.2.0/RELEASING.md +43 -0
- {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.0}/hotdata_runtime/__init__.py +0 -6
- {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.0}/hotdata_runtime/client.py +61 -32
- {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.0}/hotdata_runtime/databases.py +6 -36
- {hotdata_runtime-0.1.1 → 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.1 → hotdata_runtime-0.2.0}/tests/test_contract.py +0 -3
- 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.1 → hotdata_runtime-0.2.0}/uv.lock +5 -5
- hotdata_runtime-0.1.1/tests/test_databases.py +0 -209
- {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.0}/.gitignore +0 -0
- {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.0}/README.md +0 -0
- {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.0}/examples/basic_usage.py +0 -0
- {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.0}/hotdata_runtime/env.py +0 -0
- {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.0}/hotdata_runtime/health.py +0 -0
- {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.0}/hotdata_runtime/http.py +0 -0
- {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.0}/hotdata_runtime/result.py +0 -0
- {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.0}/tests/test_client.py +0 -0
- {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.0}/tests/test_health.py +0 -0
- {hotdata_runtime-0.1.1 → hotdata_runtime-0.2.0}/tests/test_result.py +0 -0
- {hotdata_runtime-0.1.1 → 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.
|
|
@@ -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
|
|
62
|
-
- `resolve_managed_database(name_or_id)` resolves a
|
|
63
|
-
- `create_managed_database(
|
|
64
|
-
- `delete_managed_database(name_or_id)` deletes a
|
|
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.
|
|
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.2.
|
|
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
|
-
|
|
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.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
186
|
+
if match_id is None:
|
|
170
187
|
raise KeyError(f"No database named or with id {name_or_id!r}")
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
|
@@ -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
|
-
|
|
20
|
-
|
|
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
|
|
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(
|
|
85
|
-
|
|
86
|
-
|
|
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.
|
|
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.2.
|
|
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()
|