#!/usr/bin/env python3 """ Check Current Meeting Data Status for Priority States Analyzes what meeting data exists and what needs to be loaded/scraped. Usage: python scripts/localview/check_meeting_data.py python scripts/localview/check_meeting_data.py --states AL,GA,IN,MA,MT,WA,WI """ import argparse import sys from pathlib import Path from typing import List, Dict from datetime import datetime sys.path.insert(0, str(Path(__file__).parent.parent.parent)) from loguru import logger try: import polars as pl POLARS_AVAILABLE = True except ImportError: POLARS_AVAILABLE = False logger.warning("polars not available - install with: pip install polars") # Priority states for development PRIORITY_STATES = ['AL', 'GA', 'IN', 'MA', 'WA', 'WI'] def check_openstates_events(states: List[str]) -> Dict[str, Dict]: """Check OpenStates legislative events (committee hearings, etc.)""" logger.info("šŸ“‹ Checking OpenStates Legislative Events...") results = {} gold_dir = Path("data/gold/states") for state in states: state_dir = gold_dir / state events_file = state_dir / "events_events.parquet" participants_file = state_dir / "events_participants.parquet" state_data = { "has_events": events_file.exists(), "has_participants": participants_file.exists(), "event_count": 0, "participant_count": 0, "date_range": None } if POLARS_AVAILABLE and events_file.exists(): try: df = pl.read_parquet(events_file) state_data["event_count"] = len(df) if "start_date" in df.columns: dates = df["start_date"].drop_nulls() if len(dates) > 0: state_data["date_range"] = f"{dates.min()} to {dates.max()}" except Exception as e: logger.warning(f"Could not read {events_file}: {e}") if POLARS_AVAILABLE and participants_file.exists(): try: df = pl.read_parquet(participants_file) state_data["participant_count"] = len(df) except Exception as e: logger.warning(f"Could not read {participants_file}: {e}") results[state] = state_data # Print summary print("\n" + "="*70) print("šŸ“‹ OPENSTATES LEGISLATIVE EVENTS (Committee Hearings, Sessions)") print("="*70) for state, data in results.items(): print(f"\n{state}:") if data["has_events"]: print(f" āœ… Events: {data['event_count']:,}") print(f" āœ… Participants: {data['participant_count']:,}") if data["date_range"]: print(f" šŸ“… Date Range: {data['date_range']}") else: print(f" āŒ No event data found") return results def check_localview_meetings(states: List[str]) -> Dict[str, Dict]: """Check LocalView municipal meetings (city council, county boards)""" logger.info("šŸ›ļø Checking LocalView Municipal Meetings...") results = {} gold_dir = Path("data/gold/states") meetings_global = Path("data/gold/meetings/meetings_transcripts.parquet") # Check state-specific files for state in states: state_dir = gold_dir / state meetings_file = state_dir / "meetings_local.parquet" state_data = { "has_meetings": meetings_file.exists(), "meeting_count": 0, "date_range": None } if POLARS_AVAILABLE and meetings_file.exists(): try: df = pl.read_parquet(meetings_file) state_data["meeting_count"] = len(df) if "meeting_date" in df.columns: dates = df["meeting_date"].drop_nulls() if len(dates) > 0: state_data["date_range"] = f"{dates.min()} to {dates.max()}" except Exception as e: logger.warning(f"Could not read {meetings_file}: {e}") results[state] = state_data # Check global meetings file global_exists = meetings_global.exists() global_count = 0 global_range = None if POLARS_AVAILABLE and global_exists: try: df = pl.read_parquet(meetings_global) global_count = len(df) if "meeting_date" in df.columns: dates = df["meeting_date"].drop_nulls() if len(dates) > 0: global_range = f"{dates.min()} to {dates.max()}" # Count by state if "state" in df.columns: state_counts = df.group_by("state").count() print("\n" + "="*70) print("šŸ›ļø LOCALVIEW MUNICIPAL MEETINGS (City Council, County Boards)") print("="*70) print(f"\nGlobal Dataset: {global_count:,} total meetings") if global_range: print(f"Date Range: {global_range}") print("\nBy State:") for row in state_counts.iter_rows(named=True): state = row.get("state", "Unknown") count = row.get("count", 0) if state in states: print(f" {state}: {count:,} meetings") results[state]["meeting_count"] = count except Exception as e: logger.warning(f"Could not read {meetings_global}: {e}") else: print("\n" + "="*70) print("šŸ›ļø LOCALVIEW MUNICIPAL MEETINGS") print("="*70) print("\nāŒ No LocalView data found") print("\nTo load LocalView data:") print("1. Download from: https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/NJTBEM") print("2. Save to: data/cache/localview/") print("3. Run: python scripts/datasources/localview/localview_ingestion.py") return results def check_youtube_cache(states: List[str]) -> Dict[str, Dict]: """Check cached YouTube scraping data""" logger.info("šŸ“¹ Checking YouTube Scraping Cache...") cache_dir = Path("data/cache/localview") results = {} # Check for municipality channels channels_file = cache_dir / "municipality_channels.csv" has_channels = channels_file.exists() if POLARS_AVAILABLE and has_channels: try: df = pl.read_csv(channels_file) print("\n" + "="*70) print("šŸ“¹ YOUTUBE SCRAPING CACHE") print("="*70) print(f"\nMunicipality Channels: {len(df):,} total") if "state" in df.columns: for state in states: state_channels = df.filter(pl.col("state") == state) count = len(state_channels) results[state] = {"channel_count": count} print(f" {state}: {count} channels") # Show municipalities if count > 0 and "municipality" in df.columns: munis = state_channels["municipality"].to_list()[:5] print(f" {', '.join(munis)}") if count > 5: print(f" ... and {count - 5} more") # Check for video cache video_files = list(cache_dir.glob("videos_*.parquet")) if video_files: print(f"\nāœ… Cached video data: {len(video_files)} files") for vf in video_files[:5]: print(f" - {vf.name}") else: print("\nāš ļø No cached video data") except Exception as e: logger.warning(f"Could not read {channels_file}: {e}") else: print("\n" + "="*70) print("šŸ“¹ YOUTUBE SCRAPING CACHE") print("="*70) print("\nāŒ No YouTube channel data found") print("\nTo discover channels:") print("python scripts/localview/update_municipality_list.py --states AL,GA,IN,MA,MT,WA,WI") return results def print_recommendations(states: List[str]): """Print recommended next steps""" print("\n" + "="*70) print("šŸŽÆ RECOMMENDED NEXT STEPS") print("="*70) print("\n1ļøāƒ£ Download LocalView Historical Data (2006-2023)") print(" Visit: https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/NJTBEM") print(" Save to: data/cache/localview/") print(" Run: python scripts/datasources/localview/localview_ingestion.py") print("\n2ļøāƒ£ Discover Municipal YouTube Channels") print(f" python scripts/localview/update_municipality_list.py \\") print(f" --states {','.join(states)}") print("\n3ļøāƒ£ Scrape Recent Meetings (2024-2026)") print(" # First, get YouTube API key from Google Cloud Console") print(" # Add to .env: YOUTUBE_API_KEY=your_key_here") print(f" python scripts/localview/scrape_youtube_channels.py \\") print(f" --states {','.join(states)} \\") print(" --since 2024-01-01") print("\n4ļøāƒ£ Extract Transcripts and Contacts") print(f" python scripts/localview/extract_transcripts.py \\") print(f" --states {','.join(states)}") print("") print(" python scripts/manage_contacts.py extract \\") print(f" --states {','.join(states)} \\") print(" --batch-size 1000") print("\n" + "="*70) def main(): parser = argparse.ArgumentParser( description="Check meeting data status for priority states" ) parser.add_argument( '--states', default=','.join(PRIORITY_STATES), help=f'Comma-separated state codes (default: {",".join(PRIORITY_STATES)})' ) args = parser.parse_args() states = [s.strip().upper() for s in args.states.split(',')] logger.info(f"Checking meeting data for: {', '.join(states)}") # Run all checks openstates = check_openstates_events(states) localview = check_localview_meetings(states) youtube = check_youtube_cache(states) # Print recommendations print_recommendations(states) if __name__ == "__main__": main()