Skip to content

Commit 8735d47

Browse files
committed
Add Modulo Range Condition under Feature Flags
1 parent 1995d0f commit 8735d47

File tree

4 files changed

+62
-16
lines changed

4 files changed

+62
-16
lines changed

aws_lambda_powertools/utilities/feature_flags/time_conditions.py renamed to aws_lambda_powertools/utilities/feature_flags/comparators.py

+24-13
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from datetime import datetime, tzinfo
2-
from typing import Dict, Optional
2+
from typing import Any, Dict, Optional
33

44
from dateutil.tz import gettz
55

6-
from .schema import HOUR_MIN_SEPARATOR, TimeValues
6+
from .schema import HOUR_MIN_SEPARATOR, ModuloRangeValues, TimeValues
77

88

99
def _get_now_from_timezone(timezone: Optional[tzinfo]) -> datetime:
@@ -16,23 +16,23 @@ def _get_now_from_timezone(timezone: Optional[tzinfo]) -> datetime:
1616
return datetime.now(timezone)
1717

1818

19-
def compare_days_of_week(action: str, values: Dict) -> bool:
20-
timezone_name = values.get(TimeValues.TIMEZONE.value, "UTC")
19+
def compare_days_of_week(context_value: Any, condition_value: Dict) -> bool:
20+
timezone_name = condition_value.get(TimeValues.TIMEZONE.value, "UTC")
2121

2222
# %A = Weekday as locale’s full name.
2323
current_day = _get_now_from_timezone(gettz(timezone_name)).strftime("%A").upper()
2424

25-
days = values.get(TimeValues.DAYS.value, [])
25+
days = condition_value.get(TimeValues.DAYS.value, [])
2626
return current_day in days
2727

2828

29-
def compare_datetime_range(action: str, values: Dict) -> bool:
30-
timezone_name = values.get(TimeValues.TIMEZONE.value, "UTC")
29+
def compare_datetime_range(context_value: Any, condition_value: Dict) -> bool:
30+
timezone_name = condition_value.get(TimeValues.TIMEZONE.value, "UTC")
3131
timezone = gettz(timezone_name)
3232
current_time: datetime = _get_now_from_timezone(timezone)
3333

34-
start_date_str = values.get(TimeValues.START.value, "")
35-
end_date_str = values.get(TimeValues.END.value, "")
34+
start_date_str = condition_value.get(TimeValues.START.value, "")
35+
end_date_str = condition_value.get(TimeValues.END.value, "")
3636

3737
# Since start_date and end_date doesn't include timezone information, we mark the timestamp
3838
# with the same timezone as the current_time. This way all the 3 timestamps will be on
@@ -42,12 +42,12 @@ def compare_datetime_range(action: str, values: Dict) -> bool:
4242
return start_date <= current_time <= end_date
4343

4444

45-
def compare_time_range(action: str, values: Dict) -> bool:
46-
timezone_name = values.get(TimeValues.TIMEZONE.value, "UTC")
45+
def compare_time_range(context_value: Any, condition_value: Dict) -> bool:
46+
timezone_name = condition_value.get(TimeValues.TIMEZONE.value, "UTC")
4747
current_time: datetime = _get_now_from_timezone(gettz(timezone_name))
4848

49-
start_hour, start_min = values.get(TimeValues.START.value, "").split(HOUR_MIN_SEPARATOR)
50-
end_hour, end_min = values.get(TimeValues.END.value, "").split(HOUR_MIN_SEPARATOR)
49+
start_hour, start_min = condition_value.get(TimeValues.START.value, "").split(HOUR_MIN_SEPARATOR)
50+
end_hour, end_min = condition_value.get(TimeValues.END.value, "").split(HOUR_MIN_SEPARATOR)
5151

5252
start_time = current_time.replace(hour=int(start_hour), minute=int(start_min))
5353
end_time = current_time.replace(hour=int(end_hour), minute=int(end_min))
@@ -71,3 +71,14 @@ def compare_time_range(action: str, values: Dict) -> bool:
7171
else:
7272
# In normal circumstances, we need to assert **both** conditions
7373
return start_time <= current_time <= end_time
74+
75+
76+
def compare_modulo_range(context_value: int, condition_value: Dict) -> bool:
77+
"""
78+
Returns for a given context 'a' and modulo condition 'b' -> b.start <= a % b.base <= b.end
79+
"""
80+
base = condition_value.get(ModuloRangeValues.BASE.value, 1)
81+
start = condition_value.get(ModuloRangeValues.START.value, 1)
82+
end = condition_value.get(ModuloRangeValues.END.value, 1)
83+
84+
return start <= context_value % base <= end

aws_lambda_powertools/utilities/feature_flags/feature_flags.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
from ...shared.types import JSONType
66
from . import schema
77
from .base import StoreProvider
8-
from .exceptions import ConfigurationStoreError
9-
from .time_conditions import (
8+
from .comparators import (
109
compare_datetime_range,
1110
compare_days_of_week,
11+
compare_modulo_range,
1212
compare_time_range,
1313
)
14+
from .exceptions import ConfigurationStoreError
1415

1516

1617
class FeatureFlags:
@@ -65,6 +66,7 @@ def _match_by_action(self, action: str, condition_value: Any, context_value: Any
6566
schema.RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value: lambda a, b: compare_time_range(a, b),
6667
schema.RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value: lambda a, b: compare_datetime_range(a, b),
6768
schema.RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value: lambda a, b: compare_days_of_week(a, b),
69+
schema.RuleAction.MODULO_RANGE.value: lambda a, b: lambda a, b: compare_modulo_range(a, b),
6870
}
6971

7072
try:

aws_lambda_powertools/utilities/feature_flags/schema.py

+33
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class RuleAction(str, Enum):
4141
SCHEDULE_BETWEEN_TIME_RANGE = "SCHEDULE_BETWEEN_TIME_RANGE" # hour:min 24 hours clock
4242
SCHEDULE_BETWEEN_DATETIME_RANGE = "SCHEDULE_BETWEEN_DATETIME_RANGE" # full datetime format, excluding timezone
4343
SCHEDULE_BETWEEN_DAYS_OF_WEEK = "SCHEDULE_BETWEEN_DAYS_OF_WEEK" # MONDAY, TUESDAY, .... see TimeValues enum
44+
MODULO_RANGE = "MODULO_RANGE"
4445

4546

4647
class TimeKeys(Enum):
@@ -71,6 +72,16 @@ class TimeValues(Enum):
7172
SATURDAY = "SATURDAY"
7273

7374

75+
class ModuloRangeValues(Enum):
76+
"""
77+
Possible values when using modulo range rule
78+
"""
79+
80+
BASE = "BASE"
81+
START = "START"
82+
END = "END"
83+
84+
7485
class SchemaValidator(BaseValidator):
7586
"""Validates feature flag schema configuration
7687
@@ -341,6 +352,9 @@ def validate_condition_value(condition: Dict[str, Any], rule_name: str):
341352
)
342353
elif action == RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value:
343354
ConditionsValidator._validate_schedule_between_days_of_week(value, rule_name)
355+
# modulo range condition needs validation on base, start, and end attributes
356+
elif action == RuleAction.MODULO_RANGE.value:
357+
ConditionsValidator._validate_modulo_range(value, rule_name)
344358

345359
@staticmethod
346360
def _validate_datetime_value(datetime_str: str, rule_name: str):
@@ -434,3 +448,22 @@ def _validate_schedule_between_time_and_datetime_ranges(
434448
# try to see if the timezone string corresponds to any known timezone
435449
if not tz.gettz(timezone):
436450
raise SchemaValidationError(f"'TIMEZONE' value must represent a valid IANA timezone, rule={rule_name}")
451+
452+
@staticmethod
453+
def _validate_modulo_range(value: Any, rule_name: str):
454+
error_str = f"condition with a 'MODULO_RANGE' action must have a condition value type dictionary with 'BASE', 'START' and 'END' keys, rule={rule_name}" # noqa: E501
455+
if not isinstance(value, dict):
456+
raise SchemaValidationError(error_str)
457+
458+
base = value.get(ModuloRangeValues.BASE.value)
459+
start = value.get(ModuloRangeValues.START.value)
460+
end = value.get(ModuloRangeValues.END.value)
461+
if base is None or start is None or end is None:
462+
raise SchemaValidationError(error_str)
463+
if not isinstance(base, int) or not isinstance(start, int) or not isinstance(end, int):
464+
raise SchemaValidationError(f"'BASE', 'START' and 'END' must be integers, rule={rule_name}")
465+
466+
if not 0 <= start <= end <= base - 1:
467+
raise SchemaValidationError(
468+
f"condition with 'MODULO_RANGE' action must satisfy 0 <= START <= END <= BASE-1, rule={rule_name}"
469+
)

tests/functional/feature_flags/test_time_based_actions.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def evaluate_mocked_schema(
3636

3737
# Mock the current time
3838
year, month, day, hour, minute, second, timezone = mocked_time
39-
time = mocker.patch("aws_lambda_powertools.utilities.feature_flags.time_conditions._get_now_from_timezone")
39+
time = mocker.patch("aws_lambda_powertools.utilities.feature_flags.comparators._get_now_from_timezone")
4040
time.return_value = datetime.datetime(
4141
year=year, month=month, day=day, hour=hour, minute=minute, second=second, microsecond=0, tzinfo=timezone
4242
)

0 commit comments

Comments
 (0)