Claude Code for croniter: Cron Expression Parsing in Python — Claude Skills 360 Blog
Blog / AI / Claude Code for croniter: Cron Expression Parsing in Python
AI

Claude Code for croniter: Cron Expression Parsing in Python

Published: April 17, 2028
Read time: 5 min read
By: Claude Skills 360

croniter parses cron expressions and iterates over matching datetimes. pip install croniter. Basic: from croniter import croniter; cron = croniter("0 9 * * 1-5", start_time); cron.get_next(datetime). Fields: second(optional) minute hour day_of_month month day_of_week. Next: cron.get_next() → timestamp; cron.get_next(datetime) → datetime. Prev: cron.get_prev(datetime). Match: croniter.match("0 9 * * *", dt) → bool. Valid: croniter.is_valid("*/5 * * * *"). Timezone: croniter("0 9 * * 1-5", tz=pytz.timezone("US/Eastern")). Hash: croniter("H H * * *", hash_id="myapp") — deterministic random offset. Multiple: croniter("0 8,12,18 * * *", ...). Ranges: */5 every 5; 1-5 range; 1,3,5 list. L: "0 0 L * *" — last day. W: "0 0 15W * *" — nearest weekday. Second-level: croniter("*/30 * * * * *", hash_id="", second_at_beginning=True). Cursor: cron.set_current(new_start). cron.get_current(). Iterator: for dt in cron: .... Next_n: list(itertools.islice(cron.all_next(datetime), 5)). Expand: croniter.expand("0 9 * * 1-5") → canonical. Claude Code generates croniter schedule helpers, next-run calculators, and cron-based task dispatchers.

CLAUDE.md for croniter

## croniter Stack
- Version: croniter >= 1.4 | pip install croniter
- Next: croniter("expr", start).get_next(datetime) → next datetime
- Prev: .get_prev(datetime) → previous datetime
- Match: croniter.match("expr", dt) → bool
- Valid: croniter.is_valid("expr") → bool
- TZ: croniter("expr", tz=pytz.timezone("America/New_York"))
- Hash: croniter("H H * * *", hash_id="service") — deterministic jitter

croniter Schedule Pipeline

# app/schedule.py — croniter parsing, next/prev run, match, timezone, task dispatcher
from __future__ import annotations

import datetime
import itertools
from typing import Any, Callable, Iterator

from croniter import croniter


# ─────────────────────────────────────────────────────────────────────────────
# 1. Core helpers
# ─────────────────────────────────────────────────────────────────────────────

UTC = datetime.timezone.utc


def next_run(expr: str, after: datetime.datetime | None = None) -> datetime.datetime:
    """
    Return the next run time for a cron expression.

    Example:
        next_run("0 9 * * 1-5")  → next weekday at 09:00 UTC
    """
    start = after or datetime.datetime.now(tz=UTC)
    return croniter(expr, start).get_next(datetime.datetime)


def prev_run(expr: str, before: datetime.datetime | None = None) -> datetime.datetime:
    """Return the most recent (previous) run time for a cron expression."""
    start = before or datetime.datetime.now(tz=UTC)
    return croniter(expr, start).get_prev(datetime.datetime)


def matches(expr: str, dt: datetime.datetime) -> bool:
    """Return True if dt matches the cron expression."""
    return croniter.match(expr, dt)


def is_valid(expr: str) -> bool:
    """Return True if the cron expression is syntactically valid."""
    return croniter.is_valid(expr)


def next_n(expr: str, n: int, after: datetime.datetime | None = None) -> list[datetime.datetime]:
    """Return the next n run times after the given start time."""
    start = after or datetime.datetime.now(tz=UTC)
    cron = croniter(expr, start)
    return list(itertools.islice(cron.all_next(datetime.datetime), n))


def schedule_iter(
    expr: str,
    start: datetime.datetime | None = None,
    direction: str = "next",
) -> Iterator[datetime.datetime]:
    """
    Yield datetimes matching a cron expression indefinitely.
    direction: "next" (forward) or "prev" (backward).
    """
    current = start or datetime.datetime.now(tz=UTC)
    cron = croniter(expr, current)
    getter = cron.all_next if direction == "next" else cron.all_prev
    yield from getter(datetime.datetime)


# ─────────────────────────────────────────────────────────────────────────────
# 2. Schedule inspection
# ─────────────────────────────────────────────────────────────────────────────

def time_until_next(expr: str, now: datetime.datetime | None = None) -> datetime.timedelta:
    """Return timedelta until the next run."""
    t = now or datetime.datetime.now(tz=UTC)
    return next_run(expr, after=t) - t


def describe_schedule(expr: str, n: int = 5) -> dict[str, Any]:
    """
    Return a human-readable summary of a cron schedule.
    """
    if not is_valid(expr):
        return {"valid": False, "expr": expr}

    now = datetime.datetime.now(tz=UTC)
    upcoming = next_n(expr, n, after=now)
    avg_interval = None
    if len(upcoming) >= 2:
        gaps = [(upcoming[i+1] - upcoming[i]).total_seconds()
                for i in range(len(upcoming)-1)]
        avg_interval = sum(gaps) / len(gaps)

    return {
        "valid": True,
        "expr": expr,
        "next": upcoming[0] if upcoming else None,
        "upcoming": upcoming,
        "avg_interval_seconds": avg_interval,
        "time_until_next": time_until_next(expr, now),
    }


def runs_within(
    expr: str,
    window_start: datetime.datetime,
    window_end: datetime.datetime,
) -> list[datetime.datetime]:
    """
    Return all datetimes where the schedule fires within [window_start, window_end].
    """
    results = []
    cron = croniter(expr, window_start)
    while True:
        dt = cron.get_next(datetime.datetime)
        if dt > window_end:
            break
        results.append(dt)
    return results


def daily_run_count(expr: str, date: datetime.date) -> int:
    """Count how many times a cron fires on a given date."""
    start = datetime.datetime.combine(date, datetime.time.min, tzinfo=UTC)
    end   = datetime.datetime.combine(date, datetime.time.max, tzinfo=UTC)
    return len(runs_within(expr, start, end))


# ─────────────────────────────────────────────────────────────────────────────
# 3. Timezone-aware scheduling
# ─────────────────────────────────────────────────────────────────────────────

def next_run_tz(
    expr: str,
    timezone_name: str,
    after: datetime.datetime | None = None,
) -> datetime.datetime:
    """
    Return the next run time in a specific timezone.
    Requires: pip install pytz

    Example:
        next_run_tz("0 9 * * 1-5", "America/New_York")
    """
    import pytz
    tz = pytz.timezone(timezone_name)
    start = after or datetime.datetime.now(tz=tz)
    if start.tzinfo is None:
        start = tz.localize(start)
    return croniter(expr, start, hash_id=None).get_next(datetime.datetime)


# ─────────────────────────────────────────────────────────────────────────────
# 4. Task registry / dispatcher
# ─────────────────────────────────────────────────────────────────────────────

class ScheduledTask:
    """A task with a cron expression and a callable."""

    def __init__(self, name: str, expr: str, fn: Callable, enabled: bool = True):
        if not is_valid(expr):
            raise ValueError(f"Invalid cron expression: {expr!r}")
        self.name    = name
        self.expr    = expr
        self.fn      = fn
        self.enabled = enabled

    def is_due(self, now: datetime.datetime | None = None) -> bool:
        """Return True if this task should run at the given time (truncated to minute)."""
        t = now or datetime.datetime.now(tz=UTC)
        t_trunc = t.replace(second=0, microsecond=0)
        return self.enabled and matches(self.expr, t_trunc)

    def next_run(self, after: datetime.datetime | None = None) -> datetime.datetime:
        return next_run(self.expr, after=after)

    def run(self, **kwargs) -> Any:
        return self.fn(**kwargs)


class TaskRegistry:
    """
    Registry of ScheduledTask objects.
    Call dispatch() once per minute to run due tasks.

    Usage:
        registry = TaskRegistry()

        @registry.task("*/5 * * * *")
        def poll_api():
            ...

        while True:
            registry.dispatch()
            time.sleep(60)
    """

    def __init__(self):
        self._tasks: dict[str, ScheduledTask] = {}

    def register(
        self,
        name: str,
        expr: str,
        fn: Callable,
        enabled: bool = True,
    ) -> ScheduledTask:
        task = ScheduledTask(name, expr, fn, enabled=enabled)
        self._tasks[name] = task
        return task

    def task(self, expr: str, name: str | None = None, enabled: bool = True):
        """Decorator factory for registering tasks."""
        def decorator(fn: Callable) -> Callable:
            task_name = name or fn.__name__
            self.register(task_name, expr, fn, enabled=enabled)
            return fn
        return decorator

    def dispatch(self, now: datetime.datetime | None = None) -> list[str]:
        """Run all due tasks. Returns list of task names that ran."""
        t = now or datetime.datetime.now(tz=UTC)
        ran = []
        for task in self._tasks.values():
            if task.is_due(t):
                try:
                    task.run()
                    ran.append(task.name)
                except Exception as exc:
                    print(f"[TaskRegistry] {task.name} failed: {exc}")
        return ran

    def next_runs(self, n: int = 3) -> dict[str, list[datetime.datetime]]:
        """Return upcoming run times for every registered task."""
        return {name: next_n(task.expr, n) for name, task in self._tasks.items()}

    def due_tasks(self, now: datetime.datetime | None = None) -> list[str]:
        """Return names of tasks due now."""
        t = now or datetime.datetime.now(tz=UTC)
        return [name for name, task in self._tasks.items() if task.is_due(t)]


# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    now = datetime.datetime(2028, 4, 17, 8, 0, 0, tzinfo=UTC)

    print("=== next_run ===")
    # Every weekday at 09:00
    n = next_run("0 9 * * 1-5", after=now)
    print(f"  Next weekday 09:00: {n}")

    # Every 15 minutes
    n2 = next_run("*/15 * * * *", after=now)
    print(f"  Next */15:          {n2}")

    print("\n=== next_n ===")
    upcoming = next_n("0 9 * * 1-5", 5, after=now)
    for dt in upcoming:
        print(f"  {dt.strftime('%a %Y-%m-%d %H:%M')}")

    print("\n=== matches ===")
    mon_9am = datetime.datetime(2028, 4, 17, 9, 0, tzinfo=UTC)
    print(f"  Mon 09:00 matches '0 9 * * 1-5': {matches('0 9 * * 1-5', mon_9am)}")
    print(f"  Mon 10:00 matches '0 9 * * 1-5': {matches('0 9 * * 1-5', mon_9am.replace(hour=10))}")

    print("\n=== time_until_next ===")
    delta = time_until_next("0 9 * * *", now=now)
    print(f"  Until next daily 09:00: {delta}")

    print("\n=== runs_within (today) ===")
    today_start = datetime.datetime(2028, 4, 17, 0, 0, tzinfo=UTC)
    today_end   = datetime.datetime(2028, 4, 17, 23, 59, tzinfo=UTC)
    runs = runs_within("0 */4 * * *", today_start, today_end)
    print(f"  Every 4h runs today ({len(runs)}): {[r.strftime('%H:%M') for r in runs]}")

    print("\n=== describe_schedule ===")
    info = describe_schedule("30 8 * * 1-5", n=3)
    print(f"  expr={info['expr']}")
    print(f"  next={info['next']}")
    print(f"  avg_interval={info['avg_interval_seconds']} s")

    print("\n=== TaskRegistry ===")
    registry = TaskRegistry()
    log: list[str] = []

    @registry.task("*/5 * * * *", name="poll_api")
    def poll_api():
        log.append("poll_api")

    @registry.task("0 * * * *", name="hourly_report")
    def hourly_report():
        log.append("hourly_report")

    @registry.task("0 0 * * *", name="daily_cleanup")
    def daily_cleanup():
        log.append("daily_cleanup")

    # Simulate 08:00 — nothing due (hours not */5 boundary)
    t1 = datetime.datetime(2028, 4, 17, 8, 0, tzinfo=UTC)
    ran = registry.dispatch(now=t1)
    print(f"  08:00 ran: {ran}")

    # Simulate 08:05 — poll due
    t2 = datetime.datetime(2028, 4, 17, 8, 5, tzinfo=UTC)
    ran2 = registry.dispatch(now=t2)
    print(f"  08:05 ran: {ran2}")

    # Simulate 09:00 — poll + hourly
    t3 = datetime.datetime(2028, 4, 17, 9, 0, tzinfo=UTC)
    ran3 = registry.dispatch(now=t3)
    print(f"  09:00 ran: {ran3}")

    print("\n=== Next runs per task ===")
    for task_name, times in registry.next_runs(3).items():
        print(f"  {task_name}: {[t.strftime('%Y-%m-%d %H:%M') for t in times]}")

For the APScheduler alternative — APScheduler is a full scheduler with background threads, async support, persistent job stores, and a cron trigger; croniter is the underlying cron iterator that APScheduler itself uses — use croniter when you only need to compute next/prev run times, check if a time matches a schedule, or iterate over future run times without a running scheduler process. For the schedule library alternative — the schedule library provides a simple human-readable API (schedule.every().monday.at("09:00").do(fn)) but has limited cron expression support; croniter supports the full 5-field (or 6-field with seconds) cron syntax including L, W, and # modifiers for last-day, nearest-weekday, and nth-weekday expressions. The Claude Skills 360 bundle includes croniter skill sets covering next_run()/prev_run()/matches()/is_valid(), next_n() and schedule_iter() generators, time_until_next()/describe_schedule(), runs_within()/daily_run_count() window queries, next_run_tz() timezone-aware scheduling, ScheduledTask is_due()/next_run()/run(), and TaskRegistry with @task decorator, dispatch(), and next_runs(). Start with the free tier to try cron schedule code generation.

Keep Reading

AI

Claude Code for email.contentmanager: Python Email Content Accessors

Read and write EmailMessage body content with Python's email.contentmanager module and Claude Code — email contentmanager ContentManager for the class that maps content types to get and set handler functions allowing EmailMessage to support get_content and set_content with type-specific behaviour, email contentmanager raw_data_manager for the ContentManager instance that handles raw bytes and str payloads without any conversion, email contentmanager content_manager for the standard ContentManager instance used by email.policy.default that intelligently handles text plain text html multipart and binary content types, email contentmanager get_content_text for the handler that returns the decoded text payload of a text-star message part as a str, email contentmanager get_content_binary for the handler that returns the raw decoded bytes payload of a non-text message part, email contentmanager get_data_manager for the get-handler lookup used by EmailMessage get_content to find the right reader function for the content type, email contentmanager set_content text for the handler that creates and sets a text part correctly choosing charset and transfer encoding, email contentmanager set_content bytes for the handler that creates and sets a binary part with base64 encoding and optional filename Content-Disposition, email contentmanager EmailMessage get_content for the method that reads the message body using the registered content manager handlers, email contentmanager EmailMessage set_content for the method that sets the message body and MIME headers in one call, email contentmanager EmailMessage make_alternative make_mixed make_related for the methods that convert a simple message into a multipart container, email contentmanager EmailMessage add_attachment for the method that attaches a file or bytes to a multipart message, and email contentmanager integration with email.message and email.policy and email.mime and io for building high-level email readers attachment extractors text body accessors HTML readers and policy-aware MIME construction pipelines.

5 min read Feb 12, 2029
AI

Claude Code for email.charset: Python Email Charset Encoding

Control header and body encoding for international email with Python's email.charset module and Claude Code — email charset Charset for the class that wraps a character set name with the encoding rules for header encoding and body encoding describing how to encode text for that charset in email messages, email charset Charset header_encoding for the attribute specifying whether headers using this charset should use QP quoted-printable encoding BASE64 encoding or no encoding, email charset Charset body_encoding for the attribute specifying the Content-Transfer-Encoding to use for message bodies in this charset such as QP or BASE64, email charset Charset output_codec for the attribute giving the Python codec name used to encode the string to bytes for the wire format, email charset Charset input_codec for the attribute giving the Python codec name used to decode incoming bytes to str, email charset Charset get_output_charset for returning the output charset name, email charset Charset header_encode for encoding a header string using the charset's header_encoding method, email charset Charset body_encode for encoding body content using the charset's body_encoding, email charset Charset convert for converting a string from the input_codec to the output_codec, email charset add_charset for registering a new charset with custom encoding rules in the global charset registry, email charset add_alias for adding an alias name that maps to an existing registered charset, email charset add_codec for registering a codec name mapping for use by the charset machinery, and email charset integration with email.message and email.mime and email.policy and email.encoders for building international email senders non-ASCII header encoders Content-Transfer-Encoding selectors charset-aware message constructors and MIME encoding pipelines.

5 min read Feb 11, 2029
AI

Claude Code for email.utils: Python Email Address and Header Utilities

Parse and format RFC 2822 email addresses and dates with Python's email.utils module and Claude Code — email utils parseaddr for splitting a display-name plus angle-bracket address string into a realname and email address tuple, email utils formataddr for combining a realname and address string into a properly quoted RFC 2822 address with angle brackets, email utils getaddresses for parsing a list of raw address header strings each potentially containing multiple comma-separated addresses into a list of realname address tuples, email utils parsedate for parsing an RFC 2822 date string into a nine-tuple compatible with time.mktime, email utils parsedate_tz for parsing an RFC 2822 date string into a ten-tuple that includes the UTC offset timezone in seconds, email utils parsedate_to_datetime for parsing an RFC 2822 date string into an aware datetime object with timezone, email utils formatdate for formatting a POSIX timestamp or the current time as an RFC 2822 date string with optional usegmt and localtime flags, email utils format_datetime for formatting a datetime object as an RFC 2822 date string, email utils make_msgid for generating a globally unique Message-ID string with optional idstring and domain components, email utils decode_rfc2231 for decoding an RFC 2231 encoded parameter value into a tuple of charset language and value, email utils encode_rfc2231 for encoding a string as an RFC 2231 encoded parameter value, email utils collapse_rfc2231_value for collapsing a decoded RFC 2231 tuple to a Unicode string, and email utils integration with email.message and email.headerregistry and datetime and time for building address parsers date formatters message-id generators header extractors and RFC-compliant email construction utilities.

5 min read Feb 10, 2029

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free