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