{
  "output": "import boto3\nimport json\nimport os\nimport subprocess\nimport time\nfrom datetime import datetime, date\nfrom dateutil.relativedelta import relativedelta\n\n# Attempt to load .env file if python-dotenv is installed\ntry:\n    from dotenv import load_dotenv\n\n    load_dotenv()\nexcept ImportError:\n    pass\n\n\n# --- GOOGLE SHEETS CONFIG ---\ndef get_google_client_secret_file():\n    return os.getenv(\"GOOGLE_CLIENT_SECRET_FILE\", \"client_secret.json\")\n\n\ndef get_google_authorized_user_file():\n    return os.getenv(\"GOOGLE_AUTHORIZED_USER_FILE\", \"authorized_user.json\")\n\n\nSCOPES = [\n    \"https://www.googleapis.com/auth/spreadsheets\",\n    \"https://www.googleapis.com/auth/drive\",\n]\n\n\ndef get_google_auth():\n    \"\"\"Authenticates with Google Sheets API.\"\"\"\n    import gspread\n    from google_auth_oauthlib.flow import InstalledAppFlow\n    from google.auth.transport.requests import Request\n    from google.oauth2.credentials import Credentials\n\n    creds = None\n    auth_user_file = get_google_authorized_user_file()\n    client_secret_file = get_google_client_secret_file()\n\n    if os.path.exists(auth_user_file):\n        creds = Credentials.from_authorized_user_file(auth_user_file, SCOPES)\n\n    if not creds or not creds.valid:\n        if creds and creds.expired and creds.refresh_token:\n            creds.refresh(Request())\n        else:\n            if not os.path.exists(client_secret_file):\n                print(\n                    f\"Warning: {client_secret_file} not found. Google Sheets export will be skipped.\"\n                )\n                return None\n            flow = InstalledAppFlow.from_client_secrets_file(client_secret_file, SCOPES)\n            creds = flow.run_local_server(port=0)\n        with open(auth_user_file, \"w\") as token:\n            token.write(creds.to_json())\n\n    return gspread.authorize(creds)\n\n\ndef export_to_sheets(title, headers, rows):\n    \"\"\"\n    Creates a Google Sheet and populates it with headers and rows.\n    Title will be appended with ISO8601 date.\n    \"\"\"\n    gc = get_google_auth()\n    if not gc:\n        return None\n\n    full_title = f\"{title}_{date.today().isoformat()}\"\n    print(f\"Exporting to Google Sheet: {full_title}...\")\n\n    try:\n        sh = gc.create(full_title)\n\n        # Share with domain if configured\n        org_domain = os.getenv(\"GOOGLE_ORGANIZATION_DOMAIN\")\n        if org_domain:\n            try:\n                print(f\"Sharing with organization domain: {org_domain}...\")\n                sh.share(org_domain, perm_type=\"domain\", role=\"reader\")\n            except Exception as share_error:\n                print(\n                    f\"Warning: Failed to share with domain {org_domain}: {share_error}\"\n                )\n\n        ws = sh.get_worksheet(0)\n        ws.update_title(\"Data\")\n\n        # Prepare data: headers + rows\n        data = [headers] + rows\n        ws.update(data)\n\n        # Basic formatting\n        ws.format(\"A1:Z1\", {\"textFormat\": {\"bold\": True}})\n        ws.freeze(rows=1)\n\n        print(f\"Successfully exported to: {sh.url}\")\n        return sh.url\n    except Exception as e:\n        print(f\"Error exporting to Google Sheets: {e}\")\n        return None\n\n\ndef get_boto_session():\n    \"\"\"\n    Returns a boto3 session for the parent profile.\n    Ensures SSO login is valid and unsets conflicting env vars.\n    \"\"\"\n    ensure_sso_login()\n    parent_profile = os.getenv(\"AWS_PARENT_PROFILE\", \"default\")\n\n    # Unset env vars that would override the profile\n    for var in [\"AWS_ACCESS_KEY_ID\", \"AWS_SECRET_ACCESS_KEY\", \"AWS_SESSION_TOKEN\"]:\n        if var in os.environ:\n            del os.environ[var]\n\n    os.environ[\"AWS_PROFILE\"] = parent_profile\n    return boto3.Session(profile_name=parent_profile)\n\n\ndef ensure_sso_login():\n    \"\"\"\n    Checks if the current session has valid credentials.\n    If not, attempts to run 'aws sso login'.\n    Uses AWS_PARENT_PROFILE from env or defaults to 'default'.\n    \"\"\"\n    parent_profile = os.getenv(\"AWS_PARENT_PROFILE\", \"default\")\n\n    # We create a temporary session to check credentials\n    session = boto3.Session(profile_name=parent_profile)\n\n    try:\n        # Check if we can get an identity (indicates valid session)\n        sts = session.client(\"sts\")\n        sts.get_caller_identity()\n    except Exception:\n        print(\n            f\"Session for profile '{parent_profile}' expired or invalid. Attempting SSO login...\"\n        )\n        try:\n            # We use subprocess to call the CLI for login as it handles the browser flow\n            subprocess.run(\n                [\"aws\", \"sso\", \"login\", \"--profile\", parent_profile], check=True\n            )\n            print(\"SSO login successful.\")\n        except subprocess.CalledProcessError:\n            print(\"Error: 'aws sso login' failed. Please login manually.\")\n            return False\n        except Exception as e:\n            print(f\"An unexpected error occurred during login: {e}\")\n            return False\n\n    return True\n\n\ndef get_skip_accounts():\n    \"\"\"Returns a list of account IDs to skip from SKIP_ACCOUNTS env var.\"\"\"\n    skip_str = os.getenv(\"SKIP_ACCOUNTS\", \"\")\n    if not skip_str:\n        return []\n    return [s.strip() for s in skip_str.split(\",\") if s.strip()]\n\n\ndef get_ou_ids():\n    \"\"\"Returns a list of OU IDs from OU_IDS env var.\"\"\"\n    ou_str = os.getenv(\"OU_IDS\", \"\")\n    if ou_str:\n        return [o.strip() for o in ou_str.split(\",\") if o.strip()]\n    return []\n\n\ndef get_account_names():\n    \"\"\"Fetches account names from AWS Organizations, excluding skipped accounts.\"\"\"\n    session = get_boto_session()\n    org_client = session.client(\"organizations\")\n    skip_accounts = get_skip_accounts()\n    accounts = {}\n    try:\n        paginator = org_client.get_paginator(\"list_accounts\")\n        for page in paginator.paginate():\n            for account in page[\"Accounts\"]:\n                if account[\"Status\"] == \"ACTIVE\" and account[\"Id\"] not in skip_accounts:\n                    accounts[account[\"Id\"]] = account[\"Name\"]\n    except Exception as e:\n        sts = session.client(\"sts\")\n        try:\n            identity = sts.get_caller_identity()[\"Arn\"]\n        except:\n            identity = \"Unknown\"\n        print(f\"Error fetching account names (Identity: {identity}): {e}\")\n        print(\n            \"Tip: If you don't have permission to list all accounts, try specifying OU_IDS in your .env file.\"\n        )\n    return accounts\n\n\ndef get_previous_month_range():\n    \"\"\"Returns (start_date, end_date) for the previous month in YYYY-MM-DD format.\"\"\"\n    today = date.today()\n    first_day_curr = today.replace(day=1)\n    last_day_prev = first_day_curr - relativedelta(days=1)\n    start_date = last_day_prev.replace(day=1).strftime(\"%Y-%m-%d\")\n    end_date = first_day_curr.strftime(\"%Y-%m-%d\")\n    return start_date, end_date\n\n\ndef get_last_n_months_ranges(n=3):\n    \"\"\"Returns a list of (start_date, end_date, label) for the last n months.\"\"\"\n    ranges = []\n    current_date = datetime.now().replace(day=1)\n    for i in range(1, n + 1):\n        start_dt = current_date - relativedelta(months=i)\n        end_dt = current_date - relativedelta(months=i - 1)\n        ranges.append(\n            (\n                start_dt.strftime(\"%Y-%m-%d\"),\n                end_dt.strftime(\"%Y-%m-%d\"),\n                start_dt.strftime(\"%Y-%m\"),\n            )\n        )\n    return ranges\n\n\ndef get_aws_pricing(service_code, filters):\n    \"\"\"Generic helper to fetch on-demand price from AWS Pricing API (us-east-1).\"\"\"\n    session = get_boto_session()\n    pricing_client = session.client(\"pricing\", region_name=\"us-east-1\")\n    try:\n        response = pricing_client.get_products(\n            ServiceCode=service_code, Filters=filters\n        )\n        if response[\"PriceList\"]:\n            price_item = json.loads(response[\"PriceList\"][0])\n            on_demand = price_item[\"terms\"][\"OnDemand\"]\n            term_key = list(on_demand.keys())[0]\n            price_dimensions = on_demand[term_key][\"priceDimensions\"]\n            dim_key = list(price_dimensions.keys())[0]\n            return float(price_dimensions[dim_key][\"pricePerUnit\"][\"USD\"])\n    except Exception as e:\n        print(f\"Error fetching pricing for {service_code}: {e}\")\n    return None\n\n\ndef setup_org_accounts_session(ou_ids=None, profile_suffix=\".admin\"):\n    \"\"\"\n    Yields (account_dict, profile_name) for active accounts in OUs.\n    Handles boto3 session setup for each account.\n    Excludes accounts in SKIP_ACCOUNTS env var.\n    If no OUs are provided, scans the entire organization.\n    \"\"\"\n    session = get_boto_session()\n\n    if ou_ids is None:\n        ou_ids = get_ou_ids()\n\n    skip_accounts = get_skip_accounts()\n    org_client = session.client(\"organizations\")\n\n    if not ou_ids:\n        # Fallback: Scan all accounts in the organization if no OUs specified\n        try:\n            paginator = org_client.get_paginator(\"list_accounts\")\n            for page in paginator.paginate():\n                for account in page[\"Accounts\"]:\n                    if (\n                        account[\"Status\"] == \"ACTIVE\"\n                        and account[\"Id\"] not in skip_accounts\n                    ):\n                        # Sanitize account name for profile use\n                        account_name = (\n                            account[\"Name\"].replace(\" - \", \"-\").replace(\" \", \"-\")\n                        )\n                        profile_name = f\"{account_name}{profile_suffix}\"\n                        yield account, profile_name\n            return\n        except Exception as e:\n            sts = session.client(\"sts\")\n            try:\n                identity = sts.get_caller_identity()[\"Arn\"]\n            except:\n                identity = \"Unknown\"\n            print(\n                f\"Error fetching all accounts in organization (Identity: {identity}): {e}\"\n            )\n            print(\n                \"Tip: If you don't have permission to list all accounts, try specifying OU_IDS in your .env file.\"\n            )\n            return\n\n    for ou_id in ou_ids:\n        try:\n            paginator = org_client.get_paginator(\"list_accounts_for_parent\")\n            for page in paginator.paginate(ParentId=ou_id):\n                for account in page[\"Accounts\"]:\n                    if (\n                        account[\"Status\"] == \"ACTIVE\"\n                        and account[\"Id\"] not in skip_accounts\n                    ):\n                        # Sanitize account name for profile use\n                        account_name = (\n                            account[\"Name\"].replace(\" - \", \"-\").replace(\" \", \"-\")\n                        )\n                        profile_name = f\"{account_name}{profile_suffix}\"\n                        yield account, profile_name\n        except Exception as e:\n            print(f\"Error fetching accounts for OU {ou_id}: {e}\")\n"
}