optimizely-sdk 5.6.0__tar.gz → 5.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/CHANGELOG.md +11 -0
- {optimizely_sdk-5.6.0/optimizely_sdk.egg-info → optimizely_sdk-5.7.0}/PKG-INFO +20 -9
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/decision_service.py +48 -20
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/entities.py +2 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/optimizely.py +26 -2
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/version.py +1 -1
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0/optimizely_sdk.egg-info}/PKG-INFO +20 -9
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/LICENSE +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/MANIFEST.in +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/README.md +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/__init__.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/bucketer.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/cmab/__init__.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/cmab/cmab_client.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/cmab/cmab_service.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/config_manager.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/decision/__init__.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/decision/optimizely_decide_option.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/decision/optimizely_decision.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/decision/optimizely_decision_message.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/error_handler.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event/__init__.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event/event_factory.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event/event_id_normalizer.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event/event_processor.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event/log_event.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event/payload.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event/user_event.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event/user_event_factory.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event_builder.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event_dispatcher.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/exceptions.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/__init__.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/audience.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/condition.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/condition_tree_evaluator.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/constants.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/enums.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/event_tag_utils.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/experiment.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/sdk_settings.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/types.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/validator.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/lib/__init__.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/lib/pymmh3.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/logger.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/notification_center.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/notification_center_registry.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/__init__.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/lru_cache.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/odp_config.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/odp_event.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/odp_event_api_manager.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/odp_event_manager.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/odp_manager.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/odp_segment_api_manager.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/odp_segment_manager.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/optimizely_odp_option.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/optimizely_config.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/optimizely_factory.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/optimizely_user_context.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/project_config.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/py.typed +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/user_profile.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely_sdk.egg-info/SOURCES.txt +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely_sdk.egg-info/dependency_links.txt +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely_sdk.egg-info/requires.txt +7 -7
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely_sdk.egg-info/top_level.txt +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/requirements/core.txt +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/requirements/docs.txt +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/requirements/test.txt +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/requirements/typing.txt +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/setup.cfg +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/setup.py +0 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Optimizely Python SDK Changelog
|
|
2
2
|
|
|
3
|
+
## 5.7.0
|
|
4
|
+
September 2nd, 2026
|
|
5
|
+
|
|
6
|
+
### New Features
|
|
7
|
+
|
|
8
|
+
**Holdout Targeted Delivery Exclusion**: Holdouts can now be configured to exclude Targeted Delivery rules, so users held out of experiments still receive their Targeted Delivery experiences. This gives you a cleaner measure of experiment impact without holding users back from ongoing rollouts.
|
|
9
|
+
|
|
10
|
+
- Add holdout exclusion logic for Targeted Delivery rules ([#522](https://github.com/optimizely/python-sdk/pull/522))
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
3
14
|
## 5.6.0
|
|
4
15
|
July 10th, 2026
|
|
5
16
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: optimizely-sdk
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.7.0
|
|
4
4
|
Summary: Python SDK for Optimizely Feature Experimentation, Optimizely Full Stack (legacy), and Optimizely Rollouts.
|
|
5
5
|
Home-page: https://github.com/optimizely/python-sdk
|
|
6
6
|
Author: Optimizely
|
|
@@ -24,17 +24,17 @@ Requires-Dist: requests>=2.21
|
|
|
24
24
|
Requires-Dist: idna>=2.10
|
|
25
25
|
Requires-Dist: rpds-py<0.20.0; python_version < "3.11"
|
|
26
26
|
Provides-Extra: test
|
|
27
|
-
Requires-Dist: pytest>=6.2.0; extra == "test"
|
|
28
|
-
Requires-Dist: coverage; extra == "test"
|
|
29
|
-
Requires-Dist: pytest-cov; extra == "test"
|
|
30
|
-
Requires-Dist: jsonschema>=3.2.0; extra == "test"
|
|
31
|
-
Requires-Dist: pyrsistent>=0.16.0; extra == "test"
|
|
32
|
-
Requires-Dist: requests>=2.21; extra == "test"
|
|
33
|
-
Requires-Dist: rpds-py<0.20.0; python_version < "3.11" and extra == "test"
|
|
34
|
-
Requires-Dist: python-coveralls; extra == "test"
|
|
35
27
|
Requires-Dist: funcsigs>=0.4; extra == "test"
|
|
36
28
|
Requires-Dist: idna>=2.10; extra == "test"
|
|
37
29
|
Requires-Dist: ruff>=0.15.0; extra == "test"
|
|
30
|
+
Requires-Dist: pyrsistent>=0.16.0; extra == "test"
|
|
31
|
+
Requires-Dist: rpds-py<0.20.0; python_version < "3.11" and extra == "test"
|
|
32
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
33
|
+
Requires-Dist: requests>=2.21; extra == "test"
|
|
34
|
+
Requires-Dist: coverage; extra == "test"
|
|
35
|
+
Requires-Dist: python-coveralls; extra == "test"
|
|
36
|
+
Requires-Dist: jsonschema>=3.2.0; extra == "test"
|
|
37
|
+
Requires-Dist: pytest>=6.2.0; extra == "test"
|
|
38
38
|
Dynamic: author
|
|
39
39
|
Dynamic: author-email
|
|
40
40
|
Dynamic: classifier
|
|
@@ -304,6 +304,17 @@ idna (BSD 3-Clause License https://github.com/kjd/idna/blob/master/LICENSE.md)
|
|
|
304
304
|
- Swift - https://github.com/optimizely/swift-sdk
|
|
305
305
|
# Optimizely Python SDK Changelog
|
|
306
306
|
|
|
307
|
+
## 5.7.0
|
|
308
|
+
September 2nd, 2026
|
|
309
|
+
|
|
310
|
+
### New Features
|
|
311
|
+
|
|
312
|
+
**Holdout Targeted Delivery Exclusion**: Holdouts can now be configured to exclude Targeted Delivery rules, so users held out of experiments still receive their Targeted Delivery experiences. This gives you a cleaner measure of experiment impact without holding users back from ongoing rollouts.
|
|
313
|
+
|
|
314
|
+
- Add holdout exclusion logic for Targeted Delivery rules ([#522](https://github.com/optimizely/python-sdk/pull/522))
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
307
318
|
## 5.6.0
|
|
308
319
|
July 10th, 2026
|
|
309
320
|
|
|
@@ -64,15 +64,11 @@ class VariationResult(TypedDict):
|
|
|
64
64
|
variation: Optional[Union[entities.Variation, VariationDict]]
|
|
65
65
|
|
|
66
66
|
|
|
67
|
-
class
|
|
68
|
-
|
|
69
|
-
A TypedDict representing the result of a decision process.
|
|
67
|
+
class _DecisionResultOptional(TypedDict, total=False):
|
|
68
|
+
holdout_decision: Decision
|
|
70
69
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
error (bool): Indicates whether an error occurred during the decision process.
|
|
74
|
-
reasons (List[str]): A list of reasons explaining the decision or any errors encountered.
|
|
75
|
-
"""
|
|
70
|
+
|
|
71
|
+
class DecisionResult(_DecisionResultOptional):
|
|
76
72
|
decision: Decision
|
|
77
73
|
error: bool
|
|
78
74
|
reasons: List[str]
|
|
@@ -610,7 +606,6 @@ class DecisionService:
|
|
|
610
606
|
return Decision(experiment=rule, variation=forced_decision_variation,
|
|
611
607
|
source=enums.DecisionSources.ROLLOUT, cmab_uuid=None), decide_reasons
|
|
612
608
|
|
|
613
|
-
# Check local holdouts targeting this specific delivery rule (FSSDK-12369)
|
|
614
609
|
local_holdouts = project_config.get_holdouts_for_rule(rule.id)
|
|
615
610
|
for holdout in local_holdouts:
|
|
616
611
|
local_holdout_decision = self.get_variation_for_holdout(
|
|
@@ -751,6 +746,9 @@ class DecisionService:
|
|
|
751
746
|
reasons = decide_reasons.copy() if decide_reasons else []
|
|
752
747
|
user_id = user_context.user_id
|
|
753
748
|
|
|
749
|
+
global_holdout_result: DecisionResult | None = None
|
|
750
|
+
global_holdout_key: str | None = None
|
|
751
|
+
|
|
754
752
|
# Check global holdouts (flag level — before any rules are evaluated)
|
|
755
753
|
global_holdouts = project_config.get_global_holdouts()
|
|
756
754
|
for holdout in global_holdouts:
|
|
@@ -758,7 +756,6 @@ class DecisionService:
|
|
|
758
756
|
reasons.extend(holdout_decision['reasons'])
|
|
759
757
|
|
|
760
758
|
decision = holdout_decision['decision']
|
|
761
|
-
# Check if user was bucketed into holdout (has a variation)
|
|
762
759
|
if decision.variation is None:
|
|
763
760
|
continue
|
|
764
761
|
|
|
@@ -768,18 +765,33 @@ class DecisionService:
|
|
|
768
765
|
)
|
|
769
766
|
self.logger.info(message)
|
|
770
767
|
reasons.append(message)
|
|
771
|
-
return {
|
|
772
|
-
'decision': holdout_decision['decision'],
|
|
773
|
-
'error': False,
|
|
774
|
-
'reasons': reasons
|
|
775
|
-
}
|
|
776
768
|
|
|
777
|
-
|
|
769
|
+
if not holdout.exclude_targeted_deliveries:
|
|
770
|
+
return {
|
|
771
|
+
'decision': holdout_decision['decision'],
|
|
772
|
+
'error': False,
|
|
773
|
+
'reasons': reasons
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
message = (
|
|
777
|
+
f"Holdout '{holdout.key}' excludes targeted deliveries. "
|
|
778
|
+
f"Targeted delivery rules will be evaluated."
|
|
779
|
+
)
|
|
780
|
+
self.logger.info(message)
|
|
781
|
+
reasons.append(message)
|
|
782
|
+
global_holdout_result = holdout_decision
|
|
783
|
+
global_holdout_key = holdout.key
|
|
784
|
+
break
|
|
785
|
+
|
|
786
|
+
# Check experiments then rollouts
|
|
778
787
|
if feature_flag.experimentIds:
|
|
779
788
|
for experiment_id in feature_flag.experimentIds:
|
|
780
789
|
experiment = project_config.get_experiment_from_id(experiment_id)
|
|
781
790
|
|
|
782
791
|
if experiment:
|
|
792
|
+
if global_holdout_result is not None and experiment.type != enums.ExperimentTypes.td:
|
|
793
|
+
continue
|
|
794
|
+
|
|
783
795
|
# Check for forced decision
|
|
784
796
|
optimizely_decision_context = OptimizelyUserContext.OptimizelyDecisionContext(
|
|
785
797
|
feature_flag.key, experiment.key)
|
|
@@ -790,13 +802,15 @@ class DecisionService:
|
|
|
790
802
|
if forced_decision_variation:
|
|
791
803
|
decision = Decision(experiment, forced_decision_variation,
|
|
792
804
|
enums.DecisionSources.FEATURE_TEST, None)
|
|
793
|
-
|
|
805
|
+
result: DecisionResult = {
|
|
794
806
|
'decision': decision,
|
|
795
807
|
'error': False,
|
|
796
808
|
'reasons': reasons
|
|
797
809
|
}
|
|
810
|
+
if global_holdout_result is not None:
|
|
811
|
+
result['holdout_decision'] = global_holdout_result['decision']
|
|
812
|
+
return result
|
|
798
813
|
|
|
799
|
-
# Check local holdouts targeting this specific experiment rule (FSSDK-12369)
|
|
800
814
|
local_holdouts = project_config.get_holdouts_for_rule(experiment.id)
|
|
801
815
|
for holdout in local_holdouts:
|
|
802
816
|
local_holdout_decision = self.get_variation_for_holdout(
|
|
@@ -837,11 +851,22 @@ class DecisionService:
|
|
|
837
851
|
decision = Decision(experiment, variation_result['variation'],
|
|
838
852
|
enums.DecisionSources.FEATURE_TEST,
|
|
839
853
|
variation_result['cmab_uuid'])
|
|
840
|
-
|
|
854
|
+
result = {
|
|
841
855
|
'decision': decision,
|
|
842
856
|
'error': False,
|
|
843
857
|
'reasons': reasons
|
|
844
858
|
}
|
|
859
|
+
if global_holdout_result is not None:
|
|
860
|
+
result['holdout_decision'] = global_holdout_result['decision']
|
|
861
|
+
return result
|
|
862
|
+
|
|
863
|
+
if global_holdout_result is not None:
|
|
864
|
+
message = (
|
|
865
|
+
f"Holdout \"{global_holdout_key}\" has excludeTargetedDeliveries enabled, "
|
|
866
|
+
f"continuing to rollout evaluation."
|
|
867
|
+
)
|
|
868
|
+
self.logger.info(message)
|
|
869
|
+
reasons.append(message)
|
|
845
870
|
|
|
846
871
|
# If no experiment decision, check rollouts
|
|
847
872
|
rollout_decision, rollout_reasons = self.get_variation_for_rollout(
|
|
@@ -863,11 +888,14 @@ class DecisionService:
|
|
|
863
888
|
else:
|
|
864
889
|
self.logger.debug(f'User "{user_id}" not bucketed into any rollout for feature "{feature_flag.key}".')
|
|
865
890
|
|
|
866
|
-
|
|
891
|
+
final_result: DecisionResult = {
|
|
867
892
|
'decision': rollout_decision,
|
|
868
893
|
'error': False,
|
|
869
894
|
'reasons': reasons
|
|
870
895
|
}
|
|
896
|
+
if global_holdout_result is not None:
|
|
897
|
+
final_result['holdout_decision'] = global_holdout_result['decision']
|
|
898
|
+
return final_result
|
|
871
899
|
|
|
872
900
|
def get_variation_for_holdout(
|
|
873
901
|
self,
|
|
@@ -224,6 +224,7 @@ class Holdout(BaseEntity):
|
|
|
224
224
|
audienceIds: list[str],
|
|
225
225
|
audienceConditions: Optional[Sequence[str | list[str]]] = None,
|
|
226
226
|
includedRules: Optional[list[str]] = None,
|
|
227
|
+
excludeTargetedDeliveries: bool = False,
|
|
227
228
|
**kwargs: Any
|
|
228
229
|
):
|
|
229
230
|
self.id = id
|
|
@@ -236,6 +237,7 @@ class Holdout(BaseEntity):
|
|
|
236
237
|
# Per-rule targeting for local holdouts. Scope comes from the datafile
|
|
237
238
|
# section, not this field; ProjectConfig strips it on 'holdouts' entries.
|
|
238
239
|
self.included_rules: Optional[list[str]] = includedRules
|
|
240
|
+
self.exclude_targeted_deliveries: bool = excludeTargetedDeliveries
|
|
239
241
|
|
|
240
242
|
def get_audience_conditions_or_ids(self) -> Sequence[str | list[str]]:
|
|
241
243
|
"""Returns audienceConditions if present, otherwise audienceIds.
|
|
@@ -1245,7 +1245,8 @@ class Optimizely:
|
|
|
1245
1245
|
flag_decision: Decision,
|
|
1246
1246
|
decision_reasons: Optional[list[str]],
|
|
1247
1247
|
decide_options: list[str],
|
|
1248
|
-
project_config: ProjectConfig
|
|
1248
|
+
project_config: ProjectConfig,
|
|
1249
|
+
holdout_decision: Optional[Decision] = None
|
|
1249
1250
|
) -> OptimizelyDecision:
|
|
1250
1251
|
user_id = user_context.user_id
|
|
1251
1252
|
feature_enabled = False
|
|
@@ -1264,6 +1265,26 @@ class Optimizely:
|
|
|
1264
1265
|
|
|
1265
1266
|
feature_flag = project_config.feature_key_map.get(flag_key)
|
|
1266
1267
|
|
|
1268
|
+
# Send holdout impression when user was bucketed into a holdout bypassed due to exclude_targeted_deliveries
|
|
1269
|
+
if (holdout_decision is not None
|
|
1270
|
+
and decision_source != DecisionSources.HOLDOUT
|
|
1271
|
+
and OptimizelyDecideOption.DISABLE_DECISION_EVENT not in decide_options):
|
|
1272
|
+
holdout_enabled = self._get_feature_enabled(holdout_decision.variation)
|
|
1273
|
+
holdout_rule_key = holdout_decision.experiment.key if holdout_decision.experiment else ''
|
|
1274
|
+
self._send_impression_event(
|
|
1275
|
+
project_config,
|
|
1276
|
+
holdout_decision.experiment,
|
|
1277
|
+
holdout_decision.variation,
|
|
1278
|
+
flag_key,
|
|
1279
|
+
holdout_rule_key,
|
|
1280
|
+
str(DecisionSources.HOLDOUT),
|
|
1281
|
+
holdout_enabled,
|
|
1282
|
+
user_id,
|
|
1283
|
+
attributes,
|
|
1284
|
+
holdout_decision.cmab_uuid
|
|
1285
|
+
)
|
|
1286
|
+
decision_event_dispatched = True
|
|
1287
|
+
|
|
1267
1288
|
# Send impression event if Decision came from a feature
|
|
1268
1289
|
if OptimizelyDecideOption.DISABLE_DECISION_EVENT not in decide_options:
|
|
1269
1290
|
if (decision_source == DecisionSources.FEATURE_TEST or
|
|
@@ -1448,11 +1469,13 @@ class Optimizely:
|
|
|
1448
1469
|
user_context,
|
|
1449
1470
|
merged_decide_options
|
|
1450
1471
|
)
|
|
1472
|
+
holdout_decisions: dict[str, Optional[Decision]] = {}
|
|
1451
1473
|
for i in range(0, len(flags_without_forced_decision)):
|
|
1452
1474
|
decision = decision_list[i]['decision']
|
|
1453
1475
|
reasons = decision_list[i]['reasons']
|
|
1454
1476
|
error = decision_list[i]['error']
|
|
1455
1477
|
flag_key = flags_without_forced_decision[i].key
|
|
1478
|
+
holdout_decisions[flag_key] = decision_list[i].get('holdout_decision')
|
|
1456
1479
|
# store error decision against key and remove key from valid keys
|
|
1457
1480
|
if error:
|
|
1458
1481
|
optimizely_decision = OptimizelyDecision.new_error_decision(flags_without_forced_decision[i].key,
|
|
@@ -1472,7 +1495,8 @@ class Optimizely:
|
|
|
1472
1495
|
flag_decision,
|
|
1473
1496
|
decision_reasons,
|
|
1474
1497
|
merged_decide_options,
|
|
1475
|
-
project_config
|
|
1498
|
+
project_config,
|
|
1499
|
+
holdout_decision=holdout_decisions.get(key)
|
|
1476
1500
|
)
|
|
1477
1501
|
enabled_flags_only_missing = OptimizelyDecideOption.ENABLED_FLAGS_ONLY not in merged_decide_options
|
|
1478
1502
|
is_enabled = optimizely_decision.enabled
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: optimizely-sdk
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.7.0
|
|
4
4
|
Summary: Python SDK for Optimizely Feature Experimentation, Optimizely Full Stack (legacy), and Optimizely Rollouts.
|
|
5
5
|
Home-page: https://github.com/optimizely/python-sdk
|
|
6
6
|
Author: Optimizely
|
|
@@ -24,17 +24,17 @@ Requires-Dist: requests>=2.21
|
|
|
24
24
|
Requires-Dist: idna>=2.10
|
|
25
25
|
Requires-Dist: rpds-py<0.20.0; python_version < "3.11"
|
|
26
26
|
Provides-Extra: test
|
|
27
|
-
Requires-Dist: pytest>=6.2.0; extra == "test"
|
|
28
|
-
Requires-Dist: coverage; extra == "test"
|
|
29
|
-
Requires-Dist: pytest-cov; extra == "test"
|
|
30
|
-
Requires-Dist: jsonschema>=3.2.0; extra == "test"
|
|
31
|
-
Requires-Dist: pyrsistent>=0.16.0; extra == "test"
|
|
32
|
-
Requires-Dist: requests>=2.21; extra == "test"
|
|
33
|
-
Requires-Dist: rpds-py<0.20.0; python_version < "3.11" and extra == "test"
|
|
34
|
-
Requires-Dist: python-coveralls; extra == "test"
|
|
35
27
|
Requires-Dist: funcsigs>=0.4; extra == "test"
|
|
36
28
|
Requires-Dist: idna>=2.10; extra == "test"
|
|
37
29
|
Requires-Dist: ruff>=0.15.0; extra == "test"
|
|
30
|
+
Requires-Dist: pyrsistent>=0.16.0; extra == "test"
|
|
31
|
+
Requires-Dist: rpds-py<0.20.0; python_version < "3.11" and extra == "test"
|
|
32
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
33
|
+
Requires-Dist: requests>=2.21; extra == "test"
|
|
34
|
+
Requires-Dist: coverage; extra == "test"
|
|
35
|
+
Requires-Dist: python-coveralls; extra == "test"
|
|
36
|
+
Requires-Dist: jsonschema>=3.2.0; extra == "test"
|
|
37
|
+
Requires-Dist: pytest>=6.2.0; extra == "test"
|
|
38
38
|
Dynamic: author
|
|
39
39
|
Dynamic: author-email
|
|
40
40
|
Dynamic: classifier
|
|
@@ -304,6 +304,17 @@ idna (BSD 3-Clause License https://github.com/kjd/idna/blob/master/LICENSE.md)
|
|
|
304
304
|
- Swift - https://github.com/optimizely/swift-sdk
|
|
305
305
|
# Optimizely Python SDK Changelog
|
|
306
306
|
|
|
307
|
+
## 5.7.0
|
|
308
|
+
September 2nd, 2026
|
|
309
|
+
|
|
310
|
+
### New Features
|
|
311
|
+
|
|
312
|
+
**Holdout Targeted Delivery Exclusion**: Holdouts can now be configured to exclude Targeted Delivery rules, so users held out of experiments still receive their Targeted Delivery experiences. This gives you a cleaner measure of experiment impact without holding users back from ongoing rollouts.
|
|
313
|
+
|
|
314
|
+
- Add holdout exclusion logic for Targeted Delivery rules ([#522](https://github.com/optimizely/python-sdk/pull/522))
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
307
318
|
## 5.6.0
|
|
308
319
|
July 10th, 2026
|
|
309
320
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/decision/optimizely_decide_option.py
RENAMED
|
File without changes
|
|
File without changes
|
{optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/decision/optimizely_decision_message.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/condition_tree_evaluator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -7,16 +7,16 @@ idna>=2.10
|
|
|
7
7
|
rpds-py<0.20.0
|
|
8
8
|
|
|
9
9
|
[test]
|
|
10
|
-
pytest>=6.2.0
|
|
11
|
-
coverage
|
|
12
|
-
pytest-cov
|
|
13
|
-
jsonschema>=3.2.0
|
|
14
|
-
pyrsistent>=0.16.0
|
|
15
|
-
requests>=2.21
|
|
16
|
-
python-coveralls
|
|
17
10
|
funcsigs>=0.4
|
|
18
11
|
idna>=2.10
|
|
19
12
|
ruff>=0.15.0
|
|
13
|
+
pyrsistent>=0.16.0
|
|
14
|
+
pytest-cov
|
|
15
|
+
requests>=2.21
|
|
16
|
+
coverage
|
|
17
|
+
python-coveralls
|
|
18
|
+
jsonschema>=3.2.0
|
|
19
|
+
pytest>=6.2.0
|
|
20
20
|
|
|
21
21
|
[test:python_version < "3.11"]
|
|
22
22
|
rpds-py<0.20.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|