diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000..b8ab91c --- /dev/null +++ b/skills/README.md @@ -0,0 +1,60 @@ +# Streamlit Agent Skills + +This directory contains agent skills for building Streamlit applications. These skills are designed to help AI coding assistants provide better guidance when developers are building Streamlit apps. + +## Available Skills + +| Skill | Description | Lines | +|-------|-------------|-------| +| [streamlit-best-practices](./streamlit-best-practices/) | Opinionated coding conventions and anti-patterns | 252 | +| [building-streamlit-apps](./building-streamlit-apps/) | Core execution model, session state, widget patterns, caching basics | 189 | +| [optimizing-streamlit-performance](./optimizing-streamlit-performance/) | Caching strategies, fragments, forms, large data handling | 202 | +| [building-streamlit-chat-apps](./building-streamlit-chat-apps/) | Chat interfaces, LLM integration, streaming responses | 217 | +| [displaying-streamlit-data](./displaying-streamlit-data/) | DataFrames, charts, metrics, column configuration | 228 | +| [designing-streamlit-layouts](./designing-streamlit-layouts/) | Columns, sidebar, tabs, dialogs, status elements | 243 | +| [building-streamlit-multipage-apps](./building-streamlit-multipage-apps/) | Navigation, cross-page state, URL parameters | 181 | +| [connecting-streamlit-data](./connecting-streamlit-data/) | Database connections, st.connection, secrets | 152 | +| [securing-streamlit-apps](./securing-streamlit-apps/) | Secrets management, OIDC authentication, security | 220 | +| [testing-streamlit-apps](./testing-streamlit-apps/) | AppTest framework, pytest integration | 253 | + +## Skill Format + +Each skill follows the [Agent Skills specification](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview): + +- **YAML frontmatter** with `name` and `description` +- **Concise instructions** (all files under 500 lines) +- **Code examples** showing good vs bad patterns +- **Reference links** to official documentation + +## Usage + +These skills can be used with: +- [Claude Code](https://code.claude.com/) +- [Claude Agent SDK](https://docs.anthropic.com/en/agent-sdk/) +- [Anthropic API](https://docs.anthropic.com/) +- [Cursor](https://cursor.com/) +- Other AI coding assistants that support agent skills + +## Skill Triggering + +Skills are triggered based on their descriptions. Key trigger phrases for each: + +- **streamlit-best-practices**: "best practices", "conventions", "code quality", "anti-patterns" +- **building-streamlit-apps**: "Streamlit app", "session state", "widget", "rerun" +- **optimizing-streamlit-performance**: "slow", "performance", "cache", "large data" +- **building-streamlit-chat-apps**: "chat", "chatbot", "LLM", "OpenAI", "streaming" +- **displaying-streamlit-data**: "dataframe", "chart", "metric", "visualization" +- **designing-streamlit-layouts**: "layout", "columns", "sidebar", "tabs", "dialog" +- **building-streamlit-multipage-apps**: "multipage", "navigation", "pages" +- **connecting-streamlit-data**: "database", "connection", "SQL", "API" +- **securing-streamlit-apps**: "secrets", "authentication", "security", "login" +- **testing-streamlit-apps**: "test", "pytest", "AppTest", "CI/CD" + +## Contributing + +When updating skills: +1. Keep files under 500 lines +2. Use concise language (Claude already knows basics) +3. Provide good/bad code examples +4. Include links to official docs +5. Test with multiple Claude models (Haiku, Sonnet, Opus) diff --git a/skills/building-streamlit-apps/SKILL.md b/skills/building-streamlit-apps/SKILL.md new file mode 100644 index 0000000..aa3e352 --- /dev/null +++ b/skills/building-streamlit-apps/SKILL.md @@ -0,0 +1,162 @@ +--- +name: building-streamlit-apps +description: Builds Streamlit web applications with proper state management, caching, and widget patterns. Use when creating Streamlit apps, handling session state, managing widget behavior, or encountering unexpected reruns or state issues. +--- + +# Building Streamlit Apps + +Streamlit reruns the entire script top-to-bottom on every user interaction. This execution model requires specific patterns for state management and widget behavior. + +## Session State + +Variables reset on every rerun unless stored in `st.session_state`. + +```python +# BAD: Lost on rerun +count = 0 +count += 1 + +# GOOD: Persists across reruns +if "count" not in st.session_state: + st.session_state.count = 0 +st.session_state.count += 1 +``` + +**Key rules:** +- Always check existence before accessing: `if "key" not in st.session_state` +- Use `st.session_state.get("key", default)` for safe access with defaults +- Session state is per-user, per-tab, and temporary (lost on tab close) + +## Widget Keys and State + +Widgets can store values in session state via the `key` parameter. + +```python +# Widget value accessible via session state +name = st.text_input("Name", key="user_name") +# st.session_state.user_name contains the same value +``` + +Use the widget's `key` in callbacks, not the return variable: + +```python +# BAD: Updates every second click +def increment(): + slide_val += 1 # Wrong - using variable + +# GOOD: Use the key +def increment(): + st.session_state.slider += 1 # Correct - using key + +st.button("Add", on_click=increment) +slide_val = st.slider("Value", 0, 10, key="slider") +``` + +## Button Behavior + +Buttons return `True` only during the rerun triggered by the click. They do NOT retain state. + +```python +# BAD: Content disappears after any other interaction +if st.button("Load data"): + data = load_data() # Lost on next rerun! + st.dataframe(data) + +# GOOD: Store result in session state +if st.button("Load data"): + st.session_state.data = load_data() + +if "data" in st.session_state: + st.dataframe(st.session_state.data) +``` + +**Anti-patterns to avoid:** +- Nested buttons never work (outer button is `False` when inner clicked) +- Widgets inside `if st.button()` blocks disappear after any interaction +- Don't set `st.session_state.my_button` - button state is not settable + +**Toggle pattern:** + +```python +if "show_details" not in st.session_state: + st.session_state.show_details = False + +def toggle(): + st.session_state.show_details = not st.session_state.show_details + +st.button("Toggle details", on_click=toggle) + +if st.session_state.show_details: + st.write("Details here...") +``` + +## Caching Basics + +Cache expensive operations to avoid recomputation on every rerun. + +```python +@st.cache_data # For data: DataFrames, API responses, computations +def load_data(): + return pd.read_csv("large_file.csv") + +@st.cache_resource # For resources: DB connections, ML models +def get_model(): + return load_ml_model() +``` + +**When to use which:** +- `@st.cache_data`: Returns a copy each call. Safe for data that might be modified. +- `@st.cache_resource`: Returns the same object. Use for connections/models. + +**Important:** Set `ttl` or `max_entries` for data that changes to prevent unbounded memory growth. + +```python +@st.cache_data(ttl=3600) # Refresh after 1 hour +def get_live_data(): + return fetch_from_api() +``` + +## Callbacks + +Use callbacks for immediate state changes before the rerun: + +```python +def on_change(): + st.session_state.processed = process(st.session_state.input_text) + +st.text_input("Enter text", key="input_text", on_change=on_change) + +# processed value available immediately in this rerun +if "processed" in st.session_state: + st.write(st.session_state.processed) +``` + +## Custom Classes + +Classes defined in the main script are redefined on each rerun, breaking identity checks. + +```python +# BAD: Class redefined each rerun +class User: + def __init__(self, name): + self.name = name + +# isinstance() and == checks may fail unexpectedly +``` + +**Solutions:** +1. Define classes in separate modules (imported modules aren't redefined) +2. Implement custom `__eq__` and `__hash__` methods +3. Use dataclasses with `frozen=True` +4. Store serializable data (dicts) instead of class instances + +## Common Gotchas + +1. **Duplicate widget keys**: Each widget needs a unique key if used multiple times +2. **Modifying cached data**: Don't mutate `@st.cache_resource` returns (affects all users) + +## Reference + +- [Session State docs](https://docs.streamlit.io/develop/concepts/architecture/session-state) +- [Caching docs](https://docs.streamlit.io/develop/concepts/architecture/caching) +- [Widget behavior](https://docs.streamlit.io/develop/concepts/architecture/widget-behavior) diff --git a/skills/building-streamlit-chat-apps/SKILL.md b/skills/building-streamlit-chat-apps/SKILL.md new file mode 100644 index 0000000..0f302cd --- /dev/null +++ b/skills/building-streamlit-chat-apps/SKILL.md @@ -0,0 +1,182 @@ +--- +name: building-streamlit-chat-apps +description: Builds chat interfaces and LLM-powered apps with Streamlit. Use when creating chatbots, conversational AI interfaces, integrating OpenAI/Anthropic/other LLMs, or implementing streaming responses. +--- + +# Building Streamlit Chat Apps + +Streamlit provides chat elements for building conversational interfaces. The key is properly managing message history in session state. + +## Basic Chat Pattern + +```python +import streamlit as st + +st.title("Chat App") + +# Initialize message history +if "messages" not in st.session_state: + st.session_state.messages = [] + +# Display message history +for msg in st.session_state.messages: + with st.chat_message(msg["role"]): + st.markdown(msg["content"]) + +# Handle new input +if prompt := st.chat_input("Message"): + # Add user message + st.session_state.messages.append({"role": "user", "content": prompt}) + with st.chat_message("user"): + st.markdown(prompt) + + # Generate and display response + response = generate_response(prompt) + st.session_state.messages.append({"role": "assistant", "content": response}) + with st.chat_message("assistant"): + st.markdown(response) +``` + +Store messages in session state. Without this, history is lost on every rerun. + +## Streaming Responses + +Use `st.write_stream()` for typewriter-effect output: + +```python +from openai import OpenAI + +client = OpenAI(api_key=st.secrets["OPENAI_API_KEY"]) + +if prompt := st.chat_input("Message"): + st.session_state.messages.append({"role": "user", "content": prompt}) + with st.chat_message("user"): + st.markdown(prompt) + + with st.chat_message("assistant"): + stream = client.chat.completions.create( + model="gpt-4", + messages=st.session_state.messages, + stream=True, + ) + response = st.write_stream(stream) # Returns full text when done + + st.session_state.messages.append({"role": "assistant", "content": response}) +``` + +`st.write_stream()` works with: +- OpenAI streaming responses (directly) +- Anthropic streaming responses +- Any generator yielding strings + +### Custom Generator + +```python +import time + +def response_generator(text): + for word in text.split(): + yield word + " " + time.sleep(0.05) + +with st.chat_message("assistant"): + response = st.write_stream(response_generator("Hello, how can I help?")) +``` + +## With Anthropic Claude + +```python +import anthropic + +client = anthropic.Anthropic(api_key=st.secrets["ANTHROPIC_API_KEY"]) + +if prompt := st.chat_input("Message"): + st.session_state.messages.append({"role": "user", "content": prompt}) + with st.chat_message("user"): + st.markdown(prompt) + + with st.chat_message("assistant"): + with client.messages.stream( + model="claude-sonnet-4-20250514", + max_tokens=1024, + messages=st.session_state.messages, + ) as stream: + response = st.write_stream(stream.text_stream) + + st.session_state.messages.append({"role": "assistant", "content": response}) +``` + +## Chat Message Options + +```python +# Custom avatars +with st.chat_message("user", avatar=":material/person:"): + st.write("User message") + +with st.chat_message("assistant", avatar=":material/smart_toy:"): + st.write("Assistant message") + +# Rich content in messages +with st.chat_message("assistant"): + st.markdown("Here's a chart:") + st.line_chart(data) + st.code("print('hello')") +``` + +## User Feedback + +```python +with st.chat_message("assistant"): + st.markdown(response) + feedback = st.feedback("thumbs") + if feedback is not None: + st.session_state.feedback = { + "message": response, + "rating": feedback # 0 = thumbs down, 1 = thumbs up + } +``` + +## Clear Chat + +```python +if st.sidebar.button("Clear chat"): + st.session_state.messages = [] + st.rerun() +``` + +## Complete Example + +```python +import streamlit as st +from openai import OpenAI + +st.title("πŸ’¬ Chat") + +client = OpenAI(api_key=st.secrets["OPENAI_API_KEY"]) + +if "messages" not in st.session_state: + st.session_state.messages = [] + +for msg in st.session_state.messages: + with st.chat_message(msg["role"]): + st.markdown(msg["content"]) + +if prompt := st.chat_input("Message"): + st.session_state.messages.append({"role": "user", "content": prompt}) + with st.chat_message("user"): + st.markdown(prompt) + + with st.chat_message("assistant"): + stream = client.chat.completions.create( + model="gpt-4", + messages=st.session_state.messages, + stream=True, + ) + response = st.write_stream(stream) + st.session_state.messages.append({"role": "assistant", "content": response}) +``` + +## Reference + +- [Chat elements docs](https://docs.streamlit.io/develop/api-reference/chat) +- [Build conversational apps tutorial](https://docs.streamlit.io/develop/tutorials/chat-and-llm-apps/build-conversational-apps) diff --git a/skills/building-streamlit-multipage-apps/SKILL.md b/skills/building-streamlit-multipage-apps/SKILL.md new file mode 100644 index 0000000..e09fa49 --- /dev/null +++ b/skills/building-streamlit-multipage-apps/SKILL.md @@ -0,0 +1,189 @@ +--- +name: building-streamlit-multipage-apps +description: Builds multipage Streamlit applications with navigation, cross-page state, and URL routing. Use when creating apps with multiple pages, implementing navigation, sharing state between pages, or using st.Page and st.navigation. +--- + +# Building Streamlit Multipage Apps + +Streamlit supports multipage apps via directory structure or explicit page definitions. + +## Recommended: st.navigation + +Explicit control over pages and navigation: + +```python +# app.py (entrypoint) +import streamlit as st + +pages = [ + st.Page("app_pages/dashboard.py", title="Dashboard", icon=":material/dashboard:"), + st.Page("app_pages/analytics.py", title="Analytics", icon=":material/analytics:"), + st.Page("app_pages/settings.py", title="Settings", icon=":material/settings:"), +] + +nav = st.navigation(pages) +nav.run() +``` + +**Benefits:** +- Full control over page order and visibility +- Custom icons (Material Icons supported) +- Conditional page visibility based on auth/permissions +- Pages can be in any directory + +**Notes:** `st.Page(url_path=...)` cannot include `/` (no subdirectories). + +### Page Groups + +```python +pages = { + "": [ + st.Page("app_pages/home.py", title="Home"), + st.Page("app_pages/about.py", title="About"), + ], + "Data": [ + st.Page("app_pages/explorer.py", title="Explorer") + ], + "Admin": [ + st.Page("app_pages/users.py", title="Users"), + st.Page("app_pages/settings.py", title="Settings"), + ], +} + +nav = st.navigation(pages) +nav.run() +``` + +### Conditional Pages + +```python +pages = [st.Page("app_pages/home.py", title="Home")] + +if st.user.is_logged_in: + pages.append(st.Page("app_pages/dashboard.py", title="Dashboard")) + +if st.session_state.get("is_admin"): + pages.append(st.Page("app_pages/admin.py", title="Admin")) + +nav = st.navigation(pages) +nav.run() +``` + +## Alternative: pages/ Directory + +Simpler but less flexible: + +``` +my_app/ +β”œβ”€β”€ app.py # Main entrypoint +└── pages/ + β”œβ”€β”€ 1_Dashboard.py # Numbered for ordering + β”œβ”€β”€ 2_Analytics.py + └── 3_Settings.py +``` + +Pages appear in sidebar automatically. Prefix with numbers to control order. + +**Notes:** Once any session runs `st.navigation`, the `pages/` directory is ignored for all sessions until the app restarts. + +## Sharing State Across Pages + +Widgets are NOT stateful across pages. Use session state: + +```python +# Page 1: Set state +st.session_state.selected_user = st.selectbox("User", users, key="user") + +# Page 2: Read state +if "selected_user" in st.session_state: + st.write(f"Selected: {st.session_state.selected_user}") +else: + st.warning("No user selected. Go to page 1 first.") +``` + +### Shared Widgets Pattern + +Put common widgets in the entrypoint (before `nav.run()`): + +```python +# app.py +import streamlit as st + +# Sidebar widgets available on all pages +with st.sidebar: + st.session_state.theme = st.selectbox("Theme", ["Light", "Dark"]) + +pages = [...] +nav = st.navigation(pages) +nav.run() +``` + +## Programmatic Navigation + +```python +# Navigate to another page +if st.button("Go to Settings"): + st.switch_page("app_pages/settings.py") + +# In-page navigation links +st.page_link("app_pages/home.py", label="Home", icon=":material/home:") +st.page_link("https://example.com", label="External", icon=":material/open_in_new:") +``` + +## URL Query Parameters + +Use `st.query_params` for shareable URLs: + +```python +# Read params +user_id = st.query_params.get("user_id") + +# Set params +st.query_params["user_id"] = "123" +st.query_params.from_dict({"user_id": "123", "tab": "overview"}) + +# Clear params +st.query_params.clear() +``` + +**Notes:** +- Query params are strings. Use `get_all()` for repeated keys. +- Query params are cleared when navigating between pages. + +## Project Structure + +``` +my_app/ +β”œβ”€β”€ streamlit_app.py # Entrypoint: st.navigation setup +β”œβ”€β”€ app_pages/ # Page files +β”‚ β”œβ”€β”€ dashboard.py +β”‚ β”œβ”€β”€ analytics.py +β”‚ └── settings.py +β”œβ”€β”€ utils/ # Shared utilities +β”‚ β”œβ”€β”€ __init__.py +β”‚ β”œβ”€β”€ data.py +β”‚ └── auth.py +└── .streamlit/ + β”œβ”€β”€ config.toml + └── secrets.toml +``` + +## Page Configuration + +Its recommended to set page config at the top of the page: + +```python +st.set_page_config( + page_title="Dashboard", + page_icon="πŸ“Š", + layout="wide", # or "centered" + initial_sidebar_state="expanded", # or "collapsed", "auto" +) +``` + +## Reference + +- [Multipage apps docs](https://docs.streamlit.io/develop/concepts/multipage-apps) +- [st.navigation API](https://docs.streamlit.io/develop/api-reference/navigation/st.navigation) +- [st.Page API](https://docs.streamlit.io/develop/api-reference/navigation/st.page) +- [st.query_params API](https://docs.streamlit.io/develop/api-reference/caching-and-state/st.query_params) diff --git a/skills/connecting-streamlit-data/SKILL.md b/skills/connecting-streamlit-data/SKILL.md new file mode 100644 index 0000000..b675568 --- /dev/null +++ b/skills/connecting-streamlit-data/SKILL.md @@ -0,0 +1,154 @@ +--- +name: connecting-streamlit-data +description: Connects Streamlit apps to databases and external data sources using st.connection. Use when connecting to SQL databases, Snowflake, APIs, or building custom data connections. +--- + +# Connecting Streamlit to Data + +Use `st.connection` for managed, cached connections to databases and APIs. + +**Note:** `st.connection` is cached with `st.cache_resource`. The connection object is shared across sessions, so avoid per-user state or mutation. + +## SQL Databases + +```python +# Configure in .streamlit/secrets.toml +# [connections.mydb] +# dialect = "postgresql" +# host = "localhost" +# port = 5432 +# database = "mydb" +# username = "user" +# password = "password" + +conn = st.connection("mydb", type="sql") + +# Query with caching (set ttl for live data) +df = conn.query("SELECT * FROM users", ttl=600) # Cache 10 minutes +st.dataframe(df) +``` + +### Supported Databases + +- PostgreSQL: `dialect = "postgresql"` +- MySQL: `dialect = "mysql"` +- SQLite: `dialect = "sqlite"` (use `database = "path/to/db.sqlite"`) +- SQL Server: `dialect = "mssql+pyodbc"` + +### SQLAlchemy URL + +Alternatively, use a connection URL: + +```toml +# .streamlit/secrets.toml +[connections.mydb] +url = "postgresql://user:password@localhost:5432/mydb" +``` + +## Snowflake + +```python +# .streamlit/secrets.toml +# [connections.snowflake] +# account = "xxx" +# user = "xxx" +# password = "xxx" +# warehouse = "xxx" +# database = "xxx" +# schema = "xxx" + +conn = st.connection("snowflake", type="snowflake") +df = conn.query("SELECT * FROM my_table", ttl=600) +``` + +## Query Parameters + +```python +# Use parameters to prevent SQL injection +user_id = st.text_input("User ID") +df = conn.query( + "SELECT * FROM users WHERE id = :id", + params={"id": user_id}, + ttl=600, +) +``` + +## Session for Write Operations + +```python +with conn.session as session: + session.execute( + text("INSERT INTO users (name) VALUES (:name)"), + {"name": new_name}, + ) + session.commit() +``` + +## Custom Connections + +Build connections for APIs or unsupported databases: + +```python +from streamlit.connections import BaseConnection +import requests + +class APIConnection(BaseConnection[requests.Session]): + def _connect(self, **kwargs) -> requests.Session: + session = requests.Session() + session.headers["Authorization"] = f"Bearer {self._secrets['api_key']}" + return session + + def query(self, endpoint: str, ttl: int = 3600): + @st.cache_data(ttl=ttl) + def _query(endpoint): + return self._instance.get(f"{self._secrets['base_url']}{endpoint}").json() + return _query(endpoint) + +# Usage +conn = st.connection("my_api", type=APIConnection) +data = conn.query("/users", ttl=300) +``` + +## Secrets Configuration + +### Per-Connection Secrets + +```toml +# .streamlit/secrets.toml +[connections.mydb] +host = "localhost" +password = "secret" + +[connections.my_api] +api_key = "xxx" +base_url = "https://api.example.com" +``` + +### Global vs Project Secrets + +- Global: `~/.streamlit/secrets.toml` (shared across apps) +- Project: `.streamlit/secrets.toml` (project-specific, overrides global) + +**Always add to `.gitignore`:** +``` +.streamlit/secrets.toml +``` + +## TTL Best Practices + +```python +# BAD: No TTL = stale data forever +df = conn.query("SELECT * FROM live_metrics") + +# GOOD: Set appropriate TTL +df = conn.query("SELECT * FROM live_metrics", ttl=60) # Refresh every minute + +# For static reference data, longer TTL is fine +df = conn.query("SELECT * FROM countries", ttl=86400) # Daily refresh +``` + +## Reference + +- [Connections docs](https://docs.streamlit.io/develop/concepts/connections) +- [st.connection API](https://docs.streamlit.io/develop/api-reference/connections/st.connection) +- [Secrets management](https://docs.streamlit.io/develop/concepts/connections/secrets-management) diff --git a/skills/designing-streamlit-layouts/SKILL.md b/skills/designing-streamlit-layouts/SKILL.md new file mode 100644 index 0000000..5a82d97 --- /dev/null +++ b/skills/designing-streamlit-layouts/SKILL.md @@ -0,0 +1,302 @@ +--- +name: designing-streamlit-layouts +description: Designs Streamlit app layouts using columns, sidebar, tabs, containers, and dialogs. Use when organizing UI elements, creating dashboards, building navigation, or structuring app content. +--- + +# Designing Streamlit Layouts + +Organize your app's UI with layout containers for professional, scannable interfaces. + +## Core Layout Components + +### Columns + +```python +# Equal columns +col1, col2, col3 = st.columns(3) + +# Custom widths (ratios) +col1, col2 = st.columns([2, 1]) # 2:1 ratio + +# With gap control +col1, col2 = st.columns(2, gap="large") # small, medium, large + +# Usage +with col1: + st.metric("Revenue", "$1M") +with col2: + st.metric("Users", "5K") + +# Or without context manager +col1.metric("Revenue", "$1M") +col2.metric("Users", "5K") +``` + +**Limitation:** Nested columns are restricted. Design layouts without deep nesting. + +### Sidebar + +```python +# Method 1: Context manager +with st.sidebar: + st.title("Settings") + option = st.selectbox("Choose", ["A", "B"]) + +# Method 2: Direct access +st.sidebar.title("Settings") +option = st.sidebar.selectbox("Choose", ["A", "B"]) +``` + +**Best practice:** Place filters, settings, and navigation in sidebar to keep main content clean. + +### Tabs + +```python +tab1, tab2, tab3 = st.tabs(["Chart", "Data", "Settings"]) + +with tab1: + st.line_chart(data) +with tab2: + st.dataframe(df) +with tab3: + st.slider("Threshold", 0, 100) +``` + +### Expander + +```python +with st.expander("See details"): + st.write("Hidden content here") + st.code("print('hello')") + +# Expanded by default +with st.expander("Configuration", expanded=True): + st.text_input("API Key") +``` + +### Container + +Control element ordering: + +```python +# Placeholder for later content +header = st.container() +main = st.container() + +# Write to main first +with main: + result = expensive_computation() + +# Then update header +with header: + st.write(f"Computed: {result}") +``` + +### Flex Containers + +Use `horizontal=True` for flexible row layouts: + +```python +# Basic horizontal layout +with st.container(horizontal=True): + st.button("Action 1") + st.button("Action 2") + st.button("Action 3") + +# Right-aligned buttons +with st.container(horizontal=True, horizontal_alignment="right"): + st.button("Cancel") + st.button("Save", type="primary") + +# Centered with custom gap +with st.container(horizontal=True, horizontal_alignment="center", gap="medium"): + st.metric("Revenue", "$1M") + st.metric("Users", "5K") + st.metric("Orders", "1.2K") + +# Distributed (evenly spaced) +with st.container(horizontal=True, horizontal_alignment="distribute"): + for i in range(4): + st.button(f"Option {i+1}") +``` + +**Alignment options:** +- `horizontal_alignment`: `"left"`, `"center"`, `"right"`, `"distribute"` +- `vertical_alignment`: `"top"`, `"center"`, `"bottom"`, `"distribute"` + +**Gap sizes:** `"xxsmall"`, `"xsmall"`, `"small"` (default), `"medium"`, `"large"`, `"xlarge"`, `"xxlarge"`, `None` + +**When to use flex vs columns:** +- Flex (`horizontal=True`): Dynamic number of items, wrapping, alignment control +- Columns: Fixed grid layout, precise width ratios + +### Empty + +Single-element placeholder that can be updated: + +```python +placeholder = st.empty() + +# Update the placeholder +placeholder.text("Loading...") +result = load_data() +placeholder.dataframe(result) + +# Clear it +placeholder.empty() +``` + +For multiple elements in a placeholder: + +```python +placeholder = st.empty() + +with placeholder.container(): + st.write("Line 1") + st.write("Line 2") +``` + +### Space + +Add vertical or horizontal spacing: + +```python +st.write("Section 1") +st.space("medium") # Add medium vertical space +st.write("Section 2") + +# Inside horizontal containers, adds horizontal space +with st.container(horizontal=True): + st.button("Left") + st.space("large") + st.button("Right") +``` + +**Size options:** `"xxsmall"`, `"xsmall"`, `"small"` (default), `"medium"`, `"large"`, `"xlarge"`, `"xxlarge"` + +## Dialogs + +Modal overlays for focused interactions: + +```python +@st.dialog("Edit User") +def edit_user(user_id): + name = st.text_input("Name", value=get_name(user_id)) + if st.button("Save"): + save_user(user_id, name) + st.rerun() # Close dialog and refresh + +# Trigger dialog +if st.button("Edit"): + edit_user(user_id) +``` + +**Key points:** +- Dialogs rerun independently from the main script +- Call `st.rerun()` to close dialog and refresh main app +- Use `dismissible=False` for forced actions and `on_dismiss` for cleanup +- `st.sidebar` is not supported inside dialogs +- Use for forms, confirmations, detail views + +## Status Elements + +### Spinner (Blocking) + +```python +with st.spinner("Loading data..."): + data = load_data() +st.success("Done!") +``` + +### Progress Bar + +```python +progress = st.progress(0, text="Processing...") +for i in range(100): + progress.progress(i + 1, text=f"Processing {i+1}%") +``` + +### Status Container (Multi-step) + +```python +with st.status("Downloading data...", expanded=True) as status: + st.write("Fetching from API...") + fetch_data() + st.write("Processing...") + process_data() + status.update(label="Complete!", state="complete", expanded=False) +``` + +### Toast (Non-blocking) + +```python +st.toast("File saved!", icon="βœ…") +``` + +### Alerts + +```python +st.success("Operation completed") +st.info("FYI: New features available") +st.warning("Check your input") +st.error("Something went wrong") +st.exception(e) # Display exception with traceback +``` + +## Common Layout Patterns + +### Dashboard Header + +```python +st.title("Dashboard") + +col1, col2, col3, col4 = st.columns(4) +col1.metric("Revenue", "$1.2M", "+12%") +col2.metric("Users", "5,432", "+8%") +col3.metric("Orders", "1,234", "-2%") +col4.metric("Conversion", "2.4%", "+0.3%") + +st.divider() +``` + +### Sidebar Filters + +```python +with st.sidebar: + st.header("Filters") + date_range = st.date_input("Date range", []) + category = st.multiselect("Category", categories) + st.divider() + if st.button("Reset filters"): + st.rerun() +``` + +### Two-Panel Layout + +```python +left, right = st.columns([1, 2]) + +with left: + st.subheader("Selection") + selected = st.selectbox("Item", items) + +with right: + st.subheader("Details") + st.write(get_details(selected)) +``` + +## Popover + +Hover/click to reveal content: + +```python +with st.popover("Settings βš™οΈ"): + st.checkbox("Dark mode") + st.slider("Font size", 10, 24) +``` + +## Reference + +- [Layout docs](https://docs.streamlit.io/develop/api-reference/layout) +- [Status elements](https://docs.streamlit.io/develop/api-reference/status) +- [st.dialog API](https://docs.streamlit.io/develop/api-reference/execution-flow/st.dialog) diff --git a/skills/displaying-streamlit-data/SKILL.md b/skills/displaying-streamlit-data/SKILL.md new file mode 100644 index 0000000..63f28b9 --- /dev/null +++ b/skills/displaying-streamlit-data/SKILL.md @@ -0,0 +1,271 @@ +--- +name: displaying-streamlit-data +description: Displays data, tables, charts, and metrics in Streamlit apps. Use when showing DataFrames, creating visualizations, building dashboards, configuring data tables, or working with st.dataframe, st.data_editor, or chart elements. +--- + +# Displaying Streamlit Data + +Choose the right element based on your use case and configure for optimal user experience. + +## Choosing Display Elements + +| Element | Use Case | +|---------|----------| +| `st.dataframe` | Interactive exploration, sorting, filtering | +| `st.data_editor` | User-editable tables | +| `st.table` | Static display, no interaction needed | +| `st.metric` | KPIs with delta indicators | +| `st.json` | Structured data inspection | + +## DataFrames + +```python +# Basic display +st.dataframe(df) + +# With configuration +st.dataframe( + df, + height=400, + width="stretch", + hide_index=True, +) +``` + +### Column Configuration + +Format and customize columns with `st.column_config`: + +```python +st.dataframe( + df, + column_config={ + "price": st.column_config.NumberColumn( + "Price", + format="$%.2f", + ), + "rating": st.column_config.ProgressColumn( + "Rating", + min_value=0, + max_value=5, + ), + "website": st.column_config.LinkColumn("Website"), + "thumbnail": st.column_config.ImageColumn("Preview"), + "date": st.column_config.DateColumn( + "Date", + format="DD/MM/YYYY", + ), + }, +) +``` + +**Available column types:** +- `Column` - Generic column with label, width, help text +- `TextColumn` - Text with max chars, validation +- `NumberColumn` - Numbers with formatting, min/max +- `CheckboxColumn` - Boolean as checkbox +- `SelectboxColumn` - Single dropdown selection +- `MultiSelectColumn` - Multiple dropdown selections +- `DateColumn`, `TimeColumn`, `DatetimeColumn` - Temporal data +- `LinkColumn` - Clickable URLs +- `ImageColumn` - Display images from URLs +- `ListColumn` - Display Python lists +- `JsonColumn` - Display JSON/dict data +- `ProgressColumn` - Progress bars (0-1 or custom range) +- `BarChartColumn`, `LineChartColumn`, `AreaChartColumn` - Inline charts + +### DataFrame Selection + +Enable row/column selection and react to user picks: + +```python +event = st.dataframe( + df, + key="orders", + on_select="rerun", + selection_mode="multi-row", # Or "single-row" +) + +if event and event.selection.rows: + selected = df.iloc[event.selection.rows] + st.write(selected) +``` + +## Data Editor + +For editable tables: + +```python +edited_df = st.data_editor( + df, + num_rows="dynamic", # Allow adding/deleting rows + key="editor", +) + +# Access edit details via session state +if "editor" in st.session_state: + changes = st.session_state.editor + # changes contains: edited_rows, added_rows, deleted_rows +``` + +**Restrict editing:** + +```python +st.data_editor( + df, + disabled=["id", "created_at"], # Read-only columns + column_config={ + "status": st.column_config.SelectboxColumn( + "Status", + options=["pending", "approved", "rejected"], + ), + }, +) +``` + +## Metrics + +Display KPIs with change indicators: + +```python +col1, col2, col3 = st.columns(3) + +col1.metric("Revenue", "$1.2M", "+12%") +col2.metric("Users", "5,432", "-3%", delta_color="inverse") +col3.metric("Conversion", "2.4%", "0.1%") +``` + +**delta_color options:** +- `"normal"` (default): Green for positive, red for negative +- `"inverse"`: Red for positive, green for negative +- `"off"`: No color + +### Advanced Metric Options + +```python +# With border +st.metric("Revenue", "$1.2M", "+12%", border=True) + +# With sparkline chart +st.metric( + "Stock Price", + "$142.50", + "+2.3%", + chart_data=[100, 120, 115, 130, 142], + chart_type="line", # or "bar", "area" +) + +# Custom number formatting +st.metric( + "Large Number", + 1234567, + format="$,.0f", # Formats as "$1,234,567" +) +``` + +## Charts + +### Built-in Charts (Quick & Simple) + +```python +# Line chart +st.line_chart(df, x="date", y=["revenue", "costs"]) + +# Bar chart +st.bar_chart(df, x="category", y="count") + +# Area chart +st.area_chart(df, x="date", y="value") + +# Scatter plot +st.scatter_chart(df, x="x", y="y", color="category", size="value") +``` + +### Altair (Declarative, Flexible) + +```python +import altair as alt + +chart = alt.Chart(df).mark_line().encode( + x="date:T", + y="value:Q", + color="category:N", +) +st.altair_chart(chart, width="stretch") +``` + +### Plotly (Interactive) + +```python +import plotly.express as px + +fig = px.scatter(df, x="x", y="y", color="category", hover_data=["name"]) +st.plotly_chart(fig, width="stretch") +``` + +## Large Datasets + +### Pagination Pattern + +```python +PAGE_SIZE = 100 + +if "page" not in st.session_state: + st.session_state.page = 0 + +total_pages = (len(df) + PAGE_SIZE - 1) // PAGE_SIZE + +col1, col2, col3 = st.columns([1, 2, 1]) +with col1: + if st.button("Previous") and st.session_state.page > 0: + st.session_state.page -= 1 +with col3: + if st.button("Next") and st.session_state.page < total_pages - 1: + st.session_state.page += 1 + +start = st.session_state.page * PAGE_SIZE +st.dataframe(df.iloc[start:start + PAGE_SIZE]) +``` + +### Server-side Filtering + +```python +@st.cache_data +def load_filtered(category: str, limit: int = 1000): + return db.query(f"SELECT * FROM data WHERE category = '{category}' LIMIT {limit}") + +category = st.selectbox("Category", categories) +df = load_filtered(category) +st.dataframe(df) +``` + +## Timezone Handling + +Streamlit displays datetime values exactly as provided (no auto-conversion to user's timezone): + +```python +# Naive datetime - displays without timezone info +df["date"] = pd.to_datetime(df["date"]) + +# Timezone-aware - displays with timezone suffix +df["date"] = pd.to_datetime(df["date"]).dt.tz_localize("UTC") +``` + +## JSON Display + +```python +# Expandable JSON +st.json(data, expanded=False) + +# Expanded by default +st.json(data, expanded=True) + +# Expand to specific depth +st.json(data, expanded=2) +``` + +## Reference + +- [Data elements docs](https://docs.streamlit.io/develop/api-reference/data) +- [Chart elements docs](https://docs.streamlit.io/develop/api-reference/charts) +- [Column configuration docs](https://docs.streamlit.io/develop/api-reference/data/st.column_config) diff --git a/skills/optimizing-streamlit-performance/SKILL.md b/skills/optimizing-streamlit-performance/SKILL.md new file mode 100644 index 0000000..360f11c --- /dev/null +++ b/skills/optimizing-streamlit-performance/SKILL.md @@ -0,0 +1,214 @@ +--- +name: optimizing-streamlit-performance +description: Optimizes Streamlit app performance using caching, fragments, and efficient data handling. Use when apps are slow, working with large datasets, experiencing unnecessary reruns, or need performance optimization. +--- + +# Optimizing Streamlit Performance + +Streamlit reruns the entire script on every interaction. Use these patterns to minimize recomputation and improve responsiveness. + +## Caching Strategy + +### @st.cache_data - For Data + +Returns a new copy each call. Use for DataFrames, API responses, computed results. + +```python +@st.cache_data(ttl=3600) # Set ttl for live data +def fetch_users(): + return db.query("SELECT * FROM users") + +@st.cache_data(max_entries=100) # Limit memory usage +def compute_stats(data): + return expensive_computation(data) +``` + +**Key parameters:** +- `ttl`: Time-to-live in seconds. Recommended for data that changes. +- `max_entries`: Maximum cached results. Prevents memory bloat. +- `show_spinner`: Show loading indicator (default: True) + +### @st.cache_resource - For Resources + +Returns the same object each call. Use for DB connections, ML models, heavy objects. + +```python +@st.cache_resource +def get_database(): + return create_connection() + +@st.cache_resource +def load_model(): + return torch.load("model.pt") +``` + +**Cleanup with `on_release`:** Use to clean up resources when evicted from cache: + +```python +def cleanup_connection(conn): + conn.close() + +@st.cache_resource(on_release=cleanup_connection) +def get_database(): + return create_connection() +``` + +**Critical warning:** Never mutate `@st.cache_resource` returnsβ€”changes affect all users: + +```python +# BAD: Mutating shared resource +@st.cache_resource +def get_config(): + return {"setting": "default"} + +config = get_config() +config["setting"] = "custom" # Affects ALL users! + +# GOOD: Return immutable or copy before modifying +config = get_config().copy() +config["setting"] = "custom" +``` + +### Caching Anti-patterns + +```python +# BAD: Caching function that reads widgets +@st.cache_data +def filtered_data(): + query = st.text_input("Query") # Widget inside cached function! + return df[df["name"].str.contains(query)] + +# GOOD: Pass widget values as parameters +@st.cache_data +def filtered_data(query: str): + return df[df["name"].str.contains(query)] + +query = st.text_input("Query") +result = filtered_data(query) +``` + +## Fragments for Partial Reruns + +`@st.fragment` reruns only its contents when widgets inside it change, not the whole app. + +```python +@st.fragment +def chart_controls(): + chart_type = st.selectbox("Chart", ["line", "bar"]) + # Only this fragment reruns when chart_type changes + if chart_type == "line": + st.line_chart(data) + else: + st.bar_chart(data) + +# This expensive operation doesn't rerun when chart_controls changes +data = load_large_dataset() +chart_controls() +``` + +**Use fragments for:** +- Independent UI sections (charts, filters, forms) +- Components that update frequently +- Isolating expensive operations from frequent interactions + +**Limitations:** +- Cannot combine `@st.fragment` with `@st.cache_data` on same function +- Return values are ignored on fragment reruns (use session state) +- Widgets can't be rendered in containers created outside the fragment body +- Auto-rerun with `run_every` for periodic updates: `@st.fragment(run_every="5s")` + +## Forms for Batched Input + +Forms prevent reruns until the submit button is clicked. + +```python +with st.form("settings"): + name = st.text_input("Name") + email = st.text_input("Email") + age = st.number_input("Age", 0, 120) + + # REQUIRED: Every form needs a submit button + if st.form_submit_button("Save"): + save_user(name, email, age) +``` + +**Use forms when:** +- Multiple related inputs should be submitted together +- Each keystroke would cause expensive reruns +- User needs to fill multiple fields before processing + +## Large Data Handling + +### For datasets under ~100M rows + +```python +@st.cache_data +def load_data(): + return pd.read_parquet("large_file.parquet") +``` + +### For very large datasets + +`@st.cache_data` uses pickle which slows with huge data. Use `@st.cache_resource` instead: + +```python +@st.cache_resource # No serialization overhead +def load_huge_data(): + return pd.read_parquet("huge_file.parquet") + +# WARNING: Don't mutate the returned DataFrame! +``` + +### Sampling for exploration + +```python +@st.cache_data(ttl=3600) +def load_sample(n=10000): + df = pd.read_parquet("huge.parquet") + return df.sample(n=n) +``` + +## Multithreading + +Custom threads cannot call Streamlit commands (no session context). + +```python +import threading + +def fetch_in_background(url, results, index): + results[index] = requests.get(url).json() # No st.* calls! + +# Collect results, then display in main thread +results = [None] * 3 +threads = [ + threading.Thread(target=fetch_in_background, args=(url, results, i)) + for i, url in enumerate(urls) +] +for t in threads: + t.start() +for t in threads: + t.join() + +# Now display in main thread +for result in results: + st.write(result) +``` + +**Prefer alternatives when possible:** +- `@st.cache_data` for expensive computations +- `@st.fragment(run_every="5s")` for periodic updates +- `asyncio` with `st.write_stream()` for async operations + +## Performance Checklist + +1. **Cache all expensive operations** - data loading, API calls, computations +2. **Set TTL on cached data** - prevent stale data in production +3. **Use fragments** - isolate frequently-updating sections +4. **Use forms** - batch related inputs +5. **Profile before optimizing** - use `st.cache_data(show_spinner="Loading...")` to identify slow operations + +## Reference + +- [Caching docs](https://docs.streamlit.io/develop/concepts/architecture/caching) +- [Fragments docs](https://docs.streamlit.io/develop/concepts/architecture/fragments) +- [Forms docs](https://docs.streamlit.io/develop/concepts/architecture/forms) diff --git a/skills/securing-streamlit-apps/SKILL.md b/skills/securing-streamlit-apps/SKILL.md new file mode 100644 index 0000000..6d17182 --- /dev/null +++ b/skills/securing-streamlit-apps/SKILL.md @@ -0,0 +1,228 @@ +--- +name: securing-streamlit-apps +description: Secures Streamlit apps with secrets management, authentication, and security best practices. Use when handling credentials, implementing user authentication, managing secrets, or deploying apps to production. +--- + +# Securing Streamlit Apps + +Security essentials for production Streamlit applications. + +## Secrets Management + +**Never hardcode credentials.** Use `st.secrets` with `secrets.toml`. + +```python +# BAD: Hardcoded secrets +api_key = "sk-abc123..." # NEVER DO THIS + +# GOOD: Use st.secrets +api_key = st.secrets["OPENAI_API_KEY"] +``` + +### secrets.toml Structure + +```toml +# .streamlit/secrets.toml +OPENAI_API_KEY = "sk-..." +DATABASE_URL = "postgresql://..." + +# Nested secrets +[database] +host = "localhost" +password = "secret" + +# Connection-specific +[connections.mydb] +host = "localhost" +password = "secret" +``` + +### Accessing Secrets + +```python +# Root level +api_key = st.secrets["OPENAI_API_KEY"] + +# Nested (both notations work) +db_host = st.secrets["database"]["host"] +db_host = st.secrets.database.host + +# With default +value = st.secrets.get("OPTIONAL_KEY", "default") +``` + +### Critical: Add to .gitignore + +``` +# .gitignore +.streamlit/secrets.toml +``` + +## User Authentication (OIDC) + +Streamlit supports OpenID Connect authentication with major providers. + +### Configuration + +```toml +# .streamlit/secrets.toml +[auth] +redirect_uri = "http://localhost:8501/oauth2callback" +cookie_secret = "generate-a-strong-random-string" +client_id = "your-client-id" +client_secret = "your-client-secret" +server_metadata_url = "https://accounts.google.com/.well-known/openid-configuration" +``` + +### Login Flow + +```python +import streamlit as st + +if not st.user.is_logged_in: + st.button("Log in", on_click=st.login) + st.stop() + +st.button("Log out", on_click=st.logout) +st.write(f"Welcome, {st.user.name}!") +st.write(f"Email: {st.user.email}") +``` + +**Note:** `st.login()` and `st.logout()` start a new session after updating the auth cookie. Don't rely on previous session state. + +**Guard:** When auth isn't configured, `st.user` has no attributes. Use a safe check: + +```python +if not getattr(st.user, "is_logged_in", False): + st.stop() +``` + +### Multiple Providers + +```toml +# .streamlit/secrets.toml +[auth] +redirect_uri = "http://localhost:8501/oauth2callback" +cookie_secret = "xxx" + +[auth.google] +client_id = "xxx" +client_secret = "xxx" +server_metadata_url = "https://accounts.google.com/.well-known/openid-configuration" + +[auth.microsoft] +client_id = "xxx" +client_secret = "xxx" +server_metadata_url = "https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration" +``` + +```python +if not getattr(st.user, "is_logged_in", False): + st.button("Log in with Google", on_click=st.login, args=["google"]) + st.button("Log in with Microsoft", on_click=st.login, args=["microsoft"]) + st.stop() +``` + +### Conditional Content + +```python +if st.user.is_logged_in: + if st.user.email.endswith("@company.com"): + st.write("Internal dashboard content") + else: + st.warning("Access restricted to company users") +``` + +## Security Best Practices + +### Pickle Security + +`st.cache_data` and `st.session_state` use pickle internally. + +```python +# RISK: Deserializing untrusted data +import pickle +data = pickle.loads(user_uploaded_bytes) # DANGEROUS! + +# SAFE: Only deserialize trusted sources +# Streamlit's internal pickle usage is safe for normal operations +``` + +### File Upload Validation + +```python +uploaded_file = st.file_uploader("Upload", type=["csv", "xlsx"]) + +if uploaded_file: + # Validate file type + if uploaded_file.type not in ["text/csv", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"]: + st.error("Invalid file type") + st.stop() + + # Validate file size + if uploaded_file.size > 10 * 1024 * 1024: # 10MB + st.error("File too large") + st.stop() +``` + +### SQL Injection Prevention + +```python +# BAD: String formatting +query = f"SELECT * FROM users WHERE id = {user_input}" # VULNERABLE! + +# GOOD: Parameterized queries +df = conn.query( + "SELECT * FROM users WHERE id = :id", + params={"id": user_input}, + ttl=600 +) +``` + +### Input Validation + +```python +user_input = st.text_input("Enter ID") + +# Validate before using +if not user_input.isdigit(): + st.error("Please enter a valid numeric ID") + st.stop() + +# Now safe to use +user_id = int(user_input) +``` + +## Static File Serving + +For serving static assets: + +```toml +# .streamlit/config.toml +[server] +enableStaticServing = true +``` + +Place files in `./static/` directory, accessible at `app/static/filename.ext`. + +**Limitations:** +- Only certain file types served with correct MIME type (.jpg, .png, .gif, .pdf, .json, etc.) +- Other types served as `text/plain` for security + +## Environment Variables + +```python +import os + +# Read from environment +api_key = os.environ.get("API_KEY") +``` + +**Tip:** Root-level `secrets.toml` entries are also exposed as environment variables. + +## Reference + +- [Secrets management](https://docs.streamlit.io/develop/concepts/connections/secrets-management) +- [Authentication docs](https://docs.streamlit.io/develop/concepts/connections/authentication) +- [st.user API](https://docs.streamlit.io/develop/api-reference/user/st.user) +- [Security reminders](https://docs.streamlit.io/develop/concepts/connections/security-reminders) diff --git a/skills/streamlit-api-changelog/SKILL.md b/skills/streamlit-api-changelog/SKILL.md new file mode 100644 index 0000000..b917f5f --- /dev/null +++ b/skills/streamlit-api-changelog/SKILL.md @@ -0,0 +1,71 @@ +--- +name: streamlit-api-changelog +description: Streamlit API changelog and version history for recent releases. Use when checking what changed between versions, finding when a feature was added or deprecated, understanding breaking changes, or migrating between Streamlit versions. +--- + +# Streamlit API Changelog + +Reference for API changes across the last 10 Streamlit releases. + +## When to Use + +- Checking what changed in a specific version +- Finding when a feature was introduced or deprecated +- Understanding breaking changes before upgrading +- Migrating code between Streamlit versions + +## Check Your Current Version + +From the command line: + +```bash +streamlit --version +# or +python -c "import streamlit; print(streamlit.__version__)" +``` + +or from Python code: + +```python +import streamlit as st +st.__version__ # Returns version string, e.g., '1.40.0' +``` + +## Available Versions + +| Version | Release Date | +|---------|--------------------| +| 1.53.0 | January 14, 2026 | +| 1.52.0 | December 3, 2025 | +| 1.51.0 | October 29, 2025 | +| 1.50.0 | September 23, 2025 | +| 1.49.0 | August 26, 2025 | +| 1.48.0 | August 5, 2025 | +| 1.47.0 | July 16, 2025 | +| 1.46.0 | June 18, 2025 | +| 1.45.0 | April 29, 2025 | +| 1.44.0 | March 25, 2025 | + +## Quick Reference + +### Recent Breaking Changes + +- **1.xx**: TODO - describe breaking change +- **1.xx**: TODO - describe breaking change + +## Detailed Changelogs + +For complete details on a specific version, refer to: + +- `references/CHANGELOG-1.53.md` +- `references/CHANGELOG-1.52.md` +- `references/CHANGELOG-1.51.md` +- `references/CHANGELOG-1.50.md` +- `references/CHANGELOG-1.49.md` +- `references/CHANGELOG-1.48.md` +- `references/CHANGELOG-1.47.md` +- `references/CHANGELOG-1.46.md` +- `references/CHANGELOG-1.45.md` +- `references/CHANGELOG-1.44.md` + +See `references/BREAKING-CHANGES.md` for a consolidated list of all breaking changes. diff --git a/skills/streamlit-api-changelog/references/BREAKING-CHANGES.md b/skills/streamlit-api-changelog/references/BREAKING-CHANGES.md new file mode 100644 index 0000000..b765660 --- /dev/null +++ b/skills/streamlit-api-changelog/references/BREAKING-CHANGES.md @@ -0,0 +1,43 @@ +# Breaking Changes Summary + +Consolidated list of breaking changes across recent Streamlit releases. + +## 1.53.0 + +- TODO: List breaking changes + +## 1.52.0 + +- TODO: List breaking changes + +## 1.51.0 + +- TODO: List breaking changes + +## 1.50.0 + +- TODO: List breaking changes + +## 1.49.0 + +- TODO: List breaking changes + +## 1.48.0 + +- TODO: List breaking changes + +## 1.47.0 + +- TODO: List breaking changes + +## 1.46.0 + +- TODO: List breaking changes + +## 1.45.0 + +- TODO: List breaking changes + +## 1.44.0 + +- TODO: List breaking changes diff --git a/skills/streamlit-api-changelog/references/CHANGELOG-1.44.md b/skills/streamlit-api-changelog/references/CHANGELOG-1.44.md new file mode 100644 index 0000000..936aefd --- /dev/null +++ b/skills/streamlit-api-changelog/references/CHANGELOG-1.44.md @@ -0,0 +1,27 @@ +# Streamlit 1.44.0 Changelog + +**Release Date:** March 25, 2025 + +## Highlights + +- TODO: Add release highlights + +## New Features + +- TODO: List new features + +## Improvements + +- TODO: List improvements + +## Bug Fixes + +- TODO: List bug fixes + +## Breaking Changes + +- TODO: List breaking changes (if any) + +## Deprecations + +- TODO: List deprecations (if any) diff --git a/skills/streamlit-api-changelog/references/CHANGELOG-1.45.md b/skills/streamlit-api-changelog/references/CHANGELOG-1.45.md new file mode 100644 index 0000000..0d11f04 --- /dev/null +++ b/skills/streamlit-api-changelog/references/CHANGELOG-1.45.md @@ -0,0 +1,27 @@ +# Streamlit 1.45.0 Changelog + +**Release Date:** April 29, 2025 + +## Highlights + +- TODO: Add release highlights + +## New Features + +- TODO: List new features + +## Improvements + +- TODO: List improvements + +## Bug Fixes + +- TODO: List bug fixes + +## Breaking Changes + +- TODO: List breaking changes (if any) + +## Deprecations + +- TODO: List deprecations (if any) diff --git a/skills/streamlit-api-changelog/references/CHANGELOG-1.46.md b/skills/streamlit-api-changelog/references/CHANGELOG-1.46.md new file mode 100644 index 0000000..f226a11 --- /dev/null +++ b/skills/streamlit-api-changelog/references/CHANGELOG-1.46.md @@ -0,0 +1,27 @@ +# Streamlit 1.46.0 Changelog + +**Release Date:** June 18, 2025 + +## Highlights + +- TODO: Add release highlights + +## New Features + +- TODO: List new features + +## Improvements + +- TODO: List improvements + +## Bug Fixes + +- TODO: List bug fixes + +## Breaking Changes + +- TODO: List breaking changes (if any) + +## Deprecations + +- TODO: List deprecations (if any) diff --git a/skills/streamlit-api-changelog/references/CHANGELOG-1.47.md b/skills/streamlit-api-changelog/references/CHANGELOG-1.47.md new file mode 100644 index 0000000..79c571b --- /dev/null +++ b/skills/streamlit-api-changelog/references/CHANGELOG-1.47.md @@ -0,0 +1,27 @@ +# Streamlit 1.47.0 Changelog + +**Release Date:** July 16, 2025 + +## Highlights + +- TODO: Add release highlights + +## New Features + +- TODO: List new features + +## Improvements + +- TODO: List improvements + +## Bug Fixes + +- TODO: List bug fixes + +## Breaking Changes + +- TODO: List breaking changes (if any) + +## Deprecations + +- TODO: List deprecations (if any) diff --git a/skills/streamlit-api-changelog/references/CHANGELOG-1.48.md b/skills/streamlit-api-changelog/references/CHANGELOG-1.48.md new file mode 100644 index 0000000..2aba2fe --- /dev/null +++ b/skills/streamlit-api-changelog/references/CHANGELOG-1.48.md @@ -0,0 +1,27 @@ +# Streamlit 1.48.0 Changelog + +**Release Date:** August 5, 2025 + +## Highlights + +- TODO: Add release highlights + +## New Features + +- TODO: List new features + +## Improvements + +- TODO: List improvements + +## Bug Fixes + +- TODO: List bug fixes + +## Breaking Changes + +- TODO: List breaking changes (if any) + +## Deprecations + +- TODO: List deprecations (if any) diff --git a/skills/streamlit-api-changelog/references/CHANGELOG-1.49.md b/skills/streamlit-api-changelog/references/CHANGELOG-1.49.md new file mode 100644 index 0000000..b517512 --- /dev/null +++ b/skills/streamlit-api-changelog/references/CHANGELOG-1.49.md @@ -0,0 +1,27 @@ +# Streamlit 1.49.0 Changelog + +**Release Date:** August 26, 2025 + +## Highlights + +- TODO: Add release highlights + +## New Features + +- TODO: List new features + +## Improvements + +- TODO: List improvements + +## Bug Fixes + +- TODO: List bug fixes + +## Breaking Changes + +- TODO: List breaking changes (if any) + +## Deprecations + +- TODO: List deprecations (if any) diff --git a/skills/streamlit-api-changelog/references/CHANGELOG-1.50.md b/skills/streamlit-api-changelog/references/CHANGELOG-1.50.md new file mode 100644 index 0000000..74f496d --- /dev/null +++ b/skills/streamlit-api-changelog/references/CHANGELOG-1.50.md @@ -0,0 +1,27 @@ +# Streamlit 1.50.0 Changelog + +**Release Date:** September 23, 2025 + +## Highlights + +- TODO: Add release highlights + +## New Features + +- TODO: List new features + +## Improvements + +- TODO: List improvements + +## Bug Fixes + +- TODO: List bug fixes + +## Breaking Changes + +- TODO: List breaking changes (if any) + +## Deprecations + +- TODO: List deprecations (if any) diff --git a/skills/streamlit-api-changelog/references/CHANGELOG-1.51.md b/skills/streamlit-api-changelog/references/CHANGELOG-1.51.md new file mode 100644 index 0000000..1320153 --- /dev/null +++ b/skills/streamlit-api-changelog/references/CHANGELOG-1.51.md @@ -0,0 +1,27 @@ +# Streamlit 1.51.0 Changelog + +**Release Date:** October 29, 2025 + +## Highlights + +- TODO: Add release highlights + +## New Features + +- TODO: List new features + +## Improvements + +- TODO: List improvements + +## Bug Fixes + +- TODO: List bug fixes + +## Breaking Changes + +- TODO: List breaking changes (if any) + +## Deprecations + +- TODO: List deprecations (if any) diff --git a/skills/streamlit-api-changelog/references/CHANGELOG-1.52.md b/skills/streamlit-api-changelog/references/CHANGELOG-1.52.md new file mode 100644 index 0000000..3421945 --- /dev/null +++ b/skills/streamlit-api-changelog/references/CHANGELOG-1.52.md @@ -0,0 +1,27 @@ +# Streamlit 1.52.0 Changelog + +**Release Date:** December 3, 2025 + +## Highlights + +- TODO: Add release highlights + +## New Features + +- TODO: List new features + +## Improvements + +- TODO: List improvements + +## Bug Fixes + +- TODO: List bug fixes + +## Breaking Changes + +- TODO: List breaking changes (if any) + +## Deprecations + +- TODO: List deprecations (if any) diff --git a/skills/streamlit-api-changelog/references/CHANGELOG-1.53.md b/skills/streamlit-api-changelog/references/CHANGELOG-1.53.md new file mode 100644 index 0000000..b41d59e --- /dev/null +++ b/skills/streamlit-api-changelog/references/CHANGELOG-1.53.md @@ -0,0 +1,27 @@ +# Streamlit 1.53.0 Changelog + +**Release Date:** January 14, 2026 + +## Highlights + +- TODO: Add release highlights + +## New Features + +- TODO: List new features + +## Improvements + +- TODO: List improvements + +## Bug Fixes + +- TODO: List bug fixes + +## Breaking Changes + +- TODO: List breaking changes (if any) + +## Deprecations + +- TODO: List deprecations (if any) diff --git a/skills/streamlit-best-practices/SKILL.md b/skills/streamlit-best-practices/SKILL.md new file mode 100644 index 0000000..99c5894 --- /dev/null +++ b/skills/streamlit-best-practices/SKILL.md @@ -0,0 +1,271 @@ +--- +name: streamlit-best-practices +description: Opinionated best practices and coding conventions for Streamlit apps. Use when starting a new Streamlit project, reviewing code quality, or seeking guidance on recommended patterns and anti-patterns. +--- + +# Streamlit Best Practices + +Opinionated guidelines for writing clean, performant, and maintainable Streamlit apps. + +## Styling + +**Don't use custom CSS.** Rely on native features and theming instead. + +```python +# BAD: Custom CSS +st.markdown("", unsafe_allow_html=True) + +# GOOD: Use theming in .streamlit/config.toml +# [theme] +# primaryColor = "#FF4B4B" +# backgroundColor = "#FFFFFF" +``` + +**Prefer Material Icons over emojis** for a professional look: + +```python +# BAD: Emojis +st.page_link("home.py", label="Home", icon="🏠") + +# GOOD: Material Icons +st.page_link("home.py", label="Home", icon=":material/home:") +``` + +## Layout + +**Prefer `st.container(border=True)` for visual grouping:** + +```python +# GOOD: Clean visual grouping +with st.container(border=True): + st.metric("Revenue", "$1M") + st.caption("Last 30 days") +``` + +**Use `width` parameter instead of deprecated `use_container_width`:** + +```python +# BAD: Deprecated +st.dataframe(df, use_container_width=True) + +# GOOD: New parameter +st.dataframe(df, width="stretch") # Was use_container_width=True +st.dataframe(df, width="content") # Was use_container_width=False +``` + +**Prefer flex containers over columns** for simple row layouts: + +```python +# BAD: Columns for simple button row +col1, col2, col3 = st.columns(3) +col1.button("A") +col2.button("B") +col3.button("C") + +# GOOD: Flex container with alignment +with st.container(horizontal=True, horizontal_alignment="right"): + st.button("Cancel") + st.button("Save", type="primary") + +# GOOD: Distributed metrics +with st.container(horizontal=True, horizontal_alignment="distribute"): + st.metric("Revenue", "$1M") + st.metric("Users", "5K") + st.metric("Orders", "1.2K") +``` + +**When to use columns vs flex:** +- Columns: Fixed grid layouts with precise width ratios +- Flex: Dynamic items, alignment control, wrapping behavior + + +## Navigation + +**Prefer `st.navigation` over `pages/` folder** for multipage apps: + +```python +# GOOD: Explicit control, conditional pages, custom icons +pages = [ + st.Page("app_pages/home.py", title="Home", icon=":material/home:"), + st.Page("app_pages/dashboard.py", title="Dashboard", icon=":material/dashboard:"), +] +nav = st.navigation(pages) +nav.run() +``` + +## Caching + +**Always cache expensive operations with appropriate limits:** + +```python +# BAD: No TTL, unbounded growth +@st.cache_data +def load_data(): + return fetch_from_api() + +# GOOD: TTL and max_entries prevent issues +@st.cache_data(ttl=3600, max_entries=100) +def load_data(): + return fetch_from_api() +``` + +**Cache at the right granularity:** + +```python +# BAD: Caching too much +@st.cache_data +def get_and_filter_data(filter_value): # New cache entry per filter! + data = load_all_data() + return data[data["col"] == filter_value] + +# GOOD: Cache the expensive part, filter separately +@st.cache_data(ttl=3600) +def load_all_data(): + return fetch_from_database() + +data = load_all_data() +filtered = data[data["col"] == filter_value] +``` + +## Charts + +**Prefer Vega-based charts** over pyplot (matplotlib) and plotly: + +```python +# GOOD: Native, fast, consistent styling +st.line_chart(df, x="date", y="value") +st.bar_chart(df, x="category", y="count") +st.scatter_chart(df, x="x", y="y", color="category") +st.area_chart(df, x="date", y="value") + +# For complex charts, use Altair +import altair as alt +chart = alt.Chart(df).mark_line().encode(x="date:T", y="value:Q") +st.altair_chart(chart, width="stretch") +``` + +**Why avoid pyplot/plotly:** +- Inconsistent theming +- Slower rendering +- More dependencies + +## Session State + +**Initialize all session state in one place:** + +```python +# GOOD: Clear initialization at top of app +def init_state(): +st.session_state.setdefault("user", None) +st.session_state.setdefault("page", "home") +st.session_state.setdefault("filters", {}) +``` + +**Avoid module-level mutable state:** + +```python +# BAD: In imported modules, module-level state is shared across all users! +# utils.py +cache = {} # This persists across reruns and users + +# GOOD: Use session state for per-user data +st.session_state.setdefault("cache", {}) +``` + +## Widget Best Practices + +**Consider setting a `key` for widgets if:** + +1. **You have identical widgets** - Avoids `DuplicateWidgetID` errors when multiple widgets share the same label +2. **Parameters change dynamically** - With a key, changing label, placeholder, help text, or default value won't reset the widget +3. **You need programmatic access** - Read/write widget values via `st.session_state["key"]` + +```python +# Without key: changing the label resets the widget value +st.text_input(f"Search {category}") # Resets when category changes + +# With key: widget keeps its value even if label changes +st.text_input(f"Search {category}", key="search_query") + +# Access the value programmatically +if st.session_state.get("search_query"): + results = search(st.session_state.search_query) +``` + +**Use callbacks for immediate state changes:** + +```python +# GOOD: State updated before rerun +def on_submit(): + st.session_state.submitted = True + st.session_state.result = process(st.session_state.input_value) + +st.text_input("Input", key="input_value") +st.button("Submit", on_click=on_submit) +``` + +## Page Configuration + +**It is recommended to set page config at the top of each page:** + +```python +import streamlit as st + +st.set_page_config( + page_title="My App", + page_icon=":material/dashboard:", + layout="wide", +) + +# All other imports and code below +``` + +## Error Handling + +Use `print`/`logger` for developer logs (server-side, not visible to users). Use Streamlit's status elements (`st.error`, `st.warning`, `st.success`, `st.info`) for user-facing feedback. + +```python +# BAD: User won't see this - only appears in server logs +print("Processing complete!") +logger.error(f"Validation failed: {e}") + +# GOOD: User-friendly feedback with status elements +try: + result = process_data(data) + st.success("Processing complete!") +except ValidationError as e: + st.error(f"Invalid input: {e}") +except Exception as e: + logger.exception("Unexpected error") # For developer debugging + st.error("An error occurred. Please try again.") # For user +``` + +## File Organization + +**Keep pages focused, extract shared logic:** + +``` +my_app/ +β”œβ”€β”€ streamlit_app.py # `st.navigation` only +β”œβ”€β”€ app_pages/ # Page files (UI logic) +β”‚ β”œβ”€β”€ home.py +β”‚ └── dashboard.py +β”œβ”€β”€ utils/ # Shared business logic +β”‚ β”œβ”€β”€ data.py +β”‚ └── auth.py +└── .streamlit/ + β”œβ”€β”€ config.toml + └── secrets.toml +``` + +## Performance Tips + +1. **Profile before optimizing** - Use `st.spinner` to identify slow operations +2. **Lazy load heavy components** - Use `st.fragment` for independent sections +3. **Truncate large data** - Don't load 1M rows into a dataframe + +## Reference + +- [Theming docs](https://docs.streamlit.io/develop/concepts/configuration/theming) +- [Material Icons](https://fonts.google.com/icons) +- [st.navigation](https://docs.streamlit.io/develop/api-reference/navigation/st.navigation) diff --git a/skills/testing-streamlit-apps/SKILL.md b/skills/testing-streamlit-apps/SKILL.md new file mode 100644 index 0000000..701e750 --- /dev/null +++ b/skills/testing-streamlit-apps/SKILL.md @@ -0,0 +1,260 @@ +--- +name: testing-streamlit-apps +description: Tests Streamlit applications using the AppTest framework. Use when writing automated tests for Streamlit apps, testing widget interactions, verifying session state, or setting up CI/CD pipelines. +--- + +# Testing Streamlit Apps + +Use Streamlit's `AppTest` framework to test apps programmatically without a browser. + +## Basic Test Structure + +```python +from streamlit.testing.v1 import AppTest + +def test_basic(): + # Initialize and run the app + at = AppTest.from_file("app.py") + at.run() + + # Make assertions + assert at.title[0].value == "My App" + assert not at.exception +``` + +## Running Tests with pytest + +```bash +# Install pytest +pip install pytest + +# Run tests +pytest tests/ +``` + +**File naming convention:** `test_*.py` or `*_test.py` + +## Retrieving Elements + +Elements are accessed by type and index (display order): + +```python +at.run() + +# Text elements +at.title[0].value +at.header[0].value +at.markdown[0].value +at.text[0].value + +# Widgets +at.button[0] +at.text_input[0] +at.selectbox[0] +at.slider[0] +at.checkbox[0] + +# Data elements +at.dataframe[0] +at.table[0] +at.json[0] +``` + +### By Key + +Retrieve widgets by their key: + +```python +# If widget has key="username" +at.text_input("username").value +at.button("submit").click() # key as positional arg +``` + +### Containers + +```python +# Sidebar elements +at.sidebar.button[0] +at.sidebar.selectbox[0] + +# Columns +at.columns[0].button[0] # First button in first column + +# Tabs +at.tabs[0].write[0] # Content in first tab +``` + +## Interacting with Widgets + +### Buttons + +```python +at.run() +at.button[0].click() +at.run() # Must run again after interaction + +assert at.session_state.clicked is True +``` + +### Text Input + +```python +at.text_input[0].input("hello") +at.run() + +assert at.text_input[0].value == "hello" +``` + +### Selectbox + +```python +at.selectbox[0].select("Option B") +at.run() + +# Or by index +at.selectbox[0].select_index(1) +at.run() +``` + +### Slider + +```python +# Single-value slider +at.slider[0].set_value(50) +at.run() + +# Range slider +at.slider[1].set_range(10, 50) +at.run() + +# Increment/decrement +at.number_input[0].increment() +at.run() +``` + +### Checkbox + +```python +at.checkbox[0].check() +at.run() + +# Or use check/uncheck +at.checkbox[0].uncheck() +at.run() +``` + +## Testing Session State + +```python +def test_session_state(): + at = AppTest.from_file("app.py") + at.run() + + # Check initial state + assert "count" in at.session_state + assert at.session_state.count == 0 + + # Interact and verify state change + at.button[0].click() + at.run() + + assert at.session_state.count == 1 +``` + +### Initialize Session State + +```python +def test_with_initial_state(): + at = AppTest.from_file("app.py") + at.session_state["user"] = "test_user" + at.run() + + assert "Welcome, test_user" in at.markdown[0].value +``` + +## Testing with Secrets + +```python +def test_with_secrets(): + at = AppTest.from_file("app.py") + at.secrets["API_KEY"] = "test-key" + at.run() + + # App can now use st.secrets["API_KEY"] +``` + +## Testing Multipage Apps + +**Important:** `AppTest` supports one page per instance and does not work with `st.navigation`/`st.Page`. Test each page file directly. + +```python +def test_page(): + at = AppTest.from_file("pages/dashboard.py") + at.run() + + assert at.title[0].value == "Dashboard" +``` + +## Error Handling + +```python +def test_no_exceptions(): + at = AppTest.from_file("app.py") + at.run() + + # Check no exceptions occurred + assert not at.exception + + # Or check specific exception + if at.exception: + assert "expected error" in str(at.exception[0].value) +``` + +## Complete Example + +```python +# test_counter.py +from streamlit.testing.v1 import AppTest + +def test_counter_app(): + """Test a counter app with increment button.""" + at = AppTest.from_file("counter.py") + at.run() + + # Initial state + assert at.session_state.count == 0 + assert "Count: 0" in at.markdown[0].value + + # Click increment + at.button[0].click() + at.run() + + assert at.session_state.count == 1 + assert "Count: 1" in at.markdown[0].value + + +def test_counter_reset(): + """Test the reset button.""" + at = AppTest.from_file("counter.py") + at.session_state["count"] = 10 + at.run() + + # Click reset + at.button("reset").click() + at.run() + + assert at.session_state.count == 0 +``` + +## Best Practices + +1. **Keep tests focused** - One behavior per test +2. **Use descriptive names** - `test_login_with_valid_credentials` +3. **Test edge cases** - Empty inputs, boundary values +4. **Mock external services** - Don't hit real APIs in tests +5. **Run after each interaction** - Always call `at.run()` after widget interactions + +## Reference + +- [App testing docs](https://docs.streamlit.io/develop/concepts/app-testing) +- [AppTest API](https://docs.streamlit.io/develop/api-reference/app-testing/st.testing.v1.apptest)