Files
wehub-resource-sync bb5c75ce05
Component Security Validation / Security Audit (push) Waiting to run
Deploy to Cloudflare Pages / deploy (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:38:58 +08:00

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()