dify/api/libs/schedule_utils.py
Yeuoly b76e17b25d
feat: introduce trigger functionality (#27644)
Signed-off-by: lyzno1 <yuanyouhuilyz@gmail.com>
Co-authored-by: Stream <Stream_2@qq.com>
Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: zhsama <torvalds@linux.do>
Co-authored-by: Harry <xh001x@hotmail.com>
Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com>
Co-authored-by: yessenia <yessenia.contact@gmail.com>
Co-authored-by: hjlarry <hjlarry@163.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: WTW0313 <twwu@dify.ai>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-12 17:59:37 +08:00

109 lines
3.4 KiB
Python

from datetime import UTC, datetime
import pytz
from croniter import croniter
def calculate_next_run_at(
cron_expression: str,
timezone: str,
base_time: datetime | None = None,
) -> datetime:
"""
Calculate the next run time for a cron expression in a specific timezone.
Args:
cron_expression: Standard 5-field cron expression or predefined expression
timezone: Timezone string (e.g., 'UTC', 'America/New_York')
base_time: Base time to calculate from (defaults to current UTC time)
Returns:
Next run time in UTC
Note:
Supports enhanced cron syntax including:
- Month abbreviations: JAN, FEB, MAR-JUN, JAN,JUN,DEC
- Day abbreviations: MON, TUE, MON-FRI, SUN,WED,FRI
- Predefined expressions: @daily, @weekly, @monthly, @yearly, @hourly
- Special characters: ? wildcard, L (last day), Sunday as 7
- Standard 5-field format only (minute hour day month dayOfWeek)
"""
# Validate cron expression format to match frontend behavior
parts = cron_expression.strip().split()
# Support both 5-field format and predefined expressions (matching frontend)
if len(parts) != 5 and not cron_expression.startswith("@"):
raise ValueError(
f"Cron expression must have exactly 5 fields or be a predefined expression "
f"(@daily, @weekly, etc.). Got {len(parts)} fields: '{cron_expression}'"
)
tz = pytz.timezone(timezone)
if base_time is None:
base_time = datetime.now(UTC)
base_time_tz = base_time.astimezone(tz)
cron = croniter(cron_expression, base_time_tz)
next_run_tz = cron.get_next(datetime)
next_run_utc = next_run_tz.astimezone(UTC)
return next_run_utc
def convert_12h_to_24h(time_str: str) -> tuple[int, int]:
"""
Parse 12-hour time format to 24-hour format for cron compatibility.
Args:
time_str: Time string in format "HH:MM AM/PM" (e.g., "12:30 PM")
Returns:
Tuple of (hour, minute) in 24-hour format
Raises:
ValueError: If time string format is invalid or values are out of range
Examples:
- "12:00 AM" -> (0, 0) # Midnight
- "12:00 PM" -> (12, 0) # Noon
- "1:30 PM" -> (13, 30)
- "11:59 PM" -> (23, 59)
"""
if not time_str or not time_str.strip():
raise ValueError("Time string cannot be empty")
parts = time_str.strip().split()
if len(parts) != 2:
raise ValueError(f"Invalid time format: '{time_str}'. Expected 'HH:MM AM/PM'")
time_part, period = parts
period = period.upper()
if period not in ["AM", "PM"]:
raise ValueError(f"Invalid period: '{period}'. Must be 'AM' or 'PM'")
time_parts = time_part.split(":")
if len(time_parts) != 2:
raise ValueError(f"Invalid time format: '{time_part}'. Expected 'HH:MM'")
try:
hour = int(time_parts[0])
minute = int(time_parts[1])
except ValueError as e:
raise ValueError(f"Invalid time values: {e}")
if hour < 1 or hour > 12:
raise ValueError(f"Invalid hour: {hour}. Must be between 1 and 12")
if minute < 0 or minute > 59:
raise ValueError(f"Invalid minute: {minute}. Must be between 0 and 59")
# Handle 12-hour to 24-hour edge cases
if period == "PM" and hour != 12:
hour += 12
elif period == "AM" and hour == 12:
hour = 0
return hour, minute