Skip to content

Commit 1c6b777

Browse files
committed
Merge remote-tracking branch 'predicate-logic/master' into pr-cherrypick
2 parents abc73d5 + 8cacb50 commit 1c6b777

File tree

5 files changed

+237
-18
lines changed

5 files changed

+237
-18
lines changed

README.rst

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ Extensions
192192
+==============+==============================================+
193193
| len | - $.objects.`len` |
194194
+--------------+----------------------------------------------+
195+
| keys | - $.dict_field.`keys`(returns a list of keys)|
196+
+--------------+----------------------------------------------+
195197
| sub | - $.field.`sub(/foo\\\\+(.*)/, \\\\1)` |
196198
+--------------+----------------------------------------------+
197199
| split | - $.field.`split(+, 2, -1)` |
@@ -203,8 +205,10 @@ Extensions
203205
+--------------+----------------------------------------------+
204206
| filter | - $.objects[?(@some_field > 5)] |
205207
| | - $.objects[?some_field = "foobar")] |
206-
| | - $.objects[?some_field =~ "foobar")] |
207-
| | - $.objects[?some_field > 5 & other < 2)] |
208+
| | - $.objects[?some_field > 5 & other < 2)] and|
209+
| | - $.objects[?some_field>5 |some_field<2)] or |
210+
| | - $.objects[?(!(field>5 | field<2))] not|
211+
| | - $.objects[?@.field ~= "a.+a"] regex|
208212
+--------------+----------------------------------------------+
209213
| arithmetic | - $.foo + "_" + $.bar |
210214
| (-+*/) | - $.foo * 12 |

jsonpath_ng/ext/filter.py

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,15 @@
1414
import operator
1515
import re
1616
from six import moves
17+
import re
1718

1819
from .. import JSONPath, DatumInContext, Index
1920

21+
def contains(a,b):
22+
if re.search(b,a):
23+
return True
24+
return False
25+
2026

2127
OPERATOR_MAP = {
2228
'!=': operator.ne,
@@ -26,9 +32,37 @@
2632
'<': operator.lt,
2733
'>=': operator.ge,
2834
'>': operator.gt,
29-
'=~': lambda a, b: True if re.search(b, a) else False,
35+
'~=': contains
3036
}
3137

38+
def eval_exp(expressions,val):
39+
for expression in expressions:
40+
if type(expression)==tuple and expression[0]=='|':
41+
val1=eval_exp(expression[1],val)
42+
val2=eval_exp(expression[2],val)
43+
if (val1 or val2):
44+
return True
45+
else:
46+
return False
47+
if type(expression)==tuple and expression[0]=='&':
48+
val1=eval_exp(expression[1],val)
49+
val2=eval_exp(expression[2],val)
50+
if (val1 and val2):
51+
return True
52+
else:
53+
return False
54+
if type(expression)==tuple and expression[0]=='!':
55+
val1=eval_exp(expression[1],val)
56+
if (val1):
57+
return False
58+
else:
59+
return True
60+
else:
61+
if(len([expression])==len(list(filter(lambda x: x.find(val),[expression])))):
62+
return True
63+
else:
64+
return False
65+
3266

3367
class Filter(JSONPath):
3468
"""The JSONQuery filter"""
@@ -47,12 +81,11 @@ def find(self, datum):
4781

4882
if not isinstance(datum.value, list):
4983
return []
50-
51-
return [DatumInContext(datum.value[i], path=Index(i), context=datum)
52-
for i in moves.range(0, len(datum.value))
53-
if (len(self.expressions) ==
54-
len(list(filter(lambda x: x.find(datum.value[i]),
55-
self.expressions))))]
84+
res=[]
85+
for i in moves.range(0,len(datum.value)):
86+
if eval_exp(self.expressions,datum.value[i]):
87+
res.append(DatumInContext(datum.value[i], path=Index(i), context=datum))
88+
return(res)
5689

5790
def update(self, data, val):
5891
if type(data) is list:
@@ -86,7 +119,6 @@ def __init__(self, target, op, value):
86119

87120
def find(self, datum):
88121
datum = self.target.find(DatumInContext.wrap(datum))
89-
90122
if not datum:
91123
return []
92124
if self.op is None:

jsonpath_ng/ext/iterable.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,29 @@ def __str__(self):
9090

9191
def __repr__(self):
9292
return 'Len()'
93+
94+
class Keys(JSONPath):
95+
"""The JSONPath referring to the keys of the current object.
96+
97+
Concrete syntax is '`keys`'.
98+
"""
99+
100+
def find(self, datum):
101+
datum = DatumInContext.wrap(datum)
102+
try:
103+
value = datum.value.keys()
104+
except Exception as e:
105+
return []
106+
else:
107+
return [DatumInContext(value[i],
108+
context=None,
109+
path=Keys()) for i in range (0, len(datum.value))]
110+
111+
def __eq__(self, other):
112+
return isinstance(other, Keys)
113+
114+
def __str__(self):
115+
return '`keys`'
116+
117+
def __repr__(self):
118+
return 'Keys()'

jsonpath_ng/ext/parser.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from .. import lexer
1515
from .. import parser
1616
from .. import Fields, This, Child
17-
1817
from . import arithmetic as _arithmetic
1918
from . import filter as _filter
2019
from . import iterable as _iterable
@@ -23,12 +22,11 @@
2322

2423
class ExtendedJsonPathLexer(lexer.JsonPathLexer):
2524
"""Custom LALR-lexer for JsonPath"""
26-
literals = lexer.JsonPathLexer.literals + ['?', '@', '+', '*', '/', '-']
25+
literals = lexer.JsonPathLexer.literals + ['?', '@', '+', '*', '/', '-', '!','~']
2726
tokens = (['BOOL'] +
2827
parser.JsonPathLexer.tokens +
2928
['FILTER_OP', 'SORT_DIRECTION', 'FLOAT'])
30-
31-
t_FILTER_OP = r'=~|==?|<=|>=|!=|<|>'
29+
t_FILTER_OP = r'==?|<=|>=|!=|<|>|~='
3230

3331
def t_BOOL(self, t):
3432
r'true|false'
@@ -94,6 +92,8 @@ def p_jsonpath_named_operator(self, p):
9492
"jsonpath : NAMED_OPERATOR"
9593
if p[1] == 'len':
9694
p[0] = _iterable.Len()
95+
elif p[1] == 'keys':
96+
p[0] = _iterable.Keys()
9797
elif p[1] == 'sorted':
9898
p[0] = _iterable.SortedThis()
9999
elif p[1].startswith("split("):
@@ -121,11 +121,18 @@ def p_expression(self, p):
121121
def p_expressions_expression(self, p):
122122
"expressions : expression"
123123
p[0] = [p[1]]
124-
124+
125+
def p_expressions_not(self, p):
126+
"expressions : '!' expressions"
127+
p[0]=[('!',p[2])]
128+
125129
def p_expressions_and(self, p):
126130
"expressions : expressions '&' expressions"
127-
# TODO(sileht): implements '|'
128-
p[0] = p[1] + p[3]
131+
p[0] = [('&',p[1],p[3])]
132+
133+
def p_expressions_or(self, p):
134+
"expressions : expressions '|' expressions"
135+
p[0] = [('|',p[1],p[3])]
129136

130137
def p_expressions_parens(self, p):
131138
"expressions : '(' expressions ')'"

tests/test_jsonpath_rw_ext.py

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,53 @@
2525
import testscenarios
2626

2727
from jsonpath_ng.ext import parser
28-
28+
rest_response1={
29+
"items":
30+
[
31+
{
32+
"status": "UP",
33+
"kind": "compute#region",
34+
"description": "us-central1",
35+
"quotas": [
36+
{"usage": 3.0, "metric": "CPUS", "limit": 72.0},
37+
{"usage": 261.0, "metric": "DISKS", "limit": 40960.0},
38+
{"usage": 0.0, "metric": "STATIC", "limit": 21.0},
39+
{"usage": 0.0, "metric": "IN_USE", "limit": 69.0},
40+
{"usage": 0.0, "metric": "SSD", "limit": 20480.0}
41+
],
42+
"id": "1000",
43+
"name": "us-central1"
44+
},
45+
{
46+
"status": "UP",
47+
"kind": "compute#region",
48+
"description": "us-central2",
49+
"quotas": [
50+
{"usage": 0.0, "metric": "CPUS", "limit": 72.0},
51+
{"usage": 0.0, "metric": "DISKS", "limit": 40960.0},
52+
{"usage": 0.0, "metric": "STATIC", "limit": 21.0},
53+
{"usage": 0.0, "metric": "IN_USE", "limit": 69.0},
54+
{"usage": 0.0, "metric": "SSD", "limit": 20480.0}
55+
],
56+
"id": "1001",
57+
"name": "us-central2"
58+
},
59+
{
60+
"status": "UP",
61+
"kind": "compute#region",
62+
"description": "us-central3",
63+
"quotas": [
64+
{"usage": 0.0, "metric": "CPUS", "limit": 90.0},
65+
{"usage": 0.0, "metric": "DISKS", "limit": 2040.0},
66+
{"usage": 0.0, "metric": "STATIC", "limit": 46.0},
67+
{"usage": 0.0, "metric": "IN_USE", "limit": 80.0},
68+
{"usage": 500.0, "metric": "SSD", "limit": 20480.0}
69+
],
70+
"id": "1002",
71+
"name": "us-central3"
72+
}
73+
]
74+
}
2975

3076
class Testjsonpath_ng_ext(testscenarios.WithScenarios,
3177
base.BaseTestCase):
@@ -50,12 +96,31 @@ class Testjsonpath_ng_ext(testscenarios.WithScenarios,
5096
('len_list', dict(string='objects.`len`',
5197
data={'objects': ['alpha', 'gamma', 'beta']},
5298
target=3)),
99+
('filter_list', dict(string='objects[?@="alpha"]',
100+
data={'objects': ['alpha', 'gamma', 'beta']},
101+
target=['alpha'])),
102+
('filter_list_2', dict(string='objects[?@ ~= "a.+"]',
103+
data={'objects': ['alpha', 'gamma', 'beta']},
104+
target=['alpha','gamma'])),
105+
('keys_list', dict(string='objects.`keys`',
106+
data={'objects': ['alpha', 'gamma', 'beta']},
107+
target=[])),
53108
('len_dict', dict(string='objects.`len`',
54109
data={'objects': {'cow': 'moo', 'cat': 'neigh'}},
55110
target=2)),
111+
('keys_dict', dict(string='objects.`keys`',
112+
data={'objects': {'cow': 'moo', 'cat': 'neigh'}},
113+
target=['cow','cat'])),
114+
#('filter_keys_dict', dict(string='objects.`keys`[?`this`="cow"]',
115+
# data={'objects': {'cow': 'moo', 'cat': 'neigh'}},
116+
# target=['cow'])),
117+
#TODO make keys dictionaries filterable
56118
('len_str', dict(string='objects[0].`len`',
57119
data={'objects': ['alpha', 'gamma']},
58120
target=5)),
121+
('contains_filter', dict(string='objects[?id ~= "v.*[1-9]"].id',
122+
data={'objects': [{'id':'vasll1'},{'id':'v2'},{'id':'vaal3'},{'id':'other'},{'id':'val'}]},
123+
target=['vasll1','v2','vaal3'])),
59124

60125
('filter_exists_syntax1', dict(string='objects[?cow]',
61126
data={'objects': [{'cow': 'moo'},
@@ -94,6 +159,12 @@ class Testjsonpath_ng_ext(testscenarios.WithScenarios,
94159
{'cow': 5},
95160
{'cow': 'neigh'}]},
96161
target=[{'cow': 8}, {'cow': 7}])),
162+
('filter_gt_negation', dict(string='objects[?!cow<=5]',
163+
data={'objects': [{'cow': 8},
164+
{'cow': 7},
165+
{'cow': 5},
166+
{'cow': 'neigh'}]},
167+
target=[{'cow': 8}, {'cow': 7},{'cow':'neigh'}])),
97168
('filter_and', dict(string='objects[?cow>5&cat=2]',
98169
data={'objects': [{'cow': 8, 'cat': 2},
99170
{'cow': 7, 'cat': 2},
@@ -102,6 +173,85 @@ class Testjsonpath_ng_ext(testscenarios.WithScenarios,
102173
{'cow': 8, 'cat': 3}]},
103174
target=[{'cow': 8, 'cat': 2},
104175
{'cow': 7, 'cat': 2}])),
176+
('filter_and_demorgans', dict(string='objects[?!(cow<=5|cat!=2)]',
177+
data={'objects': [{'cow': 8, 'cat': 2},
178+
{'cow': 7, 'cat': 2},
179+
{'cow': 2, 'cat': 2},
180+
{'cow': 5, 'cat': 3},
181+
{'cow': 8, 'cat': 3}]},
182+
target=[{'cow': 8, 'cat': 2},
183+
{'cow': 7, 'cat': 2}])),
184+
('filter_or', dict(string='objects[?cow=8|cat=3]',
185+
data={'objects': [{'cow': 8, 'cat': 2},
186+
{'cow': 7, 'cat': 2},
187+
{'cow': 2, 'cat': 2},
188+
{'cow': 5, 'cat': 3},
189+
{'cow': 8, 'cat': 3}]},
190+
target=[{'cow': 8, 'cat': 2},
191+
{'cow': 5, 'cat': 3},
192+
{'cow': 8, 'cat': 3}])),
193+
('filter_or_demorgans', dict(string='objects[?!(cow!=8&cat!=3)]',
194+
data={'objects': [{'cow': 8, 'cat': 2},
195+
{'cow': 7, 'cat': 2},
196+
{'cow': 2, 'cat': 2},
197+
{'cow': 5, 'cat': 3},
198+
{'cow': 8, 'cat': 3}]},
199+
target=[{'cow': 8, 'cat': 2},
200+
{'cow': 5, 'cat': 3},
201+
{'cow': 8, 'cat': 3}])),
202+
('filter_or_and', dict(string='objects[?cow=8&cat=2|cat=3]',
203+
data={'objects': [{'cow': 8, 'cat': 2},
204+
{'cow': 7, 'cat': 2},
205+
{'cow': 2, 'cat': 2},
206+
{'cow': 5, 'cat': 3},
207+
{'cow': 8, 'cat': 3}]},
208+
target=[{'cow': 8, 'cat': 2},
209+
{'cow': 5, 'cat': 3},
210+
{'cow': 8, 'cat': 3}])),
211+
('filter_or_and_overide', dict(string='objects[?cow=8&(cat=2|cat=3)]',
212+
data={'objects': [{'cow': 8, 'cat': 2},
213+
{'cow': 7, 'cat': 2},
214+
{'cow': 2, 'cat': 2},
215+
{'cow': 5, 'cat': 3},
216+
{'cow': 8, 'cat': 3}]},
217+
target=[{'cow': 8, 'cat': 2},
218+
{'cow': 8, 'cat': 3}])),
219+
('filter_or_and', dict(string='objects[?dog=1|cat=3&cow=8]',
220+
data={'objects': [{'cow': 8, 'cat': 2, 'dog':1},
221+
{'cow': 7, 'cat': 2},
222+
{'cow': 2, 'cat': 2},
223+
{'cow': 5, 'cat': 3},
224+
{'cow': 8, 'cat': 3}]},
225+
target=[{'cow': 8, 'cat': 2, 'dog':1},
226+
{'cow': 8, 'cat': 3}])),
227+
('filter_complex', dict(string='$.items[?((!(val==4))&(id==2))|(!((id!=1)&(id!=3)))]',
228+
data={"items":[{"id":1, "val":1, "info":1},{"id":2, "val":4},{"id":2,"val":2},{"id":3,"val":3}]},
229+
target=[{'info': 1, 'id': 1, 'val': 1},
230+
{'id': 2, 'val': 2},
231+
{'id': 3, 'val': 3}])),
232+
('filter_complex2', dict(string="$.items[?(@.quotas[?((@.metric='SSD' & @.usage>0) | (@.metric='CPU' & @.usage>0) | (@.metric='DISKS' & @.usage>0))])]",
233+
data=rest_response1,
234+
target=[{'description': 'us-central1',
235+
'id': '1000',
236+
'kind': 'compute#region',
237+
'name': 'us-central1',
238+
'quotas': [{'limit': 72.0, 'metric': 'CPUS', 'usage': 3.0},
239+
{'limit': 40960.0, 'metric': 'DISKS', 'usage': 261.0},
240+
{'limit': 21.0, 'metric': 'STATIC', 'usage': 0.0},
241+
{'limit': 69.0, 'metric': 'IN_USE', 'usage': 0.0},
242+
{'limit': 20480.0, 'metric': 'SSD', 'usage': 0.0}],
243+
'status': 'UP'},
244+
{'description': 'us-central3',
245+
'id': '1002',
246+
'kind': 'compute#region',
247+
'name': 'us-central3',
248+
'quotas': [{'limit': 90.0, 'metric': 'CPUS', 'usage': 0.0},
249+
{'limit': 2040.0, 'metric': 'DISKS', 'usage': 0.0},
250+
{'limit': 46.0, 'metric': 'STATIC', 'usage': 0.0},
251+
{'limit': 80.0, 'metric': 'IN_USE', 'usage': 0.0},
252+
{'limit': 20480.0, 'metric': 'SSD', 'usage': 500.0}],
253+
'status': 'UP'}])),
254+
105255
('filter_float_gt', dict(
106256
string='objects[?confidence>=0.5].prediction',
107257
data={

0 commit comments

Comments
 (0)