usage-cli 0.29.32__py3-none-any.whl

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.
Files changed (109) hide show
  1. adapters/__init__.py +5 -0
  2. adapters/agy.py +68 -0
  3. adapters/claude.py +215 -0
  4. adapters/codex.py +209 -0
  5. adapters/rate_limits.py +76 -0
  6. adapters/registry.py +17 -0
  7. adapters/types.py +139 -0
  8. agy_disk_cache.py +135 -0
  9. agy_loader.py +416 -0
  10. agy_quota_probe.py +748 -0
  11. agy_window_keeper.py +185 -0
  12. analyzer/__init__.py +5 -0
  13. analyzer/aggregator.py +139 -0
  14. analyzer/blocks.py +80 -0
  15. analyzer/diagnoser.py +638 -0
  16. analyzer/insights.py +277 -0
  17. analyzer/persona_loader.py +199 -0
  18. analyzer/reporter.py +989 -0
  19. analyzer/subscription.py +108 -0
  20. burn_rate.py +75 -0
  21. cache_quarantine.py +50 -0
  22. codex_disk_cache.py +227 -0
  23. codex_events.py +136 -0
  24. codex_fork_replay.py +111 -0
  25. codex_loader.py +1426 -0
  26. codex_paths.py +20 -0
  27. critter_frames.py +26 -0
  28. discussion_bridge.py +1196 -0
  29. discussion_cli.py +844 -0
  30. discussion_session.py +622 -0
  31. discussion_usage.py +13 -0
  32. discussion_window.py +955 -0
  33. disk_cache_common.py +132 -0
  34. disk_cache_lifecycle.py +39 -0
  35. doctor.py +452 -0
  36. fsevents_watch.py +207 -0
  37. history_disk_cache.py +110 -0
  38. history_loader.py +416 -0
  39. i18n.py +88 -0
  40. jsonl_limits.py +17 -0
  41. jsonl_utils.py +40 -0
  42. login_item.py +154 -0
  43. main.py +387 -0
  44. menubar.py +1201 -0
  45. menubar_actions.py +204 -0
  46. menubar_agy.py +193 -0
  47. menubar_chrome.py +156 -0
  48. menubar_menu.py +169 -0
  49. menubar_notify.py +102 -0
  50. menubar_popover.py +233 -0
  51. menubar_prefs.py +118 -0
  52. menubar_refresh.py +285 -0
  53. menubar_state.py +1200 -0
  54. menubar_title.py +157 -0
  55. menubar_update.py +123 -0
  56. panel_window.py +78 -0
  57. panel_window_state.py +159 -0
  58. panels/__init__.py +186 -0
  59. panels/base.py +83 -0
  60. panels/dynamic_height.py +140 -0
  61. panels/payload.py +178 -0
  62. panels/web_panel.py +513 -0
  63. panels/window_drag.py +56 -0
  64. prefs.py +44 -0
  65. pricing.py +452 -0
  66. project_resolver.py +112 -0
  67. service_status.py +383 -0
  68. session_hooks.py +1154 -0
  69. setup_app.py +171 -0
  70. setup_hook.py +1011 -0
  71. statusline_settings.py +160 -0
  72. talent_market_bridge.py +243 -0
  73. time_utils.py +24 -0
  74. tui.py +288 -0
  75. tui_sprite.py +206 -0
  76. ui/__init__.py +5 -0
  77. ui/html_report.py +923 -0
  78. ui/report_scripts.py +251 -0
  79. ui/report_styles.py +370 -0
  80. ui/tables.py +888 -0
  81. update_checker.py +156 -0
  82. update_gate.py +66 -0
  83. update_release_notes.py +49 -0
  84. usage_cli-0.29.32.data/data/share/usage/i18n.json +2427 -0
  85. usage_cli-0.29.32.dist-info/METADATA +223 -0
  86. usage_cli-0.29.32.dist-info/RECORD +109 -0
  87. usage_cli-0.29.32.dist-info/WHEEL +5 -0
  88. usage_cli-0.29.32.dist-info/entry_points.txt +3 -0
  89. usage_cli-0.29.32.dist-info/licenses/LICENSE +663 -0
  90. usage_cli-0.29.32.dist-info/top_level.txt +80 -0
  91. usage_cli.py +827 -0
  92. usage_client.py +487 -0
  93. usage_diagnosis_snapshot.py +143 -0
  94. usage_dir_sweeper.py +100 -0
  95. usage_lang.py +79 -0
  96. usage_logging.py +75 -0
  97. usage_notifications.py +96 -0
  98. usage_rate.py +97 -0
  99. usage_session_resume.py +913 -0
  100. usage_statusline.py +810 -0
  101. usage_statusline_agy.py +397 -0
  102. usage_statusline_forwarder.py +88 -0
  103. usage_terse_mode.py +223 -0
  104. usage_terse_reminder.py +151 -0
  105. win_login_item.py +53 -0
  106. window_keeper.py +264 -0
  107. windows_watch.py +443 -0
  108. wintray.py +2014 -0
  109. wintray_menu.py +136 -0
menubar_refresh.py ADDED
@@ -0,0 +1,285 @@
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
+ """Background-safe refresh loading and popover-state construction."""
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import logging
13
+ import os
14
+ import time
15
+ from collections.abc import Coroutine
16
+ from dataclasses import dataclass
17
+ from typing import Any, Protocol
18
+
19
+ import agy_window_keeper
20
+ import menubar_agy
21
+ import menubar_state
22
+ import talent_market_bridge
23
+ import window_keeper
24
+ from burn_rate import BurnRateTracker
25
+ from history_loader import UsageEntry
26
+ from menubar_prefs import (
27
+ _hide_agy_enabled,
28
+ _hide_claude_enabled,
29
+ _hide_codex_enabled,
30
+ _quota_card_order,
31
+ )
32
+ from service_status import CLAUDE_STATUS, CODEX_STATUS, get_service_status
33
+ from usage_client import PollOutcome, PollState
34
+ from usage_rate import UsageRateTracker
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ type ProjectRows = list[tuple[str, int, float | None]]
39
+
40
+
41
+ class _RefreshApp(Protocol):
42
+ language: str
43
+ mock: bool
44
+ tracker: UsageRateTracker
45
+ codex_tracker: UsageRateTracker
46
+ agy_tracker: UsageRateTracker
47
+ burn_rate_trackers: dict[str, BurnRateTracker]
48
+ latest_state: menubar_state.PopoverState
49
+ active_panel: Any
50
+ _history_load_error_key: str | None
51
+
52
+ def _fetch(self) -> Coroutine[Any, Any, PollOutcome]: ...
53
+
54
+ def _load_codex_refresh_result(self) -> dict[str, Any]: ...
55
+
56
+ def _load_history_entries(
57
+ self, *, scan: menubar_state.HistorySourceScan | None = None
58
+ ) -> list[UsageEntry]: ...
59
+
60
+ def _project_rows(
61
+ self, hours_back: int = 24, entries: list[UsageEntry] | None = None
62
+ ) -> ProjectRows: ...
63
+
64
+ def _statusline_setup_available(self) -> bool: ...
65
+
66
+
67
+ @dataclass(slots=True)
68
+ class RefreshSources:
69
+ codex_result: dict[str, Any]
70
+ history_scan: menubar_state.HistorySourceScan | None
71
+ agy_result: menubar_agy.AgyRefreshResult
72
+ agy_projection: menubar_agy.AgyQuotaProjection
73
+ animation_groups: tuple[int, int, int]
74
+ debug_timing: bool
75
+
76
+ def measure(self, stage: str, started_at: float) -> None:
77
+ if self.debug_timing:
78
+ elapsed_ms = (time.monotonic() - started_at) * 1000
79
+ logger.debug("refresh_timing stage=%s elapsed_ms=%.1f", stage, elapsed_ms)
80
+
81
+
82
+ def load_sources(app: _RefreshApp) -> RefreshSources:
83
+ debug_timing = os.environ.get("USAGE_DEBUG") == "1"
84
+ animation_groups = (
85
+ int(getattr(app, "critter_group", 0)),
86
+ int(getattr(app, "dragon_group", 0)),
87
+ int(getattr(app, "lion_group", 0)),
88
+ )
89
+ started_at = time.monotonic() if debug_timing else 0.0
90
+ codex_result = app._load_codex_refresh_result()
91
+ history_scan = codex_result.get("_history_scan")
92
+ if debug_timing:
93
+ elapsed_ms = (time.monotonic() - started_at) * 1000
94
+ logger.debug("refresh_timing stage=%s elapsed_ms=%.1f", "codex_load", elapsed_ms)
95
+ started_at = time.monotonic() if debug_timing else 0.0
96
+ agy_result = menubar_agy.load_refresh_result(app.language)
97
+ if debug_timing:
98
+ elapsed_ms = (time.monotonic() - started_at) * 1000
99
+ logger.debug("refresh_timing stage=%s elapsed_ms=%.1f", "agy_load", elapsed_ms)
100
+ agy_projection = agy_result.projection or menubar_agy.fallback_projection(app.language)
101
+ return RefreshSources(
102
+ codex_result=codex_result,
103
+ history_scan=(
104
+ history_scan if isinstance(history_scan, menubar_state.HistorySourceScan) else None
105
+ ),
106
+ agy_result=agy_result,
107
+ agy_projection=agy_projection,
108
+ animation_groups=animation_groups,
109
+ debug_timing=debug_timing,
110
+ )
111
+
112
+
113
+ def build_result(app: _RefreshApp, sources: RefreshSources) -> dict[str, Any]:
114
+ codex_result = sources.codex_result
115
+ agy_result = sources.agy_result
116
+ agy_projection = sources.agy_projection
117
+ animation_groups = sources.animation_groups
118
+ fallback_state = getattr(app, "latest_state", menubar_state._empty_state(app.language))
119
+ project_rows = list(fallback_state.projects)
120
+ project_rows_yesterday = list(fallback_state.projects_yesterday)
121
+ project_rows_7d = list(fallback_state.projects_7d)
122
+ project_rows_30d = list(fallback_state.projects_30d)
123
+ project_rows_all = list(fallback_state.projects_all)
124
+ today_text = fallback_state.today_text
125
+ yesterday_text = fallback_state.yesterday_text
126
+ statusline = fallback_state.statusline
127
+ hide_claude = fallback_state.hide_claude
128
+ hide_codex = fallback_state.hide_codex
129
+ hide_agy = agy_result.hide_agy or _hide_agy_enabled()
130
+ card_order = _quota_card_order()
131
+ try:
132
+ started_at = time.monotonic() if sources.debug_timing else 0.0
133
+ if sources.history_scan is not None:
134
+ all_entries = app._load_history_entries(scan=sources.history_scan)
135
+ else:
136
+ all_entries = app._load_history_entries()
137
+ sources.measure("history_load", started_at)
138
+ started_at = time.monotonic() if sources.debug_timing else 0.0
139
+ if app.mock:
140
+ project_rows = app._project_rows(hours_back=24, entries=all_entries)
141
+ project_rows_yesterday = project_rows
142
+ project_rows_7d = app._project_rows(hours_back=168, entries=all_entries)
143
+ project_rows_30d = app._project_rows(hours_back=720, entries=all_entries)
144
+ project_rows_all = app._project_rows(hours_back=0, entries=all_entries)
145
+ else:
146
+ (
147
+ project_rows,
148
+ project_rows_yesterday,
149
+ project_rows_7d,
150
+ project_rows_30d,
151
+ project_rows_all,
152
+ ) = menubar_state.project_rows_for_windows(all_entries)
153
+ sources.measure("project_rows_windows", started_at)
154
+ today_text = menubar_state._today_title(app.mock, app.language, entries=all_entries)
155
+ yesterday_text = menubar_state._yesterday_title(
156
+ app.mock, app.language, entries=all_entries
157
+ )
158
+ statusline = menubar_state._statusline_payload(app.language)
159
+ hide_claude = _hide_claude_enabled()
160
+ hide_codex = _hide_codex_enabled()
161
+ hide_agy = agy_result.hide_agy or _hide_agy_enabled()
162
+ except Exception:
163
+ if os.environ.get("USAGE_DEBUG") == "1":
164
+ logger.warning("local usage refresh failed", exc_info=True)
165
+ try:
166
+ started_at = time.monotonic() if sources.debug_timing else 0.0
167
+ outcome = asyncio.run(app._fetch())
168
+ sources.measure("fetch", started_at)
169
+ codex_rows = codex_result["codex_rows"]
170
+ codex_5h_pct = codex_result["codex_5h_pct"]
171
+ codex_model = codex_result.get("codex_model", "unknown")
172
+ codex_stale = codex_result.get("codex_stale")
173
+ codex_credits = codex_result.get("codex_credits")
174
+ show_install_button = (
175
+ not hide_claude
176
+ and outcome.state == PollState.TOKEN_ERROR
177
+ and app._statusline_setup_available()
178
+ )
179
+ service_statuses = (
180
+ get_service_status(CLAUDE_STATUS),
181
+ get_service_status(CODEX_STATUS),
182
+ )
183
+ group = int(app.tracker.group())
184
+ try:
185
+ codex_group = int(app.codex_tracker.group())
186
+ except Exception:
187
+ codex_group = 0
188
+ if os.environ.get("USAGE_DEBUG") == "1":
189
+ logger.warning("Codex animation group load failed", exc_info=True)
190
+ try:
191
+ agy_group = int(app.agy_tracker.group())
192
+ except Exception:
193
+ agy_group = 0
194
+ if os.environ.get("USAGE_DEBUG") == "1":
195
+ logger.warning("Antigravity animation group load failed", exc_info=True)
196
+ animation_groups = (
197
+ group,
198
+ codex_group,
199
+ agy_group,
200
+ )
201
+ state = menubar_state.build_popover_state(
202
+ outcome=outcome,
203
+ codex_rows=codex_rows,
204
+ agy_rows=(agy_projection.session, agy_projection.weekly),
205
+ agy_group_name=agy_projection.group_name,
206
+ projects=project_rows,
207
+ projects_yesterday=project_rows_yesterday,
208
+ projects_7d=project_rows_7d,
209
+ projects_30d=project_rows_30d,
210
+ projects_all=project_rows_all,
211
+ language=app.language,
212
+ group=group,
213
+ burn_rate_trackers=app.burn_rate_trackers,
214
+ today_text=today_text,
215
+ yesterday_text=yesterday_text,
216
+ statusline=statusline,
217
+ show_install_button=show_install_button,
218
+ hide_claude=hide_claude,
219
+ hide_codex=hide_codex,
220
+ hide_agy=hide_agy,
221
+ codex_stale=codex_stale,
222
+ codex_credits=codex_credits,
223
+ agy_stale=agy_projection.stale,
224
+ card_order=card_order,
225
+ history_error=menubar_state.history_load_error_state(
226
+ app._history_load_error_key, app.language
227
+ ),
228
+ service_statuses=service_statuses,
229
+ )
230
+ if outcome.snapshot is not None:
231
+ window_keeper.maybe_ping(
232
+ outcome.snapshot.current_reset_at,
233
+ outcome.snapshot.current_percent,
234
+ outcome.snapshot.data_source,
235
+ app.mock,
236
+ )
237
+ agy_window_keeper.maybe_ping(agy_result, app.mock)
238
+ except Exception as exc:
239
+ if os.environ.get("USAGE_DEBUG") == "1":
240
+ logger.warning("refresh failed", exc_info=True)
241
+ codex_rows = codex_result["codex_rows"]
242
+ codex_5h_pct = codex_result["codex_5h_pct"]
243
+ codex_model = codex_result.get("codex_model", "unknown")
244
+ state = menubar_state._error_state(type(exc).__name__, app.mock, app.language)
245
+ state.codex_session = codex_rows[0]
246
+ state.codex_weekly = codex_rows[1]
247
+ state.codex_stale = codex_result.get("codex_stale")
248
+ state.agy_session = agy_projection.session
249
+ state.agy_weekly = agy_projection.weekly
250
+ state.agy_group_name = agy_projection.group_name
251
+ state.agy_stale = agy_projection.stale
252
+ state.history_error = menubar_state.history_load_error_state(
253
+ app._history_load_error_key, app.language
254
+ )
255
+ state.projects = project_rows
256
+ state.projects_yesterday = project_rows_yesterday
257
+ state.projects_7d = project_rows_7d
258
+ state.projects_30d = project_rows_30d
259
+ state.projects_all = project_rows_all
260
+ state.today_text = today_text
261
+ state.yesterday_text = yesterday_text
262
+ state.statusline = statusline
263
+ state.hide_claude = hide_claude
264
+ state.hide_codex = hide_codex
265
+ state.hide_agy = hide_agy
266
+ state.card_order = card_order
267
+
268
+ # Talent-market data is panel-local (no quota numbers). Fetch it
269
+ # only when that panel is active so classic/matrix users never pay
270
+ # the subprocess cost. list_state already swallows CLI errors and
271
+ # returns {ok:False,...}, so the panel shows its empty state.
272
+ active_panel = getattr(app, "active_panel", None)
273
+ if active_panel is not None and active_panel.id == "talent_market":
274
+ try:
275
+ state.talent = talent_market_bridge.list_state(app.language)
276
+ except Exception:
277
+ if os.environ.get("USAGE_DEBUG") == "1":
278
+ logger.warning("talent market state load failed", exc_info=True)
279
+
280
+ return {
281
+ "state": state,
282
+ "codex_5h_pct": codex_5h_pct,
283
+ "codex_model": codex_model,
284
+ "animation_groups": animation_groups,
285
+ }