optimizely-sdk 5.6.0__tar.gz → 5.7.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 (74) hide show
  1. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/CHANGELOG.md +11 -0
  2. {optimizely_sdk-5.6.0/optimizely_sdk.egg-info → optimizely_sdk-5.7.0}/PKG-INFO +20 -9
  3. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/decision_service.py +48 -20
  4. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/entities.py +2 -0
  5. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/optimizely.py +26 -2
  6. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/version.py +1 -1
  7. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0/optimizely_sdk.egg-info}/PKG-INFO +20 -9
  8. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/LICENSE +0 -0
  9. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/MANIFEST.in +0 -0
  10. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/README.md +0 -0
  11. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/__init__.py +0 -0
  12. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/bucketer.py +0 -0
  13. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/cmab/__init__.py +0 -0
  14. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/cmab/cmab_client.py +0 -0
  15. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/cmab/cmab_service.py +0 -0
  16. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/config_manager.py +0 -0
  17. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/decision/__init__.py +0 -0
  18. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/decision/optimizely_decide_option.py +0 -0
  19. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/decision/optimizely_decision.py +0 -0
  20. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/decision/optimizely_decision_message.py +0 -0
  21. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/error_handler.py +0 -0
  22. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event/__init__.py +0 -0
  23. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event/event_factory.py +0 -0
  24. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event/event_id_normalizer.py +0 -0
  25. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event/event_processor.py +0 -0
  26. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event/log_event.py +0 -0
  27. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event/payload.py +0 -0
  28. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event/user_event.py +0 -0
  29. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event/user_event_factory.py +0 -0
  30. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event_builder.py +0 -0
  31. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/event_dispatcher.py +0 -0
  32. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/exceptions.py +0 -0
  33. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/__init__.py +0 -0
  34. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/audience.py +0 -0
  35. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/condition.py +0 -0
  36. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/condition_tree_evaluator.py +0 -0
  37. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/constants.py +0 -0
  38. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/enums.py +0 -0
  39. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/event_tag_utils.py +0 -0
  40. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/experiment.py +0 -0
  41. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/sdk_settings.py +0 -0
  42. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/types.py +0 -0
  43. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/helpers/validator.py +0 -0
  44. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/lib/__init__.py +0 -0
  45. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/lib/pymmh3.py +0 -0
  46. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/logger.py +0 -0
  47. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/notification_center.py +0 -0
  48. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/notification_center_registry.py +0 -0
  49. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/__init__.py +0 -0
  50. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/lru_cache.py +0 -0
  51. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/odp_config.py +0 -0
  52. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/odp_event.py +0 -0
  53. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/odp_event_api_manager.py +0 -0
  54. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/odp_event_manager.py +0 -0
  55. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/odp_manager.py +0 -0
  56. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/odp_segment_api_manager.py +0 -0
  57. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/odp_segment_manager.py +0 -0
  58. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/odp/optimizely_odp_option.py +0 -0
  59. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/optimizely_config.py +0 -0
  60. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/optimizely_factory.py +0 -0
  61. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/optimizely_user_context.py +0 -0
  62. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/project_config.py +0 -0
  63. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/py.typed +0 -0
  64. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely/user_profile.py +0 -0
  65. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely_sdk.egg-info/SOURCES.txt +0 -0
  66. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely_sdk.egg-info/dependency_links.txt +0 -0
  67. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely_sdk.egg-info/requires.txt +7 -7
  68. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/optimizely_sdk.egg-info/top_level.txt +0 -0
  69. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/requirements/core.txt +0 -0
  70. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/requirements/docs.txt +0 -0
  71. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/requirements/test.txt +0 -0
  72. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/requirements/typing.txt +0 -0
  73. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/setup.cfg +0 -0
  74. {optimizely_sdk-5.6.0 → optimizely_sdk-5.7.0}/setup.py +0 -0
@@ -1,5 +1,16 @@
1
1
  # Optimizely Python SDK Changelog
2
2
 
3
+ ## 5.7.0
4
+ September 2nd, 2026
5
+
6
+ ### New Features
7
+
8
+ **Holdout Targeted Delivery Exclusion**: Holdouts can now be configured to exclude Targeted Delivery rules, so users held out of experiments still receive their Targeted Delivery experiences. This gives you a cleaner measure of experiment impact without holding users back from ongoing rollouts.
9
+
10
+ - Add holdout exclusion logic for Targeted Delivery rules ([#522](https://github.com/optimizely/python-sdk/pull/522))
11
+
12
+ ---
13
+
3
14
  ## 5.6.0
4
15
  July 10th, 2026
5
16
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: optimizely-sdk
3
- Version: 5.6.0
3
+ Version: 5.7.0
4
4
  Summary: Python SDK for Optimizely Feature Experimentation, Optimizely Full Stack (legacy), and Optimizely Rollouts.
5
5
  Home-page: https://github.com/optimizely/python-sdk
6
6
  Author: Optimizely
@@ -24,17 +24,17 @@ Requires-Dist: requests>=2.21
24
24
  Requires-Dist: idna>=2.10
25
25
  Requires-Dist: rpds-py<0.20.0; python_version < "3.11"
26
26
  Provides-Extra: test
27
- Requires-Dist: pytest>=6.2.0; extra == "test"
28
- Requires-Dist: coverage; extra == "test"
29
- Requires-Dist: pytest-cov; extra == "test"
30
- Requires-Dist: jsonschema>=3.2.0; extra == "test"
31
- Requires-Dist: pyrsistent>=0.16.0; extra == "test"
32
- Requires-Dist: requests>=2.21; extra == "test"
33
- Requires-Dist: rpds-py<0.20.0; python_version < "3.11" and extra == "test"
34
- Requires-Dist: python-coveralls; extra == "test"
35
27
  Requires-Dist: funcsigs>=0.4; extra == "test"
36
28
  Requires-Dist: idna>=2.10; extra == "test"
37
29
  Requires-Dist: ruff>=0.15.0; extra == "test"
30
+ 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,17 @@ 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.7.0
308
+ September 2nd, 2026
309
+
310
+ ### New Features
311
+
312
+ **Holdout Targeted Delivery Exclusion**: Holdouts can now be configured to exclude Targeted Delivery rules, so users held out of experiments still receive their Targeted Delivery experiences. This gives you a cleaner measure of experiment impact without holding users back from ongoing rollouts.
313
+
314
+ - Add holdout exclusion logic for Targeted Delivery rules ([#522](https://github.com/optimizely/python-sdk/pull/522))
315
+
316
+ ---
317
+
307
318
  ## 5.6.0
308
319
  July 10th, 2026
309
320
 
@@ -64,15 +64,11 @@ class VariationResult(TypedDict):
64
64
  variation: Optional[Union[entities.Variation, VariationDict]]
65
65
 
66
66
 
67
- class DecisionResult(TypedDict):
68
- """
69
- A TypedDict representing the result of a decision process.
67
+ class _DecisionResultOptional(TypedDict, total=False):
68
+ holdout_decision: Decision
70
69
 
71
- Attributes:
72
- decision (Decision): The decision object containing the outcome of the evaluation.
73
- error (bool): Indicates whether an error occurred during the decision process.
74
- reasons (List[str]): A list of reasons explaining the decision or any errors encountered.
75
- """
70
+
71
+ class DecisionResult(_DecisionResultOptional):
76
72
  decision: Decision
77
73
  error: bool
78
74
  reasons: List[str]
@@ -610,7 +606,6 @@ class DecisionService:
610
606
  return Decision(experiment=rule, variation=forced_decision_variation,
611
607
  source=enums.DecisionSources.ROLLOUT, cmab_uuid=None), decide_reasons
612
608
 
613
- # Check local holdouts targeting this specific delivery rule (FSSDK-12369)
614
609
  local_holdouts = project_config.get_holdouts_for_rule(rule.id)
615
610
  for holdout in local_holdouts:
616
611
  local_holdout_decision = self.get_variation_for_holdout(
@@ -751,6 +746,9 @@ class DecisionService:
751
746
  reasons = decide_reasons.copy() if decide_reasons else []
752
747
  user_id = user_context.user_id
753
748
 
749
+ global_holdout_result: DecisionResult | None = None
750
+ global_holdout_key: str | None = None
751
+
754
752
  # Check global holdouts (flag level — before any rules are evaluated)
755
753
  global_holdouts = project_config.get_global_holdouts()
756
754
  for holdout in global_holdouts:
@@ -758,7 +756,6 @@ class DecisionService:
758
756
  reasons.extend(holdout_decision['reasons'])
759
757
 
760
758
  decision = holdout_decision['decision']
761
- # Check if user was bucketed into holdout (has a variation)
762
759
  if decision.variation is None:
763
760
  continue
764
761
 
@@ -768,18 +765,33 @@ class DecisionService:
768
765
  )
769
766
  self.logger.info(message)
770
767
  reasons.append(message)
771
- return {
772
- 'decision': holdout_decision['decision'],
773
- 'error': False,
774
- 'reasons': reasons
775
- }
776
768
 
777
- # If no global holdout decision, check experiments then rollouts
769
+ if not holdout.exclude_targeted_deliveries:
770
+ return {
771
+ 'decision': holdout_decision['decision'],
772
+ 'error': False,
773
+ 'reasons': reasons
774
+ }
775
+
776
+ message = (
777
+ f"Holdout '{holdout.key}' excludes targeted deliveries. "
778
+ f"Targeted delivery rules will be evaluated."
779
+ )
780
+ self.logger.info(message)
781
+ reasons.append(message)
782
+ global_holdout_result = holdout_decision
783
+ global_holdout_key = holdout.key
784
+ break
785
+
786
+ # Check experiments then rollouts
778
787
  if feature_flag.experimentIds:
779
788
  for experiment_id in feature_flag.experimentIds:
780
789
  experiment = project_config.get_experiment_from_id(experiment_id)
781
790
 
782
791
  if experiment:
792
+ if global_holdout_result is not None and experiment.type != enums.ExperimentTypes.td:
793
+ continue
794
+
783
795
  # Check for forced decision
784
796
  optimizely_decision_context = OptimizelyUserContext.OptimizelyDecisionContext(
785
797
  feature_flag.key, experiment.key)
@@ -790,13 +802,15 @@ class DecisionService:
790
802
  if forced_decision_variation:
791
803
  decision = Decision(experiment, forced_decision_variation,
792
804
  enums.DecisionSources.FEATURE_TEST, None)
793
- return {
805
+ result: DecisionResult = {
794
806
  'decision': decision,
795
807
  'error': False,
796
808
  'reasons': reasons
797
809
  }
810
+ if global_holdout_result is not None:
811
+ result['holdout_decision'] = global_holdout_result['decision']
812
+ return result
798
813
 
799
- # Check local holdouts targeting this specific experiment rule (FSSDK-12369)
800
814
  local_holdouts = project_config.get_holdouts_for_rule(experiment.id)
801
815
  for holdout in local_holdouts:
802
816
  local_holdout_decision = self.get_variation_for_holdout(
@@ -837,11 +851,22 @@ class DecisionService:
837
851
  decision = Decision(experiment, variation_result['variation'],
838
852
  enums.DecisionSources.FEATURE_TEST,
839
853
  variation_result['cmab_uuid'])
840
- return {
854
+ result = {
841
855
  'decision': decision,
842
856
  'error': False,
843
857
  'reasons': reasons
844
858
  }
859
+ if global_holdout_result is not None:
860
+ result['holdout_decision'] = global_holdout_result['decision']
861
+ return result
862
+
863
+ if global_holdout_result is not None:
864
+ message = (
865
+ f"Holdout \"{global_holdout_key}\" has excludeTargetedDeliveries enabled, "
866
+ f"continuing to rollout evaluation."
867
+ )
868
+ self.logger.info(message)
869
+ reasons.append(message)
845
870
 
846
871
  # If no experiment decision, check rollouts
847
872
  rollout_decision, rollout_reasons = self.get_variation_for_rollout(
@@ -863,11 +888,14 @@ class DecisionService:
863
888
  else:
864
889
  self.logger.debug(f'User "{user_id}" not bucketed into any rollout for feature "{feature_flag.key}".')
865
890
 
866
- return {
891
+ final_result: DecisionResult = {
867
892
  'decision': rollout_decision,
868
893
  'error': False,
869
894
  'reasons': reasons
870
895
  }
896
+ if global_holdout_result is not None:
897
+ final_result['holdout_decision'] = global_holdout_result['decision']
898
+ return final_result
871
899
 
872
900
  def get_variation_for_holdout(
873
901
  self,
@@ -224,6 +224,7 @@ class Holdout(BaseEntity):
224
224
  audienceIds: list[str],
225
225
  audienceConditions: Optional[Sequence[str | list[str]]] = None,
226
226
  includedRules: Optional[list[str]] = None,
227
+ excludeTargetedDeliveries: bool = False,
227
228
  **kwargs: Any
228
229
  ):
229
230
  self.id = id
@@ -236,6 +237,7 @@ class Holdout(BaseEntity):
236
237
  # Per-rule targeting for local holdouts. Scope comes from the datafile
237
238
  # section, not this field; ProjectConfig strips it on 'holdouts' entries.
238
239
  self.included_rules: Optional[list[str]] = includedRules
240
+ self.exclude_targeted_deliveries: bool = excludeTargetedDeliveries
239
241
 
240
242
  def get_audience_conditions_or_ids(self) -> Sequence[str | list[str]]:
241
243
  """Returns audienceConditions if present, otherwise audienceIds.
@@ -1245,7 +1245,8 @@ class Optimizely:
1245
1245
  flag_decision: Decision,
1246
1246
  decision_reasons: Optional[list[str]],
1247
1247
  decide_options: list[str],
1248
- project_config: ProjectConfig
1248
+ project_config: ProjectConfig,
1249
+ holdout_decision: Optional[Decision] = None
1249
1250
  ) -> OptimizelyDecision:
1250
1251
  user_id = user_context.user_id
1251
1252
  feature_enabled = False
@@ -1264,6 +1265,26 @@ class Optimizely:
1264
1265
 
1265
1266
  feature_flag = project_config.feature_key_map.get(flag_key)
1266
1267
 
1268
+ # Send holdout impression when user was bucketed into a holdout bypassed due to exclude_targeted_deliveries
1269
+ if (holdout_decision is not None
1270
+ and decision_source != DecisionSources.HOLDOUT
1271
+ and OptimizelyDecideOption.DISABLE_DECISION_EVENT not in decide_options):
1272
+ holdout_enabled = self._get_feature_enabled(holdout_decision.variation)
1273
+ holdout_rule_key = holdout_decision.experiment.key if holdout_decision.experiment else ''
1274
+ self._send_impression_event(
1275
+ project_config,
1276
+ holdout_decision.experiment,
1277
+ holdout_decision.variation,
1278
+ flag_key,
1279
+ holdout_rule_key,
1280
+ str(DecisionSources.HOLDOUT),
1281
+ holdout_enabled,
1282
+ user_id,
1283
+ attributes,
1284
+ holdout_decision.cmab_uuid
1285
+ )
1286
+ decision_event_dispatched = True
1287
+
1267
1288
  # Send impression event if Decision came from a feature
1268
1289
  if OptimizelyDecideOption.DISABLE_DECISION_EVENT not in decide_options:
1269
1290
  if (decision_source == DecisionSources.FEATURE_TEST or
@@ -1448,11 +1469,13 @@ class Optimizely:
1448
1469
  user_context,
1449
1470
  merged_decide_options
1450
1471
  )
1472
+ holdout_decisions: dict[str, Optional[Decision]] = {}
1451
1473
  for i in range(0, len(flags_without_forced_decision)):
1452
1474
  decision = decision_list[i]['decision']
1453
1475
  reasons = decision_list[i]['reasons']
1454
1476
  error = decision_list[i]['error']
1455
1477
  flag_key = flags_without_forced_decision[i].key
1478
+ holdout_decisions[flag_key] = decision_list[i].get('holdout_decision')
1456
1479
  # store error decision against key and remove key from valid keys
1457
1480
  if error:
1458
1481
  optimizely_decision = OptimizelyDecision.new_error_decision(flags_without_forced_decision[i].key,
@@ -1472,7 +1495,8 @@ class Optimizely:
1472
1495
  flag_decision,
1473
1496
  decision_reasons,
1474
1497
  merged_decide_options,
1475
- project_config
1498
+ project_config,
1499
+ holdout_decision=holdout_decisions.get(key)
1476
1500
  )
1477
1501
  enabled_flags_only_missing = OptimizelyDecideOption.ENABLED_FLAGS_ONLY not in merged_decide_options
1478
1502
  is_enabled = optimizely_decision.enabled
@@ -11,5 +11,5 @@
11
11
  # See the License for the specific language governing permissions and
12
12
  # limitations under the License.
13
13
 
14
- version_info = (5, 6, 0)
14
+ version_info = (5, 7, 0)
15
15
  __version__ = '.'.join(str(v) for v in version_info)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: optimizely-sdk
3
- Version: 5.6.0
3
+ Version: 5.7.0
4
4
  Summary: Python SDK for Optimizely Feature Experimentation, Optimizely Full Stack (legacy), and Optimizely Rollouts.
5
5
  Home-page: https://github.com/optimizely/python-sdk
6
6
  Author: Optimizely
@@ -24,17 +24,17 @@ Requires-Dist: requests>=2.21
24
24
  Requires-Dist: idna>=2.10
25
25
  Requires-Dist: rpds-py<0.20.0; python_version < "3.11"
26
26
  Provides-Extra: test
27
- Requires-Dist: pytest>=6.2.0; extra == "test"
28
- Requires-Dist: coverage; extra == "test"
29
- Requires-Dist: pytest-cov; extra == "test"
30
- Requires-Dist: jsonschema>=3.2.0; extra == "test"
31
- Requires-Dist: pyrsistent>=0.16.0; extra == "test"
32
- Requires-Dist: requests>=2.21; extra == "test"
33
- Requires-Dist: rpds-py<0.20.0; python_version < "3.11" and extra == "test"
34
- Requires-Dist: python-coveralls; extra == "test"
35
27
  Requires-Dist: funcsigs>=0.4; extra == "test"
36
28
  Requires-Dist: idna>=2.10; extra == "test"
37
29
  Requires-Dist: ruff>=0.15.0; extra == "test"
30
+ 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,17 @@ 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.7.0
308
+ September 2nd, 2026
309
+
310
+ ### New Features
311
+
312
+ **Holdout Targeted Delivery Exclusion**: Holdouts can now be configured to exclude Targeted Delivery rules, so users held out of experiments still receive their Targeted Delivery experiences. This gives you a cleaner measure of experiment impact without holding users back from ongoing rollouts.
313
+
314
+ - Add holdout exclusion logic for Targeted Delivery rules ([#522](https://github.com/optimizely/python-sdk/pull/522))
315
+
316
+ ---
317
+
307
318
  ## 5.6.0
308
319
  July 10th, 2026
309
320
 
File without changes
File without changes
@@ -7,16 +7,16 @@ idna>=2.10
7
7
  rpds-py<0.20.0
8
8
 
9
9
  [test]
10
- pytest>=6.2.0
11
- coverage
12
- pytest-cov
13
- jsonschema>=3.2.0
14
- pyrsistent>=0.16.0
15
- requests>=2.21
16
- python-coveralls
17
10
  funcsigs>=0.4
18
11
  idna>=2.10
19
12
  ruff>=0.15.0
13
+ 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