optimizely-sdk 5.7.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.7.0 → optimizely_sdk-5.8.0}/CHANGELOG.md +16 -0
- {optimizely_sdk-5.7.0/optimizely_sdk.egg-info → optimizely_sdk-5.8.0}/PKG-INFO +24 -8
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/bucketer.py +1 -8
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/decision/optimizely_decide_option.py +1 -6
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/decision/optimizely_decision_message.py +1 -6
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/decision_service.py +57 -47
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/entities.py +1 -7
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/event/event_factory.py +1 -7
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/event/event_processor.py +1 -8
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/event/log_event.py +1 -8
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/event/user_event.py +1 -8
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/event_builder.py +1 -7
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/event_dispatcher.py +1 -6
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/helpers/condition.py +1 -8
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/helpers/enums.py +1 -6
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/helpers/event_tag_utils.py +1 -7
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/helpers/types.py +1 -8
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/logger.py +1 -7
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/notification_center.py +1 -7
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/odp/lru_cache.py +1 -7
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/odp/optimizely_odp_option.py +1 -6
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/optimizely.py +22 -21
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/project_config.py +1 -7
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/user_profile.py +5 -11
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/version.py +1 -1
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0/optimizely_sdk.egg-info}/PKG-INFO +24 -8
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/LICENSE +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/MANIFEST.in +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/README.md +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/__init__.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/cmab/__init__.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/cmab/cmab_client.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/cmab/cmab_service.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/config_manager.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/decision/__init__.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/decision/optimizely_decision.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/error_handler.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/event/__init__.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/event/event_id_normalizer.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/event/payload.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/event/user_event_factory.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/exceptions.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/helpers/__init__.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/helpers/audience.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/helpers/condition_tree_evaluator.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/helpers/constants.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/helpers/experiment.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/helpers/sdk_settings.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/helpers/validator.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/lib/__init__.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/lib/pymmh3.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/notification_center_registry.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/odp/__init__.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/odp/odp_config.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/odp/odp_event.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/odp/odp_event_api_manager.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/odp/odp_event_manager.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/odp/odp_manager.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/odp/odp_segment_api_manager.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/odp/odp_segment_manager.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/optimizely_config.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/optimizely_factory.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/optimizely_user_context.py +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely/py.typed +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely_sdk.egg-info/SOURCES.txt +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely_sdk.egg-info/dependency_links.txt +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely_sdk.egg-info/requires.txt +6 -6
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/optimizely_sdk.egg-info/top_level.txt +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/requirements/core.txt +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/requirements/docs.txt +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/requirements/test.txt +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/requirements/typing.txt +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/setup.cfg +0 -0
- {optimizely_sdk-5.7.0 → optimizely_sdk-5.8.0}/setup.py +0 -0
|
@@ -1,5 +1,21 @@
|
|
|
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
|
+
|
|
3
19
|
## 5.7.0
|
|
4
20
|
September 2nd, 2026
|
|
5
21
|
|
|
@@ -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
|
|
@@ -25,16 +25,16 @@ Requires-Dist: idna>=2.10
|
|
|
25
25
|
Requires-Dist: rpds-py<0.20.0; python_version < "3.11"
|
|
26
26
|
Provides-Extra: test
|
|
27
27
|
Requires-Dist: funcsigs>=0.4; extra == "test"
|
|
28
|
+
Requires-Dist: python-coveralls; extra == "test"
|
|
29
|
+
Requires-Dist: requests>=2.21; extra == "test"
|
|
30
|
+
Requires-Dist: pytest>=6.2.0; extra == "test"
|
|
31
|
+
Requires-Dist: jsonschema>=3.2.0; extra == "test"
|
|
32
|
+
Requires-Dist: rpds-py<0.20.0; python_version < "3.11" and extra == "test"
|
|
28
33
|
Requires-Dist: idna>=2.10; extra == "test"
|
|
34
|
+
Requires-Dist: coverage; extra == "test"
|
|
35
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
29
36
|
Requires-Dist: ruff>=0.15.0; extra == "test"
|
|
30
37
|
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,22 @@ 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
|
+
|
|
307
323
|
## 5.7.0
|
|
308
324
|
September 2nd, 2026
|
|
309
325
|
|
|
@@ -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.7.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.7.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
|
|
@@ -65,7 +65,8 @@ class VariationResult(TypedDict):
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
class _DecisionResultOptional(TypedDict, total=False):
|
|
68
|
-
holdout_decision: Decision
|
|
68
|
+
holdout_decision: Decision # Deprecated: first entry of holdout_decisions.
|
|
69
|
+
holdout_decisions: List[Decision]
|
|
69
70
|
|
|
70
71
|
|
|
71
72
|
class DecisionResult(_DecisionResultOptional):
|
|
@@ -552,7 +553,11 @@ class DecisionService:
|
|
|
552
553
|
}
|
|
553
554
|
|
|
554
555
|
def get_variation_for_rollout(
|
|
555
|
-
self,
|
|
556
|
+
self,
|
|
557
|
+
project_config: ProjectConfig,
|
|
558
|
+
feature: entities.FeatureFlag,
|
|
559
|
+
user_context: OptimizelyUserContext,
|
|
560
|
+
holdout_decisions: Optional[list[Decision]] = None
|
|
556
561
|
) -> tuple[Decision, list[str]]:
|
|
557
562
|
""" Determine which experiment/variation the user is in for a given rollout.
|
|
558
563
|
Returns the variation of the first experiment the user qualifies for.
|
|
@@ -562,7 +567,7 @@ class DecisionService:
|
|
|
562
567
|
flagKey: Feature key.
|
|
563
568
|
rollout: Rollout for which we are getting the variation.
|
|
564
569
|
user: ID and attributes for user.
|
|
565
|
-
|
|
570
|
+
holdout_decisions: If provided, local holdout decisions for skipped rules are appended to it.
|
|
566
571
|
|
|
567
572
|
Returns:
|
|
568
573
|
Decision namedtuple consisting of experiment and variation for the user and
|
|
@@ -606,22 +611,14 @@ class DecisionService:
|
|
|
606
611
|
return Decision(experiment=rule, variation=forced_decision_variation,
|
|
607
612
|
source=enums.DecisionSources.ROLLOUT, cmab_uuid=None), decide_reasons
|
|
608
613
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
if local_decision.variation is not None:
|
|
618
|
-
message = (
|
|
619
|
-
f"The user '{user_id}' is bucketed into local holdout '{holdout.key}' "
|
|
620
|
-
f"for delivery rule '{rule.key}'."
|
|
621
|
-
)
|
|
622
|
-
self.logger.info(message)
|
|
623
|
-
decide_reasons.append(message)
|
|
624
|
-
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
|
|
625
622
|
|
|
626
623
|
bucketing_id, bucket_reasons = self._get_bucketing_id(user_id, attributes)
|
|
627
624
|
decide_reasons += bucket_reasons
|
|
@@ -748,6 +745,7 @@ class DecisionService:
|
|
|
748
745
|
|
|
749
746
|
global_holdout_result: DecisionResult | None = None
|
|
750
747
|
global_holdout_key: str | None = None
|
|
748
|
+
holdout_decisions: list[Decision] = []
|
|
751
749
|
|
|
752
750
|
# Check global holdouts (flag level — before any rules are evaluated)
|
|
753
751
|
global_holdouts = project_config.get_global_holdouts()
|
|
@@ -781,6 +779,7 @@ class DecisionService:
|
|
|
781
779
|
reasons.append(message)
|
|
782
780
|
global_holdout_result = holdout_decision
|
|
783
781
|
global_holdout_key = holdout.key
|
|
782
|
+
holdout_decisions.append(holdout_decision['decision'])
|
|
784
783
|
break
|
|
785
784
|
|
|
786
785
|
# Check experiments then rollouts
|
|
@@ -807,30 +806,17 @@ class DecisionService:
|
|
|
807
806
|
'error': False,
|
|
808
807
|
'reasons': reasons
|
|
809
808
|
}
|
|
810
|
-
if
|
|
811
|
-
result['
|
|
809
|
+
if holdout_decisions:
|
|
810
|
+
result['holdout_decisions'] = holdout_decisions
|
|
811
|
+
result['holdout_decision'] = holdout_decisions[0]
|
|
812
812
|
return result
|
|
813
813
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
)
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
local_decision = local_holdout_decision['decision']
|
|
822
|
-
if local_decision.variation is not None:
|
|
823
|
-
message = (
|
|
824
|
-
f"The user '{user_id}' is bucketed into local holdout '{holdout.key}' "
|
|
825
|
-
f"for experiment rule '{experiment.key}'."
|
|
826
|
-
)
|
|
827
|
-
self.logger.info(message)
|
|
828
|
-
reasons.append(message)
|
|
829
|
-
return {
|
|
830
|
-
'decision': local_holdout_decision['decision'],
|
|
831
|
-
'error': False,
|
|
832
|
-
'reasons': reasons
|
|
833
|
-
}
|
|
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
|
|
834
820
|
|
|
835
821
|
# Get variation for experiment
|
|
836
822
|
variation_result = self.get_variation(
|
|
@@ -856,8 +842,9 @@ class DecisionService:
|
|
|
856
842
|
'error': False,
|
|
857
843
|
'reasons': reasons
|
|
858
844
|
}
|
|
859
|
-
if
|
|
860
|
-
result['
|
|
845
|
+
if holdout_decisions:
|
|
846
|
+
result['holdout_decisions'] = holdout_decisions
|
|
847
|
+
result['holdout_decision'] = holdout_decisions[0]
|
|
861
848
|
return result
|
|
862
849
|
|
|
863
850
|
if global_holdout_result is not None:
|
|
@@ -870,7 +857,7 @@ class DecisionService:
|
|
|
870
857
|
|
|
871
858
|
# If no experiment decision, check rollouts
|
|
872
859
|
rollout_decision, rollout_reasons = self.get_variation_for_rollout(
|
|
873
|
-
project_config, feature_flag, user_context
|
|
860
|
+
project_config, feature_flag, user_context, holdout_decisions
|
|
874
861
|
)
|
|
875
862
|
if rollout_reasons:
|
|
876
863
|
reasons.extend(rollout_reasons)
|
|
@@ -893,10 +880,33 @@ class DecisionService:
|
|
|
893
880
|
'error': False,
|
|
894
881
|
'reasons': reasons
|
|
895
882
|
}
|
|
896
|
-
if
|
|
897
|
-
final_result['
|
|
883
|
+
if holdout_decisions:
|
|
884
|
+
final_result['holdout_decisions'] = holdout_decisions
|
|
885
|
+
final_result['holdout_decision'] = holdout_decisions[0]
|
|
898
886
|
return final_result
|
|
899
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
|
|
909
|
+
|
|
900
910
|
def get_variation_for_holdout(
|
|
901
911
|
self,
|
|
902
912
|
holdout: entities.Holdout,
|
|
@@ -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:
|
|
@@ -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
|
|
@@ -1246,7 +1246,7 @@ class Optimizely:
|
|
|
1246
1246
|
decision_reasons: Optional[list[str]],
|
|
1247
1247
|
decide_options: list[str],
|
|
1248
1248
|
project_config: ProjectConfig,
|
|
1249
|
-
|
|
1249
|
+
holdout_decisions: Optional[list[Decision]] = None
|
|
1250
1250
|
) -> OptimizelyDecision:
|
|
1251
1251
|
user_id = user_context.user_id
|
|
1252
1252
|
feature_enabled = False
|
|
@@ -1265,24 +1265,25 @@ class Optimizely:
|
|
|
1265
1265
|
|
|
1266
1266
|
feature_flag = project_config.feature_key_map.get(flag_key)
|
|
1267
1267
|
|
|
1268
|
-
#
|
|
1269
|
-
if (
|
|
1268
|
+
# Skipped local holdouts and global holdouts bypassed by exclude_targeted_deliveries
|
|
1269
|
+
if (holdout_decisions
|
|
1270
1270
|
and decision_source != DecisionSources.HOLDOUT
|
|
1271
1271
|
and OptimizelyDecideOption.DISABLE_DECISION_EVENT not in decide_options):
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
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
|
+
)
|
|
1286
1287
|
decision_event_dispatched = True
|
|
1287
1288
|
|
|
1288
1289
|
# Send impression event if Decision came from a feature
|
|
@@ -1469,13 +1470,13 @@ class Optimizely:
|
|
|
1469
1470
|
user_context,
|
|
1470
1471
|
merged_decide_options
|
|
1471
1472
|
)
|
|
1472
|
-
holdout_decisions: dict[str, Optional[Decision]] = {}
|
|
1473
|
+
holdout_decisions: dict[str, Optional[list[Decision]]] = {}
|
|
1473
1474
|
for i in range(0, len(flags_without_forced_decision)):
|
|
1474
1475
|
decision = decision_list[i]['decision']
|
|
1475
1476
|
reasons = decision_list[i]['reasons']
|
|
1476
1477
|
error = decision_list[i]['error']
|
|
1477
1478
|
flag_key = flags_without_forced_decision[i].key
|
|
1478
|
-
holdout_decisions[flag_key] = decision_list[i].get('
|
|
1479
|
+
holdout_decisions[flag_key] = decision_list[i].get('holdout_decisions')
|
|
1479
1480
|
# store error decision against key and remove key from valid keys
|
|
1480
1481
|
if error:
|
|
1481
1482
|
optimizely_decision = OptimizelyDecision.new_error_decision(flags_without_forced_decision[i].key,
|
|
@@ -1496,7 +1497,7 @@ class Optimizely:
|
|
|
1496
1497
|
decision_reasons,
|
|
1497
1498
|
merged_decide_options,
|
|
1498
1499
|
project_config,
|
|
1499
|
-
|
|
1500
|
+
holdout_decisions=holdout_decisions.get(key)
|
|
1500
1501
|
)
|
|
1501
1502
|
enabled_flags_only_missing = OptimizelyDecideOption.ENABLED_FLAGS_ONLY not in merged_decide_options
|
|
1502
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
|
|
@@ -25,16 +25,16 @@ Requires-Dist: idna>=2.10
|
|
|
25
25
|
Requires-Dist: rpds-py<0.20.0; python_version < "3.11"
|
|
26
26
|
Provides-Extra: test
|
|
27
27
|
Requires-Dist: funcsigs>=0.4; extra == "test"
|
|
28
|
+
Requires-Dist: python-coveralls; extra == "test"
|
|
29
|
+
Requires-Dist: requests>=2.21; extra == "test"
|
|
30
|
+
Requires-Dist: pytest>=6.2.0; extra == "test"
|
|
31
|
+
Requires-Dist: jsonschema>=3.2.0; extra == "test"
|
|
32
|
+
Requires-Dist: rpds-py<0.20.0; python_version < "3.11" and extra == "test"
|
|
28
33
|
Requires-Dist: idna>=2.10; extra == "test"
|
|
34
|
+
Requires-Dist: coverage; extra == "test"
|
|
35
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
29
36
|
Requires-Dist: ruff>=0.15.0; extra == "test"
|
|
30
37
|
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,22 @@ 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
|
+
|
|
307
323
|
## 5.7.0
|
|
308
324
|
September 2nd, 2026
|
|
309
325
|
|
|
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.7.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
|
|
@@ -8,15 +8,15 @@ rpds-py<0.20.0
|
|
|
8
8
|
|
|
9
9
|
[test]
|
|
10
10
|
funcsigs>=0.4
|
|
11
|
+
python-coveralls
|
|
12
|
+
requests>=2.21
|
|
13
|
+
pytest>=6.2.0
|
|
14
|
+
jsonschema>=3.2.0
|
|
11
15
|
idna>=2.10
|
|
16
|
+
coverage
|
|
17
|
+
pytest-cov
|
|
12
18
|
ruff>=0.15.0
|
|
13
19
|
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
|