usage-cli 0.33.5__tar.gz → 0.33.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {usage_cli-0.33.5/usage_cli.egg-info → usage_cli-0.33.7}/PKG-INFO +2 -1
- {usage_cli-0.33.5 → usage_cli-0.33.7}/README.md +1 -0
- usage_cli-0.33.7/analyzer/history_persist.py +116 -0
- usage_cli-0.33.7/analyzer/ledger_backfill.py +110 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/reporter.py +62 -16
- {usage_cli-0.33.5 → usage_cli-0.33.7}/i18n.json +30 -20
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/agy_quota_probe.py +31 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/agy.py +3 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/refresh.py +2 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/state.py +5 -1
- {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/update.py +2 -1
- {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/payload.py +1 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/web_panel.py +1 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/pyproject.toml +1 -1
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_agy_quota_probe.py +65 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_claude_pane.py +4 -1
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_dispatch_ledger.py +22 -0
- usage_cli-0.33.7/tests/test_history_persist.py +172 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_html_report_snapshot.py +32 -5
- usage_cli-0.33.7/tests/test_ledger_backfill.py +136 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_menubar.py +5 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_menubar_agy.py +6 -0
- usage_cli-0.33.7/tests/test_panel_footer_layout.py +88 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_report_daily_chart.py +25 -1
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_report_filter_js.py +77 -7
- usage_cli-0.33.7/tests/test_report_share_mask.py +51 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_reporter_build_data.py +113 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_snapshot.py +21 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_web_panel_payload.py +3 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_wintray.py +7 -1
- {usage_cli-0.33.5 → usage_cli-0.33.7}/ui/html_report.py +41 -13
- {usage_cli-0.33.5 → usage_cli-0.33.7}/ui/report_daily_chart.py +61 -12
- {usage_cli-0.33.5 → usage_cli-0.33.7}/ui/report_filter.py +18 -7
- {usage_cli-0.33.5 → usage_cli-0.33.7}/ui/report_ledger.py +1 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/ui/report_scripts.py +2 -2
- {usage_cli-0.33.5 → usage_cli-0.33.7}/ui/report_styles.py +7 -5
- {usage_cli-0.33.5 → usage_cli-0.33.7/usage_cli.egg-info}/PKG-INFO +2 -1
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_cli.egg-info/SOURCES.txt +5 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/app.py +5 -1
- usage_cli-0.33.5/tests/test_panel_footer_layout.py +0 -44
- {usage_cli-0.33.5 → usage_cli-0.33.7}/LICENSE +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/THIRD_PARTY_NOTICES.md +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/adapters/__init__.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/adapters/agy.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/adapters/claude.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/adapters/codex.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/adapters/grok.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/adapters/muse.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/adapters/rate_limits.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/adapters/registry.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/adapters/types.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/__init__.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/aggregator.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/blocks.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/diagnoser.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/diagnosis_snapshot.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/dispatch_ledger.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/insights.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/persona_loader.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/subscription.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/usage_snapshot.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/installer/__init__.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/installer/claude_pane.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/installer/codex_hook_trust.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/installer/login_item.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/installer/session_hooks.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/installer/setup_hook.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/installer/statusline_settings.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/__init__.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/_shared_cache_file.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/agy_disk_cache.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/agy_loader.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/cache_quarantine.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/chromium_cache.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/claude_desktop.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/claude_paths.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/claude_usage.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/codex_disk_cache.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/codex_events.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/codex_fork_replay.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/codex_loader.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/codex_paths.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/disk_cache_common.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/disk_cache_lifecycle.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/grok_loader.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/grok_quota_probe.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/history_disk_cache.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/history_loader.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/jsonl_limits.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/jsonl_utils.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/muse_loader.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/main.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/__init__.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/actions.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/app.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/chrome.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/fsevents_watch.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/grok.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/manual_refresh.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/menu.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/new_badge.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/notify.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/popover.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/prefs.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/title.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/whats_new.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/__init__.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/base.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/catalog.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/dynamic_height.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/panel_scale.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/panel_window.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/panel_window_state.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/window_drag.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/__init__.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/agy_window_keeper.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/burn_rate.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/codex_window_keeper.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/keeper_outcome.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/notifications.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/quota_snapshot.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/status_payload.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/usage_rate.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/window_keeper.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/setup.cfg +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/setup_app.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_adapters_claude.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_adapters_codex.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_adapters_rate_limits.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_aggregator.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_agy_adapter.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_agy_disk_cache.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_agy_loader.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_agy_window_keeper.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_analyzer_blocks.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_analyzer_pipeline.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_app_version.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_burn_rate.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_cache_quarantine.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_check_upstream.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_chromium_cache.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_claude_desktop.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_claude_paths.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_codex_consistency.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_codex_hook_trust.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_codex_loader.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_codex_window_keeper.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_desktop_fresh_cache.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_desktop_quota_display.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_desktop_quota_review.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_diagnoser.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_disk_cache_shards.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_doctor.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_dynamic_height.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_fsevents_watch.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_grok_loader.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_grok_quota_probe.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_history_loader.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_hook_scripts_are_standalone.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_i18n_format_fallback.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_i18n_key_parity.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_i18n_packaged_path.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_insights.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_jsonl_utils.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_keeper_menu.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_keeper_outcome.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_login_item.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_main.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_manual_refresh.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_menubar_grok.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_menubar_notify.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_menubar_prefs.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_menubar_state.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_menubar_update.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_muse_loader.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_new_badge.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_packaged_resources.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_packaging.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_panel_scale.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_panel_window_state.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_panels.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_panels_lazy_import.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_persona_loader.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_pricing.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_project_resolver.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_quota_aware.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_quota_label.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_service_status.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_session_resume.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_setup_app.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_setup_hook.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_setup_hook_coexistence.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_setup_hook_resume.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_setup_hook_terse.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_subprocess_utils.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_subscription.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_taskbar_menu.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_time_utils.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_tui.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_tui_sprite.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_ui_tables.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_update_checker.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_update_gate.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_update_release_notes.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_cli.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_client.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_diagnosis_snapshot.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_dir_sweeper.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_lang.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_logging.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_notifications.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_rate.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_snapshot_replay.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_statusline.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_statusline_agy.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_statusline_forwarder.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_statusline_grok.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_terse_mode.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_terse_reminder.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_whats_new.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_wheel_packaging.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_win_login_item.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_window_keeper.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_windows_manifest.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tui/__init__.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tui/app.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/tui/sprite.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/ui/__init__.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/ui/report_charts.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/ui/tables.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/updates/__init__.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/updates/checker.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/updates/gate.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/updates/release_notes.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/updates/whats_new.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_app/__init__.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_app/cli.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_app/doctor.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_cli.egg-info/dependency_links.txt +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_cli.egg-info/entry_points.txt +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_cli.egg-info/requires.txt +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_cli.egg-info/top_level.txt +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/__init__.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/app_version.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/i18n.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/prefs.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/pricing.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/project_resolver.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/service_status.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/subprocess_utils.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/time_utils.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/usage_dir_sweeper.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/usage_lang.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/usage_logging.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_hooks/__init__.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_hooks/usage_quota_aware.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_hooks/usage_session_resume.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_hooks/usage_statusline.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_hooks/usage_statusline_agy.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_hooks/usage_statusline_forwarder.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_hooks/usage_statusline_grok.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_hooks/usage_terse_mode.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_hooks/usage_terse_reminder.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/__init__.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/login_item.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/menu.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/prefs.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/quota_label.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/taskbar_menu.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/taskbar_obstacles.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/taskbar_overlay.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/watch.py +0 -0
- {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/whats_new.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: usage-cli
|
|
3
|
-
Version: 0.33.
|
|
3
|
+
Version: 0.33.7
|
|
4
4
|
Summary: usage — 在 macOS menu bar 與 Windows 系統匣顯示 Claude Code、Codex、Antigravity 用量的小工具(也提供終端機 TUI)
|
|
5
5
|
License-Expression: AGPL-3.0-only
|
|
6
6
|
Requires-Python: >=3.13
|
|
@@ -274,6 +274,7 @@ If the menu bar shows `--`, it's usually not broken — there's just no local da
|
|
|
274
274
|
- **Antigravity**, only if you use it, needs network access: quota is fetched from Google's official quota endpoint with the OAuth credential the Antigravity CLI already stored after sign-in — read from macOS Keychain, Windows Credential Manager, or a local token file depending on CLI version. `usage` never writes that credential back and keeps any refreshed access token in memory only; the call itself reads quota metadata.
|
|
275
275
|
- **Other background network activity:** Public Claude and Codex status pages to flag outages, a public model-pricing table to estimate cost (built-in prices are used offline), and an occasional GitHub check for a new version.
|
|
276
276
|
- **Beginner Mode**, only if you turn it on, sends Claude Code's latest answer to Claude Haiku through your own Claude Code sign-in to pick terms. Your term list stays in `~/.usage/glossary.json`.
|
|
277
|
+
- **Long-term history:** Claude Code deletes transcripts older than 30 days by default. While the app runs, `usage` saves daily totals to `~/.usage/usage_snapshot.json` and `~/.usage/year_ledger.json` every 6 hours, so reports keep older days. Nothing leaves your machine. When you move to a new computer, copy the whole `~/.usage/` folder too.
|
|
277
278
|
|
|
278
279
|
<details>
|
|
279
280
|
<summary>How Claude Desktop quota is read</summary>
|
|
@@ -252,6 +252,7 @@ If the menu bar shows `--`, it's usually not broken — there's just no local da
|
|
|
252
252
|
- **Antigravity**, only if you use it, needs network access: quota is fetched from Google's official quota endpoint with the OAuth credential the Antigravity CLI already stored after sign-in — read from macOS Keychain, Windows Credential Manager, or a local token file depending on CLI version. `usage` never writes that credential back and keeps any refreshed access token in memory only; the call itself reads quota metadata.
|
|
253
253
|
- **Other background network activity:** Public Claude and Codex status pages to flag outages, a public model-pricing table to estimate cost (built-in prices are used offline), and an occasional GitHub check for a new version.
|
|
254
254
|
- **Beginner Mode**, only if you turn it on, sends Claude Code's latest answer to Claude Haiku through your own Claude Code sign-in to pick terms. Your term list stays in `~/.usage/glossary.json`.
|
|
255
|
+
- **Long-term history:** Claude Code deletes transcripts older than 30 days by default. While the app runs, `usage` saves daily totals to `~/.usage/usage_snapshot.json` and `~/.usage/year_ledger.json` every 6 hours, so reports keep older days. Nothing leaves your machine. When you move to a new computer, copy the whole `~/.usage/` folder too.
|
|
255
256
|
|
|
256
257
|
<details>
|
|
257
258
|
<summary>How Claude Desktop quota is read</summary>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
|
|
3
|
+
#
|
|
4
|
+
# Part of "usage". Free software licensed under the GNU Affero General Public
|
|
5
|
+
# License v3.0 only; see the LICENSE file for full terms and the warranty disclaimer.
|
|
6
|
+
|
|
7
|
+
"""Periodically preserve real usage without scanning on the app thread."""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import threading
|
|
14
|
+
from datetime import UTC, datetime, timedelta
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from adapters.registry import detect_agents
|
|
18
|
+
from analyzer import diagnosis_snapshot, reporter, usage_snapshot
|
|
19
|
+
from usage_common.time_utils import parse_optional_iso8601_utc
|
|
20
|
+
|
|
21
|
+
STATE_PATH = Path.home() / ".usage" / "history_persist_state.json"
|
|
22
|
+
_INTERVAL = timedelta(hours=6)
|
|
23
|
+
_LOOKBACK_DAYS = 45
|
|
24
|
+
_lock = threading.Lock()
|
|
25
|
+
_refresh_in_flight = False
|
|
26
|
+
_timer: threading.Timer | None = None
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _remaining(now: datetime) -> float:
|
|
31
|
+
try:
|
|
32
|
+
state = json.loads(STATE_PATH.read_text(encoding="utf-8"))
|
|
33
|
+
last = (
|
|
34
|
+
parse_optional_iso8601_utc(state.get("last_success"))
|
|
35
|
+
if isinstance(state, dict)
|
|
36
|
+
else None
|
|
37
|
+
)
|
|
38
|
+
except (OSError, ValueError):
|
|
39
|
+
last = None
|
|
40
|
+
return max(0.0, (_INTERVAL - (now - last)).total_seconds()) if last else 0.0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _schedule_next_locked(delay: float) -> None:
|
|
44
|
+
global _timer
|
|
45
|
+
if _timer is None:
|
|
46
|
+
_timer = threading.Timer(delay, _on_timer)
|
|
47
|
+
_timer.daemon = True
|
|
48
|
+
try:
|
|
49
|
+
_timer.start()
|
|
50
|
+
except Exception:
|
|
51
|
+
_timer = None
|
|
52
|
+
raise
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _on_timer() -> None:
|
|
56
|
+
global _timer
|
|
57
|
+
with _lock:
|
|
58
|
+
_timer = None
|
|
59
|
+
maybe_schedule_refresh()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def maybe_schedule_refresh() -> None:
|
|
63
|
+
global _refresh_in_flight, _timer
|
|
64
|
+
try:
|
|
65
|
+
with _lock:
|
|
66
|
+
if _refresh_in_flight:
|
|
67
|
+
return
|
|
68
|
+
remaining = _remaining(datetime.now(UTC))
|
|
69
|
+
if remaining > 0:
|
|
70
|
+
_schedule_next_locked(remaining)
|
|
71
|
+
return
|
|
72
|
+
if _timer is not None:
|
|
73
|
+
_timer.cancel()
|
|
74
|
+
_timer = None
|
|
75
|
+
_refresh_in_flight = True
|
|
76
|
+
threading.Thread(target=_refresh_in_background, daemon=True).start()
|
|
77
|
+
except Exception:
|
|
78
|
+
with _lock:
|
|
79
|
+
_refresh_in_flight = False
|
|
80
|
+
logger.warning("Failed to schedule usage history persistence", exc_info=True)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _refresh_in_background() -> None:
|
|
84
|
+
global _refresh_in_flight
|
|
85
|
+
try:
|
|
86
|
+
refresh_history()
|
|
87
|
+
except Exception:
|
|
88
|
+
logger.warning("Failed to persist usage history", exc_info=True)
|
|
89
|
+
finally:
|
|
90
|
+
with _lock:
|
|
91
|
+
_refresh_in_flight = False
|
|
92
|
+
try:
|
|
93
|
+
_schedule_next_locked(_INTERVAL.total_seconds())
|
|
94
|
+
except Exception:
|
|
95
|
+
logger.warning("Failed to reschedule usage history persistence", exc_info=True)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def refresh_history(now: datetime | None = None) -> bool:
|
|
99
|
+
current = now or datetime.now(UTC)
|
|
100
|
+
if _remaining(current) > 0:
|
|
101
|
+
return False
|
|
102
|
+
today = current.astimezone().date()
|
|
103
|
+
entries = reporter.load_history_entries(
|
|
104
|
+
detect_agents(), today - timedelta(days=_LOOKBACK_DAYS - 1), today
|
|
105
|
+
)
|
|
106
|
+
snapshot = usage_snapshot.record_entries(entries)
|
|
107
|
+
ledger = reporter.persist_year_entries(entries, today)
|
|
108
|
+
# Existing writers swallow I/O errors. Read back before marking success.
|
|
109
|
+
for path, expected in (
|
|
110
|
+
(usage_snapshot.SNAPSHOT_PATH, snapshot),
|
|
111
|
+
(reporter.YEAR_LEDGER_PATH, ledger),
|
|
112
|
+
):
|
|
113
|
+
if json.loads(path.read_text(encoding="utf-8")) != expected:
|
|
114
|
+
raise OSError(f"Usage history write verification failed: {path}")
|
|
115
|
+
diagnosis_snapshot._atomic_write_json(STATE_PATH, {"last_success": current.isoformat()})
|
|
116
|
+
return True
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
|
|
3
|
+
#
|
|
4
|
+
# Part of "usage". Free software licensed under the GNU Affero General Public
|
|
5
|
+
# License v3.0 only; see the LICENSE file for full terms and the warranty disclaimer.
|
|
6
|
+
|
|
7
|
+
"""Rehydrate only the daily token gap left after snapshot replay."""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from collections.abc import Mapping, Sequence
|
|
13
|
+
from datetime import date, datetime
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from adapters.types import UsageEntry
|
|
17
|
+
from usage_common.pricing import calculate_cost
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from analyzer.reporter import _YearDay
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_ledger_entry(entry: UsageEntry) -> bool:
|
|
24
|
+
return entry.session_id.startswith("usage-ledger:")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _allocate(
|
|
28
|
+
ledger: Mapping[str, int], current: Mapping[str, int], gap: int
|
|
29
|
+
) -> list[tuple[str, int]]:
|
|
30
|
+
weights = {key: max(tokens - current.get(key, 0), 0) for key, tokens in ledger.items()}
|
|
31
|
+
if not any(weights.values()):
|
|
32
|
+
weights = {key: max(tokens, 0) for key, tokens in ledger.items()}
|
|
33
|
+
total = sum(weights.values())
|
|
34
|
+
if total == 0:
|
|
35
|
+
return [("unknown", gap)]
|
|
36
|
+
# Integer largest-remainder allocation keeps every dimension's sum exact.
|
|
37
|
+
shares = {key: gap * weight // total for key, weight in weights.items()}
|
|
38
|
+
ranked = sorted(weights, key=lambda key: (-(gap * weights[key] % total), key))
|
|
39
|
+
for key in ranked[: gap - sum(shares.values())]:
|
|
40
|
+
shares[key] += 1
|
|
41
|
+
return [(key, shares[key]) for key in sorted(shares) if shares[key] > 0]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def merge_with_ledger(
|
|
45
|
+
entries: Sequence[UsageEntry],
|
|
46
|
+
entry_dates: Mapping[int, date],
|
|
47
|
+
days: Mapping[str, _YearDay],
|
|
48
|
+
date_from: date,
|
|
49
|
+
date_to: date,
|
|
50
|
+
) -> tuple[list[UsageEntry], dict[int, date]]:
|
|
51
|
+
merged = list(entries)
|
|
52
|
+
dates = dict(entry_dates)
|
|
53
|
+
by_day: dict[date, list[UsageEntry]] = defaultdict(list)
|
|
54
|
+
for entry in entries:
|
|
55
|
+
by_day[entry_dates[id(entry)]].append(entry)
|
|
56
|
+
for day_key, ledger in sorted(days.items()):
|
|
57
|
+
day = date.fromisoformat(day_key)
|
|
58
|
+
if not date_from <= day <= date_to or not any(
|
|
59
|
+
tokens > 0 for tokens in ledger["agent_tokens"].values()
|
|
60
|
+
):
|
|
61
|
+
continue
|
|
62
|
+
current = by_day.get(day, [])
|
|
63
|
+
gap = ledger["total_tokens"] - sum(entry.total_tokens for entry in current)
|
|
64
|
+
if gap <= 0:
|
|
65
|
+
continue
|
|
66
|
+
dimensions: list[list[tuple[str, int]]] = []
|
|
67
|
+
for attribute, ledger_tokens in (
|
|
68
|
+
("agent_id", ledger["agent_tokens"]),
|
|
69
|
+
("project", ledger["project_tokens"]),
|
|
70
|
+
("model", ledger["model_tokens"]),
|
|
71
|
+
):
|
|
72
|
+
totals: dict[str, int] = defaultdict(int)
|
|
73
|
+
for entry in current:
|
|
74
|
+
totals[getattr(entry, attribute) or "unknown"] += entry.total_tokens
|
|
75
|
+
dimensions.append(_allocate(ledger_tokens, totals, gap))
|
|
76
|
+
cost_gap = max(ledger["cost"] - sum(calculate_cost(entry) for entry in current), 0.0)
|
|
77
|
+
# Intersect the three allocations without claiming historical correlations.
|
|
78
|
+
indices = [0, 0, 0]
|
|
79
|
+
remaining = [dimension[0][1] for dimension in dimensions]
|
|
80
|
+
allocated = 0
|
|
81
|
+
while allocated < gap:
|
|
82
|
+
tokens = min(remaining)
|
|
83
|
+
agent, project, model = (
|
|
84
|
+
dimension[index][0] for dimension, index in zip(dimensions, indices, strict=True)
|
|
85
|
+
)
|
|
86
|
+
synthetic_id = f"usage-ledger:{day_key}:{allocated}"
|
|
87
|
+
entry = UsageEntry(
|
|
88
|
+
timestamp=datetime.combine(day, datetime.min.time()),
|
|
89
|
+
session_id=f"usage-ledger:{day_key}",
|
|
90
|
+
message_id=synthetic_id,
|
|
91
|
+
request_id=synthetic_id,
|
|
92
|
+
agent_id=agent,
|
|
93
|
+
project=project,
|
|
94
|
+
model=model,
|
|
95
|
+
input_tokens=tokens,
|
|
96
|
+
output_tokens=0,
|
|
97
|
+
cache_creation_tokens=0,
|
|
98
|
+
cache_read_tokens=0,
|
|
99
|
+
cost_usd=cost_gap * tokens / gap,
|
|
100
|
+
message_count=0,
|
|
101
|
+
)
|
|
102
|
+
merged.append(entry)
|
|
103
|
+
dates[id(entry)] = day
|
|
104
|
+
allocated += tokens
|
|
105
|
+
for axis in range(3):
|
|
106
|
+
remaining[axis] -= tokens
|
|
107
|
+
if remaining[axis] == 0 and allocated < gap:
|
|
108
|
+
indices[axis] += 1
|
|
109
|
+
remaining[axis] = dimensions[axis][indices[axis]][1]
|
|
110
|
+
return merged, dates
|
|
@@ -27,7 +27,7 @@ from adapters.types import AgentInfo, UsageEntry
|
|
|
27
27
|
from usage_common.pricing import calculate_cost, is_model_priced
|
|
28
28
|
|
|
29
29
|
from .aggregator import aggregate_sessions
|
|
30
|
-
from . import diagnoser, dispatch_ledger, usage_snapshot
|
|
30
|
+
from . import diagnoser, dispatch_ledger, ledger_backfill, usage_snapshot
|
|
31
31
|
|
|
32
32
|
logger = logging.getLogger(__name__)
|
|
33
33
|
|
|
@@ -317,6 +317,18 @@ def _load_agent_entries(
|
|
|
317
317
|
return entries
|
|
318
318
|
|
|
319
319
|
|
|
320
|
+
def load_history_entries(
|
|
321
|
+
agents: list[AgentInfo], date_from: date, date_to: date
|
|
322
|
+
) -> list[UsageEntry]:
|
|
323
|
+
hours_back = ((date_to - date_from).days + 2) * 24
|
|
324
|
+
return [
|
|
325
|
+
entry
|
|
326
|
+
for agent in agents
|
|
327
|
+
for entry in _load_agent_entries(agent, hours_back)
|
|
328
|
+
if date_from <= _entry_date(entry) <= date_to and not ledger_backfill.is_ledger_entry(entry)
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
|
|
320
332
|
def _load_recent_claude_entries(hours_back: int) -> list[UsageEntry]:
|
|
321
333
|
entries: list[UsageEntry] = []
|
|
322
334
|
seen: set[str] = set()
|
|
@@ -798,6 +810,15 @@ def build_year_data(agents: list[AgentInfo]) -> YearReportData:
|
|
|
798
810
|
)
|
|
799
811
|
|
|
800
812
|
|
|
813
|
+
def persist_year_entries(entries: list[UsageEntry], today: date) -> _YearLedger:
|
|
814
|
+
grid_end = today + timedelta(days=(5 - today.weekday()) % 7)
|
|
815
|
+
grid_start = grid_end - timedelta(days=_YEAR_WEEKS * 7 - 1)
|
|
816
|
+
return _merge_year_ledger(
|
|
817
|
+
_aggregate_year_days(entries),
|
|
818
|
+
trim_before=grid_start - timedelta(days=_YEAR_LEDGER_TRIM_BUFFER_DAYS),
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
|
|
801
822
|
def _load_year_data_cached(agents: list[AgentInfo]) -> YearReportData:
|
|
802
823
|
cached = _read_year_cache()
|
|
803
824
|
if cached is not None:
|
|
@@ -923,10 +944,21 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
|
|
|
923
944
|
raw_entries.extend(_load_agent_entries(agent, hours_back))
|
|
924
945
|
entry_dates = {id(entry): _entry_date(entry) for entry in raw_entries}
|
|
925
946
|
|
|
947
|
+
ledger_days = _read_year_ledger()["days"]
|
|
926
948
|
if date_from is None:
|
|
927
949
|
earliest_live = min(entry_dates.values()) if entry_dates else None
|
|
928
950
|
earliest_snap = usage_snapshot.earliest_date()
|
|
929
|
-
|
|
951
|
+
earliest_ledger = min(
|
|
952
|
+
(
|
|
953
|
+
date.fromisoformat(day)
|
|
954
|
+
for day, value in ledger_days.items()
|
|
955
|
+
if any(tokens > 0 for tokens in value["agent_tokens"].values())
|
|
956
|
+
),
|
|
957
|
+
default=None,
|
|
958
|
+
)
|
|
959
|
+
earliest_candidates = [
|
|
960
|
+
day for day in (earliest_live, earliest_snap, earliest_ledger) if day is not None
|
|
961
|
+
]
|
|
930
962
|
date_from = min(earliest_candidates) if earliest_candidates else date_to
|
|
931
963
|
|
|
932
964
|
span_from = date_from
|
|
@@ -940,12 +972,16 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
|
|
|
940
972
|
raw_entries, entry_dates = usage_snapshot.merge_live_with_snapshot(
|
|
941
973
|
raw_entries, entry_dates, span_from, span_to
|
|
942
974
|
)
|
|
975
|
+
raw_entries, entry_dates = ledger_backfill.merge_with_ledger(
|
|
976
|
+
raw_entries, entry_dates, ledger_days, span_from, span_to
|
|
977
|
+
)
|
|
943
978
|
|
|
944
979
|
entries = [entry for entry in raw_entries if date_from <= entry_dates[id(entry)] <= date_to]
|
|
945
980
|
|
|
981
|
+
real_entries = [entry for entry in entries if not ledger_backfill.is_ledger_entry(entry)]
|
|
946
982
|
total_tokens = sum(entry.total_tokens for entry in entries)
|
|
947
983
|
total_cost = 0.0
|
|
948
|
-
session_ids = {entry.session_id for entry in
|
|
984
|
+
session_ids = {entry.session_id for entry in real_entries}
|
|
949
985
|
active_dates = {entry_dates[id(entry)] for entry in entries}
|
|
950
986
|
total_days = (date_to - date_from).days + 1
|
|
951
987
|
comparison = _build_comparison(raw_entries, entry_dates, period, date_from, date_to)
|
|
@@ -974,7 +1010,7 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
|
|
|
974
1010
|
lambda: defaultdict(lambda: defaultdict(int))
|
|
975
1011
|
)
|
|
976
1012
|
daily_totals: dict[date, dict[str, Any]] = defaultdict(lambda: {"tokens": 0, "cost": 0.0})
|
|
977
|
-
cube_totals: dict[tuple[date, str, str, str], dict[str, int | float]] = defaultdict(
|
|
1013
|
+
cube_totals: dict[tuple[date, str, str, str, bool], dict[str, int | float]] = defaultdict(
|
|
978
1014
|
lambda: {
|
|
979
1015
|
"input_tokens": 0,
|
|
980
1016
|
"output_tokens": 0,
|
|
@@ -990,18 +1026,20 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
|
|
|
990
1026
|
total_cost += cost
|
|
991
1027
|
agent_totals = by_agent_totals[entry.agent_id or "unknown"]
|
|
992
1028
|
agent_totals["tokens"] += entry.total_tokens
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1029
|
+
if not ledger_backfill.is_ledger_entry(entry):
|
|
1030
|
+
agent_totals["input_tokens"] += entry.input_tokens
|
|
1031
|
+
agent_totals["output_tokens"] += entry.output_tokens
|
|
1032
|
+
agent_totals["cache_creation_tokens"] += entry.cache_creation_tokens
|
|
1033
|
+
agent_totals["cache_read_tokens"] += entry.cache_read_tokens
|
|
1034
|
+
agent_totals["sessions"].add(entry.session_id)
|
|
997
1035
|
agent_totals["cost"] += cost
|
|
998
|
-
agent_totals["sessions"].add(entry.session_id)
|
|
999
1036
|
agent_totals["messages"] += entry.message_count
|
|
1000
1037
|
|
|
1001
1038
|
project = by_project_totals[entry.project or "unknown"]
|
|
1002
1039
|
project["tokens"] += entry.total_tokens
|
|
1003
1040
|
project["cost"] += cost
|
|
1004
|
-
|
|
1041
|
+
if not ledger_backfill.is_ledger_entry(entry):
|
|
1042
|
+
project["sessions"].add(entry.session_id)
|
|
1005
1043
|
|
|
1006
1044
|
model = by_model_totals[entry.model or "unknown"]
|
|
1007
1045
|
model["tokens"] += entry.total_tokens
|
|
@@ -1026,6 +1064,7 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
|
|
|
1026
1064
|
entry.agent_id or "unknown",
|
|
1027
1065
|
entry.model or "unknown",
|
|
1028
1066
|
entry.project or "unknown",
|
|
1067
|
+
ledger_backfill.is_ledger_entry(entry),
|
|
1029
1068
|
)
|
|
1030
1069
|
]
|
|
1031
1070
|
cube_row["input_tokens"] += entry.input_tokens
|
|
@@ -1118,6 +1157,7 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
|
|
|
1118
1157
|
cube_agent_id,
|
|
1119
1158
|
cube_model_name,
|
|
1120
1159
|
cube_project_name,
|
|
1160
|
+
_cube_is_ledger,
|
|
1121
1161
|
), values in cube_totals.items():
|
|
1122
1162
|
row_tokens = (
|
|
1123
1163
|
int(values["input_tokens"])
|
|
@@ -1162,6 +1202,7 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
|
|
|
1162
1202
|
cube_agent_id,
|
|
1163
1203
|
cube_model_name,
|
|
1164
1204
|
cube_project_name,
|
|
1205
|
+
cube_is_ledger,
|
|
1165
1206
|
), values in cube_totals.items():
|
|
1166
1207
|
cube_rows.append(
|
|
1167
1208
|
[
|
|
@@ -1175,6 +1216,8 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
|
|
|
1175
1216
|
int(values["cache_read_tokens"]),
|
|
1176
1217
|
_round_cost(float(values["cost"])),
|
|
1177
1218
|
int(values["message_count"]),
|
|
1219
|
+
# Optional index 10 marks ledger totals; legacy real rows stay unchanged.
|
|
1220
|
+
*([1] if cube_is_ledger else []),
|
|
1178
1221
|
]
|
|
1179
1222
|
)
|
|
1180
1223
|
cube_rows.sort(key=lambda row: (row[0], row[1], row[2], row[3]))
|
|
@@ -1202,10 +1245,13 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
|
|
|
1202
1245
|
top_sessions: list[TopSessionReportRow] = []
|
|
1203
1246
|
now = datetime.now().astimezone(timezone.utc)
|
|
1204
1247
|
weekly_entries = raw_entries if weekly_preloaded is None else weekly_preloaded
|
|
1248
|
+
weekly_entries = [
|
|
1249
|
+
entry for entry in weekly_entries if not ledger_backfill.is_ledger_entry(entry)
|
|
1250
|
+
]
|
|
1205
1251
|
windows = dispatch_ledger.load_windows() if weekly_entries else {}
|
|
1206
1252
|
estimates = dispatch_ledger.estimate_sessions(weekly_entries, windows, now)
|
|
1207
1253
|
sessions_by_cost = sorted(
|
|
1208
|
-
aggregate_sessions(
|
|
1254
|
+
aggregate_sessions(real_entries), key=lambda session: session.cost_usd, reverse=True
|
|
1209
1255
|
)
|
|
1210
1256
|
sessions_by_tokens = sorted(
|
|
1211
1257
|
sessions_by_cost, key=lambda session: session.total_tokens, reverse=True
|
|
@@ -1238,7 +1284,7 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
|
|
|
1238
1284
|
)
|
|
1239
1285
|
|
|
1240
1286
|
year_data = _load_year_data_cached(agents)
|
|
1241
|
-
usage_snapshot.record_entries(
|
|
1287
|
+
usage_snapshot.record_entries(real_entries, entry_dates=entry_dates)
|
|
1242
1288
|
|
|
1243
1289
|
return {
|
|
1244
1290
|
"period": period,
|
|
@@ -1247,10 +1293,10 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
|
|
|
1247
1293
|
"date_to": date_to.isoformat(),
|
|
1248
1294
|
"summary": {
|
|
1249
1295
|
"total_tokens": total_tokens,
|
|
1250
|
-
"input_tokens": sum(entry.input_tokens for entry in
|
|
1251
|
-
"output_tokens": sum(entry.output_tokens for entry in
|
|
1252
|
-
"cache_creation_tokens": sum(entry.cache_creation_tokens for entry in
|
|
1253
|
-
"cache_read_tokens": sum(entry.cache_read_tokens for entry in
|
|
1296
|
+
"input_tokens": sum(entry.input_tokens for entry in real_entries),
|
|
1297
|
+
"output_tokens": sum(entry.output_tokens for entry in real_entries),
|
|
1298
|
+
"cache_creation_tokens": sum(entry.cache_creation_tokens for entry in real_entries),
|
|
1299
|
+
"cache_read_tokens": sum(entry.cache_read_tokens for entry in real_entries),
|
|
1254
1300
|
"cost_usd": _round_cost(total_cost),
|
|
1255
1301
|
"sessions": len(session_ids),
|
|
1256
1302
|
# by_project 只留前 10 名,敘述句要報真實專案數就得另外帶
|
|
@@ -260,8 +260,8 @@
|
|
|
260
260
|
"report_fixed_range": "固定區間",
|
|
261
261
|
"report_fixed_group_note": "以下區塊不受上方日期篩選影響",
|
|
262
262
|
"report_model": "模型",
|
|
263
|
-
"report_appendix_title": "
|
|
264
|
-
"report_appendix_desc": "
|
|
263
|
+
"report_appendix_title": "花費怎麼算的",
|
|
264
|
+
"report_appendix_desc": "公開 API 價目換算 · 花費可信度",
|
|
265
265
|
"report_narrative": "過去這段時間你燒了 {tokens} tokens。{peak_date} 是高峰({peak_tokens}/天),主力 LLM 是 {top_model}。",
|
|
266
266
|
"report_period": "區間",
|
|
267
267
|
"report_project": "專案",
|
|
@@ -270,7 +270,7 @@
|
|
|
270
270
|
"report_persona_active_hours": "活躍時段",
|
|
271
271
|
"report_persona_caption": "你最常在 {h1}{h2} 與 AI 協作",
|
|
272
272
|
"report_persona_caption_second": " 和 {h2}",
|
|
273
|
-
"report_persona_peak": "峰值 {count}
|
|
273
|
+
"report_persona_peak": "峰值 {count} 則訊息(含工具呼叫)",
|
|
274
274
|
"report_persona_empty": "這段期間還沒有足夠資料",
|
|
275
275
|
"report_recent_titles_heading": "最近在做什麼",
|
|
276
276
|
"report_persona_section": "使用習慣",
|
|
@@ -539,7 +539,9 @@
|
|
|
539
539
|
"quota_aware_action_failed": "無法切換額度感知模式。",
|
|
540
540
|
"claude_beginner_error": "新手模式:{error}",
|
|
541
541
|
"claude_desktop_updated": "Claude 桌面版 · {minutes} 分鐘前更新",
|
|
542
|
-
"claude_desktop_stale": "Claude 桌面版 · 資料過期 · {minutes} 分鐘前更新"
|
|
542
|
+
"claude_desktop_stale": "Claude 桌面版 · 資料過期 · {minutes} 分鐘前更新",
|
|
543
|
+
"report_project_others": "其他專案",
|
|
544
|
+
"report_ledger_pct_note": "Claude、Codex、Antigravity 三欄是約佔該工具週額度的百分比。"
|
|
543
545
|
},
|
|
544
546
|
"en": {
|
|
545
547
|
"panel_default_name": "Default",
|
|
@@ -802,8 +804,8 @@
|
|
|
802
804
|
"report_fixed_range": "Fixed range",
|
|
803
805
|
"report_fixed_group_note": "The sections below don't follow the date filter above",
|
|
804
806
|
"report_model": "Model",
|
|
805
|
-
"report_appendix_title": "How
|
|
806
|
-
"report_appendix_desc": "
|
|
807
|
+
"report_appendix_title": "How cost is calculated",
|
|
808
|
+
"report_appendix_desc": "Public API price conversion · Cost confidence",
|
|
807
809
|
"report_narrative": "You burned {tokens} tokens. Peak was {peak_date} ({peak_tokens}/day). Top model: {top_model}.",
|
|
808
810
|
"report_period": "Period",
|
|
809
811
|
"report_project": "Project",
|
|
@@ -812,7 +814,7 @@
|
|
|
812
814
|
"report_persona_active_hours": "Active hours",
|
|
813
815
|
"report_persona_caption": "You most often collaborate with AI at {h1}{h2}",
|
|
814
816
|
"report_persona_caption_second": " and {h2}",
|
|
815
|
-
"report_persona_peak": "Peak {count} messages",
|
|
817
|
+
"report_persona_peak": "Peak: {count} messages (incl. tool calls)",
|
|
816
818
|
"report_persona_empty": "Not enough data in this period yet",
|
|
817
819
|
"report_recent_titles_heading": "What you worked on",
|
|
818
820
|
"report_persona_section": "Usage habits",
|
|
@@ -1081,7 +1083,9 @@
|
|
|
1081
1083
|
"quota_aware_action_failed": "Could not change Quota-Aware Mode.",
|
|
1082
1084
|
"claude_beginner_error": "Beginner Mode: {error}",
|
|
1083
1085
|
"claude_desktop_updated": "Claude Desktop · updated {minutes}m ago",
|
|
1084
|
-
"claude_desktop_stale": "Claude Desktop · stale · updated {minutes}m ago"
|
|
1086
|
+
"claude_desktop_stale": "Claude Desktop · stale · updated {minutes}m ago",
|
|
1087
|
+
"report_project_others": "Other projects",
|
|
1088
|
+
"report_ledger_pct_note": "The Claude, Codex, and Antigravity columns are approximate shares of each tool’s weekly quota."
|
|
1085
1089
|
},
|
|
1086
1090
|
"zh-CN": {
|
|
1087
1091
|
"panel_default_name": "默认",
|
|
@@ -1344,8 +1348,8 @@
|
|
|
1344
1348
|
"report_fixed_range": "Fixed range",
|
|
1345
1349
|
"report_fixed_group_note": "以下区块不受上方日期筛选影响",
|
|
1346
1350
|
"report_model": "模型",
|
|
1347
|
-
"report_appendix_title": "
|
|
1348
|
-
"report_appendix_desc": "
|
|
1351
|
+
"report_appendix_title": "花费怎么算的",
|
|
1352
|
+
"report_appendix_desc": "公开 API 价格换算 · 花费可信度",
|
|
1349
1353
|
"report_narrative": "过去这段时间你烧了 {tokens} tokens。{peak_date} 是高峰({peak_tokens}/天),主力 LLM 是 {top_model}。",
|
|
1350
1354
|
"report_period": "区间",
|
|
1351
1355
|
"report_project": "项目",
|
|
@@ -1354,7 +1358,7 @@
|
|
|
1354
1358
|
"report_persona_active_hours": "活跃时段",
|
|
1355
1359
|
"report_persona_caption": "你最常在 {h1}{h2} 与 AI 协作",
|
|
1356
1360
|
"report_persona_caption_second": " 和 {h2}",
|
|
1357
|
-
"report_persona_peak": "峰值 {count}
|
|
1361
|
+
"report_persona_peak": "峰值 {count} 条消息(含工具调用)",
|
|
1358
1362
|
"report_persona_empty": "这段期间还没有足够数据",
|
|
1359
1363
|
"report_recent_titles_heading": "最近在做什么",
|
|
1360
1364
|
"report_persona_section": "使用习惯",
|
|
@@ -1623,7 +1627,9 @@
|
|
|
1623
1627
|
"quota_aware_action_failed": "无法切换额度感知模式。",
|
|
1624
1628
|
"claude_beginner_error": "新手模式:{error}",
|
|
1625
1629
|
"claude_desktop_updated": "Claude 桌面版 · {minutes} 分钟前更新",
|
|
1626
|
-
"claude_desktop_stale": "Claude 桌面版 · 数据过期 · {minutes} 分钟前更新"
|
|
1630
|
+
"claude_desktop_stale": "Claude 桌面版 · 数据过期 · {minutes} 分钟前更新",
|
|
1631
|
+
"report_project_others": "其他项目",
|
|
1632
|
+
"report_ledger_pct_note": "Claude、Codex、Antigravity 三列是约占该工具周额度的百分比。"
|
|
1627
1633
|
},
|
|
1628
1634
|
"ja": {
|
|
1629
1635
|
"panel_default_name": "デフォルト",
|
|
@@ -1886,8 +1892,8 @@
|
|
|
1886
1892
|
"report_fixed_range": "Fixed range",
|
|
1887
1893
|
"report_fixed_group_note": "以下のセクションは上の日付フィルターの影響を受けません",
|
|
1888
1894
|
"report_model": "モデル",
|
|
1889
|
-
"report_appendix_title": "
|
|
1890
|
-
"report_appendix_desc": "
|
|
1895
|
+
"report_appendix_title": "コストの算出方法",
|
|
1896
|
+
"report_appendix_desc": "公開 API 料金での換算 · コストの信頼度",
|
|
1891
1897
|
"report_narrative": "この期間に {tokens} tokens を使いました。ピークは {peak_date}({peak_tokens}/日)。主力モデル:{top_model}。",
|
|
1892
1898
|
"report_period": "期間",
|
|
1893
1899
|
"report_project": "プロジェクト",
|
|
@@ -1896,7 +1902,7 @@
|
|
|
1896
1902
|
"report_persona_active_hours": "アクティブな時間帯",
|
|
1897
1903
|
"report_persona_caption": "{h1}{h2} の時間帯に AI との共同作業が最も多い傾向があります",
|
|
1898
1904
|
"report_persona_caption_second": " と {h2}",
|
|
1899
|
-
"report_persona_peak": "
|
|
1905
|
+
"report_persona_peak": "ピーク: {count} 件のメッセージ(ツール呼び出しを含む)",
|
|
1900
1906
|
"report_persona_empty": "この期間はまだ十分なデータがありません",
|
|
1901
1907
|
"report_recent_titles_heading": "最近の作業",
|
|
1902
1908
|
"report_persona_section": "利用習慣",
|
|
@@ -2165,7 +2171,9 @@
|
|
|
2165
2171
|
"quota_aware_action_failed": "クォータ認識モードを切り替えられませんでした。",
|
|
2166
2172
|
"claude_beginner_error": "初心者モード:{error}",
|
|
2167
2173
|
"claude_desktop_updated": "Claude デスクトップ · {minutes}分前に更新",
|
|
2168
|
-
"claude_desktop_stale": "Claude デスクトップ · 古いデータ · {minutes}分前に更新"
|
|
2174
|
+
"claude_desktop_stale": "Claude デスクトップ · 古いデータ · {minutes}分前に更新",
|
|
2175
|
+
"report_project_others": "その他のプロジェクト",
|
|
2176
|
+
"report_ledger_pct_note": "Claude、Codex、Antigravity の各列は、各ツールの週間上限に対する推定使用率です。"
|
|
2169
2177
|
},
|
|
2170
2178
|
"ko": {
|
|
2171
2179
|
"panel_default_name": "기본",
|
|
@@ -2428,8 +2436,8 @@
|
|
|
2428
2436
|
"report_fixed_range": "Fixed range",
|
|
2429
2437
|
"report_fixed_group_note": "아래 섹션은 위의 날짜 필터와 관계없이 표시됩니다",
|
|
2430
2438
|
"report_model": "모델",
|
|
2431
|
-
"report_appendix_title": "
|
|
2432
|
-
"report_appendix_desc": "
|
|
2439
|
+
"report_appendix_title": "비용 계산 방식",
|
|
2440
|
+
"report_appendix_desc": "공개 API 가격 환산 · 비용 신뢰도",
|
|
2433
2441
|
"report_narrative": "이 기간 동안 {tokens} tokens을 사용했습니다. 최고점은 {peak_date}({peak_tokens}/일). 주력 모델: {top_model}.",
|
|
2434
2442
|
"report_period": "기간",
|
|
2435
2443
|
"report_project": "프로젝트",
|
|
@@ -2438,7 +2446,7 @@
|
|
|
2438
2446
|
"report_persona_active_hours": "활동 시간대",
|
|
2439
2447
|
"report_persona_caption": "주로 {h1}{h2}에 AI와 협업합니다",
|
|
2440
2448
|
"report_persona_caption_second": " 및 {h2}",
|
|
2441
|
-
"report_persona_peak": "최고 {count}
|
|
2449
|
+
"report_persona_peak": "최고 {count}개 메시지(도구 호출 포함)",
|
|
2442
2450
|
"report_persona_empty": "이 기간에는 아직 충분한 데이터가 없습니다",
|
|
2443
2451
|
"report_recent_titles_heading": "최근 작업",
|
|
2444
2452
|
"report_persona_section": "사용 습관",
|
|
@@ -2707,6 +2715,8 @@
|
|
|
2707
2715
|
"quota_aware_action_failed": "할당량 인식 모드를 변경할 수 없습니다.",
|
|
2708
2716
|
"claude_beginner_error": "초보자 모드: {error}",
|
|
2709
2717
|
"claude_desktop_updated": "Claude 데스크톱 · {minutes}분 전 업데이트",
|
|
2710
|
-
"claude_desktop_stale": "Claude 데스크톱 · 오래된 데이터 · {minutes}분 전 업데이트"
|
|
2718
|
+
"claude_desktop_stale": "Claude 데스크톱 · 오래된 데이터 · {minutes}분 전 업데이트",
|
|
2719
|
+
"report_project_others": "기타 프로젝트",
|
|
2720
|
+
"report_ledger_pct_note": "Claude, Codex, Antigravity 열은 각 도구의 주간 한도 대비 대략적인 사용 비율입니다."
|
|
2711
2721
|
}
|
|
2712
2722
|
}
|
|
@@ -76,6 +76,9 @@ _CLIENT_SECRET = "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"
|
|
|
76
76
|
_DEFAULT_RETRY_AFTER_SECONDS = 60.0
|
|
77
77
|
_MAX_RETRY_AFTER_SECONDS = 3600.0
|
|
78
78
|
_rate_limit_until_monotonic = 0.0
|
|
79
|
+
# When each group last appeared in a real API response; in memory only, reset on restart.
|
|
80
|
+
_group_seen_at: dict[str, datetime] = {}
|
|
81
|
+
_MISSING_GROUP_GRACE_MINUTES = 30
|
|
79
82
|
|
|
80
83
|
|
|
81
84
|
def _user_agent() -> str:
|
|
@@ -160,10 +163,38 @@ def load_quota(max_age_minutes: float = 5) -> AgyQuotaResult | None:
|
|
|
160
163
|
probed = probe_quota()
|
|
161
164
|
if probed is None:
|
|
162
165
|
return cached
|
|
166
|
+
probed = _carry_missing_groups(probed, cached)
|
|
163
167
|
_write_cache(probed)
|
|
164
168
|
return probed
|
|
165
169
|
|
|
166
170
|
|
|
171
|
+
def _carry_missing_groups(probed: AgyQuotaResult, cached: AgyQuotaResult | None) -> AgyQuotaResult:
|
|
172
|
+
"""Keep a group the API skipped this once, so the group toggle has something to show.
|
|
173
|
+
|
|
174
|
+
A group is carried only while the API last returned it within the grace window, so a
|
|
175
|
+
group Google really removes drops out instead of living on in the cache forever.
|
|
176
|
+
"""
|
|
177
|
+
now = datetime.now(UTC)
|
|
178
|
+
for group in probed.groups:
|
|
179
|
+
_group_seen_at[group.name] = now
|
|
180
|
+
if cached is None:
|
|
181
|
+
return probed
|
|
182
|
+
names = {group.name for group in probed.groups}
|
|
183
|
+
grace = timedelta(minutes=_MISSING_GROUP_GRACE_MINUTES)
|
|
184
|
+
missing = [
|
|
185
|
+
group
|
|
186
|
+
for group in cached.groups
|
|
187
|
+
if group.name not in names
|
|
188
|
+
and group.name in _group_seen_at
|
|
189
|
+
and now - _group_seen_at[group.name] <= grace
|
|
190
|
+
]
|
|
191
|
+
if not missing:
|
|
192
|
+
return probed
|
|
193
|
+
order = {group.name: index for index, group in enumerate(cached.groups)}
|
|
194
|
+
groups = sorted([*probed.groups, *missing], key=lambda group: order.get(group.name, 0))
|
|
195
|
+
return AgyQuotaResult(groups=groups, fetched_at=probed.fetched_at)
|
|
196
|
+
|
|
197
|
+
|
|
167
198
|
def _resolve_access_token(timeout: float) -> str | None:
|
|
168
199
|
"""Return a usable access token, refreshing when stale.
|
|
169
200
|
|