866 lines
32 KiB
Python
Executable File
866 lines
32 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Trending Data Generator Script
|
|
Fetches download data from Supabase and generates trending-data.json for the Claude Code Templates project.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import requests
|
|
import time
|
|
from datetime import datetime, timedelta, timezone
|
|
from collections import defaultdict, Counter
|
|
from dotenv import load_dotenv
|
|
|
|
# Load environment variables
|
|
load_dotenv()
|
|
|
|
def fetch_with_retry(url, headers, max_retries=5, timeout=60):
|
|
"""
|
|
Fetch data from API with retry logic and exponential backoff.
|
|
Handles 500, 503, timeouts, and connection errors with aggressive retries.
|
|
|
|
Args:
|
|
url: The URL to fetch
|
|
headers: Request headers
|
|
max_retries: Maximum number of retry attempts
|
|
timeout: Request timeout in seconds
|
|
|
|
Returns:
|
|
Response object or None if all retries failed
|
|
"""
|
|
retryable_statuses = {500, 502, 503, 504}
|
|
|
|
for attempt in range(max_retries):
|
|
try:
|
|
response = requests.get(url, headers=headers, timeout=timeout)
|
|
|
|
# Return successful responses
|
|
if response.status_code in [200, 206]:
|
|
return response
|
|
|
|
# Retry on server errors with exponential backoff
|
|
if response.status_code in retryable_statuses:
|
|
if attempt < max_retries - 1:
|
|
wait_time = (2 ** attempt) * 3 # 3s, 6s, 12s, 24s
|
|
print(f"⏳ Server error {response.status_code} on attempt {attempt + 1}/{max_retries}. Retrying in {wait_time}s...")
|
|
time.sleep(wait_time)
|
|
continue
|
|
else:
|
|
print(f"⚠️ Server error {response.status_code} after {max_retries} attempts")
|
|
return None
|
|
|
|
# For non-retryable errors, return immediately
|
|
print(f"⚠️ API returned status {response.status_code}: {response.text[:200]}")
|
|
return None
|
|
|
|
except requests.exceptions.Timeout:
|
|
if attempt < max_retries - 1:
|
|
wait_time = (2 ** attempt) * 3
|
|
print(f"⏳ Request timeout on attempt {attempt + 1}/{max_retries}. Retrying in {wait_time}s...")
|
|
time.sleep(wait_time)
|
|
continue
|
|
else:
|
|
print(f"❌ Request timed out after {max_retries} attempts")
|
|
return None
|
|
|
|
except requests.exceptions.ConnectionError:
|
|
if attempt < max_retries - 1:
|
|
wait_time = (2 ** attempt) * 3
|
|
print(f"⏳ Connection error on attempt {attempt + 1}/{max_retries}. Retrying in {wait_time}s...")
|
|
time.sleep(wait_time)
|
|
continue
|
|
else:
|
|
print(f"❌ Connection failed after {max_retries} attempts")
|
|
return None
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
print(f"❌ Request error: {str(e)}")
|
|
return None
|
|
|
|
return None
|
|
|
|
def main():
|
|
"""Main function to generate trending data"""
|
|
print("🚀 Generating trending data from Supabase...")
|
|
|
|
# Get Supabase credentials
|
|
supabase_url = os.getenv("SUPABASE_URL")
|
|
supabase_api_key = os.getenv("SUPABASE_API_KEY")
|
|
|
|
if not supabase_url or not supabase_api_key:
|
|
print("❌ Error: Missing Supabase credentials in .env file")
|
|
return
|
|
|
|
try:
|
|
# Fetch all component downloads using REST API
|
|
print("📊 Fetching download data from Supabase...")
|
|
|
|
headers = {
|
|
'apikey': supabase_api_key,
|
|
'Authorization': f'Bearer {supabase_api_key}',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
|
|
# Get total count first
|
|
count_url = f"{supabase_url}/rest/v1/component_downloads"
|
|
count_headers = {**headers, 'Prefer': 'count=exact'}
|
|
count_response = requests.head(count_url, headers=count_headers)
|
|
|
|
total_count = 0
|
|
if 'content-range' in count_response.headers:
|
|
total_count = int(count_response.headers['content-range'].split('/')[-1])
|
|
|
|
print(f"📊 Total records in database: {total_count}")
|
|
|
|
# Fetch ALL data using cursor-based pagination
|
|
all_downloads = []
|
|
page_size = 1000
|
|
last_id = 0
|
|
page_num = 0
|
|
consecutive_errors = 0
|
|
max_consecutive_errors = 3
|
|
|
|
print("📊 Using cursor-based pagination to fetch all records...")
|
|
|
|
while True:
|
|
page_num += 1
|
|
|
|
api_url = f"{supabase_url}/rest/v1/component_downloads?id=gt.{last_id}&order=id.asc&limit={page_size}"
|
|
|
|
response = fetch_with_retry(api_url, headers, max_retries=5, timeout=60)
|
|
|
|
if response is None:
|
|
consecutive_errors += 1
|
|
if consecutive_errors >= max_consecutive_errors:
|
|
print(f"⚠️ {max_consecutive_errors} consecutive failures. Stopping at {len(all_downloads):,} records.")
|
|
break
|
|
# Skip ahead by estimating next ID range to recover from persistent errors
|
|
last_id += page_size
|
|
print(f"⚠️ Skipping ahead to id > {last_id} (attempt {consecutive_errors}/{max_consecutive_errors})")
|
|
time.sleep(5)
|
|
continue
|
|
|
|
page_data = response.json()
|
|
if not page_data:
|
|
print(f"✅ Reached end of data at page {page_num}")
|
|
break
|
|
|
|
consecutive_errors = 0 # Reset on success
|
|
all_downloads.extend(page_data)
|
|
last_id = page_data[-1]['id']
|
|
|
|
# Progress indicator every 50 pages
|
|
if page_num % 50 == 0:
|
|
pct = (len(all_downloads) / total_count * 100) if total_count > 0 else 0
|
|
print(f"📄 Page {page_num}: {len(all_downloads):,}/{total_count:,} records ({pct:.1f}%)")
|
|
|
|
if len(page_data) < page_size:
|
|
print(f"✅ Fetched final page {page_num} with {len(page_data)} records")
|
|
break
|
|
|
|
if len(all_downloads) >= 10000000:
|
|
print(f"⚠️ Reached safety limit of 10,000,000 records")
|
|
break
|
|
|
|
if not all_downloads:
|
|
print("❌ No data fetched from Supabase")
|
|
print("📝 Generating fallback trending data...")
|
|
trending_data = generate_fallback_trending_data()
|
|
else:
|
|
print(f"\n✅ Successfully fetched {len(all_downloads):,} total records from Supabase")
|
|
print(f"📊 Processing download data to generate trending statistics...")
|
|
# Process the real data
|
|
trending_data = process_downloads_data(all_downloads)
|
|
|
|
# Write to JSON file
|
|
output_file = "docs/trending-data.json"
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|
json.dump(trending_data, f, indent=2, ensure_ascii=False)
|
|
|
|
print(f"✅ Successfully generated {output_file}")
|
|
print(f"📊 Statistics:")
|
|
for component_type, items in trending_data['trending'].items():
|
|
print(f" • {component_type}: {len(items)} items")
|
|
|
|
except Exception as e:
|
|
print(f"❌ Error: {str(e)}")
|
|
print("📝 Generating fallback trending data...")
|
|
trending_data = generate_fallback_trending_data()
|
|
|
|
# Write fallback data
|
|
output_file = "docs/trending-data.json"
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|
json.dump(trending_data, f, indent=2, ensure_ascii=False)
|
|
|
|
print(f"✅ Generated fallback {output_file}")
|
|
|
|
def process_downloads_data(downloads):
|
|
"""Process raw download data and generate trending structure"""
|
|
|
|
# Components to exclude from trending (test/internal components)
|
|
EXCLUDED_COMPONENTS = {
|
|
'test-command', 'test-agent', 'test-setting', 'test-hook',
|
|
'test-mcp', 'test-skill', 'test-template', 'test-from-production',
|
|
'test-component', 'test', 'demo-component', 'example-component'
|
|
}
|
|
|
|
# Calculate date ranges with timezone awareness
|
|
now = datetime.now(timezone.utc)
|
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
week_start = today_start - timedelta(days=7)
|
|
month_start = today_start - timedelta(days=30)
|
|
|
|
# For charting - collect daily data for the last 30 days
|
|
chart_data = defaultdict(lambda: defaultdict(int)) # {date: {category: count}}
|
|
|
|
# Group downloads by component
|
|
component_stats = defaultdict(lambda: {
|
|
'total': 0,
|
|
'today': 0,
|
|
'week': 0,
|
|
'month': 0,
|
|
'component_type': '',
|
|
'category': '',
|
|
'name': ''
|
|
})
|
|
|
|
# Track unique countries
|
|
unique_countries = set()
|
|
# Track downloads by country
|
|
country_downloads = Counter()
|
|
|
|
# Debug: Show sample of downloads and date ranges
|
|
print(f"🔍 Processing {len(downloads)} downloads...")
|
|
print(f"📄 Sample download record: {downloads[0] if downloads else 'None'}")
|
|
print(f"📅 Date ranges being used:")
|
|
print(f" • now: {now}")
|
|
print(f" • today_start: {today_start}")
|
|
print(f" • week_start: {week_start}")
|
|
print(f" • month_start: {month_start}")
|
|
|
|
# Debug: Track downloads by period
|
|
today_count = 0
|
|
week_count = 0
|
|
month_count = 0
|
|
|
|
for download in downloads:
|
|
# Parse download timestamp with proper timezone handling
|
|
timestamp_str = download['download_timestamp']
|
|
if timestamp_str.endswith('Z'):
|
|
timestamp_str = timestamp_str.replace('Z', '+00:00')
|
|
elif '+' not in timestamp_str and '-' not in timestamp_str[-6:]:
|
|
# No timezone info, assume UTC
|
|
timestamp_str = timestamp_str + '+00:00'
|
|
|
|
try:
|
|
download_time = datetime.fromisoformat(timestamp_str)
|
|
# Convert to UTC if not already
|
|
if download_time.tzinfo is None:
|
|
download_time = download_time.replace(tzinfo=timezone.utc)
|
|
except:
|
|
# Fallback to current time if parsing fails
|
|
download_time = datetime.now(timezone.utc)
|
|
|
|
# Create key that matches generate_components_json.py structure
|
|
# The key should match format: component_type/category/name
|
|
category = download.get('category', 'general')
|
|
component_name = download['component_name']
|
|
component_type = download['component_type']
|
|
|
|
# Handle case where component_name already includes category (like "frontend/react-expert")
|
|
if '/' in component_name:
|
|
category = component_name.split('/')[0]
|
|
actual_name = component_name.split('/')[-1]
|
|
else:
|
|
actual_name = component_name
|
|
|
|
# Skip test/internal components
|
|
if actual_name.lower() in EXCLUDED_COMPONENTS:
|
|
continue
|
|
|
|
component_key = f"{component_type}-{actual_name}"
|
|
stats = component_stats[component_key]
|
|
|
|
# Set component info
|
|
stats['name'] = actual_name
|
|
stats['component_type'] = component_type
|
|
stats['category'] = category
|
|
|
|
# Count downloads by time period
|
|
stats['total'] += 1
|
|
|
|
# Track unique countries
|
|
country = download.get('country', 'Unknown')
|
|
if country and country != 'Unknown':
|
|
unique_countries.add(country)
|
|
country_downloads[country] += 1
|
|
|
|
if download_time >= today_start:
|
|
stats['today'] += 1
|
|
today_count += 1
|
|
|
|
if download_time >= week_start:
|
|
stats['week'] += 1
|
|
week_count += 1
|
|
|
|
if download_time >= month_start:
|
|
stats['month'] += 1
|
|
month_count += 1
|
|
|
|
# Collect daily data for chart (last 30 days only)
|
|
if download_time >= month_start:
|
|
download_date = download_time.strftime('%Y-%m-%d')
|
|
|
|
# Map component types to plural for consistency
|
|
type_mapping = {
|
|
'command': 'commands',
|
|
'agent': 'agents',
|
|
'setting': 'settings',
|
|
'hook': 'hooks',
|
|
'mcp': 'mcps',
|
|
'skill': 'skills',
|
|
'template': 'templates',
|
|
'plugin': 'plugins',
|
|
'sandbox': 'sandbox'
|
|
}
|
|
mapped_type = type_mapping.get(component_type, component_type + 's')
|
|
chart_data[download_date][mapped_type] += 1
|
|
|
|
# Debug: Print total counts by period
|
|
print(f"📊 Total downloads by period:")
|
|
print(f" • Today: {today_count}")
|
|
print(f" • Week: {week_count}")
|
|
print(f" • Month: {month_count}")
|
|
print(f" • Total processed: {len(downloads)}")
|
|
|
|
# Debug: Show oldest and newest records
|
|
if downloads:
|
|
print(f"📅 Date range in data:")
|
|
print(f" • Newest: {downloads[0]['download_timestamp']}")
|
|
print(f" • Oldest: {downloads[-1]['download_timestamp']}")
|
|
|
|
# Group by component type and create trending structure
|
|
trending_by_type = defaultdict(list)
|
|
|
|
for component_key, stats in component_stats.items():
|
|
component_type = stats['component_type']
|
|
|
|
trending_item = {
|
|
'id': component_key.lower().replace(' ', '-'),
|
|
'name': stats['name'],
|
|
'category': stats['category'],
|
|
'downloadsToday': stats['today'],
|
|
'downloadsWeek': stats['week'],
|
|
'downloadsMonth': stats['month'],
|
|
'downloadsTotal': stats['total']
|
|
}
|
|
|
|
trending_by_type[component_type].append(trending_item)
|
|
|
|
# Sort each type by weekly downloads (most trending)
|
|
for component_type in trending_by_type:
|
|
trending_by_type[component_type].sort(
|
|
key=lambda x: x['downloadsWeek'],
|
|
reverse=True
|
|
)
|
|
# Keep top 10 for each type
|
|
trending_by_type[component_type] = trending_by_type[component_type][:10]
|
|
|
|
# Process chart data for cumulative growth
|
|
chart_dates = []
|
|
chart_categories = ['commands', 'agents', 'settings', 'hooks', 'mcps', 'skills', 'templates']
|
|
chart_series = {category: [] for category in chart_categories}
|
|
|
|
# Generate the last 30 days
|
|
for i in range(29, -1, -1): # 29 days ago to today
|
|
date = (today_start - timedelta(days=i)).strftime('%Y-%m-%d')
|
|
chart_dates.append(date)
|
|
|
|
# Calculate cumulative data for each category
|
|
for category in chart_categories:
|
|
cumulative = 0
|
|
for date in chart_dates:
|
|
daily_count = chart_data.get(date, {}).get(category, 0)
|
|
cumulative += daily_count
|
|
chart_series[category].append(cumulative)
|
|
|
|
# Calculate global statistics from ALL components (before limiting to top 10)
|
|
total_components = len(component_stats)
|
|
total_all_downloads = sum(stats['total'] for stats in component_stats.values())
|
|
total_month_downloads = sum(stats['month'] for stats in component_stats.values())
|
|
total_week_downloads = sum(stats['week'] for stats in component_stats.values())
|
|
total_today_downloads = sum(stats['today'] for stats in component_stats.values())
|
|
|
|
print(f"📊 Global Statistics:")
|
|
print(f" • Total Components: {total_components}")
|
|
print(f" • Total Downloads: {total_all_downloads:,}")
|
|
print(f" • Monthly Downloads: {total_month_downloads:,}")
|
|
print(f" • Weekly Downloads: {total_week_downloads:,}")
|
|
print(f" • Today Downloads: {total_today_downloads:,}")
|
|
print(f" • Unique Countries: {len(unique_countries)}")
|
|
|
|
# Get top 5 countries by downloads
|
|
top_countries = country_downloads.most_common(5)
|
|
|
|
# Country code to name and flag mapping
|
|
country_info = {
|
|
'US': {'name': 'United States', 'flag': '🇺🇸'},
|
|
'GB': {'name': 'United Kingdom', 'flag': '🇬🇧'},
|
|
'IN': {'name': 'India', 'flag': '🇮🇳'},
|
|
'DE': {'name': 'Germany', 'flag': '🇩🇪'},
|
|
'CA': {'name': 'Canada', 'flag': '🇨🇦'},
|
|
'FR': {'name': 'France', 'flag': '🇫🇷'},
|
|
'AU': {'name': 'Australia', 'flag': '🇦🇺'},
|
|
'JP': {'name': 'Japan', 'flag': '🇯🇵'},
|
|
'BR': {'name': 'Brazil', 'flag': '🇧🇷'},
|
|
'ES': {'name': 'Spain', 'flag': '🇪🇸'},
|
|
'IT': {'name': 'Italy', 'flag': '🇮🇹'},
|
|
'NL': {'name': 'Netherlands', 'flag': '🇳🇱'},
|
|
'SE': {'name': 'Sweden', 'flag': '🇸🇪'},
|
|
'CH': {'name': 'Switzerland', 'flag': '🇨🇭'},
|
|
'PL': {'name': 'Poland', 'flag': '🇵🇱'},
|
|
'MX': {'name': 'Mexico', 'flag': '🇲🇽'},
|
|
'CN': {'name': 'China', 'flag': '🇨🇳'},
|
|
'KR': {'name': 'South Korea', 'flag': '🇰🇷'},
|
|
'SG': {'name': 'Singapore', 'flag': '🇸🇬'},
|
|
'IE': {'name': 'Ireland', 'flag': '🇮🇪'},
|
|
'NO': {'name': 'Norway', 'flag': '🇳🇴'},
|
|
'FI': {'name': 'Finland', 'flag': '🇫🇮'},
|
|
'DK': {'name': 'Denmark', 'flag': '🇩🇰'},
|
|
'BE': {'name': 'Belgium', 'flag': '🇧🇪'},
|
|
'AT': {'name': 'Austria', 'flag': '🇦🇹'},
|
|
'NZ': {'name': 'New Zealand', 'flag': '🇳🇿'},
|
|
'PT': {'name': 'Portugal', 'flag': '🇵🇹'},
|
|
'IL': {'name': 'Israel', 'flag': '🇮🇱'},
|
|
'AR': {'name': 'Argentina', 'flag': '🇦🇷'},
|
|
'CO': {'name': 'Colombia', 'flag': '🇨🇴'},
|
|
'CL': {'name': 'Chile', 'flag': '🇨🇱'},
|
|
'ZA': {'name': 'South Africa', 'flag': '🇿🇦'},
|
|
'RU': {'name': 'Russia', 'flag': '🇷🇺'},
|
|
'TR': {'name': 'Turkey', 'flag': '🇹🇷'},
|
|
'TH': {'name': 'Thailand', 'flag': '🇹🇭'},
|
|
'MY': {'name': 'Malaysia', 'flag': '🇲🇾'},
|
|
'ID': {'name': 'Indonesia', 'flag': '🇮🇩'},
|
|
'PH': {'name': 'Philippines', 'flag': '🇵🇭'},
|
|
'VN': {'name': 'Vietnam', 'flag': '🇻🇳'},
|
|
'PK': {'name': 'Pakistan', 'flag': '🇵🇰'},
|
|
'BD': {'name': 'Bangladesh', 'flag': '🇧🇩'},
|
|
'UA': {'name': 'Ukraine', 'flag': '🇺🇦'},
|
|
'RO': {'name': 'Romania', 'flag': '🇷🇴'},
|
|
'CZ': {'name': 'Czech Republic', 'flag': '🇨🇿'},
|
|
'GR': {'name': 'Greece', 'flag': '🇬🇷'},
|
|
'HU': {'name': 'Hungary', 'flag': '🇭🇺'}
|
|
}
|
|
|
|
# Format top countries data
|
|
top_countries_data = []
|
|
for country_code, downloads in top_countries:
|
|
country_data = country_info.get(country_code, {'name': country_code, 'flag': '🌍'})
|
|
percentage = (downloads / total_all_downloads * 100) if total_all_downloads > 0 else 0
|
|
|
|
top_countries_data.append({
|
|
'code': country_code,
|
|
'name': country_data['name'],
|
|
'flag': country_data['flag'],
|
|
'downloads': downloads,
|
|
'percentage': round(percentage, 1)
|
|
})
|
|
|
|
print(f"🌍 Top 5 Countries:")
|
|
for country in top_countries_data:
|
|
print(f" • {country['flag']} {country['name']}: {country['downloads']:,} ({country['percentage']}%)")
|
|
|
|
# Create final structure
|
|
trending_data = {
|
|
"lastUpdated": now.isoformat() + "Z",
|
|
"globalStats": {
|
|
"totalComponents": total_components,
|
|
"totalDownloads": total_all_downloads,
|
|
"monthlyDownloads": total_month_downloads,
|
|
"weeklyDownloads": total_week_downloads,
|
|
"todayDownloads": total_today_downloads,
|
|
"totalCountries": len(unique_countries)
|
|
},
|
|
"topCountries": top_countries_data,
|
|
"trending": {},
|
|
"chartData": {
|
|
"dates": chart_dates,
|
|
"series": chart_series
|
|
}
|
|
}
|
|
|
|
# Map component types to expected names
|
|
type_mapping = {
|
|
'command': 'commands',
|
|
'commands': 'commands',
|
|
'agent': 'agents',
|
|
'agents': 'agents',
|
|
'setting': 'settings',
|
|
'settings': 'settings',
|
|
'hook': 'hooks',
|
|
'hooks': 'hooks',
|
|
'mcp': 'mcps',
|
|
'mcps': 'mcps',
|
|
'skill': 'skills',
|
|
'skills': 'skills',
|
|
'template': 'templates',
|
|
'templates': 'templates',
|
|
'plugin': 'plugins',
|
|
'plugins': 'plugins',
|
|
'sandbox': 'sandbox'
|
|
}
|
|
|
|
# Debug: Print what component types we found
|
|
print(f"🔍 Component types found in data: {list(trending_by_type.keys())}")
|
|
|
|
# Populate trending data with real data or fallback
|
|
processed_types = set()
|
|
for db_type, json_type in type_mapping.items():
|
|
if json_type not in processed_types and db_type in trending_by_type:
|
|
trending_data['trending'][json_type] = trending_by_type[db_type]
|
|
processed_types.add(json_type)
|
|
print(f"✅ Using real data for {json_type}: {len(trending_by_type[db_type])} items")
|
|
|
|
# Add fallback data only for types that don't have real data
|
|
for json_type in ['commands', 'agents', 'settings', 'hooks', 'mcps', 'skills', 'templates']:
|
|
if json_type not in trending_data['trending']:
|
|
trending_data['trending'][json_type] = create_fallback_data(json_type)
|
|
print(f"⚠️ Using fallback data for {json_type}")
|
|
|
|
# Add "all" category with top 10 across all categories
|
|
all_items = []
|
|
for items in trending_by_type.values():
|
|
all_items.extend(items)
|
|
|
|
# Sort all items by weekly downloads and take top 10
|
|
all_items.sort(key=lambda x: x['downloadsWeek'], reverse=True)
|
|
trending_data['trending']['all'] = all_items[:10]
|
|
|
|
return trending_data
|
|
|
|
def create_fallback_data(component_type):
|
|
"""Create fallback data for component types with no real data"""
|
|
|
|
fallback_data = {
|
|
'commands': [
|
|
{
|
|
'id': 'react-component-generator',
|
|
'name': 'React Component Generator',
|
|
'category': 'frontend',
|
|
'downloadsToday': 45,
|
|
'downloadsWeek': 234,
|
|
'downloadsMonth': 567,
|
|
'downloadsTotal': 1248
|
|
},
|
|
{
|
|
'id': 'api-endpoint-generator',
|
|
'name': 'API Endpoint Generator',
|
|
'category': 'backend',
|
|
'downloadsToday': 32,
|
|
'downloadsWeek': 189,
|
|
'downloadsMonth': 445,
|
|
'downloadsTotal': 967
|
|
}
|
|
],
|
|
'agents': [
|
|
{
|
|
'id': 'react-expert',
|
|
'name': 'React Performance Expert',
|
|
'category': 'frontend',
|
|
'downloadsToday': 28,
|
|
'downloadsWeek': 156,
|
|
'downloadsMonth': 389,
|
|
'downloadsTotal': 834
|
|
},
|
|
{
|
|
'id': 'security-analyst',
|
|
'name': 'Security Code Analyst',
|
|
'category': 'security',
|
|
'downloadsToday': 35,
|
|
'downloadsWeek': 178,
|
|
'downloadsMonth': 423,
|
|
'downloadsTotal': 912
|
|
}
|
|
],
|
|
'settings': [
|
|
{
|
|
'id': 'vscode-theme',
|
|
'name': 'Optimized VSCode Settings',
|
|
'category': 'editor',
|
|
'downloadsToday': 67,
|
|
'downloadsWeek': 345,
|
|
'downloadsMonth': 892,
|
|
'downloadsTotal': 1876
|
|
}
|
|
],
|
|
'hooks': [
|
|
{
|
|
'id': 'pre-commit-tests',
|
|
'name': 'Pre-commit Test Runner',
|
|
'category': 'testing',
|
|
'downloadsToday': 23,
|
|
'downloadsWeek': 123,
|
|
'downloadsMonth': 298,
|
|
'downloadsTotal': 645
|
|
}
|
|
],
|
|
'mcps': [
|
|
{
|
|
'id': 'github-integration',
|
|
'name': 'GitHub API Integration',
|
|
'category': 'git',
|
|
'downloadsToday': 41,
|
|
'downloadsWeek': 198,
|
|
'downloadsMonth': 456,
|
|
'downloadsTotal': 1023
|
|
}
|
|
],
|
|
'templates': [
|
|
{
|
|
'id': 'nextjs-starter',
|
|
'name': 'Next.js Starter Template',
|
|
'category': 'frontend',
|
|
'downloadsToday': 52,
|
|
'downloadsWeek': 267,
|
|
'downloadsMonth': 634,
|
|
'downloadsTotal': 1387
|
|
}
|
|
],
|
|
'skills': [
|
|
{
|
|
'id': 'data-visualization',
|
|
'name': 'Data Visualization Expert',
|
|
'category': 'data-science',
|
|
'downloadsToday': 38,
|
|
'downloadsWeek': 201,
|
|
'downloadsMonth': 512,
|
|
'downloadsTotal': 1129
|
|
},
|
|
{
|
|
'id': 'api-documentation',
|
|
'name': 'API Documentation Generator',
|
|
'category': 'documentation',
|
|
'downloadsToday': 29,
|
|
'downloadsWeek': 167,
|
|
'downloadsMonth': 423,
|
|
'downloadsTotal': 934
|
|
}
|
|
]
|
|
}
|
|
|
|
return fallback_data.get(component_type, [])
|
|
|
|
def generate_fallback_trending_data():
|
|
"""Generate complete fallback trending data structure"""
|
|
return {
|
|
"lastUpdated": datetime.now().isoformat() + "Z",
|
|
"trending": {
|
|
"commands": [
|
|
{
|
|
'id': 'react-component-generator',
|
|
'name': 'React Component Generator',
|
|
'category': 'frontend',
|
|
'downloadsToday': 127,
|
|
'downloadsWeek': 892,
|
|
'downloadsMonth': 2847,
|
|
'downloadsTotal': 5634
|
|
},
|
|
{
|
|
'id': 'api-endpoint-generator',
|
|
'name': 'API Endpoint Generator',
|
|
'category': 'backend',
|
|
'downloadsToday': 74,
|
|
'downloadsWeek': 389,
|
|
'downloadsMonth': 1089,
|
|
'downloadsTotal': 2834
|
|
},
|
|
{
|
|
'id': 'database-migration-system',
|
|
'name': 'Database Migration System',
|
|
'category': 'database',
|
|
'downloadsToday': 45,
|
|
'downloadsWeek': 234,
|
|
'downloadsMonth': 567,
|
|
'downloadsTotal': 1432
|
|
},
|
|
{
|
|
'id': 'docker-setup-wizard',
|
|
'name': 'Docker Setup Wizard',
|
|
'category': 'devops',
|
|
'downloadsToday': 89,
|
|
'downloadsWeek': 445,
|
|
'downloadsMonth': 1234,
|
|
'downloadsTotal': 2967
|
|
},
|
|
{
|
|
'id': 'unit-test-generator',
|
|
'name': 'Unit Test Generator',
|
|
'category': 'testing',
|
|
'downloadsToday': 56,
|
|
'downloadsWeek': 298,
|
|
'downloadsMonth': 789,
|
|
'downloadsTotal': 1876
|
|
}
|
|
],
|
|
"agents": [
|
|
{
|
|
'id': 'react-expert',
|
|
'name': 'React Performance Expert',
|
|
'category': 'frontend',
|
|
'downloadsToday': 98,
|
|
'downloadsWeek': 567,
|
|
'downloadsMonth': 1456,
|
|
'downloadsTotal': 3245
|
|
},
|
|
{
|
|
'id': 'security-analyst',
|
|
'name': 'Security Code Analyst',
|
|
'category': 'security',
|
|
'downloadsToday': 112,
|
|
'downloadsWeek': 634,
|
|
'downloadsMonth': 1789,
|
|
'downloadsTotal': 4123
|
|
},
|
|
{
|
|
'id': 'api-architect',
|
|
'name': 'API Architecture Specialist',
|
|
'category': 'backend',
|
|
'downloadsToday': 67,
|
|
'downloadsWeek': 345,
|
|
'downloadsMonth': 923,
|
|
'downloadsTotal': 2456
|
|
},
|
|
{
|
|
'id': 'database-optimizer',
|
|
'name': 'Database Performance Optimizer',
|
|
'category': 'database',
|
|
'downloadsToday': 43,
|
|
'downloadsWeek': 198,
|
|
'downloadsMonth': 534,
|
|
'downloadsTotal': 1234
|
|
}
|
|
],
|
|
"settings": [
|
|
{
|
|
'id': 'vscode-theme',
|
|
'name': 'Optimized VSCode Settings',
|
|
'category': 'editor',
|
|
'downloadsToday': 234,
|
|
'downloadsWeek': 1234,
|
|
'downloadsMonth': 3456,
|
|
'downloadsTotal': 7891
|
|
},
|
|
{
|
|
'id': 'eslint-config',
|
|
'name': 'Strict ESLint Configuration',
|
|
'category': 'linting',
|
|
'downloadsToday': 156,
|
|
'downloadsWeek': 789,
|
|
'downloadsMonth': 2134,
|
|
'downloadsTotal': 4567
|
|
},
|
|
{
|
|
'id': 'prettier-setup',
|
|
'name': 'Team Prettier Standards',
|
|
'category': 'formatting',
|
|
'downloadsToday': 134,
|
|
'downloadsWeek': 678,
|
|
'downloadsMonth': 1876,
|
|
'downloadsTotal': 3892
|
|
}
|
|
],
|
|
"hooks": [
|
|
{
|
|
'id': 'pre-commit-tests',
|
|
'name': 'Pre-commit Test Runner',
|
|
'category': 'testing',
|
|
'downloadsToday': 87,
|
|
'downloadsWeek': 456,
|
|
'downloadsMonth': 1123,
|
|
'downloadsTotal': 2789
|
|
},
|
|
{
|
|
'id': 'code-formatter',
|
|
'name': 'Auto Code Formatter',
|
|
'category': 'formatting',
|
|
'downloadsToday': 76,
|
|
'downloadsWeek': 389,
|
|
'downloadsMonth': 934,
|
|
'downloadsTotal': 2134
|
|
}
|
|
],
|
|
"mcps": [
|
|
{
|
|
'id': 'github-integration',
|
|
'name': 'GitHub API Integration',
|
|
'category': 'git',
|
|
'downloadsToday': 145,
|
|
'downloadsWeek': 723,
|
|
'downloadsMonth': 1987,
|
|
'downloadsTotal': 4321
|
|
},
|
|
{
|
|
'id': 'slack-notifications',
|
|
'name': 'Slack Notification System',
|
|
'category': 'communication',
|
|
'downloadsToday': 98,
|
|
'downloadsWeek': 456,
|
|
'downloadsMonth': 1234,
|
|
'downloadsTotal': 2987
|
|
}
|
|
],
|
|
"templates": [
|
|
{
|
|
'id': 'nextjs-starter',
|
|
'name': 'Next.js Starter Template',
|
|
'category': 'frontend',
|
|
'downloadsToday': 189,
|
|
'downloadsWeek': 945,
|
|
'downloadsMonth': 2567,
|
|
'downloadsTotal': 5432
|
|
},
|
|
{
|
|
'id': 'express-api',
|
|
'name': 'Express API Template',
|
|
'category': 'backend',
|
|
'downloadsToday': 123,
|
|
'downloadsWeek': 612,
|
|
'downloadsMonth': 1678,
|
|
'downloadsTotal': 3456
|
|
}
|
|
],
|
|
"skills": [
|
|
{
|
|
'id': 'data-visualization',
|
|
'name': 'Data Visualization Expert',
|
|
'category': 'data-science',
|
|
'downloadsToday': 87,
|
|
'downloadsWeek': 478,
|
|
'downloadsMonth': 1289,
|
|
'downloadsTotal': 2834
|
|
},
|
|
{
|
|
'id': 'api-documentation',
|
|
'name': 'API Documentation Generator',
|
|
'category': 'documentation',
|
|
'downloadsToday': 64,
|
|
'downloadsWeek': 356,
|
|
'downloadsMonth': 923,
|
|
'downloadsTotal': 2145
|
|
},
|
|
{
|
|
'id': 'code-review',
|
|
'name': 'Intelligent Code Reviewer',
|
|
'category': 'quality-assurance',
|
|
'downloadsToday': 101,
|
|
'downloadsWeek': 534,
|
|
'downloadsMonth': 1456,
|
|
'downloadsTotal': 3127
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
if __name__ == "__main__":
|
|
main() |