Skip to content

Instantly share code, notes, and snippets.

@ultrasonex
Last active January 23, 2024 09:25
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save ultrasonex/e1fdb8354408a56df91aa4902d17aa6a to your computer and use it in GitHub Desktop.
Save ultrasonex/e1fdb8354408a56df91aa4902d17aa6a to your computer and use it in GitHub Desktop.
AWS Cron Validator : Schedule expression
# Author : Niloy Chakraborty
# AWS Schedule Expression cron expression validator
import re
from voluptuous import Invalid
import datetime
minute_regex = r"^([*]|([0]?[0-5]?[0-9]?)|(([0]?[0-5]?[0-9]?)(\/|\-)([0]?[0-5]?[0-9]?))|" \
"(([0]?[0-5]?[0-9]?)((\,)([0]?[0-5]?[0-9]?))*))$"
hour_regex = r"^([*]|[01]?[0-9]|2[0-3]|(([01]?[0-9]|2[0-3]?)(\/|\-)([01]?[0-9]|2[0-3]?))|" \
"(([01]?[0-9]|2[0-3]?)((\,)([01]?[0-9]|2[0-3]?))*))$"
d_m_regex = r"^([*]|[?]|([0-2]?[0-9]|3[0-1])|(([0-2]?[0-9]|3[0-1])(\/|\-)([0-2]?[0-9]|3[0-1]))|" \
"(([0-2]?[0-9]|3[0-1])((\,)([0-2]?[0-9]|3[0-1]))*))$"
month_regex = r"^([*]|([0]?[0-9]|1[0-2])|(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)|" \
"((([0]?[0-9]|1[0-2])|(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))(\/|\-)(([0]?[0-9]|1[0-2])|" \
"(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)))|((([0]?[0-9]|1[0-2])|" \
"(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))((\,)(([0]?[0-9]|1[0-2])|" \
"(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)))*))$"
d_w_regex = r"^([*]|[?]|([0]?[1-7])|(SUN|MON|TUE|WED|THU|FRI|SAT)|((([0]?[1-7])|(SUN|MON|TUE|WED|" \
"THU|FRI|SAT))(\/|\-|\,|\#)(([0]?[1-7])|(SUN|MON|TUE|WED|THU|FRI|SAT)))|((([0]?[1-7])|(SUN|MON|TUE|WED|" \
"THU|FRI|SAT))((\,)(([0]?[1-7])|(SUN|MON|TUE|WED|THU|FRI|SAT)))*))$"
year_regex = r"^([*]|([1-2][01][0-9][0-9])|(([1-2][01][0-9][0-9])(\/|\-)([1-2][01][0-9][0-9]))|" \
"(([1-2][01][0-9][0-9])((\,)([1-2][01][0-9][0-9]))*))$"
def validate_aws_regex(regex_val):
regex_splits = regex_val.split(" ")
if len(regex_splits) < 6:
raise Invalid(
"Schedule parameter should have 6 field minute,hour,day_of_the_month,month,day_of_the_week,year.Please check the value")
minute_val = regex_splits[0]
hour_val = regex_splits[1]
d_m_val = regex_splits[2]
month_val = regex_splits[3]
d_w_val = regex_splits[4]
year_val = regex_splits[5]
print(
"Values of Min is %s , hour is %s , day_of_month is %s , month is %s ,day of the week is %s and year is %s" % (
minute_val, hour_val, d_m_val, month_val, d_w_val, year_val))
if not ((d_m_val == '?' and d_w_val != '?') or (d_m_val != '?' and d_w_val == '?')):
raise Invalid("Either day-of-month or day-of-week values must be a question mark (?)")
minute_match = re.fullmatch(minute_regex, minute_val)
hour_match = re.fullmatch(hour_regex, hour_val)
d_m_match = re.fullmatch(d_m_regex, d_m_val)
month_match = re.fullmatch(month_regex, month_val)
d_w_match = re.fullmatch(d_w_regex, d_w_val)
year_match = re.fullmatch(year_regex, year_val)
if not minute_match:
raise Invalid("Schedule expression has an invalid minute column value")
if not hour_match:
raise Invalid("Schedule expression has an invalid hour column value")
if not d_m_match:
raise Invalid("Schedule expression has an invalid day of the month column value")
if not month_match:
raise Invalid("Schedule expression has an invalid month column value")
if not d_w_match:
raise Invalid("Schedule expression has an invalid day of the week column value")
if not year_match:
raise Invalid("Schedule expression has an invalid year column value")
if '#' in d_w_val:
nd = int(d_w_val.split('#')[1])
if nd > 5:
raise Invalid(
"Schedule expression has an invalid day of the week column value. Nth day of week cannot be more than 5")
current_year = datetime.datetime.today().year
year_split_comma_val = year_val.split(",")
year_split_dash_val = year_val.split("-")
if "," in year_val:
if any(yr_val for yr_val in year_split_comma_val if int(yr_val) < current_year):
raise \
Invalid("Schedule expression has an invalid year column value. " \
"Year value[s] should be greater or equal to %s and less than 2199 " % current_year)
if "-" in year_val:
if int(year_split_dash_val[1]) < current_year:
raise \
Invalid("Schedule expression has an invalid year column value. " \
"Year value[s] should be greater or equal to %s and less than 2199 " % current_year)
return regex_val
# Valid
assert validate_aws_regex("0 18 ? * MON-FRI *") == "0 18 ? * MON-FRI *"
assert validate_aws_regex("0 10 * * ? *") == "0 10 * * ? *"
assert validate_aws_regex("15 12 * * ? *") == "15 12 * * ? *"
assert validate_aws_regex("0 8 1 * ? *") == "0 8 1 * ? *"
assert validate_aws_regex("0/5 8-17 ? * MON-FRI *") == "0/5 8-17 ? * MON-FRI *"
assert validate_aws_regex("0 9 ? * 2#1 *") == "0 9 ? * 2#1 *"
assert validate_aws_regex("0 07/12 ? * * *") == "0 07/12 ? * * *"
assert validate_aws_regex("10,20,30,40 07/12 ? * * *") == "10,20,30,40 07/12 ? * * *"
assert validate_aws_regex("10 10,15,20,23 ? * * *") == "10 10,15,20,23 ? * * *"
assert validate_aws_regex("10 10 15,30,31 * ? *") == "10 10 15,30,31 * ? *"
assert validate_aws_regex("10 10 15 JAN,JUL,DEC ? *") == "10 10 15 JAN,JUL,DEC ? *"
assert validate_aws_regex("10 10 31 04,09,12 ? *") == "10 10 31 04,09,12 ? *"
assert validate_aws_regex("0,5 07/12 ? * 01,05,7 *") == "0,5 07/12 ? * 01,05,7 *"
assert validate_aws_regex("0,5 07/12 ? * 01,05,7 2020,2021,2028,2199") == "0,5 07/12 ? * 01,05,7 2020,2021,2028,2199"
assert validate_aws_regex("0,5 07/12 ? * 01,05,7 2020,2021,2028,2199")
# Invalid
# validate_aws_regex("0 18 ? * MON-FRI")
# validate_aws_regex("0 18 * * * *")
# validate_aws_regex("0 65 * * ? *")
# validate_aws_regex("89 10 * * ? *")
# validate_aws_regex("15/65 10 * * ? *")
# validate_aws_regex("15/30 10 * * ? 2400")
# validate_aws_regex("0 9 ? * 2#6 *")
# validate_aws_regex("0 9 ? * ? *")
# validate_aws_regex("10 10 31 04,09,13 ? *")
# validate_aws_regex("0,5 07/12 ? * 01,05,8 *")
# validate_aws_regex("0,5 07/12 ? * 01,05,7 2020,2021,2028,1111")
#validate_aws_regex("0,5 07/12 ? * 01,05,7 2020,2021,2028,2017")
#validate_aws_regex("0,5 07/12 ? * 01,05,7 2017-2100")
@grumBit
Copy link

grumBit commented Mar 11, 2022

Hi @ultrasonex, I was looking to use this to validate AWS EventBridge Cron Expressions, but I'm not sure if your validator is for a different AWS cron format. If it is for EventBridge, I found a couple of things in your validator:

  • It doesn't handle the W and L options for Day-of-month and Day-of-week.
  • Old years may not match, but are valid. Especially for ranges ( e.g. 2017-2100 is a valid range and will match for many decades). In my application expressions will be saved for an unknown period, so an expression that is valid now should remain valid if validated in the future. 2017 may never match again, but it does conform to the allowed 1970-2199 years, so is valid.

Anyway, thank you for you great work.

@stpnvntn
Copy link

Hi @ultrasonex, thanks for sharing it

As mentioned above some of the regex have issues. I fixed some of theme. Here is a fixed version:

// Minutes 
/^([*]|(([0-5]?\d))|((([0-5]?\d))(\/|-)([0-5]?\d))|((([0-5]?\d))((,)([0-5]?\d))*))$/;

// Hours
/^([*]|[01]?\d|2[0-3]|(([01]?\d|2[0-3]?)(\/|-)([01]?\d|2[0-3]?))|(([01]?\d|2[0-3]?)((,)([01]?\d|2[0-3]?))*))$/;

// Day of months
/^([*]|[?]|(([1-9]|[12]\d|3[01])[LW]?)|(([1-9]|[12]\d|3[01])(\/|-)([1-9]|[12]\d|3[01]))|(([1-9]|[12]\d|3[01])((,)([1-9]|[12]\d|3[01]))*))$/;

// Months
/^([*]|([2-9]|1[0-2]?)|(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)|((([2-9]|1[0-2]?)|(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))(\/|-)(([2-9]|1[0-2]?)|(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)))|((([2-9]|1[0-2]?)|(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))((,)(([2-9]|1[0-2]?)|(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)))*))$/;

// Day of Week
/^([*]|[?]|([1-7]L?)|(SUN|MON|TUE|WED|THU|FRI|SAT)|((([1-7])|(SUN|MON|TUE|WED|THU|FRI|SAT))(\/|-|,|#)(([1-7])|(SUN|MON|TUE|WED|THU|FRI|SAT)))|((([1-7])|(SUN|MON|TUE|WED|THU|FRI|SAT))((,)(([1-7])|(SUN|MON|TUE|WED|THU|FRI|SAT)))*))$/;

// Year
/^([*]|([1-2]\d{3})|(([1-2]\d{3})(\/|-)([1-2]\d{0,3}))|(([1-2]\d{3})((,)([1-2]\d{3}))*))$/

@ultrasonex
Copy link
Author

Hi @ultrasonex, I was looking to use this to validate AWS EventBridge Cron Expressions, but I'm not sure if your validator is for a different AWS cron format. If it is for EventBridge, I found a couple of things in your validator:

  • It doesn't handle the W and L options for Day-of-month and Day-of-week.
  • Old years may not match, but are valid. Especially for ranges ( e.g. 2017-2100 is a valid range and will match for many decades). In my application expressions will be saved for an unknown period, so an expression that is valid now should remain valid if validated in the future. 2017 may never match again, but it does conform to the allowed 1970-2199 years, so is valid.

Anyway, thank you for you great work.

Hi @grumBit , I am glad you were able to use it. Yes at the time of writing, we had a requirement to expose a UI which did not deal with AWS like cron but more of a standard cron so that did not require W or L, which is why I kind of left that part. I will try to update it to include them.

@grumBit
Copy link

grumBit commented Sep 9, 2022

Hi @grumBit , I am glad you were able to use it. Yes at the time of writing, we had a requirement to expose a UI which did not deal with AWS like cron but more of a standard cron so that did not require W or L, which is why I kind of left that part. I will try to update it to include them.

Hi @ultrasonex, I took inspiration from your work and added support for W and L in a new project over here. I decided to generate the regex strings as they were getting very long and difficult to read, added some testing and packaged it up to PyPi. I'm not sure if it's of any use to you at this stage 🤷 .

Many thanks, Graham.

@ultrasonex
Copy link
Author

Hi @grumBit , I am glad you were able to use it. Yes at the time of writing, we had a requirement to expose a UI which did not deal with AWS like cron but more of a standard cron so that did not require W or L, which is why I kind of left that part. I will try to update it to include them.

Hi @ultrasonex, I took inspiration from your work and added support for W and L in a new project over here. I decided to generate the regex strings as they were getting very long and difficult to read, added some testing and packaged it up to PyPi. I'm not sure if it's of any use to you at this stage 🤷 .

Many thanks, Graham.

Thanks a lot!! 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment