cctally 1.80.4 → 1.81.0
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 +24 -0
- package/bin/_cctally_account.py +397 -0
- package/bin/_cctally_alerts.py +58 -2
- package/bin/_cctally_cache.py +696 -166
- package/bin/_cctally_cache_report.py +21 -3
- package/bin/_cctally_config.py +119 -8
- package/bin/_cctally_core.py +343 -48
- package/bin/_cctally_dashboard.py +32 -7
- package/bin/_cctally_dashboard_conversation.py +6 -2
- package/bin/_cctally_dashboard_envelope.py +49 -0
- package/bin/_cctally_dashboard_share.py +78 -1
- package/bin/_cctally_dashboard_sources.py +434 -48
- package/bin/_cctally_db.py +659 -152
- package/bin/_cctally_diff.py +9 -0
- package/bin/_cctally_doctor.py +201 -26
- package/bin/_cctally_five_hour.py +110 -69
- package/bin/_cctally_forecast.py +112 -43
- package/bin/_cctally_journal.py +591 -80
- package/bin/_cctally_milestones.py +180 -67
- package/bin/_cctally_parser.py +87 -0
- package/bin/_cctally_percent_breakdown.py +24 -15
- package/bin/_cctally_project.py +12 -1
- package/bin/_cctally_quota.py +150 -39
- package/bin/_cctally_record.py +491 -141
- package/bin/_cctally_reporting.py +66 -14
- package/bin/_cctally_setup.py +9 -1
- package/bin/_cctally_source_analytics.py +56 -7
- package/bin/_cctally_statusline.py +55 -8
- package/bin/_cctally_store.py +20 -8
- package/bin/_cctally_sync_week.py +30 -10
- package/bin/_cctally_tui.py +99 -18
- package/bin/_cctally_weekrefs.py +38 -15
- package/bin/_lib_accounts.py +325 -0
- package/bin/_lib_alerts_payload.py +25 -4
- package/bin/_lib_cache_writer_lock.py +77 -0
- package/bin/_lib_codex_hooks.py +40 -1
- package/bin/_lib_diff_kernel.py +28 -14
- package/bin/_lib_doctor.py +160 -0
- package/bin/_lib_journal.py +65 -2
- package/bin/_lib_quota.py +81 -4
- package/bin/_lib_render.py +19 -5
- package/bin/_lib_share.py +25 -0
- package/bin/_lib_snapshot_cache.py +8 -0
- package/bin/_lib_subscription_weeks.py +16 -2
- package/bin/_lib_view_models.py +18 -9
- package/bin/cctally +43 -4
- package/dashboard/static/assets/index-DJP4gEB7.js +92 -0
- package/dashboard/static/assets/index-Dk1nplOz.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +4 -1
- package/dashboard/static/assets/index-BzWujBS3.js +0 -92
- package/dashboard/static/assets/index-Cc2ykqF_.css +0 -1
|
@@ -56,12 +56,18 @@ def _resolve_block_selector(
|
|
|
56
56
|
*,
|
|
57
57
|
block_start: str | None,
|
|
58
58
|
ago: int | None,
|
|
59
|
+
account_key: "str | None" = None,
|
|
59
60
|
) -> dict | None:
|
|
60
61
|
"""Resolve a five-hour-breakdown selector to one ``five_hour_blocks`` row.
|
|
61
62
|
|
|
62
63
|
Returns a dict-mapped ``sqlite3.Row`` (or ``None`` if no block matches).
|
|
63
64
|
Raises ``ValueError`` on conflicting / malformed input.
|
|
64
65
|
|
|
66
|
+
``account_key`` (#341, spec §3): ``None`` = merged / byte-stable; a real key /
|
|
67
|
+
``unattributed`` scopes selection to that account's blocks — required because
|
|
68
|
+
``five_hour_window_key`` is shared across accounts (one physical window can
|
|
69
|
+
own one block per account).
|
|
70
|
+
|
|
65
71
|
Selector rules (spec §3.1):
|
|
66
72
|
* Both ``None`` -> most-recent block (highest ``block_start_at``).
|
|
67
73
|
* ``ago=N`` -> the (N+1)-th most-recent block; ``N=0`` == default.
|
|
@@ -74,6 +80,8 @@ def _resolve_block_selector(
|
|
|
74
80
|
(cannot derive a unique canonical 5h key from a date alone).
|
|
75
81
|
"""
|
|
76
82
|
_c = _cctally()
|
|
83
|
+
_acct_pred = "" if account_key is None else " AND account_key = ?"
|
|
84
|
+
_acct_p: tuple = () if account_key is None else (account_key,)
|
|
77
85
|
if block_start is not None and ago is not None:
|
|
78
86
|
raise ValueError(
|
|
79
87
|
"--block-start and --ago are mutually exclusive"
|
|
@@ -95,8 +103,9 @@ def _resolve_block_selector(
|
|
|
95
103
|
resets_epoch = int(parsed.timestamp()) + 5 * 3600
|
|
96
104
|
key = _c._canonical_5h_window_key(resets_epoch)
|
|
97
105
|
row = conn.execute(
|
|
98
|
-
"SELECT * FROM five_hour_blocks WHERE five_hour_window_key = ?"
|
|
99
|
-
|
|
106
|
+
"SELECT * FROM five_hour_blocks WHERE five_hour_window_key = ?"
|
|
107
|
+
+ _acct_pred,
|
|
108
|
+
(key,) + _acct_p,
|
|
100
109
|
).fetchone()
|
|
101
110
|
return dict(row) if row else None
|
|
102
111
|
|
|
@@ -105,12 +114,10 @@ def _resolve_block_selector(
|
|
|
105
114
|
if offset < 0:
|
|
106
115
|
raise ValueError(f"--ago must be non-negative (got {ago})")
|
|
107
116
|
row = conn.execute(
|
|
108
|
-
""
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
""",
|
|
113
|
-
(offset,),
|
|
117
|
+
"SELECT * FROM five_hour_blocks"
|
|
118
|
+
+ (" WHERE account_key = ?" if account_key is not None else "")
|
|
119
|
+
+ " ORDER BY block_start_at DESC, id DESC LIMIT 1 OFFSET ?",
|
|
120
|
+
_acct_p + (offset,),
|
|
114
121
|
).fetchone()
|
|
115
122
|
return dict(row) if row else None
|
|
116
123
|
|
|
@@ -913,7 +920,7 @@ def _block_is_active(
|
|
|
913
920
|
|
|
914
921
|
|
|
915
922
|
def _latest_seven_day_and_window(
|
|
916
|
-
conn: sqlite3.Connection,
|
|
923
|
+
conn: sqlite3.Connection, *, account_key: "str | None" = None,
|
|
917
924
|
) -> tuple[float | None, int | None]:
|
|
918
925
|
"""Return ``(latest_7d_percent, latest_5h_window_key)`` from
|
|
919
926
|
``weekly_usage_snapshots``.
|
|
@@ -923,15 +930,18 @@ def _latest_seven_day_and_window(
|
|
|
923
930
|
Either or both elements may be ``None``. Used by
|
|
924
931
|
``cmd_five_hour_breakdown`` to override
|
|
925
932
|
``seven_day_pct_at_block_end`` on the active row.
|
|
933
|
+
|
|
934
|
+
``account_key`` (#341, spec §3): ``None`` = merged / byte-stable; a real key /
|
|
935
|
+
``unattributed`` scopes the latest-snapshot pick to that account.
|
|
926
936
|
"""
|
|
937
|
+
acct_pred = "" if account_key is None else " WHERE account_key = ?"
|
|
938
|
+
acct_p: tuple = () if account_key is None else (account_key,)
|
|
927
939
|
try:
|
|
928
940
|
row = conn.execute(
|
|
929
|
-
""
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
LIMIT 1
|
|
934
|
-
"""
|
|
941
|
+
"SELECT weekly_percent, five_hour_window_key "
|
|
942
|
+
"FROM weekly_usage_snapshots" + acct_pred +
|
|
943
|
+
" ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
|
|
944
|
+
acct_p,
|
|
935
945
|
).fetchone()
|
|
936
946
|
except sqlite3.DatabaseError:
|
|
937
947
|
return None, None
|
|
@@ -998,6 +1008,12 @@ def cmd_five_hour_blocks(args: argparse.Namespace) -> int:
|
|
|
998
1008
|
# cmd_five_hour_breakdown). Used by the active-predicate to gate
|
|
999
1009
|
# natural expiration so an idle-past-reset block doesn't render ACTIVE.
|
|
1000
1010
|
now_utc = _command_as_of()
|
|
1011
|
+
# #341 --account: resolve the render filter (provider=claude). This command
|
|
1012
|
+
# renders from the five_hour_blocks stats table (not the entry cache), so
|
|
1013
|
+
# needs_cache=False. None = merged / byte-stable.
|
|
1014
|
+
acct_key, acct_exit = _c.resolve_account_filter(args, "claude", needs_cache=False)
|
|
1015
|
+
if acct_exit is not None:
|
|
1016
|
+
return acct_exit
|
|
1001
1017
|
conn = open_db()
|
|
1002
1018
|
try:
|
|
1003
1019
|
# Date filter parsing — same convention as cmd_blocks.
|
|
@@ -1026,6 +1042,9 @@ def cmd_five_hour_blocks(args: argparse.Namespace) -> int:
|
|
|
1026
1042
|
until_dt = dt.date.fromisoformat(until_iso) + dt.timedelta(days=1)
|
|
1027
1043
|
where.append("block_start_at < ?")
|
|
1028
1044
|
params.append(until_dt.isoformat())
|
|
1045
|
+
if acct_key is not None: # #341: scope blocks to the selected account
|
|
1046
|
+
where.append("account_key = ?")
|
|
1047
|
+
params.append(acct_key)
|
|
1029
1048
|
clause = ("WHERE " + " AND ".join(where)) if where else ""
|
|
1030
1049
|
|
|
1031
1050
|
# No filter → cap at 50; with filter → unbounded.
|
|
@@ -1071,19 +1090,21 @@ def cmd_five_hour_blocks(args: argparse.Namespace) -> int:
|
|
|
1071
1090
|
# SAME filter set (none here, but kept symmetric for clarity).
|
|
1072
1091
|
truncated = False
|
|
1073
1092
|
if cap is not None and len(rows) == cap:
|
|
1093
|
+
# #341: the cap path implies no date filter, but an --account filter
|
|
1094
|
+
# can still apply — probe over the SAME account scope.
|
|
1095
|
+
_probe_pred = "" if acct_key is None else " WHERE account_key = ?"
|
|
1096
|
+
_probe_p: tuple = () if acct_key is None else (acct_key,)
|
|
1074
1097
|
extra = conn.execute(
|
|
1075
|
-
""
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
LIMIT 1 OFFSET ?
|
|
1079
|
-
""",
|
|
1080
|
-
(cap,),
|
|
1098
|
+
"SELECT 1 FROM five_hour_blocks" + _probe_pred +
|
|
1099
|
+
" ORDER BY block_start_at DESC, id DESC LIMIT 1 OFFSET ?",
|
|
1100
|
+
_probe_p + (cap,),
|
|
1081
1101
|
).fetchone()
|
|
1082
1102
|
truncated = extra is not None
|
|
1083
1103
|
|
|
1084
1104
|
# Latest live 7d% from the latest weekly_usage_snapshots row, used
|
|
1085
1105
|
# to fill seven_day_pct_at_block_end on the active row.
|
|
1086
|
-
latest_7d, latest_window_key = _latest_seven_day_and_window(
|
|
1106
|
+
latest_7d, latest_window_key = _latest_seven_day_and_window(
|
|
1107
|
+
conn, account_key=acct_key)
|
|
1087
1108
|
|
|
1088
1109
|
# Pre-load credit events for every window_key the rows query
|
|
1089
1110
|
# returned. Single index-scan over `five_hour_reset_events`;
|
|
@@ -1091,11 +1112,14 @@ def cmd_five_hour_blocks(args: argparse.Namespace) -> int:
|
|
|
1091
1112
|
# JOIN against each block dict. Used by both the text/JSON
|
|
1092
1113
|
# render path AND the share-output snapshot wiring (spec §5.1.1).
|
|
1093
1114
|
# Loaded in a single pass — no per-block SELECT.
|
|
1115
|
+
_credit_pred = "" if acct_key is None else " WHERE account_key = ?"
|
|
1116
|
+
_credit_p: tuple = () if acct_key is None else (acct_key,)
|
|
1094
1117
|
credit_rows = conn.execute(
|
|
1095
1118
|
"SELECT five_hour_window_key, prior_percent, post_percent, "
|
|
1096
1119
|
" effective_reset_at_utc "
|
|
1097
|
-
" FROM five_hour_reset_events
|
|
1098
|
-
" ORDER BY five_hour_window_key, effective_reset_at_utc"
|
|
1120
|
+
" FROM five_hour_reset_events" + _credit_pred +
|
|
1121
|
+
" ORDER BY five_hour_window_key, effective_reset_at_utc",
|
|
1122
|
+
_credit_p,
|
|
1099
1123
|
).fetchall()
|
|
1100
1124
|
credits_by_window: dict[int, list[dict]] = {}
|
|
1101
1125
|
for cr in credit_rows:
|
|
@@ -1199,13 +1223,12 @@ def cmd_five_hour_blocks(args: argparse.Namespace) -> int:
|
|
|
1199
1223
|
)
|
|
1200
1224
|
|
|
1201
1225
|
if args.json:
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
))
|
|
1226
|
+
_fhb_payload = _c._five_hour_blocks_to_json(
|
|
1227
|
+
block_dicts, since_iso, until_iso,
|
|
1228
|
+
cap, truncated, args.breakdown,
|
|
1229
|
+
)
|
|
1230
|
+
_fhb_payload.update(_c.account_json_fields(acct_key)) # #341 R8
|
|
1231
|
+
print(json.dumps(_fhb_payload, indent=2))
|
|
1209
1232
|
return 0
|
|
1210
1233
|
|
|
1211
1234
|
_c._render_five_hour_blocks_table(block_dicts, args)
|
|
@@ -1224,6 +1247,11 @@ def cmd_five_hour_breakdown(args: argparse.Namespace) -> int:
|
|
|
1224
1247
|
# other testing-hook-only commands). Used for the active-block elapsed
|
|
1225
1248
|
# display below so fixture-pinned harnesses get deterministic output.
|
|
1226
1249
|
now_utc = _command_as_of()
|
|
1250
|
+
# #341 --account: resolve the render filter (provider=claude). Reads only the
|
|
1251
|
+
# five_hour_* stats tables (not the entry cache), so needs_cache=False.
|
|
1252
|
+
acct_key, acct_exit = _c.resolve_account_filter(args, "claude", needs_cache=False)
|
|
1253
|
+
if acct_exit is not None:
|
|
1254
|
+
return acct_exit
|
|
1227
1255
|
conn = open_db()
|
|
1228
1256
|
try:
|
|
1229
1257
|
try:
|
|
@@ -1231,6 +1259,7 @@ def cmd_five_hour_breakdown(args: argparse.Namespace) -> int:
|
|
|
1231
1259
|
conn,
|
|
1232
1260
|
block_start=args.block_start,
|
|
1233
1261
|
ago=args.ago,
|
|
1262
|
+
account_key=acct_key,
|
|
1234
1263
|
)
|
|
1235
1264
|
except ValueError as e:
|
|
1236
1265
|
print(f"five-hour-breakdown: {e}", file=sys.stderr)
|
|
@@ -1271,14 +1300,14 @@ def cmd_five_hour_breakdown(args: argparse.Namespace) -> int:
|
|
|
1271
1300
|
# renderer can interleave a ``⚡ CREDIT -Xpp @ HH:MM`` divider
|
|
1272
1301
|
# row between pre- and post-credit milestone segments and JSON
|
|
1273
1302
|
# consumers see the parallel ``credits[]`` array (Section 5.2).
|
|
1303
|
+
_bd_credit_pred = "" if acct_key is None else " AND account_key = ?"
|
|
1304
|
+
_bd_credit_p: tuple = () if acct_key is None else (acct_key,)
|
|
1274
1305
|
credit_rows = conn.execute(
|
|
1275
|
-
""
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
""",
|
|
1281
|
-
(block["five_hour_window_key"],),
|
|
1306
|
+
"SELECT effective_reset_at_utc, prior_percent, post_percent "
|
|
1307
|
+
"FROM five_hour_reset_events "
|
|
1308
|
+
"WHERE five_hour_window_key = ?" + _bd_credit_pred +
|
|
1309
|
+
" ORDER BY effective_reset_at_utc ASC",
|
|
1310
|
+
(block["five_hour_window_key"],) + _bd_credit_p,
|
|
1282
1311
|
).fetchall()
|
|
1283
1312
|
credits_list: list[dict] = [
|
|
1284
1313
|
{
|
|
@@ -1297,7 +1326,8 @@ def cmd_five_hour_breakdown(args: argparse.Namespace) -> int:
|
|
|
1297
1326
|
p_end = block.get("seven_day_pct_at_block_end")
|
|
1298
1327
|
|
|
1299
1328
|
# Live 7d_end on active row.
|
|
1300
|
-
latest_7d, latest_window_key = _latest_seven_day_and_window(
|
|
1329
|
+
latest_7d, latest_window_key = _latest_seven_day_and_window(
|
|
1330
|
+
conn, account_key=acct_key)
|
|
1301
1331
|
is_active = _block_is_active(block, latest_window_key, now_utc)
|
|
1302
1332
|
if is_active and latest_7d is not None:
|
|
1303
1333
|
p_end = latest_7d
|
|
@@ -1352,15 +1382,14 @@ def cmd_five_hour_breakdown(args: argparse.Namespace) -> int:
|
|
|
1352
1382
|
# ``milestones`` — same shape as the ``credits`` field on
|
|
1353
1383
|
# ``five-hour-blocks --json`` (§5.1). Stacked credits across
|
|
1354
1384
|
# distinct 10-min slots produce multiple entries.
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
))
|
|
1385
|
+
_bd_payload = {
|
|
1386
|
+
"schemaVersion": 1,
|
|
1387
|
+
"block": block_out,
|
|
1388
|
+
"milestones": ms_out,
|
|
1389
|
+
"credits": credits_list,
|
|
1390
|
+
}
|
|
1391
|
+
_bd_payload.update(_c.account_json_fields(acct_key)) # #341 R8
|
|
1392
|
+
print(json.dumps(_bd_payload, indent=2))
|
|
1364
1393
|
return 0
|
|
1365
1394
|
|
|
1366
1395
|
# Human-readable header line.
|
|
@@ -1493,18 +1522,21 @@ def _backfill_five_hour_blocks(
|
|
|
1493
1522
|
# 5h percent. The percent guard is critical: MAX(five_hour_percent)
|
|
1494
1523
|
# over a NULL-only window is NULL, which would trip the
|
|
1495
1524
|
# final_five_hour_percent NOT NULL constraint at insert time.
|
|
1525
|
+
# #341: re-materialize one open block per (account_key, window_key). A
|
|
1526
|
+
# shared physical 5h window observed by two accounts yields two distinct
|
|
1527
|
+
# open blocks so rebuild reproduces per-account ownership.
|
|
1496
1528
|
keys_sql = """
|
|
1497
|
-
SELECT DISTINCT five_hour_window_key
|
|
1529
|
+
SELECT DISTINCT five_hour_window_key, account_key
|
|
1498
1530
|
FROM weekly_usage_snapshots
|
|
1499
1531
|
WHERE five_hour_window_key IS NOT NULL
|
|
1500
1532
|
AND five_hour_percent IS NOT NULL
|
|
1501
1533
|
"""
|
|
1502
1534
|
if only_missing:
|
|
1503
1535
|
keys_sql += (
|
|
1504
|
-
" AND five_hour_window_key NOT IN "
|
|
1505
|
-
"(SELECT five_hour_window_key FROM five_hour_blocks)"
|
|
1536
|
+
" AND (five_hour_window_key, account_key) NOT IN "
|
|
1537
|
+
"(SELECT five_hour_window_key, account_key FROM five_hour_blocks)"
|
|
1506
1538
|
)
|
|
1507
|
-
keys = [int(r[0]) for r in conn.execute(keys_sql).fetchall()]
|
|
1539
|
+
keys = [(int(r[0]), r[1]) for r in conn.execute(keys_sql).fetchall()]
|
|
1508
1540
|
|
|
1509
1541
|
now_iso = now_utc_iso()
|
|
1510
1542
|
now_dt = parse_iso_datetime(now_iso, "now")
|
|
@@ -1520,30 +1552,32 @@ def _backfill_five_hour_blocks(
|
|
|
1520
1552
|
# See cctally-dev#87.
|
|
1521
1553
|
conn.execute("BEGIN IMMEDIATE")
|
|
1522
1554
|
try:
|
|
1523
|
-
for key in keys:
|
|
1555
|
+
for key, acct in keys:
|
|
1524
1556
|
# MIN-captured row defines the immutable block boundary
|
|
1525
1557
|
# values (deterministic — picking "any in-window row"
|
|
1526
1558
|
# would be nondeterministic under seconds-level Anthropic
|
|
1527
|
-
# ISO jitter that the canonical key collapses).
|
|
1559
|
+
# ISO jitter that the canonical key collapses). Scoped to
|
|
1560
|
+
# (window_key, account_key) so a shared window's per-account
|
|
1561
|
+
# blocks each read their own account's boundary rows (#341).
|
|
1528
1562
|
min_row = conn.execute(
|
|
1529
1563
|
"""
|
|
1530
1564
|
SELECT five_hour_resets_at, captured_at_utc, weekly_percent
|
|
1531
1565
|
FROM weekly_usage_snapshots
|
|
1532
|
-
WHERE five_hour_window_key = ?
|
|
1566
|
+
WHERE five_hour_window_key = ? AND account_key = ?
|
|
1533
1567
|
AND five_hour_percent IS NOT NULL
|
|
1534
1568
|
ORDER BY captured_at_utc ASC, id ASC LIMIT 1
|
|
1535
1569
|
""",
|
|
1536
|
-
(key,),
|
|
1570
|
+
(key, acct),
|
|
1537
1571
|
).fetchone()
|
|
1538
1572
|
max_row = conn.execute(
|
|
1539
1573
|
"""
|
|
1540
1574
|
SELECT captured_at_utc, weekly_percent, five_hour_percent
|
|
1541
1575
|
FROM weekly_usage_snapshots
|
|
1542
|
-
WHERE five_hour_window_key = ?
|
|
1576
|
+
WHERE five_hour_window_key = ? AND account_key = ?
|
|
1543
1577
|
AND five_hour_percent IS NOT NULL
|
|
1544
1578
|
ORDER BY captured_at_utc DESC, id DESC LIMIT 1
|
|
1545
1579
|
""",
|
|
1546
|
-
(key,),
|
|
1580
|
+
(key, acct),
|
|
1547
1581
|
).fetchone()
|
|
1548
1582
|
if min_row is None or max_row is None:
|
|
1549
1583
|
continue # defensive — should be unreachable per the keys query
|
|
@@ -1574,22 +1608,24 @@ def _backfill_five_hour_blocks(
|
|
|
1574
1608
|
cross_row = conn.execute(
|
|
1575
1609
|
"""
|
|
1576
1610
|
SELECT 1 FROM week_reset_events
|
|
1577
|
-
WHERE
|
|
1611
|
+
WHERE account_key = ?
|
|
1612
|
+
AND unixepoch(effective_reset_at_utc) >= unixepoch(?)
|
|
1578
1613
|
AND unixepoch(effective_reset_at_utc) <= unixepoch(?)
|
|
1579
1614
|
LIMIT 1
|
|
1580
1615
|
""",
|
|
1581
|
-
(block_start_at, last_obs),
|
|
1616
|
+
(acct, block_start_at, last_obs),
|
|
1582
1617
|
).fetchone()
|
|
1583
1618
|
if cross_row is None:
|
|
1584
1619
|
cross_row = conn.execute(
|
|
1585
1620
|
"""
|
|
1586
1621
|
SELECT 1 FROM weekly_usage_snapshots
|
|
1587
1622
|
WHERE week_start_at IS NOT NULL
|
|
1623
|
+
AND account_key = ?
|
|
1588
1624
|
AND unixepoch(week_start_at) > unixepoch(?)
|
|
1589
1625
|
AND unixepoch(week_start_at) <= unixepoch(?)
|
|
1590
1626
|
LIMIT 1
|
|
1591
1627
|
""",
|
|
1592
|
-
(block_start_at, last_obs),
|
|
1628
|
+
(acct, block_start_at, last_obs),
|
|
1593
1629
|
).fetchone()
|
|
1594
1630
|
crossed = 1 if cross_row is not None else 0
|
|
1595
1631
|
|
|
@@ -1624,9 +1660,10 @@ def _backfill_five_hour_blocks(
|
|
|
1624
1660
|
total_cost_usd,
|
|
1625
1661
|
is_closed,
|
|
1626
1662
|
created_at_utc,
|
|
1627
|
-
last_updated_at_utc
|
|
1663
|
+
last_updated_at_utc,
|
|
1664
|
+
account_key
|
|
1628
1665
|
)
|
|
1629
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1666
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1630
1667
|
""",
|
|
1631
1668
|
(
|
|
1632
1669
|
key,
|
|
@@ -1646,6 +1683,7 @@ def _backfill_five_hour_blocks(
|
|
|
1646
1683
|
is_closed,
|
|
1647
1684
|
now_iso,
|
|
1648
1685
|
now_iso,
|
|
1686
|
+
acct,
|
|
1649
1687
|
),
|
|
1650
1688
|
)
|
|
1651
1689
|
inserted += cur.rowcount or 0
|
|
@@ -1655,8 +1693,9 @@ def _backfill_five_hour_blocks(
|
|
|
1655
1693
|
# re-runs (UNIQUE(five_hour_window_key, model|project_path)).
|
|
1656
1694
|
# Same transaction as the parent INSERT.
|
|
1657
1695
|
parent_id_row = conn.execute(
|
|
1658
|
-
"SELECT id FROM five_hour_blocks
|
|
1659
|
-
|
|
1696
|
+
"SELECT id FROM five_hour_blocks "
|
|
1697
|
+
"WHERE five_hour_window_key = ? AND account_key = ?",
|
|
1698
|
+
(key, acct),
|
|
1660
1699
|
).fetchone()
|
|
1661
1700
|
if parent_id_row is not None:
|
|
1662
1701
|
parent_id = int(parent_id_row["id"])
|
|
@@ -1667,9 +1706,9 @@ def _backfill_five_hour_blocks(
|
|
|
1667
1706
|
block_id, five_hour_window_key, model,
|
|
1668
1707
|
input_tokens, output_tokens,
|
|
1669
1708
|
cache_create_tokens, cache_read_tokens,
|
|
1670
|
-
cost_usd, entry_count
|
|
1709
|
+
cost_usd, entry_count, account_key
|
|
1671
1710
|
)
|
|
1672
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1711
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1673
1712
|
""",
|
|
1674
1713
|
[
|
|
1675
1714
|
(
|
|
@@ -1682,6 +1721,7 @@ def _backfill_five_hour_blocks(
|
|
|
1682
1721
|
b["cache_read_tokens"],
|
|
1683
1722
|
b["cost_usd"],
|
|
1684
1723
|
b["entry_count"],
|
|
1724
|
+
acct,
|
|
1685
1725
|
)
|
|
1686
1726
|
for model, b in totals["by_model"].items()
|
|
1687
1727
|
],
|
|
@@ -1693,9 +1733,9 @@ def _backfill_five_hour_blocks(
|
|
|
1693
1733
|
block_id, five_hour_window_key, project_path,
|
|
1694
1734
|
input_tokens, output_tokens,
|
|
1695
1735
|
cache_create_tokens, cache_read_tokens,
|
|
1696
|
-
cost_usd, entry_count
|
|
1736
|
+
cost_usd, entry_count, account_key
|
|
1697
1737
|
)
|
|
1698
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1738
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1699
1739
|
""",
|
|
1700
1740
|
[
|
|
1701
1741
|
(
|
|
@@ -1708,6 +1748,7 @@ def _backfill_five_hour_blocks(
|
|
|
1708
1748
|
b["cache_read_tokens"],
|
|
1709
1749
|
b["cost_usd"],
|
|
1710
1750
|
b["entry_count"],
|
|
1751
|
+
acct,
|
|
1711
1752
|
)
|
|
1712
1753
|
for project_path, b in totals["by_project"].items()
|
|
1713
1754
|
],
|