Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
39 changes: 21 additions & 18 deletions examples/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,41 +43,44 @@ def _(hm, workspace):
)
recent = hm.recent_results(client, limit=20)
history = hm.run_history(client, limit=10)
return browser, client, editor, history, recent, status, workspace
return browser, client, editor, history, recent, status


@app.cell
def _(browser, editor, mo, recent, status, workspace):
return mo.vstack(
[
workspace.ui,
status,
browser.ui,
editor.ui,
recent.ui,
],
gap=2,
)
def _(mo):
mo.md(r"""
## HotData explorer
Use the tabs below to switch between available workspaces, connection status, dataset browsing, and SQL queries.
""")
return


@app.cell
def _(history):
return history
def _(browser, editor, history, mo, recent, status, workspace):
mo.ui.tabs({
"Workspaces": workspace.ui,
"Connections": status,
"Datasets": browser.ui,
"SQL query": editor.ui,
"Recent results": recent.ui,
"Run history": history,
})
return


@app.cell
def _(editor, hm):
def _(editor):
# Explicitly touch nested widget values so Marimo reruns this cell on clicks.
_run = editor.run.value
_rerun = editor.rerun.value
_clear = editor.clear.value
return hm.query_result(editor.result), _clear, _rerun, _run
return


@app.cell
def _(hm, recent):
def _(recent):
_selected = recent.pick.value
return hm.query_result(recent.result, label="Recent result"), _selected
return
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: this looks like a behavior regression that is out of scope for a dedupe refactor. The previous version had these cells return hm.query_result(editor.result) / hm.query_result(recent.result, label="Recent result") — accessing .result is what triggers the SQL execution (and rendered the result component). After this change nothing in the demo accesses editor.result or recent.result, so clicking Run in the SQL editor or picking a recent result produces no visible output and the underlying query is never invoked. If the tab restructuring is intentional, consider re-adding a SQL result / Recent result tab (or rendering the result component inside the existing tabs) so the editor and recent picker still do something. Otherwise this seems orthogonal to the helper-dedupe goal and could move to its own PR. (not blocking)



@app.cell
Expand Down
107 changes: 107 additions & 0 deletions hotdata_marimo/_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Shared dropdown option helpers for Marimo UI widgets."""

from __future__ import annotations

from collections.abc import Callable
from typing import Any

import marimo as mo

from hotdata_runtime import HotdataClient


def unique_label_options(
pairs: list[tuple[str, str]],
*,
disambiguate: Callable[[str, str, int], str] | None = None,
) -> dict[str, str]:
"""Build a label→value map, suffixing repeated labels when needed."""
counts: dict[str, int] = {}
options: dict[str, str] = {}
for label, value in pairs:
count = counts.get(label, 0)
counts[label] = count + 1
if count == 0:
key = label
elif disambiguate is not None:
key = disambiguate(label, value, count)
else:
key = f"{label} ({count + 1})"
options[key] = value
return options


def empty_dropdown(
*,
label: str,
message: str,
full_width: bool = True,
):
return mo.ui.dropdown(
options={message: ""},
label=label,
full_width=full_width,
)


def connection_options(conns: list[Any]) -> dict[str, str]:
pairs = [(str(c.name), str(c.id)) for c in conns]
return unique_label_options(
pairs,
disambiguate=lambda label, value, count: f"{label} ({value})",
)


def connection_picker_from_connections(
conns: list[Any],
*,
label: str = "Connection",
full_width: bool = True,
):
if not conns:
return empty_dropdown(
label=label,
message="(no connections)",
full_width=full_width,
)
return mo.ui.dropdown(
options=connection_options(conns),
label=label,
full_width=full_width,
)


def connection_picker(
client: HotdataClient,
*,
label: str = "Connection",
full_width: bool = True,
):
conns = client.connections().list_connections().connections
return connection_picker_from_connections(
conns,
label=label,
full_width=full_width,
)


def resolve_connection_picker(
client: HotdataClient,
*,
label: str = "Connection",
full_width: bool = True,
) -> tuple[Any | None, str | None]:
"""Return ``(dropdown_or_none, implicit_connection_id)`` for table browsers."""
conns = client.connections().list_connections().connections
if not conns:
return None, ""
if len(conns) == 1:
return None, conns[0].id
return (
connection_picker_from_connections(
conns,
label=label,
full_width=full_width,
),
None,
)
27 changes: 10 additions & 17 deletions hotdata_marimo/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,7 @@

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
from hotdata_marimo._options import empty_dropdown, unique_label_options


def query_result(
Expand Down Expand Up @@ -76,11 +65,15 @@ def __init__(self, client: HotdataClient, *, limit: int = 50) -> None:
(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",
full_width=True,
options = unique_label_options(option_pairs)
self.pick = (
empty_dropdown(label="Recent results", message="(no results)")
if not options
else mo.ui.dropdown(
options=options,
label="Recent results",
full_width=True,
)
)

@property
Expand Down
14 changes: 5 additions & 9 deletions hotdata_marimo/sql_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ def __init__(
) -> None:
super().__init__(connection, engine_name)
self._connections_cache: list[Any] | None = None
self._connection_id_cache: dict[str, str] | None = None

@property
def source(self) -> str:
Expand Down Expand Up @@ -71,15 +70,12 @@ def _resolve_should_auto_discover(
return True
return value

def _connection_ids(self) -> dict[str, str]:
if self._connection_id_cache is None:
self._connection_id_cache = {
str(c.name): str(c.id) for c in self._connections()
}
return self._connection_id_cache

def _connection_id(self, connection_name: str) -> str | None:
return self._connection_ids().get(connection_name)
try:
return self._connection.connection_id_by_name().get(connection_name)
except RuntimeError as e:
LOGGER.warning("%s", e)
return None
Comment on lines 73 to +78
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: this drops the local _connection_id_cache and now calls self._connection.connection_id_by_name() on every invocation. _connection_id is hit from get_schemas, get_tables_in_schema, and get_table_details — each of which can fire several times per render — so unless connection_id_by_name() is cheap or memoized on the runtime side, this will multiply backend calls during catalog browsing. Worth confirming the runtime caches the mapping, or memoize locally (e.g., one cached dict reused alongside _connections_cache). (not blocking)


def _connections(self) -> list[Any]:
if self._connections_cache is None:
Expand Down
99 changes: 33 additions & 66 deletions hotdata_marimo/table_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,11 @@

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


def connection_picker(
client: HotdataClient,
*,
label: str = "Connection",
full_width: bool = True,
):
listing = client.connections().list_connections()
conns = listing.connections
if not conns:
return mo.ui.dropdown(
options={"(no connections)": ""},
label=label,
full_width=full_width,
)
options = _connection_options(conns)
return mo.ui.dropdown(
options=options,
label=label,
full_width=full_width,
)
from hotdata_marimo._options import (
connection_picker,
empty_dropdown,
resolve_connection_picker,
)
Comment on lines +9 to +13
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

super nit: connection_picker isn't referenced anywhere in this module's body — it's imported solely so that from hotdata_marimo.table_browser import connection_picker in __init__.py keeps working. That's fine, but a one-line comment (or # re-exported for backwards-compat) would make the intent obvious to a future reader who might otherwise drop it as an unused import. (not blocking)



class TableBrowser:
Expand Down Expand Up @@ -66,44 +38,40 @@ def __init__(
self._implicit_connection_id: str | None = None

if self._override_connection_id is None:
listing = client.connections().list_connections()
conns = listing.connections
if len(conns) > 1:
self._conn_pick = connection_picker(client)
elif len(conns) == 1:
self._implicit_connection_id = conns[0].id
else:
self._implicit_connection_id = ""
self._conn_pick, self._implicit_connection_id = resolve_connection_picker(
client
)

self._table_pick_ctx: str | None = None
self._init_table_pick()

def _init_table_pick(self) -> None:
if self._conn_pick is not None:
self.table_pick = mo.ui.dropdown(
options={"(select connection above)": ""},
self.table_pick = empty_dropdown(
label="Table",
full_width=True,
message="(select connection above)",
)
self._empty_catalog = True
self._all_names = []
return

names = self._names_for_active_connection()
self._all_names = names
if not names:
self.table_pick = empty_dropdown(
label="Table",
message="(no tables in catalog)",
)
self._empty_catalog = True
self._all_names: list[str] = []
else:
names = self._names_for_active_connection()
self._all_names = names
if not names:
self.table_pick = mo.ui.dropdown(
options={"(no tables in catalog)": ""},
label="Table",
full_width=True,
)
self._empty_catalog = True
else:
self._empty_catalog = False
self.table_pick = mo.ui.dropdown(
options={n: n for n in names},
label="Table",
full_width=True,
searchable=True,
)
self._table_pick_ctx = self._active_connection_id()
self._empty_catalog = False
self.table_pick = mo.ui.dropdown(
options={n: n for n in names},
label="Table",
full_width=True,
searchable=True,
)
self._table_pick_ctx = self._active_connection_id()

def _active_connection_id(self) -> str | None:
if self._override_connection_id is not None:
Expand All @@ -128,10 +96,9 @@ def _rebuild_table_pick(self, names: list[str]) -> None:
self._all_names = names
if not names:
self._empty_catalog = True
self.table_pick = mo.ui.dropdown(
options={"(no tables in catalog)": ""},
self.table_pick = empty_dropdown(
label="Table",
full_width=True,
message="(no tables in catalog)",
)
else:
self._empty_catalog = False
Expand Down
Loading
Loading