Skip to content

Commit 5abf718

Browse files
authored
Merge pull request #576 from splitio/feature/prerequisites
Feature/prerequisites
2 parents c30a18b + 21635a8 commit 5abf718

File tree

14 files changed

+664
-34
lines changed

14 files changed

+664
-34
lines changed

splitio/engine/evaluator.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from splitio.models.grammar.matchers.misc import DependencyMatcher
88
from splitio.models.grammar.matchers.keys import UserDefinedSegmentMatcher
99
from splitio.models.grammar.matchers import RuleBasedSegmentMatcher
10+
from splitio.models.grammar.matchers.prerequisites import PrerequisitesMatcher
1011
from splitio.models.rule_based_segments import SegmentType
1112
from splitio.optional.loaders import asyncio
1213

@@ -56,12 +57,22 @@ def eval_with_context(self, key, bucketing, feature_name, attrs, ctx):
5657
label = Label.KILLED
5758
_treatment = feature.default_treatment
5859
else:
59-
treatment, label = self._treatment_for_flag(feature, key, bucketing, attrs, ctx)
60-
if treatment is None:
61-
label = Label.NO_CONDITION_MATCHED
62-
_treatment = feature.default_treatment
63-
else:
64-
_treatment = treatment
60+
if feature.prerequisites is not None:
61+
prerequisites_matcher = PrerequisitesMatcher(feature.prerequisites)
62+
if not prerequisites_matcher.match(key, attrs, {
63+
'evaluator': self,
64+
'bucketing_key': bucketing,
65+
'ec': ctx}):
66+
label = Label.PREREQUISITES_NOT_MET
67+
_treatment = feature.default_treatment
68+
69+
if _treatment == CONTROL:
70+
treatment, label = self._treatment_for_flag(feature, key, bucketing, attrs, ctx)
71+
if treatment is None:
72+
label = Label.NO_CONDITION_MATCHED
73+
_treatment = feature.default_treatment
74+
else:
75+
_treatment = treatment
6576

6677
return {
6778
'treatment': _treatment,
@@ -133,7 +144,6 @@ def context_for(self, key, feature_names):
133144
rb_segments
134145
)
135146

136-
137147
class AsyncEvaluationDataFactory:
138148

139149
def __init__(self, split_storage, segment_storage, rbs_segment_storage):
@@ -199,6 +209,7 @@ def get_pending_objects(features, splits, rbsegments, rb_segments, pending_membe
199209
pending_rbs = set()
200210
for feature in features.values():
201211
cf, cs, crbs = get_dependencies(feature)
212+
cf.extend(get_prerequisites(feature))
202213
pending.update(filter(lambda f: f not in splits, cf))
203214
pending_memberships.update(cs)
204215
pending_rbs.update(filter(lambda f: f not in rb_segments, crbs))
@@ -223,4 +234,6 @@ def update_objects(fetched, fetched_rbs, splits, rb_segments):
223234
rb_segments.update(rbsegments)
224235

225236
return features, rbsegments, splits, rb_segments
226-
237+
238+
def get_prerequisites(feature):
239+
return [prerequisite.feature_flag_name for prerequisite in feature.prerequisites]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Prerequisites matcher classes."""
2+
3+
class PrerequisitesMatcher(object):
4+
5+
def __init__(self, prerequisites):
6+
"""
7+
Build a PrerequisitesMatcher.
8+
9+
:param prerequisites: prerequisites
10+
:type raw_matcher: List of Prerequisites
11+
"""
12+
self._prerequisites = prerequisites
13+
14+
def match(self, key, attributes=None, context=None):
15+
"""
16+
Evaluate user input against a matcher and return whether the match is successful.
17+
18+
:param key: User key.
19+
:type key: str.
20+
:param attributes: Custom user attributes.
21+
:type attributes: dict.
22+
:param context: Evaluation context
23+
:type context: dict
24+
25+
:returns: Wheter the match is successful.
26+
:rtype: bool
27+
"""
28+
if self._prerequisites == None:
29+
return True
30+
31+
evaluator = context.get('evaluator')
32+
bucketing_key = context.get('bucketing_key')
33+
for prerequisite in self._prerequisites:
34+
result = evaluator.eval_with_context(key, bucketing_key, prerequisite.feature_flag_name, attributes, context['ec'])
35+
if result['treatment'] not in prerequisite.treatments:
36+
return False
37+
38+
return True

splitio/models/impressions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,8 @@ class Label(object): # pylint: disable=too-few-public-methods
6060
# Treatment: control
6161
# Label: not ready
6262
NOT_READY = 'not ready'
63+
64+
# Condition: Prerequisites not met
65+
# Treatment: Default treatment
66+
# Label: prerequisites not met
67+
PREREQUISITES_NOT_MET = "prerequisites not met"

splitio/models/splits.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
SplitView = namedtuple(
1212
'SplitView',
13-
['name', 'traffic_type', 'killed', 'treatments', 'change_number', 'configs', 'default_treatment', 'sets', 'impressions_disabled']
13+
['name', 'traffic_type', 'killed', 'treatments', 'change_number', 'configs', 'default_treatment', 'sets', 'impressions_disabled', 'prerequisites']
1414
)
1515

1616
_DEFAULT_CONDITIONS_TEMPLATE = {
@@ -40,7 +40,28 @@
4040
"label": "targeting rule type unsupported by sdk"
4141
}
4242

43+
class Prerequisites(object):
44+
"""Prerequisites."""
45+
def __init__(self, feature_flag_name, treatments):
46+
self._feature_flag_name = feature_flag_name
47+
self._treatments = treatments
48+
49+
@property
50+
def feature_flag_name(self):
51+
"""Return featur eflag name."""
52+
return self._feature_flag_name
4353

54+
@property
55+
def treatments(self):
56+
"""Return treatments."""
57+
return self._treatments
58+
59+
def to_json(self):
60+
to_return = []
61+
for feature_flag_name in self._feature_flag_name:
62+
to_return.append({"n": feature_flag_name, "ts": [treatment for treatment in self._treatments]})
63+
64+
return to_return
4465

4566
class Status(Enum):
4667
"""Split status."""
@@ -74,7 +95,8 @@ def __init__( # pylint: disable=too-many-arguments
7495
traffic_allocation_seed=None,
7596
configurations=None,
7697
sets=None,
77-
impressions_disabled=None
98+
impressions_disabled=None,
99+
prerequisites = None
78100
):
79101
"""
80102
Class constructor.
@@ -99,6 +121,8 @@ def __init__( # pylint: disable=too-many-arguments
99121
:type sets: list
100122
:pram impressions_disabled: track impressions flag
101123
:type impressions_disabled: boolean
124+
:pram prerequisites: prerequisites
125+
:type prerequisites: List of Preqreuisites
102126
"""
103127
self._name = name
104128
self._seed = seed
@@ -129,6 +153,7 @@ def __init__( # pylint: disable=too-many-arguments
129153
self._configurations = configurations
130154
self._sets = set(sets) if sets is not None else set()
131155
self._impressions_disabled = impressions_disabled if impressions_disabled is not None else False
156+
self._prerequisites = prerequisites if prerequisites is not None else []
132157

133158
@property
134159
def name(self):
@@ -194,6 +219,11 @@ def sets(self):
194219
def impressions_disabled(self):
195220
"""Return impressions_disabled of the split."""
196221
return self._impressions_disabled
222+
223+
@property
224+
def prerequisites(self):
225+
"""Return prerequisites of the split."""
226+
return self._prerequisites
197227

198228
def get_configurations_for(self, treatment):
199229
"""Return the mapping of treatments to configurations."""
@@ -224,7 +254,8 @@ def to_json(self):
224254
'conditions': [c.to_json() for c in self.conditions],
225255
'configurations': self._configurations,
226256
'sets': list(self._sets),
227-
'impressionsDisabled': self._impressions_disabled
257+
'impressionsDisabled': self._impressions_disabled,
258+
'prerequisites': [prerequisite.to_json() for prerequisite in self._prerequisites]
228259
}
229260

230261
def to_split_view(self):
@@ -243,7 +274,8 @@ def to_split_view(self):
243274
self._configurations if self._configurations is not None else {},
244275
self._default_treatment,
245276
list(self._sets) if self._sets is not None else [],
246-
self._impressions_disabled
277+
self._impressions_disabled,
278+
self._prerequisites
247279
)
248280

249281
def local_kill(self, default_treatment, change_number):
@@ -300,5 +332,13 @@ def from_raw(raw_split):
300332
traffic_allocation_seed=raw_split.get('trafficAllocationSeed'),
301333
configurations=raw_split.get('configurations'),
302334
sets=set(raw_split.get('sets')) if raw_split.get('sets') is not None else [],
303-
impressions_disabled=raw_split.get('impressionsDisabled') if raw_split.get('impressionsDisabled') is not None else False
335+
impressions_disabled=raw_split.get('impressionsDisabled') if raw_split.get('impressionsDisabled') is not None else False,
336+
prerequisites=from_raw_prerequisites(raw_split.get('prerequisites')) if raw_split.get('prerequisites') is not None else []
304337
)
338+
339+
def from_raw_prerequisites(raw_prerequisites):
340+
to_return = []
341+
for prerequisite in raw_prerequisites:
342+
to_return.append(Prerequisites(prerequisite['n'], prerequisite['ts']))
343+
344+
return to_return

splitio/sync/split.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,8 @@ def _make_feature_flag(feature_flag_name, conditions, configs=None):
433433
'defaultTreatment': 'control',
434434
'algo': 2,
435435
'conditions': conditions,
436-
'configurations': configs
436+
'configurations': configs,
437+
'prerequisites': []
437438
})
438439

439440
@staticmethod
@@ -542,6 +543,8 @@ def _sanitize_feature_flag_elements(self, parsed_feature_flags):
542543
if 'sets' not in feature_flag:
543544
feature_flag['sets'] = []
544545
feature_flag['sets'] = validate_flag_sets(feature_flag['sets'], 'Localhost Validator')
546+
if 'prerequisites' not in feature_flag:
547+
feature_flag['prerequisites'] = []
545548
sanitized_feature_flags.append(feature_flag)
546549
return sanitized_feature_flags
547550

@@ -560,6 +563,7 @@ def _sanitize_rb_segment_elements(self, parsed_rb_segments):
560563
if 'name' not in rb_segment or rb_segment['name'].strip() == '':
561564
_LOGGER.warning("A rule based segment in json file does not have (Name) or property is empty, skipping.")
562565
continue
566+
563567
for element in [('trafficTypeName', 'user', None, None, None, None),
564568
('status', splits.Status.ACTIVE.value, None, None, [e.value for e in splits.Status], None),
565569
('changeNumber', 0, 0, None, None, None)]:

tests/client/test_input_validator.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def test_get_treatment(self, mocker):
2828
conditions_mock = mocker.PropertyMock()
2929
conditions_mock.return_value = []
3030
type(split_mock).conditions = conditions_mock
31+
type(split_mock).prerequisites = []
3132
storage_mock = mocker.Mock(spec=SplitStorage)
3233
storage_mock.fetch_many.return_value = {'some_feature': split_mock}
3334
rbs_storage = mocker.Mock(spec=InMemoryRuleBasedSegmentStorage)
@@ -264,6 +265,7 @@ def test_get_treatment_with_config(self, mocker):
264265
conditions_mock = mocker.PropertyMock()
265266
conditions_mock.return_value = []
266267
type(split_mock).conditions = conditions_mock
268+
type(split_mock).prerequisites = []
267269

268270
def _configs(treatment):
269271
return '{"some": "property"}' if treatment == 'default_treatment' else None
@@ -819,6 +821,8 @@ def test_get_treatments(self, mocker):
819821
conditions_mock = mocker.PropertyMock()
820822
conditions_mock.return_value = []
821823
type(split_mock).conditions = conditions_mock
824+
type(split_mock).prerequisites = []
825+
822826
storage_mock = mocker.Mock(spec=SplitStorage)
823827
storage_mock.fetch_many.return_value = {
824828
'some_feature': split_mock
@@ -965,6 +969,7 @@ def test_get_treatments_with_config(self, mocker):
965969
conditions_mock = mocker.PropertyMock()
966970
conditions_mock.return_value = []
967971
type(split_mock).conditions = conditions_mock
972+
type(split_mock).prerequisites = []
968973

969974
storage_mock = mocker.Mock(spec=SplitStorage)
970975
storage_mock.fetch_many.return_value = {
@@ -1113,6 +1118,7 @@ def test_get_treatments_by_flag_set(self, mocker):
11131118
conditions_mock = mocker.PropertyMock()
11141119
conditions_mock.return_value = []
11151120
type(split_mock).conditions = conditions_mock
1121+
type(split_mock).prerequisites = []
11161122
storage_mock = mocker.Mock(spec=InMemorySplitStorage)
11171123
storage_mock.fetch_many.return_value = {
11181124
'some_feature': split_mock
@@ -1231,6 +1237,7 @@ def test_get_treatments_by_flag_sets(self, mocker):
12311237
conditions_mock = mocker.PropertyMock()
12321238
conditions_mock.return_value = []
12331239
type(split_mock).conditions = conditions_mock
1240+
type(split_mock).prerequisites = []
12341241
storage_mock = mocker.Mock(spec=InMemorySplitStorage)
12351242
storage_mock.fetch_many.return_value = {
12361243
'some_feature': split_mock
@@ -1358,6 +1365,7 @@ def _configs(treatment):
13581365
conditions_mock = mocker.PropertyMock()
13591366
conditions_mock.return_value = []
13601367
type(split_mock).conditions = conditions_mock
1368+
type(split_mock).prerequisites = []
13611369
storage_mock = mocker.Mock(spec=InMemorySplitStorage)
13621370
storage_mock.fetch_many.return_value = {
13631371
'some_feature': split_mock
@@ -1481,6 +1489,7 @@ def _configs(treatment):
14811489
conditions_mock = mocker.PropertyMock()
14821490
conditions_mock.return_value = []
14831491
type(split_mock).conditions = conditions_mock
1492+
type(split_mock).prerequisites = []
14841493
storage_mock = mocker.Mock(spec=InMemorySplitStorage)
14851494
storage_mock.fetch_many.return_value = {
14861495
'some_feature': split_mock
@@ -1632,6 +1641,7 @@ async def test_get_treatment(self, mocker):
16321641
conditions_mock = mocker.PropertyMock()
16331642
conditions_mock.return_value = []
16341643
type(split_mock).conditions = conditions_mock
1644+
type(split_mock).prerequisites = []
16351645
storage_mock = mocker.Mock(spec=SplitStorage)
16361646
async def fetch_many(*_):
16371647
return {
@@ -1889,6 +1899,7 @@ async def test_get_treatment_with_config(self, mocker):
18891899
conditions_mock = mocker.PropertyMock()
18901900
conditions_mock.return_value = []
18911901
type(split_mock).conditions = conditions_mock
1902+
type(split_mock).prerequisites = []
18921903

18931904
def _configs(treatment):
18941905
return '{"some": "property"}' if treatment == 'default_treatment' else None
@@ -2423,6 +2434,7 @@ async def test_get_treatments(self, mocker):
24232434
conditions_mock = mocker.PropertyMock()
24242435
conditions_mock.return_value = []
24252436
type(split_mock).conditions = conditions_mock
2437+
type(split_mock).prerequisites = []
24262438
storage_mock = mocker.Mock(spec=SplitStorage)
24272439
async def get(*_):
24282440
return split_mock
@@ -2586,6 +2598,7 @@ async def test_get_treatments_with_config(self, mocker):
25862598
conditions_mock = mocker.PropertyMock()
25872599
conditions_mock.return_value = []
25882600
type(split_mock).conditions = conditions_mock
2601+
type(split_mock).prerequisites = []
25892602

25902603
storage_mock = mocker.Mock(spec=SplitStorage)
25912604
async def get(*_):
@@ -2749,6 +2762,7 @@ async def test_get_treatments_by_flag_set(self, mocker):
27492762
conditions_mock = mocker.PropertyMock()
27502763
conditions_mock.return_value = []
27512764
type(split_mock).conditions = conditions_mock
2765+
type(split_mock).prerequisites = []
27522766
storage_mock = mocker.Mock(spec=SplitStorage)
27532767
async def get(*_):
27542768
return split_mock
@@ -2893,6 +2907,7 @@ async def test_get_treatments_by_flag_sets(self, mocker):
28932907
conditions_mock = mocker.PropertyMock()
28942908
conditions_mock.return_value = []
28952909
type(split_mock).conditions = conditions_mock
2910+
type(split_mock).prerequisites = []
28962911
storage_mock = mocker.Mock(spec=SplitStorage)
28972912
async def get(*_):
28982913
return split_mock
@@ -3048,6 +3063,7 @@ def _configs(treatment):
30483063
conditions_mock = mocker.PropertyMock()
30493064
conditions_mock.return_value = []
30503065
type(split_mock).conditions = conditions_mock
3066+
type(split_mock).prerequisites = []
30513067
storage_mock = mocker.Mock(spec=SplitStorage)
30523068
async def get(*_):
30533069
return split_mock
@@ -3195,6 +3211,7 @@ def _configs(treatment):
31953211
conditions_mock = mocker.PropertyMock()
31963212
conditions_mock.return_value = []
31973213
type(split_mock).conditions = conditions_mock
3214+
type(split_mock).prerequisites = []
31983215
storage_mock = mocker.Mock(spec=SplitStorage)
31993216
async def get(*_):
32003217
return split_mock

0 commit comments

Comments
 (0)