Skip to content

Commit 52525df

Browse files
[FSSDK-12265] Add experiments field and mapping to Holdout data model
- Add experiments field to Holdout entity with default empty list - Add is_local property that returns True when experiments list is not empty - Add experiment_holdouts_map to ProjectConfig for experiment-to-holdout mappings - Add get_holdouts_for_experiment method to retrieve holdouts for a specific experiment - Add comprehensive unit tests covering all new functionality - Ensure backward compatibility with existing holdout functionality This enables experiment-specific holdouts to be identified and retrieved efficiently.
1 parent f98886a commit 52525df

File tree

4 files changed

+446
-0
lines changed

4 files changed

+446
-0
lines changed

optimizely/entities.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ def __init__(
223223
includedFlags: Optional[list[str]] = None,
224224
excludedFlags: Optional[list[str]] = None,
225225
audienceConditions: Optional[Sequence[str | list[str]]] = None,
226+
experiments: Optional[list[str]] = None,
226227
**kwargs: Any
227228
):
228229
self.id = id
@@ -234,6 +235,7 @@ def __init__(
234235
self.audienceConditions = audienceConditions
235236
self.includedFlags = includedFlags or []
236237
self.excludedFlags = excludedFlags or []
238+
self.experiments = experiments or []
237239

238240
def get_audience_conditions_or_ids(self) -> Sequence[str | list[str]]:
239241
"""Returns audienceConditions if present, otherwise audienceIds.
@@ -255,6 +257,19 @@ def is_activated(self) -> bool:
255257
"""
256258
return self.status == self.Status.RUNNING
257259

260+
@property
261+
def is_local(self) -> bool:
262+
"""Check if the holdout is local (experiment-specific).
263+
264+
A holdout is considered local if it targets specific experiments.
265+
Matches Swift's isLocal computed property:
266+
var isLocal: Bool { return !experiments.isEmpty }
267+
268+
Returns:
269+
True if experiments list is not empty, False otherwise.
270+
"""
271+
return len(self.experiments) > 0
272+
258273
def __str__(self) -> str:
259274
return self.key
260275

optimizely/helpers/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,4 @@ class HoldoutDict(ExperimentDict):
128128
holdoutStatus: HoldoutStatus
129129
includedFlags: list[str]
130130
excludedFlags: list[str]
131+
experiments: list[str]

optimizely/project_config.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ def __init__(self, datafile: str | bytes, logger: Logger, error_handler: Any):
9797
self.included_holdouts: dict[str, list[entities.Holdout]] = {}
9898
self.excluded_holdouts: dict[str, list[entities.Holdout]] = {}
9999
self.flag_holdouts_map: dict[str, list[entities.Holdout]] = {}
100+
self.experiment_holdouts_map: dict[str, list[entities.Holdout]] = {}
100101

101102
# Convert holdout dicts to Holdout entities
102103
for holdout_data in holdouts_data:
@@ -131,6 +132,13 @@ def __init__(self, datafile: str | bytes, logger: Logger, error_handler: Any):
131132
self.included_holdouts[flag_id] = []
132133
self.included_holdouts[flag_id].append(holdout)
133134

135+
# Build experiment-to-holdout mappings for local holdouts
136+
if holdout.experiments:
137+
for experiment_id in holdout.experiments:
138+
if experiment_id not in self.experiment_holdouts_map:
139+
self.experiment_holdouts_map[experiment_id] = []
140+
self.experiment_holdouts_map[experiment_id].append(holdout)
141+
134142
# Utility maps for quick lookup
135143
self.group_id_map: dict[str, entities.Group] = self._generate_key_map(self.groups, 'id', entities.Group)
136144
self.experiment_id_map: dict[str, entities.Experiment] = self._generate_key_map(
@@ -876,3 +884,18 @@ def get_holdout(self, holdout_id: str) -> Optional[entities.Holdout]:
876884

877885
self.logger.error(f'Holdout with ID "{holdout_id}" not found.')
878886
return None
887+
888+
def get_holdouts_for_experiment(self, experiment_id: str) -> list[entities.Holdout]:
889+
""" Helper method to get holdouts targeting a specific experiment.
890+
891+
Args:
892+
experiment_id: ID of the experiment.
893+
894+
Returns:
895+
The holdouts that apply to this experiment as Holdout entity objects.
896+
Returns empty list if no holdouts target this experiment.
897+
"""
898+
if not self.holdouts:
899+
return []
900+
901+
return self.experiment_holdouts_map.get(experiment_id, [])

0 commit comments

Comments
 (0)