GigaProjects

← Back to claude-usage-gnome

fetch-usage.py

#!/usr/bin/env python3
import json
import os
import urllib.error
import urllib.request
from datetime import datetime, timezone


CREDENTIALS_PATH = "~/.claude/.credentials.json"
USAGE_URL = "https://api.anthropic.com/api/oauth/usage"
REQUEST_TIMEOUT_SECONDS = 10
FIVE_HOURS_IN_SECONDS = 5 * 60 * 60


class DisplayError(Exception):
    """An expected failure with a short message suitable for the panel."""


def load_access_token():
    credentials_path = os.path.expanduser(CREDENTIALS_PATH)

    try:
        with open(credentials_path, encoding="utf-8") as credentials_file:
            credentials = json.load(credentials_file)
    except FileNotFoundError as error:
        raise DisplayError("Claude: log in") from error
    except (OSError, json.JSONDecodeError) as error:
        raise DisplayError("Claude: auth error") from error

    try:
        access_token = credentials["claudeAiOauth"]["accessToken"]
    except (KeyError, TypeError) as error:
        raise DisplayError("Claude: auth error") from error

    if not isinstance(access_token, str) or not access_token:
        raise DisplayError("Claude: auth error")

    return access_token


def fetch_usage(access_token):
    request = urllib.request.Request(
        USAGE_URL,
        headers={
            "Authorization": f"Bearer {access_token}",
            "Accept": "application/json",
            "anthropic-version": "2023-06-01",
            "anthropic-beta": "oauth-2025-04-20",
        },
    )

    try:
        with urllib.request.urlopen(
            request,
            timeout=REQUEST_TIMEOUT_SECONDS,
        ) as response:
            return json.load(response)
    except urllib.error.HTTPError as error:
        if error.code in (401, 403):
            raise DisplayError("Claude: log in") from error

        raise DisplayError("Claude: service error") from error
    except (urllib.error.URLError, TimeoutError) as error:
        raise DisplayError("Claude: offline") from error
    except json.JSONDecodeError as error:
        raise DisplayError("Claude: bad data") from error


def parse_reset_time(reset_time_text):
    reset_time = datetime.fromisoformat(reset_time_text)
    if reset_time.tzinfo is None:
        raise ValueError("reset time must include a timezone")

    return reset_time


def pace_arrow(used_percentage, reset_time_text, total_seconds, now):
    reset_time = parse_reset_time(reset_time_text)
    remaining_seconds = (reset_time - now).total_seconds()
    elapsed_percentage = (1 - remaining_seconds / total_seconds) * 100

    if used_percentage <= elapsed_percentage:
        return "↑"

    return "↓"


def format_remaining(reset_time_text, now):
    reset_time = parse_reset_time(reset_time_text)
    remaining_seconds = int((reset_time - now).total_seconds())

    if remaining_seconds <= 0:
        return "0m"

    hours, minutes = divmod(remaining_seconds // 60, 60)
    if hours:
        return f"{hours}h{minutes:02d}m"

    return f"{minutes}m"


def format_usage(usage, now):
    five_hour_usage = usage["five_hour"]
    seven_day_usage = usage["seven_day"]

    reset_time_text = five_hour_usage["resets_at"]
    seven_day_percentage = round(seven_day_usage["utilization"])

    if not reset_time_text:
        return f"5h: -- | 7d: {seven_day_percentage}%"

    five_hour_percentage = round(five_hour_usage["utilization"])
    arrow = pace_arrow(
        five_hour_percentage,
        reset_time_text,
        FIVE_HOURS_IN_SECONDS,
        now,
    )
    remaining = format_remaining(reset_time_text, now)

    return (
        f"5h: {five_hour_percentage}%{arrow} {remaining}"
        f" | 7d: {seven_day_percentage}%"
    )


def main():
    try:
        access_token = load_access_token()
        usage = fetch_usage(access_token)
        now = datetime.now(timezone.utc)
        print(format_usage(usage, now))
    except DisplayError as error:
        print(error)
    except (KeyError, TypeError, ValueError):
        print("Claude: bad data")


if __name__ == "__main__":
    main()

Run this code