Browse Source

Initial code commit

Johann Schmitz 3 years ago
parent
commit
28b3dfddc7
Signed by: Johann Schmitz <johann@j-schmitz.net> GPG Key ID: A084064277C501ED
6 changed files with 248 additions and 1 deletions
  1. 2
    0
      .gitignore
  2. 2
    1
      requirements.txt
  3. 2
    0
      src/phylter/__init__.py
  4. 51
    0
      src/phylter/conditions.py
  5. 83
    0
      src/phylter/parser.py
  6. 108
    0
      tests/test_parser.py

+ 2
- 0
.gitignore View File

@@ -59,3 +59,5 @@ docs/_build/
59 59
 target/
60 60
 # PyCharm
61 61
 .idea
62
+
63
+src/test.py

+ 2
- 1
requirements.txt View File

@@ -1 +1,2 @@
1
-# Project dependencies
1
+# Project dependencies
2
+pyparsing

+ 2
- 0
src/phylter/__init__.py View File

@@ -0,0 +1,2 @@
1
+# -*- coding: utf-8 -*-
2
+

+ 51
- 0
src/phylter/conditions.py View File

@@ -0,0 +1,51 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+class Condition(object):
4
+
5
+	def __init__(self, left, right):
6
+		self.left = left
7
+		self.right = right
8
+
9
+
10
+class EqualsCondition(Condition):
11
+
12
+	def __str__(self):
13
+		return "%s == %s" % (self.left, self.right)
14
+
15
+
16
+class GreaterThanCondition(Condition):
17
+
18
+	def __str__(self):
19
+		return "%s > %s" % (self.left, self.right)
20
+
21
+
22
+class GreaterThanOrEqualCondition(Condition):
23
+
24
+	def __str__(self):
25
+		return "%s >= %s" % (self.left, self.right)
26
+
27
+
28
+class LessThanCondition(Condition):
29
+
30
+	def __str__(self):
31
+		return "%s < %s" % (self.left, self.right)
32
+
33
+
34
+class LessThanOrEqualCondition(Condition):
35
+
36
+	def __str__(self):
37
+		return "%s <= %s" % (self.left, self.right)
38
+
39
+
40
+class Operator(object):
41
+	def __init__(self, left, right):
42
+		self.left = left
43
+		self.right = right
44
+
45
+
46
+class AndOperator(Operator):
47
+	pass
48
+
49
+class OrOperator(Operator):
50
+	pass
51
+

+ 83
- 0
src/phylter/parser.py View File

@@ -0,0 +1,83 @@
1
+# -*- coding: utf-8 -*-
2
+import pyparsing
3
+
4
+from phylter.conditions import EqualsCondition, GreaterThanCondition, LessThanCondition, GreaterThanOrEqualCondition, \
5
+	LessThanOrEqualCondition
6
+
7
+field = pyparsing.Word(pyparsing.alphanums)
8
+operator = pyparsing.oneOf(('==', '!=', '>', '<', '>=', '<='))
9
+value = pyparsing.quotedString | pyparsing.Word(pyparsing.alphanums)
10
+
11
+#and_or = pyparsing.oneOf(['and', 'or'], caseless=True)
12
+
13
+field_op_value = field + operator + value
14
+
15
+#andor_field_op_value = and_or + field_op_value
16
+
17
+pattern = field_op_value #+ pyparsing.Optional(pyparsing.OneOrMore(andor_field_op_value))
18
+
19
+
20
+class ConsumableIter(object):
21
+	def __init__(self, iterable):
22
+		self.iterable = iterable or []
23
+		self.length = len(self.iterable)
24
+		self.pos = 0
25
+
26
+	@property
27
+	def remaining(self):
28
+		return self.length - self.pos
29
+
30
+	@property
31
+	def has_more(self):
32
+		return self.remaining > 0
33
+
34
+	@property
35
+	def current(self):
36
+		if self.pos >= self.length-1:
37
+			return None
38
+
39
+		return self.iterable[self.pos]
40
+
41
+	def consume(self, length=1):
42
+		if length is None or length < 0 or length > self.length:
43
+			raise ValueError("'length' argument must be 0 <= length")
44
+
45
+		if length > self.remaining:
46
+			raise ValueError("Cannot consume more than %s remaining elements" % self.remaining)
47
+
48
+		if length == 0:
49
+			return None
50
+
51
+		if length == 1:
52
+			element = self.iterable[self.pos]
53
+			self.pos += length
54
+			return element
55
+
56
+		elements = self.iterable[self.pos:self.pos+length]
57
+		self.pos += length
58
+		return elements
59
+
60
+
61
+class Parser(object):
62
+
63
+	def __init__(self, *args, **kwargs):
64
+		pass
65
+
66
+	def parse(self, query):
67
+		chunks = ConsumableIter(pattern.parseString(query, parseAll=True))
68
+
69
+		while chunks.has_more:
70
+			left, operator, right = tuple(chunks.consume(3))
71
+			return [self._get_condition_class(operator)(left, right)]
72
+
73
+	def _get_condition_class(self, operator):
74
+		d = {
75
+			'==': EqualsCondition,
76
+			'>': GreaterThanCondition,
77
+			'<': LessThanCondition,
78
+			'>=': GreaterThanOrEqualCondition,
79
+			'<=': LessThanOrEqualCondition
80
+		}
81
+		if not operator in d:
82
+			raise Exception("Unknown operator '%s'" % operator)
83
+		return d[operator]

+ 108
- 0
tests/test_parser.py View File

@@ -0,0 +1,108 @@
1
+# -*- coding: utf-8 -*-
2
+import pytest
3
+
4
+from phylter.conditions import EqualsCondition, GreaterThanCondition, GreaterThanOrEqualCondition, LessThanCondition, \
5
+	LessThanOrEqualCondition
6
+from phylter.parser import ConsumableIter, Parser
7
+
8
+
9
+class TestConsumableIter(object):
10
+
11
+	def test_constructor(self):
12
+		for v in (None, []):
13
+			ci = ConsumableIter(v)
14
+			assert ci.length == 0
15
+			assert ci.remaining == 0
16
+
17
+		ci = ConsumableIter([1, 2, 3])
18
+		assert ci.length == 3
19
+		assert ci.remaining == 3
20
+
21
+	def test_current(self):
22
+		ci = ConsumableIter([1, 2, 3])
23
+		assert ci.current == 1
24
+
25
+	def test_current_empty(self):
26
+		ci = ConsumableIter([])
27
+		assert ci.current is None
28
+
29
+	def test_has_more(self):
30
+		ci = ConsumableIter([1, 2, 3])
31
+		assert ci.has_more
32
+
33
+	def test_has_more_empty(self):
34
+		ci = ConsumableIter([])
35
+		assert not ci.has_more
36
+
37
+	def test_consume(self):
38
+		ci = ConsumableIter([1, 2, 3])
39
+		with pytest.raises(ValueError):
40
+			ci.consume(None)
41
+
42
+		ci = ConsumableIter([1, 2, 3])
43
+		with pytest.raises(ValueError):
44
+			ci.consume(-1)
45
+
46
+		ci = ConsumableIter([1, 2, 3])
47
+		with pytest.raises(ValueError):
48
+			ci.consume(100)
49
+
50
+		ci = ConsumableIter([1, 2, 3])
51
+		ci.consume(1)
52
+		with pytest.raises(ValueError):
53
+			ci.consume(3)
54
+
55
+		ci = ConsumableIter([1, 2, 3])
56
+		assert ci.consume(0) is None
57
+		assert ci.consume(1) == 1
58
+		assert ci.consume(1) == 2
59
+		assert ci.consume(1) == 3
60
+		assert ci.remaining == 0
61
+		assert not ci.has_more
62
+
63
+		ci = ConsumableIter([1, 2, 3])
64
+		assert ci.consume(3) == [1, 2, 3]
65
+		assert ci.remaining == 0
66
+		assert not ci.has_more
67
+
68
+	def test_consume_empty(self):
69
+		ci = ConsumableIter([])
70
+
71
+		with pytest.raises(ValueError):
72
+			ci.consume(1)
73
+
74
+
75
+class TestParser(object):
76
+
77
+	def test_constructor(self):
78
+		p = Parser()
79
+		p = Parser('foo')
80
+		p = Parser('foo', bar='baz')
81
+
82
+	def test_get_condition_class(self):
83
+		p = Parser()
84
+
85
+		assert p._get_condition_class('==') == EqualsCondition
86
+		assert p._get_condition_class('>') == GreaterThanCondition
87
+		assert p._get_condition_class('<') == LessThanCondition
88
+		assert p._get_condition_class('>=') == GreaterThanOrEqualCondition
89
+		assert p._get_condition_class('<=') == LessThanOrEqualCondition
90
+
91
+		for x in (None, 'foobar'):
92
+			with pytest.raises(Exception) as e:
93
+				p._get_condition_class(x)
94
+				assert e
95
+
96
+	def test_parse(self):
97
+		for query, left, right, clazz in (
98
+			('foo == bar', 'foo', 'bar', EqualsCondition),
99
+			('foo > bar', 'foo', 'bar', GreaterThanCondition),
100
+			('foo < bar', 'foo', 'bar', LessThanCondition),
101
+			('foo >= bar', 'foo', 'bar', GreaterThanOrEqualCondition),
102
+			('foo <= bar', 'foo', 'bar', LessThanOrEqualCondition),
103
+		):
104
+			q = Parser().parse(query)
105
+			assert len(q) == 1
106
+			assert isinstance(q[0], clazz)
107
+			assert q[0].left == left
108
+			assert q[0].right == right