Skip to content

Commit 538b292

Browse files
authored
Merge pull request #1 from hotdata-dev/feat/use-runtime-workspace-selection
Use runtime workspace resolver in Marimo
2 parents b2eaf75 + 77feaab commit 538b292

14 files changed

Lines changed: 697 additions & 127 deletions

README.md

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
Marimo UI helpers for [Hotdata](https://hotdata.dev): run SQL from a notebook, browse catalog metadata, and render results as tables.
44

5+
## Features
6+
7+
- **Workspace-aware setup** — build a `HotdataClient` from environment variables, or use `workspace_selector_from_env()` to choose a workspace interactively when no workspace is pinned.
8+
- **Connection health** — show a compact status callout with API, workspace, and optional sandbox context.
9+
- **Catalog browsing** — browse Hotdata connections, schemas, tables, and columns from Marimo UI controls.
10+
- **SQL editor widget** — run SQL against Hotdata, cache the latest successful result, and render results in downstream reactive cells.
11+
- **Native `mo.sql` engine** — register `HotdataMarimoEngine` so Marimo SQL cells can execute through a live `HotdataClient` with `engine=client`.
12+
- **Result display helpers** — render query results, recent results, and run history as notebook-friendly UI.
13+
- **Marimo UI aliases** — importing `hotdata_marimo` attaches helpers such as `mo.ui.hotdata_sql_editor` and `mo.ui.hotdata_table_browser` for discoverability.
14+
515
## Install
616

717
```bash
@@ -39,13 +49,45 @@ Importing `hotdata_marimo` registers discoverability aliases on Marimo’s UI na
3949

4050
Use `hm.connection_status(client)` (or `mo.ui.hotdata_connection_status(client)`) for a small API/workspace health callout.
4151

52+
## Marimo SQL Cells
53+
54+
Register the Hotdata SQL engine once during setup, then pass a `HotdataClient` to Marimo SQL cells:
55+
56+
```python
57+
import hotdata_marimo as hm
58+
59+
hm.register_hotdata_sql_engine()
60+
client = hm.from_env()
61+
```
62+
63+
```python
64+
_df = mo.sql(
65+
"""
66+
SELECT 1 AS example_value
67+
""",
68+
engine=client,
69+
)
70+
```
71+
72+
The engine also exposes Hotdata catalog metadata to Marimo's data-source UI. Hotdata connections are labeled **Hotdata** in the SQL connection picker.
73+
4274
## Two-cell pattern
4375

4476
Keep the editor in one cell and consume `editor.result` in another. The editor caches the last successful run so downstream cells do not re-query the API on every refresh; click **Run on Hotdata** again after you change SQL. While a query is running, a Marimo status spinner is shown.
4577

4678
Marimo only shows **what you `return` from a cell**. Calling `mo.vstack(...)` or `hm.query_result(...)` without returning it produces no visible output.
4779

48-
See `examples/hotdata_basic.py` for a full notebook: five Python cells (`mo.vstack` for **controls only**, then a separate cell `return hm.query_result(editor.result)` so results show immediately — **avoid** `mo.lazy` here: it only renders after the block scrolls into view, which looks like an empty cell). If Marimo shows **empty cells**, quit and remove `examples/__marimo__/` so the UI reloads from the `.py` file only.
80+
See `examples/demo.py` for a full runnable notebook flow.
81+
82+
## Examples
83+
84+
- `examples/demo.py` — end-to-end browser + editor + result rendering flow.
85+
86+
Run:
87+
88+
```bash
89+
uv run marimo edit examples/demo.py --no-token
90+
```
4991

5092
## Layout
5193

@@ -58,7 +100,7 @@ This package depends on [**hotdata-runtime**](https://github.com/hotdata-dev/hot
58100
```bash
59101
uv sync --locked
60102
uv run pytest
61-
marimo edit examples/hotdata_basic.py --no-token
103+
marimo edit examples/demo.py --no-token
62104
```
63105

64106
To pin **hotdata-runtime** from Git instead of the sibling path, remove the `[tool.uv.sources]` block, set the dependency line as needed, and run `uv lock` again.

examples/demo.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import marimo
2+
3+
__generated_with = "0.23.5"
4+
app = marimo.App()
5+
6+
7+
@app.cell
8+
def _():
9+
import os
10+
11+
import marimo as mo
12+
13+
import hotdata_marimo as hm
14+
15+
hm.register_hotdata_sql_engine()
16+
return hm, mo, os
17+
18+
19+
@app.cell
20+
def _(hm, mo, os):
21+
mo.stop(
22+
not os.environ.get("HOTDATA_API_KEY"),
23+
mo.callout(
24+
mo.md(
25+
"Add **HOTDATA_API_KEY** to your environment "
26+
"to run this example."
27+
),
28+
kind="warn",
29+
),
30+
)
31+
workspace = hm.workspace_selector_from_env()
32+
return (workspace,)
33+
34+
35+
@app.cell
36+
def _(hm, workspace):
37+
client = workspace.client
38+
status = hm.connection_status(client)
39+
browser = hm.table_browser(client)
40+
editor = hm.sql_editor(
41+
client,
42+
default_sql="SELECT 1 AS ok",
43+
)
44+
recent = hm.recent_results(client, limit=20)
45+
history = hm.run_history(client, limit=10)
46+
return browser, client, editor, history, recent, status, workspace
47+
48+
49+
@app.cell
50+
def _(browser, editor, mo, recent, status, workspace):
51+
return mo.vstack(
52+
[
53+
workspace.ui,
54+
status,
55+
browser.ui,
56+
editor.ui,
57+
recent.ui,
58+
],
59+
gap=2,
60+
)
61+
62+
63+
@app.cell
64+
def _(history):
65+
return history
66+
67+
68+
@app.cell
69+
def _(editor, hm):
70+
# Explicitly touch nested widget values so Marimo reruns this cell on clicks.
71+
_run = editor.run.value
72+
_rerun = editor.rerun.value
73+
_clear = editor.clear.value
74+
return hm.query_result(editor.result), _clear, _rerun, _run
75+
76+
77+
@app.cell
78+
def _(hm, recent):
79+
_selected = recent.pick.value
80+
return hm.query_result(recent.result, label="Recent result"), _selected
81+
82+
83+
@app.cell
84+
def _(client, mo):
85+
_df = mo.sql(
86+
"""
87+
SELECT 1 AS example_value
88+
""",
89+
engine=client,
90+
)
91+
return
92+
93+
94+
if __name__ == "__main__":
95+
app.run()

examples/hotdata_basic.py

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

hotdata_marimo/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,19 @@
1616
recent_results,
1717
run_history,
1818
)
19+
from hotdata_marimo.sql_engine import (
20+
HotdataMarimoEngine,
21+
register_hotdata_sql_engine,
22+
unregister_hotdata_sql_engine,
23+
)
1924
from hotdata_marimo.sql_editor import SqlEditor, sql_editor
2025
from hotdata_marimo.table_browser import TableBrowser, connection_picker, table_browser
2126
from hotdata_marimo.workspace_selector import WorkspaceSelector, workspace_selector_from_env
2227

2328
__all__ = [
2429
"__version__",
2530
"HotdataClient",
31+
"HotdataMarimoEngine",
2632
"QueryResult",
2733
"RecentResults",
2834
"SqlEditor",
@@ -39,11 +45,13 @@
3945
"hotdata_workspace_selector",
4046
"query_result",
4147
"recent_results",
48+
"register_hotdata_sql_engine",
49+
"register_mo_ui_hotdata_aliases",
4250
"run_history",
4351
"sql_editor",
4452
"table_browser",
53+
"unregister_hotdata_sql_engine",
4554
"workspace_selector_from_env",
46-
"register_mo_ui_hotdata_aliases",
4755
]
4856

4957
hotdata_sql_editor = sql_editor

hotdata_marimo/display.py

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,20 @@
44

55
import marimo as mo
66

7-
from hotdata_runtime.client import HotdataClient
8-
from hotdata_runtime.health import workspace_health_lines
9-
from hotdata_runtime.result import QueryResult
7+
from hotdata_runtime import HotdataClient, QueryResult, workspace_health_lines
8+
9+
10+
def _option_map_with_unique_labels(
11+
pairs: list[tuple[str, str]],
12+
) -> dict[str, str]:
13+
counts: dict[str, int] = {}
14+
options: dict[str, str] = {}
15+
for label, value in pairs:
16+
count = counts.get(label, 0)
17+
counts[label] = count + 1
18+
key = label if count == 0 else f"{label} ({count + 1})"
19+
options[key] = value
20+
return options
1021

1122

1223
def query_result(
@@ -27,17 +38,18 @@ def query_result(
2738
)
2839
else:
2940
trunc = None
41+
meta = result.metadata_dict()
3042
meta_bits = []
31-
if result.result_id:
32-
meta_bits.append(f"**result_id** `{result.result_id}`")
33-
if result.query_run_id:
34-
meta_bits.append(f"**query_run_id** `{result.query_run_id}`")
35-
if result.execution_time_ms is not None:
36-
meta_bits.append(f"**execution_time_ms** {result.execution_time_ms}")
37-
if result.warning:
38-
meta_bits.append(f"**warning** {result.warning}")
39-
if result.error_message:
40-
meta_bits.append(f"**error** {result.error_message}")
43+
if meta["result_id"]:
44+
meta_bits.append(f"**result_id** `{meta['result_id']}`")
45+
if meta["query_run_id"]:
46+
meta_bits.append(f"**query_run_id** `{meta['query_run_id']}`")
47+
if meta["execution_time_ms"] is not None:
48+
meta_bits.append(f"**execution_time_ms** {meta['execution_time_ms']}")
49+
if meta["warning"]:
50+
meta_bits.append(f"**warning** {meta['warning']}")
51+
if meta["error_message"]:
52+
meta_bits.append(f"**error** {meta['error_message']}")
4153
header = mo.md(" · ".join(meta_bits) if meta_bits else "_No metadata._")
4254
df = result.to_pandas()
4355
tbl = mo.ui.table(
@@ -59,11 +71,12 @@ def query_result(
5971
class RecentResults:
6072
def __init__(self, client: HotdataClient, *, limit: int = 50) -> None:
6173
self._client = client
62-
listing = client.results().list_results(limit=limit, offset=0)
63-
self._results = listing.results
64-
options = {
65-
f"{r.created_at} · {r.status} · {r.id}": r.id for r in self._results
66-
}
74+
self._results = client.list_recent_results(limit=limit, offset=0)
75+
option_pairs = [
76+
(f"{r.created_at} · {r.status} · {r.result_id}", r.result_id)
77+
for r in self._results
78+
]
79+
options = _option_map_with_unique_labels(option_pairs)
6780
self.pick = mo.ui.dropdown(
6881
options=options or {"(no results)": ""},
6982
label="Recent results",
@@ -97,20 +110,19 @@ def run_history(
97110
limit: int = 20,
98111
label: str = "Run history",
99112
):
100-
runs = client.query_runs().list_query_runs(limit=limit).query_runs
113+
runs = client.list_run_history(limit=limit)
101114
if not runs:
102115
return mo.md("_No query runs returned._")
103116

104117
rows: list[dict[str, object]] = []
105118
for r in runs:
106119
rows.append(
107120
{
108-
"created_at": getattr(r, "created_at", None),
109-
"status": getattr(r, "status", None),
110-
"execution_time_ms": getattr(r, "execution_time_ms", None),
111-
"result_id": getattr(r, "result_id", None),
112-
"query_run_id": getattr(r, "id", None)
113-
or getattr(r, "query_run_id", None),
121+
"created_at": r.created_at,
122+
"status": r.status,
123+
"execution_time_ms": r.execution_time_ms,
124+
"result_id": r.result_id,
125+
"query_run_id": r.query_run_id,
114126
}
115127
)
116128

hotdata_marimo/sql_editor.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
import marimo as mo
44

5-
from hotdata_runtime.client import HotdataClient
6-
from hotdata_runtime.result import QueryResult
5+
from hotdata_runtime import HotdataClient, QueryResult
76

87

98
class SqlEditor:

0 commit comments

Comments
 (0)