[AI-FSSDK] [FSSDK-12368] Local Holdouts - Cleanup flag base setup and add includedRules and rule-level lookup#501
[AI-FSSDK] [FSSDK-12368] Local Holdouts - Cleanup flag base setup and add includedRules and rule-level lookup#501
Conversation
Add Local Holdouts support to replace legacy flag-level holdouts with rule-level targeting. Changes: - Add includedRules field to Holdout model (replaces includedFlags/excludedFlags) - Add isGlobal property for global vs local holdout detection - Update HoldoutConfig mapping from flag-level to rule-level - Implement get_global_holdouts() and get_holdouts_for_rule() methods - Integrate local holdout evaluation in decision flow (per-rule, before audience/traffic) - Handle edge cases (missing field, empty array, invalid rule IDs, cross-flag targeting) - Add comprehensive unit tests for local holdouts - Update existing tests to use new API Quality Metrics: - Tests: 71/71 passing (100%) - Critical Issues: 0 - Warnings: 0 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Update copyright year to 2026 - Remove unnecessary if __name__ == '__main__' block from test file Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Update copyright headers in modified files to include 2026. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Remove unused imports flagged by ruff: - unittest (using unittest.mock instead) - decision_service (not used) - OptimizelyDecideOption (not used) - enums (not used) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
FarhanAnjum-opti
left a comment
There was a problem hiding this comment.
Changes look good to me. A few things that needs addressing are commented.
There was a problem hiding this comment.
Tests look good. But this file breaks existing convention of module level testing. The Python SDK organizes tests by source module (e.g., test_config.py for config/entities,
test_decision_service_holdout.py for decision service holdout logic), not by feature. The tests in LocalHoldoutsTest (entity properties, project config methods) should be added to
test_config.py, and LocalHoldoutsDecisionFlowTest should be added to test_decision_service_holdout.py. This file should then be removed.
| if version_info < (3, 11): | ||
| from typing_extensions import NotRequired | ||
| else: | ||
| from typing import NotRequired # type: ignore | ||
|
|
There was a problem hiding this comment.
NotRequired with the conditional import adds unnecessary complexity here. These TypedDicts are just intermediate types for type signatures (as noted in the file itself), and backward
compatibility is already handled in Holdout.__init__ where includedRules: Optional[list[str]] = None with **kwargs: Any — if an older datafile omits the
field, **kwargs absorbs any unknown keys and includedRules defaults to None (global holdout). The type system doesn't need to model the "key absent" case when the runtime already
handles it. Just using Optional[list[str]] would be simpler and avoids the new typing_extensions dependency for Python < 3.11.
| if version_info < (3, 11): | |
| from typing_extensions import NotRequired | |
| else: | |
| from typing import NotRequired # type: ignore |
| holdoutStatus: HoldoutStatus | ||
| includedFlags: list[str] | ||
| excludedFlags: list[str] | ||
| includedRules: NotRequired[Optional[list[str]]] |
There was a problem hiding this comment.
| includedRules: NotRequired[Optional[list[str]]] | |
| includedRules: Optional[list[str]] |
…dule-level files - Remove NotRequired import and use Optional[list[str]] for HoldoutDict.includedRules - Migrate test fixtures from includedFlags/excludedFlags to includedRules - Move LocalHoldoutsTest into test_config.py (HoldoutConfigTest + LocalHoldoutConfigTest) - Move LocalHoldoutsDecisionFlowTest into test_decision_service_holdout.py - Delete test_local_holdouts.py (violated module-level test convention) - Tighten weakened assertions to exact counts and content checks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Implements Local Holdouts support for the Python SDK, replacing legacy flag-level holdout targeting (
includedFlags/excludedFlags) with rule-level targeting (includedRules).Related Ticket: FSSDK-12368
Changes
Data Model
included_rulesfield toHoldoutmodel (optional array of rule IDs)is_globalproperty (Truewhenincluded_rules is None)included_flags,excluded_flagsHoldout Configuration
HoldoutConfigfrom flag-level to rule-level mappingget_global_holdouts()- returns holdouts withincluded_rules == Noneget_holdouts_for_rule(rule_id)- returns holdouts targeting specific ruleDecision Flow
source="holdout",experiment_id,ruleKeyEdge Cases
included_rulesfield defaults toNone(global holdout)[]vsNonehandled correctly (empty = local with no rules, None = global)Testing
New Tests (11 comprehensive tests in
tests/test_local_holdouts.py)Updated Tests (60 existing tests)
included_flags/excluded_flagstoincluded_rulesis_globalpropertyQuality Metrics
Files Modified
optimizely/entities.py- Holdout model updatesoptimizely/project_config.py- Mapping and retrieval logicoptimizely/decision_service.py- Decision flow integrationoptimizely/helpers/types.py- Type definitionstests/test_local_holdouts.py- New comprehensive test suitetests/test_decision_service_holdout.py- API updatestests/test_config.py- Model updatesTest Plan
🤖 Generated with Claude Code