Пример #1
0
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
Пример #2
0
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)
Пример #3
0
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)
Пример #4
0
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)
Пример #5
0
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)
Пример #6
0
    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
Пример #7
0
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)
Пример #8
0
 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
Пример #9
0
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
Пример #10
0
 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
Пример #11
0
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()))))
Пример #12
0
    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
Пример #13
0
 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)
Пример #14
0
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)
Пример #15
0
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
Пример #16
0
 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)
Пример #17
0
    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))
Пример #18
0
 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())))
Пример #19
0
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
Пример #20
0
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))
Пример #21
0
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
Пример #22
0
 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
Пример #23
0
 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
Пример #24
0
 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
Пример #25
0
 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
Пример #26
0
 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())))
Пример #27
0
 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)