Add support for cron expressions for scheduling timer functions

This commit is contained in:
Peter Stockings
2025-11-27 15:51:15 +11:00
parent f3c2664b31
commit 3f9fa79515
7 changed files with 152 additions and 44 deletions

View File

@@ -17,10 +17,11 @@ CREATE TABLE timer_functions (
user_id INT NOT NULL, -- the referencing column
trigger_type VARCHAR(20) NOT NULL CHECK (
trigger_type IN ('interval', 'date')
trigger_type IN ('interval', 'date', 'cron')
),
frequency_minutes INT, -- used if trigger_type = 'interval'
run_date TIMESTAMPTZ, -- used if trigger_type = 'date' (one-off)
cron_expression TEXT, -- used if trigger_type = 'cron'
next_run TIMESTAMPTZ,
last_run TIMESTAMPTZ,
@@ -154,7 +155,7 @@ def overview():
timer_functions = db.execute("""
SELECT id, name, code, environment, trigger_type,
frequency_minutes, run_date, next_run,
last_run, enabled, invocation_count, runtime
last_run, enabled, invocation_count, runtime, cron_expression
FROM timer_functions
WHERE user_id = %s
ORDER BY id DESC
@@ -185,7 +186,7 @@ def new():
runtime = data.get('runtime', 'node')
# Validate trigger type
if trigger_type not in ('interval', 'date'):
if trigger_type not in ('interval', 'date', 'cron'):
return jsonify({
"status": "error",
"message": "Invalid trigger type"
@@ -193,19 +194,41 @@ def new():
# Calculate next_run based on trigger type
next_run = None
frequency_minutes = None
run_date = None
cron_expression = None
if trigger_type == 'interval':
frequency_minutes = int(data.get('frequency_minutes'))
next_run = datetime.now(timezone.utc) + timedelta(minutes=frequency_minutes)
elif trigger_type == 'date':
run_date = datetime.fromisoformat(data.get('run_date'))
next_run = run_date
elif trigger_type == 'cron':
from croniter import croniter
cron_expression = data.get('cron_expression')
# Validate cron expression
try:
if not croniter.is_valid(cron_expression):
return jsonify({
"status": "error",
"message": "Invalid cron expression format"
}), 400
# Calculate first run time
next_run = croniter(cron_expression, datetime.now(timezone.utc)).get_next(datetime)
except Exception as e:
return jsonify({
"status": "error",
"message": f"Invalid cron expression: {str(e)}"
}), 400
# Insert new timer function
db.execute("""
INSERT INTO timer_functions
(name, code, environment, user_id, trigger_type,
frequency_minutes, run_date, next_run, enabled, runtime)
VALUES (%s, %s, %s::jsonb, %s, %s, %s, %s, %s, %s, %s)
frequency_minutes, run_date, cron_expression, next_run, enabled, runtime)
VALUES (%s, %s, %s::jsonb, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
""", [
data.get('name'),
@@ -213,8 +236,9 @@ def new():
data.get('environment_info'),
current_user.id,
trigger_type,
frequency_minutes if trigger_type == 'interval' else None,
run_date if trigger_type == 'date' else None,
frequency_minutes,
run_date,
cron_expression,
next_run,
True,
runtime
@@ -239,8 +263,8 @@ def edit(function_id):
# Fetch the timer function
timer_function = db.execute("""
SELECT id, name, code, environment, version_number, trigger_type,
frequency_minutes, run_date, next_run,
last_run, enabled, invocation_count, runtime
frequency_minutes, run_date, cron_expression, next_run,
last_run, enabled, invocation_count, runtime
FROM timer_functions
WHERE id = %s AND user_id = %s
""", [function_id, current_user.id], one=True)
@@ -271,7 +295,7 @@ def edit(function_id):
runtime = data.get('runtime', 'node')
# Validate trigger type
if trigger_type not in ('interval', 'date'):
if trigger_type not in ('interval', 'date', 'cron'):
return jsonify({
"status": "error",
"message": "Invalid trigger type"
@@ -279,12 +303,34 @@ def edit(function_id):
# Calculate next_run based on trigger type
next_run = None
frequency_minutes = None
run_date = None
cron_expression = None
if trigger_type == 'interval':
frequency_minutes = int(data.get('frequency_minutes'))
next_run = datetime.now(timezone.utc) + timedelta(minutes=frequency_minutes)
elif trigger_type == 'date':
run_date = datetime.fromisoformat(data.get('run_date'))
next_run = run_date
elif trigger_type == 'cron':
from croniter import croniter
cron_expression = data.get('cron_expression')
# Validate cron expression
try:
if not croniter.is_valid(cron_expression):
return jsonify({
"status": "error",
"message": "Invalid cron expression format"
}), 400
# Calculate first run time
next_run = croniter(cron_expression, datetime.now(timezone.utc)).get_next(datetime)
except Exception as e:
return jsonify({
"status": "error",
"message": f"Invalid cron expression: {str(e)}"
}), 400
# Update timer function
db.execute("""
@@ -295,6 +341,7 @@ def edit(function_id):
trigger_type = %s,
frequency_minutes = %s,
run_date = %s,
cron_expression = %s,
next_run = %s,
enabled = %s,
runtime = %s
@@ -305,8 +352,9 @@ def edit(function_id):
data.get('script_content'),
data.get('environment_info'),
trigger_type,
frequency_minutes if trigger_type == 'interval' else None,
run_date if trigger_type == 'date' else None,
frequency_minutes,
run_date,
cron_expression,
next_run,
data.get('is_enabled', True), # Default to True if not provided
runtime,
@@ -371,12 +419,12 @@ def toggle(function_id):
"status": "error",
"message": "Timer function not found or unauthorized"
}), 404
# Fetch updated timer functions for the overview template
timer_functions = db.execute("""
SELECT id, name, code, environment, trigger_type,
frequency_minutes, run_date, next_run,
last_run, enabled, invocation_count, runtime
last_run, enabled, invocation_count, runtime, cron_expression
FROM timer_functions
WHERE user_id = %s
ORDER BY id DESC