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.
- hotdata_marimo/__init__.py +80 -0
- hotdata_marimo/_options.py +107 -0
- hotdata_marimo/display.py +209 -0
- hotdata_marimo/sql_editor.py +201 -0
- hotdata_marimo/sql_engine.py +371 -0
- hotdata_marimo/table_browser.py +221 -0
- hotdata_marimo/workspace_selector.py +93 -0
- hotdata_marimo-0.1.0.dist-info/METADATA +124 -0
- hotdata_marimo-0.1.0.dist-info/RECORD +10 -0
- hotdata_marimo-0.1.0.dist-info/WHEEL +4 -0
|
@@ -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
|
+
)
|