From 930e853bfda8db44a4447424f550fa910e60fd3d Mon Sep 17 00:00:00 2001 From: Ran Isenberg Date: Fri, 28 Oct 2022 09:56:48 +0300 Subject: [PATCH 01/34] feature: time conditions --- .../utilities/feature_flags/feature_flags.py | 11 +- .../utilities/feature_flags/schema.py | 20 ++ .../feature_flags/time_conditions.py | 46 ++++ .../feature_flags/test_time_based_actions.py | 248 ++++++++++++++++++ 4 files changed, 323 insertions(+), 2 deletions(-) create mode 100644 aws_lambda_powertools/utilities/feature_flags/time_conditions.py create mode 100644 tests/functional/feature_flags/test_time_based_actions.py diff --git a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py index 36a74c4c58a..f37921259a5 100644 --- a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py +++ b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py @@ -6,6 +6,7 @@ from . import schema from .base import StoreProvider from .exceptions import ConfigurationStoreError +from .time_conditions import time_range_compare, time_selected_days_compare class FeatureFlags: @@ -59,6 +60,8 @@ def _match_by_action(self, action: str, condition_value: Any, context_value: Any schema.RuleAction.KEY_NOT_IN_VALUE.value: lambda a, b: a not in b, schema.RuleAction.VALUE_IN_KEY.value: lambda a, b: b in a, schema.RuleAction.VALUE_NOT_IN_KEY.value: lambda a, b: b not in a, + schema.RuleAction.TIME_RANGE.value: lambda a, b: time_range_compare(a, b), + schema.RuleAction.TIME_SELECTED_DAYS.value: lambda a, b: time_selected_days_compare(a, b), } try: @@ -87,6 +90,10 @@ def _evaluate_conditions( cond_action = condition.get(schema.CONDITION_ACTION, "") cond_value = condition.get(schema.CONDITION_VALUE) + # rule based actions have no user context. the context is the condition key + if cond_action == schema.RuleAction.TIME_RANGE.value: + context_value = condition.get(schema.CONDITION_KEY) + if not self._match_by_action(action=cond_action, condition_value=cond_value, context_value=context_value): self.logger.debug( f"rule did not match action, rule_name={rule_name}, rule_value={rule_match_value}, " @@ -228,7 +235,7 @@ def evaluate(self, *, name: str, context: Optional[Dict[str, Any]] = None, defau # method `get_matching_features` returning Dict[feature_name, feature_value] boolean_feature = feature.get( schema.FEATURE_DEFAULT_VAL_TYPE_KEY, True - ) # backwards compatability ,assume feature flag + ) # backwards compatibility ,assume feature flag if not rules: self.logger.debug( f"no rules found, returning feature default, name={name}, default={str(feat_default)}, boolean_feature={boolean_feature}" # noqa: E501 @@ -287,7 +294,7 @@ def get_enabled_features(self, *, context: Optional[Dict[str, Any]] = None) -> L feature_default_value = feature.get(schema.FEATURE_DEFAULT_VAL_KEY) boolean_feature = feature.get( schema.FEATURE_DEFAULT_VAL_TYPE_KEY, True - ) # backwards compatability ,assume feature flag + ) # backwards compatibility ,assume feature flag if feature_default_value and not rules: self.logger.debug(f"feature is enabled by default and has no defined rules, name={name}") diff --git a/aws_lambda_powertools/utilities/feature_flags/schema.py b/aws_lambda_powertools/utilities/feature_flags/schema.py index 2fa3140b15e..abd78ae543b 100644 --- a/aws_lambda_powertools/utilities/feature_flags/schema.py +++ b/aws_lambda_powertools/utilities/feature_flags/schema.py @@ -31,6 +31,26 @@ class RuleAction(str, Enum): KEY_NOT_IN_VALUE = "KEY_NOT_IN_VALUE" VALUE_IN_KEY = "VALUE_IN_KEY" VALUE_NOT_IN_KEY = "VALUE_NOT_IN_KEY" + TIME_RANGE = "TIME_RANGE" # 24 hours clock UTC time + TIME_SELECTED_DAYS = "TIME_SELECTED_DAYS" + + +class TimeKeys(str, Enum): + CURRENT_HOUR_UTC = "CURRENT_HOUR_UTC" + CURRENT_DAY_UTC = "CURRENT_DAY_UTC" + CURRENT_TIME_UTC = "CURRENT_TIME_UTC" + + +class TimeValues(str, Enum): + START_TIME = "START_TIME" + END_TIME = "END_TIME" + SUNDAY = "SUNDAY" + MONDAY = "MONDAY" + TUESDAY = "TUESDAY" + WEDNESDAY = "WEDNESDAY" + THURSDAY = "THURSDAY" + FRIDAY = "FRIDAY" + SATURDAY = "SATURDAY" class SchemaValidator(BaseValidator): diff --git a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py new file mode 100644 index 00000000000..f8f24224712 --- /dev/null +++ b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py @@ -0,0 +1,46 @@ +from datetime import datetime, timezone +from typing import Dict + +from .schema import TimeKeys, TimeValues + +HOUR_MIN_SEPARATOR = ":" + + +def time_range_compare(action: str, values: Dict) -> bool: + if action == TimeKeys.CURRENT_TIME_UTC.value: + return _time_range_compare_current_time_utc(action, values) + elif action == TimeKeys.CURRENT_HOUR_UTC.value: + return _time_range_compare_current_time_utc(action, values) + # we assume it passed validation right? so no need to raise an error + return False + + +def time_selected_days_compare(action: str, values: Dict) -> bool: + if action == TimeKeys.CURRENT_DAY_UTC.value: + return _time_selected_days_current_days_compare(action, values) + # we assume it passed validation right? so no need to raise an error + return False + + +def _time_selected_days_current_days_compare(action: str, values: Dict) -> bool: + # implement here + return True + + +def _time_range_compare_current_time_utc(action: str, values: Dict) -> bool: + current_time_utc: datetime = datetime.now(timezone.utc) + start_date = datetime.strptime(values.get(TimeValues.START_TIME, ""), "%Y-%m-%dT%H:%M:%S%z") + end_date = datetime.strptime(values.get(TimeValues.END_TIME, ""), "%Y-%m-%dT%H:%M:%S%z") + return current_time_utc >= start_date and current_time_utc <= end_date + + +def _time_range_compare_current_hour_utc(action: str, values: Dict) -> bool: + current_time_utc: datetime = datetime.now(timezone.utc) + start_hour, start_min = values.get(TimeValues.START_TIME, HOUR_MIN_SEPARATOR).split(HOUR_MIN_SEPARATOR) + end_hour, end_min = values.get(TimeValues.END_TIME, HOUR_MIN_SEPARATOR).split(HOUR_MIN_SEPARATOR) + return ( + current_time_utc.hour >= int(start_hour) + and current_time_utc.hour <= int(end_hour) + and current_time_utc.minute >= int(start_min) + and current_time_utc.minute <= int(end_min) + ) diff --git a/tests/functional/feature_flags/test_time_based_actions.py b/tests/functional/feature_flags/test_time_based_actions.py new file mode 100644 index 00000000000..9ecf26b7f76 --- /dev/null +++ b/tests/functional/feature_flags/test_time_based_actions.py @@ -0,0 +1,248 @@ +from typing import Dict, Optional + +import pytest +from botocore.config import Config + +from aws_lambda_powertools.utilities.feature_flags.appconfig import AppConfigStore +from aws_lambda_powertools.utilities.feature_flags.feature_flags import FeatureFlags +from aws_lambda_powertools.utilities.feature_flags.schema import ( + CONDITION_ACTION, + CONDITION_KEY, + CONDITION_VALUE, + CONDITIONS_KEY, + FEATURE_DEFAULT_VAL_KEY, + RULE_MATCH_VALUE, + RULES_KEY, + RuleAction, + TimeKeys, + TimeValues, +) + + +@pytest.fixture(scope="module") +def config(): + return Config(region_name="us-east-1") + + +def init_feature_flags( + mocker, mock_schema: Dict, config: Config, envelope: str = "", jmespath_options: Optional[Dict] = None +) -> FeatureFlags: + mocked_get_conf = mocker.patch("aws_lambda_powertools.utilities.parameters.AppConfigProvider.get") + mocked_get_conf.return_value = mock_schema + + app_conf_fetcher = AppConfigStore( + environment="test_env", + application="test_app", + name="test_conf_name", + max_age=600, + sdk_config=config, + envelope=envelope, + jmespath_options=jmespath_options, + ) + feature_flags: FeatureFlags = FeatureFlags(store=app_conf_fetcher) + return feature_flags + + +def init_fetcher_side_effect(mocker, config: Config, side_effect) -> AppConfigStore: + mocked_get_conf = mocker.patch("aws_lambda_powertools.utilities.parameters.AppConfigProvider.get") + mocked_get_conf.side_effect = side_effect + return AppConfigStore( + environment="env", + application="application", + name="conf", + max_age=1, + sdk_config=config, + ) + + +def test_time_based_utc_in_between_time_range_rule_match(mocker, config): + expected_value = True + mocked_app_config_schema = { + "my_feature": { + FEATURE_DEFAULT_VAL_KEY: False, + RULES_KEY: { + "lambda time is between UTC 11:11-23:59": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + CONDITION_VALUE: {TimeValues.START_TIME.value: "11:11", TimeValues.END_TIME.value: "23:59"}, + }, + ], + } + }, + } + } + # mock time for rule match + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={}, + default=False, + ) + assert toggle == expected_value + + +def test_time_based_utc_in_between_full_time_range_rule_match(mocker, config): + expected_value = True + mocked_app_config_schema = { + "my_feature": { + FEATURE_DEFAULT_VAL_KEY: False, + RULES_KEY: { + "lambda time is between UTC october 5th 2022 12:14:32PM to october 10th 2022 12:15:00 PM": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_VALUE: { + TimeValues.START_TIME.value: "2022-10-05T12:15:00Z", + TimeValues.END_TIME.value: "2022-10-10T12:15:00Z", + }, + }, + ], + } + }, + } + } + # mock time for rule match + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={}, + default=False, + ) + assert toggle == expected_value + + +def test_time_based_multiple_conditions_utc_in_between_time_range_rule_match(mocker, config): + expected_value = True + mocked_app_config_schema = { + "my_feature": { + FEATURE_DEFAULT_VAL_KEY: False, + RULES_KEY: { + "lambda time is between UTC 09:00-17:00 and username is ran": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC, + CONDITION_VALUE: {TimeValues.START_TIME: "09:00", TimeValues.END_TIME: "17:00"}, + }, + { + CONDITION_ACTION: RuleAction.EQUALS.value, + CONDITION_KEY: "tenant", + CONDITION_VALUE: {"username": "ran"}, + }, + ], + } + }, + } + } + # mock time for rule match + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={"username": "ran"}, + default=False, + ) + assert toggle == expected_value + + +def test_time_based_utc_days_range_rule_match(mocker, config): + expected_value = True + mocked_app_config_schema = { + "my_feature": { + FEATURE_DEFAULT_VAL_KEY: False, + RULES_KEY: { + "match only monday through friday": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.TIME_SELECTED_DAYS.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC, # similar to "IN" actions + CONDITION_VALUE: [ + TimeValues.MONDAY, + TimeValues.TUESDAY, + TimeValues.WEDNESDAY, + TimeValues.THURSDAY, + TimeValues.FRIDAY, + ], + }, + ], + } + }, + } + } + # mock time for rule match + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={}, + default=False, + ) + assert toggle == expected_value + + +def test_time_based_utc_only_weekend_rule_match(mocker, config): + expected_value = True + mocked_app_config_schema = { + "my_feature": { + FEATURE_DEFAULT_VAL_KEY: False, + RULES_KEY: { + "match only on weekend": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.TIME_SELECTED_DAYS.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC, # similar to "IN" actions + CONDITION_VALUE: [TimeValues.SATURDAY, TimeValues.SUNDAY], + }, + ], + } + }, + } + } + # mock time for rule match + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={}, + default=False, + ) + assert toggle == expected_value + + +def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_rule_match(mocker, config): + expected_value = True + mocked_app_config_schema = { + "my_feature": { + FEATURE_DEFAULT_VAL_KEY: False, + RULES_KEY: { + "match when lambda time is between UTC 11:00-23:00 and day is either monday or thursday": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC, + CONDITION_VALUE: {TimeValues.START_TIME: "11:00", TimeValues.END_TIME: "23:00"}, + }, + { + CONDITION_ACTION: RuleAction.TIME_SELECTED_DAYS.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC, + CONDITION_VALUE: [TimeValues.MONDAY, TimeValues.THURSDAY], + }, + ], + } + }, + } + } + # mock time for rule match + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={}, + default=False, + ) + assert toggle == expected_value From 65ac67fd6cfedd4eefcded3cb8e89e2c5b64e3e2 Mon Sep 17 00:00:00 2001 From: Ran Isenberg Date: Fri, 28 Oct 2022 10:11:19 +0300 Subject: [PATCH 02/34] feature: time conditions --- .../utilities/feature_flags/feature_flags.py | 2 +- .../feature_flags/time_conditions.py | 29 ++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py index f37921259a5..93e6dfd0fb2 100644 --- a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py +++ b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py @@ -91,7 +91,7 @@ def _evaluate_conditions( cond_value = condition.get(schema.CONDITION_VALUE) # rule based actions have no user context. the context is the condition key - if cond_action == schema.RuleAction.TIME_RANGE.value: + if cond_action == schema.RuleAction.TIME_RANGE.value or schema.RuleAction.TIME_SELECTED_DAYS: context_value = condition.get(schema.CONDITION_KEY) if not self._match_by_action(action=cond_action, condition_value=cond_value, context_value=context_value): diff --git a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py index f8f24224712..84c41ea2049 100644 --- a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py +++ b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py @@ -1,40 +1,49 @@ from datetime import datetime, timezone -from typing import Dict +from typing import Dict, List from .schema import TimeKeys, TimeValues HOUR_MIN_SEPARATOR = ":" +DAY_MAPPING = { + 1: TimeValues.MONDAY, + 2: TimeValues.TUESDAY, + 3: TimeValues.WEDNESDAY, + 4: TimeValues.THURSDAY, + 5: TimeValues.FRIDAY, + 6: TimeValues.SATURDAY, + 7: TimeValues.SUNDAY, +} def time_range_compare(action: str, values: Dict) -> bool: if action == TimeKeys.CURRENT_TIME_UTC.value: - return _time_range_compare_current_time_utc(action, values) + return _time_range_compare_current_time_utc(values) elif action == TimeKeys.CURRENT_HOUR_UTC.value: - return _time_range_compare_current_time_utc(action, values) + return _time_range_compare_current_time_utc(values) # we assume it passed validation right? so no need to raise an error return False -def time_selected_days_compare(action: str, values: Dict) -> bool: +def time_selected_days_compare(action: str, values: List[str]) -> bool: if action == TimeKeys.CURRENT_DAY_UTC.value: - return _time_selected_days_current_days_compare(action, values) + return _time_selected_days_current_days_compare(values) # we assume it passed validation right? so no need to raise an error return False -def _time_selected_days_current_days_compare(action: str, values: Dict) -> bool: - # implement here - return True +def _time_selected_days_current_days_compare(values: List[str]) -> bool: + current_day_number: datetime = datetime.now(timezone.utc).isoweekday() + return DAY_MAPPING.get(current_day_number, "") in values -def _time_range_compare_current_time_utc(action: str, values: Dict) -> bool: +def _time_range_compare_current_time_utc(values: Dict) -> bool: current_time_utc: datetime = datetime.now(timezone.utc) start_date = datetime.strptime(values.get(TimeValues.START_TIME, ""), "%Y-%m-%dT%H:%M:%S%z") end_date = datetime.strptime(values.get(TimeValues.END_TIME, ""), "%Y-%m-%dT%H:%M:%S%z") return current_time_utc >= start_date and current_time_utc <= end_date -def _time_range_compare_current_hour_utc(action: str, values: Dict) -> bool: +def _time_range_compare_current_hour_utc(values: Dict) -> bool: current_time_utc: datetime = datetime.now(timezone.utc) start_hour, start_min = values.get(TimeValues.START_TIME, HOUR_MIN_SEPARATOR).split(HOUR_MIN_SEPARATOR) end_hour, end_min = values.get(TimeValues.END_TIME, HOUR_MIN_SEPARATOR).split(HOUR_MIN_SEPARATOR) From 5b41a6272a701a4640f06fdabf91252f480a85ea Mon Sep 17 00:00:00 2001 From: Ran Isenberg <60175085+ran-isenberg@users.noreply.github.com> Date: Tue, 1 Nov 2022 15:26:32 +0200 Subject: [PATCH 03/34] Update aws_lambda_powertools/utilities/feature_flags/time_conditions.py Co-authored-by: Leandro Damascena Signed-off-by: Ran Isenberg <60175085+ran-isenberg@users.noreply.github.com> --- .../utilities/feature_flags/time_conditions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py index 84c41ea2049..b13d48c5a24 100644 --- a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py +++ b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py @@ -32,8 +32,8 @@ def time_selected_days_compare(action: str, values: List[str]) -> bool: def _time_selected_days_current_days_compare(values: List[str]) -> bool: - current_day_number: datetime = datetime.now(timezone.utc).isoweekday() - return DAY_MAPPING.get(current_day_number, "") in values + current_day: datetime = datetime.now(timezone.utc).strftime('%A').lower() + return current_day in values def _time_range_compare_current_time_utc(values: Dict) -> bool: From 4042651c30a2f0b0a2b85fd703d0950310322f33 Mon Sep 17 00:00:00 2001 From: Ran Isenberg <60175085+ran-isenberg@users.noreply.github.com> Date: Tue, 1 Nov 2022 15:26:37 +0200 Subject: [PATCH 04/34] Update aws_lambda_powertools/utilities/feature_flags/time_conditions.py Co-authored-by: Leandro Damascena Signed-off-by: Ran Isenberg <60175085+ran-isenberg@users.noreply.github.com> --- .../utilities/feature_flags/time_conditions.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py index b13d48c5a24..f2fb876b2a3 100644 --- a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py +++ b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py @@ -4,15 +4,6 @@ from .schema import TimeKeys, TimeValues HOUR_MIN_SEPARATOR = ":" -DAY_MAPPING = { - 1: TimeValues.MONDAY, - 2: TimeValues.TUESDAY, - 3: TimeValues.WEDNESDAY, - 4: TimeValues.THURSDAY, - 5: TimeValues.FRIDAY, - 6: TimeValues.SATURDAY, - 7: TimeValues.SUNDAY, -} def time_range_compare(action: str, values: Dict) -> bool: From 623d52757f26aadff18ae827e328524a612b73cf Mon Sep 17 00:00:00 2001 From: Ran Isenberg Date: Fri, 18 Nov 2022 09:40:56 +0200 Subject: [PATCH 05/34] fixed all tests, added tests --- .../utilities/feature_flags/feature_flags.py | 2 +- .../feature_flags/time_conditions.py | 25 +++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py index 93e6dfd0fb2..8087ed55e61 100644 --- a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py +++ b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py @@ -91,7 +91,7 @@ def _evaluate_conditions( cond_value = condition.get(schema.CONDITION_VALUE) # rule based actions have no user context. the context is the condition key - if cond_action == schema.RuleAction.TIME_RANGE.value or schema.RuleAction.TIME_SELECTED_DAYS: + if cond_action in [schema.RuleAction.TIME_RANGE.value, schema.RuleAction.TIME_SELECTED_DAYS.value]: context_value = condition.get(schema.CONDITION_KEY) if not self._match_by_action(action=cond_action, condition_value=cond_value, context_value=context_value): diff --git a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py index f2fb876b2a3..74e8bfb86d1 100644 --- a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py +++ b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py @@ -6,11 +6,22 @@ HOUR_MIN_SEPARATOR = ":" +DAY_MAPPING = { + 1: TimeValues.MONDAY.value, + 2: TimeValues.TUESDAY.value, + 3: TimeValues.WEDNESDAY.value, + 4: TimeValues.THURSDAY.value, + 5: TimeValues.FRIDAY.value, + 6: TimeValues.SATURDAY.value, + 7: TimeValues.SUNDAY.value, +} + + def time_range_compare(action: str, values: Dict) -> bool: if action == TimeKeys.CURRENT_TIME_UTC.value: return _time_range_compare_current_time_utc(values) elif action == TimeKeys.CURRENT_HOUR_UTC.value: - return _time_range_compare_current_time_utc(values) + return _time_range_compare_current_hour_utc(values) # we assume it passed validation right? so no need to raise an error return False @@ -22,20 +33,24 @@ def time_selected_days_compare(action: str, values: List[str]) -> bool: return False +def _get_utc_time_now() -> datetime: + return datetime.now(timezone.utc) + + def _time_selected_days_current_days_compare(values: List[str]) -> bool: - current_day: datetime = datetime.now(timezone.utc).strftime('%A').lower() - return current_day in values + current_day_number = _get_utc_time_now().isoweekday() + return DAY_MAPPING.get(current_day_number, "") in values def _time_range_compare_current_time_utc(values: Dict) -> bool: - current_time_utc: datetime = datetime.now(timezone.utc) + current_time_utc: datetime = _get_utc_time_now() start_date = datetime.strptime(values.get(TimeValues.START_TIME, ""), "%Y-%m-%dT%H:%M:%S%z") end_date = datetime.strptime(values.get(TimeValues.END_TIME, ""), "%Y-%m-%dT%H:%M:%S%z") return current_time_utc >= start_date and current_time_utc <= end_date def _time_range_compare_current_hour_utc(values: Dict) -> bool: - current_time_utc: datetime = datetime.now(timezone.utc) + current_time_utc: datetime = _get_utc_time_now() start_hour, start_min = values.get(TimeValues.START_TIME, HOUR_MIN_SEPARATOR).split(HOUR_MIN_SEPARATOR) end_hour, end_min = values.get(TimeValues.END_TIME, HOUR_MIN_SEPARATOR).split(HOUR_MIN_SEPARATOR) return ( From 76489d6819b349df7e6f3f04050a121af64b6550 Mon Sep 17 00:00:00 2001 From: Ran Isenberg Date: Fri, 18 Nov 2022 09:43:17 +0200 Subject: [PATCH 06/34] fixed all tests, added tests --- .../feature_flags/test_time_based_actions.py | 253 +++++++++++++++++- 1 file changed, 251 insertions(+), 2 deletions(-) diff --git a/tests/functional/feature_flags/test_time_based_actions.py b/tests/functional/feature_flags/test_time_based_actions.py index 9ecf26b7f76..ef4873232cc 100644 --- a/tests/functional/feature_flags/test_time_based_actions.py +++ b/tests/functional/feature_flags/test_time_based_actions.py @@ -1,7 +1,9 @@ +import datetime from typing import Dict, Optional import pytest from botocore.config import Config +from dateutil import tz from aws_lambda_powertools.utilities.feature_flags.appconfig import AppConfigStore from aws_lambda_powertools.utilities.feature_flags.feature_flags import FeatureFlags @@ -55,6 +57,20 @@ def init_fetcher_side_effect(mocker, config: Config, side_effect) -> AppConfigSt ) +def mock_current_utc_time( + mocker, + year: int, + month: int, + day: int, + hour: int, + minute: int, + second: int, + msec: int, +) -> None: + mocked_time = mocker.patch("aws_lambda_powertools.utilities.feature_flags.time_conditions._get_utc_time_now") + mocked_time.return_value = datetime.datetime(year, month, day, hour, minute, second, msec, tz.gettz("UTC")) + + def test_time_based_utc_in_between_time_range_rule_match(mocker, config): expected_value = True mocked_app_config_schema = { @@ -74,6 +90,8 @@ def test_time_based_utc_in_between_time_range_rule_match(mocker, config): }, } } + mock_current_utc_time(mocker, 2022, 2, 15, 11, 12, 0, 0) # will rule match + # mock time for rule match feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) toggle = feature_flags.evaluate( @@ -81,6 +99,38 @@ def test_time_based_utc_in_between_time_range_rule_match(mocker, config): context={}, default=False, ) + + assert toggle == expected_value + + +def test_time_based_utc_in_between_time_range_no_rule_match(mocker, config): + expected_value = False + mocked_app_config_schema = { + "my_feature": { + FEATURE_DEFAULT_VAL_KEY: False, + RULES_KEY: { + "lambda time is between UTC 11:11-23:59": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + CONDITION_VALUE: {TimeValues.START_TIME.value: "11:11", TimeValues.END_TIME.value: "23:59"}, + }, + ], + } + }, + } + } + mock_current_utc_time(mocker, 2022, 2, 15, 7, 12, 0, 0) # no rule match 7:12 am + + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={}, + default=False, + ) + assert toggle == expected_value @@ -106,6 +156,9 @@ def test_time_based_utc_in_between_full_time_range_rule_match(mocker, config): }, } } + + mock_current_utc_time(mocker, 2022, 10, 7, 10, 0, 0, 0) # will rule match + # mock time for rule match feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) toggle = feature_flags.evaluate( @@ -116,6 +169,40 @@ def test_time_based_utc_in_between_full_time_range_rule_match(mocker, config): assert toggle == expected_value +def test_time_based_utc_in_between_full_time_range_no_rule_match(mocker, config): + expected_value = False + mocked_app_config_schema = { + "my_feature": { + FEATURE_DEFAULT_VAL_KEY: False, + RULES_KEY: { + "lambda time is between UTC october 5th 2022 12:14:32PM to october 10th 2022 12:15:00 PM": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_VALUE: { + TimeValues.START_TIME.value: "2022-10-05T12:15:00Z", + TimeValues.END_TIME.value: "2022-10-10T12:15:00Z", + }, + }, + ], + } + }, + } + } + + mock_current_utc_time(mocker, 2022, 9, 7, 10, 0, 0, 0) # will not rule match + + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={}, + default=False, + ) + assert toggle == expected_value + + def test_time_based_multiple_conditions_utc_in_between_time_range_rule_match(mocker, config): expected_value = True mocked_app_config_schema = { @@ -132,14 +219,54 @@ def test_time_based_multiple_conditions_utc_in_between_time_range_rule_match(moc }, { CONDITION_ACTION: RuleAction.EQUALS.value, - CONDITION_KEY: "tenant", - CONDITION_VALUE: {"username": "ran"}, + CONDITION_KEY: "username", + CONDITION_VALUE: "ran", }, ], } }, } } + + mock_current_utc_time(mocker, 2022, 10, 7, 10, 0, 0, 0) # will rule match + + # mock time for rule match + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={"username": "ran"}, + default=False, + ) + assert toggle == expected_value + + +def test_time_based_multiple_conditions_utc_in_between_time_range_no_rule_match(mocker, config): + expected_value = False + mocked_app_config_schema = { + "my_feature": { + FEATURE_DEFAULT_VAL_KEY: False, + RULES_KEY: { + "lambda time is between UTC 09:00-17:00 and username is ran": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC, + CONDITION_VALUE: {TimeValues.START_TIME: "09:00", TimeValues.END_TIME: "17:00"}, + }, + { + CONDITION_ACTION: RuleAction.EQUALS.value, + CONDITION_KEY: "username", + CONDITION_VALUE: "ran", + }, + ], + } + }, + } + } + + mock_current_utc_time(mocker, 2022, 10, 7, 7, 0, 0, 0) # will cause no rule match, 7:00 + # mock time for rule match feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) toggle = feature_flags.evaluate( @@ -176,6 +303,42 @@ def test_time_based_utc_days_range_rule_match(mocker, config): } } # mock time for rule match + mock_current_utc_time(mocker, 2022, 11, 18, 10, 0, 0, 0) # friday + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={}, + default=False, + ) + assert toggle == expected_value + + +def test_time_based_utc_days_range_no_rule_match(mocker, config): + expected_value = False + mocked_app_config_schema = { + "my_feature": { + FEATURE_DEFAULT_VAL_KEY: False, + RULES_KEY: { + "match only monday through friday": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.TIME_SELECTED_DAYS.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC, # similar to "IN" actions + CONDITION_VALUE: [ + TimeValues.MONDAY, + TimeValues.TUESDAY, + TimeValues.WEDNESDAY, + TimeValues.THURSDAY, + TimeValues.FRIDAY, + ], + }, + ], + } + }, + } + } + mock_current_utc_time(mocker, 2022, 11, 20, 10, 0, 0, 0) # sunday, no match feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) toggle = feature_flags.evaluate( name="my_feature", @@ -205,6 +368,36 @@ def test_time_based_utc_only_weekend_rule_match(mocker, config): } } # mock time for rule match + mock_current_utc_time(mocker, 2022, 11, 19, 10, 0, 0, 0) # saturday + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={}, + default=False, + ) + assert toggle == expected_value + + +def test_time_based_utc_only_weekend_no_rule_match(mocker, config): + expected_value = False + mocked_app_config_schema = { + "my_feature": { + FEATURE_DEFAULT_VAL_KEY: False, + RULES_KEY: { + "match only on weekend": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.TIME_SELECTED_DAYS.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC, # similar to "IN" actions + CONDITION_VALUE: [TimeValues.SATURDAY, TimeValues.SUNDAY], + }, + ], + } + }, + } + } + mock_current_utc_time(mocker, 2022, 11, 18, 10, 0, 0, 0) # friday, no match feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) toggle = feature_flags.evaluate( name="my_feature", @@ -239,6 +432,62 @@ def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_rule_ma } } # mock time for rule match + mock_current_utc_time(mocker, 2022, 11, 17, 16, 0, 0, 0) # thursday 16:00 + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={}, + default=False, + ) + assert toggle == expected_value + + +def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_no_rule_match(mocker, config): + expected_value = False + mocked_app_config_schema = { + "my_feature": { + FEATURE_DEFAULT_VAL_KEY: False, + RULES_KEY: { + "match when lambda time is between UTC 11:00-23:00 and day is either monday or thursday": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC, + CONDITION_VALUE: {TimeValues.START_TIME: "11:00", TimeValues.END_TIME: "23:00"}, + }, + { + CONDITION_ACTION: RuleAction.TIME_SELECTED_DAYS.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC, + CONDITION_VALUE: [TimeValues.MONDAY, TimeValues.THURSDAY], + }, + ], + } + }, + } + } + # first condition fail, second match + mock_current_utc_time(mocker, 2022, 11, 17, 9, 0, 0, 0) # thursday 9:00 + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={}, + default=False, + ) + assert toggle == expected_value + + # second condition fail, first match + mock_current_utc_time(mocker, 2022, 11, 18, 13, 0, 0, 0) # friday 16:00 + feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + toggle = feature_flags.evaluate( + name="my_feature", + context={}, + default=False, + ) + assert toggle == expected_value + + # both conditions fail + mock_current_utc_time(mocker, 2022, 11, 18, 9, 0, 0, 0) # friday 9:00 feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) toggle = feature_flags.evaluate( name="my_feature", From e1f48a53da794bd740ef0d591c20a21d432e2636 Mon Sep 17 00:00:00 2001 From: Ran Isenberg Date: Fri, 18 Nov 2022 10:06:50 +0200 Subject: [PATCH 07/34] even better --- .../feature_flags/time_conditions.py | 15 +----- .../feature_flags/test_time_based_actions.py | 50 +++++++++---------- 2 files changed, 27 insertions(+), 38 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py index 74e8bfb86d1..3b20b80f061 100644 --- a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py +++ b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py @@ -6,17 +6,6 @@ HOUR_MIN_SEPARATOR = ":" -DAY_MAPPING = { - 1: TimeValues.MONDAY.value, - 2: TimeValues.TUESDAY.value, - 3: TimeValues.WEDNESDAY.value, - 4: TimeValues.THURSDAY.value, - 5: TimeValues.FRIDAY.value, - 6: TimeValues.SATURDAY.value, - 7: TimeValues.SUNDAY.value, -} - - def time_range_compare(action: str, values: Dict) -> bool: if action == TimeKeys.CURRENT_TIME_UTC.value: return _time_range_compare_current_time_utc(values) @@ -38,8 +27,8 @@ def _get_utc_time_now() -> datetime: def _time_selected_days_current_days_compare(values: List[str]) -> bool: - current_day_number = _get_utc_time_now().isoweekday() - return DAY_MAPPING.get(current_day_number, "") in values + current_day = _get_utc_time_now().strftime("%A").upper() + return current_day in values def _time_range_compare_current_time_utc(values: Dict) -> bool: diff --git a/tests/functional/feature_flags/test_time_based_actions.py b/tests/functional/feature_flags/test_time_based_actions.py index ef4873232cc..236d0694d3b 100644 --- a/tests/functional/feature_flags/test_time_based_actions.py +++ b/tests/functional/feature_flags/test_time_based_actions.py @@ -214,7 +214,7 @@ def test_time_based_multiple_conditions_utc_in_between_time_range_rule_match(moc CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC, + CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, CONDITION_VALUE: {TimeValues.START_TIME: "09:00", TimeValues.END_TIME: "17:00"}, }, { @@ -251,7 +251,7 @@ def test_time_based_multiple_conditions_utc_in_between_time_range_no_rule_match( CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC, + CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, CONDITION_VALUE: {TimeValues.START_TIME: "09:00", TimeValues.END_TIME: "17:00"}, }, { @@ -288,13 +288,13 @@ def test_time_based_utc_days_range_rule_match(mocker, config): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.TIME_SELECTED_DAYS.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC, # similar to "IN" actions + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions CONDITION_VALUE: [ - TimeValues.MONDAY, - TimeValues.TUESDAY, - TimeValues.WEDNESDAY, - TimeValues.THURSDAY, - TimeValues.FRIDAY, + TimeValues.MONDAY.value, + TimeValues.TUESDAY.value, + TimeValues.WEDNESDAY.value, + TimeValues.THURSDAY.value, + TimeValues.FRIDAY.value, ], }, ], @@ -324,13 +324,13 @@ def test_time_based_utc_days_range_no_rule_match(mocker, config): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.TIME_SELECTED_DAYS.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC, # similar to "IN" actions + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions CONDITION_VALUE: [ - TimeValues.MONDAY, - TimeValues.TUESDAY, - TimeValues.WEDNESDAY, - TimeValues.THURSDAY, - TimeValues.FRIDAY, + TimeValues.MONDAY.value, + TimeValues.TUESDAY.value, + TimeValues.WEDNESDAY.value, + TimeValues.THURSDAY.value, + TimeValues.FRIDAY.value, ], }, ], @@ -359,8 +359,8 @@ def test_time_based_utc_only_weekend_rule_match(mocker, config): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.TIME_SELECTED_DAYS.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC, # similar to "IN" actions - CONDITION_VALUE: [TimeValues.SATURDAY, TimeValues.SUNDAY], + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions + CONDITION_VALUE: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], }, ], } @@ -389,8 +389,8 @@ def test_time_based_utc_only_weekend_no_rule_match(mocker, config): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.TIME_SELECTED_DAYS.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC, # similar to "IN" actions - CONDITION_VALUE: [TimeValues.SATURDAY, TimeValues.SUNDAY], + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions + CONDITION_VALUE: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], }, ], } @@ -418,13 +418,13 @@ def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_rule_ma CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC, - CONDITION_VALUE: {TimeValues.START_TIME: "11:00", TimeValues.END_TIME: "23:00"}, + CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + CONDITION_VALUE: {TimeValues.START_TIME.value: "11:00", TimeValues.END_TIME.value: "23:00"}, }, { CONDITION_ACTION: RuleAction.TIME_SELECTED_DAYS.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC, - CONDITION_VALUE: [TimeValues.MONDAY, TimeValues.THURSDAY], + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, + CONDITION_VALUE: [TimeValues.MONDAY.value, TimeValues.THURSDAY.value], }, ], } @@ -453,13 +453,13 @@ def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_no_rule CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC, + CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, CONDITION_VALUE: {TimeValues.START_TIME: "11:00", TimeValues.END_TIME: "23:00"}, }, { CONDITION_ACTION: RuleAction.TIME_SELECTED_DAYS.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC, - CONDITION_VALUE: [TimeValues.MONDAY, TimeValues.THURSDAY], + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, + CONDITION_VALUE: [TimeValues.MONDAY.value, TimeValues.THURSDAY.value], }, ], } From ab4d1dbb21c08f5b25452f7bbcf07867b1f07549 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 22 Nov 2022 16:15:39 +0100 Subject: [PATCH 08/34] fix(feature_flags): subtle bugs on enum manipulation --- .../utilities/feature_flags/feature_flags.py | 8 ++++---- aws_lambda_powertools/utilities/feature_flags/schema.py | 6 +++--- .../utilities/feature_flags/time_conditions.py | 8 ++++---- tests/functional/feature_flags/test_time_based_actions.py | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py index 8087ed55e61..6202edeb0ca 100644 --- a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py +++ b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py @@ -86,13 +86,13 @@ def _evaluate_conditions( return False for condition in conditions: - context_value = context.get(str(condition.get(schema.CONDITION_KEY))) + context_value = context.get(condition.get(schema.CONDITION_KEY, "")) cond_action = condition.get(schema.CONDITION_ACTION, "") cond_value = condition.get(schema.CONDITION_VALUE) - # rule based actions have no user context. the context is the condition key - if cond_action in [schema.RuleAction.TIME_RANGE.value, schema.RuleAction.TIME_SELECTED_DAYS.value]: - context_value = condition.get(schema.CONDITION_KEY) + # time based rule actions have no user context. the context is the condition key + if cond_action in (schema.RuleAction.TIME_RANGE.value, schema.RuleAction.TIME_SELECTED_DAYS.value): + context_value = condition.get(schema.CONDITION_KEY) # e.g., CURRENT_HOUR_UTC if not self._match_by_action(action=cond_action, condition_value=cond_value, context_value=context_value): self.logger.debug( diff --git a/aws_lambda_powertools/utilities/feature_flags/schema.py b/aws_lambda_powertools/utilities/feature_flags/schema.py index abd78ae543b..ed8f6f7eb38 100644 --- a/aws_lambda_powertools/utilities/feature_flags/schema.py +++ b/aws_lambda_powertools/utilities/feature_flags/schema.py @@ -16,7 +16,7 @@ FEATURE_DEFAULT_VAL_TYPE_KEY = "boolean_type" -class RuleAction(str, Enum): +class RuleAction(Enum): EQUALS = "EQUALS" NOT_EQUALS = "NOT_EQUALS" KEY_GREATER_THAN_VALUE = "KEY_GREATER_THAN_VALUE" @@ -35,13 +35,13 @@ class RuleAction(str, Enum): TIME_SELECTED_DAYS = "TIME_SELECTED_DAYS" -class TimeKeys(str, Enum): +class TimeKeys(Enum): CURRENT_HOUR_UTC = "CURRENT_HOUR_UTC" CURRENT_DAY_UTC = "CURRENT_DAY_UTC" CURRENT_TIME_UTC = "CURRENT_TIME_UTC" -class TimeValues(str, Enum): +class TimeValues(Enum): START_TIME = "START_TIME" END_TIME = "END_TIME" SUNDAY = "SUNDAY" diff --git a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py index 3b20b80f061..9ab42ab9d31 100644 --- a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py +++ b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py @@ -33,15 +33,15 @@ def _time_selected_days_current_days_compare(values: List[str]) -> bool: def _time_range_compare_current_time_utc(values: Dict) -> bool: current_time_utc: datetime = _get_utc_time_now() - start_date = datetime.strptime(values.get(TimeValues.START_TIME, ""), "%Y-%m-%dT%H:%M:%S%z") - end_date = datetime.strptime(values.get(TimeValues.END_TIME, ""), "%Y-%m-%dT%H:%M:%S%z") + start_date = datetime.strptime(values.get(TimeValues.START_TIME.value, ""), "%Y-%m-%dT%H:%M:%S%z") + end_date = datetime.strptime(values.get(TimeValues.END_TIME.value, ""), "%Y-%m-%dT%H:%M:%S%z") return current_time_utc >= start_date and current_time_utc <= end_date def _time_range_compare_current_hour_utc(values: Dict) -> bool: current_time_utc: datetime = _get_utc_time_now() - start_hour, start_min = values.get(TimeValues.START_TIME, HOUR_MIN_SEPARATOR).split(HOUR_MIN_SEPARATOR) - end_hour, end_min = values.get(TimeValues.END_TIME, HOUR_MIN_SEPARATOR).split(HOUR_MIN_SEPARATOR) + start_hour, start_min = values.get(TimeValues.START_TIME.value, HOUR_MIN_SEPARATOR).split(HOUR_MIN_SEPARATOR) + end_hour, end_min = values.get(TimeValues.END_TIME.value, HOUR_MIN_SEPARATOR).split(HOUR_MIN_SEPARATOR) return ( current_time_utc.hour >= int(start_hour) and current_time_utc.hour <= int(end_hour) diff --git a/tests/functional/feature_flags/test_time_based_actions.py b/tests/functional/feature_flags/test_time_based_actions.py index 236d0694d3b..3826da94c16 100644 --- a/tests/functional/feature_flags/test_time_based_actions.py +++ b/tests/functional/feature_flags/test_time_based_actions.py @@ -215,7 +215,7 @@ def test_time_based_multiple_conditions_utc_in_between_time_range_rule_match(moc { CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, - CONDITION_VALUE: {TimeValues.START_TIME: "09:00", TimeValues.END_TIME: "17:00"}, + CONDITION_VALUE: {TimeValues.START_TIME.value: "09:00", TimeValues.END_TIME.value: "17:00"}, }, { CONDITION_ACTION: RuleAction.EQUALS.value, From 17c4eb04438b41c9c65e82c4c0cdca74c14dc2f3 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 22 Nov 2022 16:19:43 +0100 Subject: [PATCH 09/34] chore: use built-in datetime utc tz over 3P --- .../feature_flags/test_time_based_actions.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/functional/feature_flags/test_time_based_actions.py b/tests/functional/feature_flags/test_time_based_actions.py index 3826da94c16..61cf8351872 100644 --- a/tests/functional/feature_flags/test_time_based_actions.py +++ b/tests/functional/feature_flags/test_time_based_actions.py @@ -3,7 +3,6 @@ import pytest from botocore.config import Config -from dateutil import tz from aws_lambda_powertools.utilities.feature_flags.appconfig import AppConfigStore from aws_lambda_powertools.utilities.feature_flags.feature_flags import FeatureFlags @@ -68,7 +67,16 @@ def mock_current_utc_time( msec: int, ) -> None: mocked_time = mocker.patch("aws_lambda_powertools.utilities.feature_flags.time_conditions._get_utc_time_now") - mocked_time.return_value = datetime.datetime(year, month, day, hour, minute, second, msec, tz.gettz("UTC")) + mocked_time.return_value = datetime.datetime( + year=year, + month=month, + day=day, + hour=hour, + minute=minute, + second=second, + microsecond=msec, + tzinfo=datetime.timezone.utc, + ) def test_time_based_utc_in_between_time_range_rule_match(mocker, config): From 81bbf7a738faf471ab99b2b8c37f3a3aef88a6d2 Mon Sep 17 00:00:00 2001 From: Ran Isenberg Date: Tue, 22 Nov 2022 21:34:22 +0200 Subject: [PATCH 10/34] cr fixes --- .../utilities/feature_flags/feature_flags.py | 17 ++++++--- .../utilities/feature_flags/schema.py | 7 ++-- .../feature_flags/time_conditions.py | 24 +++---------- .../feature_flags/test_time_based_actions.py | 36 +++++++++---------- 4 files changed, 39 insertions(+), 45 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py index 6202edeb0ca..f2a0009f75c 100644 --- a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py +++ b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py @@ -6,7 +6,11 @@ from . import schema from .base import StoreProvider from .exceptions import ConfigurationStoreError -from .time_conditions import time_range_compare, time_selected_days_compare +from .time_conditions import ( + compare_between_utc_days, + compare_utc_date_range, + compare_utc_datetime_range, +) class FeatureFlags: @@ -60,8 +64,9 @@ def _match_by_action(self, action: str, condition_value: Any, context_value: Any schema.RuleAction.KEY_NOT_IN_VALUE.value: lambda a, b: a not in b, schema.RuleAction.VALUE_IN_KEY.value: lambda a, b: b in a, schema.RuleAction.VALUE_NOT_IN_KEY.value: lambda a, b: b not in a, - schema.RuleAction.TIME_RANGE.value: lambda a, b: time_range_compare(a, b), - schema.RuleAction.TIME_SELECTED_DAYS.value: lambda a, b: time_selected_days_compare(a, b), + schema.RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value: lambda a, b: compare_utc_date_range(a, b), + schema.RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value: lambda a, b: compare_utc_datetime_range(a, b), + schema.RuleAction.SCHEDULE_BETWEEN_DAYS.value: lambda a, b: compare_between_utc_days(a, b), } try: @@ -91,7 +96,11 @@ def _evaluate_conditions( cond_value = condition.get(schema.CONDITION_VALUE) # time based rule actions have no user context. the context is the condition key - if cond_action in (schema.RuleAction.TIME_RANGE.value, schema.RuleAction.TIME_SELECTED_DAYS.value): + if cond_action in ( + schema.RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, + schema.RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, + schema.RuleAction.SCHEDULE_BETWEEN_DAYS.value, + ): context_value = condition.get(schema.CONDITION_KEY) # e.g., CURRENT_HOUR_UTC if not self._match_by_action(action=cond_action, condition_value=cond_value, context_value=context_value): diff --git a/aws_lambda_powertools/utilities/feature_flags/schema.py b/aws_lambda_powertools/utilities/feature_flags/schema.py index ed8f6f7eb38..0ded1ecd92d 100644 --- a/aws_lambda_powertools/utilities/feature_flags/schema.py +++ b/aws_lambda_powertools/utilities/feature_flags/schema.py @@ -31,14 +31,15 @@ class RuleAction(Enum): KEY_NOT_IN_VALUE = "KEY_NOT_IN_VALUE" VALUE_IN_KEY = "VALUE_IN_KEY" VALUE_NOT_IN_KEY = "VALUE_NOT_IN_KEY" - TIME_RANGE = "TIME_RANGE" # 24 hours clock UTC time - TIME_SELECTED_DAYS = "TIME_SELECTED_DAYS" + SCHEDULE_BETWEEN_TIME_RANGE = "SCHEDULE_BETWEEN_TIME_RANGE" # hour:min 24 hours clock UTC time + SCHEDULE_BETWEEN_DATETIME_RANGE = "SCHEDULE_BETWEEN_DATETIME_RANGE" # full datetime format + SCHEDULE_BETWEEN_DAYS = "SCHEDULE_BETWEEN_DAYS" class TimeKeys(Enum): CURRENT_HOUR_UTC = "CURRENT_HOUR_UTC" CURRENT_DAY_UTC = "CURRENT_DAY_UTC" - CURRENT_TIME_UTC = "CURRENT_TIME_UTC" + CURRENT_DATETIME_UTC = "CURRENT_DATETIME_UTC" class TimeValues(Enum): diff --git a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py index 9ab42ab9d31..420c94881a8 100644 --- a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py +++ b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py @@ -1,44 +1,28 @@ from datetime import datetime, timezone from typing import Dict, List -from .schema import TimeKeys, TimeValues +from .schema import TimeValues HOUR_MIN_SEPARATOR = ":" -def time_range_compare(action: str, values: Dict) -> bool: - if action == TimeKeys.CURRENT_TIME_UTC.value: - return _time_range_compare_current_time_utc(values) - elif action == TimeKeys.CURRENT_HOUR_UTC.value: - return _time_range_compare_current_hour_utc(values) - # we assume it passed validation right? so no need to raise an error - return False - - -def time_selected_days_compare(action: str, values: List[str]) -> bool: - if action == TimeKeys.CURRENT_DAY_UTC.value: - return _time_selected_days_current_days_compare(values) - # we assume it passed validation right? so no need to raise an error - return False - - def _get_utc_time_now() -> datetime: return datetime.now(timezone.utc) -def _time_selected_days_current_days_compare(values: List[str]) -> bool: +def compare_between_utc_days(action: str, values: List[str]) -> bool: current_day = _get_utc_time_now().strftime("%A").upper() return current_day in values -def _time_range_compare_current_time_utc(values: Dict) -> bool: +def compare_utc_datetime_range(action: str, values: Dict) -> bool: current_time_utc: datetime = _get_utc_time_now() start_date = datetime.strptime(values.get(TimeValues.START_TIME.value, ""), "%Y-%m-%dT%H:%M:%S%z") end_date = datetime.strptime(values.get(TimeValues.END_TIME.value, ""), "%Y-%m-%dT%H:%M:%S%z") return current_time_utc >= start_date and current_time_utc <= end_date -def _time_range_compare_current_hour_utc(values: Dict) -> bool: +def compare_utc_date_range(action: str, values: Dict) -> bool: current_time_utc: datetime = _get_utc_time_now() start_hour, start_min = values.get(TimeValues.START_TIME.value, HOUR_MIN_SEPARATOR).split(HOUR_MIN_SEPARATOR) end_hour, end_min = values.get(TimeValues.END_TIME.value, HOUR_MIN_SEPARATOR).split(HOUR_MIN_SEPARATOR) diff --git a/tests/functional/feature_flags/test_time_based_actions.py b/tests/functional/feature_flags/test_time_based_actions.py index 61cf8351872..8a6cd857155 100644 --- a/tests/functional/feature_flags/test_time_based_actions.py +++ b/tests/functional/feature_flags/test_time_based_actions.py @@ -89,7 +89,7 @@ def test_time_based_utc_in_between_time_range_rule_match(mocker, config): RULE_MATCH_VALUE: True, CONDITIONS_KEY: [ { - CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, CONDITION_VALUE: {TimeValues.START_TIME.value: "11:11", TimeValues.END_TIME.value: "23:59"}, }, @@ -121,7 +121,7 @@ def test_time_based_utc_in_between_time_range_no_rule_match(mocker, config): RULE_MATCH_VALUE: True, CONDITIONS_KEY: [ { - CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, CONDITION_VALUE: {TimeValues.START_TIME.value: "11:11", TimeValues.END_TIME.value: "23:59"}, }, @@ -152,8 +152,8 @@ def test_time_based_utc_in_between_full_time_range_rule_match(mocker, config): RULE_MATCH_VALUE: True, CONDITIONS_KEY: [ { - CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, # condition matches + CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, CONDITION_VALUE: { TimeValues.START_TIME.value: "2022-10-05T12:15:00Z", TimeValues.END_TIME.value: "2022-10-10T12:15:00Z", @@ -187,8 +187,8 @@ def test_time_based_utc_in_between_full_time_range_no_rule_match(mocker, config) RULE_MATCH_VALUE: True, CONDITIONS_KEY: [ { - CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, # condition matches + CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, CONDITION_VALUE: { TimeValues.START_TIME.value: "2022-10-05T12:15:00Z", TimeValues.END_TIME.value: "2022-10-10T12:15:00Z", @@ -221,7 +221,7 @@ def test_time_based_multiple_conditions_utc_in_between_time_range_rule_match(moc RULE_MATCH_VALUE: True, CONDITIONS_KEY: [ { - CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, CONDITION_VALUE: {TimeValues.START_TIME.value: "09:00", TimeValues.END_TIME.value: "17:00"}, }, @@ -258,9 +258,9 @@ def test_time_based_multiple_conditions_utc_in_between_time_range_no_rule_match( RULE_MATCH_VALUE: True, CONDITIONS_KEY: [ { - CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, - CONDITION_VALUE: {TimeValues.START_TIME: "09:00", TimeValues.END_TIME: "17:00"}, + CONDITION_VALUE: {TimeValues.START_TIME.value: "09:00", TimeValues.END_TIME.value: "17:00"}, }, { CONDITION_ACTION: RuleAction.EQUALS.value, @@ -295,7 +295,7 @@ def test_time_based_utc_days_range_rule_match(mocker, config): RULE_MATCH_VALUE: True, CONDITIONS_KEY: [ { - CONDITION_ACTION: RuleAction.TIME_SELECTED_DAYS.value, # this condition matches + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS.value, # this condition matches CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions CONDITION_VALUE: [ TimeValues.MONDAY.value, @@ -331,7 +331,7 @@ def test_time_based_utc_days_range_no_rule_match(mocker, config): RULE_MATCH_VALUE: True, CONDITIONS_KEY: [ { - CONDITION_ACTION: RuleAction.TIME_SELECTED_DAYS.value, # this condition matches + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS.value, # this condition matches CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions CONDITION_VALUE: [ TimeValues.MONDAY.value, @@ -366,7 +366,7 @@ def test_time_based_utc_only_weekend_rule_match(mocker, config): RULE_MATCH_VALUE: True, CONDITIONS_KEY: [ { - CONDITION_ACTION: RuleAction.TIME_SELECTED_DAYS.value, # this condition matches + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS.value, # this condition matches CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions CONDITION_VALUE: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], }, @@ -396,7 +396,7 @@ def test_time_based_utc_only_weekend_no_rule_match(mocker, config): RULE_MATCH_VALUE: True, CONDITIONS_KEY: [ { - CONDITION_ACTION: RuleAction.TIME_SELECTED_DAYS.value, # this condition matches + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS.value, # this condition matches CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions CONDITION_VALUE: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], }, @@ -425,12 +425,12 @@ def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_rule_ma RULE_MATCH_VALUE: True, CONDITIONS_KEY: [ { - CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, CONDITION_VALUE: {TimeValues.START_TIME.value: "11:00", TimeValues.END_TIME.value: "23:00"}, }, { - CONDITION_ACTION: RuleAction.TIME_SELECTED_DAYS.value, # this condition matches + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS.value, # this condition matches CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, CONDITION_VALUE: [TimeValues.MONDAY.value, TimeValues.THURSDAY.value], }, @@ -460,12 +460,12 @@ def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_no_rule RULE_MATCH_VALUE: True, CONDITIONS_KEY: [ { - CONDITION_ACTION: RuleAction.TIME_RANGE.value, # this condition matches + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, - CONDITION_VALUE: {TimeValues.START_TIME: "11:00", TimeValues.END_TIME: "23:00"}, + CONDITION_VALUE: {TimeValues.START_TIME.value: "11:00", TimeValues.END_TIME.value: "23:00"}, }, { - CONDITION_ACTION: RuleAction.TIME_SELECTED_DAYS.value, # this condition matches + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS.value, # this condition matches CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, CONDITION_VALUE: [TimeValues.MONDAY.value, TimeValues.THURSDAY.value], }, From 6760fbb8d9235cf391ac3144dc9d129ac2dd2f0f Mon Sep 17 00:00:00 2001 From: Ran Isenberg Date: Wed, 23 Nov 2022 22:32:40 +0200 Subject: [PATCH 11/34] add schema validation for SCHEDULE_BETWEEN_TIME_RANGE action --- .../utilities/feature_flags/schema.py | 33 ++++ .../feature_flags/test_schema_validation.py | 173 +++++++++++++++++- 2 files changed, 205 insertions(+), 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/schema.py b/aws_lambda_powertools/utilities/feature_flags/schema.py index 0ded1ecd92d..08523fca8ec 100644 --- a/aws_lambda_powertools/utilities/feature_flags/schema.py +++ b/aws_lambda_powertools/utilities/feature_flags/schema.py @@ -1,4 +1,5 @@ import logging +from datetime import datetime from enum import Enum from typing import Any, Dict, List, Optional, Union @@ -285,9 +286,41 @@ def validate_condition_key(condition: Dict[str, Any], rule_name: str): key = condition.get(CONDITION_KEY, "") if not key or not isinstance(key, str): raise SchemaValidationError(f"'key' value must be a non empty string, rule={rule_name}") + action = condition.get(CONDITION_ACTION, "") + if action == RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value and key != TimeKeys.CURRENT_HOUR_UTC.value: + raise SchemaValidationError( + f"'condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a 'CURRENT_HOUR_UTC' condition key, rule={rule_name}" # noqa: E501 + ) @staticmethod def validate_condition_value(condition: Dict[str, Any], rule_name: str): value = condition.get(CONDITION_VALUE, "") if not value: raise SchemaValidationError(f"'value' key must not be empty, rule={rule_name}") + action = condition.get(CONDITION_ACTION, "") + key = condition.get(CONDITION_KEY, "") + if action == RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value and key == TimeKeys.CURRENT_HOUR_UTC.value: + ConditionsValidator._validate_schedule_between_time_range_value(value, rule_name) + + @staticmethod + def _validate_schedule_between_time_range_value(value: Any, rule_name: str): + error_str = f"condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a condition value type dictionary with 'START_TIME' and 'END_TIME' keys, rule={rule_name}" # noqa: E501 + if not isinstance(value, dict): + raise SchemaValidationError(error_str) + start_time = value.get(TimeValues.START_TIME.value) + end_time = value.get(TimeValues.END_TIME.value) + if not start_time or not end_time: + raise SchemaValidationError(error_str) + if not isinstance(start_time, str) or not isinstance(end_time, str): + raise SchemaValidationError(f"'START_TIME' and 'END_TIME' must be a non empty string, rule={rule_name}") + ConditionsValidator._validate_time_value(start_time, rule_name) + ConditionsValidator._validate_time_value(end_time, rule_name) + + @staticmethod + def _validate_time_value(time: str, rule_name: str): + try: + datetime.strptime(time, "%H:%M") + except Exception: + raise SchemaValidationError( + f"'START_TIME' and 'END_TIME' must be a valid 24 hours time format %H:%M, rule={rule_name}" + ) diff --git a/tests/functional/feature_flags/test_schema_validation.py b/tests/functional/feature_flags/test_schema_validation.py index 0366a5609ee..ce48c918c68 100644 --- a/tests/functional/feature_flags/test_schema_validation.py +++ b/tests/functional/feature_flags/test_schema_validation.py @@ -18,6 +18,8 @@ RuleAction, RulesValidator, SchemaValidator, + TimeKeys, + TimeValues, ) logger = logging.getLogger(__name__) @@ -355,7 +357,7 @@ def test_validate_rule_invalid_when_match_type_boolean_feature_is_not_set(): def test_validate_rule_boolean_feature_is_set(): # GIVEN a rule with a boolean when_match and feature type boolean # WHEN calling validate_rule - # THEN schema is validated and decalared as valid + # THEN schema is validated and declared as valid rule_name = "dummy" rule = { RULE_MATCH_VALUE: True, @@ -366,3 +368,172 @@ def test_validate_rule_boolean_feature_is_set(): }, } RulesValidator.validate_rule(rule=rule, rule_name=rule_name, feature_name="dummy", boolean_feature=True) + + +def test_validate_time_condition_between_time_range_invalid_condition_key(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, + # value of between 11:11 to 23:59 and a key of CURRENT_DATETIME_UTC + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, + CONDITION_VALUE: {TimeValues.START_TIME.value: "11:11", TimeValues.END_TIME.value: "23:59"}, + CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + } + rule_name = "dummy" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=f"'condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a 'CURRENT_HOUR_UTC' condition key, rule={rule_name}", # noqa: E501 + ): + ConditionsValidator.validate_condition_key(condition=condition, rule_name=rule_name) + + +def test_validate_time_condition_between_time_range_invalid_condition_value(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_HOUR_UTC and invalid value of string + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, + CONDITION_VALUE: "11:00-22:33", + CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + } + rule_name = "dummy" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=f"condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a condition value type dictionary with 'START_TIME' and 'END_TIME' keys, rule={rule_name}", # noqa: E501 + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +def test_validate_time_condition_between_time_range_invalid_condition_value_no_start_time(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_HOUR_UTC and invalid value + # dict without START_TIME key + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, + CONDITION_VALUE: {TimeValues.END_TIME.value: "23:59"}, + CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + } + rule_name = "dummy" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=f"condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a condition value type dictionary with 'START_TIME' and 'END_TIME' keys, rule={rule_name}", # noqa: E501 + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +def test_validate_time_condition_between_time_range_invalid_condition_value_no_end_time(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_HOUR_UTC and invalid value + # dict without END_TIME key + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, + CONDITION_VALUE: {TimeValues.START_TIME.value: "23:59"}, + CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + } + rule_name = "dummy" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=f"condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a condition value type dictionary with 'START_TIME' and 'END_TIME' keys, rule={rule_name}", # noqa: E501 + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +def test_validate_time_condition_between_time_range_invalid_condition_value_invalid_start_time_type(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_HOUR_UTC and + # invalid START_TIME value as a number + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, + CONDITION_VALUE: {TimeValues.START_TIME.value: 4, TimeValues.END_TIME.value: "23:59"}, + CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + } + rule_name = "dummy" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=f"'START_TIME' and 'END_TIME' must be a non empty string, rule={rule_name}", + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +def test_validate_time_condition_between_time_range_invalid_condition_value_invalid_end_time_type(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_HOUR_UTC and + # invalid START_TIME value as a number + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, + CONDITION_VALUE: {TimeValues.START_TIME.value: "11:11", TimeValues.END_TIME.value: 4}, + CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + } + rule_name = "dummy" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=f"'START_TIME' and 'END_TIME' must be a non empty string, rule={rule_name}", + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +def test_validate_time_condition_between_time_range_invalid_condition_value_invalid_start_time_value(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_HOUR_UTC and + # invalid START_TIME value as an invalid time format + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, + CONDITION_VALUE: {TimeValues.START_TIME.value: "11-11", TimeValues.END_TIME.value: "23:59"}, + CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + } + rule_name = "dummy" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=f"'START_TIME' and 'END_TIME' must be a valid 24 hours time format %H:%M, rule={rule_name}", + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + condition[CONDITION_VALUE] = {TimeValues.START_TIME.value: "24:99", TimeValues.END_TIME.value: "23:59"} + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=f"'START_TIME' and 'END_TIME' must be a valid 24 hours time format %H:%M, rule={rule_name}", + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +def test_validate_time_condition_between_time_range_invalid_condition_value_invalid_end_time_value(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_HOUR_UTC and + # invalid END_TIME value as an invalid time format + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, + CONDITION_VALUE: {TimeValues.START_TIME.value: "10:11", TimeValues.END_TIME.value: "11-11"}, + CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + } + rule_name = "dummy" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=f"'START_TIME' and 'END_TIME' must be a valid 24 hours time format %H:%M, rule={rule_name}", + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + condition[CONDITION_VALUE] = {TimeValues.START_TIME.value: "10:11", TimeValues.END_TIME.value: "999:59"} + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=f"'START_TIME' and 'END_TIME' must be a valid 24 hours time format %H:%M, rule={rule_name}", + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) From cef82c429ab47287dbe50bd33eda1936fb4f86ba Mon Sep 17 00:00:00 2001 From: Ran Isenberg Date: Fri, 25 Nov 2022 16:08:57 +0200 Subject: [PATCH 12/34] added schema validation for datetime range --- .../utilities/feature_flags/schema.py | 45 +++-- .../feature_flags/test_schema_validation.py | 191 +++++++++++++++++- 2 files changed, 216 insertions(+), 20 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/schema.py b/aws_lambda_powertools/utilities/feature_flags/schema.py index 08523fca8ec..e4b8415359a 100644 --- a/aws_lambda_powertools/utilities/feature_flags/schema.py +++ b/aws_lambda_powertools/utilities/feature_flags/schema.py @@ -249,6 +249,10 @@ def validate_rule_default_value(rule: Dict, rule_name: str, boolean_feature: boo class ConditionsValidator(BaseValidator): + + TIME_RANGE_FORMAT = "%H:%M" + DATETIME_RANGE_FORMAT = "%Y-%m-%dT%H:%M:%S%z" + def __init__(self, rule: Dict[str, Any], rule_name: str, logger: Optional[Union[logging.Logger, Logger]] = None): self.conditions: List[Dict[str, Any]] = rule.get(CONDITIONS_KEY, {}) self.rule_name = rule_name @@ -291,6 +295,10 @@ def validate_condition_key(condition: Dict[str, Any], rule_name: str): raise SchemaValidationError( f"'condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a 'CURRENT_HOUR_UTC' condition key, rule={rule_name}" # noqa: E501 ) + elif action == RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value and key != TimeKeys.CURRENT_DATETIME_UTC.value: + raise SchemaValidationError( + f"'condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a 'CURRENT_DATETIME_UTC' condition key, rule={rule_name}" # noqa: E501 + ) @staticmethod def validate_condition_value(condition: Dict[str, Any], rule_name: str): @@ -299,12 +307,30 @@ def validate_condition_value(condition: Dict[str, Any], rule_name: str): raise SchemaValidationError(f"'value' key must not be empty, rule={rule_name}") action = condition.get(CONDITION_ACTION, "") key = condition.get(CONDITION_KEY, "") + # time actions if action == RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value and key == TimeKeys.CURRENT_HOUR_UTC.value: - ConditionsValidator._validate_schedule_between_time_range_value(value, rule_name) + ConditionsValidator._validate_schedule_between_time_and_datetime_ranges( + value, rule_name, action, ConditionsValidator.TIME_RANGE_FORMAT + ) + elif action == RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value and key == TimeKeys.CURRENT_DATETIME_UTC.value: + ConditionsValidator._validate_schedule_between_time_and_datetime_ranges( + value, rule_name, action, ConditionsValidator.DATETIME_RANGE_FORMAT + ) @staticmethod - def _validate_schedule_between_time_range_value(value: Any, rule_name: str): - error_str = f"condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a condition value type dictionary with 'START_TIME' and 'END_TIME' keys, rule={rule_name}" # noqa: E501 + def _validate_time_value(time: str, rule_name: str, date_format: str): + try: + datetime.strptime(time, date_format) + except Exception: + raise SchemaValidationError( + f"'START_TIME' and 'END_TIME' must be a valid time format, time_format={date_format}, rule={rule_name}" + ) + + @staticmethod + def _validate_schedule_between_time_and_datetime_ranges( + value: Any, rule_name: str, action_name: str, date_format: str + ): + error_str = f"condition with a '{action_name}' action must have a condition value type dictionary with 'START_TIME' and 'END_TIME' keys, rule={rule_name}" # noqa: E501 if not isinstance(value, dict): raise SchemaValidationError(error_str) start_time = value.get(TimeValues.START_TIME.value) @@ -313,14 +339,5 @@ def _validate_schedule_between_time_range_value(value: Any, rule_name: str): raise SchemaValidationError(error_str) if not isinstance(start_time, str) or not isinstance(end_time, str): raise SchemaValidationError(f"'START_TIME' and 'END_TIME' must be a non empty string, rule={rule_name}") - ConditionsValidator._validate_time_value(start_time, rule_name) - ConditionsValidator._validate_time_value(end_time, rule_name) - - @staticmethod - def _validate_time_value(time: str, rule_name: str): - try: - datetime.strptime(time, "%H:%M") - except Exception: - raise SchemaValidationError( - f"'START_TIME' and 'END_TIME' must be a valid 24 hours time format %H:%M, rule={rule_name}" - ) + ConditionsValidator._validate_time_value(start_time, rule_name, date_format) + ConditionsValidator._validate_time_value(end_time, rule_name, date_format) diff --git a/tests/functional/feature_flags/test_schema_validation.py b/tests/functional/feature_flags/test_schema_validation.py index ce48c918c68..f6a23edcbf8 100644 --- a/tests/functional/feature_flags/test_schema_validation.py +++ b/tests/functional/feature_flags/test_schema_validation.py @@ -492,12 +492,12 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_inva CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, } rule_name = "dummy" - + match_str = f"START_TIME' and 'END_TIME' must be a valid time format, time_format=%H:%M, rule={rule_name}" # WHEN calling validate_condition # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"'START_TIME' and 'END_TIME' must be a valid 24 hours time format %H:%M, rule={rule_name}", + match=match_str, ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) @@ -506,7 +506,7 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_inva # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"'START_TIME' and 'END_TIME' must be a valid 24 hours time format %H:%M, rule={rule_name}", + match=match_str, ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) @@ -520,12 +520,12 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_inva CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, } rule_name = "dummy" - + match_str = f"START_TIME' and 'END_TIME' must be a valid time format, time_format=%H:%M, rule={rule_name}" # WHEN calling validate_condition # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"'START_TIME' and 'END_TIME' must be a valid 24 hours time format %H:%M, rule={rule_name}", + match=match_str, ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) @@ -534,6 +534,185 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_inva # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"'START_TIME' and 'END_TIME' must be a valid 24 hours time format %H:%M, rule={rule_name}", + match=match_str, + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +def test_validate_time_condition_between_datetime_range_invalid_condition_key(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, + # value of between "2022-10-05T12:15:00Z" to "2022-10-10T12:15:00Z" and a key of CURRENT_HOUR_UTC + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, + CONDITION_VALUE: { + TimeValues.START_TIME.value: "2022-10-05T12:15:00Z", + TimeValues.END_TIME.value: "2022-10-10T12:15:00Z", + }, # noqa: E501 + CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + } + rule_name = "dummy" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=f"'condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a 'CURRENT_DATETIME_UTC' condition key, rule={rule_name}", # noqa: E501 + ): + ConditionsValidator.validate_condition_key(condition=condition, rule_name=rule_name) + + +####### + + +def test_a_validate_time_condition_between_datetime_range_invalid_condition_value(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and invalid value of string # noqa: E501 + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, + CONDITION_VALUE: "11:00-22:33", + CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + } + rule_name = "dummy" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=f"condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a condition value type dictionary with 'START_TIME' and 'END_TIME' keys, rule={rule_name}", # noqa: E501 ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +def test_validate_time_condition_between_datetime_range_invalid_condition_value_no_start_time(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and invalid value + # dict without START_TIME key + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, + CONDITION_VALUE: {TimeValues.END_TIME.value: "2022-10-10T12:15:00Z"}, + CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + } + rule_name = "dummy" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=f"condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a condition value type dictionary with 'START_TIME' and 'END_TIME' keys, rule={rule_name}", # noqa: E501 + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +def test_validate_time_condition_between_datetime_range_invalid_condition_value_no_end_time(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and invalid value + # dict without END_TIME key + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, + CONDITION_VALUE: {TimeValues.START_TIME.value: "2022-10-10T12:15:00Z"}, + CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + } + rule_name = "dummy" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=f"condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a condition value type dictionary with 'START_TIME' and 'END_TIME' keys, rule={rule_name}", # noqa: E501 + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +def test_validate_time_condition_between_datetime_range_invalid_condition_value_invalid_start_time_type(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and + # invalid START_TIME value as a number + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, + CONDITION_VALUE: {TimeValues.START_TIME.value: 4, TimeValues.END_TIME.value: "2022-10-10T12:15:00Z"}, + CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + } + rule_name = "dummy" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=f"'START_TIME' and 'END_TIME' must be a non empty string, rule={rule_name}", + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +def test_validate_time_condition_between_datetime_range_invalid_condition_value_invalid_end_time_type(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and + # invalid START_TIME value as a number + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, + CONDITION_VALUE: {TimeValues.END_TIME.value: 4, TimeValues.START_TIME.value: "2022-10-10T12:15:00Z"}, + CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + } + rule_name = "dummy" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=f"'START_TIME' and 'END_TIME' must be a non empty string, rule={rule_name}", + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +def test_validate_time_condition_between_datetime_range_invalid_condition_value_invalid_start_time_value(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and + # invalid START_TIME value as an invalid time format + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, + CONDITION_VALUE: {TimeValues.START_TIME.value: "11:11", TimeValues.END_TIME.value: "2022-10-10T12:15:00Z"}, + CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + } + rule_name = "dummy" + match_str = ( + f"START_TIME' and 'END_TIME' must be a valid time format, time_format=%Y-%m-%dT%H:%M:%S%z, rule={rule_name}" + ) + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=match_str, + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + condition[CONDITION_VALUE] = { + TimeValues.START_TIME.value: "24:99", + TimeValues.END_TIME.value: "2022-10-10T12:15:00Z", + } + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=match_str, + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +def test_validate_time_condition_between_datetime_range_invalid_condition_value_invalid_end_time_value(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and + # invalid END_TIME value as an invalid time format + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, + CONDITION_VALUE: {TimeValues.END_TIME.value: "2022-10-10", TimeValues.START_TIME.value: "2022-10-10T12:15:00Z"}, + CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + } + rule_name = "dummy" + match_str = ( + f"START_TIME' and 'END_TIME' must be a valid time format, time_format=%Y-%m-%dT%H:%M:%S%z, rule={rule_name}" + ) + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises(SchemaValidationError, match=match_str): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + condition[CONDITION_VALUE] = { + TimeValues.END_TIME.value: "2022-10-10", + TimeValues.START_TIME.value: "2022-10-10 12:15", + } + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises(SchemaValidationError, match=match_str): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) From 6f534d67a5c26af55453397bc0a6d64f1f01e6fe Mon Sep 17 00:00:00 2001 From: Ran Isenberg Date: Fri, 25 Nov 2022 23:32:27 +0200 Subject: [PATCH 13/34] finished validation --- .../utilities/feature_flags/schema.py | 33 ++++- .../feature_flags/test_schema_validation.py | 139 ++++++++++++------ 2 files changed, 122 insertions(+), 50 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/schema.py b/aws_lambda_powertools/utilities/feature_flags/schema.py index e4b8415359a..60376d29115 100644 --- a/aws_lambda_powertools/utilities/feature_flags/schema.py +++ b/aws_lambda_powertools/utilities/feature_flags/schema.py @@ -295,10 +295,14 @@ def validate_condition_key(condition: Dict[str, Any], rule_name: str): raise SchemaValidationError( f"'condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a 'CURRENT_HOUR_UTC' condition key, rule={rule_name}" # noqa: E501 ) - elif action == RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value and key != TimeKeys.CURRENT_DATETIME_UTC.value: + if action == RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value and key != TimeKeys.CURRENT_DATETIME_UTC.value: raise SchemaValidationError( f"'condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a 'CURRENT_DATETIME_UTC' condition key, rule={rule_name}" # noqa: E501 ) + if action == RuleAction.SCHEDULE_BETWEEN_DAYS.value and key != TimeKeys.CURRENT_DAY_UTC.value: + raise SchemaValidationError( + f"'condition with a 'SCHEDULE_BETWEEN_DAYS' action must have a 'CURRENT_DAY_UTC' condition key, rule={rule_name}" # noqa: E501 + ) @staticmethod def validate_condition_value(condition: Dict[str, Any], rule_name: str): @@ -306,16 +310,17 @@ def validate_condition_value(condition: Dict[str, Any], rule_name: str): if not value: raise SchemaValidationError(f"'value' key must not be empty, rule={rule_name}") action = condition.get(CONDITION_ACTION, "") - key = condition.get(CONDITION_KEY, "") # time actions - if action == RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value and key == TimeKeys.CURRENT_HOUR_UTC.value: + if action == RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value: ConditionsValidator._validate_schedule_between_time_and_datetime_ranges( value, rule_name, action, ConditionsValidator.TIME_RANGE_FORMAT ) - elif action == RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value and key == TimeKeys.CURRENT_DATETIME_UTC.value: + elif action == RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value: ConditionsValidator._validate_schedule_between_time_and_datetime_ranges( value, rule_name, action, ConditionsValidator.DATETIME_RANGE_FORMAT ) + elif action == RuleAction.SCHEDULE_BETWEEN_DAYS.value: + ConditionsValidator._validate_schedule_between_days(value, rule_name) @staticmethod def _validate_time_value(time: str, rule_name: str, date_format: str): @@ -326,6 +331,26 @@ def _validate_time_value(time: str, rule_name: str, date_format: str): f"'START_TIME' and 'END_TIME' must be a valid time format, time_format={date_format}, rule={rule_name}" ) + @staticmethod + def _validate_schedule_between_days(value: Any, rule_name: str): + if not isinstance(value, list) or not value: + raise SchemaValidationError( + f"condition with a CURRENT_DAY_UTC action must have a non empty condition value type list, rule={rule_name}" # noqa: E501 + ) + for day in value: + if not isinstance(day, str) or day not in [ + TimeValues.SUNDAY.value, + TimeValues.MONDAY.value, + TimeValues.TUESDAY.value, + TimeValues.WEDNESDAY.value, + TimeValues.THURSDAY.value, + TimeValues.FRIDAY.value, + TimeValues.SATURDAY.value, + ]: + raise SchemaValidationError( + f"condition value must represent a week day string defined in 'TimeValues' enum, rule={rule_name}" + ) + @staticmethod def _validate_schedule_between_time_and_datetime_ranges( value: Any, rule_name: str, action_name: str, date_format: str diff --git a/tests/functional/feature_flags/test_schema_validation.py b/tests/functional/feature_flags/test_schema_validation.py index f6a23edcbf8..7473b6a2704 100644 --- a/tests/functional/feature_flags/test_schema_validation.py +++ b/tests/functional/feature_flags/test_schema_validation.py @@ -483,12 +483,19 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_inva ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) -def test_validate_time_condition_between_time_range_invalid_condition_value_invalid_start_time_value(): +@pytest.mark.parametrize( + "cond_value", + [ + {TimeValues.START_TIME.value: "11-11", TimeValues.END_TIME.value: "23:59"}, + {TimeValues.START_TIME.value: "24:99", TimeValues.END_TIME.value: "23:59"}, + ], +) +def test_validate_time_condition_between_time_range_invalid_condition_value_invalid_start_time_value(cond_value): # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_HOUR_UTC and # invalid START_TIME value as an invalid time format condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, - CONDITION_VALUE: {TimeValues.START_TIME.value: "11-11", TimeValues.END_TIME.value: "23:59"}, + CONDITION_VALUE: cond_value, CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, } rule_name = "dummy" @@ -501,22 +508,20 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_inva ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) - condition[CONDITION_VALUE] = {TimeValues.START_TIME.value: "24:99", TimeValues.END_TIME.value: "23:59"} - # WHEN calling validate_condition - # THEN raise SchemaValidationError - with pytest.raises( - SchemaValidationError, - match=match_str, - ): - ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) - -def test_validate_time_condition_between_time_range_invalid_condition_value_invalid_end_time_value(): +@pytest.mark.parametrize( + "cond_value", + [ + {TimeValues.START_TIME.value: "10:11", TimeValues.END_TIME.value: "11-11"}, + {TimeValues.START_TIME.value: "10:11", TimeValues.END_TIME.value: "999:59"}, + ], +) +def test_validate_time_condition_between_time_range_invalid_condition_value_invalid_end_time_value(cond_value): # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_HOUR_UTC and # invalid END_TIME value as an invalid time format condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, - CONDITION_VALUE: {TimeValues.START_TIME.value: "10:11", TimeValues.END_TIME.value: "11-11"}, + CONDITION_VALUE: cond_value, CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, } rule_name = "dummy" @@ -529,15 +534,6 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_inva ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) - condition[CONDITION_VALUE] = {TimeValues.START_TIME.value: "10:11", TimeValues.END_TIME.value: "999:59"} - # WHEN calling validate_condition - # THEN raise SchemaValidationError - with pytest.raises( - SchemaValidationError, - match=match_str, - ): - ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) - def test_validate_time_condition_between_datetime_range_invalid_condition_key(): # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, @@ -547,7 +543,7 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_key(): CONDITION_VALUE: { TimeValues.START_TIME.value: "2022-10-05T12:15:00Z", TimeValues.END_TIME.value: "2022-10-10T12:15:00Z", - }, # noqa: E501 + }, CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, } rule_name = "dummy" @@ -561,9 +557,6 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_key(): ConditionsValidator.validate_condition_key(condition=condition, rule_name=rule_name) -####### - - def test_a_validate_time_condition_between_datetime_range_invalid_condition_value(): # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and invalid value of string # noqa: E501 condition = { @@ -658,12 +651,20 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) -def test_validate_time_condition_between_datetime_range_invalid_condition_value_invalid_start_time_value(): +@pytest.mark.parametrize( + "cond_value", + [ + {TimeValues.START_TIME.value: "11:11", TimeValues.END_TIME.value: "2022-10-10T12:15:00Z"}, + {TimeValues.START_TIME.value: "24:99", TimeValues.END_TIME.value: "2022-10-10T12:15:00Z"}, + {TimeValues.START_TIME.value: "2022-10-10T", TimeValues.END_TIME.value: "2022-10-10T12:15:00Z"}, + ], +) +def test_validate_time_condition_between_datetime_range_invalid_condition_value_invalid_start_time_value(cond_value): # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and # invalid START_TIME value as an invalid time format condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, - CONDITION_VALUE: {TimeValues.START_TIME.value: "11:11", TimeValues.END_TIME.value: "2022-10-10T12:15:00Z"}, + CONDITION_VALUE: cond_value, CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, } rule_name = "dummy" @@ -678,25 +679,20 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) - condition[CONDITION_VALUE] = { - TimeValues.START_TIME.value: "24:99", - TimeValues.END_TIME.value: "2022-10-10T12:15:00Z", - } - # WHEN calling validate_condition - # THEN raise SchemaValidationError - with pytest.raises( - SchemaValidationError, - match=match_str, - ): - ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) - -def test_validate_time_condition_between_datetime_range_invalid_condition_value_invalid_end_time_value(): +@pytest.mark.parametrize( + "cond_value", + [ + {TimeValues.END_TIME.value: "10:10", TimeValues.START_TIME.value: "2022-10-10T12:15:00Z"}, + {TimeValues.END_TIME.value: "2022-10-10", TimeValues.START_TIME.value: "2022-10-10T12:15:00Z"}, + ], +) +def test_validate_time_condition_between_datetime_range_invalid_condition_value_invalid_end_time_value(cond_value): # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and # invalid END_TIME value as an invalid time format condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, - CONDITION_VALUE: {TimeValues.END_TIME.value: "2022-10-10", TimeValues.START_TIME.value: "2022-10-10T12:15:00Z"}, + CONDITION_VALUE: cond_value, CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, } rule_name = "dummy" @@ -708,11 +704,62 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ with pytest.raises(SchemaValidationError, match=match_str): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) - condition[CONDITION_VALUE] = { - TimeValues.END_TIME.value: "2022-10-10", - TimeValues.START_TIME.value: "2022-10-10 12:15", + +def test_validate_time_condition_between_days_range_invalid_condition_key(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_DAYS action, + # value of SUNDAY and a key of CURRENT_HOUR_UTC + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS.value, + CONDITION_VALUE: [TimeValues.SUNDAY.value], + CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, } + rule_name = "dummy" + # WHEN calling validate_condition # THEN raise SchemaValidationError - with pytest.raises(SchemaValidationError, match=match_str): + with pytest.raises( + SchemaValidationError, + match=f"'condition with a 'SCHEDULE_BETWEEN_DAYS' action must have a 'CURRENT_DAY_UTC' condition key, rule={rule_name}", # noqa: E501 + ): + ConditionsValidator.validate_condition_key(condition=condition, rule_name=rule_name) + + +def test_validate_time_condition_between_days_range_invalid_condition_type(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_DAYS action, key CURRENT_DAY_UTC and invalid value type string + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS.value, + CONDITION_VALUE: TimeValues.SATURDAY.value, + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, + } + rule_name = "dummy" + match_str = f"condition with a CURRENT_DAY_UTC action must have a non empty condition value type list, rule={rule_name}" # noqa: E501 + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=match_str, + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +@pytest.mark.parametrize( + "cond_value", [[TimeValues.SUNDAY.value, "funday"], [TimeValues.SUNDAY, TimeValues.MONDAY.value]] +) +def test_validate_time_condition_between_days_range_invalid_condition_value(cond_value): + # GIVEN a configuration with a SCHEDULE_BETWEEN_DAYS action, key CURRENT_DAY_UTC and invalid value not day string + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS.value, + CONDITION_VALUE: cond_value, + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, + } + rule_name = "dummy" + match_str = ( + f"condition value must represent a week day string defined in 'TimeValues' enum, rule={rule_name}" # noqa: E501 + ) + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=match_str, + ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) From 7f07c2480230c8a4f5bfc79866e91a91f8cde813 Mon Sep 17 00:00:00 2001 From: Ran Isenberg Date: Fri, 2 Dec 2022 12:36:18 +0200 Subject: [PATCH 14/34] pre cr changes --- .../utilities/feature_flags/feature_flags.py | 6 +- .../utilities/feature_flags/schema.py | 28 ++-- .../feature_flags/time_conditions.py | 8 +- .../feature_flags/test_schema_validation.py | 150 +++++++++--------- .../feature_flags/test_time_based_actions.py | 44 ++--- 5 files changed, 117 insertions(+), 119 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py index f2a0009f75c..9ee1ecb20b2 100644 --- a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py +++ b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py @@ -66,7 +66,7 @@ def _match_by_action(self, action: str, condition_value: Any, context_value: Any schema.RuleAction.VALUE_NOT_IN_KEY.value: lambda a, b: b not in a, schema.RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value: lambda a, b: compare_utc_date_range(a, b), schema.RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value: lambda a, b: compare_utc_datetime_range(a, b), - schema.RuleAction.SCHEDULE_BETWEEN_DAYS.value: lambda a, b: compare_between_utc_days(a, b), + schema.RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value: lambda a, b: compare_between_utc_days(a, b), } try: @@ -99,9 +99,9 @@ def _evaluate_conditions( if cond_action in ( schema.RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, schema.RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, - schema.RuleAction.SCHEDULE_BETWEEN_DAYS.value, + schema.RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, ): - context_value = condition.get(schema.CONDITION_KEY) # e.g., CURRENT_HOUR_UTC + context_value = condition.get(schema.CONDITION_KEY) # e.g., CURRENT_TIME_UTC if not self._match_by_action(action=cond_action, condition_value=cond_value, context_value=context_value): self.logger.debug( diff --git a/aws_lambda_powertools/utilities/feature_flags/schema.py b/aws_lambda_powertools/utilities/feature_flags/schema.py index 60376d29115..3a2b6fe0df7 100644 --- a/aws_lambda_powertools/utilities/feature_flags/schema.py +++ b/aws_lambda_powertools/utilities/feature_flags/schema.py @@ -34,18 +34,18 @@ class RuleAction(Enum): VALUE_NOT_IN_KEY = "VALUE_NOT_IN_KEY" SCHEDULE_BETWEEN_TIME_RANGE = "SCHEDULE_BETWEEN_TIME_RANGE" # hour:min 24 hours clock UTC time SCHEDULE_BETWEEN_DATETIME_RANGE = "SCHEDULE_BETWEEN_DATETIME_RANGE" # full datetime format - SCHEDULE_BETWEEN_DAYS = "SCHEDULE_BETWEEN_DAYS" + SCHEDULE_BETWEEN_DAYS_OF_WEEK = "SCHEDULE_BETWEEN_DAYS_OF_WEEK" class TimeKeys(Enum): - CURRENT_HOUR_UTC = "CURRENT_HOUR_UTC" + CURRENT_TIME_UTC = "CURRENT_TIME_UTC" CURRENT_DAY_UTC = "CURRENT_DAY_UTC" CURRENT_DATETIME_UTC = "CURRENT_DATETIME_UTC" class TimeValues(Enum): - START_TIME = "START_TIME" - END_TIME = "END_TIME" + START = "START" + END = "END" SUNDAY = "SUNDAY" MONDAY = "MONDAY" TUESDAY = "TUESDAY" @@ -291,17 +291,17 @@ def validate_condition_key(condition: Dict[str, Any], rule_name: str): if not key or not isinstance(key, str): raise SchemaValidationError(f"'key' value must be a non empty string, rule={rule_name}") action = condition.get(CONDITION_ACTION, "") - if action == RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value and key != TimeKeys.CURRENT_HOUR_UTC.value: + if action == RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value and key != TimeKeys.CURRENT_TIME_UTC.value: raise SchemaValidationError( - f"'condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a 'CURRENT_HOUR_UTC' condition key, rule={rule_name}" # noqa: E501 + f"'condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a 'CURRENT_TIME_UTC' condition key, rule={rule_name}" # noqa: E501 ) if action == RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value and key != TimeKeys.CURRENT_DATETIME_UTC.value: raise SchemaValidationError( f"'condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a 'CURRENT_DATETIME_UTC' condition key, rule={rule_name}" # noqa: E501 ) - if action == RuleAction.SCHEDULE_BETWEEN_DAYS.value and key != TimeKeys.CURRENT_DAY_UTC.value: + if action == RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value and key != TimeKeys.CURRENT_DAY_UTC.value: raise SchemaValidationError( - f"'condition with a 'SCHEDULE_BETWEEN_DAYS' action must have a 'CURRENT_DAY_UTC' condition key, rule={rule_name}" # noqa: E501 + f"'condition with a 'SCHEDULE_BETWEEN_DAYS_OF_WEEK' action must have a 'CURRENT_DAY_UTC' condition key, rule={rule_name}" # noqa: E501 ) @staticmethod @@ -319,7 +319,7 @@ def validate_condition_value(condition: Dict[str, Any], rule_name: str): ConditionsValidator._validate_schedule_between_time_and_datetime_ranges( value, rule_name, action, ConditionsValidator.DATETIME_RANGE_FORMAT ) - elif action == RuleAction.SCHEDULE_BETWEEN_DAYS.value: + elif action == RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value: ConditionsValidator._validate_schedule_between_days(value, rule_name) @staticmethod @@ -328,7 +328,7 @@ def _validate_time_value(time: str, rule_name: str, date_format: str): datetime.strptime(time, date_format) except Exception: raise SchemaValidationError( - f"'START_TIME' and 'END_TIME' must be a valid time format, time_format={date_format}, rule={rule_name}" + f"'START' and 'END' must be a valid time format, time_format={date_format}, rule={rule_name}" ) @staticmethod @@ -355,14 +355,14 @@ def _validate_schedule_between_days(value: Any, rule_name: str): def _validate_schedule_between_time_and_datetime_ranges( value: Any, rule_name: str, action_name: str, date_format: str ): - error_str = f"condition with a '{action_name}' action must have a condition value type dictionary with 'START_TIME' and 'END_TIME' keys, rule={rule_name}" # noqa: E501 + 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 if not isinstance(value, dict): raise SchemaValidationError(error_str) - start_time = value.get(TimeValues.START_TIME.value) - end_time = value.get(TimeValues.END_TIME.value) + start_time = value.get(TimeValues.START.value) + end_time = value.get(TimeValues.END.value) if not start_time or not end_time: raise SchemaValidationError(error_str) if not isinstance(start_time, str) or not isinstance(end_time, str): - raise SchemaValidationError(f"'START_TIME' and 'END_TIME' must be a non empty string, rule={rule_name}") + raise SchemaValidationError(f"'START' and 'END' must be a non empty string, rule={rule_name}") ConditionsValidator._validate_time_value(start_time, rule_name, date_format) ConditionsValidator._validate_time_value(end_time, rule_name, date_format) diff --git a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py index 420c94881a8..9f9bf9464e1 100644 --- a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py +++ b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py @@ -17,15 +17,15 @@ def compare_between_utc_days(action: str, values: List[str]) -> bool: def compare_utc_datetime_range(action: str, values: Dict) -> bool: current_time_utc: datetime = _get_utc_time_now() - start_date = datetime.strptime(values.get(TimeValues.START_TIME.value, ""), "%Y-%m-%dT%H:%M:%S%z") - end_date = datetime.strptime(values.get(TimeValues.END_TIME.value, ""), "%Y-%m-%dT%H:%M:%S%z") + start_date = datetime.strptime(values.get(TimeValues.START.value, ""), "%Y-%m-%dT%H:%M:%S%z") + end_date = datetime.strptime(values.get(TimeValues.END.value, ""), "%Y-%m-%dT%H:%M:%S%z") return current_time_utc >= start_date and current_time_utc <= end_date def compare_utc_date_range(action: str, values: Dict) -> bool: current_time_utc: datetime = _get_utc_time_now() - start_hour, start_min = values.get(TimeValues.START_TIME.value, HOUR_MIN_SEPARATOR).split(HOUR_MIN_SEPARATOR) - end_hour, end_min = values.get(TimeValues.END_TIME.value, HOUR_MIN_SEPARATOR).split(HOUR_MIN_SEPARATOR) + start_hour, start_min = values.get(TimeValues.START.value, HOUR_MIN_SEPARATOR).split(HOUR_MIN_SEPARATOR) + end_hour, end_min = values.get(TimeValues.END.value, HOUR_MIN_SEPARATOR).split(HOUR_MIN_SEPARATOR) return ( current_time_utc.hour >= int(start_hour) and current_time_utc.hour <= int(end_hour) diff --git a/tests/functional/feature_flags/test_schema_validation.py b/tests/functional/feature_flags/test_schema_validation.py index 7473b6a2704..fb674ba55b2 100644 --- a/tests/functional/feature_flags/test_schema_validation.py +++ b/tests/functional/feature_flags/test_schema_validation.py @@ -375,7 +375,7 @@ def test_validate_time_condition_between_time_range_invalid_condition_key(): # value of between 11:11 to 23:59 and a key of CURRENT_DATETIME_UTC condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, - CONDITION_VALUE: {TimeValues.START_TIME.value: "11:11", TimeValues.END_TIME.value: "23:59"}, + CONDITION_VALUE: {TimeValues.START.value: "11:11", TimeValues.END.value: "23:59"}, CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, } rule_name = "dummy" @@ -384,17 +384,17 @@ def test_validate_time_condition_between_time_range_invalid_condition_key(): # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"'condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a 'CURRENT_HOUR_UTC' condition key, rule={rule_name}", # noqa: E501 + match=f"'condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a 'CURRENT_TIME_UTC' condition key, rule={rule_name}", # noqa: E501 ): ConditionsValidator.validate_condition_key(condition=condition, rule_name=rule_name) def test_validate_time_condition_between_time_range_invalid_condition_value(): - # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_HOUR_UTC and invalid value of string + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME_UTC and invalid value of string condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, CONDITION_VALUE: "11:00-22:33", - CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, } rule_name = "dummy" @@ -402,18 +402,18 @@ def test_validate_time_condition_between_time_range_invalid_condition_value(): # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a condition value type dictionary with 'START_TIME' and 'END_TIME' keys, rule={rule_name}", # noqa: E501 + match=f"condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a condition value type dictionary with 'START' and 'END' keys, rule={rule_name}", # noqa: E501 ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) def test_validate_time_condition_between_time_range_invalid_condition_value_no_start_time(): - # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_HOUR_UTC and invalid value - # dict without START_TIME key + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME_UTC and invalid value + # dict without START key condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, - CONDITION_VALUE: {TimeValues.END_TIME.value: "23:59"}, - CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + CONDITION_VALUE: {TimeValues.END.value: "23:59"}, + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, } rule_name = "dummy" @@ -421,18 +421,18 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_no_s # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a condition value type dictionary with 'START_TIME' and 'END_TIME' keys, rule={rule_name}", # noqa: E501 + match=f"condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a condition value type dictionary with 'START' and 'END' keys, rule={rule_name}", # noqa: E501 ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) def test_validate_time_condition_between_time_range_invalid_condition_value_no_end_time(): - # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_HOUR_UTC and invalid value - # dict without END_TIME key + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME_UTC and invalid value + # dict without END key condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, - CONDITION_VALUE: {TimeValues.START_TIME.value: "23:59"}, - CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + CONDITION_VALUE: {TimeValues.START.value: "23:59"}, + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, } rule_name = "dummy" @@ -440,18 +440,18 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_no_e # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a condition value type dictionary with 'START_TIME' and 'END_TIME' keys, rule={rule_name}", # noqa: E501 + match=f"condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a condition value type dictionary with 'START' and 'END' keys, rule={rule_name}", # noqa: E501 ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) def test_validate_time_condition_between_time_range_invalid_condition_value_invalid_start_time_type(): - # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_HOUR_UTC and - # invalid START_TIME value as a number + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME_UTC and + # invalid START value as a number condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, - CONDITION_VALUE: {TimeValues.START_TIME.value: 4, TimeValues.END_TIME.value: "23:59"}, - CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + CONDITION_VALUE: {TimeValues.START.value: 4, TimeValues.END.value: "23:59"}, + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, } rule_name = "dummy" @@ -459,18 +459,18 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_inva # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"'START_TIME' and 'END_TIME' must be a non empty string, rule={rule_name}", + match=f"'START' and 'END' must be a non empty string, rule={rule_name}", ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) def test_validate_time_condition_between_time_range_invalid_condition_value_invalid_end_time_type(): - # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_HOUR_UTC and - # invalid START_TIME value as a number + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME_UTC and + # invalid START value as a number condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, - CONDITION_VALUE: {TimeValues.START_TIME.value: "11:11", TimeValues.END_TIME.value: 4}, - CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + CONDITION_VALUE: {TimeValues.START.value: "11:11", TimeValues.END.value: 4}, + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, } rule_name = "dummy" @@ -478,7 +478,7 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_inva # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"'START_TIME' and 'END_TIME' must be a non empty string, rule={rule_name}", + match=f"'START' and 'END' must be a non empty string, rule={rule_name}", ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) @@ -486,20 +486,20 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_inva @pytest.mark.parametrize( "cond_value", [ - {TimeValues.START_TIME.value: "11-11", TimeValues.END_TIME.value: "23:59"}, - {TimeValues.START_TIME.value: "24:99", TimeValues.END_TIME.value: "23:59"}, + {TimeValues.START.value: "11-11", TimeValues.END.value: "23:59"}, + {TimeValues.START.value: "24:99", TimeValues.END.value: "23:59"}, ], ) def test_validate_time_condition_between_time_range_invalid_condition_value_invalid_start_time_value(cond_value): - # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_HOUR_UTC and - # invalid START_TIME value as an invalid time format + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME_UTC and + # invalid START value as an invalid time format condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, CONDITION_VALUE: cond_value, - CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, } rule_name = "dummy" - match_str = f"START_TIME' and 'END_TIME' must be a valid time format, time_format=%H:%M, rule={rule_name}" + match_str = f"START' and 'END' must be a valid time format, time_format=%H:%M, rule={rule_name}" # WHEN calling validate_condition # THEN raise SchemaValidationError with pytest.raises( @@ -512,20 +512,20 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_inva @pytest.mark.parametrize( "cond_value", [ - {TimeValues.START_TIME.value: "10:11", TimeValues.END_TIME.value: "11-11"}, - {TimeValues.START_TIME.value: "10:11", TimeValues.END_TIME.value: "999:59"}, + {TimeValues.START.value: "10:11", TimeValues.END.value: "11-11"}, + {TimeValues.START.value: "10:11", TimeValues.END.value: "999:59"}, ], ) def test_validate_time_condition_between_time_range_invalid_condition_value_invalid_end_time_value(cond_value): - # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_HOUR_UTC and - # invalid END_TIME value as an invalid time format + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME_UTC and + # invalid END value as an invalid time format condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, CONDITION_VALUE: cond_value, - CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, } rule_name = "dummy" - match_str = f"START_TIME' and 'END_TIME' must be a valid time format, time_format=%H:%M, rule={rule_name}" + match_str = f"START' and 'END' must be a valid time format, time_format=%H:%M, rule={rule_name}" # WHEN calling validate_condition # THEN raise SchemaValidationError with pytest.raises( @@ -537,14 +537,14 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_inva def test_validate_time_condition_between_datetime_range_invalid_condition_key(): # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, - # value of between "2022-10-05T12:15:00Z" to "2022-10-10T12:15:00Z" and a key of CURRENT_HOUR_UTC + # value of between "2022-10-05T12:15:00Z" to "2022-10-10T12:15:00Z" and a key of CURRENT_TIME_UTC condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, CONDITION_VALUE: { - TimeValues.START_TIME.value: "2022-10-05T12:15:00Z", - TimeValues.END_TIME.value: "2022-10-10T12:15:00Z", + TimeValues.START.value: "2022-10-05T12:15:00Z", + TimeValues.END.value: "2022-10-10T12:15:00Z", }, - CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, } rule_name = "dummy" @@ -570,17 +570,17 @@ def test_a_validate_time_condition_between_datetime_range_invalid_condition_valu # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a condition value type dictionary with 'START_TIME' and 'END_TIME' keys, rule={rule_name}", # noqa: E501 + match=f"condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a condition value type dictionary with 'START' and 'END' keys, rule={rule_name}", # noqa: E501 ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) def test_validate_time_condition_between_datetime_range_invalid_condition_value_no_start_time(): # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and invalid value - # dict without START_TIME key + # dict without START key condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, - CONDITION_VALUE: {TimeValues.END_TIME.value: "2022-10-10T12:15:00Z"}, + CONDITION_VALUE: {TimeValues.END.value: "2022-10-10T12:15:00Z"}, CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, } rule_name = "dummy" @@ -589,17 +589,17 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a condition value type dictionary with 'START_TIME' and 'END_TIME' keys, rule={rule_name}", # noqa: E501 + match=f"condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a condition value type dictionary with 'START' and 'END' keys, rule={rule_name}", # noqa: E501 ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) def test_validate_time_condition_between_datetime_range_invalid_condition_value_no_end_time(): # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and invalid value - # dict without END_TIME key + # dict without END key condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, - CONDITION_VALUE: {TimeValues.START_TIME.value: "2022-10-10T12:15:00Z"}, + CONDITION_VALUE: {TimeValues.START.value: "2022-10-10T12:15:00Z"}, CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, } rule_name = "dummy" @@ -608,17 +608,17 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a condition value type dictionary with 'START_TIME' and 'END_TIME' keys, rule={rule_name}", # noqa: E501 + match=f"condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a condition value type dictionary with 'START' and 'END' keys, rule={rule_name}", # noqa: E501 ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) def test_validate_time_condition_between_datetime_range_invalid_condition_value_invalid_start_time_type(): # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and - # invalid START_TIME value as a number + # invalid START value as a number condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, - CONDITION_VALUE: {TimeValues.START_TIME.value: 4, TimeValues.END_TIME.value: "2022-10-10T12:15:00Z"}, + CONDITION_VALUE: {TimeValues.START.value: 4, TimeValues.END.value: "2022-10-10T12:15:00Z"}, CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, } rule_name = "dummy" @@ -627,17 +627,17 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"'START_TIME' and 'END_TIME' must be a non empty string, rule={rule_name}", + match=f"'START' and 'END' must be a non empty string, rule={rule_name}", ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) def test_validate_time_condition_between_datetime_range_invalid_condition_value_invalid_end_time_type(): # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and - # invalid START_TIME value as a number + # invalid START value as a number condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, - CONDITION_VALUE: {TimeValues.END_TIME.value: 4, TimeValues.START_TIME.value: "2022-10-10T12:15:00Z"}, + CONDITION_VALUE: {TimeValues.END.value: 4, TimeValues.START.value: "2022-10-10T12:15:00Z"}, CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, } rule_name = "dummy" @@ -646,7 +646,7 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"'START_TIME' and 'END_TIME' must be a non empty string, rule={rule_name}", + match=f"'START' and 'END' must be a non empty string, rule={rule_name}", ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) @@ -654,23 +654,21 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ @pytest.mark.parametrize( "cond_value", [ - {TimeValues.START_TIME.value: "11:11", TimeValues.END_TIME.value: "2022-10-10T12:15:00Z"}, - {TimeValues.START_TIME.value: "24:99", TimeValues.END_TIME.value: "2022-10-10T12:15:00Z"}, - {TimeValues.START_TIME.value: "2022-10-10T", TimeValues.END_TIME.value: "2022-10-10T12:15:00Z"}, + {TimeValues.START.value: "11:11", TimeValues.END.value: "2022-10-10T12:15:00Z"}, + {TimeValues.START.value: "24:99", TimeValues.END.value: "2022-10-10T12:15:00Z"}, + {TimeValues.START.value: "2022-10-10T", TimeValues.END.value: "2022-10-10T12:15:00Z"}, ], ) def test_validate_time_condition_between_datetime_range_invalid_condition_value_invalid_start_time_value(cond_value): # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and - # invalid START_TIME value as an invalid time format + # invalid START value as an invalid time format condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, CONDITION_VALUE: cond_value, CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, } rule_name = "dummy" - match_str = ( - f"START_TIME' and 'END_TIME' must be a valid time format, time_format=%Y-%m-%dT%H:%M:%S%z, rule={rule_name}" - ) + match_str = f"START' and 'END' must be a valid time format, time_format=%Y-%m-%dT%H:%M:%S%z, rule={rule_name}" # WHEN calling validate_condition # THEN raise SchemaValidationError with pytest.raises( @@ -683,22 +681,20 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ @pytest.mark.parametrize( "cond_value", [ - {TimeValues.END_TIME.value: "10:10", TimeValues.START_TIME.value: "2022-10-10T12:15:00Z"}, - {TimeValues.END_TIME.value: "2022-10-10", TimeValues.START_TIME.value: "2022-10-10T12:15:00Z"}, + {TimeValues.END.value: "10:10", TimeValues.START.value: "2022-10-10T12:15:00Z"}, + {TimeValues.END.value: "2022-10-10", TimeValues.START.value: "2022-10-10T12:15:00Z"}, ], ) def test_validate_time_condition_between_datetime_range_invalid_condition_value_invalid_end_time_value(cond_value): # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and - # invalid END_TIME value as an invalid time format + # invalid END value as an invalid time format condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, CONDITION_VALUE: cond_value, CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, } rule_name = "dummy" - match_str = ( - f"START_TIME' and 'END_TIME' must be a valid time format, time_format=%Y-%m-%dT%H:%M:%S%z, rule={rule_name}" - ) + match_str = f"START' and 'END' must be a valid time format, time_format=%Y-%m-%dT%H:%M:%S%z, rule={rule_name}" # WHEN calling validate_condition # THEN raise SchemaValidationError with pytest.raises(SchemaValidationError, match=match_str): @@ -706,12 +702,12 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ def test_validate_time_condition_between_days_range_invalid_condition_key(): - # GIVEN a configuration with a SCHEDULE_BETWEEN_DAYS action, - # value of SUNDAY and a key of CURRENT_HOUR_UTC + # GIVEN a configuration with a SCHEDULE_BETWEEN_DAYS_OF_WEEK action, + # value of SUNDAY and a key of CURRENT_TIME_UTC condition = { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS.value, + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, CONDITION_VALUE: [TimeValues.SUNDAY.value], - CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, } rule_name = "dummy" @@ -719,15 +715,16 @@ def test_validate_time_condition_between_days_range_invalid_condition_key(): # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"'condition with a 'SCHEDULE_BETWEEN_DAYS' action must have a 'CURRENT_DAY_UTC' condition key, rule={rule_name}", # noqa: E501 + match=f"'condition with a 'SCHEDULE_BETWEEN_DAYS_OF_WEEK' action must have a 'CURRENT_DAY_UTC' condition key, rule={rule_name}", # noqa: E501 ): ConditionsValidator.validate_condition_key(condition=condition, rule_name=rule_name) def test_validate_time_condition_between_days_range_invalid_condition_type(): - # GIVEN a configuration with a SCHEDULE_BETWEEN_DAYS action, key CURRENT_DAY_UTC and invalid value type string + # GIVEN a configuration with a SCHEDULE_BETWEEN_DAYS_OF_WEEK action + # key CURRENT_DAY_UTC and invalid value type string condition = { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS.value, + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, CONDITION_VALUE: TimeValues.SATURDAY.value, CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, } @@ -746,9 +743,10 @@ def test_validate_time_condition_between_days_range_invalid_condition_type(): "cond_value", [[TimeValues.SUNDAY.value, "funday"], [TimeValues.SUNDAY, TimeValues.MONDAY.value]] ) def test_validate_time_condition_between_days_range_invalid_condition_value(cond_value): - # GIVEN a configuration with a SCHEDULE_BETWEEN_DAYS action, key CURRENT_DAY_UTC and invalid value not day string + # GIVEN a configuration with a SCHEDULE_BETWEEN_DAYS_OF_WEEK action + # key CURRENT_DAY_UTC and invalid value not day string condition = { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS.value, + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, CONDITION_VALUE: cond_value, CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, } diff --git a/tests/functional/feature_flags/test_time_based_actions.py b/tests/functional/feature_flags/test_time_based_actions.py index 8a6cd857155..282982f36b1 100644 --- a/tests/functional/feature_flags/test_time_based_actions.py +++ b/tests/functional/feature_flags/test_time_based_actions.py @@ -90,8 +90,8 @@ def test_time_based_utc_in_between_time_range_rule_match(mocker, config): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, - CONDITION_VALUE: {TimeValues.START_TIME.value: "11:11", TimeValues.END_TIME.value: "23:59"}, + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_VALUE: {TimeValues.START.value: "11:11", TimeValues.END.value: "23:59"}, }, ], } @@ -122,8 +122,8 @@ def test_time_based_utc_in_between_time_range_no_rule_match(mocker, config): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, - CONDITION_VALUE: {TimeValues.START_TIME.value: "11:11", TimeValues.END_TIME.value: "23:59"}, + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_VALUE: {TimeValues.START.value: "11:11", TimeValues.END.value: "23:59"}, }, ], } @@ -155,8 +155,8 @@ def test_time_based_utc_in_between_full_time_range_rule_match(mocker, config): CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, # condition matches CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, CONDITION_VALUE: { - TimeValues.START_TIME.value: "2022-10-05T12:15:00Z", - TimeValues.END_TIME.value: "2022-10-10T12:15:00Z", + TimeValues.START.value: "2022-10-05T12:15:00Z", + TimeValues.END.value: "2022-10-10T12:15:00Z", }, }, ], @@ -190,8 +190,8 @@ def test_time_based_utc_in_between_full_time_range_no_rule_match(mocker, config) CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, # condition matches CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, CONDITION_VALUE: { - TimeValues.START_TIME.value: "2022-10-05T12:15:00Z", - TimeValues.END_TIME.value: "2022-10-10T12:15:00Z", + TimeValues.START.value: "2022-10-05T12:15:00Z", + TimeValues.END.value: "2022-10-10T12:15:00Z", }, }, ], @@ -222,8 +222,8 @@ def test_time_based_multiple_conditions_utc_in_between_time_range_rule_match(moc CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, - CONDITION_VALUE: {TimeValues.START_TIME.value: "09:00", TimeValues.END_TIME.value: "17:00"}, + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_VALUE: {TimeValues.START.value: "09:00", TimeValues.END.value: "17:00"}, }, { CONDITION_ACTION: RuleAction.EQUALS.value, @@ -259,8 +259,8 @@ def test_time_based_multiple_conditions_utc_in_between_time_range_no_rule_match( CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, - CONDITION_VALUE: {TimeValues.START_TIME.value: "09:00", TimeValues.END_TIME.value: "17:00"}, + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_VALUE: {TimeValues.START.value: "09:00", TimeValues.END.value: "17:00"}, }, { CONDITION_ACTION: RuleAction.EQUALS.value, @@ -295,7 +295,7 @@ def test_time_based_utc_days_range_rule_match(mocker, config): RULE_MATCH_VALUE: True, CONDITIONS_KEY: [ { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS.value, # this condition matches + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions CONDITION_VALUE: [ TimeValues.MONDAY.value, @@ -331,7 +331,7 @@ def test_time_based_utc_days_range_no_rule_match(mocker, config): RULE_MATCH_VALUE: True, CONDITIONS_KEY: [ { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS.value, # this condition matches + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions CONDITION_VALUE: [ TimeValues.MONDAY.value, @@ -366,7 +366,7 @@ def test_time_based_utc_only_weekend_rule_match(mocker, config): RULE_MATCH_VALUE: True, CONDITIONS_KEY: [ { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS.value, # this condition matches + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions CONDITION_VALUE: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], }, @@ -396,7 +396,7 @@ def test_time_based_utc_only_weekend_no_rule_match(mocker, config): RULE_MATCH_VALUE: True, CONDITIONS_KEY: [ { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS.value, # this condition matches + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions CONDITION_VALUE: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], }, @@ -426,11 +426,11 @@ def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_rule_ma CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, - CONDITION_VALUE: {TimeValues.START_TIME.value: "11:00", TimeValues.END_TIME.value: "23:00"}, + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_VALUE: {TimeValues.START.value: "11:00", TimeValues.END.value: "23:00"}, }, { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS.value, # this condition matches + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, CONDITION_VALUE: [TimeValues.MONDAY.value, TimeValues.THURSDAY.value], }, @@ -461,11 +461,11 @@ def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_no_rule CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_HOUR_UTC.value, - CONDITION_VALUE: {TimeValues.START_TIME.value: "11:00", TimeValues.END_TIME.value: "23:00"}, + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_VALUE: {TimeValues.START.value: "11:00", TimeValues.END.value: "23:00"}, }, { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS.value, # this condition matches + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, CONDITION_VALUE: [TimeValues.MONDAY.value, TimeValues.THURSDAY.value], }, From e88a2680159f8c127ada19fa47c357e7a3c43074 Mon Sep 17 00:00:00 2001 From: Ran Isenberg Date: Fri, 2 Dec 2022 14:52:04 +0200 Subject: [PATCH 15/34] first cr fixes --- .../utilities/feature_flags/feature_flags.py | 12 ++++----- .../utilities/feature_flags/schema.py | 27 +++++++++---------- .../feature_flags/time_conditions.py | 16 +++++------ .../feature_flags/test_schema_validation.py | 16 +++++------ .../feature_flags/test_time_based_actions.py | 12 ++++----- 5 files changed, 39 insertions(+), 44 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py index 9ee1ecb20b2..1722e2cbd40 100644 --- a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py +++ b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py @@ -7,9 +7,9 @@ from .base import StoreProvider from .exceptions import ConfigurationStoreError from .time_conditions import ( - compare_between_utc_days, - compare_utc_date_range, compare_utc_datetime_range, + compare_utc_days_of_week, + compare_utc_time_range, ) @@ -64,9 +64,9 @@ def _match_by_action(self, action: str, condition_value: Any, context_value: Any schema.RuleAction.KEY_NOT_IN_VALUE.value: lambda a, b: a not in b, schema.RuleAction.VALUE_IN_KEY.value: lambda a, b: b in a, schema.RuleAction.VALUE_NOT_IN_KEY.value: lambda a, b: b not in a, - schema.RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value: lambda a, b: compare_utc_date_range(a, b), + schema.RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value: lambda a, b: compare_utc_time_range(a, b), schema.RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value: lambda a, b: compare_utc_datetime_range(a, b), - schema.RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value: lambda a, b: compare_between_utc_days(a, b), + schema.RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value: lambda a, b: compare_utc_days_of_week(a, b), } try: @@ -244,7 +244,7 @@ def evaluate(self, *, name: str, context: Optional[Dict[str, Any]] = None, defau # method `get_matching_features` returning Dict[feature_name, feature_value] boolean_feature = feature.get( schema.FEATURE_DEFAULT_VAL_TYPE_KEY, True - ) # backwards compatibility ,assume feature flag + ) # backwards compatibility, assume feature flag if not rules: self.logger.debug( f"no rules found, returning feature default, name={name}, default={str(feat_default)}, boolean_feature={boolean_feature}" # noqa: E501 @@ -303,7 +303,7 @@ def get_enabled_features(self, *, context: Optional[Dict[str, Any]] = None) -> L feature_default_value = feature.get(schema.FEATURE_DEFAULT_VAL_KEY) boolean_feature = feature.get( schema.FEATURE_DEFAULT_VAL_TYPE_KEY, True - ) # backwards compatibility ,assume feature flag + ) # backwards compatibility, assume feature flag if feature_default_value and not rules: self.logger.debug(f"feature is enabled by default and has no defined rules, name={name}") diff --git a/aws_lambda_powertools/utilities/feature_flags/schema.py b/aws_lambda_powertools/utilities/feature_flags/schema.py index 3a2b6fe0df7..534dc6fa442 100644 --- a/aws_lambda_powertools/utilities/feature_flags/schema.py +++ b/aws_lambda_powertools/utilities/feature_flags/schema.py @@ -15,6 +15,9 @@ CONDITION_VALUE = "value" CONDITION_ACTION = "action" FEATURE_DEFAULT_VAL_TYPE_KEY = "boolean_type" +TIME_RANGE_FORMAT = "%H:%M" +DATETIME_RANGE_FORMAT = "%Y-%m-%dT%H:%M:%S%z" +HOUR_MIN_SEPARATOR = ":" class RuleAction(Enum): @@ -39,7 +42,7 @@ class RuleAction(Enum): class TimeKeys(Enum): CURRENT_TIME_UTC = "CURRENT_TIME_UTC" - CURRENT_DAY_UTC = "CURRENT_DAY_UTC" + CURRENT_DAY_OF_WEEK_UTC = "CURRENT_DAY_OF_WEEK_UTC" CURRENT_DATETIME_UTC = "CURRENT_DATETIME_UTC" @@ -249,10 +252,6 @@ def validate_rule_default_value(rule: Dict, rule_name: str, boolean_feature: boo class ConditionsValidator(BaseValidator): - - TIME_RANGE_FORMAT = "%H:%M" - DATETIME_RANGE_FORMAT = "%Y-%m-%dT%H:%M:%S%z" - def __init__(self, rule: Dict[str, Any], rule_name: str, logger: Optional[Union[logging.Logger, Logger]] = None): self.conditions: List[Dict[str, Any]] = rule.get(CONDITIONS_KEY, {}) self.rule_name = rule_name @@ -299,9 +298,9 @@ def validate_condition_key(condition: Dict[str, Any], rule_name: str): raise SchemaValidationError( f"'condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a 'CURRENT_DATETIME_UTC' condition key, rule={rule_name}" # noqa: E501 ) - if action == RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value and key != TimeKeys.CURRENT_DAY_UTC.value: + if action == RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value and key != TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value: raise SchemaValidationError( - f"'condition with a 'SCHEDULE_BETWEEN_DAYS_OF_WEEK' action must have a 'CURRENT_DAY_UTC' condition key, rule={rule_name}" # noqa: E501 + f"'condition with a 'SCHEDULE_BETWEEN_DAYS_OF_WEEK' action must have a 'CURRENT_DAY_OF_WEEK_UTC' condition key, rule={rule_name}" # noqa: E501 ) @staticmethod @@ -313,14 +312,14 @@ def validate_condition_value(condition: Dict[str, Any], rule_name: str): # time actions if action == RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value: ConditionsValidator._validate_schedule_between_time_and_datetime_ranges( - value, rule_name, action, ConditionsValidator.TIME_RANGE_FORMAT + value, rule_name, action, TIME_RANGE_FORMAT ) elif action == RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value: ConditionsValidator._validate_schedule_between_time_and_datetime_ranges( - value, rule_name, action, ConditionsValidator.DATETIME_RANGE_FORMAT + value, rule_name, action, DATETIME_RANGE_FORMAT ) elif action == RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value: - ConditionsValidator._validate_schedule_between_days(value, rule_name) + ConditionsValidator._validate_schedule_between_days_of_week(value, rule_name) @staticmethod def _validate_time_value(time: str, rule_name: str, date_format: str): @@ -332,23 +331,23 @@ def _validate_time_value(time: str, rule_name: str, date_format: str): ) @staticmethod - def _validate_schedule_between_days(value: Any, rule_name: str): + def _validate_schedule_between_days_of_week(value: Any, rule_name: str): if not isinstance(value, list) or not value: raise SchemaValidationError( - f"condition with a CURRENT_DAY_UTC action must have a non empty condition value type list, rule={rule_name}" # noqa: E501 + f"condition with a CURRENT_DAY_OF_WEEK_UTC action must have a non empty condition value type list, rule={rule_name}" # noqa: E501 ) for day in value: if not isinstance(day, str) or day not in [ - TimeValues.SUNDAY.value, TimeValues.MONDAY.value, TimeValues.TUESDAY.value, TimeValues.WEDNESDAY.value, TimeValues.THURSDAY.value, TimeValues.FRIDAY.value, TimeValues.SATURDAY.value, + TimeValues.SUNDAY.value, ]: raise SchemaValidationError( - f"condition value must represent a week day string defined in 'TimeValues' enum, rule={rule_name}" + f"condition value must represent a day of the week in 'TimeValues' enum, rule={rule_name}" ) @staticmethod diff --git a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py index 9f9bf9464e1..5b2e30e2e3e 100644 --- a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py +++ b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py @@ -1,31 +1,29 @@ from datetime import datetime, timezone from typing import Dict, List -from .schema import TimeValues - -HOUR_MIN_SEPARATOR = ":" +from .schema import DATETIME_RANGE_FORMAT, HOUR_MIN_SEPARATOR, TimeValues def _get_utc_time_now() -> datetime: return datetime.now(timezone.utc) -def compare_between_utc_days(action: str, values: List[str]) -> bool: +def compare_utc_days_of_week(action: str, values: List[str]) -> bool: current_day = _get_utc_time_now().strftime("%A").upper() return current_day in values def compare_utc_datetime_range(action: str, values: Dict) -> bool: current_time_utc: datetime = _get_utc_time_now() - start_date = datetime.strptime(values.get(TimeValues.START.value, ""), "%Y-%m-%dT%H:%M:%S%z") - end_date = datetime.strptime(values.get(TimeValues.END.value, ""), "%Y-%m-%dT%H:%M:%S%z") + start_date = datetime.strptime(values.get(TimeValues.START.value, ""), DATETIME_RANGE_FORMAT) + end_date = datetime.strptime(values.get(TimeValues.END.value, ""), DATETIME_RANGE_FORMAT) return current_time_utc >= start_date and current_time_utc <= end_date -def compare_utc_date_range(action: str, values: Dict) -> bool: +def compare_utc_time_range(action: str, values: Dict) -> bool: current_time_utc: datetime = _get_utc_time_now() - start_hour, start_min = values.get(TimeValues.START.value, HOUR_MIN_SEPARATOR).split(HOUR_MIN_SEPARATOR) - end_hour, end_min = values.get(TimeValues.END.value, HOUR_MIN_SEPARATOR).split(HOUR_MIN_SEPARATOR) + start_hour, start_min = values.get(TimeValues.START.value, "").split(HOUR_MIN_SEPARATOR) + end_hour, end_min = values.get(TimeValues.END.value, "").split(HOUR_MIN_SEPARATOR) return ( current_time_utc.hour >= int(start_hour) and current_time_utc.hour <= int(end_hour) diff --git a/tests/functional/feature_flags/test_schema_validation.py b/tests/functional/feature_flags/test_schema_validation.py index fb674ba55b2..a9f25d5628f 100644 --- a/tests/functional/feature_flags/test_schema_validation.py +++ b/tests/functional/feature_flags/test_schema_validation.py @@ -715,21 +715,21 @@ def test_validate_time_condition_between_days_range_invalid_condition_key(): # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"'condition with a 'SCHEDULE_BETWEEN_DAYS_OF_WEEK' action must have a 'CURRENT_DAY_UTC' condition key, rule={rule_name}", # noqa: E501 + match=f"'condition with a 'SCHEDULE_BETWEEN_DAYS_OF_WEEK' action must have a 'CURRENT_DAY_OF_WEEK_UTC' condition key, rule={rule_name}", # noqa: E501 ): ConditionsValidator.validate_condition_key(condition=condition, rule_name=rule_name) def test_validate_time_condition_between_days_range_invalid_condition_type(): # GIVEN a configuration with a SCHEDULE_BETWEEN_DAYS_OF_WEEK action - # key CURRENT_DAY_UTC and invalid value type string + # key CURRENT_DAY_OF_WEEK_UTC and invalid value type string condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, CONDITION_VALUE: TimeValues.SATURDAY.value, - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, } rule_name = "dummy" - match_str = f"condition with a CURRENT_DAY_UTC action must have a non empty condition value type list, rule={rule_name}" # noqa: E501 + match_str = f"condition with a CURRENT_DAY_OF_WEEK_UTC action must have a non empty condition value type list, rule={rule_name}" # noqa: E501 # WHEN calling validate_condition # THEN raise SchemaValidationError with pytest.raises( @@ -744,16 +744,14 @@ def test_validate_time_condition_between_days_range_invalid_condition_type(): ) def test_validate_time_condition_between_days_range_invalid_condition_value(cond_value): # GIVEN a configuration with a SCHEDULE_BETWEEN_DAYS_OF_WEEK action - # key CURRENT_DAY_UTC and invalid value not day string + # key CURRENT_DAY_OF_WEEK_UTC and invalid value not day string condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, CONDITION_VALUE: cond_value, - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, } rule_name = "dummy" - match_str = ( - f"condition value must represent a week day string defined in 'TimeValues' enum, rule={rule_name}" # noqa: E501 - ) + match_str = f"condition value must represent a day of the week in 'TimeValues' enum, rule={rule_name}" # noqa: E501 # WHEN calling validate_condition # THEN raise SchemaValidationError with pytest.raises( diff --git a/tests/functional/feature_flags/test_time_based_actions.py b/tests/functional/feature_flags/test_time_based_actions.py index 282982f36b1..84360daecef 100644 --- a/tests/functional/feature_flags/test_time_based_actions.py +++ b/tests/functional/feature_flags/test_time_based_actions.py @@ -296,7 +296,7 @@ def test_time_based_utc_days_range_rule_match(mocker, config): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, # similar to "IN" actions CONDITION_VALUE: [ TimeValues.MONDAY.value, TimeValues.TUESDAY.value, @@ -332,7 +332,7 @@ def test_time_based_utc_days_range_no_rule_match(mocker, config): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, # similar to "IN" actions CONDITION_VALUE: [ TimeValues.MONDAY.value, TimeValues.TUESDAY.value, @@ -367,7 +367,7 @@ def test_time_based_utc_only_weekend_rule_match(mocker, config): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, # similar to "IN" actions CONDITION_VALUE: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], }, ], @@ -397,7 +397,7 @@ def test_time_based_utc_only_weekend_no_rule_match(mocker, config): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, # similar to "IN" actions CONDITION_VALUE: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], }, ], @@ -431,7 +431,7 @@ def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_rule_ma }, { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, CONDITION_VALUE: [TimeValues.MONDAY.value, TimeValues.THURSDAY.value], }, ], @@ -466,7 +466,7 @@ def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_no_rule }, { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, CONDITION_VALUE: [TimeValues.MONDAY.value, TimeValues.THURSDAY.value], }, ], From 707a27dd7a91062cb48c715c5a9e457a7c903689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Fri, 2 Dec 2022 16:47:28 +0100 Subject: [PATCH 16/34] chore: re-wrote time based actions to reduce size --- .../feature_flags/test_time_based_actions.py | 677 +++++++----------- 1 file changed, 268 insertions(+), 409 deletions(-) diff --git a/tests/functional/feature_flags/test_time_based_actions.py b/tests/functional/feature_flags/test_time_based_actions.py index 84360daecef..8504dae6ee8 100644 --- a/tests/functional/feature_flags/test_time_based_actions.py +++ b/tests/functional/feature_flags/test_time_based_actions.py @@ -1,7 +1,6 @@ import datetime -from typing import Dict, Optional +from typing import Optional -import pytest from botocore.config import Config from aws_lambda_powertools.utilities.feature_flags.appconfig import AppConfigStore @@ -20,52 +19,23 @@ ) -@pytest.fixture(scope="module") -def config(): - return Config(region_name="us-east-1") - - -def init_feature_flags( - mocker, mock_schema: Dict, config: Config, envelope: str = "", jmespath_options: Optional[Dict] = None -) -> FeatureFlags: - mocked_get_conf = mocker.patch("aws_lambda_powertools.utilities.parameters.AppConfigProvider.get") - mocked_get_conf.return_value = mock_schema - - app_conf_fetcher = AppConfigStore( - environment="test_env", - application="test_app", - name="test_conf_name", - max_age=600, - sdk_config=config, - envelope=envelope, - jmespath_options=jmespath_options, - ) - feature_flags: FeatureFlags = FeatureFlags(store=app_conf_fetcher) - return feature_flags - - -def init_fetcher_side_effect(mocker, config: Config, side_effect) -> AppConfigStore: - mocked_get_conf = mocker.patch("aws_lambda_powertools.utilities.parameters.AppConfigProvider.get") - mocked_get_conf.side_effect = side_effect - return AppConfigStore( - environment="env", - application="application", - name="conf", - max_age=1, - sdk_config=config, - ) - - -def mock_current_utc_time( +def evaluate_mocked_schema( mocker, - year: int, - month: int, - day: int, - hour: int, - minute: int, - second: int, - msec: int, -) -> None: + rules: dict, + expected_value: bool, + mocked_time: tuple[int, int, int, int, int, int], + context: Optional[dict] = None, +): + """ + This helper does the following: + 1. mocks the current time + 2. mocks the feature flag payload returend from AppConfig + 3. evaluates the rules against the expected value + """ + + # Mock the current time + year, month, day, hour, minute, second = mocked_time + mocked_time = mocker.patch("aws_lambda_powertools.utilities.feature_flags.time_conditions._get_utc_time_now") mocked_time.return_value = datetime.datetime( year=year, @@ -74,432 +44,321 @@ def mock_current_utc_time( hour=hour, minute=minute, second=second, - microsecond=msec, + microsecond=0, tzinfo=datetime.timezone.utc, ) - -def test_time_based_utc_in_between_time_range_rule_match(mocker, config): - expected_value = True - mocked_app_config_schema = { + # Mock the returned data from AppConfig + mocked_get_conf = mocker.patch("aws_lambda_powertools.utilities.parameters.AppConfigProvider.get") + mocked_get_conf.return_value = { "my_feature": { FEATURE_DEFAULT_VAL_KEY: False, - RULES_KEY: { - "lambda time is between UTC 11:11-23:59": { - RULE_MATCH_VALUE: True, - CONDITIONS_KEY: [ - { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, - CONDITION_VALUE: {TimeValues.START.value: "11:11", TimeValues.END.value: "23:59"}, - }, - ], - } - }, + RULES_KEY: rules, } } - mock_current_utc_time(mocker, 2022, 2, 15, 11, 12, 0, 0) # will rule match - # mock time for rule match - feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) - toggle = feature_flags.evaluate( - name="my_feature", - context={}, - default=False, + # Create a dummy AppConfigStore that returns our mocked FeatureFlag + app_conf_fetcher = AppConfigStore( + environment="test_env", + application="test_app", + name="test_conf_name", + max_age=600, + sdk_config=Config(region_name="us-east-1"), ) + feature_flags: FeatureFlags = FeatureFlags(store=app_conf_fetcher) - assert toggle == expected_value - - -def test_time_based_utc_in_between_time_range_no_rule_match(mocker, config): - expected_value = False - mocked_app_config_schema = { - "my_feature": { - FEATURE_DEFAULT_VAL_KEY: False, - RULES_KEY: { - "lambda time is between UTC 11:11-23:59": { - RULE_MATCH_VALUE: True, - CONDITIONS_KEY: [ - { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, - CONDITION_VALUE: {TimeValues.START.value: "11:11", TimeValues.END.value: "23:59"}, - }, - ], - } - }, - } - } - mock_current_utc_time(mocker, 2022, 2, 15, 7, 12, 0, 0) # no rule match 7:12 am - - feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) + # Evaluate our feature flag + context = {} if context is None else context toggle = feature_flags.evaluate( name="my_feature", - context={}, + context=context, default=False, ) + # Assert result against expected value assert toggle == expected_value -def test_time_based_utc_in_between_full_time_range_rule_match(mocker, config): - expected_value = True - mocked_app_config_schema = { - "my_feature": { - FEATURE_DEFAULT_VAL_KEY: False, - RULES_KEY: { - "lambda time is between UTC october 5th 2022 12:14:32PM to october 10th 2022 12:15:00 PM": { - RULE_MATCH_VALUE: True, - CONDITIONS_KEY: [ - { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, # condition matches - CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, - CONDITION_VALUE: { - TimeValues.START.value: "2022-10-05T12:15:00Z", - TimeValues.END.value: "2022-10-10T12:15:00Z", - }, - }, - ], - } - }, - } - } +def test_time_based_utc_in_between_time_range_rule_match(mocker): + evaluate_mocked_schema( + mocker=mocker, + rules={ + "lambda time is between UTC 11:11-23:59": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_VALUE: {TimeValues.START.value: "11:11", TimeValues.END.value: "23:59"}, + }, + ], + } + }, + expected_value=True, + mocked_time=(2022, 2, 15, 11, 12, 0), + ) - mock_current_utc_time(mocker, 2022, 10, 7, 10, 0, 0, 0) # will rule match - # mock time for rule match - feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) - toggle = feature_flags.evaluate( - name="my_feature", - context={}, - default=False, +def test_time_based_utc_in_between_time_range_no_rule_match(mocker): + evaluate_mocked_schema( + mocker=mocker, + rules={ + "lambda time is between UTC 11:11-23:59": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_VALUE: {TimeValues.START.value: "11:11", TimeValues.END.value: "23:59"}, + }, + ], + } + }, + expected_value=False, + mocked_time=(2022, 2, 15, 7, 12, 0), # no rule match 7:12 am ) - assert toggle == expected_value -def test_time_based_utc_in_between_full_time_range_no_rule_match(mocker, config): - expected_value = False - mocked_app_config_schema = { - "my_feature": { - FEATURE_DEFAULT_VAL_KEY: False, - RULES_KEY: { - "lambda time is between UTC october 5th 2022 12:14:32PM to october 10th 2022 12:15:00 PM": { - RULE_MATCH_VALUE: True, - CONDITIONS_KEY: [ - { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, # condition matches - CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, - CONDITION_VALUE: { - TimeValues.START.value: "2022-10-05T12:15:00Z", - TimeValues.END.value: "2022-10-10T12:15:00Z", - }, +def test_time_based_utc_in_between_full_time_range_rule_match(mocker): + evaluate_mocked_schema( + mocker=mocker, + rules={ + "lambda time is between UTC october 5th 2022 12:14:32PM to october 10th 2022 12:15:00 PM": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, # condition matches + CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + CONDITION_VALUE: { + TimeValues.START.value: "2022-10-05T12:15:00Z", + TimeValues.END.value: "2022-10-10T12:15:00Z", }, - ], - } - }, - } - } - - mock_current_utc_time(mocker, 2022, 9, 7, 10, 0, 0, 0) # will not rule match - - feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) - toggle = feature_flags.evaluate( - name="my_feature", - context={}, - default=False, + }, + ], + } + }, + expected_value=True, + mocked_time=(2022, 10, 7, 10, 0, 0), # will match rule ) - assert toggle == expected_value -def test_time_based_multiple_conditions_utc_in_between_time_range_rule_match(mocker, config): - expected_value = True - mocked_app_config_schema = { - "my_feature": { - FEATURE_DEFAULT_VAL_KEY: False, - RULES_KEY: { - "lambda time is between UTC 09:00-17:00 and username is ran": { - RULE_MATCH_VALUE: True, - CONDITIONS_KEY: [ - { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, - CONDITION_VALUE: {TimeValues.START.value: "09:00", TimeValues.END.value: "17:00"}, +def test_time_based_utc_in_between_full_time_range_no_rule_match(mocker): + evaluate_mocked_schema( + mocker=mocker, + rules={ + "lambda time is between UTC october 5th 2022 12:14:32PM to october 10th 2022 12:15:00 PM": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, # condition matches + CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + CONDITION_VALUE: { + TimeValues.START.value: "2022-10-05T12:15:00Z", + TimeValues.END.value: "2022-10-10T12:15:00Z", }, - { - CONDITION_ACTION: RuleAction.EQUALS.value, - CONDITION_KEY: "username", - CONDITION_VALUE: "ran", - }, - ], - } - }, - } - } + }, + ], + } + }, + expected_value=False, + mocked_time=(2022, 9, 7, 10, 0, 0), # will not rule match + ) - mock_current_utc_time(mocker, 2022, 10, 7, 10, 0, 0, 0) # will rule match - # mock time for rule match - feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) - toggle = feature_flags.evaluate( - name="my_feature", +def test_time_based_multiple_conditions_utc_in_between_time_range_rule_match(mocker): + evaluate_mocked_schema( + mocker=mocker, + rules={ + "lambda time is between UTC 09:00-17:00 and username is ran": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_VALUE: {TimeValues.START.value: "09:00", TimeValues.END.value: "17:00"}, + }, + { + CONDITION_ACTION: RuleAction.EQUALS.value, + CONDITION_KEY: "username", + CONDITION_VALUE: "ran", + }, + ], + } + }, + expected_value=True, + mocked_time=(2022, 10, 7, 10, 0, 0), # will rule match context={"username": "ran"}, - default=False, ) - assert toggle == expected_value - - -def test_time_based_multiple_conditions_utc_in_between_time_range_no_rule_match(mocker, config): - expected_value = False - mocked_app_config_schema = { - "my_feature": { - FEATURE_DEFAULT_VAL_KEY: False, - RULES_KEY: { - "lambda time is between UTC 09:00-17:00 and username is ran": { - RULE_MATCH_VALUE: True, - CONDITIONS_KEY: [ - { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, - CONDITION_VALUE: {TimeValues.START.value: "09:00", TimeValues.END.value: "17:00"}, - }, - { - CONDITION_ACTION: RuleAction.EQUALS.value, - CONDITION_KEY: "username", - CONDITION_VALUE: "ran", - }, - ], - } - }, - } - } - mock_current_utc_time(mocker, 2022, 10, 7, 7, 0, 0, 0) # will cause no rule match, 7:00 - # mock time for rule match - feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) - toggle = feature_flags.evaluate( - name="my_feature", +def test_time_based_multiple_conditions_utc_in_between_time_range_no_rule_match(mocker): + evaluate_mocked_schema( + mocker=mocker, + rules={ + "lambda time is between UTC 09:00-17:00 and username is ran": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_VALUE: {TimeValues.START.value: "09:00", TimeValues.END.value: "17:00"}, + }, + { + CONDITION_ACTION: RuleAction.EQUALS.value, + CONDITION_KEY: "username", + CONDITION_VALUE: "ran", + }, + ], + } + }, + expected_value=False, + mocked_time=(2022, 10, 7, 7, 0, 0), # will cause no rule match, 7:00 context={"username": "ran"}, - default=False, ) - assert toggle == expected_value -def test_time_based_utc_days_range_rule_match(mocker, config): - expected_value = True - mocked_app_config_schema = { - "my_feature": { - FEATURE_DEFAULT_VAL_KEY: False, - RULES_KEY: { - "match only monday through friday": { - RULE_MATCH_VALUE: True, - CONDITIONS_KEY: [ - { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, # similar to "IN" actions - CONDITION_VALUE: [ - TimeValues.MONDAY.value, - TimeValues.TUESDAY.value, - TimeValues.WEDNESDAY.value, - TimeValues.THURSDAY.value, - TimeValues.FRIDAY.value, - ], - }, - ], - } - }, - } - } - # mock time for rule match - mock_current_utc_time(mocker, 2022, 11, 18, 10, 0, 0, 0) # friday - feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) - toggle = feature_flags.evaluate( - name="my_feature", - context={}, - default=False, +def test_time_based_utc_days_range_rule_match(mocker): + evaluate_mocked_schema( + mocker=mocker, + rules={ + "match only monday through friday": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions + CONDITION_VALUE: [ + TimeValues.MONDAY.value, + TimeValues.TUESDAY.value, + TimeValues.WEDNESDAY.value, + TimeValues.THURSDAY.value, + TimeValues.FRIDAY.value, + ], + }, + ], + } + }, + expected_value=True, + mocked_time=(2022, 11, 18, 10, 0, 0), # friday ) - assert toggle == expected_value -def test_time_based_utc_days_range_no_rule_match(mocker, config): - expected_value = False - mocked_app_config_schema = { - "my_feature": { - FEATURE_DEFAULT_VAL_KEY: False, - RULES_KEY: { - "match only monday through friday": { - RULE_MATCH_VALUE: True, - CONDITIONS_KEY: [ - { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, # similar to "IN" actions - CONDITION_VALUE: [ - TimeValues.MONDAY.value, - TimeValues.TUESDAY.value, - TimeValues.WEDNESDAY.value, - TimeValues.THURSDAY.value, - TimeValues.FRIDAY.value, - ], - }, - ], - } - }, - } - } - mock_current_utc_time(mocker, 2022, 11, 20, 10, 0, 0, 0) # sunday, no match - feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) - toggle = feature_flags.evaluate( - name="my_feature", - context={}, - default=False, +def test_time_based_utc_days_range_no_rule_match(mocker): + evaluate_mocked_schema( + mocker=mocker, + rules={ + "match only monday through friday": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions + CONDITION_VALUE: [ + TimeValues.MONDAY.value, + TimeValues.TUESDAY.value, + TimeValues.WEDNESDAY.value, + TimeValues.THURSDAY.value, + TimeValues.FRIDAY.value, + ], + }, + ], + } + }, + expected_value=False, + mocked_time=(2022, 11, 20, 10, 0, 0), # sunday, no match ) - assert toggle == expected_value -def test_time_based_utc_only_weekend_rule_match(mocker, config): - expected_value = True - mocked_app_config_schema = { - "my_feature": { - FEATURE_DEFAULT_VAL_KEY: False, - RULES_KEY: { - "match only on weekend": { - RULE_MATCH_VALUE: True, - CONDITIONS_KEY: [ - { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, # similar to "IN" actions - CONDITION_VALUE: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], - }, - ], - } - }, - } - } - # mock time for rule match - mock_current_utc_time(mocker, 2022, 11, 19, 10, 0, 0, 0) # saturday - feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) - toggle = feature_flags.evaluate( - name="my_feature", - context={}, - default=False, +def test_time_based_utc_only_weekend_rule_match(mocker): + evaluate_mocked_schema( + mocker=mocker, + rules={ + "match only on weekend": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions + CONDITION_VALUE: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], + }, + ], + } + }, + expected_value=True, + mocked_time=(2022, 11, 19, 10, 0, 0), # saturday ) - assert toggle == expected_value -def test_time_based_utc_only_weekend_no_rule_match(mocker, config): - expected_value = False - mocked_app_config_schema = { - "my_feature": { - FEATURE_DEFAULT_VAL_KEY: False, - RULES_KEY: { - "match only on weekend": { - RULE_MATCH_VALUE: True, - CONDITIONS_KEY: [ - { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, # similar to "IN" actions - CONDITION_VALUE: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], - }, - ], - } - }, - } - } - mock_current_utc_time(mocker, 2022, 11, 18, 10, 0, 0, 0) # friday, no match - feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) - toggle = feature_flags.evaluate( - name="my_feature", - context={}, - default=False, +def test_time_based_utc_only_weekend_no_rule_match(mocker): + evaluate_mocked_schema( + mocker=mocker, + rules={ + "match only on weekend": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions + CONDITION_VALUE: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], + }, + ], + } + }, + expected_value=False, + mocked_time=(2022, 11, 18, 10, 0, 0), # friday, no match ) - assert toggle == expected_value -def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_rule_match(mocker, config): - expected_value = True - mocked_app_config_schema = { - "my_feature": { - FEATURE_DEFAULT_VAL_KEY: False, - RULES_KEY: { - "match when lambda time is between UTC 11:00-23:00 and day is either monday or thursday": { - RULE_MATCH_VALUE: True, - CONDITIONS_KEY: [ - { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, - CONDITION_VALUE: {TimeValues.START.value: "11:00", TimeValues.END.value: "23:00"}, - }, - { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, - CONDITION_VALUE: [TimeValues.MONDAY.value, TimeValues.THURSDAY.value], - }, - ], - } - }, - } - } - # mock time for rule match - mock_current_utc_time(mocker, 2022, 11, 17, 16, 0, 0, 0) # thursday 16:00 - feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) - toggle = feature_flags.evaluate( - name="my_feature", - context={}, - default=False, +def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_rule_match(mocker): + evaluate_mocked_schema( + mocker=mocker, + rules={ + "match when lambda time is between UTC 11:00-23:00 and day is either monday or thursday": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_VALUE: {TimeValues.START.value: "11:00", TimeValues.END.value: "23:00"}, + }, + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, + CONDITION_VALUE: [TimeValues.MONDAY.value, TimeValues.THURSDAY.value], + }, + ], + } + }, + expected_value=True, + mocked_time=(2022, 11, 17, 16, 0, 0), # thursday 16:00 ) - assert toggle == expected_value -def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_no_rule_match(mocker, config): - expected_value = False - mocked_app_config_schema = { - "my_feature": { - FEATURE_DEFAULT_VAL_KEY: False, - RULES_KEY: { +def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_no_rule_match(mocker): + def evaluate(mocked_time: tuple[int, int, int, int, int, int], expected_value: bool): + evaluate_mocked_schema( + mocker=mocker, + rules={ "match when lambda time is between UTC 11:00-23:00 and day is either monday or thursday": { RULE_MATCH_VALUE: True, CONDITIONS_KEY: [ { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, CONDITION_VALUE: {TimeValues.START.value: "11:00", TimeValues.END.value: "23:00"}, }, { - CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, + CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, CONDITION_VALUE: [TimeValues.MONDAY.value, TimeValues.THURSDAY.value], }, ], } }, - } - } - # first condition fail, second match - mock_current_utc_time(mocker, 2022, 11, 17, 9, 0, 0, 0) # thursday 9:00 - feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) - toggle = feature_flags.evaluate( - name="my_feature", - context={}, - default=False, - ) - assert toggle == expected_value - - # second condition fail, first match - mock_current_utc_time(mocker, 2022, 11, 18, 13, 0, 0, 0) # friday 16:00 - feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) - toggle = feature_flags.evaluate( - name="my_feature", - context={}, - default=False, - ) - assert toggle == expected_value + expected_value=expected_value, + mocked_time=mocked_time, + ) - # both conditions fail - mock_current_utc_time(mocker, 2022, 11, 18, 9, 0, 0, 0) # friday 9:00 - feature_flags = init_feature_flags(mocker, mocked_app_config_schema, config) - toggle = feature_flags.evaluate( - name="my_feature", - context={}, - default=False, - ) - assert toggle == expected_value + evaluate(mocked_time=(2022, 11, 17, 9, 0, 0), expected_value=False) # thursday 9:00 + evaluate(mocked_time=(2022, 11, 18, 13, 0, 0), expected_value=False) # friday 16:00 + evaluate(mocked_time=(2022, 11, 18, 9, 0, 0), expected_value=False) # friday 9:00 From 00e47a39ac23f94bff1793ece5a16efbf49131a7 Mon Sep 17 00:00:00 2001 From: Ran Isenberg Date: Fri, 2 Dec 2022 18:38:45 +0200 Subject: [PATCH 17/34] fix tests and typo --- .../feature_flags/test_time_based_actions.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/functional/feature_flags/test_time_based_actions.py b/tests/functional/feature_flags/test_time_based_actions.py index 8504dae6ee8..22d977f27a1 100644 --- a/tests/functional/feature_flags/test_time_based_actions.py +++ b/tests/functional/feature_flags/test_time_based_actions.py @@ -29,7 +29,7 @@ def evaluate_mocked_schema( """ This helper does the following: 1. mocks the current time - 2. mocks the feature flag payload returend from AppConfig + 2. mocks the feature flag payload returned from AppConfig 3. evaluates the rules against the expected value """ @@ -226,7 +226,7 @@ def test_time_based_utc_days_range_rule_match(mocker): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, # similar to "IN" actions CONDITION_VALUE: [ TimeValues.MONDAY.value, TimeValues.TUESDAY.value, @@ -252,7 +252,7 @@ def test_time_based_utc_days_range_no_rule_match(mocker): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, # similar to "IN" actions CONDITION_VALUE: [ TimeValues.MONDAY.value, TimeValues.TUESDAY.value, @@ -278,7 +278,7 @@ def test_time_based_utc_only_weekend_rule_match(mocker): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, # similar to "IN" actions CONDITION_VALUE: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], }, ], @@ -298,7 +298,7 @@ def test_time_based_utc_only_weekend_no_rule_match(mocker): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, # similar to "IN" actions + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, # similar to "IN" actions CONDITION_VALUE: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], }, ], @@ -323,7 +323,7 @@ def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_rule_ma }, { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, CONDITION_VALUE: [TimeValues.MONDAY.value, TimeValues.THURSDAY.value], }, ], @@ -349,7 +349,7 @@ def evaluate(mocked_time: tuple[int, int, int, int, int, int], expected_value: b }, { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, - CONDITION_KEY: TimeKeys.CURRENT_DAY_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, CONDITION_VALUE: [TimeValues.MONDAY.value, TimeValues.THURSDAY.value], }, ], From 41d63d6aa038b018ab39bf13ff1e828c4bf66858 Mon Sep 17 00:00:00 2001 From: Ran Isenberg Date: Fri, 2 Dec 2022 19:06:42 +0200 Subject: [PATCH 18/34] no mo' fixture --- tests/functional/feature_flags/test_time_based_actions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/functional/feature_flags/test_time_based_actions.py b/tests/functional/feature_flags/test_time_based_actions.py index 22d977f27a1..45b64d697ea 100644 --- a/tests/functional/feature_flags/test_time_based_actions.py +++ b/tests/functional/feature_flags/test_time_based_actions.py @@ -1,5 +1,5 @@ import datetime -from typing import Optional +from typing import Any, Dict, Optional, Tuple from botocore.config import Config @@ -21,10 +21,10 @@ def evaluate_mocked_schema( mocker, - rules: dict, + rules: Dict[str, Any], expected_value: bool, - mocked_time: tuple[int, int, int, int, int, int], - context: Optional[dict] = None, + mocked_time: Tuple[int, int, int, int, int, int], # year, month, day, hour, minute, second + context: Optional[Dict[str, Any]] = None, ): """ This helper does the following: From 03ad6ae6a99774687355ec8cdba412d6797a0798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Tue, 6 Dec 2022 13:26:29 +0100 Subject: [PATCH 19/34] chore: change strptime with fromisoformat --- .../utilities/feature_flags/schema.py | 28 ++++++++++++------- .../feature_flags/time_conditions.py | 14 +++++++--- .../feature_flags/test_schema_validation.py | 15 +++------- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/schema.py b/aws_lambda_powertools/utilities/feature_flags/schema.py index 534dc6fa442..cb30c815d4f 100644 --- a/aws_lambda_powertools/utilities/feature_flags/schema.py +++ b/aws_lambda_powertools/utilities/feature_flags/schema.py @@ -1,7 +1,7 @@ import logging from datetime import datetime from enum import Enum -from typing import Any, Dict, List, Optional, Union +from typing import Any, Callable, Dict, List, Optional, Union from ... import Logger from .base import BaseValidator @@ -16,7 +16,6 @@ CONDITION_ACTION = "action" FEATURE_DEFAULT_VAL_TYPE_KEY = "boolean_type" TIME_RANGE_FORMAT = "%H:%M" -DATETIME_RANGE_FORMAT = "%Y-%m-%dT%H:%M:%S%z" HOUR_MIN_SEPARATOR = ":" @@ -312,22 +311,31 @@ def validate_condition_value(condition: Dict[str, Any], rule_name: str): # time actions if action == RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value: ConditionsValidator._validate_schedule_between_time_and_datetime_ranges( - value, rule_name, action, TIME_RANGE_FORMAT + value, rule_name, action, ConditionsValidator._validate_time_value ) elif action == RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value: ConditionsValidator._validate_schedule_between_time_and_datetime_ranges( - value, rule_name, action, DATETIME_RANGE_FORMAT + value, rule_name, action, ConditionsValidator._validate_datetime_value ) elif action == RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value: ConditionsValidator._validate_schedule_between_days_of_week(value, rule_name) @staticmethod - def _validate_time_value(time: str, rule_name: str, date_format: str): + def _validate_datetime_value(datetime_str: str, rule_name: str): try: - datetime.strptime(time, date_format) + # python < 3.11 don't support the Z timezone on datetime.fromisoformat, + # so we replace any Z with the equivalent "+00:00" + datetime.fromisoformat(datetime_str.replace("Z", "+00:00")) + except Exception: + raise SchemaValidationError(f"'START' and 'END' must be a valid ISO8601 time format, rule={rule_name}") + + @staticmethod + def _validate_time_value(time: str, rule_name: str): + try: + datetime.strptime(time, TIME_RANGE_FORMAT) except Exception: raise SchemaValidationError( - f"'START' and 'END' must be a valid time format, time_format={date_format}, rule={rule_name}" + f"'START' and 'END' must be a valid time format, time_format={TIME_RANGE_FORMAT}, rule={rule_name}" ) @staticmethod @@ -352,7 +360,7 @@ def _validate_schedule_between_days_of_week(value: Any, rule_name: str): @staticmethod def _validate_schedule_between_time_and_datetime_ranges( - value: Any, rule_name: str, action_name: str, date_format: str + value: Any, rule_name: str, action_name: str, validator: Callable[[str, str], None] ): 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 if not isinstance(value, dict): @@ -363,5 +371,5 @@ def _validate_schedule_between_time_and_datetime_ranges( raise SchemaValidationError(error_str) if not isinstance(start_time, str) or not isinstance(end_time, str): raise SchemaValidationError(f"'START' and 'END' must be a non empty string, rule={rule_name}") - ConditionsValidator._validate_time_value(start_time, rule_name, date_format) - ConditionsValidator._validate_time_value(end_time, rule_name, date_format) + validator(start_time, rule_name) + validator(end_time, rule_name) diff --git a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py index 5b2e30e2e3e..9ec9369fb57 100644 --- a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py +++ b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py @@ -1,7 +1,7 @@ from datetime import datetime, timezone from typing import Dict, List -from .schema import DATETIME_RANGE_FORMAT, HOUR_MIN_SEPARATOR, TimeValues +from .schema import HOUR_MIN_SEPARATOR, TimeValues def _get_utc_time_now() -> datetime: @@ -15,9 +15,15 @@ def compare_utc_days_of_week(action: str, values: List[str]) -> bool: def compare_utc_datetime_range(action: str, values: Dict) -> bool: current_time_utc: datetime = _get_utc_time_now() - start_date = datetime.strptime(values.get(TimeValues.START.value, ""), DATETIME_RANGE_FORMAT) - end_date = datetime.strptime(values.get(TimeValues.END.value, ""), DATETIME_RANGE_FORMAT) - return current_time_utc >= start_date and current_time_utc <= end_date + + # python < 3.11 don't support Z as a timezone on datetime.fromisoformat, + # so we replace any Z with the equivalent "+00:00 + start_date_str = values.get(TimeValues.START.value, "").replace("Z", "+00:00") + end_date_str = values.get(TimeValues.END.value, "").replace("Z", "+00:00") + + start_date = datetime.fromisoformat(start_date_str) + end_date = datetime.fromisoformat(end_date_str) + return start_date <= current_time_utc <= end_date def compare_utc_time_range(action: str, values: Dict) -> bool: diff --git a/tests/functional/feature_flags/test_schema_validation.py b/tests/functional/feature_flags/test_schema_validation.py index a9f25d5628f..2be8f56e11d 100644 --- a/tests/functional/feature_flags/test_schema_validation.py +++ b/tests/functional/feature_flags/test_schema_validation.py @@ -668,7 +668,7 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, } rule_name = "dummy" - match_str = f"START' and 'END' must be a valid time format, time_format=%Y-%m-%dT%H:%M:%S%z, rule={rule_name}" + match_str = f"START' and 'END' must be a valid ISO8601 time format, rule={rule_name}" # WHEN calling validate_condition # THEN raise SchemaValidationError with pytest.raises( @@ -678,23 +678,16 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) -@pytest.mark.parametrize( - "cond_value", - [ - {TimeValues.END.value: "10:10", TimeValues.START.value: "2022-10-10T12:15:00Z"}, - {TimeValues.END.value: "2022-10-10", TimeValues.START.value: "2022-10-10T12:15:00Z"}, - ], -) -def test_validate_time_condition_between_datetime_range_invalid_condition_value_invalid_end_time_value(cond_value): +def test_validate_time_condition_between_datetime_range_invalid_condition_value_invalid_end_time_value(): # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and # invalid END value as an invalid time format condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, - CONDITION_VALUE: cond_value, + CONDITION_VALUE: {TimeValues.END.value: "10:10", TimeValues.START.value: "2022-10-10T12:15:00Z"}, CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, } rule_name = "dummy" - match_str = f"START' and 'END' must be a valid time format, time_format=%Y-%m-%dT%H:%M:%S%z, rule={rule_name}" + match_str = f"START' and 'END' must be a valid ISO8601 time format, rule={rule_name}" # WHEN calling validate_condition # THEN raise SchemaValidationError with pytest.raises(SchemaValidationError, match=match_str): From 2dfbfc32ff43e204dcf00cc93a2f7a8a06a31b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Tue, 6 Dec 2022 13:44:35 +0100 Subject: [PATCH 20/34] fix: tests under py3.9 --- tests/functional/feature_flags/test_time_based_actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/feature_flags/test_time_based_actions.py b/tests/functional/feature_flags/test_time_based_actions.py index 45b64d697ea..d485bad9990 100644 --- a/tests/functional/feature_flags/test_time_based_actions.py +++ b/tests/functional/feature_flags/test_time_based_actions.py @@ -335,7 +335,7 @@ def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_rule_ma def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_no_rule_match(mocker): - def evaluate(mocked_time: tuple[int, int, int, int, int, int], expected_value: bool): + def evaluate(mocked_time: Tuple[int, int, int, int, int, int], expected_value: bool): evaluate_mocked_schema( mocker=mocker, rules={ From 0b77d7f5db04ddf92946d69eb7bdf0c3bdb12665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Tue, 6 Dec 2022 15:16:12 +0100 Subject: [PATCH 21/34] chore: add initial documentation --- docs/utilities/feature_flags.md | 79 +++++++++++++++---- .../feature_flags/src/timebased_feature.py | 16 ++++ .../src/timebased_feature_event.json | 5 ++ .../feature_flags/src/timebased_features.json | 27 +++++++ .../src/timebased_happyhour_feature.py | 13 +++ .../src/timebased_happyhour_features.json | 20 +++++ 6 files changed, 146 insertions(+), 14 deletions(-) create mode 100644 examples/feature_flags/src/timebased_feature.py create mode 100644 examples/feature_flags/src/timebased_feature_event.json create mode 100644 examples/feature_flags/src/timebased_features.json create mode 100644 examples/feature_flags/src/timebased_happyhour_feature.py create mode 100644 examples/feature_flags/src/timebased_happyhour_features.json diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index ec4c28699e7..0b54efa2f9c 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -447,6 +447,44 @@ Feature flags can return any JSON values when `boolean_type` parameter is set to } ``` +#### Time based feature flags + +Feature flags can also return enabled features based on time or datetime ranges. +This allows you to have features that are only enabled on certain days of the week, certain time +intervals or between certain calendar dates. + +=== "app.py" + + ```python hl_lines="12" + --8<-- "examples/feature_flags/src/timebased_feature.py" + ``` + +=== "event.json" + + ```json hl_lines="3" + --8<-- "examples/feature_flags/src/timebased_feature_event.json" + ``` + +=== "features.json" + + ```json hl_lines="15 19-21" + --8<-- "examples/feature_flags/src/timebased_features.json" + ``` + +You can also have features enabled only at certain times of the day. + +=== "app.py" + + ```python hl_lines="9" + --8<-- "examples/feature_flags/src/timebased_happyhour_feature.py" + ``` + +=== "features.json" + + ```json hl_lines="9-14" + --8<-- "examples/feature_flags/src/timebased_happyhour_features.json" + ``` + ## Advanced ### Adjusting in-memory cache @@ -580,24 +618,37 @@ The `conditions` block is a list of conditions that contain `action`, `key`, and The `action` configuration can have the following values, where the expressions **`a`** is the `key` and **`b`** is the `value` above: -| Action | Equivalent expression | -| ----------------------------------- | ------------------------------ | -| **EQUALS** | `lambda a, b: a == b` | -| **NOT_EQUALS** | `lambda a, b: a != b` | -| **KEY_GREATER_THAN_VALUE** | `lambda a, b: a > b` | -| **KEY_GREATER_THAN_OR_EQUAL_VALUE** | `lambda a, b: a >= b` | -| **KEY_LESS_THAN_VALUE** | `lambda a, b: a < b` | -| **KEY_LESS_THAN_OR_EQUAL_VALUE** | `lambda a, b: a <= b` | -| **STARTSWITH** | `lambda a, b: a.startswith(b)` | -| **ENDSWITH** | `lambda a, b: a.endswith(b)` | -| **KEY_IN_VALUE** | `lambda a, b: a in b` | -| **KEY_NOT_IN_VALUE** | `lambda a, b: a not in b` | -| **VALUE_IN_KEY** | `lambda a, b: b in a` | -| **VALUE_NOT_IN_KEY** | `lambda a, b: b not in a` | +| Action | Equivalent expression | +|-------------------------------------|----------------------------------------------------------| +| **EQUALS** | `lambda a, b: a == b` | +| **NOT_EQUALS** | `lambda a, b: a != b` | +| **KEY_GREATER_THAN_VALUE** | `lambda a, b: a > b` | +| **KEY_GREATER_THAN_OR_EQUAL_VALUE** | `lambda a, b: a >= b` | +| **KEY_LESS_THAN_VALUE** | `lambda a, b: a < b` | +| **KEY_LESS_THAN_OR_EQUAL_VALUE** | `lambda a, b: a <= b` | +| **STARTSWITH** | `lambda a, b: a.startswith(b)` | +| **ENDSWITH** | `lambda a, b: a.endswith(b)` | +| **KEY_IN_VALUE** | `lambda a, b: a in b` | +| **KEY_NOT_IN_VALUE** | `lambda a, b: a not in b` | +| **VALUE_IN_KEY** | `lambda a, b: b in a` | +| **VALUE_NOT_IN_KEY** | `lambda a, b: b not in a` | +| **SCHEDULE_BETWEEN_TIME_RANGE** | `lambda a, b: time(a).start <= b <= time(a).end` | +| **SCHEDULE_BETWEEN_DATETIME_RANGE** | `lambda a, b: datetime(a).start <= b <= datetime(b).end` | +| **SCHEDULE_BETWEEN_DAYS_OF_WEEK** | `lambda a, b: day_of_week(a) in b` | ???+ info The `**key**` and `**value**` will be compared to the input from the `**context**` parameter. +???+ "Time based keys" + + For time based keys, we provide a list of predefined keys. These will automatically get converted to the corresponding timestamp on each invocation of your Lambda function. + + | Key | Meaning (always in UTC) | + |-------------------------|--------------------------------------------------------------------------| + | CURRENT_TIME_UTC | The current time, 24 hour format (HH:mm) | + | CURRENT_DATETIME_UTC | The current datetime ([ISO8601](https://en.wikipedia.org/wiki/ISO_8601)) | + | CURRENT_DAY_OF_WEEK_UTC | The current day of the week (Monday-Sunday) | + **For multiple conditions**, we will evaluate the list of conditions as a logical `AND`, so all conditions needs to match to return `when_match` value. #### Rule engine flowchart diff --git a/examples/feature_flags/src/timebased_feature.py b/examples/feature_flags/src/timebased_feature.py new file mode 100644 index 00000000000..0b0963489f4 --- /dev/null +++ b/examples/feature_flags/src/timebased_feature.py @@ -0,0 +1,16 @@ +from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags + +app_config = AppConfigStore(environment="dev", application="product-catalogue", name="features") + +feature_flags = FeatureFlags(store=app_config) + + +def lambda_handler(event, context): + # Get customer's tier from incoming request + ctx = {"tier": event.get("tier", "standard")} + + weekend_premium_discount = feature_flags.evaluate(name="weekend_premium_discount", default=False, context=ctx) + + if weekend_premium_discount: + # Enable special discount for premium members on weekends + pass diff --git a/examples/feature_flags/src/timebased_feature_event.json b/examples/feature_flags/src/timebased_feature_event.json new file mode 100644 index 00000000000..66067eb487b --- /dev/null +++ b/examples/feature_flags/src/timebased_feature_event.json @@ -0,0 +1,5 @@ +{ + "username": "rubefons", + "tier": "premium", + "basked_id": "random_id" +} diff --git a/examples/feature_flags/src/timebased_features.json b/examples/feature_flags/src/timebased_features.json new file mode 100644 index 00000000000..77394640eaa --- /dev/null +++ b/examples/feature_flags/src/timebased_features.json @@ -0,0 +1,27 @@ +{ + "weekend_premium_discount": { + "default": false, + "rules": { + "customer tier equals premium": { + "when_match": true, + "conditions": [ + { + "action": "EQUALS", + "key": "tier", + "value": "premium" + } + ] + }, + "is weekend": { + "when_match": true, + "conditions": [ + { + "action": "SCHEDULE_BETWEEN_DAYS_OF_WEEK", + "key": "CURRENT_DAY_OF_WEEK_UTC", + "value": ["SATURDAY", "SUNDAY"] + } + ] + } + } + } +} diff --git a/examples/feature_flags/src/timebased_happyhour_feature.py b/examples/feature_flags/src/timebased_happyhour_feature.py new file mode 100644 index 00000000000..b008481c722 --- /dev/null +++ b/examples/feature_flags/src/timebased_happyhour_feature.py @@ -0,0 +1,13 @@ +from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags + +app_config = AppConfigStore(environment="dev", application="product-catalogue", name="features") + +feature_flags = FeatureFlags(store=app_config) + + +def lambda_handler(event, context): + is_happy_hour = feature_flags.evaluate(name="happy_hour", default=False) + + if is_happy_hour: + # Apply special discount + pass diff --git a/examples/feature_flags/src/timebased_happyhour_features.json b/examples/feature_flags/src/timebased_happyhour_features.json new file mode 100644 index 00000000000..cd49f385663 --- /dev/null +++ b/examples/feature_flags/src/timebased_happyhour_features.json @@ -0,0 +1,20 @@ +{ + "happy_hour": { + "default": false, + "rules": { + "is happy hour": { + "when_match": true, + "conditions": [ + { + "action": "SCHEDULE_BETWEEN_TIME_RANGE", + "key": "CURRENT_TIME_UTC", + "value": { + "START": "17:00", + "END": "19:00" + } + } + ] + } + } + } +} From 2e6519da6fecc6faf6e53f54c374239621f5b191 Mon Sep 17 00:00:00 2001 From: Ran Isenberg Date: Sat, 17 Dec 2022 21:25:42 +0200 Subject: [PATCH 22/34] cr fix --- .../feature_flags/test_time_based_actions.py | 52 +++++++------------ 1 file changed, 18 insertions(+), 34 deletions(-) diff --git a/tests/functional/feature_flags/test_time_based_actions.py b/tests/functional/feature_flags/test_time_based_actions.py index d485bad9990..1df39d2eb60 100644 --- a/tests/functional/feature_flags/test_time_based_actions.py +++ b/tests/functional/feature_flags/test_time_based_actions.py @@ -22,15 +22,14 @@ def evaluate_mocked_schema( mocker, rules: Dict[str, Any], - expected_value: bool, mocked_time: Tuple[int, int, int, int, int, int], # year, month, day, hour, minute, second context: Optional[Dict[str, Any]] = None, -): +) -> bool: """ This helper does the following: 1. mocks the current time 2. mocks the feature flag payload returned from AppConfig - 3. evaluates the rules against the expected value + 3. evaluates the rules and return True for a rule match, otherwise a False """ # Mock the current time @@ -69,18 +68,15 @@ def evaluate_mocked_schema( # Evaluate our feature flag context = {} if context is None else context - toggle = feature_flags.evaluate( + return feature_flags.evaluate( name="my_feature", context=context, default=False, ) - # Assert result against expected value - assert toggle == expected_value - def test_time_based_utc_in_between_time_range_rule_match(mocker): - evaluate_mocked_schema( + assert evaluate_mocked_schema( mocker=mocker, rules={ "lambda time is between UTC 11:11-23:59": { @@ -94,13 +90,12 @@ def test_time_based_utc_in_between_time_range_rule_match(mocker): ], } }, - expected_value=True, mocked_time=(2022, 2, 15, 11, 12, 0), ) def test_time_based_utc_in_between_time_range_no_rule_match(mocker): - evaluate_mocked_schema( + assert not evaluate_mocked_schema( mocker=mocker, rules={ "lambda time is between UTC 11:11-23:59": { @@ -114,13 +109,12 @@ def test_time_based_utc_in_between_time_range_no_rule_match(mocker): ], } }, - expected_value=False, mocked_time=(2022, 2, 15, 7, 12, 0), # no rule match 7:12 am ) def test_time_based_utc_in_between_full_time_range_rule_match(mocker): - evaluate_mocked_schema( + assert evaluate_mocked_schema( mocker=mocker, rules={ "lambda time is between UTC october 5th 2022 12:14:32PM to october 10th 2022 12:15:00 PM": { @@ -137,13 +131,12 @@ def test_time_based_utc_in_between_full_time_range_rule_match(mocker): ], } }, - expected_value=True, mocked_time=(2022, 10, 7, 10, 0, 0), # will match rule ) def test_time_based_utc_in_between_full_time_range_no_rule_match(mocker): - evaluate_mocked_schema( + assert not evaluate_mocked_schema( mocker=mocker, rules={ "lambda time is between UTC october 5th 2022 12:14:32PM to october 10th 2022 12:15:00 PM": { @@ -160,13 +153,12 @@ def test_time_based_utc_in_between_full_time_range_no_rule_match(mocker): ], } }, - expected_value=False, mocked_time=(2022, 9, 7, 10, 0, 0), # will not rule match ) def test_time_based_multiple_conditions_utc_in_between_time_range_rule_match(mocker): - evaluate_mocked_schema( + assert evaluate_mocked_schema( mocker=mocker, rules={ "lambda time is between UTC 09:00-17:00 and username is ran": { @@ -185,14 +177,13 @@ def test_time_based_multiple_conditions_utc_in_between_time_range_rule_match(moc ], } }, - expected_value=True, mocked_time=(2022, 10, 7, 10, 0, 0), # will rule match context={"username": "ran"}, ) def test_time_based_multiple_conditions_utc_in_between_time_range_no_rule_match(mocker): - evaluate_mocked_schema( + assert not evaluate_mocked_schema( mocker=mocker, rules={ "lambda time is between UTC 09:00-17:00 and username is ran": { @@ -211,14 +202,13 @@ def test_time_based_multiple_conditions_utc_in_between_time_range_no_rule_match( ], } }, - expected_value=False, mocked_time=(2022, 10, 7, 7, 0, 0), # will cause no rule match, 7:00 context={"username": "ran"}, ) def test_time_based_utc_days_range_rule_match(mocker): - evaluate_mocked_schema( + assert evaluate_mocked_schema( mocker=mocker, rules={ "match only monday through friday": { @@ -238,13 +228,12 @@ def test_time_based_utc_days_range_rule_match(mocker): ], } }, - expected_value=True, mocked_time=(2022, 11, 18, 10, 0, 0), # friday ) def test_time_based_utc_days_range_no_rule_match(mocker): - evaluate_mocked_schema( + assert not evaluate_mocked_schema( mocker=mocker, rules={ "match only monday through friday": { @@ -264,13 +253,12 @@ def test_time_based_utc_days_range_no_rule_match(mocker): ], } }, - expected_value=False, mocked_time=(2022, 11, 20, 10, 0, 0), # sunday, no match ) def test_time_based_utc_only_weekend_rule_match(mocker): - evaluate_mocked_schema( + assert evaluate_mocked_schema( mocker=mocker, rules={ "match only on weekend": { @@ -284,13 +272,12 @@ def test_time_based_utc_only_weekend_rule_match(mocker): ], } }, - expected_value=True, mocked_time=(2022, 11, 19, 10, 0, 0), # saturday ) def test_time_based_utc_only_weekend_no_rule_match(mocker): - evaluate_mocked_schema( + assert not evaluate_mocked_schema( mocker=mocker, rules={ "match only on weekend": { @@ -304,13 +291,12 @@ def test_time_based_utc_only_weekend_no_rule_match(mocker): ], } }, - expected_value=False, mocked_time=(2022, 11, 18, 10, 0, 0), # friday, no match ) def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_rule_match(mocker): - evaluate_mocked_schema( + assert evaluate_mocked_schema( mocker=mocker, rules={ "match when lambda time is between UTC 11:00-23:00 and day is either monday or thursday": { @@ -329,13 +315,12 @@ def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_rule_ma ], } }, - expected_value=True, mocked_time=(2022, 11, 17, 16, 0, 0), # thursday 16:00 ) def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_no_rule_match(mocker): - def evaluate(mocked_time: Tuple[int, int, int, int, int, int], expected_value: bool): + def evaluate(mocked_time: Tuple[int, int, int, int, int, int]): evaluate_mocked_schema( mocker=mocker, rules={ @@ -355,10 +340,9 @@ def evaluate(mocked_time: Tuple[int, int, int, int, int, int], expected_value: b ], } }, - expected_value=expected_value, mocked_time=mocked_time, ) - evaluate(mocked_time=(2022, 11, 17, 9, 0, 0), expected_value=False) # thursday 9:00 - evaluate(mocked_time=(2022, 11, 18, 13, 0, 0), expected_value=False) # friday 16:00 - evaluate(mocked_time=(2022, 11, 18, 9, 0, 0), expected_value=False) # friday 9:00 + assert not evaluate(mocked_time=(2022, 11, 17, 9, 0, 0)) # thursday 9:00 + assert not evaluate(mocked_time=(2022, 11, 18, 13, 0, 0)) # friday 16:00 + assert not evaluate(mocked_time=(2022, 11, 18, 9, 0, 0)) # friday 9:00 From d550d0358c9198981498eadb4c037726781063a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Thu, 5 Jan 2023 15:52:09 +0100 Subject: [PATCH 23/34] fix(docs): added use cases to documentation --- docs/utilities/feature_flags.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index 0b54efa2f9c..8361dcb4737 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -453,6 +453,12 @@ Feature flags can also return enabled features based on time or datetime ranges. This allows you to have features that are only enabled on certain days of the week, certain time intervals or between certain calendar dates. +Use cases: + +* Enable maintenance mode during a weekend +* Disable support/chat feature after working hours +* Launch a new feature on a specific date and time + === "app.py" ```python hl_lines="12" From ebe5abb71ba7dc73f4bf2f47d3cf92047585a1ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Fri, 6 Jan 2023 10:24:13 +0100 Subject: [PATCH 24/34] fix: improve performance by removing one more srptime --- aws_lambda_powertools/utilities/feature_flags/schema.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/schema.py b/aws_lambda_powertools/utilities/feature_flags/schema.py index cb30c815d4f..f091dc1e871 100644 --- a/aws_lambda_powertools/utilities/feature_flags/schema.py +++ b/aws_lambda_powertools/utilities/feature_flags/schema.py @@ -1,4 +1,5 @@ import logging +import re from datetime import datetime from enum import Enum from typing import Any, Callable, Dict, List, Optional, Union @@ -16,6 +17,7 @@ CONDITION_ACTION = "action" FEATURE_DEFAULT_VAL_TYPE_KEY = "boolean_type" TIME_RANGE_FORMAT = "%H:%M" +TIME_RANGE_RE_PATTERN = re.compile(r"([0-2]\d):[0-5]\d") HOUR_MIN_SEPARATOR = ":" @@ -331,9 +333,10 @@ def _validate_datetime_value(datetime_str: str, rule_name: str): @staticmethod def _validate_time_value(time: str, rule_name: str): - try: - datetime.strptime(time, TIME_RANGE_FORMAT) - except Exception: + # Using a regex instead of strptime because it's several orders of magnitude faster + match = TIME_RANGE_RE_PATTERN.match(time) + + if not match or int(match.groups()[0]) > 23: raise SchemaValidationError( f"'START' and 'END' must be a valid time format, time_format={TIME_RANGE_FORMAT}, rule={rule_name}" ) From b1462e7264e96ec9e9e8217ca7c2f003d08694b6 Mon Sep 17 00:00:00 2001 From: Ran Isenberg Date: Fri, 6 Jan 2023 15:50:10 +0200 Subject: [PATCH 25/34] cr fix --- aws_lambda_powertools/utilities/feature_flags/schema.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/schema.py b/aws_lambda_powertools/utilities/feature_flags/schema.py index f091dc1e871..5eb2095d809 100644 --- a/aws_lambda_powertools/utilities/feature_flags/schema.py +++ b/aws_lambda_powertools/utilities/feature_flags/schema.py @@ -17,7 +17,7 @@ CONDITION_ACTION = "action" FEATURE_DEFAULT_VAL_TYPE_KEY = "boolean_type" TIME_RANGE_FORMAT = "%H:%M" -TIME_RANGE_RE_PATTERN = re.compile(r"([0-2]\d):[0-5]\d") +TIME_RANGE_RE_PATTERN = re.compile(r"2[0-3]:[0-5]\d|[0-1]\d:[0-5]\d") HOUR_MIN_SEPARATOR = ":" @@ -336,7 +336,7 @@ def _validate_time_value(time: str, rule_name: str): # Using a regex instead of strptime because it's several orders of magnitude faster match = TIME_RANGE_RE_PATTERN.match(time) - if not match or int(match.groups()[0]) > 23: + if not match: raise SchemaValidationError( f"'START' and 'END' must be a valid time format, time_format={TIME_RANGE_FORMAT}, rule={rule_name}" ) From 62dd956674a667331c2a936a4b5ee7c68a227fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Fri, 13 Jan 2023 16:17:14 +0100 Subject: [PATCH 26/34] chore: support non UTC timestamps everywhere --- .../utilities/feature_flags/feature_flags.py | 14 +- .../utilities/feature_flags/schema.py | 63 ++- .../feature_flags/time_conditions.py | 55 +- poetry.lock | 518 +++++++++++------- pyproject.toml | 1 + .../feature_flags/test_schema_validation.py | 205 +++++-- .../feature_flags/test_time_based_actions.py | 258 ++++++--- 7 files changed, 747 insertions(+), 367 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py index 1722e2cbd40..2bf49187b58 100644 --- a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py +++ b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py @@ -7,9 +7,9 @@ from .base import StoreProvider from .exceptions import ConfigurationStoreError from .time_conditions import ( - compare_utc_datetime_range, - compare_utc_days_of_week, - compare_utc_time_range, + compare_datetime_range, + compare_days_of_week, + compare_time_range, ) @@ -64,9 +64,9 @@ def _match_by_action(self, action: str, condition_value: Any, context_value: Any schema.RuleAction.KEY_NOT_IN_VALUE.value: lambda a, b: a not in b, schema.RuleAction.VALUE_IN_KEY.value: lambda a, b: b in a, schema.RuleAction.VALUE_NOT_IN_KEY.value: lambda a, b: b not in a, - schema.RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value: lambda a, b: compare_utc_time_range(a, b), - schema.RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value: lambda a, b: compare_utc_datetime_range(a, b), - schema.RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value: lambda a, b: compare_utc_days_of_week(a, b), + schema.RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value: lambda a, b: compare_time_range(a, b), + schema.RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value: lambda a, b: compare_datetime_range(a, b), + schema.RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value: lambda a, b: compare_days_of_week(a, b), } try: @@ -101,7 +101,7 @@ def _evaluate_conditions( schema.RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, schema.RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, ): - context_value = condition.get(schema.CONDITION_KEY) # e.g., CURRENT_TIME_UTC + context_value = condition.get(schema.CONDITION_KEY) # e.g., CURRENT_TIME if not self._match_by_action(action=cond_action, condition_value=cond_value, context_value=context_value): self.logger.debug( diff --git a/aws_lambda_powertools/utilities/feature_flags/schema.py b/aws_lambda_powertools/utilities/feature_flags/schema.py index 5eb2095d809..aebc82417f7 100644 --- a/aws_lambda_powertools/utilities/feature_flags/schema.py +++ b/aws_lambda_powertools/utilities/feature_flags/schema.py @@ -4,6 +4,8 @@ from enum import Enum from typing import Any, Callable, Dict, List, Optional, Union +from dateutil import tz + from ... import Logger from .base import BaseValidator from .exceptions import SchemaValidationError @@ -42,14 +44,16 @@ class RuleAction(Enum): class TimeKeys(Enum): - CURRENT_TIME_UTC = "CURRENT_TIME_UTC" - CURRENT_DAY_OF_WEEK_UTC = "CURRENT_DAY_OF_WEEK_UTC" - CURRENT_DATETIME_UTC = "CURRENT_DATETIME_UTC" + CURRENT_TIME = "CURRENT_TIME" + CURRENT_DAY_OF_WEEK = "CURRENT_DAY_OF_WEEK" + CURRENT_DATETIME = "CURRENT_DATETIME" class TimeValues(Enum): START = "START" END = "END" + TIMEZONE = "TIMEZONE" + DAYS = "DAYS" SUNDAY = "SUNDAY" MONDAY = "MONDAY" TUESDAY = "TUESDAY" @@ -291,17 +295,17 @@ def validate_condition_key(condition: Dict[str, Any], rule_name: str): if not key or not isinstance(key, str): raise SchemaValidationError(f"'key' value must be a non empty string, rule={rule_name}") action = condition.get(CONDITION_ACTION, "") - if action == RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value and key != TimeKeys.CURRENT_TIME_UTC.value: + if action == RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value and key != TimeKeys.CURRENT_TIME.value: raise SchemaValidationError( - f"'condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a 'CURRENT_TIME_UTC' condition key, rule={rule_name}" # noqa: E501 + f"'condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a 'CURRENT_TIME' condition key, rule={rule_name}" # noqa: E501 ) - if action == RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value and key != TimeKeys.CURRENT_DATETIME_UTC.value: + if action == RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value and key != TimeKeys.CURRENT_DATETIME.value: raise SchemaValidationError( - f"'condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a 'CURRENT_DATETIME_UTC' condition key, rule={rule_name}" # noqa: E501 + f"'condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a 'CURRENT_DATETIME' condition key, rule={rule_name}" # noqa: E501 ) - if action == RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value and key != TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value: + if action == RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value and key != TimeKeys.CURRENT_DAY_OF_WEEK.value: raise SchemaValidationError( - f"'condition with a 'SCHEDULE_BETWEEN_DAYS_OF_WEEK' action must have a 'CURRENT_DAY_OF_WEEK_UTC' condition key, rule={rule_name}" # noqa: E501 + f"'condition with a 'SCHEDULE_BETWEEN_DAYS_OF_WEEK' action must have a 'CURRENT_DAY_OF_WEEK' condition key, rule={rule_name}" # noqa: E501 ) @staticmethod @@ -324,13 +328,25 @@ def validate_condition_value(condition: Dict[str, Any], rule_name: str): @staticmethod def _validate_datetime_value(datetime_str: str, rule_name: str): + date = None + try: # python < 3.11 don't support the Z timezone on datetime.fromisoformat, # so we replace any Z with the equivalent "+00:00" - datetime.fromisoformat(datetime_str.replace("Z", "+00:00")) + date = datetime.fromisoformat(datetime_str.replace("Z", "+00:00")) except Exception: raise SchemaValidationError(f"'START' and 'END' must be a valid ISO8601 time format, rule={rule_name}") + # we only allow timezone information to be set via the TIMEZONE field + # this way we can encode DST into the calculation. For instance, Copenhagen is + # UTC+2 during winter, and UTC+1 during summer, which would be impossible to define + # using a single ISO datetime string + if date.tzinfo is not None: + raise SchemaValidationError( + "'START' and 'END' must not include timezone information. Set the timezone using the 'TIMEZONE' " + f"field, rule={rule_name} " + ) + @staticmethod def _validate_time_value(time: str, rule_name: str): # Using a regex instead of strptime because it's several orders of magnitude faster @@ -343,11 +359,14 @@ def _validate_time_value(time: str, rule_name: str): @staticmethod def _validate_schedule_between_days_of_week(value: Any, rule_name: str): - if not isinstance(value, list) or not value: - raise SchemaValidationError( - f"condition with a CURRENT_DAY_OF_WEEK_UTC action must have a non empty condition value type list, rule={rule_name}" # noqa: E501 - ) - for day in value: + error_str = f"condition with a CURRENT_DAY_OF_WEEK action must have a condition value dictionary with 'DAYS' and 'TIMEZONE' (optional) keys, rule={rule_name}" # noqa: E501 + if not isinstance(value, dict): + raise SchemaValidationError(error_str) + + days = value.get(TimeValues.DAYS.value) + if not isinstance(days, list) or not value: + raise SchemaValidationError(error_str) + for day in days: if not isinstance(day, str) or day not in [ TimeValues.MONDAY.value, TimeValues.TUESDAY.value, @@ -358,9 +377,15 @@ def _validate_schedule_between_days_of_week(value: Any, rule_name: str): TimeValues.SUNDAY.value, ]: raise SchemaValidationError( - f"condition value must represent a day of the week in 'TimeValues' enum, rule={rule_name}" + f"condition value DAYS must represent a day of the week in 'TimeValues' enum, rule={rule_name}" ) + timezone = value.get(TimeValues.TIMEZONE.value, "UTC") + if not isinstance(timezone, str): + raise SchemaValidationError(error_str) + if not tz.gettz(timezone): + raise SchemaValidationError(f"'TIMEZONE' value must represent a valid IANA timezone, rule={rule_name}") + @staticmethod def _validate_schedule_between_time_and_datetime_ranges( value: Any, rule_name: str, action_name: str, validator: Callable[[str, str], None] @@ -376,3 +401,9 @@ def _validate_schedule_between_time_and_datetime_ranges( raise SchemaValidationError(f"'START' and 'END' must be a non empty string, rule={rule_name}") validator(start_time, rule_name) validator(end_time, rule_name) + + timezone = value.get(TimeValues.TIMEZONE.value, "UTC") + if not isinstance(timezone, str): + raise SchemaValidationError(f"'TIMEZONE' must be a string, rule={rule_name}") + if not tz.gettz(timezone): + raise SchemaValidationError(f"'TIMEZONE' value must represent a valid IANA timezone, rule={rule_name}") diff --git a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py index 9ec9369fb57..b75be2fecea 100644 --- a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py +++ b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py @@ -1,38 +1,49 @@ -from datetime import datetime, timezone -from typing import Dict, List +from datetime import datetime, tzinfo +from typing import Dict, Optional + +from dateutil.tz import gettz from .schema import HOUR_MIN_SEPARATOR, TimeValues -def _get_utc_time_now() -> datetime: - return datetime.now(timezone.utc) +def _get_now_from_timezone(timezone: Optional[tzinfo]) -> datetime: + timezone = gettz("UTC") if timezone is None else timezone + return datetime.now(timezone) + + +def compare_days_of_week(action: str, values: Dict) -> bool: + timezone_name = values.get(TimeValues.TIMEZONE.value, "UTC") + current_day = _get_now_from_timezone(gettz(timezone_name)).strftime("%A").upper() + days = values.get(TimeValues.DAYS.value, []) + return current_day in days -def compare_utc_days_of_week(action: str, values: List[str]) -> bool: - current_day = _get_utc_time_now().strftime("%A").upper() - return current_day in values +def compare_datetime_range(action: str, values: Dict) -> bool: + timezone_name = values.get(TimeValues.TIMEZONE.value, "UTC") + timezone = gettz(timezone_name) + current_time: datetime = _get_now_from_timezone(timezone) -def compare_utc_datetime_range(action: str, values: Dict) -> bool: - current_time_utc: datetime = _get_utc_time_now() + start_date_str = values.get(TimeValues.START.value, "") + end_date_str = values.get(TimeValues.END.value, "") - # python < 3.11 don't support Z as a timezone on datetime.fromisoformat, - # so we replace any Z with the equivalent "+00:00 - start_date_str = values.get(TimeValues.START.value, "").replace("Z", "+00:00") - end_date_str = values.get(TimeValues.END.value, "").replace("Z", "+00:00") + # Since start_date and end_date don't include timezone information, we mark the timestamp + # with the same timezone as the current_time. This way all the 3 timestamps will be on + # the same timezone + start_date = datetime.fromisoformat(start_date_str).replace(tzinfo=timezone) + end_date = datetime.fromisoformat(end_date_str).replace(tzinfo=timezone) + return start_date <= current_time <= end_date - start_date = datetime.fromisoformat(start_date_str) - end_date = datetime.fromisoformat(end_date_str) - return start_date <= current_time_utc <= end_date +def compare_time_range(action: str, values: Dict) -> bool: + timezone_name = values.get(TimeValues.TIMEZONE.value, "UTC") + current_time: datetime = _get_now_from_timezone(gettz(timezone_name)) -def compare_utc_time_range(action: str, values: Dict) -> bool: - current_time_utc: datetime = _get_utc_time_now() start_hour, start_min = values.get(TimeValues.START.value, "").split(HOUR_MIN_SEPARATOR) end_hour, end_min = values.get(TimeValues.END.value, "").split(HOUR_MIN_SEPARATOR) return ( - current_time_utc.hour >= int(start_hour) - and current_time_utc.hour <= int(end_hour) - and current_time_utc.minute >= int(start_min) - and current_time_utc.minute <= int(end_min) + current_time.hour >= int(start_hour) + and current_time.hour <= int(end_hour) + and current_time.minute >= int(start_min) + and current_time.minute <= int(end_min) ) diff --git a/poetry.lock b/poetry.lock index e492bb83d6e..8209f76ad1b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,32 +2,33 @@ [[package]] name = "attrs" -version = "22.1.0" +version = "22.2.0" description = "Classes Without Boilerplate" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, - {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, ] [package.extras] -dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] -docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] -tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] +tests = ["attrs[tests-no-zope]", "zope.interface"] +tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] [[package]] name = "aws-cdk-asset-awscli-v1" -version = "2.2.31" +version = "2.2.49" description = "A library that contains the AWS CLI for use in Lambda Layers" category = "dev" optional = false python-versions = "~=3.7" files = [ - {file = "aws-cdk.asset-awscli-v1-2.2.31.tar.gz", hash = "sha256:c7e01034ae5f128f6853332a61282e51d8112bd975df6897436678bf358eceed"}, - {file = "aws_cdk.asset_awscli_v1-2.2.31-py3-none-any.whl", hash = "sha256:3194fbf956578d85d16cd9bb0541f9011bc5564baa9c4495b47dbe3889165d44"}, + {file = "aws-cdk.asset-awscli-v1-2.2.49.tar.gz", hash = "sha256:d367da8bdc83357792b1ef16b6166d400ef15f2389cf0032b607b6327768a41a"}, + {file = "aws_cdk.asset_awscli_v1-2.2.49-py3-none-any.whl", hash = "sha256:28df4487e2fa5314d5c39c114e12d366714a1fab2de3269d55c4e544876cae44"}, ] [package.dependencies] @@ -71,57 +72,57 @@ typeguard = ">=2.13.3,<2.14.0" [[package]] name = "aws-cdk-aws-apigatewayv2-alpha" -version = "2.53.0a0" +version = "2.60.0a0" description = "The CDK Construct Library for AWS::APIGatewayv2" category = "dev" optional = false python-versions = "~=3.7" files = [ - {file = "aws-cdk.aws-apigatewayv2-alpha-2.53.0a0.tar.gz", hash = "sha256:7bfd688d3c22676266ff161012f11ff3f60e11e50ba0d39d18a313ff07f69bbd"}, - {file = "aws_cdk.aws_apigatewayv2_alpha-2.53.0a0-py3-none-any.whl", hash = "sha256:6864d15ea12c903ae6ca679aaec49dd6c65fc1b537cd317c62cd334f7a382683"}, + {file = "aws-cdk.aws-apigatewayv2-alpha-2.60.0a0.tar.gz", hash = "sha256:34a132475b4b8a44bc15ef555bc1d8ce37a1550368455e27bd7caa715abf8986"}, + {file = "aws_cdk.aws_apigatewayv2_alpha-2.60.0a0-py3-none-any.whl", hash = "sha256:fabeeb5afd4b4ace03baeab7abdb3f0dcd32a603e51e1502730daf77505e40ab"}, ] [package.dependencies] -aws-cdk-lib = ">=2.53.0,<3.0.0" +aws-cdk-lib = ">=2.60.0,<3.0.0" constructs = ">=10.0.0,<11.0.0" -jsii = ">=1.71.0,<2.0.0" +jsii = ">=1.72.0,<2.0.0" publication = ">=0.0.3" typeguard = ">=2.13.3,<2.14.0" [[package]] name = "aws-cdk-aws-apigatewayv2-integrations-alpha" -version = "2.53.0a0" +version = "2.60.0a0" description = "Integrations for AWS APIGateway V2" category = "dev" optional = false python-versions = "~=3.7" files = [ - {file = "aws-cdk.aws-apigatewayv2-integrations-alpha-2.53.0a0.tar.gz", hash = "sha256:fb8bf5812908787a776a9dc5a13bbacec0b39f07517f9a57ebba33d9585ab131"}, - {file = "aws_cdk.aws_apigatewayv2_integrations_alpha-2.53.0a0-py3-none-any.whl", hash = "sha256:4419283b9a0f41c0cfc5e4a8ba0b0236ca948d874c1c165f058c2c1677ffe6d1"}, + {file = "aws-cdk.aws-apigatewayv2-integrations-alpha-2.60.0a0.tar.gz", hash = "sha256:e35556c79cb21b1abdb4955dd78b57c8ba0d8f9682bd9af48c88da35b1bb37a5"}, + {file = "aws_cdk.aws_apigatewayv2_integrations_alpha-2.60.0a0-py3-none-any.whl", hash = "sha256:fabe4d554cc3229cbffaba27f1dcfd8e1eb087193ec519c2dabbd75580745684"}, ] [package.dependencies] -"aws-cdk.aws-apigatewayv2-alpha" = "2.53.0.a0" -aws-cdk-lib = ">=2.53.0,<3.0.0" +"aws-cdk.aws-apigatewayv2-alpha" = "2.60.0.a0" +aws-cdk-lib = ">=2.60.0,<3.0.0" constructs = ">=10.0.0,<11.0.0" -jsii = ">=1.71.0,<2.0.0" +jsii = ">=1.72.0,<2.0.0" publication = ">=0.0.3" typeguard = ">=2.13.3,<2.14.0" [[package]] name = "aws-cdk-lib" -version = "2.59.0" +version = "2.60.0" description = "Version 2 of the AWS Cloud Development Kit library" category = "dev" optional = false python-versions = "~=3.7" files = [ - {file = "aws-cdk-lib-2.59.0.tar.gz", hash = "sha256:1faeced63e37a4caa58472ba368664e3aea935b3193ccbc3f4f55a81d93b59d1"}, - {file = "aws_cdk_lib-2.59.0-py3-none-any.whl", hash = "sha256:0f8718be6951facac8044c286e49c6459a00a833d9d8c56274f27af41ed194c6"}, + {file = "aws-cdk-lib-2.60.0.tar.gz", hash = "sha256:05f69bdc8c1130b01cc4a8551b44da3bfce4e7d717dc8194f9c0884fad4c7eba"}, + {file = "aws_cdk_lib-2.60.0-py3-none-any.whl", hash = "sha256:6abc46cac6ebfd64d20a0dcd653c17e590ec17e09909828ead3a36a3f0f185d8"}, ] [package.dependencies] -"aws-cdk.asset-awscli-v1" = ">=2.2.30,<3.0.0" +"aws-cdk.asset-awscli-v1" = ">=2.2.49,<3.0.0" "aws-cdk.asset-kubectl-v20" = ">=2.1.1,<3.0.0" "aws-cdk.asset-node-proxy-agent-v5" = ">=2.0.38,<3.0.0" constructs = ">=10.0.0,<11.0.0" @@ -131,23 +132,25 @@ typeguard = ">=2.13.3,<2.14.0" [[package]] name = "aws-sam-translator" -version = "1.55.0" +version = "1.57.0" description = "AWS SAM Translator is a library that transform SAM templates into AWS CloudFormation templates" category = "dev" optional = false python-versions = ">=3.7, <=4.0, !=4.0" files = [ - {file = "aws-sam-translator-1.55.0.tar.gz", hash = "sha256:08e182e76d6fabc13ce2f38b8a3932b3131407c6ad29ec2849ef3d9a41576b94"}, - {file = "aws_sam_translator-1.55.0-py2-none-any.whl", hash = "sha256:e86a67b87329a0de7d531d33257d1a448d0d6ecd84aee058d084957f28a8e4b1"}, - {file = "aws_sam_translator-1.55.0-py3-none-any.whl", hash = "sha256:93dc74614ab291c86be681e025679d08f4fa685ed6b55d410f62f2f235012205"}, + {file = "aws-sam-translator-1.57.0.tar.gz", hash = "sha256:5953b973468f72c11ce6fe3ae4c5bea11fb774bf46c91970e3ab4460c5e1798e"}, + {file = "aws_sam_translator-1.57.0-py2-none-any.whl", hash = "sha256:b7fd46bf28d94d5d5174883534469358edb4612b64a6eff1db884d267a45a6e3"}, + {file = "aws_sam_translator-1.57.0-py3-none-any.whl", hash = "sha256:8bfdb6dd8cdc9b777e54de1924e60eddc6f068218016e28f629db2bd41af953e"}, ] [package.dependencies] boto3 = ">=1.19.5,<2.0.0" -jsonschema = ">=3.2,<4.0" +jsonschema = ">=3.2,<5" +pydantic = ">=1.10.2,<1.11.0" +typing-extensions = ">=4.4.0,<4.5.0" [package.extras] -dev = ["black (==20.8b1)", "boto3 (>=1.23,<2)", "boto3-stubs[appconfig,serverlessrepo] (>=1.19.5,<2.0.0)", "click (>=7.1,<8.0)", "coverage (>=5.3,<6.0)", "dateparser (>=0.7,<1.0)", "docopt (>=0.6.2,<0.7.0)", "flake8 (>=3.8.4,<3.9.0)", "mypy (==0.971)", "parameterized (>=0.7.4,<0.8.0)", "pylint (>=2.15.0,<2.16.0)", "pytest (>=6.2.5,<6.3.0)", "pytest-cov (>=2.10.1,<2.11.0)", "pytest-env (>=0.6.2,<0.7.0)", "pytest-rerunfailures (>=9.1.1,<9.2.0)", "pytest-xdist (>=2.5,<3.0)", "pyyaml (>=5.4,<6.0)", "requests (>=2.24.0,<2.25.0)", "ruamel.yaml (==0.17.21)", "tenacity (>=7.0.0,<7.1.0)", "tox (>=3.24,<4.0)", "types-PyYAML (>=5.4,<6.0)", "types-jsonschema (>=3.2,<4.0)"] +dev = ["black (==20.8b1)", "boto3 (>=1.23,<2)", "boto3-stubs[appconfig,serverlessrepo] (>=1.19.5,<2.0.0)", "click (>=7.1,<8.0)", "coverage (>=5.3,<6.0)", "dateparser (>=0.7,<1.0)", "docopt (>=0.6.2,<0.7.0)", "flake8 (>=3.8.4,<3.9.0)", "mypy (==0.971)", "parameterized (>=0.7.4,<0.8.0)", "pylint (>=2.15.0,<2.16.0)", "pytest (>=6.2.5,<6.3.0)", "pytest-cov (>=2.10.1,<2.11.0)", "pytest-env (>=0.6.2,<0.7.0)", "pytest-rerunfailures (>=9.1.1,<9.2.0)", "pytest-xdist (>=2.5,<3.0)", "pyyaml (>=5.4,<6.0)", "requests (>=2.25.0,<2.26.0)", "ruamel.yaml (==0.17.21)", "tenacity (>=7.0.0,<7.1.0)", "tox (>=3.24,<4.0)", "types-PyYAML (>=5.4,<6.0)", "types-jsonschema (>=3.2,<4.0)"] [[package]] name = "aws-xray-sdk" @@ -227,18 +230,18 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "boto3" -version = "1.26.18" +version = "1.26.49" description = "The AWS SDK for Python" category = "main" optional = false python-versions = ">= 3.7" files = [ - {file = "boto3-1.26.18-py3-none-any.whl", hash = "sha256:933c88b189112a5fdd82d49ef00f95b9dd649d195e557a81aecb773a3e01c517"}, - {file = "boto3-1.26.18.tar.gz", hash = "sha256:3c7315da16eb0b41823965e5ce55f99cb07e94680e0ed7830c581f505fb5bd15"}, + {file = "boto3-1.26.49-py3-none-any.whl", hash = "sha256:2260cb121b202c9f3483e1c5fabff2009a160369824f6da5608261e7c232a75c"}, + {file = "boto3-1.26.49.tar.gz", hash = "sha256:6c9628f71d0723bf16b5ae4d4e78bbde53da80577e74b17762f3aea17c3c0f85"}, ] [package.dependencies] -botocore = ">=1.29.18,<1.30.0" +botocore = ">=1.29.49,<1.30.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.6.0,<0.7.0" @@ -247,14 +250,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.29.18" +version = "1.29.49" description = "Low-level, data-driven core of boto 3." category = "main" optional = false python-versions = ">= 3.7" files = [ - {file = "botocore-1.29.18-py3-none-any.whl", hash = "sha256:2aba44433b6eac6d3a12cf93f2985e2d7a843307c1a527042fc48dd09b273992"}, - {file = "botocore-1.29.18.tar.gz", hash = "sha256:26e86fce95049f6cc18b5611901549943c4c22522fa8a3b6b265404f673977b2"}, + {file = "botocore-1.29.49-py3-none-any.whl", hash = "sha256:1d93d602fdc3512c4e1db6f418f274a3f5758fc3bd6dea1d6f9ea29bcfe32ae7"}, + {file = "botocore-1.29.49.tar.gz", hash = "sha256:0a198d33a426fc8b3342ce238c6bccb7f5d3b0b6a33f624170f1c11aa2483a15"}, ] [package.dependencies] @@ -263,7 +266,7 @@ python-dateutil = ">=2.1,<3.0.0" urllib3 = ">=1.25.4,<1.27" [package.extras] -crt = ["awscrt (==0.14.0)"] +crt = ["awscrt (==0.15.3)"] [[package]] name = "cattrs" @@ -318,19 +321,102 @@ sarif-om = ">=1.0.4,<1.1.0" [[package]] name = "charset-normalizer" -version = "2.1.1" +version = "3.0.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "dev" optional = false -python-versions = ">=3.6.0" +python-versions = "*" files = [ - {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, - {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, + {file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"}, + {file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"}, ] -[package.extras] -unicode-backport = ["unicodedata2"] - [[package]] name = "checksumdir" version = "1.2.0" @@ -373,18 +459,18 @@ files = [ [[package]] name = "constructs" -version = "10.1.174" +version = "10.1.218" description = "A programming model for software-defined state" category = "dev" optional = false python-versions = "~=3.7" files = [ - {file = "constructs-10.1.174-py3-none-any.whl", hash = "sha256:6968efd5f837f3a43d5a93808424744cdcd03f53df764be78123f60e81b39b0a"}, - {file = "constructs-10.1.174.tar.gz", hash = "sha256:af9bd1f0bd6882a6a608bc335a4f8a230f498072bff83d9126d43c486e30305b"}, + {file = "constructs-10.1.218-py3-none-any.whl", hash = "sha256:525365d41e7c7cb46b4f0ea8e7b5820554f1f3180074932dacfa76a38a50b648"}, + {file = "constructs-10.1.218.tar.gz", hash = "sha256:9ce7fd475d213bd7ef50635f362f9f68d2fe37c138a14dc7558d7c40014b1485"}, ] [package.dependencies] -jsii = ">=1.71.0,<2.0.0" +jsii = ">=1.73.0,<2.0.0" publication = ">=0.0.3" typeguard = ">=2.13.3,<2.14.0" @@ -481,14 +567,14 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.0.4" +version = "1.1.0" description = "Backport of PEP 654 (exception groups)" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.0.4-py3-none-any.whl", hash = "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828"}, - {file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"}, + {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, + {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, ] [package.extras] @@ -708,13 +794,13 @@ files = [ [[package]] name = "future" -version = "0.18.2" +version = "0.18.3" description = "Clean single-source support for Python 3 and 2" category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ - {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, + {file = "future-0.18.3.tar.gz", hash = "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307"}, ] [[package]] @@ -887,16 +973,35 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker perf = ["ipython"] testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] +[[package]] +name = "importlib-resources" +version = "5.10.2" +description = "Read resources from Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "importlib_resources-5.10.2-py3-none-any.whl", hash = "sha256:7d543798b0beca10b6a01ac7cafda9f822c54db9e8376a6bf57e0cbd74d486b6"}, + {file = "importlib_resources-5.10.2.tar.gz", hash = "sha256:e4a96c8cc0339647ff9a5e0550d9f276fc5a01ffa276012b58ec108cfd7b8484"}, +] + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + [[package]] name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] [[package]] @@ -966,14 +1071,14 @@ pbr = "*" [[package]] name = "jsii" -version = "1.72.0" +version = "1.73.0" description = "Python client for jsii runtime" category = "dev" optional = false python-versions = "~=3.7" files = [ - {file = "jsii-1.72.0-py3-none-any.whl", hash = "sha256:4dcf65eca9400c15a6a7c9d85b27f4bfe15c96ad3e36272a502f391556858151"}, - {file = "jsii-1.72.0.tar.gz", hash = "sha256:6daf1c17362bd07c50c299e08d9a4454075550eb78e035a160ecf9ea68ded3cf"}, + {file = "jsii-1.73.0-py3-none-any.whl", hash = "sha256:13e8496c3afee70d85401ad1eef2ddedbdb88e7e7abb3e68302dd6e61527191e"}, + {file = "jsii-1.73.0.tar.gz", hash = "sha256:be6458236e787be0b02c2fe869b6f4ed906398b6cc537190d61a60d2b5c9dfbb"}, ] [package.dependencies] @@ -1001,14 +1106,14 @@ jsonpointer = ">=1.9" [[package]] name = "jsonpickle" -version = "2.2.0" +version = "3.0.1" description = "Python library for serializing any arbitrary object graph into JSON" category = "dev" optional = false -python-versions = ">=2.7" +python-versions = ">=3.7" files = [ - {file = "jsonpickle-2.2.0-py2.py3-none-any.whl", hash = "sha256:de7f2613818aa4f234138ca11243d6359ff83ae528b2185efdd474f62bcf9ae1"}, - {file = "jsonpickle-2.2.0.tar.gz", hash = "sha256:7b272918b0554182e53dc340ddd62d9b7f902fec7e7b05620c04f3ccef479a0e"}, + {file = "jsonpickle-3.0.1-py2.py3-none-any.whl", hash = "sha256:130d8b293ea0add3845de311aaba55e6d706d0bb17bc123bd2c8baf8a39ac77c"}, + {file = "jsonpickle-3.0.1.tar.gz", hash = "sha256:032538804795e73b94ead410800ac387fdb6de98f8882ac957fcd247e3a85200"}, ] [package.dependencies] @@ -1016,8 +1121,8 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [package.extras] docs = ["jaraco.packaging (>=3.2)", "rst.linker (>=1.9)", "sphinx"] -testing = ["ecdsa", "enum34", "feedparser", "jsonlib", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-black-multipy", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-flake8 (<1.1.0)", "pytest-flake8 (>=1.1.1)", "scikit-learn", "sqlalchemy"] -testing-libs = ["simplejson", "ujson", "yajl"] +testing = ["ecdsa", "feedparser", "gmpy2", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-black-multipy", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-flake8 (>=1.1.1)", "scikit-learn", "sqlalchemy"] +testing-libs = ["simplejson", "ujson"] [[package]] name = "jsonpointer" @@ -1033,26 +1138,27 @@ files = [ [[package]] name = "jsonschema" -version = "3.2.0" +version = "4.17.3" description = "An implementation of JSON Schema validation for Python" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, - {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, + {file = "jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6"}, + {file = "jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d"}, ] [package.dependencies] attrs = ">=17.4.0" importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -pyrsistent = ">=0.14.0" -setuptools = "*" -six = ">=1.11.0" +importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} +pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""} +pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] -format-nongpl = ["idna", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "webcolors"] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] [[package]] name = "junit-xml" @@ -1281,14 +1387,14 @@ mkdocs = ">=0.17" [[package]] name = "mkdocs-material" -version = "9.0.3" +version = "9.0.4" description = "Documentation that simply works" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "mkdocs_material-9.0.3-py3-none-any.whl", hash = "sha256:cedbbf84e156370489907d3c5b79999fcf6563f61a96965ec4c2513d303fa706"}, - {file = "mkdocs_material-9.0.3.tar.gz", hash = "sha256:918fe38f504ca397b388b6c45445c22cb9acab61f00ade78d5f3edf299b6c9df"}, + {file = "mkdocs_material-9.0.4-py3-none-any.whl", hash = "sha256:f5f94f5daa0e07deb2f192453f9812c66c4bc0cd48078c60bdde32af137f7357"}, + {file = "mkdocs_material-9.0.4.tar.gz", hash = "sha256:4f429b4e50242544020f0fc21b9e10062418d3cd1e591c040c4c155506250a66"}, ] [package.dependencies] @@ -1298,7 +1404,7 @@ markdown = ">=3.2" mkdocs = ">=1.4.2" mkdocs-material-extensions = ">=1.1" pygments = ">=2.14" -pymdown-extensions = ">=9.9" +pymdown-extensions = ">=9.9.1" regex = ">=2022.4.24" requests = ">=2.26" @@ -1436,14 +1542,14 @@ typing-extensions = ">=4.1.0" [[package]] name = "mypy-boto3-lambda" -version = "1.26.18" -description = "Type annotations for boto3.Lambda 1.26.18 service generated with mypy-boto3-builder 7.11.11" +version = "1.26.49" +description = "Type annotations for boto3.Lambda 1.26.49 service generated with mypy-boto3-builder 7.12.3" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "mypy-boto3-lambda-1.26.18.tar.gz", hash = "sha256:ad36a98e4bd7c95eb6bf750bc63932367cbed8bbe79bca1fdb7e753e2a689a8b"}, - {file = "mypy_boto3_lambda-1.26.18-py3-none-any.whl", hash = "sha256:8514bf21fe3158c3f555906c2575403b3bbbc3891b3cff5869ec75a7fa8477ce"}, + {file = "mypy-boto3-lambda-1.26.49.tar.gz", hash = "sha256:748222e6dfd602a667b76b9ce0e8c8b31664bc3bd78cc43363fb22ca2885b4c3"}, + {file = "mypy_boto3_lambda-1.26.49-py3-none-any.whl", hash = "sha256:ef346c1fbbc80a907c1d44f19ea9335f8c3889fb48766cb0f7e4439e339fae2b"}, ] [package.dependencies] @@ -1451,14 +1557,14 @@ typing-extensions = ">=4.1.0" [[package]] name = "mypy-boto3-logs" -version = "1.26.43" -description = "Type annotations for boto3.CloudWatchLogs 1.26.43 service generated with mypy-boto3-builder 7.12.2" +version = "1.26.49" +description = "Type annotations for boto3.CloudWatchLogs 1.26.49 service generated with mypy-boto3-builder 7.12.3" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "mypy-boto3-logs-1.26.43.tar.gz", hash = "sha256:fe992d6d973518668151ec6308c59b86ceb4c93eb4b5d3733007ba1d7b5d1aac"}, - {file = "mypy_boto3_logs-1.26.43-py3-none-any.whl", hash = "sha256:47c7a1d4d38f369e2464ff66779cdf60c0dd879d8fe98a5004edae21eda16353"}, + {file = "mypy-boto3-logs-1.26.49.tar.gz", hash = "sha256:33cdbdc26f71c2bdf7d5f16f7640835fda8364f286a8321435e7be6b7d671f9f"}, + {file = "mypy_boto3_logs-1.26.49-py3-none-any.whl", hash = "sha256:ba28e786393e08c6d03c17881042df1a5e906446c74f30f15c85301f393ad879"}, ] [package.dependencies] @@ -1481,14 +1587,14 @@ typing-extensions = ">=4.1.0" [[package]] name = "mypy-boto3-secretsmanager" -version = "1.26.40" -description = "Type annotations for boto3.SecretsManager 1.26.40 service generated with mypy-boto3-builder 7.12.2" +version = "1.26.49" +description = "Type annotations for boto3.SecretsManager 1.26.49 service generated with mypy-boto3-builder 7.12.3" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "mypy-boto3-secretsmanager-1.26.40.tar.gz", hash = "sha256:278b6c83df97a6db7411a8d0b75e0f49a093b2ba502f9d330ec92656a1b797b7"}, - {file = "mypy_boto3_secretsmanager-1.26.40-py3-none-any.whl", hash = "sha256:6d00a8fac86eca9139d6af05ad5dd61fbbe8de25e6f4e8022e4f2d51a3fb72b8"}, + {file = "mypy-boto3-secretsmanager-1.26.49.tar.gz", hash = "sha256:bd5bda2a8b65bf799793bad67c66bcb2662b182b8632ae1561aecc1bd30ac04d"}, + {file = "mypy_boto3_secretsmanager-1.26.49-py3-none-any.whl", hash = "sha256:2adbf9b5a32975c97479f12bbb5d98ceea30feb6a16012d308eee46fc3dd8b17"}, ] [package.dependencies] @@ -1557,41 +1663,38 @@ test = ["codecov (>=2.1)", "pytest (>=6.2)", "pytest-cov (>=2.12)"] [[package]] name = "packaging" -version = "21.3" +version = "23.0" description = "Core utilities for Python packages" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, ] -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" - [[package]] name = "pathspec" -version = "0.10.2" +version = "0.10.3" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pathspec-0.10.2-py3-none-any.whl", hash = "sha256:88c2606f2c1e818b978540f73ecc908e13999c6c3a383daf3705652ae79807a5"}, - {file = "pathspec-0.10.2.tar.gz", hash = "sha256:8f6bf73e5758fd365ef5d58ce09ac7c27d2833a8d7da51712eac6e27e35141b0"}, + {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, + {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, ] [[package]] name = "pbr" -version = "5.11.0" +version = "5.11.1" description = "Python Build Reasonableness" category = "dev" optional = false python-versions = ">=2.6" files = [ - {file = "pbr-5.11.0-py2.py3-none-any.whl", hash = "sha256:db2317ff07c84c4c63648c9064a79fe9d9f5c7ce85a9099d4b6258b3db83225a"}, - {file = "pbr-5.11.0.tar.gz", hash = "sha256:b97bc6695b2aff02144133c2e7399d5885223d42b7912ffaec2ca3898e673bfe"}, + {file = "pbr-5.11.1-py2.py3-none-any.whl", hash = "sha256:567f09558bae2b3ab53cb3c1e2e33e726ff3338e7bae3db5dc954b3a44eef12b"}, + {file = "pbr-5.11.1.tar.gz", hash = "sha256:aefc51675b0b533d56bb5fd1c8c6c0522fe31896679882e1c4c63d5e4a0fccb3"}, ] [[package]] @@ -1610,21 +1713,36 @@ files = [ mako = "*" markdown = ">=3.0" +[[package]] +name = "pkgutil-resolve-name" +version = "1.3.10" +description = "Resolve a name to an object." +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, + {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, +] + [[package]] name = "platformdirs" -version = "2.5.4" +version = "2.6.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-2.5.4-py3-none-any.whl", hash = "sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10"}, - {file = "platformdirs-2.5.4.tar.gz", hash = "sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7"}, + {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, + {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, ] +[package.dependencies] +typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""} + [package.extras] -docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"] -test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" @@ -1710,7 +1828,7 @@ name = "pydantic" version = "1.10.4" description = "Data validation and settings management using python type hints" category = "main" -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "pydantic-1.10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854"}, @@ -1799,64 +1917,54 @@ plugins = ["importlib-metadata"] [[package]] name = "pymdown-extensions" -version = "9.9" +version = "9.9.1" description = "Extension pack for Python Markdown." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pymdown_extensions-9.9-py3-none-any.whl", hash = "sha256:ac698c15265680db5eb13cd4342abfcde2079ac01e5486028f47a1b41547b859"}, - {file = "pymdown_extensions-9.9.tar.gz", hash = "sha256:0f8fb7b74a37a61cc34e90b2c91865458b713ec774894ffad64353a5fce85cfc"}, + {file = "pymdown_extensions-9.9.1-py3-none-any.whl", hash = "sha256:8a8973933ab45b6fe8f5f8da1de25766356b1f91dee107bf4a34efd158dc340b"}, + {file = "pymdown_extensions-9.9.1.tar.gz", hash = "sha256:abed29926960bbb3b40f5ed5fa6375e29724d4e3cb86ced7c2bbd37ead1afeea"}, ] [package.dependencies] markdown = ">=3.2" -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "dev" -optional = false -python-versions = ">=3.6.8" -files = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - [[package]] name = "pyrsistent" -version = "0.19.2" +version = "0.19.3" description = "Persistent/Functional/Immutable data structures" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pyrsistent-0.19.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d6982b5a0237e1b7d876b60265564648a69b14017f3b5f908c5be2de3f9abb7a"}, - {file = "pyrsistent-0.19.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:187d5730b0507d9285a96fca9716310d572e5464cadd19f22b63a6976254d77a"}, - {file = "pyrsistent-0.19.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:055ab45d5911d7cae397dc418808d8802fb95262751872c841c170b0dbf51eed"}, - {file = "pyrsistent-0.19.2-cp310-cp310-win32.whl", hash = "sha256:456cb30ca8bff00596519f2c53e42c245c09e1a4543945703acd4312949bfd41"}, - {file = "pyrsistent-0.19.2-cp310-cp310-win_amd64.whl", hash = "sha256:b39725209e06759217d1ac5fcdb510e98670af9e37223985f330b611f62e7425"}, - {file = "pyrsistent-0.19.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2aede922a488861de0ad00c7630a6e2d57e8023e4be72d9d7147a9fcd2d30712"}, - {file = "pyrsistent-0.19.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879b4c2f4d41585c42df4d7654ddffff1239dc4065bc88b745f0341828b83e78"}, - {file = "pyrsistent-0.19.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c43bec251bbd10e3cb58ced80609c5c1eb238da9ca78b964aea410fb820d00d6"}, - {file = "pyrsistent-0.19.2-cp37-cp37m-win32.whl", hash = "sha256:d690b18ac4b3e3cab73b0b7aa7dbe65978a172ff94970ff98d82f2031f8971c2"}, - {file = "pyrsistent-0.19.2-cp37-cp37m-win_amd64.whl", hash = "sha256:3ba4134a3ff0fc7ad225b6b457d1309f4698108fb6b35532d015dca8f5abed73"}, - {file = "pyrsistent-0.19.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a178209e2df710e3f142cbd05313ba0c5ebed0a55d78d9945ac7a4e09d923308"}, - {file = "pyrsistent-0.19.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e371b844cec09d8dc424d940e54bba8f67a03ebea20ff7b7b0d56f526c71d584"}, - {file = "pyrsistent-0.19.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111156137b2e71f3a9936baf27cb322e8024dac3dc54ec7fb9f0bcf3249e68bb"}, - {file = "pyrsistent-0.19.2-cp38-cp38-win32.whl", hash = "sha256:e5d8f84d81e3729c3b506657dddfe46e8ba9c330bf1858ee33108f8bb2adb38a"}, - {file = "pyrsistent-0.19.2-cp38-cp38-win_amd64.whl", hash = "sha256:9cd3e9978d12b5d99cbdc727a3022da0430ad007dacf33d0bf554b96427f33ab"}, - {file = "pyrsistent-0.19.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f1258f4e6c42ad0b20f9cfcc3ada5bd6b83374516cd01c0960e3cb75fdca6770"}, - {file = "pyrsistent-0.19.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21455e2b16000440e896ab99e8304617151981ed40c29e9507ef1c2e4314ee95"}, - {file = "pyrsistent-0.19.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd880614c6237243ff53a0539f1cb26987a6dc8ac6e66e0c5a40617296a045e"}, - {file = "pyrsistent-0.19.2-cp39-cp39-win32.whl", hash = "sha256:71d332b0320642b3261e9fee47ab9e65872c2bd90260e5d225dabeed93cbd42b"}, - {file = "pyrsistent-0.19.2-cp39-cp39-win_amd64.whl", hash = "sha256:dec3eac7549869365fe263831f576c8457f6c833937c68542d08fde73457d291"}, - {file = "pyrsistent-0.19.2-py3-none-any.whl", hash = "sha256:ea6b79a02a28550c98b6ca9c35b9f492beaa54d7c5c9e9949555893c8a9234d0"}, - {file = "pyrsistent-0.19.2.tar.gz", hash = "sha256:bfa0351be89c9fcbcb8c9879b826f4353be10f58f8a677efab0c017bf7137ec2"}, + {file = "pyrsistent-0.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a"}, + {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64"}, + {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf"}, + {file = "pyrsistent-0.19.3-cp310-cp310-win32.whl", hash = "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a"}, + {file = "pyrsistent-0.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da"}, + {file = "pyrsistent-0.19.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9"}, + {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393"}, + {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19"}, + {file = "pyrsistent-0.19.3-cp311-cp311-win32.whl", hash = "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3"}, + {file = "pyrsistent-0.19.3-cp311-cp311-win_amd64.whl", hash = "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-win32.whl", hash = "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1"}, + {file = "pyrsistent-0.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b"}, + {file = "pyrsistent-0.19.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8"}, + {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a"}, + {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c"}, + {file = "pyrsistent-0.19.3-cp38-cp38-win32.whl", hash = "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c"}, + {file = "pyrsistent-0.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7"}, + {file = "pyrsistent-0.19.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc"}, + {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2"}, + {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3"}, + {file = "pyrsistent-0.19.3-cp39-cp39-win32.whl", hash = "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2"}, + {file = "pyrsistent-0.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98"}, + {file = "pyrsistent-0.19.3-py3-none-any.whl", hash = "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64"}, + {file = "pyrsistent-0.19.3.tar.gz", hash = "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440"}, ] [[package]] @@ -2238,19 +2346,19 @@ files = [ [[package]] name = "requests" -version = "2.28.1" +version = "2.28.2" description = "Python HTTP for Humans." category = "dev" optional = false python-versions = ">=3.7, <4" files = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, + {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, + {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, ] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" +charset-normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" @@ -2308,23 +2416,6 @@ files = [ attrs = "*" pbr = "*" -[[package]] -name = "setuptools" -version = "65.6.3" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, - {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - [[package]] name = "six" version = "1.16.0" @@ -2427,6 +2518,18 @@ files = [ doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] test = ["mypy", "pytest", "typing-extensions"] +[[package]] +name = "types-python-dateutil" +version = "2.8.19.5" +description = "Typing stubs for python-dateutil" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "types-python-dateutil-2.8.19.5.tar.gz", hash = "sha256:ab91fc5f715f7d76d9a50d3dd74d0c68dfe38a54f0239cfa0506575ae4d87a9d"}, + {file = "types_python_dateutil-2.8.19.5-py3-none-any.whl", hash = "sha256:253c267e71cac148003db200cb3fc572ab0e2f994b34a4c1de5d3f550f0ad5b2"}, +] + [[package]] name = "types-requests" version = "2.28.11.7" @@ -2468,14 +2571,14 @@ files = [ [[package]] name = "urllib3" -version = "1.26.13" +version = "1.26.14" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, - {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, + {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, + {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, ] [package.extras] @@ -2500,37 +2603,40 @@ test = ["coverage", "flake8 (>=3.7)", "mypy", "pretend", "pytest"] [[package]] name = "watchdog" -version = "2.1.9" +version = "2.2.1" description = "Filesystem events monitoring" category = "dev" optional = false python-versions = ">=3.6" files = [ - {file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330"}, - {file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b17d302850c8d412784d9246cfe8d7e3af6bcd45f958abb2d08a6f8bedf695d"}, - {file = "watchdog-2.1.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee3e38a6cc050a8830089f79cbec8a3878ec2fe5160cdb2dc8ccb6def8552658"}, - {file = "watchdog-2.1.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64a27aed691408a6abd83394b38503e8176f69031ca25d64131d8d640a307591"}, - {file = "watchdog-2.1.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:195fc70c6e41237362ba720e9aaf394f8178bfc7fa68207f112d108edef1af33"}, - {file = "watchdog-2.1.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bfc4d351e6348d6ec51df007432e6fe80adb53fd41183716017026af03427846"}, - {file = "watchdog-2.1.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8250546a98388cbc00c3ee3cc5cf96799b5a595270dfcfa855491a64b86ef8c3"}, - {file = "watchdog-2.1.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:117ffc6ec261639a0209a3252546b12800670d4bf5f84fbd355957a0595fe654"}, - {file = "watchdog-2.1.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:97f9752208f5154e9e7b76acc8c4f5a58801b338de2af14e7e181ee3b28a5d39"}, - {file = "watchdog-2.1.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:247dcf1df956daa24828bfea5a138d0e7a7c98b1a47cf1fa5b0c3c16241fcbb7"}, - {file = "watchdog-2.1.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:226b3c6c468ce72051a4c15a4cc2ef317c32590d82ba0b330403cafd98a62cfd"}, - {file = "watchdog-2.1.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d9820fe47c20c13e3c9dd544d3706a2a26c02b2b43c993b62fcd8011bcc0adb3"}, - {file = "watchdog-2.1.9-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:70af927aa1613ded6a68089a9262a009fbdf819f46d09c1a908d4b36e1ba2b2d"}, - {file = "watchdog-2.1.9-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed80a1628cee19f5cfc6bb74e173f1b4189eb532e705e2a13e3250312a62e0c9"}, - {file = "watchdog-2.1.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9f05a5f7c12452f6a27203f76779ae3f46fa30f1dd833037ea8cbc2887c60213"}, - {file = "watchdog-2.1.9-py3-none-manylinux2014_armv7l.whl", hash = "sha256:255bb5758f7e89b1a13c05a5bceccec2219f8995a3a4c4d6968fe1de6a3b2892"}, - {file = "watchdog-2.1.9-py3-none-manylinux2014_i686.whl", hash = "sha256:d3dda00aca282b26194bdd0adec21e4c21e916956d972369359ba63ade616153"}, - {file = "watchdog-2.1.9-py3-none-manylinux2014_ppc64.whl", hash = "sha256:186f6c55abc5e03872ae14c2f294a153ec7292f807af99f57611acc8caa75306"}, - {file = "watchdog-2.1.9-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:083171652584e1b8829581f965b9b7723ca5f9a2cd7e20271edf264cfd7c1412"}, - {file = "watchdog-2.1.9-py3-none-manylinux2014_s390x.whl", hash = "sha256:b530ae007a5f5d50b7fbba96634c7ee21abec70dc3e7f0233339c81943848dc1"}, - {file = "watchdog-2.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:4f4e1c4aa54fb86316a62a87b3378c025e228178d55481d30d857c6c438897d6"}, - {file = "watchdog-2.1.9-py3-none-win32.whl", hash = "sha256:5952135968519e2447a01875a6f5fc8c03190b24d14ee52b0f4b1682259520b1"}, - {file = "watchdog-2.1.9-py3-none-win_amd64.whl", hash = "sha256:7a833211f49143c3d336729b0020ffd1274078e94b0ae42e22f596999f50279c"}, - {file = "watchdog-2.1.9-py3-none-win_ia64.whl", hash = "sha256:ad576a565260d8f99d97f2e64b0f97a48228317095908568a9d5c786c829d428"}, - {file = "watchdog-2.1.9.tar.gz", hash = "sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609"}, + {file = "watchdog-2.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a09483249d25cbdb4c268e020cb861c51baab2d1affd9a6affc68ffe6a231260"}, + {file = "watchdog-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5100eae58133355d3ca6c1083a33b81355c4f452afa474c2633bd2fbbba398b3"}, + {file = "watchdog-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e618a4863726bc7a3c64f95c218437f3349fb9d909eb9ea3a1ed3b567417c661"}, + {file = "watchdog-2.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:102a60093090fc3ff76c983367b19849b7cc24ec414a43c0333680106e62aae1"}, + {file = "watchdog-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:748ca797ff59962e83cc8e4b233f87113f3cf247c23e6be58b8a2885c7337aa3"}, + {file = "watchdog-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ccd8d84b9490a82b51b230740468116b8205822ea5fdc700a553d92661253a3"}, + {file = "watchdog-2.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6e01d699cd260d59b84da6bda019dce0a3353e3fcc774408ae767fe88ee096b7"}, + {file = "watchdog-2.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8586d98c494690482c963ffb24c49bf9c8c2fe0589cec4dc2f753b78d1ec301d"}, + {file = "watchdog-2.2.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:adaf2ece15f3afa33a6b45f76b333a7da9256e1360003032524d61bdb4c422ae"}, + {file = "watchdog-2.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:83a7cead445008e880dbde833cb9e5cc7b9a0958edb697a96b936621975f15b9"}, + {file = "watchdog-2.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8ac23ff2c2df4471a61af6490f847633024e5aa120567e08d07af5718c9d092"}, + {file = "watchdog-2.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d0f29fd9f3f149a5277929de33b4f121a04cf84bb494634707cfa8ea8ae106a8"}, + {file = "watchdog-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:967636031fa4c4955f0f3f22da3c5c418aa65d50908d31b73b3b3ffd66d60640"}, + {file = "watchdog-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96cbeb494e6cbe3ae6aacc430e678ce4b4dd3ae5125035f72b6eb4e5e9eb4f4e"}, + {file = "watchdog-2.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61fdb8e9c57baf625e27e1420e7ca17f7d2023929cd0065eb79c83da1dfbeacd"}, + {file = "watchdog-2.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4cb5ecc332112017fbdb19ede78d92e29a8165c46b68a0b8ccbd0a154f196d5e"}, + {file = "watchdog-2.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a480d122740debf0afac4ddd583c6c0bb519c24f817b42ed6f850e2f6f9d64a8"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:978a1aed55de0b807913b7482d09943b23a2d634040b112bdf31811a422f6344"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:8c28c23972ec9c524967895ccb1954bc6f6d4a557d36e681a36e84368660c4ce"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_i686.whl", hash = "sha256:c27d8c1535fd4474e40a4b5e01f4ba6720bac58e6751c667895cbc5c8a7af33c"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d6b87477752bd86ac5392ecb9eeed92b416898c30bd40c7e2dd03c3146105646"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:cece1aa596027ff56369f0b50a9de209920e1df9ac6d02c7f9e5d8162eb4f02b"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:8b5cde14e5c72b2df5d074774bdff69e9b55da77e102a91f36ef26ca35f9819c"}, + {file = "watchdog-2.2.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e038be858425c4f621900b8ff1a3a1330d9edcfeaa1c0468aeb7e330fb87693e"}, + {file = "watchdog-2.2.1-py3-none-win32.whl", hash = "sha256:bc43c1b24d2f86b6e1cc15f68635a959388219426109233e606517ff7d0a5a73"}, + {file = "watchdog-2.2.1-py3-none-win_amd64.whl", hash = "sha256:17f1708f7410af92ddf591e94ae71a27a13974559e72f7e9fde3ec174b26ba2e"}, + {file = "watchdog-2.2.1-py3-none-win_ia64.whl", hash = "sha256:195ab1d9d611a4c1e5311cbf42273bc541e18ea8c32712f2fb703cfc6ff006f9"}, + {file = "watchdog-2.2.1.tar.gz", hash = "sha256:cdcc23c9528601a8a293eb4369cbd14f6b4f34f07ae8769421252e9c22718b6f"}, ] [package.extras] @@ -2653,4 +2759,4 @@ validation = ["fastjsonschema"] [metadata] lock-version = "2.0" python-versions = "^3.7.4" -content-hash = "f0f65f1a63f7a02134227ccc6208b0023b91cb6a51b62c826571f4bb02df4a64" +content-hash = "5714107e22e0e34da5c5247c28262c8135b7c3bc718b309bac7ed62de27e16e5" diff --git a/pyproject.toml b/pyproject.toml index 58fe9fa1852..bf524ff9e5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,7 @@ mypy-boto3-ssm = "^1.26.43" mypy-boto3-s3 = "^1.26.0" mypy-boto3-xray = "^1.26.11" types-requests = "^2.28.11" +types-python-dateutil = "^2.8.19.5" typing-extensions = "^4.4.0" mkdocs-material = "^9.0.3" filelock = "^3.9.0" diff --git a/tests/functional/feature_flags/test_schema_validation.py b/tests/functional/feature_flags/test_schema_validation.py index 2be8f56e11d..73246f97e91 100644 --- a/tests/functional/feature_flags/test_schema_validation.py +++ b/tests/functional/feature_flags/test_schema_validation.py @@ -1,4 +1,5 @@ import logging +import re import pytest # noqa: F401 @@ -372,11 +373,11 @@ def test_validate_rule_boolean_feature_is_set(): def test_validate_time_condition_between_time_range_invalid_condition_key(): # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, - # value of between 11:11 to 23:59 and a key of CURRENT_DATETIME_UTC + # value of between 11:11 to 23:59 and a key of CURRENT_DATETIME condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, CONDITION_VALUE: {TimeValues.START.value: "11:11", TimeValues.END.value: "23:59"}, - CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_DATETIME.value, } rule_name = "dummy" @@ -384,17 +385,17 @@ def test_validate_time_condition_between_time_range_invalid_condition_key(): # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"'condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a 'CURRENT_TIME_UTC' condition key, rule={rule_name}", # noqa: E501 + match=f"'condition with a 'SCHEDULE_BETWEEN_TIME_RANGE' action must have a 'CURRENT_TIME' condition key, rule={rule_name}", # noqa: E501 ): ConditionsValidator.validate_condition_key(condition=condition, rule_name=rule_name) def test_validate_time_condition_between_time_range_invalid_condition_value(): - # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME_UTC and invalid value of string + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME and invalid value of string condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, CONDITION_VALUE: "11:00-22:33", - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, } rule_name = "dummy" @@ -408,12 +409,12 @@ def test_validate_time_condition_between_time_range_invalid_condition_value(): def test_validate_time_condition_between_time_range_invalid_condition_value_no_start_time(): - # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME_UTC and invalid value + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME and invalid value # dict without START key condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, CONDITION_VALUE: {TimeValues.END.value: "23:59"}, - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, } rule_name = "dummy" @@ -427,12 +428,12 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_no_s def test_validate_time_condition_between_time_range_invalid_condition_value_no_end_time(): - # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME_UTC and invalid value + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME and invalid value # dict without END key condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, CONDITION_VALUE: {TimeValues.START.value: "23:59"}, - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, } rule_name = "dummy" @@ -446,12 +447,12 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_no_e def test_validate_time_condition_between_time_range_invalid_condition_value_invalid_start_time_type(): - # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME_UTC and + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME and # invalid START value as a number condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, CONDITION_VALUE: {TimeValues.START.value: 4, TimeValues.END.value: "23:59"}, - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, } rule_name = "dummy" @@ -465,12 +466,12 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_inva def test_validate_time_condition_between_time_range_invalid_condition_value_invalid_end_time_type(): - # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME_UTC and + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME and # invalid START value as a number condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, CONDITION_VALUE: {TimeValues.START.value: "11:11", TimeValues.END.value: 4}, - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, } rule_name = "dummy" @@ -491,15 +492,15 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_inva ], ) def test_validate_time_condition_between_time_range_invalid_condition_value_invalid_start_time_value(cond_value): - # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME_UTC and + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME and # invalid START value as an invalid time format condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, CONDITION_VALUE: cond_value, - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, } rule_name = "dummy" - match_str = f"START' and 'END' must be a valid time format, time_format=%H:%M, rule={rule_name}" + match_str = f"'START' and 'END' must be a valid time format, time_format=%H:%M, rule={rule_name}" # WHEN calling validate_condition # THEN raise SchemaValidationError with pytest.raises( @@ -517,15 +518,15 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_inva ], ) def test_validate_time_condition_between_time_range_invalid_condition_value_invalid_end_time_value(cond_value): - # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME_UTC and + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME and # invalid END value as an invalid time format condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, CONDITION_VALUE: cond_value, - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, } rule_name = "dummy" - match_str = f"START' and 'END' must be a valid time format, time_format=%H:%M, rule={rule_name}" + match_str = f"'START' and 'END' must be a valid time format, time_format=%H:%M, rule={rule_name}" # WHEN calling validate_condition # THEN raise SchemaValidationError with pytest.raises( @@ -535,16 +536,56 @@ def test_validate_time_condition_between_time_range_invalid_condition_value_inva ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) +def test_validate_time_condition_between_time_range_invalid_timezone(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME and + # invalid timezone + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, + CONDITION_VALUE: { + TimeValues.START.value: "10:11", + TimeValues.END.value: "10:59", + TimeValues.TIMEZONE.value: "Europe/Tokyo", + }, + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, + } + rule_name = "dummy" + match_str = f"'TIMEZONE' value must represent a valid IANA timezone, rule={rule_name}" + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=match_str, + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +def test_validate_time_condition_between_time_range_valid_timezone(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_TIME_RANGE action, key CURRENT_TIME and + # valid timezone + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, + CONDITION_VALUE: { + TimeValues.START.value: "10:11", + TimeValues.END.value: "10:59", + TimeValues.TIMEZONE.value: "Europe/Copenhagen", + }, + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, + } + # WHEN calling validate_condition + # THEN nothing is raised + ConditionsValidator.validate_condition_value(condition=condition, rule_name="dummy") + + def test_validate_time_condition_between_datetime_range_invalid_condition_key(): # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, - # value of between "2022-10-05T12:15:00Z" to "2022-10-10T12:15:00Z" and a key of CURRENT_TIME_UTC + # value of between "2022-10-05T12:15:00Z" to "2022-10-10T12:15:00Z" and a key of CURRENT_TIME condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, CONDITION_VALUE: { TimeValues.START.value: "2022-10-05T12:15:00Z", TimeValues.END.value: "2022-10-10T12:15:00Z", }, - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, } rule_name = "dummy" @@ -552,17 +593,17 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_key(): # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"'condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a 'CURRENT_DATETIME_UTC' condition key, rule={rule_name}", # noqa: E501 + match=f"'condition with a 'SCHEDULE_BETWEEN_DATETIME_RANGE' action must have a 'CURRENT_DATETIME' condition key, rule={rule_name}", # noqa: E501 ): ConditionsValidator.validate_condition_key(condition=condition, rule_name=rule_name) def test_a_validate_time_condition_between_datetime_range_invalid_condition_value(): - # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and invalid value of string # noqa: E501 + # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME and invalid value of string # noqa: E501 condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, CONDITION_VALUE: "11:00-22:33", - CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_DATETIME.value, } rule_name = "dummy" @@ -576,12 +617,12 @@ def test_a_validate_time_condition_between_datetime_range_invalid_condition_valu def test_validate_time_condition_between_datetime_range_invalid_condition_value_no_start_time(): - # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and invalid value + # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME and invalid value # dict without START key condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, CONDITION_VALUE: {TimeValues.END.value: "2022-10-10T12:15:00Z"}, - CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_DATETIME.value, } rule_name = "dummy" @@ -595,12 +636,12 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ def test_validate_time_condition_between_datetime_range_invalid_condition_value_no_end_time(): - # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and invalid value + # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME and invalid value # dict without END key condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, CONDITION_VALUE: {TimeValues.START.value: "2022-10-10T12:15:00Z"}, - CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_DATETIME.value, } rule_name = "dummy" @@ -614,12 +655,12 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ def test_validate_time_condition_between_datetime_range_invalid_condition_value_invalid_start_time_type(): - # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and + # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME and # invalid START value as a number condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, CONDITION_VALUE: {TimeValues.START.value: 4, TimeValues.END.value: "2022-10-10T12:15:00Z"}, - CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_DATETIME.value, } rule_name = "dummy" @@ -633,12 +674,12 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ def test_validate_time_condition_between_datetime_range_invalid_condition_value_invalid_end_time_type(): - # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and + # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME and # invalid START value as a number condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, CONDITION_VALUE: {TimeValues.END.value: 4, TimeValues.START.value: "2022-10-10T12:15:00Z"}, - CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_DATETIME.value, } rule_name = "dummy" @@ -660,15 +701,15 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ ], ) def test_validate_time_condition_between_datetime_range_invalid_condition_value_invalid_start_time_value(cond_value): - # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and + # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME and # invalid START value as an invalid time format condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, CONDITION_VALUE: cond_value, - CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_DATETIME.value, } rule_name = "dummy" - match_str = f"START' and 'END' must be a valid ISO8601 time format, rule={rule_name}" + match_str = f"'START' and 'END' must be a valid ISO8601 time format, rule={rule_name}" # WHEN calling validate_condition # THEN raise SchemaValidationError with pytest.raises( @@ -679,15 +720,34 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ def test_validate_time_condition_between_datetime_range_invalid_condition_value_invalid_end_time_value(): - # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME_UTC and + # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME and # invalid END value as an invalid time format condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, - CONDITION_VALUE: {TimeValues.END.value: "10:10", TimeValues.START.value: "2022-10-10T12:15:00Z"}, - CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + CONDITION_VALUE: {TimeValues.END.value: "10:10", TimeValues.START.value: "2022-10-10T12:15:00"}, + CONDITION_KEY: TimeKeys.CURRENT_DATETIME.value, + } + rule_name = "dummy" + match_str = f"'START' and 'END' must be a valid ISO8601 time format, rule={rule_name}" + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises(SchemaValidationError, match=match_str): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +def test_validate_time_condition_between_datetime_range_including_timezone(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_DATETIME_RANGE action, key CURRENT_DATETIME and + # invalid START and END timestamps with timezone information + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, + CONDITION_VALUE: {TimeValues.END.value: "2022-10-10T11:15:00Z", TimeValues.START.value: "2022-10-10T12:15:00Z"}, + CONDITION_KEY: TimeKeys.CURRENT_DATETIME.value, } rule_name = "dummy" - match_str = f"START' and 'END' must be a valid ISO8601 time format, rule={rule_name}" + match_str = ( + f"'START' and 'END' must not include timezone information. Set the timezone using the 'TIMEZONE' " + f"field, rule={rule_name} " + ) # WHEN calling validate_condition # THEN raise SchemaValidationError with pytest.raises(SchemaValidationError, match=match_str): @@ -696,11 +756,13 @@ def test_validate_time_condition_between_datetime_range_invalid_condition_value_ def test_validate_time_condition_between_days_range_invalid_condition_key(): # GIVEN a configuration with a SCHEDULE_BETWEEN_DAYS_OF_WEEK action, - # value of SUNDAY and a key of CURRENT_TIME_UTC + # value of SUNDAY and a key of CURRENT_TIME condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, - CONDITION_VALUE: [TimeValues.SUNDAY.value], - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_VALUE: { + TimeValues.DAYS.value: [TimeValues.SUNDAY.value], + }, + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, } rule_name = "dummy" @@ -708,43 +770,49 @@ def test_validate_time_condition_between_days_range_invalid_condition_key(): # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=f"'condition with a 'SCHEDULE_BETWEEN_DAYS_OF_WEEK' action must have a 'CURRENT_DAY_OF_WEEK_UTC' condition key, rule={rule_name}", # noqa: E501 + match=f"'condition with a 'SCHEDULE_BETWEEN_DAYS_OF_WEEK' action must have a 'CURRENT_DAY_OF_WEEK' condition key, rule={rule_name}", # noqa: E501 ): ConditionsValidator.validate_condition_key(condition=condition, rule_name=rule_name) def test_validate_time_condition_between_days_range_invalid_condition_type(): # GIVEN a configuration with a SCHEDULE_BETWEEN_DAYS_OF_WEEK action - # key CURRENT_DAY_OF_WEEK_UTC and invalid value type string + # key CURRENT_DAY_OF_WEEK and invalid value type string condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, CONDITION_VALUE: TimeValues.SATURDAY.value, - CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK.value, } rule_name = "dummy" - match_str = f"condition with a CURRENT_DAY_OF_WEEK_UTC action must have a non empty condition value type list, rule={rule_name}" # noqa: E501 + match_str = f"condition with a CURRENT_DAY_OF_WEEK action must have a condition value dictionary with 'DAYS' and 'TIMEZONE' (optional) keys, rule={rule_name}" # noqa: E501 # WHEN calling validate_condition # THEN raise SchemaValidationError with pytest.raises( SchemaValidationError, - match=match_str, + match=re.escape(match_str), ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) @pytest.mark.parametrize( - "cond_value", [[TimeValues.SUNDAY.value, "funday"], [TimeValues.SUNDAY, TimeValues.MONDAY.value]] + "cond_value", + [ + {TimeValues.DAYS.value: [TimeValues.SUNDAY.value, "funday"]}, + {TimeValues.DAYS.value: [TimeValues.SUNDAY, TimeValues.MONDAY.value]}, + ], ) def test_validate_time_condition_between_days_range_invalid_condition_value(cond_value): # GIVEN a configuration with a SCHEDULE_BETWEEN_DAYS_OF_WEEK action - # key CURRENT_DAY_OF_WEEK_UTC and invalid value not day string + # key CURRENT_DAY_OF_WEEK and invalid value not day string condition = { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, CONDITION_VALUE: cond_value, - CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK.value, } rule_name = "dummy" - match_str = f"condition value must represent a day of the week in 'TimeValues' enum, rule={rule_name}" # noqa: E501 + match_str = ( + f"condition value DAYS must represent a day of the week in 'TimeValues' enum, rule={rule_name}" # noqa: E501 + ) # WHEN calling validate_condition # THEN raise SchemaValidationError with pytest.raises( @@ -752,3 +820,38 @@ def test_validate_time_condition_between_days_range_invalid_condition_value(cond match=match_str, ): ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +def test_validate_time_condition_between_days_range_invalid_timezone(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_DAYS_OF_WEEK action + # key CURRENT_DAY_OF_WEEK and an invalid timezone + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, + CONDITION_VALUE: {TimeValues.DAYS.value: [TimeValues.SUNDAY.value], TimeValues.TIMEZONE.value: "Europe/Tokyo"}, + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK.value, + } + rule_name = "dummy" + match_str = f"'TIMEZONE' value must represent a valid IANA timezone, rule={rule_name}" + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=match_str, + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +def test_validate_time_condition_between_days_range_valid_timezone(): + # GIVEN a configuration with a SCHEDULE_BETWEEN_DAYS_OF_WEEK action + # key CURRENT_DAY_OF_WEEK and a valid timezone + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, + CONDITION_VALUE: { + TimeValues.DAYS.value: [TimeValues.SUNDAY.value], + TimeValues.TIMEZONE.value: "Europe/Copenhagen", + }, + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK.value, + } + # WHEN calling validate_condition + # THEN nothing is raised + ConditionsValidator.validate_condition_value(condition=condition, rule_name="dummy") diff --git a/tests/functional/feature_flags/test_time_based_actions.py b/tests/functional/feature_flags/test_time_based_actions.py index 1df39d2eb60..54bba34fa02 100644 --- a/tests/functional/feature_flags/test_time_based_actions.py +++ b/tests/functional/feature_flags/test_time_based_actions.py @@ -2,7 +2,9 @@ from typing import Any, Dict, Optional, Tuple from botocore.config import Config +from dateutil.tz import gettz +from aws_lambda_powertools.shared.types import JSONType from aws_lambda_powertools.utilities.feature_flags.appconfig import AppConfigStore from aws_lambda_powertools.utilities.feature_flags.feature_flags import FeatureFlags from aws_lambda_powertools.utilities.feature_flags.schema import ( @@ -22,9 +24,9 @@ def evaluate_mocked_schema( mocker, rules: Dict[str, Any], - mocked_time: Tuple[int, int, int, int, int, int], # year, month, day, hour, minute, second + mocked_time: Tuple[int, int, int, int, int, int, datetime.tzinfo], # year, month, day, hour, minute, second context: Optional[Dict[str, Any]] = None, -) -> bool: +) -> JSONType: """ This helper does the following: 1. mocks the current time @@ -33,18 +35,10 @@ def evaluate_mocked_schema( """ # Mock the current time - year, month, day, hour, minute, second = mocked_time - - mocked_time = mocker.patch("aws_lambda_powertools.utilities.feature_flags.time_conditions._get_utc_time_now") - mocked_time.return_value = datetime.datetime( - year=year, - month=month, - day=day, - hour=hour, - minute=minute, - second=second, - microsecond=0, - tzinfo=datetime.timezone.utc, + year, month, day, hour, minute, second, timezone = mocked_time + time = mocker.patch("aws_lambda_powertools.utilities.feature_flags.time_conditions._get_now_from_timezone") + time.return_value = datetime.datetime( + year=year, month=month, day=day, hour=hour, minute=minute, second=second, microsecond=0, tzinfo=timezone ) # Mock the returned data from AppConfig @@ -84,13 +78,13 @@ def test_time_based_utc_in_between_time_range_rule_match(mocker): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, CONDITION_VALUE: {TimeValues.START.value: "11:11", TimeValues.END.value: "23:59"}, }, ], } }, - mocked_time=(2022, 2, 15, 11, 12, 0), + mocked_time=(2022, 2, 15, 11, 12, 0, datetime.timezone.utc), ) @@ -103,13 +97,63 @@ def test_time_based_utc_in_between_time_range_no_rule_match(mocker): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, CONDITION_VALUE: {TimeValues.START.value: "11:11", TimeValues.END.value: "23:59"}, }, ], } }, - mocked_time=(2022, 2, 15, 7, 12, 0), # no rule match 7:12 am + mocked_time=(2022, 2, 15, 7, 12, 0, datetime.timezone.utc), # no rule match 7:12 am + ) + + +def test_time_based_between_time_range_rule_timezone_match(mocker): + timezone_name = "Europe/Copenhagen" + + assert evaluate_mocked_schema( + mocker=mocker, + rules={ + "lambda time is between UTC 11:11-23:59, Copenhagen Time": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, + CONDITION_VALUE: { + TimeValues.START.value: "11:11", + TimeValues.END.value: "23:59", + TimeValues.TIMEZONE.value: timezone_name, + }, + }, + ], + } + }, + mocked_time=(2022, 2, 15, 11, 11, 0, gettz(timezone_name)), # rule match 11:11 am, Europe/Copenhagen + ) + + +def test_time_based_between_time_range_rule_timezone_no_match(mocker): + timezone_name = "Europe/Copenhagen" + + assert not evaluate_mocked_schema( + mocker=mocker, + rules={ + "lambda time is between UTC 11:11-23:59, Copenhagen Time": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, + CONDITION_VALUE: { + TimeValues.START.value: "11:11", + TimeValues.END.value: "23:59", + TimeValues.TIMEZONE.value: timezone_name, + }, + }, + ], + } + }, + mocked_time=(2022, 2, 15, 10, 11, 0, gettz(timezone_name)), # no rule match 10:11 am, Europe/Copenhagen ) @@ -122,20 +166,45 @@ def test_time_based_utc_in_between_full_time_range_rule_match(mocker): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, # condition matches - CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_DATETIME.value, CONDITION_VALUE: { - TimeValues.START.value: "2022-10-05T12:15:00Z", - TimeValues.END.value: "2022-10-10T12:15:00Z", + TimeValues.START.value: "2022-10-05T12:15:00", + TimeValues.END.value: "2022-10-10T12:15:00", }, }, ], } }, - mocked_time=(2022, 10, 7, 10, 0, 0), # will match rule + mocked_time=(2022, 10, 7, 10, 0, 0, datetime.timezone.utc), # will match rule ) def test_time_based_utc_in_between_full_time_range_no_rule_match(mocker): + timezone_name = "Europe/Copenhagen" + + assert not evaluate_mocked_schema( + mocker=mocker, + rules={ + "lambda time is between UTC october 5th 2022 12:14:32PM to october 10th 2022 12:15:00 PM": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, # condition matches + CONDITION_KEY: TimeKeys.CURRENT_DATETIME.value, + CONDITION_VALUE: { + TimeValues.START.value: "2022-10-05T12:15:00", + TimeValues.END.value: "2022-10-10T12:15:00", + TimeValues.TIMEZONE.value: timezone_name, + }, + }, + ], + } + }, + mocked_time=(2022, 9, 7, 10, 0, 0, gettz(timezone_name)), # will not rule match + ) + + +def test_time_based_utc_in_between_full_time_range_timezone_no_match(mocker): assert not evaluate_mocked_schema( mocker=mocker, rules={ @@ -144,16 +213,17 @@ def test_time_based_utc_in_between_full_time_range_no_rule_match(mocker): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value, # condition matches - CONDITION_KEY: TimeKeys.CURRENT_DATETIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_DATETIME.value, CONDITION_VALUE: { - TimeValues.START.value: "2022-10-05T12:15:00Z", - TimeValues.END.value: "2022-10-10T12:15:00Z", + TimeValues.START.value: "2022-10-05T12:15:00", + TimeValues.END.value: "2022-10-10T12:15:00", + TimeValues.TIMEZONE.value: "Europe/Copenhagen", }, }, ], } }, - mocked_time=(2022, 9, 7, 10, 0, 0), # will not rule match + mocked_time=(2022, 10, 10, 12, 15, 0, gettz("America/New_York")), # will not rule match, it's too late ) @@ -166,7 +236,7 @@ def test_time_based_multiple_conditions_utc_in_between_time_range_rule_match(moc CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, CONDITION_VALUE: {TimeValues.START.value: "09:00", TimeValues.END.value: "17:00"}, }, { @@ -177,7 +247,7 @@ def test_time_based_multiple_conditions_utc_in_between_time_range_rule_match(moc ], } }, - mocked_time=(2022, 10, 7, 10, 0, 0), # will rule match + mocked_time=(2022, 10, 7, 10, 0, 0, datetime.timezone.utc), # will rule match context={"username": "ran"}, ) @@ -191,7 +261,7 @@ def test_time_based_multiple_conditions_utc_in_between_time_range_no_rule_match( CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, CONDITION_VALUE: {TimeValues.START.value: "09:00", TimeValues.END.value: "17:00"}, }, { @@ -202,7 +272,7 @@ def test_time_based_multiple_conditions_utc_in_between_time_range_no_rule_match( ], } }, - mocked_time=(2022, 10, 7, 7, 0, 0), # will cause no rule match, 7:00 + mocked_time=(2022, 10, 7, 7, 0, 0, datetime.timezone.utc), # will cause no rule match, 7:00 context={"username": "ran"}, ) @@ -216,19 +286,21 @@ def test_time_based_utc_days_range_rule_match(mocker): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, # similar to "IN" actions - CONDITION_VALUE: [ - TimeValues.MONDAY.value, - TimeValues.TUESDAY.value, - TimeValues.WEDNESDAY.value, - TimeValues.THURSDAY.value, - TimeValues.FRIDAY.value, - ], + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK.value, # similar to "IN" actions + CONDITION_VALUE: { + TimeValues.DAYS.value: [ + TimeValues.MONDAY.value, + TimeValues.TUESDAY.value, + TimeValues.WEDNESDAY.value, + TimeValues.THURSDAY.value, + TimeValues.FRIDAY.value, + ], + }, }, ], } }, - mocked_time=(2022, 11, 18, 10, 0, 0), # friday + mocked_time=(2022, 11, 18, 10, 0, 0, datetime.timezone.utc), # friday ) @@ -241,19 +313,21 @@ def test_time_based_utc_days_range_no_rule_match(mocker): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, # similar to "IN" actions - CONDITION_VALUE: [ - TimeValues.MONDAY.value, - TimeValues.TUESDAY.value, - TimeValues.WEDNESDAY.value, - TimeValues.THURSDAY.value, - TimeValues.FRIDAY.value, - ], + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK.value, # similar to "IN" actions + CONDITION_VALUE: { + TimeValues.DAYS.value: [ + TimeValues.MONDAY.value, + TimeValues.TUESDAY.value, + TimeValues.WEDNESDAY.value, + TimeValues.THURSDAY.value, + TimeValues.FRIDAY.value, + ], + }, }, ], } }, - mocked_time=(2022, 11, 20, 10, 0, 0), # sunday, no match + mocked_time=(2022, 11, 20, 10, 0, 0, datetime.timezone.utc), # sunday, no match ) @@ -266,13 +340,63 @@ def test_time_based_utc_only_weekend_rule_match(mocker): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, # similar to "IN" actions - CONDITION_VALUE: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK.value, # similar to "IN" actions + CONDITION_VALUE: { + TimeValues.DAYS.value: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], + }, + }, + ], + } + }, + mocked_time=(2022, 11, 19, 10, 0, 0, datetime.timezone.utc), # saturday + ) + + +def test_time_based_utc_only_weekend_with_timezone_rule_match(mocker): + timezone_name = "Europe/Copenhagen" + + assert evaluate_mocked_schema( + mocker=mocker, + rules={ + "match only on weekend": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK.value, # similar to "IN" actions + CONDITION_VALUE: { + TimeValues.DAYS.value: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], + TimeValues.TIMEZONE.value: timezone_name, + }, + }, + ], + } + }, + mocked_time=(2022, 11, 19, 10, 0, 0, gettz(timezone_name)), # saturday + ) + + +def test_time_based_utc_only_weekend_with_timezone_rule_no_match(mocker): + timezone_name = "Europe/Copenhagen" + + assert not evaluate_mocked_schema( + mocker=mocker, + rules={ + "match only on weekend": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK.value, # similar to "IN" actions + CONDITION_VALUE: { + TimeValues.DAYS.value: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], + TimeValues.TIMEZONE.value: timezone_name, + }, }, ], } }, - mocked_time=(2022, 11, 19, 10, 0, 0), # saturday + mocked_time=(2022, 11, 21, 0, 0, 0, gettz("Europe/Copenhagen")), # monday, 00:00 ) @@ -285,13 +409,15 @@ def test_time_based_utc_only_weekend_no_rule_match(mocker): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, # similar to "IN" actions - CONDITION_VALUE: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK.value, # similar to "IN" actions + CONDITION_VALUE: { + TimeValues.DAYS.value: [TimeValues.SATURDAY.value, TimeValues.SUNDAY.value], + }, }, ], } }, - mocked_time=(2022, 11, 18, 10, 0, 0), # friday, no match + mocked_time=(2022, 11, 18, 10, 0, 0, datetime.timezone.utc), # friday, no match ) @@ -304,23 +430,23 @@ def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_rule_ma CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, CONDITION_VALUE: {TimeValues.START.value: "11:00", TimeValues.END.value: "23:00"}, }, { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, # this condition matches - CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, - CONDITION_VALUE: [TimeValues.MONDAY.value, TimeValues.THURSDAY.value], + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK.value, + CONDITION_VALUE: {TimeValues.DAYS.value: [TimeValues.MONDAY.value, TimeValues.THURSDAY.value]}, }, ], } }, - mocked_time=(2022, 11, 17, 16, 0, 0), # thursday 16:00 + mocked_time=(2022, 11, 17, 16, 0, 0, datetime.timezone.utc), # thursday 16:00 ) def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_no_rule_match(mocker): - def evaluate(mocked_time: Tuple[int, int, int, int, int, int]): + def evaluate(mocked_time: Tuple[int, int, int, int, int, int, datetime.tzinfo]): evaluate_mocked_schema( mocker=mocker, rules={ @@ -329,13 +455,15 @@ def evaluate(mocked_time: Tuple[int, int, int, int, int, int]): CONDITIONS_KEY: [ { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, - CONDITION_KEY: TimeKeys.CURRENT_TIME_UTC.value, + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, CONDITION_VALUE: {TimeValues.START.value: "11:00", TimeValues.END.value: "23:00"}, }, { CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, - CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK_UTC.value, - CONDITION_VALUE: [TimeValues.MONDAY.value, TimeValues.THURSDAY.value], + CONDITION_KEY: TimeKeys.CURRENT_DAY_OF_WEEK.value, + CONDITION_VALUE: { + TimeValues.DAYS.value: [TimeValues.MONDAY.value, TimeValues.THURSDAY.value] + }, }, ], } @@ -343,6 +471,6 @@ def evaluate(mocked_time: Tuple[int, int, int, int, int, int]): mocked_time=mocked_time, ) - assert not evaluate(mocked_time=(2022, 11, 17, 9, 0, 0)) # thursday 9:00 - assert not evaluate(mocked_time=(2022, 11, 18, 13, 0, 0)) # friday 16:00 - assert not evaluate(mocked_time=(2022, 11, 18, 9, 0, 0)) # friday 9:00 + assert not evaluate(mocked_time=(2022, 11, 17, 9, 0, 0, datetime.timezone.utc)) # thursday 9:00 + assert not evaluate(mocked_time=(2022, 11, 18, 13, 0, 0, datetime.timezone.utc)) # friday 16:00 + assert not evaluate(mocked_time=(2022, 11, 18, 9, 0, 0, datetime.timezone.utc)) # friday 9:00 From b1d6ff264950e51a20933b10e931e72831f679ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Wed, 11 Jan 2023 12:05:54 +0100 Subject: [PATCH 27/34] fix: updated doc examples --- .../src/timebased_feature_event.json | 6 +-- .../feature_flags/src/timebased_features.json | 52 +++++++++++-------- .../src/timebased_happyhour_features.json | 33 ++++++------ 3 files changed, 49 insertions(+), 42 deletions(-) diff --git a/examples/feature_flags/src/timebased_feature_event.json b/examples/feature_flags/src/timebased_feature_event.json index 66067eb487b..894a250d5ec 100644 --- a/examples/feature_flags/src/timebased_feature_event.json +++ b/examples/feature_flags/src/timebased_feature_event.json @@ -1,5 +1,5 @@ { - "username": "rubefons", - "tier": "premium", - "basked_id": "random_id" + "username": "rubefons", + "tier": "premium", + "basked_id": "random_id" } diff --git a/examples/feature_flags/src/timebased_features.json b/examples/feature_flags/src/timebased_features.json index 77394640eaa..5c47286f679 100644 --- a/examples/feature_flags/src/timebased_features.json +++ b/examples/feature_flags/src/timebased_features.json @@ -1,27 +1,33 @@ { - "weekend_premium_discount": { - "default": false, - "rules": { - "customer tier equals premium": { - "when_match": true, - "conditions": [ - { - "action": "EQUALS", - "key": "tier", - "value": "premium" - } - ] - }, - "is weekend": { - "when_match": true, - "conditions": [ - { - "action": "SCHEDULE_BETWEEN_DAYS_OF_WEEK", - "key": "CURRENT_DAY_OF_WEEK_UTC", - "value": ["SATURDAY", "SUNDAY"] - } - ] + "weekend_premium_discount": { + "default": false, + "rules": { + "customer tier equals premium": { + "when_match": true, + "conditions": [ + { + "action": "EQUALS", + "key": "tier", + "value": "premium" + } + ] + }, + "is weekend": { + "when_match": true, + "conditions": [ + { + "action": "SCHEDULE_BETWEEN_DAYS_OF_WEEK", + "key": "CURRENT_DAY_OF_WEEK", + "value": { + "DAYS": [ + "SATURDAY", + "SUNDAY" + ], + "TIMEZONE": "America/New_York" } - } + } + ] + } } + } } diff --git a/examples/feature_flags/src/timebased_happyhour_features.json b/examples/feature_flags/src/timebased_happyhour_features.json index cd49f385663..22a239882cc 100644 --- a/examples/feature_flags/src/timebased_happyhour_features.json +++ b/examples/feature_flags/src/timebased_happyhour_features.json @@ -1,20 +1,21 @@ { - "happy_hour": { - "default": false, - "rules": { - "is happy hour": { - "when_match": true, - "conditions": [ - { - "action": "SCHEDULE_BETWEEN_TIME_RANGE", - "key": "CURRENT_TIME_UTC", - "value": { - "START": "17:00", - "END": "19:00" - } - } - ] + "happy_hour": { + "default": false, + "rules": { + "is happy hour": { + "when_match": true, + "conditions": [ + { + "action": "SCHEDULE_BETWEEN_TIME_RANGE", + "key": "CURRENT_TIME", + "value": { + "START": "17:00", + "END": "19:00", + "TIMEZONE": "Europe/Copenhagen" } - } + } + ] + } } + } } From beec3ad2e50afae5f63b6ad531fad0869e6853f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Wed, 11 Jan 2023 12:18:53 +0100 Subject: [PATCH 28/34] docs: update docs --- docs/utilities/feature_flags.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index 8361dcb4737..f1eed662106 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -473,7 +473,7 @@ Use cases: === "features.json" - ```json hl_lines="15 19-21" + ```json hl_lines="15 19-27" --8<-- "examples/feature_flags/src/timebased_features.json" ``` @@ -487,10 +487,18 @@ You can also have features enabled only at certain times of the day. === "features.json" - ```json hl_lines="9-14" + ```json hl_lines="9-15" --8<-- "examples/feature_flags/src/timebased_happyhour_features.json" ``` +???+ info "How should I use timezones?" + You can use any [IANA time zone](https://www.iana.org/time-zones) (as originally specified + in [PEP 615](https://peps.python.org/pep-0615/)) as part of your rules definition. + Powertools takes care of converting and calculate the correct timestamps for you. + + When using `SCHEDULE_BETWEEN_DATETIME_RANGE`, use timestamps without timezone information, and + specify the timezone manually. This way, you'll avoid hitting problems with day light savings. + ## Advanced ### Adjusting in-memory cache @@ -649,11 +657,13 @@ The `action` configuration can have the following values, where the expressions For time based keys, we provide a list of predefined keys. These will automatically get converted to the corresponding timestamp on each invocation of your Lambda function. - | Key | Meaning (always in UTC) | + | Key | Meaning | |-------------------------|--------------------------------------------------------------------------| - | CURRENT_TIME_UTC | The current time, 24 hour format (HH:mm) | - | CURRENT_DATETIME_UTC | The current datetime ([ISO8601](https://en.wikipedia.org/wiki/ISO_8601)) | - | CURRENT_DAY_OF_WEEK_UTC | The current day of the week (Monday-Sunday) | + | CURRENT_TIME | The current time, 24 hour format (HH:mm) | + | CURRENT_DATETIME | The current datetime ([ISO8601](https://en.wikipedia.org/wiki/ISO_8601)) | + | CURRENT_DAY_OF_WEEK | The current day of the week (Monday-Sunday) | + + If not specified, the timezone used for calculations will be UTC. **For multiple conditions**, we will evaluate the list of conditions as a logical `AND`, so all conditions needs to match to return `when_match` value. From 8b0e14e0206021776eea36ecf9863e663d923970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Wed, 11 Jan 2023 14:14:17 +0100 Subject: [PATCH 29/34] chore: add more doc strings --- .../utilities/feature_flags/schema.py | 37 ++++++++++++++++--- .../feature_flags/time_conditions.py | 11 +++++- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/schema.py b/aws_lambda_powertools/utilities/feature_flags/schema.py index aebc82417f7..a2c4752af62 100644 --- a/aws_lambda_powertools/utilities/feature_flags/schema.py +++ b/aws_lambda_powertools/utilities/feature_flags/schema.py @@ -18,8 +18,8 @@ CONDITION_VALUE = "value" CONDITION_ACTION = "action" FEATURE_DEFAULT_VAL_TYPE_KEY = "boolean_type" -TIME_RANGE_FORMAT = "%H:%M" -TIME_RANGE_RE_PATTERN = re.compile(r"2[0-3]:[0-5]\d|[0-1]\d:[0-5]\d") +TIME_RANGE_FORMAT = "%H:%M" # hour:min 24 hours clock +TIME_RANGE_RE_PATTERN = re.compile(r"2[0-3]:[0-5]\d|[0-1]\d:[0-5]\d") # 24 hour clock HOUR_MIN_SEPARATOR = ":" @@ -38,18 +38,26 @@ class RuleAction(Enum): KEY_NOT_IN_VALUE = "KEY_NOT_IN_VALUE" VALUE_IN_KEY = "VALUE_IN_KEY" VALUE_NOT_IN_KEY = "VALUE_NOT_IN_KEY" - SCHEDULE_BETWEEN_TIME_RANGE = "SCHEDULE_BETWEEN_TIME_RANGE" # hour:min 24 hours clock UTC time - SCHEDULE_BETWEEN_DATETIME_RANGE = "SCHEDULE_BETWEEN_DATETIME_RANGE" # full datetime format - SCHEDULE_BETWEEN_DAYS_OF_WEEK = "SCHEDULE_BETWEEN_DAYS_OF_WEEK" + SCHEDULE_BETWEEN_TIME_RANGE = "SCHEDULE_BETWEEN_TIME_RANGE" # hour:min 24 hours clock + SCHEDULE_BETWEEN_DATETIME_RANGE = "SCHEDULE_BETWEEN_DATETIME_RANGE" # full datetime format, excluding timezone + SCHEDULE_BETWEEN_DAYS_OF_WEEK = "SCHEDULE_BETWEEN_DAYS_OF_WEEK" # MONDAY, TUESDAY, .... see TimeValues enum class TimeKeys(Enum): + """ + Possible keys when using time rules + """ + CURRENT_TIME = "CURRENT_TIME" CURRENT_DAY_OF_WEEK = "CURRENT_DAY_OF_WEEK" CURRENT_DATETIME = "CURRENT_DATETIME" class TimeValues(Enum): + """ + Possible values when using time rules + """ + START = "START" END = "END" TIMEZONE = "TIMEZONE" @@ -294,6 +302,11 @@ def validate_condition_key(condition: Dict[str, Any], rule_name: str): key = condition.get(CONDITION_KEY, "") if not key or not isinstance(key, str): raise SchemaValidationError(f"'key' value must be a non empty string, rule={rule_name}") + + # time actions need to have very specific keys + # SCHEDULE_BETWEEN_TIME_RANGE => CURRENT_TIME + # SCHEDULE_BETWEEN_DATETIME_RANGE => CURRENT_DATETIME + # SCHEDULE_BETWEEN_DAYS_OF_WEEK => CURRENT_DAY_OF_WEEK action = condition.get(CONDITION_ACTION, "") if action == RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value and key != TimeKeys.CURRENT_TIME.value: raise SchemaValidationError( @@ -314,7 +327,8 @@ def validate_condition_value(condition: Dict[str, Any], rule_name: str): if not value: raise SchemaValidationError(f"'value' key must not be empty, rule={rule_name}") action = condition.get(CONDITION_ACTION, "") - # time actions + + # time actions need to be parsed to make sure date and time format is valid and timezone is recognized if action == RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value: ConditionsValidator._validate_schedule_between_time_and_datetime_ranges( value, rule_name, action, ConditionsValidator._validate_time_value @@ -330,9 +344,14 @@ def validate_condition_value(condition: Dict[str, Any], rule_name: str): def _validate_datetime_value(datetime_str: str, rule_name: str): date = None + # We try to parse first with timezone information in order to return the correct error messages + # when a timestamp with timezone is used. Otherwise, the user would get the first error "must be a valid + # ISO8601 time format" which is misleading + try: # python < 3.11 don't support the Z timezone on datetime.fromisoformat, # so we replace any Z with the equivalent "+00:00" + # datetime.fromisoformat is orders of magnitude faster than datetime.strptime date = datetime.fromisoformat(datetime_str.replace("Z", "+00:00")) except Exception: 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): timezone = value.get(TimeValues.TIMEZONE.value, "UTC") if not isinstance(timezone, str): raise SchemaValidationError(error_str) + + # try to see if the timezone string corresponds to any known timezone if not tz.gettz(timezone): raise SchemaValidationError(f"'TIMEZONE' value must represent a valid IANA timezone, rule={rule_name}") @@ -393,17 +414,21 @@ def _validate_schedule_between_time_and_datetime_ranges( 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 if not isinstance(value, dict): raise SchemaValidationError(error_str) + start_time = value.get(TimeValues.START.value) end_time = value.get(TimeValues.END.value) if not start_time or not end_time: raise SchemaValidationError(error_str) if not isinstance(start_time, str) or not isinstance(end_time, str): raise SchemaValidationError(f"'START' and 'END' must be a non empty string, rule={rule_name}") + validator(start_time, rule_name) validator(end_time, rule_name) timezone = value.get(TimeValues.TIMEZONE.value, "UTC") if not isinstance(timezone, str): raise SchemaValidationError(f"'TIMEZONE' must be a string, rule={rule_name}") + + # try to see if the timezone string corresponds to any known timezone if not tz.gettz(timezone): raise SchemaValidationError(f"'TIMEZONE' value must represent a valid IANA timezone, rule={rule_name}") diff --git a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py index b75be2fecea..e6cec7185e8 100644 --- a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py +++ b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py @@ -7,12 +7,19 @@ def _get_now_from_timezone(timezone: Optional[tzinfo]) -> datetime: + """ + Returns now in the specified timezone. Defaults to UTC if not present. + At this stage, we already validated that the passed timezone string is valid, so we assume that + gettz() will return a tzinfo object. + """ timezone = gettz("UTC") if timezone is None else timezone return datetime.now(timezone) def compare_days_of_week(action: str, values: Dict) -> bool: timezone_name = values.get(TimeValues.TIMEZONE.value, "UTC") + + # %A = Weekday as locale’s full name. current_day = _get_now_from_timezone(gettz(timezone_name)).strftime("%A").upper() days = values.get(TimeValues.DAYS.value, []) @@ -27,9 +34,9 @@ def compare_datetime_range(action: str, values: Dict) -> bool: start_date_str = values.get(TimeValues.START.value, "") end_date_str = values.get(TimeValues.END.value, "") - # Since start_date and end_date don't include timezone information, we mark the timestamp + # Since start_date and end_date doesn't include timezone information, we mark the timestamp # with the same timezone as the current_time. This way all the 3 timestamps will be on - # the same timezone + # the same timezone. start_date = datetime.fromisoformat(start_date_str).replace(tzinfo=timezone) end_date = datetime.fromisoformat(end_date_str).replace(tzinfo=timezone) return start_date <= current_time <= end_date From f789ee51702335b53748c28b574116cd018e551a Mon Sep 17 00:00:00 2001 From: Ran Isenberg Date: Fri, 13 Jan 2023 12:24:11 +0200 Subject: [PATCH 30/34] fixed exampe, added example --- docs/utilities/feature_flags.md | 30 ++++++++++++++----- .../feature_flags/src/datetime_feature.json | 21 +++++++++++++ .../feature_flags/src/datetime_feature.py | 14 +++++++++ .../feature_flags/src/timebased_features.json | 11 ++----- 4 files changed, 61 insertions(+), 15 deletions(-) create mode 100644 examples/feature_flags/src/datetime_feature.json create mode 100644 examples/feature_flags/src/datetime_feature.py diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index f1eed662106..2953f6e773c 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -459,6 +459,8 @@ Use cases: * Disable support/chat feature after working hours * Launch a new feature on a specific date and time +You can also have features enabled only at certain times of the day for premium tier customers + === "app.py" ```python hl_lines="12" @@ -473,7 +475,7 @@ Use cases: === "features.json" - ```json hl_lines="15 19-27" + ```json hl_lines="9-11 14-21" --8<-- "examples/feature_flags/src/timebased_features.json" ``` @@ -491,6 +493,20 @@ You can also have features enabled only at certain times of the day. --8<-- "examples/feature_flags/src/timebased_happyhour_features.json" ``` +You can also have features enabled only at specific days, for example: enable christmas sale discount during specific dates. + +=== "app.py" + + ```python hl_lines="10" + --8<-- "examples/feature_flags/src/datetime_feature.py" + ``` + +=== "features.json" + + ```json hl_lines="9-14" + --8<-- "examples/feature_flags/src/datetime_feature.json" + ``` + ???+ info "How should I use timezones?" You can use any [IANA time zone](https://www.iana.org/time-zones) (as originally specified in [PEP 615](https://peps.python.org/pep-0615/)) as part of your rules definition. @@ -633,7 +649,7 @@ The `conditions` block is a list of conditions that contain `action`, `key`, and The `action` configuration can have the following values, where the expressions **`a`** is the `key` and **`b`** is the `value` above: | Action | Equivalent expression | -|-------------------------------------|----------------------------------------------------------| +| ----------------------------------- | -------------------------------------------------------- | | **EQUALS** | `lambda a, b: a == b` | | **NOT_EQUALS** | `lambda a, b: a != b` | | **KEY_GREATER_THAN_VALUE** | `lambda a, b: a > b` | @@ -657,11 +673,11 @@ The `action` configuration can have the following values, where the expressions For time based keys, we provide a list of predefined keys. These will automatically get converted to the corresponding timestamp on each invocation of your Lambda function. - | Key | Meaning | - |-------------------------|--------------------------------------------------------------------------| - | CURRENT_TIME | The current time, 24 hour format (HH:mm) | - | CURRENT_DATETIME | The current datetime ([ISO8601](https://en.wikipedia.org/wiki/ISO_8601)) | - | CURRENT_DAY_OF_WEEK | The current day of the week (Monday-Sunday) | + | Key | Meaning | + | ------------------- | ------------------------------------------------------------------------ | + | CURRENT_TIME | The current time, 24 hour format (HH:mm) | + | CURRENT_DATETIME | The current datetime ([ISO8601](https://en.wikipedia.org/wiki/ISO_8601)) | + | CURRENT_DAY_OF_WEEK | The current day of the week (Monday-Sunday) | If not specified, the timezone used for calculations will be UTC. diff --git a/examples/feature_flags/src/datetime_feature.json b/examples/feature_flags/src/datetime_feature.json new file mode 100644 index 00000000000..191ebf83dc5 --- /dev/null +++ b/examples/feature_flags/src/datetime_feature.json @@ -0,0 +1,21 @@ +{ + "christmas_discount": { + "default": false, + "rules": { + "enable discount during christmas": { + "when_match": true, + "conditions": [ + { + "action": "SCHEDULE_BETWEEN_DATETIME_RANGE", + "key": "CURRENT_DATETIME", + "value": { + "START": "2022-12-25T12:00:00", + "END": "2022-12-31T23:59:59", + "TIMEZONE": "America/New_York" + } + } + ] + } + } + } +} diff --git a/examples/feature_flags/src/datetime_feature.py b/examples/feature_flags/src/datetime_feature.py new file mode 100644 index 00000000000..55c11ea6e7d --- /dev/null +++ b/examples/feature_flags/src/datetime_feature.py @@ -0,0 +1,14 @@ +from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags + +app_config = AppConfigStore(environment="dev", application="product-catalogue", name="features") + +feature_flags = FeatureFlags(store=app_config) + + +def lambda_handler(event, context): + # Get customer's tier from incoming request + xmas_discount = feature_flags.evaluate(name="christmas_discount", default=False) + + if xmas_discount: + # Enable special discount on christmas: + pass diff --git a/examples/feature_flags/src/timebased_features.json b/examples/feature_flags/src/timebased_features.json index 5c47286f679..8e10588a0ac 100644 --- a/examples/feature_flags/src/timebased_features.json +++ b/examples/feature_flags/src/timebased_features.json @@ -2,19 +2,14 @@ "weekend_premium_discount": { "default": false, "rules": { - "customer tier equals premium": { + "customer tier equals premium and its time for a discount": { "when_match": true, "conditions": [ { "action": "EQUALS", "key": "tier", "value": "premium" - } - ] - }, - "is weekend": { - "when_match": true, - "conditions": [ + }, { "action": "SCHEDULE_BETWEEN_DAYS_OF_WEEK", "key": "CURRENT_DAY_OF_WEEK", @@ -30,4 +25,4 @@ } } } -} +} \ No newline at end of file From c07d1c7936782da38f4d737e74472a8ea7d58998 Mon Sep 17 00:00:00 2001 From: Release bot Date: Sat, 14 Jan 2023 07:45:47 +0000 Subject: [PATCH 31/34] update changelog with latest changes --- CHANGELOG.md | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e6dc30b814..d28afe5ea38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ # Unreleased +## Maintenance + +* **deps:** bump future from 0.18.2 to 0.18.3 ([#1836](https://github.com/awslabs/aws-lambda-powertools-python/issues/1836)) +* **deps-dev:** bump mkdocs-material from 9.0.3 to 9.0.4 ([#1833](https://github.com/awslabs/aws-lambda-powertools-python/issues/1833)) +* **deps-dev:** bump mypy-boto3-logs from 1.26.43 to 1.26.49 ([#1834](https://github.com/awslabs/aws-lambda-powertools-python/issues/1834)) +* **deps-dev:** bump mypy-boto3-secretsmanager from 1.26.40 to 1.26.49 ([#1835](https://github.com/awslabs/aws-lambda-powertools-python/issues/1835)) +* **deps-dev:** bump mypy-boto3-lambda from 1.26.18 to 1.26.49 ([#1832](https://github.com/awslabs/aws-lambda-powertools-python/issues/1832)) +* **deps-dev:** bump aws-cdk-lib from 2.59.0 to 2.60.0 ([#1831](https://github.com/awslabs/aws-lambda-powertools-python/issues/1831)) + + + +## [v2.6.0] - 2023-01-12 ## Bug Fixes * **api_gateway:** fixed custom metrics issue when using debug mode ([#1827](https://github.com/awslabs/aws-lambda-powertools-python/issues/1827)) @@ -15,29 +27,30 @@ ## Maintenance +* update v2 layer ARN on documentation * **deps:** bump pydantic from 1.10.2 to 1.10.4 ([#1817](https://github.com/awslabs/aws-lambda-powertools-python/issues/1817)) * **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 2.0.1 to 2.0.3 ([#1801](https://github.com/awslabs/aws-lambda-powertools-python/issues/1801)) * **deps:** bump release-drafter/release-drafter from 5.21.1 to 5.22.0 ([#1802](https://github.com/awslabs/aws-lambda-powertools-python/issues/1802)) -* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 2.0.3 to 2.0.4 ([#1821](https://github.com/awslabs/aws-lambda-powertools-python/issues/1821)) * **deps:** bump gitpython from 3.1.29 to 3.1.30 ([#1812](https://github.com/awslabs/aws-lambda-powertools-python/issues/1812)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 2.0.3 to 2.0.4 ([#1821](https://github.com/awslabs/aws-lambda-powertools-python/issues/1821)) * **deps:** bump peaceiris/actions-gh-pages from 3.9.0 to 3.9.1 ([#1814](https://github.com/awslabs/aws-lambda-powertools-python/issues/1814)) -* **deps-dev:** bump coverage from 7.0.4 to 7.0.5 ([#1829](https://github.com/awslabs/aws-lambda-powertools-python/issues/1829)) +* **deps-dev:** bump mkdocs-material from 8.5.11 to 9.0.2 ([#1808](https://github.com/awslabs/aws-lambda-powertools-python/issues/1808)) * **deps-dev:** bump mypy-boto3-ssm from 1.26.11.post1 to 1.26.43 ([#1819](https://github.com/awslabs/aws-lambda-powertools-python/issues/1819)) * **deps-dev:** bump mypy-boto3-logs from 1.26.27 to 1.26.43 ([#1820](https://github.com/awslabs/aws-lambda-powertools-python/issues/1820)) * **deps-dev:** bump filelock from 3.8.2 to 3.9.0 ([#1816](https://github.com/awslabs/aws-lambda-powertools-python/issues/1816)) * **deps-dev:** bump mypy-boto3-cloudformation from 1.26.11.post1 to 1.26.35.post1 ([#1818](https://github.com/awslabs/aws-lambda-powertools-python/issues/1818)) -* **deps-dev:** bump mkdocs-material from 8.5.11 to 9.0.2 ([#1808](https://github.com/awslabs/aws-lambda-powertools-python/issues/1808)) +* **deps-dev:** bump ijson from 3.1.4 to 3.2.0.post0 ([#1815](https://github.com/awslabs/aws-lambda-powertools-python/issues/1815)) * **deps-dev:** bump coverage from 6.5.0 to 7.0.3 ([#1806](https://github.com/awslabs/aws-lambda-powertools-python/issues/1806)) * **deps-dev:** bump flake8-builtins from 2.0.1 to 2.1.0 ([#1799](https://github.com/awslabs/aws-lambda-powertools-python/issues/1799)) -* **deps-dev:** bump ijson from 3.1.4 to 3.2.0.post0 ([#1815](https://github.com/awslabs/aws-lambda-powertools-python/issues/1815)) +* **deps-dev:** bump coverage from 7.0.3 to 7.0.4 ([#1822](https://github.com/awslabs/aws-lambda-powertools-python/issues/1822)) * **deps-dev:** bump mypy-boto3-secretsmanager from 1.26.12 to 1.26.40 ([#1811](https://github.com/awslabs/aws-lambda-powertools-python/issues/1811)) * **deps-dev:** bump isort from 5.11.3 to 5.11.4 ([#1809](https://github.com/awslabs/aws-lambda-powertools-python/issues/1809)) * **deps-dev:** bump aws-cdk-lib from 2.55.1 to 2.59.0 ([#1810](https://github.com/awslabs/aws-lambda-powertools-python/issues/1810)) * **deps-dev:** bump importlib-metadata from 5.1.0 to 6.0.0 ([#1804](https://github.com/awslabs/aws-lambda-powertools-python/issues/1804)) -* **deps-dev:** bump coverage from 7.0.3 to 7.0.4 ([#1822](https://github.com/awslabs/aws-lambda-powertools-python/issues/1822)) +* **deps-dev:** bump mkdocs-material from 9.0.2 to 9.0.3 ([#1823](https://github.com/awslabs/aws-lambda-powertools-python/issues/1823)) * **deps-dev:** bump black from 22.10.0 to 22.12.0 ([#1770](https://github.com/awslabs/aws-lambda-powertools-python/issues/1770)) * **deps-dev:** bump flake8-black from 0.3.5 to 0.3.6 ([#1792](https://github.com/awslabs/aws-lambda-powertools-python/issues/1792)) -* **deps-dev:** bump mkdocs-material from 9.0.2 to 9.0.3 ([#1823](https://github.com/awslabs/aws-lambda-powertools-python/issues/1823)) +* **deps-dev:** bump coverage from 7.0.4 to 7.0.5 ([#1829](https://github.com/awslabs/aws-lambda-powertools-python/issues/1829)) * **deps-dev:** bump types-requests from 2.28.11.5 to 2.28.11.7 ([#1795](https://github.com/awslabs/aws-lambda-powertools-python/issues/1795)) @@ -2748,7 +2761,8 @@ * Merge pull request [#5](https://github.com/awslabs/aws-lambda-powertools-python/issues/5) from jfuss/feat/python38 -[Unreleased]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v2.5.0...HEAD +[Unreleased]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v2.6.0...HEAD +[v2.6.0]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v2.5.0...v2.6.0 [v2.5.0]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v2.4.0...v2.5.0 [v2.4.0]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v2.3.1...v2.4.0 [v2.3.1]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v2.3.0...v2.3.1 From 18859f56e254259dd2c304efc90a54df8a0acffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Mon, 16 Jan 2023 14:47:52 +0100 Subject: [PATCH 32/34] fix: handle time conditions across the day boundary --- .../feature_flags/time_conditions.py | 29 ++++++++-- .../feature_flags/test_time_based_actions.py | 57 +++++++++++++++++++ 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py index e6cec7185e8..61f742d4b94 100644 --- a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py +++ b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py @@ -48,9 +48,26 @@ def compare_time_range(action: str, values: Dict) -> bool: start_hour, start_min = values.get(TimeValues.START.value, "").split(HOUR_MIN_SEPARATOR) end_hour, end_min = values.get(TimeValues.END.value, "").split(HOUR_MIN_SEPARATOR) - return ( - current_time.hour >= int(start_hour) - and current_time.hour <= int(end_hour) - and current_time.minute >= int(start_min) - and current_time.minute <= int(end_min) - ) + + start_datetime = current_time.replace(hour=int(start_hour), minute=int(start_min)) + end_datetime = current_time.replace(hour=int(end_hour), minute=int(end_min)) + + if int(end_hour) < int(start_hour): + # When the end hour is smaller than start hour, it means we are crossing a day's boundary. + # In this case we need to assert that current_time is **either** on one side or the other side of the boundary + # + # ┌─────┐ ┌─────┐ ┌─────┐ + # │20.00│ │00.00│ │04.00│ + # └─────┘ └─────┘ └─────┘ + # ───────────────────────────────────────────┬─────────────────────────────────────────▶ + # ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ + # │ │ │ + # │ either this area │ │ or this area + # │ │ │ + # └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ + # │ + + return (start_datetime <= current_time) or (current_time <= end_datetime) + else: + # In normal circumstances, we need to assert **both** conditions + return (start_datetime <= current_time) and (current_time <= end_datetime) diff --git a/tests/functional/feature_flags/test_time_based_actions.py b/tests/functional/feature_flags/test_time_based_actions.py index 54bba34fa02..358f310103f 100644 --- a/tests/functional/feature_flags/test_time_based_actions.py +++ b/tests/functional/feature_flags/test_time_based_actions.py @@ -107,6 +107,63 @@ def test_time_based_utc_in_between_time_range_no_rule_match(mocker): ) +def test_time_based_utc_in_between_time_range_full_hour_rule_match(mocker): + assert evaluate_mocked_schema( + mocker=mocker, + rules={ + "lambda time is between UTC 20:00-23:00": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, + CONDITION_VALUE: {TimeValues.START.value: "20:00", TimeValues.END.value: "23:00"}, + }, + ], + } + }, + mocked_time=(2022, 2, 15, 21, 12, 0, datetime.timezone.utc), # rule match 21:12 + ) + + +def test_time_based_utc_in_between_time_range_between_days_rule_match(mocker): + assert evaluate_mocked_schema( + mocker=mocker, + rules={ + "lambda time is between UTC 23:00-04:00": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, + CONDITION_VALUE: {TimeValues.START.value: "23:00", TimeValues.END.value: "04:00"}, + }, + ], + } + }, + mocked_time=(2022, 2, 15, 2, 3, 0, datetime.timezone.utc), # rule match 2:03 am + ) + + +def test_time_based_utc_in_between_time_range_between_days_rule_no_match(mocker): + assert not evaluate_mocked_schema( + mocker=mocker, + rules={ + "lambda time is between UTC 23:00-04:00": { + RULE_MATCH_VALUE: True, + CONDITIONS_KEY: [ + { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, # this condition matches + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, + CONDITION_VALUE: {TimeValues.START.value: "23:00", TimeValues.END.value: "04:00"}, + }, + ], + } + }, + mocked_time=(2022, 2, 15, 5, 0, 0, datetime.timezone.utc), # rule no match 5:00 am + ) + + def test_time_based_between_time_range_rule_timezone_match(mocker): timezone_name = "Europe/Copenhagen" From 3739ab69e0bfe29dd050d063101be6f767661b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Mon, 16 Jan 2023 15:45:20 +0100 Subject: [PATCH 33/34] fix: clarified condition --- .../utilities/feature_flags/time_conditions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py index 61f742d4b94..80dbc919f1a 100644 --- a/aws_lambda_powertools/utilities/feature_flags/time_conditions.py +++ b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py @@ -49,8 +49,8 @@ def compare_time_range(action: str, values: Dict) -> bool: start_hour, start_min = values.get(TimeValues.START.value, "").split(HOUR_MIN_SEPARATOR) end_hour, end_min = values.get(TimeValues.END.value, "").split(HOUR_MIN_SEPARATOR) - start_datetime = current_time.replace(hour=int(start_hour), minute=int(start_min)) - end_datetime = current_time.replace(hour=int(end_hour), minute=int(end_min)) + start_time = current_time.replace(hour=int(start_hour), minute=int(start_min)) + end_time = current_time.replace(hour=int(end_hour), minute=int(end_min)) if int(end_hour) < int(start_hour): # When the end hour is smaller than start hour, it means we are crossing a day's boundary. @@ -67,7 +67,7 @@ def compare_time_range(action: str, values: Dict) -> bool: # └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ # │ - return (start_datetime <= current_time) or (current_time <= end_datetime) + return (start_time <= current_time) or (current_time <= end_time) else: # In normal circumstances, we need to assert **both** conditions - return (start_datetime <= current_time) and (current_time <= end_datetime) + return start_time <= current_time <= end_time From 540659d89d259a44ac07107e9c7e0b31b5522258 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Jan 2023 20:28:54 +0000 Subject: [PATCH 34/34] chore(deps-dev): bump mypy-boto3-lambda from 1.26.18 to 1.26.49 (#1832) Bumps [mypy-boto3-lambda](https://github.com/youtype/mypy_boto3_builder) from 1.26.18 to 1.26.49. - [Release notes](https://github.com/youtype/mypy_boto3_builder/releases) - [Commits](https://github.com/youtype/mypy_boto3_builder/commits) --- updated-dependencies: - dependency-name: mypy-boto3-lambda dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index fb2c9f4cd0f..ae8de9a4cd9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2759,4 +2759,4 @@ validation = ["fastjsonschema"] [metadata] lock-version = "2.0" python-versions = "^3.7.4" -content-hash = "f5ebadf79a02abdcd47c68d763db1f4dae6a2aec9c1f291324c54848464ff7da" +content-hash = "68c48ad8be866ea55c784614e529a20ec08eb39b47536c461be0b6adc87c8d38"