add limits to info
This commit is contained in:
181
chatmock/cli.py
181
chatmock/cli.py
@@ -1,19 +1,191 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
import argparse
|
||||
import json
|
||||
import errno
|
||||
import os
|
||||
import sys
|
||||
import webbrowser
|
||||
from datetime import datetime
|
||||
|
||||
from .app import create_app
|
||||
from .config import CLIENT_ID_DEFAULT
|
||||
from .limits import RateLimitWindow, compute_reset_at, load_rate_limit_snapshot
|
||||
from .oauth import OAuthHTTPServer, OAuthHandler, REQUIRED_PORT, URL_BASE
|
||||
from .utils import eprint, get_home_dir, load_chatgpt_tokens, parse_jwt_claims, read_auth_file
|
||||
import os
|
||||
|
||||
|
||||
_STATUS_LIMIT_BAR_SEGMENTS = 30
|
||||
_STATUS_LIMIT_BAR_FILLED = "█"
|
||||
_STATUS_LIMIT_BAR_EMPTY = "░"
|
||||
_STATUS_LIMIT_BAR_PARTIAL = "▓"
|
||||
|
||||
|
||||
def _clamp_percent(value: float) -> float:
|
||||
try:
|
||||
percent = float(value)
|
||||
except Exception:
|
||||
return 0.0
|
||||
if percent != percent:
|
||||
return 0.0
|
||||
if percent < 0.0:
|
||||
return 0.0
|
||||
if percent > 100.0:
|
||||
return 100.0
|
||||
return percent
|
||||
|
||||
|
||||
def _render_progress_bar(percent_used: float) -> str:
|
||||
ratio = max(0.0, min(1.0, percent_used / 100.0))
|
||||
filled_exact = ratio * _STATUS_LIMIT_BAR_SEGMENTS
|
||||
filled = int(filled_exact)
|
||||
partial = filled_exact - filled
|
||||
|
||||
has_partial = partial > 0.5
|
||||
if has_partial:
|
||||
filled += 1
|
||||
|
||||
filled = max(0, min(_STATUS_LIMIT_BAR_SEGMENTS, filled))
|
||||
empty = _STATUS_LIMIT_BAR_SEGMENTS - filled
|
||||
|
||||
if has_partial and filled > 0:
|
||||
bar = _STATUS_LIMIT_BAR_FILLED * (filled - 1) + _STATUS_LIMIT_BAR_PARTIAL + _STATUS_LIMIT_BAR_EMPTY * empty
|
||||
else:
|
||||
bar = _STATUS_LIMIT_BAR_FILLED * filled + _STATUS_LIMIT_BAR_EMPTY * empty
|
||||
|
||||
return f"[{bar}]"
|
||||
|
||||
|
||||
def _get_usage_color(percent_used: float) -> str:
|
||||
if percent_used >= 90:
|
||||
return "\033[91m"
|
||||
elif percent_used >= 75:
|
||||
return "\033[93m"
|
||||
elif percent_used >= 50:
|
||||
return "\033[94m"
|
||||
else:
|
||||
return "\033[92m"
|
||||
|
||||
|
||||
def _reset_color() -> str:
|
||||
"""ANSI reset color code"""
|
||||
return "\033[0m"
|
||||
|
||||
|
||||
def _format_window_duration(minutes: int | None) -> str | None:
|
||||
if minutes is None:
|
||||
return None
|
||||
try:
|
||||
total = int(minutes)
|
||||
except Exception:
|
||||
return None
|
||||
if total <= 0:
|
||||
return None
|
||||
minutes = total
|
||||
weeks, remainder = divmod(minutes, 7 * 24 * 60)
|
||||
days, remainder = divmod(remainder, 24 * 60)
|
||||
hours, remainder = divmod(remainder, 60)
|
||||
parts = []
|
||||
if weeks:
|
||||
parts.append(f"{weeks} week" + ("s" if weeks != 1 else ""))
|
||||
if days:
|
||||
parts.append(f"{days} day" + ("s" if days != 1 else ""))
|
||||
if hours:
|
||||
parts.append(f"{hours} hour" + ("s" if hours != 1 else ""))
|
||||
if remainder:
|
||||
parts.append(f"{remainder} minute" + ("s" if remainder != 1 else ""))
|
||||
if not parts:
|
||||
parts.append(f"{minutes} minute" + ("s" if minutes != 1 else ""))
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def _format_reset_duration(seconds: int | None) -> str | None:
|
||||
if seconds is None:
|
||||
return None
|
||||
try:
|
||||
value = int(seconds)
|
||||
except Exception:
|
||||
return None
|
||||
if value < 0:
|
||||
value = 0
|
||||
days, remainder = divmod(value, 86400)
|
||||
hours, remainder = divmod(remainder, 3600)
|
||||
minutes, remainder = divmod(remainder, 60)
|
||||
parts: list[str] = []
|
||||
if days:
|
||||
parts.append(f"{days}d")
|
||||
if hours:
|
||||
parts.append(f"{hours}h")
|
||||
if minutes:
|
||||
parts.append(f"{minutes}m")
|
||||
if not parts and remainder:
|
||||
parts.append("under 1m")
|
||||
if not parts:
|
||||
parts.append("0m")
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def _format_local_datetime(dt: datetime) -> str:
|
||||
local = dt.astimezone()
|
||||
tz_name = local.tzname() or "local"
|
||||
return f"{local.strftime('%b %d, %Y %H:%M')} {tz_name}"
|
||||
|
||||
|
||||
def _print_usage_limits_block() -> None:
|
||||
stored = load_rate_limit_snapshot()
|
||||
|
||||
print("📊 Usage Limits")
|
||||
|
||||
if stored is None:
|
||||
print(" No usage data available yet. Send a request through ChatMock first.")
|
||||
print()
|
||||
return
|
||||
|
||||
update_time = _format_local_datetime(stored.captured_at)
|
||||
print(f"Last updated: {update_time}")
|
||||
print()
|
||||
|
||||
windows: list[tuple[str, str, RateLimitWindow]] = []
|
||||
if stored.snapshot.primary is not None:
|
||||
windows.append(("⚡", "5 hour limit", stored.snapshot.primary))
|
||||
if stored.snapshot.secondary is not None:
|
||||
windows.append(("📅", "Weekly limit", stored.snapshot.secondary))
|
||||
|
||||
if not windows:
|
||||
print(" Usage data was captured but no limit windows were provided.")
|
||||
print()
|
||||
return
|
||||
|
||||
for i, (icon_label, desc, window) in enumerate(windows):
|
||||
if i > 0:
|
||||
print()
|
||||
|
||||
percent_used = _clamp_percent(window.used_percent)
|
||||
remaining = max(0.0, 100.0 - percent_used)
|
||||
color = _get_usage_color(percent_used)
|
||||
reset = _reset_color()
|
||||
|
||||
progress = _render_progress_bar(percent_used)
|
||||
usage_text = f"{percent_used:5.1f}% used"
|
||||
remaining_text = f"{remaining:5.1f}% left"
|
||||
|
||||
print(f"{icon_label} {desc}")
|
||||
print(f"{color}{progress}{reset} {color}{usage_text}{reset} | {remaining_text}")
|
||||
|
||||
reset_in = _format_reset_duration(window.resets_in_seconds)
|
||||
reset_at = compute_reset_at(stored.captured_at, window)
|
||||
|
||||
if reset_in and reset_at:
|
||||
reset_at_str = _format_local_datetime(reset_at)
|
||||
print(f" ⏳ Resets in: {reset_in} at {reset_at_str}")
|
||||
elif reset_in:
|
||||
print(f" ⏳ Resets in: {reset_in}")
|
||||
elif reset_at:
|
||||
reset_at_str = _format_local_datetime(reset_at)
|
||||
print(f" ⏳ Resets at: {reset_at_str}")
|
||||
|
||||
print()
|
||||
|
||||
def cmd_login(no_browser: bool, verbose: bool) -> int:
|
||||
home_dir = get_home_dir()
|
||||
client_id = CLIENT_ID_DEFAULT
|
||||
@@ -197,6 +369,8 @@ def main() -> None:
|
||||
print("👤 Account")
|
||||
print(" • Not signed in")
|
||||
print(" • Run: python3 chatmock.py login")
|
||||
print("")
|
||||
_print_usage_limits_block()
|
||||
sys.exit(0)
|
||||
|
||||
id_claims = parse_jwt_claims(id_token) or {}
|
||||
@@ -219,6 +393,8 @@ def main() -> None:
|
||||
print(f" • Plan: {plan}")
|
||||
if account_id:
|
||||
print(f" • Account ID: {account_id}")
|
||||
print("")
|
||||
_print_usage_limits_block()
|
||||
sys.exit(0)
|
||||
else:
|
||||
parser.error("Unknown command")
|
||||
@@ -226,4 +402,3 @@ def main() -> None:
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user