Source code for pyzeta.framework.feature_toggle.toggle_collection

"""
Very simple base implementation for collections of feature flags. This is part
of the `PyZeta` framework.

Usage example:

.. code-block:: python

    MyToggles(ToggleCollection):
        toggle1: bool
        toggle2: bool

    toggles = MyToggles("toggles.json")

Authors:\n
- Philipp Schuette\n
"""

from json import load
from typing import Dict, Type, TypedDict

from pyzeta.framework.feature_toggle.feature_flag import FeatureFlag
from pyzeta.framework.feature_toggle.toggle_exception import ToggleException
from pyzeta.framework.pyzeta_logging.loggable import Loggable


[docs] class LoadedToggleRequired(TypedDict): "This is need to be able to mix required and optional keys pre python3.11." name: str value: bool description: str
[docs] class LoadedToggle(LoadedToggleRequired, total=False): "The type of a toggle configuration loaded from json." timesAccessible: int
# pylint: disable=too-few-public-methods
[docs] class ToggleCollection(Loggable): "Subclass this class to implement your own collection of feature flags."
[docs] def __init__(self, filename: str) -> None: """ Initialize a new collection of feature toggles from a given `.json` config file. :param filename: name of the .json config file """ annotations = self._retrieveAnnotations() self._config = self._loadFromJson(filename) if undeclaredToggles := set(self._config) - set(annotations): raise ToggleException( f"excess toggles [{undeclaredToggles}] in config file!" ) if unconfiguredToggles := set(annotations) - set(self._config): raise ToggleException( f"unconfigured toggles [{unconfiguredToggles}] in script!" ) for toggle, config in self._config.items(): self._createAttr(toggle, config)
def __str__(self) -> str: "Custom string representation of a collection of feature flags." return f"{self._config}" def _retrieveAnnotations(self) -> Dict[str, Type[object]]: "Retrieve the annotations of the toggle collection." if not (annotations := getattr(self, "__annotations__", None)): raise ToggleException( "valid collection must contain >=1 annotated variables!" ) result: Dict[str, Type[object]] = {} for attr, attrType in annotations.items(): if attrType != bool: raise ToggleException(f"{attr} must be bool, not {attrType}!") # further checks on the attributes present could be done here result[attr] = attrType return result def _loadFromJson(self, filename: str) -> Dict[str, LoadedToggle]: "Load a collection of toggles from a .json file." try: with open(filename, "r", encoding="utf-8") as configFile: config = load(configFile) except Exception as ex: raise ToggleException(f"config file {filename} invalid!") from ex result: Dict[str, LoadedToggle] = {} for toggleName, toggleConfig in config.items(): # further checks on the loaded configuration could be done here result[toggleName] = toggleConfig return result def _createAttr(self, toggle: str, config: LoadedToggle) -> None: "Create a toggle instance attribute from a config." flag = FeatureFlag(**config, logger=self.logger) setattr(self, toggle, flag)