Skip to content
29 changes: 21 additions & 8 deletions splitio/engine/evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from splitio.models.grammar.matchers.misc import DependencyMatcher
from splitio.models.grammar.matchers.keys import UserDefinedSegmentMatcher
from splitio.models.grammar.matchers import RuleBasedSegmentMatcher
from splitio.models.grammar.matchers.prerequisites import PrerequisitesMatcher
from splitio.models.rule_based_segments import SegmentType
from splitio.optional.loaders import asyncio

Expand Down Expand Up @@ -56,12 +57,22 @@ def eval_with_context(self, key, bucketing, feature_name, attrs, ctx):
label = Label.KILLED
_treatment = feature.default_treatment
else:
treatment, label = self._treatment_for_flag(feature, key, bucketing, attrs, ctx)
if treatment is None:
label = Label.NO_CONDITION_MATCHED
_treatment = feature.default_treatment
else:
_treatment = treatment
if feature.prerequisites is not None:
prerequisites_matcher = PrerequisitesMatcher(feature.prerequisites)
if not prerequisites_matcher.match(key, attrs, {
'evaluator': self,
'bucketing_key': bucketing,
'ec': ctx}):
label = Label.PREREQUISITES_NOT_MET
_treatment = feature.default_treatment

if _treatment == CONTROL:
treatment, label = self._treatment_for_flag(feature, key, bucketing, attrs, ctx)
if treatment is None:
label = Label.NO_CONDITION_MATCHED
_treatment = feature.default_treatment
else:
_treatment = treatment

return {
'treatment': _treatment,
Expand Down Expand Up @@ -133,7 +144,6 @@ def context_for(self, key, feature_names):
rb_segments
)


class AsyncEvaluationDataFactory:

def __init__(self, split_storage, segment_storage, rbs_segment_storage):
Expand Down Expand Up @@ -199,6 +209,7 @@ def get_pending_objects(features, splits, rbsegments, rb_segments, pending_membe
pending_rbs = set()
for feature in features.values():
cf, cs, crbs = get_dependencies(feature)
cf.extend(get_prerequisites(feature))
pending.update(filter(lambda f: f not in splits, cf))
pending_memberships.update(cs)
pending_rbs.update(filter(lambda f: f not in rb_segments, crbs))
Expand All @@ -223,4 +234,6 @@ def update_objects(fetched, fetched_rbs, splits, rb_segments):
rb_segments.update(rbsegments)

return features, rbsegments, splits, rb_segments


def get_prerequisites(feature):
return [prerequisite.feature_flag_name for prerequisite in feature.prerequisites]
38 changes: 38 additions & 0 deletions splitio/models/grammar/matchers/prerequisites.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Prerequisites matcher classes."""

class PrerequisitesMatcher(object):

def __init__(self, prerequisites):
"""
Build a PrerequisitesMatcher.

:param prerequisites: prerequisites
:type raw_matcher: List of Prerequisites
"""
self._prerequisites = prerequisites

def match(self, key, attributes=None, context=None):
"""
Evaluate user input against a matcher and return whether the match is successful.

:param key: User key.
:type key: str.
:param attributes: Custom user attributes.
:type attributes: dict.
:param context: Evaluation context
:type context: dict

:returns: Wheter the match is successful.
:rtype: bool
"""
if self._prerequisites == None:
return True

evaluator = context.get('evaluator')
bucketing_key = context.get('bucketing_key')
for prerequisite in self._prerequisites:
result = evaluator.eval_with_context(key, bucketing_key, prerequisite.feature_flag_name, attributes, context['ec'])
if result['treatment'] not in prerequisite.treatments:
return False

return True
5 changes: 5 additions & 0 deletions splitio/models/impressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,8 @@ class Label(object): # pylint: disable=too-few-public-methods
# Treatment: control
# Label: not ready
NOT_READY = 'not ready'

# Condition: Prerequisites not met
# Treatment: Default treatment
# Label: prerequisites not met
PREREQUISITES_NOT_MET = "prerequisites not met"
50 changes: 45 additions & 5 deletions splitio/models/splits.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

SplitView = namedtuple(
'SplitView',
['name', 'traffic_type', 'killed', 'treatments', 'change_number', 'configs', 'default_treatment', 'sets', 'impressions_disabled']
['name', 'traffic_type', 'killed', 'treatments', 'change_number', 'configs', 'default_treatment', 'sets', 'impressions_disabled', 'prerequisites']
)

_DEFAULT_CONDITIONS_TEMPLATE = {
Expand Down Expand Up @@ -40,7 +40,28 @@
"label": "targeting rule type unsupported by sdk"
}

class Prerequisites(object):
"""Prerequisites."""
def __init__(self, feature_flag_name, treatments):
self._feature_flag_name = feature_flag_name
self._treatments = treatments

@property
def feature_flag_name(self):
"""Return featur eflag name."""
return self._feature_flag_name

@property
def treatments(self):
"""Return treatments."""
return self._treatments

def to_json(self):
to_return = []
for feature_flag_name in self._feature_flag_name:
to_return.append({"n": feature_flag_name, "ts": [treatment for treatment in self._treatments]})

return to_return

class Status(Enum):
"""Split status."""
Expand Down Expand Up @@ -74,7 +95,8 @@ def __init__( # pylint: disable=too-many-arguments
traffic_allocation_seed=None,
configurations=None,
sets=None,
impressions_disabled=None
impressions_disabled=None,
prerequisites = None
):
"""
Class constructor.
Expand All @@ -99,6 +121,8 @@ def __init__( # pylint: disable=too-many-arguments
:type sets: list
:pram impressions_disabled: track impressions flag
:type impressions_disabled: boolean
:pram prerequisites: prerequisites
:type prerequisites: List of Preqreuisites
"""
self._name = name
self._seed = seed
Expand Down Expand Up @@ -129,6 +153,7 @@ def __init__( # pylint: disable=too-many-arguments
self._configurations = configurations
self._sets = set(sets) if sets is not None else set()
self._impressions_disabled = impressions_disabled if impressions_disabled is not None else False
self._prerequisites = prerequisites if prerequisites is not None else []

@property
def name(self):
Expand Down Expand Up @@ -194,6 +219,11 @@ def sets(self):
def impressions_disabled(self):
"""Return impressions_disabled of the split."""
return self._impressions_disabled

@property
def prerequisites(self):
"""Return prerequisites of the split."""
return self._prerequisites

def get_configurations_for(self, treatment):
"""Return the mapping of treatments to configurations."""
Expand Down Expand Up @@ -224,7 +254,8 @@ def to_json(self):
'conditions': [c.to_json() for c in self.conditions],
'configurations': self._configurations,
'sets': list(self._sets),
'impressionsDisabled': self._impressions_disabled
'impressionsDisabled': self._impressions_disabled,
'prerequisites': [prerequisite.to_json() for prerequisite in self._prerequisites]
}

def to_split_view(self):
Expand All @@ -243,7 +274,8 @@ def to_split_view(self):
self._configurations if self._configurations is not None else {},
self._default_treatment,
list(self._sets) if self._sets is not None else [],
self._impressions_disabled
self._impressions_disabled,
self._prerequisites
)

def local_kill(self, default_treatment, change_number):
Expand Down Expand Up @@ -300,5 +332,13 @@ def from_raw(raw_split):
traffic_allocation_seed=raw_split.get('trafficAllocationSeed'),
configurations=raw_split.get('configurations'),
sets=set(raw_split.get('sets')) if raw_split.get('sets') is not None else [],
impressions_disabled=raw_split.get('impressionsDisabled') if raw_split.get('impressionsDisabled') is not None else False
impressions_disabled=raw_split.get('impressionsDisabled') if raw_split.get('impressionsDisabled') is not None else False,
prerequisites=from_raw_prerequisites(raw_split.get('prerequisites')) if raw_split.get('prerequisites') is not None else []
)

def from_raw_prerequisites(raw_prerequisites):
to_return = []
for prerequisite in raw_prerequisites:
to_return.append(Prerequisites(prerequisite['n'], prerequisite['ts']))

return to_return
6 changes: 5 additions & 1 deletion splitio/sync/split.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,8 @@ def _make_feature_flag(feature_flag_name, conditions, configs=None):
'defaultTreatment': 'control',
'algo': 2,
'conditions': conditions,
'configurations': configs
'configurations': configs,
'prerequisites': []
})

@staticmethod
Expand Down Expand Up @@ -542,6 +543,8 @@ def _sanitize_feature_flag_elements(self, parsed_feature_flags):
if 'sets' not in feature_flag:
feature_flag['sets'] = []
feature_flag['sets'] = validate_flag_sets(feature_flag['sets'], 'Localhost Validator')
if 'prerequisites' not in feature_flag:
feature_flag['prerequisites'] = []
sanitized_feature_flags.append(feature_flag)
return sanitized_feature_flags

Expand All @@ -560,6 +563,7 @@ def _sanitize_rb_segment_elements(self, parsed_rb_segments):
if 'name' not in rb_segment or rb_segment['name'].strip() == '':
_LOGGER.warning("A rule based segment in json file does not have (Name) or property is empty, skipping.")
continue

for element in [('trafficTypeName', 'user', None, None, None, None),
('status', splits.Status.ACTIVE.value, None, None, [e.value for e in splits.Status], None),
('changeNumber', 0, 0, None, None, None)]:
Expand Down
17 changes: 17 additions & 0 deletions tests/client/test_input_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def test_get_treatment(self, mocker):
conditions_mock = mocker.PropertyMock()
conditions_mock.return_value = []
type(split_mock).conditions = conditions_mock
type(split_mock).prerequisites = []
storage_mock = mocker.Mock(spec=SplitStorage)
storage_mock.fetch_many.return_value = {'some_feature': split_mock}
rbs_storage = mocker.Mock(spec=InMemoryRuleBasedSegmentStorage)
Expand Down Expand Up @@ -264,6 +265,7 @@ def test_get_treatment_with_config(self, mocker):
conditions_mock = mocker.PropertyMock()
conditions_mock.return_value = []
type(split_mock).conditions = conditions_mock
type(split_mock).prerequisites = []

def _configs(treatment):
return '{"some": "property"}' if treatment == 'default_treatment' else None
Expand Down Expand Up @@ -819,6 +821,8 @@ def test_get_treatments(self, mocker):
conditions_mock = mocker.PropertyMock()
conditions_mock.return_value = []
type(split_mock).conditions = conditions_mock
type(split_mock).prerequisites = []

storage_mock = mocker.Mock(spec=SplitStorage)
storage_mock.fetch_many.return_value = {
'some_feature': split_mock
Expand Down Expand Up @@ -965,6 +969,7 @@ def test_get_treatments_with_config(self, mocker):
conditions_mock = mocker.PropertyMock()
conditions_mock.return_value = []
type(split_mock).conditions = conditions_mock
type(split_mock).prerequisites = []

storage_mock = mocker.Mock(spec=SplitStorage)
storage_mock.fetch_many.return_value = {
Expand Down Expand Up @@ -1113,6 +1118,7 @@ def test_get_treatments_by_flag_set(self, mocker):
conditions_mock = mocker.PropertyMock()
conditions_mock.return_value = []
type(split_mock).conditions = conditions_mock
type(split_mock).prerequisites = []
storage_mock = mocker.Mock(spec=InMemorySplitStorage)
storage_mock.fetch_many.return_value = {
'some_feature': split_mock
Expand Down Expand Up @@ -1231,6 +1237,7 @@ def test_get_treatments_by_flag_sets(self, mocker):
conditions_mock = mocker.PropertyMock()
conditions_mock.return_value = []
type(split_mock).conditions = conditions_mock
type(split_mock).prerequisites = []
storage_mock = mocker.Mock(spec=InMemorySplitStorage)
storage_mock.fetch_many.return_value = {
'some_feature': split_mock
Expand Down Expand Up @@ -1358,6 +1365,7 @@ def _configs(treatment):
conditions_mock = mocker.PropertyMock()
conditions_mock.return_value = []
type(split_mock).conditions = conditions_mock
type(split_mock).prerequisites = []
storage_mock = mocker.Mock(spec=InMemorySplitStorage)
storage_mock.fetch_many.return_value = {
'some_feature': split_mock
Expand Down Expand Up @@ -1481,6 +1489,7 @@ def _configs(treatment):
conditions_mock = mocker.PropertyMock()
conditions_mock.return_value = []
type(split_mock).conditions = conditions_mock
type(split_mock).prerequisites = []
storage_mock = mocker.Mock(spec=InMemorySplitStorage)
storage_mock.fetch_many.return_value = {
'some_feature': split_mock
Expand Down Expand Up @@ -1632,6 +1641,7 @@ async def test_get_treatment(self, mocker):
conditions_mock = mocker.PropertyMock()
conditions_mock.return_value = []
type(split_mock).conditions = conditions_mock
type(split_mock).prerequisites = []
storage_mock = mocker.Mock(spec=SplitStorage)
async def fetch_many(*_):
return {
Expand Down Expand Up @@ -1889,6 +1899,7 @@ async def test_get_treatment_with_config(self, mocker):
conditions_mock = mocker.PropertyMock()
conditions_mock.return_value = []
type(split_mock).conditions = conditions_mock
type(split_mock).prerequisites = []

def _configs(treatment):
return '{"some": "property"}' if treatment == 'default_treatment' else None
Expand Down Expand Up @@ -2423,6 +2434,7 @@ async def test_get_treatments(self, mocker):
conditions_mock = mocker.PropertyMock()
conditions_mock.return_value = []
type(split_mock).conditions = conditions_mock
type(split_mock).prerequisites = []
storage_mock = mocker.Mock(spec=SplitStorage)
async def get(*_):
return split_mock
Expand Down Expand Up @@ -2586,6 +2598,7 @@ async def test_get_treatments_with_config(self, mocker):
conditions_mock = mocker.PropertyMock()
conditions_mock.return_value = []
type(split_mock).conditions = conditions_mock
type(split_mock).prerequisites = []

storage_mock = mocker.Mock(spec=SplitStorage)
async def get(*_):
Expand Down Expand Up @@ -2749,6 +2762,7 @@ async def test_get_treatments_by_flag_set(self, mocker):
conditions_mock = mocker.PropertyMock()
conditions_mock.return_value = []
type(split_mock).conditions = conditions_mock
type(split_mock).prerequisites = []
storage_mock = mocker.Mock(spec=SplitStorage)
async def get(*_):
return split_mock
Expand Down Expand Up @@ -2893,6 +2907,7 @@ async def test_get_treatments_by_flag_sets(self, mocker):
conditions_mock = mocker.PropertyMock()
conditions_mock.return_value = []
type(split_mock).conditions = conditions_mock
type(split_mock).prerequisites = []
storage_mock = mocker.Mock(spec=SplitStorage)
async def get(*_):
return split_mock
Expand Down Expand Up @@ -3048,6 +3063,7 @@ def _configs(treatment):
conditions_mock = mocker.PropertyMock()
conditions_mock.return_value = []
type(split_mock).conditions = conditions_mock
type(split_mock).prerequisites = []
storage_mock = mocker.Mock(spec=SplitStorage)
async def get(*_):
return split_mock
Expand Down Expand Up @@ -3195,6 +3211,7 @@ def _configs(treatment):
conditions_mock = mocker.PropertyMock()
conditions_mock.return_value = []
type(split_mock).conditions = conditions_mock
type(split_mock).prerequisites = []
storage_mock = mocker.Mock(spec=SplitStorage)
async def get(*_):
return split_mock
Expand Down
Loading