foundry-mcp 0.8.22__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 foundry-mcp might be problematic. Click here for more details.

Files changed (153) hide show
  1. foundry_mcp/__init__.py +13 -0
  2. foundry_mcp/cli/__init__.py +67 -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 +640 -0
  13. foundry_mcp/cli/commands/pr.py +393 -0
  14. foundry_mcp/cli/commands/review.py +667 -0
  15. foundry_mcp/cli/commands/session.py +472 -0
  16. foundry_mcp/cli/commands/specs.py +686 -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 +298 -0
  22. foundry_mcp/cli/logging.py +212 -0
  23. foundry_mcp/cli/main.py +44 -0
  24. foundry_mcp/cli/output.py +122 -0
  25. foundry_mcp/cli/registry.py +110 -0
  26. foundry_mcp/cli/resilience.py +178 -0
  27. foundry_mcp/cli/transcript.py +217 -0
  28. foundry_mcp/config.py +1454 -0
  29. foundry_mcp/core/__init__.py +144 -0
  30. foundry_mcp/core/ai_consultation.py +1773 -0
  31. foundry_mcp/core/batch_operations.py +1202 -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/health.py +749 -0
  40. foundry_mcp/core/intake.py +933 -0
  41. foundry_mcp/core/journal.py +700 -0
  42. foundry_mcp/core/lifecycle.py +412 -0
  43. foundry_mcp/core/llm_config.py +1376 -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 +146 -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 +387 -0
  57. foundry_mcp/core/prometheus.py +564 -0
  58. foundry_mcp/core/prompts/__init__.py +464 -0
  59. foundry_mcp/core/prompts/fidelity_review.py +691 -0
  60. foundry_mcp/core/prompts/markdown_plan_review.py +515 -0
  61. foundry_mcp/core/prompts/plan_review.py +627 -0
  62. foundry_mcp/core/providers/__init__.py +237 -0
  63. foundry_mcp/core/providers/base.py +515 -0
  64. foundry_mcp/core/providers/claude.py +472 -0
  65. foundry_mcp/core/providers/codex.py +637 -0
  66. foundry_mcp/core/providers/cursor_agent.py +630 -0
  67. foundry_mcp/core/providers/detectors.py +515 -0
  68. foundry_mcp/core/providers/gemini.py +426 -0
  69. foundry_mcp/core/providers/opencode.py +718 -0
  70. foundry_mcp/core/providers/opencode_wrapper.js +308 -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 +857 -0
  76. foundry_mcp/core/rate_limit.py +427 -0
  77. foundry_mcp/core/research/__init__.py +68 -0
  78. foundry_mcp/core/research/memory.py +528 -0
  79. foundry_mcp/core/research/models.py +1234 -0
  80. foundry_mcp/core/research/providers/__init__.py +40 -0
  81. foundry_mcp/core/research/providers/base.py +242 -0
  82. foundry_mcp/core/research/providers/google.py +507 -0
  83. foundry_mcp/core/research/providers/perplexity.py +442 -0
  84. foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
  85. foundry_mcp/core/research/providers/tavily.py +383 -0
  86. foundry_mcp/core/research/workflows/__init__.py +25 -0
  87. foundry_mcp/core/research/workflows/base.py +298 -0
  88. foundry_mcp/core/research/workflows/chat.py +271 -0
  89. foundry_mcp/core/research/workflows/consensus.py +539 -0
  90. foundry_mcp/core/research/workflows/deep_research.py +4142 -0
  91. foundry_mcp/core/research/workflows/ideate.py +682 -0
  92. foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
  93. foundry_mcp/core/resilience.py +600 -0
  94. foundry_mcp/core/responses.py +1624 -0
  95. foundry_mcp/core/review.py +366 -0
  96. foundry_mcp/core/security.py +438 -0
  97. foundry_mcp/core/spec.py +4119 -0
  98. foundry_mcp/core/task.py +2463 -0
  99. foundry_mcp/core/testing.py +839 -0
  100. foundry_mcp/core/validation.py +2357 -0
  101. foundry_mcp/dashboard/__init__.py +32 -0
  102. foundry_mcp/dashboard/app.py +119 -0
  103. foundry_mcp/dashboard/components/__init__.py +17 -0
  104. foundry_mcp/dashboard/components/cards.py +88 -0
  105. foundry_mcp/dashboard/components/charts.py +177 -0
  106. foundry_mcp/dashboard/components/filters.py +136 -0
  107. foundry_mcp/dashboard/components/tables.py +195 -0
  108. foundry_mcp/dashboard/data/__init__.py +11 -0
  109. foundry_mcp/dashboard/data/stores.py +433 -0
  110. foundry_mcp/dashboard/launcher.py +300 -0
  111. foundry_mcp/dashboard/views/__init__.py +12 -0
  112. foundry_mcp/dashboard/views/errors.py +217 -0
  113. foundry_mcp/dashboard/views/metrics.py +164 -0
  114. foundry_mcp/dashboard/views/overview.py +96 -0
  115. foundry_mcp/dashboard/views/providers.py +83 -0
  116. foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
  117. foundry_mcp/dashboard/views/tool_usage.py +139 -0
  118. foundry_mcp/prompts/__init__.py +9 -0
  119. foundry_mcp/prompts/workflows.py +525 -0
  120. foundry_mcp/resources/__init__.py +9 -0
  121. foundry_mcp/resources/specs.py +591 -0
  122. foundry_mcp/schemas/__init__.py +38 -0
  123. foundry_mcp/schemas/intake-schema.json +89 -0
  124. foundry_mcp/schemas/sdd-spec-schema.json +414 -0
  125. foundry_mcp/server.py +150 -0
  126. foundry_mcp/tools/__init__.py +10 -0
  127. foundry_mcp/tools/unified/__init__.py +92 -0
  128. foundry_mcp/tools/unified/authoring.py +3620 -0
  129. foundry_mcp/tools/unified/context_helpers.py +98 -0
  130. foundry_mcp/tools/unified/documentation_helpers.py +268 -0
  131. foundry_mcp/tools/unified/environment.py +1341 -0
  132. foundry_mcp/tools/unified/error.py +479 -0
  133. foundry_mcp/tools/unified/health.py +225 -0
  134. foundry_mcp/tools/unified/journal.py +841 -0
  135. foundry_mcp/tools/unified/lifecycle.py +640 -0
  136. foundry_mcp/tools/unified/metrics.py +777 -0
  137. foundry_mcp/tools/unified/plan.py +876 -0
  138. foundry_mcp/tools/unified/pr.py +294 -0
  139. foundry_mcp/tools/unified/provider.py +589 -0
  140. foundry_mcp/tools/unified/research.py +1283 -0
  141. foundry_mcp/tools/unified/review.py +1042 -0
  142. foundry_mcp/tools/unified/review_helpers.py +314 -0
  143. foundry_mcp/tools/unified/router.py +102 -0
  144. foundry_mcp/tools/unified/server.py +565 -0
  145. foundry_mcp/tools/unified/spec.py +1283 -0
  146. foundry_mcp/tools/unified/task.py +3846 -0
  147. foundry_mcp/tools/unified/test.py +431 -0
  148. foundry_mcp/tools/unified/verification.py +520 -0
  149. foundry_mcp-0.8.22.dist-info/METADATA +344 -0
  150. foundry_mcp-0.8.22.dist-info/RECORD +153 -0
  151. foundry_mcp-0.8.22.dist-info/WHEEL +4 -0
  152. foundry_mcp-0.8.22.dist-info/entry_points.txt +3 -0
  153. foundry_mcp-0.8.22.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,177 @@
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 bar_chart(
40
+ df: "pd.DataFrame",
41
+ x: str,
42
+ y: str,
43
+ title: Optional[str] = None,
44
+ color: Optional[str] = None,
45
+ orientation: str = "v",
46
+ height: int = 400,
47
+ ) -> None:
48
+ """Render an interactive bar chart.
49
+
50
+ Args:
51
+ df: DataFrame with data
52
+ x: Column name for x-axis (or values if horizontal)
53
+ y: Column name for y-axis (or categories if horizontal)
54
+ title: Optional chart title
55
+ color: Optional column for color grouping
56
+ orientation: "v" for vertical, "h" for horizontal
57
+ height: Chart height in pixels
58
+ """
59
+ if not _check_deps():
60
+ return
61
+
62
+ if df is None or df.empty:
63
+ st.info("No data to display")
64
+ return
65
+
66
+ fig = px.bar(
67
+ df,
68
+ x=x,
69
+ y=y,
70
+ title=title,
71
+ color=color,
72
+ orientation=orientation,
73
+ )
74
+
75
+ fig.update_layout(
76
+ template="plotly_dark",
77
+ height=height,
78
+ margin=dict(l=20, r=20, t=40, b=20),
79
+ )
80
+
81
+ st.plotly_chart(fig, use_container_width=True)
82
+
83
+
84
+ def pie_chart(
85
+ df: "pd.DataFrame",
86
+ values: str,
87
+ names: str,
88
+ title: Optional[str] = None,
89
+ hole: float = 0.4,
90
+ height: int = 400,
91
+ ) -> None:
92
+ """Render an interactive pie/donut chart.
93
+
94
+ Args:
95
+ df: DataFrame with data
96
+ values: Column name for values
97
+ names: Column name for category names
98
+ title: Optional chart title
99
+ hole: Hole size for donut (0 for pie, 0.4 for donut)
100
+ height: Chart height in pixels
101
+ """
102
+ if not _check_deps():
103
+ return
104
+
105
+ if df is None or df.empty:
106
+ st.info("No data to display")
107
+ return
108
+
109
+ # Custom colors for status charts
110
+ color_map = {
111
+ "completed": "#10b981",
112
+ "in_progress": "#3b82f6",
113
+ "pending": "#9ca3af",
114
+ "blocked": "#ef4444",
115
+ }
116
+
117
+ fig = px.pie(
118
+ df,
119
+ values=values,
120
+ names=names,
121
+ title=title,
122
+ hole=hole,
123
+ color=names,
124
+ color_discrete_map=color_map,
125
+ )
126
+
127
+ fig.update_layout(
128
+ template="plotly_dark",
129
+ height=height,
130
+ margin=dict(l=20, r=20, t=40, b=20),
131
+ )
132
+
133
+ st.plotly_chart(fig, use_container_width=True)
134
+
135
+
136
+ def treemap_chart(
137
+ df: "pd.DataFrame",
138
+ path: list[str],
139
+ values: str,
140
+ title: Optional[str] = None,
141
+ height: int = 400,
142
+ ) -> None:
143
+ """Render an interactive treemap chart.
144
+
145
+ Args:
146
+ df: DataFrame with data
147
+ path: List of column names for hierarchy path
148
+ values: Column name for values
149
+ title: Optional chart title
150
+ height: Chart height in pixels
151
+ """
152
+ if not _check_deps():
153
+ return
154
+
155
+ if df is None or df.empty:
156
+ st.info("No data to display")
157
+ return
158
+
159
+ fig = px.treemap(
160
+ df,
161
+ path=path,
162
+ values=values,
163
+ title=title,
164
+ )
165
+
166
+ fig.update_layout(
167
+ template="plotly_dark",
168
+ height=height,
169
+ margin=dict(l=20, r=20, t=40, b=20),
170
+ )
171
+
172
+ st.plotly_chart(fig, use_container_width=True)
173
+
174
+
175
+ def empty_chart(message: str = "No data available") -> None:
176
+ """Display a placeholder for empty charts."""
177
+ 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)