Skip to content

Commit 67a0be8

Browse files
update: Implement DefaultCmabService
1 parent 046d457 commit 67a0be8

File tree

3 files changed

+100
-0
lines changed

3 files changed

+100
-0
lines changed

optimizely/cmab/cmab_service.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import uuid
2+
import json
3+
import hashlib
4+
5+
from typing import Optional, List, TypedDict
6+
from optimizely.cmab.cmab_client import DefaultCmabClient
7+
from optimizely.odp.lru_cache import LRUCache
8+
from optimizely.optimizely_user_context import OptimizelyUserContext, UserAttributes
9+
from optimizely.project_config import ProjectConfig
10+
from optimizely.decision.optimizely_decide_option import OptimizelyDecideOption
11+
from optimizely import logger as _logging
12+
13+
14+
class CmabDecision(TypedDict):
15+
variation_id: str
16+
cmab_uuid: str
17+
18+
19+
class CmabCacheValue(TypedDict):
20+
attributes_hash: str
21+
variation_id: str
22+
cmab_uuid: str
23+
24+
25+
class DefaultCmabService:
26+
def __init__(self, cmab_cache: LRUCache[str, CmabCacheValue],
27+
cmab_client: DefaultCmabClient, logger: Optional[_logging.Logger] = None):
28+
self.cmab_cache = cmab_cache
29+
self.cmab_client = cmab_client
30+
self.logger = logger
31+
32+
def get_decision(self, project_config: ProjectConfig, user_context: OptimizelyUserContext,
33+
rule_id: str, options: List[str]) -> CmabDecision:
34+
35+
filtered_attributes = self._filter_attributes(project_config, user_context, rule_id)
36+
37+
if OptimizelyDecideOption.IGNORE_CMAB_CACHE in options:
38+
return self._fetch_decision(rule_id, user_context.user_id, filtered_attributes)
39+
40+
if OptimizelyDecideOption.RESET_CMAB_CACHE in options:
41+
self.cmab_cache.reset()
42+
43+
cache_key = self._get_cache_key(user_context.user_id, rule_id)
44+
45+
if OptimizelyDecideOption.INVALIDATE_USER_CMAB_CACHE in options:
46+
self.cmab_cache.remove(cache_key)
47+
48+
cached_value = self.cmab_cache.lookup(cache_key)
49+
50+
attributes_hash = self._hash_attributes(filtered_attributes)
51+
52+
if cached_value :
53+
if cached_value['attributes_hash'] == attributes_hash:
54+
return CmabDecision(variation_id=cached_value['variation_id'], cmab_uuid=cached_value['cmab_uuid'])
55+
else:
56+
self.cmab_cache.remove(cache_key)
57+
58+
cmab_decision = self._fetch_decision(rule_id, user_context.user_id, filtered_attributes)
59+
self.cmab_cache.save(cache_key, {
60+
'attributes_hash': attributes_hash,
61+
'variation_id': cmab_decision['variation_id'],
62+
'cmab_uuid': cmab_decision['cmab_uuid'],
63+
})
64+
return cmab_decision
65+
66+
def _fetch_decision(self, rule_id: str, user_id: str, attributes: UserAttributes) -> CmabDecision:
67+
cmab_uuid = str(uuid.uuid4())
68+
variation_id = self.cmab_client.fetch_decision(rule_id, user_id, attributes, cmab_uuid)
69+
cmab_decision = CmabDecision(variation_id=variation_id, cmab_uuid=cmab_uuid)
70+
return cmab_decision
71+
72+
def _filter_attributes(self, project_config: ProjectConfig,
73+
user_context: OptimizelyUserContext, rule_id: str) -> UserAttributes:
74+
user_attributes = user_context.get_user_attributes()
75+
filtered_user_attributes = UserAttributes({})
76+
77+
experiment = project_config.experiment_id_map.get(rule_id)
78+
if not experiment or not experiment.cmab:
79+
return filtered_user_attributes
80+
81+
cmab_attribute_ids = experiment.cmab['attributeIds']
82+
for attribute_id in cmab_attribute_ids:
83+
attribute = project_config.attribute_id_map.get(attribute_id)
84+
if attribute and attribute.key in user_attributes:
85+
filtered_user_attributes[attribute.key] = user_attributes[attribute.key]
86+
87+
return filtered_user_attributes
88+
89+
def _get_cache_key(self, user_id: str, rule_id: str) -> str:
90+
return f"{len(user_id)}-{user_id}-{rule_id}"
91+
92+
def _hash_attributes(self, attributes: UserAttributes) -> str:
93+
sorted_attrs = json.dumps(attributes, sort_keys=True)
94+
return hashlib.md5(sorted_attrs.encode()).hexdigest()

optimizely/decision/optimizely_decide_option.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,6 @@ class OptimizelyDecideOption:
2525
IGNORE_USER_PROFILE_SERVICE: Final = 'IGNORE_USER_PROFILE_SERVICE'
2626
INCLUDE_REASONS: Final = 'INCLUDE_REASONS'
2727
EXCLUDE_VARIABLES: Final = 'EXCLUDE_VARIABLES'
28+
IGNORE_CMAB_CACHE: Final = "IGNORE_CMAB_CACHE"
29+
RESET_CMAB_CACHE: Final = "RESET_CMAB_CACHE"
30+
INVALIDATE_USER_CMAB_CACHE: Final = "INVALIDATE_USER_CMAB_CACHE"

optimizely/project_config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ def __init__(self, datafile: str | bytes, logger: Logger, error_handler: Any):
9797
self.attribute_id_to_key_map: dict[str, str] = {}
9898
for attribute in self.attributes:
9999
self.attribute_id_to_key_map[attribute['id']] = attribute['key']
100+
self.attribute_id_map: dict[str, entities.Attribute] = self._generate_key_map(
101+
self.attributes, '', entities.Attribute
102+
)
100103
self.audience_id_map: dict[str, entities.Audience] = self._generate_key_map(
101104
self.audiences, 'id', entities.Audience
102105
)

0 commit comments

Comments
 (0)