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.
Files changed (71) hide show
  1. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/CHANGELOG.md +5 -0
  2. {optimizely-sdk-5.0.1/optimizely_sdk.egg-info → optimizely_sdk-5.1.0}/PKG-INFO +11 -6
  3. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/decision_service.py +108 -61
  4. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/optimizely.py +123 -58
  5. optimizely_sdk-5.1.0/optimizely/user_profile.py +157 -0
  6. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/version.py +1 -1
  7. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0/optimizely_sdk.egg-info}/PKG-INFO +11 -6
  8. optimizely-sdk-5.0.1/optimizely/user_profile.py +0 -92
  9. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/LICENSE +0 -0
  10. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/MANIFEST.in +0 -0
  11. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/README.md +0 -0
  12. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/__init__.py +0 -0
  13. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/bucketer.py +0 -0
  14. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/config_manager.py +0 -0
  15. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/decision/__init__.py +0 -0
  16. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/decision/optimizely_decide_option.py +0 -0
  17. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/decision/optimizely_decision.py +0 -0
  18. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/decision/optimizely_decision_message.py +0 -0
  19. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/entities.py +0 -0
  20. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/error_handler.py +0 -0
  21. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/event/__init__.py +0 -0
  22. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/event/event_factory.py +0 -0
  23. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/event/event_processor.py +0 -0
  24. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/event/log_event.py +0 -0
  25. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/event/payload.py +0 -0
  26. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/event/user_event.py +0 -0
  27. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/event/user_event_factory.py +0 -0
  28. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/event_builder.py +0 -0
  29. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/event_dispatcher.py +0 -0
  30. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/exceptions.py +0 -0
  31. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/__init__.py +0 -0
  32. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/audience.py +0 -0
  33. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/condition.py +0 -0
  34. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/condition_tree_evaluator.py +0 -0
  35. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/constants.py +0 -0
  36. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/enums.py +0 -0
  37. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/event_tag_utils.py +0 -0
  38. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/experiment.py +0 -0
  39. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/sdk_settings.py +0 -0
  40. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/types.py +0 -0
  41. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/helpers/validator.py +0 -0
  42. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/lib/__init__.py +0 -0
  43. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/lib/pymmh3.py +0 -0
  44. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/logger.py +0 -0
  45. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/notification_center.py +0 -0
  46. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/notification_center_registry.py +0 -0
  47. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/__init__.py +0 -0
  48. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/lru_cache.py +0 -0
  49. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/odp_config.py +0 -0
  50. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/odp_event.py +0 -0
  51. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/odp_event_api_manager.py +0 -0
  52. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/odp_event_manager.py +0 -0
  53. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/odp_manager.py +0 -0
  54. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/odp_segment_api_manager.py +0 -0
  55. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/odp_segment_manager.py +0 -0
  56. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/odp/optimizely_odp_option.py +0 -0
  57. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/optimizely_config.py +0 -0
  58. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/optimizely_factory.py +0 -0
  59. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/optimizely_user_context.py +0 -0
  60. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/project_config.py +0 -0
  61. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely/py.typed +0 -0
  62. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely_sdk.egg-info/SOURCES.txt +0 -0
  63. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely_sdk.egg-info/dependency_links.txt +0 -0
  64. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely_sdk.egg-info/requires.txt +5 -5
  65. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/optimizely_sdk.egg-info/top_level.txt +0 -0
  66. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/requirements/core.txt +0 -0
  67. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/requirements/docs.txt +0 -0
  68. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/requirements/test.txt +0 -0
  69. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/requirements/typing.txt +0 -0
  70. {optimizely-sdk-5.0.1 → optimizely_sdk-5.1.0}/setup.cfg +0 -0
  71. {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.1
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: pyrsistent>=0.16.0; extra == "test"
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
- user_profile = UserProfile(user_id)
300
- if not ignore_user_profile and self.user_profile_service:
301
- try:
302
- retrieved_profile = self.user_profile_service.lookup(user_id)
303
- except:
304
- self.logger.exception(f'Unable to retrieve user profile for user "{user_id}" as lookup failed.')
305
- retrieved_profile = None
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 ignore_user_profile and self.user_profile_service:
340
+ if user_profile_tracker is not None and not ignore_user_profile:
344
341
  try:
345
- user_profile.save_variation_for_experiment(experiment.id, variation.id)
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
- decide_reasons = []
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
- variation, _ = self.decision_service.get_variation(project_config, experiment, user_context)
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
- # Create Optimizely Decision Result.
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
- rule_key = None
1128
- flag_key = key
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
- experiment = None
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
- # Check forced decisions first
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 config.send_flag_decisions:
1169
- self._send_impression_event(config, experiment, variation, flag_key, rule_key or '',
1170
- decision_source, feature_enabled,
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 = config.get_variable_value_for_variation(variable, decision.variation)
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 = config.get_typecast_value(variable_value, variable.type)
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': reasons if should_include_reasons else [],
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=reasons if should_include_reasons else []
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
- merged_decide_options += self.default_decide_options
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
- enabled_flags_only = OptimizelyDecideOption.ENABLED_FLAGS_ONLY in merged_decide_options
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
- decisions = {}
1304
+ if project_config is None:
1305
+ return decisions
1288
1306
  for key in keys:
1289
- decision = self._decide(user_context, key, decide_options)
1290
- if enabled_flags_only and not decision.enabled:
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
- decisions[key] = decision
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)
@@ -11,5 +11,5 @@
11
11
  # See the License for the specific language governing permissions and
12
12
  # limitations under the License.
13
13
 
14
- version_info = (5, 0, 1)
14
+ version_info = (5, 1, 0)
15
15
  __version__ = '.'.join(str(v) for v in version_info)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: optimizely-sdk
3
- Version: 5.0.1
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: pyrsistent>=0.16.0; extra == "test"
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
@@ -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
- pyrsistent>=0.16.0
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