cctally 1.117.0 → 1.117.2

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 CHANGED
@@ -5,6 +5,22 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.117.2] - 2026-11-03
9
+
10
+ ### Fixed
11
+ - With more than one Claude account, the `cctally dashboard` Current Week card and modal show one account's cycle, milestones, spend and `$/1%` beside that account's label, and the older/newer cycle buttons stay on that account.
12
+ - `cctally five-hour-breakdown` and the dashboard's five-hour milestones show `—` (`null` in `--json`) instead of a weekly percentage that a weekly credit later retired.
13
+ - With more than one Claude account, the `cctally dashboard` All view shows its combined spend headline, summing each account's current cycle, instead of usually leaving it out.
14
+ - Under Python 3.11, `cctally quota --json` no longer reports family shares and calibration figures that differ in their last digits from newer Python versions.
15
+
16
+ ## [1.117.1] - 2026-11-03
17
+
18
+ ### Fixed
19
+ - With more than one Claude account, the `cctally dashboard` Current Week card and modal show one account's cycle, milestones, spend and `$/1%` beside that account's label, and the older/newer cycle buttons stay on that account.
20
+ - `cctally five-hour-breakdown` and the dashboard's five-hour milestones show `—` (`null` in `--json`) instead of a weekly percentage that a weekly credit later retired.
21
+ - With more than one Claude account, the `cctally dashboard` All view shows its combined spend headline, summing each account's current cycle, instead of usually leaving it out.
22
+ - Under Python 3.11, `cctally quota --json` no longer reports family shares and calibration figures that differ in their last digits from newer Python versions.
23
+
8
24
  ## [1.117.0] - 2026-11-02
9
25
 
10
26
  ### Added
@@ -11544,12 +11544,20 @@ class DashboardHTTPHandler(BaseHTTPRequestHandler):
11544
11544
  assembly runs inside one transaction per DB. 400 malformed key/source;
11545
11545
  404 ``{code: "unknown_key", reason}`` for keys that don't resolve.
11546
11546
 
11547
- ``?account=<account_key|unattributed>`` (#416, Codex only) focuses the
11548
- cycle on one account: its own crossings, its own spend. ABSENT is the
11549
- shipped merged response and is byte-identical — the client sends the
11550
- qualifier only under account focus, exactly as `/api/source/` does. The
11547
+ ``?account=<account_key|unattributed>`` (#416 Codex; #836 Claude)
11548
+ focuses the cycle on one account: its own crossings, its own spend.
11549
+ ABSENT is the shipped merged response and is byte-identical — the
11550
+ client sends the qualifier only under account focus (Codex) or for the
11551
+ displayed account (Claude), exactly as `/api/source/` does. The
11551
11552
  vendor-wide ``*`` sentinel is rejected here (unlike `/api/source/`,
11552
11553
  where it labels an alert row): it names no cycle to render.
11554
+
11555
+ Claude (#836 spec 5.6) accepts the qualifier only while Claude is
11556
+ decorated (more than one REAL account), checked in the same read
11557
+ transaction as the assembly: 400 on an undecorated store, 404
11558
+ ``unknown_key`` for an unknown or non-Claude account or a key outside
11559
+ that account's scoped index, and never a retry against the merged
11560
+ index.
11553
11561
  """
11554
11562
  import re as _re
11555
11563
  import urllib.parse as _urlparse
@@ -11576,11 +11584,6 @@ class DashboardHTTPHandler(BaseHTTPRequestHandler):
11576
11584
  ):
11577
11585
  self._send_milestones_json(400, {"error": "invalid account"})
11578
11586
  return
11579
- if source != "codex":
11580
- # Claude weeks are not account-partitioned on this route yet;
11581
- # accepting the qualifier silently would be a privacy lie.
11582
- self._send_milestones_json(400, {"error": "invalid account"})
11583
- return
11584
11587
  account_key = raw_account[0]
11585
11588
  c = sys.modules["cctally"]
11586
11589
  try:
@@ -11592,12 +11595,38 @@ class DashboardHTTPHandler(BaseHTTPRequestHandler):
11592
11595
  self._send_milestones_json(400, {"error": "invalid cycle key"})
11593
11596
  return
11594
11597
  conn = open_db()
11598
+ rejected = False
11599
+ detail = None
11595
11600
  try:
11596
11601
  conn.execute("BEGIN")
11597
- detail = c.build_claude_week_detail(conn, key)
11602
+ if account_key is None:
11603
+ detail = c.build_claude_week_detail(conn, key)
11604
+ else:
11605
+ # #836 spec 5.6. Only a DECORATED store partitions
11606
+ # Claude history by account. On an undecorated one the
11607
+ # qualifier is refused, never ignored: answering it with
11608
+ # the merged week would claim an account scope the
11609
+ # payload does not have, which is the privacy lie the
11610
+ # earlier blanket rejection guarded against. Checked
11611
+ # inside this transaction so the gate and the assembly
11612
+ # read one state.
11613
+ import _cctally_account
11614
+ if not _cctally_account.provider_is_decorated(
11615
+ conn, "claude"):
11616
+ rejected = True
11617
+ elif account_key == "unattributed" or conn.execute(
11618
+ "SELECT 1 FROM accounts WHERE account_key = ? "
11619
+ "AND provider = 'claude'",
11620
+ (account_key,)).fetchone() is not None:
11621
+ detail = c.build_claude_week_detail(
11622
+ conn, key, account_key=account_key,
11623
+ now_utc=_command_as_of())
11598
11624
  conn.commit()
11599
11625
  finally:
11600
11626
  conn.close()
11627
+ if rejected:
11628
+ self._send_milestones_json(400, {"error": "invalid account"})
11629
+ return
11601
11630
  if detail is None:
11602
11631
  self._send_milestones_json(404, {
11603
11632
  "error": "unknown week", "code": "unknown_key",
@@ -2557,6 +2557,16 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
2557
2557
  # PREVIEW badge when set.
2558
2558
  **sys.modules["_cctally_dashboard"]._channel_env_fragment(),
2559
2559
  }
2560
+ # #836 (spec 5.1): the displayed account, decorated builds ONLY. Below the
2561
+ # R8 threshold both keys are omitted (never null), so an undecorated
2562
+ # `current_week` keeps its bytes. Appended after the literal so the key
2563
+ # order of every existing field is unchanged. This identity — never
2564
+ # `five_hour_block.account_key`, absent with no live block — is what the
2565
+ # client labels and scopes its history navigation by.
2566
+ if (cw is not None and getattr(cw, "account_decorated", False)
2567
+ and isinstance(envelope.get("current_week"), dict)):
2568
+ envelope["current_week"]["account_key"] = cw.account_key
2569
+ envelope["current_week"]["account_label"] = cw.account_label
2560
2570
  envelope.update(_source_bundle_to_envelope(getattr(snap, "source_bundle", None)))
2561
2571
  _overlay_claude_source_session_titles(envelope, snap, transcripts_visible)
2562
2572
  _overlay_codex_source_session_labels(envelope, snap, transcripts_visible)
@@ -44,6 +44,7 @@ import sqlite3
44
44
  import sys
45
45
  from dataclasses import dataclass
46
46
 
47
+ import _cctally_core # `weekly_held_exclusion`: the held-column guard (#836)
47
48
  import _lib_accounts # pure stdlib kernel; UNATTRIBUTED sentinel (#341)
48
49
  import _lib_rebuild_progress # #997 D9 progress ticks (stdlib only)
49
50
  from _lib_five_hour import latest_five_hour_reading
@@ -1086,13 +1087,57 @@ def _credit_aware_block_weekly_axes(conn, block, *, now_utc, cache=None):
1086
1087
  block.get("seven_day_pct_at_block_end"))
1087
1088
 
1088
1089
 
1090
+ def _weekly_retired_sql(conn, alias: str) -> "str | None":
1091
+ """The suppression-retired predicate over ``alias``, or None on a store
1092
+ without the held column (#836, spec 5.4).
1093
+
1094
+ A held WRITE stores ``"weeklyObservationHeld": true`` in ``payload_json``
1095
+ beside its effective basis value (`_cctally_record`, the held branch of
1096
+ the snapshot write). A weekly credit's cleanup instead turns an UNHELD row,
1097
+ whose payload never carried that marker, into a held one and leaves its
1098
+ stale `weekly_percent` in place (`_delete_doomed_snapshot_rows`, replay
1099
+ `_cctally_journal._retire_weekly_effects`). The marker therefore separates
1100
+ the two exactly. A store without the column cannot hold a held row, the
1101
+ reason `_cctally_core.weekly_held_exclusion` omits its own predicate there.
1102
+ """
1103
+ if not _cctally_core.weekly_held_exclusion(conn):
1104
+ return None
1105
+ return (f"({alias}.weekly_observation_held = 1 AND "
1106
+ f"json_extract({alias}.payload_json, '$.weeklyObservationHeld') "
1107
+ f"IS NOT 1)")
1108
+
1109
+
1110
+ def effective_weekly_value_sql(conn, alias: str = "s") -> str:
1111
+ """The joined snapshot's EFFECTIVE weekly value (#836, spec 5.4): NULL for
1112
+ a suppression-retired row, its stored `weekly_percent` otherwise. A
1113
+ clamped held write and a genuine climb-back keep their values; no
1114
+ percentage band is involved."""
1115
+ retired = _weekly_retired_sql(conn, alias)
1116
+ if retired is None:
1117
+ return f"{alias}.weekly_percent"
1118
+ return f"CASE WHEN {retired} THEN NULL ELSE {alias}.weekly_percent END"
1119
+
1120
+
1121
+ def weekly_join_unavailable_sql(conn, alias: str = "s") -> str:
1122
+ """A five-hour milestone's effective weekly join is UNAVAILABLE (#836,
1123
+ spec 5.4): the joined snapshot is absent — deleted, or the ``0`` sentinel
1124
+ `trg_usage_snapshot_suppression` writes — or suppression-retired. The
1125
+ shared predicate of the detail stamp and the dashboard's retired-join
1126
+ signature leg, over a ``LEFT JOIN`` aliased ``alias``."""
1127
+ retired = _weekly_retired_sql(conn, alias)
1128
+ if retired is None:
1129
+ return f"({alias}.id IS NULL)"
1130
+ return f"({alias}.id IS NULL OR {retired})"
1131
+
1132
+
1089
1133
  #: The six `five_hour_milestones` columns every surface already published, plus
1090
- #: the joined EFFECTIVE weekly value. Ordering is `captured_at_utc, id` — spec
1091
- #: §5.2's merged-stream order, which post-credit threshold repeats depend on.
1134
+ #: the joined EFFECTIVE weekly value (`effective_weekly_value_sql`, filled in
1135
+ #: as ``{effective}``). Ordering is `captured_at_utc, id` — spec §5.2's
1136
+ #: merged-stream order, which post-credit threshold repeats depend on.
1092
1137
  _FIVE_HOUR_MILESTONE_SQL = """
1093
1138
  SELECT m.percent_threshold, m.captured_at_utc, m.block_cost_usd,
1094
1139
  m.marginal_cost_usd, m.seven_day_pct_at_crossing, m.reset_event_id,
1095
- s.weekly_percent AS effective_seven_day_pct_at_crossing
1140
+ {effective} AS effective_seven_day_pct_at_crossing
1096
1141
  FROM five_hour_milestones m
1097
1142
  LEFT JOIN weekly_usage_snapshots s ON s.id = m.usage_snapshot_id
1098
1143
  WHERE {selector}
@@ -1139,6 +1184,12 @@ def _load_five_hour_milestones(
1139
1184
  store is corrected by `db rederive`'s reviewed `retract`. A missing joined
1140
1185
  snapshot still publishes None and never falls back to the raw value.
1141
1186
 
1187
+ **A suppression-retired snapshot publishes None** (#836, spec 5.4). The
1188
+ fold rejected its weekly value, and the cleanup kept the row only for its
1189
+ five-hour evidence; its provenance is exact (no held-write marker on a
1190
+ held row), so this is not the withdrawn band. See
1191
+ `effective_weekly_value_sql`.
1192
+
1142
1193
  SELECTOR. Exactly one of `block_id` or `five_hour_window_key` must be given.
1143
1194
  `block_id` is the precise form — `five_hour_milestones.block_id` names one
1144
1195
  block outright. The pair form filters on the window key AND the account,
@@ -1166,7 +1217,9 @@ def _load_five_hour_milestones(
1166
1217
  selector += " AND m.account_key = ?"
1167
1218
  params += (account_key,)
1168
1219
  rows = conn.execute(
1169
- _FIVE_HOUR_MILESTONE_SQL.format(selector=selector), params,
1220
+ _FIVE_HOUR_MILESTONE_SQL.format(
1221
+ selector=selector,
1222
+ effective=effective_weekly_value_sql(conn, "s")), params,
1170
1223
  ).fetchall()
1171
1224
  out: list[dict] = []
1172
1225
  for r in rows:
@@ -52,6 +52,12 @@ from _cctally_core import (
52
52
  _BudgetConfigError, _get_budget_config,
53
53
  BUDGET_PERIODS as _CCTALLY_BUDGET_PERIODS,
54
54
  )
55
+ # #836: the decorated Current Week's owner and cycle identity read the reset
56
+ # chokepoint and the held-column guard directly; neither is ns-patched.
57
+ from _cctally_core import (
58
+ _canonicalize_optional_iso, _latest_reset_event_for_end,
59
+ weekly_held_exclusion,
60
+ )
55
61
 
56
62
  import importlib.util as _ilu
57
63
 
@@ -389,6 +395,272 @@ def _apply_midweek_reset_override(
389
395
  return week_start_at, samples
390
396
 
391
397
 
398
+ # === #836: the decorated Current Week — one owner, one cycle identity =======
399
+ #
400
+ # On a store with more than one REAL Claude account (`provider_is_decorated`)
401
+ # the Current Week card, its weekly milestones and the history index's current
402
+ # entry describe ONE account: the owner of the newest non-held weekly sample at
403
+ # the build's as-of instant (#839's contract, spec 5.2). The merged reads above
404
+ # stay unchanged for every undecorated store and every other caller.
405
+
406
+ #: Every subscription window that can contain an instant starts no more than
407
+ #: seven days (plus host-offset slack on `week_start_date`) before it, so this
408
+ #: leading `week_start_date` bound keeps every candidate read an
409
+ #: `idx_usage_week_time` range rather than a scan of the whole history.
410
+ _CLAUDE_CYCLE_CANDIDATE_DAYS = 8
411
+
412
+
413
+ @dataclass(frozen=True)
414
+ class ClaudeCycleScope:
415
+ """The decorated Current Week's cycle identity (#836, spec 5.2 step 2).
416
+
417
+ Resolved once per build by `_claude_current_cycle_scope` and shared by the
418
+ card, the weekly milestones and the scoped history index, so the three
419
+ readers of one cycle cannot choose it differently.
420
+
421
+ ``account_key``: the owner. ``as_of_utc``: the build instant (ISO-8601
422
+ ``Z``); reset events are admitted only at or before it.
423
+ ``week_start_date``: the storage key milestones are recorded under.
424
+ ``week_start_at``: the owner's stored boundary text (``None`` on the
425
+ date-only leg). ``week_end_at``: the canonical end
426
+ (`_canonicalize_optional_iso`, the `week_reset_events.new_week_end_at`
427
+ form; ``None`` on the date-only leg). ``effective_start_at``: the week
428
+ start, or the active reset's effective instant when later (ISO-8601 ``Z``;
429
+ ``None`` on the date-only leg). ``reset_event_id``: the active reset's id
430
+ (the milestone segment), ``0`` when none.
431
+ """
432
+ account_key: str
433
+ as_of_utc: str
434
+ week_start_date: str
435
+ week_start_at: "str | None"
436
+ week_end_at: "str | None"
437
+ effective_start_at: "str | None"
438
+ reset_event_id: int
439
+
440
+
441
+ def _claude_newest_weekly_sample(conn, now_utc: dt.datetime, *,
442
+ account_key: "str | None" = None):
443
+ """The newest non-held weekly sample captured at or before ``now_utc``
444
+ among windows containing it, or ``None`` (#836, spec 5.2 step 1).
445
+
446
+ Only non-held rows take part, so a newer held row of another account (a
447
+ copy of an older weekly value under a newer capture) never chooses the
448
+ window — the defect of the held-inclusive ``MAX(captured_at_utc)`` window
449
+ choice in `_fetch_current_week_snapshots` (spec D-C). Captures after
450
+ ``now_utc`` never participate. Boundaryful windows
451
+ (``[week_start_at, week_end_at)`` containing ``now_utc``) are preferred;
452
+ when none qualifies, the date-only leg (``week_start_date`` ..
453
+ ``week_end_date`` containing today's host-local date) applies, as in
454
+ `_fetch_current_week_snapshots`. Ties break by ``(captured_at_utc, id)``.
455
+
456
+ Returns a dict of the row's identity and boundary columns plus
457
+ ``boundaryful``: whether the CHOSEN row carries both parseable bounds,
458
+ never which pool it came from. On the window's end date, after its reset
459
+ instant and before any next-window sample, a bounded row reaches the
460
+ date-only pool; its scope is still built from its own bounds, so the
461
+ account's own reset stays in effect (#836 review F1).
462
+ """
463
+ floor_date = (now_utc.astimezone(dt.timezone.utc).date()
464
+ - dt.timedelta(days=_CLAUDE_CYCLE_CANDIDATE_DAYS)).isoformat()
465
+ acct_pred = "" if account_key is None else " AND account_key = ?"
466
+ acct_p: tuple = () if account_key is None else (account_key,)
467
+ rows = conn.execute(
468
+ "SELECT id, account_key, week_start_date, week_end_date, "
469
+ " week_start_at, week_end_at, captured_at_utc "
470
+ "FROM weekly_usage_snapshots "
471
+ "WHERE week_start_date >= ?" + acct_pred
472
+ + weekly_held_exclusion(conn),
473
+ (floor_date,) + acct_p,
474
+ ).fetchall()
475
+ # internal fallback: host-local intentional (the date-only leg's day,
476
+ # matching `_fetch_current_week_snapshots`)
477
+ today_local = now_utc.astimezone().date().isoformat()
478
+ boundaryful = []
479
+ date_only = []
480
+ for r in rows:
481
+ try:
482
+ cap = parse_iso_datetime(r[6], "captured_at_utc")
483
+ except (TypeError, ValueError):
484
+ continue
485
+ if cap > now_utc:
486
+ continue
487
+ rank = (cap, int(r[0]))
488
+ try:
489
+ ws = parse_iso_datetime(r[4], "week_start_at") if r[4] else None
490
+ we = parse_iso_datetime(r[5], "week_end_at") if r[5] else None
491
+ except ValueError:
492
+ ws = we = None
493
+ bounded = ws is not None and we is not None
494
+ if bounded and ws <= now_utc < we:
495
+ boundaryful.append((rank, r, True))
496
+ elif (r[2] is not None and r[3] is not None
497
+ and r[2] <= today_local <= r[3]):
498
+ date_only.append((rank, r, bounded))
499
+ pool = boundaryful or date_only
500
+ if not pool:
501
+ return None
502
+ _rank, r, bounded = max(pool, key=lambda item: item[0])
503
+ return {
504
+ "id": int(r[0]),
505
+ "account_key": r[1],
506
+ "week_start_date": r[2],
507
+ "week_end_date": r[3],
508
+ "week_start_at": r[4],
509
+ "week_end_at": r[5],
510
+ "captured_at_utc": r[6],
511
+ "boundaryful": bounded,
512
+ }
513
+
514
+
515
+ def _claude_select_current_owner(conn, now_utc: dt.datetime) -> "str | None":
516
+ """The displayed account: the owner of the newest non-held weekly sample
517
+ at ``now_utc`` (#836, spec 5.2 step 1), or ``None`` when there is none."""
518
+ sample = _claude_newest_weekly_sample(conn, now_utc)
519
+ return None if sample is None else sample["account_key"]
520
+
521
+
522
+ def _claude_current_cycle_scope(conn, now_utc: dt.datetime, *,
523
+ account_key: str) -> "ClaudeCycleScope | None":
524
+ """``account_key``'s current cycle identity at ``now_utc`` (#836, spec 5.2
525
+ step 2), or ``None`` when the account has no eligible non-held sample.
526
+
527
+ The window is the one holding the account's newest eligible non-held
528
+ sample. The active reset is `_latest_reset_event_for_end` for the
529
+ canonical end, scoped to the account and admitted only at or before
530
+ ``now_utc`` — the #750 S3 chokepoint's effective-instant order — so another
531
+ account's credit never re-anchors this cycle (spec D-A) and a reset that
532
+ takes effect later does not cut it yet. The effective start is that
533
+ reset's instant exactly when the history index cuts the account's storage
534
+ bucket there (`_claude_reset_cuts_cycle`, the one rule both use) — which
535
+ can be EARLIER than the nominal week start after a same-date early
536
+ re-anchor (#836 review F7). The date-only leg has no end text, so it
537
+ carries no reset.
538
+ """
539
+ sample = _claude_newest_weekly_sample(conn, now_utc, account_key=account_key)
540
+ if sample is None:
541
+ return None
542
+ as_of = _iso_z(now_utc)
543
+ if not sample["boundaryful"]:
544
+ return ClaudeCycleScope(
545
+ account_key=account_key, as_of_utc=as_of,
546
+ week_start_date=sample["week_start_date"],
547
+ week_start_at=None, week_end_at=None, effective_start_at=None,
548
+ reset_event_id=0)
549
+ week_start = parse_iso_datetime(sample["week_start_at"], "week_start_at")
550
+ canon_end = _canonicalize_optional_iso(sample["week_end_at"],
551
+ "claude.cycle.end")
552
+ effective_start = week_start
553
+ reset_event_id = 0
554
+ if canon_end:
555
+ event = _latest_reset_event_for_end(
556
+ conn, canon_end, account_key=account_key, as_of_utc=as_of)
557
+ if event is not None:
558
+ reset_event_id = int(event["id"])
559
+ if event["effective_reset_at_utc"]:
560
+ try:
561
+ reset_dt = parse_iso_datetime(
562
+ event["effective_reset_at_utc"], "reset_event.effective")
563
+ except ValueError:
564
+ reset_dt = None
565
+ if reset_dt is not None:
566
+ import _cctally_milestone_history as _mhist
567
+ outer = _mhist._claude_storage_boundaries(
568
+ conn, sample["week_start_date"],
569
+ account_key=account_key)
570
+ if _mhist._claude_reset_cuts_cycle(outer, reset_dt):
571
+ effective_start = reset_dt
572
+ return ClaudeCycleScope(
573
+ account_key=account_key, as_of_utc=as_of,
574
+ week_start_date=sample["week_start_date"],
575
+ week_start_at=sample["week_start_at"],
576
+ week_end_at=canon_end,
577
+ effective_start_at=_iso_z(effective_start),
578
+ reset_event_id=reset_event_id)
579
+
580
+
581
+ def _claude_cycle_scope_samples(conn, scope: ClaudeCycleScope,
582
+ now_utc: dt.datetime):
583
+ """The decorated card's held-inclusive samples for ``scope`` (#836).
584
+
585
+ The shape of ``_fetch_current_week_snapshots(include_held=True,
586
+ include_account=True, include_scope=True)`` — ``(week_start_at,
587
+ week_end_at, samples, week_scope)`` — restricted to the scope's account and
588
+ window, so the card reads the window its owner was chosen in rather than
589
+ one `_fetch_current_week_snapshots` would choose by its held-inclusive
590
+ ``MAX(captured_at_utc)``. ``week_start_at`` is the NOMINAL start; the
591
+ caller filters from ``scope.effective_start_at``. Samples are filtered to
592
+ ``captured_at <= now_utc``. Returns ``None`` when the window cannot be
593
+ resolved.
594
+ """
595
+ floor_date = (now_utc.astimezone(dt.timezone.utc).date()
596
+ - dt.timedelta(days=_CLAUDE_CYCLE_CANDIDATE_DAYS)).isoformat()
597
+ if scope.week_start_at is None:
598
+ drow = conn.execute(
599
+ "SELECT week_end_date FROM weekly_usage_snapshots "
600
+ "WHERE week_start_date = ? AND account_key = ? "
601
+ "AND week_end_date IS NOT NULL "
602
+ "ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
603
+ (scope.week_start_date, scope.account_key),
604
+ ).fetchone()
605
+ if drow is None:
606
+ return None
607
+ # internal fallback: host-local intentional (the date-only leg)
608
+ local_tz = dt.datetime.now().astimezone().tzinfo
609
+ ws_date = dt.date.fromisoformat(scope.week_start_date)
610
+ we_date = dt.date.fromisoformat(drow[0])
611
+ week_start_at = dt.datetime.combine(
612
+ ws_date, dt.time(0, 0), local_tz).astimezone(dt.timezone.utc)
613
+ week_end_at = dt.datetime.combine(
614
+ we_date + dt.timedelta(days=1), dt.time(0, 0),
615
+ local_tz).astimezone(dt.timezone.utc)
616
+ rows = conn.execute(
617
+ "SELECT captured_at_utc, weekly_percent, five_hour_percent, "
618
+ " weekly_observation_held, account_key "
619
+ "FROM weekly_usage_snapshots "
620
+ "WHERE week_start_date = ? AND account_key = ? "
621
+ "ORDER BY captured_at_utc ASC, id ASC",
622
+ (scope.week_start_date, scope.account_key),
623
+ ).fetchall()
624
+ samples = _shape_week_samples(rows, include_held=True,
625
+ include_account=True)
626
+ samples = [s for s in samples if s[0] <= now_utc]
627
+ return (week_start_at, week_end_at, samples,
628
+ (scope.week_start_date, None))
629
+ week_start_at = parse_iso_datetime(scope.week_start_at, "week_start_at")
630
+ week_end_at = parse_iso_datetime(scope.week_end_at, "week_end_at")
631
+ # Every textual spelling of the same start instant, as the merged read
632
+ # gathers them for an upgrade-in-progress window.
633
+ matching_texts: list[str] = []
634
+ for (text,) in conn.execute(
635
+ "SELECT DISTINCT week_start_at FROM weekly_usage_snapshots "
636
+ "WHERE week_start_date >= ? AND account_key = ? "
637
+ "AND week_start_at IS NOT NULL AND week_end_at IS NOT NULL",
638
+ (floor_date, scope.account_key)).fetchall():
639
+ try:
640
+ if parse_iso_datetime(text, "week_start_at") == week_start_at:
641
+ matching_texts.append(text)
642
+ except ValueError:
643
+ continue
644
+ if scope.week_start_at not in matching_texts:
645
+ matching_texts.append(scope.week_start_at)
646
+ matching_texts.sort()
647
+ placeholders = ",".join("?" * len(matching_texts))
648
+ rows = _week_rows_by_capture(conn.execute(
649
+ f"SELECT captured_at_utc, weekly_percent, five_hour_percent, "
650
+ f" weekly_observation_held, account_key, id "
651
+ f"FROM weekly_usage_snapshots "
652
+ f"WHERE (week_start_at IN ({placeholders}) "
653
+ f" OR (week_start_at IS NULL AND week_start_date = ?)) "
654
+ f" AND account_key = ?",
655
+ tuple(matching_texts) + (scope.week_start_date, scope.account_key),
656
+ ), by_id=True)
657
+ samples = _shape_week_samples(rows, include_held=True,
658
+ include_account=True)
659
+ samples = [s for s in samples if s[0] <= now_utc]
660
+ return (week_start_at, week_end_at, samples,
661
+ (scope.week_start_date, tuple(matching_texts)))
662
+
663
+
392
664
  def _current_week_five_hour_percent(conn, now_utc, week_scope, *,
393
665
  account_key, week_start_at,
394
666
  week_start_before_override):