hotdata-marimo 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,80 @@
1
+ """Marimo-native UI and helpers for Hotdata (built on hotdata-runtime)."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("hotdata-marimo")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0+unknown"
9
+
10
+ from hotdata_runtime import HotdataClient, QueryResult, from_env
11
+
12
+ from hotdata_marimo.display import (
13
+ RecentResults,
14
+ connection_status,
15
+ connections_panel,
16
+ query_result,
17
+ recent_results,
18
+ run_history,
19
+ )
20
+ from hotdata_marimo.sql_engine import (
21
+ HotdataMarimoEngine,
22
+ register_hotdata_sql_engine,
23
+ unregister_hotdata_sql_engine,
24
+ )
25
+ from hotdata_marimo.sql_editor import SqlEditor, sql_editor
26
+ from hotdata_marimo.table_browser import TableBrowser, connection_picker, table_browser
27
+ from hotdata_marimo.workspace_selector import WorkspaceSelector, workspace_selector_from_env
28
+
29
+ __all__ = [
30
+ "__version__",
31
+ "HotdataClient",
32
+ "HotdataMarimoEngine",
33
+ "QueryResult",
34
+ "RecentResults",
35
+ "SqlEditor",
36
+ "TableBrowser",
37
+ "WorkspaceSelector",
38
+ "connection_picker",
39
+ "connection_status",
40
+ "connections_panel",
41
+ "from_env",
42
+ "hotdata_connection_picker",
43
+ "hotdata_query_result",
44
+ "hotdata_recent_results",
45
+ "hotdata_sql_editor",
46
+ "hotdata_table_browser",
47
+ "hotdata_workspace_selector",
48
+ "query_result",
49
+ "recent_results",
50
+ "register_hotdata_sql_engine",
51
+ "register_mo_ui_hotdata_aliases",
52
+ "run_history",
53
+ "sql_editor",
54
+ "table_browser",
55
+ "unregister_hotdata_sql_engine",
56
+ "workspace_selector_from_env",
57
+ ]
58
+
59
+ hotdata_sql_editor = sql_editor
60
+ hotdata_table_browser = table_browser
61
+ hotdata_query_result = query_result
62
+ hotdata_connection_picker = connection_picker
63
+ hotdata_workspace_selector = workspace_selector_from_env
64
+ hotdata_recent_results = recent_results
65
+
66
+
67
+ def register_mo_ui_hotdata_aliases() -> None:
68
+ """Attach Hotdata helpers to ``marimo.ui`` for discoverability (``mo.ui.hotdata_*``)."""
69
+ import marimo as mo
70
+
71
+ mo.ui.hotdata_sql_editor = hotdata_sql_editor # type: ignore[attr-defined]
72
+ mo.ui.hotdata_table_browser = hotdata_table_browser # type: ignore[attr-defined]
73
+ mo.ui.hotdata_query_result = hotdata_query_result # type: ignore[attr-defined]
74
+ mo.ui.hotdata_connection_status = connection_status # type: ignore[attr-defined]
75
+ mo.ui.hotdata_connection_picker = hotdata_connection_picker # type: ignore[attr-defined]
76
+ mo.ui.hotdata_workspace_selector = hotdata_workspace_selector # type: ignore[attr-defined]
77
+ mo.ui.hotdata_recent_results = hotdata_recent_results # type: ignore[attr-defined]
78
+
79
+
80
+ register_mo_ui_hotdata_aliases()
@@ -0,0 +1,107 @@
1
+ """Shared dropdown option helpers for Marimo UI widgets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import Any
7
+
8
+ import marimo as mo
9
+
10
+ from hotdata_runtime import HotdataClient
11
+
12
+
13
+ def unique_label_options(
14
+ pairs: list[tuple[str, str]],
15
+ *,
16
+ disambiguate: Callable[[str, str, int], str] | None = None,
17
+ ) -> dict[str, str]:
18
+ """Build a label→value map, suffixing repeated labels when needed."""
19
+ counts: dict[str, int] = {}
20
+ options: dict[str, str] = {}
21
+ for label, value in pairs:
22
+ count = counts.get(label, 0)
23
+ counts[label] = count + 1
24
+ if count == 0:
25
+ key = label
26
+ elif disambiguate is not None:
27
+ key = disambiguate(label, value, count)
28
+ else:
29
+ key = f"{label} ({count + 1})"
30
+ options[key] = value
31
+ return options
32
+
33
+
34
+ def empty_dropdown(
35
+ *,
36
+ label: str,
37
+ message: str,
38
+ full_width: bool = True,
39
+ ):
40
+ return mo.ui.dropdown(
41
+ options={message: ""},
42
+ label=label,
43
+ full_width=full_width,
44
+ )
45
+
46
+
47
+ def connection_options(conns: list[Any]) -> dict[str, str]:
48
+ pairs = [(str(c.name), str(c.id)) for c in conns]
49
+ return unique_label_options(
50
+ pairs,
51
+ disambiguate=lambda label, value, count: f"{label} ({value})",
52
+ )
53
+
54
+
55
+ def connection_picker_from_connections(
56
+ conns: list[Any],
57
+ *,
58
+ label: str = "Connection",
59
+ full_width: bool = True,
60
+ ):
61
+ if not conns:
62
+ return empty_dropdown(
63
+ label=label,
64
+ message="(no connections)",
65
+ full_width=full_width,
66
+ )
67
+ return mo.ui.dropdown(
68
+ options=connection_options(conns),
69
+ label=label,
70
+ full_width=full_width,
71
+ )
72
+
73
+
74
+ def connection_picker(
75
+ client: HotdataClient,
76
+ *,
77
+ label: str = "Connection",
78
+ full_width: bool = True,
79
+ ):
80
+ conns = client.connections().list_connections().connections
81
+ return connection_picker_from_connections(
82
+ conns,
83
+ label=label,
84
+ full_width=full_width,
85
+ )
86
+
87
+
88
+ def resolve_connection_picker(
89
+ client: HotdataClient,
90
+ *,
91
+ label: str = "Connection",
92
+ full_width: bool = True,
93
+ ) -> tuple[Any | None, str | None]:
94
+ """Return ``(dropdown_or_none, implicit_connection_id)`` for table browsers."""
95
+ conns = client.connections().list_connections().connections
96
+ if not conns:
97
+ return None, ""
98
+ if len(conns) == 1:
99
+ return None, conns[0].id
100
+ return (
101
+ connection_picker_from_connections(
102
+ conns,
103
+ label=label,
104
+ full_width=full_width,
105
+ ),
106
+ None,
107
+ )
@@ -0,0 +1,209 @@
1
+ """Marimo views for query results, history, and workspace status."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import marimo as mo
6
+
7
+ from hotdata_runtime import HotdataClient, QueryResult, workspace_health_lines
8
+
9
+
10
+ def query_result(
11
+ result: QueryResult,
12
+ *,
13
+ label: str = "Hotdata result",
14
+ page_size: int = 25,
15
+ max_height: int = 480,
16
+ ):
17
+ shown_rows = len(result.rows)
18
+ if result.row_count > shown_rows and shown_rows > 0:
19
+ trunc = mo.callout(
20
+ mo.md(
21
+ f"Showing **{shown_rows}** of **{result.row_count}** rows. "
22
+ "Consider adding a `LIMIT` clause for faster iteration."
23
+ ),
24
+ kind="warn",
25
+ )
26
+ else:
27
+ trunc = None
28
+ meta = result.metadata_dict()
29
+ meta_bits = []
30
+ if meta["result_id"]:
31
+ meta_bits.append(f"**result_id** `{meta['result_id']}`")
32
+ if meta["query_run_id"]:
33
+ meta_bits.append(f"**query_run_id** `{meta['query_run_id']}`")
34
+ if meta["execution_time_ms"] is not None:
35
+ meta_bits.append(f"**execution_time_ms** {meta['execution_time_ms']}")
36
+ if meta["warning"]:
37
+ meta_bits.append(f"**warning** {meta['warning']}")
38
+ if meta["error_message"]:
39
+ meta_bits.append(f"**error** {meta['error_message']}")
40
+ header = mo.md(" · ".join(meta_bits) if meta_bits else "_No metadata._")
41
+ df = result.to_pandas()
42
+ tbl = mo.ui.table(
43
+ df,
44
+ label=label,
45
+ pagination=True,
46
+ page_size=page_size,
47
+ selection=None,
48
+ max_height=max_height,
49
+ )
50
+ summary = mo.md(f"**{result.row_count}** rows · **{len(result.columns)}** columns")
51
+ bits = [summary]
52
+ if trunc is not None:
53
+ bits.append(trunc)
54
+ bits.extend([header, tbl])
55
+ return mo.vstack(bits, gap=1)
56
+
57
+
58
+ class RecentResults:
59
+ def __init__(self, client: HotdataClient, *, limit: int = 50) -> None:
60
+ self._client = client
61
+ self._results = client.list_recent_results(limit=limit, offset=0)
62
+ self._rows: list[dict[str, object]] = [
63
+ {
64
+ "created_at": r.created_at,
65
+ "status": r.status,
66
+ "result_id": r.result_id,
67
+ }
68
+ for r in self._results
69
+ ]
70
+ self.table = (
71
+ mo.ui.table(
72
+ self._rows,
73
+ label="Recent results",
74
+ pagination=True,
75
+ page_size=min(10, limit),
76
+ selection="single",
77
+ max_height=320,
78
+ )
79
+ if self._rows
80
+ else None
81
+ )
82
+
83
+ @property
84
+ def selected_result_id(self) -> str | None:
85
+ if self.table is None:
86
+ return None
87
+ selected = self.table.value
88
+ if not selected:
89
+ return None
90
+ row = selected[0]
91
+ if not isinstance(row, dict):
92
+ return None
93
+ rid = row.get("result_id")
94
+ return rid if rid else None
95
+
96
+ @property
97
+ def result(self) -> QueryResult:
98
+ rid = self.selected_result_id
99
+ mo.stop(rid is None, mo.md("Select a result row to load."))
100
+ return self._client.get_result(rid or "")
101
+
102
+ @property
103
+ def result_panel(self):
104
+ rid = self.selected_result_id
105
+ if rid is None:
106
+ return mo.md("_Select a result row to load._")
107
+ return query_result(self._client.get_result(rid), label="Recent result")
108
+
109
+ @property
110
+ def tab_ui(self):
111
+ if self.table is not None:
112
+ _ = self.table.value
113
+ return mo.vstack([self.ui, self.result_panel], gap=2)
114
+
115
+ @property
116
+ def ui(self):
117
+ if self.table is None:
118
+ return mo.md("_No recent results._")
119
+ _ = self.table.value
120
+ return mo.vstack(
121
+ [
122
+ mo.md("### Recent results"),
123
+ self.table,
124
+ ],
125
+ gap=1,
126
+ )
127
+
128
+
129
+ def recent_results(client: HotdataClient, *, limit: int = 50) -> RecentResults:
130
+ return RecentResults(client, limit=limit)
131
+
132
+
133
+ def run_history(
134
+ client: HotdataClient,
135
+ *,
136
+ limit: int = 20,
137
+ label: str = "Run history",
138
+ ):
139
+ runs = client.list_run_history(limit=limit)
140
+ if not runs:
141
+ return mo.md("_No query runs returned._")
142
+
143
+ rows: list[dict[str, object]] = []
144
+ for r in runs:
145
+ rows.append(
146
+ {
147
+ "created_at": r.created_at,
148
+ "status": r.status,
149
+ "execution_time_ms": r.execution_time_ms,
150
+ "result_id": r.result_id,
151
+ "query_run_id": r.query_run_id,
152
+ }
153
+ )
154
+
155
+ return mo.vstack(
156
+ [
157
+ mo.md(f"### {label}"),
158
+ mo.ui.table(
159
+ rows,
160
+ pagination=True,
161
+ page_size=min(10, limit),
162
+ selection=None,
163
+ max_height=320,
164
+ ),
165
+ ],
166
+ gap=1,
167
+ )
168
+
169
+
170
+ def connection_status(client: HotdataClient):
171
+ """Small status line: API reachability, workspace id, connection count, sandbox."""
172
+ ok, parts = workspace_health_lines(client)
173
+ if ok:
174
+ return mo.callout(mo.md(" · ".join(parts)), kind="success")
175
+ return mo.callout(
176
+ mo.md(f"**API** error — {parts[0]}"),
177
+ kind="danger",
178
+ )
179
+
180
+
181
+ def connections_panel(client: HotdataClient):
182
+ """Workspace health callout plus a table of configured connections."""
183
+ status = connection_status(client)
184
+ conns = client.connections().list_connections().connections
185
+ if not conns:
186
+ return mo.vstack([status, mo.md("_No connections in this workspace._")], gap=1)
187
+ rows: list[dict[str, object]] = []
188
+ for c in conns:
189
+ rows.append(
190
+ {
191
+ "name": c.name,
192
+ "id": c.id,
193
+ "source_type": getattr(c, "source_type", None),
194
+ }
195
+ )
196
+ return mo.vstack(
197
+ [
198
+ status,
199
+ mo.ui.table(
200
+ rows,
201
+ label="Connections",
202
+ pagination=True,
203
+ page_size=min(10, len(rows)),
204
+ selection=None,
205
+ max_height=320,
206
+ ),
207
+ ],
208
+ gap=1,
209
+ )
@@ -0,0 +1,201 @@
1
+ from __future__ import annotations
2
+
3
+ import marimo as mo
4
+
5
+ from hotdata_runtime import HotdataClient, QueryResult
6
+
7
+
8
+ class SqlEditor:
9
+ """SQL workspace: textarea plus Run, with `result` after the button is pressed.
10
+
11
+ Marimo does not allow reading ``.value`` on UI elements in the same cell that
12
+ constructs them. Instantiate ``SqlEditor`` in one cell and use ``.ui`` / read
13
+ ``.result`` in other cells (see the package README two-cell pattern).
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ client: HotdataClient,
19
+ *,
20
+ default_sql: str = "",
21
+ label: str = "SQL",
22
+ run_label: str = "Run on Hotdata",
23
+ ) -> None:
24
+ self._client = client
25
+ self.sql = mo.ui.text_area(default_sql, label=label)
26
+ self.run = mo.ui.button(
27
+ value=0,
28
+ on_click=lambda n: n + 1,
29
+ label=run_label,
30
+ kind="success",
31
+ )
32
+ self.rerun = mo.ui.button(
33
+ value=0,
34
+ on_click=lambda n: n + 1,
35
+ label="Rerun last",
36
+ )
37
+ self.clear = mo.ui.button(
38
+ value=0,
39
+ on_click=lambda n: n + 1,
40
+ label="Clear result",
41
+ kind="neutral",
42
+ )
43
+ self._result_cache: QueryResult | None = None
44
+ self._cached_sql: str | None = None
45
+ self._last_run_n: int | None = None
46
+ self._last_rerun_n: int | None = None
47
+ self._last_clear_n: int | None = None
48
+ self.show_history = mo.ui.checkbox(value=False, label="Show run history")
49
+
50
+ @property
51
+ def ui(self):
52
+ _ = self.run.value
53
+ _ = self.rerun.value
54
+ _ = self.clear.value
55
+ _ = self.show_history.value
56
+ return mo.vstack(
57
+ [
58
+ self.sql,
59
+ mo.hstack(
60
+ [
61
+ self.run,
62
+ self.rerun,
63
+ self.clear,
64
+ self.show_history,
65
+ ],
66
+ gap=1,
67
+ ),
68
+ (
69
+ mo.accordion(
70
+ {
71
+ "Run history": mo.lazy(
72
+ lambda: __import__(
73
+ "hotdata_marimo.display",
74
+ fromlist=["run_history"],
75
+ ).run_history(self._client)
76
+ )
77
+ }
78
+ )
79
+ if self.show_history.value
80
+ else mo.md("")
81
+ ),
82
+ ],
83
+ gap=1,
84
+ )
85
+
86
+ def _apply_clear(self, clear_n: int) -> bool:
87
+ if clear_n > 0 and self._last_clear_n != clear_n:
88
+ self._result_cache = None
89
+ self._cached_sql = None
90
+ self._last_clear_n = clear_n
91
+ return True
92
+ return False
93
+
94
+ def _execute_or_cached(self) -> QueryResult | None:
95
+ sql_text = self.sql.value
96
+ run_n = self.run.value
97
+ rerun_n = self.rerun.value
98
+
99
+ if rerun_n > 0 and rerun_n != self._last_rerun_n:
100
+ if self._cached_sql is None:
101
+ return None
102
+ with mo.status.spinner(
103
+ title="Running on Hotdata",
104
+ subtitle="Re-running last query and waiting for results…",
105
+ ):
106
+ result = self._client.execute_sql(self._cached_sql or "")
107
+ self._result_cache = result
108
+ self._last_rerun_n = rerun_n
109
+ return result
110
+
111
+ if run_n > 0 and run_n != self._last_run_n:
112
+ with mo.status.spinner(
113
+ title="Running on Hotdata",
114
+ subtitle="Executing query and waiting for results…",
115
+ ):
116
+ result = self._client.execute_sql(sql_text)
117
+ self._result_cache = result
118
+ self._cached_sql = sql_text
119
+ self._last_run_n = run_n
120
+ return result
121
+
122
+ if self._result_cache is not None and sql_text == self._cached_sql:
123
+ return self._result_cache
124
+
125
+ return None
126
+
127
+ @property
128
+ def result_panel(self):
129
+ from hotdata_marimo.display import query_result
130
+
131
+ run_n = self.run.value
132
+ rerun_n = self.rerun.value
133
+ clear_n = self.clear.value
134
+
135
+ if self._apply_clear(clear_n):
136
+ return mo.md("Result cleared. Click **Run on Hotdata** to execute again.")
137
+
138
+ if run_n == 0 and rerun_n == 0 and self._result_cache is None:
139
+ return mo.md("_Click **Run on Hotdata** to execute._")
140
+
141
+ if rerun_n > 0 and rerun_n != self._last_rerun_n and self._cached_sql is None:
142
+ return mo.md("No previous SQL to rerun yet — click **Run on Hotdata** first.")
143
+
144
+ result = self._execute_or_cached()
145
+ if result is not None:
146
+ return query_result(result)
147
+
148
+ return mo.md("SQL changed — click **Run on Hotdata** again to execute.")
149
+
150
+ @property
151
+ def tab_ui(self):
152
+ _ = self.run.value
153
+ _ = self.rerun.value
154
+ _ = self.clear.value
155
+ _ = self.show_history.value
156
+ return mo.vstack([self.ui, self.result_panel], gap=2)
157
+
158
+ @property
159
+ def result(self) -> QueryResult:
160
+ run_n = self.run.value
161
+ rerun_n = self.rerun.value
162
+ clear_n = self.clear.value
163
+
164
+ if self._apply_clear(clear_n):
165
+ mo.stop(True, mo.md("Result cleared. Click **Run on Hotdata** to execute again."))
166
+
167
+ mo.stop(
168
+ run_n == 0 and rerun_n == 0,
169
+ mo.md(
170
+ "**Run on Hotdata** is on the SQL editor UI (a cell that **returns** "
171
+ "`editor.ui` or `mo.vstack([browser.ui, editor.ui])`). Click it there, "
172
+ "then this cell will run."
173
+ ),
174
+ )
175
+
176
+ if rerun_n > 0 and rerun_n != self._last_rerun_n:
177
+ mo.stop(
178
+ self._cached_sql is None,
179
+ mo.md("No previous SQL to rerun yet — click **Run on Hotdata** first."),
180
+ )
181
+
182
+ result = self._execute_or_cached()
183
+ if result is not None:
184
+ return result
185
+
186
+ mo.stop(
187
+ True,
188
+ mo.md("SQL changed — click **Run on Hotdata** again to execute."),
189
+ )
190
+
191
+
192
+ def sql_editor(
193
+ client: HotdataClient,
194
+ *,
195
+ default_sql: str = "",
196
+ label: str = "SQL",
197
+ run_label: str = "Run on Hotdata",
198
+ ) -> SqlEditor:
199
+ return SqlEditor(
200
+ client, default_sql=default_sql, label=label, run_label=run_label
201
+ )