def test(self, _filter: str, obj_or_dissection: Union[object, List[str]]) -> bool: """Tests the given filter against the dissection of an object. Returns whether the key:value pairs match logically. Case insensitive. That is, if all AND sections within any OR section evaluate to True, in the expanded filter.""" if isinstance(obj_or_dissection, list): dissection = obj_or_dissection else: dissection = self.dissect(obj_or_dissection) _filter = _filter.lower() for or_split, _ in split_unescaped(expand(_filter), OR_GATES): passes_and = True for and_split, _ in split_unescaped(or_split, AND_GATES): without_not_gate, not_gate = extract_not(and_split) if not_gate: if self.test_kvpair(without_not_gate, dissection): passes_and = False break else: if not self.test_kvpair(and_split, dissection): passes_and = False break if passes_and: return True return False
def test_split_unescaped_special_quotes(): generator = split_unescaped( "type:“One & two “x | y””&user:“some two or three”", ("&", "|", " or ")) assert next(generator, None) == ("type:“One & two “x | y””", "&") assert next(generator, None) == ("user:“some two or three”", None) assert next(generator, None) is None
def test_split_unescaped(): generator = split_unescaped( "type:\"One & two \"x | y\"\"&user:\"some two or three\"", ("&", "|", " or ")) assert next(generator, None) == ("type:\"One & two \"x | y\"\"", "&") assert next(generator, None) == ("user:\"some two or three\"", None) assert next(generator, None) is None
def get_invalid_words(_filter: str) -> Generator[str, None, None]: """Returns all space-separated instances of text, which are neither key-value pairs nor logical gates, in the given filter.""" for split, _ in split_unescaped(expand(_filter), (" ",)): is_key_value_pair = re.match(KEY_VALUE_PATTERN, split) is_logical_gate = split.lower() in map(lambda gate: gate.replace(" ", ""), NOT_GATES + AND_GATES + OR_GATES) if not is_key_value_pair and not is_logical_gate: yield split
def parse_command(content: str, context: Message=None, client: Client=None) -> Command: """Returns the given content string as a command, if it's formatted as one, else None. Optionally with the given message as context.""" match = regex.search(r"^" + regex.escape(get_prefix(context)) + r"([A-Za-z]+) ?(.+)?", content) if match: name = match.group(1) args = match.group(2) if args: args_respecting_quotes = [unescape(arg) for arg, _ in split_unescaped(args, (" ",))] return Command(name, *args_respecting_quotes, context=context, client=client) return Command(name, context=context, client=client) return None
def test_split_unescaped_cache_timing(): iterations = 10000 time = datetime.utcnow() generator = split_unescaped( "type:\"one and two\" and user:three and " * iterations, (" and ", )) for i in range(iterations - 1): assert next(generator, None) == ("type:\"one and two\"", " and ") assert next(generator, None) == ("user:three", " and ") delta_time_uncached = datetime.utcnow() - time time = datetime.utcnow() generator = split_unescaped( "type:\"one and two\" and user:three and " * iterations, (" and ", )) for i in range(iterations - 1): assert next(generator, None) == ("type:\"one and two\"", " and ") assert next(generator, None) == ("user:three", " and ") delta_time_cached = datetime.utcnow() - time # Retrieving from cache should be approximately 10 times faster. assert delta_time_uncached.total_seconds( ) / delta_time_cached.total_seconds() > 10
def get_missing_gate(_filter: str) -> Generator[str, None, None]: """Returns a tuple of the first two space-separated instances of text, which have no gate between them, in the given filter.""" was_gate = False prev_split = None for split, _ in split_unescaped(expand(_filter), (" ",)): is_key_value_pair = re.match(KEY_VALUE_PATTERN, split) is_logical_gate = split.lower() in map(lambda gate: gate.replace(" ", ""), NOT_GATES + AND_GATES + OR_GATES) if prev_split and is_key_value_pair and not was_gate: return (prev_split, split) was_gate = is_logical_gate prev_split = split
def filter_to_sql(_filter: str) -> Tuple[str, tuple]: """Returns a tuple of the filter converted to an SQL WHERE clause and the inputs to the WHERE clause (e.g. ("type=%s", ("nominate",)) ), for use with the scraper database.""" if not _filter: # Without a filter, we simply let everything through. return ("TRUE", ()) if not is_valid(_filter, filter_context): raise ValueError("Received an invalid filter; cannot convert to sql.") converted_words = [] converted_values = [] for word, _ in split_unescaped(expand(_filter), (" ",)): # Convert gate symbols in the filter (e.g. "&", "!", "and", "|") to "AND", "OR", and "NOT". if any(map(lambda gate: word.lower() == gate.strip().lower(), AND_GATES)): word = "AND" if any(map(lambda gate: word.lower() == gate.strip().lower(), OR_GATES)): word = "OR" if any(map(lambda gate: word.lower() == gate.strip().lower(), NOT_GATES)): word = "NOT" if word in ["AND", "OR", "NOT"]: converted_words.append(word) continue key, value = next(get_key_value_pairs(word)) tag = filter_context.get_tag(key) if not tag: continue values = [] # Support type aliases (e.g. "resolve" should be converted to "issue-resolve"). if key.lower() == "type": for _type in TYPE_ALIASES: if value.lower() in TYPE_ALIASES[_type]: values.append(_type) # Support group aliases (e.g. "nat" should be converted to "7"). if key.lower() == "group": for group in GROUP_ALIASES: if value.lower() in GROUP_ALIASES[group]: values.append(group) if not values: # Our value is not an alias, so we can use it directly. values.append(value) if len(values) > 1: converted_words.append("(" + " OR ".join([TAG_TO_SQL[tag]] * len(values)) + ")") else: converted_words.append(TAG_TO_SQL[tag]) converted_values.extend(values) return (" ".join(converted_words), tuple(converted_values))
def test_split_unescaped_long_delimiter(): generator = split_unescaped("type:\"one and two\" and user:three", (" and ", )) assert next(generator, None) == ("type:\"one and two\"", " and ") assert next(generator, None) == ("user:three", None) assert next(generator, None) is None