Skip to content

Commit 54ef9b5

Browse files
Merge pull request #129 from Flagsmith/release/2.0.6
Release 2.0.6
2 parents bfbffec + 651e603 commit 54ef9b5

File tree

13 files changed

+79
-53
lines changed

13 files changed

+79
-53
lines changed

flag_engine/api/filters.py

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,7 @@
11
import typing
22

33

4-
class Reverser:
5-
"""
6-
Helper class to allow us to reverse the sort direction for individual keys when
7-
sorting by multiple keys
8-
9-
e.g. s = sorted(l, key=lambda e: (attr1, Reverser(attr2)))
10-
11-
"""
12-
13-
def __init__(self, obj: typing.Any):
14-
self.obj = obj
15-
16-
def __eq__(self, other):
17-
return self.obj == other.obj
18-
19-
def __lt__(self, other):
20-
return other.obj < self.obj
21-
22-
23-
def sort_and_filter_feature_segments(
4+
def filter_feature_segments(
245
feature_segments: typing.Iterable, environment_api_key: str
256
) -> typing.List:
267
"""
@@ -41,7 +22,4 @@ def sort_and_filter_feature_segments(
4122
)
4223
)
4324

44-
# TODO: determine why this sorting is necessary
45-
return sorted(
46-
feature_segments, key=lambda fs: (fs.feature_id, Reverser(fs.priority))
47-
)
25+
return feature_segments

flag_engine/api/schemas.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
DjangoFeatureStatesRelatedManagerField,
77
DjangoRelatedManagerField,
88
)
9-
from flag_engine.api.filters import sort_and_filter_feature_segments
9+
from flag_engine.api.filters import filter_feature_segments
1010
from flag_engine.environments.schemas import (
1111
BaseEnvironmentAPIKeySchema,
1212
BaseEnvironmentSchema,
@@ -83,7 +83,7 @@ def serialize_feature_states(self, instance: typing.Any) -> typing.List[dict]:
8383
# TODO: move this logic to Django so we can optimise queries
8484
# api key is set in the context using a pre_dump method on EnvironmentSchema.
8585
environment_api_key = self.context.get("environment_api_key")
86-
feature_segments = sort_and_filter_feature_segments(
86+
feature_segments = filter_feature_segments(
8787
instance.feature_segments.all(), environment_api_key
8888
)
8989

@@ -100,7 +100,6 @@ def serialize_feature_states(self, instance: typing.Any) -> typing.List[dict]:
100100
feature_state.version > existing_feature_state.version
101101
):
102102
feature_states[feature_state.feature_id] = feature_state
103-
104103
return self.feature_state_schema.dump(list(feature_states.values()), many=True)
105104

106105

flag_engine/engine.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,11 @@ def _get_identity_feature_states_dict(
101101
identity_segments = get_identity_segments(environment, identity, override_traits)
102102
for matching_segment in identity_segments:
103103
for feature_state in matching_segment.feature_states:
104-
# note that feature states are stored on the segment in descending priority
105-
# order so we only care that the last one is added
106-
# TODO: can we optimise this?
104+
if feature_state.feature in feature_states:
105+
if feature_states[feature_state.feature].is_higher_segment_priority(
106+
feature_state
107+
):
108+
continue
107109
feature_states[feature_state.feature] = feature_state
108110

109111
# Override with any feature states defined directly the identity

flag_engine/features/models.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import math
12
import typing
23
import uuid
34
from dataclasses import dataclass, field
@@ -32,11 +33,17 @@ class MultivariateFeatureStateValueModel:
3233
mv_fs_value_uuid: str = field(default_factory=uuid.uuid4)
3334

3435

36+
@dataclass
37+
class FeatureSegmentModel:
38+
priority: int = None
39+
40+
3541
@dataclass
3642
class FeatureStateModel:
3743
feature: FeatureModel
3844
enabled: bool
3945
django_id: int = None
46+
feature_segment: FeatureSegmentModel = None
4047
featurestate_uuid: str = field(default_factory=uuid.uuid4)
4148
feature_state_value: typing.Any = field(default=None, init=False)
4249
multivariate_feature_state_values: typing.List[
@@ -58,6 +65,34 @@ def get_value(self, identity_id: typing.Union[int, str] = None) -> typing.Any:
5865
return self._get_multivariate_value(identity_id)
5966
return self.feature_state_value
6067

68+
def is_higher_segment_priority(self, other: "FeatureStateModel") -> bool:
69+
"""
70+
Returns `True` if `self` is higher segment priority than `other`
71+
(i.e. has lower value for feature_segment.priority)
72+
73+
NOTE:
74+
A segment will be considered higher priority only if:
75+
1. `other` does not have a feature segment(i.e: it is an environment feature state or it's a
76+
feature state with feature segment but from an old document that does not have `feature_segment.priority`)
77+
but `self` does.
78+
79+
2. `other` have a feature segment with high priority
80+
81+
"""
82+
83+
try:
84+
return (
85+
getattr(
86+
self.feature_segment,
87+
"priority",
88+
math.inf,
89+
)
90+
< other.feature_segment.priority
91+
)
92+
93+
except (TypeError, AttributeError):
94+
return False
95+
6196
def _get_multivariate_value(
6297
self, identity_id: typing.Union[int, str]
6398
) -> typing.Any:

flag_engine/features/schemas.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from flag_engine.features.models import (
66
FeatureModel,
7+
FeatureSegmentModel,
78
FeatureStateModel,
89
MultivariateFeatureOptionModel,
910
MultivariateFeatureStateValueModel,
@@ -12,6 +13,13 @@
1213
from flag_engine.utils.marshmallow.schemas import LoadToModelSchema
1314

1415

16+
class FeatureSegmentSchema(LoadToModelSchema):
17+
priority = fields.Int()
18+
19+
class Meta:
20+
model_class = FeatureSegmentModel
21+
22+
1523
class FeatureSchema(LoadToModelSchema):
1624
id = fields.Int()
1725
name = fields.Str()
@@ -43,6 +51,8 @@ class BaseFeatureStateSchema(Schema):
4351
featurestate_uuid = fields.Str(dump_default=uuid.uuid4)
4452
feature = fields.Nested(FeatureSchema)
4553
enabled = fields.Bool()
54+
# Used for storing feature segment priority
55+
feature_segment = fields.Nested(FeatureSegmentSchema, allow_none=True)
4656

4757
class Meta:
4858
unknown = EXCLUDE

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
setup(
44
name="flagsmith-flag-engine",
5-
version="2.0.5",
5+
version="2.0.6",
66
author="Flagsmith",
77
author_email="support@flagsmith.com",
88
packages=find_packages(include=["flag_engine", "flag_engine.*"]),

tests/end_to_end/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def mock_django_segment(
5151
)
5252
django_feature_segment = DjangoFeatureSegment(
5353
id_=1,
54+
priority=0,
5455
environment=mock_environment,
5556
feature_states=[
5657
DjangoFeatureState(

tests/mock_django_classes.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,13 @@ class DjangoFeatureSegment:
7272
def __init__(
7373
self,
7474
id_: int,
75+
priority: int,
7576
environment: "DjangoEnvironment",
7677
feature_states: typing.List["DjangoFeatureState"] = None,
7778
):
7879
self.id = id_
7980
self.environment = environment
81+
self.priority = priority
8082
self.feature_states = DjangoFeatureStateRelatedManager(feature_states or [])
8183

8284

tests/unit/api/conftest.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,15 +166,18 @@ def django_segment_rule(django_segment_condition):
166166

167167

168168
@pytest.fixture()
169-
def django_feature_segment(
170-
mocker, django_disabled_feature_state, django_environment_api_key
171-
):
169+
def django_feature_segment(mocker, django_disabled_feature_state, random_api_key):
170+
environment = mocker.MagicMock(api_key=random_api_key)
172171
feature_state = copy.deepcopy(django_disabled_feature_state)
173172
feature_state.id += 1
173+
feature_state.feature_segment = DjangoFeatureSegment(
174+
id_=1, priority=0, environment=environment
175+
)
174176
feature_state.enabled = True
175177
return DjangoFeatureSegment(
176178
id_=1,
177-
environment=mocker.MagicMock(api_key=django_environment_api_key),
179+
priority=0,
180+
environment=environment,
178181
feature_states=[feature_state],
179182
)
180183

0 commit comments

Comments
 (0)