croniter get_next and get_prev for cron schedule iteration: working versions (tested 2026-10-16)
Walk cron expressions forward and backward with croniter, returning datetimes or epoch seconds, and handle the day_or OR versus AND gotcha.
passed6 passed · 0 failed2026-10-16 09:57 UTC- Status
- passed
- Last passed
- 2026-10-16 09:57 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
croniter@6.2.4pytest@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 38883899161
The code
Exactly the files that passed. Your agent can pull them with get_recipe("croniter-next-prev") (install), or see them on GitHub.
requirements.txt
croniter==6.2.4 pytest==9.1.1
schedule.py
from datetime import datetime
from itertools import islice
from croniter import croniter
def next_runs(expr: str, start: datetime, count: int = 1, day_or: bool = True) -> list[datetime]:
it = croniter(expr, start, day_or=day_or)
return [it.get_next(datetime) for _ in range(count)]
def prev_runs(expr: str, start: datetime, count: int = 1) -> list[datetime]:
it = croniter(expr, start)
return [it.get_prev(datetime) for _ in range(count)]
def upcoming(expr: str, start: datetime, count: int) -> list[datetime]:
# all_next() is an infinite generator: always bound it with islice.
it = croniter(expr, start)
return list(islice(it.all_next(datetime), count))
def is_valid(expr: str) -> bool:
return croniter.is_valid(expr)
tests/test_schedule.py
from datetime import datetime, timezone
import pytest
from croniter import CroniterBadCronError, croniter
from schedule import is_valid, next_runs, prev_runs, upcoming
def test_next_is_strictly_after_matching_start():
start = datetime(2024, 1, 1, 0, 0)
# start already matches the cron, so the next hit is the following day
assert next_runs("0 0 * * *", start)[0] == datetime(2024, 1, 2)
assert upcoming("0 0 * * *", start, 3) == [
datetime(2024, 1, 2),
datetime(2024, 1, 3),
datetime(2024, 1, 4),
]
def test_prev_walks_backwards():
start = datetime(2024, 1, 1, 0, 0)
assert prev_runs("0 0 * * *", start, 2) == [
datetime(2023, 12, 31),
datetime(2023, 12, 30),
]
def test_default_ret_type_is_epoch_seconds():
start = datetime(2024, 1, 1, tzinfo=timezone.utc)
ts = croniter("0 0 * * *", start).get_next()
assert isinstance(ts, float)
assert datetime.fromtimestamp(ts, tz=timezone.utc) == datetime(2024, 1, 2, tzinfo=timezone.utc)
def test_day_of_week_seven_equals_sunday():
start = datetime(2024, 1, 2)
assert next_runs("0 0 * * 0", start)[0] == next_runs("0 0 * * 7", start)[0] == datetime(2024, 1, 7)
def test_day_or_toggles_or_and_semantics():
start = datetime(2024, 1, 2)
# default day_or=True: day-of-month OR day-of-week -> next Monday
assert next_runs("0 0 1 * 1", start)[0] == datetime(2024, 1, 8)
# day_or=False: day-of-month AND day-of-week -> first 1st that is a Monday
assert next_runs("0 0 1 * 1", start, day_or=False)[0] == datetime(2024, 4, 1)
def test_invalid_expression():
assert is_valid("0 0 * * *") is True
assert is_valid("61 0 * * *") is False
with pytest.raises(CroniterBadCronError):
croniter("61 0 * * *", datetime(2024, 1, 1))
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.