#!/usr/bin/env python3 """ Cost Report Fetcher - Fetch granular cost data from Claude Admin API. API Endpoint: GET /v1/organizations/cost_report Provides workspace-level cost breakdowns by model and token type. """ import json import os import sys import time from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any # Add parent directory to path for imports sys.path.insert(0, str(Path(__file__).parent.parent)) from src.cost_report.cost_report_types import CostBucket, parse_cost_bucket from src.shared.api_client import fetch_api_data RAW_OUTPUT_DIR = Path('output/cost_report/raw') def fetch_cost_report_for_date( date_str: str, group_by: list[str] | None = None, output_dir: Path = RAW_OUTPUT_DIR, admin_key: str | None = None, ) -> list[CostBucket]: """ Fetch cost report for a single day and save raw response. Args: date_str: Date in YYYY-MM-DD format group_by: Optional list of grouping dimensions ('workspace_id', 'description') output_dir: Directory to save raw API responses admin_key: Admin API key (defaults to CLAUDE_ADMIN_KEY env var) Returns: List of CostBucket objects for that day Raises: ValueError: If admin_key is not provided and CLAUDE_ADMIN_KEY env var is not set Exception: If API request fails """ # Get admin key from parameter or environment if admin_key is None: admin_key = os.getenv('CLAUDE_ADMIN_KEY') if admin_key is None: raise ValueError('CLAUDE_ADMIN_KEY environment variable not set and admin_key not provided') # Ensure output directory exists output_dir.mkdir(parents=True, exist_ok=True) # Convert date to RFC 3339 timestamps # API requires ending_at to be after starting_at by at least one bucket width (1d) # So for a single day, we set ending_at to midnight of the NEXT day start_dt = datetime.fromisoformat(date_str).replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=timezone.utc) end_dt = start_dt + timedelta(days=1) starting_at = start_dt.isoformat() ending_at = end_dt.isoformat() # Build API parameters params: dict[str, Any] = { 'starting_at': starting_at, 'ending_at': ending_at, 'bucket_width': '1d', } # Handle group_by array parameter if group_by: params['group_by[]'] = group_by # Fetch data from API endpoint = '/v1/organizations/cost_report' print(f'Fetching cost report for {date_str}...') response = fetch_api_data(endpoint, params, admin_key) # Save raw response with date-based filename output_path = output_dir / f'{date_str}.json' with open(output_path, 'w') as f: json.dump(response, f, indent=2) print(f'Raw response saved to: {output_path}') # Parse response into CostBucket dataclasses cost_buckets = [] for time_bucket in response.get('data', []): for result in time_bucket.get('results', []): # Add time period info from parent bucket result['starting_at'] = time_bucket.get('starting_at', '') result['ending_at'] = time_bucket.get('ending_at', '') cost_buckets.append(parse_cost_bucket(result)) return cost_buckets def fetch_cost_report_for_date_range( start_date: str, end_date: str, group_by: list[str] | None = None, output_dir: Path = RAW_OUTPUT_DIR, admin_key: str | None = None, ) -> list[CostBucket]: """ Fetch cost report for a date range by fetching each day individually. Args: start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format group_by: Optional list of grouping dimensions output_dir: Directory to save raw API responses admin_key: Admin API key (defaults to CLAUDE_ADMIN_KEY env var) Returns: List of all CostBucket objects fetched across all days Raises: ValueError: If date format is invalid or admin_key is missing """ # Get admin key from parameter or environment (validate once) if admin_key is None: admin_key = os.getenv('CLAUDE_ADMIN_KEY') if admin_key is None: raise ValueError('CLAUDE_ADMIN_KEY environment variable not set and admin_key not provided') # Parse dates try: start_dt = datetime.fromisoformat(start_date) end_dt = datetime.fromisoformat(end_date) except ValueError as e: raise ValueError(f'Invalid date format (expected YYYY-MM-DD): {e}') # Fetch each day individually all_buckets: list[CostBucket] = [] current_dt = start_dt while current_dt <= end_dt: date_str = current_dt.strftime('%Y-%m-%d') buckets = fetch_cost_report_for_date( date_str=date_str, group_by=group_by, output_dir=output_dir, admin_key=admin_key, ) all_buckets.extend(buckets) current_dt += timedelta(days=1) time.sleep(2) # Rate limit protection print(f'Fetched {len(all_buckets)} total cost buckets across {(end_dt - start_dt).days + 1} days') return all_buckets if __name__ == '__main__': """ Example usage: python -m src.cost_report.cost_report_fetcher """ # Fetch last 7 days of cost data grouped by workspace end = datetime.now(timezone.utc) start = end - timedelta(days=7) buckets = fetch_cost_report_for_date_range( start_date=start.strftime('%Y-%m-%d'), end_date=end.strftime('%Y-%m-%d'), group_by=['workspace_id', 'description'], ) print(f'\nExample: Fetched {len(buckets)} cost buckets') if buckets: print(f'First bucket: {buckets[0]}')