mcli-framework 7.5.1__py3-none-any.whl → 7.6.1__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.
Potentially problematic release.
This version of mcli-framework might be problematic. Click here for more details.
- mcli/app/commands_cmd.py +51 -39
- mcli/app/completion_helpers.py +4 -13
- mcli/app/main.py +21 -25
- mcli/app/model_cmd.py +119 -9
- mcli/lib/custom_commands.py +16 -11
- mcli/ml/api/app.py +1 -5
- mcli/ml/dashboard/app.py +2 -2
- mcli/ml/dashboard/app_integrated.py +168 -116
- mcli/ml/dashboard/app_supabase.py +7 -3
- mcli/ml/dashboard/app_training.py +3 -6
- mcli/ml/dashboard/components/charts.py +74 -115
- mcli/ml/dashboard/components/metrics.py +24 -44
- mcli/ml/dashboard/components/tables.py +32 -40
- mcli/ml/dashboard/overview.py +102 -78
- mcli/ml/dashboard/pages/cicd.py +103 -56
- mcli/ml/dashboard/pages/debug_dependencies.py +35 -28
- mcli/ml/dashboard/pages/gravity_viz.py +374 -313
- mcli/ml/dashboard/pages/monte_carlo_predictions.py +50 -48
- mcli/ml/dashboard/pages/predictions_enhanced.py +396 -248
- mcli/ml/dashboard/pages/scrapers_and_logs.py +299 -273
- mcli/ml/dashboard/pages/test_portfolio.py +153 -121
- mcli/ml/dashboard/pages/trading.py +238 -169
- mcli/ml/dashboard/pages/workflows.py +129 -84
- mcli/ml/dashboard/streamlit_extras_utils.py +70 -79
- mcli/ml/dashboard/utils.py +24 -21
- mcli/ml/dashboard/warning_suppression.py +6 -4
- mcli/ml/database/session.py +16 -5
- mcli/ml/mlops/pipeline_orchestrator.py +1 -3
- mcli/ml/predictions/monte_carlo.py +6 -18
- mcli/ml/trading/alpaca_client.py +95 -96
- mcli/ml/trading/migrations.py +76 -40
- mcli/ml/trading/models.py +78 -60
- mcli/ml/trading/paper_trading.py +92 -74
- mcli/ml/trading/risk_management.py +106 -85
- mcli/ml/trading/trading_service.py +155 -110
- mcli/ml/training/train_model.py +1 -3
- mcli/{app → self}/completion_cmd.py +6 -6
- mcli/self/self_cmd.py +100 -57
- mcli/test/test_cmd.py +30 -0
- mcli/workflow/daemon/daemon.py +2 -0
- mcli/workflow/model_service/openai_adapter.py +347 -0
- mcli/workflow/politician_trading/models.py +6 -2
- mcli/workflow/politician_trading/scrapers_corporate_registry.py +39 -88
- mcli/workflow/politician_trading/scrapers_free_sources.py +32 -39
- mcli/workflow/politician_trading/scrapers_third_party.py +21 -39
- mcli/workflow/politician_trading/seed_database.py +70 -89
- {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/METADATA +1 -1
- {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/RECORD +56 -54
- /mcli/{app → self}/logs_cmd.py +0 -0
- /mcli/{app → self}/redis_cmd.py +0 -0
- /mcli/{app → self}/visual_cmd.py +0 -0
- /mcli/{app → test}/cron_test_cmd.py +0 -0
- {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/WHEEL +0 -0
- {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/licenses/LICENSE +0 -0
- {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/top_level.txt +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""Reusable chart components for Streamlit dashboards"""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
5
|
import pandas as pd
|
|
6
|
+
import plotly.express as px
|
|
7
|
+
import plotly.graph_objects as go
|
|
6
8
|
import streamlit as st
|
|
7
|
-
from typing import Optional, Dict, List, Any
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def create_timeline_chart(
|
|
@@ -13,24 +14,13 @@ def create_timeline_chart(
|
|
|
13
14
|
y_col: str,
|
|
14
15
|
title: str = "Timeline",
|
|
15
16
|
color_col: Optional[str] = None,
|
|
16
|
-
height: int = 400
|
|
17
|
+
height: int = 400,
|
|
17
18
|
) -> go.Figure:
|
|
18
19
|
"""Create a timeline chart with Plotly"""
|
|
19
20
|
|
|
20
|
-
fig = px.line(
|
|
21
|
-
data,
|
|
22
|
-
x=x_col,
|
|
23
|
-
y=y_col,
|
|
24
|
-
color=color_col,
|
|
25
|
-
title=title,
|
|
26
|
-
markers=True
|
|
27
|
-
)
|
|
21
|
+
fig = px.line(data, x=x_col, y=y_col, color=color_col, title=title, markers=True)
|
|
28
22
|
|
|
29
|
-
fig.update_layout(
|
|
30
|
-
height=height,
|
|
31
|
-
hovermode='x unified',
|
|
32
|
-
showlegend=True if color_col else False
|
|
33
|
-
)
|
|
23
|
+
fig.update_layout(height=height, hovermode="x unified", showlegend=True if color_col else False)
|
|
34
24
|
|
|
35
25
|
return fig
|
|
36
26
|
|
|
@@ -39,7 +29,7 @@ def create_status_pie_chart(
|
|
|
39
29
|
data: pd.DataFrame,
|
|
40
30
|
status_col: str,
|
|
41
31
|
title: str = "Status Distribution",
|
|
42
|
-
color_map: Optional[Dict[str, str]] = None
|
|
32
|
+
color_map: Optional[Dict[str, str]] = None,
|
|
43
33
|
) -> go.Figure:
|
|
44
34
|
"""Create a pie chart for status distribution"""
|
|
45
35
|
|
|
@@ -47,28 +37,28 @@ def create_status_pie_chart(
|
|
|
47
37
|
|
|
48
38
|
if color_map is None:
|
|
49
39
|
color_map = {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
40
|
+
"completed": "#10b981",
|
|
41
|
+
"running": "#3b82f6",
|
|
42
|
+
"pending": "#f59e0b",
|
|
43
|
+
"failed": "#ef4444",
|
|
44
|
+
"cancelled": "#6b7280",
|
|
55
45
|
}
|
|
56
46
|
|
|
57
|
-
colors = [color_map.get(status.lower(),
|
|
58
|
-
|
|
59
|
-
fig = go.Figure(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
height=350,
|
|
69
|
-
showlegend=True
|
|
47
|
+
colors = [color_map.get(status.lower(), "#6b7280") for status in status_counts.index]
|
|
48
|
+
|
|
49
|
+
fig = go.Figure(
|
|
50
|
+
data=[
|
|
51
|
+
go.Pie(
|
|
52
|
+
labels=status_counts.index,
|
|
53
|
+
values=status_counts.values,
|
|
54
|
+
marker=dict(colors=colors),
|
|
55
|
+
hole=0.4,
|
|
56
|
+
)
|
|
57
|
+
]
|
|
70
58
|
)
|
|
71
59
|
|
|
60
|
+
fig.update_layout(title=title, height=350, showlegend=True)
|
|
61
|
+
|
|
72
62
|
return fig
|
|
73
63
|
|
|
74
64
|
|
|
@@ -77,21 +67,23 @@ def create_metric_trend_chart(
|
|
|
77
67
|
time_col: str,
|
|
78
68
|
metric_col: str,
|
|
79
69
|
title: str,
|
|
80
|
-
target_value: Optional[float] = None
|
|
70
|
+
target_value: Optional[float] = None,
|
|
81
71
|
) -> go.Figure:
|
|
82
72
|
"""Create a metric trend chart with optional target line"""
|
|
83
73
|
|
|
84
74
|
fig = go.Figure()
|
|
85
75
|
|
|
86
76
|
# Add metric line
|
|
87
|
-
fig.add_trace(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
77
|
+
fig.add_trace(
|
|
78
|
+
go.Scatter(
|
|
79
|
+
x=data[time_col],
|
|
80
|
+
y=data[metric_col],
|
|
81
|
+
mode="lines+markers",
|
|
82
|
+
name=metric_col,
|
|
83
|
+
line=dict(width=3),
|
|
84
|
+
marker=dict(size=8),
|
|
85
|
+
)
|
|
86
|
+
)
|
|
95
87
|
|
|
96
88
|
# Add target line if specified
|
|
97
89
|
if target_value is not None:
|
|
@@ -99,15 +91,11 @@ def create_metric_trend_chart(
|
|
|
99
91
|
y=target_value,
|
|
100
92
|
line_dash="dash",
|
|
101
93
|
line_color="red",
|
|
102
|
-
annotation_text=f"Target: {target_value}"
|
|
94
|
+
annotation_text=f"Target: {target_value}",
|
|
103
95
|
)
|
|
104
96
|
|
|
105
97
|
fig.update_layout(
|
|
106
|
-
title=title,
|
|
107
|
-
xaxis_title=time_col,
|
|
108
|
-
yaxis_title=metric_col,
|
|
109
|
-
height=400,
|
|
110
|
-
hovermode='x unified'
|
|
98
|
+
title=title, xaxis_title=time_col, yaxis_title=metric_col, height=400, hovermode="x unified"
|
|
111
99
|
)
|
|
112
100
|
|
|
113
101
|
return fig
|
|
@@ -119,23 +107,13 @@ def create_heatmap(
|
|
|
119
107
|
y_col: str,
|
|
120
108
|
value_col: str,
|
|
121
109
|
title: str = "Heatmap",
|
|
122
|
-
color_scale: str = "Blues"
|
|
110
|
+
color_scale: str = "Blues",
|
|
123
111
|
) -> go.Figure:
|
|
124
112
|
"""Create a heatmap visualization"""
|
|
125
113
|
|
|
126
|
-
pivot_data = data.pivot_table(
|
|
127
|
-
index=y_col,
|
|
128
|
-
columns=x_col,
|
|
129
|
-
values=value_col,
|
|
130
|
-
aggfunc='mean'
|
|
131
|
-
)
|
|
114
|
+
pivot_data = data.pivot_table(index=y_col, columns=x_col, values=value_col, aggfunc="mean")
|
|
132
115
|
|
|
133
|
-
fig = px.imshow(
|
|
134
|
-
pivot_data,
|
|
135
|
-
color_continuous_scale=color_scale,
|
|
136
|
-
title=title,
|
|
137
|
-
aspect="auto"
|
|
138
|
-
)
|
|
116
|
+
fig = px.imshow(pivot_data, color_continuous_scale=color_scale, title=title, aspect="auto")
|
|
139
117
|
|
|
140
118
|
fig.update_layout(height=400)
|
|
141
119
|
|
|
@@ -148,17 +126,12 @@ def create_gantt_chart(
|
|
|
148
126
|
start_col: str,
|
|
149
127
|
end_col: str,
|
|
150
128
|
status_col: Optional[str] = None,
|
|
151
|
-
title: str = "Timeline"
|
|
129
|
+
title: str = "Timeline",
|
|
152
130
|
) -> go.Figure:
|
|
153
131
|
"""Create a Gantt chart for job/task scheduling"""
|
|
154
132
|
|
|
155
133
|
fig = px.timeline(
|
|
156
|
-
data,
|
|
157
|
-
x_start=start_col,
|
|
158
|
-
x_end=end_col,
|
|
159
|
-
y=task_col,
|
|
160
|
-
color=status_col,
|
|
161
|
-
title=title
|
|
134
|
+
data, x_start=start_col, x_end=end_col, y=task_col, color=status_col, title=title
|
|
162
135
|
)
|
|
163
136
|
|
|
164
137
|
fig.update_yaxes(categoryorder="total ascending")
|
|
@@ -168,9 +141,7 @@ def create_gantt_chart(
|
|
|
168
141
|
|
|
169
142
|
|
|
170
143
|
def create_multi_metric_gauge(
|
|
171
|
-
values: Dict[str, float],
|
|
172
|
-
max_values: Dict[str, float],
|
|
173
|
-
title: str = "Metrics"
|
|
144
|
+
values: Dict[str, float], max_values: Dict[str, float], title: str = "Metrics"
|
|
174
145
|
) -> go.Figure:
|
|
175
146
|
"""Create multiple gauge charts in a grid"""
|
|
176
147
|
|
|
@@ -183,8 +154,8 @@ def create_multi_metric_gauge(
|
|
|
183
154
|
fig = make_subplots(
|
|
184
155
|
rows=rows,
|
|
185
156
|
cols=cols,
|
|
186
|
-
specs=[[{
|
|
187
|
-
subplot_titles=list(values.keys())
|
|
157
|
+
specs=[[{"type": "indicator"}] * cols] * rows,
|
|
158
|
+
subplot_titles=list(values.keys()),
|
|
188
159
|
)
|
|
189
160
|
|
|
190
161
|
for idx, (metric, value) in enumerate(values.items()):
|
|
@@ -196,63 +167,51 @@ def create_multi_metric_gauge(
|
|
|
196
167
|
go.Indicator(
|
|
197
168
|
mode="gauge+number+delta",
|
|
198
169
|
value=value,
|
|
199
|
-
title={
|
|
170
|
+
title={"text": metric},
|
|
200
171
|
gauge={
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
{
|
|
205
|
-
{
|
|
172
|
+
"axis": {"range": [None, max_val]},
|
|
173
|
+
"bar": {"color": "darkblue"},
|
|
174
|
+
"steps": [
|
|
175
|
+
{"range": [0, max_val * 0.5], "color": "lightgray"},
|
|
176
|
+
{"range": [max_val * 0.5, max_val * 0.8], "color": "gray"},
|
|
206
177
|
],
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
}
|
|
178
|
+
"threshold": {
|
|
179
|
+
"line": {"color": "red", "width": 4},
|
|
180
|
+
"thickness": 0.75,
|
|
181
|
+
"value": max_val * 0.9,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
213
184
|
),
|
|
214
185
|
row=row,
|
|
215
|
-
col=col
|
|
186
|
+
col=col,
|
|
216
187
|
)
|
|
217
188
|
|
|
218
|
-
fig.update_layout(
|
|
219
|
-
title=title,
|
|
220
|
-
height=300 * rows
|
|
221
|
-
)
|
|
189
|
+
fig.update_layout(title=title, height=300 * rows)
|
|
222
190
|
|
|
223
191
|
return fig
|
|
224
192
|
|
|
225
193
|
|
|
226
194
|
def create_waterfall_chart(
|
|
227
|
-
categories: List[str],
|
|
228
|
-
values: List[float],
|
|
229
|
-
title: str = "Waterfall Chart"
|
|
195
|
+
categories: List[str], values: List[float], title: str = "Waterfall Chart"
|
|
230
196
|
) -> go.Figure:
|
|
231
197
|
"""Create a waterfall chart for step-by-step changes"""
|
|
232
198
|
|
|
233
|
-
fig = go.Figure(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
fig.update_layout(
|
|
243
|
-
title=title,
|
|
244
|
-
showlegend=False,
|
|
245
|
-
height=400
|
|
199
|
+
fig = go.Figure(
|
|
200
|
+
go.Waterfall(
|
|
201
|
+
name="",
|
|
202
|
+
orientation="v",
|
|
203
|
+
measure=["relative"] * (len(values) - 1) + ["total"],
|
|
204
|
+
x=categories,
|
|
205
|
+
y=values,
|
|
206
|
+
connector={"line": {"color": "rgb(63, 63, 63)"}},
|
|
207
|
+
)
|
|
246
208
|
)
|
|
247
209
|
|
|
210
|
+
fig.update_layout(title=title, showlegend=False, height=400)
|
|
211
|
+
|
|
248
212
|
return fig
|
|
249
213
|
|
|
250
214
|
|
|
251
215
|
def render_chart(fig: go.Figure, key: Optional[str] = None):
|
|
252
216
|
"""Helper to render Plotly chart with consistent configuration"""
|
|
253
|
-
st.plotly_chart(
|
|
254
|
-
fig,
|
|
255
|
-
width="stretch",
|
|
256
|
-
config={"responsive": True},
|
|
257
|
-
key=key
|
|
258
|
-
)
|
|
217
|
+
st.plotly_chart(fig, width="stretch", config={"responsive": True}, key=key)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Reusable metric display components"""
|
|
2
2
|
|
|
3
|
-
import streamlit as st
|
|
4
3
|
from typing import Optional, Union
|
|
5
4
|
|
|
5
|
+
import streamlit as st
|
|
6
|
+
|
|
6
7
|
|
|
7
8
|
def display_metric_card(
|
|
8
9
|
label: str,
|
|
@@ -10,42 +11,33 @@ def display_metric_card(
|
|
|
10
11
|
delta: Optional[Union[int, float, str]] = None,
|
|
11
12
|
delta_color: str = "normal",
|
|
12
13
|
help_text: Optional[str] = None,
|
|
13
|
-
icon: Optional[str] = None
|
|
14
|
+
icon: Optional[str] = None,
|
|
14
15
|
):
|
|
15
16
|
"""Display a metric card with optional delta and icon"""
|
|
16
17
|
|
|
17
18
|
if icon:
|
|
18
19
|
label = f"{icon} {label}"
|
|
19
20
|
|
|
20
|
-
st.metric(
|
|
21
|
-
label=label,
|
|
22
|
-
value=value,
|
|
23
|
-
delta=delta,
|
|
24
|
-
delta_color=delta_color,
|
|
25
|
-
help=help_text
|
|
26
|
-
)
|
|
21
|
+
st.metric(label=label, value=value, delta=delta, delta_color=delta_color, help=help_text)
|
|
27
22
|
|
|
28
23
|
|
|
29
|
-
def display_status_badge(
|
|
30
|
-
status: str,
|
|
31
|
-
size: str = "medium"
|
|
32
|
-
) -> str:
|
|
24
|
+
def display_status_badge(status: str, size: str = "medium") -> str:
|
|
33
25
|
"""Return a colored status badge"""
|
|
34
26
|
|
|
35
27
|
status_colors = {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
28
|
+
"completed": "🟢",
|
|
29
|
+
"success": "🟢",
|
|
30
|
+
"running": "🔵",
|
|
31
|
+
"in_progress": "🔵",
|
|
32
|
+
"pending": "🟡",
|
|
33
|
+
"waiting": "🟡",
|
|
34
|
+
"failed": "🔴",
|
|
35
|
+
"error": "🔴",
|
|
36
|
+
"cancelled": "⚪",
|
|
37
|
+
"unknown": "⚫",
|
|
46
38
|
}
|
|
47
39
|
|
|
48
|
-
icon = status_colors.get(status.lower(),
|
|
40
|
+
icon = status_colors.get(status.lower(), "⚫")
|
|
49
41
|
|
|
50
42
|
if size == "small":
|
|
51
43
|
return f"{icon} {status}"
|
|
@@ -68,21 +60,17 @@ def display_kpi_row(metrics: dict, columns: Optional[int] = None):
|
|
|
68
60
|
if isinstance(value, dict):
|
|
69
61
|
display_metric_card(
|
|
70
62
|
label=label,
|
|
71
|
-
value=value.get(
|
|
72
|
-
delta=value.get(
|
|
73
|
-
delta_color=value.get(
|
|
74
|
-
help_text=value.get(
|
|
75
|
-
icon=value.get(
|
|
63
|
+
value=value.get("value", "-"),
|
|
64
|
+
delta=value.get("delta"),
|
|
65
|
+
delta_color=value.get("delta_color", "normal"),
|
|
66
|
+
help_text=value.get("help"),
|
|
67
|
+
icon=value.get("icon"),
|
|
76
68
|
)
|
|
77
69
|
else:
|
|
78
70
|
st.metric(label=label, value=value)
|
|
79
71
|
|
|
80
72
|
|
|
81
|
-
def display_progress_bar(
|
|
82
|
-
label: str,
|
|
83
|
-
progress: float,
|
|
84
|
-
show_percentage: bool = True
|
|
85
|
-
):
|
|
73
|
+
def display_progress_bar(label: str, progress: float, show_percentage: bool = True):
|
|
86
74
|
"""Display a progress bar with label"""
|
|
87
75
|
|
|
88
76
|
st.text(label)
|
|
@@ -92,11 +80,7 @@ def display_progress_bar(
|
|
|
92
80
|
st.caption(f"{progress * 100:.1f}%")
|
|
93
81
|
|
|
94
82
|
|
|
95
|
-
def display_health_indicator(
|
|
96
|
-
component: str,
|
|
97
|
-
is_healthy: bool,
|
|
98
|
-
details: Optional[str] = None
|
|
99
|
-
):
|
|
83
|
+
def display_health_indicator(component: str, is_healthy: bool, details: Optional[str] = None):
|
|
100
84
|
"""Display a health status indicator"""
|
|
101
85
|
|
|
102
86
|
if is_healthy:
|
|
@@ -105,11 +89,7 @@ def display_health_indicator(
|
|
|
105
89
|
st.error(f"❌ {component}: Unhealthy" + (f" ({details})" if details else ""))
|
|
106
90
|
|
|
107
91
|
|
|
108
|
-
def display_alert(
|
|
109
|
-
message: str,
|
|
110
|
-
alert_type: str = "info",
|
|
111
|
-
icon: Optional[str] = None
|
|
112
|
-
):
|
|
92
|
+
def display_alert(message: str, alert_type: str = "info", icon: Optional[str] = None):
|
|
113
93
|
"""Display an alert message"""
|
|
114
94
|
|
|
115
95
|
if icon:
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Reusable table components"""
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from typing import Any, Callable, List, Optional
|
|
4
|
+
|
|
4
5
|
import pandas as pd
|
|
5
|
-
|
|
6
|
+
import streamlit as st
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def display_dataframe_with_search(
|
|
@@ -10,7 +11,7 @@ def display_dataframe_with_search(
|
|
|
10
11
|
search_columns: Optional[List[str]] = None,
|
|
11
12
|
default_sort_column: Optional[str] = None,
|
|
12
13
|
page_size: int = 20,
|
|
13
|
-
key_prefix: str = "table"
|
|
14
|
+
key_prefix: str = "table",
|
|
14
15
|
) -> pd.DataFrame:
|
|
15
16
|
"""Display a dataframe with search and pagination"""
|
|
16
17
|
|
|
@@ -23,7 +24,7 @@ def display_dataframe_with_search(
|
|
|
23
24
|
search_term = st.text_input(
|
|
24
25
|
"🔍 Search",
|
|
25
26
|
key=f"{key_prefix}_search",
|
|
26
|
-
placeholder=f"Search in: {', '.join(search_columns)}"
|
|
27
|
+
placeholder=f"Search in: {', '.join(search_columns)}",
|
|
27
28
|
)
|
|
28
29
|
|
|
29
30
|
if search_term:
|
|
@@ -44,11 +45,7 @@ def display_dataframe_with_search(
|
|
|
44
45
|
if len(df) > page_size:
|
|
45
46
|
total_pages = (len(df) - 1) // page_size + 1
|
|
46
47
|
page = st.number_input(
|
|
47
|
-
"Page",
|
|
48
|
-
min_value=1,
|
|
49
|
-
max_value=total_pages,
|
|
50
|
-
value=1,
|
|
51
|
-
key=f"{key_prefix}_page"
|
|
48
|
+
"Page", min_value=1, max_value=total_pages, value=1, key=f"{key_prefix}_page"
|
|
52
49
|
)
|
|
53
50
|
start_idx = (page - 1) * page_size
|
|
54
51
|
end_idx = start_idx + page_size
|
|
@@ -63,9 +60,7 @@ def display_dataframe_with_search(
|
|
|
63
60
|
|
|
64
61
|
|
|
65
62
|
def display_filterable_dataframe(
|
|
66
|
-
df: pd.DataFrame,
|
|
67
|
-
filter_columns: Optional[dict] = None,
|
|
68
|
-
key_prefix: str = "filter"
|
|
63
|
+
df: pd.DataFrame, filter_columns: Optional[dict] = None, key_prefix: str = "filter"
|
|
69
64
|
) -> pd.DataFrame:
|
|
70
65
|
"""Display a dataframe with column-specific filters"""
|
|
71
66
|
|
|
@@ -88,31 +83,32 @@ def display_filterable_dataframe(
|
|
|
88
83
|
col_name,
|
|
89
84
|
options=unique_values,
|
|
90
85
|
default=unique_values,
|
|
91
|
-
key=f"{key_prefix}_{col_name}"
|
|
86
|
+
key=f"{key_prefix}_{col_name}",
|
|
92
87
|
)
|
|
93
88
|
if selected:
|
|
94
89
|
df = df[df[col_name].isin(selected)]
|
|
95
90
|
|
|
96
91
|
elif filter_type == "text":
|
|
97
|
-
search_text = st.text_input(
|
|
98
|
-
col_name,
|
|
99
|
-
key=f"{key_prefix}_{col_name}"
|
|
100
|
-
)
|
|
92
|
+
search_text = st.text_input(col_name, key=f"{key_prefix}_{col_name}")
|
|
101
93
|
if search_text:
|
|
102
|
-
df = df[
|
|
94
|
+
df = df[
|
|
95
|
+
df[col_name]
|
|
96
|
+
.astype(str)
|
|
97
|
+
.str.contains(search_text, case=False, na=False)
|
|
98
|
+
]
|
|
103
99
|
|
|
104
100
|
elif filter_type == "date_range":
|
|
105
101
|
if pd.api.types.is_datetime64_any_dtype(df[col_name]):
|
|
106
102
|
min_date = df[col_name].min()
|
|
107
103
|
max_date = df[col_name].max()
|
|
108
104
|
date_range = st.date_input(
|
|
109
|
-
col_name,
|
|
110
|
-
value=(min_date, max_date),
|
|
111
|
-
key=f"{key_prefix}_{col_name}"
|
|
105
|
+
col_name, value=(min_date, max_date), key=f"{key_prefix}_{col_name}"
|
|
112
106
|
)
|
|
113
107
|
if len(date_range) == 2:
|
|
114
|
-
df = df[
|
|
115
|
-
|
|
108
|
+
df = df[
|
|
109
|
+
(df[col_name] >= pd.Timestamp(date_range[0]))
|
|
110
|
+
& (df[col_name] <= pd.Timestamp(date_range[1]))
|
|
111
|
+
]
|
|
116
112
|
|
|
117
113
|
st.dataframe(df, width="stretch")
|
|
118
114
|
|
|
@@ -120,10 +116,7 @@ def display_filterable_dataframe(
|
|
|
120
116
|
|
|
121
117
|
|
|
122
118
|
def display_table_with_actions(
|
|
123
|
-
df: pd.DataFrame,
|
|
124
|
-
actions: List[dict],
|
|
125
|
-
row_id_column: str = "id",
|
|
126
|
-
key_prefix: str = "action"
|
|
119
|
+
df: pd.DataFrame, actions: List[dict], row_id_column: str = "id", key_prefix: str = "action"
|
|
127
120
|
):
|
|
128
121
|
"""Display a table with action buttons for each row
|
|
129
122
|
|
|
@@ -145,14 +138,13 @@ def display_table_with_actions(
|
|
|
145
138
|
# Action buttons
|
|
146
139
|
for action_idx, action in enumerate(actions):
|
|
147
140
|
with data_cols[action_idx + 1]:
|
|
148
|
-
icon = action.get(
|
|
149
|
-
button_label = f"{icon} {action['label']}" if icon else action[
|
|
141
|
+
icon = action.get("icon", "")
|
|
142
|
+
button_label = f"{icon} {action['label']}" if icon else action["label"]
|
|
150
143
|
|
|
151
144
|
if st.button(
|
|
152
|
-
button_label,
|
|
153
|
-
key=f"{key_prefix}_{row[row_id_column]}_{action_idx}"
|
|
145
|
+
button_label, key=f"{key_prefix}_{row[row_id_column]}_{action_idx}"
|
|
154
146
|
):
|
|
155
|
-
action[
|
|
147
|
+
action["callback"](row)
|
|
156
148
|
|
|
157
149
|
st.divider()
|
|
158
150
|
|
|
@@ -162,7 +154,7 @@ def display_expandable_table(
|
|
|
162
154
|
summary_columns: List[str],
|
|
163
155
|
detail_callback: Callable[[Any], None],
|
|
164
156
|
row_id_column: str = "id",
|
|
165
|
-
key_prefix: str = "expand"
|
|
157
|
+
key_prefix: str = "expand",
|
|
166
158
|
):
|
|
167
159
|
"""Display a table where each row can be expanded for details"""
|
|
168
160
|
|
|
@@ -183,7 +175,7 @@ def export_dataframe(
|
|
|
183
175
|
df: pd.DataFrame,
|
|
184
176
|
filename: str = "data",
|
|
185
177
|
formats: List[str] = ["csv", "json"],
|
|
186
|
-
key_prefix: str = "export"
|
|
178
|
+
key_prefix: str = "export",
|
|
187
179
|
):
|
|
188
180
|
"""Provide export buttons for a dataframe"""
|
|
189
181
|
|
|
@@ -195,34 +187,34 @@ def export_dataframe(
|
|
|
195
187
|
for idx, fmt in enumerate(formats):
|
|
196
188
|
with cols[idx]:
|
|
197
189
|
if fmt == "csv":
|
|
198
|
-
csv = df.to_csv(index=False).encode(
|
|
190
|
+
csv = df.to_csv(index=False).encode("utf-8")
|
|
199
191
|
st.download_button(
|
|
200
192
|
label="📥 Download CSV",
|
|
201
193
|
data=csv,
|
|
202
194
|
file_name=f"{filename}.csv",
|
|
203
195
|
mime="text/csv",
|
|
204
|
-
key=f"{key_prefix}_csv"
|
|
196
|
+
key=f"{key_prefix}_csv",
|
|
205
197
|
)
|
|
206
198
|
elif fmt == "json":
|
|
207
|
-
json_str = df.to_json(orient=
|
|
199
|
+
json_str = df.to_json(orient="records", indent=2)
|
|
208
200
|
st.download_button(
|
|
209
201
|
label="📥 Download JSON",
|
|
210
202
|
data=json_str,
|
|
211
203
|
file_name=f"{filename}.json",
|
|
212
204
|
mime="application/json",
|
|
213
|
-
key=f"{key_prefix}_json"
|
|
205
|
+
key=f"{key_prefix}_json",
|
|
214
206
|
)
|
|
215
207
|
elif fmt == "excel":
|
|
216
208
|
# Requires openpyxl
|
|
217
209
|
try:
|
|
218
210
|
buffer = BytesIO()
|
|
219
|
-
df.to_excel(buffer, index=False, engine=
|
|
211
|
+
df.to_excel(buffer, index=False, engine="openpyxl")
|
|
220
212
|
st.download_button(
|
|
221
213
|
label="📥 Download Excel",
|
|
222
214
|
data=buffer.getvalue(),
|
|
223
215
|
file_name=f"{filename}.xlsx",
|
|
224
216
|
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
225
|
-
key=f"{key_prefix}_excel"
|
|
217
|
+
key=f"{key_prefix}_excel",
|
|
226
218
|
)
|
|
227
219
|
except ImportError:
|
|
228
220
|
st.warning("Excel export requires openpyxl package")
|