From ea5c612446b444f9479aac9a45586bc95cf0fe00 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 13 May 2026 09:29:48 +0000 Subject: [PATCH] feat(analysis): _detect_gaps su timestamp consecutivi (> 20 min) Co-Authored-By: Claude Sonnet 4.6 --- src/cerbero_bite/analysis/data_audit.py | 16 ++++++++++++ tests/unit/test_data_audit.py | 34 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/cerbero_bite/analysis/data_audit.py b/src/cerbero_bite/analysis/data_audit.py index 41c6602..d5522e1 100644 --- a/src/cerbero_bite/analysis/data_audit.py +++ b/src/cerbero_bite/analysis/data_audit.py @@ -124,3 +124,19 @@ def _expected_ticks(since: datetime, until: datetime) -> int: # aligned multiples and would mis-count non-aligned spans. span_seconds = (until - first_tick).total_seconds() return math.ceil(span_seconds / (_TICK_INTERVAL_MIN * 60)) + + +def _detect_gaps(timestamps: list[datetime]) -> tuple[GapRecord, ...]: + """Return gaps where consecutive timestamps differ by > threshold.""" + out: list[GapRecord] = [] + for prev, nxt in zip(timestamps, timestamps[1:], strict=False): + delta_min = int((nxt - prev).total_seconds() // 60) + if delta_min > _GAP_THRESHOLD_MIN: + out.append( + GapRecord( + prev_timestamp=prev, + next_timestamp=nxt, + gap_minutes=delta_min, + ) + ) + return tuple(out) diff --git a/tests/unit/test_data_audit.py b/tests/unit/test_data_audit.py index 26e3195..1e20366 100644 --- a/tests/unit/test_data_audit.py +++ b/tests/unit/test_data_audit.py @@ -43,3 +43,37 @@ def test_expected_ticks_exact_quarter_boundary_is_excluded() -> None: until = datetime(2026, 5, 12, 12, 45, tzinfo=UTC) # Ticks at 12:00, 12:15, 12:30 → 3 (12:45 excluded) assert _expected_ticks(since, until) == 3 + + +from cerbero_bite.analysis.data_audit import _detect_gaps # noqa: PLC2701 + + +def test_detect_gaps_returns_empty_when_no_gap() -> None: + ts = [ + datetime(2026, 5, 12, 12, 0, tzinfo=UTC), + datetime(2026, 5, 12, 12, 15, tzinfo=UTC), + datetime(2026, 5, 12, 12, 30, tzinfo=UTC), + ] + assert _detect_gaps(ts) == () + + +def test_detect_gaps_flags_above_threshold() -> None: + ts = [ + datetime(2026, 5, 12, 12, 0, tzinfo=UTC), + datetime(2026, 5, 12, 12, 45, tzinfo=UTC), # 45-min gap + datetime(2026, 5, 12, 13, 0, tzinfo=UTC), + ] + gaps = _detect_gaps(ts) + assert len(gaps) == 1 + assert gaps[0].gap_minutes == 45 + assert gaps[0].prev_timestamp == ts[0] + assert gaps[0].next_timestamp == ts[1] + + +def test_detect_gaps_ignores_threshold_boundary() -> None: + # 20-min gap is exactly the threshold → NOT flagged (strict >) + ts = [ + datetime(2026, 5, 12, 12, 0, tzinfo=UTC), + datetime(2026, 5, 12, 12, 20, tzinfo=UTC), + ] + assert _detect_gaps(ts) == ()