22import hashlib
33import logging
44import re
5+ from typing import Optional
56
67from dateutil import parser
8+ from dateutil .relativedelta import relativedelta
79
810from posthog .utils import convert_to_datetime_aware , is_valid_regex
911
@@ -117,15 +119,17 @@ def match_property(property, property_values) -> bool:
117119
118120 override_value = property_values [key ]
119121
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" ):
124123
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 )
129133
130134 if operator == "is_set" :
131135 return key in property_values
@@ -142,41 +146,64 @@ def match_property(property, property_values) -> bool:
142146 if operator == "not_regex" :
143147 return is_valid_regex (str (value )) and re .compile (str (value )).search (str (override_value )) is None
144148
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 } " )
150163
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
153169
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 )
156177
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" ]:
158179 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 :
162189 raise InconclusiveMatchError ("The date set on the flag is not a valid format" )
163190
164191 if isinstance (override_value , datetime .datetime ):
165192 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" ) :
167194 return override_date < parsed_date
168195 else :
169196 return override_date > parsed_date
170197 elif isinstance (override_value , datetime .date ):
171- if operator == "is_date_before" :
198+ if operator in ( "is_date_before" , "is_relative_date_before" ) :
172199 return override_value < parsed_date .date ()
173200 else :
174201 return override_value > parsed_date .date ()
175202 elif isinstance (override_value , str ):
176203 try :
177204 override_date = parser .parse (override_value )
178205 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" ) :
180207 return override_date < parsed_date
181208 else :
182209 return override_date > parsed_date
@@ -185,7 +212,8 @@ def match_property(property, property_values) -> bool:
185212 else :
186213 raise InconclusiveMatchError ("The date provided must be a string or date object" )
187214
188- return False
215+ # if we get here, we don't know how to handle the operator
216+ raise InconclusiveMatchError (f"Unknown operator { operator } " )
189217
190218
191219def match_cohort (property , property_values , cohort_properties ) -> bool :
@@ -271,3 +299,33 @@ def match_property_group(property_group, property_values, cohort_properties) ->
271299
272300 # if we get here, all matched in AND case, or none matched in OR case
273301 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