Skip to content

Commit a33097a

Browse files
author
Matthew Elwell
authored
Merge pull request #75 from Flagsmith/release/1.5.1
release 1.5.1
2 parents 56ee70b + 4141cb8 commit a33097a

File tree

16 files changed

+234
-13
lines changed

16 files changed

+234
-13
lines changed

.github/workflows/pull-request.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ jobs:
2222
uses: actions/checkout@v2
2323
with:
2424
fetch-depth: 0
25+
submodules: recursive
2526

2627
- name: Set up Python ${{ matrix.python-version }}
2728
uses: actions/setup-python@v2
@@ -43,5 +44,4 @@ jobs:
4344
run: flake8 .
4445

4546
- name: Run Tests
46-
run: |
47-
pytest -p no:warnings
47+
run: pytest -p no:warnings

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "tests/engine_tests/engine-test-data"]
2+
path = tests/engine_tests/engine-test-data
3+
url = [email protected]:Flagsmith/engine-test-data.git

flag_engine/features/models.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ def __hash__(self):
2121
@dataclass
2222
class MultivariateFeatureOptionModel:
2323
value: typing.Any
24+
id: int = None
2425

2526

2627
@dataclass
2728
class MultivariateFeatureStateValueModel:
28-
id: int
2929
multivariate_feature_option: MultivariateFeatureOptionModel
3030
percentage_allocation: float
31+
id: int = None
32+
mv_fs_value_uuid: str = field(default_factory=uuid.uuid4)
3133

3234

3335
@dataclass
@@ -65,7 +67,8 @@ def _get_multivariate_value(self, identity_id: int) -> typing.Any:
6567
# percentage value.
6668
start_percentage = 0
6769
for mv_value in sorted(
68-
self.multivariate_feature_state_values, key=lambda v: v.id
70+
self.multivariate_feature_state_values,
71+
key=lambda v: v.id or v.mv_fs_value_uuid,
6972
):
7073
limit = mv_value.percentage_allocation + start_percentage
7174
if start_percentage <= percentage_value < limit:

flag_engine/features/schemas.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import uuid
22

3-
from marshmallow import EXCLUDE, Schema, fields, post_load, validate
3+
from marshmallow import EXCLUDE, Schema, fields, post_dump, post_load, validate
44

55
from flag_engine.features.models import (
66
FeatureModel,
77
FeatureStateModel,
88
MultivariateFeatureOptionModel,
99
MultivariateFeatureStateValueModel,
1010
)
11+
from flag_engine.utils.exceptions import InvalidPercentageAllocation
1112
from flag_engine.utils.marshmallow.schemas import LoadToModelSchema
1213

1314

@@ -21,14 +22,16 @@ class Meta:
2122

2223

2324
class MultivariateFeatureOptionSchema(LoadToModelSchema):
25+
id = fields.Int(allow_none=True)
2426
value = fields.Field(allow_none=True)
2527

2628
class Meta:
2729
model_class = MultivariateFeatureOptionModel
2830

2931

3032
class MultivariateFeatureStateValueSchema(LoadToModelSchema):
31-
id = fields.Int()
33+
id = fields.Int(allow_none=True)
34+
mv_fs_value_uuid = fields.Str(dump_default=uuid.uuid4)
3235
multivariate_feature_option = fields.Nested(MultivariateFeatureOptionSchema)
3336
percentage_allocation = fields.Decimal(validate=[validate.Range(0, 100)])
3437

@@ -50,6 +53,7 @@ class FeatureStateSchema(BaseFeatureStateSchema):
5053
multivariate_feature_state_values = fields.List(
5154
fields.Nested(MultivariateFeatureStateValueSchema)
5255
)
56+
django_id = fields.Int(allow_none=True)
5357

5458
class Meta:
5559
unknown = EXCLUDE
@@ -60,3 +64,18 @@ def make_feature_state(self, data, **kwargs) -> FeatureStateModel:
6064
feature_state = FeatureStateModel(**data)
6165
feature_state.set_value(value)
6266
return feature_state
67+
68+
@post_dump()
69+
def validate_percentage_allocations(self, data, **kwargs):
70+
"""Since we do support modifying percentage allocation on a per identity override bases
71+
we need to validate the percentage before building the document(dict)"""
72+
total_allocation = sum(
73+
mvfsv["percentage_allocation"]
74+
for mvfsv in data["multivariate_feature_state_values"]
75+
)
76+
if total_allocation > 100:
77+
78+
raise InvalidPercentageAllocation(
79+
"Total percentage allocation should not be more than 100"
80+
)
81+
return data

flag_engine/identities/schemas.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from flag_engine.features.schemas import FeatureStateSchema
77
from flag_engine.identities.models import IdentityModel
8+
from flag_engine.utils.marshmallow.fields import IdentityFeaturesListField
89
from flag_engine.utils.marshmallow.schemas import LoadToModelMixin
910

1011
from .traits.schemas import TraitSchema
@@ -29,7 +30,10 @@ def generate_composite_key(self, data: typing.Dict[str, typing.Any], **kwargs):
2930

3031
class IdentitySchema(LoadToModelMixin, BaseIdentitySchema):
3132
identity_traits = fields.List(fields.Nested(TraitSchema), required=False)
32-
identity_features = fields.List(fields.Nested(FeatureStateSchema), required=False)
33+
identity_features = IdentityFeaturesListField(
34+
fields.Nested(FeatureStateSchema), required=False
35+
)
36+
3337
django_id = fields.Int(required=False, allow_none=True)
3438
composite_key = fields.Str(dump_only=True)
3539

flag_engine/utils/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ class FeatureStateNotFound(Exception):
44

55
class DuplicateFeatureState(Exception):
66
pass
7+
8+
9+
class InvalidPercentageAllocation(Exception):
10+
pass
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from marshmallow import fields
2+
3+
from flag_engine.utils.collections import IdentityFeaturesList
4+
5+
6+
class IdentityFeaturesListField(fields.List):
7+
def _deserialize(self, value, attr, data, **kwargs) -> IdentityFeaturesList:
8+
return IdentityFeaturesList(super()._deserialize(value, attr, data, **kwargs))

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.5.0",
5+
version="1.5.1",
66
author="Flagsmith",
77
author_email="[email protected]",
88
packages=find_packages(include=["flag_engine", "flag_engine.*"]),
Submodule engine-test-data added at f10e3e5

tests/engine_tests/test_engine.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import json
2+
import typing
3+
from pathlib import Path
4+
5+
import pytest
6+
7+
from flag_engine.engine import get_identity_feature_states
8+
from flag_engine.environments.builders import build_environment_model
9+
from flag_engine.environments.models import EnvironmentModel
10+
from flag_engine.identities.builders import build_identity_model
11+
from flag_engine.identities.models import IdentityModel
12+
13+
MODULE_PATH = Path(__file__).parent.resolve()
14+
15+
16+
def _extract_test_cases(
17+
file_path: Path,
18+
) -> typing.Iterable[typing.Tuple[EnvironmentModel, IdentityModel, dict]]:
19+
"""
20+
Extract the test cases from the json data file which should be in the following
21+
format.
22+
23+
{
24+
"environment": {...}, // the environment document as found in DynamoDB
25+
"identities_and_responses": [
26+
{
27+
"identity": {...}, // the identity as found in DynamoDB,
28+
"response": {...}, // the response that was obtained from the current API
29+
}
30+
]
31+
}
32+
33+
:param file_path: the path to the json data file
34+
:return: a list of tuples containing the environment, identity and api response
35+
"""
36+
with open(file_path, "r") as f:
37+
test_data = json.loads(f.read())
38+
environment_model = build_environment_model(test_data["environment"])
39+
return [
40+
(
41+
environment_model,
42+
build_identity_model(test_case["identity"]),
43+
test_case["response"],
44+
)
45+
for test_case in test_data["identities_and_responses"]
46+
]
47+
48+
49+
@pytest.mark.parametrize(
50+
"environment_model, identity_model, api_response",
51+
_extract_test_cases(
52+
MODULE_PATH / "engine-test-data/data/environment_n9fbf9h3v4fFgH3U3ngWhb.json"
53+
),
54+
)
55+
def test_engine(environment_model, identity_model, api_response):
56+
# When
57+
# we get the feature states from the engine
58+
engine_response = get_identity_feature_states(environment_model, identity_model)
59+
60+
# and we sort the flags and feature states so we can iterate over them and compare
61+
sorted_engine_flags = sorted(engine_response, key=lambda fs: fs.feature.name)
62+
sorted_api_flags = sorted(api_response["flags"], key=lambda f: f["feature"]["name"])
63+
64+
# Then
65+
# there are an equal number of flags and feature states
66+
assert len(sorted_engine_flags) == len(sorted_api_flags)
67+
68+
# and the values and enabled status of each of the feature states returned by the
69+
# engine are identical to those returned by the Django API (i.e. the test data).
70+
for i, feature_state in enumerate(sorted_engine_flags):
71+
assert (
72+
feature_state.get_value(identity_model.django_id)
73+
== sorted_api_flags[i]["feature_state_value"]
74+
)
75+
assert feature_state.enabled == sorted_api_flags[i]["enabled"]

0 commit comments

Comments
 (0)