4
4
from six .moves import xrange
5
5
from itertools import *
6
6
import functools
7
+ import operator
7
8
8
9
logger = logging .getLogger (__name__ )
9
10
14
15
class JSONPath (object ):
15
16
"""
16
17
The base class for JSONPath abstract syntax; those
17
- methods stubbed here are the interface to supported
18
+ methods stubbed here are the interface to supported
18
19
JSONPath semantics.
19
20
"""
20
21
@@ -53,8 +54,8 @@ class DatumInContext(object):
53
54
"""
54
55
Represents a datum along a path from a context.
55
56
56
- Essentially a zipper but with a structure represented by JsonPath,
57
- and where the context is more of a parent pointer than a proper
57
+ Essentially a zipper but with a structure represented by JsonPath,
58
+ and where the context is more of a parent pointer than a proper
58
59
representation of the context.
59
60
60
61
For quick-and-dirty work, this proxies any non-special attributes
@@ -115,17 +116,17 @@ class AutoIdForDatum(DatumInContext):
115
116
"""
116
117
This behaves like a DatumInContext, but the value is
117
118
always the path leading up to it, not including the "id",
118
- and with any "id" fields along the way replacing the prior
119
+ and with any "id" fields along the way replacing the prior
119
120
segment of the path
120
121
121
122
For example, it will make "foo.bar.id" return a datum
122
123
that behaves like DatumInContext(value="foo.bar", path="foo.bar.id").
123
124
124
125
This is disabled by default; it can be turned on by
125
126
settings the `auto_id_field` global to a value other
126
- than `None`.
127
+ than `None`.
127
128
"""
128
-
129
+
129
130
def __init__ (self , datum , id_field = None ):
130
131
"""
131
132
Invariant is that datum.path is the path from context to datum. The auto id
@@ -212,7 +213,7 @@ class Child(JSONPath):
212
213
JSONPath that first matches the left, then the right.
213
214
Concrete syntax is <left> '.' <right>
214
215
"""
215
-
216
+
216
217
def __init__ (self , left , right ):
217
218
self .left = left
218
219
self .right = right
@@ -222,7 +223,7 @@ def find(self, datum):
222
223
Extra special case: auto ids do not have children,
223
224
so cut it off right now rather than auto id the auto id
224
225
"""
225
-
226
+
226
227
return [submatch
227
228
for subdata in self .left .find (datum )
228
229
if not isinstance (subdata , AutoIdForDatum )
@@ -256,7 +257,7 @@ def __str__(self):
256
257
257
258
def __repr__ (self ):
258
259
return 'Parent()'
259
-
260
+
260
261
261
262
class Where (JSONPath ):
262
263
"""
@@ -267,7 +268,7 @@ class Where(JSONPath):
267
268
WARNING: Subject to change. May want to have "contains"
268
269
or some other better word for it.
269
270
"""
270
-
271
+
271
272
def __init__ (self , left , right ):
272
273
self .left = left
273
274
self .right = right
@@ -286,7 +287,7 @@ class Descendants(JSONPath):
286
287
JSONPath that matches first the left expression then any descendant
287
288
of it which matches the right expression.
288
289
"""
289
-
290
+
290
291
def __init__ (self , left , right ):
291
292
self .left = left
292
293
self .right = right
@@ -295,7 +296,7 @@ def find(self, datum):
295
296
# <left> .. <right> ==> <left> . (<right> | *..<right> | [*]..<right>)
296
297
#
297
298
# With with a wonky caveat that since Slice() has funky coercions
298
- # we cannot just delegate to that equivalence or we'll hit an
299
+ # we cannot just delegate to that equivalence or we'll hit an
299
300
# infinite loop. So right here we implement the coercion-free version.
300
301
301
302
# Get all left matches into a list
@@ -321,12 +322,12 @@ def match_recursively(datum):
321
322
recursive_matches = []
322
323
323
324
return right_matches + list (recursive_matches )
324
-
325
+
325
326
# TODO: repeatable iterator instead of list?
326
327
return [submatch
327
328
for left_match in left_matches
328
329
for submatch in match_recursively (left_match )]
329
-
330
+
330
331
def is_singular ():
331
332
return False
332
333
@@ -385,7 +386,7 @@ class Fields(JSONPath):
385
386
WARNING: If '*' is any of the field names, then they will
386
387
all be returned.
387
388
"""
388
-
389
+
389
390
def __init__ (self , * fields ):
390
391
self .fields = fields
391
392
@@ -411,7 +412,7 @@ def reified_fields(self, datum):
411
412
412
413
def find (self , datum ):
413
414
datum = DatumInContext .wrap (datum )
414
-
415
+
415
416
return [field_datum
416
417
for field_datum in [self .get_field_datum (datum , field ) for field in self .reified_fields (datum )]
417
418
if field_datum is not None ]
@@ -429,7 +430,7 @@ def __eq__(self, other):
429
430
class Index (JSONPath ):
430
431
"""
431
432
JSONPath that matches indices of the current datum, or none if not large enough.
432
- Concrete syntax is brackets.
433
+ Concrete syntax is brackets.
433
434
434
435
WARNING: If the datum is not long enough, it will not crash but will not match anything.
435
436
NOTE: For the concrete syntax of `[*]`, the abstract syntax is a Slice() with no parameters (equiv to `[:]`
@@ -440,7 +441,7 @@ def __init__(self, index):
440
441
441
442
def find (self , datum ):
442
443
datum = DatumInContext .wrap (datum )
443
-
444
+
444
445
if len (datum .value ) > self .index :
445
446
return [DatumInContext (datum .value [self .index ], path = self , context = datum )]
446
447
else :
@@ -454,15 +455,15 @@ def __str__(self):
454
455
455
456
class Slice (JSONPath ):
456
457
"""
457
- JSONPath matching a slice of an array.
458
+ JSONPath matching a slice of an array.
458
459
459
460
Because of a mismatch between JSON and XML when schema-unaware,
460
461
this always returns an iterable; if the incoming data
461
462
was not a list, then it returns a one element list _containing_ that
462
463
data.
463
464
464
465
Consider these two docs, and their schema-unaware translation to JSON:
465
-
466
+
466
467
<a><b>hello</b></a> ==> {"a": {"b": "hello"}}
467
468
<a><b>hello</b><b>goodbye</b></a> ==> {"a": {"b": ["hello", "goodbye"]}}
468
469
@@ -480,10 +481,10 @@ def __init__(self, start=None, end=None, step=None):
480
481
self .start = start
481
482
self .end = end
482
483
self .step = step
483
-
484
+
484
485
def find (self , datum ):
485
486
datum = DatumInContext .wrap (datum )
486
-
487
+
487
488
# Here's the hack. If it is a dictionary or some kind of constant,
488
489
# put it in a single-element list
489
490
if (isinstance (datum .value , dict ) or isinstance (datum .value , six .integer_types ) or isinstance (datum .value , six .string_types )):
@@ -500,7 +501,7 @@ def __str__(self):
500
501
if self .start == None and self .end == None and self .step == None :
501
502
return '[*]'
502
503
else :
503
- return '[%s%s%s]' % (self .start or '' ,
504
+ return '[%s%s%s]' % (self .start or '' ,
504
505
':%d' % self .end if self .end else '' ,
505
506
':%d' % self .step if self .step else '' )
506
507
@@ -559,3 +560,90 @@ def __repr__(self):
559
560
560
561
def __str__ (self ):
561
562
return '[?%s]' % self .expressions
563
+
564
+
565
+ OPERATOR_MAP = {
566
+ '!=' : operator .ne ,
567
+ '==' : operator .eq ,
568
+ '=' : operator .eq ,
569
+ '<=' : operator .le ,
570
+ '<' : operator .lt ,
571
+ '>=' : operator .ge ,
572
+ '>' : operator .gt ,
573
+ }
574
+
575
+
576
+ class Filter (JSONPath ):
577
+ """The JSONQuery filter"""
578
+
579
+ def __init__ (self , expressions ):
580
+ self .expressions = expressions
581
+
582
+ def find (self , datum ):
583
+ if not self .expressions :
584
+ return []
585
+
586
+ datum = DatumInContext .wrap (datum )
587
+ return [DatumInContext (datum .value [i ],
588
+ path = Index (i ),
589
+ context = datum )
590
+ for i in xrange (0 , len (datum .value ))
591
+ if (len (self .expressions ) ==
592
+ len (list (filter (lambda x : x .find (datum .value [i ]),
593
+ self .expressions ))))]
594
+
595
+ def __repr__ (self ):
596
+ return '%s(%r)' % (self .__class__ .__name__ , self .expressions )
597
+
598
+ def __str__ (self ):
599
+ return '[?%s]' % self .expressions
600
+
601
+
602
+ class FilterExpression (JSONPath ):
603
+ """The JSONQuery expression"""
604
+
605
+ def __init__ (self , target , op , value ):
606
+ self .target = target
607
+ self .op = op
608
+ self .value = value
609
+
610
+ def find (self , datum ):
611
+ datum = self .target .find (DatumInContext .wrap (datum ))
612
+
613
+ if not datum :
614
+ return []
615
+ if self .op is None :
616
+ return datum
617
+
618
+ found = []
619
+ for data in datum :
620
+ value = data .value
621
+ if isinstance (self .value , int ):
622
+ try :
623
+ value = int (value )
624
+ except ValueError :
625
+ continue
626
+
627
+ if OPERATOR_MAP [self .op ](value , self .value ):
628
+ found .append (data )
629
+
630
+ return found
631
+
632
+ def __eq__ (self , other ):
633
+ return (isinstance (other , Filter ) and
634
+ self .target == other .target and
635
+ self .op == other .op and
636
+ self .value == other .value )
637
+
638
+ def __repr__ (self ):
639
+ if self .op is None :
640
+ return '%s(%r)' % (self .__class__ .__name__ , self .target )
641
+ else :
642
+ return '%s(%r %s %r)' % (self .__class__ .__name__ ,
643
+ self .target , self .op , self .value )
644
+
645
+ def __str__ (self ):
646
+ if self .op is None :
647
+ return '%s' % self .target
648
+ else :
649
+ return '%s %s %s' % (self .target , self .op , self .value )
0 commit comments