Update next time execution time when starting function

This commit is contained in:
Peter Stockings
2025-11-30 00:17:40 +11:00
parent a2367f3ed9
commit 6c55eaa930

View File

@@ -149,6 +149,75 @@ DEFAULT_ENVIRONMENT = """{
timer = Blueprint('timer', __name__)
def calculate_next_run(trigger_type, frequency_minutes=None, run_date=None, cron_expression=None, base_time=None):
"""
Calculate the next execution time based on trigger configuration.
Args:
trigger_type: One of 'interval', 'date', or 'cron'
frequency_minutes: Minutes between executions (for interval type)
run_date: Specific datetime to run (for date type)
cron_expression: Cron expression string (for cron type)
base_time: Base datetime to calculate from (defaults to now)
Returns:
tuple: (next_run datetime, error_response dict or None)
If error_response is not None, it contains the error to return to client
"""
if base_time is None:
base_time = datetime.now(timezone.utc)
next_run = None
if trigger_type == 'interval':
if frequency_minutes is None:
return None, {
"status": "error",
"message": "frequency_minutes is required for interval trigger type"
}
next_run = base_time + timedelta(minutes=int(frequency_minutes))
elif trigger_type == 'date':
if run_date is None:
return None, {
"status": "error",
"message": "run_date is required for date trigger type"
}
# run_date should already be a datetime object
if isinstance(run_date, str):
run_date = datetime.fromisoformat(run_date)
next_run = run_date
elif trigger_type == 'cron':
if cron_expression is None:
return None, {
"status": "error",
"message": "cron_expression is required for cron trigger type"
}
from croniter import croniter
# Validate cron expression
try:
if not croniter.is_valid(cron_expression):
return None, {
"status": "error",
"message": "Invalid cron expression format"
}
# Calculate next run time from base_time
next_run = croniter(cron_expression, base_time).get_next(datetime)
except Exception as e:
return None, {
"status": "error",
"message": f"Invalid cron expression: {str(e)}"
}
else:
return None, {
"status": "error",
"message": f"Invalid trigger type: {trigger_type}"
}
return next_run, None
@timer.route('/overview')
@login_required
def overview():
@@ -192,36 +261,28 @@ def new():
"message": "Invalid trigger type"
}), 400
# Calculate next_run based on trigger type
next_run = None
# Calculate next_run based on trigger type using centralized function
next_run, error = calculate_next_run(
trigger_type,
frequency_minutes=data.get('frequency_minutes'),
run_date=data.get('run_date'),
cron_expression=data.get('cron_expression')
)
if error:
return jsonify(error), 400
# Extract individual variables for database storage
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("""
@@ -301,36 +362,28 @@ def edit(function_id):
"message": "Invalid trigger type"
}), 400
# Calculate next_run based on trigger type
next_run = None
# Calculate next_run based on trigger type using centralized function
next_run, error = calculate_next_run(
trigger_type,
frequency_minutes=data.get('frequency_minutes'),
run_date=data.get('run_date'),
cron_expression=data.get('cron_expression')
)
if error:
return jsonify(error), 400
# Extract individual variables for database storage
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("""
@@ -406,20 +459,48 @@ def delete(function_id):
@login_required
def toggle(function_id):
try:
# Toggle the enabled status
result = db.execute("""
UPDATE timer_functions
SET enabled = NOT enabled
# Fetch timer function details first to get trigger configuration
timer_function = db.execute("""
SELECT enabled, trigger_type, frequency_minutes, run_date, cron_expression
FROM timer_functions
WHERE id = %s AND user_id = %s
RETURNING enabled
""", [function_id, current_user.id], commit=True, one=True)
""", [function_id, current_user.id], one=True)
if not result:
if not timer_function:
return jsonify({
"status": "error",
"message": "Timer function not found or unauthorized"
}), 404
# Determine new enabled state (toggling)
new_enabled = not timer_function['enabled']
# If enabling the timer, recalculate next_run to prevent stale execution times
if new_enabled:
next_run, error = calculate_next_run(
timer_function['trigger_type'],
frequency_minutes=timer_function['frequency_minutes'],
run_date=timer_function['run_date'],
cron_expression=timer_function['cron_expression']
)
if error:
return jsonify(error), 400
# Update both enabled status and next_run
db.execute("""
UPDATE timer_functions
SET enabled = %s, next_run = %s
WHERE id = %s AND user_id = %s
""", [new_enabled, next_run, function_id, current_user.id], commit=True)
else:
# Just toggle the enabled status
db.execute("""
UPDATE timer_functions
SET enabled = %s
WHERE id = %s AND user_id = %s
""", [new_enabled, function_id, current_user.id], commit=True)
# Fetch updated timer functions for the overview template
timer_functions = db.execute("""
SELECT id, name, code, environment, trigger_type,