mirror of
https://github.com/davila7/claude-code-templates.git
synced 2026-09-19 01:30:23 +08:00
55b546cb22
The pagination safety limit (1,000,000 records) was cutting off before reaching the end of the table (1,382,542 records total), and since pagination orders by id ascending (oldest first), the cutoff always excluded the most recent ~380k downloads — exactly the data needed to compute today/week/month trending correctly. This produced suspicious output where todayDownloads == weeklyDownloads == monthlyDownloads (94,484 each), which should be mathematically near-impossible. Raised the limit to 10,000,000 (plenty of headroom over the current table size) and regenerated both trending-data.json copies. New numbers show the expected progression: today (132,189) < week (152,849) < month (227,654) < total (1,340,064). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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() |