foundry-mcp 0.3.3__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.
Files changed (135) hide show
  1. foundry_mcp/__init__.py +7 -0
  2. foundry_mcp/cli/__init__.py +80 -0
  3. foundry_mcp/cli/__main__.py +9 -0
  4. foundry_mcp/cli/agent.py +96 -0
  5. foundry_mcp/cli/commands/__init__.py +37 -0
  6. foundry_mcp/cli/commands/cache.py +137 -0
  7. foundry_mcp/cli/commands/dashboard.py +148 -0
  8. foundry_mcp/cli/commands/dev.py +446 -0
  9. foundry_mcp/cli/commands/journal.py +377 -0
  10. foundry_mcp/cli/commands/lifecycle.py +274 -0
  11. foundry_mcp/cli/commands/modify.py +824 -0
  12. foundry_mcp/cli/commands/plan.py +633 -0
  13. foundry_mcp/cli/commands/pr.py +393 -0
  14. foundry_mcp/cli/commands/review.py +652 -0
  15. foundry_mcp/cli/commands/session.py +479 -0
  16. foundry_mcp/cli/commands/specs.py +856 -0
  17. foundry_mcp/cli/commands/tasks.py +807 -0
  18. foundry_mcp/cli/commands/testing.py +676 -0
  19. foundry_mcp/cli/commands/validate.py +982 -0
  20. foundry_mcp/cli/config.py +98 -0
  21. foundry_mcp/cli/context.py +259 -0
  22. foundry_mcp/cli/flags.py +266 -0
  23. foundry_mcp/cli/logging.py +212 -0
  24. foundry_mcp/cli/main.py +44 -0
  25. foundry_mcp/cli/output.py +122 -0
  26. foundry_mcp/cli/registry.py +110 -0
  27. foundry_mcp/cli/resilience.py +178 -0
  28. foundry_mcp/cli/transcript.py +217 -0
  29. foundry_mcp/config.py +850 -0
  30. foundry_mcp/core/__init__.py +144 -0
  31. foundry_mcp/core/ai_consultation.py +1636 -0
  32. foundry_mcp/core/cache.py +195 -0
  33. foundry_mcp/core/capabilities.py +446 -0
  34. foundry_mcp/core/concurrency.py +898 -0
  35. foundry_mcp/core/context.py +540 -0
  36. foundry_mcp/core/discovery.py +1603 -0
  37. foundry_mcp/core/error_collection.py +728 -0
  38. foundry_mcp/core/error_store.py +592 -0
  39. foundry_mcp/core/feature_flags.py +592 -0
  40. foundry_mcp/core/health.py +749 -0
  41. foundry_mcp/core/journal.py +694 -0
  42. foundry_mcp/core/lifecycle.py +412 -0
  43. foundry_mcp/core/llm_config.py +1350 -0
  44. foundry_mcp/core/llm_patterns.py +510 -0
  45. foundry_mcp/core/llm_provider.py +1569 -0
  46. foundry_mcp/core/logging_config.py +374 -0
  47. foundry_mcp/core/metrics_persistence.py +584 -0
  48. foundry_mcp/core/metrics_registry.py +327 -0
  49. foundry_mcp/core/metrics_store.py +641 -0
  50. foundry_mcp/core/modifications.py +224 -0
  51. foundry_mcp/core/naming.py +123 -0
  52. foundry_mcp/core/observability.py +1216 -0
  53. foundry_mcp/core/otel.py +452 -0
  54. foundry_mcp/core/otel_stubs.py +264 -0
  55. foundry_mcp/core/pagination.py +255 -0
  56. foundry_mcp/core/progress.py +317 -0
  57. foundry_mcp/core/prometheus.py +577 -0
  58. foundry_mcp/core/prompts/__init__.py +464 -0
  59. foundry_mcp/core/prompts/fidelity_review.py +546 -0
  60. foundry_mcp/core/prompts/markdown_plan_review.py +511 -0
  61. foundry_mcp/core/prompts/plan_review.py +623 -0
  62. foundry_mcp/core/providers/__init__.py +225 -0
  63. foundry_mcp/core/providers/base.py +476 -0
  64. foundry_mcp/core/providers/claude.py +460 -0
  65. foundry_mcp/core/providers/codex.py +619 -0
  66. foundry_mcp/core/providers/cursor_agent.py +642 -0
  67. foundry_mcp/core/providers/detectors.py +488 -0
  68. foundry_mcp/core/providers/gemini.py +405 -0
  69. foundry_mcp/core/providers/opencode.py +616 -0
  70. foundry_mcp/core/providers/opencode_wrapper.js +302 -0
  71. foundry_mcp/core/providers/package-lock.json +24 -0
  72. foundry_mcp/core/providers/package.json +25 -0
  73. foundry_mcp/core/providers/registry.py +607 -0
  74. foundry_mcp/core/providers/test_provider.py +171 -0
  75. foundry_mcp/core/providers/validation.py +729 -0
  76. foundry_mcp/core/rate_limit.py +427 -0
  77. foundry_mcp/core/resilience.py +600 -0
  78. foundry_mcp/core/responses.py +934 -0
  79. foundry_mcp/core/review.py +366 -0
  80. foundry_mcp/core/security.py +438 -0
  81. foundry_mcp/core/spec.py +1650 -0
  82. foundry_mcp/core/task.py +1289 -0
  83. foundry_mcp/core/testing.py +450 -0
  84. foundry_mcp/core/validation.py +2081 -0
  85. foundry_mcp/dashboard/__init__.py +32 -0
  86. foundry_mcp/dashboard/app.py +119 -0
  87. foundry_mcp/dashboard/components/__init__.py +17 -0
  88. foundry_mcp/dashboard/components/cards.py +88 -0
  89. foundry_mcp/dashboard/components/charts.py +234 -0
  90. foundry_mcp/dashboard/components/filters.py +136 -0
  91. foundry_mcp/dashboard/components/tables.py +195 -0
  92. foundry_mcp/dashboard/data/__init__.py +11 -0
  93. foundry_mcp/dashboard/data/stores.py +433 -0
  94. foundry_mcp/dashboard/launcher.py +289 -0
  95. foundry_mcp/dashboard/views/__init__.py +12 -0
  96. foundry_mcp/dashboard/views/errors.py +217 -0
  97. foundry_mcp/dashboard/views/metrics.py +174 -0
  98. foundry_mcp/dashboard/views/overview.py +160 -0
  99. foundry_mcp/dashboard/views/providers.py +83 -0
  100. foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
  101. foundry_mcp/dashboard/views/tool_usage.py +139 -0
  102. foundry_mcp/prompts/__init__.py +9 -0
  103. foundry_mcp/prompts/workflows.py +525 -0
  104. foundry_mcp/resources/__init__.py +9 -0
  105. foundry_mcp/resources/specs.py +591 -0
  106. foundry_mcp/schemas/__init__.py +38 -0
  107. foundry_mcp/schemas/sdd-spec-schema.json +386 -0
  108. foundry_mcp/server.py +164 -0
  109. foundry_mcp/tools/__init__.py +10 -0
  110. foundry_mcp/tools/unified/__init__.py +71 -0
  111. foundry_mcp/tools/unified/authoring.py +1487 -0
  112. foundry_mcp/tools/unified/context_helpers.py +98 -0
  113. foundry_mcp/tools/unified/documentation_helpers.py +198 -0
  114. foundry_mcp/tools/unified/environment.py +939 -0
  115. foundry_mcp/tools/unified/error.py +462 -0
  116. foundry_mcp/tools/unified/health.py +225 -0
  117. foundry_mcp/tools/unified/journal.py +841 -0
  118. foundry_mcp/tools/unified/lifecycle.py +632 -0
  119. foundry_mcp/tools/unified/metrics.py +777 -0
  120. foundry_mcp/tools/unified/plan.py +745 -0
  121. foundry_mcp/tools/unified/pr.py +294 -0
  122. foundry_mcp/tools/unified/provider.py +629 -0
  123. foundry_mcp/tools/unified/review.py +685 -0
  124. foundry_mcp/tools/unified/review_helpers.py +299 -0
  125. foundry_mcp/tools/unified/router.py +102 -0
  126. foundry_mcp/tools/unified/server.py +580 -0
  127. foundry_mcp/tools/unified/spec.py +808 -0
  128. foundry_mcp/tools/unified/task.py +2202 -0
  129. foundry_mcp/tools/unified/test.py +370 -0
  130. foundry_mcp/tools/unified/verification.py +520 -0
  131. foundry_mcp-0.3.3.dist-info/METADATA +337 -0
  132. foundry_mcp-0.3.3.dist-info/RECORD +135 -0
  133. foundry_mcp-0.3.3.dist-info/WHEEL +4 -0
  134. foundry_mcp-0.3.3.dist-info/entry_points.txt +3 -0
  135. foundry_mcp-0.3.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,32 @@
1
+ """Streamlit-based dashboard for foundry-mcp observability.
2
+
3
+ This module provides a web UI for viewing errors, metrics, provider status,
4
+ and SDD workflow progress.
5
+
6
+ Public API:
7
+ launch_dashboard: Start the Streamlit dashboard server
8
+ stop_dashboard: Stop the running dashboard server
9
+ get_dashboard_status: Check if dashboard is running
10
+
11
+ Usage:
12
+ from foundry_mcp.dashboard import launch_dashboard, stop_dashboard
13
+
14
+ # Start dashboard
15
+ result = launch_dashboard(port=8501, open_browser=True)
16
+ print(f"Dashboard running at {result['url']}")
17
+
18
+ # Stop dashboard
19
+ stop_dashboard()
20
+ """
21
+
22
+ from foundry_mcp.dashboard.launcher import (
23
+ get_dashboard_status,
24
+ launch_dashboard,
25
+ stop_dashboard,
26
+ )
27
+
28
+ __all__ = [
29
+ "launch_dashboard",
30
+ "stop_dashboard",
31
+ "get_dashboard_status",
32
+ ]
@@ -0,0 +1,119 @@
1
+ """Main Streamlit dashboard application.
2
+
3
+ This is the entry point for the Streamlit dashboard.
4
+ Run with: streamlit run src/foundry_mcp/dashboard/app.py
5
+ """
6
+
7
+ import streamlit as st
8
+
9
+ # Page config must be first Streamlit command
10
+ st.set_page_config(
11
+ page_title="foundry-mcp Dashboard",
12
+ page_icon=":chart_with_upwards_trend:",
13
+ layout="wide",
14
+ initial_sidebar_state="expanded",
15
+ )
16
+
17
+ # Import pages after config
18
+ from foundry_mcp.dashboard.views import overview, errors, metrics, tool_usage
19
+
20
+ # Custom dark theme CSS
21
+ st.markdown(
22
+ """
23
+ <style>
24
+ /* Consistent dark theme styling */
25
+ .stMetric {
26
+ background-color: rgba(30, 30, 46, 0.8);
27
+ padding: 1rem;
28
+ border-radius: 0.5rem;
29
+ border: 1px solid rgba(100, 100, 150, 0.3);
30
+ }
31
+ .stMetric label {
32
+ color: #a0a0b0 !important;
33
+ }
34
+ .stMetric [data-testid="stMetricValue"] {
35
+ color: #e0e0f0 !important;
36
+ }
37
+ /* Card-like containers */
38
+ [data-testid="stVerticalBlock"] > [data-testid="stVerticalBlock"] {
39
+ background-color: rgba(30, 30, 46, 0.5);
40
+ border-radius: 0.5rem;
41
+ padding: 0.5rem;
42
+ }
43
+ </style>
44
+ """,
45
+ unsafe_allow_html=True,
46
+ )
47
+
48
+ # Initialize session state
49
+ if "auto_refresh" not in st.session_state:
50
+ st.session_state.auto_refresh = False
51
+ if "refresh_interval" not in st.session_state:
52
+ st.session_state.refresh_interval = 5
53
+
54
+
55
+ def render_sidebar():
56
+ """Render navigation sidebar."""
57
+ with st.sidebar:
58
+ st.title(":cube: foundry-mcp")
59
+ st.caption("Observability Dashboard")
60
+
61
+ st.divider()
62
+
63
+ # Navigation
64
+ page = st.radio(
65
+ "Navigate",
66
+ options=["Overview", "Tool Usage", "Errors", "Metrics"],
67
+ label_visibility="collapsed",
68
+ )
69
+
70
+ st.divider()
71
+
72
+ # Auto-refresh controls
73
+ st.subheader("Settings")
74
+ st.session_state.auto_refresh = st.checkbox(
75
+ "Auto-refresh",
76
+ value=st.session_state.auto_refresh,
77
+ )
78
+ if st.session_state.auto_refresh:
79
+ st.session_state.refresh_interval = st.slider(
80
+ "Interval (sec)",
81
+ min_value=5,
82
+ max_value=60,
83
+ value=st.session_state.refresh_interval,
84
+ )
85
+
86
+ # Manual refresh button
87
+ if st.button("Refresh Now", use_container_width=True):
88
+ st.rerun()
89
+
90
+ return page
91
+
92
+
93
+ def main():
94
+ """Main dashboard entry point."""
95
+ # Render sidebar and get page selection
96
+ page = render_sidebar()
97
+
98
+ # Route to page
99
+ page_map = {
100
+ "Overview": overview.render,
101
+ "Tool Usage": tool_usage.render,
102
+ "Errors": errors.render,
103
+ "Metrics": metrics.render,
104
+ }
105
+
106
+ # Render selected page
107
+ render_func = page_map.get(page, overview.render)
108
+ render_func()
109
+
110
+ # Auto-refresh logic
111
+ if st.session_state.auto_refresh:
112
+ import time
113
+
114
+ time.sleep(st.session_state.refresh_interval)
115
+ st.rerun()
116
+
117
+
118
+ if __name__ == "__main__":
119
+ main()
@@ -0,0 +1,17 @@
1
+ """Reusable dashboard UI components.
2
+
3
+ Components:
4
+ cards: KPI metric card helpers
5
+ charts: Plotly chart builders
6
+ filters: Time range and filter widgets
7
+ tables: Data table configurations
8
+ """
9
+
10
+ from foundry_mcp.dashboard.components import cards, charts, filters, tables
11
+
12
+ __all__ = [
13
+ "cards",
14
+ "charts",
15
+ "filters",
16
+ "tables",
17
+ ]
@@ -0,0 +1,88 @@
1
+ """KPI card components for dashboard."""
2
+
3
+ from typing import Optional
4
+
5
+ import streamlit as st
6
+
7
+
8
+ def metric_card(
9
+ label: str,
10
+ value: str | int | float,
11
+ delta: Optional[str] = None,
12
+ delta_color: str = "normal",
13
+ help_text: Optional[str] = None,
14
+ ) -> None:
15
+ """Render a metric card with optional delta indicator.
16
+
17
+ Args:
18
+ label: Card title/label
19
+ value: Main value to display
20
+ delta: Optional change indicator (e.g., "+5%", "-3")
21
+ delta_color: Color for delta ("normal", "inverse", "off")
22
+ help_text: Optional tooltip help text
23
+ """
24
+ st.metric(
25
+ label=label,
26
+ value=value,
27
+ delta=delta,
28
+ delta_color=delta_color,
29
+ help=help_text,
30
+ )
31
+
32
+
33
+ def status_badge(status: str, label: Optional[str] = None) -> None:
34
+ """Render a status badge with color coding.
35
+
36
+ Args:
37
+ status: Status value ("healthy", "unhealthy", "warning", "unknown")
38
+ label: Optional label to show before status
39
+ """
40
+ colors = {
41
+ "healthy": ":green_circle:",
42
+ "unhealthy": ":red_circle:",
43
+ "warning": ":yellow_circle:",
44
+ "unknown": ":white_circle:",
45
+ "available": ":green_circle:",
46
+ "unavailable": ":red_circle:",
47
+ }
48
+
49
+ emoji = colors.get(status.lower(), ":white_circle:")
50
+ text = label if label else status.title()
51
+ st.markdown(f"{emoji} **{text}**")
52
+
53
+
54
+ def info_card(title: str, content: str, icon: Optional[str] = None) -> None:
55
+ """Render an info card with title and content.
56
+
57
+ Args:
58
+ title: Card title
59
+ content: Card content (markdown supported)
60
+ icon: Optional emoji icon
61
+ """
62
+ with st.container(border=True):
63
+ header = f"{icon} {title}" if icon else title
64
+ st.subheader(header)
65
+ st.markdown(content)
66
+
67
+
68
+ def kpi_row(
69
+ metrics: list[dict],
70
+ columns: int = 6,
71
+ ) -> None:
72
+ """Render a row of KPI metric cards.
73
+
74
+ Args:
75
+ metrics: List of metric dicts with keys: label, value, delta, help
76
+ columns: Number of columns (default 6)
77
+ """
78
+ cols = st.columns(columns)
79
+
80
+ for i, m in enumerate(metrics):
81
+ with cols[i % columns]:
82
+ metric_card(
83
+ label=m.get("label", ""),
84
+ value=m.get("value", 0),
85
+ delta=m.get("delta"),
86
+ delta_color=m.get("delta_color", "normal"),
87
+ help_text=m.get("help"),
88
+ )
@@ -0,0 +1,234 @@
1
+ """Plotly chart builders for dashboard."""
2
+
3
+ from typing import Optional
4
+
5
+ import streamlit as st
6
+
7
+ # Try importing plotly - it's an optional dependency
8
+ try:
9
+ import plotly.express as px
10
+ import plotly.graph_objects as go
11
+
12
+ PLOTLY_AVAILABLE = True
13
+ except ImportError:
14
+ PLOTLY_AVAILABLE = False
15
+ px = None
16
+ go = None
17
+
18
+ # Try importing pandas
19
+ try:
20
+ import pandas as pd
21
+
22
+ PANDAS_AVAILABLE = True
23
+ except ImportError:
24
+ PANDAS_AVAILABLE = False
25
+ pd = None
26
+
27
+
28
+ def _check_deps():
29
+ """Check if required dependencies are available."""
30
+ if not PLOTLY_AVAILABLE:
31
+ st.warning("Plotly not installed. Install with: pip install plotly")
32
+ return False
33
+ if not PANDAS_AVAILABLE:
34
+ st.warning("Pandas not installed. Install with: pip install pandas")
35
+ return False
36
+ return True
37
+
38
+
39
+ def line_chart(
40
+ df: "pd.DataFrame",
41
+ x: str,
42
+ y: str,
43
+ title: Optional[str] = None,
44
+ color: Optional[str] = None,
45
+ height: int = 400,
46
+ ) -> None:
47
+ """Render an interactive line chart.
48
+
49
+ Args:
50
+ df: DataFrame with data
51
+ x: Column name for x-axis
52
+ y: Column name for y-axis
53
+ title: Optional chart title
54
+ color: Optional column for color grouping
55
+ height: Chart height in pixels
56
+ """
57
+ if not _check_deps():
58
+ return
59
+
60
+ if df is None or df.empty:
61
+ st.info("No data to display")
62
+ return
63
+
64
+ fig = px.line(
65
+ df,
66
+ x=x,
67
+ y=y,
68
+ title=title,
69
+ color=color,
70
+ )
71
+
72
+ # Configure layout for dark theme
73
+ fig.update_layout(
74
+ template="plotly_dark",
75
+ height=height,
76
+ margin=dict(l=20, r=20, t=40, b=20),
77
+ xaxis=dict(
78
+ rangeselector=dict(
79
+ buttons=list(
80
+ [
81
+ dict(count=1, label="1h", step="hour", stepmode="backward"),
82
+ dict(count=6, label="6h", step="hour", stepmode="backward"),
83
+ dict(count=24, label="24h", step="hour", stepmode="backward"),
84
+ dict(step="all", label="All"),
85
+ ]
86
+ )
87
+ ),
88
+ rangeslider=dict(visible=True),
89
+ type="date",
90
+ ),
91
+ )
92
+
93
+ st.plotly_chart(fig, use_container_width=True)
94
+
95
+
96
+ def bar_chart(
97
+ df: "pd.DataFrame",
98
+ x: str,
99
+ y: str,
100
+ title: Optional[str] = None,
101
+ color: Optional[str] = None,
102
+ orientation: str = "v",
103
+ height: int = 400,
104
+ ) -> None:
105
+ """Render an interactive bar chart.
106
+
107
+ Args:
108
+ df: DataFrame with data
109
+ x: Column name for x-axis (or values if horizontal)
110
+ y: Column name for y-axis (or categories if horizontal)
111
+ title: Optional chart title
112
+ color: Optional column for color grouping
113
+ orientation: "v" for vertical, "h" for horizontal
114
+ height: Chart height in pixels
115
+ """
116
+ if not _check_deps():
117
+ return
118
+
119
+ if df is None or df.empty:
120
+ st.info("No data to display")
121
+ return
122
+
123
+ fig = px.bar(
124
+ df,
125
+ x=x,
126
+ y=y,
127
+ title=title,
128
+ color=color,
129
+ orientation=orientation,
130
+ )
131
+
132
+ fig.update_layout(
133
+ template="plotly_dark",
134
+ height=height,
135
+ margin=dict(l=20, r=20, t=40, b=20),
136
+ )
137
+
138
+ st.plotly_chart(fig, use_container_width=True)
139
+
140
+
141
+ def pie_chart(
142
+ df: "pd.DataFrame",
143
+ values: str,
144
+ names: str,
145
+ title: Optional[str] = None,
146
+ hole: float = 0.4,
147
+ height: int = 400,
148
+ ) -> None:
149
+ """Render an interactive pie/donut chart.
150
+
151
+ Args:
152
+ df: DataFrame with data
153
+ values: Column name for values
154
+ names: Column name for category names
155
+ title: Optional chart title
156
+ hole: Hole size for donut (0 for pie, 0.4 for donut)
157
+ height: Chart height in pixels
158
+ """
159
+ if not _check_deps():
160
+ return
161
+
162
+ if df is None or df.empty:
163
+ st.info("No data to display")
164
+ return
165
+
166
+ # Custom colors for status charts
167
+ color_map = {
168
+ "completed": "#10b981",
169
+ "in_progress": "#3b82f6",
170
+ "pending": "#9ca3af",
171
+ "blocked": "#ef4444",
172
+ }
173
+
174
+ fig = px.pie(
175
+ df,
176
+ values=values,
177
+ names=names,
178
+ title=title,
179
+ hole=hole,
180
+ color=names,
181
+ color_discrete_map=color_map,
182
+ )
183
+
184
+ fig.update_layout(
185
+ template="plotly_dark",
186
+ height=height,
187
+ margin=dict(l=20, r=20, t=40, b=20),
188
+ )
189
+
190
+ st.plotly_chart(fig, use_container_width=True)
191
+
192
+
193
+ def treemap_chart(
194
+ df: "pd.DataFrame",
195
+ path: list[str],
196
+ values: str,
197
+ title: Optional[str] = None,
198
+ height: int = 400,
199
+ ) -> None:
200
+ """Render an interactive treemap chart.
201
+
202
+ Args:
203
+ df: DataFrame with data
204
+ path: List of column names for hierarchy path
205
+ values: Column name for values
206
+ title: Optional chart title
207
+ height: Chart height in pixels
208
+ """
209
+ if not _check_deps():
210
+ return
211
+
212
+ if df is None or df.empty:
213
+ st.info("No data to display")
214
+ return
215
+
216
+ fig = px.treemap(
217
+ df,
218
+ path=path,
219
+ values=values,
220
+ title=title,
221
+ )
222
+
223
+ fig.update_layout(
224
+ template="plotly_dark",
225
+ height=height,
226
+ margin=dict(l=20, r=20, t=40, b=20),
227
+ )
228
+
229
+ st.plotly_chart(fig, use_container_width=True)
230
+
231
+
232
+ def empty_chart(message: str = "No data available") -> None:
233
+ """Display a placeholder for empty charts."""
234
+ st.info(message)
@@ -0,0 +1,136 @@
1
+ """Filter widget components for dashboard."""
2
+
3
+ from datetime import datetime, timedelta
4
+ from typing import Optional
5
+
6
+ import streamlit as st
7
+
8
+
9
+ def time_range_filter(
10
+ key: str = "time_range",
11
+ default: str = "24h",
12
+ ) -> int:
13
+ """Render a time range selector.
14
+
15
+ Args:
16
+ key: Unique key for the widget
17
+ default: Default selection
18
+
19
+ Returns:
20
+ Number of hours for the selected range
21
+ """
22
+ options = {
23
+ "1 hour": 1,
24
+ "6 hours": 6,
25
+ "24 hours": 24,
26
+ "7 days": 168,
27
+ "30 days": 720,
28
+ }
29
+
30
+ # Find default index
31
+ default_hours = {"1h": 1, "6h": 6, "24h": 24, "7d": 168, "30d": 720}.get(default, 24)
32
+ default_label = next((k for k, v in options.items() if v == default_hours), "24 hours")
33
+ default_idx = list(options.keys()).index(default_label)
34
+
35
+ selected = st.selectbox(
36
+ "Time Range",
37
+ options=list(options.keys()),
38
+ index=default_idx,
39
+ key=key,
40
+ )
41
+
42
+ return options[selected]
43
+
44
+
45
+ def date_range_filter(
46
+ key: str = "date_range",
47
+ default_days: int = 7,
48
+ ) -> tuple[datetime, datetime]:
49
+ """Render a date range picker.
50
+
51
+ Args:
52
+ key: Unique key for the widget
53
+ default_days: Default number of days back
54
+
55
+ Returns:
56
+ Tuple of (start_date, end_date)
57
+ """
58
+ end_date = datetime.now()
59
+ start_date = end_date - timedelta(days=default_days)
60
+
61
+ dates = st.date_input(
62
+ "Date Range",
63
+ value=(start_date.date(), end_date.date()),
64
+ key=key,
65
+ )
66
+
67
+ if isinstance(dates, tuple) and len(dates) == 2:
68
+ return (
69
+ datetime.combine(dates[0], datetime.min.time()),
70
+ datetime.combine(dates[1], datetime.max.time()),
71
+ )
72
+ else:
73
+ # Single date selected
74
+ return (
75
+ datetime.combine(dates, datetime.min.time()),
76
+ datetime.combine(dates, datetime.max.time()),
77
+ )
78
+
79
+
80
+ def multi_select_filter(
81
+ label: str,
82
+ options: list[str],
83
+ key: str,
84
+ default: Optional[list[str]] = None,
85
+ ) -> list[str]:
86
+ """Render a multi-select filter.
87
+
88
+ Args:
89
+ label: Filter label
90
+ options: List of options to choose from
91
+ key: Unique key for the widget
92
+ default: Default selected values
93
+
94
+ Returns:
95
+ List of selected values
96
+ """
97
+ return st.multiselect(
98
+ label,
99
+ options=options,
100
+ default=default,
101
+ key=key,
102
+ )
103
+
104
+
105
+ def text_filter(
106
+ label: str,
107
+ key: str,
108
+ placeholder: Optional[str] = None,
109
+ ) -> str:
110
+ """Render a text input filter.
111
+
112
+ Args:
113
+ label: Filter label
114
+ key: Unique key for the widget
115
+ placeholder: Optional placeholder text
116
+
117
+ Returns:
118
+ Input text value
119
+ """
120
+ return st.text_input(
121
+ label,
122
+ key=key,
123
+ placeholder=placeholder or f"Filter by {label.lower()}...",
124
+ )
125
+
126
+
127
+ def filter_row(num_cols: int = 4):
128
+ """Create a row of filter columns.
129
+
130
+ Args:
131
+ num_cols: Number of columns
132
+
133
+ Returns:
134
+ Tuple of column objects
135
+ """
136
+ return st.columns(num_cols)