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 diff --git a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py index 36a74c4c58a..2bf49187b58 100644 --- a/aws_lambda_powertools/utilities/feature_flags/feature_flags.py +++ b/aws_lambda_powertools/utilities/feature_flags/feature_flags.py @@ -6,6 +6,11 @@ from . import schema from .base import StoreProvider from .exceptions import ConfigurationStoreError +from .time_conditions import ( + compare_datetime_range, + compare_days_of_week, + compare_time_range, +) class FeatureFlags: @@ -59,6 +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_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: @@ -83,10 +91,18 @@ 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) + # time based rule actions have no user context. the context is the condition key + if cond_action in ( + schema.RuleAction.SCHEDULE_BETWEEN_TIME_RANGE.value, + 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 + 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 +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 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 +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 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..a2c4752af62 100644 --- a/aws_lambda_powertools/utilities/feature_flags/schema.py +++ b/aws_lambda_powertools/utilities/feature_flags/schema.py @@ -1,6 +1,10 @@ import logging +import re +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 dateutil import tz from ... import Logger from .base import BaseValidator @@ -14,9 +18,12 @@ CONDITION_VALUE = "value" CONDITION_ACTION = "action" FEATURE_DEFAULT_VAL_TYPE_KEY = "boolean_type" +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 = ":" -class RuleAction(str, Enum): +class RuleAction(Enum): EQUALS = "EQUALS" NOT_EQUALS = "NOT_EQUALS" KEY_GREATER_THAN_VALUE = "KEY_GREATER_THAN_VALUE" @@ -31,6 +38,37 @@ 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" + 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" + DAYS = "DAYS" + SUNDAY = "SUNDAY" + MONDAY = "MONDAY" + TUESDAY = "TUESDAY" + WEDNESDAY = "WEDNESDAY" + THURSDAY = "THURSDAY" + FRIDAY = "FRIDAY" + SATURDAY = "SATURDAY" class SchemaValidator(BaseValidator): @@ -265,8 +303,132 @@ 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}") + # 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( + 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.value: + raise SchemaValidationError( + 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.value: + raise SchemaValidationError( + 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 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, "") + + # 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 + ) + elif action == RuleAction.SCHEDULE_BETWEEN_DATETIME_RANGE.value: + ConditionsValidator._validate_schedule_between_time_and_datetime_ranges( + 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_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}") + + # 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 + match = TIME_RANGE_RE_PATTERN.match(time) + + if not match: + raise SchemaValidationError( + f"'START' and 'END' must be a valid time format, time_format={TIME_RANGE_FORMAT}, rule={rule_name}" + ) + + @staticmethod + def _validate_schedule_between_days_of_week(value: Any, rule_name: str): + 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, + TimeValues.WEDNESDAY.value, + TimeValues.THURSDAY.value, + TimeValues.FRIDAY.value, + TimeValues.SATURDAY.value, + TimeValues.SUNDAY.value, + ]: + raise SchemaValidationError( + 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) + + # 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}") + + @staticmethod + def _validate_schedule_between_time_and_datetime_ranges( + 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): + 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 new file mode 100644 index 00000000000..80dbc919f1a --- /dev/null +++ b/aws_lambda_powertools/utilities/feature_flags/time_conditions.py @@ -0,0 +1,73 @@ +from datetime import datetime, tzinfo +from typing import Dict, Optional + +from dateutil.tz import gettz + +from .schema import HOUR_MIN_SEPARATOR, TimeValues + + +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, []) + return current_day in days + + +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) + + start_date_str = values.get(TimeValues.START.value, "") + end_date_str = values.get(TimeValues.END.value, "") + + # 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. + 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 + + +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)) + + 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_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. + # 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_time <= current_time) or (current_time <= end_time) + else: + # In normal circumstances, we need to assert **both** conditions + return start_time <= current_time <= end_time diff --git a/docs/utilities/feature_flags.md b/docs/utilities/feature_flags.md index ec4c28699e7..2953f6e773c 100644 --- a/docs/utilities/feature_flags.md +++ b/docs/utilities/feature_flags.md @@ -447,6 +447,74 @@ 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. + +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 + +You can also have features enabled only at certain times of the day for premium tier customers + +=== "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="9-11 14-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-15" + --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. + 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 @@ -580,24 +648,39 @@ 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 | + | ------------------- | ------------------------------------------------------------------------ | + | 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. #### Rule engine flowchart 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_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..894a250d5ec --- /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..8e10588a0ac --- /dev/null +++ b/examples/feature_flags/src/timebased_features.json @@ -0,0 +1,28 @@ +{ + "weekend_premium_discount": { + "default": false, + "rules": { + "customer tier equals premium and its time for a discount": { + "when_match": true, + "conditions": [ + { + "action": "EQUALS", + "key": "tier", + "value": "premium" + }, + { + "action": "SCHEDULE_BETWEEN_DAYS_OF_WEEK", + "key": "CURRENT_DAY_OF_WEEK", + "value": { + "DAYS": [ + "SATURDAY", + "SUNDAY" + ], + "TIMEZONE": "America/New_York" + } + } + ] + } + } + } +} \ No newline at end of file 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..22a239882cc --- /dev/null +++ b/examples/feature_flags/src/timebased_happyhour_features.json @@ -0,0 +1,21 @@ +{ + "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" + } + } + ] + } + } + } +} diff --git a/poetry.lock b/poetry.lock index de095784cb7..ae8de9a4cd9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,21 +2,22 @@ [[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" @@ -71,40 +72,40 @@ 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" @@ -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.50" 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.50-py3-none-any.whl", hash = "sha256:9c434bcd02c527485c89d6efbd38b7c205e06ab06abe80e5dbf9a8be836c77c2"}, + {file = "boto3-1.26.50.tar.gz", hash = "sha256:3737d8a506f50065bb2366a6b8e7545d88034f4771527790a125e0abd307d8e8"}, ] [package.dependencies] -botocore = ">=1.29.18,<1.30.0" +botocore = ">=1.29.50,<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.50" 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.50-py3-none-any.whl", hash = "sha256:0e9ab19787ad7a079c00d3e40b16bc66423e54bc0e8a203b70b543bd8854d5ad"}, + {file = "botocore-1.29.50.tar.gz", hash = "sha256:5cc68b78a48217550c18b4639420b7c3b48ed9e09e749343143acbfa423ceec5"}, ] [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.219" 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.219-py3-none-any.whl", hash = "sha256:62008af2e02fe7ddfc04afe488f944f4a44d55ef030bd5d27ebd3b68ec464aea"}, + {file = "constructs-10.1.219.tar.gz", hash = "sha256:a03fed938006bbd1ee8d7924e51517f59849147a464cf60eb2778a8482ae7d1c"}, ] [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] @@ -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" @@ -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"}, @@ -1812,51 +1930,41 @@ files = [ [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 = "04c085c5a4f6c4026e160297b0b9d5c297639e27d356430a66deedd1c3b9bc48" +content-hash = "68c48ad8be866ea55c784614e529a20ec08eb39b47536c461be0b6adc87c8d38" diff --git a/pyproject.toml b/pyproject.toml index dd4049c196e..aceb20958c9 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.4" 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 0366a5609ee..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 @@ -18,6 +19,8 @@ RuleAction, RulesValidator, SchemaValidator, + TimeKeys, + TimeValues, ) logger = logging.getLogger(__name__) @@ -355,7 +358,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 +369,489 @@ 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 + 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.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_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 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.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' 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_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.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' 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_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.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' 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_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.value, + } + rule_name = "dummy" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + 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_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.value, + } + rule_name = "dummy" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=f"'START' and 'END' must be a non empty string, rule={rule_name}", + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +@pytest.mark.parametrize( + "cond_value", + [ + {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_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.value, + } + rule_name = "dummy" + 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( + SchemaValidationError, + match=match_str, + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +@pytest.mark.parametrize( + "cond_value", + [ + {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_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.value, + } + rule_name = "dummy" + 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( + SchemaValidationError, + match=match_str, + ): + 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 + 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.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' 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 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.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' 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 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.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' 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 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.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' 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 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.value, + } + rule_name = "dummy" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + 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 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.value, + } + rule_name = "dummy" + + # WHEN calling validate_condition + # THEN raise SchemaValidationError + with pytest.raises( + SchemaValidationError, + match=f"'START' and 'END' must be a non empty string, rule={rule_name}", + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +@pytest.mark.parametrize( + "cond_value", + [ + {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 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.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_invalid_condition_value_invalid_end_time_value(): + # 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: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 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): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +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 + condition = { + CONDITION_ACTION: RuleAction.SCHEDULE_BETWEEN_DAYS_OF_WEEK.value, + CONDITION_VALUE: { + TimeValues.DAYS.value: [TimeValues.SUNDAY.value], + }, + CONDITION_KEY: TimeKeys.CURRENT_TIME.value, + } + rule_name = "dummy" + + # WHEN calling validate_condition + # 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' 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 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.value, + } + rule_name = "dummy" + 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=re.escape(match_str), + ): + ConditionsValidator.validate_condition_value(condition=condition, rule_name=rule_name) + + +@pytest.mark.parametrize( + "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 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.value, + } + rule_name = "dummy" + 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( + SchemaValidationError, + 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 new file mode 100644 index 00000000000..358f310103f --- /dev/null +++ b/tests/functional/feature_flags/test_time_based_actions.py @@ -0,0 +1,533 @@ +import datetime +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 ( + CONDITION_ACTION, + CONDITION_KEY, + CONDITION_VALUE, + CONDITIONS_KEY, + FEATURE_DEFAULT_VAL_KEY, + RULE_MATCH_VALUE, + RULES_KEY, + RuleAction, + TimeKeys, + TimeValues, +) + + +def evaluate_mocked_schema( + mocker, + rules: Dict[str, Any], + mocked_time: Tuple[int, int, int, int, int, int, datetime.tzinfo], # year, month, day, hour, minute, second + context: Optional[Dict[str, Any]] = None, +) -> JSONType: + """ + This helper does the following: + 1. mocks the current time + 2. mocks the feature flag payload returned from AppConfig + 3. evaluates the rules and return True for a rule match, otherwise a False + """ + + # Mock the current time + 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 + 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: rules, + } + } + + # 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) + + # Evaluate our feature flag + context = {} if context is None else context + return feature_flags.evaluate( + name="my_feature", + context=context, + default=False, + ) + + +def test_time_based_utc_in_between_time_range_rule_match(mocker): + assert 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.value, + CONDITION_VALUE: {TimeValues.START.value: "11:11", TimeValues.END.value: "23:59"}, + }, + ], + } + }, + mocked_time=(2022, 2, 15, 11, 12, 0, datetime.timezone.utc), + ) + + +def test_time_based_utc_in_between_time_range_no_rule_match(mocker): + assert not 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.value, + CONDITION_VALUE: {TimeValues.START.value: "11:11", TimeValues.END.value: "23:59"}, + }, + ], + } + }, + mocked_time=(2022, 2, 15, 7, 12, 0, datetime.timezone.utc), # no rule match 7:12 am + ) + + +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" + + 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 + ) + + +def test_time_based_utc_in_between_full_time_range_rule_match(mocker): + 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": { + 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", + }, + }, + ], + } + }, + 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={ + "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: "Europe/Copenhagen", + }, + }, + ], + } + }, + mocked_time=(2022, 10, 10, 12, 15, 0, gettz("America/New_York")), # will not rule match, it's too late + ) + + +def test_time_based_multiple_conditions_utc_in_between_time_range_rule_match(mocker): + assert 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.value, + CONDITION_VALUE: {TimeValues.START.value: "09:00", TimeValues.END.value: "17:00"}, + }, + { + CONDITION_ACTION: RuleAction.EQUALS.value, + CONDITION_KEY: "username", + CONDITION_VALUE: "ran", + }, + ], + } + }, + mocked_time=(2022, 10, 7, 10, 0, 0, datetime.timezone.utc), # will rule match + context={"username": "ran"}, + ) + + +def test_time_based_multiple_conditions_utc_in_between_time_range_no_rule_match(mocker): + assert not 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.value, + CONDITION_VALUE: {TimeValues.START.value: "09:00", TimeValues.END.value: "17:00"}, + }, + { + CONDITION_ACTION: RuleAction.EQUALS.value, + CONDITION_KEY: "username", + CONDITION_VALUE: "ran", + }, + ], + } + }, + mocked_time=(2022, 10, 7, 7, 0, 0, datetime.timezone.utc), # will cause no rule match, 7:00 + context={"username": "ran"}, + ) + + +def test_time_based_utc_days_range_rule_match(mocker): + assert 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_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, datetime.timezone.utc), # friday + ) + + +def test_time_based_utc_days_range_no_rule_match(mocker): + assert not 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_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, datetime.timezone.utc), # sunday, no match + ) + + +def test_time_based_utc_only_weekend_rule_match(mocker): + 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], + }, + }, + ], + } + }, + 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, 21, 0, 0, 0, gettz("Europe/Copenhagen")), # monday, 00:00 + ) + + +def test_time_based_utc_only_weekend_no_rule_match(mocker): + 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], + }, + }, + ], + } + }, + mocked_time=(2022, 11, 18, 10, 0, 0, datetime.timezone.utc), # friday, no match + ) + + +def test_time_based_multiple_conditions_utc_days_range_and_certain_hours_rule_match(mocker): + 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": { + 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: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.value, + CONDITION_VALUE: {TimeValues.DAYS.value: [TimeValues.MONDAY.value, TimeValues.THURSDAY.value]}, + }, + ], + } + }, + 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, datetime.tzinfo]): + 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, + 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.value, + CONDITION_VALUE: { + TimeValues.DAYS.value: [TimeValues.MONDAY.value, TimeValues.THURSDAY.value] + }, + }, + ], + } + }, + mocked_time=mocked_time, + ) + + 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