croniter

repository·main·Indexed 20 days ago

https://github.com/pallets-eco/croniter

A Python library that provides iteration for datetime objects using cron-like expressions. It allows developers to calculate the next or previous occurrence of scheduled events, validate cron expressions with is_valid(), and generate sequences of dates using croniter_range(). The library supports Vixie cron-style @ keywords, Jenkins-style hashed expressions (H), random expressions (R), and timezone-aware datetimes for DST handling.

Tokens
6.7K
Snippets
30
Records
37
Agent score
68%

What's inside croniter

  1. Understand croniter's range and Sunday deviations

    main

    Croniter implements several deviations from the standard cron specification:

    1. Lax Ranges: Ranges can be defined in reverse (e.g., Apr-Jan means April through January). If a step / is provided, it is respected.
    2. Equal Bounds: A range where both bounds are equal (e.g., JAN-JAN or SUN-SUN) represents the whole cycle of that field, not just the single value.
    3. Sunday Support: SUNDAY can be expressed as 7 (e.g., 0 0 * * 7 or 6-7).
  2. Shift cron cycles using expand_from_start_time

    main

    By default, croniter calculates cycles based on calendar days. By setting expand_from_start_time=True and providing a start_time, you can force the phase of a cycle (for fields with steps like */15) to be taken from the start_time instead of the field's minimum value.

    Note: The phase is read from start_time in its own timezone. Every field re-bases, including optional seconds and years. Day-of-month, month, and year take their phase from the field minimum rather than the value itself.

    >>> # Default behavior: cycle starts from field minimum (0)
    >>> croniter('*/15 * * * *', datetime(2024, 7, 11, 10, 7)).expanded[0]
    [0, 15, 30, 45]
    
    >>> # With expand_from_start_time: cycle phase taken from start_time (7)
    >>> croniter('*/15 * * * *', datetime(2024, 7, 11, 10, 7), expand_from_start_time=True).expanded[0]
    [7, 22, 37, 52]
  3. Initialize croniter with day_or parameter

    main

    The croniter constructor accepts a day_or boolean parameter to control how the day and day_of_week fields are combined:

    • day_or=True (Default): Uses OR logic. The trigger fires if either the day-of-month or the day-of-week matches. This is standard POSIX cron behavior.
    • day_or=False: Uses AND logic. The trigger fires only if both the day-of-month and the day-of-week match. This is useful for patterns like "the first Tuesday of the month" (e.g., 1-7 * 2).
    # OR (default): fires on every Wednesday OR on the 1st of the month
    iter = croniter('2 4 1 * wed', datetime(2010, 1, 25))
    
    # AND: fires only on the 1st of the month IF it is a Wednesday
    iter = croniter('2 4 1 * wed', datetime(2010, 1, 25), day_or=False)
  4. Handle Daylight Saving Time (DST) with timezone-aware datetimes

    main

    To ensure croniter handles DST transitions correctly, always initialize your croniter instance with a timezone-aware datetime object. You can use zoneinfo, pytz, dateutil.tz, or Python's built-in timezone module.

    import zoneinfo
    from datetime import datetime
    
    tz = zoneinfo.ZoneInfo("Europe/Berlin")
    local_date = datetime(2017, 3, 26, tzinfo=tz)
    val = croniter('0 0 * * *', local_date).get_next(datetime)
  5. Use croniter for datetime iteration

    main

    The croniter class provides iteration for datetime objects using a cron-like format. You can initialize it with a cron expression and a starting datetime object, then use get_next() to find the next occurrence or get_prev() to find the previous one.

    from croniter import croniter
    from datetime import datetime
    
    base = datetime(2010, 1, 25, 4, 46)
    # Every 5 minutes
    iter = croniter('*/5 * * * *', base)
    print(iter.get_next(datetime))   # 2010-01-25 04:50:00
    print(iter.get_next(datetime))   # 2010-01-25 04:55:00
    
    # Previous occurrence
    base = datetime(2010, 8, 25)
    itr = croniter('0 0 1 * *', base)
    print(itr.get_prev(datetime))  # 2010-08-01 00:00:00
  6. Use hashed/random expressions with `H` syntax

    main

    Croniter supports hashed/randomized expressions using the H syntax, which allows for distributing tasks more evenly. This requires providing a hash_id to ensure deterministic results.

    Supported patterns:

    • H: A single random value within the field's range.
    • H(range_begin-range_end): A random value within a specific sub-range.
    • H/step: A random start value within the field's range, followed by a step.
    • H(range_begin-range_end)/step: A random start value within a sub-range, followed by a step.

    Note: When using these expressions, you must provide a hash_id (bytes or string) to the expand or is_valid methods.

    # Example of how H syntax is used conceptually
    # H/15 in the minute field might expand to something like 7-59/15
    croniter.expand('H/15 * * * *', hash_id='my_salt')
  7. Run croniter using Docker Compose

    main

    You can run the croniter application using Docker Compose. The service is configured to use a specific image defined by the CRONITER_IMAGE environment variable, defaulting to pallets-eco/croniter:latest if not provided.

    When running via Docker Compose, the following local files are mounted into the container to provide the application source and configuration:

    • ./docker-entry.sh -> /app/docker-entry.sh
    • ./.dockertox -> /app/.tox
    • ./pyproject.toml -> /app/pyproject.toml
    • ./src -> /app/src
    • ./tox.ini -> /app/tox.ini
    services:
      app:
        image: "${CRONITER_IMAGE:-pallets-eco/croniter:latest}"
        volumes:
        - ./docker-entry.sh:/app/docker-entry.sh
        - ./.dockertox:/app/.tox
        - ./pyproject.toml:/app/pyproject.toml
        - ./src:/app/src
        - ./tox.ini:/app/tox.ini
  8. Enable Vixie cron bug compatibility

    main

    Some Vixie/ISC cron implementations have a bug where expressions starting with * in the day-of-month or day-of-week fields (e.g., */32,1-7) use AND logic instead of OR. To replicate this behavior, set implement_cron_bug=True in the croniter constructor.

    # Replicating the bug for specific patterns
    iter = croniter('1 1 */32,1-7 * 2', datetime(2024, 7, 12), implement_cron_bug=True)
  9. Test if a datetime matches a cron expression

    main

    Use croniter.match() to check if a specific datetime matches a cron pattern.

    Precision:

    • For 5-field expressions: default precision is 60 seconds.
    • For 6-field expressions (with seconds): default precision is 1 second.

    You can override this behavior using the precision_in_seconds parameter.

    >>> # Standard match
    >>> croniter.match("0 0 * * *", datetime(2019, 1, 14, 0, 0, 0, 0))
    True
    
    >>> # Custom precision (only exact match within 1s)
    >>> croniter.match("0 0 * * *", datetime(2019, 1, 14, 0, 0, 59, 0), precision_in_seconds=1)
    False
  10. Configure max_years_between_matches to prevent CPU exhaustion

    main

    To prevent high CPU usage when iterating over sparse cron expressions (where matches are far apart), use the max_years_between_matches parameter.

    • If a match is not found within this window, croniter will stop iterating or raise a CroniterBadDateError (depending on usage).
    • Setting this value explicitly allows the iterator to simply stop instead of raising an error, which is useful for handling untrusted or sparse expressions.
    # Limit search to a 15-year window to find matches for rare events
    >>> it = croniter("0 4 1 1 fri", datetime(2000,1,1), day_or=False, max_years_between_matches=15).all_next(datetime)
  11. Use the year field in cron expressions

    main

    Croniter supports a year field as the seventh field in a cron expression (placed after the optional seconds field). The supported range is from 1970 to 2099. To ignore the seconds field and use the year field, set the seconds field to 0 or any other constant.

    >>> from datetime import datetime
    >>> base = datetime(2012, 4, 6, 2, 6, 59)
    >>> itr = croniter('0 0 1 1 * 0 2020/2', base)
    >>> itr.get_next(datetime) # 2020 1/1 0:0:0
    >>> itr.get_next(datetime) # 2022 1/1 0:0:0
    >>> itr.get_next(datetime) # 2024 1/1 0:0:0