13
13
from .evaluationcontext import EvaluationContext
14
14
from .evaluationlogbuilder import EvaluationLogBuilder
15
15
from .logger import Logger
16
+ from datetime import datetime
16
17
17
18
from .user import User
18
- from .utils import unicode_to_utf8 , encode_utf8
19
+ from .utils import unicode_to_utf8 , encode_utf8 , get_seconds_since_epoch
19
20
20
21
21
22
def sha256 (value_utf8 , salt , context_salt ):
@@ -142,26 +143,42 @@ def _format_rule(self, comparison_attribute, comparator, comparison_value):
142
143
% (comparison_attribute , comparator_text ,
143
144
EvaluationLogBuilder .trunc_comparison_value_if_needed (comparator , comparison_value ))
144
145
145
- def _get_user_attribute (self , context , user , attribute ):
146
- user_attribute = user .get_attribute (attribute )
147
- if user_attribute is not None :
148
- try :
149
- string_types = (str , unicode ) # Python 2.7
150
- except NameError :
151
- string_types = (str ,) # Python 3.x
152
-
153
- if not isinstance (user_attribute , string_types ):
154
- if attribute not in context .type_mismatched_logged_user_attributes :
155
- self .log .warning ('Evaluation of setting \' %s\' may not produce the expected result '
156
- '(the User.%s attribute is not a string value, thus it was converted to \' %s\' '
157
- 'using the runtime\' s default conversion). User Object attribute values should be '
158
- 'passed as strings. You can use the static helper methods of the `User` class to '
159
- 'produce attribute values with the correct type and format.' ,
160
- context .key , attribute , user_attribute , event_id = 4004 )
161
- context .type_mismatched_logged_user_attributes .add (attribute )
162
- user_attribute = str (user_attribute )
163
-
164
- return user_attribute
146
+ def _user_attribute_value_to_string (self , value ):
147
+ if value is None :
148
+ return None
149
+
150
+ if isinstance (value , datetime ):
151
+ value = self ._get_user_attribute_value_as_seconds_since_epoch (value )
152
+ elif isinstance (value , list ):
153
+ value = self .get_user_attribute_value_as_string_list (value )
154
+
155
+ return str (value )
156
+
157
+ def _get_user_attribute_value_as_text (self , attribute_name , attribute_value , condition , key ):
158
+ if isinstance (attribute_value , str ):
159
+ return attribute_value
160
+
161
+ self .log .warning ('Evaluation of condition (%s) for setting \' %s\' may not produce the expected result '
162
+ '(the User.%s attribute is not a string value, thus it was automatically converted to '
163
+ 'the string value \' %s\' ). Please make sure that using a non-string value was intended.' ,
164
+ condition , key , attribute_name , attribute_value , event_id = 3005 )
165
+ return self ._user_attribute_value_to_string (attribute_value )
166
+
167
+ def _get_user_attribute_value_as_seconds_since_epoch (self , attribute_value ):
168
+ if isinstance (attribute_value , datetime ):
169
+ return get_seconds_since_epoch (attribute_value )
170
+
171
+ return float (str (attribute_value ).replace ("," , "." ))
172
+
173
+ def get_user_attribute_value_as_string_list (self , attribute_value ):
174
+ if not isinstance (attribute_value , list ):
175
+ attribute_value_list = json .loads (attribute_value )
176
+ else :
177
+ attribute_value_list = attribute_value
178
+ if not isinstance (attribute_value_list , list ):
179
+ raise ValueError ()
180
+
181
+ return attribute_value_list
165
182
166
183
def _handle_invalid_user_attribute (self , comparison_attribute , comparator , comparison_value , key , validation_error ):
167
184
"""
@@ -201,7 +218,7 @@ def _evaluate_percentage_options(self, percentage_options, context, percentage_r
201
218
202
219
user_attribute_name = percentage_rule_attribute if percentage_rule_attribute is not None else 'Identifier'
203
220
if percentage_rule_attribute is not None :
204
- user_key = self . _get_user_attribute ( context , user , percentage_rule_attribute )
221
+ user_key = user . get_attribute ( percentage_rule_attribute )
205
222
else :
206
223
user_key = user .get_identifier ()
207
224
if percentage_rule_attribute is not None and user_key is None :
@@ -218,7 +235,7 @@ def _evaluate_percentage_options(self, percentage_options, context, percentage_r
218
235
'Skipping %% options because the User.%s attribute is missing.' % user_attribute_name )
219
236
return False , None , None , None
220
237
221
- hash_candidate = ('%s%s' % (key , user_key )).encode ('utf-8' )
238
+ hash_candidate = ('%s%s' % (key , self . _user_attribute_value_to_string ( user_key ) )).encode ('utf-8' )
222
239
hash_val = int (hashlib .sha1 (hash_candidate ).hexdigest ()[:7 ], 16 ) % 100
223
240
224
241
bucket = 0
@@ -463,13 +480,14 @@ def _evaluate_user_condition(self, user_condition, context, context_salt, salt,
463
480
comparison_attribute = user_condition .get (COMPARISON_ATTRIBUTE )
464
481
comparator = user_condition .get (COMPARATOR )
465
482
comparison_value = user_condition .get (COMPARISON_VALUES [comparator ])
483
+ condition = self ._format_rule (comparison_attribute , comparator , comparison_value )
466
484
error = None
467
485
468
486
if comparison_attribute is None :
469
487
raise ValueError ('Comparison attribute name is missing.' )
470
488
471
489
if log_builder :
472
- log_builder .append (self . _format_rule ( comparison_attribute , comparator , comparison_value ) + ' ' )
490
+ log_builder .append (condition + ' ' )
473
491
474
492
if user is None :
475
493
if not context .is_missing_user_object_logged :
@@ -483,33 +501,35 @@ def _evaluate_user_condition(self, user_condition, context, context_salt, salt,
483
501
error = 'cannot evaluate, User Object is missing'
484
502
return False , error
485
503
486
- user_value = self . _get_user_attribute ( context , user , comparison_attribute )
504
+ user_value = user . get_attribute ( comparison_attribute )
487
505
if user_value is None or not user_value :
488
506
self .log .warning ('Cannot evaluate condition (%s) for setting \' %s\' '
489
507
'(the User.%s attribute is missing). You should set the User.%s attribute in order to make '
490
508
'targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/' ,
491
- self ._format_rule (comparison_attribute , comparator , comparison_value ), key ,
492
- comparison_attribute , comparison_attribute ,
493
- event_id = 3003 )
509
+ condition , key , comparison_attribute , comparison_attribute , event_id = 3003 )
494
510
error = 'cannot evaluate, the User.{} attribute is missing' .format (comparison_attribute )
495
511
return False , error
496
512
497
513
# IS ONE OF
498
514
if comparator == Comparator .IS_ONE_OF :
499
- if str (user_value ) in comparison_value :
515
+ user_value = self ._get_user_attribute_value_as_text (comparison_attribute , user_value , condition , key )
516
+ if user_value in comparison_value :
500
517
return True , error
501
518
# IS NOT ONE OF
502
519
elif comparator == Comparator .IS_NOT_ONE_OF :
503
- if str (user_value ) not in comparison_value :
520
+ user_value = self ._get_user_attribute_value_as_text (comparison_attribute , user_value , condition , key )
521
+ if user_value not in comparison_value :
504
522
return True , error
505
523
# CONTAINS ANY OF
506
524
elif comparator == Comparator .CONTAINS_ANY_OF :
525
+ user_value = self ._get_user_attribute_value_as_text (comparison_attribute , user_value , condition , key )
507
526
for comparison in comparison_value :
508
- if str ( comparison ) in str ( user_value ) :
527
+ if comparison in user_value :
509
528
return True , error
510
529
# NOT CONTAINS ANY OF
511
530
elif comparator == Comparator .NOT_CONTAINS_ANY_OF :
512
- if not any (str (comparison ) in str (user_value ) for comparison in comparison_value ):
531
+ user_value = self ._get_user_attribute_value_as_text (comparison_attribute , user_value , condition , key )
532
+ if not any (comparison in user_value for comparison in comparison_value ):
513
533
return True , error
514
534
# IS ONE OF, IS NOT ONE OF (Semantic version)
515
535
elif Comparator .IS_ONE_OF_SEMVER <= comparator <= Comparator .IS_NOT_ONE_OF_SEMVER :
@@ -557,16 +577,18 @@ def _evaluate_user_condition(self, user_condition, context, context_salt, salt,
557
577
return True , error
558
578
# IS ONE OF (hashed)
559
579
elif comparator == Comparator .IS_ONE_OF_HASHED :
580
+ user_value = self ._get_user_attribute_value_as_text (comparison_attribute , user_value , condition , key )
560
581
if sha256 (encode_utf8 (user_value ), salt , context_salt ) in comparison_value :
561
582
return True , error
562
583
# IS NOT ONE OF (hashed)
563
584
elif comparator == Comparator .IS_NOT_ONE_OF_HASHED :
585
+ user_value = self ._get_user_attribute_value_as_text (comparison_attribute , user_value , condition , key )
564
586
if sha256 (encode_utf8 (user_value ), salt , context_salt ) not in comparison_value :
565
587
return True , error
566
588
# BEFORE, AFTER (UTC datetime)
567
589
elif Comparator .BEFORE_DATETIME <= comparator <= Comparator .AFTER_DATETIME :
568
590
try :
569
- user_value_float = float ( str ( user_value ). replace ( "," , "." ) )
591
+ user_value_float = self . _get_user_attribute_value_as_seconds_since_epoch ( user_value )
570
592
except ValueError :
571
593
validation_error = "'%s' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)" % \
572
594
str (user_value )
@@ -581,17 +603,20 @@ def _evaluate_user_condition(self, user_condition, context, context_salt, salt,
581
603
return True , error
582
604
# EQUALS (hashed)
583
605
elif comparator == Comparator .EQUALS_HASHED :
606
+ user_value = self ._get_user_attribute_value_as_text (comparison_attribute , user_value , condition , key )
584
607
if sha256 (encode_utf8 (user_value ), salt , context_salt ) == comparison_value :
585
608
return True , error
586
609
# NOT EQUALS (hashed)
587
610
elif comparator == Comparator .NOT_EQUALS_HASHED :
611
+ user_value = self ._get_user_attribute_value_as_text (comparison_attribute , user_value , condition , key )
588
612
if sha256 (encode_utf8 (user_value ), salt , context_salt ) != comparison_value :
589
613
return True , error
590
614
# STARTS WITH ANY OF, NOT STARTS WITH ANY OF, ENDS WITH ANY OF, NOT ENDS WITH ANY OF (hashed)
591
615
elif Comparator .STARTS_WITH_ANY_OF_HASHED <= comparator <= Comparator .NOT_ENDS_WITH_ANY_OF_HASHED :
592
616
for comparison in comparison_value :
593
617
underscore_index = comparison .index ('_' )
594
618
length = int (comparison [:underscore_index ])
619
+ user_value = self ._get_user_attribute_value_as_text (comparison_attribute , user_value , condition , key )
595
620
user_value_utf8 = encode_utf8 (user_value )
596
621
597
622
if len (user_value_utf8 ) >= length :
@@ -613,14 +638,12 @@ def _evaluate_user_condition(self, user_condition, context, context_salt, salt,
613
638
# ARRAY CONTAINS ANY OF, ARRAY NOT CONTAINS ANY OF (hashed)
614
639
elif Comparator .ARRAY_CONTAINS_ANY_OF_HASHED <= comparator <= Comparator .ARRAY_NOT_CONTAINS_ANY_OF_HASHED :
615
640
try :
616
- user_value_list = json .loads (user_value )
617
- if not isinstance (user_value_list , list ):
618
- raise ValueError ()
641
+ user_value_list = self .get_user_attribute_value_as_string_list (user_value )
619
642
620
643
if sys .version_info [0 ] == 2 :
621
644
user_value_list = unicode_to_utf8 (user_value_list ) # On Python 2.7, convert unicode to utf-8
622
645
except ValueError :
623
- validation_error = "'%s' is not a valid JSON string array" % str (user_value )
646
+ validation_error = "'%s' is not a valid string array" % str (user_value )
624
647
error = self ._handle_invalid_user_attribute (comparison_attribute , comparator , comparison_value , key ,
625
648
validation_error )
626
649
return False , error
@@ -637,14 +660,17 @@ def _evaluate_user_condition(self, user_condition, context, context_salt, salt,
637
660
return True , error
638
661
# EQUALS
639
662
elif comparator == Comparator .EQUALS :
663
+ user_value = self ._get_user_attribute_value_as_text (comparison_attribute , user_value , condition , key )
640
664
if user_value == comparison_value :
641
665
return True , error
642
666
# NOT EQUALS
643
667
elif comparator == Comparator .NOT_EQUALS :
668
+ user_value = self ._get_user_attribute_value_as_text (comparison_attribute , user_value , condition , key )
644
669
if user_value != comparison_value :
645
670
return True , error
646
671
# STARTS WITH ANY OF, NOT STARTS WITH ANY OF, ENDS WITH ANY OF, NOT ENDS WITH ANY OF
647
672
elif Comparator .STARTS_WITH_ANY_OF <= comparator <= Comparator .NOT_ENDS_WITH_ANY_OF :
673
+ user_value = self ._get_user_attribute_value_as_text (comparison_attribute , user_value , condition , key )
648
674
for comparison in comparison_value :
649
675
if (comparator == Comparator .STARTS_WITH_ANY_OF and user_value .startswith (comparison )) or \
650
676
(comparator == Comparator .ENDS_WITH_ANY_OF and user_value .endswith (comparison )):
@@ -659,14 +685,12 @@ def _evaluate_user_condition(self, user_condition, context, context_salt, salt,
659
685
# ARRAY CONTAINS ANY OF, ARRAY NOT CONTAINS ANY OF
660
686
elif Comparator .ARRAY_CONTAINS_ANY_OF <= comparator <= Comparator .ARRAY_NOT_CONTAINS_ANY_OF :
661
687
try :
662
- user_value_list = json .loads (user_value )
663
- if not isinstance (user_value_list , list ):
664
- raise ValueError ()
688
+ user_value_list = self .get_user_attribute_value_as_string_list (user_value )
665
689
666
690
if sys .version_info [0 ] == 2 :
667
691
user_value_list = unicode_to_utf8 (user_value_list ) # On Python 2.7, convert unicode to utf-8
668
692
except ValueError :
669
- validation_error = "'%s' is not a valid JSON string array" % str (user_value )
693
+ validation_error = "'%s' is not a valid string array" % str (user_value )
670
694
error = self ._handle_invalid_user_attribute (comparison_attribute , comparator , comparison_value , key ,
671
695
validation_error )
672
696
return False , error
0 commit comments