mcli-framework 7.1.3__py3-none-any.whl → 7.2.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.

Potentially problematic release.


This version of mcli-framework might be problematic. Click here for more details.

Files changed (38) hide show
  1. mcli/app/main.py +10 -0
  2. mcli/lib/custom_commands.py +424 -0
  3. mcli/lib/paths.py +12 -0
  4. mcli/ml/dashboard/app.py +13 -13
  5. mcli/ml/dashboard/app_integrated.py +1292 -148
  6. mcli/ml/dashboard/app_supabase.py +46 -21
  7. mcli/ml/dashboard/app_training.py +14 -14
  8. mcli/ml/dashboard/components/charts.py +258 -0
  9. mcli/ml/dashboard/components/metrics.py +125 -0
  10. mcli/ml/dashboard/components/tables.py +228 -0
  11. mcli/ml/dashboard/pages/cicd.py +382 -0
  12. mcli/ml/dashboard/pages/predictions_enhanced.py +820 -0
  13. mcli/ml/dashboard/pages/scrapers_and_logs.py +1060 -0
  14. mcli/ml/dashboard/pages/workflows.py +533 -0
  15. mcli/ml/training/train_model.py +569 -0
  16. mcli/self/self_cmd.py +322 -94
  17. mcli/workflow/politician_trading/data_sources.py +259 -1
  18. mcli/workflow/politician_trading/models.py +159 -1
  19. mcli/workflow/politician_trading/scrapers_corporate_registry.py +846 -0
  20. mcli/workflow/politician_trading/scrapers_free_sources.py +516 -0
  21. mcli/workflow/politician_trading/scrapers_third_party.py +391 -0
  22. mcli/workflow/politician_trading/seed_database.py +539 -0
  23. mcli/workflow/workflow.py +8 -27
  24. {mcli_framework-7.1.3.dist-info → mcli_framework-7.2.0.dist-info}/METADATA +1 -1
  25. {mcli_framework-7.1.3.dist-info → mcli_framework-7.2.0.dist-info}/RECORD +29 -25
  26. mcli/workflow/daemon/api_daemon.py +0 -800
  27. mcli/workflow/daemon/commands.py +0 -1196
  28. mcli/workflow/dashboard/dashboard_cmd.py +0 -120
  29. mcli/workflow/file/file.py +0 -100
  30. mcli/workflow/git_commit/commands.py +0 -430
  31. mcli/workflow/politician_trading/commands.py +0 -1939
  32. mcli/workflow/scheduler/commands.py +0 -493
  33. mcli/workflow/sync/sync_cmd.py +0 -437
  34. mcli/workflow/videos/videos.py +0 -242
  35. {mcli_framework-7.1.3.dist-info → mcli_framework-7.2.0.dist-info}/WHEEL +0 -0
  36. {mcli_framework-7.1.3.dist-info → mcli_framework-7.2.0.dist-info}/entry_points.txt +0 -0
  37. {mcli_framework-7.1.3.dist-info → mcli_framework-7.2.0.dist-info}/licenses/LICENSE +0 -0
  38. {mcli_framework-7.1.3.dist-info → mcli_framework-7.2.0.dist-info}/top_level.txt +0 -0
@@ -42,18 +42,34 @@ st.markdown(
42
42
 
43
43
  @st.cache_resource
44
44
  def get_supabase_client() -> Client:
45
- """Get Supabase client"""
46
- url = os.getenv("SUPABASE_URL", "")
47
- # Try both SUPABASE_KEY and SUPABASE_ANON_KEY
48
- key = os.getenv("SUPABASE_KEY", "") or os.getenv("SUPABASE_ANON_KEY", "")
45
+ """Get Supabase client with Streamlit Cloud secrets support"""
46
+ # Try Streamlit secrets first (for Streamlit Cloud), then fall back to environment variables (for local dev)
47
+ try:
48
+ url = st.secrets.get("SUPABASE_URL", "")
49
+ key = st.secrets.get("SUPABASE_KEY", "") or st.secrets.get("SUPABASE_SERVICE_ROLE_KEY", "")
50
+ except (AttributeError, FileNotFoundError):
51
+ # Secrets not available, try environment variables
52
+ url = os.getenv("SUPABASE_URL", "")
53
+ key = os.getenv("SUPABASE_KEY", "") or os.getenv("SUPABASE_SERVICE_ROLE_KEY", "")
49
54
 
50
55
  if not url or not key:
51
56
  st.warning(
52
- "⚠️ Supabase credentials not found. Set SUPABASE_URL and SUPABASE_ANON_KEY environment variables."
57
+ "⚠️ Supabase credentials not found. Configure SUPABASE_URL and SUPABASE_KEY in Streamlit Cloud secrets or environment variables."
53
58
  )
54
59
  return None
55
60
 
56
- return create_client(url, key)
61
+ try:
62
+ client = create_client(url, key)
63
+ # Test connection
64
+ try:
65
+ client.table("politicians").select("id").limit(1).execute()
66
+ return client
67
+ except Exception as e:
68
+ st.error(f"❌ Supabase connection test failed: {e}")
69
+ return None
70
+ except Exception as e:
71
+ st.error(f"❌ Failed to create Supabase client: {e}")
72
+ return None
57
73
 
58
74
 
59
75
  @st.cache_data(ttl=30)
@@ -91,9 +107,18 @@ def get_disclosures_data():
91
107
  .limit(500)
92
108
  .execute()
93
109
  )
94
- return pd.DataFrame(response.data)
110
+ df = pd.DataFrame(response.data)
111
+
112
+ # Convert datetime columns to proper datetime format
113
+ date_columns = ['transaction_date', 'disclosure_date', 'created_at', 'updated_at']
114
+ for col in date_columns:
115
+ if col in df.columns:
116
+ df[col] = pd.to_datetime(df[col], format='ISO8601', errors='coerce')
117
+
118
+ return df
95
119
  except Exception as e:
96
120
  st.error(f"Error fetching disclosures: {e}")
121
+ print(f"Error details: {e}") # Debug output
97
122
  return pd.DataFrame()
98
123
 
99
124
 
@@ -330,7 +355,7 @@ def show_overview():
330
355
  fig = px.pie(
331
356
  values=type_counts.values, names=type_counts.index, title="Transaction Types"
332
357
  )
333
- st.plotly_chart(fig, use_container_width=True)
358
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
334
359
  else:
335
360
  st.info("No disclosure data available")
336
361
 
@@ -344,7 +369,7 @@ def show_overview():
344
369
  orientation="h",
345
370
  title="Most Traded Stocks",
346
371
  )
347
- st.plotly_chart(fig, use_container_width=True)
372
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
348
373
  else:
349
374
  st.info("No ticker data available")
350
375
 
@@ -383,7 +408,7 @@ def show_politicians():
383
408
  filtered = filtered[filtered["is_active"] == True]
384
409
 
385
410
  # Display data
386
- st.dataframe(filtered, use_container_width=True)
411
+ st.dataframe(filtered, width="stretch")
387
412
 
388
413
  # Stats
389
414
  col1, col2 = st.columns(2)
@@ -393,14 +418,14 @@ def show_politicians():
393
418
  fig = px.pie(
394
419
  values=party_dist.values, names=party_dist.index, title="Party Distribution"
395
420
  )
396
- st.plotly_chart(fig, use_container_width=True)
421
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
397
422
  with col2:
398
423
  if "state" in filtered:
399
424
  state_dist = filtered["state"].value_counts().head(10)
400
425
  fig = px.bar(
401
426
  x=state_dist.values, y=state_dist.index, orientation="h", title="Top States"
402
427
  )
403
- st.plotly_chart(fig, use_container_width=True)
428
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
404
429
  else:
405
430
  st.warning("No politician data available")
406
431
 
@@ -447,7 +472,7 @@ def show_disclosures():
447
472
  ]
448
473
 
449
474
  # Display data
450
- st.dataframe(filtered, use_container_width=True)
475
+ st.dataframe(filtered, width="stretch")
451
476
 
452
477
  # Analysis
453
478
  if not filtered.empty:
@@ -463,7 +488,7 @@ def show_disclosures():
463
488
  y=daily_volume.values,
464
489
  title="Trading Volume Over Time",
465
490
  )
466
- st.plotly_chart(fig, use_container_width=True)
491
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
467
492
 
468
493
  with col2:
469
494
  # Top politicians by trading
@@ -475,7 +500,7 @@ def show_disclosures():
475
500
  orientation="h",
476
501
  title="Most Active Traders",
477
502
  )
478
- st.plotly_chart(fig, use_container_width=True)
503
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
479
504
  else:
480
505
  st.warning("No disclosure data available")
481
506
 
@@ -487,14 +512,14 @@ def show_predictions():
487
512
  predictions = get_predictions_data()
488
513
 
489
514
  if not predictions.empty:
490
- st.dataframe(predictions, use_container_width=True)
515
+ st.dataframe(predictions, width="stretch")
491
516
 
492
517
  # Add prediction analysis charts if we have data
493
518
  if "confidence" in predictions:
494
519
  fig = px.histogram(
495
520
  predictions, x="confidence", title="Prediction Confidence Distribution"
496
521
  )
497
- st.plotly_chart(fig, use_container_width=True)
522
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
498
523
  else:
499
524
  st.info(
500
525
  "No ML predictions available yet. The ML pipeline will generate predictions once sufficient data is collected."
@@ -521,7 +546,7 @@ def show_jobs():
521
546
  st.metric("Failed", status_counts.get("failed", 0))
522
547
 
523
548
  # Jobs table
524
- st.dataframe(jobs, use_container_width=True)
549
+ st.dataframe(jobs, width="stretch")
525
550
 
526
551
  # Success rate over time
527
552
  if "created_at" in jobs:
@@ -543,7 +568,7 @@ def show_jobs():
543
568
  )
544
569
 
545
570
  fig.update_layout(title="Job Status Over Time", xaxis_title="Date", yaxis_title="Count")
546
- st.plotly_chart(fig, use_container_width=True)
571
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
547
572
  else:
548
573
  st.warning("No job data available")
549
574
 
@@ -617,11 +642,11 @@ def show_system_health():
617
642
 
618
643
  col1, col2 = st.columns(2)
619
644
  with col1:
620
- st.dataframe(stats_df, use_container_width=True)
645
+ st.dataframe(stats_df, width="stretch")
621
646
 
622
647
  with col2:
623
648
  fig = px.bar(stats_df, x="Entity", y="Count", title="Database Records")
624
- st.plotly_chart(fig, use_container_width=True)
649
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
625
650
 
626
651
 
627
652
  if __name__ == "__main__":
@@ -203,7 +203,7 @@ def show_model_comparison():
203
203
  sorted_df.style.highlight_max(
204
204
  subset=["test_accuracy", "r2"], color="lightgreen"
205
205
  ).highlight_min(subset=["test_loss", "rmse", "mae"], color="lightgreen"),
206
- use_container_width=True,
206
+ width="stretch",
207
207
  )
208
208
 
209
209
  # Visualization section
@@ -222,7 +222,7 @@ def show_model_comparison():
222
222
  labels={"value": "Accuracy", "variable": "Split"},
223
223
  )
224
224
  fig.update_layout(xaxis_tickangle=-45)
225
- st.plotly_chart(fig, use_container_width=True)
225
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
226
226
 
227
227
  with col2:
228
228
  # Loss comparison
@@ -235,7 +235,7 @@ def show_model_comparison():
235
235
  labels={"value": "Loss", "variable": "Split"},
236
236
  )
237
237
  fig.update_layout(xaxis_tickangle=-45)
238
- st.plotly_chart(fig, use_container_width=True)
238
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
239
239
 
240
240
  # Additional metrics if available
241
241
  if models_df["rmse"].max() > 0:
@@ -253,7 +253,7 @@ def show_model_comparison():
253
253
  hover_data=["name"],
254
254
  title="RMSE vs MAE (sized by R²)",
255
255
  )
256
- st.plotly_chart(fig, use_container_width=True)
256
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
257
257
 
258
258
  with col2:
259
259
  # R² score comparison
@@ -267,7 +267,7 @@ def show_model_comparison():
267
267
  color_continuous_scale="Greens",
268
268
  )
269
269
  fig.update_layout(xaxis_tickangle=-45)
270
- st.plotly_chart(fig, use_container_width=True)
270
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
271
271
 
272
272
 
273
273
  def show_residual_analysis():
@@ -313,7 +313,7 @@ def show_residual_analysis():
313
313
  fig.update_layout(
314
314
  xaxis_title="Prediction Index", yaxis_title="Residuals", hovermode="x unified"
315
315
  )
316
- st.plotly_chart(fig, use_container_width=True)
316
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
317
317
 
318
318
  # Statistics
319
319
  col1, col2, col3 = st.columns(3)
@@ -347,7 +347,7 @@ def show_residual_analysis():
347
347
  )
348
348
 
349
349
  fig.update_layout(xaxis_title="Residuals", yaxis_title="Frequency", showlegend=True)
350
- st.plotly_chart(fig, use_container_width=True)
350
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
351
351
 
352
352
  # Normality tests
353
353
  _, p_value = stats.normaltest(residuals)
@@ -389,7 +389,7 @@ def show_residual_analysis():
389
389
  yaxis_title="Sample Quantiles",
390
390
  title="Q-Q Plot (Normal Distribution)",
391
391
  )
392
- st.plotly_chart(fig, use_container_width=True)
392
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
393
393
 
394
394
  st.info(f"Correlation with normal distribution: {r:.4f}")
395
395
 
@@ -415,7 +415,7 @@ def show_residual_analysis():
415
415
  yaxis_title="Residuals",
416
416
  title="Residuals vs Predicted (looking for patterns)",
417
417
  )
418
- st.plotly_chart(fig, use_container_width=True)
418
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
419
419
 
420
420
  st.info(
421
421
  "💡 Ideally, residuals should be randomly scattered around zero with no clear patterns."
@@ -463,11 +463,11 @@ def show_feature_importance():
463
463
  color_continuous_scale="Viridis",
464
464
  )
465
465
  fig.update_layout(height=600, yaxis={"categoryorder": "total ascending"})
466
- st.plotly_chart(fig, use_container_width=True)
466
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
467
467
 
468
468
  # Feature importance table
469
469
  st.subheader("Feature Importance Table")
470
- st.dataframe(feature_df.head(top_n), use_container_width=True)
470
+ st.dataframe(feature_df.head(top_n), width="stretch")
471
471
 
472
472
  # Feature categories (similar to bitcoin project)
473
473
  st.subheader("Feature Categories")
@@ -524,7 +524,7 @@ def show_feature_importance():
524
524
  names="Category",
525
525
  title="Feature Importance by Category",
526
526
  )
527
- st.plotly_chart(fig, use_container_width=True)
527
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
528
528
  else:
529
529
  st.warning("No feature information available for this model")
530
530
  finally:
@@ -565,7 +565,7 @@ def show_training_history():
565
565
  display_df = jobs_df[["name", "status", "started_at", "duration_seconds"]].copy()
566
566
  display_df["duration_minutes"] = display_df["duration_seconds"] / 60
567
567
 
568
- st.dataframe(display_df, use_container_width=True)
568
+ st.dataframe(display_df, width="stretch")
569
569
 
570
570
  # Training duration distribution
571
571
  if not jobs_df["duration_seconds"].isna().all():
@@ -578,7 +578,7 @@ def show_training_history():
578
578
  title="Training Duration Distribution",
579
579
  labels={"duration_seconds": "Duration (seconds)"},
580
580
  )
581
- st.plotly_chart(fig, use_container_width=True)
581
+ st.plotly_chart(fig, width="stretch", config={"responsive": True})
582
582
 
583
583
 
584
584
  def main():
@@ -0,0 +1,258 @@
1
+ """Reusable chart components for Streamlit dashboards"""
2
+
3
+ import plotly.graph_objects as go
4
+ import plotly.express as px
5
+ import pandas as pd
6
+ import streamlit as st
7
+ from typing import Optional, Dict, List, Any
8
+
9
+
10
+ def create_timeline_chart(
11
+ data: pd.DataFrame,
12
+ x_col: str,
13
+ y_col: str,
14
+ title: str = "Timeline",
15
+ color_col: Optional[str] = None,
16
+ height: int = 400
17
+ ) -> go.Figure:
18
+ """Create a timeline chart with Plotly"""
19
+
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
+ )
28
+
29
+ fig.update_layout(
30
+ height=height,
31
+ hovermode='x unified',
32
+ showlegend=True if color_col else False
33
+ )
34
+
35
+ return fig
36
+
37
+
38
+ def create_status_pie_chart(
39
+ data: pd.DataFrame,
40
+ status_col: str,
41
+ title: str = "Status Distribution",
42
+ color_map: Optional[Dict[str, str]] = None
43
+ ) -> go.Figure:
44
+ """Create a pie chart for status distribution"""
45
+
46
+ status_counts = data[status_col].value_counts()
47
+
48
+ if color_map is None:
49
+ color_map = {
50
+ 'completed': '#10b981',
51
+ 'running': '#3b82f6',
52
+ 'pending': '#f59e0b',
53
+ 'failed': '#ef4444',
54
+ 'cancelled': '#6b7280'
55
+ }
56
+
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
70
+ )
71
+
72
+ return fig
73
+
74
+
75
+ def create_metric_trend_chart(
76
+ data: pd.DataFrame,
77
+ time_col: str,
78
+ metric_col: str,
79
+ title: str,
80
+ target_value: Optional[float] = None
81
+ ) -> go.Figure:
82
+ """Create a metric trend chart with optional target line"""
83
+
84
+ fig = go.Figure()
85
+
86
+ # 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
+ ))
95
+
96
+ # Add target line if specified
97
+ if target_value is not None:
98
+ fig.add_hline(
99
+ y=target_value,
100
+ line_dash="dash",
101
+ line_color="red",
102
+ annotation_text=f"Target: {target_value}"
103
+ )
104
+
105
+ fig.update_layout(
106
+ title=title,
107
+ xaxis_title=time_col,
108
+ yaxis_title=metric_col,
109
+ height=400,
110
+ hovermode='x unified'
111
+ )
112
+
113
+ return fig
114
+
115
+
116
+ def create_heatmap(
117
+ data: pd.DataFrame,
118
+ x_col: str,
119
+ y_col: str,
120
+ value_col: str,
121
+ title: str = "Heatmap",
122
+ color_scale: str = "Blues"
123
+ ) -> go.Figure:
124
+ """Create a heatmap visualization"""
125
+
126
+ pivot_data = data.pivot_table(
127
+ index=y_col,
128
+ columns=x_col,
129
+ values=value_col,
130
+ aggfunc='mean'
131
+ )
132
+
133
+ fig = px.imshow(
134
+ pivot_data,
135
+ color_continuous_scale=color_scale,
136
+ title=title,
137
+ aspect="auto"
138
+ )
139
+
140
+ fig.update_layout(height=400)
141
+
142
+ return fig
143
+
144
+
145
+ def create_gantt_chart(
146
+ data: pd.DataFrame,
147
+ task_col: str,
148
+ start_col: str,
149
+ end_col: str,
150
+ status_col: Optional[str] = None,
151
+ title: str = "Timeline"
152
+ ) -> go.Figure:
153
+ """Create a Gantt chart for job/task scheduling"""
154
+
155
+ 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
162
+ )
163
+
164
+ fig.update_yaxes(categoryorder="total ascending")
165
+ fig.update_layout(height=max(400, len(data) * 30))
166
+
167
+ return fig
168
+
169
+
170
+ def create_multi_metric_gauge(
171
+ values: Dict[str, float],
172
+ max_values: Dict[str, float],
173
+ title: str = "Metrics"
174
+ ) -> go.Figure:
175
+ """Create multiple gauge charts in a grid"""
176
+
177
+ from plotly.subplots import make_subplots
178
+
179
+ n_metrics = len(values)
180
+ cols = min(3, n_metrics)
181
+ rows = (n_metrics + cols - 1) // cols
182
+
183
+ fig = make_subplots(
184
+ rows=rows,
185
+ cols=cols,
186
+ specs=[[{'type': 'indicator'}] * cols] * rows,
187
+ subplot_titles=list(values.keys())
188
+ )
189
+
190
+ for idx, (metric, value) in enumerate(values.items()):
191
+ row = idx // cols + 1
192
+ col = idx % cols + 1
193
+ max_val = max_values.get(metric, 100)
194
+
195
+ fig.add_trace(
196
+ go.Indicator(
197
+ mode="gauge+number+delta",
198
+ value=value,
199
+ title={'text': metric},
200
+ 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"}
206
+ ],
207
+ 'threshold': {
208
+ 'line': {'color': "red", 'width': 4},
209
+ 'thickness': 0.75,
210
+ 'value': max_val * 0.9
211
+ }
212
+ }
213
+ ),
214
+ row=row,
215
+ col=col
216
+ )
217
+
218
+ fig.update_layout(
219
+ title=title,
220
+ height=300 * rows
221
+ )
222
+
223
+ return fig
224
+
225
+
226
+ def create_waterfall_chart(
227
+ categories: List[str],
228
+ values: List[float],
229
+ title: str = "Waterfall Chart"
230
+ ) -> go.Figure:
231
+ """Create a waterfall chart for step-by-step changes"""
232
+
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
246
+ )
247
+
248
+ return fig
249
+
250
+
251
+ def render_chart(fig: go.Figure, key: Optional[str] = None):
252
+ """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
+ )
@@ -0,0 +1,125 @@
1
+ """Reusable metric display components"""
2
+
3
+ import streamlit as st
4
+ from typing import Optional, Union
5
+
6
+
7
+ def display_metric_card(
8
+ label: str,
9
+ value: Union[int, float, str],
10
+ delta: Optional[Union[int, float, str]] = None,
11
+ delta_color: str = "normal",
12
+ help_text: Optional[str] = None,
13
+ icon: Optional[str] = None
14
+ ):
15
+ """Display a metric card with optional delta and icon"""
16
+
17
+ if icon:
18
+ label = f"{icon} {label}"
19
+
20
+ st.metric(
21
+ label=label,
22
+ value=value,
23
+ delta=delta,
24
+ delta_color=delta_color,
25
+ help=help_text
26
+ )
27
+
28
+
29
+ def display_status_badge(
30
+ status: str,
31
+ size: str = "medium"
32
+ ) -> str:
33
+ """Return a colored status badge"""
34
+
35
+ status_colors = {
36
+ 'completed': '🟢',
37
+ 'success': '🟢',
38
+ 'running': '🔵',
39
+ 'in_progress': '🔵',
40
+ 'pending': '🟡',
41
+ 'waiting': '🟡',
42
+ 'failed': '🔴',
43
+ 'error': '🔴',
44
+ 'cancelled': '⚪',
45
+ 'unknown': '⚫'
46
+ }
47
+
48
+ icon = status_colors.get(status.lower(), '⚫')
49
+
50
+ if size == "small":
51
+ return f"{icon} {status}"
52
+ elif size == "large":
53
+ return f"## {icon} {status}"
54
+ else: # medium
55
+ return f"### {icon} {status}"
56
+
57
+
58
+ def display_kpi_row(metrics: dict, columns: Optional[int] = None):
59
+ """Display a row of KPIs in columns"""
60
+
61
+ if columns is None:
62
+ columns = len(metrics)
63
+
64
+ cols = st.columns(columns)
65
+
66
+ for idx, (label, value) in enumerate(metrics.items()):
67
+ with cols[idx % columns]:
68
+ if isinstance(value, dict):
69
+ display_metric_card(
70
+ 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')
76
+ )
77
+ else:
78
+ st.metric(label=label, value=value)
79
+
80
+
81
+ def display_progress_bar(
82
+ label: str,
83
+ progress: float,
84
+ show_percentage: bool = True
85
+ ):
86
+ """Display a progress bar with label"""
87
+
88
+ st.text(label)
89
+ st.progress(min(1.0, max(0.0, progress)))
90
+
91
+ if show_percentage:
92
+ st.caption(f"{progress * 100:.1f}%")
93
+
94
+
95
+ def display_health_indicator(
96
+ component: str,
97
+ is_healthy: bool,
98
+ details: Optional[str] = None
99
+ ):
100
+ """Display a health status indicator"""
101
+
102
+ if is_healthy:
103
+ st.success(f"✅ {component}: Healthy" + (f" ({details})" if details else ""))
104
+ else:
105
+ st.error(f"❌ {component}: Unhealthy" + (f" ({details})" if details else ""))
106
+
107
+
108
+ def display_alert(
109
+ message: str,
110
+ alert_type: str = "info",
111
+ icon: Optional[str] = None
112
+ ):
113
+ """Display an alert message"""
114
+
115
+ if icon:
116
+ message = f"{icon} {message}"
117
+
118
+ if alert_type == "success":
119
+ st.success(message)
120
+ elif alert_type == "warning":
121
+ st.warning(message)
122
+ elif alert_type == "error":
123
+ st.error(message)
124
+ else: # info
125
+ st.info(message)