PK!11lucyparser/__init__.pyfrom .parsing import parse __version__ = '0.1.0' PK!t2((lucyparser/cursor.pyfrom dataclasses import dataclass from typing import Optional from .exceptions import LucyUnexpectedEndException, LucyUnexpectedCharacter @dataclass class Cursor: """ Peekable iterator implementation. For parsing lookaheades """ input: str cursor: int = 0 def pop(self) -> str: next_char = self.peek() if next_char: self.cursor += 1 return next_char raise LucyUnexpectedEndException() def peek(self, n=0) -> Optional[str]: """ Peek a character n elements ahead """ if len(self.input) <= self.cursor + n: return None return self.input[self.cursor + n] def empty(self) -> bool: return len(self.input) <= self.cursor def starts_with_a_word(self, word: str) -> bool: counter = 0 for expected_char in word: actual_char = self.peek(counter) if not actual_char: return False if actual_char.casefold() != expected_char.casefold(): return False counter += 1 char_after_word = self.peek(counter) if not char_after_word: # Probably something is wrong but, we will notice it later return True # Checkin a next character to chack that match was not a part of a bigger word return char_after_word.isspace() or char_after_word == "(" def starts_with_a_char(self, char: str) -> bool: return self.peek() == char def consume_known_char(self, char: str): actual_char = self.pop() if actual_char != char: raise LucyUnexpectedCharacter(unexpected=actual_char, expected=char) def consume(self, n: int): for _ in range(n): if not self.pop(): raise LucyUnexpectedEndException() def consume_spaces(self): while 1: next_char = self.peek() if next_char is None: break if next_char.isspace(): self.pop() else: break PK!qrlucyparser/exceptions.pyclass BaseLucyException(Exception): pass class LucyUnexpectedEndException(BaseLucyException): def __init__(self): super().__init__("Unexpected end of input") class LucyUnexpectedCharacter(BaseLucyException): def __init__(self, unexpected, expected): super().__init__(f"Unexpected character {unexpected}, expected one of {expected}") class LucyUndefinedOperator(BaseLucyException): def __init__(self, operator): super().__init__(f"Undefined operator: {operator}") class LucyIllegalLiteral(BaseLucyException): def __init__(self, literal): super().__init__(f"Illegal literal with escaped slash: {literal}") PK! P33lucyparser/parsing.pyimport string from typing import List from .cursor import Cursor from .exceptions import LucyUnexpectedEndException, LucyUnexpectedCharacter, LucyIllegalLiteral from .tree import BaseNode, simplify, NotNode, AndNode, ExpressionNode, LogicalNode, get_logical_node, LogicalOperator, \ RawOperator, RAW_OPERATOR_TO_OPERATOR, Operator def parse(string: str) -> BaseNode: """ User facing parse function. All user needs to know about """ cursor = Cursor(string) cursor.consume_spaces() parser = Parser() tree = parser.read_tree(cursor) cursor.consume_spaces() if not cursor.empty(): raise LucyUnexpectedEndException() return tree class Parser: name_chars = string.ascii_letters + string.digits + "_." name_first_chars = string.ascii_letters + "_" value_chars = string.ascii_letters + string.digits + "-.*_?!;,:@" escaped_chars = { "\\": "\\", "n": "\n", '"': '"', "'": "'", "a": "\a", "b": "\b", "f": "\f", "r": "\r", "t": "\t", "v": "\v" } def read_tree(self, cur: Cursor) -> BaseNode: tree = self.read_expressions(cur) return simplify(tree) def read_expressions(self, cur: Cursor) -> BaseNode: """ Read several expressions, separated with logical operators """ def pop_expression_from_stack() -> LogicalNode: right = expressions_stack.pop() left = expressions_stack.pop() return get_logical_node(logical_operator=operators_stack.pop(), children=[left, right]) expression = self.read_expression(cur) cur.consume_spaces() operators_stack: List[LogicalOperator] = [] expressions_stack: List[BaseNode] = [expression] while 1: if cur.starts_with_a_word("and"): expressions_stack.append(self.read_and_operator(cur)) operators_stack.append(LogicalOperator.AND) cur.consume_spaces() elif cur.starts_with_a_word("or"): node = self.read_or_operator(cur) if operators_stack and operators_stack[-1] == LogicalOperator.AND: expressions_stack.append(pop_expression_from_stack()) operators_stack.append(LogicalOperator.OR) expressions_stack.append(node) cur.consume_spaces() else: break while operators_stack: expressions_stack.append(pop_expression_from_stack()) return expressions_stack[0] def _read_operator(self, cur: Cursor, length: int) -> BaseNode: """ Read operator and following expression from the stream """ cur.consume(length) cur.consume_spaces() expression = self.read_expression(cur) cur.consume_spaces() return expression def read_or_operator(self, cur: Cursor) -> BaseNode: return self._read_operator(cur=cur, length=2) def read_and_operator(self, cur: Cursor) -> BaseNode: return self._read_operator(cur=cur, length=3) def read_expression(self, cur: Cursor) -> BaseNode: """ Read a single expression: Expression is: - multiple expressions combined (in some way) in braces - negation of something - a single condition in name:value form """ if cur.starts_with_a_char("("): cur.consume_known_char("(") tree = self.read_tree(cur) cur.consume_known_char(")") return tree if cur.starts_with_a_word("not"): cur.consume(3) cur.consume_spaces() tree = NotNode(children=[self.read_expression(cur)]) cur.consume_spaces() return tree return AndNode(children=[self.read_condition(cur)]) def read_condition(self, cur: Cursor) -> ExpressionNode: """ Read a single entry of "name: value" """ cur.consume_spaces() name = self.read_field_name(cur) cur.consume_spaces() operator = self.read_operator(cur=cur) cur.consume_spaces() value = self.read_field_value(cur) return ExpressionNode(name=name, value=value, operator=operator) def read_operator(self, cur: Cursor) -> Operator: current_operator = cur.pop() if current_operator in RawOperator.equal_not_required: return RAW_OPERATOR_TO_OPERATOR[current_operator] equal_is_possible = current_operator in RawOperator.equal_is_possible if not equal_is_possible: raise LucyUnexpectedCharacter(unexpected=current_operator, expected="".join(RawOperator.equal_is_possible)) next_char = cur.peek() if next_char == RawOperator.EQUAL_SIGN: cur.pop() current_operator += RawOperator.EQUAL_SIGN return RAW_OPERATOR_TO_OPERATOR[current_operator] def read_field_name(self, cur: Cursor) -> str: name = cur.pop() if name not in self.name_first_chars: raise LucyUnexpectedCharacter(unexpected=name, expected=self.name_first_chars) while 1: next_char = cur.peek() if next_char and next_char in self.name_chars: name += cur.pop() else: return name def read_field_value(self, cur: Cursor) -> str: def read_until(terminator: str) -> str: value = "" while 1: char = cur.pop() if char == "\\": char = cur.pop() char_with_escaped_slash = self.escaped_chars.get(char) if char_with_escaped_slash is None: raise LucyIllegalLiteral(literal=char) value += char_with_escaped_slash continue elif char == terminator: return value value += char if cur.starts_with_a_char('"'): cur.consume_known_char('"') return read_until('"') if cur.starts_with_a_char("'"): cur.consume_known_char("'") return read_until("'") next_char = cur.peek() if not next_char: raise LucyUnexpectedEndException() if next_char not in self.value_chars: raise LucyUnexpectedCharacter(unexpected=next_char, expected=self.value_chars) value = cur.pop() while 1: next_char = cur.peek() if not next_char or next_char not in self.value_chars: return value value += cur.pop() PK!#fQ Q lucyparser/tree.pyimport enum from dataclasses import dataclass, field from typing import List, Any, Optional from .exceptions import LucyUndefinedOperator class Operator(enum.Enum): GTE = enum.auto() LTE = enum.auto() GT = enum.auto() LT = enum.auto() EQ = enum.auto() NEQ = enum.auto() class RawOperator: NEQ = "!" EQ = ":" GT = ">" LT = "<" GTE = ">=" LTE = "<=" equal_not_required = [EQ, NEQ] equal_is_possible = [GT, LT] EQUAL_SIGN = "=" RAW_OPERATOR_TO_OPERATOR = { RawOperator.NEQ: Operator.NEQ, RawOperator.EQ: Operator.EQ, RawOperator.GT: Operator.GT, RawOperator.LT: Operator.LT, RawOperator.GTE: Operator.GTE, RawOperator.LTE: Operator.LTE, } class LogicalOperator(enum.Enum): NOT = enum.auto() AND = enum.auto() OR = enum.auto() @dataclass class BaseNode: def pprint(self, pad=0): print(" " * pad + str(self.operator)) @dataclass class LogicalNode(BaseNode): children: List = field(default_factory=list) _logical_operator = Optional[LogicalOperator] @property def operator(self): return self._logical_operator def pprint(self, pad=0): super().pprint(pad=pad) pad += 2 for child in self.children: child.pprint(pad) @dataclass class AndNode(LogicalNode): _logical_operator = LogicalOperator.AND @dataclass class OrNode(LogicalNode): _logical_operator = LogicalOperator.OR @dataclass class NotNode(LogicalNode): _logical_operator = LogicalOperator.NOT def get_logical_node(logical_operator: LogicalOperator, children: List = field(default_factory=list)): node_class = { LogicalOperator.AND: AndNode, LogicalOperator.OR: OrNode, LogicalOperator.NOT: NotNode, }.get(logical_operator) if node_class is None: raise LucyUndefinedOperator(operator=logical_operator) return node_class(children=children) @dataclass class ExpressionNode(BaseNode): name: Optional[str] value: Any operator: Operator def simplify(tree: BaseNode) -> BaseNode: """ Merge nested ORs and ANDs Transform AND a AND b c into AND a b c """ if not isinstance(tree, LogicalNode): return tree if isinstance(tree, AndNode) and (len(tree.children) == 1): return tree.children[0] tree.children = [simplify(child) if isinstance(child, LogicalNode) else child for child in tree.children] if not isinstance(tree, NotNode): new_children: List[BaseNode] = [] for child in tree.children: if isinstance(child, LogicalNode) and type(child) == type(tree): new_children.extend(child.children) else: new_children.append(child) tree.children = new_children return tree PK!HnHTU lucyparser-0.1.2.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H\H1#lucyparser-0.1.2.dist-info/METADATAj1 E @m(L[ d: }DuČDPN:} G`QU N E3; RAFa@3P:!×&ќמv?ZP5?`S)ӯEA =Hw*Ov3^ &sknz_ݭROz)x@Ѯt-+ PK!HȄY!lucyparser-0.1.2.dist-info/RECORD}9r@@gidN$MKBA%@CFp \C^5¹MI{M1$ɦA /|Բ##M0E>";jmKJoPK!11lucyparser/__init__.pyPK!t2((elucyparser/cursor.pyPK!qrlucyparser/exceptions.pyPK! P33 lucyparser/parsing.pyPK!#fQ Q %lucyparser/tree.pyPK!HnHTU t1lucyparser-0.1.2.dist-info/WHEELPK!H\H1#2lucyparser-0.1.2.dist-info/METADATAPK!HȄY! 3lucyparser-0.1.2.dist-info/RECORDPK=4