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.
Files changed (73) hide show
  1. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/CHANGELOG.md +19 -0
  2. {optimizely_sdk-5.3.0/optimizely_sdk.egg-info → optimizely-sdk-5.4.0}/PKG-INFO +22 -29
  3. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/bucketer.py +49 -65
  4. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/decision_service.py +137 -124
  5. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/entities.py +75 -0
  6. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/event/event_factory.py +5 -1
  7. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/event/user_event.py +4 -3
  8. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/event/user_event_factory.py +6 -5
  9. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/optimizely.py +12 -6
  10. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/project_config.py +79 -67
  11. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/version.py +1 -1
  12. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0/optimizely_sdk.egg-info}/PKG-INFO +22 -29
  13. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/LICENSE +0 -0
  14. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/MANIFEST.in +0 -0
  15. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/README.md +0 -0
  16. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/__init__.py +0 -0
  17. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/cmab/__init__.py +0 -0
  18. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/cmab/cmab_client.py +0 -0
  19. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/cmab/cmab_service.py +0 -0
  20. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/config_manager.py +0 -0
  21. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/decision/__init__.py +0 -0
  22. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/decision/optimizely_decide_option.py +0 -0
  23. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/decision/optimizely_decision.py +0 -0
  24. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/decision/optimizely_decision_message.py +0 -0
  25. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/error_handler.py +0 -0
  26. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/event/__init__.py +0 -0
  27. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/event/event_processor.py +0 -0
  28. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/event/log_event.py +0 -0
  29. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/event/payload.py +0 -0
  30. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/event_builder.py +0 -0
  31. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/event_dispatcher.py +0 -0
  32. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/exceptions.py +0 -0
  33. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/__init__.py +0 -0
  34. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/audience.py +0 -0
  35. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/condition.py +0 -0
  36. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/condition_tree_evaluator.py +0 -0
  37. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/constants.py +0 -0
  38. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/enums.py +0 -0
  39. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/event_tag_utils.py +0 -0
  40. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/experiment.py +0 -0
  41. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/sdk_settings.py +0 -0
  42. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/types.py +0 -0
  43. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/helpers/validator.py +0 -0
  44. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/lib/__init__.py +0 -0
  45. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/lib/pymmh3.py +0 -0
  46. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/logger.py +0 -0
  47. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/notification_center.py +0 -0
  48. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/notification_center_registry.py +0 -0
  49. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/__init__.py +0 -0
  50. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/lru_cache.py +0 -0
  51. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/odp_config.py +0 -0
  52. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/odp_event.py +0 -0
  53. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/odp_event_api_manager.py +0 -0
  54. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/odp_event_manager.py +0 -0
  55. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/odp_manager.py +0 -0
  56. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/odp_segment_api_manager.py +0 -0
  57. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/odp_segment_manager.py +0 -0
  58. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/odp/optimizely_odp_option.py +0 -0
  59. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/optimizely_config.py +0 -0
  60. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/optimizely_factory.py +0 -0
  61. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/optimizely_user_context.py +0 -0
  62. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/py.typed +0 -0
  63. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely/user_profile.py +0 -0
  64. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely_sdk.egg-info/SOURCES.txt +0 -0
  65. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely_sdk.egg-info/dependency_links.txt +0 -0
  66. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely_sdk.egg-info/requires.txt +6 -6
  67. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/optimizely_sdk.egg-info/top_level.txt +0 -0
  68. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/requirements/core.txt +0 -0
  69. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/requirements/docs.txt +0 -0
  70. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/requirements/test.txt +0 -0
  71. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/requirements/typing.txt +0 -0
  72. {optimizely_sdk-5.3.0 → optimizely-sdk-5.4.0}/setup.cfg +0 -0
  73. {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.4
1
+ Metadata-Version: 2.1
2
2
  Name: optimizely-sdk
3
- Version: 5.3.0
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
- Requires-Dist: flake8>=4.0.1; extra == "test"
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[Optional[Variation], list[str]]:
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
- if isinstance(experiment, dict):
129
- # This is a holdout dictionary
130
- experiment_key = experiment.get('key', '')
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
- if isinstance(experiment, dict):
145
- # For holdouts, find the variation in the holdout's variations array
146
- variations = experiment.get('variations', [])
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 objects and holdout dictionaries
180
- if isinstance(experiment, dict):
181
- # This is a holdout dictionary - holdouts don't have groups
182
- experiment_key = experiment.get('key', '')
183
- experiment_id = experiment.get('id', '')
184
- traffic_allocations = experiment.get('trafficAllocation', [])
185
- has_cmab = False
186
- group_policy = None
187
- else:
188
- # This is an Experiment object
189
- experiment_key = experiment.key
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
- # Determine if experiment is in a mutually exclusive group.
196
- # This will not affect evaluation of rollout rules or holdouts.
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
- if not group:
201
- return None, decide_reasons
187
+ user_experiment_id = self.find_bucket(
188
+ project_config, bucketing_id, experiment.groupId, group.trafficAllocation,
189
+ )
202
190
 
203
- user_experiment_id = self.find_bucket(
204
- project_config, bucketing_id, experiment.groupId, group.trafficAllocation,
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
- if not user_experiment_id:
208
- message = f'User "{user_id}" is in no experiment.'
209
- project_config.logger.info(message)
210
- decide_reasons.append(message)
211
- return None, decide_reasons
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
- if user_experiment_id != experiment_id:
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
- return None, decide_reasons
218
-
219
- message = f'User "{user_id}" is in experiment {experiment_key} of group {experiment.groupId}.'
220
- project_config.logger.info(message)
221
- decide_reasons.append(message)
222
-
223
- if has_cmab:
224
- if experiment.cmab:
225
- traffic_allocations = [
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,