Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,17 @@ Keep the editor in one cell and consume `editor.result` in another. The editor c

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

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.
See `examples/hotdata_basic.py` for a full runnable notebook flow.

## Examples

- `examples/hotdata_basic.py` — end-to-end browser + editor + result rendering flow.

Run:

```bash
uv run marimo edit examples/hotdata_basic.py --no-token
```

## Layout

Expand Down
29 changes: 6 additions & 23 deletions examples/hotdata_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,10 @@ def _():
@app.cell
def _(hm, mo, os):
mo.stop(
not (
os.environ.get("HOTDATA_API_KEY")
or os.environ.get("HOTDATA_TOKEN")
),
not os.environ.get("HOTDATA_API_KEY"),
mo.callout(
mo.md(
"Add **HOTDATA_API_KEY** (or **HOTDATA_TOKEN**) to your environment "
"Add **HOTDATA_API_KEY** to your environment "
"to run this example."
),
kind="warn",
Expand All @@ -36,30 +33,17 @@ def _(hm, mo, os):

@app.cell
def _(client, hm, mo):
id_map = client.connection_id_by_name()
tpch_id = id_map.get("tpch")
mo.stop(
not tpch_id,
mo.callout(
mo.md(
"This example expects a connection named **tpch**. "
"Create it in Hotdata or adjust the name in the notebook."
),
kind="warn",
),
)
browser = hm.table_browser(client, connection_id=tpch_id)
browser = hm.table_browser(client)
editor = hm.sql_editor(
client,
default_sql="SELECT * FROM tpch.tpch_sf1.nation LIMIT 5",
default_sql="SELECT 1 AS ok",
)
return browser, editor


@app.cell
def _(browser, editor, mo):
mo.vstack([browser.ui, editor.ui], gap=2)
return
return mo.vstack([browser.ui, editor.ui], gap=2)


@app.cell
Expand All @@ -68,8 +52,7 @@ def _(editor, hm):
_run = editor.run.value
_rerun = editor.rerun.value
_clear = editor.clear.value
hm.query_result(editor.result)
return _clear, _rerun, _run
return hm.query_result(editor.result), _clear, _rerun, _run


if __name__ == "__main__":
Expand Down
62 changes: 37 additions & 25 deletions hotdata_marimo/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,20 @@

import marimo as mo

from hotdata_runtime.client import HotdataClient
from hotdata_runtime.health import workspace_health_lines
from hotdata_runtime.result import QueryResult
from hotdata_runtime import HotdataClient, QueryResult, workspace_health_lines


def _option_map_with_unique_labels(
pairs: list[tuple[str, str]],
) -> dict[str, str]:
counts: dict[str, int] = {}
options: dict[str, str] = {}
for label, value in pairs:
count = counts.get(label, 0)
counts[label] = count + 1
key = label if count == 0 else f"{label} ({count + 1})"
options[key] = value
return options


def query_result(
Expand All @@ -27,17 +38,18 @@ def query_result(
)
else:
trunc = None
meta = result.metadata_dict()
meta_bits = []
if result.result_id:
meta_bits.append(f"**result_id** `{result.result_id}`")
if result.query_run_id:
meta_bits.append(f"**query_run_id** `{result.query_run_id}`")
if result.execution_time_ms is not None:
meta_bits.append(f"**execution_time_ms** {result.execution_time_ms}")
if result.warning:
meta_bits.append(f"**warning** {result.warning}")
if result.error_message:
meta_bits.append(f"**error** {result.error_message}")
if meta["result_id"]:
meta_bits.append(f"**result_id** `{meta['result_id']}`")
if meta["query_run_id"]:
meta_bits.append(f"**query_run_id** `{meta['query_run_id']}`")
if meta["execution_time_ms"] is not None:
meta_bits.append(f"**execution_time_ms** {meta['execution_time_ms']}")
if meta["warning"]:
meta_bits.append(f"**warning** {meta['warning']}")
if meta["error_message"]:
meta_bits.append(f"**error** {meta['error_message']}")
header = mo.md(" · ".join(meta_bits) if meta_bits else "_No metadata._")
df = result.to_pandas()
tbl = mo.ui.table(
Expand All @@ -59,11 +71,12 @@ def query_result(
class RecentResults:
def __init__(self, client: HotdataClient, *, limit: int = 50) -> None:
self._client = client
listing = client.results().list_results(limit=limit, offset=0)
self._results = listing.results
options = {
f"{r.created_at} · {r.status} · {r.id}": r.id for r in self._results
}
self._results = client.list_recent_results(limit=limit, offset=0)
option_pairs = [
(f"{r.created_at} · {r.status} · {r.result_id}", r.result_id)
for r in self._results
]
options = _option_map_with_unique_labels(option_pairs)
self.pick = mo.ui.dropdown(
options=options or {"(no results)": ""},
label="Recent results",
Expand Down Expand Up @@ -97,20 +110,19 @@ def run_history(
limit: int = 20,
label: str = "Run history",
):
runs = client.query_runs().list_query_runs(limit=limit).query_runs
runs = client.list_run_history(limit=limit)
if not runs:
return mo.md("_No query runs returned._")

rows: list[dict[str, object]] = []
for r in runs:
rows.append(
{
"created_at": getattr(r, "created_at", None),
"status": getattr(r, "status", None),
"execution_time_ms": getattr(r, "execution_time_ms", None),
"result_id": getattr(r, "result_id", None),
"query_run_id": getattr(r, "id", None)
or getattr(r, "query_run_id", None),
"created_at": r.created_at,
"status": r.status,
"execution_time_ms": r.execution_time_ms,
"result_id": r.result_id,
"query_run_id": r.query_run_id,
}
)

Expand Down
3 changes: 1 addition & 2 deletions hotdata_marimo/sql_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

import marimo as mo

from hotdata_runtime.client import HotdataClient
from hotdata_runtime.result import QueryResult
from hotdata_runtime import HotdataClient, QueryResult


class SqlEditor:
Expand Down
21 changes: 18 additions & 3 deletions hotdata_marimo/table_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,19 @@

import marimo as mo

from hotdata_runtime.client import HotdataClient
from hotdata_runtime import HotdataClient


def _connection_options(conns: list[Any]) -> dict[str, str]:
counts: dict[str, int] = {}
options: dict[str, str] = {}
for c in conns:
label = c.name
count = counts.get(label, 0)
counts[label] = count + 1
key = label if count == 0 else f"{label} ({c.id})"
options[key] = c.id
return options
Comment thread
eddietejeda marked this conversation as resolved.


def connection_picker(
Expand All @@ -21,7 +33,7 @@ def connection_picker(
label=label,
full_width=full_width,
)
options = {c.name: c.id for c in conns}
options = _connection_options(conns)
return mo.ui.dropdown(
options=options,
label=label,
Expand Down Expand Up @@ -182,7 +194,10 @@ def ui(self):
stack.append(self.table_pick)
return mo.vstack(stack, gap=1)

cols = self._client.columns_for_qualified(sel)
cols = self._client.columns_for_qualified(
sel,
connection_id=self.selected_connection_id,
)
if not cols:
body = mo.md("_No column metadata returned (check catalog sync)._")
else:
Expand Down
27 changes: 10 additions & 17 deletions hotdata_marimo/workspace_selector.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
from __future__ import annotations

import marimo as mo
from hotdata_runtime.client import HotdataClient
from hotdata_runtime.env import (
from hotdata_runtime import (
HotdataClient,
default_api_key,
default_host,
default_session_id,
explicit_workspace_id,
list_workspaces,
resolve_workspace_selection,
)


Expand All @@ -25,25 +24,19 @@ def __init__(
self._api_key = api_key
self._host = host or default_host()
self._session_id = session_id
self._explicit = explicit_workspace_id()

workspaces = list_workspaces(api_key, self._host, session_id)
if not workspaces:
raise RuntimeError("No Hotdata workspaces found for this API key.")

selection = resolve_workspace_selection(api_key, self._host, session_id)
self._explicit = selection.source == "explicit_env"
if self._explicit:
self._pick = None
self._workspace_id = self._explicit
self._workspace_id = selection.workspace_id
return

workspaces = selection.workspaces
if len(workspaces) == 1:
self._pick = None
self._workspace_id = workspaces[0].public_id
return

active = [w for w in workspaces if w.active]
chosen = active[0] if active else workspaces[0]

labels: list[tuple[str, str]] = []
seen: set[str] = set()
for w in workspaces:
Expand All @@ -52,10 +45,10 @@ def __init__(
seen.add(base)
labels.append((label_text, w.public_id))

labels.sort(key=lambda t: 0 if t[1] == chosen.public_id else 1)
labels.sort(key=lambda t: 0 if t[1] == selection.workspace_id else 1)
options = {k: v for k, v in labels}
self._pick = mo.ui.dropdown(options=options, label=label, full_width=True)
self._workspace_id = chosen.public_id
self._workspace_id = selection.workspace_id
Comment on lines 34 to +51
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: the prior code raised RuntimeError("No Hotdata workspaces found for this API key.") before reaching the picker logic. With this refactor, if resolve_workspace_selection returns source != "explicit_env" with an empty workspaces list, neither branch fires and we fall through to build a dropdown with options={} (and selection.workspace_id is whatever the runtime defaulted it to). Either confirm the runtime contract guarantees a non-empty list / raises for this case, or restore an explicit guard here so the failure mode stays a clear error rather than a broken dropdown. (not blocking)


@property
def workspace_id(self) -> str:
Expand Down Expand Up @@ -84,7 +77,7 @@ def ui(self):
def workspace_selector_from_env(*, label: str = "Workspace") -> WorkspaceSelector:
api_key = default_api_key()
if not api_key:
raise RuntimeError("HOTDATA_API_KEY or HOTDATA_TOKEN must be set.")
raise RuntimeError("HOTDATA_API_KEY must be set.")
host = default_host()
session = default_session_id()
return WorkspaceSelector(
Expand Down
26 changes: 26 additions & 0 deletions tests/test_architecture_guardrails.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from __future__ import annotations

import re
from pathlib import Path


REPO_ROOT = Path(__file__).resolve().parents[1]
SOURCE_ROOT = REPO_ROOT / "hotdata_marimo"


def test_source_uses_hotdata_runtime_root_imports() -> None:
violations: list[str] = []
pattern = re.compile(
r"(?m)^\s*(?:from\s+hotdata_runtime\.(client|env|result|health)\s+import"
r"|import\s+hotdata_runtime\.(client|env|result|health)(?:\s|$|,|as))"
)

for path in SOURCE_ROOT.rglob("*.py"):
text = path.read_text(encoding="utf-8")
if pattern.search(text):
violations.append(str(path.relative_to(REPO_ROOT)))

assert not violations, (
"Use `from hotdata_runtime import ...` in package source; "
f"found submodule imports in: {', '.join(violations)}"
)
31 changes: 31 additions & 0 deletions tests/test_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from __future__ import annotations

from types import SimpleNamespace

from hotdata_marimo.display import _option_map_with_unique_labels
from hotdata_marimo.table_browser import _connection_options


def test_option_map_with_unique_labels_keeps_all_values():
options = _option_map_with_unique_labels(
[("dup", "a"), ("dup", "b"), ("dup", "c")]
)
assert options == {
"dup": "a",
"dup (2)": "b",
"dup (3)": "c",
}


def test_connection_options_disambiguates_duplicate_names():
conns = [
SimpleNamespace(name="Warehouse", id="conn_1"),
SimpleNamespace(name="Warehouse", id="conn_2"),
SimpleNamespace(name="Analytics", id="conn_3"),
]
options = _connection_options(conns)
assert options == {
"Warehouse": "conn_1",
"Warehouse (conn_2)": "conn_2",
"Analytics": "conn_3",
}
5 changes: 4 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading