hotdata-framework 0.11.0__tar.gz → 0.12.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 (45) hide show
  1. hotdata_framework-0.12.1/.github/dependabot.yml +17 -0
  2. hotdata_framework-0.12.1/.github/workflows/publish.yml +105 -0
  3. hotdata_framework-0.12.1/.github/workflows/release.yml +92 -0
  4. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/CHANGELOG.md +39 -0
  5. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/CONTRACT.md +6 -1
  6. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/PKG-INFO +2 -2
  7. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/RELEASING.md +21 -0
  8. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/hotdata_framework/__init__.py +7 -0
  9. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/hotdata_framework/client.py +218 -8
  10. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/hotdata_framework/databases.py +48 -0
  11. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/pyproject.toml +2 -2
  12. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/tests/test_client.py +331 -1
  13. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/tests/test_contract.py +3 -0
  14. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/tests/test_databases.py +173 -0
  15. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/uv.lock +5 -5
  16. hotdata_framework-0.11.0/.github/dependabot.yml +0 -8
  17. hotdata_framework-0.11.0/.github/workflows/publish.yml +0 -69
  18. hotdata_framework-0.11.0/.github/workflows/release.yml +0 -54
  19. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/.github/CODEOWNERS +0 -0
  20. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/.github/workflows/check-release.yml +0 -0
  21. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/.github/workflows/ci.yml +0 -0
  22. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/.github/workflows/dependabot-automerge.yml +0 -0
  23. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/.gitignore +0 -0
  24. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/README.md +0 -0
  25. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/examples/basic_usage.py +0 -0
  26. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/hotdata_framework/env.py +0 -0
  27. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/hotdata_framework/errors.py +0 -0
  28. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/hotdata_framework/health.py +0 -0
  29. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/hotdata_framework/managed_client.py +0 -0
  30. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/hotdata_framework/py.typed +0 -0
  31. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/hotdata_framework/result.py +0 -0
  32. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/scripts/check-release.py +0 -0
  33. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/scripts/extract-changelog.py +0 -0
  34. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/scripts/publish-workflow.sh +0 -0
  35. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/scripts/release.sh +0 -0
  36. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/scripts/update_changelog.py +0 -0
  37. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/tests/test_errors.py +0 -0
  38. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/tests/test_health.py +0 -0
  39. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/tests/test_indexes.py +0 -0
  40. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/tests/test_managed_client.py +0 -0
  41. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/tests/test_request_timeout.py +0 -0
  42. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/tests/test_result.py +0 -0
  43. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/tests/test_retry_policy.py +0 -0
  44. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/tests/test_update_changelog.py +0 -0
  45. {hotdata_framework-0.11.0 → hotdata_framework-0.12.1}/tests/test_version.py +0 -0
@@ -0,0 +1,17 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: uv
4
+ directory: "/"
5
+ schedule:
6
+ interval: daily
7
+ allow:
8
+ - dependency-name: hotdata
9
+
10
+ # Action pins had no watcher, which is how gh-action-pypi-publish sat at
11
+ # v1.13.0 until its bundled twine broke a release at upload time. Unlike the
12
+ # uv entry there is no `allow` filter: the point is to see every stale pin,
13
+ # not a chosen one.
14
+ - package-ecosystem: github-actions
15
+ directory: "/"
16
+ schedule:
17
+ interval: weekly
@@ -0,0 +1,105 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v[0-9]*'
7
+ # Retry an existing tag without moving it. A publish can fail for reasons that
8
+ # have nothing to do with the code — a stale action pin, a PyPI outage — and
9
+ # with only the tag trigger the choices were to delete and re-push the tag or
10
+ # to burn a version number on a CI fix. Neither is a good answer to
11
+ # "the upload failed, run it again".
12
+ workflow_dispatch:
13
+ inputs:
14
+ tag:
15
+ description: Existing tag to build and publish (e.g. v1.2.3)
16
+ required: true
17
+ type: string
18
+
19
+ concurrency:
20
+ group: pypi-publish-${{ inputs.tag || github.ref_name }}
21
+ cancel-in-progress: false
22
+
23
+ permissions:
24
+ contents: read
25
+
26
+ jobs:
27
+ build:
28
+ name: Build distribution
29
+ runs-on: ubuntu-latest
30
+ env:
31
+ # The tag being released, whether it arrived by push or by dispatch.
32
+ TAG: ${{ inputs.tag || github.ref_name }}
33
+ steps:
34
+ # Before checkout, because checkout resolves the input as an arbitrary ref:
35
+ # a branch or SHA is fetched first and only rejected later by the version
36
+ # match below, which is also looser (`^v[0-9]`). Strict here so the dispatch
37
+ # contract matches release.yml — release.sh only ever produces X.Y.Z.
38
+ - name: Validate release tag format
39
+ if: github.event_name == 'workflow_dispatch'
40
+ env:
41
+ INPUT_TAG: ${{ inputs.tag }}
42
+ run: |
43
+ set -euo pipefail
44
+ if [[ ! "$INPUT_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
45
+ echo "tag must look like vX.Y.Z, got: $INPUT_TAG" >&2
46
+ exit 1
47
+ fi
48
+
49
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
50
+ with:
51
+ ref: ${{ inputs.tag || github.ref_name }}
52
+
53
+ - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
54
+ with:
55
+ python-version: '3.12'
56
+
57
+ - name: Install build tooling
58
+ run: python -m pip install --upgrade build twine
59
+
60
+ - name: Verify tag matches pyproject version
61
+ run: |
62
+ if [[ ! "$TAG" =~ ^v[0-9] ]]; then
63
+ echo "Release tag '$TAG' must start with 'v' followed by a digit (e.g. v1.0.0)" >&2
64
+ exit 1
65
+ fi
66
+ tag="${TAG#v}"
67
+ pkg_version=$(python -c "import tomllib,pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
68
+ if [ "$tag" != "$pkg_version" ]; then
69
+ echo "Release tag ($tag) does not match pyproject.toml version ($pkg_version)" >&2
70
+ exit 1
71
+ fi
72
+
73
+ - name: Build sdist and wheel
74
+ run: python -m build
75
+
76
+ - name: Check distribution metadata
77
+ run: python -m twine check --strict dist/*
78
+
79
+ - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
80
+ with:
81
+ name: dist
82
+ path: dist/
83
+
84
+ publish:
85
+ name: Publish to PyPI
86
+ needs: build
87
+ runs-on: ubuntu-latest
88
+ environment:
89
+ name: pypi
90
+ url: https://pypi.org/p/hotdata-framework
91
+ permissions:
92
+ id-token: write
93
+ steps:
94
+ - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
95
+ with:
96
+ name: dist
97
+ path: dist/
98
+
99
+ # v1.13.0's bundled twine rejects `Metadata-Version: 2.5`, which current
100
+ # hatchling emits: `InvalidDistribution: '2.5' is not a valid metadata
101
+ # version`. The build job's own `twine check --strict` passes, because it
102
+ # pip-installs a current twine — so the failure appears only at upload,
103
+ # after the tag is already public.
104
+ - name: Publish via Trusted Publishing
105
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
@@ -0,0 +1,92 @@
1
+ name: GitHub Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v[0-9]*'
7
+ # Repair the Release for a tag that already exists, without moving it. See
8
+ # RELEASING.md, "If a release workflow fails" — added alongside this trigger,
9
+ # since a capability nobody can find is not much better than not having it.
10
+ workflow_dispatch:
11
+ inputs:
12
+ tag:
13
+ description: Existing tag to create or update a Release for (e.g. v1.2.3)
14
+ required: true
15
+ type: string
16
+
17
+ permissions:
18
+ contents: write
19
+
20
+ jobs:
21
+ release:
22
+ name: Create GitHub Release
23
+ runs-on: ubuntu-latest
24
+ env:
25
+ # The tag being released, whether it arrived by push or by dispatch.
26
+ TAG: ${{ inputs.tag || github.ref_name }}
27
+ steps:
28
+ # Before checkout, because checkout resolves the input as an arbitrary ref
29
+ # — and this workflow needs the guard more than publish.yml does. It holds
30
+ # `contents: write`, and action-gh-release CREATES a tag when tag_name does
31
+ # not resolve to one, so an unvalidated `tag: main` would check out cleanly
32
+ # and leave refs/tags/main plus a release named for it. The push path is
33
+ # constrained by the v[0-9]* filter; the dispatch path was not constrained
34
+ # at all.
35
+ - name: Validate release tag format
36
+ if: github.event_name == 'workflow_dispatch'
37
+ env:
38
+ INPUT_TAG: ${{ inputs.tag }}
39
+ run: |
40
+ set -euo pipefail
41
+ if [[ ! "$INPUT_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
42
+ echo "tag must look like vX.Y.Z, got: $INPUT_TAG" >&2
43
+ exit 1
44
+ fi
45
+
46
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
47
+ with:
48
+ ref: ${{ inputs.tag || github.ref_name }}
49
+
50
+ - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
51
+ with:
52
+ python-version: '3.12'
53
+
54
+ - name: Read package metadata
55
+ id: meta
56
+ run: |
57
+ pkg_name=$(python -c "import tomllib,pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['name'])")
58
+ pkg_version="${TAG#v}"
59
+ echo "name=${pkg_name}" >> "$GITHUB_OUTPUT"
60
+ echo "version=${pkg_version}" >> "$GITHUB_OUTPUT"
61
+
62
+ - name: Extract changelog notes
63
+ id: notes
64
+ run: |
65
+ set -euo pipefail
66
+ version="${TAG#v}"
67
+ if [[ -f CHANGELOG.md ]]; then
68
+ body="$(python scripts/extract-changelog.py "$version")"
69
+ else
70
+ body="Release ${version}."
71
+ fi
72
+ delimiter="EOF_${RANDOM}_${RANDOM}"
73
+ {
74
+ echo "body<<${delimiter}"
75
+ echo "$body"
76
+ echo "${delimiter}"
77
+ } >> "$GITHUB_OUTPUT"
78
+
79
+ - name: Create GitHub Release
80
+ uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2
81
+ with:
82
+ tag_name: ${{ inputs.tag || github.ref_name }}
83
+ name: ${{ steps.meta.outputs.name }} ${{ steps.meta.outputs.version }}
84
+ body: ${{ steps.notes.outputs.body }}
85
+ generate_release_notes: false
86
+ # Let GitHub decide by tag date/semver rather than by run order. On a
87
+ # push the tag is the newest version and becomes latest; on a dispatch
88
+ # repairing an older tag, a newer release keeps the badge. Note `false`
89
+ # is not "leave alone" — it explicitly marks a release NOT latest, so
90
+ # gating on the event would demote the newest tag in the very case this
91
+ # trigger exists for: repairing its Release after a failed push run.
92
+ make_latest: legacy
@@ -7,6 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+
11
+ ## [0.12.1] - 2026-08-18
12
+
13
+ ### Fixed
14
+
15
+ - fix(load): submit managed loads as a job and poll, instead of holding one request open
16
+
17
+ ## [0.12.0] - 2026-08-11
18
+
19
+ ### Added
20
+
21
+ - Table storage layout, both directions. `add_managed_table()` and
22
+ `create_managed_database()` take `partition_by` / `sorted_by`, and
23
+ `managed_table_layout()` reads back what was actually declared as a
24
+ `TableLayout`. `TablePartitionKey` and `TableSortKey` are re-exported so
25
+ callers need one import.
26
+
27
+ Both halves matter because a layout is fixed when the table is created and
28
+ there is no alter path: a table declared without one keeps that shape until it
29
+ is recreated and its data rewritten. So declaring is not enough — a caller has
30
+ to be able to confirm it took, and to refuse to load when it cannot.
31
+
32
+ `managed_table_layout()` raises `KeyError` for a table that is not declared,
33
+ rather than returning an empty layout. "Not there" and "declared without a
34
+ layout" lead to opposite decisions for a caller.
35
+
36
+ Until now this package could not express a layout at all, which is why at least
37
+ one consumer hand-built the HTTP request instead. The generated key models are
38
+ passed through rather than wrapped, so the transform vocabulary stays exactly
39
+ the API's.
40
+
41
+ ### Changed
42
+
43
+ - Require `hotdata>=0.9.0,<0.10`. 0.9.0 is the first release whose models carry
44
+ `partition_by` / `sorted_by` on the add-table request, the create-database
45
+ table declarations, and the table-info response. On an older `hotdata` the
46
+ fields would be silently dropped by the model and the table declared without a
47
+ layout, returning success — which is the failure this feature exists to end.
48
+
10
49
  ## [0.11.0] - 2026-08-11
11
50
 
12
51
  ### Changed
@@ -31,6 +31,9 @@ The supported import surface is:
31
31
  - `WorkspaceSelection`
32
32
  - `ManagedDatabase`
33
33
  - `ManagedTable`
34
+ - `TableLayout`
35
+ - `TablePartitionKey`
36
+ - `TableSortKey`
34
37
  - `LoadManagedTableResult`
35
38
  - `CreateIndexResult`
36
39
  - `DEFAULT_SCHEMA`
@@ -56,6 +59,8 @@ Adapters should import from `hotdata_framework` and treat this surface as the st
56
59
  adapters should pass `connection_id` when known.
57
60
  - `uploads()` returns the uploads API wrapper for parquet staging.
58
61
  - `list_managed_databases()` returns all databases via the `/databases` API.
62
+ - `add_managed_table(...)` and `create_managed_database(...)` accept `partition_by` / `sorted_by` to declare a table's storage layout. The layout is fixed when the table is created and cannot be altered afterwards, so omitting it is permanent for that table.
63
+ - `managed_table_layout(database, table, schema=...)` returns the declared layout as `TableLayout`. Empty lists mean no layout was declared — sound only because the table is resolved through a managed database. Raises `KeyError` when the table is not declared, keeping "absent" distinct from "declared without a layout".
59
64
  - `resolve_managed_database(name_or_id)` resolves a database by id (direct lookup) or description (list scan). A `403` from `/databases` surfaces as `RuntimeError` (forbidden, not absent), preserving the underlying `ApiException` as `__cause__`.
60
65
  - `create_managed_database(description=..., schema=..., tables=..., expires_at=...)` creates a database via the `/databases` API and optionally declares tables up front. Returns a `ManagedDatabase` (id + `default_connection_id`) sufficient to load without a further read.
61
66
  - `delete_managed_database(name_or_id)` deletes a database via the `/databases` API.
@@ -64,7 +69,7 @@ Adapters should import from `hotdata_framework` and treat this surface as the st
64
69
  - `load_managed_table(database, table, schema=..., upload_id=..., file=...)` publishes parquet data into a declared managed table.
65
70
  - `delete_managed_table(database, table, schema=...)` deletes a managed table.
66
71
  - `create_index(database, table, schema=..., columns=..., index_type=..., index_name=...)` builds a `"sorted"`, `"bm25"`, or `"vector"` index on a managed table and returns a `CreateIndexResult`. It is the framework-side equivalent of the CLI's `hotdata indexes create`; indexing a table on a plain (non-managed) connection is out of scope. `index_name` defaults to `{table}_{columns}_{index_type}`, matching the CLI's derivation when `--name` is omitted. `index_type` is required rather than defaulting to the API's `"sorted"`. The build runs as a background job; the call polls it to a terminal state and raises `RuntimeError` with the job's `error_message` when it fails, because the submit call reports success regardless. `wait=False` returns as soon as the job is accepted, with `status="pending"` and a `job_id` for the caller to poll. For `index_type="vector"`, omitting `embedding_provider_id` indexes an existing vector column and `metric` (`"l2"`, `"cosine"`, `"dot"`) selects the distance function the index accelerates — a query using a different function silently falls back to a full scan; setting `embedding_provider_id` indexes a source *text* column instead, and the returned `source_column` names the column to pass to `vector_distance`. Argument combinations the server would silently ignore raise `ValueError` before any request is sent.
67
- - The `database` argument of `list_managed_tables`, `load_managed_table`, `add_managed_table`, `delete_managed_table`, `delete_managed_database`, `create_index`, and `execute_sql` accepts a name/id **or** an already-resolved `ManagedDatabase`. Passing a `ManagedDatabase` skips the name/id read probe, so a create-scoped key that cannot read `/databases` can load into a database it just created.
72
+ - The `database` argument of `list_managed_tables`, `load_managed_table`, `add_managed_table`, `delete_managed_table`, `delete_managed_database`, `create_index`, `managed_table_layout`, and `execute_sql` accepts a name/id **or** an already-resolved `ManagedDatabase`. Passing a `ManagedDatabase` skips the name/id read probe, so a create-scoped key that cannot read `/databases` can load into a database it just created.
68
73
 
69
74
  ### `QueryResult`
70
75
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hotdata-framework
3
- Version: 0.11.0
3
+ Version: 0.12.1
4
4
  Summary: Python framework for building Hotdata integrations: workspace runtime, query execution, and managed databases
5
5
  Project-URL: Homepage, https://www.hotdata.dev
6
6
  Project-URL: Documentation, https://www.hotdata.dev/docs
@@ -21,7 +21,7 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
21
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
22
  Classifier: Typing :: Typed
23
23
  Requires-Python: >=3.10
24
- Requires-Dist: hotdata<0.9,>=0.8.0
24
+ Requires-Dist: hotdata<0.10,>=0.9.0
25
25
  Requires-Dist: pandas>=2.0
26
26
  Requires-Dist: pyarrow>=14.0
27
27
  Description-Content-Type: text/markdown
@@ -34,6 +34,27 @@ Pushing a `vX.Y.Z` tag triggers two workflows:
34
34
  | `publish.yml` | Build wheel/sdist and publish to PyPI |
35
35
  | `release.yml` | Create the GitHub Release with notes from `CHANGELOG.md` |
36
36
 
37
+ ## If a release workflow fails
38
+
39
+ Both workflows also accept a manual re-run against an existing tag, so a failure
40
+ unrelated to the code — a stale action pin, a PyPI outage — does not require
41
+ deleting the tag or burning a version number:
42
+
43
+ ```bash
44
+ gh workflow run "Publish to PyPI" --ref main -f tag=vX.Y.Z
45
+ gh workflow run "GitHub Release" --ref main -f tag=vX.Y.Z
46
+ ```
47
+
48
+ `--ref main` selects the workflow *definition*, so a fix to the workflow file
49
+ itself is picked up; everything it runs — including `scripts/extract-changelog.py`
50
+ — still comes from the tag, since that is what is being built and released. The
51
+ two refs serve different purposes, which is why they can differ.
52
+
53
+ A version is only spent once PyPI has accepted an upload. If the publish failed
54
+ before that, the same version can still be published — check with
55
+ `curl -s -o /dev/null -w '%{http_code}' https://pypi.org/pypi/<pkg>/<version>/json`
56
+ returning 404.
57
+
37
58
  ## Enforcement
38
59
 
39
60
  - **PR check** (`check-release.yml`): if `pyproject.toml` version changes, `CHANGELOG.md` must contain a matching `## [X.Y.Z]` section.
@@ -2,6 +2,9 @@
2
2
 
3
3
  from importlib.metadata import PackageNotFoundError, version
4
4
 
5
+ from hotdata.models.table_partition_key import TablePartitionKey
6
+ from hotdata.models.table_sort_key import TableSortKey
7
+
5
8
  from hotdata_framework.client import (
6
9
  HotdataClient,
7
10
  ResultSummary,
@@ -14,6 +17,7 @@ from hotdata_framework.databases import (
14
17
  LoadManagedTableResult,
15
18
  ManagedDatabase,
16
19
  ManagedTable,
20
+ TableLayout,
17
21
  is_parquet_path,
18
22
  )
19
23
  from hotdata_framework.env import (
@@ -55,6 +59,9 @@ __all__ = [
55
59
  "QueryResult",
56
60
  "ResultSummary",
57
61
  "RunHistoryItem",
62
+ "TableLayout",
63
+ "TablePartitionKey",
64
+ "TableSortKey",
58
65
  "WorkspaceSelection",
59
66
  "__version__",
60
67
  "classify_sdk_error",