optimizely-sdk 5.6.0__tar.gz → 5.8.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.8.0}/CHANGELOG.md +27 -0
- {optimizely_sdk-5.6.0/optimizely_sdk.egg-info → optimizely_sdk-5.8.0}/PKG-INFO +34 -7
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/bucketer.py +1 -8
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/decision/optimizely_decide_option.py +1 -6
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/decision/optimizely_decision_message.py +1 -6
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/decision_service.py +98 -60
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/entities.py +3 -7
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/event/event_factory.py +1 -7
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/event/event_processor.py +1 -8
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/event/log_event.py +1 -8
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/event/user_event.py +1 -8
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/event_builder.py +1 -7
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/event_dispatcher.py +1 -6
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/helpers/condition.py +1 -8
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/helpers/enums.py +1 -6
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/helpers/event_tag_utils.py +1 -7
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/helpers/types.py +1 -8
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/logger.py +1 -7
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/notification_center.py +1 -7
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/odp/lru_cache.py +1 -7
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/odp/optimizely_odp_option.py +1 -6
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/optimizely.py +28 -3
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/project_config.py +1 -7
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/user_profile.py +5 -11
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/version.py +1 -1
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0/optimizely_sdk.egg-info}/PKG-INFO +34 -7
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/LICENSE +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/MANIFEST.in +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/README.md +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/__init__.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/cmab/__init__.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/cmab/cmab_client.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/cmab/cmab_service.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/config_manager.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/decision/__init__.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/decision/optimizely_decision.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/error_handler.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/event/__init__.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/event/event_id_normalizer.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/event/payload.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/event/user_event_factory.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/exceptions.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/helpers/__init__.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/helpers/audience.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/helpers/condition_tree_evaluator.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/helpers/constants.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/helpers/experiment.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/helpers/sdk_settings.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/helpers/validator.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/lib/__init__.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/lib/pymmh3.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/notification_center_registry.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/odp/__init__.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/odp/odp_config.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/odp/odp_event.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/odp/odp_event_api_manager.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/odp/odp_event_manager.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/odp/odp_manager.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/odp/odp_segment_api_manager.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/odp/odp_segment_manager.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/optimizely_config.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/optimizely_factory.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/optimizely_user_context.py +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/py.typed +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely_sdk.egg-info/SOURCES.txt +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely_sdk.egg-info/dependency_links.txt +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely_sdk.egg-info/requires.txt +6 -6
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely_sdk.egg-info/top_level.txt +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/requirements/core.txt +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/requirements/docs.txt +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/requirements/test.txt +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/requirements/typing.txt +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/setup.cfg +0 -0
- {optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/setup.py +0 -0
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Optimizely Python SDK Changelog
|
|
2
2
|
|
|
3
|
+
## 5.8.0
|
|
4
|
+
October 30th, 2026
|
|
5
|
+
|
|
6
|
+
### Changed
|
|
7
|
+
|
|
8
|
+
**Local Holdouts: rule evaluation continues after a holdout hit**:
|
|
9
|
+
When a user matches a local holdout on a rule, the SDK now excludes the user from that rule only
|
|
10
|
+
and continues evaluating the remaining rules, instead of returning the holdout variation and skipping
|
|
11
|
+
the rest of the flag. Each local holdout hit sends its own holdout impression event (using the
|
|
12
|
+
existing event format), regardless of `sendFlagDecisions`. Global holdouts are unchanged.
|
|
13
|
+
|
|
14
|
+
- Continue rule evaluation after local holdout hit ([#525](https://github.com/optimizely/python-sdk/pull/525))
|
|
15
|
+
- Remove dead Python 3.7 typing guards ([#526](https://github.com/optimizely/python-sdk/pull/526))
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 5.7.0
|
|
20
|
+
September 2nd, 2026
|
|
21
|
+
|
|
22
|
+
### New Features
|
|
23
|
+
|
|
24
|
+
**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.
|
|
25
|
+
|
|
26
|
+
- Add holdout exclusion logic for Targeted Delivery rules ([#522](https://github.com/optimizely/python-sdk/pull/522))
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
3
30
|
## 5.6.0
|
|
4
31
|
July 10th, 2026
|
|
5
32
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: optimizely-sdk
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.8.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: funcsigs>=0.4; extra == "test"
|
|
28
|
+
Requires-Dist: python-coveralls; extra == "test"
|
|
29
|
+
Requires-Dist: requests>=2.21; extra == "test"
|
|
27
30
|
Requires-Dist: pytest>=6.2.0; extra == "test"
|
|
28
|
-
Requires-Dist: coverage; extra == "test"
|
|
29
|
-
Requires-Dist: pytest-cov; extra == "test"
|
|
30
31
|
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
32
|
Requires-Dist: rpds-py<0.20.0; python_version < "3.11" and extra == "test"
|
|
34
|
-
Requires-Dist: python-coveralls; extra == "test"
|
|
35
|
-
Requires-Dist: funcsigs>=0.4; extra == "test"
|
|
36
33
|
Requires-Dist: idna>=2.10; extra == "test"
|
|
34
|
+
Requires-Dist: coverage; extra == "test"
|
|
35
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
37
36
|
Requires-Dist: ruff>=0.15.0; extra == "test"
|
|
37
|
+
Requires-Dist: pyrsistent>=0.16.0; extra == "test"
|
|
38
38
|
Dynamic: author
|
|
39
39
|
Dynamic: author-email
|
|
40
40
|
Dynamic: classifier
|
|
@@ -304,6 +304,33 @@ 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.8.0
|
|
308
|
+
October 30th, 2026
|
|
309
|
+
|
|
310
|
+
### Changed
|
|
311
|
+
|
|
312
|
+
**Local Holdouts: rule evaluation continues after a holdout hit**:
|
|
313
|
+
When a user matches a local holdout on a rule, the SDK now excludes the user from that rule only
|
|
314
|
+
and continues evaluating the remaining rules, instead of returning the holdout variation and skipping
|
|
315
|
+
the rest of the flag. Each local holdout hit sends its own holdout impression event (using the
|
|
316
|
+
existing event format), regardless of `sendFlagDecisions`. Global holdouts are unchanged.
|
|
317
|
+
|
|
318
|
+
- Continue rule evaluation after local holdout hit ([#525](https://github.com/optimizely/python-sdk/pull/525))
|
|
319
|
+
- Remove dead Python 3.7 typing guards ([#526](https://github.com/optimizely/python-sdk/pull/526))
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## 5.7.0
|
|
324
|
+
September 2nd, 2026
|
|
325
|
+
|
|
326
|
+
### New Features
|
|
327
|
+
|
|
328
|
+
**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.
|
|
329
|
+
|
|
330
|
+
- Add holdout exclusion logic for Targeted Delivery rules ([#522](https://github.com/optimizely/python-sdk/pull/522))
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
307
334
|
## 5.6.0
|
|
308
335
|
July 10th, 2026
|
|
309
336
|
|
|
@@ -12,19 +12,12 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
|
-
from typing import Optional, TYPE_CHECKING, cast
|
|
15
|
+
from typing import Optional, TYPE_CHECKING, cast, Final
|
|
16
16
|
import math
|
|
17
|
-
from sys import version_info
|
|
18
17
|
|
|
19
18
|
from .lib import pymmh3 as mmh3
|
|
20
19
|
|
|
21
20
|
|
|
22
|
-
if version_info < (3, 8):
|
|
23
|
-
from typing_extensions import Final
|
|
24
|
-
else:
|
|
25
|
-
from typing import Final
|
|
26
|
-
|
|
27
|
-
|
|
28
21
|
if TYPE_CHECKING:
|
|
29
22
|
# prevent circular dependenacy by skipping import at runtime
|
|
30
23
|
from .project_config import ProjectConfig
|
{optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/decision/optimizely_decide_option.py
RENAMED
|
@@ -11,12 +11,7 @@
|
|
|
11
11
|
# See the License for the specific language governing permissions and
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
|
|
14
|
-
from
|
|
15
|
-
|
|
16
|
-
if version_info < (3, 8):
|
|
17
|
-
from typing_extensions import Final
|
|
18
|
-
else:
|
|
19
|
-
from typing import Final
|
|
14
|
+
from typing import Final
|
|
20
15
|
|
|
21
16
|
|
|
22
17
|
class OptimizelyDecideOption:
|
{optimizely_sdk-5.6.0 → optimizely_sdk-5.8.0}/optimizely/decision/optimizely_decision_message.py
RENAMED
|
@@ -11,12 +11,7 @@
|
|
|
11
11
|
# See the License for the specific language governing permissions and
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
|
|
14
|
-
from
|
|
15
|
-
|
|
16
|
-
if version_info < (3, 8):
|
|
17
|
-
from typing_extensions import Final
|
|
18
|
-
else:
|
|
19
|
-
from typing import Final
|
|
14
|
+
from typing import Final
|
|
20
15
|
|
|
21
16
|
|
|
22
17
|
class OptimizelyDecisionMessage:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2017-2022, Optimizely
|
|
1
|
+
# Copyright 2017-2022, 2026, Optimizely
|
|
2
2
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
3
|
# you may not use this file except in compliance with the License.
|
|
4
4
|
# You may obtain a copy of the License at
|
|
@@ -64,15 +64,12 @@ class VariationResult(TypedDict):
|
|
|
64
64
|
variation: Optional[Union[entities.Variation, VariationDict]]
|
|
65
65
|
|
|
66
66
|
|
|
67
|
-
class
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
class _DecisionResultOptional(TypedDict, total=False):
|
|
68
|
+
holdout_decision: Decision # Deprecated: first entry of holdout_decisions.
|
|
69
|
+
holdout_decisions: List[Decision]
|
|
70
70
|
|
|
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
|
-
"""
|
|
71
|
+
|
|
72
|
+
class DecisionResult(_DecisionResultOptional):
|
|
76
73
|
decision: Decision
|
|
77
74
|
error: bool
|
|
78
75
|
reasons: List[str]
|
|
@@ -556,7 +553,11 @@ class DecisionService:
|
|
|
556
553
|
}
|
|
557
554
|
|
|
558
555
|
def get_variation_for_rollout(
|
|
559
|
-
self,
|
|
556
|
+
self,
|
|
557
|
+
project_config: ProjectConfig,
|
|
558
|
+
feature: entities.FeatureFlag,
|
|
559
|
+
user_context: OptimizelyUserContext,
|
|
560
|
+
holdout_decisions: Optional[list[Decision]] = None
|
|
560
561
|
) -> tuple[Decision, list[str]]:
|
|
561
562
|
""" Determine which experiment/variation the user is in for a given rollout.
|
|
562
563
|
Returns the variation of the first experiment the user qualifies for.
|
|
@@ -566,7 +567,7 @@ class DecisionService:
|
|
|
566
567
|
flagKey: Feature key.
|
|
567
568
|
rollout: Rollout for which we are getting the variation.
|
|
568
569
|
user: ID and attributes for user.
|
|
569
|
-
|
|
570
|
+
holdout_decisions: If provided, local holdout decisions for skipped rules are appended to it.
|
|
570
571
|
|
|
571
572
|
Returns:
|
|
572
573
|
Decision namedtuple consisting of experiment and variation for the user and
|
|
@@ -610,23 +611,14 @@ class DecisionService:
|
|
|
610
611
|
return Decision(experiment=rule, variation=forced_decision_variation,
|
|
611
612
|
source=enums.DecisionSources.ROLLOUT, cmab_uuid=None), decide_reasons
|
|
612
613
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
local_decision = local_holdout_decision['decision']
|
|
622
|
-
if local_decision.variation is not None:
|
|
623
|
-
message = (
|
|
624
|
-
f"The user '{user_id}' is bucketed into local holdout '{holdout.key}' "
|
|
625
|
-
f"for delivery rule '{rule.key}'."
|
|
626
|
-
)
|
|
627
|
-
self.logger.info(message)
|
|
628
|
-
decide_reasons.append(message)
|
|
629
|
-
return local_decision, decide_reasons
|
|
614
|
+
local_holdout = self._get_local_holdout_decision(
|
|
615
|
+
project_config, rule, user_context, 'delivery', decide_reasons
|
|
616
|
+
)
|
|
617
|
+
if local_holdout is not None:
|
|
618
|
+
if holdout_decisions is not None:
|
|
619
|
+
holdout_decisions.append(local_holdout)
|
|
620
|
+
index += 1
|
|
621
|
+
continue
|
|
630
622
|
|
|
631
623
|
bucketing_id, bucket_reasons = self._get_bucketing_id(user_id, attributes)
|
|
632
624
|
decide_reasons += bucket_reasons
|
|
@@ -751,6 +743,10 @@ class DecisionService:
|
|
|
751
743
|
reasons = decide_reasons.copy() if decide_reasons else []
|
|
752
744
|
user_id = user_context.user_id
|
|
753
745
|
|
|
746
|
+
global_holdout_result: DecisionResult | None = None
|
|
747
|
+
global_holdout_key: str | None = None
|
|
748
|
+
holdout_decisions: list[Decision] = []
|
|
749
|
+
|
|
754
750
|
# Check global holdouts (flag level — before any rules are evaluated)
|
|
755
751
|
global_holdouts = project_config.get_global_holdouts()
|
|
756
752
|
for holdout in global_holdouts:
|
|
@@ -758,7 +754,6 @@ class DecisionService:
|
|
|
758
754
|
reasons.extend(holdout_decision['reasons'])
|
|
759
755
|
|
|
760
756
|
decision = holdout_decision['decision']
|
|
761
|
-
# Check if user was bucketed into holdout (has a variation)
|
|
762
757
|
if decision.variation is None:
|
|
763
758
|
continue
|
|
764
759
|
|
|
@@ -768,18 +763,34 @@ class DecisionService:
|
|
|
768
763
|
)
|
|
769
764
|
self.logger.info(message)
|
|
770
765
|
reasons.append(message)
|
|
771
|
-
return {
|
|
772
|
-
'decision': holdout_decision['decision'],
|
|
773
|
-
'error': False,
|
|
774
|
-
'reasons': reasons
|
|
775
|
-
}
|
|
776
766
|
|
|
777
|
-
|
|
767
|
+
if not holdout.exclude_targeted_deliveries:
|
|
768
|
+
return {
|
|
769
|
+
'decision': holdout_decision['decision'],
|
|
770
|
+
'error': False,
|
|
771
|
+
'reasons': reasons
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
message = (
|
|
775
|
+
f"Holdout '{holdout.key}' excludes targeted deliveries. "
|
|
776
|
+
f"Targeted delivery rules will be evaluated."
|
|
777
|
+
)
|
|
778
|
+
self.logger.info(message)
|
|
779
|
+
reasons.append(message)
|
|
780
|
+
global_holdout_result = holdout_decision
|
|
781
|
+
global_holdout_key = holdout.key
|
|
782
|
+
holdout_decisions.append(holdout_decision['decision'])
|
|
783
|
+
break
|
|
784
|
+
|
|
785
|
+
# Check experiments then rollouts
|
|
778
786
|
if feature_flag.experimentIds:
|
|
779
787
|
for experiment_id in feature_flag.experimentIds:
|
|
780
788
|
experiment = project_config.get_experiment_from_id(experiment_id)
|
|
781
789
|
|
|
782
790
|
if experiment:
|
|
791
|
+
if global_holdout_result is not None and experiment.type != enums.ExperimentTypes.td:
|
|
792
|
+
continue
|
|
793
|
+
|
|
783
794
|
# Check for forced decision
|
|
784
795
|
optimizely_decision_context = OptimizelyUserContext.OptimizelyDecisionContext(
|
|
785
796
|
feature_flag.key, experiment.key)
|
|
@@ -790,33 +801,22 @@ class DecisionService:
|
|
|
790
801
|
if forced_decision_variation:
|
|
791
802
|
decision = Decision(experiment, forced_decision_variation,
|
|
792
803
|
enums.DecisionSources.FEATURE_TEST, None)
|
|
793
|
-
|
|
804
|
+
result: DecisionResult = {
|
|
794
805
|
'decision': decision,
|
|
795
806
|
'error': False,
|
|
796
807
|
'reasons': reasons
|
|
797
808
|
}
|
|
809
|
+
if holdout_decisions:
|
|
810
|
+
result['holdout_decisions'] = holdout_decisions
|
|
811
|
+
result['holdout_decision'] = holdout_decisions[0]
|
|
812
|
+
return result
|
|
798
813
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
reasons.extend(local_holdout_decision['reasons'])
|
|
806
|
-
|
|
807
|
-
local_decision = local_holdout_decision['decision']
|
|
808
|
-
if local_decision.variation is not None:
|
|
809
|
-
message = (
|
|
810
|
-
f"The user '{user_id}' is bucketed into local holdout '{holdout.key}' "
|
|
811
|
-
f"for experiment rule '{experiment.key}'."
|
|
812
|
-
)
|
|
813
|
-
self.logger.info(message)
|
|
814
|
-
reasons.append(message)
|
|
815
|
-
return {
|
|
816
|
-
'decision': local_holdout_decision['decision'],
|
|
817
|
-
'error': False,
|
|
818
|
-
'reasons': reasons
|
|
819
|
-
}
|
|
814
|
+
local_holdout = self._get_local_holdout_decision(
|
|
815
|
+
project_config, experiment, user_context, 'experiment', reasons
|
|
816
|
+
)
|
|
817
|
+
if local_holdout is not None:
|
|
818
|
+
holdout_decisions.append(local_holdout)
|
|
819
|
+
continue
|
|
820
820
|
|
|
821
821
|
# Get variation for experiment
|
|
822
822
|
variation_result = self.get_variation(
|
|
@@ -837,15 +837,27 @@ class DecisionService:
|
|
|
837
837
|
decision = Decision(experiment, variation_result['variation'],
|
|
838
838
|
enums.DecisionSources.FEATURE_TEST,
|
|
839
839
|
variation_result['cmab_uuid'])
|
|
840
|
-
|
|
840
|
+
result = {
|
|
841
841
|
'decision': decision,
|
|
842
842
|
'error': False,
|
|
843
843
|
'reasons': reasons
|
|
844
844
|
}
|
|
845
|
+
if holdout_decisions:
|
|
846
|
+
result['holdout_decisions'] = holdout_decisions
|
|
847
|
+
result['holdout_decision'] = holdout_decisions[0]
|
|
848
|
+
return result
|
|
849
|
+
|
|
850
|
+
if global_holdout_result is not None:
|
|
851
|
+
message = (
|
|
852
|
+
f"Holdout \"{global_holdout_key}\" has excludeTargetedDeliveries enabled, "
|
|
853
|
+
f"continuing to rollout evaluation."
|
|
854
|
+
)
|
|
855
|
+
self.logger.info(message)
|
|
856
|
+
reasons.append(message)
|
|
845
857
|
|
|
846
858
|
# If no experiment decision, check rollouts
|
|
847
859
|
rollout_decision, rollout_reasons = self.get_variation_for_rollout(
|
|
848
|
-
project_config, feature_flag, user_context
|
|
860
|
+
project_config, feature_flag, user_context, holdout_decisions
|
|
849
861
|
)
|
|
850
862
|
if rollout_reasons:
|
|
851
863
|
reasons.extend(rollout_reasons)
|
|
@@ -863,11 +875,37 @@ class DecisionService:
|
|
|
863
875
|
else:
|
|
864
876
|
self.logger.debug(f'User "{user_id}" not bucketed into any rollout for feature "{feature_flag.key}".')
|
|
865
877
|
|
|
866
|
-
|
|
878
|
+
final_result: DecisionResult = {
|
|
867
879
|
'decision': rollout_decision,
|
|
868
880
|
'error': False,
|
|
869
881
|
'reasons': reasons
|
|
870
882
|
}
|
|
883
|
+
if holdout_decisions:
|
|
884
|
+
final_result['holdout_decisions'] = holdout_decisions
|
|
885
|
+
final_result['holdout_decision'] = holdout_decisions[0]
|
|
886
|
+
return final_result
|
|
887
|
+
|
|
888
|
+
def _get_local_holdout_decision(
|
|
889
|
+
self,
|
|
890
|
+
project_config: ProjectConfig,
|
|
891
|
+
rule: entities.Experiment,
|
|
892
|
+
user_context: OptimizelyUserContext,
|
|
893
|
+
rule_type: str,
|
|
894
|
+
reasons: list[str]
|
|
895
|
+
) -> Optional[Decision]:
|
|
896
|
+
"""Returns the first local holdout decision the user is bucketed into for the rule, if any."""
|
|
897
|
+
for holdout in project_config.get_holdouts_for_rule(rule.id):
|
|
898
|
+
holdout_result = self.get_variation_for_holdout(holdout, user_context, project_config)
|
|
899
|
+
reasons.extend(holdout_result['reasons'])
|
|
900
|
+
if holdout_result['decision'].variation is not None:
|
|
901
|
+
message = (
|
|
902
|
+
f"The user '{user_context.user_id}' is bucketed into local holdout '{holdout.key}' "
|
|
903
|
+
f"for {rule_type} rule '{rule.key}'. Skipping the rule."
|
|
904
|
+
)
|
|
905
|
+
self.logger.info(message)
|
|
906
|
+
reasons.append(message)
|
|
907
|
+
return holdout_result['decision']
|
|
908
|
+
return None
|
|
871
909
|
|
|
872
910
|
def get_variation_for_holdout(
|
|
873
911
|
self,
|
|
@@ -11,13 +11,7 @@
|
|
|
11
11
|
# See the License for the specific language governing permissions and
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
from __future__ import annotations
|
|
14
|
-
from typing import TYPE_CHECKING, Any, Optional, Sequence
|
|
15
|
-
from sys import version_info
|
|
16
|
-
|
|
17
|
-
if version_info < (3, 8):
|
|
18
|
-
from typing_extensions import Final
|
|
19
|
-
else:
|
|
20
|
-
from typing import Final
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Optional, Sequence, Final
|
|
21
15
|
|
|
22
16
|
|
|
23
17
|
if TYPE_CHECKING:
|
|
@@ -224,6 +218,7 @@ class Holdout(BaseEntity):
|
|
|
224
218
|
audienceIds: list[str],
|
|
225
219
|
audienceConditions: Optional[Sequence[str | list[str]]] = None,
|
|
226
220
|
includedRules: Optional[list[str]] = None,
|
|
221
|
+
excludeTargetedDeliveries: bool = False,
|
|
227
222
|
**kwargs: Any
|
|
228
223
|
):
|
|
229
224
|
self.id = id
|
|
@@ -236,6 +231,7 @@ class Holdout(BaseEntity):
|
|
|
236
231
|
# Per-rule targeting for local holdouts. Scope comes from the datafile
|
|
237
232
|
# section, not this field; ProjectConfig strips it on 'holdouts' entries.
|
|
238
233
|
self.included_rules: Optional[list[str]] = includedRules
|
|
234
|
+
self.exclude_targeted_deliveries: bool = excludeTargetedDeliveries
|
|
239
235
|
|
|
240
236
|
def get_audience_conditions_or_ids(self) -> Sequence[str | list[str]]:
|
|
241
237
|
"""Returns audienceConditions if present, otherwise audienceIds.
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
|
-
from typing import TYPE_CHECKING, Optional, Sequence, cast, List
|
|
16
|
-
from sys import version_info
|
|
15
|
+
from typing import TYPE_CHECKING, Optional, Sequence, cast, List, Final
|
|
17
16
|
from optimizely import entities
|
|
18
17
|
from optimizely.helpers import enums
|
|
19
18
|
from optimizely.helpers import event_tag_utils
|
|
@@ -23,11 +22,6 @@ from . import log_event
|
|
|
23
22
|
from . import payload
|
|
24
23
|
from . import user_event
|
|
25
24
|
|
|
26
|
-
if version_info < (3, 8):
|
|
27
|
-
from typing_extensions import Final
|
|
28
|
-
else:
|
|
29
|
-
from typing import Final
|
|
30
|
-
|
|
31
25
|
if TYPE_CHECKING:
|
|
32
26
|
# prevent circular dependenacy by skipping import at runtime
|
|
33
27
|
from optimizely.project_config import ProjectConfig
|
|
@@ -17,10 +17,9 @@ import numbers
|
|
|
17
17
|
import threading
|
|
18
18
|
import time
|
|
19
19
|
|
|
20
|
-
from typing import Optional
|
|
20
|
+
from typing import Optional, Final
|
|
21
21
|
from datetime import timedelta
|
|
22
22
|
import queue
|
|
23
|
-
from sys import version_info
|
|
24
23
|
|
|
25
24
|
from optimizely import logger as _logging
|
|
26
25
|
from optimizely import notification_center as _notification_center
|
|
@@ -31,12 +30,6 @@ from .event_factory import EventFactory
|
|
|
31
30
|
from .user_event import UserEvent
|
|
32
31
|
|
|
33
32
|
|
|
34
|
-
if version_info < (3, 8):
|
|
35
|
-
from typing_extensions import Final
|
|
36
|
-
else:
|
|
37
|
-
from typing import Final
|
|
38
|
-
|
|
39
|
-
|
|
40
33
|
class BaseEventProcessor(ABC):
|
|
41
34
|
""" Class encapsulating event processing. Override with your own implementation. """
|
|
42
35
|
|
|
@@ -12,17 +12,10 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
|
-
from typing import Optional, Any
|
|
16
|
-
from sys import version_info
|
|
15
|
+
from typing import Optional, Any, Literal
|
|
17
16
|
from optimizely import event_builder
|
|
18
17
|
|
|
19
18
|
|
|
20
|
-
if version_info < (3, 8):
|
|
21
|
-
from typing_extensions import Literal
|
|
22
|
-
else:
|
|
23
|
-
from typing import Literal
|
|
24
|
-
|
|
25
|
-
|
|
26
19
|
class LogEvent(event_builder.Event):
|
|
27
20
|
""" Representation of an event which can be sent to Optimizely events API. """
|
|
28
21
|
|
|
@@ -13,18 +13,11 @@
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
import time
|
|
15
15
|
import uuid
|
|
16
|
-
from typing import TYPE_CHECKING, Optional
|
|
17
|
-
from sys import version_info
|
|
16
|
+
from typing import TYPE_CHECKING, Optional, Final
|
|
18
17
|
|
|
19
18
|
from optimizely import version
|
|
20
19
|
|
|
21
20
|
|
|
22
|
-
if version_info < (3, 8):
|
|
23
|
-
from typing_extensions import Final
|
|
24
|
-
else:
|
|
25
|
-
from typing import Final
|
|
26
|
-
|
|
27
|
-
|
|
28
21
|
if TYPE_CHECKING:
|
|
29
22
|
# prevent circular dependenacy by skipping import at runtime
|
|
30
23
|
from optimizely.entities import Experiment, Variation, Event, Holdout
|
|
@@ -13,9 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
import time
|
|
16
|
-
from typing import TYPE_CHECKING, Any, Optional
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Optional, Final, Literal
|
|
17
17
|
import uuid
|
|
18
|
-
from sys import version_info
|
|
19
18
|
|
|
20
19
|
from . import version
|
|
21
20
|
from .event import event_id_normalizer
|
|
@@ -23,11 +22,6 @@ from .helpers import enums
|
|
|
23
22
|
from .helpers import event_tag_utils
|
|
24
23
|
from .helpers import validator
|
|
25
24
|
|
|
26
|
-
if version_info < (3, 8):
|
|
27
|
-
from typing_extensions import Final, Literal
|
|
28
|
-
else:
|
|
29
|
-
from typing import Final, Literal
|
|
30
|
-
|
|
31
25
|
if TYPE_CHECKING:
|
|
32
26
|
# prevent circular dependenacy by skipping import at runtime
|
|
33
27
|
from .entities import Experiment
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import json
|
|
15
15
|
import logging
|
|
16
|
-
from
|
|
16
|
+
from typing import Protocol
|
|
17
17
|
|
|
18
18
|
import requests
|
|
19
19
|
from requests import exceptions as request_exception
|
|
@@ -23,11 +23,6 @@ from urllib3.util.retry import Retry
|
|
|
23
23
|
from . import event_builder
|
|
24
24
|
from .helpers.enums import HTTPVerbs, EventDispatchConfig
|
|
25
25
|
|
|
26
|
-
if version_info < (3, 8):
|
|
27
|
-
from typing_extensions import Protocol
|
|
28
|
-
else:
|
|
29
|
-
from typing import Protocol
|
|
30
|
-
|
|
31
26
|
|
|
32
27
|
class CustomEventDispatcher(Protocol):
|
|
33
28
|
"""Interface for a custom event dispatcher and required method `dispatch_event`. """
|
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
import json
|
|
16
16
|
import numbers
|
|
17
|
-
from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
18
|
-
from sys import version_info
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional, Literal, Final
|
|
19
18
|
|
|
20
19
|
from . import validator
|
|
21
20
|
from optimizely import optimizely_user_context
|
|
@@ -29,12 +28,6 @@ if TYPE_CHECKING:
|
|
|
29
28
|
from optimizely.logger import Logger
|
|
30
29
|
|
|
31
30
|
|
|
32
|
-
if version_info < (3, 8):
|
|
33
|
-
from typing_extensions import Literal, Final
|
|
34
|
-
else:
|
|
35
|
-
from typing import Literal, Final
|
|
36
|
-
|
|
37
|
-
|
|
38
31
|
class ConditionOperatorTypes:
|
|
39
32
|
AND: Final = 'and'
|
|
40
33
|
OR: Final = 'or'
|
|
@@ -12,12 +12,7 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
|
|
14
14
|
import logging
|
|
15
|
-
from
|
|
16
|
-
|
|
17
|
-
if version_info < (3, 8):
|
|
18
|
-
from typing_extensions import Final
|
|
19
|
-
else:
|
|
20
|
-
from typing import Final
|
|
15
|
+
from typing import Final
|
|
21
16
|
|
|
22
17
|
|
|
23
18
|
class CommonAudienceEvaluationLogs:
|
|
@@ -12,16 +12,10 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
|
-
from typing import TYPE_CHECKING, Any, Optional, NewType, Dict
|
|
15
|
+
from typing import TYPE_CHECKING, Any, Optional, NewType, Dict, Final
|
|
16
16
|
from . import enums
|
|
17
17
|
import math
|
|
18
18
|
import numbers
|
|
19
|
-
from sys import version_info
|
|
20
|
-
|
|
21
|
-
if version_info < (3, 8):
|
|
22
|
-
from typing_extensions import Final
|
|
23
|
-
else:
|
|
24
|
-
from typing import Final
|
|
25
19
|
|
|
26
20
|
|
|
27
21
|
if TYPE_CHECKING:
|
|
@@ -12,14 +12,7 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
-
from typing import Literal, Optional, Any
|
|
16
|
-
from sys import version_info
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if version_info < (3, 8):
|
|
20
|
-
from typing_extensions import TypedDict
|
|
21
|
-
else:
|
|
22
|
-
from typing import TypedDict # type: ignore
|
|
15
|
+
from typing import Literal, Optional, Any, TypedDict
|
|
23
16
|
|
|
24
17
|
|
|
25
18
|
# Intermediate types for type checking deserialized datafile json before actual class instantiation.
|
|
@@ -11,17 +11,11 @@
|
|
|
11
11
|
# See the License for the specific language governing permissions and
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
import logging
|
|
14
|
-
from typing import Any, Optional, Union
|
|
14
|
+
from typing import Any, Optional, Union, Final
|
|
15
15
|
import warnings
|
|
16
|
-
from sys import version_info
|
|
17
16
|
|
|
18
17
|
from .helpers import enums
|
|
19
18
|
|
|
20
|
-
if version_info < (3, 8):
|
|
21
|
-
from typing_extensions import Final
|
|
22
|
-
else:
|
|
23
|
-
from typing import Final
|
|
24
|
-
|
|
25
19
|
|
|
26
20
|
_DEFAULT_LOG_FORMAT: Final = '%(levelname)-8s %(asctime)s %(filename)s:%(lineno)s:%(message)s'
|
|
27
21
|
|
|
@@ -12,15 +12,9 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
|
-
from typing import Any, Callable, Optional
|
|
15
|
+
from typing import Any, Callable, Optional, Final
|
|
16
16
|
from .helpers import enums
|
|
17
17
|
from . import logger as optimizely_logger
|
|
18
|
-
from sys import version_info
|
|
19
|
-
|
|
20
|
-
if version_info < (3, 8):
|
|
21
|
-
from typing_extensions import Final
|
|
22
|
-
else:
|
|
23
|
-
from typing import Final
|
|
24
18
|
|
|
25
19
|
|
|
26
20
|
NOTIFICATION_TYPES: Final = tuple(
|
|
@@ -16,13 +16,7 @@ from dataclasses import dataclass, field
|
|
|
16
16
|
import threading
|
|
17
17
|
from time import time
|
|
18
18
|
from collections import OrderedDict
|
|
19
|
-
from typing import Optional, Generic, TypeVar, Hashable
|
|
20
|
-
from sys import version_info
|
|
21
|
-
|
|
22
|
-
if version_info < (3, 8):
|
|
23
|
-
from typing_extensions import Protocol
|
|
24
|
-
else:
|
|
25
|
-
from typing import Protocol
|
|
19
|
+
from typing import Optional, Generic, TypeVar, Hashable, Protocol
|
|
26
20
|
|
|
27
21
|
# generic type definitions for LRUCache parameters
|
|
28
22
|
K = TypeVar('K', bound=Hashable, contravariant=True)
|
|
@@ -11,12 +11,7 @@
|
|
|
11
11
|
# See the License for the specific language governing permissions and
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
|
|
14
|
-
from
|
|
15
|
-
|
|
16
|
-
if version_info < (3, 8):
|
|
17
|
-
from typing_extensions import Final
|
|
18
|
-
else:
|
|
19
|
-
from typing import Final
|
|
14
|
+
from typing import Final
|
|
20
15
|
|
|
21
16
|
|
|
22
17
|
class OptimizelyOdpOption:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2016-2023, Optimizely
|
|
1
|
+
# Copyright 2016-2023, 2026, Optimizely
|
|
2
2
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
3
|
# you may not use this file except in compliance with the License.
|
|
4
4
|
# You may obtain a copy of the License at
|
|
@@ -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_decisions: Optional[list[Decision]] = None
|
|
1249
1250
|
) -> OptimizelyDecision:
|
|
1250
1251
|
user_id = user_context.user_id
|
|
1251
1252
|
feature_enabled = False
|
|
@@ -1264,6 +1265,27 @@ class Optimizely:
|
|
|
1264
1265
|
|
|
1265
1266
|
feature_flag = project_config.feature_key_map.get(flag_key)
|
|
1266
1267
|
|
|
1268
|
+
# Skipped local holdouts and global holdouts bypassed by exclude_targeted_deliveries
|
|
1269
|
+
if (holdout_decisions
|
|
1270
|
+
and decision_source != DecisionSources.HOLDOUT
|
|
1271
|
+
and OptimizelyDecideOption.DISABLE_DECISION_EVENT not in decide_options):
|
|
1272
|
+
for holdout_decision in holdout_decisions:
|
|
1273
|
+
holdout_enabled = self._get_feature_enabled(holdout_decision.variation)
|
|
1274
|
+
holdout_rule_key = holdout_decision.experiment.key if holdout_decision.experiment else ''
|
|
1275
|
+
self._send_impression_event(
|
|
1276
|
+
project_config,
|
|
1277
|
+
holdout_decision.experiment,
|
|
1278
|
+
holdout_decision.variation,
|
|
1279
|
+
flag_key,
|
|
1280
|
+
holdout_rule_key,
|
|
1281
|
+
str(DecisionSources.HOLDOUT),
|
|
1282
|
+
holdout_enabled,
|
|
1283
|
+
user_id,
|
|
1284
|
+
attributes,
|
|
1285
|
+
holdout_decision.cmab_uuid
|
|
1286
|
+
)
|
|
1287
|
+
decision_event_dispatched = True
|
|
1288
|
+
|
|
1267
1289
|
# Send impression event if Decision came from a feature
|
|
1268
1290
|
if OptimizelyDecideOption.DISABLE_DECISION_EVENT not in decide_options:
|
|
1269
1291
|
if (decision_source == DecisionSources.FEATURE_TEST or
|
|
@@ -1448,11 +1470,13 @@ class Optimizely:
|
|
|
1448
1470
|
user_context,
|
|
1449
1471
|
merged_decide_options
|
|
1450
1472
|
)
|
|
1473
|
+
holdout_decisions: dict[str, Optional[list[Decision]]] = {}
|
|
1451
1474
|
for i in range(0, len(flags_without_forced_decision)):
|
|
1452
1475
|
decision = decision_list[i]['decision']
|
|
1453
1476
|
reasons = decision_list[i]['reasons']
|
|
1454
1477
|
error = decision_list[i]['error']
|
|
1455
1478
|
flag_key = flags_without_forced_decision[i].key
|
|
1479
|
+
holdout_decisions[flag_key] = decision_list[i].get('holdout_decisions')
|
|
1456
1480
|
# store error decision against key and remove key from valid keys
|
|
1457
1481
|
if error:
|
|
1458
1482
|
optimizely_decision = OptimizelyDecision.new_error_decision(flags_without_forced_decision[i].key,
|
|
@@ -1472,7 +1496,8 @@ class Optimizely:
|
|
|
1472
1496
|
flag_decision,
|
|
1473
1497
|
decision_reasons,
|
|
1474
1498
|
merged_decide_options,
|
|
1475
|
-
project_config
|
|
1499
|
+
project_config,
|
|
1500
|
+
holdout_decisions=holdout_decisions.get(key)
|
|
1476
1501
|
)
|
|
1477
1502
|
enabled_flags_only_missing = OptimizelyDecideOption.ENABLED_FLAGS_ONLY not in merged_decide_options
|
|
1478
1503
|
is_enabled = optimizely_decision.enabled
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
import json
|
|
15
|
-
from typing import TYPE_CHECKING, Optional, Type, TypeVar, Union, cast, Any, Iterable, List
|
|
16
|
-
from sys import version_info
|
|
15
|
+
from typing import TYPE_CHECKING, Optional, Type, TypeVar, Union, cast, Any, Iterable, List, Final
|
|
17
16
|
|
|
18
17
|
from . import entities
|
|
19
18
|
from . import exceptions
|
|
@@ -21,11 +20,6 @@ from .helpers import condition as condition_helper
|
|
|
21
20
|
from .helpers import enums
|
|
22
21
|
from .helpers import types
|
|
23
22
|
|
|
24
|
-
if version_info < (3, 8):
|
|
25
|
-
from typing_extensions import Final
|
|
26
|
-
else:
|
|
27
|
-
from typing import Final
|
|
28
|
-
|
|
29
23
|
if TYPE_CHECKING:
|
|
30
24
|
# prevent circular dependenacy by skipping import at runtime
|
|
31
25
|
from .logger import Logger
|
|
@@ -12,19 +12,13 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
|
-
from typing import Any, Optional
|
|
16
|
-
from sys import version_info
|
|
15
|
+
from typing import Any, Optional, Final, TYPE_CHECKING
|
|
17
16
|
from . import logger as _logging
|
|
18
17
|
|
|
19
|
-
if
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
from
|
|
23
|
-
|
|
24
|
-
if TYPE_CHECKING:
|
|
25
|
-
# prevent circular dependenacy by skipping import at runtime
|
|
26
|
-
from .entities import Experiment, Variation
|
|
27
|
-
from optimizely.error_handler import BaseErrorHandler
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
# prevent circular dependenacy by skipping import at runtime
|
|
20
|
+
from .entities import Experiment, Variation
|
|
21
|
+
from optimizely.error_handler import BaseErrorHandler
|
|
28
22
|
|
|
29
23
|
|
|
30
24
|
class UserProfile:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: optimizely-sdk
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.8.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: funcsigs>=0.4; extra == "test"
|
|
28
|
+
Requires-Dist: python-coveralls; extra == "test"
|
|
29
|
+
Requires-Dist: requests>=2.21; extra == "test"
|
|
27
30
|
Requires-Dist: pytest>=6.2.0; extra == "test"
|
|
28
|
-
Requires-Dist: coverage; extra == "test"
|
|
29
|
-
Requires-Dist: pytest-cov; extra == "test"
|
|
30
31
|
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
32
|
Requires-Dist: rpds-py<0.20.0; python_version < "3.11" and extra == "test"
|
|
34
|
-
Requires-Dist: python-coveralls; extra == "test"
|
|
35
|
-
Requires-Dist: funcsigs>=0.4; extra == "test"
|
|
36
33
|
Requires-Dist: idna>=2.10; extra == "test"
|
|
34
|
+
Requires-Dist: coverage; extra == "test"
|
|
35
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
37
36
|
Requires-Dist: ruff>=0.15.0; extra == "test"
|
|
37
|
+
Requires-Dist: pyrsistent>=0.16.0; extra == "test"
|
|
38
38
|
Dynamic: author
|
|
39
39
|
Dynamic: author-email
|
|
40
40
|
Dynamic: classifier
|
|
@@ -304,6 +304,33 @@ 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.8.0
|
|
308
|
+
October 30th, 2026
|
|
309
|
+
|
|
310
|
+
### Changed
|
|
311
|
+
|
|
312
|
+
**Local Holdouts: rule evaluation continues after a holdout hit**:
|
|
313
|
+
When a user matches a local holdout on a rule, the SDK now excludes the user from that rule only
|
|
314
|
+
and continues evaluating the remaining rules, instead of returning the holdout variation and skipping
|
|
315
|
+
the rest of the flag. Each local holdout hit sends its own holdout impression event (using the
|
|
316
|
+
existing event format), regardless of `sendFlagDecisions`. Global holdouts are unchanged.
|
|
317
|
+
|
|
318
|
+
- Continue rule evaluation after local holdout hit ([#525](https://github.com/optimizely/python-sdk/pull/525))
|
|
319
|
+
- Remove dead Python 3.7 typing guards ([#526](https://github.com/optimizely/python-sdk/pull/526))
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## 5.7.0
|
|
324
|
+
September 2nd, 2026
|
|
325
|
+
|
|
326
|
+
### New Features
|
|
327
|
+
|
|
328
|
+
**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.
|
|
329
|
+
|
|
330
|
+
- Add holdout exclusion logic for Targeted Delivery rules ([#522](https://github.com/optimizely/python-sdk/pull/522))
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
307
334
|
## 5.6.0
|
|
308
335
|
July 10th, 2026
|
|
309
336
|
|
|
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
|
{optimizely_sdk-5.6.0 → optimizely_sdk-5.8.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
|
|
@@ -7,16 +7,16 @@ idna>=2.10
|
|
|
7
7
|
rpds-py<0.20.0
|
|
8
8
|
|
|
9
9
|
[test]
|
|
10
|
+
funcsigs>=0.4
|
|
11
|
+
python-coveralls
|
|
12
|
+
requests>=2.21
|
|
10
13
|
pytest>=6.2.0
|
|
11
|
-
coverage
|
|
12
|
-
pytest-cov
|
|
13
14
|
jsonschema>=3.2.0
|
|
14
|
-
pyrsistent>=0.16.0
|
|
15
|
-
requests>=2.21
|
|
16
|
-
python-coveralls
|
|
17
|
-
funcsigs>=0.4
|
|
18
15
|
idna>=2.10
|
|
16
|
+
coverage
|
|
17
|
+
pytest-cov
|
|
19
18
|
ruff>=0.15.0
|
|
19
|
+
pyrsistent>=0.16.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
|