Skip to content

Commit a0463d1

Browse files
refactor(feature_flags): add from __future__ import annotations (#4960)
* refactor(feature_flags): add from __future__ import annotations and update code according to ruff rules TCH, UP006, UP007, UP037 and FA100. * Fixing constants * Fix type alias in Python 3.8 See https://bugs.python.org/issue45117 * Fix cast to work with Python 3.8/3.9 --------- Co-authored-by: Leandro Damascena <[email protected]>
1 parent 918844d commit a0463d1

File tree

8 files changed

+110
-85
lines changed

8 files changed

+110
-85
lines changed

aws_lambda_powertools/utilities/feature_flags/appconfig.py

+18-14
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1+
from __future__ import annotations
2+
13
import logging
24
import traceback
3-
from typing import Any, Dict, Optional, Union, cast
4-
5-
from botocore.config import Config
5+
from typing import TYPE_CHECKING, Any, cast
66

7-
from aws_lambda_powertools.logging import Logger
87
from aws_lambda_powertools.utilities import jmespath_utils
98
from aws_lambda_powertools.utilities.feature_flags.base import StoreProvider
109
from aws_lambda_powertools.utilities.feature_flags.exceptions import ConfigurationStoreError, StoreClientError
@@ -14,6 +13,11 @@
1413
TransformParameterError,
1514
)
1615

16+
if TYPE_CHECKING:
17+
from botocore.config import Config
18+
19+
from aws_lambda_powertools.logging import Logger
20+
1721

1822
class AppConfigStore(StoreProvider):
1923
def __init__(
@@ -22,10 +26,10 @@ def __init__(
2226
application: str,
2327
name: str,
2428
max_age: int = 5,
25-
sdk_config: Optional[Config] = None,
26-
envelope: Optional[str] = "",
27-
jmespath_options: Optional[Dict] = None,
28-
logger: Optional[Union[logging.Logger, Logger]] = None,
29+
sdk_config: Config | None = None,
30+
envelope: str | None = "",
31+
jmespath_options: dict | None = None,
32+
logger: logging.Logger | Logger | None = None,
2933
):
3034
"""This class fetches JSON schemas from AWS AppConfig
3135
@@ -39,11 +43,11 @@ def __init__(
3943
AppConfig configuration name e.g. `my_conf`
4044
max_age: int
4145
cache expiration time in seconds, or how often to call AppConfig to fetch latest configuration
42-
sdk_config: Optional[Config]
46+
sdk_config: Config | None
4347
Botocore Config object to pass during client initialization
44-
envelope : Optional[str]
48+
envelope : str | None
4549
JMESPath expression to pluck feature flags data from config
46-
jmespath_options : Optional[Dict]
50+
jmespath_options : dict | None
4751
Alternative JMESPath options to be included when filtering expr
4852
logger: A logging object
4953
Used to log messages. If None is supplied, one will be created.
@@ -60,7 +64,7 @@ def __init__(
6064
self._conf_store = AppConfigProvider(environment=environment, application=application, boto_config=sdk_config)
6165

6266
@property
63-
def get_raw_configuration(self) -> Dict[str, Any]:
67+
def get_raw_configuration(self) -> dict[str, Any]:
6468
"""Fetch feature schema configuration from AWS AppConfig"""
6569
try:
6670
# parse result conf as JSON, keep in cache for self.max_age seconds
@@ -82,7 +86,7 @@ def get_raw_configuration(self) -> Dict[str, Any]:
8286
raise StoreClientError(err_msg) from exc
8387
raise ConfigurationStoreError("Unable to get AWS AppConfig configuration file") from exc
8488

85-
def get_configuration(self) -> Dict[str, Any]:
89+
def get_configuration(self) -> dict[str, Any]:
8690
"""Fetch feature schema configuration from AWS AppConfig
8791
8892
If envelope is set, it'll extract and return feature flags from configuration,
@@ -95,7 +99,7 @@ def get_configuration(self) -> Dict[str, Any]:
9599
96100
Returns
97101
-------
98-
Dict[str, Any]
102+
dict[str, Any]
99103
parsed JSON dictionary
100104
"""
101105
config = self.get_raw_configuration

aws_lambda_powertools/utilities/feature_flags/base.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1+
from __future__ import annotations
2+
13
from abc import ABC, abstractmethod
2-
from typing import Any, Dict
4+
from typing import Any
35

46

57
class StoreProvider(ABC):
68
@property
79
@abstractmethod
8-
def get_raw_configuration(self) -> Dict[str, Any]:
10+
def get_raw_configuration(self) -> dict[str, Any]:
911
"""Get configuration from any store and return the parsed JSON dictionary"""
1012
raise NotImplementedError() # pragma: no cover
1113

1214
@abstractmethod
13-
def get_configuration(self) -> Dict[str, Any]:
15+
def get_configuration(self) -> dict[str, Any]:
1416
"""Get configuration from any store and return the parsed JSON dictionary
1517
1618
If envelope is set, it'll extract and return feature flags from configuration,
@@ -23,7 +25,7 @@ def get_configuration(self) -> Dict[str, Any]:
2325
2426
Returns
2527
-------
26-
Dict[str, Any]
28+
dict[str, Any]
2729
parsed JSON dictionary
2830
2931
**Example**

aws_lambda_powertools/utilities/feature_flags/comparators.py

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
from __future__ import annotations
22

33
from datetime import datetime, tzinfo
4-
from typing import Any, Dict, Optional
4+
from typing import Any
55

66
from dateutil.tz import gettz
77

8-
from aws_lambda_powertools.utilities.feature_flags.schema import HOUR_MIN_SEPARATOR, ModuloRangeValues, TimeValues
8+
from aws_lambda_powertools.utilities.feature_flags.constants import HOUR_MIN_SEPARATOR
9+
from aws_lambda_powertools.utilities.feature_flags.schema import ModuloRangeValues, TimeValues
910

1011

11-
def _get_now_from_timezone(timezone: Optional[tzinfo]) -> datetime:
12+
def _get_now_from_timezone(timezone: tzinfo | None) -> datetime:
1213
"""
1314
Returns now in the specified timezone. Defaults to UTC if not present.
1415
At this stage, we already validated that the passed timezone string is valid, so we assume that
@@ -18,7 +19,7 @@ def _get_now_from_timezone(timezone: Optional[tzinfo]) -> datetime:
1819
return datetime.now(timezone)
1920

2021

21-
def compare_days_of_week(context_value: Any, condition_value: Dict) -> bool:
22+
def compare_days_of_week(context_value: Any, condition_value: dict) -> bool:
2223
timezone_name = condition_value.get(TimeValues.TIMEZONE.value, "UTC")
2324

2425
# %A = Weekday as locale’s full name.
@@ -28,7 +29,7 @@ def compare_days_of_week(context_value: Any, condition_value: Dict) -> bool:
2829
return current_day in days
2930

3031

31-
def compare_datetime_range(context_value: Any, condition_value: Dict) -> bool:
32+
def compare_datetime_range(context_value: Any, condition_value: dict) -> bool:
3233
timezone_name = condition_value.get(TimeValues.TIMEZONE.value, "UTC")
3334
timezone = gettz(timezone_name)
3435
current_time: datetime = _get_now_from_timezone(timezone)
@@ -44,7 +45,7 @@ def compare_datetime_range(context_value: Any, condition_value: Dict) -> bool:
4445
return start_date <= current_time <= end_date
4546

4647

47-
def compare_time_range(context_value: Any, condition_value: Dict) -> bool:
48+
def compare_time_range(context_value: Any, condition_value: dict) -> bool:
4849
timezone_name = condition_value.get(TimeValues.TIMEZONE.value, "UTC")
4950
current_time: datetime = _get_now_from_timezone(gettz(timezone_name))
5051

@@ -75,7 +76,7 @@ def compare_time_range(context_value: Any, condition_value: Dict) -> bool:
7576
return start_time <= current_time <= end_time
7677

7778

78-
def compare_modulo_range(context_value: int, condition_value: Dict) -> bool:
79+
def compare_modulo_range(context_value: int, condition_value: dict) -> bool:
7980
"""
8081
Returns for a given context 'a' and modulo condition 'b' -> b.start <= a % b.base <= b.end
8182
"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import re
2+
3+
RULES_KEY = "rules"
4+
FEATURE_DEFAULT_VAL_KEY = "default"
5+
CONDITIONS_KEY = "conditions"
6+
RULE_MATCH_VALUE = "when_match"
7+
CONDITION_KEY = "key"
8+
CONDITION_VALUE = "value"
9+
CONDITION_ACTION = "action"
10+
FEATURE_DEFAULT_VAL_TYPE_KEY = "boolean_type"
11+
TIME_RANGE_FORMAT = "%H:%M" # hour:min 24 hours clock
12+
TIME_RANGE_PATTERN = re.compile(r"2[0-3]:[0-5]\d|[0-1]\d:[0-5]\d") # 24 hour clock
13+
HOUR_MIN_SEPARATOR = ":"

aws_lambda_powertools/utilities/feature_flags/exceptions.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class SchemaValidationError(Exception):
77

88

99
class StoreClientError(Exception):
10-
"""When a store raises an exception that should be propagated to the client to fix
10+
"""When a store raises an exception that should be propagated to the client
1111
1212
For example, Access Denied errors when the client doesn't permissions to fetch config
1313
"""

aws_lambda_powertools/utilities/feature_flags/feature_flags.py

+24-25
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
from __future__ import annotations
22

33
import logging
4-
from typing import Any, Callable, Dict, List, Optional, TypeVar, Union, cast
4+
from typing import TYPE_CHECKING, Any, Callable, List, cast
55

6-
from typing_extensions import ParamSpec
7-
8-
from aws_lambda_powertools.logging import Logger
96
from aws_lambda_powertools.utilities.feature_flags import schema
10-
from aws_lambda_powertools.utilities.feature_flags.base import StoreProvider
117
from aws_lambda_powertools.utilities.feature_flags.comparators import (
128
compare_all_in_list,
139
compare_any_in_list,
@@ -18,10 +14,13 @@
1814
compare_time_range,
1915
)
2016
from aws_lambda_powertools.utilities.feature_flags.exceptions import ConfigurationStoreError
21-
from aws_lambda_powertools.utilities.feature_flags.types import JSONType
17+
from aws_lambda_powertools.utilities.feature_flags.types import P, T
18+
19+
if TYPE_CHECKING:
20+
from aws_lambda_powertools.logging import Logger
21+
from aws_lambda_powertools.utilities.feature_flags.base import StoreProvider
22+
from aws_lambda_powertools.utilities.feature_flags.types import JSONType
2223

23-
T = TypeVar("T")
24-
P = ParamSpec("P")
2524

2625
RULE_ACTION_MAPPING = {
2726
schema.RuleAction.EQUALS.value: lambda a, b: a == b,
@@ -49,7 +48,7 @@
4948

5049

5150
class FeatureFlags:
52-
def __init__(self, store: StoreProvider, logger: Optional[Union[logging.Logger, Logger]] = None):
51+
def __init__(self, store: StoreProvider, logger: logging.Logger | Logger | None = None):
5352
"""Evaluates whether feature flags should be enabled based on a given context.
5453
5554
It uses the provided store to fetch feature flag rules before evaluating them.
@@ -100,12 +99,12 @@ def _evaluate_conditions(
10099
self,
101100
rule_name: str,
102101
feature_name: str,
103-
rule: Dict[str, Any],
104-
context: Dict[str, Any],
102+
rule: dict[str, Any],
103+
context: dict[str, Any],
105104
) -> bool:
106105
"""Evaluates whether context matches conditions, return False otherwise"""
107106
rule_match_value = rule.get(schema.RULE_MATCH_VALUE)
108-
conditions = cast(List[Dict], rule.get(schema.CONDITIONS_KEY))
107+
conditions = cast(List[dict], rule.get(schema.CONDITIONS_KEY))
109108

110109
if not conditions:
111110
self.logger.debug(
@@ -141,9 +140,9 @@ def _evaluate_rules(
141140
self,
142141
*,
143142
feature_name: str,
144-
context: Dict[str, Any],
143+
context: dict[str, Any],
145144
feat_default: Any,
146-
rules: Dict[str, Any],
145+
rules: dict[str, Any],
147146
boolean_feature: bool,
148147
) -> bool:
149148
"""Evaluates whether context matches rules and conditions, otherwise return feature default"""
@@ -164,7 +163,7 @@ def _evaluate_rules(
164163
)
165164
return feat_default
166165

167-
def get_configuration(self) -> Dict:
166+
def get_configuration(self) -> dict:
168167
"""Get validated feature flag schema from configured store.
169168
170169
Largely used to aid testing, since it's called by `evaluate` and `get_enabled_features` methods.
@@ -178,7 +177,7 @@ def get_configuration(self) -> Dict:
178177
179178
Returns
180179
------
181-
Dict[str, Dict]
180+
dict[str, dict]
182181
parsed JSON dictionary
183182
184183
**Example**
@@ -208,13 +207,13 @@ def get_configuration(self) -> Dict:
208207
"""
209208
# parse result conf as JSON, keep in cache for max age defined in store
210209
self.logger.debug(f"Fetching schema from registered store, store={self.store}")
211-
config: Dict = self.store.get_configuration()
210+
config: dict = self.store.get_configuration()
212211
validator = schema.SchemaValidator(schema=config, logger=self.logger)
213212
validator.validate()
214213

215214
return config
216215

217-
def evaluate(self, *, name: str, context: Optional[Dict[str, Any]] = None, default: JSONType) -> JSONType:
216+
def evaluate(self, *, name: str, context: dict[str, Any] | None = None, default: JSONType) -> JSONType:
218217
"""Evaluate whether a feature flag should be enabled according to stored schema and input context
219218
220219
**Logic when evaluating a feature flag**
@@ -243,7 +242,7 @@ def evaluate(self, *, name: str, context: Optional[Dict[str, Any]] = None, defau
243242
----------
244243
name: str
245244
feature name to evaluate
246-
context: Optional[Dict[str, Any]]
245+
context: dict[str, Any] | None
247246
Attributes that should be evaluated against the stored schema.
248247
249248
for example: `{"tenant_id": "X", "username": "Y", "region": "Z"}`
@@ -306,7 +305,7 @@ def lambda_handler(event: dict, context: LambdaContext):
306305
# Maintenance: Revisit before going GA. We might to simplify customers on-boarding by not requiring it
307306
# for non-boolean flags. It'll need minor implementation changes, docs changes, and maybe refactor
308307
# get_enabled_features. We can minimize breaking change, despite Beta label, by having a new
309-
# method `get_matching_features` returning Dict[feature_name, feature_value]
308+
# method `get_matching_features` returning dict[feature_name, feature_value]
310309
boolean_feature = feature.get(
311310
schema.FEATURE_DEFAULT_VAL_TYPE_KEY,
312311
True,
@@ -330,19 +329,19 @@ def lambda_handler(event: dict, context: LambdaContext):
330329
boolean_feature=boolean_feature,
331330
)
332331

333-
def get_enabled_features(self, *, context: Optional[Dict[str, Any]] = None) -> List[str]:
332+
def get_enabled_features(self, *, context: dict[str, Any] | None = None) -> list[str]:
334333
"""Get all enabled feature flags while also taking into account context
335334
(when a feature has defined rules)
336335
337336
Parameters
338337
----------
339-
context: Optional[Dict[str, Any]]
338+
context: dict[str, Any] | None
340339
dict of attributes that you would like to match the rules
341340
against, can be `{'tenant_id: 'X', 'username':' 'Y', 'region': 'Z'}` etc.
342341
343342
Returns
344343
----------
345-
List[str]
344+
list[str]
346345
list of all feature names that either matches context or have True as default
347346
348347
**Example**
@@ -359,10 +358,10 @@ def get_enabled_features(self, *, context: Optional[Dict[str, Any]] = None) -> L
359358
if context is None:
360359
context = {}
361360

362-
features_enabled: List[str] = []
361+
features_enabled: list[str] = []
363362

364363
try:
365-
features: Dict[str, Any] = self.get_configuration()
364+
features: dict[str, Any] = self.get_configuration()
366365
except ConfigurationStoreError as err:
367366
self.logger.debug(f"Failed to fetch feature flags from store, returning empty list, reason={err}")
368367
return features_enabled

0 commit comments

Comments
 (0)