optimizely-sdk 5.0.1__tar.gz → 5.1.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.
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/CHANGELOG.md +5 -0
- {optimizely-sdk-5.0.1/optimizely_sdk.egg-info → optimizely_sdk-5.1.0}/PKG-INFO +11 -6
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/decision_service.py +108 -61
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/optimizely.py +123 -58
- optimizely_sdk-5.1.0/optimizely/user_profile.py +157 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/version.py +1 -1
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0/optimizely_sdk.egg-info}/PKG-INFO +11 -6
- optimizely-sdk-5.0.1/optimizely/user_profile.py +0 -92
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/LICENSE +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/MANIFEST.in +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/README.md +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/__init__.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/bucketer.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/config_manager.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/decision/__init__.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/decision/optimizely_decide_option.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/decision/optimizely_decision.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/decision/optimizely_decision_message.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/entities.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/error_handler.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/event/__init__.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/event/event_factory.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/event/event_processor.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/event/log_event.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/event/payload.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/event/user_event.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/event/user_event_factory.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/event_builder.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/event_dispatcher.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/exceptions.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/__init__.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/audience.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/condition.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/condition_tree_evaluator.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/constants.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/enums.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/event_tag_utils.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/experiment.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/sdk_settings.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/types.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/validator.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/lib/__init__.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/lib/pymmh3.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/logger.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/notification_center.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/notification_center_registry.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/__init__.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/lru_cache.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/odp_config.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/odp_event.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/odp_event_api_manager.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/odp_event_manager.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/odp_manager.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/odp_segment_api_manager.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/odp_segment_manager.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/optimizely_odp_option.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/optimizely_config.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/optimizely_factory.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/optimizely_user_context.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/project_config.py +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/py.typed +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely_sdk.egg-info/SOURCES.txt +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely_sdk.egg-info/dependency_links.txt +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely_sdk.egg-info/requires.txt +5 -5
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely_sdk.egg-info/top_level.txt +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/requirements/core.txt +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/requirements/docs.txt +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/requirements/test.txt +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/requirements/typing.txt +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/setup.cfg +0 -0
- {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/setup.py +0 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Optimizely Python SDK Changelog
|
|
2
2
|
|
|
3
|
+
## 5.1.0
|
|
4
|
+
November 27th, 2024
|
|
5
|
+
|
|
6
|
+
Added support for batch processing in DecideAll and DecideForKeys, enabling more efficient handling of multiple decisions in the User Profile Service.([#440](https://github.com/optimizely/python-sdk/pull/440))
|
|
7
|
+
|
|
3
8
|
## 5.0.1
|
|
4
9
|
June 26th, 2024
|
|
5
10
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: optimizely-sdk
|
|
3
|
-
Version: 5.0
|
|
3
|
+
Version: 5.1.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
|
|
@@ -23,16 +23,16 @@ Requires-Dist: pyrsistent>=0.16.0
|
|
|
23
23
|
Requires-Dist: requests>=2.21
|
|
24
24
|
Requires-Dist: idna>=2.10
|
|
25
25
|
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
26
27
|
Requires-Dist: idna>=2.10; extra == "test"
|
|
27
28
|
Requires-Dist: jsonschema>=3.2.0; extra == "test"
|
|
28
|
-
Requires-Dist: python-coveralls; extra == "test"
|
|
29
|
-
Requires-Dist: flake8>=4.0.1; extra == "test"
|
|
30
|
-
Requires-Dist: pytest>=6.2.0; extra == "test"
|
|
31
|
-
Requires-Dist: pytest-cov; extra == "test"
|
|
32
29
|
Requires-Dist: requests>=2.21; extra == "test"
|
|
33
30
|
Requires-Dist: funcsigs>=0.4; extra == "test"
|
|
34
|
-
Requires-Dist:
|
|
31
|
+
Requires-Dist: pytest>=6.2.0; extra == "test"
|
|
32
|
+
Requires-Dist: python-coveralls; extra == "test"
|
|
35
33
|
Requires-Dist: coverage; extra == "test"
|
|
34
|
+
Requires-Dist: flake8>=4.0.1; extra == "test"
|
|
35
|
+
Requires-Dist: pyrsistent>=0.16.0; extra == "test"
|
|
36
36
|
|
|
37
37
|
Optimizely Feature Experimentation is A/B testing and feature management for product development teams. Experiment in any application. Make every feature on your roadmap an opportunity to learn. Learn more at https://www.optimizely.com/products/experiment/feature-experimentation/ or see our documentation at https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/welcome. # Optimizely Python SDK
|
|
38
38
|
|
|
@@ -292,6 +292,11 @@ idna (BSD 3-Clause License https://github.com/kjd/idna/blob/master/LICENSE.md)
|
|
|
292
292
|
- Swift - https://github.com/optimizely/swift-sdk
|
|
293
293
|
# Optimizely Python SDK Changelog
|
|
294
294
|
|
|
295
|
+
## 5.1.0
|
|
296
|
+
November 27th, 2024
|
|
297
|
+
|
|
298
|
+
Added support for batch processing in DecideAll and DecideForKeys, enabling more efficient handling of multiple decisions in the User Profile Service.([#440](https://github.com/optimizely/python-sdk/pull/440))
|
|
299
|
+
|
|
295
300
|
## 5.0.1
|
|
296
301
|
June 26th, 2024
|
|
297
302
|
|
|
@@ -22,7 +22,7 @@ from .helpers import enums
|
|
|
22
22
|
from .helpers import experiment as experiment_helper
|
|
23
23
|
from .helpers import validator
|
|
24
24
|
from .optimizely_user_context import OptimizelyUserContext, UserAttributes
|
|
25
|
-
from .user_profile import UserProfile, UserProfileService
|
|
25
|
+
from .user_profile import UserProfile, UserProfileService, UserProfileTracker
|
|
26
26
|
|
|
27
27
|
if TYPE_CHECKING:
|
|
28
28
|
# prevent circular dependenacy by skipping import at runtime
|
|
@@ -35,7 +35,7 @@ class Decision(NamedTuple):
|
|
|
35
35
|
None if no experiment/variation was selected."""
|
|
36
36
|
experiment: Optional[entities.Experiment]
|
|
37
37
|
variation: Optional[entities.Variation]
|
|
38
|
-
source: str
|
|
38
|
+
source: Optional[str]
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
class DecisionService:
|
|
@@ -247,6 +247,8 @@ class DecisionService:
|
|
|
247
247
|
project_config: ProjectConfig,
|
|
248
248
|
experiment: entities.Experiment,
|
|
249
249
|
user_context: OptimizelyUserContext,
|
|
250
|
+
user_profile_tracker: Optional[UserProfileTracker],
|
|
251
|
+
reasons: list[str] = [],
|
|
250
252
|
options: Optional[Sequence[str]] = None
|
|
251
253
|
) -> tuple[Optional[entities.Variation], list[str]]:
|
|
252
254
|
""" Top-level function to help determine variation user should be put in.
|
|
@@ -260,7 +262,9 @@ class DecisionService:
|
|
|
260
262
|
Args:
|
|
261
263
|
project_config: Instance of ProjectConfig.
|
|
262
264
|
experiment: Experiment for which user variation needs to be determined.
|
|
263
|
-
user_context: contains user id and attributes
|
|
265
|
+
user_context: contains user id and attributes.
|
|
266
|
+
user_profile_tracker: tracker for reading and updating user profile of the user.
|
|
267
|
+
reasons: Decision reasons.
|
|
264
268
|
options: Decide options.
|
|
265
269
|
|
|
266
270
|
Returns:
|
|
@@ -275,6 +279,8 @@ class DecisionService:
|
|
|
275
279
|
ignore_user_profile = False
|
|
276
280
|
|
|
277
281
|
decide_reasons = []
|
|
282
|
+
if reasons is not None:
|
|
283
|
+
decide_reasons += reasons
|
|
278
284
|
# Check if experiment is running
|
|
279
285
|
if not experiment_helper.is_experiment_running(experiment):
|
|
280
286
|
message = f'Experiment "{experiment.key}" is not running.'
|
|
@@ -296,23 +302,14 @@ class DecisionService:
|
|
|
296
302
|
return variation, decide_reasons
|
|
297
303
|
|
|
298
304
|
# Check to see if user has a decision available for the given experiment
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
self.logger.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if retrieved_profile and validator.is_user_profile_valid(retrieved_profile):
|
|
308
|
-
user_profile = UserProfile(**retrieved_profile)
|
|
309
|
-
variation = self.get_stored_variation(project_config, experiment, user_profile)
|
|
310
|
-
if variation:
|
|
311
|
-
message = f'Returning previously activated variation ID "{variation}" of experiment ' \
|
|
312
|
-
f'"{experiment}" for user "{user_id}" from user profile.'
|
|
313
|
-
self.logger.info(message)
|
|
314
|
-
decide_reasons.append(message)
|
|
315
|
-
return variation, decide_reasons
|
|
305
|
+
if user_profile_tracker is not None and not ignore_user_profile:
|
|
306
|
+
variation = self.get_stored_variation(project_config, experiment, user_profile_tracker.get_user_profile())
|
|
307
|
+
if variation:
|
|
308
|
+
message = f'Returning previously activated variation ID "{variation}" of experiment ' \
|
|
309
|
+
f'"{experiment}" for user "{user_id}" from user profile.'
|
|
310
|
+
self.logger.info(message)
|
|
311
|
+
decide_reasons.append(message)
|
|
312
|
+
return variation, decide_reasons
|
|
316
313
|
else:
|
|
317
314
|
self.logger.warning('User profile has invalid format.')
|
|
318
315
|
|
|
@@ -340,10 +337,9 @@ class DecisionService:
|
|
|
340
337
|
self.logger.info(message)
|
|
341
338
|
decide_reasons.append(message)
|
|
342
339
|
# Store this new decision and return the variation for the user
|
|
343
|
-
if not
|
|
340
|
+
if user_profile_tracker is not None and not ignore_user_profile:
|
|
344
341
|
try:
|
|
345
|
-
|
|
346
|
-
self.user_profile_service.save(user_profile.__dict__)
|
|
342
|
+
user_profile_tracker.update_user_profile(experiment, variation)
|
|
347
343
|
except:
|
|
348
344
|
self.logger.exception(f'Unable to save user profile for user "{user_id}".')
|
|
349
345
|
return variation, decide_reasons
|
|
@@ -479,44 +475,7 @@ class DecisionService:
|
|
|
479
475
|
Returns:
|
|
480
476
|
Decision namedtuple consisting of experiment and variation for the user.
|
|
481
477
|
"""
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
# Check if the feature flag is under an experiment and the the user is bucketed into one of these experiments
|
|
485
|
-
if feature.experimentIds:
|
|
486
|
-
# Evaluate each experiment ID and return the first bucketed experiment variation
|
|
487
|
-
for experiment_id in feature.experimentIds:
|
|
488
|
-
experiment = project_config.get_experiment_from_id(experiment_id)
|
|
489
|
-
decision_variation = None
|
|
490
|
-
|
|
491
|
-
if experiment:
|
|
492
|
-
optimizely_decision_context = OptimizelyUserContext.OptimizelyDecisionContext(feature.key,
|
|
493
|
-
experiment.key)
|
|
494
|
-
|
|
495
|
-
forced_decision_variation, reasons_received = self.validated_forced_decision(
|
|
496
|
-
project_config, optimizely_decision_context, user_context)
|
|
497
|
-
decide_reasons += reasons_received
|
|
498
|
-
|
|
499
|
-
if forced_decision_variation:
|
|
500
|
-
decision_variation = forced_decision_variation
|
|
501
|
-
else:
|
|
502
|
-
decision_variation, variation_reasons = self.get_variation(project_config,
|
|
503
|
-
experiment, user_context, options)
|
|
504
|
-
decide_reasons += variation_reasons
|
|
505
|
-
|
|
506
|
-
if decision_variation:
|
|
507
|
-
message = f'User "{user_context.user_id}" bucketed into a ' \
|
|
508
|
-
f'experiment "{experiment.key}" of feature "{feature.key}".'
|
|
509
|
-
self.logger.debug(message)
|
|
510
|
-
return Decision(experiment, decision_variation,
|
|
511
|
-
enums.DecisionSources.FEATURE_TEST), decide_reasons
|
|
512
|
-
|
|
513
|
-
message = f'User "{user_context.user_id}" is not bucketed into any of the ' \
|
|
514
|
-
f'experiments on the feature "{feature.key}".'
|
|
515
|
-
self.logger.debug(message)
|
|
516
|
-
variation, rollout_variation_reasons = self.get_variation_for_rollout(project_config, feature, user_context)
|
|
517
|
-
if rollout_variation_reasons:
|
|
518
|
-
decide_reasons += rollout_variation_reasons
|
|
519
|
-
return variation, decide_reasons
|
|
478
|
+
return self.get_variations_for_feature_list(project_config, [feature], user_context, options)[0]
|
|
520
479
|
|
|
521
480
|
def validated_forced_decision(
|
|
522
481
|
self,
|
|
@@ -580,3 +539,91 @@ class DecisionService:
|
|
|
580
539
|
user_context.logger.info(user_has_forced_decision_but_invalid)
|
|
581
540
|
|
|
582
541
|
return None, reasons
|
|
542
|
+
|
|
543
|
+
def get_variations_for_feature_list(
|
|
544
|
+
self,
|
|
545
|
+
project_config: ProjectConfig,
|
|
546
|
+
features: list[entities.FeatureFlag],
|
|
547
|
+
user_context: OptimizelyUserContext,
|
|
548
|
+
options: Optional[Sequence[str]] = None
|
|
549
|
+
) -> list[tuple[Decision, list[str]]]:
|
|
550
|
+
"""
|
|
551
|
+
Returns the list of experiment/variation the user is bucketed in for the given list of features.
|
|
552
|
+
Args:
|
|
553
|
+
project_config: Instance of ProjectConfig.
|
|
554
|
+
features: List of features for which we are determining if it is enabled or not for the given user.
|
|
555
|
+
user_context: user context for user.
|
|
556
|
+
options: Decide options.
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
List of Decision namedtuple consisting of experiment and variation for the user.
|
|
560
|
+
"""
|
|
561
|
+
decide_reasons: list[str] = []
|
|
562
|
+
|
|
563
|
+
if options:
|
|
564
|
+
ignore_ups = OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE in options
|
|
565
|
+
else:
|
|
566
|
+
ignore_ups = False
|
|
567
|
+
|
|
568
|
+
user_profile_tracker: Optional[UserProfileTracker] = None
|
|
569
|
+
if self.user_profile_service is not None and not ignore_ups:
|
|
570
|
+
user_profile_tracker = UserProfileTracker(user_context.user_id, self.user_profile_service, self.logger)
|
|
571
|
+
user_profile_tracker.load_user_profile(decide_reasons, None)
|
|
572
|
+
|
|
573
|
+
decisions = []
|
|
574
|
+
|
|
575
|
+
for feature in features:
|
|
576
|
+
feature_reasons = decide_reasons.copy()
|
|
577
|
+
experiment_decision_found = False # Track if an experiment decision was made for the feature
|
|
578
|
+
|
|
579
|
+
# Check if the feature flag is under an experiment
|
|
580
|
+
if feature.experimentIds:
|
|
581
|
+
for experiment_id in feature.experimentIds:
|
|
582
|
+
experiment = project_config.get_experiment_from_id(experiment_id)
|
|
583
|
+
decision_variation = None
|
|
584
|
+
|
|
585
|
+
if experiment:
|
|
586
|
+
optimizely_decision_context = OptimizelyUserContext.OptimizelyDecisionContext(
|
|
587
|
+
feature.key, experiment.key)
|
|
588
|
+
forced_decision_variation, reasons_received = self.validated_forced_decision(
|
|
589
|
+
project_config, optimizely_decision_context, user_context)
|
|
590
|
+
feature_reasons.extend(reasons_received)
|
|
591
|
+
|
|
592
|
+
if forced_decision_variation:
|
|
593
|
+
decision_variation = forced_decision_variation
|
|
594
|
+
else:
|
|
595
|
+
decision_variation, variation_reasons = self.get_variation(
|
|
596
|
+
project_config, experiment, user_context, user_profile_tracker, feature_reasons, options
|
|
597
|
+
)
|
|
598
|
+
feature_reasons.extend(variation_reasons)
|
|
599
|
+
|
|
600
|
+
if decision_variation:
|
|
601
|
+
self.logger.debug(
|
|
602
|
+
f'User "{user_context.user_id}" '
|
|
603
|
+
f'bucketed into experiment "{experiment.key}" of feature "{feature.key}".'
|
|
604
|
+
)
|
|
605
|
+
decision = Decision(experiment, decision_variation, enums.DecisionSources.FEATURE_TEST)
|
|
606
|
+
decisions.append((decision, feature_reasons))
|
|
607
|
+
experiment_decision_found = True # Mark that a decision was found
|
|
608
|
+
break # Stop after the first successful experiment decision
|
|
609
|
+
|
|
610
|
+
# Only process rollout if no experiment decision was found
|
|
611
|
+
if not experiment_decision_found:
|
|
612
|
+
rollout_decision, rollout_reasons = self.get_variation_for_rollout(project_config,
|
|
613
|
+
feature,
|
|
614
|
+
user_context)
|
|
615
|
+
if rollout_reasons:
|
|
616
|
+
feature_reasons.extend(rollout_reasons)
|
|
617
|
+
if rollout_decision:
|
|
618
|
+
self.logger.debug(f'User "{user_context.user_id}" '
|
|
619
|
+
f'bucketed into rollout for feature "{feature.key}".')
|
|
620
|
+
else:
|
|
621
|
+
self.logger.debug(f'User "{user_context.user_id}" '
|
|
622
|
+
f'not bucketed into any rollout for feature "{feature.key}".')
|
|
623
|
+
|
|
624
|
+
decisions.append((rollout_decision, feature_reasons))
|
|
625
|
+
|
|
626
|
+
if self.user_profile_service is not None and user_profile_tracker is not None and ignore_ups is False:
|
|
627
|
+
user_profile_tracker.save_user_profile()
|
|
628
|
+
|
|
629
|
+
return decisions
|
|
@@ -21,6 +21,7 @@ from . import event_builder
|
|
|
21
21
|
from . import exceptions
|
|
22
22
|
from . import logger as _logging
|
|
23
23
|
from . import project_config
|
|
24
|
+
from . import user_profile
|
|
24
25
|
from .config_manager import AuthDatafilePollingConfigManager
|
|
25
26
|
from .config_manager import BaseConfigManager
|
|
26
27
|
from .config_manager import PollingConfigManager
|
|
@@ -42,6 +43,7 @@ from .odp.lru_cache import LRUCache
|
|
|
42
43
|
from .odp.odp_manager import OdpManager
|
|
43
44
|
from .optimizely_config import OptimizelyConfig, OptimizelyConfigService
|
|
44
45
|
from .optimizely_user_context import OptimizelyUserContext, UserAttributes
|
|
46
|
+
from .project_config import ProjectConfig
|
|
45
47
|
|
|
46
48
|
if TYPE_CHECKING:
|
|
47
49
|
# prevent circular dependency by skipping import at runtime
|
|
@@ -168,6 +170,7 @@ class Optimizely:
|
|
|
168
170
|
|
|
169
171
|
self.event_builder = event_builder.EventBuilder()
|
|
170
172
|
self.decision_service = decision_service.DecisionService(self.logger, user_profile_service)
|
|
173
|
+
self.user_profile_service = user_profile_service
|
|
171
174
|
|
|
172
175
|
def _validate_instantiation_options(self) -> None:
|
|
173
176
|
""" Helper method to validate all instantiation parameters.
|
|
@@ -629,8 +632,13 @@ class Optimizely:
|
|
|
629
632
|
return None
|
|
630
633
|
|
|
631
634
|
user_context = OptimizelyUserContext(self, self.logger, user_id, attributes, False)
|
|
632
|
-
|
|
633
|
-
|
|
635
|
+
user_profile_tracker = user_profile.UserProfileTracker(user_id, self.user_profile_service, self.logger)
|
|
636
|
+
user_profile_tracker.load_user_profile()
|
|
637
|
+
variation, _ = self.decision_service.get_variation(project_config,
|
|
638
|
+
experiment,
|
|
639
|
+
user_context,
|
|
640
|
+
user_profile_tracker)
|
|
641
|
+
user_profile_tracker.save_user_profile()
|
|
634
642
|
if variation:
|
|
635
643
|
variation_key = variation.key
|
|
636
644
|
|
|
@@ -701,7 +709,7 @@ class Optimizely:
|
|
|
701
709
|
if (is_source_rollout or not decision.variation) and project_config.get_send_flag_decisions_value():
|
|
702
710
|
self._send_impression_event(
|
|
703
711
|
project_config, decision.experiment, decision.variation, feature.key, decision.experiment.key if
|
|
704
|
-
decision.experiment else '', decision.source, feature_enabled, user_id, attributes
|
|
712
|
+
decision.experiment else '', str(decision.source), feature_enabled, user_id, attributes
|
|
705
713
|
)
|
|
706
714
|
|
|
707
715
|
# Send event if Decision came from an experiment.
|
|
@@ -712,7 +720,7 @@ class Optimizely:
|
|
|
712
720
|
}
|
|
713
721
|
self._send_impression_event(
|
|
714
722
|
project_config, decision.experiment, decision.variation, feature.key, decision.experiment.key,
|
|
715
|
-
decision.source, feature_enabled, user_id, attributes
|
|
723
|
+
str(decision.source), feature_enabled, user_id, attributes
|
|
716
724
|
)
|
|
717
725
|
|
|
718
726
|
if feature_enabled:
|
|
@@ -1118,73 +1126,70 @@ class Optimizely:
|
|
|
1118
1126
|
self.logger.debug('Provided decide options is not an array. Using default decide options.')
|
|
1119
1127
|
decide_options = self.default_decide_options
|
|
1120
1128
|
|
|
1121
|
-
|
|
1129
|
+
if OptimizelyDecideOption.ENABLED_FLAGS_ONLY in decide_options:
|
|
1130
|
+
decide_options.remove(OptimizelyDecideOption.ENABLED_FLAGS_ONLY)
|
|
1131
|
+
|
|
1132
|
+
decision = self._decide_for_keys(
|
|
1133
|
+
user_context,
|
|
1134
|
+
[key],
|
|
1135
|
+
decide_options,
|
|
1136
|
+
True
|
|
1137
|
+
)[key]
|
|
1138
|
+
|
|
1139
|
+
return decision
|
|
1140
|
+
|
|
1141
|
+
def _create_optimizely_decision(
|
|
1142
|
+
self,
|
|
1143
|
+
user_context: OptimizelyUserContext,
|
|
1144
|
+
flag_key: str,
|
|
1145
|
+
flag_decision: Decision,
|
|
1146
|
+
decision_reasons: Optional[list[str]],
|
|
1147
|
+
decide_options: list[str],
|
|
1148
|
+
project_config: ProjectConfig
|
|
1149
|
+
) -> OptimizelyDecision:
|
|
1122
1150
|
user_id = user_context.user_id
|
|
1123
|
-
attributes = user_context.get_user_attributes()
|
|
1124
|
-
variation_key = None
|
|
1125
|
-
variation = None
|
|
1126
1151
|
feature_enabled = False
|
|
1127
|
-
|
|
1128
|
-
|
|
1152
|
+
if flag_decision.variation is not None:
|
|
1153
|
+
if flag_decision.variation.featureEnabled:
|
|
1154
|
+
feature_enabled = True
|
|
1155
|
+
|
|
1156
|
+
self.logger.info(f'Feature {flag_key} is enabled for user {user_id} {feature_enabled}"')
|
|
1157
|
+
|
|
1158
|
+
# Create Optimizely Decision Result.
|
|
1159
|
+
attributes = user_context.get_user_attributes()
|
|
1160
|
+
rule_key = flag_decision.experiment.key if flag_decision.experiment else None
|
|
1129
1161
|
all_variables = {}
|
|
1130
|
-
|
|
1131
|
-
decision_source = DecisionSources.ROLLOUT
|
|
1132
|
-
source_info: dict[str, Any] = {}
|
|
1162
|
+
decision_source = flag_decision.source
|
|
1133
1163
|
decision_event_dispatched = False
|
|
1134
1164
|
|
|
1135
|
-
|
|
1136
|
-
optimizely_decision_context = OptimizelyUserContext.OptimizelyDecisionContext(flag_key=key, rule_key=rule_key)
|
|
1137
|
-
forced_decision_response = self.decision_service.validated_forced_decision(config,
|
|
1138
|
-
optimizely_decision_context,
|
|
1139
|
-
user_context)
|
|
1140
|
-
variation, decision_reasons = forced_decision_response
|
|
1141
|
-
reasons += decision_reasons
|
|
1142
|
-
|
|
1143
|
-
if variation:
|
|
1144
|
-
decision = Decision(None, variation, enums.DecisionSources.FEATURE_TEST)
|
|
1145
|
-
else:
|
|
1146
|
-
# Regular decision
|
|
1147
|
-
decision, decision_reasons = self.decision_service.get_variation_for_feature(config,
|
|
1148
|
-
feature_flag,
|
|
1149
|
-
user_context, decide_options)
|
|
1150
|
-
|
|
1151
|
-
reasons += decision_reasons
|
|
1152
|
-
|
|
1153
|
-
# Fill in experiment and variation if returned (rollouts can have featureEnabled variables as well.)
|
|
1154
|
-
if decision.experiment is not None:
|
|
1155
|
-
experiment = decision.experiment
|
|
1156
|
-
source_info["experiment"] = experiment
|
|
1157
|
-
rule_key = experiment.key if experiment else None
|
|
1158
|
-
if decision.variation is not None:
|
|
1159
|
-
variation = decision.variation
|
|
1160
|
-
variation_key = variation.key
|
|
1161
|
-
feature_enabled = variation.featureEnabled
|
|
1162
|
-
decision_source = decision.source
|
|
1163
|
-
source_info["variation"] = variation
|
|
1165
|
+
feature_flag = project_config.feature_key_map.get(flag_key)
|
|
1164
1166
|
|
|
1165
1167
|
# Send impression event if Decision came from a feature
|
|
1166
1168
|
# test and decide options doesn't include disableDecisionEvent
|
|
1167
1169
|
if OptimizelyDecideOption.DISABLE_DECISION_EVENT not in decide_options:
|
|
1168
|
-
if decision_source == DecisionSources.FEATURE_TEST or
|
|
1169
|
-
self._send_impression_event(
|
|
1170
|
-
|
|
1170
|
+
if decision_source == DecisionSources.FEATURE_TEST or project_config.send_flag_decisions:
|
|
1171
|
+
self._send_impression_event(project_config,
|
|
1172
|
+
flag_decision.experiment,
|
|
1173
|
+
flag_decision.variation,
|
|
1174
|
+
flag_key, rule_key or '',
|
|
1175
|
+
str(decision_source), feature_enabled,
|
|
1171
1176
|
user_id, attributes)
|
|
1172
1177
|
|
|
1173
1178
|
decision_event_dispatched = True
|
|
1174
1179
|
|
|
1175
1180
|
# Generate all variables map if decide options doesn't include excludeVariables
|
|
1176
|
-
if OptimizelyDecideOption.EXCLUDE_VARIABLES not in decide_options:
|
|
1181
|
+
if OptimizelyDecideOption.EXCLUDE_VARIABLES not in decide_options and feature_flag:
|
|
1177
1182
|
for variable_key, variable in feature_flag.variables.items():
|
|
1178
1183
|
variable_value = variable.defaultValue
|
|
1179
1184
|
if feature_enabled:
|
|
1180
|
-
variable_value =
|
|
1185
|
+
variable_value = project_config.get_variable_value_for_variation(variable, flag_decision.variation)
|
|
1181
1186
|
self.logger.debug(
|
|
1182
1187
|
f'Got variable value "{variable_value}" for '
|
|
1183
1188
|
f'variable "{variable_key}" of feature flag "{flag_key}".'
|
|
1184
1189
|
)
|
|
1185
1190
|
|
|
1186
1191
|
try:
|
|
1187
|
-
actual_value =
|
|
1192
|
+
actual_value = project_config.get_typecast_value(variable_value, variable.type)
|
|
1188
1193
|
except:
|
|
1189
1194
|
self.logger.error('Unable to cast value. Returning None.')
|
|
1190
1195
|
actual_value = None
|
|
@@ -1192,7 +1197,11 @@ class Optimizely:
|
|
|
1192
1197
|
all_variables[variable_key] = actual_value
|
|
1193
1198
|
|
|
1194
1199
|
should_include_reasons = OptimizelyDecideOption.INCLUDE_REASONS in decide_options
|
|
1195
|
-
|
|
1200
|
+
variation_key = (
|
|
1201
|
+
flag_decision.variation.key
|
|
1202
|
+
if flag_decision is not None and flag_decision.variation is not None
|
|
1203
|
+
else None
|
|
1204
|
+
)
|
|
1196
1205
|
# Send notification
|
|
1197
1206
|
self.notification_center.send_notifications(
|
|
1198
1207
|
enums.NotificationTypes.DECISION,
|
|
@@ -1205,7 +1214,7 @@ class Optimizely:
|
|
|
1205
1214
|
'variables': all_variables,
|
|
1206
1215
|
'variation_key': variation_key,
|
|
1207
1216
|
'rule_key': rule_key,
|
|
1208
|
-
'reasons':
|
|
1217
|
+
'reasons': decision_reasons if should_include_reasons else [],
|
|
1209
1218
|
'decision_event_dispatched': decision_event_dispatched
|
|
1210
1219
|
|
|
1211
1220
|
},
|
|
@@ -1213,7 +1222,7 @@ class Optimizely:
|
|
|
1213
1222
|
|
|
1214
1223
|
return OptimizelyDecision(variation_key=variation_key, enabled=feature_enabled, variables=all_variables,
|
|
1215
1224
|
rule_key=rule_key, flag_key=flag_key,
|
|
1216
|
-
user_context=user_context, reasons=
|
|
1225
|
+
user_context=user_context, reasons=decision_reasons if should_include_reasons else []
|
|
1217
1226
|
)
|
|
1218
1227
|
|
|
1219
1228
|
def _decide_all(
|
|
@@ -1253,7 +1262,8 @@ class Optimizely:
|
|
|
1253
1262
|
self,
|
|
1254
1263
|
user_context: Optional[OptimizelyUserContext],
|
|
1255
1264
|
keys: list[str],
|
|
1256
|
-
decide_options: Optional[list[str]] = None
|
|
1265
|
+
decide_options: Optional[list[str]] = None,
|
|
1266
|
+
ignore_default_options: bool = False
|
|
1257
1267
|
) -> dict[str, OptimizelyDecision]:
|
|
1258
1268
|
"""
|
|
1259
1269
|
Args:
|
|
@@ -1277,19 +1287,74 @@ class Optimizely:
|
|
|
1277
1287
|
merged_decide_options: list[str] = []
|
|
1278
1288
|
if isinstance(decide_options, list):
|
|
1279
1289
|
merged_decide_options = decide_options[:]
|
|
1280
|
-
|
|
1290
|
+
if not ignore_default_options:
|
|
1291
|
+
merged_decide_options += self.default_decide_options
|
|
1281
1292
|
else:
|
|
1282
1293
|
self.logger.debug('Provided decide options is not an array. Using default decide options.')
|
|
1283
1294
|
merged_decide_options = self.default_decide_options
|
|
1284
1295
|
|
|
1285
|
-
|
|
1296
|
+
decisions: dict[str, OptimizelyDecision] = {}
|
|
1297
|
+
valid_keys = []
|
|
1298
|
+
decision_reasons_dict = {}
|
|
1299
|
+
|
|
1300
|
+
project_config = self.config_manager.get_config()
|
|
1301
|
+
flags_without_forced_decision: list[entities.FeatureFlag] = []
|
|
1302
|
+
flag_decisions: dict[str, Decision] = {}
|
|
1286
1303
|
|
|
1287
|
-
|
|
1304
|
+
if project_config is None:
|
|
1305
|
+
return decisions
|
|
1288
1306
|
for key in keys:
|
|
1289
|
-
|
|
1290
|
-
if
|
|
1307
|
+
feature_flag = project_config.feature_key_map.get(key)
|
|
1308
|
+
if feature_flag is None:
|
|
1309
|
+
decisions[key] = OptimizelyDecision(None, False, None, None, key, user_context, [])
|
|
1291
1310
|
continue
|
|
1292
|
-
|
|
1311
|
+
valid_keys.append(key)
|
|
1312
|
+
decision_reasons: list[str] = []
|
|
1313
|
+
decision_reasons_dict[key] = decision_reasons
|
|
1314
|
+
|
|
1315
|
+
optimizely_decision_context = OptimizelyUserContext.OptimizelyDecisionContext(flag_key=key, rule_key=None)
|
|
1316
|
+
forced_decision_response = self.decision_service.validated_forced_decision(project_config,
|
|
1317
|
+
optimizely_decision_context,
|
|
1318
|
+
user_context)
|
|
1319
|
+
variation, decision_reasons = forced_decision_response
|
|
1320
|
+
decision_reasons_dict[key] += decision_reasons
|
|
1321
|
+
|
|
1322
|
+
if variation:
|
|
1323
|
+
decision = Decision(None, variation, enums.DecisionSources.FEATURE_TEST)
|
|
1324
|
+
flag_decisions[key] = decision
|
|
1325
|
+
else:
|
|
1326
|
+
flags_without_forced_decision.append(feature_flag)
|
|
1327
|
+
|
|
1328
|
+
decision_list = self.decision_service.get_variations_for_feature_list(
|
|
1329
|
+
project_config,
|
|
1330
|
+
flags_without_forced_decision,
|
|
1331
|
+
user_context,
|
|
1332
|
+
merged_decide_options
|
|
1333
|
+
)
|
|
1334
|
+
|
|
1335
|
+
for i in range(0, len(flags_without_forced_decision)):
|
|
1336
|
+
decision = decision_list[i][0]
|
|
1337
|
+
reasons = decision_list[i][1]
|
|
1338
|
+
flag_key = flags_without_forced_decision[i].key
|
|
1339
|
+
flag_decisions[flag_key] = decision
|
|
1340
|
+
decision_reasons_dict[flag_key] += reasons
|
|
1341
|
+
|
|
1342
|
+
for key in valid_keys:
|
|
1343
|
+
flag_decision = flag_decisions[key]
|
|
1344
|
+
decision_reasons = decision_reasons_dict[key]
|
|
1345
|
+
optimizely_decision = self._create_optimizely_decision(
|
|
1346
|
+
user_context,
|
|
1347
|
+
key,
|
|
1348
|
+
flag_decision,
|
|
1349
|
+
decision_reasons,
|
|
1350
|
+
merged_decide_options,
|
|
1351
|
+
project_config
|
|
1352
|
+
)
|
|
1353
|
+
enabled_flags_only_missing = OptimizelyDecideOption.ENABLED_FLAGS_ONLY not in merged_decide_options
|
|
1354
|
+
is_enabled = optimizely_decision.enabled
|
|
1355
|
+
if enabled_flags_only_missing or is_enabled:
|
|
1356
|
+
decisions[key] = optimizely_decision
|
|
1357
|
+
|
|
1293
1358
|
return decisions
|
|
1294
1359
|
|
|
1295
1360
|
def _setup_odp(self, sdk_key: Optional[str]) -> None:
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Copyright 2017, 2022, Optimizely
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
# See the License for the specific language governing permissions and
|
|
12
|
+
# limitations under the License.
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
from sys import version_info
|
|
17
|
+
from . import logger as _logging
|
|
18
|
+
|
|
19
|
+
if version_info < (3, 8):
|
|
20
|
+
from typing_extensions import Final
|
|
21
|
+
else:
|
|
22
|
+
from typing import Final, TYPE_CHECKING # type: ignore
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
# prevent circular dependenacy by skipping import at runtime
|
|
26
|
+
from .entities import Experiment, Variation
|
|
27
|
+
from optimizely.error_handler import BaseErrorHandler
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class UserProfile:
|
|
31
|
+
""" Class encapsulating information representing a user's profile.
|
|
32
|
+
|
|
33
|
+
user_id: User's identifier.
|
|
34
|
+
experiment_bucket_map: Dict mapping experiment ID to dict consisting of the
|
|
35
|
+
variation ID identifying the variation for the user.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
USER_ID_KEY: Final = 'user_id'
|
|
39
|
+
EXPERIMENT_BUCKET_MAP_KEY: Final = 'experiment_bucket_map'
|
|
40
|
+
VARIATION_ID_KEY: Final = 'variation_id'
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
user_id: str,
|
|
45
|
+
experiment_bucket_map: Optional[dict[str, dict[str, Optional[str]]]] = None,
|
|
46
|
+
**kwargs: Any
|
|
47
|
+
):
|
|
48
|
+
self.user_id = user_id
|
|
49
|
+
self.experiment_bucket_map = experiment_bucket_map or {}
|
|
50
|
+
|
|
51
|
+
def __eq__(self, other: object) -> bool:
|
|
52
|
+
return self.__dict__ == other.__dict__
|
|
53
|
+
|
|
54
|
+
def get_variation_for_experiment(self, experiment_id: str) -> Optional[str]:
|
|
55
|
+
""" Helper method to retrieve variation ID for given experiment.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
experiment_id: ID for experiment for which variation needs to be looked up for.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Variation ID corresponding to the experiment. None if no decision available.
|
|
62
|
+
"""
|
|
63
|
+
return self.experiment_bucket_map.get(experiment_id, {self.VARIATION_ID_KEY: None}).get(self.VARIATION_ID_KEY)
|
|
64
|
+
|
|
65
|
+
def save_variation_for_experiment(self, experiment_id: str, variation_id: str) -> None:
|
|
66
|
+
""" Helper method to save new experiment/variation as part of the user's profile.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
experiment_id: ID for experiment for which the decision is to be stored.
|
|
70
|
+
variation_id: ID for variation that the user saw.
|
|
71
|
+
"""
|
|
72
|
+
self.experiment_bucket_map.update({experiment_id: {self.VARIATION_ID_KEY: variation_id}})
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class UserProfileService:
|
|
76
|
+
""" Class encapsulating user profile service functionality.
|
|
77
|
+
Override with your own implementation for storing and retrieving the user profile. """
|
|
78
|
+
|
|
79
|
+
def lookup(self, user_id: str) -> dict[str, Any]:
|
|
80
|
+
""" Fetch the user profile dict corresponding to the user ID.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
user_id: ID for user whose profile needs to be retrieved.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Dict representing the user's profile.
|
|
87
|
+
"""
|
|
88
|
+
return UserProfile(user_id).__dict__
|
|
89
|
+
|
|
90
|
+
def save(self, user_profile: dict[str, Any]) -> None:
|
|
91
|
+
""" Save the user profile dict sent to this method.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
user_profile: Dict representing the user's profile.
|
|
95
|
+
"""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class UserProfileTracker:
|
|
100
|
+
def __init__(self,
|
|
101
|
+
user_id: str,
|
|
102
|
+
user_profile_service: Optional[UserProfileService],
|
|
103
|
+
logger: Optional[_logging.Logger] = None):
|
|
104
|
+
self.user_id = user_id
|
|
105
|
+
self.user_profile_service = user_profile_service
|
|
106
|
+
self.logger = _logging.adapt_logger(logger or _logging.NoOpLogger())
|
|
107
|
+
self.profile_updated = False
|
|
108
|
+
self.user_profile = UserProfile(user_id, {})
|
|
109
|
+
|
|
110
|
+
def get_user_profile(self) -> UserProfile:
|
|
111
|
+
return self.user_profile
|
|
112
|
+
|
|
113
|
+
def load_user_profile(self, reasons: Optional[list[str]] = [],
|
|
114
|
+
error_handler: Optional[BaseErrorHandler] = None) -> None:
|
|
115
|
+
if reasons is None:
|
|
116
|
+
reasons = []
|
|
117
|
+
try:
|
|
118
|
+
user_profile = self.user_profile_service.lookup(self.user_id) if self.user_profile_service else None
|
|
119
|
+
if user_profile is None:
|
|
120
|
+
message = "Unable to get a user profile from the UserProfileService."
|
|
121
|
+
reasons.append(message)
|
|
122
|
+
else:
|
|
123
|
+
if 'user_id' in user_profile and 'experiment_bucket_map' in user_profile:
|
|
124
|
+
self.user_profile = UserProfile(
|
|
125
|
+
user_profile['user_id'],
|
|
126
|
+
user_profile['experiment_bucket_map']
|
|
127
|
+
)
|
|
128
|
+
self.logger.info("User profile loaded successfully.")
|
|
129
|
+
else:
|
|
130
|
+
missing_keys = [key for key in ['user_id', 'experiment_bucket_map'] if key not in user_profile]
|
|
131
|
+
message = f"User profile is missing keys: {', '.join(missing_keys)}"
|
|
132
|
+
reasons.append(message)
|
|
133
|
+
except Exception as exception:
|
|
134
|
+
message = str(exception)
|
|
135
|
+
reasons.append(message)
|
|
136
|
+
self.logger.exception(f'Unable to retrieve user profile for user "{self.user_id}" as lookup failed.')
|
|
137
|
+
if error_handler:
|
|
138
|
+
error_handler.handle_error(exception)
|
|
139
|
+
|
|
140
|
+
def update_user_profile(self, experiment: Experiment, variation: Variation) -> None:
|
|
141
|
+
variation_id = variation.id
|
|
142
|
+
experiment_id = experiment.id
|
|
143
|
+
self.user_profile.save_variation_for_experiment(experiment_id, variation_id)
|
|
144
|
+
self.profile_updated = True
|
|
145
|
+
|
|
146
|
+
def save_user_profile(self, error_handler: Optional[BaseErrorHandler] = None) -> None:
|
|
147
|
+
if not self.profile_updated:
|
|
148
|
+
return
|
|
149
|
+
try:
|
|
150
|
+
if self.user_profile_service:
|
|
151
|
+
self.user_profile_service.save(self.user_profile.__dict__)
|
|
152
|
+
self.logger.info(f'Saved user profile of user "{self.user_profile.user_id}".')
|
|
153
|
+
except Exception as exception:
|
|
154
|
+
self.logger.warning(f'Failed to save user profile of user "{self.user_profile.user_id}" '
|
|
155
|
+
f'for exception:{exception}".')
|
|
156
|
+
if error_handler:
|
|
157
|
+
error_handler.handle_error(exception)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: optimizely-sdk
|
|
3
|
-
Version: 5.0
|
|
3
|
+
Version: 5.1.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
|
|
@@ -23,16 +23,16 @@ Requires-Dist: pyrsistent>=0.16.0
|
|
|
23
23
|
Requires-Dist: requests>=2.21
|
|
24
24
|
Requires-Dist: idna>=2.10
|
|
25
25
|
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
26
27
|
Requires-Dist: idna>=2.10; extra == "test"
|
|
27
28
|
Requires-Dist: jsonschema>=3.2.0; extra == "test"
|
|
28
|
-
Requires-Dist: python-coveralls; extra == "test"
|
|
29
|
-
Requires-Dist: flake8>=4.0.1; extra == "test"
|
|
30
|
-
Requires-Dist: pytest>=6.2.0; extra == "test"
|
|
31
|
-
Requires-Dist: pytest-cov; extra == "test"
|
|
32
29
|
Requires-Dist: requests>=2.21; extra == "test"
|
|
33
30
|
Requires-Dist: funcsigs>=0.4; extra == "test"
|
|
34
|
-
Requires-Dist:
|
|
31
|
+
Requires-Dist: pytest>=6.2.0; extra == "test"
|
|
32
|
+
Requires-Dist: python-coveralls; extra == "test"
|
|
35
33
|
Requires-Dist: coverage; extra == "test"
|
|
34
|
+
Requires-Dist: flake8>=4.0.1; extra == "test"
|
|
35
|
+
Requires-Dist: pyrsistent>=0.16.0; extra == "test"
|
|
36
36
|
|
|
37
37
|
Optimizely Feature Experimentation is A/B testing and feature management for product development teams. Experiment in any application. Make every feature on your roadmap an opportunity to learn. Learn more at https://www.optimizely.com/products/experiment/feature-experimentation/ or see our documentation at https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/welcome. # Optimizely Python SDK
|
|
38
38
|
|
|
@@ -292,6 +292,11 @@ idna (BSD 3-Clause License https://github.com/kjd/idna/blob/master/LICENSE.md)
|
|
|
292
292
|
- Swift - https://github.com/optimizely/swift-sdk
|
|
293
293
|
# Optimizely Python SDK Changelog
|
|
294
294
|
|
|
295
|
+
## 5.1.0
|
|
296
|
+
November 27th, 2024
|
|
297
|
+
|
|
298
|
+
Added support for batch processing in DecideAll and DecideForKeys, enabling more efficient handling of multiple decisions in the User Profile Service.([#440](https://github.com/optimizely/python-sdk/pull/440))
|
|
299
|
+
|
|
295
300
|
## 5.0.1
|
|
296
301
|
June 26th, 2024
|
|
297
302
|
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
# Copyright 2017, 2022, Optimizely
|
|
2
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
-
# you may not use this file except in compliance with the License.
|
|
4
|
-
# You may obtain a copy of the License at
|
|
5
|
-
#
|
|
6
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
-
#
|
|
8
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
-
# See the License for the specific language governing permissions and
|
|
12
|
-
# limitations under the License.
|
|
13
|
-
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
from typing import Any, Optional
|
|
16
|
-
from sys import version_info
|
|
17
|
-
|
|
18
|
-
if version_info < (3, 8):
|
|
19
|
-
from typing_extensions import Final
|
|
20
|
-
else:
|
|
21
|
-
from typing import Final # type: ignore
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class UserProfile:
|
|
25
|
-
""" Class encapsulating information representing a user's profile.
|
|
26
|
-
|
|
27
|
-
user_id: User's identifier.
|
|
28
|
-
experiment_bucket_map: Dict mapping experiment ID to dict consisting of the
|
|
29
|
-
variation ID identifying the variation for the user.
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
USER_ID_KEY: Final = 'user_id'
|
|
33
|
-
EXPERIMENT_BUCKET_MAP_KEY: Final = 'experiment_bucket_map'
|
|
34
|
-
VARIATION_ID_KEY: Final = 'variation_id'
|
|
35
|
-
|
|
36
|
-
def __init__(
|
|
37
|
-
self,
|
|
38
|
-
user_id: str,
|
|
39
|
-
experiment_bucket_map: Optional[dict[str, dict[str, Optional[str]]]] = None,
|
|
40
|
-
**kwargs: Any
|
|
41
|
-
):
|
|
42
|
-
self.user_id = user_id
|
|
43
|
-
self.experiment_bucket_map = experiment_bucket_map or {}
|
|
44
|
-
|
|
45
|
-
def __eq__(self, other: object) -> bool:
|
|
46
|
-
return self.__dict__ == other.__dict__
|
|
47
|
-
|
|
48
|
-
def get_variation_for_experiment(self, experiment_id: str) -> Optional[str]:
|
|
49
|
-
""" Helper method to retrieve variation ID for given experiment.
|
|
50
|
-
|
|
51
|
-
Args:
|
|
52
|
-
experiment_id: ID for experiment for which variation needs to be looked up for.
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
Variation ID corresponding to the experiment. None if no decision available.
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
return self.experiment_bucket_map.get(experiment_id, {self.VARIATION_ID_KEY: None}).get(self.VARIATION_ID_KEY)
|
|
59
|
-
|
|
60
|
-
def save_variation_for_experiment(self, experiment_id: str, variation_id: str) -> None:
|
|
61
|
-
""" Helper method to save new experiment/variation as part of the user's profile.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
experiment_id: ID for experiment for which the decision is to be stored.
|
|
65
|
-
variation_id: ID for variation that the user saw.
|
|
66
|
-
"""
|
|
67
|
-
|
|
68
|
-
self.experiment_bucket_map.update({experiment_id: {self.VARIATION_ID_KEY: variation_id}})
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
class UserProfileService:
|
|
72
|
-
""" Class encapsulating user profile service functionality.
|
|
73
|
-
Override with your own implementation for storing and retrieving the user profile. """
|
|
74
|
-
|
|
75
|
-
def lookup(self, user_id: str) -> dict[str, Any]:
|
|
76
|
-
""" Fetch the user profile dict corresponding to the user ID.
|
|
77
|
-
|
|
78
|
-
Args:
|
|
79
|
-
user_id: ID for user whose profile needs to be retrieved.
|
|
80
|
-
|
|
81
|
-
Returns:
|
|
82
|
-
Dict representing the user's profile.
|
|
83
|
-
"""
|
|
84
|
-
return UserProfile(user_id).__dict__
|
|
85
|
-
|
|
86
|
-
def save(self, user_profile: dict[str, Any]) -> None:
|
|
87
|
-
""" Save the user profile dict sent to this method.
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
user_profile: Dict representing the user's profile.
|
|
91
|
-
"""
|
|
92
|
-
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/decision/optimizely_decide_option.py
RENAMED
|
File without changes
|
|
File without changes
|
{optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/decision/optimizely_decision_message.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/condition_tree_evaluator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -4,13 +4,13 @@ requests>=2.21
|
|
|
4
4
|
idna>=2.10
|
|
5
5
|
|
|
6
6
|
[test]
|
|
7
|
+
pytest-cov
|
|
7
8
|
idna>=2.10
|
|
8
9
|
jsonschema>=3.2.0
|
|
9
|
-
python-coveralls
|
|
10
|
-
flake8>=4.0.1
|
|
11
|
-
pytest>=6.2.0
|
|
12
|
-
pytest-cov
|
|
13
10
|
requests>=2.21
|
|
14
11
|
funcsigs>=0.4
|
|
15
|
-
|
|
12
|
+
pytest>=6.2.0
|
|
13
|
+
python-coveralls
|
|
16
14
|
coverage
|
|
15
|
+
flake8>=4.0.1
|
|
16
|
+
pyrsistent>=0.16.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|