cctally 1.21.3 → 1.22.1
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.
- package/CHANGELOG.md +23 -0
- package/bin/_cctally_alerts.py +26 -1
- package/bin/_cctally_config.py +135 -0
- package/bin/_cctally_core.py +120 -0
- package/bin/_cctally_dashboard.py +155 -23
- package/bin/_cctally_db.py +3 -0
- package/bin/_cctally_parser.py +2541 -0
- package/bin/_cctally_record.py +148 -0
- package/bin/_cctally_share.py +1707 -0
- package/bin/_lib_alerts_payload.py +50 -0
- package/bin/_lib_budget.py +133 -0
- package/bin/_lib_doctor.py +74 -0
- package/bin/_lib_pricing.py +213 -13
- package/bin/_lib_pricing_check.py +201 -0
- package/bin/cctally +1263 -4266
- package/bin/cctally-budget +4 -0
- package/dashboard/static/assets/index-BxmaYT1y.css +1 -0
- package/dashboard/static/assets/index-CLcd-Tnm.js +18 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +6 -1
- package/dashboard/static/assets/index-BJ16SzRL.js +0 -18
- package/dashboard/static/assets/index-C1xH9GBW.css +0 -1
package/bin/_cctally_record.py
CHANGED
|
@@ -171,6 +171,7 @@ from _cctally_core import (
|
|
|
171
171
|
make_week_ref,
|
|
172
172
|
_get_alerts_config,
|
|
173
173
|
_AlertsConfigError,
|
|
174
|
+
_BudgetConfigError,
|
|
174
175
|
_command_as_of,
|
|
175
176
|
)
|
|
176
177
|
from _lib_five_hour import _canonical_5h_window_key
|
|
@@ -258,6 +259,30 @@ def _build_alert_payload_five_hour(*args, **kwargs):
|
|
|
258
259
|
return sys.modules["cctally"]._build_alert_payload_five_hour(*args, **kwargs)
|
|
259
260
|
|
|
260
261
|
|
|
262
|
+
def _build_alert_payload_budget(*args, **kwargs):
|
|
263
|
+
return sys.modules["cctally"]._build_alert_payload_budget(*args, **kwargs)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _get_budget_config(*args, **kwargs):
|
|
267
|
+
return sys.modules["cctally"]._get_budget_config(*args, **kwargs)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _budget_alerts_active(*args, **kwargs):
|
|
271
|
+
return sys.modules["cctally"]._budget_alerts_active(*args, **kwargs)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _resolve_current_budget_window(*args, **kwargs):
|
|
275
|
+
return sys.modules["cctally"]._resolve_current_budget_window(*args, **kwargs)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _sum_cost_for_range(*args, **kwargs):
|
|
279
|
+
return sys.modules["cctally"]._sum_cost_for_range(*args, **kwargs)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def insert_budget_milestone(*args, **kwargs):
|
|
283
|
+
return sys.modules["cctally"].insert_budget_milestone(*args, **kwargs)
|
|
284
|
+
|
|
285
|
+
|
|
261
286
|
def _dispatch_alert_notification(*args, **kwargs):
|
|
262
287
|
return sys.modules["cctally"]._dispatch_alert_notification(*args, **kwargs)
|
|
263
288
|
|
|
@@ -266,6 +291,10 @@ def _warn_alerts_bad_config_once(*args, **kwargs):
|
|
|
266
291
|
return sys.modules["cctally"]._warn_alerts_bad_config_once(*args, **kwargs)
|
|
267
292
|
|
|
268
293
|
|
|
294
|
+
def _warn_budget_bad_config_once(*args, **kwargs):
|
|
295
|
+
return sys.modules["cctally"]._warn_budget_bad_config_once(*args, **kwargs)
|
|
296
|
+
|
|
297
|
+
|
|
269
298
|
def _get_oauth_usage_config(*args, **kwargs):
|
|
270
299
|
return sys.modules["cctally"]._get_oauth_usage_config(*args, **kwargs)
|
|
271
300
|
|
|
@@ -644,6 +673,118 @@ def maybe_record_milestone(
|
|
|
644
673
|
conn.close()
|
|
645
674
|
|
|
646
675
|
|
|
676
|
+
def maybe_record_budget_milestone(saved: dict[str, Any]) -> None:
|
|
677
|
+
"""Fire equiv-$ budget alerts on ACTUAL-spend threshold crossings
|
|
678
|
+
(Approach A — called from ``cmd_record_usage`` alongside the weekly-% /
|
|
679
|
+
5h-% milestone helpers). Gated, hot-path-cheap, set-then-dispatch,
|
|
680
|
+
fire-once. Errors are logged, not raised (the caller also wraps).
|
|
681
|
+
|
|
682
|
+
``saved`` is accepted for call-site symmetry with
|
|
683
|
+
``maybe_record_milestone`` / ``maybe_update_five_hour_block`` but is
|
|
684
|
+
unused: the budget window + live spend are resolved from the DB +
|
|
685
|
+
``session_entries`` independently (a budget crossing depends on
|
|
686
|
+
cumulative equiv-$ spend, not on the just-recorded 7d-% snapshot).
|
|
687
|
+
"""
|
|
688
|
+
# Gate FIRST (hot-path discipline): no budget or alerts off → zero
|
|
689
|
+
# overhead for non-budget users. `load_config()` is safe outside any
|
|
690
|
+
# writer lock — atomic-rename guarantees whole-byte reads. A malformed
|
|
691
|
+
# budget block is a quiet warn-once no-op (mirrors weekly/5h), NOT an
|
|
692
|
+
# unthrottled per-tick stderr via the caller's wrapper.
|
|
693
|
+
try:
|
|
694
|
+
budget_cfg = _get_budget_config(load_config())
|
|
695
|
+
except _BudgetConfigError as exc:
|
|
696
|
+
_warn_budget_bad_config_once(exc)
|
|
697
|
+
return
|
|
698
|
+
if not _budget_alerts_active(budget_cfg):
|
|
699
|
+
return
|
|
700
|
+
target = budget_cfg["weekly_usd"]
|
|
701
|
+
thresholds = budget_cfg["alert_thresholds"]
|
|
702
|
+
if not thresholds:
|
|
703
|
+
return
|
|
704
|
+
|
|
705
|
+
now_utc = _command_as_of()
|
|
706
|
+
pending_alerts: list[dict[str, Any]] = []
|
|
707
|
+
conn = open_db()
|
|
708
|
+
try:
|
|
709
|
+
window = _resolve_current_budget_window(conn, now_utc)
|
|
710
|
+
if window is None:
|
|
711
|
+
return # no resolvable week window yet (spec §6 worst case)
|
|
712
|
+
week_start_at, _week_end_at = window
|
|
713
|
+
week_key = week_start_at.isoformat(timespec="seconds")
|
|
714
|
+
|
|
715
|
+
# Pre-probe (hot-path discipline + [Dedup mustn't gate side effects]):
|
|
716
|
+
# which configured thresholds are STILL un-recorded for this week?
|
|
717
|
+
# The cost SUM is skipped ONLY when every threshold already has a row
|
|
718
|
+
# — so a partial prior run that recorded some-but-not-all thresholds
|
|
719
|
+
# still gets the remaining ones a SUM + crossing-check. The skip never
|
|
720
|
+
# owes a crossing: an un-recorded threshold always forces the SUM.
|
|
721
|
+
present = {
|
|
722
|
+
int(r[0]) for r in conn.execute(
|
|
723
|
+
"SELECT threshold FROM budget_milestones WHERE week_start_at = ?",
|
|
724
|
+
(week_key,),
|
|
725
|
+
)
|
|
726
|
+
}
|
|
727
|
+
pending = [t for t in sorted(thresholds) if t not in present]
|
|
728
|
+
if not pending:
|
|
729
|
+
return # nothing left to cross this week → skip the cost SUM
|
|
730
|
+
|
|
731
|
+
spent = _sum_cost_for_range(week_start_at, now_utc, mode="auto")
|
|
732
|
+
# target > 0 is guaranteed by _get_budget_config (weekly_usd None is
|
|
733
|
+
# excluded by _budget_alerts_active above); the else is belt-and-suspenders.
|
|
734
|
+
consumption_pct = (spent / target * 100.0) if target > 0 else 0.0
|
|
735
|
+
for t in pending:
|
|
736
|
+
# +1e-9 snap-up: spent/target*100 can land one ULP below an
|
|
737
|
+
# integer threshold (CLAUDE.md float-floor gotcha).
|
|
738
|
+
if consumption_pct + 1e-9 >= t:
|
|
739
|
+
inserted = insert_budget_milestone(
|
|
740
|
+
conn,
|
|
741
|
+
week_start_at=week_key,
|
|
742
|
+
threshold=t,
|
|
743
|
+
budget_usd=target,
|
|
744
|
+
spent_usd=spent,
|
|
745
|
+
consumption_pct=consumption_pct,
|
|
746
|
+
commit=False,
|
|
747
|
+
)
|
|
748
|
+
# Only the genuine-new-crossing winner (rowcount==1) dispatches;
|
|
749
|
+
# a racing record-usage instance gets rowcount==0 and skips.
|
|
750
|
+
if inserted == 1:
|
|
751
|
+
crossed_at = now_utc_iso()
|
|
752
|
+
# set-then-dispatch: alerted_at lands on the row BEFORE
|
|
753
|
+
# the osascript Popen, sharing this transaction with the
|
|
754
|
+
# INSERT (commit=False) so a crash between them is
|
|
755
|
+
# impossible. `alerted_at IS NULL` guard is write-once
|
|
756
|
+
# defense-in-depth.
|
|
757
|
+
conn.execute(
|
|
758
|
+
"UPDATE budget_milestones SET alerted_at = ? "
|
|
759
|
+
"WHERE week_start_at = ? AND threshold = ? "
|
|
760
|
+
" AND alerted_at IS NULL",
|
|
761
|
+
(crossed_at, week_key, t),
|
|
762
|
+
)
|
|
763
|
+
pending_alerts.append(_build_alert_payload_budget(
|
|
764
|
+
threshold=t,
|
|
765
|
+
crossed_at_utc=crossed_at,
|
|
766
|
+
week_start_at=week_key,
|
|
767
|
+
budget_usd=target,
|
|
768
|
+
spent_usd=spent,
|
|
769
|
+
consumption_pct=consumption_pct,
|
|
770
|
+
))
|
|
771
|
+
# Single commit: every INSERT + its alerted_at marker durable together.
|
|
772
|
+
conn.commit()
|
|
773
|
+
except Exception as exc:
|
|
774
|
+
eprint(f"[budget-milestone] error recording budget milestone: {exc}")
|
|
775
|
+
finally:
|
|
776
|
+
conn.close()
|
|
777
|
+
|
|
778
|
+
# Dispatch AFTER commit; a dispatch failure NEVER rolls back the milestone
|
|
779
|
+
# (set-then-dispatch invariant — one queue attempt per crossing, deduped
|
|
780
|
+
# on the alerted_at column).
|
|
781
|
+
for payload in pending_alerts:
|
|
782
|
+
try:
|
|
783
|
+
_dispatch_alert_notification(payload, mode="real")
|
|
784
|
+
except Exception as dispatch_exc:
|
|
785
|
+
eprint(f"[budget-alerts] dispatch failed: {dispatch_exc}")
|
|
786
|
+
|
|
787
|
+
|
|
647
788
|
def _compute_block_totals(
|
|
648
789
|
block_start_at: dt.datetime,
|
|
649
790
|
range_end: dt.datetime,
|
|
@@ -2178,6 +2319,13 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
2178
2319
|
except Exception as exc:
|
|
2179
2320
|
eprint(f"[5h-block] unexpected error: {exc}")
|
|
2180
2321
|
|
|
2322
|
+
# NEW: equiv-$ budget alert firing (Approach A, issue #19). Gated on a
|
|
2323
|
+
# set budget + alerts_enabled FIRST — non-budget users pay zero overhead.
|
|
2324
|
+
try:
|
|
2325
|
+
maybe_record_budget_milestone(saved)
|
|
2326
|
+
except Exception as exc:
|
|
2327
|
+
eprint(f"[budget-milestone] unexpected error: {exc}")
|
|
2328
|
+
|
|
2181
2329
|
# Write high-water mark so the status line never displays a regression.
|
|
2182
2330
|
# The file contains "week_start_date weekly_percent" on one line.
|
|
2183
2331
|
try:
|