2
2
import hashlib
3
3
import logging
4
4
import re
5
+ from typing import Optional
5
6
6
7
from dateutil import parser
8
+ from dateutil .relativedelta import relativedelta
7
9
8
10
from posthog .utils import convert_to_datetime_aware , is_valid_regex
9
11
@@ -117,15 +119,17 @@ def match_property(property, property_values) -> bool:
117
119
118
120
override_value = property_values [key ]
119
121
120
- if operator == "exact" :
121
- if isinstance (value , list ):
122
- return override_value in value
123
- return value == override_value
122
+ if operator in ("exact" , "is_not" ):
124
123
125
- if operator == "is_not" :
126
- if isinstance (value , list ):
127
- return override_value not in value
128
- return value != override_value
124
+ def compute_exact_match (value , override_value ):
125
+ if isinstance (value , list ):
126
+ return str (override_value ).lower () in [str (val ).lower () for val in value ]
127
+ return str (value ).lower () == str (override_value ).lower ()
128
+
129
+ if operator == "exact" :
130
+ return compute_exact_match (value , override_value )
131
+ else :
132
+ return not compute_exact_match (value , override_value )
129
133
130
134
if operator == "is_set" :
131
135
return key in property_values
@@ -142,41 +146,64 @@ def match_property(property, property_values) -> bool:
142
146
if operator == "not_regex" :
143
147
return is_valid_regex (str (value )) and re .compile (str (value )).search (str (override_value )) is None
144
148
145
- if operator == "gt" :
146
- return type (override_value ) is type (value ) and override_value > value
147
-
148
- if operator == "gte" :
149
- return type (override_value ) is type (value ) and override_value >= value
149
+ if operator in ("gt" , "gte" , "lt" , "lte" ):
150
+ # :TRICKY: We adjust comparison based on the override value passed in,
151
+ # to make sure we handle both numeric and string comparisons appropriately.
152
+ def compare (lhs , rhs , operator ):
153
+ if operator == "gt" :
154
+ return lhs > rhs
155
+ elif operator == "gte" :
156
+ return lhs >= rhs
157
+ elif operator == "lt" :
158
+ return lhs < rhs
159
+ elif operator == "lte" :
160
+ return lhs <= rhs
161
+ else :
162
+ raise ValueError (f"Invalid operator: { operator } " )
150
163
151
- if operator == "lt" :
152
- return type (override_value ) is type (value ) and override_value < value
164
+ parsed_value = None
165
+ try :
166
+ parsed_value = float (value ) # type: ignore
167
+ except Exception :
168
+ pass
153
169
154
- if operator == "lte" :
155
- return type (override_value ) is type (value ) and override_value <= value
170
+ if parsed_value is not None and override_value is not None :
171
+ if isinstance (override_value , str ):
172
+ return compare (override_value , str (value ), operator )
173
+ else :
174
+ return compare (override_value , parsed_value , operator )
175
+ else :
176
+ return compare (str (override_value ), str (value ), operator )
156
177
157
- if operator in ["is_date_before" , "is_date_after" ]:
178
+ if operator in ["is_date_before" , "is_date_after" , "is_relative_date_before" , "is_relative_date_after" ]:
158
179
try :
159
- parsed_date = parser .parse (value )
160
- parsed_date = convert_to_datetime_aware (parsed_date )
161
- except Exception :
180
+ if operator in ["is_relative_date_before" , "is_relative_date_after" ]:
181
+ parsed_date = relative_date_parse_for_feature_flag_matching (str (value ))
182
+ else :
183
+ parsed_date = parser .parse (str (value ))
184
+ parsed_date = convert_to_datetime_aware (parsed_date )
185
+ except Exception as e :
186
+ raise InconclusiveMatchError ("The date set on the flag is not a valid format" ) from e
187
+
188
+ if not parsed_date :
162
189
raise InconclusiveMatchError ("The date set on the flag is not a valid format" )
163
190
164
191
if isinstance (override_value , datetime .datetime ):
165
192
override_date = convert_to_datetime_aware (override_value )
166
- if operator == "is_date_before" :
193
+ if operator in ( "is_date_before" , "is_relative_date_before" ) :
167
194
return override_date < parsed_date
168
195
else :
169
196
return override_date > parsed_date
170
197
elif isinstance (override_value , datetime .date ):
171
- if operator == "is_date_before" :
198
+ if operator in ( "is_date_before" , "is_relative_date_before" ) :
172
199
return override_value < parsed_date .date ()
173
200
else :
174
201
return override_value > parsed_date .date ()
175
202
elif isinstance (override_value , str ):
176
203
try :
177
204
override_date = parser .parse (override_value )
178
205
override_date = convert_to_datetime_aware (override_date )
179
- if operator == "is_date_before" :
206
+ if operator in ( "is_date_before" , "is_relative_date_before" ) :
180
207
return override_date < parsed_date
181
208
else :
182
209
return override_date > parsed_date
@@ -185,7 +212,8 @@ def match_property(property, property_values) -> bool:
185
212
else :
186
213
raise InconclusiveMatchError ("The date provided must be a string or date object" )
187
214
188
- return False
215
+ # if we get here, we don't know how to handle the operator
216
+ raise InconclusiveMatchError (f"Unknown operator { operator } " )
189
217
190
218
191
219
def match_cohort (property , property_values , cohort_properties ) -> bool :
@@ -271,3 +299,33 @@ def match_property_group(property_group, property_values, cohort_properties) ->
271
299
272
300
# if we get here, all matched in AND case, or none matched in OR case
273
301
return property_group_type == "AND"
302
+
303
+
304
+ def relative_date_parse_for_feature_flag_matching (value : str ) -> Optional [datetime .datetime ]:
305
+ regex = r"^(?P<number>[0-9]+)(?P<interval>[a-z])$"
306
+ match = re .search (regex , value )
307
+ parsed_dt = datetime .datetime .now (datetime .timezone .utc )
308
+ if match :
309
+ number = int (match .group ("number" ))
310
+
311
+ if number >= 10_000 :
312
+ # Guard against overflow, disallow numbers greater than 10_000
313
+ return None
314
+
315
+ interval = match .group ("interval" )
316
+ if interval == "h" :
317
+ parsed_dt = parsed_dt - relativedelta (hours = number )
318
+ elif interval == "d" :
319
+ parsed_dt = parsed_dt - relativedelta (days = number )
320
+ elif interval == "w" :
321
+ parsed_dt = parsed_dt - relativedelta (weeks = number )
322
+ elif interval == "m" :
323
+ parsed_dt = parsed_dt - relativedelta (months = number )
324
+ elif interval == "y" :
325
+ parsed_dt = parsed_dt - relativedelta (years = number )
326
+ else :
327
+ return None
328
+
329
+ return parsed_dt
330
+ else :
331
+ return None
0 commit comments