#!/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()