CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-croniter

croniter provides iteration for datetime object with cron like format

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

advanced-features.mddocs/

Advanced Features

Advanced croniter features including Jenkins-style hashed expressions, random expressions, custom start time expansion, specialized cron syntax, and timezone handling capabilities.

Capabilities

Hashed Expressions (Jenkins-Style)

Jenkins-style hashed expressions using the "H" keyword provide consistent but distributed scheduling across different job IDs.

# Hashed expressions in croniter constructor
iter = croniter("H H * * *", hash_id="unique-job-id")

Hashed expressions remain consistent for the same hash_id but distribute differently across different hash_id values, enabling even distribution of jobs without manual coordination.

Random Expressions

Random "R" expressions provide randomized scheduling that remains consistent within a croniter instance but varies between instances.

# Random expressions in croniter constructor  
iter = croniter("R R * * *")

Random expressions generate different times for each croniter instance but remain consistent for subsequent calls within the same instance.

Custom Start Time Expansion

Control how interval expressions are calculated relative to start time versus calendar boundaries.

iter = croniter(
    '0 0 */7 * *',  # Every 7 days
    start_time=datetime(2024, 7, 11),
    expand_from_start_time=True  # Calculate from start_time, not calendar
)

When expand_from_start_time=True, intervals are calculated from the specified start time rather than calendar boundaries (e.g., every 7 days from July 11th rather than from the 1st of each month).

Second-Level Precision

Support for second-level cron expressions using 6-field format.

# Seconds as 6th field (default)
iter = croniter('* * * * * 15,45', base_time)  # At 15 and 45 seconds of each minute

# Seconds as 1st field (alternative)
iter = croniter('15,45 * * * * *', base_time, second_at_beginning=True)

Year Field Support

Support for year field using 7-field format for precise year-based scheduling.

# Year as 7th field (seconds=0 for simplicity)
iter = croniter('0 0 1 1 * 0 2020/2', base_time)  # January 1st every 2 years from 2020

Year field supports range 1970-2099 and standard cron syntax (ranges, steps, lists).

Special Cron Syntax

Advanced cron syntax including nth weekday of month and last weekday specifications.

# Nth weekday of month
iter = croniter('0 0 * * sat#1,sun#2', base_time)  # 1st Saturday and 2nd Sunday

# Last weekday of month
iter = croniter('0 0 * * L5', base_time)  # Last Friday of each month

# Combined nth and last
iter = croniter('0 0 * * 5#3,L5', base_time)  # 3rd and last Friday

Usage Examples

Hashed Expressions

from croniter import croniter
from datetime import datetime

base = datetime(2021, 4, 10)

# Same hash_id produces consistent results
iter1 = croniter("H H * * *", base, hash_id="hello")
iter2 = croniter("H H * * *", base, hash_id="hello")

print(iter1.get_next(datetime))  # 2021-04-10 11:10:00
print(iter2.get_next(datetime))  # 2021-04-10 11:10:00 (same result)

# Different hash_id produces different results
iter3 = croniter("H H * * *", base, hash_id="bonjour") 
print(iter3.get_next(datetime))  # 2021-04-10 20:52:00 (different result)

# Hashed ranges
iter4 = croniter("H(0-30) H(9-17) * * *", base, hash_id="business-job")
print(iter4.get_next(datetime))  # Random minute 0-30, random hour 9-17

Random Expressions

from croniter import croniter
from datetime import datetime

base = datetime(2021, 4, 10)

# Each instance gets different random values
iter1 = croniter("R R * * *", base)
iter2 = croniter("R R * * *", base)

print(iter1.get_next(datetime))  # 2021-04-10 22:56:00
print(iter2.get_next(datetime))  # 2021-04-10 07:31:00 (different)

# But consistent within same instance
print(iter1.get_next(datetime))  # 2021-04-11 22:56:00 (same time next day)

# Random ranges
iter3 = croniter("R(0-30) R(9-17) * * *", base)
print(iter3.get_next(datetime))  # Random minute 0-30, random hour 9-17

Custom Start Time Expansion

from croniter import croniter
from datetime import datetime

start = datetime(2024, 7, 11)  # July 11th

# Default behavior: every 7 days from calendar (1st, 8th, 15th, 22nd, 29th)
iter1 = croniter('0 0 */7 * *', start, expand_from_start_time=False)
matches1 = [iter1.get_next(datetime) for _ in range(5)]
print("Calendar-based:", [dt.day for dt in matches1])
# [15, 22, 29, 1, 8] (calendar boundaries)

# Custom expansion: every 7 days from start_time (11th, 18th, 25th, 1st, 8th)  
iter2 = croniter('0 0 */7 * *', start, expand_from_start_time=True)
matches2 = [iter2.get_next(datetime) for _ in range(5)]
print("Start-time-based:", [dt.day for dt in matches2])
# [18, 25, 1, 8, 15] (from start time)

Second-Level Precision

from croniter import croniter
from datetime import datetime

base = datetime(2012, 4, 6, 13, 26, 10)

# Seconds as 6th field (default)
iter1 = croniter('* * * * * 15,25', base)
print(iter1.get_next(datetime))  # 2012-04-06 13:26:15
print(iter1.get_next(datetime))  # 2012-04-06 13:26:25
print(iter1.get_next(datetime))  # 2012-04-06 13:27:15

# Every second (be careful with performance!)
iter2 = croniter('* * * * * *', base)
print(iter2.get_next(datetime))  # 2012-04-06 13:26:11

# Seconds as 1st field  
iter3 = croniter('15,25 * * * * *', base, second_at_beginning=True)
print(iter3.get_next(datetime))  # 2012-04-06 13:26:15

# Mixed with other precision
iter4 = croniter('30 */5 * * * *', base)  # Every 5 minutes at 30 seconds
print(iter4.get_next(datetime))  # 2012-04-06 13:30:30

Year Field Support

from croniter import croniter
from datetime import datetime

base = datetime(2012, 4, 6, 2, 6, 59)

# New Year's Day every 2 years starting 2020
iter1 = croniter('0 0 1 1 * 0 2020/2', base)
print(iter1.get_next(datetime))  # 2020-01-01 00:00:00
print(iter1.get_next(datetime))  # 2022-01-01 00:00:00
print(iter1.get_next(datetime))  # 2024-01-01 00:00:00

# Specific years list
iter2 = croniter('0 0 1 1 * 0 2025,2030,2035', base)
print(iter2.get_next(datetime))  # 2025-01-01 00:00:00

# Year ranges
iter3 = croniter('0 0 1 1 * 0 2025-2030', base)
matches = [iter3.get_next(datetime) for _ in range(6)]
print([dt.year for dt in matches])  # [2025, 2026, 2027, 2028, 2029, 2030]

Special Weekday Syntax

from croniter import croniter
from datetime import datetime

base = datetime(2010, 1, 1)

# First Saturday and second Sunday of each month
iter1 = croniter('0 0 * * sat#1,sun#2', base)
print(iter1.get_next(datetime))  # 2010-01-02 00:00:00 (1st Saturday)
print(iter1.get_next(datetime))  # 2010-01-10 00:00:00 (2nd Sunday)

# Third and last Friday of each month
iter2 = croniter('0 0 * * 5#3,L5', base)
print(iter2.get_next(datetime))  # 2010-01-15 00:00:00 (3rd Friday)
print(iter2.get_next(datetime))  # 2010-01-29 00:00:00 (last Friday)

# Last day of each month (using L)
iter3 = croniter('0 0 L * *', base)
print(iter3.get_next(datetime))  # 2010-01-31 00:00:00
print(iter3.get_next(datetime))  # 2010-02-28 00:00:00 (handles Feb correctly)

Timezone Handling

import pytz
from croniter import croniter
from datetime import datetime

# Create timezone-aware datetime
tz = pytz.timezone("Europe/Paris")
local_date = tz.localize(datetime(2017, 3, 26))  # Near DST transition

# Croniter preserves timezone information
iter1 = croniter('0 0 * * *', local_date)
next_match = iter1.get_next(datetime)
print(next_match.tzinfo)  # <DstTzInfo 'Europe/Paris' CET+1:00:00 STD>

# Works with DST transitions
iter2 = croniter('0 2 * * *', local_date)  # 2 AM daily
for i in range(5):
    match = iter2.get_next(datetime)
    print(f"{match} - DST: {match.dst()}")

# Using dateutil timezone
import dateutil.tz
tz2 = dateutil.tz.gettz('Asia/Tokyo')
tokyo_date = datetime(2017, 3, 26, tzinfo=tz2)
iter3 = croniter('0 0 * * *', tokyo_date)

Performance Optimization

from croniter import croniter
from datetime import datetime

# Limit search window for sparse expressions
iter1 = croniter(
    "0 4 1 1 fri",  # 4 AM on January 1st if it's Friday (very sparse!)
    datetime(2000, 1, 1),
    day_or=False,
    max_years_between_matches=15  # Limit to prevent infinite search
)

# With explicit limit, all_next() won't raise CroniterBadDateError
matches = []
for match in iter1.all_next(datetime):
    matches.append(match)
    if len(matches) >= 5:  # Get first 5 matches
        break

print(matches)
# [2010-01-01 04:00:00, 2016-01-01 04:00:00, 2021-01-01 04:00:00, ...]

# Without limit might raise CroniterBadDateError for very sparse expressions
try:
    iter2 = croniter("0 4 1 1 fri", datetime(2000, 1, 1), day_or=False)
    # This might fail if no matches found within default 50-year window
    match = iter2.get_next(datetime)
except Exception as e:
    print(f"Error: {e}")

Combining Advanced Features

from croniter import croniter
from datetime import datetime
import pytz

# Complex example combining multiple advanced features
tz = pytz.timezone("US/Eastern")
start = tz.localize(datetime(2024, 1, 1))

# Hashed expression with seconds and custom expansion
iter1 = croniter(
    'H H(9-17) * * 1-5 30',  # Random time during business hours, 30 seconds
    start,
    hash_id="business-report",
    expand_from_start_time=True,
    second_at_beginning=False
)

# Random expression with year field
iter2 = croniter(
    'R R 1 1 * 0 2025-2030',  # Random time on New Year's Day 2025-2030
    start
)

# Special syntax with timezone
iter3 = croniter(
    '0 9 * * L5',  # 9 AM on last Friday of each month
    start
)

# Get next matches
print("Business report:", iter1.get_next(datetime))
print("New Year random:", iter2.get_next(datetime))  
print("Monthly meeting:", iter3.get_next(datetime))

Install with Tessl CLI

npx tessl i tessl/pypi-croniter

docs

advanced-features.md

core-iteration.md

index.md

range-operations.md

validation-matching.md

tile.json