mcli-framework 7.6.0__py3-none-any.whl → 7.6.2__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.

Files changed (49) hide show
  1. mcli/app/commands_cmd.py +51 -39
  2. mcli/app/main.py +10 -2
  3. mcli/app/model_cmd.py +1 -1
  4. mcli/lib/custom_commands.py +4 -10
  5. mcli/ml/api/app.py +1 -5
  6. mcli/ml/dashboard/app.py +2 -2
  7. mcli/ml/dashboard/app_integrated.py +168 -116
  8. mcli/ml/dashboard/app_supabase.py +7 -3
  9. mcli/ml/dashboard/app_training.py +3 -6
  10. mcli/ml/dashboard/components/charts.py +74 -115
  11. mcli/ml/dashboard/components/metrics.py +24 -44
  12. mcli/ml/dashboard/components/tables.py +32 -40
  13. mcli/ml/dashboard/overview.py +102 -78
  14. mcli/ml/dashboard/pages/cicd.py +103 -56
  15. mcli/ml/dashboard/pages/debug_dependencies.py +35 -28
  16. mcli/ml/dashboard/pages/gravity_viz.py +374 -313
  17. mcli/ml/dashboard/pages/monte_carlo_predictions.py +50 -48
  18. mcli/ml/dashboard/pages/predictions_enhanced.py +396 -248
  19. mcli/ml/dashboard/pages/scrapers_and_logs.py +299 -273
  20. mcli/ml/dashboard/pages/test_portfolio.py +153 -121
  21. mcli/ml/dashboard/pages/trading.py +238 -169
  22. mcli/ml/dashboard/pages/workflows.py +129 -84
  23. mcli/ml/dashboard/streamlit_extras_utils.py +70 -79
  24. mcli/ml/dashboard/utils.py +24 -21
  25. mcli/ml/dashboard/warning_suppression.py +6 -4
  26. mcli/ml/database/session.py +16 -5
  27. mcli/ml/mlops/pipeline_orchestrator.py +1 -3
  28. mcli/ml/predictions/monte_carlo.py +6 -18
  29. mcli/ml/trading/alpaca_client.py +95 -96
  30. mcli/ml/trading/migrations.py +76 -40
  31. mcli/ml/trading/models.py +78 -60
  32. mcli/ml/trading/paper_trading.py +92 -74
  33. mcli/ml/trading/risk_management.py +106 -85
  34. mcli/ml/trading/trading_service.py +155 -110
  35. mcli/ml/training/train_model.py +1 -3
  36. mcli/self/self_cmd.py +71 -57
  37. mcli/workflow/daemon/daemon.py +2 -0
  38. mcli/workflow/model_service/openai_adapter.py +6 -2
  39. mcli/workflow/politician_trading/models.py +6 -2
  40. mcli/workflow/politician_trading/scrapers_corporate_registry.py +39 -88
  41. mcli/workflow/politician_trading/scrapers_free_sources.py +32 -39
  42. mcli/workflow/politician_trading/scrapers_third_party.py +21 -39
  43. mcli/workflow/politician_trading/seed_database.py +70 -89
  44. {mcli_framework-7.6.0.dist-info → mcli_framework-7.6.2.dist-info}/METADATA +1 -1
  45. {mcli_framework-7.6.0.dist-info → mcli_framework-7.6.2.dist-info}/RECORD +49 -49
  46. {mcli_framework-7.6.0.dist-info → mcli_framework-7.6.2.dist-info}/WHEEL +0 -0
  47. {mcli_framework-7.6.0.dist-info → mcli_framework-7.6.2.dist-info}/entry_points.txt +0 -0
  48. {mcli_framework-7.6.0.dist-info → mcli_framework-7.6.2.dist-info}/licenses/LICENSE +0 -0
  49. {mcli_framework-7.6.0.dist-info → mcli_framework-7.6.2.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,11 @@
1
1
  """Reusable chart components for Streamlit dashboards"""
2
2
 
3
- import plotly.graph_objects as go
4
- import plotly.express as px
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
- 'completed': '#10b981',
51
- 'running': '#3b82f6',
52
- 'pending': '#f59e0b',
53
- 'failed': '#ef4444',
54
- 'cancelled': '#6b7280'
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(), '#6b7280') for status in status_counts.index]
58
-
59
- fig = go.Figure(data=[go.Pie(
60
- labels=status_counts.index,
61
- values=status_counts.values,
62
- marker=dict(colors=colors),
63
- hole=0.4
64
- )])
65
-
66
- fig.update_layout(
67
- title=title,
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(go.Scatter(
88
- x=data[time_col],
89
- y=data[metric_col],
90
- mode='lines+markers',
91
- name=metric_col,
92
- line=dict(width=3),
93
- marker=dict(size=8)
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=[[{'type': 'indicator'}] * cols] * rows,
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={'text': metric},
170
+ title={"text": metric},
200
171
  gauge={
201
- 'axis': {'range': [None, max_val]},
202
- 'bar': {'color': "darkblue"},
203
- 'steps': [
204
- {'range': [0, max_val * 0.5], 'color': "lightgray"},
205
- {'range': [max_val * 0.5, max_val * 0.8], 'color': "gray"}
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
- 'threshold': {
208
- 'line': {'color': "red", 'width': 4},
209
- 'thickness': 0.75,
210
- 'value': max_val * 0.9
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(go.Waterfall(
234
- name="",
235
- orientation="v",
236
- measure=["relative"] * (len(values) - 1) + ["total"],
237
- x=categories,
238
- y=values,
239
- connector={"line": {"color": "rgb(63, 63, 63)"}},
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
- 'completed': '🟢',
37
- 'success': '🟢',
38
- 'running': '🔵',
39
- 'in_progress': '🔵',
40
- 'pending': '🟡',
41
- 'waiting': '🟡',
42
- 'failed': '🔴',
43
- 'error': '🔴',
44
- 'cancelled': '',
45
- 'unknown': ''
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('value', '-'),
72
- delta=value.get('delta'),
73
- delta_color=value.get('delta_color', 'normal'),
74
- help_text=value.get('help'),
75
- icon=value.get('icon')
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 streamlit as st
3
+ from typing import Any, Callable, List, Optional
4
+
4
5
  import pandas as pd
5
- from typing import Optional, List, Callable, Any
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[df[col_name].astype(str).str.contains(search_text, case=False, na=False)]
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[(df[col_name] >= pd.Timestamp(date_range[0])) &
115
- (df[col_name] <= pd.Timestamp(date_range[1]))]
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('icon', '')
149
- button_label = f"{icon} {action['label']}" if icon else action['label']
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['callback'](row)
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('utf-8')
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='records', indent=2)
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='openpyxl')
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")