Skip to content

Commit faa38e0

Browse files
Merge pull request #97 from Flagsmith/release/1.6.5
Release 1.6.5
2 parents d4b2a94 + 23772d2 commit faa38e0

File tree

17 files changed

+225
-44
lines changed

17 files changed

+225
-44
lines changed
Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import typing
2+
from datetime import datetime
23

34
from marshmallow import fields
45

@@ -8,14 +9,29 @@ def __init__(self, *args, filter_func: typing.Callable = None, **kwargs):
89
super().__init__(*args, **kwargs)
910
self.filter_func = filter_func
1011

11-
def _serialize(
12-
self, value, *args, **kwargs
13-
) -> typing.Optional[typing.List[typing.Any]]:
12+
def get_value(self, obj, attr, **kwargs) -> typing.List[typing.Any]:
1413
# indicates that the object was passed from django and the 'list' is
1514
# actually a related manager field, so grab all of the related objects
1615
# and filter them as necessary (this is done outside of the django ORM to
1716
# allow for prefetch related optimisations).
18-
value = list(value.all())
17+
value = list(getattr(obj, attr).all())
1918
if self.filter_func:
2019
value = list(filter(self.filter_func, value))
21-
return super(DjangoRelatedManagerField, self)._serialize(value, *args, **kwargs)
20+
return value
21+
22+
23+
class DjangoFeatureStatesRelatedManagerField(DjangoRelatedManagerField):
24+
def get_value(self, obj, attr, **kwargs):
25+
now = datetime.now()
26+
features_map = {}
27+
for fs in getattr(obj, attr).all():
28+
if self.filter_func and not self.filter_func(fs):
29+
continue
30+
31+
existing_feature_state = features_map.get(fs.feature_id)
32+
if not existing_feature_state or (
33+
fs.live_from < now and fs.version > existing_feature_state.version
34+
):
35+
features_map[fs.feature_id] = fs
36+
37+
return list(features_map.values())

flag_engine/django_transform/schemas.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import typing
2+
from datetime import datetime
23

34
from marshmallow import fields, post_dump, pre_dump
45

5-
from flag_engine.django_transform.fields import DjangoRelatedManagerField
6+
from flag_engine.django_transform.fields import (
7+
DjangoFeatureStatesRelatedManagerField,
8+
DjangoRelatedManagerField,
9+
)
610
from flag_engine.django_transform.filters import sort_and_filter_feature_segments
711
from flag_engine.environments.schemas import (
812
BaseEnvironmentAPIKeySchema,
@@ -59,26 +63,34 @@ def __init__(self, *args, **kwargs):
5963
self.feature_state_schema = DjangoFeatureStateSchema()
6064

6165
def serialize_feature_states(self, instance: typing.Any) -> typing.List[dict]:
66+
# TODO: move this logic to Django so we can optimise queries
6267
# api key is set in the context using a pre_dump method on EnvironmentSchema.
6368
environment_api_key = self.context.get("environment_api_key")
6469
feature_segments = sort_and_filter_feature_segments(
6570
instance.feature_segments.all(), environment_api_key
6671
)
6772

68-
# Django datamodel incorrectly uses a foreign key for the
69-
# FeatureState -> FeatureSegment relationship so we have to recursively
70-
# build the list like this
71-
feature_states = []
73+
# iterate over the feature segments and related feature states to end up with
74+
# a list consisting of the latest version feature state for each feature
75+
feature_states = {}
76+
now = datetime.now()
7277
for feature_segment in feature_segments:
73-
feature_states.extend(feature_segment.feature_states.all())
74-
return self.feature_state_schema.dump(feature_states, many=True)
78+
for feature_state in feature_segment.feature_states.all():
79+
existing_feature_state = feature_states.get(feature_state.feature_id)
80+
if not existing_feature_state or (
81+
feature_state.version > existing_feature_state.version
82+
and feature_state.live_from < now
83+
):
84+
feature_states[feature_state.feature_id] = feature_state
85+
86+
return self.feature_state_schema.dump(list(feature_states.values()), many=True)
7587

7688

7789
class DjangoIdentitySchema(BaseIdentitySchema):
7890
identity_traits = DjangoRelatedManagerField(
7991
fields.Nested(TraitSchema), required=False
8092
)
81-
identity_features = DjangoRelatedManagerField(
93+
identity_features = DjangoFeatureStatesRelatedManagerField(
8294
fields.Nested(DjangoFeatureStateSchema), required=False
8395
)
8496
django_id = fields.Int(attribute="id")
@@ -105,7 +117,7 @@ class DjangoProjectSchema(BaseProjectSchema):
105117

106118

107119
class DjangoEnvironmentSchema(BaseEnvironmentSchema):
108-
feature_states = DjangoRelatedManagerField(
120+
feature_states = DjangoFeatureStatesRelatedManagerField(
109121
fields.Nested(DjangoFeatureStateSchema),
110122
filter_func=lambda e: e.feature_segment_id is None and e.identity_id is None,
111123
dump_only=True,

flag_engine/environments/models.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from flag_engine.environments.integrations.models import IntegrationModel
66
from flag_engine.features.models import FeatureStateModel
77
from flag_engine.projects.models import ProjectModel
8+
from flag_engine.utils.datetime import utcnow_with_tz
89

910

1011
@dataclass
@@ -19,7 +20,9 @@ class EnvironmentAPIKeyModel:
1920

2021
@property
2122
def is_valid(self):
22-
return self.active and (not self.expires_at or self.expires_at > datetime.now())
23+
return self.active and (
24+
not self.expires_at or self.expires_at > utcnow_with_tz()
25+
)
2326

2427

2528
@dataclass

flag_engine/features/models.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,6 @@ def get_value(self, identity_id: typing.Union[int, str] = None) -> typing.Any:
5858
return self._get_multivariate_value(identity_id)
5959
return self._value
6060

61-
def get_feature_state_value(self):
62-
"""Mimick django method name to simplify serialization logic"""
63-
return self.get_value()
64-
6561
def _get_multivariate_value(
6662
self, identity_id: typing.Union[int, str]
6763
) -> typing.Any:

flag_engine/identities/models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55

66
from flag_engine.identities.traits.models import TraitModel
77
from flag_engine.utils.collections import IdentityFeaturesList
8+
from flag_engine.utils.datetime import utcnow_with_tz
89

910

1011
@dataclass
1112
class IdentityModel:
1213
identifier: str
1314
environment_api_key: str
14-
created_date: datetime = field(default_factory=datetime.datetime.now)
15+
created_date: datetime = field(default_factory=utcnow_with_tz)
1516
identity_features: IdentityFeaturesList = field(
1617
default_factory=IdentityFeaturesList
1718
)

flag_engine/segments/evaluator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def evaluate_identity_in_segment(
4141
override_traits or identity.identity_traits,
4242
rule,
4343
segment.id,
44-
identity.composite_key,
44+
identity.django_id or identity.composite_key,
4545
)
4646
for rule in segment.rules
4747
)

flag_engine/utils/datetime.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from datetime import datetime, timezone
2+
3+
4+
def utcnow_with_tz() -> datetime:
5+
return datetime.now(tz=timezone.utc)

flag_engine/utils/hashing.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ def get_hashed_percentage_for_object_ids(
66
object_ids: typing.Iterable[typing.Any], iterations: int = 1
77
) -> float:
88
"""
9-
Given a list of object ids, get a floating point number between 0 and 1 based on
10-
the hash of those ids. This should give the same value every time for any
11-
list of ids.
9+
Given a list of object ids, get a floating point number between 0 (inclusive) and
10+
100 (exclusive) based on the hash of those ids. This should give the same value
11+
every time for any list of ids.
1212
13-
:param object_ids: list of object ids to calculate the has for
13+
:param object_ids: list of object ids to calculate the hash for
1414
:param iterations: num times to include each id in the generated string to hash
1515
:return: (float) number between 0 (inclusive) and 100 (exclusive)
1616
"""
@@ -21,8 +21,8 @@ def get_hashed_percentage_for_object_ids(
2121
value = ((hashed_value_as_int % 9999) / 9998) * 100
2222

2323
if value == 100:
24-
# since we want a number between 0 (inclusive) and 1 (exclusive), in the
25-
# unlikely case that we get the exact number 1, we call the method again
24+
# since we want a number between 0 (inclusive) and 100 (exclusive), in the
25+
# unlikely case that we get the exact number 100, we call the method again
2626
# and increase the number of iterations to ensure we get a different result
2727
return get_hashed_percentage_for_object_ids(
2828
object_ids=object_ids, iterations=iterations + 1

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="1.6.4",
5+
version="1.6.5",
66
author="Flagsmith",
77
author_email="[email protected]",
88
packages=find_packages(include=["flag_engine", "flag_engine.*"]),

tests/end_to_end/test_flag_engine_end_to_end.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def test_environment_end_to_end(mock_django_environment):
2929
# and the feature states should be correct
3030
assert len(feature_states) == 1
3131
assert feature_states[0].enabled is True
32-
assert feature_states[0].get_feature_state_value() == "foobar"
32+
assert feature_states[0].get_value() == "foobar"
3333

3434

3535
def test_identity_end_to_end(mock_django_environment, mock_django_feature):
@@ -79,4 +79,4 @@ def test_identity_end_to_end(mock_django_environment, mock_django_feature):
7979
)
8080
assert feature_states
8181
assert len(feature_states) == 1
82-
assert feature_states[0].get_feature_state_value() == identity_feature_state_value
82+
assert feature_states[0].get_value() == identity_feature_state_value

0 commit comments

Comments
 (0)