18
18
CONDITION_VALUE = "value"
19
19
CONDITION_ACTION = "action"
20
20
FEATURE_DEFAULT_VAL_TYPE_KEY = "boolean_type"
21
- TIME_RANGE_FORMAT = "%H:%M"
22
- TIME_RANGE_RE_PATTERN = re .compile (r"2[0-3]:[0-5]\d|[0-1]\d:[0-5]\d" )
21
+ TIME_RANGE_FORMAT = "%H:%M" # hour:min 24 hours clock
22
+ TIME_RANGE_RE_PATTERN = re .compile (r"2[0-3]:[0-5]\d|[0-1]\d:[0-5]\d" ) # 24 hour clock
23
23
HOUR_MIN_SEPARATOR = ":"
24
24
25
25
@@ -38,18 +38,26 @@ class RuleAction(Enum):
38
38
KEY_NOT_IN_VALUE = "KEY_NOT_IN_VALUE"
39
39
VALUE_IN_KEY = "VALUE_IN_KEY"
40
40
VALUE_NOT_IN_KEY = "VALUE_NOT_IN_KEY"
41
- SCHEDULE_BETWEEN_TIME_RANGE = "SCHEDULE_BETWEEN_TIME_RANGE" # hour:min 24 hours clock UTC time
42
- SCHEDULE_BETWEEN_DATETIME_RANGE = "SCHEDULE_BETWEEN_DATETIME_RANGE" # full datetime format
43
- SCHEDULE_BETWEEN_DAYS_OF_WEEK = "SCHEDULE_BETWEEN_DAYS_OF_WEEK"
41
+ SCHEDULE_BETWEEN_TIME_RANGE = "SCHEDULE_BETWEEN_TIME_RANGE" # hour:min 24 hours clock
42
+ SCHEDULE_BETWEEN_DATETIME_RANGE = "SCHEDULE_BETWEEN_DATETIME_RANGE" # full datetime format, excluding timezone
43
+ SCHEDULE_BETWEEN_DAYS_OF_WEEK = "SCHEDULE_BETWEEN_DAYS_OF_WEEK" # MONDAY, TUESDAY, .... see TimeValues enum
44
44
45
45
46
46
class TimeKeys (Enum ):
47
+ """
48
+ Possible keys when using time rules
49
+ """
50
+
47
51
CURRENT_TIME = "CURRENT_TIME"
48
52
CURRENT_DAY_OF_WEEK = "CURRENT_DAY_OF_WEEK"
49
53
CURRENT_DATETIME = "CURRENT_DATETIME"
50
54
51
55
52
56
class TimeValues (Enum ):
57
+ """
58
+ Possible values when using time rules
59
+ """
60
+
53
61
START = "START"
54
62
END = "END"
55
63
TIMEZONE = "TIMEZONE"
@@ -294,6 +302,11 @@ def validate_condition_key(condition: Dict[str, Any], rule_name: str):
294
302
key = condition .get (CONDITION_KEY , "" )
295
303
if not key or not isinstance (key , str ):
296
304
raise SchemaValidationError (f"'key' value must be a non empty string, rule={ rule_name } " )
305
+
306
+ # time actions need to have very specific keys
307
+ # SCHEDULE_BETWEEN_TIME_RANGE => CURRENT_TIME
308
+ # SCHEDULE_BETWEEN_DATETIME_RANGE => CURRENT_DATETIME
309
+ # SCHEDULE_BETWEEN_DAYS_OF_WEEK => CURRENT_DAY_OF_WEEK
297
310
action = condition .get (CONDITION_ACTION , "" )
298
311
if action == RuleAction .SCHEDULE_BETWEEN_TIME_RANGE .value and key != TimeKeys .CURRENT_TIME .value :
299
312
raise SchemaValidationError (
@@ -314,7 +327,8 @@ def validate_condition_value(condition: Dict[str, Any], rule_name: str):
314
327
if not value :
315
328
raise SchemaValidationError (f"'value' key must not be empty, rule={ rule_name } " )
316
329
action = condition .get (CONDITION_ACTION , "" )
317
- # time actions
330
+
331
+ # time actions need to be parsed to make sure date and time format is valid and timezone is recognized
318
332
if action == RuleAction .SCHEDULE_BETWEEN_TIME_RANGE .value :
319
333
ConditionsValidator ._validate_schedule_between_time_and_datetime_ranges (
320
334
value , rule_name , action , ConditionsValidator ._validate_time_value
@@ -330,9 +344,14 @@ def validate_condition_value(condition: Dict[str, Any], rule_name: str):
330
344
def _validate_datetime_value (datetime_str : str , rule_name : str ):
331
345
date = None
332
346
347
+ # We try to parse first with timezone information in order to return the correct error messages
348
+ # when a timestamp with timezone is used. Otherwise, the user would get the first error "must be a valid
349
+ # ISO8601 time format" which is misleading
350
+
333
351
try :
334
352
# python < 3.11 don't support the Z timezone on datetime.fromisoformat,
335
353
# so we replace any Z with the equivalent "+00:00"
354
+ # datetime.fromisoformat is orders of magnitude faster than datetime.strptime
336
355
date = datetime .fromisoformat (datetime_str .replace ("Z" , "+00:00" ))
337
356
except Exception :
338
357
raise SchemaValidationError (f"'START' and 'END' must be a valid ISO8601 time format, rule={ rule_name } " )
@@ -383,6 +402,8 @@ def _validate_schedule_between_days_of_week(value: Any, rule_name: str):
383
402
timezone = value .get (TimeValues .TIMEZONE .value , "UTC" )
384
403
if not isinstance (timezone , str ):
385
404
raise SchemaValidationError (error_str )
405
+
406
+ # try to see if the timezone string corresponds to any known timezone
386
407
if not tz .gettz (timezone ):
387
408
raise SchemaValidationError (f"'TIMEZONE' value must represent a valid IANA timezone, rule={ rule_name } " )
388
409
@@ -393,17 +414,21 @@ def _validate_schedule_between_time_and_datetime_ranges(
393
414
error_str = f"condition with a '{ action_name } ' action must have a condition value type dictionary with 'START' and 'END' keys, rule={ rule_name } " # noqa: E501
394
415
if not isinstance (value , dict ):
395
416
raise SchemaValidationError (error_str )
417
+
396
418
start_time = value .get (TimeValues .START .value )
397
419
end_time = value .get (TimeValues .END .value )
398
420
if not start_time or not end_time :
399
421
raise SchemaValidationError (error_str )
400
422
if not isinstance (start_time , str ) or not isinstance (end_time , str ):
401
423
raise SchemaValidationError (f"'START' and 'END' must be a non empty string, rule={ rule_name } " )
424
+
402
425
validator (start_time , rule_name )
403
426
validator (end_time , rule_name )
404
427
405
428
timezone = value .get (TimeValues .TIMEZONE .value , "UTC" )
406
429
if not isinstance (timezone , str ):
407
430
raise SchemaValidationError (f"'TIMEZONE' must be a string, rule={ rule_name } " )
431
+
432
+ # try to see if the timezone string corresponds to any known timezone
408
433
if not tz .gettz (timezone ):
409
434
raise SchemaValidationError (f"'TIMEZONE' value must represent a valid IANA timezone, rule={ rule_name } " )
0 commit comments