# DISPOSABLE CALENDAR-LIBRARY PROBE — NOT PRODUCTION CODE. # See README.md in this directory. Requires croniter==6.2.4 and tzdata # (plus apscheduler==3.11.0 for the Part B rejected-candidate evidence) # on Python 3.14 (isolated env, not repo env). """Pin the croniter 6.2.4 calendar contract for deployment scheduling. Policy: croniter owns calendar calculation, including DST resolution. The adapter consumes it thinly — convert the query instant into the schedule's named zone, ask for the next/previous occurrence, convert the result to UTC — and applies no correction of its own. Each test prints OBSERVED lines pinning 6.2.4 behavior. After any calendar-dependency upgrade a failure IS the finding: re-probe, do not hand-roll around it. Part A pins the chosen-library contract (shipping acceptance). Part B preserves rejected-candidate evidence (NOT acceptance). Run: uv run --project python -m pytest -q -p no:cacheprovider -o addopts="" """ from __future__ import annotations import time from datetime import datetime, timedelta, timezone import pytest # This probe runs ONLY in the isolated calendar env (see README.md). # Skip — do not error — when collected by a default repo test run. pytest.importorskip("apscheduler", reason="isolated calendar-probe env only") pytest.importorskip("croniter", reason="isolated calendar-probe env only") from apscheduler.triggers.cron import CronTrigger # noqa: E402 (Part B only) from croniter import ( # noqa: E402 CroniterBadCronError, CroniterBadDateError, ) from croniter import ( croniter as Croniter, ) try: # noqa: E402 from zoneinfo import ZoneInfo except ImportError: # pragma: no cover from backports.zoneinfo import ZoneInfo # type: ignore[no-redef] from importlib.metadata import version as _pkg_version # noqa: E402 import apscheduler # noqa: E402 CRONITER_VERSION = _pkg_version("croniter") print(f"croniter=={CRONITER_VERSION}") print(f"apscheduler=={apscheduler.__version__} (rejected candidate, Part B only)") UTC = timezone.utc HCMC = ZoneInfo("Asia/Ho_Chi_Minh") NYC = ZoneInfo("America/New_York") # 2026 DST transitions (US): spring forward 2026-03-08 02:00 -> 03:00, # fall back 2026-11-01 02:00 -> 01:00. def utc(*args) -> datetime: return datetime(*args, tzinfo=UTC) # --------------------------------------------------------------------------- # Part A — chosen-library contract (croniter; shipping acceptance) # --------------------------------------------------------------------------- def test_versions_pinned(): print(f"OBSERVED croniter version={CRONITER_VERSION}") print(f"OBSERVED apscheduler.__version__={apscheduler.__version__}") # Exact pin: a calendar-dependency upgrade must fail here deliberately, # forcing a re-probe before any pin update. assert CRONITER_VERSION == "6.2.4" assert apscheduler.__version__.startswith("3.") def test_utc_daily_next_is_strictly_increasing_and_unique(): it = Croniter("30 9 * * *", utc(2026, 9, 1, 0, 0, 0)) seen: set[datetime] = set() prev = None for _ in range(10): nxt = it.get_next(datetime) assert nxt.tzinfo is not None as_utc = nxt.astimezone(UTC) assert as_utc not in seen, f"duplicate UTC instant {as_utc}" seen.add(as_utc) if prev is not None: assert as_utc > prev, "not strictly increasing" print(f"OBSERVED utc-daily next={as_utc.isoformat()}") prev = as_utc assert len(seen) == 10 def test_hcmc_daily_converts_and_hourly_counts(): # Asia/Ho_Chi_Minh is UTC+7 with no DST. 09:30 local == 02:30 UTC. nxt = Croniter("30 9 * * *", datetime(2026, 9, 1, 0, 0, tzinfo=HCMC)).get_next( datetime ) as_utc = nxt.astimezone(UTC) print(f"OBSERVED hcmc next local={nxt.isoformat()} utc={as_utc.isoformat()}") assert (as_utc.hour, as_utc.minute) == (2, 30) assert as_utc.date().isoformat() == "2026-09-01" # No-DST zone: hourly occurrence count over two 48-hour windows # (March and November, starting off-tick) must be exactly 48 each. for label, start in ( ("march", utc(2026, 3, 7, 17, 30, 0)), ("november", utc(2026, 10, 31, 17, 30, 0)), ): it = Croniter("0 * * * *", start.astimezone(HCMC)) count = 0 while True: hit = it.get_next(datetime).astimezone(UTC) if hit >= start + timedelta(hours=48): break count += 1 assert count < 60 print(f"OBSERVED hcmc hourly count window={label} count={count}") assert count == 48, f"{label}: no-DST zone must yield exactly 48, got {count}" def test_expression_forms_wildcard_step_list_range(): """Wildcards, steps, lists, and ranges resolve through plain get_next on an ordinary day (no DST involved).""" cases = { "* * * * *": (datetime(2026, 9, 8, 0, 0, tzinfo=UTC), "2026-09-08T00:01:00"), "5/15 * * * *": (datetime(2026, 9, 8, 0, 0, tzinfo=UTC), "2026-09-08T00:05:00"), "0,30 1-2 * * *": ( datetime(2026, 9, 8, 0, 0, tzinfo=UTC), "2026-09-08T01:00:00", ), "*/20 1-3 * * *": ( datetime(2026, 9, 8, 1, 50, tzinfo=UTC), "2026-09-08T02:00:00", ), } for expr, (start, want) in cases.items(): nxt = Croniter(expr, start).get_next(datetime) print( f"OBSERVED croniter {expr!r} from {start.isoformat()} -> {nxt.isoformat()}" ) assert nxt.astimezone(UTC).isoformat() == want + "+00:00" def test_weekday_names_and_numbers_unix(): """Pin croniter 6.2.4 weekday dialect (re-probe on upgrade). Unix convention: numeric 0 AND 7 mean Sunday; 1/mon mean Monday.""" monday = datetime(2026, 9, 7, 0, 0, tzinfo=UTC) # a Monday cases = { "0 12 * * 0": 6, # Sunday "0 12 * * 7": 6, # Sunday (alias) "0 12 * * 1": 0, # Monday "0 12 * * mon": 0, "0 12 * * sun": 6, } for expr, want_wd in cases.items(): nxt = Croniter(expr, monday).get_next(datetime) print( f"OBSERVED croniter {expr!r} -> {nxt.date().isoformat()} weekday={nxt.weekday()}" ) assert nxt.weekday() == want_wd, f"{expr}: want weekday={want_wd}" assert Croniter("0 12 * * 0", monday).get_next(datetime) == Croniter( "0 12 * * sun", monday ).get_next(datetime) def test_dom_dow_day_or_true_is_selected(): """Day-of-month/day-of-week uses croniter's standard day_or=True (Unix OR). The alternative (AND) is pinned for reference only.""" start = datetime(2026, 9, 1, 0, 0, tzinfo=UTC) it_or = Croniter("0 12 13 * fri", start, day_or=True) hits_or = [it_or.get_next(datetime).date().isoformat() for _ in range(4)] print(f"OBSERVED croniter day_or=True hits={hits_or}") # Fridays plus the 13th (a Sunday): classic Unix OR. assert hits_or == ["2026-09-04", "2026-09-11", "2026-09-13", "2026-09-18"] it_and = Croniter("0 12 13 * fri", start, day_or=False) first_and = it_and.get_next(datetime).date() print( f"OBSERVED croniter day_or=False first={first_and.isoformat()} (not selected)" ) assert (first_and.day, first_and.weekday()) == (13, 4) def test_zone_conversion_is_the_callers_job(): """croniter iterates in the tz of the supplied datetime and performs no conversion: a UTC start yields UTC results. The adapter converts now into the schedule zone before querying and back to UTC after.""" utc_start = Croniter( "30 2 * * *", datetime(2026, 3, 7, 12, 0, tzinfo=UTC) ).get_next(datetime) print( f"OBSERVED croniter utc-start gap next={utc_start.isoformat()} (no zone conversion)" ) assert utc_start.tzinfo is UTC hcmc_start = Croniter( "30 9 * * *", datetime(2026, 9, 1, 0, 0, tzinfo=HCMC) ).get_next(datetime) print(f"OBSERVED croniter hcmc-start next={hcmc_start.isoformat()}") assert hcmc_start.utcoffset() == timedelta(hours=7) def test_gap_nonexistent_wall_times_resolve_forward(): """Observed 6.2.4 gap behavior: daily 02:30 on 2026-03-08 (a wall time that never existed in America/New_York) resolves to 03:00-04:00 the same day. That resolution IS the occurrence — there is no separate validity concept and no day is skipped over.""" first = Croniter("30 2 * * *", datetime(2026, 3, 7, 12, 0, tzinfo=NYC)).get_next( datetime ) print(f"OBSERVED croniter gap-day daily-0230 resolves={first.isoformat()}") assert first.isoformat() == "2026-03-08T03:00:00-04:00" following = Croniter("30 2 * * *", first).get_next(datetime) print(f"OBSERVED croniter gap following={following.isoformat()}") assert following.isoformat() == "2026-03-09T02:30:00-04:00" def test_gap_per_minute_stream_jumps_forward(): """Per-minute iteration across the spring gap jumps 01:59 EST straight to 03:00 EDT: no 02:xx wall times are emitted, UTC is strictly increasing and unique.""" it = Croniter("* * * * *", datetime(2026, 3, 8, 0, 0, tzinfo=NYC)) seq = [it.get_next(datetime) for _ in range(300)] us = [d.astimezone(UTC) for d in seq] assert all(b > a for a, b in zip(us, us[1:])) assert len(set(us)) == len(us) gap_day = [d for d in seq if d.date().isoformat() == "2026-03-08"] assert all(d.hour != 2 for d in gap_day), "no 02:xx wall times emitted" assert "2026-03-08T01:59:00-05:00" in (d.isoformat() for d in gap_day) assert "2026-03-08T03:00:00-04:00" in (d.isoformat() for d in gap_day) print( "OBSERVED per-minute gap jump 01:59-05:00 -> 03:00-04:00, " f"{len(gap_day)} gap-day hits" ) def test_gap_backward_queries_return_library_resolution(): """Backward queries resolve the same way: get_prev on the gap day returns the forward-resolved 03:00-04:00, not a skipped-over day. Latest-missed on a gap day is that resolved instant.""" cases = { "2026-03-08T12:00": "2026-03-08T03:00:00-04:00", "2026-03-09T00:00": "2026-03-08T03:00:00-04:00", "2026-03-07T12:00": "2026-03-07T02:30:00-05:00", "2026-03-09T12:00": "2026-03-09T02:30:00-04:00", } for start_iso, want in cases.items(): start = datetime.fromisoformat(start_iso).replace(tzinfo=NYC) got = Croniter("30 2 * * *", start).get_prev(datetime) print(f"OBSERVED croniter gap get_prev from {start_iso} -> {got.isoformat()}") assert got.isoformat() == want def test_fold_repeated_times_are_distinct_utc(): """Both 01:30s on 2026-11-01 occur as distinct UTC instants (05:30Z EDT, then 06:30Z EST).""" it = Croniter("30 1 * * *", datetime(2026, 10, 31, 12, 0, tzinfo=NYC)) first = it.get_next(datetime) second = it.get_next(datetime) print( f"OBSERVED croniter fold first={first.isoformat()} second={second.isoformat()}" ) assert first.astimezone(UTC) == utc(2026, 11, 1, 5, 30) assert second.astimezone(UTC) == utc(2026, 11, 1, 6, 30) def test_fold_per_minute_unique_increasing(): """Per-minute iteration across the fall fold emits both fold hours with strictly increasing unique UTC instants.""" it = Croniter("* * * * *", datetime(2026, 10, 31, 20, 0, tzinfo=NYC)) seq = [it.get_next(datetime) for _ in range(560)] us = [d.astimezone(UTC) for d in seq] print(f"OBSERVED croniter fold-span count={len(us)} last={us[-1].isoformat()}") assert all(b > a for a, b in zip(us, us[1:])), "must be strictly increasing" assert len(set(us)) == len(us), "UTC identities must be unique" iso = {d.isoformat() for d in us} assert "2026-11-01T05:30:00+00:00" in iso, "first 01:30 (EDT) must occur" assert "2026-11-01T06:30:00+00:00" in iso, "second 01:30 (EST) must occur" def test_latest_missed_after_years_of_downtime(): """Per-minute schedule, ~3 years of downtime: get_prev answers the latest missed occurrence directly — no enumeration of missed years.""" now = utc(2026, 9, 8, 12, 0, 0) t0 = time.perf_counter() latest = Croniter("* * * * *", now).get_prev(datetime) elapsed = time.perf_counter() - t0 print( f"OBSERVED croniter get_prev({now.isoformat()})={latest.isoformat()} " f"elapsed={elapsed:.4f}s" ) assert latest.tzinfo is not None, "get_prev must preserve tz-awareness" assert elapsed < 5, "latest-missed lookup must be bounded" assert latest <= now assert (now - latest) < timedelta(minutes=2) now_nyc = datetime(2026, 9, 8, 12, 0, tzinfo=NYC) latest_nyc = Croniter("* * * * *", now_nyc).get_prev(datetime) print( f"OBSERVED croniter nyc get_prev now={now_nyc.isoformat()} " f"latest={latest_nyc.isoformat()}" ) assert latest_nyc.tzinfo is not None assert latest_nyc <= now_nyc assert (now_nyc - latest_nyc) < timedelta(minutes=2) assert latest_nyc.astimezone(UTC).isoformat() == "2026-09-08T15:59:00+00:00" def test_iteration_is_exclusive_both_directions(): """Both get_next and get_prev are exclusive of their start: a query from exactly a due instant returns the neighboring occurrence, not the instant itself. Adapter consequence (T01, not enforced here): query forward from the last-consumed instant and compare catch-up results against the same watermark, so a due occurrence is admitted exactly once — neither missed by exclusivity nor admitted twice.""" due = utc(2026, 9, 8, 13, 0, 0) fwd = Croniter("0 13 * * *", due).get_next(datetime) back = Croniter("0 13 * * *", due).get_prev(datetime) print(f"OBSERVED exclusive get_next(due)={fwd.isoformat()}") print(f"OBSERVED exclusive get_prev(due)={back.isoformat()}") assert fwd.astimezone(UTC) == utc(2026, 9, 9, 13, 0, 0) assert (back.year, back.month, back.day, back.hour, back.minute) == ( 2026, 9, 7, 13, 0, ) just_before = Croniter("0 13 * * *", due - timedelta(seconds=1)).get_next(datetime) assert just_before.astimezone(UTC) == due, "a tick just before due admits it" def test_impossible_schedule_raises_promptly(): """February 30th never occurs: the library raises its documented exhaustion error promptly instead of searching forever. The adapter maps this to exhausted — exhaustion must never look like progress.""" start = time.perf_counter() with pytest.raises(CroniterBadDateError): Croniter("0 12 30 2 *", utc(2026, 1, 1)).get_next(datetime) elapsed = time.perf_counter() - start print( f"OBSERVED impossible-schedule raised CroniterBadDateError elapsed={elapsed:.3f}s" ) assert elapsed < 5, f"search must be bounded, took {elapsed:.1f}s" def test_bad_expressions_rejected_naive_passes_through(): """Malformed expressions fail at construction with a documented error. Naive datetimes are NOT rejected by the library — they pass straight through — so the adapter rejects naive itself per spec.""" with pytest.raises(CroniterBadCronError): Croniter("nonsense", utc(2026, 1, 1)) print("OBSERVED malformed expression rejected with CroniterBadCronError") naive = Croniter("* * * * *", datetime(2026, 9, 8, 12, 0)).get_next(datetime) print(f"OBSERVED naive start passes through tzinfo={naive.tzinfo}") assert naive.tzinfo is None # --------------------------------------------------------------------------- # Part B — rejected-candidate evidence (APScheduler; NOT acceptance) # --------------------------------------------------------------------------- @pytest.mark.xfail( strict=True, reason="REJECTED CANDIDATE: APScheduler 3.11.0 fabricates a phantom " "02:30-05:00 for the DST gap. Kept as evidence for the rejection; " "the chosen library's gap behavior is pinned in Part A. Strict so " "re-opening the candidacy fails loudly. See implementation plan.", ) def test_rejected_candidate_gap_phantom(): # 02:30 does not exist in America/New_York on 2026-03-08. The # disqualifier is the phantom itself: whatever the library returns # must not be a nonexistent wall time. trig = CronTrigger(hour=2, minute=30, second=0, timezone="America/New_York") nxt = trig.get_next_fire_time(None, utc(2026, 3, 7, 12, 0, 0)) assert nxt is not None local = nxt.astimezone(NYC) print(f"OBSERVED apscheduler dst-gap next local={local.isoformat()}") assert not ( (local.year, local.month, local.day) == (2026, 3, 8) and local.hour == 2 ), f"library must not fabricate a nonexistent wall time, got {local.isoformat()}" @pytest.mark.xfail( strict=True, reason="REJECTED CANDIDATE: APScheduler 3.11.0 replays 05:01Z-06:00Z " "(~59 past minutes) after the fall fold, duplicating UTC occurrence " "identities. Kept as evidence for the rejection; the chosen " "library's fold behavior is pinned in Part A. See implementation plan.", ) def test_rejected_candidate_fold_replay(): trig = CronTrigger(minute="*", second=0, timezone="America/New_York") now = utc(2026, 11, 1, 0, 0, 0) prev = None seen: set[str] = set() last: datetime | None = None for _ in range(450): # spans past 07:00Z: covers BOTH fold hours nxt = trig.get_next_fire_time(prev, now) assert nxt is not None as_utc = nxt.astimezone(UTC) ident = f"sched-1|{as_utc.isoformat()}" assert ident not in seen, f"duplicate occurrence identity {ident}" seen.add(ident) if last is not None: assert as_utc > last, "occurrences must be strictly increasing" last = as_utc prev, now = nxt, nxt print(f"OBSERVED fold-span count={len(seen)} unique, last={last.isoformat()}")