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.
Files changed (96) hide show
  1. {aiuse-3.2.1/src/aiuse.egg-info → aiuse-3.2.2}/PKG-INFO +1 -1
  2. {aiuse-3.2.1 → aiuse-3.2.2}/pyproject.toml +1 -1
  3. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/__init__.py +1 -1
  4. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/report.py +10 -2
  5. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/watch.py +11 -7
  6. {aiuse-3.2.1 → aiuse-3.2.2/src/aiuse.egg-info}/PKG-INFO +1 -1
  7. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_watch.py +26 -1
  8. {aiuse-3.2.1 → aiuse-3.2.2}/LICENSE +0 -0
  9. {aiuse-3.2.1 → aiuse-3.2.2}/README.md +0 -0
  10. {aiuse-3.2.1 → aiuse-3.2.2}/setup.cfg +0 -0
  11. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/__main__.py +0 -0
  12. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/agent_notes.py +0 -0
  13. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/ai_stub.py +0 -0
  14. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/analysis/__init__.py +0 -0
  15. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/analysis/history.py +0 -0
  16. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/analysis/local_runtimes.py +0 -0
  17. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/analysis/pace.py +0 -0
  18. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/analysis/selfdescribe.py +0 -0
  19. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/analysis/suggest.py +0 -0
  20. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/analysis/use_or_lose.py +0 -0
  21. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/attribute.py +0 -0
  22. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/chat_format.py +0 -0
  23. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/cli.py +0 -0
  24. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/__init__.py +0 -0
  25. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/bailian.py +0 -0
  26. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/base.py +0 -0
  27. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/caut.py +0 -0
  28. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/clinepass.py +0 -0
  29. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/codexbar.py +0 -0
  30. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/cswap.py +0 -0
  31. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/grok.py +0 -0
  32. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/hermes.py +0 -0
  33. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/muse.py +0 -0
  34. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/opencode_go.py +0 -0
  35. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/opencode_zen.py +0 -0
  36. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/openrouter.py +0 -0
  37. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/openusage.py +0 -0
  38. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/openusage_sh.py +0 -0
  39. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/qwencloud.py +0 -0
  40. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/runner.py +0 -0
  41. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/collectors/tokscale.py +0 -0
  42. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/config.py +0 -0
  43. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/credentials.py +0 -0
  44. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/ledger.py +0 -0
  45. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/macos_trust.py +0 -0
  46. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/models.py +0 -0
  47. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/sampler.py +0 -0
  48. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/secretspec.py +0 -0
  49. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/serve.py +0 -0
  50. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/tty.py +0 -0
  51. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/tui/__init__.py +0 -0
  52. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/tui/app.py +0 -0
  53. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse/tui/builders.py +0 -0
  54. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse.egg-info/SOURCES.txt +0 -0
  55. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse.egg-info/dependency_links.txt +0 -0
  56. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse.egg-info/entry_points.txt +0 -0
  57. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse.egg-info/requires.txt +0 -0
  58. {aiuse-3.2.1 → aiuse-3.2.2}/src/aiuse.egg-info/top_level.txt +0 -0
  59. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_attribution.py +0 -0
  60. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_bailian.py +0 -0
  61. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_base_run_json.py +0 -0
  62. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_caut_parse.py +0 -0
  63. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_chat_format.py +0 -0
  64. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_cli.py +0 -0
  65. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_clinepass.py +0 -0
  66. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_codexbar_parse.py +0 -0
  67. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_config.py +0 -0
  68. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_credentials.py +0 -0
  69. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_cswap_parse.py +0 -0
  70. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_grok.py +0 -0
  71. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_hermes_parse.py +0 -0
  72. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_history.py +0 -0
  73. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_install_deps.py +0 -0
  74. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_local_runtimes.py +0 -0
  75. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_macos_trust.py +0 -0
  76. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_models_cli_binary.py +0 -0
  77. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_models_pace.py +0 -0
  78. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_muse.py +0 -0
  79. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_opencode_go.py +0 -0
  80. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_opencode_zen.py +0 -0
  81. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_openrouter.py +0 -0
  82. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_openusage_parse.py +0 -0
  83. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_pace.py +0 -0
  84. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_qwencloud.py +0 -0
  85. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_release_script.py +0 -0
  86. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_report.py +0 -0
  87. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_runner_consolidation.py +0 -0
  88. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_selfdescribe.py +0 -0
  89. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_serve.py +0 -0
  90. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_shared_quota_semantics.py +0 -0
  91. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_snapshots_persistence.py +0 -0
  92. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_suggest.py +0 -0
  93. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_tokscale_parse.py +0 -0
  94. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_tty.py +0 -0
  95. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_tui.py +0 -0
  96. {aiuse-3.2.1 → aiuse-3.2.2}/tests/test_use_or_lose.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiuse
3
- Version: 3.2.1
3
+ Version: 3.2.2
4
4
  Summary: Aggregate AI subscription/API usage and flag use-it-or-lose-it allotments
5
5
  Author: djbclark
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aiuse"
7
- version = "3.2.1"
7
+ version = "3.2.2"
8
8
  description = "Aggregate AI subscription/API usage and flag use-it-or-lose-it allotments"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """Aggregate provider quotas and flag use-it-or-lose-it allotments."""
2
2
 
3
- __version__ = "3.2.1"
3
+ __version__ = "3.2.2"
@@ -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.isoformat()}"
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.isoformat()}"
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.astimezone().strftime('%H:%M:%S')}")
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.astimezone().strftime('%H:%M:%S')}")
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.astimezone().strftime('%H:%M:%S')}"
209
- f" · next {due.astimezone().strftime('%H:%M:%S')}{overdue} · {tier} tier"
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
- lines.append(render_clock_matrix(alerts, snapshot=snapshot, config=config, color=color).rstrip())
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.append(footer)
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiuse
3
- Version: 3.2.1
3
+ Version: 3.2.2
4
4
  Summary: Aggregate AI subscription/API usage and flag use-it-or-lose-it allotments
5
5
  Author: djbclark
6
6
  License-Expression: MIT
@@ -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.astimezone().strftime("%H:%M:%S")
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