aiuse 3.2.1__tar.gz → 3.2.2__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.
- {aiuse-3.2.1/src/aiuse.egg-info → aiuse-3.2.2}/PKG-INFO +1 -1
- {aiuse-3.2.1 → aiuse-3.2.2}/pyproject.toml +1 -1
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/__init__.py +1 -1
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/report.py +10 -2
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/watch.py +11 -7
- {aiuse-3.2.1 → aiuse-3.2.2/src/aiuse.egg-info}/PKG-INFO +1 -1
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_watch.py +26 -1
- {aiuse-3.2.1 → aiuse-3.2.2}/LICENSE +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/README.md +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/setup.cfg +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/__main__.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/agent_notes.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/ai_stub.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/analysis/__init__.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/analysis/history.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/analysis/local_runtimes.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/analysis/pace.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/analysis/selfdescribe.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/analysis/suggest.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/analysis/use_or_lose.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/attribute.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/chat_format.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/cli.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/__init__.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/bailian.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/base.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/caut.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/clinepass.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/codexbar.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/cswap.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/grok.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/hermes.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/muse.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/opencode_go.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/opencode_zen.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/openrouter.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/openusage.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/openusage_sh.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/qwencloud.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/runner.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/tokscale.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/config.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/credentials.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/ledger.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/macos_trust.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/models.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/sampler.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/secretspec.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/serve.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/tty.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/tui/__init__.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/tui/app.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/tui/builders.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse.egg-info/SOURCES.txt +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse.egg-info/dependency_links.txt +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse.egg-info/entry_points.txt +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse.egg-info/requires.txt +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse.egg-info/top_level.txt +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_attribution.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_bailian.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_base_run_json.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_caut_parse.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_chat_format.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_cli.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_clinepass.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_codexbar_parse.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_config.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_credentials.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_cswap_parse.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_grok.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_hermes_parse.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_history.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_install_deps.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_local_runtimes.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_macos_trust.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_models_cli_binary.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_models_pace.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_muse.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_opencode_go.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_opencode_zen.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_openrouter.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_openusage_parse.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_pace.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_qwencloud.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_release_script.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_report.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_runner_consolidation.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_selfdescribe.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_serve.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_shared_quota_semantics.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_snapshots_persistence.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_suggest.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_tokscale_parse.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_tty.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_tui.py +0 -0
- {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_use_or_lose.py +0 -0
|
@@ -280,7 +280,7 @@ def render_report(
|
|
|
280
280
|
lines.append(s.bold("=" * width))
|
|
281
281
|
title = "AI USAGE — USE IT OR LOSE IT (full)"
|
|
282
282
|
lines.append(s.bold(s.cyan(title)))
|
|
283
|
-
meta = f"Collected at {snapshot.collected_at
|
|
283
|
+
meta = f"Collected at {format_clock(snapshot.collected_at, date=True)}"
|
|
284
284
|
meta += f" · {n_accounts} account{'s' if n_accounts != 1 else ''}"
|
|
285
285
|
if n_actionable:
|
|
286
286
|
meta += f" · {n_actionable} alert{'s' if n_actionable != 1 else ''}"
|
|
@@ -1703,6 +1703,14 @@ def _priority_account_line(
|
|
|
1703
1703
|
return f"{_priority_tag(s, band)} {body}"
|
|
1704
1704
|
|
|
1705
1705
|
|
|
1706
|
+
def format_clock(when: datetime, *, seconds: bool = False, date: bool = False) -> str:
|
|
1707
|
+
"""Local 12-hour time as people say it: ``9:02pm``, ``9:02:07pm``, ``Sat Oct 5 9:02pm``."""
|
|
1708
|
+
local = when.astimezone()
|
|
1709
|
+
clock = f"{local.hour % 12 or 12}:{local:%M}" + (f":{local:%S}" if seconds else "")
|
|
1710
|
+
clock += "am" if local.hour < 12 else "pm"
|
|
1711
|
+
return f"{local:%a %b} {local.day} {clock}" if date else clock
|
|
1712
|
+
|
|
1713
|
+
|
|
1706
1714
|
def render_stderr_meta(
|
|
1707
1715
|
snapshot: Snapshot,
|
|
1708
1716
|
alerts: list[UseOrLoseAlert],
|
|
@@ -1715,7 +1723,7 @@ def render_stderr_meta(
|
|
|
1715
1723
|
n_accounts = len(accounts)
|
|
1716
1724
|
n_actionable = sum(1 for a in alerts if a.urgency not in (Urgency.INFO, Urgency.NONE))
|
|
1717
1725
|
lines: list[str] = []
|
|
1718
|
-
meta = f"Collected at {snapshot.collected_at
|
|
1726
|
+
meta = f"Collected at {format_clock(snapshot.collected_at, date=True)}"
|
|
1719
1727
|
meta += f" · {n_accounts} account{'s' if n_accounts != 1 else ''}"
|
|
1720
1728
|
if n_actionable:
|
|
1721
1729
|
meta += f" · {n_actionable} alert{'s' if n_actionable != 1 else ''}"
|
|
@@ -20,7 +20,7 @@ from aiuse.analysis.local_runtimes import maybe_local_runtime_alerts
|
|
|
20
20
|
from aiuse.analysis.use_or_lose import analyze_use_or_lose
|
|
21
21
|
from aiuse.collectors.runner import run_collectors
|
|
22
22
|
from aiuse.models import Snapshot, UseOrLoseAlert, utcnow
|
|
23
|
-
from aiuse.report import render_clock_matrix, render_stderr_meta
|
|
23
|
+
from aiuse.report import _strip_ansi, format_clock, render_clock_matrix, render_stderr_meta
|
|
24
24
|
|
|
25
25
|
DEFAULT_INTERVAL_S = 600.0
|
|
26
26
|
MIN_INTERVAL_S = 5.0
|
|
@@ -188,10 +188,10 @@ def render_watch_board(
|
|
|
188
188
|
"""
|
|
189
189
|
header_bits = ["aiuse watch"]
|
|
190
190
|
if now is not None:
|
|
191
|
-
header_bits.append(f"now: {now
|
|
191
|
+
header_bits.append(f"now: {format_clock(now, seconds=True)}")
|
|
192
192
|
data_at = snapshot.collected_at if snapshot is not None else last_at
|
|
193
193
|
if data_at is not None:
|
|
194
|
-
header_bits.append(f"last: {data_at
|
|
194
|
+
header_bits.append(f"last: {format_clock(data_at, seconds=True)}")
|
|
195
195
|
else:
|
|
196
196
|
header_bits.append("last: —")
|
|
197
197
|
if collecting_for is not None:
|
|
@@ -205,17 +205,21 @@ def render_watch_board(
|
|
|
205
205
|
previous, due, tier = sample_schedule
|
|
206
206
|
overdue = " (due)" if now is not None and due <= now else ""
|
|
207
207
|
lines.append(
|
|
208
|
-
f"sampler: previous {previous
|
|
209
|
-
f" · next {due
|
|
208
|
+
f"sampler: previous {format_clock(previous, seconds=True)}"
|
|
209
|
+
f" · next {format_clock(due, seconds=True)}{overdue} · {tier} tier"
|
|
210
210
|
)
|
|
211
211
|
if error:
|
|
212
212
|
lines.append(f"collect error: {error}")
|
|
213
213
|
if snapshot is not None:
|
|
214
|
-
|
|
214
|
+
matrix = render_clock_matrix(alerts, snapshot=snapshot, config=config, color=color).rstrip()
|
|
215
|
+
lines.append(matrix)
|
|
215
216
|
if not quiet:
|
|
216
217
|
footer = render_stderr_meta(snapshot, alerts, color=color).rstrip()
|
|
217
218
|
if footer:
|
|
218
|
-
lines.
|
|
219
|
+
# Centered under the table, like the legend lines above it.
|
|
220
|
+
width = max((len(_strip_ansi(row)) for row in matrix.splitlines()), default=0)
|
|
221
|
+
for row in footer.splitlines():
|
|
222
|
+
lines.append(" " * max(0, (width - len(_strip_ansi(row))) // 2) + row)
|
|
219
223
|
else:
|
|
220
224
|
lines.append("waiting for first collection…")
|
|
221
225
|
return "\n".join(lines)
|
|
@@ -344,6 +344,7 @@ def test_board_header_shows_now_data_time_and_the_sampler_schedule():
|
|
|
344
344
|
from datetime import datetime, timedelta, timezone
|
|
345
345
|
|
|
346
346
|
from aiuse import sampler
|
|
347
|
+
from aiuse.report import format_clock
|
|
347
348
|
from aiuse.watch import render_watch_board
|
|
348
349
|
|
|
349
350
|
snap = _snap()
|
|
@@ -356,7 +357,7 @@ def test_board_header_shows_now_data_time_and_the_sampler_schedule():
|
|
|
356
357
|
header, sampler_line = board.splitlines()[:2]
|
|
357
358
|
|
|
358
359
|
def hms(value):
|
|
359
|
-
return value
|
|
360
|
+
return format_clock(value, seconds=True)
|
|
360
361
|
|
|
361
362
|
assert f"now: {hms(now)}" in header and f"last: {hms(snap.collected_at)}" in header
|
|
362
363
|
assert sampler_line == f"sampler: previous {hms(snap.collected_at)} · next {hms(schedule[1])} · active tier"
|
|
@@ -393,3 +394,27 @@ def test_watch_frame_overlays_newer_burst_samples_on_the_full_snapshot(monkeypat
|
|
|
393
394
|
used = {a.provider: a.windows[0].used_percent for a in snapshot.accounts}
|
|
394
395
|
assert used == {"clinepass": 47, "codex": 10}
|
|
395
396
|
assert seen == [snapshot], "alerts are re-derived from the overlaid readings"
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def test_format_clock_is_twelve_hour_without_leading_zeros():
|
|
400
|
+
from datetime import datetime
|
|
401
|
+
|
|
402
|
+
from aiuse.report import format_clock
|
|
403
|
+
|
|
404
|
+
evening = datetime(2026, 10, 3, 21, 2, 7).astimezone()
|
|
405
|
+
assert format_clock(evening) == "9:02pm"
|
|
406
|
+
assert format_clock(evening, seconds=True) == "9:02:07pm"
|
|
407
|
+
assert format_clock(evening, date=True) == "Sat Oct 3 9:02pm"
|
|
408
|
+
assert format_clock(datetime(2026, 10, 3, 0, 5).astimezone()) == "12:05am"
|
|
409
|
+
assert format_clock(datetime(2026, 10, 3, 12, 0).astimezone()) == "12:00pm"
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def test_board_footer_is_centered_under_the_table():
|
|
413
|
+
from aiuse.watch import render_watch_board
|
|
414
|
+
|
|
415
|
+
rows = render_watch_board(_snap(), [], color=False).splitlines()
|
|
416
|
+
width = max(len(r) for r in rows[: rows.index(next(r for r in rows if "Collected at" in r))] if "watch" not in r)
|
|
417
|
+
meta = next(r for r in rows if "Collected at" in r)
|
|
418
|
+
left = len(meta) - len(meta.lstrip())
|
|
419
|
+
assert left == max(0, (width - len(meta.strip())) // 2)
|
|
420
|
+
assert "am ·" in meta or "pm ·" in meta
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|