Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/check-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Check release metadata

on:
pull_request:
paths:
- 'pyproject.toml'
- 'CHANGELOG.md'

permissions:
contents: read

jobs:
check:
name: Verify changelog matches version bump
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0

- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.12'

- name: Check release metadata
run: python scripts/check-release.py
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: CI

on:
push:
branches: ["main", "master"]
pull_request:

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
test:
name: Test (Python 3.12)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: pyproject.toml declares requires-python = ">=3.10", but CI only exercises Python 3.12. The code uses 3.10+ generic syntax (list[str] | None) under from __future__ import annotations, so it should work on 3.10/3.11, but nothing here verifies those versions stay green. Consider a strategy.matrix.python-version: ['3.10', '3.11', '3.12'] so a regression at the lower bounds is caught before users hit it. (not blocking)

runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v6
with:
enable-cache: true

- name: Set up Python
run: uv python install 3.12

- name: Install dependencies
run: uv sync --locked

- name: Test
run: uv run pytest -v
69 changes: 69 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Publish to PyPI

on:
push:
tags:
- 'v[0-9]*'

concurrency:
group: pypi-publish-${{ github.ref_name }}
cancel-in-progress: false

permissions:
contents: read

jobs:
build:
name: Build distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.12'

- name: Install build tooling
run: python -m pip install --upgrade build twine

- name: Verify tag matches pyproject version
run: |
if [[ ! "$GITHUB_REF_NAME" =~ ^v[0-9] ]]; then
echo "Release tag '$GITHUB_REF_NAME' must start with 'v' followed by a digit (e.g. v1.0.0)" >&2
exit 1
fi
tag="${GITHUB_REF_NAME#v}"
pkg_version=$(python -c "import tomllib,pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
if [ "$tag" != "$pkg_version" ]; then
echo "Release tag ($tag) does not match pyproject.toml version ($pkg_version)" >&2
exit 1
fi

- name: Build sdist and wheel
run: python -m build

- name: Check distribution metadata
run: python -m twine check --strict dist/*

- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: dist
path: dist/

publish:
name: Publish to PyPI
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/hotdata-langchain
permissions:
id-token: write
steps:
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
name: dist
path: dist/

- name: Publish via Trusted Publishing
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
54 changes: 54 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: GitHub Release

on:
push:
tags:
- 'v[0-9]*'

permissions:
contents: write

jobs:
release:
name: Create GitHub Release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.12'

- name: Read package metadata
id: meta
run: |
pkg_name=$(python -c "import tomllib,pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['name'])")
pkg_version="${GITHUB_REF_NAME#v}"
echo "name=${pkg_name}" >> "$GITHUB_OUTPUT"
echo "version=${pkg_version}" >> "$GITHUB_OUTPUT"

- name: Extract changelog notes
id: notes
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
if [[ -f CHANGELOG.md ]]; then
body="$(python scripts/extract-changelog.py "$version")"
else
body="Release ${version}."
fi
delimiter="EOF_${RANDOM}_${RANDOM}"
{
echo "body<<${delimiter}"
echo "$body"
echo "${delimiter}"
} >> "$GITHUB_OUTPUT"

- name: Create GitHub Release
uses: softprops/action-gh-release@1e812e8210a4a8a0b23075e5795f2a4e2b2a0b7 # v2.2.2
with:
tag_name: ${{ github.ref_name }}
name: ${{ steps.meta.outputs.name }} ${{ steps.meta.outputs.version }}
body: ${{ steps.notes.outputs.body }}
generate_release_notes: false
make_latest: true
23 changes: 23 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so

# Virtual environments
.env
.venv
env/
venv/

# Testing
.pytest_cache/
.coverage
htmlcov/

# Packaging
*.egg-info/
dist/
build/

.DS_Store
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.1.0] - 2026-05-19

### Added

- Initial release with LangChain tools for Hotdata managed databases.
60 changes: 59 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,60 @@
# hotdata-langchain
LangChain tools for Hotdata runtime

LangChain tools for [Hotdata](https://hotdata.dev), built on **hotdata-runtime**.

## Features

- **SQL tool** — run workspace SQL and return JSON rows for agents
- **Managed database tools** — list, create, and load parquet into Hotdata-owned catalogs (replaces legacy dataset uploads)

## Install

```bash
pip install hotdata-langchain
```

Requires `HOTDATA_API_KEY`. Optionally set `HOTDATA_WORKSPACE`, `HOTDATA_API_URL`, or `HOTDATA_SANDBOX`.

## Usage

```python
import hotdata_langchain as hl

client = hl.from_env()
tools = hl.make_hotdata_tools(client)

for tool in tools:
print(tool.name, tool.description)
```

Managed database example:

```python
tools = {tool.name: tool for tool in hl.make_hotdata_tools(client)}

tools["hotdata_create_managed_database"].invoke(
{"name": "sales", "schema_name": "public", "tables": "orders"}
)

tools["hotdata_load_managed_table"].invoke(
{
"database": "sales",
"table": "orders",
"file": "/path/to/orders.parquet",
}
)
```

## Examples

```bash
uv run python examples/langchain_basic.py
uv run python examples/langchain_managed_db.py
```

## Development

```bash
uv sync --locked
uv run pytest
```
43 changes: 43 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Releasing

Every release uses `./scripts/release.sh`. Do not bump versions, tag, or create GitHub Releases manually.

## One-time setup

- Install [GitHub CLI](https://cli.github.com/) (`gh`) and authenticate.
- Ensure PyPI [trusted publishing](https://docs.pypi.org/trusted-publishers/) is configured for this repo (`publish.yml` uses the `pypi` GitHub environment).

## Release steps

1. Add user-facing notes under `## [Unreleased]` in `CHANGELOG.md`.
2. Prepare the release PR:

```bash
./scripts/release.sh prepare patch # or minor | major | 1.2.3
```

3. Merge the PR after CI passes (including the changelog check).
4. Publish from a clean default branch checkout:

```bash
git checkout main # or master for hotdata-marimo
git pull
./scripts/release.sh publish
```

## What happens automatically

Pushing a `vX.Y.Z` tag triggers two workflows:

| Workflow | Purpose |
|----------|---------|
| `publish.yml` | Build wheel/sdist and publish to PyPI |
| `release.yml` | Create the GitHub Release with notes from `CHANGELOG.md` |

## Enforcement

- **PR check** (`check-release.yml`): if `pyproject.toml` version changes, `CHANGELOG.md` must contain a matching `## [X.Y.Z]` section.
- **Tag check** (`publish.yml`): the tag (without `v`) must match `[project].version` in `pyproject.toml`.
- **Publish guard** (`release.sh publish`): refuses to tag if the changelog section is missing.

Together, these make it hard to ship a version without changelog notes or a GitHub Release.
21 changes: 21 additions & 0 deletions examples/langchain_basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Minimal LangChain tool usage with hotdata-langchain."""

import hotdata_langchain as hl


def main() -> None:
client = hl.from_env()
tools = hl.make_hotdata_tools(client)
by_name = {tool.name: tool for tool in tools}

sql_tool = by_name["hotdata_execute_sql"]
print(sql_tool.invoke({"sql": "SELECT 1 AS ok"}))

list_tool = by_name["hotdata_list_managed_databases"]
print(list_tool.invoke({}))

client.close()


if __name__ == "__main__":
main()
38 changes: 38 additions & 0 deletions examples/langchain_managed_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Managed database tools for LangChain agents."""

import hotdata_langchain as hl


def main() -> None:
client = hl.from_env()
tools = hl.make_hotdata_tools(client)
by_name = {tool.name: tool for tool in tools}

create = by_name["hotdata_create_managed_database"]
print(
create.invoke(
{
"name": "demo_sales",
"schema_name": "public",
"tables": "orders\ncustomers",
}
)
)

load = by_name["hotdata_load_managed_table"]
print(
load.invoke(
{
"database": "demo_sales",
"table": "orders",
"file": "/path/to/orders.parquet",
"schema_name": "public",
}
)
)

client.close()


if __name__ == "__main__":
main()
Loading
Loading