def unescape(text: str, make_wildcards_special: bool = False) -> str: output = "" i = 0 while i < len(text): if text[i] == "\\": try: char = text[i + 1] i += 1 except IndexError: raise errors.SearchError( "Unterminated escape sequence (did you forget to escape " "the ending backslash?)" ) if char not in "*\\:-.,": raise errors.SearchError( "Unknown escape sequence (did you forget to escape " "the backslash?)" ) elif text[i] == "*" and make_wildcards_special: char = WILDCARD else: char = text[i] output += char i += 1 return output
def _parse_sort(value: str, negated: bool) -> tokens.SortToken: if value.count(',') == 0: order_str = None elif value.count(',') == 1: value, order_str = value.split(',') else: raise errors.SearchError('Too many commas in sort style token.') try: order = { 'asc': tokens.SortToken.SORT_ASC, 'desc': tokens.SortToken.SORT_DESC, '': tokens.SortToken.SORT_DEFAULT, None: tokens.SortToken.SORT_DEFAULT, }[order_str] except KeyError: raise errors.SearchError('Unknown search direction: %r.' % order_str) if negated: order = { tokens.SortToken.SORT_ASC: tokens.SortToken.SORT_DESC, tokens.SortToken.SORT_DESC: tokens.SortToken.SORT_ASC, tokens.SortToken.SORT_DEFAULT: tokens.SortToken.SORT_NEGATED_DEFAULT, tokens.SortToken.SORT_NEGATED_DEFAULT: tokens.SortToken.SORT_DEFAULT, }[order] return tokens.SortToken(value, order)
def _parse_sort(value: str, negated: bool) -> tokens.SortToken: if value.count(",") == 0: order_str = None elif value.count(",") == 1: value, order_str = value.split(",") else: raise errors.SearchError("Too many commas in sort style token.") try: order = { "asc": tokens.SortToken.SORT_ASC, "desc": tokens.SortToken.SORT_DESC, "": tokens.SortToken.SORT_DEFAULT, None: tokens.SortToken.SORT_DEFAULT, }[order_str] except KeyError: raise errors.SearchError("Unknown search direction: %r." % order_str) if negated: order = { tokens.SortToken.SORT_ASC: tokens.SortToken.SORT_DESC, tokens.SortToken.SORT_DESC: tokens.SortToken.SORT_ASC, tokens.SortToken.SORT_DEFAULT: tokens.SortToken.SORT_NEGATED_DEFAULT, # noqa: E501 tokens.SortToken.SORT_NEGATED_DEFAULT: tokens.SortToken.SORT_DEFAULT, # noqa: E501 }[order] return tokens.SortToken(value, order)
def _parse_sort(value: str, negated: bool) -> tokens.SortToken: if value.count(',') == 0: order_str = None elif value.count(',') == 1: value, order_str = value.split(',') else: raise errors.SearchError('너무 많은 쉼표가 정렬 토큰에 사용됨.') try: order = { 'asc': tokens.SortToken.SORT_ASC, 'desc': tokens.SortToken.SORT_DESC, '': tokens.SortToken.SORT_DEFAULT, None: tokens.SortToken.SORT_DEFAULT, }[order_str] except KeyError: raise errors.SearchError('알 수 없는 검색 방향: %r.' % order_str) if negated: order = { tokens.SortToken.SORT_ASC: tokens.SortToken.SORT_DESC, tokens.SortToken.SORT_DESC: tokens.SortToken.SORT_ASC, tokens.SortToken.SORT_DEFAULT: tokens.SortToken.SORT_NEGATED_DEFAULT, tokens.SortToken.SORT_NEGATED_DEFAULT: tokens.SortToken.SORT_DEFAULT, }[order] return tokens.SortToken(value, order)
def _create_criterion(original_value: str, value: str) -> criteria.BaseCriterion: if re.search(r'(?<!\\),', value): values = re.split(r'(?<!\\),', value) if any(not term.strip() for term in values): raise errors.SearchError('Empty compound value') return criteria.ArrayCriterion(original_value, values) if re.search(r'(?<!\\)\.(?<!\\)\.', value): low, high = re.split(r'(?<!\\)\.(?<!\\)\.', value, 1) if not low and not high: raise errors.SearchError('Empty ranged value') return criteria.RangedCriterion(original_value, low, high) return criteria.PlainCriterion(original_value, value)
def _prepare_db_query(self, db_query: SaQuery, search_query: SearchQuery, use_sort: bool) -> SaQuery: for anon_token in search_query.anonymous_tokens: if not self.config.anonymous_filter: raise errors.SearchError( "Anonymous tokens are not valid in this context.") db_query = self.config.anonymous_filter(db_query, anon_token.criterion, anon_token.negated) for named_token in search_query.named_tokens: if named_token.name not in self.config.named_filters: raise errors.SearchError( "Unknown named token: %r. Available named tokens: %r." % ( named_token.name, _format_dict_keys(self.config.named_filters), )) db_query = self.config.named_filters[named_token.name]( db_query, named_token.criterion, named_token.negated) for sp_token in search_query.special_tokens: if sp_token.value not in self.config.special_filters: raise errors.SearchError( "Unknown special token: %r. " "Available special tokens: %r." % ( sp_token.value, _format_dict_keys(self.config.special_filters), )) db_query = self.config.special_filters[sp_token.value]( db_query, None, sp_token.negated) if use_sort: for sort_token in search_query.sort_tokens: if sort_token.name not in self.config.sort_columns: raise errors.SearchError( "Unknown sort token: %r. " "Available sort tokens: %r." % ( sort_token.name, _format_dict_keys(self.config.sort_columns), )) column, default_order = self.config.sort_columns[ sort_token.name] order = _get_order(sort_token.order, default_order) if order == sort_token.SORT_ASC: db_query = db_query.order_by(column.asc()) elif order == sort_token.SORT_DESC: db_query = db_query.order_by(column.desc()) db_query = self.config.finalize_query(db_query) return db_query
def _create_criterion(original_value: str, value: str) -> criteria.BaseCriterion: if re.search(r"(?<!\\),", value): values = re.split(r"(?<!\\),", value) if any(not term.strip() for term in values): raise errors.SearchError("Empty compound value") return criteria.ArrayCriterion(original_value, values) if re.search(r"(?<!\\)\.(?<!\\)\.", value): low, high = re.split(r"(?<!\\)\.(?<!\\)\.", value, 1) if not low and not high: raise errors.SearchError("Empty ranged value") return criteria.RangedCriterion(original_value, low, high) if value.lower() in ["null", "none", "unknown", "?"]: return criteria.NullCriterion(original_value) return criteria.PlainCriterion(original_value, value)
def parse(self, query_text: str) -> SearchQuery: query = SearchQuery() for chunk in re.split(r'\s+', (query_text or '').lower()): if not chunk: continue negated = False if chunk[0] == '-': chunk = chunk[1:] negated = True if not chunk: raise errors.SearchError('비어있는 부정 토큰.') match = re.match(r'^(.*?)(?<!\\):(.*)$', chunk) if match: key, value = list(match.groups()) key = util.unescape(key) if key == 'sort': query.sort_tokens.append(_parse_sort(value, negated)) elif key == 'special': query.special_tokens.append(_parse_special(value, negated)) else: query.named_tokens.append(_parse_named( key, value, negated)) else: query.anonymous_tokens.append(_parse_anonymous(chunk, negated)) return query
def apply_num_criterion_to_column( column: Any, criterion: criteria.BaseCriterion, transformer: Callable[[str], Number] = integer_transformer, ) -> SaQuery: try: if isinstance(criterion, criteria.PlainCriterion): expr = column == transformer(criterion.value) elif isinstance(criterion, criteria.ArrayCriterion): expr = column.in_(transformer(value) for value in criterion.values) elif isinstance(criterion, criteria.RangedCriterion): assert criterion.min_value or criterion.max_value if criterion.min_value and criterion.max_value: expr = column.between( transformer(criterion.min_value), transformer(criterion.max_value), ) elif criterion.min_value: expr = column >= transformer(criterion.min_value) elif criterion.max_value: expr = column <= transformer(criterion.max_value) else: assert False except ValueError: raise errors.SearchError("Criterion value %r must be a number." % (criterion, )) return expr
def parse(self, query_text: str) -> SearchQuery: query = SearchQuery() for chunk in re.split(r"\s+", (query_text or "").lower()): if not chunk: continue negated = False if chunk[0] == "-": chunk = chunk[1:] negated = True if not chunk: raise errors.SearchError("Empty negated token.") match = re.match(r"^(.*?)(?<!\\):(.*)$", chunk) if match: key, value = list(match.groups()) key = util.unescape(key) if key == "sort": query.sort_tokens.append(_parse_sort(value, negated)) elif key == "special": query.special_tokens.append(_parse_special(value, negated)) else: query.named_tokens.append(_parse_named( key, value, negated)) else: query.anonymous_tokens.append(_parse_anonymous(chunk, negated)) return query
def enum_transformer(available_values: Dict[str, Any], value: str) -> str: try: return available_values[unescape(value.lower())] except KeyError: raise errors.SearchError( 'Invalid value: %r. Possible values: %r.' % (value, list(sorted(available_values.keys()))))
def _prepare_db_query(self, db_query: SaQuery, search_query: SearchQuery, use_sort: bool) -> SaQuery: for anon_token in search_query.anonymous_tokens: if not self.config.anonymous_filter: raise errors.SearchError('익명 토큰은 이 컨텍스트에서 사용할 수 없습니다.') db_query = self.config.anonymous_filter(db_query, anon_token.criterion, anon_token.negated) for named_token in search_query.named_tokens: if named_token.name not in self.config.named_filters: raise errors.SearchError( '알 수 없는 명명된 토큰: %r. 가능한 명명된 토큰: %r.' % (named_token.name, _format_dict_keys(self.config.named_filters))) db_query = self.config.named_filters[named_token.name]( db_query, named_token.criterion, named_token.negated) for sp_token in search_query.special_tokens: if sp_token.value not in self.config.special_filters: raise errors.SearchError( '알 수 없는 특수 토큰: %r. ' '가능한 특수 토큰: %r.' % (sp_token.value, _format_dict_keys(self.config.special_filters))) db_query = self.config.special_filters[sp_token.value]( db_query, None, sp_token.negated) if use_sort: for sort_token in search_query.sort_tokens: if sort_token.name not in self.config.sort_columns: raise errors.SearchError( '알 수 없는 정렬 토큰: %r. ' '가능한 정렬 토큰: %r.' % (sort_token.name, _format_dict_keys(self.config.sort_columns))) column, default_order = ( self.config.sort_columns[sort_token.name]) order = _get_order(sort_token.order, default_order) if order == sort_token.SORT_ASC: db_query = db_query.order_by(column.asc()) elif order == sort_token.SORT_DESC: db_query = db_query.order_by(column.desc()) db_query = self.config.finalize_query(db_query) return db_query
def _create_criterion(self, value, negated): if '..' in value: low, high = value.split('..', 1) if not low and not high: raise errors.SearchError('Empty ranged value') return criteria.RangedSearchCriterion(value, negated, low, high) if ',' in value: return criteria.ArraySearchCriterion( value, negated, value.split(',')) return criteria.PlainSearchCriterion(value, negated, value)
def _create_criterion(original_value: str, value: str) -> criteria.BaseCriterion: if ',' in value: return criteria.ArrayCriterion(original_value, value.split(',')) if '..' in value: low, high = value.split('..', 1) if not low and not high: raise errors.SearchError('Empty ranged value') return criteria.RangedCriterion(original_value, low, high) return criteria.PlainCriterion(original_value, value)
def unescape(text: str, make_wildcards_special: bool = False) -> str: output = '' i = 0 while i < len(text): if text[i] == '\\': try: char = text[i + 1] i += 1 except IndexError: raise errors.SearchError('끝나지 않은 이스케이프 시퀀스 ' '(마지막 백슬래시를 잊었나요?)') if char not in '*\\:-.,': raise errors.SearchError('끝나지 않은 이스케이프 시퀀스 ' '(백슬래시를 잊었나요?)') elif text[i] == '*' and make_wildcards_special: char = WILDCARD else: char = text[i] output += char i += 1 return output
def own_fav_filter(self, query, negated): assert self.user if self.user.rank == 'anonymous': raise errors.SearchError('Must be logged in to use this feature.') expr = db.Post.post_id.in_( db.session \ .query(db.PostFavorite.post_id) \ .filter(db.PostFavorite.user_id == self.user.user_id)) if negated: expr = ~expr return query.filter(expr)
def _handle_sort(self, query, value, negated): if value.count(',') == 0: dir_str = None elif value.count(',') == 1: value, dir_str = value.split(',') else: raise errors.SearchError('Too many commas in sort style token.') try: column, default_sort = self.config.sort_columns[value] except KeyError: raise errors.SearchError( 'Unknown sort style: %r. Available sort styles: %r.' % ( value, list(self.config.sort_columns.keys()))) sort_asc = self.config.SORT_ASC sort_desc = self.config.SORT_DESC try: sort_map = { 'asc': sort_asc, 'desc': sort_desc, '': default_sort, None: default_sort, } sort = sort_map[dir_str] except KeyError: raise errors.SearchError('Unknown search direction: %r.' % dir_str) if negated and sort: sort = -sort transform_map = { sort_asc: lambda input: input.asc(), sort_desc: lambda input: input.desc(), None: lambda input: input, } transform = transform_map[sort] return query.order_by(transform(column))
def _handle_named(self, query, key, value, negated): if key.endswith('-min'): key = key[:-4] value += '..' elif key.endswith('-max'): key = key[:-4] value = '..' + value criterion = self._create_criterion(value, negated) if key in self.config.named_filters: return self.config.named_filters[key](query, criterion) raise errors.SearchError( 'Unknown named token: %r. Available named tokens: %r.' % ( key, list(self.config.named_filters.keys())))
def apply_str_criterion_to_column( column: SaColumn, criterion: criteria.BaseCriterion, transformer: Callable[[str], str] = wildcard_transformer) -> SaQuery: if isinstance(criterion, criteria.PlainCriterion): expr = column.ilike(transformer(criterion.value)) elif isinstance(criterion, criteria.ArrayCriterion): expr = sa.sql.false() for value in criterion.values: expr = expr | column.ilike(transformer(value)) elif isinstance(criterion, criteria.RangedCriterion): raise errors.SearchError('범위가 지정된 기준은 이 컨텍스트에서 유효하지 않습니다. ' '점들을 이스케이프하는 것을 잊었나요?') else: assert False return expr
def _type_transformer(value): available_types = { 'image': db.Post.TYPE_IMAGE, 'animation': db.Post.TYPE_ANIMATION, 'animated': db.Post.TYPE_ANIMATION, 'anim': db.Post.TYPE_ANIMATION, 'gif': db.Post.TYPE_ANIMATION, 'video': db.Post.TYPE_VIDEO, 'webm': db.Post.TYPE_VIDEO, 'flash': db.Post.TYPE_FLASH, 'swf': db.Post.TYPE_FLASH, } try: return available_types[value.lower()] except KeyError: raise errors.SearchError('Invalid type: %r. Available types: %r.' % ( value, available_types))
def apply_str_criterion_to_column( column: SaColumn, criterion: criteria.BaseCriterion, transformer: Callable[[str], str] = wildcard_transformer) -> SaQuery: if isinstance(criterion, criteria.PlainCriterion): expr = column.ilike(transformer(criterion.value)) elif isinstance(criterion, criteria.ArrayCriterion): expr = sa.sql.false() for value in criterion.values: expr = expr | column.ilike(transformer(value)) elif isinstance(criterion, criteria.RangedCriterion): raise errors.SearchError( 'Ranged criterion is invalid in this context. ' 'Did you forget to escape the dots?') else: assert False return expr
def on_search_query_parsed(self, search_query: SearchQuery) -> SaQuery: new_special_tokens = [] for token in search_query.special_tokens: if token.value in ('fav', 'liked', 'disliked'): assert self.user if self.user.rank == 'anonymous': raise errors.SearchError('이 기능을 사용하기 위해서는 로그인해야 합니다.') criterion = criteria.PlainCriterion( original_text=self.user.name, value=self.user.name) setattr(criterion, 'internal', True) search_query.named_tokens.append( tokens.NamedToken(name=token.value, criterion=criterion, negated=token.negated)) else: new_special_tokens.append(token) search_query.special_tokens = new_special_tokens
def wrapper(query: SaQuery, criterion: Optional[criteria.BaseCriterion], negated: bool) -> SaQuery: assert criterion if not getattr(criterion, 'internal', False): raise errors.SearchError('투표는 공개적으로 확인할 수 없습니다. %r를 시도해보세요.' % 'special:liked') user_alias = sa.orm.aliased(model.User) score_alias = sa.orm.aliased(model.PostScore) expr = score_alias.score == score expr = expr & search_util.apply_str_criterion_to_column( user_alias.name, criterion) if negated: expr = ~expr ret = (query.join( score_alias, score_alias.post_id == model.Post.post_id).join( user_alias, user_alias.user_id == score_alias.user_id).filter(expr)) return ret
def on_search_query_parsed(self, search_query: SearchQuery) -> SaQuery: new_special_tokens = [] for token in search_query.special_tokens: if token.value in ("fav", "liked", "disliked"): assert self.user if self.user.rank == "anonymous": raise errors.SearchError( "Must be logged in to use this feature.") criterion = criteria.PlainCriterion( original_text=self.user.name, value=self.user.name) setattr(criterion, "internal", True) search_query.named_tokens.append( tokens.NamedToken( name=token.value, criterion=criterion, negated=token.negated, )) else: new_special_tokens.append(token) search_query.special_tokens = new_special_tokens
def wrapper( query: SaQuery, criterion: Optional[criteria.BaseCriterion], negated: bool) -> SaQuery: assert criterion if not getattr(criterion, 'internal', False): raise errors.SearchError( 'Votes cannot be seen publicly. Did you mean %r?' % 'special:liked') user_alias = sa.orm.aliased(model.User) score_alias = sa.orm.aliased(model.PostScore) expr = score_alias.score == score expr = expr & search_util.apply_str_criterion_to_column( user_alias.name, criterion) if negated: expr = ~expr ret = query \ .join(score_alias, score_alias.post_id == model.Post.post_id) \ .join(user_alias, user_alias.user_id == score_alias.user_id) \ .filter(expr) return ret
def _handle_special(self, query, value, negated): if value in self.config.special_filters: return self.config.special_filters[value](query, negated) raise errors.SearchError( 'Unknown special token: %r. Available special tokens: %r.' % ( value, list(self.config.special_filters.keys())))
def _handle_anonymous(self, query, criterion): if not self.config.anonymous_filter: raise errors.SearchError( 'Anonymous tokens are not valid in this context.') return self.config.anonymous_filter(query, criterion)