Initial commit: Shelly Manager with Textual CLI, Streamlit UI, and comprehensive .gitignore
Shelly device management app with mDNS/subnet discovery, inventory, configuration, and mass operations for Gen1/Gen2+ devices. Includes .gitignore excluding runtime data (device DB, user config), AI conversation history, build artifacts, and common Python/OS patterns.
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
[project]
|
||||
name = "dashboard-feature-usage"
|
||||
version = "1.0.0"
|
||||
description = "A feature usage analytics dashboard with filtering and starter kits"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"altair>=5.5.0",
|
||||
"numpy>=1.26.0",
|
||||
"pandas>=2.2.3",
|
||||
"snowflake-connector-python>=3.3.0",
|
||||
"streamlit[snowflake]>=1.54.0",
|
||||
]
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
"""
|
||||
API Usage Dashboard Template
|
||||
|
||||
A feature analytics dashboard demonstrating:
|
||||
- Segmented control for category selection
|
||||
- Multiselect for endpoint filtering
|
||||
- Starter kits / presets for quick selection
|
||||
- Time series visualization with normalization
|
||||
- Metric cards with 28-day deltas
|
||||
- Rolling average options
|
||||
|
||||
This template uses synthetic data. Replace generate_api_data()
|
||||
with your actual data source (e.g., Snowflake queries, APIs, etc.)
|
||||
"""
|
||||
|
||||
from datetime import date, timedelta
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import streamlit as st
|
||||
import altair as alt
|
||||
|
||||
st.set_page_config(
|
||||
page_title="API Usage Dashboard",
|
||||
page_icon=":material/api:",
|
||||
layout="wide",
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Synthetic Data Generation (Replace with your data source)
|
||||
# =============================================================================
|
||||
|
||||
# API categories and their endpoints
|
||||
API_CATEGORIES = {
|
||||
"Users": ["/users", "/users/{id}", "/users/me", "/users/search", "/users/bulk", "/users/export"],
|
||||
"Orders": ["/orders", "/orders/{id}", "/orders/create", "/orders/cancel", "/orders/refund", "/orders/status"],
|
||||
"Products": ["/products", "/products/{id}", "/products/search", "/products/categories", "/products/inventory"],
|
||||
"Analytics": ["/analytics/events", "/analytics/metrics", "/analytics/reports", "/analytics/dashboards"],
|
||||
}
|
||||
|
||||
# Starter kits - predefined endpoint selections
|
||||
STARTER_KITS = {
|
||||
"None": [],
|
||||
"Core CRUD": ["/users", "/users/{id}", "/orders", "/orders/{id}"],
|
||||
"Search": ["/users/search", "/products/search", "/products/categories"],
|
||||
"Analytics": ["/analytics/events", "/analytics/metrics", "/analytics/reports"],
|
||||
"High Volume": ["/users", "/products", "/orders", "/analytics/events"],
|
||||
}
|
||||
|
||||
ROLLING_OPTIONS = {"Raw": 1, "7-day average": 7, "28-day average": 28}
|
||||
|
||||
|
||||
def generate_api_data(
|
||||
endpoints: list[str],
|
||||
start_date: date,
|
||||
end_date: date,
|
||||
) -> pd.DataFrame:
|
||||
"""Generate synthetic API usage data.
|
||||
|
||||
Replace this function with your actual data source.
|
||||
"""
|
||||
np.random.seed(42)
|
||||
|
||||
dates = pd.date_range(start=start_date, end=end_date, freq="D")
|
||||
records = []
|
||||
|
||||
for endpoint in endpoints:
|
||||
# Each endpoint has different base traffic and growth
|
||||
base = np.random.randint(1000, 50000)
|
||||
growth = np.random.uniform(0.0005, 0.003)
|
||||
|
||||
for i, dt in enumerate(dates):
|
||||
# Base trend with growth
|
||||
trend = base * (1 + growth) ** i
|
||||
|
||||
# Weekly seasonality (lower on weekends)
|
||||
if dt.dayofweek >= 5:
|
||||
trend *= 0.4
|
||||
|
||||
# Random noise
|
||||
value = trend * np.random.uniform(0.85, 1.15)
|
||||
|
||||
records.append({
|
||||
"date": dt,
|
||||
"endpoint": endpoint,
|
||||
"request_count": int(value),
|
||||
})
|
||||
|
||||
df = pd.DataFrame(records)
|
||||
return df
|
||||
|
||||
|
||||
@st.cache_data(ttl=3600)
|
||||
def load_api_data() -> pd.DataFrame:
|
||||
"""Load all API usage data."""
|
||||
end_date = date.today() - timedelta(days=1)
|
||||
start_date = end_date - timedelta(days=365)
|
||||
|
||||
all_endpoints = []
|
||||
for endpoints in API_CATEGORIES.values():
|
||||
all_endpoints.extend(endpoints)
|
||||
|
||||
return generate_api_data(all_endpoints, start_date, end_date)
|
||||
|
||||
|
||||
def apply_rolling_average(df: pd.DataFrame, window: int) -> pd.DataFrame:
|
||||
"""Apply rolling average to request data."""
|
||||
if window == 1:
|
||||
return df
|
||||
|
||||
result = df.copy()
|
||||
result["request_count"] = (
|
||||
result.groupby("endpoint")["request_count"]
|
||||
.transform(lambda x: x.rolling(window, min_periods=1).mean())
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def normalize_data(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Normalize request counts to percentages (share of total per day)."""
|
||||
result = df.copy()
|
||||
daily_totals = result.groupby("date")["request_count"].transform("sum")
|
||||
result["request_count"] = result["request_count"] / daily_totals
|
||||
return result
|
||||
|
||||
|
||||
def calculate_delta(df: pd.DataFrame, endpoint: str) -> tuple[float, float | None]:
|
||||
"""Calculate 28-day delta for an endpoint."""
|
||||
endpoint_data = df[df["endpoint"] == endpoint].sort_values("date")
|
||||
|
||||
if len(endpoint_data) < 2:
|
||||
return endpoint_data["request_count"].iloc[-1], None
|
||||
|
||||
latest = endpoint_data["request_count"].iloc[-1]
|
||||
|
||||
if len(endpoint_data) > 28:
|
||||
previous = endpoint_data["request_count"].iloc[-29]
|
||||
else:
|
||||
previous = endpoint_data["request_count"].iloc[0]
|
||||
|
||||
delta = latest - previous
|
||||
return latest, delta
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Page Layout
|
||||
# =============================================================================
|
||||
|
||||
# Load data
|
||||
raw_data = load_api_data()
|
||||
|
||||
# Header
|
||||
st.markdown("# API Usage :material/api:")
|
||||
st.caption("Select an API category to explore endpoint usage over time.")
|
||||
|
||||
# Category selection (not centered)
|
||||
category = st.segmented_control(
|
||||
"Select category",
|
||||
options=[
|
||||
":material/person: Users",
|
||||
":material/shopping_cart: Orders",
|
||||
":material/inventory_2: Products",
|
||||
":material/analytics: Analytics",
|
||||
],
|
||||
default=":material/person: Users",
|
||||
label_visibility="collapsed",
|
||||
)
|
||||
|
||||
if not category:
|
||||
st.warning("Please select a category above.", icon=":material/warning:")
|
||||
st.stop()
|
||||
|
||||
# Map display name to category key
|
||||
category_map = {
|
||||
":material/person: Users": "Users",
|
||||
":material/shopping_cart: Orders": "Orders",
|
||||
":material/inventory_2: Products": "Products",
|
||||
":material/analytics: Analytics": "Analytics",
|
||||
}
|
||||
selected_category = category_map[category]
|
||||
|
||||
st.subheader(f"{category} endpoints", divider="gray")
|
||||
|
||||
# Layout: filters on left, chart on right
|
||||
filter_col, chart_col = st.columns([1, 2])
|
||||
|
||||
with filter_col:
|
||||
# Metric selection
|
||||
with st.expander("Metric", expanded=True, icon=":material/analytics:"):
|
||||
measure = st.selectbox(
|
||||
"Choose a measure",
|
||||
["Request count", "Unique callers", "Error rate"],
|
||||
index=0,
|
||||
label_visibility="collapsed",
|
||||
disabled=True, # Only one option in this template
|
||||
help="In production, connect to different metrics tables",
|
||||
)
|
||||
|
||||
rolling_label = st.segmented_control(
|
||||
"Time aggregation",
|
||||
list(ROLLING_OPTIONS.keys()),
|
||||
default="7-day average",
|
||||
label_visibility="collapsed",
|
||||
)
|
||||
|
||||
if rolling_label is None:
|
||||
st.caption("Please select a time aggregation.")
|
||||
st.stop()
|
||||
|
||||
rolling_window = ROLLING_OPTIONS[rolling_label]
|
||||
|
||||
normalize = st.toggle(
|
||||
"Normalize",
|
||||
value=False,
|
||||
help="Normalize to show percentage share of total requests",
|
||||
)
|
||||
|
||||
# Starter kits
|
||||
with st.expander("Starter kits", expanded=True, icon=":material/auto_awesome:"):
|
||||
starter_kit = st.pills(
|
||||
"Quick select",
|
||||
options=list(STARTER_KITS.keys()),
|
||||
default="None",
|
||||
label_visibility="collapsed",
|
||||
)
|
||||
|
||||
# Endpoint selection
|
||||
available_endpoints = API_CATEGORIES[selected_category]
|
||||
|
||||
# Determine default selection based on starter kit
|
||||
if starter_kit and starter_kit != "None":
|
||||
default_endpoints = [e for e in STARTER_KITS[starter_kit] if e in available_endpoints]
|
||||
else:
|
||||
default_endpoints = available_endpoints[:4] # First 4 endpoints
|
||||
|
||||
with st.expander("Endpoints", expanded=True, icon=":material/checklist:"):
|
||||
selected_endpoints = st.multiselect(
|
||||
"Select endpoints",
|
||||
options=available_endpoints,
|
||||
default=default_endpoints,
|
||||
label_visibility="collapsed",
|
||||
)
|
||||
|
||||
# Filter and process data
|
||||
if not selected_endpoints:
|
||||
with chart_col:
|
||||
st.info("Select at least one endpoint to view usage data.", icon=":material/info:")
|
||||
st.stop()
|
||||
|
||||
filtered_data = raw_data[raw_data["endpoint"].isin(selected_endpoints)].copy()
|
||||
filtered_data = apply_rolling_average(filtered_data, rolling_window)
|
||||
|
||||
if normalize:
|
||||
filtered_data = normalize_data(filtered_data)
|
||||
|
||||
with chart_col:
|
||||
# Latest metrics
|
||||
with st.expander("Latest numbers", expanded=True, icon=":material/numbers:"):
|
||||
metrics_row = st.container(horizontal=True)
|
||||
|
||||
for endpoint in selected_endpoints:
|
||||
latest, delta = calculate_delta(filtered_data, endpoint)
|
||||
|
||||
if normalize:
|
||||
value_str = f"{latest:.2%}"
|
||||
delta_str = f"{delta:+.2%}" if delta is not None else None
|
||||
else:
|
||||
value_str = f"{latest:,.0f}"
|
||||
delta_str = f"{delta:+,.0f}" if delta is not None else None
|
||||
|
||||
metrics_row.metric(
|
||||
label=endpoint,
|
||||
value=value_str,
|
||||
delta=delta_str,
|
||||
border=True,
|
||||
)
|
||||
|
||||
# Time series chart
|
||||
with st.expander("Time series", expanded=True, icon=":material/show_chart:"):
|
||||
y_format = ".1%" if normalize else ",.0f"
|
||||
y_title = "Share of requests" if normalize else "Request count"
|
||||
|
||||
chart = (
|
||||
alt.Chart(filtered_data)
|
||||
.mark_line()
|
||||
.encode(
|
||||
x=alt.X("date:T", title="Date"),
|
||||
y=alt.Y("request_count:Q", title=y_title, axis=alt.Axis(format=y_format)),
|
||||
color=alt.Color("endpoint:N", title="Endpoint", legend=alt.Legend(orient="bottom")),
|
||||
tooltip=[
|
||||
alt.Tooltip("date:T", title="Date", format="%Y-%m-%d"),
|
||||
alt.Tooltip("endpoint:N", title="Endpoint"),
|
||||
alt.Tooltip("request_count:Q", title="Requests", format=y_format),
|
||||
],
|
||||
)
|
||||
.properties(height=450)
|
||||
.interactive()
|
||||
)
|
||||
|
||||
st.altair_chart(chart)
|
||||
|
||||
# Raw data section
|
||||
with st.expander("Raw data", expanded=False, icon=":material/table:"):
|
||||
display_df = filtered_data.copy()
|
||||
if normalize:
|
||||
display_df["request_count"] = display_df["request_count"].apply(lambda x: f"{x:.2%}")
|
||||
st.dataframe(display_df, hide_index=True)
|
||||
Reference in New Issue
Block a user