ProvenPin Install

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

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.