cctally 1.85.1 → 1.86.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 +11 -0
- package/bin/_cctally_cache.py +136 -35
- package/bin/_cctally_dashboard_sources.py +66 -33
- package/bin/_cctally_db.py +67 -1
- package/bin/_cctally_doctor.py +16 -1
- package/bin/_cctally_journal.py +5 -0
- package/bin/_cctally_quota.py +31 -8
- package/bin/_cctally_record.py +32 -6
- package/bin/_lib_aggregators.py +3 -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 +1 -1
- package/dashboard/static/assets/index-DlVVJeS4.js +0 -92
- package/dashboard/static/assets/index-OYBkyglj.css +0 -1
package/bin/_cctally_doctor.py
CHANGED
|
@@ -995,6 +995,7 @@ def _doctor_gather_state_impl(
|
|
|
995
995
|
codex_last_entry_at = None
|
|
996
996
|
codex_project_metadata_health = None
|
|
997
997
|
codex_project_metadata_error = None
|
|
998
|
+
codex_null_reset_anchors = 0
|
|
998
999
|
try:
|
|
999
1000
|
if _cache_probe_allowed and _cctally_core.CACHE_DB_PATH.exists():
|
|
1000
1001
|
conn = sqlite3.connect(str(_cctally_core.CACHE_DB_PATH))
|
|
@@ -1008,6 +1009,18 @@ def _doctor_gather_state_impl(
|
|
|
1008
1009
|
codex_last_entry_at = parse_iso_datetime(
|
|
1009
1010
|
row[1], "codex_session_entries.timestamp_utc",
|
|
1010
1011
|
).astimezone(dt.timezone.utc)
|
|
1012
|
+
try:
|
|
1013
|
+
row = conn.execute(
|
|
1014
|
+
"SELECT COUNT(*) FROM quota_window_snapshots "
|
|
1015
|
+
"WHERE source = 'codex' "
|
|
1016
|
+
"AND canonical_resets_at_utc IS NULL"
|
|
1017
|
+
).fetchone()
|
|
1018
|
+
if row and row[0] is not None:
|
|
1019
|
+
codex_null_reset_anchors = int(row[0])
|
|
1020
|
+
except sqlite3.OperationalError:
|
|
1021
|
+
# Pre-anchor cache shapes have no column to inspect. Their
|
|
1022
|
+
# pending migration is reported by the DB checks instead.
|
|
1023
|
+
pass
|
|
1011
1024
|
# Keep the health probe on the existing read-only cache
|
|
1012
1025
|
# connection. A failed probe is health evidence, not an
|
|
1013
1026
|
# empty corpus: the kernel renders it as a distinct FAIL.
|
|
@@ -1645,6 +1658,8 @@ def _doctor_gather_state_impl(
|
|
|
1645
1658
|
cctally_version = (
|
|
1646
1659
|
cctally_version_tuple[0] if cctally_version_tuple else "unknown"
|
|
1647
1660
|
)
|
|
1661
|
+
accounts_state = _gather_accounts_state(now_utc)
|
|
1662
|
+
accounts_state["codex_null_reset_anchors"] = codex_null_reset_anchors
|
|
1648
1663
|
|
|
1649
1664
|
return _lib_doctor.DoctorState(
|
|
1650
1665
|
symlink_state=symlink_state,
|
|
@@ -1742,7 +1757,7 @@ def _doctor_gather_state_impl(
|
|
|
1742
1757
|
journal_heal_incidents=journal_heal_incidents,
|
|
1743
1758
|
journal_writer_guard=journal_writer_guard,
|
|
1744
1759
|
# Multi-account attribution legs (#341).
|
|
1745
|
-
accounts_state=
|
|
1760
|
+
accounts_state=accounts_state,
|
|
1746
1761
|
cache_repair_marker=cache_repair_marker,
|
|
1747
1762
|
backup_sync_state=backup_sync_state,
|
|
1748
1763
|
)
|
package/bin/_cctally_journal.py
CHANGED
|
@@ -1489,6 +1489,8 @@ def _resolve_obs_anchor(resolver, rec: dict) -> "str | None":
|
|
|
1489
1489
|
source_root_key=root, observed_slot=slot, logical_limit_key=key,
|
|
1490
1490
|
window_minutes=p.get("window_minutes"),
|
|
1491
1491
|
resets_at_utc=p.get("resets_at_utc"),
|
|
1492
|
+
source_path=p.get("source_path"),
|
|
1493
|
+
line_offset=p.get("line_offset"),
|
|
1492
1494
|
)
|
|
1493
1495
|
except Exception: # pragma: no cover — never fail an ingest over a label
|
|
1494
1496
|
return None
|
|
@@ -1523,6 +1525,9 @@ def _apply_quota_records(cache, records) -> None:
|
|
|
1523
1525
|
for rec in records:
|
|
1524
1526
|
covered, decided = oracle.resolve(rec)
|
|
1525
1527
|
anchor = _resolve_obs_anchor(anchors, rec)
|
|
1528
|
+
if anchors is not None:
|
|
1529
|
+
anchors.apply_pending_merges()
|
|
1530
|
+
anchors.mark_file_committed()
|
|
1526
1531
|
row_values = _quota_snapshot_values(rec, anchor)
|
|
1527
1532
|
if not has_anchor:
|
|
1528
1533
|
row_values = row_values[:-1]
|
package/bin/_cctally_quota.py
CHANGED
|
@@ -966,7 +966,9 @@ def _evaluate_quota_alerts(
|
|
|
966
966
|
return queued
|
|
967
967
|
|
|
968
968
|
|
|
969
|
-
def _reanchor_terminal_events_sql(
|
|
969
|
+
def _reanchor_terminal_events_sql(
|
|
970
|
+
key_slots: int, minute_slots: int, reset_slots: int,
|
|
971
|
+
) -> str:
|
|
970
972
|
# `UPDATE OR IGNORE`, not a plain UPDATE: if this identity already carries an
|
|
971
973
|
# anchored row at the same threshold, moving the jittered twin onto it would
|
|
972
974
|
# violate the UNIQUE key. OR IGNORE SKIPS that move (it does not delete the
|
|
@@ -978,6 +980,11 @@ def _reanchor_terminal_events_sql(key_slots: int, minute_slots: int) -> str:
|
|
|
978
980
|
# transaction.
|
|
979
981
|
keys = ",".join(f":key{i}" for i in range(key_slots))
|
|
980
982
|
minutes = ",".join(f":min{i}" for i in range(minute_slots))
|
|
983
|
+
member_epochs = ",".join(f":reset{i}" for i in range(reset_slots))
|
|
984
|
+
member_clause = (
|
|
985
|
+
f" OR unixepoch(resets_at_utc) IN ({member_epochs})"
|
|
986
|
+
if member_epochs else ""
|
|
987
|
+
)
|
|
981
988
|
return (
|
|
982
989
|
"UPDATE OR IGNORE quota_threshold_events "
|
|
983
990
|
" SET resets_at_utc = :anchor, "
|
|
@@ -989,7 +996,9 @@ def _reanchor_terminal_events_sql(key_slots: int, minute_slots: int) -> str:
|
|
|
989
996
|
f" AND observed_slot = :slot AND window_minutes IN ({minutes}) "
|
|
990
997
|
" AND (resets_at_utc <> :anchor OR logical_limit_key <> :limit_key "
|
|
991
998
|
" OR window_minutes <> :minutes) "
|
|
992
|
-
" AND
|
|
999
|
+
" AND ("
|
|
1000
|
+
" abs(unixepoch(resets_at_utc) - unixepoch(:anchor)) <= :tolerance"
|
|
1001
|
+
f"{member_clause})"
|
|
993
1002
|
)
|
|
994
1003
|
|
|
995
1004
|
|
|
@@ -1020,15 +1029,24 @@ def _reanchor_terminal_events(conn: sqlite3.Connection, block) -> None:
|
|
|
1020
1029
|
re-materialization (they share this body), and is idempotent: a row already
|
|
1021
1030
|
on the canonical identity is excluded by the three-way `<>` guard.
|
|
1022
1031
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1032
|
+
The reset match accepts either the original 600-second anchor neighbourhood
|
|
1033
|
+
or an exact raw reset retained by this block. The latter is required by
|
|
1034
|
+
#425's transitive component closure: an endpoint can be farther than 600s
|
|
1035
|
+
from the first-sight anchor while still joining it through retained bridge
|
|
1036
|
+
observations. Exact membership keeps the widened reach evidence-bound; a
|
|
1037
|
+
genuinely different cycle is never inferred from distance alone. The length
|
|
1038
|
+
axis remains bounded by its ±1 minute snap.
|
|
1028
1039
|
"""
|
|
1029
1040
|
identity = block.identity
|
|
1030
1041
|
keys = codex_snap_equivalent_limit_keys(identity.logical_limit_key)
|
|
1031
1042
|
minutes = codex_snap_equivalent_window_minutes(identity.window_minutes)
|
|
1043
|
+
membership_evidence = (
|
|
1044
|
+
block.physical_observations or block.observations
|
|
1045
|
+
)
|
|
1046
|
+
reset_epochs = sorted({
|
|
1047
|
+
int(observation.resets_at.timestamp())
|
|
1048
|
+
for observation in membership_evidence
|
|
1049
|
+
})
|
|
1032
1050
|
params: dict[str, object] = {
|
|
1033
1051
|
"anchor": _utc_iso(block.resets_at),
|
|
1034
1052
|
"source": identity.source,
|
|
@@ -1041,8 +1059,13 @@ def _reanchor_terminal_events(conn: sqlite3.Connection, block) -> None:
|
|
|
1041
1059
|
}
|
|
1042
1060
|
params.update({f"key{i}": value for i, value in enumerate(keys)})
|
|
1043
1061
|
params.update({f"min{i}": value for i, value in enumerate(minutes)})
|
|
1062
|
+
params.update({
|
|
1063
|
+
f"reset{i}": value for i, value in enumerate(reset_epochs)})
|
|
1044
1064
|
conn.execute(
|
|
1045
|
-
_reanchor_terminal_events_sql(
|
|
1065
|
+
_reanchor_terminal_events_sql(
|
|
1066
|
+
len(keys), len(minutes), len(reset_epochs)),
|
|
1067
|
+
params,
|
|
1068
|
+
)
|
|
1046
1069
|
|
|
1047
1070
|
|
|
1048
1071
|
def _apply_quota_projection_rows(
|
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
|
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
|
|