Skip to content

Commit 50f5b76

Browse files
committed
Update client class
1 parent 34f66ae commit 50f5b76

File tree

5 files changed

+661
-58
lines changed

5 files changed

+661
-58
lines changed

splitio/client/client.py

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
import logging
33
import json
44
from collections import namedtuple
5+
import copy
56

67
from splitio.engine.evaluator import Evaluator, CONTROL, EvaluationDataFactory, AsyncEvaluationDataFactory
78
from splitio.engine.splitters import Splitter
89
from splitio.models.impressions import Impression, Label, ImpressionDecorated
910
from splitio.models.events import Event, EventWrapper
1011
from splitio.models.telemetry import get_latency_bucket_index, MethodExceptionsAndLatencies
1112
from splitio.client import input_validator
13+
from splitio.client.util import get_fallback_treatment_and_label
1214
from splitio.util.time import get_current_epoch_time_ms, utctime_ms
1315

1416

@@ -39,7 +41,7 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes
3941
'impressions_disabled': False
4042
}
4143

42-
def __init__(self, factory, recorder, labels_enabled=True):
44+
def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_configuration=None):
4345
"""
4446
Construct a Client instance.
4547
@@ -64,6 +66,7 @@ def __init__(self, factory, recorder, labels_enabled=True):
6466
self._evaluator = Evaluator(self._splitter)
6567
self._telemetry_evaluation_producer = self._factory._telemetry_evaluation_producer
6668
self._telemetry_init_producer = self._factory._telemetry_init_producer
69+
self._fallback_treatments_configuration = fallback_treatments_configuration
6770

6871
@property
6972
def ready(self):
@@ -203,11 +206,23 @@ def _validate_track(self, key, traffic_type, event_type, value=None, properties=
203206
def _get_properties(self, evaluation_options):
204207
return evaluation_options.properties if evaluation_options != None else None
205208

209+
def _get_fallback_treatment_with_config(self, treatment, feature):
210+
label = ""
211+
212+
label, treatment, config = get_fallback_treatment_and_label(self._fallback_treatments_configuration,
213+
feature, treatment, label, _LOGGER)
214+
return treatment, config
215+
216+
def _get_fallback_eval_results(self, eval_result, feature):
217+
result = copy.deepcopy(eval_result)
218+
result["impression"]["label"], result["treatment"], result["configurations"] = get_fallback_treatment_and_label(self._fallback_treatments_configuration,
219+
feature, result["treatment"], result["impression"]["label"], _LOGGER)
220+
return result
206221

207222
class Client(ClientBase): # pylint: disable=too-many-instance-attributes
208223
"""Entry point for the split sdk."""
209224

210-
def __init__(self, factory, recorder, labels_enabled=True):
225+
def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_configuration=None):
211226
"""
212227
Construct a Client instance.
213228
@@ -222,7 +237,7 @@ def __init__(self, factory, recorder, labels_enabled=True):
222237
223238
:rtype: Client
224239
"""
225-
ClientBase.__init__(self, factory, recorder, labels_enabled)
240+
ClientBase.__init__(self, factory, recorder, labels_enabled, fallback_treatments_configuration)
226241
self._context_factory = EvaluationDataFactory(factory._get_storage('splits'), factory._get_storage('segments'), factory._get_storage('rule_based_segments'))
227242

228243
def destroy(self):
@@ -254,10 +269,11 @@ def get_treatment(self, key, feature_flag_name, attributes=None, evaluation_opti
254269
try:
255270
treatment, _ = self._get_treatment(MethodExceptionsAndLatencies.TREATMENT, key, feature_flag_name, attributes, evaluation_options)
256271
return treatment
257-
272+
258273
except:
259274
_LOGGER.error('get_treatment failed')
260-
return CONTROL
275+
treatment, _ = self._get_fallback_treatment_with_config(CONTROL, feature_flag_name)
276+
return treatment
261277

262278
def get_treatment_with_config(self, key, feature_flag_name, attributes=None, evaluation_options=None):
263279
"""
@@ -282,8 +298,8 @@ def get_treatment_with_config(self, key, feature_flag_name, attributes=None, eva
282298

283299
except Exception:
284300
_LOGGER.error('get_treatment_with_config failed')
285-
return CONTROL, None
286-
301+
return self._get_fallback_treatment_with_config(CONTROL, feature_flag_name)
302+
287303
def _get_treatment(self, method, key, feature, attributes=None, evaluation_options=None):
288304
"""
289305
Validate key, feature flag name and object, and get the treatment and config with an optional dictionary of attributes.
@@ -302,7 +318,7 @@ def _get_treatment(self, method, key, feature, attributes=None, evaluation_optio
302318
:rtype: dict
303319
"""
304320
if not self._client_is_usable(): # not destroyed & not waiting for a fork
305-
return CONTROL, None
321+
return self._get_fallback_treatment_with_config(CONTROL, feature)
306322

307323
start = get_current_epoch_time_ms()
308324
if not self.ready:
@@ -312,9 +328,10 @@ def _get_treatment(self, method, key, feature, attributes=None, evaluation_optio
312328
try:
313329
key, bucketing, feature, attributes, evaluation_options = self._validate_treatment_input(key, feature, attributes, method, evaluation_options)
314330
except _InvalidInputError:
315-
return CONTROL, None
331+
return self._get_fallback_treatment_with_config(CONTROL, feature)
316332

317-
result = self._NON_READY_EVAL_RESULT
333+
result = self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, feature)
334+
318335
if self.ready:
319336
try:
320337
ctx = self._context_factory.context_for(key, [feature])
@@ -324,15 +341,15 @@ def _get_treatment(self, method, key, feature, attributes=None, evaluation_optio
324341
_LOGGER.error('Error getting treatment for feature flag')
325342
_LOGGER.debug('Error: ', exc_info=True)
326343
self._telemetry_evaluation_producer.record_exception(method)
327-
result = self._FAILED_EVAL_RESULT
344+
result = self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, feature)
328345

329346
properties = self._get_properties(evaluation_options)
330-
if result['impression']['label'] != Label.SPLIT_NOT_FOUND:
347+
if result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1:
331348
impression_decorated = self._build_impression(key, bucketing, feature, result, properties)
332349
self._record_stats([(impression_decorated, attributes)], start, method)
333350

334351
return result['treatment'], result['configurations']
335-
352+
336353
def get_treatments(self, key, feature_flag_names, attributes=None, evaluation_options=None):
337354
"""
338355
Evaluate multiple feature flags and return a dictionary with all the feature flag/treatments.
@@ -356,7 +373,7 @@ def get_treatments(self, key, feature_flag_names, attributes=None, evaluation_op
356373
return {feature_flag: result[0] for (feature_flag, result) in with_config.items()}
357374

358375
except Exception:
359-
return {feature: CONTROL for feature in feature_flag_names}
376+
return {feature: self._get_fallback_treatment_with_config(CONTROL, feature)[0] for feature in feature_flag_names}
360377

361378
def get_treatments_with_config(self, key, feature_flag_names, attributes=None, evaluation_options=None):
362379
"""
@@ -380,7 +397,7 @@ def get_treatments_with_config(self, key, feature_flag_names, attributes=None, e
380397
return self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, attributes, evaluation_options)
381398

382399
except Exception:
383-
return {feature: (CONTROL, None) for feature in feature_flag_names}
400+
return {feature: (self._get_fallback_treatment_with_config(CONTROL, feature)) for feature in feature_flag_names}
384401

385402
def get_treatments_by_flag_set(self, key, flag_set, attributes=None, evaluation_options=None):
386403
"""
@@ -604,7 +621,7 @@ def _get_treatments(self, key, features, method, attributes=None, evaluation_opt
604621
"""
605622
start = get_current_epoch_time_ms()
606623
if not self._client_is_usable():
607-
return input_validator.generate_control_treatments(features)
624+
return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration)
608625

609626
if not self.ready:
610627
_LOGGER.error("Client is not ready - no calls possible")
@@ -613,9 +630,9 @@ def _get_treatments(self, key, features, method, attributes=None, evaluation_opt
613630
try:
614631
key, bucketing, features, attributes, evaluation_options = self._validate_treatments_input(key, features, attributes, method, evaluation_options)
615632
except _InvalidInputError:
616-
return input_validator.generate_control_treatments(features)
633+
return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration)
617634

618-
results = {n: self._NON_READY_EVAL_RESULT for n in features}
635+
results = {n: self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, n) for n in features}
619636
if self.ready:
620637
try:
621638
ctx = self._context_factory.context_for(key, features)
@@ -625,12 +642,12 @@ def _get_treatments(self, key, features, method, attributes=None, evaluation_opt
625642
_LOGGER.error('Error getting treatment for feature flag')
626643
_LOGGER.debug('Error: ', exc_info=True)
627644
self._telemetry_evaluation_producer.record_exception(method)
628-
results = {n: self._FAILED_EVAL_RESULT for n in features}
645+
results = {n: self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, n) for n in features}
629646

630647
properties = self._get_properties(evaluation_options)
631648
imp_decorated_attrs = [
632649
(i, attributes) for i in self._build_impressions(key, bucketing, results, properties)
633-
if i.Impression.label != Label.SPLIT_NOT_FOUND
650+
if i.Impression.label.find(Label.SPLIT_NOT_FOUND) == -1
634651
]
635652
self._record_stats(imp_decorated_attrs, start, method)
636653

@@ -706,7 +723,7 @@ def track(self, key, traffic_type, event_type, value=None, properties=None):
706723
class ClientAsync(ClientBase): # pylint: disable=too-many-instance-attributes
707724
"""Entry point for the split sdk."""
708725

709-
def __init__(self, factory, recorder, labels_enabled=True):
726+
def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_configuration=None):
710727
"""
711728
Construct a Client instance.
712729
@@ -721,7 +738,7 @@ def __init__(self, factory, recorder, labels_enabled=True):
721738
722739
:rtype: Client
723740
"""
724-
ClientBase.__init__(self, factory, recorder, labels_enabled)
741+
ClientBase.__init__(self, factory, recorder, labels_enabled, fallback_treatments_configuration)
725742
self._context_factory = AsyncEvaluationDataFactory(factory._get_storage('splits'), factory._get_storage('segments'), factory._get_storage('rule_based_segments'))
726743

727744
async def destroy(self):
@@ -756,7 +773,8 @@ async def get_treatment(self, key, feature_flag_name, attributes=None, evaluatio
756773

757774
except:
758775
_LOGGER.error('get_treatment failed')
759-
return CONTROL
776+
treatment, _ = self._get_fallback_treatment_with_config(CONTROL, feature_flag_name)
777+
return treatment
760778

761779
async def get_treatment_with_config(self, key, feature_flag_name, attributes=None, evaluation_options=None):
762780
"""
@@ -781,7 +799,7 @@ async def get_treatment_with_config(self, key, feature_flag_name, attributes=Non
781799

782800
except Exception:
783801
_LOGGER.error('get_treatment_with_config failed')
784-
return CONTROL, None
802+
return self._get_fallback_treatment_with_config(CONTROL, feature_flag_name)
785803

786804
async def _get_treatment(self, method, key, feature, attributes=None, evaluation_options=None):
787805
"""
@@ -801,7 +819,7 @@ async def _get_treatment(self, method, key, feature, attributes=None, evaluation
801819
:rtype: dict
802820
"""
803821
if not self._client_is_usable(): # not destroyed & not waiting for a fork
804-
return CONTROL, None
822+
return self._get_fallback_treatment_with_config(CONTROL, feature)
805823

806824
start = get_current_epoch_time_ms()
807825
if not self.ready:
@@ -811,9 +829,9 @@ async def _get_treatment(self, method, key, feature, attributes=None, evaluation
811829
try:
812830
key, bucketing, feature, attributes, evaluation_options = self._validate_treatment_input(key, feature, attributes, method, evaluation_options)
813831
except _InvalidInputError:
814-
return CONTROL, None
832+
return self._get_fallback_treatment_with_config(CONTROL, feature)
815833

816-
result = self._NON_READY_EVAL_RESULT
834+
result = self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, feature)
817835
if self.ready:
818836
try:
819837
ctx = await self._context_factory.context_for(key, [feature])
@@ -823,10 +841,10 @@ async def _get_treatment(self, method, key, feature, attributes=None, evaluation
823841
_LOGGER.error('Error getting treatment for feature flag')
824842
_LOGGER.debug('Error: ', exc_info=True)
825843
await self._telemetry_evaluation_producer.record_exception(method)
826-
result = self._FAILED_EVAL_RESULT
844+
result = self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, feature)
827845

828846
properties = self._get_properties(evaluation_options)
829-
if result['impression']['label'] != Label.SPLIT_NOT_FOUND:
847+
if result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1:
830848
impression_decorated = self._build_impression(key, bucketing, feature, result, properties)
831849
await self._record_stats([(impression_decorated, attributes)], start, method)
832850
return result['treatment'], result['configurations']
@@ -854,7 +872,7 @@ async def get_treatments(self, key, feature_flag_names, attributes=None, evaluat
854872
return {feature_flag: result[0] for (feature_flag, result) in with_config.items()}
855873

856874
except Exception:
857-
return {feature: CONTROL for feature in feature_flag_names}
875+
return {feature: self._get_fallback_treatment_with_config(CONTROL, feature)[0] for feature in feature_flag_names}
858876

859877
async def get_treatments_with_config(self, key, feature_flag_names, attributes=None, evaluation_options=None):
860878
"""
@@ -878,8 +896,7 @@ async def get_treatments_with_config(self, key, feature_flag_names, attributes=N
878896
return await self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, attributes, evaluation_options)
879897

880898
except Exception:
881-
_LOGGER.error("AA", exc_info=True)
882-
return {feature: (CONTROL, None) for feature in feature_flag_names}
899+
return {feature: (self._get_fallback_treatment_with_config(CONTROL, feature)) for feature in feature_flag_names}
883900

884901
async def get_treatments_by_flag_set(self, key, flag_set, attributes=None, evaluation_options=None):
885902
"""
@@ -1017,7 +1034,7 @@ async def _get_treatments(self, key, features, method, attributes=None, evaluati
10171034
"""
10181035
start = get_current_epoch_time_ms()
10191036
if not self._client_is_usable():
1020-
return input_validator.generate_control_treatments(features)
1037+
return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration)
10211038

10221039
if not self.ready:
10231040
_LOGGER.error("Client is not ready - no calls possible")
@@ -1026,9 +1043,9 @@ async def _get_treatments(self, key, features, method, attributes=None, evaluati
10261043
try:
10271044
key, bucketing, features, attributes, evaluation_options = self._validate_treatments_input(key, features, attributes, method, evaluation_options)
10281045
except _InvalidInputError:
1029-
return input_validator.generate_control_treatments(features)
1046+
return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration)
10301047

1031-
results = {n: self._NON_READY_EVAL_RESULT for n in features}
1048+
results = {n: self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, n) for n in features}
10321049
if self.ready:
10331050
try:
10341051
ctx = await self._context_factory.context_for(key, features)
@@ -1038,7 +1055,7 @@ async def _get_treatments(self, key, features, method, attributes=None, evaluati
10381055
_LOGGER.error('Error getting treatment for feature flag')
10391056
_LOGGER.debug('Error: ', exc_info=True)
10401057
await self._telemetry_evaluation_producer.record_exception(method)
1041-
results = {n: self._FAILED_EVAL_RESULT for n in features}
1058+
results = {n: self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, n) for n in features}
10421059

10431060
properties = self._get_properties(evaluation_options)
10441061
imp_decorated_attrs = [

splitio/client/input_validator.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from splitio.client.key import Key
99
from splitio.client import client
10+
from splitio.client.util import get_fallback_treatment_and_label
1011
from splitio.engine.evaluator import CONTROL
1112

1213

@@ -501,7 +502,7 @@ def validate_feature_flags_get_treatments( # pylint: disable=invalid-name
501502
valid_feature_flags.append(ff)
502503
return valid_feature_flags
503504

504-
def generate_control_treatments(feature_flags):
505+
def generate_control_treatments(feature_flags, fallback_treatments_configuration):
505506
"""
506507
Generate valid feature flags to control.
507508
@@ -516,7 +517,13 @@ def generate_control_treatments(feature_flags):
516517
to_return = {}
517518
for feature_flag in feature_flags:
518519
if isinstance(feature_flag, str) and len(feature_flag.strip())> 0:
519-
to_return[feature_flag] = (CONTROL, None)
520+
treatment = CONTROL
521+
config = None
522+
label = ""
523+
label, treatment, config = get_fallback_treatment_and_label(fallback_treatments_configuration,
524+
feature_flag, treatment, label, _LOGGER)
525+
526+
to_return[feature_flag] = (treatment, config)
520527
return to_return
521528

522529

splitio/client/util.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,22 @@ def get_metadata(config):
5151
version = 'python-%s' % __version__
5252
ip_address, hostname = _get_hostname_and_ip(config)
5353
return SdkMetadata(version, hostname, ip_address)
54+
55+
def get_fallback_treatment_and_label(fallback_treatments_configuration, feature_name, treatment, label, _logger):
56+
if fallback_treatments_configuration == None or fallback_treatments_configuration.fallback_config == None:
57+
return label, treatment, None
58+
59+
if fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment != None and \
60+
fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name) != None:
61+
_logger.debug('Using Fallback Treatment for feature: %s', feature_name)
62+
return fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).label_prefix + label, \
63+
fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).treatment, \
64+
fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).config
65+
66+
if fallback_treatments_configuration.fallback_config.global_fallback_treatment != None:
67+
_logger.debug('Using Global Fallback Treatment.')
68+
return fallback_treatments_configuration.fallback_config.global_fallback_treatment.label_prefix + label, \
69+
fallback_treatments_configuration.fallback_config.global_fallback_treatment.treatment, \
70+
fallback_treatments_configuration.fallback_config.global_fallback_treatment.config
71+
72+
return label, treatment, None

0 commit comments

Comments
 (0)