cctally 1.117.2 → 1.118.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 +30 -0
- package/README.md +7 -3
- package/bin/_cctally_cache.py +2783 -175
- package/bin/_cctally_completion.py +106 -33
- package/bin/_cctally_core.py +69 -4
- package/bin/_cctally_dashboard.py +188 -13
- package/bin/_cctally_dashboard_conversation.py +5 -1
- package/bin/_cctally_dashboard_envelope.py +50 -19
- package/bin/_cctally_dashboard_sources.py +23 -1
- package/bin/_cctally_db.py +193 -153
- package/bin/_cctally_diagnosis_sources.py +63 -7
- package/bin/_cctally_doctor.py +865 -93
- package/bin/_cctally_five_hour.py +67 -50
- package/bin/_cctally_forecast.py +35 -13
- package/bin/_cctally_journal.py +186 -18
- package/bin/_cctally_milestone_history.py +859 -62
- package/bin/_cctally_oauth_samples.py +286 -15
- package/bin/_cctally_parser.py +2 -2
- package/bin/_cctally_quota.py +104 -12
- package/bin/_cctally_record.py +153 -29
- package/bin/_cctally_rederive.py +608 -68
- package/bin/_cctally_statusline.py +108 -81
- package/bin/_cctally_store.py +179 -38
- package/bin/_cctally_tui.py +892 -181
- package/bin/_cctally_update.py +32 -13
- package/bin/_lib_codex_conversation_query.py +52 -14
- package/bin/_lib_codex_drain_admission.py +224 -6
- package/bin/_lib_codex_metadata.py +39 -0
- package/bin/_lib_conversation_anon.py +15 -8
- package/bin/_lib_conversation_dispatch.py +7 -0
- package/bin/_lib_conversation_query.py +289 -70
- package/bin/_lib_dashboard_sources.py +37 -1
- package/bin/_lib_detector_access_registry.py +497 -26
- package/bin/_lib_doctor.py +85 -20
- package/bin/_lib_doctor_scan_index.py +321 -0
- package/bin/_lib_ingest_frontier.py +20 -1
- package/bin/_lib_oauth_samples.py +23 -7
- package/bin/_lib_pricing.py +146 -16
- package/bin/_lib_quota.py +19 -0
- package/bin/_lib_rederive.py +11 -6
- package/bin/_lib_retained_records.py +516 -0
- package/bin/_lib_snapshot_cache.py +10 -0
- package/bin/_lib_stale_proof.py +272 -43
- package/bin/_lib_stats_content_revision.py +831 -0
- package/bin/_lib_write_io.py +11 -4
- package/bin/cctally +4 -1
- package/dashboard/static/assets/ConversationsView-D4Iv4ku0.js +72 -0
- package/dashboard/static/assets/{DoctorModal-B0DnU9d9.js → DoctorModal-D4QSq6uz.js} +1 -1
- package/dashboard/static/assets/ModalRoot-C9F5dhZi.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-DSxXQSa-.js → ProjectsDrillPanel-WxAdRDMt.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-BleDpQqG.js → SourceDetailModal-NyyEfXmc.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-CMqaAkSK.js → UpdateModal-CFLq9pT1.js} +1 -1
- package/dashboard/static/assets/{index-BjQV-0-D.js → index-BKTWH9JE.js} +2 -2
- package/dashboard/static/assets/{outlineNavigation-CO-VWBm7.js → outlineNavigation-BnGUwCS3.js} +3 -3
- package/dashboard/static/dashboard.html +1 -1
- package/package.json +4 -1
- package/dashboard/static/assets/ConversationsView-oqNsWzWV.js +0 -72
- package/dashboard/static/assets/ModalRoot-CYua2wP_.js +0 -1
package/bin/_lib_stale_proof.py
CHANGED
|
@@ -31,7 +31,9 @@ may be eligible for one direction and refused for the other.
|
|
|
31
31
|
"""
|
|
32
32
|
from __future__ import annotations
|
|
33
33
|
|
|
34
|
+
import bisect
|
|
34
35
|
import dataclasses
|
|
36
|
+
import functools
|
|
35
37
|
import hashlib
|
|
36
38
|
import json
|
|
37
39
|
from typing import Callable, Collection, Iterable, Literal, Mapping, Sequence
|
|
@@ -64,10 +66,12 @@ class MalformedReceipt(ValueError):
|
|
|
64
66
|
# --- instants --------------------------------------------------------------------
|
|
65
67
|
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
#: #1064 (spec 5.1.1): the bound of each scalar memo below, sized for a
|
|
70
|
+
#: week's provider samples (two instants each) with headroom.
|
|
71
|
+
_MEMO_SIZE = 65536
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _parse_interval(text: str) -> tuple[int, int] | None:
|
|
71
75
|
try:
|
|
72
76
|
start = _activity.instant_key(text)
|
|
73
77
|
digits = _activity.instant_precision(text)
|
|
@@ -76,6 +80,22 @@ def _interval(text: str | None) -> tuple[int, int] | None:
|
|
|
76
80
|
return start, start + 10 ** (9 - min(digits, 9))
|
|
77
81
|
|
|
78
82
|
|
|
83
|
+
_memo_interval = functools.lru_cache(maxsize=_MEMO_SIZE)(_parse_interval)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _interval(text: str | None) -> tuple[int, int] | None:
|
|
87
|
+
"""The ``[start, end)`` nanosecond interval a canonical instant denotes.
|
|
88
|
+
|
|
89
|
+
#1064: a pure function of the text, memoized for exact ``str`` values
|
|
90
|
+
only (a subclass is parsed uncached, so only plain text keys the
|
|
91
|
+
memo)."""
|
|
92
|
+
if not isinstance(text, str):
|
|
93
|
+
return None
|
|
94
|
+
if type(text) is str:
|
|
95
|
+
return _memo_interval(text)
|
|
96
|
+
return _parse_interval(text)
|
|
97
|
+
|
|
98
|
+
|
|
79
99
|
def after(a: str | None, b: str | None) -> bool:
|
|
80
100
|
"""Whether instant ``a`` is proven to follow instant ``b``."""
|
|
81
101
|
ia, ib = _interval(a), _interval(b)
|
|
@@ -220,15 +240,33 @@ def _window_text(window: int) -> str:
|
|
|
220
240
|
return _samples.normalize_weekly_reset(int(window))
|
|
221
241
|
|
|
222
242
|
|
|
223
|
-
def
|
|
224
|
-
if sample.weekly_resets_at_utc is None:
|
|
225
|
-
return None
|
|
243
|
+
def _parse_window_identity(value) -> str | None:
|
|
226
244
|
try:
|
|
227
|
-
return _samples.weekly_window(
|
|
245
|
+
return _samples.weekly_window(value)
|
|
228
246
|
except ValueError:
|
|
229
247
|
return None
|
|
230
248
|
|
|
231
249
|
|
|
250
|
+
_memo_window_identity = functools.lru_cache(maxsize=_MEMO_SIZE)(_parse_window_identity)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _window_identity(value) -> str | None:
|
|
254
|
+
"""The weekly window identity of a raw ``weekly_resets_at_utc`` value, or
|
|
255
|
+
None where `weekly_window` refuses it. #1064: memoized for exact ``str``
|
|
256
|
+
and ``int`` values (a bool, a subclass or another type is resolved
|
|
257
|
+
uncached, so ``True`` never shares ``1``'s entry); any other exception
|
|
258
|
+
propagates, uncached, exactly as before."""
|
|
259
|
+
if value is None:
|
|
260
|
+
return None
|
|
261
|
+
if type(value) is str or type(value) is int:
|
|
262
|
+
return _memo_window_identity(value)
|
|
263
|
+
return _parse_window_identity(value)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _sample_window(sample) -> str | None:
|
|
267
|
+
return _window_identity(sample.weekly_resets_at_utc)
|
|
268
|
+
|
|
269
|
+
|
|
232
270
|
def _in_window(samples: Iterable, window: int) -> list:
|
|
233
271
|
text = _window_text(window)
|
|
234
272
|
return [s for s in samples
|
|
@@ -260,6 +298,136 @@ def _latest(samples: Iterable):
|
|
|
260
298
|
return best
|
|
261
299
|
|
|
262
300
|
|
|
301
|
+
# --- the prepared sample view (#1064) ------------------------------------------------
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class _Window:
|
|
305
|
+
"""One ``(axis, window)`` of a `PreparedSamples`: the positions of its
|
|
306
|
+
entries in original order and, built on first use, the positions sorted
|
|
307
|
+
by `_latest`'s order with their order keys (False when the identities do
|
|
308
|
+
not sort) and the entries' axis values (False when one is not a plain
|
|
309
|
+
number)."""
|
|
310
|
+
|
|
311
|
+
__slots__ = ("positions", "ordered", "keys", "values")
|
|
312
|
+
|
|
313
|
+
def __init__(self, positions: list):
|
|
314
|
+
self.positions = positions
|
|
315
|
+
self.ordered = None
|
|
316
|
+
self.keys = None
|
|
317
|
+
self.values = None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class PreparedSamples:
|
|
321
|
+
"""One snapshot's provider samples, prepared once for every kernel call
|
|
322
|
+
that reads them (#1064 spec 5.1.2).
|
|
323
|
+
|
|
324
|
+
It keeps every original sample object in its original order (``iter``
|
|
325
|
+
yields them; nothing is deduplicated) with its request and capture
|
|
326
|
+
intervals and its `_latest` order key, and memoizes per ``(axis,
|
|
327
|
+
window)`` the in-window entries, computed on first use with the very
|
|
328
|
+
expressions `_in_window` and `_five_in_window` evaluate, so a conversion
|
|
329
|
+
error surfaces at the reader call that evaluates that window, as before.
|
|
330
|
+
It caches no fact, proof, receipt or compacted set."""
|
|
331
|
+
|
|
332
|
+
__slots__ = ("_samples", "_request", "_capture", "_order", "_windows")
|
|
333
|
+
|
|
334
|
+
def __init__(self, samples: Iterable):
|
|
335
|
+
self._samples = tuple(samples)
|
|
336
|
+
self._request = [_interval(s.requested_at_utc) for s in self._samples]
|
|
337
|
+
self._capture = [_interval(s.captured_at_utc) for s in self._samples]
|
|
338
|
+
self._order = [-1 if r is None else r[0] for r in self._request]
|
|
339
|
+
self._windows: dict = {}
|
|
340
|
+
|
|
341
|
+
def __iter__(self):
|
|
342
|
+
return iter(self._samples)
|
|
343
|
+
|
|
344
|
+
def __len__(self) -> int:
|
|
345
|
+
return len(self._samples)
|
|
346
|
+
|
|
347
|
+
def _window(self, axis: str, window) -> _Window:
|
|
348
|
+
"""The window `_axis_window` selects, memoized by its identity: the
|
|
349
|
+
normalized reset (weekly) or the exact ``int`` key (five-hour; any
|
|
350
|
+
other key type is filtered afresh on every call)."""
|
|
351
|
+
if axis == FIVE_HOUR:
|
|
352
|
+
if type(window) is not int:
|
|
353
|
+
return _Window(self._five_positions(window))
|
|
354
|
+
key = (FIVE_HOUR, window)
|
|
355
|
+
else:
|
|
356
|
+
key = (WEEKLY, _window_text(window))
|
|
357
|
+
found = self._windows.get(key)
|
|
358
|
+
if found is None:
|
|
359
|
+
positions = (self._five_positions(window) if axis == FIVE_HOUR
|
|
360
|
+
else self._weekly_positions(key[1]))
|
|
361
|
+
found = self._windows[key] = _Window(positions)
|
|
362
|
+
return found
|
|
363
|
+
|
|
364
|
+
def _weekly_positions(self, text: str) -> list:
|
|
365
|
+
return [i for i, s in enumerate(self._samples)
|
|
366
|
+
if s.weekly_percent is not None and _sample_window(s) == text]
|
|
367
|
+
|
|
368
|
+
def _five_positions(self, window) -> list:
|
|
369
|
+
return [i for i, s in enumerate(self._samples)
|
|
370
|
+
if s.five_hour_percent is not None and s.five_hour_window_key is not None
|
|
371
|
+
and int(s.five_hour_window_key) == int(window)]
|
|
372
|
+
|
|
373
|
+
def _ordered(self, w: _Window):
|
|
374
|
+
"""``w``'s positions sorted by ``(order key, observation_id, -position)``:
|
|
375
|
+
scanned from its end, the first entry that qualifies is `_latest` of
|
|
376
|
+
the qualifying entries (its strict ``>`` keeps the earliest original
|
|
377
|
+
position of a complete-key tie). False when the identities do not
|
|
378
|
+
sort; the caller then evaluates today's expressions instead."""
|
|
379
|
+
if w.ordered is None:
|
|
380
|
+
try:
|
|
381
|
+
w.ordered = sorted(w.positions, key=lambda i: (
|
|
382
|
+
self._order[i], self._samples[i].observation_id, -i))
|
|
383
|
+
except Exception: # noqa: BLE001 - the unprepared path decides
|
|
384
|
+
w.ordered = False
|
|
385
|
+
else:
|
|
386
|
+
w.keys = [self._order[i] for i in w.ordered]
|
|
387
|
+
return w.ordered
|
|
388
|
+
|
|
389
|
+
def _proof_values(self, w: _Window, axis: str):
|
|
390
|
+
"""``w``'s axis values by position when a proof may scan the sorted
|
|
391
|
+
entries (every value a plain number, so ``float`` cannot raise and
|
|
392
|
+
evaluation order is immaterial), else None."""
|
|
393
|
+
if self._ordered(w) is False:
|
|
394
|
+
return None
|
|
395
|
+
if w.values is None:
|
|
396
|
+
values = {}
|
|
397
|
+
for i in w.positions:
|
|
398
|
+
s = self._samples[i]
|
|
399
|
+
raw = s.five_hour_percent if axis == FIVE_HOUR else s.weekly_percent
|
|
400
|
+
if type(raw) not in (float, int, bool):
|
|
401
|
+
values = False
|
|
402
|
+
break
|
|
403
|
+
try:
|
|
404
|
+
values[i] = float(raw)
|
|
405
|
+
except OverflowError:
|
|
406
|
+
values = False
|
|
407
|
+
break
|
|
408
|
+
w.values = values
|
|
409
|
+
return None if w.values is False else w.values
|
|
410
|
+
|
|
411
|
+
def _latest_requested_after(self, w: _Window, bound_end: int, accept):
|
|
412
|
+
"""`_latest` of ``w``'s entries whose request starts at or after
|
|
413
|
+
``bound_end`` (an unparsable request never does) and that ``accept``
|
|
414
|
+
takes: the sorted entries from the bound on, scanned from the end."""
|
|
415
|
+
ordered, request = w.ordered, self._request
|
|
416
|
+
start = bisect.bisect_left(w.keys, bound_end)
|
|
417
|
+
for idx in range(len(ordered) - 1, start - 1, -1):
|
|
418
|
+
i = ordered[idx]
|
|
419
|
+
r = request[i]
|
|
420
|
+
if r is not None and r[0] >= bound_end and accept(i):
|
|
421
|
+
return self._samples[i]
|
|
422
|
+
return None
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def prepare(samples) -> PreparedSamples:
|
|
426
|
+
"""``samples`` as a `PreparedSamples`: itself when it already is one,
|
|
427
|
+
else a new view of it (the per-call fallback of every reader)."""
|
|
428
|
+
return samples if isinstance(samples, PreparedSamples) else PreparedSamples(samples)
|
|
429
|
+
|
|
430
|
+
|
|
263
431
|
# --- the proof -----------------------------------------------------------------------
|
|
264
432
|
|
|
265
433
|
|
|
@@ -276,14 +444,31 @@ def proven_stale(sigma: SampleIdentity, samples: Iterable, *, after_utc: str,
|
|
|
276
444
|
if sigma.axis not in AXES:
|
|
277
445
|
return None
|
|
278
446
|
reset = sigma.window if advertised_reset_epoch is None else int(advertised_reset_epoch)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
447
|
+
view = prepare(samples)
|
|
448
|
+
w = view._window(sigma.axis, sigma.window)
|
|
449
|
+
values = view._proof_values(w, sigma.axis)
|
|
450
|
+
if values is None:
|
|
451
|
+
proving = [
|
|
452
|
+
s for s in (view._samples[i] for i in w.positions)
|
|
453
|
+
if _axis_value(s, sigma.axis) < sigma.percent - TOLERANCE_PP
|
|
454
|
+
and after(s.requested_at_utc, sigma.activity_at_utc)
|
|
455
|
+
and after(s.requested_at_utc, after_utc)
|
|
456
|
+
and _before_epoch(s.captured_at_utc, reset)
|
|
457
|
+
]
|
|
458
|
+
return _latest(proving)
|
|
459
|
+
if not w.positions:
|
|
460
|
+
return None
|
|
461
|
+
limit = sigma.percent - TOLERANCE_PP
|
|
462
|
+
t_m, bound = _interval(sigma.activity_at_utc), _interval(after_utc)
|
|
463
|
+
if t_m is None or bound is None:
|
|
464
|
+
return None
|
|
465
|
+
reset_ns, capture = reset * _NS, view._capture
|
|
466
|
+
|
|
467
|
+
def accept(i):
|
|
468
|
+
c = capture[i]
|
|
469
|
+
return values[i] < limit and c is not None and c[1] <= reset_ns
|
|
470
|
+
|
|
471
|
+
return view._latest_requested_after(w, max(t_m[1], bound[1]), accept)
|
|
287
472
|
|
|
288
473
|
|
|
289
474
|
def proven_stale_low(bound_utc: str, samples: Iterable, *, axis: str, window: int,
|
|
@@ -302,13 +487,27 @@ def proven_stale_low(bound_utc: str, samples: Iterable, *, axis: str, window: in
|
|
|
302
487
|
if axis not in AXES or _interval(bound_utc) is None:
|
|
303
488
|
return None
|
|
304
489
|
reset = int(window) if advertised_reset_epoch is None else int(advertised_reset_epoch)
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
490
|
+
view = prepare(samples)
|
|
491
|
+
w = view._window(axis, window)
|
|
492
|
+
values = view._proof_values(w, axis)
|
|
493
|
+
if values is None:
|
|
494
|
+
proving = [
|
|
495
|
+
s for s in (view._samples[i] for i in w.positions)
|
|
496
|
+
if _axis_value(s, axis) > float(percent) + TOLERANCE_PP
|
|
497
|
+
and after(s.requested_at_utc, bound_utc)
|
|
498
|
+
and _before_epoch(s.captured_at_utc, reset)
|
|
499
|
+
]
|
|
500
|
+
return _latest(proving)
|
|
501
|
+
if not w.positions:
|
|
502
|
+
return None
|
|
503
|
+
limit = float(percent) + TOLERANCE_PP
|
|
504
|
+
reset_ns, capture = reset * _NS, view._capture
|
|
505
|
+
|
|
506
|
+
def accept(i):
|
|
507
|
+
c = capture[i]
|
|
508
|
+
return values[i] > limit and c is not None and c[1] <= reset_ns
|
|
509
|
+
|
|
510
|
+
return view._latest_requested_after(w, _interval(bound_utc)[1], accept)
|
|
312
511
|
|
|
313
512
|
|
|
314
513
|
# --- accounting ------------------------------------------------------------------------
|
|
@@ -384,12 +583,29 @@ def earn_receipts(members: Sequence[Member], accounted: Accounted, samples: Iter
|
|
|
384
583
|
no provider samples."""
|
|
385
584
|
if accounted.axis not in AXES or accounted.percent is None:
|
|
386
585
|
return ()
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
586
|
+
view = prepare(samples)
|
|
587
|
+
w = view._window(WEEKLY, accounted.window)
|
|
588
|
+
ordered = view._ordered(w)
|
|
589
|
+
if ordered is False:
|
|
590
|
+
segment = [s for s in (view._samples[i] for i in w.positions)
|
|
591
|
+
if accounted.floor_epoch is None
|
|
592
|
+
or _at_or_after_epoch(s.captured_at_utc, accounted.floor_epoch)]
|
|
593
|
+
generation = tuple((c.event_id, c.event_revision) for c in accounted.credits
|
|
594
|
+
if c.axis == accounted.axis)
|
|
595
|
+
latest = _latest(segment)
|
|
596
|
+
else:
|
|
597
|
+
# `_latest` of the segment: the sorted window scanned from its end
|
|
598
|
+
# to the first entry captured at or after the floor.
|
|
599
|
+
latest = None
|
|
600
|
+
floor, capture = accounted.floor_epoch, view._capture
|
|
601
|
+
for idx in range(len(ordered) - 1, -1, -1):
|
|
602
|
+
i = ordered[idx]
|
|
603
|
+
c = capture[i]
|
|
604
|
+
if floor is None or (c is not None and c[0] >= floor * _NS):
|
|
605
|
+
latest = view._samples[i]
|
|
606
|
+
break
|
|
607
|
+
generation = tuple((c.event_id, c.event_revision) for c in accounted.credits
|
|
608
|
+
if c.axis == accounted.axis)
|
|
393
609
|
earned = []
|
|
394
610
|
for m in members:
|
|
395
611
|
s = m.sigma
|
|
@@ -503,7 +719,7 @@ def refusal_facts(axis: str, members: Sequence[Member], samples: Iterable,
|
|
|
503
719
|
fields of `ALL_ELIGIBLE`."""
|
|
504
720
|
if axis not in AXES:
|
|
505
721
|
return {}
|
|
506
|
-
samples =
|
|
722
|
+
samples = prepare(samples)
|
|
507
723
|
transitions = list(transitions)
|
|
508
724
|
by_sigma: dict[str, list[Receipt]] = {}
|
|
509
725
|
for r in receipts:
|
|
@@ -653,21 +869,34 @@ def compact_samples(samples: Iterable, *, window: int,
|
|
|
653
869
|
``axis`` selects the window identity and value (five-hour: the canonical
|
|
654
870
|
key and the raw five-hour percent)."""
|
|
655
871
|
margin = int(window) - _RESET_MARGIN_SECONDS
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
872
|
+
view = prepare(samples)
|
|
873
|
+
items, order, capture = view._samples, view._order, view._capture
|
|
874
|
+
# #1064: the same sequential per-value algorithm over the prepared
|
|
875
|
+
# positions; `_latest` of two is "the later one replaces the earlier
|
|
876
|
+
# only when its (order key, observation_id) is strictly greater".
|
|
877
|
+
keep: dict[str, int] = {}
|
|
878
|
+
latest: dict[float, int] = {}
|
|
879
|
+
latest_floor: dict[float, int] = {}
|
|
880
|
+
|
|
881
|
+
def later(prev, i) -> int:
|
|
882
|
+
if prev is None or (order[i], items[i].observation_id) > (
|
|
883
|
+
order[prev], items[prev].observation_id):
|
|
884
|
+
return i
|
|
885
|
+
return prev
|
|
886
|
+
|
|
887
|
+
for i in view._window(axis, window).positions:
|
|
888
|
+
s, c = items[i], capture[i]
|
|
889
|
+
if not (c is not None and c[1] <= margin * _NS):
|
|
890
|
+
keep[s.observation_id] = i
|
|
662
891
|
continue
|
|
663
892
|
v = _axis_value(s, axis)
|
|
664
|
-
latest[v] =
|
|
665
|
-
if floor_epoch is None or
|
|
666
|
-
latest_floor[v] =
|
|
667
|
-
for
|
|
668
|
-
keep[
|
|
669
|
-
return tuple(sorted(
|
|
670
|
-
|
|
893
|
+
latest[v] = later(latest.get(v), i)
|
|
894
|
+
if floor_epoch is None or (c is not None and c[0] >= floor_epoch * _NS):
|
|
895
|
+
latest_floor[v] = later(latest_floor.get(v), i)
|
|
896
|
+
for i in [*latest.values(), *latest_floor.values()]:
|
|
897
|
+
keep[items[i].observation_id] = i
|
|
898
|
+
return tuple(items[i] for i in sorted(
|
|
899
|
+
keep.values(), key=lambda i: (order[i], items[i].observation_id)))
|
|
671
900
|
|
|
672
901
|
|
|
673
902
|
# --- codecs ------------------------------------------------------------------------------------
|