Coverage for /Users/davegaeddert/Developer/dropseed/plain/plain-flags/plain/flags/flags.py: 73%

44 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-23 11:16 -0600

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