Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions optimizely/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ def __init__(
includedFlags: Optional[list[str]] = None,
excludedFlags: Optional[list[str]] = None,
audienceConditions: Optional[Sequence[str | list[str]]] = None,
experiments: Optional[list[str]] = None,
**kwargs: Any
):
self.id = id
Expand All @@ -234,6 +235,7 @@ def __init__(
self.audienceConditions = audienceConditions
self.includedFlags = includedFlags or []
self.excludedFlags = excludedFlags or []
self.experiments = experiments or []

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

@property
def is_local(self) -> bool:
"""Check if the holdout is local (experiment-specific).

A holdout is considered local if it targets specific experiments.
Matches Swift's isLocal computed property:
var isLocal: Bool { return !experiments.isEmpty }

Returns:
True if experiments list is not empty, False otherwise.
"""
return len(self.experiments) > 0

def __str__(self) -> str:
return self.key

Expand Down
1 change: 1 addition & 0 deletions optimizely/helpers/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,4 @@ class HoldoutDict(ExperimentDict):
holdoutStatus: HoldoutStatus
includedFlags: list[str]
excludedFlags: list[str]
experiments: list[str]
23 changes: 23 additions & 0 deletions optimizely/project_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ def __init__(self, datafile: str | bytes, logger: Logger, error_handler: Any):
self.included_holdouts: dict[str, list[entities.Holdout]] = {}
self.excluded_holdouts: dict[str, list[entities.Holdout]] = {}
self.flag_holdouts_map: dict[str, list[entities.Holdout]] = {}
self.experiment_holdouts_map: dict[str, list[entities.Holdout]] = {}

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

# Build experiment-to-holdout mappings for local holdouts
if holdout.experiments:
for experiment_id in holdout.experiments:
if experiment_id not in self.experiment_holdouts_map:
self.experiment_holdouts_map[experiment_id] = []
self.experiment_holdouts_map[experiment_id].append(holdout)

# Utility maps for quick lookup
self.group_id_map: dict[str, entities.Group] = self._generate_key_map(self.groups, 'id', entities.Group)
self.experiment_id_map: dict[str, entities.Experiment] = self._generate_key_map(
Expand Down Expand Up @@ -876,3 +884,18 @@ def get_holdout(self, holdout_id: str) -> Optional[entities.Holdout]:

self.logger.error(f'Holdout with ID "{holdout_id}" not found.')
return None

def get_holdouts_for_experiment(self, experiment_id: str) -> list[entities.Holdout]:
""" Helper method to get holdouts targeting a specific experiment.

Args:
experiment_id: ID of the experiment.

Returns:
The holdouts that apply to this experiment as Holdout entity objects.
Returns empty list if no holdouts target this experiment.
"""
if not self.holdouts:
return []

return self.experiment_holdouts_map.get(experiment_id, [])
Loading