~trufas/ledgeroni

e03087ca10531e40786569d1e9508a496a893b1d — Rafael Castillo 1 year, 10 months ago 239222f
Parse payee query
3 files changed, 72 insertions(+), 9 deletions(-)

M ledgeroni/expression.py
M ledgeroni/query.py
M tests/test_expression.py
M ledgeroni/expression.py => ledgeroni/expression.py +12 -5
@@ 1,9 1,9 @@
import re
from collections import deque

from ledgeroni.query import RegexQuery, Or, Not, And
from ledgeroni.query import RegexQuery, Or, Not, And, PayeeQuery

TOKEN_REGEX = re.compile(r'(?P<token>\(|\)|[^\s\(\)]+)')
TOKEN_REGEX = re.compile(r'(?P<token>\(|\)|@|[^\s\(\)\@]+)')

def tokenize_expression(expr_str):
    expr_str = expr_str.lstrip()


@@ 15,13 15,14 @@ def tokenize_expression(expr_str):
        match = TOKEN_REGEX.match(expr_str)


PRECEDENCE = {'and': 1, 'or': 1, 'not': 2, '(': 0}
PRECEDENCE = {'and': 1, 'or': 1, 'not': 2, '@': 2, 'payee': 3, '(': 0}

def build_postfix_expression(expr_str):
    operator_stack = []
    output = deque()
    last_was_expr = False
    for token in tokenize_expression(expr_str):
        print(token)
        if token == '(':
            if last_was_expr:
                flush_op_stack('or', operator_stack, output)


@@ 35,7 36,8 @@ def build_postfix_expression(expr_str):
                output.append(op)
            if op != '(':
                raise ValueError
        elif token in ('and', 'or', 'not'):
        elif token in ('and', 'or', 'not', '@', 'payee'):
            print('ok')
            flush_op_stack(token, operator_stack, output)
            operator_stack.append(token)
        else:


@@ 44,7 46,7 @@ def build_postfix_expression(expr_str):
                operator_stack.append('or')
            output.append(token)

        last_was_expr = token not in ('and', 'or', '(', 'not')
        last_was_expr = token not in PRECEDENCE.keys()

    output += list(operator_stack[::-1])



@@ 76,6 78,11 @@ def build_expr_from_postfix(postfix_expr):
        elif token == 'not':
            a = operand_stack.pop()
            operand_stack.append(Not(a))
        elif token in ('payee', '@'):
            a = operand_stack.pop()
            if not isinstance(a, RegexQuery):
                raise ValueError
            operand_stack.append(PayeeQuery(a))
        else:
            operand = RegexQuery(re.compile(token))
            operand_stack.append(operand)

M ledgeroni/query.py => ledgeroni/query.py +11 -0
@@ 30,6 30,7 @@ class Or(Query):
        "Runs the query on data"
        return any(q.execute(data) for q in self.queries)


@dataclass(frozen=True)
class And(Query):
    "Query to combine multiple subqueries with an AND operation"


@@ 39,6 40,7 @@ class And(Query):
        "Runs the query on data"
        return all(q.execute(data) for q in self.queries)


@dataclass(frozen=True)
class Not(Query):
    "Query that wraps a query and returns the opposite result"


@@ 49,6 51,15 @@ class Not(Query):
        return not self.query.execute(data)


@dataclass(frozen=True)
class PayeeQuery(Query):
    query: RegexQuery

    def execute(self, data: str) -> bool:
        "Runs the query on data"
        return self.query.execute(data)


def build_simple_or_query(strs: Iterable[str]) -> Query:
    "Builds an or query from an iterator of regular expressions"
    queries = tuple([RegexQuery(re.compile(s)) for s in strs])

M tests/test_expression.py => tests/test_expression.py +49 -4
@@ 1,16 1,18 @@
import re
import pytest
from ledgeroni import expression
from ledgeroni.query import Or, And, Not, RegexQuery
from ledgeroni.query import Or, And, Not, RegexQuery, PayeeQuery


def test_tokenize_expression():
    expr = 'not (Expense and Reddit) or Asset'
    tokens = list(expression.tokenize_expression(expr))
    assert tokens == ['not', '(', 'Expense', 'and', 'Reddit', ')', 'or', 'Asset']
    assert tokens == [
        'not', '(', 'Expense', 'and', 'Reddit', ')', 'or', 'Asset']


class TestBuildPostfixExpression:
    def test_binary(self):
    def test_and(self):
        expr = 'x and y'
        postfix = list(expression.build_postfix_expression(expr))
        assert postfix == ['x', 'y', 'and']


@@ 19,15 21,31 @@ class TestBuildPostfixExpression:
        postfix = list(expression.build_postfix_expression(expr))
        assert postfix == ['x', 'y', 'and', 'z', 'and']

    def test_or(self):
        expr = 'x or y or z'
        postfix = list(expression.build_postfix_expression(expr))
        assert postfix == ['x', 'y', 'or', 'z', 'or']

    def test_unary(self):
    def test_not(self):
        expr = 'not x'
        postfix = list(expression.build_postfix_expression(expr))
        assert postfix == ['x', 'not']

    def test_payee(self):
        expr = 'payee x'
        postfix = list(expression.build_postfix_expression(expr))
        assert postfix == ['x', 'payee']

    def test_unary_precedence(self):
        expr = 'not payee x'
        postfix = list(expression.build_postfix_expression(expr))
        assert postfix == ['x', 'payee', 'not']

    def test_payee_at(self):
        expr = '@x'
        postfix = list(expression.build_postfix_expression(expr))
        assert postfix == ['x', '@']

    def test_precedence(self):
        expr = 'not x and y'
        postfix = list(expression.build_postfix_expression(expr))


@@ 86,6 104,33 @@ class TestBuildExprFromPostfix:

        assert result == query

    def test_payee(self):
        postfix = ['x', 'payee']
        result = expression.build_expr_from_postfix(postfix)
        query = PayeeQuery(RegexQuery(re.compile('x')))

        assert result == query

    def test_payee_at(self):
        postfix = ['x', '@']
        result = expression.build_expr_from_postfix(postfix)
        query = PayeeQuery(RegexQuery(re.compile('x')))

        assert result == query

    def test_payee_validation(self):
        postfix = ['x', 'not', '@']
        with pytest.raises(ValueError):
            expression.build_expr_from_postfix(postfix)

    def test_precedence(self):
        postfix = ['y', 'not', 'x', '@', 'and']
        result = expression.build_expr_from_postfix(postfix)
        query = And((PayeeQuery(RegexQuery(re.compile('x'))),
                    Not(RegexQuery(re.compile('y')))))

        assert result == query


class TestBuildExpression:
    def test_binary(self):