#!/usr/bin/env python3 """ Shared Claude Admin API Client - HTTP request handling with pagination. """ import json import urllib.error import urllib.parse import urllib.request from typing import Any def fetch_api_data(endpoint: str, params: dict[str, Any], admin_key: str, debug: bool = False) -> dict[str, Any]: """ Fetch data from Claude Admin API with automatic pagination. Args: endpoint: API endpoint path (e.g., '/v1/organizations/usage_report/messages') params: Query parameters admin_key: Admin API key (sk-ant-admin...) debug: Print debug messages for pagination (default: False) Returns: Complete API response with all paginated data merged Raises: Exception: If API request fails """ base_url = 'https://api.anthropic.com' # Build query string query_parts = [] for key, value in params.items(): if isinstance(value, list): for item in value: query_parts.append(f'{urllib.parse.quote(key)}={urllib.parse.quote(str(item))}') else: query_parts.append(f'{urllib.parse.quote(key)}={urllib.parse.quote(str(value))}') query_string = '&'.join(query_parts) url = f'{base_url}{endpoint}?{query_string}' headers = {'anthropic-version': '2023-06-01', 'x-api-key': admin_key} all_data: dict[str, Any] | None = None page_num = 0 while url: page_num += 1 if debug: print(f'[DEBUG] Fetching page {page_num}: {url}') req = urllib.request.Request(url, headers=headers) try: with urllib.request.urlopen(req) as response: data = json.loads(response.read().decode()) if all_data is None: all_data = data if debug: items_count = len(data.get('data', [])) print(f'[DEBUG] Page {page_num}: Received {items_count} items (first page)') else: # Merge paginated data if 'data' in data: page_items = len(data['data']) all_data['data'].extend(data['data']) total_items = len(all_data['data']) if debug: print(f'[DEBUG] Page {page_num}: Received {page_items} items, total accumulated: {total_items}') all_data['has_more'] = data.get('has_more', False) if 'next_page' in data: all_data['next_page'] = data.get('next_page') if 'last_id' in data: all_data['last_id'] = data.get('last_id') # Check for next page - handle both pagination styles has_more = data.get('has_more', False) if debug: print(f'[DEBUG] Page {page_num}: has_more={has_more}') if not has_more: if debug: print('[DEBUG] No more pages, pagination complete') url = None elif data.get('next_page'): # Messages endpoint uses next_page parameter next_page = data['next_page'] base_url_only = url.split('?')[0] if '?' in url else url separator = '&' if query_string else '' url = f'{base_url_only}?{query_string}{separator}page={urllib.parse.quote(next_page)}' if debug: print(f'[DEBUG] Using next_page pagination: next_page={next_page}') elif data.get('last_id'): # Users/API Keys endpoints use after_id with last_id cursor last_id = data['last_id'] base_url_only = url.split('?')[0] if '?' in url else url separator = '&' if query_string else '' url = f'{base_url_only}?{query_string}{separator}after_id={urllib.parse.quote(last_id)}' if debug: print(f'[DEBUG] Using last_id pagination: last_id={last_id}') else: if debug: print('[DEBUG] No pagination cursor found (next_page or last_id), stopping') url = None except urllib.error.HTTPError as e: error_body = e.read().decode() if e.fp else 'No error details' raise Exception(f'API Error {e.code}: {error_body}') if all_data is None: raise Exception('No data returned from API') if debug: total_items = len(all_data.get('data', [])) print(f'[DEBUG] Pagination complete: {page_num} pages fetched, {total_items} total items') return all_data