1
1
import datetime
2
2
import logging
3
3
import json
4
- from typing import Any , Dict , Optional
4
+ from typing import Any , Dict , Optional , Union
5
5
from eppo_client .assignment_logger import AssignmentLogger
6
- from eppo_client .bandit import BanditEvaluator , BanditResult , Attributes , ActionContexts
6
+ from eppo_client .bandit import (
7
+ ActionContextsDict ,
8
+ BanditEvaluator ,
9
+ BanditResult ,
10
+ Attributes ,
11
+ ActionContexts ,
12
+ )
7
13
from eppo_client .configuration_requestor import (
8
14
ExperimentConfigurationRequestor ,
9
15
)
10
16
from eppo_client .constants import POLL_INTERVAL_MILLIS , POLL_JITTER_MILLIS
11
17
from eppo_client .models import VariationType
12
18
from eppo_client .poller import Poller
13
19
from eppo_client .sharders import MD5Sharder
14
- from eppo_client .types import SubjectAttributes , ValueType
20
+ from eppo_client .types import AttributesDict , ValueType
15
21
from eppo_client .validation import validate_not_blank
16
22
from eppo_client .eval import FlagEvaluation , Evaluator , none_result
17
23
from eppo_client .version import __version__
@@ -43,7 +49,7 @@ def get_string_assignment(
43
49
self ,
44
50
flag_key : str ,
45
51
subject_key : str ,
46
- subject_attributes : SubjectAttributes ,
52
+ subject_attributes : AttributesDict ,
47
53
default : str ,
48
54
) -> str :
49
55
return self .get_assignment_variation (
@@ -58,7 +64,7 @@ def get_integer_assignment(
58
64
self ,
59
65
flag_key : str ,
60
66
subject_key : str ,
61
- subject_attributes : SubjectAttributes ,
67
+ subject_attributes : AttributesDict ,
62
68
default : int ,
63
69
) -> int :
64
70
return self .get_assignment_variation (
@@ -73,7 +79,7 @@ def get_numeric_assignment(
73
79
self ,
74
80
flag_key : str ,
75
81
subject_key : str ,
76
- subject_attributes : SubjectAttributes ,
82
+ subject_attributes : AttributesDict ,
77
83
default : float ,
78
84
) -> float :
79
85
# convert to float in case we get an int
@@ -91,7 +97,7 @@ def get_boolean_assignment(
91
97
self ,
92
98
flag_key : str ,
93
99
subject_key : str ,
94
- subject_attributes : SubjectAttributes ,
100
+ subject_attributes : AttributesDict ,
95
101
default : bool ,
96
102
) -> bool :
97
103
return self .get_assignment_variation (
@@ -106,7 +112,7 @@ def get_json_assignment(
106
112
self ,
107
113
flag_key : str ,
108
114
subject_key : str ,
109
- subject_attributes : SubjectAttributes ,
115
+ subject_attributes : AttributesDict ,
110
116
default : Dict [Any , Any ],
111
117
) -> Dict [Any , Any ]:
112
118
json_value = self .get_assignment_variation (
@@ -125,7 +131,7 @@ def get_assignment_variation(
125
131
self ,
126
132
flag_key : str ,
127
133
subject_key : str ,
128
- subject_attributes : SubjectAttributes ,
134
+ subject_attributes : AttributesDict ,
129
135
default : Optional [ValueType ],
130
136
expected_variation_type : VariationType ,
131
137
):
@@ -149,7 +155,7 @@ def get_assignment_detail(
149
155
self ,
150
156
flag_key : str ,
151
157
subject_key : str ,
152
- subject_attributes : SubjectAttributes ,
158
+ subject_attributes : AttributesDict ,
153
159
expected_variation_type : VariationType ,
154
160
) -> FlagEvaluation :
155
161
"""Maps a subject to a variation for a given flag
@@ -225,8 +231,8 @@ def get_bandit_action(
225
231
self ,
226
232
flag_key : str ,
227
233
subject_key : str ,
228
- subject_context : Attributes ,
229
- actions : ActionContexts ,
234
+ subject_context : Union [ Attributes , AttributesDict ] ,
235
+ actions : Union [ ActionContexts , ActionContextsDict ] ,
230
236
default : str ,
231
237
) -> BanditResult :
232
238
"""
@@ -244,9 +250,11 @@ def get_bandit_action(
244
250
Args:
245
251
flag_key (str): The feature flag key that contains the bandit as one of the variations.
246
252
subject_key (str): The key identifying the subject.
247
- subject_context (Attributes): The subject context
248
- actions (Dict[str, Attributes]): The dictionary that maps action keys
253
+ subject_context (Attributes | AttributesDict): The subject context.
254
+ If supplying an AttributesDict, it gets converted to an Attributes instance
255
+ actions (ActionContexts | ActionContextsDict): The dictionary that maps action keys
249
256
to their context of actions with their contexts.
257
+ If supplying an AttributesDict, it gets converted to an Attributes instance.
250
258
default (str): The default variation to use if the subject is not part of the bandit.
251
259
252
260
Returns:
@@ -264,7 +272,8 @@ def get_bandit_action(
264
272
categorical_attributes={"country": "USA"}),
265
273
{
266
274
"action1": Attributes(numeric_attributes={"price": 10.0}, categorical_attributes={"category": "A"}),
267
- "action2": Attributes.empty()
275
+ "action2": {"price": 10.0, "category": "B"}
276
+ "action3": Attributes.empty(),
268
277
},
269
278
"default"
270
279
)
@@ -273,7 +282,6 @@ def get_bandit_action(
273
282
else:
274
283
do_action(result.action)
275
284
"""
276
-
277
285
try :
278
286
return self .get_bandit_action_detail (
279
287
flag_key ,
@@ -292,14 +300,21 @@ def get_bandit_action_detail(
292
300
self ,
293
301
flag_key : str ,
294
302
subject_key : str ,
295
- subject_context : Attributes ,
296
- actions : ActionContexts ,
303
+ subject_context : Union [ Attributes , AttributesDict ] ,
304
+ actions : Union [ ActionContexts , ActionContextsDict ] ,
297
305
default : str ,
298
306
) -> BanditResult :
307
+ subject_attributes = convert_subject_context_to_attributes (subject_context )
308
+ action_contexts = convert_actions_to_action_contexts (actions )
309
+
299
310
# get experiment assignment
300
311
# ignoring type because Dict[str, str] satisfies Dict[str, str | ...] but mypy does not understand
301
312
variation = self .get_string_assignment (
302
- flag_key , subject_key , subject_context .categorical_attributes , default # type: ignore
313
+ flag_key ,
314
+ subject_key ,
315
+ subject_attributes .categorical_attributes
316
+ | subject_attributes .numeric_attributes , # type: ignore
317
+ default ,
303
318
)
304
319
305
320
# if the variation is not the bandit key, then the subject is not allocated in the bandit
@@ -318,8 +333,8 @@ def get_bandit_action_detail(
318
333
evaluation = self .__bandit_evaluator .evaluate_bandit (
319
334
flag_key ,
320
335
subject_key ,
321
- subject_context ,
322
- actions ,
336
+ subject_attributes ,
337
+ action_contexts ,
323
338
bandit_data .model_data ,
324
339
)
325
340
@@ -334,12 +349,12 @@ def get_bandit_action_detail(
334
349
"modelVersion" : bandit_data .model_version if evaluation else None ,
335
350
"timestamp" : datetime .datetime .utcnow ().isoformat (),
336
351
"subjectNumericAttributes" : (
337
- subject_context .numeric_attributes
352
+ subject_attributes .numeric_attributes
338
353
if evaluation .subject_attributes
339
354
else {}
340
355
),
341
356
"subjectCategoricalAttributes" : (
342
- subject_context .categorical_attributes
357
+ subject_attributes .categorical_attributes
343
358
if evaluation .subject_attributes
344
359
else {}
345
360
),
@@ -410,3 +425,20 @@ def check_value_type_match(
410
425
if expected_type == VariationType .BOOLEAN :
411
426
return isinstance (value , bool )
412
427
return False
428
+
429
+
430
+ def convert_subject_context_to_attributes (
431
+ subject_context : Union [Attributes , AttributesDict ]
432
+ ) -> Attributes :
433
+ if isinstance (subject_context , dict ):
434
+ return Attributes .from_dict (subject_context )
435
+ return subject_context
436
+
437
+
438
+ def convert_actions_to_action_contexts (
439
+ actions : Union [ActionContexts , ActionContextsDict ]
440
+ ) -> ActionContexts :
441
+ return {
442
+ k : Attributes .from_dict (v ) if isinstance (v , dict ) else v
443
+ for k , v in actions .items ()
444
+ }
0 commit comments