Skip to content

Commit 35f0741

Browse files
committed
Implement changes from Muzahid PR
1 parent 9422468 commit 35f0741

12 files changed

+960
-351
lines changed

optimizely/bucketer.py

Lines changed: 47 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
if TYPE_CHECKING:
2929
# prevent circular dependenacy by skipping import at runtime
3030
from .project_config import ProjectConfig
31-
from .entities import Experiment, Variation
32-
from .helpers.types import TrafficAllocation
31+
from .entities import Experiment, Variation, Holdout
32+
from .helpers.types import TrafficAllocation, VariationDict
3333

3434

3535
MAX_TRAFFIC_VALUE: Final = 10000
@@ -104,8 +104,8 @@ def find_bucket(
104104

105105
def bucket(
106106
self, project_config: ProjectConfig,
107-
experiment: Experiment, user_id: str, bucketing_id: str
108-
) -> tuple[Optional[Variation], list[str]]:
107+
experiment: Experiment | Holdout, user_id: str, bucketing_id: str
108+
) -> tuple[Optional[Variation | VariationDict], list[str]]:
109109
""" For a given experiment and bucketing ID determines variation to be shown to user.
110110
111111
Args:
@@ -125,14 +125,9 @@ def bucket(
125125
project_config.logger.debug(message)
126126
return None, []
127127

128-
if isinstance(experiment, dict):
129-
# This is a holdout dictionary
130-
experiment_key = experiment.get('key', '')
131-
experiment_id = experiment.get('id', '')
132-
else:
133-
# This is an Experiment object
134-
experiment_key = experiment.key
135-
experiment_id = experiment.id
128+
# Handle both Experiment and Holdout entities
129+
experiment_key = experiment.key
130+
experiment_id = experiment.id
136131

137132
if not experiment_key or not experiment_key.strip():
138133
message = 'Invalid entity key provided for bucketing. Returning nil.'
@@ -141,13 +136,7 @@ def bucket(
141136

142137
variation_id, decide_reasons = self.bucket_to_entity_id(project_config, experiment, user_id, bucketing_id)
143138
if variation_id:
144-
if isinstance(experiment, dict):
145-
# For holdouts, find the variation in the holdout's variations array
146-
variations = experiment.get('variations', [])
147-
variation = next((v for v in variations if v.get('id') == variation_id), None)
148-
else:
149-
# For experiments, use the existing method
150-
variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id)
139+
variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id)
151140
return variation, decide_reasons
152141

153142
# No variation found - log message for empty traffic range
@@ -158,7 +147,7 @@ def bucket(
158147

159148
def bucket_to_entity_id(
160149
self, project_config: ProjectConfig,
161-
experiment: Experiment, user_id: str, bucketing_id: str
150+
experiment: Experiment | Holdout, user_id: str, bucketing_id: str
162151
) -> tuple[Optional[str], list[str]]:
163152
"""
164153
For a given experiment and bucketing ID determines variation ID to be shown to user.
@@ -176,58 +165,52 @@ def bucket_to_entity_id(
176165
if not experiment:
177166
return None, decide_reasons
178167

179-
# Handle both Experiment objects and holdout dictionaries
180-
if isinstance(experiment, dict):
181-
# This is a holdout dictionary - holdouts don't have groups
182-
experiment_key = experiment.get('key', '')
183-
experiment_id = experiment.get('id', '')
184-
traffic_allocations = experiment.get('trafficAllocation', [])
185-
has_cmab = False
186-
group_policy = None
187-
else:
188-
# This is an Experiment object
189-
experiment_key = experiment.key
190-
experiment_id = experiment.id
191-
traffic_allocations = experiment.trafficAllocation
192-
has_cmab = bool(experiment.cmab)
168+
# Handle both Experiment and Holdout entities
169+
# Both entities have key, id, and trafficAllocation attributes
170+
from . import entities
171+
172+
experiment_key = experiment.key
173+
experiment_id = experiment.id
174+
traffic_allocations = experiment.trafficAllocation
175+
176+
# Determine if experiment is in a mutually exclusive group
177+
# Holdouts don't have groupId or groupPolicy - use isinstance for type narrowing
178+
if isinstance(experiment, entities.Experiment):
193179
group_policy = getattr(experiment, 'groupPolicy', None)
180+
if group_policy and group_policy in GROUP_POLICIES:
181+
group = project_config.get_group(experiment.groupId)
194182

195-
# Determine if experiment is in a mutually exclusive group.
196-
# This will not affect evaluation of rollout rules or holdouts.
197-
if group_policy and group_policy in GROUP_POLICIES:
198-
group = project_config.get_group(experiment.groupId)
183+
if not group:
184+
return None, decide_reasons
199185

200-
if not group:
201-
return None, decide_reasons
186+
user_experiment_id = self.find_bucket(
187+
project_config, bucketing_id, experiment.groupId, group.trafficAllocation,
188+
)
202189

203-
user_experiment_id = self.find_bucket(
204-
project_config, bucketing_id, experiment.groupId, group.trafficAllocation,
205-
)
190+
if not user_experiment_id:
191+
message = f'User "{user_id}" is in no experiment.'
192+
project_config.logger.info(message)
193+
decide_reasons.append(message)
194+
return None, decide_reasons
206195

207-
if not user_experiment_id:
208-
message = f'User "{user_id}" is in no experiment.'
209-
project_config.logger.info(message)
210-
decide_reasons.append(message)
211-
return None, decide_reasons
196+
if user_experiment_id != experiment_id:
197+
message = f'User "{user_id}" is not in experiment "{experiment_key}" of group {experiment.groupId}.'
198+
project_config.logger.info(message)
199+
decide_reasons.append(message)
200+
return None, decide_reasons
212201

213-
if user_experiment_id != experiment_id:
214-
message = f'User "{user_id}" is not in experiment "{experiment_key}" of group {experiment.groupId}.'
202+
message = f'User "{user_id}" is in experiment {experiment_key} of group {experiment.groupId}.'
215203
project_config.logger.info(message)
216204
decide_reasons.append(message)
217-
return None, decide_reasons
218-
219-
message = f'User "{user_id}" is in experiment {experiment_key} of group {experiment.groupId}.'
220-
project_config.logger.info(message)
221-
decide_reasons.append(message)
222-
223-
if has_cmab:
224-
if experiment.cmab:
225-
traffic_allocations = [
226-
{
227-
"entityId": "$",
228-
"endOfRange": experiment.cmab['trafficAllocation']
229-
}
230-
]
205+
206+
# Holdouts don't have cmab - use isinstance for type narrowing
207+
if isinstance(experiment, entities.Experiment) and experiment.cmab:
208+
traffic_allocations = [
209+
{
210+
"entityId": "$",
211+
"endOfRange": experiment.cmab['trafficAllocation']
212+
}
213+
]
231214

232215
# Bucket user if not in white-list and in group (if any)
233216
variation_id = self.find_bucket(project_config, bucketing_id,

0 commit comments

Comments
 (0)