plain.flags
Local feature flags via database models.
Custom flags are written as subclasses of Flag
.
You define the flag's "key" and initial value,
and the results will be stored in the database for future reference.
# app/flags.py
from plain.flags import Flag
class FooEnabled(Flag):
def __init__(self, user):
self.user = user
def get_key(self):
return self.user
def get_value(self):
# Initially all users will have this feature disabled
# and we'll enable them manually in the admin
return False
Use flags in HTML templates:
{% if flags.FooEnabled(request.user) %}
<p>Foo is enabled for you!</p>
{% else %}
<p>Foo is disabled for you.</p>
{% endif %}
Or in Python:
import flags
print(flags.FooEnabled(user).value)
Installation
INSTALLED_PACKAGES = [
...
"plain.flags",
]
Create a flags.py
at the top of your app
(or point settings.FLAGS_MODULE
to a different location).
Advanced usage
Ultimately you can do whatever you want inside of get_key
and get_value
.
class OrganizationFeature(Flag):
url_param_name = ""
def __init__(self, request=None, organization=None):
# Both of these are optional, but will usually both be given
self.request = request
self.organization = organization
def get_key(self):
if (
self.url_param_name
and self.request
and self.url_param_name in self.request.GET
):
return None
if not self.organization:
# Don't save the flag result for PRs without an organization
return None
return self.organization
def get_value(self):
if self.url_param_name and self.request:
if self.request.GET.get(self.url_param_name) == "1":
return True
if self.request.GET.get(self.url_param_name) == "0":
return False
if not self.organization:
return False
# All organizations will start with False,
# and I'll override in the DB for the ones that should be True
return False
class AIEnabled(OrganizationFeature):
pass
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