croniter next run times as pendulum timezone-aware datetimes: working versions (tested 2026-10-16)
Compute the next cron fire times as tz-aware pendulum DateTimes, handling DST gaps and folds, with focused offline tests.
passed6 passed · 0 failed2026-10-16 00:56 UTC- Status
- passed
- Last passed
- 2026-10-16 00:56 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions3
croniter@6.2.4pendulum@3.3.0pytest@9.1.1
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 38832319459
The code
Exactly the files that passed. Your agent can pull them with get_recipe("croniter-pendulum-next-run") (install), or see them on GitHub.
requirements.txt
croniter==6.2.4 pendulum==3.3.0 pytest==9.1.1
scheduler.py
from __future__ import annotations
import datetime
import pendulum
from croniter import croniter
def next_run(cron: str, after: pendulum.DateTime) -> pendulum.DateTime:
"""Next cron match strictly after `after`, as a tz-aware pendulum DateTime.
croniter keeps `after.tzinfo` (so DST is handled) but always hands back a
plain `datetime.datetime`; asking for `ret_type=pendulum.DateTime` does not
change that, so we re-wrap it with `pendulum.instance`.
"""
nxt = croniter(cron, after).get_next(datetime.datetime)
return pendulum.instance(nxt)
def upcoming(
cron: str, after: pendulum.DateTime, count: int
) -> list[pendulum.DateTime]:
"""The next `count` cron matches after `after`, in the same timezone."""
it = croniter(cron, after)
return [pendulum.instance(it.get_next(datetime.datetime)) for _ in range(count)]
tests/test_scheduler.py
import datetime
import pendulum
from croniter import croniter
from scheduler import next_run, upcoming
NY = "America/New_York"
def test_next_run_returns_tz_aware_pendulum_datetime():
start = pendulum.datetime(2024, 1, 1, 8, 0, tz=NY)
# croniter ignores non-datetime ret_type classes and returns a plain datetime
raw = croniter("0 9 * * *", start).get_next(pendulum.DateTime)
assert type(raw) is datetime.datetime and not isinstance(raw, pendulum.DateTime)
result = next_run("0 9 * * *", start)
assert isinstance(result, pendulum.DateTime)
assert result.timezone_name == NY
assert result == pendulum.datetime(2024, 1, 1, 9, 0, tz=NY)
def test_start_time_is_exclusive():
start = pendulum.datetime(2024, 1, 1, 9, 0, tz=NY)
assert next_run("0 9 * * *", start) == pendulum.datetime(2024, 1, 2, 9, 0, tz=NY)
def test_spring_forward_skips_missing_local_hour():
start = pendulum.datetime(2024, 3, 9, 12, 0, tz=NY)
result = next_run("0 2 * * *", start)
assert result == pendulum.datetime(2024, 3, 10, 3, 0, tz=NY)
assert result.tzname() == "EDT"
def test_fall_back_yields_both_folds():
start = pendulum.datetime(2024, 11, 2, 12, 0, tz=NY)
runs = upcoming("30 1 * * *", start, 2)
assert [r.tzname() for r in runs] == ["EDT", "EST"]
assert [r.fold for r in runs] == [0, 1]
assert runs[0].timestamp() != runs[1].timestamp()
def test_weekday_cron_skips_the_weekend():
friday = pendulum.datetime(2024, 1, 5, 10, 0, tz=NY)
assert next_run("0 9 * * 1-5", friday) == pendulum.datetime(2024, 1, 8, 9, 0, tz=NY)
def test_cron_is_evaluated_in_the_base_timezone():
start = pendulum.datetime(2024, 1, 1, 0, 0, tz="UTC")
tokyo = next_run("0 9 * * *", start).in_timezone("Asia/Tokyo")
assert tokyo.timezone_name == "Asia/Tokyo"
assert (tokyo.hour, tokyo.minute) == (18, 0)
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.