cctally 1.85.1 → 1.87.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 +16 -0
- package/bin/_cctally_cache.py +388 -35
- package/bin/_cctally_dashboard_sources.py +66 -33
- package/bin/_cctally_db.py +117 -1
- package/bin/_cctally_doctor.py +16 -1
- package/bin/_cctally_journal.py +5 -0
- package/bin/_cctally_quota.py +103 -11
- package/bin/_cctally_record.py +32 -6
- package/bin/_lib_aggregators.py +3 -0
- package/bin/_lib_codex_account_adoption.py +173 -0
- package/bin/_lib_doctor.py +28 -0
- package/bin/_lib_quota.py +170 -6
- package/dashboard/static/assets/index-B0ZCsoxI.css +1 -0
- package/dashboard/static/assets/index-Bvp8mxtz.js +92 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +2 -1
- package/dashboard/static/assets/index-DlVVJeS4.js +0 -92
- package/dashboard/static/assets/index-OYBkyglj.css +0 -1
package/bin/_cctally_record.py
CHANGED
|
@@ -702,6 +702,7 @@ def maybe_record_milestone(
|
|
|
702
702
|
|
|
703
703
|
# Threshold crossed — sync cost before recording so the milestone
|
|
704
704
|
# captures up-to-date cumulative cost, not a stale snapshot.
|
|
705
|
+
cost_synced = True
|
|
705
706
|
try:
|
|
706
707
|
if retained_selection is None:
|
|
707
708
|
retained_selection = _cctally().WeekSelection(
|
|
@@ -740,7 +741,13 @@ def maybe_record_milestone(
|
|
|
740
741
|
retained_selection=retained_selection,
|
|
741
742
|
)
|
|
742
743
|
except Exception as exc:
|
|
743
|
-
|
|
744
|
+
# The snapshot read below would now return a row from an EARLIER
|
|
745
|
+
# crossing, so recording here stamps that older cumulative onto
|
|
746
|
+
# this threshold — a write-once row with a $0.00 marginal and a
|
|
747
|
+
# fabricated $/1%. Fall through only far enough to reach the
|
|
748
|
+
# skip guard on the snapshot branch.
|
|
749
|
+
cost_synced = False
|
|
750
|
+
eprint(f"[milestone] cost sync failed: {exc}")
|
|
744
751
|
|
|
745
752
|
week_start = dt.date.fromisoformat(week_start_date)
|
|
746
753
|
week_end = dt.date.fromisoformat(week_end_date)
|
|
@@ -762,17 +769,36 @@ def maybe_record_milestone(
|
|
|
762
769
|
effective_ref = adjusted[0]
|
|
763
770
|
|
|
764
771
|
if _week_ref_has_reset_event(conn, effective_ref):
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
772
|
+
import _cctally_cache # fail-closed attribution guard (#341)
|
|
773
|
+
try:
|
|
774
|
+
live_cost = _compute_cost_for_weekref(
|
|
775
|
+
effective_ref,
|
|
776
|
+
account_key=account_key,
|
|
777
|
+
as_of=as_of,
|
|
778
|
+
)
|
|
779
|
+
except _cctally_cache.AccountAttributionUnavailable as exc:
|
|
780
|
+
# Same contract the budget ladder already holds (#341 Task 4):
|
|
781
|
+
# an account-scoped read that fell into the fail-closed guard
|
|
782
|
+
# SKIPS this tick and fires on the next healthy one. Never
|
|
783
|
+
# re-raised — on the passed-conn (ingest) path a bare raise
|
|
784
|
+
# would abort the whole cycle over a transient lock.
|
|
785
|
+
eprint("[milestone] account attribution unavailable, "
|
|
786
|
+
f"skipping this crossing: {exc}")
|
|
787
|
+
return
|
|
770
788
|
if live_cost is None:
|
|
771
789
|
eprint("[milestone] could not compute effective-range cost, skipping")
|
|
772
790
|
return
|
|
773
791
|
cumulative_cost = live_cost
|
|
774
792
|
cost_snapshot_id = 0 # no snapshot row to anchor against
|
|
775
793
|
else:
|
|
794
|
+
if not cost_synced:
|
|
795
|
+
# The latest snapshot predates this crossing. Milestones are
|
|
796
|
+
# write-once, so a stale cumulative here is permanent; skip
|
|
797
|
+
# instead. The next observation still sees current_floor >
|
|
798
|
+
# max_existing and records the crossing with a real cost.
|
|
799
|
+
eprint("[milestone] skipping this crossing — its cost would "
|
|
800
|
+
"come from a snapshot taken before the crossing")
|
|
801
|
+
return
|
|
776
802
|
# Account-scoped read (#341 P2-1): the cost snapshot was just
|
|
777
803
|
# materialized under `account_key`, so scope the read to it — the
|
|
778
804
|
# merged (account-blind) read would return another account's row on
|
package/bin/_lib_aggregators.py
CHANGED
|
@@ -387,6 +387,9 @@ class CodexBucketUsage:
|
|
|
387
387
|
period_end_at: dt.datetime | None = None
|
|
388
388
|
used_pct: float | None = None
|
|
389
389
|
dollar_per_pct: float | None = None
|
|
390
|
+
# #424: owning accounts for a pooled native weekly period. Empty for
|
|
391
|
+
# calendar buckets, focused account children, and undecorated providers.
|
|
392
|
+
account_keys: tuple[str, ...] = ()
|
|
390
393
|
|
|
391
394
|
|
|
392
395
|
@dataclass
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Codex window-scoped spend adoption (pure kernel).
|
|
2
|
+
|
|
3
|
+
Spec: ``docs/superpowers/specs/2026-07-30-codex-window-scoped-spend-adoption.md``
|
|
4
|
+
|
|
5
|
+
``adopt_unidentified_observations`` (``bin/_lib_quota.py``) applies the #341 §2
|
|
6
|
+
window-account continuity rule to the OBSERVATION axis: inside one physical
|
|
7
|
+
quota window, unidentified observations are adopted by the window's account iff
|
|
8
|
+
exactly one identified account is ever observed for that window key. This
|
|
9
|
+
kernel applies the same inference, with the same grouping key and the same
|
|
10
|
+
guard, to the SPEND axis — ``codex_session_entries.account_key``.
|
|
11
|
+
|
|
12
|
+
A pure leaf module: stdlib only, no cctally imports. The caller supplies window
|
|
13
|
+
descriptors (already grouped on ``_lib_quota._physical_window_key`` and already
|
|
14
|
+
folded, so ``identified_accounts`` is the window's post-fold identified set) and
|
|
15
|
+
candidate entries; this kernel decides, and only decides. Every SQL read and
|
|
16
|
+
every write stays in the glue layer.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import datetime as dt
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Iterable
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
#: Native length of the account-level Codex weekly quota window, in minutes.
|
|
26
|
+
ACCOUNT_WEEKLY_WINDOW_MINUTES = 10_080
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
#: The reserved "account could not be determined" sentinel
|
|
30
|
+
#: (``_lib_accounts.UNATTRIBUTED``), spelled here so this leaf stays import-free.
|
|
31
|
+
UNATTRIBUTED_SENTINEL = "unattributed"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def entry_is_unattributed(account_key: object) -> bool:
|
|
35
|
+
"""Whether a ``codex_session_entries`` row is still up for adoption.
|
|
36
|
+
|
|
37
|
+
``NULL`` is the stamp every never-decided row carries (#416 spec D1:
|
|
38
|
+
``stably_absent`` -> ``NULL``), and the empty string is its degenerate
|
|
39
|
+
spelling. The literal ``unattributed`` sentinel is admitted too: no producer
|
|
40
|
+
writes it to this column today, but ``_codex_cache_account_predicate`` counts
|
|
41
|
+
it in the ``unattributed`` BUCKET, so excluding it here would make such a row
|
|
42
|
+
permanently unadoptable — visible as nobody's money and ineligible for the
|
|
43
|
+
only mechanism that could give it an owner.
|
|
44
|
+
"""
|
|
45
|
+
return (
|
|
46
|
+
account_key is None
|
|
47
|
+
or account_key == ""
|
|
48
|
+
or account_key == UNATTRIBUTED_SENTINEL
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class SpendAdoptionWindow:
|
|
54
|
+
"""One physical Codex quota window, as the fold left it.
|
|
55
|
+
|
|
56
|
+
``canonical_resets_at`` is the tolerance-anchored reset (#416 §4.1), never a
|
|
57
|
+
raw jittered provider value — the same anchor
|
|
58
|
+
``_lib_quota._physical_window_key`` groups on. ``identified_accounts`` is
|
|
59
|
+
the set of non-``unattributed`` account keys observed for that key.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
source_root_key: str
|
|
63
|
+
window_minutes: int
|
|
64
|
+
canonical_resets_at: dt.datetime
|
|
65
|
+
identified_accounts: frozenset[str] = frozenset()
|
|
66
|
+
model_scoped: bool = False
|
|
67
|
+
|
|
68
|
+
def __post_init__(self) -> None:
|
|
69
|
+
if not isinstance(self.source_root_key, str) or not self.source_root_key:
|
|
70
|
+
raise ValueError("source_root_key must be a non-empty string")
|
|
71
|
+
if (not isinstance(self.window_minutes, int)
|
|
72
|
+
or isinstance(self.window_minutes, bool)
|
|
73
|
+
or self.window_minutes <= 0):
|
|
74
|
+
raise ValueError("window_minutes must be a positive integer")
|
|
75
|
+
reset = self.canonical_resets_at
|
|
76
|
+
if reset.tzinfo is None or reset.utcoffset() is None:
|
|
77
|
+
raise ValueError("canonical_resets_at must be timezone-aware")
|
|
78
|
+
object.__setattr__(
|
|
79
|
+
self, "identified_accounts", frozenset(self.identified_accounts))
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def nominal_start_at(self) -> dt.datetime:
|
|
83
|
+
return self.canonical_resets_at - dt.timedelta(
|
|
84
|
+
minutes=self.window_minutes)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def in_scope(self) -> bool:
|
|
88
|
+
"""Account-level weekly windows only.
|
|
89
|
+
|
|
90
|
+
A 5h window nests inside the weekly one and adds no evidence; a
|
|
91
|
+
model-scoped pool such as GPT-5.3-Codex-Spark is never account weekly
|
|
92
|
+
quota (#373). Both are excluded from candidacy entirely, so neither
|
|
93
|
+
stamps nor blocks.
|
|
94
|
+
"""
|
|
95
|
+
return (
|
|
96
|
+
self.window_minutes == ACCOUNT_WEEKLY_WINDOW_MINUTES
|
|
97
|
+
and not self.model_scoped
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def covers(self, timestamp: dt.datetime) -> bool:
|
|
101
|
+
"""Whether ``timestamp`` falls in the NOMINAL ``[start, reset)`` range.
|
|
102
|
+
|
|
103
|
+
Nominal rather than first-observation: spend before the window's first
|
|
104
|
+
retained observation is still spend inside the cycle.
|
|
105
|
+
"""
|
|
106
|
+
return self.nominal_start_at <= timestamp < self.canonical_resets_at
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class SpendAdoptionCandidate:
|
|
111
|
+
"""One ``codex_session_entries`` row offered to the pass."""
|
|
112
|
+
|
|
113
|
+
entry_id: int
|
|
114
|
+
source_root_key: str
|
|
115
|
+
timestamp: dt.datetime
|
|
116
|
+
account_key: "str | None" = None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True)
|
|
120
|
+
class SpendAdoptionStamp:
|
|
121
|
+
"""One decided write: give ``entry_id`` this account."""
|
|
122
|
+
|
|
123
|
+
entry_id: int
|
|
124
|
+
account_key: str
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def build_spend_adoption_plan(
|
|
128
|
+
windows: Iterable[SpendAdoptionWindow],
|
|
129
|
+
candidates: Iterable[SpendAdoptionCandidate],
|
|
130
|
+
) -> tuple[SpendAdoptionStamp, ...]:
|
|
131
|
+
"""Return the stamping plan, ordered by ``entry_id``.
|
|
132
|
+
|
|
133
|
+
An in-scope window CLAIMS every candidate its nominal range covers on its own
|
|
134
|
+
root. A candidate is stamped iff the UNION of identified accounts across
|
|
135
|
+
every claiming window is exactly one.
|
|
136
|
+
|
|
137
|
+
A claiming window that identifies no account contributes nothing to that
|
|
138
|
+
union and therefore does NOT block: absence of evidence is not evidence of
|
|
139
|
+
ambiguity. That is the same shape ``adopt_unidentified_observations`` uses on
|
|
140
|
+
the observation axis, which likewise resolves a window from its *identified*
|
|
141
|
+
observations only and treats an unidentified population as no evidence. The
|
|
142
|
+
first implementation blocked on any claiming window that resolved to nothing
|
|
143
|
+
and measured ZERO stamped rows on a real store: weekly resets move by days,
|
|
144
|
+
so one cycle overlaps many neighbours and pre-attribution history is
|
|
145
|
+
unidentified by construction — one such neighbour was always enough to veto.
|
|
146
|
+
|
|
147
|
+
Two claiming windows naming DIFFERENT accounts still leave the entry alone
|
|
148
|
+
(union of two), and so does a single window that itself saw two accounts
|
|
149
|
+
(#341 never-combine). An already-identified row is never re-stamped, so
|
|
150
|
+
re-running over this kernel's own output returns an empty plan.
|
|
151
|
+
"""
|
|
152
|
+
by_root: dict[str, list[SpendAdoptionWindow]] = {}
|
|
153
|
+
for window in windows:
|
|
154
|
+
if window.in_scope:
|
|
155
|
+
by_root.setdefault(window.source_root_key, []).append(window)
|
|
156
|
+
|
|
157
|
+
stamps: list[SpendAdoptionStamp] = []
|
|
158
|
+
for candidate in candidates:
|
|
159
|
+
if not entry_is_unattributed(candidate.account_key):
|
|
160
|
+
continue
|
|
161
|
+
identified: set[str] = set()
|
|
162
|
+
for window in by_root.get(candidate.source_root_key, ()):
|
|
163
|
+
if window.covers(candidate.timestamp):
|
|
164
|
+
identified |= window.identified_accounts
|
|
165
|
+
if len(identified) > 1:
|
|
166
|
+
break
|
|
167
|
+
if len(identified) != 1:
|
|
168
|
+
continue
|
|
169
|
+
account = next(iter(identified))
|
|
170
|
+
stamps.append(SpendAdoptionStamp(
|
|
171
|
+
entry_id=candidate.entry_id, account_key=account))
|
|
172
|
+
stamps.sort(key=lambda stamp: (stamp.entry_id, stamp.account_key))
|
|
173
|
+
return tuple(stamps)
|
package/bin/_lib_doctor.py
CHANGED
|
@@ -2607,6 +2607,33 @@ def _check_accounts_attribution(s: DoctorState) -> CheckResult:
|
|
|
2607
2607
|
)
|
|
2608
2608
|
|
|
2609
2609
|
|
|
2610
|
+
def _check_accounts_codex_reset_anchors(s: DoctorState) -> CheckResult:
|
|
2611
|
+
"""Surface Codex quota rows that landed after migration 032 with no anchor."""
|
|
2612
|
+
st = s.accounts_state or {}
|
|
2613
|
+
try:
|
|
2614
|
+
null_rows = int(st.get("codex_null_reset_anchors") or 0)
|
|
2615
|
+
except (TypeError, ValueError):
|
|
2616
|
+
null_rows = 0
|
|
2617
|
+
details = {"null_anchor_rows": null_rows}
|
|
2618
|
+
if null_rows > 0:
|
|
2619
|
+
return CheckResult(
|
|
2620
|
+
id="accounts.codex_reset_anchors",
|
|
2621
|
+
title="Codex reset anchors",
|
|
2622
|
+
severity="warn",
|
|
2623
|
+
summary=f"{null_rows} Codex quota observation(s) lack a canonical reset anchor",
|
|
2624
|
+
remediation="Run `cctally cache-sync --source codex --rebuild`",
|
|
2625
|
+
details=details,
|
|
2626
|
+
)
|
|
2627
|
+
return CheckResult(
|
|
2628
|
+
id="accounts.codex_reset_anchors",
|
|
2629
|
+
title="Codex reset anchors",
|
|
2630
|
+
severity="ok",
|
|
2631
|
+
summary="Codex reset anchors complete",
|
|
2632
|
+
remediation=None,
|
|
2633
|
+
details=details,
|
|
2634
|
+
)
|
|
2635
|
+
|
|
2636
|
+
|
|
2610
2637
|
_CATEGORY_DEFINITIONS: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...] = (
|
|
2611
2638
|
("install", "Install", (
|
|
2612
2639
|
("install.mode", "_check_install_dev_mode"),
|
|
@@ -2667,6 +2694,7 @@ _CATEGORY_DEFINITIONS: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...]
|
|
|
2667
2694
|
("accounts.registry", "_check_accounts_registry"),
|
|
2668
2695
|
("accounts.freshness", "_check_accounts_freshness"),
|
|
2669
2696
|
("accounts.attribution", "_check_accounts_attribution"),
|
|
2697
|
+
("accounts.codex_reset_anchors", "_check_accounts_codex_reset_anchors"),
|
|
2670
2698
|
)),
|
|
2671
2699
|
("pricing", "Pricing", (
|
|
2672
2700
|
("pricing.coverage", "_check_pricing_coverage"),
|
package/bin/_lib_quota.py
CHANGED
|
@@ -78,11 +78,11 @@ def resolve_reset_anchor(
|
|
|
78
78
|
anchor already in ``anchors`` or the observation's own raw value, never a
|
|
79
79
|
recomputed centroid. Assignment picks the NEAREST anchor within tolerance,
|
|
80
80
|
breaking a tie on the earlier anchor, so for a given anchor set the answer
|
|
81
|
-
does not depend on the order the anchors were established in.
|
|
82
|
-
SET can still depend on arrival order in the
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
does not depend on the order the anchors were established in. The anchor
|
|
82
|
+
SET can still depend on arrival order in the chain-of-neighbours case. This
|
|
83
|
+
is retained as migration 032's reference rule; production ingest uses
|
|
84
|
+
:class:`ResetAnchorComponents` after #425 real-data evidence proved those
|
|
85
|
+
chains occur.
|
|
86
86
|
"""
|
|
87
87
|
_require_aware(raw_reset, "raw_reset")
|
|
88
88
|
if (isinstance(anchors, ResetAnchorIndex)
|
|
@@ -121,7 +121,9 @@ class ResetAnchorIndex:
|
|
|
121
121
|
nearest within tolerance, ties broken on the earlier anchor, first sight
|
|
122
122
|
wins, the anchor never moves, and the pathological chain-of-neighbours
|
|
123
123
|
order-dependence that docstring documents is preserved exactly, because the
|
|
124
|
-
anchor SET is still whatever the caller established in whatever order.
|
|
124
|
+
anchor SET is still whatever the caller established in whatever order. This
|
|
125
|
+
class remains the exact migration-032 oracle; current ingest uses
|
|
126
|
+
``ResetAnchorComponents``.
|
|
125
127
|
|
|
126
128
|
Only the LOOKUP changes. The linear scan did a full ``datetime`` subtraction
|
|
127
129
|
against every established anchor, and a 5h group accumulates ~1,750 anchors
|
|
@@ -204,6 +206,156 @@ class ResetAnchorIndex:
|
|
|
204
206
|
return len(self._order)
|
|
205
207
|
|
|
206
208
|
|
|
209
|
+
class ResetAnchorComponents:
|
|
210
|
+
"""Tolerance-connected raw-reset components with first-sight anchors.
|
|
211
|
+
|
|
212
|
+
``ResetAnchorIndex`` intentionally preserves the original migration-032
|
|
213
|
+
rule: compare a raw reset only with already-established anchors. Real data
|
|
214
|
+
proved that rule can split a chain whose adjacent members are all within
|
|
215
|
+
tolerance. This index retains every distinct raw reset as evidence and
|
|
216
|
+
unions adjacent members transitively. The member with the smallest stable
|
|
217
|
+
physical ``order_key`` remains the completed component's canonical anchor,
|
|
218
|
+
independent of filesystem traversal or journal batch arrival. Callers that
|
|
219
|
+
omit an order key retain insertion-order behavior for compatibility.
|
|
220
|
+
|
|
221
|
+
``add`` returns both the winning anchor and any formerly independent
|
|
222
|
+
component anchors retired by the union. Writers use the retired set to
|
|
223
|
+
converge rows materialized before a later bridge observation arrived.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
__slots__ = (
|
|
227
|
+
"_tolerance", "_buckets", "_parent", "_rank",
|
|
228
|
+
"_anchor", "_first_order", "_member_order", "_next_order",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def __init__(
|
|
232
|
+
self, raws: Iterable[dt.datetime] = (),
|
|
233
|
+
*, tolerance_seconds: int = CODEX_RESET_ANCHOR_TOLERANCE_SECONDS,
|
|
234
|
+
) -> None:
|
|
235
|
+
if not isinstance(tolerance_seconds, int) or isinstance(
|
|
236
|
+
tolerance_seconds, bool):
|
|
237
|
+
raise ValueError("tolerance_seconds must be an int")
|
|
238
|
+
if tolerance_seconds <= 0:
|
|
239
|
+
raise ValueError("tolerance_seconds must be positive")
|
|
240
|
+
self._tolerance = tolerance_seconds
|
|
241
|
+
self._buckets: dict[int, list[dt.datetime]] = {}
|
|
242
|
+
self._parent: dict[dt.datetime, dt.datetime] = {}
|
|
243
|
+
self._rank: dict[dt.datetime, int] = {}
|
|
244
|
+
self._anchor: dict[dt.datetime, dt.datetime] = {}
|
|
245
|
+
self._first_order: dict[
|
|
246
|
+
dt.datetime, tuple[str, int, int]
|
|
247
|
+
] = {}
|
|
248
|
+
self._member_order: dict[
|
|
249
|
+
dt.datetime, tuple[str, int, int]
|
|
250
|
+
] = {}
|
|
251
|
+
self._next_order = 0
|
|
252
|
+
for raw in raws:
|
|
253
|
+
self.add(raw)
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def tolerance_seconds(self) -> int:
|
|
257
|
+
return self._tolerance
|
|
258
|
+
|
|
259
|
+
def _bucket(self, value: dt.datetime) -> int:
|
|
260
|
+
return int(value.timestamp() // self._tolerance)
|
|
261
|
+
|
|
262
|
+
def _find(self, value: dt.datetime) -> dt.datetime:
|
|
263
|
+
parent = self._parent[value]
|
|
264
|
+
if parent != value:
|
|
265
|
+
self._parent[value] = self._find(parent)
|
|
266
|
+
return self._parent[value]
|
|
267
|
+
|
|
268
|
+
def _union(
|
|
269
|
+
self, left: dt.datetime, right: dt.datetime,
|
|
270
|
+
) -> dt.datetime:
|
|
271
|
+
left_root = self._find(left)
|
|
272
|
+
right_root = self._find(right)
|
|
273
|
+
if left_root == right_root:
|
|
274
|
+
return left_root
|
|
275
|
+
if self._rank[left_root] < self._rank[right_root]:
|
|
276
|
+
left_root, right_root = right_root, left_root
|
|
277
|
+
self._parent[right_root] = left_root
|
|
278
|
+
if self._rank[left_root] == self._rank[right_root]:
|
|
279
|
+
self._rank[left_root] += 1
|
|
280
|
+
if self._first_order[right_root] < self._first_order[left_root]:
|
|
281
|
+
self._anchor[left_root] = self._anchor[right_root]
|
|
282
|
+
self._first_order[left_root] = self._first_order[right_root]
|
|
283
|
+
del self._anchor[right_root]
|
|
284
|
+
del self._first_order[right_root]
|
|
285
|
+
return left_root
|
|
286
|
+
|
|
287
|
+
def add(
|
|
288
|
+
self, raw_reset: dt.datetime,
|
|
289
|
+
*, order_key: "tuple[str, int, int] | None" = None,
|
|
290
|
+
) -> tuple[dt.datetime, tuple[dt.datetime, ...]]:
|
|
291
|
+
"""Add one raw reset and return ``(anchor, retired_anchors)``."""
|
|
292
|
+
_require_aware(raw_reset, "raw_reset")
|
|
293
|
+
if order_key is None:
|
|
294
|
+
order_key = ("", self._next_order, 0)
|
|
295
|
+
if (
|
|
296
|
+
not isinstance(order_key, tuple) or len(order_key) != 3
|
|
297
|
+
or not isinstance(order_key[0], str)
|
|
298
|
+
or not isinstance(order_key[1], int)
|
|
299
|
+
or isinstance(order_key[1], bool)
|
|
300
|
+
or not isinstance(order_key[2], int)
|
|
301
|
+
or isinstance(order_key[2], bool)
|
|
302
|
+
):
|
|
303
|
+
raise ValueError(
|
|
304
|
+
"order_key must be a (source_path, line_offset, row_id) tuple")
|
|
305
|
+
self._next_order += 1
|
|
306
|
+
if raw_reset in self._parent:
|
|
307
|
+
root = self._find(raw_reset)
|
|
308
|
+
previous = self._anchor[root]
|
|
309
|
+
if order_key < self._member_order[raw_reset]:
|
|
310
|
+
self._member_order[raw_reset] = order_key
|
|
311
|
+
if order_key < self._first_order[root]:
|
|
312
|
+
self._first_order[root] = order_key
|
|
313
|
+
self._anchor[root] = raw_reset
|
|
314
|
+
winner = self._anchor[root]
|
|
315
|
+
retired = (previous,) if previous != winner else ()
|
|
316
|
+
return winner, retired
|
|
317
|
+
|
|
318
|
+
probe = self._bucket(raw_reset)
|
|
319
|
+
neighbours: list[dt.datetime] = []
|
|
320
|
+
for bucket_id in (probe - 1, probe, probe + 1):
|
|
321
|
+
for candidate in self._buckets.get(bucket_id, ()):
|
|
322
|
+
if abs((raw_reset - candidate).total_seconds()) <= self._tolerance:
|
|
323
|
+
neighbours.append(candidate)
|
|
324
|
+
|
|
325
|
+
self._parent[raw_reset] = raw_reset
|
|
326
|
+
self._rank[raw_reset] = 0
|
|
327
|
+
self._anchor[raw_reset] = raw_reset
|
|
328
|
+
self._first_order[raw_reset] = order_key
|
|
329
|
+
self._member_order[raw_reset] = order_key
|
|
330
|
+
self._buckets.setdefault(probe, []).append(raw_reset)
|
|
331
|
+
|
|
332
|
+
prior_anchors = {raw_reset}
|
|
333
|
+
prior_anchors.update(
|
|
334
|
+
self._anchor[self._find(candidate)] for candidate in neighbours)
|
|
335
|
+
root = raw_reset
|
|
336
|
+
for candidate in neighbours:
|
|
337
|
+
root = self._union(root, candidate)
|
|
338
|
+
winner = self._anchor[self._find(root)]
|
|
339
|
+
return winner, tuple(sorted(prior_anchors - {winner}))
|
|
340
|
+
|
|
341
|
+
def canonical(self, raw_reset: dt.datetime) -> dt.datetime:
|
|
342
|
+
"""Return the completed component's first-sight anchor."""
|
|
343
|
+
_require_aware(raw_reset, "raw_reset")
|
|
344
|
+
return self._anchor[self._find(raw_reset)]
|
|
345
|
+
|
|
346
|
+
def __contains__(self, anchor: object) -> bool:
|
|
347
|
+
if not isinstance(anchor, dt.datetime):
|
|
348
|
+
return False
|
|
349
|
+
return anchor in self._buckets.get(self._bucket(anchor), ())
|
|
350
|
+
|
|
351
|
+
def __iter__(self):
|
|
352
|
+
"""Distinct raw-reset evidence in insertion order."""
|
|
353
|
+
return iter(self._parent)
|
|
354
|
+
|
|
355
|
+
def __len__(self) -> int:
|
|
356
|
+
return len(self._parent)
|
|
357
|
+
|
|
358
|
+
|
|
207
359
|
@dataclass(frozen=True)
|
|
208
360
|
class QuotaWindowIdentity:
|
|
209
361
|
"""One root-qualified native quota window identity.
|
|
@@ -313,6 +465,7 @@ class QuotaBlock:
|
|
|
313
465
|
last_observed_at: dt.datetime
|
|
314
466
|
first_percent: float
|
|
315
467
|
current_percent: float
|
|
468
|
+
physical_observations: tuple[QuotaObservation, ...] = ()
|
|
316
469
|
|
|
317
470
|
|
|
318
471
|
@dataclass(frozen=True)
|
|
@@ -577,7 +730,14 @@ def build_blocks(observations: Iterable[QuotaObservation]) -> tuple[QuotaBlock,
|
|
|
577
730
|
``QuotaBlock.resets_at``, which every renderer shows, is that same anchor.
|
|
578
731
|
"""
|
|
579
732
|
by_block: dict[tuple[QuotaWindowIdentity, dt.datetime], list[QuotaObservation]] = {}
|
|
733
|
+
physical_by_block: dict[
|
|
734
|
+
tuple[QuotaWindowIdentity, dt.datetime], list[QuotaObservation]
|
|
735
|
+
] = {}
|
|
580
736
|
for history in build_history(observations):
|
|
737
|
+
for observation in history.physical_observations:
|
|
738
|
+
physical_by_block.setdefault(
|
|
739
|
+
(history.identity, observation.canonical_resets_at), []
|
|
740
|
+
).append(observation)
|
|
581
741
|
for observation in history.observations:
|
|
582
742
|
by_block.setdefault(
|
|
583
743
|
(history.identity, observation.canonical_resets_at), []
|
|
@@ -599,6 +759,10 @@ def build_blocks(observations: Iterable[QuotaObservation]) -> tuple[QuotaBlock,
|
|
|
599
759
|
last_observed_at=last.captured_at,
|
|
600
760
|
first_percent=first.used_percent,
|
|
601
761
|
current_percent=last.used_percent,
|
|
762
|
+
physical_observations=tuple(sorted(
|
|
763
|
+
physical_by_block[(identity, resets_at)],
|
|
764
|
+
key=physical_order_key,
|
|
765
|
+
)),
|
|
602
766
|
))
|
|
603
767
|
return tuple(blocks)
|
|
604
768
|
|