Skip to content

Commit 395b73d

Browse files
committed
Add defaults
1 parent 14d05bc commit 395b73d

File tree

3 files changed

+155
-33
lines changed

3 files changed

+155
-33
lines changed

flagsmith/flagsmith.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,27 @@
1111

1212
from flagsmith.analytics import AnalyticsProcessor
1313
from flagsmith.exceptions import FlagsmithAPIError, FlagsmithClientError
14-
from flagsmith.models import Flags
14+
from flagsmith.models import DefaultFlag, Flags
1515
from flagsmith.polling_manager import EnvironmentDataPollingManager
1616
from flagsmith.utils.identities import generate_identities_data
1717

1818
logger = logging.getLogger(__name__)
1919

20-
API_URL = "https://api.flagsmith.com/api/v1/"
21-
FLAGS_ENDPOINT = "flags/"
22-
IDENTITY_ENDPOINT = "identities/"
23-
TRAITS_ENDPOINT = "traits/"
24-
25-
# TODO:
26-
# - defaults
20+
DEFAULT_API_URL = "https://api.flagsmith.com/api/v1/"
2721

2822

2923
class Flagsmith:
3024
def __init__(
3125
self,
3226
environment_key: str,
33-
api_url: str = API_URL,
27+
api_url: str = DEFAULT_API_URL,
3428
custom_headers: typing.Dict[str, typing.Any] = None,
3529
request_timeout: int = None,
3630
enable_client_side_evaluation: bool = False,
3731
environment_refresh_interval_seconds: int = 60,
3832
retries: Retry = None,
3933
enable_analytics: bool = False,
34+
defaults: typing.List[DefaultFlag] = None,
4035
):
4136
self.session = requests.Session()
4237
self.session.headers.update(
@@ -70,6 +65,8 @@ def __init__(
7065
else None
7166
)
7267

68+
self.defaults = defaults or []
69+
7370
def get_environment_flags(self) -> Flags:
7471
"""
7572
Get all the default for flags for the current environment.
@@ -110,6 +107,7 @@ def _get_environment_flags_from_document(self) -> Flags:
110107
return Flags.from_feature_state_models(
111108
feature_states=engine.get_environment_feature_states(self._environment),
112109
analytics_processor=self._analytics_processor,
110+
defaults=self.defaults,
113111
)
114112

115113
def _get_identity_flags_from_document(
@@ -123,12 +121,18 @@ def _get_identity_flags_from_document(
123121
feature_states=feature_states,
124122
analytics_processor=self._analytics_processor,
125123
identity_id=identity_model.composite_key,
124+
defaults=self.defaults,
126125
)
127126

128127
def _get_environment_flags_from_api(self) -> Flags:
128+
api_flags = self._get_json_response(
129+
url=self.environment_flags_url, method="GET"
130+
)
131+
129132
return Flags.from_api_flags(
130-
flags=self._get_json_response(url=self.environment_flags_url, method="GET"),
133+
api_flags=api_flags,
131134
analytics_processor=self._analytics_processor,
135+
defaults=self.defaults,
132136
)
133137

134138
def _get_identity_flags_from_api(
@@ -139,7 +143,9 @@ def _get_identity_flags_from_api(
139143
url=self.identities_url, method="POST", body=data
140144
)
141145
return Flags.from_api_flags(
142-
flags=json_response["flags"], analytics_processor=self._analytics_processor
146+
api_flags=json_response["flags"],
147+
analytics_processor=self._analytics_processor,
148+
defaults=self.defaults,
143149
)
144150

145151
def _get_json_response(self, url: str, method: str, body: dict = None):

flagsmith/models.py

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,21 @@
88

99

1010
@dataclass
11-
class Flag:
11+
class BaseFlag:
1212
enabled: bool
1313
value: typing.Any
1414
feature_name: str
15+
16+
17+
@dataclass
18+
class DefaultFlag(BaseFlag):
19+
is_default = True
20+
21+
22+
@dataclass
23+
class Flag(BaseFlag):
1524
feature_id: int
25+
is_default = False
1626

1727
@classmethod
1828
def from_feature_state_model(
@@ -37,7 +47,7 @@ def from_api_flag(cls, flag_data: dict) -> "Flag":
3747

3848
@dataclass
3949
class Flags:
40-
flags: typing.Dict[str, Flag]
50+
flags: typing.Dict[str, BaseFlag]
4151
_analytics_processor: AnalyticsProcessor = None
4252

4353
@classmethod
@@ -46,30 +56,38 @@ def from_feature_state_models(
4656
feature_states: typing.List[FeatureStateModel],
4757
analytics_processor: AnalyticsProcessor,
4858
identity_id: typing.Any = None,
59+
defaults: typing.List[DefaultFlag] = None,
4960
) -> "Flags":
50-
return cls(
51-
flags={
52-
feature_state.feature.name: Flag.from_feature_state_model(
53-
feature_state, identity_id=identity_id
54-
)
55-
for feature_state in feature_states
56-
},
57-
_analytics_processor=analytics_processor,
58-
)
61+
flags = {
62+
feature_state.feature.name: Flag.from_feature_state_model(
63+
feature_state, identity_id=identity_id
64+
)
65+
for feature_state in feature_states
66+
}
67+
68+
for default in defaults or []:
69+
flags.setdefault(default.feature_name, default)
70+
71+
return cls(flags=flags, _analytics_processor=analytics_processor)
5972

6073
@classmethod
6174
def from_api_flags(
62-
cls, flags: typing.List[dict], analytics_processor: AnalyticsProcessor
75+
cls,
76+
api_flags: typing.List[dict],
77+
analytics_processor: AnalyticsProcessor,
78+
defaults: typing.List[DefaultFlag] = None,
6379
) -> "Flags":
64-
return cls(
65-
flags={
66-
flag_data["feature"]["name"]: Flag.from_api_flag(flag_data)
67-
for flag_data in flags
68-
},
69-
_analytics_processor=analytics_processor,
70-
)
80+
flags = {
81+
flag_data["feature"]["name"]: Flag.from_api_flag(flag_data)
82+
for flag_data in api_flags
83+
}
84+
85+
for default in defaults or []:
86+
flags.setdefault(default.feature_name, default)
87+
88+
return cls(flags=flags, _analytics_processor=analytics_processor)
7189

72-
def all_flags(self) -> typing.List[Flag]:
90+
def all_flags(self) -> typing.List[BaseFlag]:
7391
"""
7492
Get a list of all Flag objects.
7593
@@ -97,7 +115,7 @@ def get_feature_value(self, feature_name: str) -> typing.Any:
97115
"""
98116
return self.get_flag(feature_name).value
99117

100-
def get_flag(self, feature_name: str) -> typing.Optional[Flag]:
118+
def get_flag(self, feature_name: str) -> typing.Optional[BaseFlag]:
101119
"""
102120
Get a specific flag given the feature name.
103121
@@ -110,7 +128,7 @@ def get_flag(self, feature_name: str) -> typing.Optional[Flag]:
110128
except KeyError:
111129
raise FlagsmithClientError("Feature does not exist: %s" % feature_name)
112130

113-
if self._analytics_processor:
131+
if self._analytics_processor and hasattr(flag, "feature_id"):
114132
self._analytics_processor.track_feature(flag.feature_id)
115133

116134
return flag

tests/test_flagsmith.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from flagsmith import Flagsmith
1010
from flagsmith.exceptions import FlagsmithAPIError
11+
from flagsmith.models import DefaultFlag
1112

1213

1314
def test_flagsmith_starts_polling_manager_on_init_if_enabled(mocker, api_key):
@@ -188,3 +189,100 @@ def test_non_200_response_raises_flagsmith_api_error(flagsmith):
188189

189190
# Then
190191
# expected exception raised
192+
193+
194+
@responses.activate()
195+
def test_default_flag_is_used_when_no_environment_flags_returned(api_key):
196+
# Given
197+
# a default flag
198+
default_flag = DefaultFlag(
199+
enabled=True, value="some-default-value", feature_name="some_feature"
200+
)
201+
flagsmith = Flagsmith(environment_key=api_key, defaults=[default_flag])
202+
203+
# and we mock the API to return an empty list of flags
204+
responses.add(
205+
url=flagsmith.environment_flags_url, method="GET", body=json.dumps([])
206+
)
207+
208+
# When
209+
flags = flagsmith.get_environment_flags()
210+
211+
# Then
212+
# the data from the default flag is used
213+
flag = flags.get_flag(default_flag.feature_name)
214+
assert flag.enabled == default_flag.enabled
215+
assert flag.value == default_flag.value
216+
assert flag.feature_name == default_flag.feature_name
217+
218+
219+
@responses.activate()
220+
def test_default_flag_is_not_used_when_environment_flags_returned(api_key, flags_json):
221+
# Given
222+
# A default flag
223+
default_flag = DefaultFlag(
224+
enabled=True, value="some-default-value", feature_name="some_feature"
225+
)
226+
flagsmith = Flagsmith(environment_key=api_key, defaults=[default_flag])
227+
228+
# but we mock the API to return an actual value for the same feature
229+
responses.add(url=flagsmith.environment_flags_url, method="GET", body=flags_json)
230+
231+
# When
232+
flags = flagsmith.get_environment_flags()
233+
234+
# Then
235+
# the data from the API response is used, not the default flag
236+
flag = flags.get_flag(default_flag.feature_name)
237+
assert flag.value != default_flag.value
238+
assert flag.value == "some-value" # hard coded value in tests/data/flags.json
239+
240+
241+
@responses.activate()
242+
def test_default_flag_is_used_when_no_identity_flags_returned(api_key):
243+
# Given
244+
# a default flag
245+
default_flag = DefaultFlag(
246+
enabled=True, value="some-default-value", feature_name="some_feature"
247+
)
248+
flagsmith = Flagsmith(environment_key=api_key, defaults=[default_flag])
249+
250+
# and we mock the API to return an empty list of flags
251+
response_data = {"flags": [], "traits": []}
252+
responses.add(
253+
url=flagsmith.identities_url, method="POST", body=json.dumps(response_data)
254+
)
255+
256+
# When
257+
flags = flagsmith.get_identity_flags(identifier="identifier")
258+
259+
# Then
260+
# the data from the default flag is used
261+
flag = flags.get_flag(default_flag.feature_name)
262+
assert flag.enabled == default_flag.enabled
263+
assert flag.value == default_flag.value
264+
assert flag.feature_name == default_flag.feature_name
265+
266+
267+
@responses.activate()
268+
def test_default_flag_is_not_used_when_identity_flags_returned(
269+
api_key, identities_json
270+
):
271+
# Given
272+
# A default flag
273+
default_flag = DefaultFlag(
274+
enabled=True, value="some-default-value", feature_name="some_feature"
275+
)
276+
flagsmith = Flagsmith(environment_key=api_key, defaults=[default_flag])
277+
278+
# but we mock the API to return an actual value for the same feature
279+
responses.add(url=flagsmith.identities_url, method="POST", body=identities_json)
280+
281+
# When
282+
flags = flagsmith.get_identity_flags(identifier="identifier")
283+
284+
# Then
285+
# the data from the API response is used, not the default flag
286+
flag = flags.get_flag(default_flag.feature_name)
287+
assert flag.value != default_flag.value
288+
assert flag.value == "some-value" # hard coded value in tests/data/identities.json

0 commit comments

Comments
 (0)