|
| 1 | +# implementation guide |
| 2 | +# https://martinfowler.com/articles/feature-toggles.html |
| 3 | +from argparse import Action, ArgumentParser |
| 4 | +from typing import Never, Self, Optional, TextIO, Sequence, Any |
| 5 | + |
| 6 | +import yaml |
| 7 | + |
| 8 | + |
| 9 | +class NonExistingFeatureToggleException(Exception): |
| 10 | + pass |
| 11 | + |
| 12 | + |
| 13 | +class FeatureToggle: |
| 14 | + CFG_KEY = "features" |
| 15 | + |
| 16 | + def __init__(self, features_default_config: dict[str, bool]): |
| 17 | + self._store: dict[str, bool] = features_default_config |
| 18 | + self._authorized_keys = list(features_default_config.keys()) |
| 19 | + |
| 20 | + def checkFeatureExistsOrRaise(self, key: str) -> bool | Never: |
| 21 | + if key in self._authorized_keys: |
| 22 | + return True |
| 23 | + raise NonExistingFeatureToggleException( |
| 24 | + f"feature toggle {key} is unknown, please check your code" |
| 25 | + ) |
| 26 | + |
| 27 | + def getAllFeatureNames(self) -> list[str]: |
| 28 | + return self._authorized_keys |
| 29 | + |
| 30 | + def featureIsEnabled(self, key: str) -> bool: |
| 31 | + self.checkFeatureExistsOrRaise(key) |
| 32 | + return bool(self._store.get(key)) |
| 33 | + |
| 34 | + def setFeature(self, key: str, toggle: bool) -> Self: |
| 35 | + self.checkFeatureExistsOrRaise(key) |
| 36 | + self._store[key] = toggle |
| 37 | + return self # chain |
| 38 | + |
| 39 | + def setFeatures(self, config: dict[str, bool]) -> Self: |
| 40 | + for feature_key, feature_toggle in config.items(): |
| 41 | + self.setFeature(feature_key, feature_toggle) |
| 42 | + return self # chain |
| 43 | + |
| 44 | + def setFeaturesFromConfig(self, config: dict) -> Self: |
| 45 | + config_dict = dict(config.get(self.CFG_KEY, dict())) |
| 46 | + self.setFeatures(config_dict) |
| 47 | + return self |
| 48 | + |
| 49 | + def setFeaturesFromYAML(self, yaml_stream_or_str: TextIO | str) -> Self: |
| 50 | + config = yaml.safe_load(yaml_stream_or_str) |
| 51 | + self.setFeaturesFromConfig(config) |
| 52 | + return self |
| 53 | + |
| 54 | + def setFeaturesFromYAMLFile(self, path: str) -> Self: |
| 55 | + with open(path, "r") as cfg_file: |
| 56 | + self.setFeaturesFromYAML(cfg_file) |
| 57 | + return self |
| 58 | + |
| 59 | + def toggleFeatureActionFactory(self, toggle_to: bool) -> type[Action]: |
| 60 | + feature_toggle_instance = self |
| 61 | + |
| 62 | + class ToggleFeatureAction(Action): |
| 63 | + def __call__( |
| 64 | + self, |
| 65 | + parser: ArgumentParser, |
| 66 | + namespace: object, |
| 67 | + values: str | Sequence[Any] | None, |
| 68 | + option_string: Optional[str] = None, |
| 69 | + ): |
| 70 | + if type(values) is list: |
| 71 | + [feature_toggle_instance.setFeature(v, toggle_to) for v in values] |
| 72 | + elif type(values) is str: |
| 73 | + feature_toggle_instance.setFeature(values, toggle_to) |
| 74 | + setattr(namespace, self.dest, values) |
| 75 | + |
| 76 | + return ToggleFeatureAction |
| 77 | + |
| 78 | + def __str__(self): |
| 79 | + features_desc = [] |
| 80 | + conv = {True: "ENABLED", False: "DISABLED"} |
| 81 | + for feature, state in self._store.items(): |
| 82 | + features_desc.append(f"{feature}: {conv.get(state)}") |
| 83 | + return ", ".join(features_desc) |
| 84 | + |
| 85 | + |
| 86 | +features = FeatureToggle({"interface-auto-descriptions": True}) |
0 commit comments