Plain is headed towards 1.0! Subscribe for development updates →

  1import logging
  2from typing import Any
  3
  4from plain.runtime import settings
  5from plain.utils import timezone
  6from plain.utils.functional import cached_property
  7
  8from . import exceptions
  9from .utils import coerce_key
 10
 11logger = logging.getLogger(__name__)
 12
 13
 14class Flag:
 15    def get_key(self) -> Any:
 16        """
 17        Determine a unique key for this instance of the flag.
 18        This should be a quick operation, as it will be called on every use of the flag.
 19
 20        For convenience, you can return an instance of a Plain Model
 21        and it will be converted to a string automatically.
 22
 23        Return a falsy value if you don't want to store the flag result.
 24        """
 25        raise NotImplementedError
 26
 27    def get_value(self) -> Any:
 28        """
 29        Compute the resulting value of the flag.
 30
 31        The value needs to be JSON serializable.
 32
 33        If get_key() returns a value, this will only be called once per key
 34        and then subsequent calls will return the saved value from the DB.
 35        """
 36        raise NotImplementedError
 37
 38    def get_db_name(self) -> str:
 39        """
 40        Should basically always be the name of the class.
 41        But this is overridable in case of renaming/refactoring/importing.
 42        """
 43        return self.__class__.__name__
 44
 45    def retrieve_or_compute_value(self) -> Any:
 46        """
 47        Retrieve the value from the DB if it exists,
 48        otherwise compute the value and save it to the DB.
 49        """
 50        from .models import Flag, FlagResult  # So Plain app is ready...
 51
 52        # Create an associated DB Flag that we can use to enable/disable
 53        # and tie the results to
 54        flag_obj, _ = Flag.objects.update_or_create(
 55            name=self.get_db_name(),
 56            defaults={"used_at": timezone.now()},
 57        )
 58        if not flag_obj.enabled:
 59            msg = f"The {flag_obj} flag has been disabled and should either not be called, or be re-enabled."
 60            if settings.DEBUG:
 61                raise exceptions.FlagDisabled(msg)
 62            else:
 63                logger.exception(msg)
 64                # Might not be the type of return value expected! Better than totally crashing now though.
 65                return None
 66
 67        key = self.get_key()
 68        if not key:
 69            # No key, so we always recompute the value and return it
 70            return self.get_value()
 71
 72        key = coerce_key(key)
 73
 74        try:
 75            flag_result = FlagResult.objects.get(flag=flag_obj, key=key)
 76            return flag_result.value
 77        except FlagResult.DoesNotExist:
 78            value = self.get_value()
 79            flag_result = FlagResult.objects.create(flag=flag_obj, key=key, value=value)
 80            return flag_result.value
 81
 82    @cached_property
 83    def value(self) -> Any:
 84        """
 85        Cached version of retrieve_or_compute_value()
 86        """
 87        return self.retrieve_or_compute_value()
 88
 89    def __bool__(self) -> bool:
 90        """
 91        Allow for use in boolean expressions.
 92        """
 93        return bool(self.value)
 94
 95    def __contains__(self, item) -> bool:
 96        """
 97        Allow for use in `in` expressions.
 98        """
 99        return item in self.value
100
101    def __eq__(self, other) -> bool:
102        """
103        Allow for use in `==` expressions.
104        """
105        return self.value == other