optimizely-sdk 5.4.0__tar.gz → 5.5.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.4.0 → optimizely_sdk-5.5.0}/CHANGELOG.md +28 -0
- {optimizely-sdk-5.4.0/optimizely_sdk.egg-info → optimizely_sdk-5.5.0}/PKG-INFO +57 -3
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/config_manager.py +6 -9
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/decision_service.py +5 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/entities.py +2 -4
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/event_dispatcher.py +1 -1
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/helpers/enums.py +8 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/helpers/types.py +4 -2
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/helpers/validator.py +2 -2
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/odp/odp_event_manager.py +8 -1
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/project_config.py +69 -37
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/version.py +1 -1
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0/optimizely_sdk.egg-info}/PKG-INFO +57 -3
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely_sdk.egg-info/requires.txt +7 -7
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/requirements/test.txt +1 -1
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/LICENSE +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/MANIFEST.in +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/README.md +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/__init__.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/bucketer.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/cmab/__init__.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/cmab/cmab_client.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/cmab/cmab_service.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/decision/__init__.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/decision/optimizely_decide_option.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/decision/optimizely_decision.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/decision/optimizely_decision_message.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/error_handler.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/event/__init__.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/event/event_factory.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/event/event_processor.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/event/log_event.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/event/payload.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/event/user_event.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/event/user_event_factory.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/event_builder.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/exceptions.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/helpers/__init__.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/helpers/audience.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/helpers/condition.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/helpers/condition_tree_evaluator.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/helpers/constants.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/helpers/event_tag_utils.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/helpers/experiment.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/helpers/sdk_settings.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/lib/__init__.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/lib/pymmh3.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/logger.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/notification_center.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/notification_center_registry.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/odp/__init__.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/odp/lru_cache.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/odp/odp_config.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/odp/odp_event.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/odp/odp_event_api_manager.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/odp/odp_manager.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/odp/odp_segment_api_manager.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/odp/odp_segment_manager.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/odp/optimizely_odp_option.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/optimizely.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/optimizely_config.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/optimizely_factory.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/optimizely_user_context.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/py.typed +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/user_profile.py +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely_sdk.egg-info/SOURCES.txt +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely_sdk.egg-info/dependency_links.txt +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely_sdk.egg-info/top_level.txt +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/requirements/core.txt +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/requirements/docs.txt +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/requirements/typing.txt +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/setup.cfg +0 -0
- {optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/setup.py +0 -0
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Optimizely Python SDK Changelog
|
|
2
2
|
|
|
3
|
+
## 5.5.0
|
|
4
|
+
April 30th, 2026
|
|
5
|
+
|
|
6
|
+
### New Features
|
|
7
|
+
|
|
8
|
+
#### **Feature Rollout Support**
|
|
9
|
+
|
|
10
|
+
- Added support for Feature Rollouts, a new rule type that combines Targeted Delivery simplicity with A/B test measurement capabilities. During project config parsing, the "everyone else" variation from the flag's rollout is automatically injected into feature rollout experiments, enabling correct evaluation without changes to decision logic. ([#499](https://github.com/optimizely/python-sdk/pull/499))
|
|
11
|
+
- Removed experiment type validation from config parsing to support flexible rule types. ([#500](https://github.com/optimizely/python-sdk/pull/500))
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
### Enhancements
|
|
16
|
+
|
|
17
|
+
- Added event retry support for reliable event dispatching. ([#475](https://github.com/optimizely/python-sdk/pull/475))
|
|
18
|
+
- Excluded CMAB experiments from UserProfileService to prevent stale cached decisions. ([#474](https://github.com/optimizely/python-sdk/pull/474))
|
|
19
|
+
- Cleaned up flag-level holdout fields for simplified configuration. ([#507](https://github.com/optimizely/python-sdk/pull/507))
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
- Fixed `return` in `finally` block silently swallowing exceptions. ([#505](https://github.com/optimizely/python-sdk/pull/505))
|
|
26
|
+
- Fixed `PollingConfigManager` rejecting valid URL-only configuration. ([#497](https://github.com/optimizely/python-sdk/pull/497))
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
|
|
3
31
|
## 5.4.0
|
|
4
32
|
December 19th, 2025
|
|
5
33
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: optimizely-sdk
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.5.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,8 +17,34 @@ 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
|
-
Provides-Extra: test
|
|
21
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
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: requests>=2.21; extra == "test"
|
|
28
|
+
Requires-Dist: idna>=2.10; extra == "test"
|
|
29
|
+
Requires-Dist: ruff>=0.15.0; extra == "test"
|
|
30
|
+
Requires-Dist: python-coveralls; extra == "test"
|
|
31
|
+
Requires-Dist: coverage; extra == "test"
|
|
32
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
33
|
+
Requires-Dist: rpds-py<0.20.0; python_version < "3.11" and extra == "test"
|
|
34
|
+
Requires-Dist: jsonschema>=3.2.0; extra == "test"
|
|
35
|
+
Requires-Dist: funcsigs>=0.4; extra == "test"
|
|
36
|
+
Requires-Dist: pytest>=6.2.0; extra == "test"
|
|
37
|
+
Requires-Dist: pyrsistent>=0.16.0; 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
|
|
22
48
|
|
|
23
49
|
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
|
|
24
50
|
|
|
@@ -278,6 +304,34 @@ idna (BSD 3-Clause License https://github.com/kjd/idna/blob/master/LICENSE.md)
|
|
|
278
304
|
- Swift - https://github.com/optimizely/swift-sdk
|
|
279
305
|
# Optimizely Python SDK Changelog
|
|
280
306
|
|
|
307
|
+
## 5.5.0
|
|
308
|
+
April 30th, 2026
|
|
309
|
+
|
|
310
|
+
### New Features
|
|
311
|
+
|
|
312
|
+
#### **Feature Rollout Support**
|
|
313
|
+
|
|
314
|
+
- Added support for Feature Rollouts, a new rule type that combines Targeted Delivery simplicity with A/B test measurement capabilities. During project config parsing, the "everyone else" variation from the flag's rollout is automatically injected into feature rollout experiments, enabling correct evaluation without changes to decision logic. ([#499](https://github.com/optimizely/python-sdk/pull/499))
|
|
315
|
+
- Removed experiment type validation from config parsing to support flexible rule types. ([#500](https://github.com/optimizely/python-sdk/pull/500))
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
### Enhancements
|
|
320
|
+
|
|
321
|
+
- Added event retry support for reliable event dispatching. ([#475](https://github.com/optimizely/python-sdk/pull/475))
|
|
322
|
+
- Excluded CMAB experiments from UserProfileService to prevent stale cached decisions. ([#474](https://github.com/optimizely/python-sdk/pull/474))
|
|
323
|
+
- Cleaned up flag-level holdout fields for simplified configuration. ([#507](https://github.com/optimizely/python-sdk/pull/507))
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
### Bug Fixes
|
|
328
|
+
|
|
329
|
+
- Fixed `return` in `finally` block silently swallowing exceptions. ([#505](https://github.com/optimizely/python-sdk/pull/505))
|
|
330
|
+
- Fixed `PollingConfigManager` rejecting valid URL-only configuration. ([#497](https://github.com/optimizely/python-sdk/pull/497))
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
|
|
281
335
|
## 5.4.0
|
|
282
336
|
December 19th, 2025
|
|
283
337
|
|
|
@@ -145,14 +145,14 @@ class StaticConfigManager(BaseConfigManager):
|
|
|
145
145
|
except optimizely_exceptions.UnsupportedDatafileVersionException as error:
|
|
146
146
|
error_msg = error.args[0]
|
|
147
147
|
error_to_handle = error
|
|
148
|
-
except:
|
|
148
|
+
except Exception:
|
|
149
149
|
error_msg = enums.Errors.INVALID_INPUT.format('datafile')
|
|
150
150
|
error_to_handle = optimizely_exceptions.InvalidInputException(error_msg)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
151
|
+
|
|
152
|
+
if error_msg or config is None:
|
|
153
|
+
self.logger.error(error_msg)
|
|
154
|
+
self.error_handler.handle_error(error_to_handle or Exception('Unknown Error'))
|
|
155
|
+
return
|
|
156
156
|
|
|
157
157
|
previous_revision = self._config.get_revision() if self._config else None
|
|
158
158
|
|
|
@@ -236,9 +236,6 @@ class PollingConfigManager(StaticConfigManager):
|
|
|
236
236
|
)
|
|
237
237
|
self._sdk_key = sdk_key or self._sdk_key
|
|
238
238
|
|
|
239
|
-
if self._sdk_key is None:
|
|
240
|
-
raise optimizely_exceptions.InvalidInputException(enums.Errors.MISSING_SDK_KEY)
|
|
241
|
-
|
|
242
239
|
self.datafile_url = self.get_datafile_url(
|
|
243
240
|
self._sdk_key, url, url_template or self.DATAFILE_URL_TEMPLATE
|
|
244
241
|
)
|
|
@@ -515,6 +515,11 @@ class DecisionService:
|
|
|
515
515
|
'reasons': decide_reasons,
|
|
516
516
|
'variation': None
|
|
517
517
|
}
|
|
518
|
+
ignore_user_profile = True
|
|
519
|
+
self.logger.debug(
|
|
520
|
+
f'Skipping user profile service for CMAB experiment "{experiment.key}". '
|
|
521
|
+
f'CMAB decisions are dynamic and not stored for sticky bucketing.'
|
|
522
|
+
)
|
|
518
523
|
variation_id = cmab_decision['variation_id'] if cmab_decision else None
|
|
519
524
|
cmab_uuid = cmab_decision['cmab_uuid'] if cmab_decision else None
|
|
520
525
|
variation = project_config.get_variation_from_id(experiment_key=experiment.key,
|
|
@@ -87,6 +87,7 @@ class Experiment(BaseEntity):
|
|
|
87
87
|
groupId: Optional[str] = None,
|
|
88
88
|
groupPolicy: Optional[str] = None,
|
|
89
89
|
cmab: Optional[CmabDict] = None,
|
|
90
|
+
type: Optional[str] = None,
|
|
90
91
|
**kwargs: Any
|
|
91
92
|
):
|
|
92
93
|
self.id = id
|
|
@@ -101,6 +102,7 @@ class Experiment(BaseEntity):
|
|
|
101
102
|
self.groupId = groupId
|
|
102
103
|
self.groupPolicy = groupPolicy
|
|
103
104
|
self.cmab = cmab
|
|
105
|
+
self.type = type
|
|
104
106
|
|
|
105
107
|
def get_audience_conditions_or_ids(self) -> Sequence[str | list[str]]:
|
|
106
108
|
""" Returns audienceConditions if present, otherwise audienceIds. """
|
|
@@ -220,8 +222,6 @@ class Holdout(BaseEntity):
|
|
|
220
222
|
variations: list[VariationDict],
|
|
221
223
|
trafficAllocation: list[TrafficAllocation],
|
|
222
224
|
audienceIds: list[str],
|
|
223
|
-
includedFlags: Optional[list[str]] = None,
|
|
224
|
-
excludedFlags: Optional[list[str]] = None,
|
|
225
225
|
audienceConditions: Optional[Sequence[str | list[str]]] = None,
|
|
226
226
|
**kwargs: Any
|
|
227
227
|
):
|
|
@@ -232,8 +232,6 @@ class Holdout(BaseEntity):
|
|
|
232
232
|
self.trafficAllocation = trafficAllocation
|
|
233
233
|
self.audienceIds = audienceIds
|
|
234
234
|
self.audienceConditions = audienceConditions
|
|
235
|
-
self.includedFlags = includedFlags or []
|
|
236
|
-
self.excludedFlags = excludedFlags or []
|
|
237
235
|
|
|
238
236
|
def get_audience_conditions_or_ids(self) -> Sequence[str | list[str]]:
|
|
239
237
|
"""Returns audienceConditions if present, otherwise audienceIds.
|
|
@@ -229,6 +229,14 @@ class OdpManagerConfig:
|
|
|
229
229
|
EVENT_TYPE: Final = 'fullstack'
|
|
230
230
|
|
|
231
231
|
|
|
232
|
+
class ExperimentTypes:
|
|
233
|
+
ab: Final = 'ab'
|
|
234
|
+
mab: Final = 'mab'
|
|
235
|
+
cmab: Final = 'cmab'
|
|
236
|
+
td: Final = 'td'
|
|
237
|
+
fr: Final = 'fr'
|
|
238
|
+
|
|
239
|
+
|
|
232
240
|
class OdpSegmentsCacheConfig:
|
|
233
241
|
"""ODP Segment Cache configs."""
|
|
234
242
|
DEFAULT_CAPACITY: Final = 10_000
|
|
@@ -117,6 +117,10 @@ class CmabDict(BaseEntity):
|
|
|
117
117
|
trafficAllocation: int
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
ExperimentType = Literal[
|
|
121
|
+
'ab', 'mab', 'cmab', 'td', 'fr'
|
|
122
|
+
]
|
|
123
|
+
|
|
120
124
|
HoldoutStatus = Literal['Draft', 'Running', 'Concluded', 'Archived']
|
|
121
125
|
|
|
122
126
|
|
|
@@ -126,5 +130,3 @@ class HoldoutDict(ExperimentDict):
|
|
|
126
130
|
Extends ExperimentDict with holdout-specific properties.
|
|
127
131
|
"""
|
|
128
132
|
holdoutStatus: HoldoutStatus
|
|
129
|
-
includedFlags: list[str]
|
|
130
|
-
excludedFlags: list[str]
|
|
@@ -193,7 +193,7 @@ def is_user_profile_valid(user_profile: dict[str, Any]) -> bool:
|
|
|
193
193
|
if not user_profile:
|
|
194
194
|
return False
|
|
195
195
|
|
|
196
|
-
if
|
|
196
|
+
if type(user_profile) is not dict:
|
|
197
197
|
return False
|
|
198
198
|
|
|
199
199
|
if UserProfile.USER_ID_KEY not in user_profile:
|
|
@@ -203,7 +203,7 @@ def is_user_profile_valid(user_profile: dict[str, Any]) -> bool:
|
|
|
203
203
|
return False
|
|
204
204
|
|
|
205
205
|
experiment_bucket_map = user_profile.get(UserProfile.EXPERIMENT_BUCKET_MAP_KEY)
|
|
206
|
-
if
|
|
206
|
+
if type(experiment_bucket_map) is not dict:
|
|
207
207
|
return False
|
|
208
208
|
|
|
209
209
|
for decision in experiment_bucket_map.values():
|
|
@@ -163,6 +163,8 @@ class OdpEventManager:
|
|
|
163
163
|
|
|
164
164
|
self.logger.debug(f'ODP event queue: flushing batch size {batch_len}.')
|
|
165
165
|
should_retry = False
|
|
166
|
+
initial_retry_interval = 0.2 # 200ms
|
|
167
|
+
max_retry_interval = 1.0 # 1 second
|
|
166
168
|
|
|
167
169
|
for i in range(1 + self.retry_count):
|
|
168
170
|
try:
|
|
@@ -176,7 +178,12 @@ class OdpEventManager:
|
|
|
176
178
|
if not should_retry:
|
|
177
179
|
break
|
|
178
180
|
if i < self.retry_count:
|
|
179
|
-
|
|
181
|
+
# Exponential backoff: 200ms, 400ms, 800ms, ... capped at 1s
|
|
182
|
+
delay = initial_retry_interval * (2 ** i)
|
|
183
|
+
if delay > max_retry_interval:
|
|
184
|
+
delay = max_retry_interval
|
|
185
|
+
self.logger.debug(f'Error dispatching ODP events, retrying after {delay}s.')
|
|
186
|
+
time.sleep(delay)
|
|
180
187
|
|
|
181
188
|
if should_retry:
|
|
182
189
|
self.logger.error(Errors.ODP_EVENT_FAILED.format(f'Failed after {i} retries: {self._current_batch}'))
|
|
@@ -93,9 +93,6 @@ class ProjectConfig:
|
|
|
93
93
|
holdouts_data: list[types.HoldoutDict] = config.get('holdouts', [])
|
|
94
94
|
self.holdouts: list[entities.Holdout] = []
|
|
95
95
|
self.holdout_id_map: dict[str, entities.Holdout] = {}
|
|
96
|
-
self.global_holdouts: list[entities.Holdout] = []
|
|
97
|
-
self.included_holdouts: dict[str, list[entities.Holdout]] = {}
|
|
98
|
-
self.excluded_holdouts: dict[str, list[entities.Holdout]] = {}
|
|
99
96
|
self.flag_holdouts_map: dict[str, list[entities.Holdout]] = {}
|
|
100
97
|
|
|
101
98
|
# Convert holdout dicts to Holdout entities
|
|
@@ -104,33 +101,13 @@ class ProjectConfig:
|
|
|
104
101
|
holdout = entities.Holdout(**holdout_data)
|
|
105
102
|
self.holdouts.append(holdout)
|
|
106
103
|
|
|
107
|
-
# Only process Running holdouts
|
|
104
|
+
# Only process Running holdouts
|
|
108
105
|
if not holdout.is_activated:
|
|
109
106
|
continue
|
|
110
107
|
|
|
111
108
|
# Map by ID for quick lookup
|
|
112
109
|
self.holdout_id_map[holdout.id] = holdout
|
|
113
110
|
|
|
114
|
-
# Categorize as global vs flag-specific
|
|
115
|
-
# Global holdouts: apply to all flags unless explicitly excluded
|
|
116
|
-
# Flag-specific holdouts: only apply to explicitly included flags
|
|
117
|
-
if not holdout.includedFlags:
|
|
118
|
-
# This is a global holdout
|
|
119
|
-
self.global_holdouts.append(holdout)
|
|
120
|
-
|
|
121
|
-
# Track which flags this global holdout excludes
|
|
122
|
-
if holdout.excludedFlags:
|
|
123
|
-
for flag_id in holdout.excludedFlags:
|
|
124
|
-
if flag_id not in self.excluded_holdouts:
|
|
125
|
-
self.excluded_holdouts[flag_id] = []
|
|
126
|
-
self.excluded_holdouts[flag_id].append(holdout)
|
|
127
|
-
else:
|
|
128
|
-
# This holdout applies to specific flags only
|
|
129
|
-
for flag_id in holdout.includedFlags:
|
|
130
|
-
if flag_id not in self.included_holdouts:
|
|
131
|
-
self.included_holdouts[flag_id] = []
|
|
132
|
-
self.included_holdouts[flag_id].append(holdout)
|
|
133
|
-
|
|
134
111
|
# Utility maps for quick lookup
|
|
135
112
|
self.group_id_map: dict[str, entities.Group] = self._generate_key_map(self.groups, 'id', entities.Group)
|
|
136
113
|
self.experiment_id_map: dict[str, entities.Experiment] = self._generate_key_map(
|
|
@@ -232,19 +209,39 @@ class ProjectConfig:
|
|
|
232
209
|
self.experiment_feature_map[exp_id] = [feature.id]
|
|
233
210
|
rules.append(self.experiment_id_map[exp_id])
|
|
234
211
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
212
|
+
# Feature Rollout support: inject the "everyone else" variation
|
|
213
|
+
# into any experiment with type == "feature_rollout"
|
|
214
|
+
everyone_else_variation = self._get_everyone_else_variation(feature)
|
|
215
|
+
if everyone_else_variation is not None:
|
|
216
|
+
for experiment in rules:
|
|
217
|
+
if experiment.type == enums.ExperimentTypes.fr:
|
|
218
|
+
experiment.variations.append({
|
|
219
|
+
'id': everyone_else_variation.id,
|
|
220
|
+
'key': everyone_else_variation.key,
|
|
221
|
+
'featureEnabled': everyone_else_variation.featureEnabled,
|
|
222
|
+
'variables': cast(
|
|
223
|
+
list[types.VariableDict],
|
|
224
|
+
everyone_else_variation.variables,
|
|
225
|
+
),
|
|
226
|
+
})
|
|
227
|
+
experiment.trafficAllocation.append({
|
|
228
|
+
'entityId': everyone_else_variation.id,
|
|
229
|
+
'endOfRange': 10000,
|
|
230
|
+
})
|
|
231
|
+
self.variation_key_map[experiment.key][everyone_else_variation.key] = everyone_else_variation
|
|
232
|
+
self.variation_id_map[experiment.key][everyone_else_variation.id] = everyone_else_variation
|
|
233
|
+
self.variation_id_map_by_experiment_id[experiment.id][everyone_else_variation.id] = (
|
|
234
|
+
everyone_else_variation
|
|
235
|
+
)
|
|
236
|
+
self.variation_key_map_by_experiment_id[experiment.id][everyone_else_variation.key] = (
|
|
237
|
+
everyone_else_variation
|
|
238
|
+
)
|
|
239
|
+
self.variation_variable_usage_map[everyone_else_variation.id] = self._generate_key_map(
|
|
240
|
+
everyone_else_variation.variables, 'id', entities.Variation.VariableUsage
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Map all running holdouts to this flag
|
|
244
|
+
applicable_holdouts = list(self.holdout_id_map.values())
|
|
248
245
|
if applicable_holdouts:
|
|
249
246
|
self.flag_holdouts_map[feature.key] = applicable_holdouts
|
|
250
247
|
|
|
@@ -667,6 +664,41 @@ class ProjectConfig:
|
|
|
667
664
|
self.logger.error(f'Rollout with ID "{rollout_id}" is not in datafile.')
|
|
668
665
|
return None
|
|
669
666
|
|
|
667
|
+
def _get_everyone_else_variation(self, flag: entities.FeatureFlag) -> Optional[entities.Variation]:
|
|
668
|
+
""" Get the "everyone else" variation for a feature flag.
|
|
669
|
+
|
|
670
|
+
The "everyone else" rule is the last experiment in the flag's rollout,
|
|
671
|
+
and its first variation is the "everyone else" variation.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
flag: The feature flag to get the everyone else variation for.
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
The "everyone else" Variation entity, or None if not available.
|
|
678
|
+
"""
|
|
679
|
+
if not flag.rolloutId:
|
|
680
|
+
return None
|
|
681
|
+
|
|
682
|
+
rollout = self.get_rollout_from_id(flag.rolloutId)
|
|
683
|
+
if not rollout or not rollout.experiments:
|
|
684
|
+
return None
|
|
685
|
+
|
|
686
|
+
everyone_else_rule = rollout.experiments[-1]
|
|
687
|
+
variations = everyone_else_rule.get('variations', [])
|
|
688
|
+
if not variations:
|
|
689
|
+
return None
|
|
690
|
+
|
|
691
|
+
variation_dict = variations[0]
|
|
692
|
+
return entities.Variation(
|
|
693
|
+
id=variation_dict['id'],
|
|
694
|
+
key=variation_dict['key'],
|
|
695
|
+
featureEnabled=bool(variation_dict.get('featureEnabled', False)),
|
|
696
|
+
variables=cast(
|
|
697
|
+
Optional[list[entities.Variable]],
|
|
698
|
+
variation_dict.get('variables'),
|
|
699
|
+
),
|
|
700
|
+
)
|
|
701
|
+
|
|
670
702
|
def get_variable_value_for_variation(
|
|
671
703
|
self, variable: Optional[entities.Variable], variation: Optional[Union[entities.Variation, VariationDict]]
|
|
672
704
|
) -> Optional[str]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: optimizely-sdk
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.5.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,8 +17,34 @@ 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
|
-
Provides-Extra: test
|
|
21
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
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: requests>=2.21; extra == "test"
|
|
28
|
+
Requires-Dist: idna>=2.10; extra == "test"
|
|
29
|
+
Requires-Dist: ruff>=0.15.0; extra == "test"
|
|
30
|
+
Requires-Dist: python-coveralls; extra == "test"
|
|
31
|
+
Requires-Dist: coverage; extra == "test"
|
|
32
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
33
|
+
Requires-Dist: rpds-py<0.20.0; python_version < "3.11" and extra == "test"
|
|
34
|
+
Requires-Dist: jsonschema>=3.2.0; extra == "test"
|
|
35
|
+
Requires-Dist: funcsigs>=0.4; extra == "test"
|
|
36
|
+
Requires-Dist: pytest>=6.2.0; extra == "test"
|
|
37
|
+
Requires-Dist: pyrsistent>=0.16.0; 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
|
|
22
48
|
|
|
23
49
|
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
|
|
24
50
|
|
|
@@ -278,6 +304,34 @@ idna (BSD 3-Clause License https://github.com/kjd/idna/blob/master/LICENSE.md)
|
|
|
278
304
|
- Swift - https://github.com/optimizely/swift-sdk
|
|
279
305
|
# Optimizely Python SDK Changelog
|
|
280
306
|
|
|
307
|
+
## 5.5.0
|
|
308
|
+
April 30th, 2026
|
|
309
|
+
|
|
310
|
+
### New Features
|
|
311
|
+
|
|
312
|
+
#### **Feature Rollout Support**
|
|
313
|
+
|
|
314
|
+
- Added support for Feature Rollouts, a new rule type that combines Targeted Delivery simplicity with A/B test measurement capabilities. During project config parsing, the "everyone else" variation from the flag's rollout is automatically injected into feature rollout experiments, enabling correct evaluation without changes to decision logic. ([#499](https://github.com/optimizely/python-sdk/pull/499))
|
|
315
|
+
- Removed experiment type validation from config parsing to support flexible rule types. ([#500](https://github.com/optimizely/python-sdk/pull/500))
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
### Enhancements
|
|
320
|
+
|
|
321
|
+
- Added event retry support for reliable event dispatching. ([#475](https://github.com/optimizely/python-sdk/pull/475))
|
|
322
|
+
- Excluded CMAB experiments from UserProfileService to prevent stale cached decisions. ([#474](https://github.com/optimizely/python-sdk/pull/474))
|
|
323
|
+
- Cleaned up flag-level holdout fields for simplified configuration. ([#507](https://github.com/optimizely/python-sdk/pull/507))
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
### Bug Fixes
|
|
328
|
+
|
|
329
|
+
- Fixed `return` in `finally` block silently swallowing exceptions. ([#505](https://github.com/optimizely/python-sdk/pull/505))
|
|
330
|
+
- Fixed `PollingConfigManager` rejecting valid URL-only configuration. ([#497](https://github.com/optimizely/python-sdk/pull/497))
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
|
|
281
335
|
## 5.4.0
|
|
282
336
|
December 19th, 2025
|
|
283
337
|
|
|
@@ -7,16 +7,16 @@ idna>=2.10
|
|
|
7
7
|
rpds-py<0.20.0
|
|
8
8
|
|
|
9
9
|
[test]
|
|
10
|
-
coverage
|
|
11
|
-
pyrsistent>=0.16.0
|
|
12
|
-
idna>=2.10
|
|
13
|
-
pytest>=6.2.0
|
|
14
|
-
pytest-cov
|
|
15
|
-
funcsigs>=0.4
|
|
16
10
|
requests>=2.21
|
|
11
|
+
idna>=2.10
|
|
12
|
+
ruff>=0.15.0
|
|
17
13
|
python-coveralls
|
|
14
|
+
coverage
|
|
15
|
+
pytest-cov
|
|
18
16
|
jsonschema>=3.2.0
|
|
19
|
-
|
|
17
|
+
funcsigs>=0.4
|
|
18
|
+
pytest>=6.2.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
|
|
File without changes
|
|
File without changes
|
{optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/decision/optimizely_decide_option.py
RENAMED
|
File without changes
|
|
File without changes
|
{optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/decision/optimizely_decision_message.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{optimizely-sdk-5.4.0 → optimizely_sdk-5.5.0}/optimizely/helpers/condition_tree_evaluator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|