APScheduler job triggers tested deterministically with time-machine: working versions (tested 2026-10-15)
Compute CronTrigger and IntervalTrigger fire times without a running scheduler, and test them offline by freezing the clock with time-machine.
passed5 passed · 0 failed2026-10-15 13:27 UTC- Status
- passed
- Last passed
- 2026-10-15 13:27 UTC
- Tests
- 5 / 5
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions4
apscheduler@3.11.3time-machine@3.5.1pytest@9.1.1tzlocal@5.4.4
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38742087302
The code
Exactly the files that passed. Your agent can pull them with get_recipe("apscheduler-time-machine-jobs") (install), or see them on GitHub.
requirements.txt
apscheduler==3.11.3 time-machine==3.5.1 pytest==9.1.1 tzlocal==5.4.4
jobs.py
from datetime import datetime, UTC
from zoneinfo import ZoneInfo
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger
TZ = ZoneInfo("America/New_York")
def report_trigger() -> CronTrigger:
return CronTrigger(hour=9, minute=0, timezone=TZ)
def heartbeat_trigger(start: datetime | None = None) -> IntervalTrigger:
return IntervalTrigger(minutes=15, start_date=start, timezone=UTC)
def next_report(now: datetime | None = None) -> datetime:
if now is None:
now = datetime.now(TZ)
return report_trigger().get_next_fire_time(None, now)
def upcoming(trigger, now: datetime, count: int) -> list[datetime]:
fire = trigger.get_next_fire_time(None, now)
out = []
while fire is not None and len(out) < count:
out.append(fire)
fire = trigger.get_next_fire_time(fire, fire)
return out
tests/test_jobs.py
import time
from datetime import datetime, UTC
from zoneinfo import ZoneInfo
import time_machine
from jobs import heartbeat_trigger, next_report, report_trigger, upcoming
NY = ZoneInfo("America/New_York")
def test_report_keeps_local_wall_clock_across_dst():
# US DST starts 2024-03-10: 09:00 stays 09:00 local, but UTC shifts from 14:00 to 13:00.
with time_machine.travel(datetime(2024, 3, 9, 12, tzinfo=UTC), tick=False):
fires = upcoming(report_trigger(), datetime.now(UTC), 2)
assert [f.astimezone(NY).hour for f in fires] == [9, 9]
assert [f.astimezone(UTC).hour for f in fires] == [14, 13]
def test_next_report_reads_frozen_clock():
with time_machine.travel(datetime(2024, 7, 1, 13, 30, tzinfo=UTC), tick=False):
assert next_report() == datetime(2024, 7, 2, 9, 0, tzinfo=NY)
def test_interval_stays_on_start_grid():
start = datetime(2024, 1, 1, 8, tzinfo=UTC)
fires = upcoming(heartbeat_trigger(start), datetime(2024, 1, 1, 8, 37, tzinfo=UTC), 2)
assert fires == [datetime(2024, 1, 1, 8, 45, tzinfo=UTC), datetime(2024, 1, 1, 9, 0, tzinfo=UTC)]
def test_default_start_is_captured_at_construction():
# The default start_date is now + interval, so build the trigger inside travel.
with time_machine.travel(datetime(2024, 1, 1, 8, tzinfo=UTC), tick=False):
trig = heartbeat_trigger()
first = trig.get_next_fire_time(None, datetime(2024, 1, 1, 9, 5, tzinfo=UTC))
assert first == datetime(2024, 1, 1, 9, 15, tzinfo=UTC)
def test_tick_false_freezes_clock():
t = datetime(2024, 1, 1, tzinfo=UTC)
with time_machine.travel(t, tick=False):
before = time.time()
time.sleep(0.01)
assert time.time() == before == t.timestamp()
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.