Skip to content

Commit 9bb0ade

Browse files
authored
Merge pull request #1 from hotdata-dev/feat/workspace-health
Feat/workspace health
2 parents dcd4d31 + bc85ee3 commit 9bb0ade

12 files changed

Lines changed: 840 additions & 42 deletions

File tree

README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
# hotdata-core-notebook
1+
# hotdata-runtime
22

3-
Shared **Hotdata** client and domain types for notebook UIs (Marimo, Jupyter, etc.). UI frameworks depend on this package; they do not belong here.
3+
Shared runtime primitives for Hotdata integrations: workspace/session semantics, execution context, query state, run history, and replayable result handles. Framework packages (Marimo, Jupyter, Streamlit, LangGraph) depend on this package.
44

55
Install:
66

77
```bash
8-
pip install hotdata-core-notebook
8+
uv pip install hotdata-runtime
9+
# or: pip install hotdata-runtime
910
```
1011

11-
Development:
12+
Development (uses **uv**; creates `.venv/` in this repo):
1213

1314
```bash
14-
pip install -e ".[dev]"
15-
pytest
15+
uv sync --locked
16+
uv run pytest
1617
```
18+
19+
`uv.lock` is checked in so CI can run `uv sync --locked`. The default **dev** group (pytest) is enabled via `[tool.uv] default-groups`.

hotdata_core_notebook/__init__.py

Lines changed: 0 additions & 26 deletions
This file was deleted.

hotdata_runtime/__init__.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Hotdata runtime primitives for notebook and app integrations."""
2+
3+
from importlib.metadata import PackageNotFoundError, version
4+
5+
from hotdata_runtime.client import HotdataClient, from_env
6+
from hotdata_runtime.env import (
7+
default_api_key,
8+
default_host,
9+
default_session_id,
10+
explicit_workspace_id,
11+
list_workspaces,
12+
normalize_host,
13+
pick_workspace,
14+
)
15+
from hotdata_runtime.health import workspace_health_lines
16+
from hotdata_runtime.result import QueryResult
17+
18+
try:
19+
__version__ = version("hotdata-runtime")
20+
except PackageNotFoundError:
21+
__version__ = "0.0.0+unknown"
22+
23+
__all__ = [
24+
"__version__",
25+
"HotdataClient",
26+
"QueryResult",
27+
"workspace_health_lines",
28+
"default_api_key",
29+
"default_host",
30+
"default_session_id",
31+
"explicit_workspace_id",
32+
"from_env",
33+
"list_workspaces",
34+
"normalize_host",
35+
"pick_workspace",
36+
]
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@
1515
from hotdata.models.query_response import QueryResponse
1616
from hotdata.models.table_info import TableInfo
1717

18-
from hotdata_core_notebook.env import (
18+
from hotdata_runtime.env import (
1919
default_api_key,
2020
default_host,
2121
default_session_id,
2222
normalize_host,
2323
pick_workspace,
2424
)
25-
from hotdata_core_notebook.result import QueryResult
25+
from hotdata_runtime.result import QueryResult
2626

2727
_TERMINAL = frozenset({"succeeded", "failed", "cancelled"})
2828

hotdata_runtime/health.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from __future__ import annotations
2+
3+
from hotdata.exceptions import ApiException
4+
5+
from hotdata_runtime.client import HotdataClient
6+
7+
8+
def workspace_health_lines(client: HotdataClient) -> tuple[bool, list[str]]:
9+
"""Return ``(ok, parts)`` where ``parts`` are short markdown fragments.
10+
11+
On failure, ``ok`` is False and ``parts`` is a single-element list with the error text.
12+
"""
13+
try:
14+
listing = client.connections().list_connections()
15+
n = len(listing.connections)
16+
lines = [
17+
"**API** reachable",
18+
f"**workspace** `{client.workspace_id}`",
19+
f"**connections** {n}",
20+
]
21+
if client.session_id:
22+
lines.append(f"**sandbox** `{client.session_id}`")
23+
return True, lines
24+
except ApiException as e:
25+
return False, [e.reason or str(e)]
26+
except Exception as e:
27+
return False, [str(e)]

pyproject.toml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ requires = ["hatchling"]
33
build-backend = "hatchling.build"
44

55
[project]
6-
name = "hotdata-core-notebook"
6+
name = "hotdata-runtime"
77
version = "0.1.0"
8-
description = "Hotdata API client and shared models for notebook integrations"
8+
description = "Workspace/session runtime primitives for Hotdata integrations"
99
readme = "README.md"
1010
requires-python = ">=3.10"
1111
license = { text = "MIT" }
@@ -14,13 +14,17 @@ dependencies = [
1414
"pandas>=2.0",
1515
]
1616

17-
[project.optional-dependencies]
17+
[dependency-groups]
1818
dev = [
19+
"packaging>=23",
1920
"pytest>=8.0",
2021
]
2122

23+
[tool.uv]
24+
default-groups = ["dev"]
25+
2226
[tool.hatch.build.targets.wheel]
23-
packages = ["hotdata_core_notebook"]
27+
packages = ["hotdata_runtime"]
2428

2529
[tool.pytest.ini_options]
2630
testpaths = ["tests"]

tests/test_client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
import pytest
88

9-
from hotdata_core_notebook.env import normalize_host, pick_workspace
10-
from hotdata_core_notebook.client import HotdataClient
9+
from hotdata_runtime.env import normalize_host, pick_workspace
10+
from hotdata_runtime.client import HotdataClient
1111

1212

1313
@pytest.mark.parametrize(
@@ -47,7 +47,7 @@ def test_pick_workspace_chooses_first_active(monkeypatch: pytest.MonkeyPatch):
4747
]
4848
listing = SimpleNamespace(workspaces=items)
4949

50-
with patch("hotdata_core_notebook.env.WorkspacesApi") as Api:
50+
with patch("hotdata_runtime.env.WorkspacesApi") as Api:
5151
Api.return_value.list_workspaces.return_value = listing
5252
assert pick_workspace("k", "https://api.hotdata.dev", None) == "ws_2"
5353

@@ -62,7 +62,7 @@ def test_pick_workspace_falls_back_to_first(monkeypatch: pytest.MonkeyPatch):
6262
]
6363
listing = SimpleNamespace(workspaces=items)
6464

65-
with patch("hotdata_core_notebook.env.WorkspacesApi") as Api:
65+
with patch("hotdata_runtime.env.WorkspacesApi") as Api:
6666
Api.return_value.list_workspaces.return_value = listing
6767
assert pick_workspace("k", "https://api.hotdata.dev", None) == "ws_1"
6868

tests/test_health.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from __future__ import annotations
2+
3+
from unittest.mock import patch
4+
5+
from hotdata.exceptions import ApiException
6+
7+
from hotdata_runtime.client import HotdataClient
8+
from hotdata_runtime.health import workspace_health_lines
9+
10+
11+
def test_workspace_health_ok():
12+
client = HotdataClient("k", "ws", host="https://api.hotdata.dev")
13+
listing = type("L", (), {"connections": [object()]})()
14+
15+
class FakeConnectionsApi:
16+
def list_connections(self):
17+
return listing
18+
19+
with patch.object(client, "connections", return_value=FakeConnectionsApi()):
20+
ok, parts = workspace_health_lines(client)
21+
assert ok is True
22+
assert any("reachable" in p for p in parts)
23+
24+
25+
def test_workspace_health_ok_includes_sandbox_when_session_set():
26+
client = HotdataClient(
27+
"k", "ws", host="https://api.hotdata.dev", session_id="sb_test"
28+
)
29+
listing = type("L", (), {"connections": [object()]})()
30+
31+
class FakeConnectionsApi:
32+
def list_connections(self):
33+
return listing
34+
35+
with patch.object(client, "connections", return_value=FakeConnectionsApi()):
36+
ok, parts = workspace_health_lines(client)
37+
assert ok is True
38+
assert any("sandbox" in p and "sb_test" in p for p in parts)
39+
40+
41+
def test_workspace_health_api_error():
42+
client = HotdataClient("k", "ws", host="https://api.hotdata.dev")
43+
44+
class Boom:
45+
def list_connections(self):
46+
raise ApiException(status=500, reason="nope")
47+
48+
with patch.object(client, "connections", return_value=Boom()):
49+
ok, parts = workspace_health_lines(client)
50+
assert ok is False
51+
assert parts == ["nope"]
52+
53+
54+
def test_workspace_health_non_api_error():
55+
client = HotdataClient("k", "ws", host="https://api.hotdata.dev")
56+
57+
class Boom:
58+
def list_connections(self):
59+
raise OSError("connection refused")
60+
61+
with patch.object(client, "connections", return_value=Boom()):
62+
ok, parts = workspace_health_lines(client)
63+
assert ok is False
64+
assert parts == ["connection refused"]

0 commit comments

Comments
 (0)