optimizely-sdk 5.3.0__tar.gz → 5.4.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.3.0 → optimizely-sdk-5.4.0}/CHANGELOG.md +19 -0
- {optimizely_sdk-5.3.0/optimizely_sdk.egg-info → optimizely-sdk-5.4.0}/PKG-INFO +22 -29
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/bucketer.py +49 -65
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/decision_service.py +137 -124
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/entities.py +75 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/event/event_factory.py +5 -1
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/event/user_event.py +4 -3
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/event/user_event_factory.py +6 -5
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/optimizely.py +12 -6
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/project_config.py +79 -67
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/version.py +1 -1
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0/optimizely_sdk.egg-info}/PKG-INFO +22 -29
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/LICENSE +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/MANIFEST.in +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/README.md +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/__init__.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/cmab/__init__.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/cmab/cmab_client.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/cmab/cmab_service.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/config_manager.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/decision/__init__.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/decision/optimizely_decide_option.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/decision/optimizely_decision.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/decision/optimizely_decision_message.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/error_handler.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/event/__init__.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/event/event_processor.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/event/log_event.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/event/payload.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/event_builder.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/event_dispatcher.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/exceptions.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/__init__.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/audience.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/condition.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/condition_tree_evaluator.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/constants.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/enums.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/event_tag_utils.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/experiment.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/sdk_settings.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/types.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/validator.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/lib/__init__.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/lib/pymmh3.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/logger.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/notification_center.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/notification_center_registry.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/__init__.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/lru_cache.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/odp_config.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/odp_event.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/odp_event_api_manager.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/odp_event_manager.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/odp_manager.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/odp_segment_api_manager.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/odp_segment_manager.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/optimizely_odp_option.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/optimizely_config.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/optimizely_factory.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/optimizely_user_context.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/py.typed +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/user_profile.py +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely_sdk.egg-info/SOURCES.txt +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely_sdk.egg-info/dependency_links.txt +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely_sdk.egg-info/requires.txt +6 -6
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely_sdk.egg-info/top_level.txt +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/requirements/core.txt +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/requirements/docs.txt +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/requirements/test.txt +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/requirements/typing.txt +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/setup.cfg +0 -0
- {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/setup.py +0 -0
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Optimizely Python SDK Changelog
|
|
2
2
|
|
|
3
|
+
## 5.4.0
|
|
4
|
+
December 19th, 2025
|
|
5
|
+
|
|
6
|
+
### New Features
|
|
7
|
+
|
|
8
|
+
Holdout features:
|
|
9
|
+
|
|
10
|
+
- Added support for Holdouts in Feature Experimentation, enabling global traffic exclusion from experiments for statistical confidence validation. ([#469](https://github.com/optimizely/python-sdk/pull/469))
|
|
11
|
+
- Added holdout support for default decision service ([#467](https://github.com/optimizely/python-sdk/pull/467))
|
|
12
|
+
- Project Config updated for Holdout ([#464](https://github.com/optimizely/python-sdk/pull/464))
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
- Resolved issues with Holdout impression event handling and notification delivery. ([#471](https://github.com/optimizely/python-sdk/pull/471))
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
|
|
3
22
|
## 5.3.0
|
|
4
23
|
November 13th, 2025
|
|
5
24
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: optimizely-sdk
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.4.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
|
|
@@ -17,34 +17,8 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
|
-
License-File: LICENSE
|
|
21
|
-
Requires-Dist: jsonschema>=3.2.0
|
|
22
|
-
Requires-Dist: pyrsistent>=0.16.0
|
|
23
|
-
Requires-Dist: requests>=2.21
|
|
24
|
-
Requires-Dist: idna>=2.10
|
|
25
|
-
Requires-Dist: rpds-py<0.20.0; python_version < "3.11"
|
|
26
20
|
Provides-Extra: test
|
|
27
|
-
|
|
28
|
-
Requires-Dist: python-coveralls; extra == "test"
|
|
29
|
-
Requires-Dist: jsonschema>=3.2.0; extra == "test"
|
|
30
|
-
Requires-Dist: rpds-py<0.20.0; python_version < "3.11" and extra == "test"
|
|
31
|
-
Requires-Dist: requests>=2.21; extra == "test"
|
|
32
|
-
Requires-Dist: pyrsistent>=0.16.0; extra == "test"
|
|
33
|
-
Requires-Dist: idna>=2.10; extra == "test"
|
|
34
|
-
Requires-Dist: pytest>=6.2.0; extra == "test"
|
|
35
|
-
Requires-Dist: coverage; extra == "test"
|
|
36
|
-
Requires-Dist: funcsigs>=0.4; extra == "test"
|
|
37
|
-
Requires-Dist: pytest-cov; extra == "test"
|
|
38
|
-
Dynamic: author
|
|
39
|
-
Dynamic: author-email
|
|
40
|
-
Dynamic: classifier
|
|
41
|
-
Dynamic: description
|
|
42
|
-
Dynamic: description-content-type
|
|
43
|
-
Dynamic: home-page
|
|
44
|
-
Dynamic: license-file
|
|
45
|
-
Dynamic: provides-extra
|
|
46
|
-
Dynamic: requires-dist
|
|
47
|
-
Dynamic: summary
|
|
21
|
+
License-File: LICENSE
|
|
48
22
|
|
|
49
23
|
Optimizely Feature Experimentation is A/B testing and feature management for product development teams. Experiment in any application. Make every feature on your roadmap an opportunity to learn. Learn more at https://www.optimizely.com/products/experiment/feature-experimentation/ or see our documentation at https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/welcome. # Optimizely Python SDK
|
|
50
24
|
|
|
@@ -304,6 +278,25 @@ idna (BSD 3-Clause License https://github.com/kjd/idna/blob/master/LICENSE.md)
|
|
|
304
278
|
- Swift - https://github.com/optimizely/swift-sdk
|
|
305
279
|
# Optimizely Python SDK Changelog
|
|
306
280
|
|
|
281
|
+
## 5.4.0
|
|
282
|
+
December 19th, 2025
|
|
283
|
+
|
|
284
|
+
### New Features
|
|
285
|
+
|
|
286
|
+
Holdout features:
|
|
287
|
+
|
|
288
|
+
- Added support for Holdouts in Feature Experimentation, enabling global traffic exclusion from experiments for statistical confidence validation. ([#469](https://github.com/optimizely/python-sdk/pull/469))
|
|
289
|
+
- Added holdout support for default decision service ([#467](https://github.com/optimizely/python-sdk/pull/467))
|
|
290
|
+
- Project Config updated for Holdout ([#464](https://github.com/optimizely/python-sdk/pull/464))
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
### Bug Fixes
|
|
295
|
+
- Resolved issues with Holdout impression event handling and notification delivery. ([#471](https://github.com/optimizely/python-sdk/pull/471))
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
|
|
307
300
|
## 5.3.0
|
|
308
301
|
November 13th, 2025
|
|
309
302
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
|
-
from typing import Optional, TYPE_CHECKING
|
|
15
|
+
from typing import Optional, TYPE_CHECKING, cast
|
|
16
16
|
import math
|
|
17
17
|
from sys import version_info
|
|
18
18
|
|
|
@@ -28,7 +28,7 @@ else:
|
|
|
28
28
|
if TYPE_CHECKING:
|
|
29
29
|
# prevent circular dependenacy by skipping import at runtime
|
|
30
30
|
from .project_config import ProjectConfig
|
|
31
|
-
from .entities import Experiment, Variation
|
|
31
|
+
from .entities import Experiment, Variation, Holdout
|
|
32
32
|
from .helpers.types import TrafficAllocation
|
|
33
33
|
|
|
34
34
|
|
|
@@ -104,8 +104,8 @@ class Bucketer:
|
|
|
104
104
|
|
|
105
105
|
def bucket(
|
|
106
106
|
self, project_config: ProjectConfig,
|
|
107
|
-
experiment: Experiment, user_id: str, bucketing_id: str
|
|
108
|
-
) -> tuple[
|
|
107
|
+
experiment: Experiment | Holdout, user_id: str, bucketing_id: str
|
|
108
|
+
) -> tuple[Variation | None, list[str]]:
|
|
109
109
|
""" For a given experiment and bucketing ID determines variation to be shown to user.
|
|
110
110
|
|
|
111
111
|
Args:
|
|
@@ -125,14 +125,9 @@ class Bucketer:
|
|
|
125
125
|
project_config.logger.debug(message)
|
|
126
126
|
return None, []
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
experiment_id = experiment.get('id', '')
|
|
132
|
-
else:
|
|
133
|
-
# This is an Experiment object
|
|
134
|
-
experiment_key = experiment.key
|
|
135
|
-
experiment_id = experiment.id
|
|
128
|
+
# Handle both Experiment and Holdout entities
|
|
129
|
+
experiment_key = experiment.key
|
|
130
|
+
experiment_id = experiment.id
|
|
136
131
|
|
|
137
132
|
if not experiment_key or not experiment_key.strip():
|
|
138
133
|
message = 'Invalid entity key provided for bucketing. Returning nil.'
|
|
@@ -141,14 +136,9 @@ class Bucketer:
|
|
|
141
136
|
|
|
142
137
|
variation_id, decide_reasons = self.bucket_to_entity_id(project_config, experiment, user_id, bucketing_id)
|
|
143
138
|
if variation_id:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
variation = next((v for v in variations if v.get('id') == variation_id), None)
|
|
148
|
-
else:
|
|
149
|
-
# For experiments, use the existing method
|
|
150
|
-
variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id)
|
|
151
|
-
return variation, decide_reasons
|
|
139
|
+
variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id)
|
|
140
|
+
# Cast is safe here because experiments always use Variation entities, not VariationDict
|
|
141
|
+
return cast('Optional[Variation]', variation), decide_reasons
|
|
152
142
|
|
|
153
143
|
# No variation found - log message for empty traffic range
|
|
154
144
|
message = 'Bucketed into an empty traffic range. Returning nil.'
|
|
@@ -158,7 +148,7 @@ class Bucketer:
|
|
|
158
148
|
|
|
159
149
|
def bucket_to_entity_id(
|
|
160
150
|
self, project_config: ProjectConfig,
|
|
161
|
-
experiment: Experiment, user_id: str, bucketing_id: str
|
|
151
|
+
experiment: Experiment | Holdout, user_id: str, bucketing_id: str
|
|
162
152
|
) -> tuple[Optional[str], list[str]]:
|
|
163
153
|
"""
|
|
164
154
|
For a given experiment and bucketing ID determines variation ID to be shown to user.
|
|
@@ -176,58 +166,52 @@ class Bucketer:
|
|
|
176
166
|
if not experiment:
|
|
177
167
|
return None, decide_reasons
|
|
178
168
|
|
|
179
|
-
# Handle both Experiment
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
experiment_id = experiment.id
|
|
191
|
-
traffic_allocations = experiment.trafficAllocation
|
|
192
|
-
has_cmab = bool(experiment.cmab)
|
|
169
|
+
# Handle both Experiment and Holdout entities
|
|
170
|
+
# Both entities have key, id, and trafficAllocation attributes
|
|
171
|
+
from . import entities
|
|
172
|
+
|
|
173
|
+
experiment_key = experiment.key
|
|
174
|
+
experiment_id = experiment.id
|
|
175
|
+
traffic_allocations = experiment.trafficAllocation
|
|
176
|
+
|
|
177
|
+
# Determine if experiment is in a mutually exclusive group
|
|
178
|
+
# Holdouts don't have groupId or groupPolicy - use isinstance for type narrowing
|
|
179
|
+
if isinstance(experiment, entities.Experiment):
|
|
193
180
|
group_policy = getattr(experiment, 'groupPolicy', None)
|
|
181
|
+
if group_policy and group_policy in GROUP_POLICIES:
|
|
182
|
+
group = project_config.get_group(experiment.groupId)
|
|
194
183
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if group_policy and group_policy in GROUP_POLICIES:
|
|
198
|
-
group = project_config.get_group(experiment.groupId)
|
|
184
|
+
if not group:
|
|
185
|
+
return None, decide_reasons
|
|
199
186
|
|
|
200
|
-
|
|
201
|
-
|
|
187
|
+
user_experiment_id = self.find_bucket(
|
|
188
|
+
project_config, bucketing_id, experiment.groupId, group.trafficAllocation,
|
|
189
|
+
)
|
|
202
190
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
191
|
+
if not user_experiment_id:
|
|
192
|
+
message = f'User "{user_id}" is in no experiment.'
|
|
193
|
+
project_config.logger.info(message)
|
|
194
|
+
decide_reasons.append(message)
|
|
195
|
+
return None, decide_reasons
|
|
206
196
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
197
|
+
if user_experiment_id != experiment_id:
|
|
198
|
+
message = f'User "{user_id}" is not in experiment "{experiment_key}" of group {experiment.groupId}.'
|
|
199
|
+
project_config.logger.info(message)
|
|
200
|
+
decide_reasons.append(message)
|
|
201
|
+
return None, decide_reasons
|
|
212
202
|
|
|
213
|
-
|
|
214
|
-
message = f'User "{user_id}" is not in experiment "{experiment_key}" of group {experiment.groupId}.'
|
|
203
|
+
message = f'User "{user_id}" is in experiment {experiment_key} of group {experiment.groupId}.'
|
|
215
204
|
project_config.logger.info(message)
|
|
216
205
|
decide_reasons.append(message)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
{
|
|
227
|
-
"entityId": "$",
|
|
228
|
-
"endOfRange": experiment.cmab['trafficAllocation']
|
|
229
|
-
}
|
|
230
|
-
]
|
|
206
|
+
|
|
207
|
+
# Holdouts don't have cmab - use isinstance for type narrowing
|
|
208
|
+
if isinstance(experiment, entities.Experiment) and experiment.cmab:
|
|
209
|
+
traffic_allocations = [
|
|
210
|
+
{
|
|
211
|
+
"entityId": "$",
|
|
212
|
+
"endOfRange": experiment.cmab['trafficAllocation']
|
|
213
|
+
}
|
|
214
|
+
]
|
|
231
215
|
|
|
232
216
|
# Bucket user if not in white-list and in group (if any)
|
|
233
217
|
variation_id = self.find_bucket(project_config, bucketing_id,
|