Package jsondata ::
Module JSONPatch
|
|
1
2 """The JSONPatch module provides for the alteration of JSON data compliant to RFC6902.
3
4 The emphasis of the design combines low resource requirement with features
5 designed for the application of large filters onto large JSON based data
6 structures.
7
8 The patch list itself is defined by RFC6902 as a JSON array. The entries
9 could be either constructed in-memory, or imported from a persistent storage.
10 The export feature provides for the persistent storage of a modified patch
11 list for later reuse.
12
13 The module contains the following classes:
14
15 * **JSONPatch**:
16 The controller for the application of patches on in-memory
17 data structures provided by the package 'json'.
18
19 * **JSONPatchItem**:
20 Representation of one patch entry in accordance to RFC6902.
21
22 * **JSONPatchItemRaw**:
23 Representation of one patch entry read as a raw entry in accordance to RFC6902.
24
25 * **JSONPatchFilter**:
26 Selection filter for the application on the current patch list
27 entries JSONPatchItem.
28
29 * **JSONPatchException**:
30 Specific exception for this module.
31
32
33 The address of the the provided 'path' components for the entries are managed
34 by the class JSONPointer in accordance to RFC6901.
35 """
36 __author__ = 'Arno-Can Uestuensoez'
37 __maintainer__ = 'Arno-Can Uestuensoez'
38 __license__ = "Artistic-License-2.0 + Forced-Fairplay-Constraints"
39 __copyright__ = "Copyright (C) 2015-2016 Arno-Can Uestuensoez @Ingenieurbuero Arno-Can Uestuensoez"
40 __version__ = '0.2.14'
41 __uuid__='63b597d6-4ada-4880-9f99-f5e0961351fb'
42
43 import sys
44
45 version = '{0}.{1}'.format(*sys.version_info[:2])
46 if not version in ('2.6','2.7',):
47 raise Exception("Requires Python-2.6.* or higher")
48
49
50
51 if sys.modules.get('json'):
52 import json as myjson
53 elif sys.modules.get('ujson'):
54 import ujson as myjson
55 else:
56 import json as myjson
57
58
59 from types import NoneType
60 from jsondata.JSONPointer import JSONPointer
61 from jsondata.JSONDataSerializer import JSONDataSerializer,MODE_SCHEMA_OFF
62
63
64 _appname = "jsonpatch"
65
66 _interactive = False
67
68
69
70 RFC6902_ADD = 1
71 RFC6902_COPY = 2
72 RFC6902_MOVE = 3
73 RFC6902_REMOVE = 4
74 RFC6902_REPLACE = 5
75 RFC6902_TEST = 6
76
77
78
79 op2str = {
80 RFC6902_ADD: "add",
81 RFC6902_COPY: "copy",
82 RFC6902_MOVE: "move",
83 RFC6902_REMOVE: "remove",
84 RFC6902_REPLACE: "replace",
85 RFC6902_TEST: "test"
86 }
87
88
89
90 str2op = {
91 "add": RFC6902_ADD,
92 "copy": RFC6902_COPY,
93 "move": RFC6902_MOVE,
94 "remove": RFC6902_REMOVE,
95 "replace": RFC6902_REPLACE,
96 "test": RFC6902_TEST
97 }
99 """Converts input into corresponding enumeration.
100 """
101 if type(x) in (int,float,):
102 return int(x)
103 elif type(x) is (str,unicode,) and x.isdigit():
104 return int(x)
105 return str2op.get(x,None)
106
107
110
113
115 """Record entry for list of patch tasks.
116
117 Attributes:
118 op: operations:
119 add, copy, move, remove, replace, test
120
121 target: JSONPointer for the modification target, see RFC6902.
122
123 value: Value, either a branch, or a leaf of the JSON data structure.
124 src: JSONPointer for the modification source, see RFC6902.
125
126 """
127 - def __init__(self,op,target,param=None):
128 """Create an entry for the patch list.
129
130 Args:
131 op: Operation: add, copy, move, remove, replace, test
132
133 target: Target node.
134
135 param: Parameter specific for the operation:
136 value: add,replace, test
137 src: copy, move
138 param:=None for 'remove'
139
140 Returns:
141 When successful returns 'True', else returns either 'False', or
142 raises an exception.
143 Success is the complete addition only, thus one failure returns
144 False.
145
146 Raises:
147 JSONDataSerializerError:
148
149 """
150 self.value = None
151 self.src = None
152
153 self.op = getOp(op)
154 self.target = JSONPointer(target)
155
156 if self.op in (RFC6902_ADD,RFC6902_REPLACE,RFC6902_TEST):
157 self.value = param
158
159 elif self.op is RFC6902_REMOVE:
160 pass
161
162 elif self.op in (RFC6902_COPY,RFC6902_MOVE):
163 self.op = op
164 self.src = param
165
166 else:
167 raise JSONPatchItemException("Unknown operation.")
168
170 """Evaluates the related task for the provided data.
171
172 Args:
173 j: JSON data the task has to be
174 applied on.
175
176 Returns:
177 Returns a tuple of:
178 0: len of the job list
179 1: list of the execution status for
180 the tasks
181
182 Raises:
183 JSONPatchException:
184 """
185 return self.apply(j)
186
188 """Compares this pointer with x.
189
190 Args:
191 x: A valid Pointer.
192
193 Returns:
194 True or False
195
196 Raises:
197 JSONPointerException
198 """
199 ret = True
200
201 if type(x) == dict:
202 ret &= self.target == x['path']
203 else:
204 ret &= self.target == x['target']
205
206 if self.op == RFC6902_ADD:
207 ret &= x['op'] in ('add',RFC6902_ADD)
208 ret &= self.value == x['value']
209 elif self.op == RFC6902_REMOVE:
210 ret &= x['op'] in ('remove',RFC6902_REMOVE)
211 elif self.op == RFC6902_REPLACE:
212 ret &= x['op'] in ('replace',RFC6902_REPLACE)
213 ret &= self.value == x['value']
214 elif self.op == RFC6902_MOVE:
215 ret &= x['op'] in ('move',RFC6902_MOVE)
216 ret &= self.src == x['from']
217 elif self.op == RFC6902_COPY:
218 ret &= x['op'] in ('copy',RFC6902_COPY)
219 ret &= self.src == x['from']
220 elif self.op == RFC6902_TEST:
221 ret &= x['op'] in ('test',RFC6902_TEST)
222 ret &= self.value == x['value']
223
224 return ret
225
227 """Support of various mappings.
228
229 #. self[key]
230
231 #. self[i:j:k]
232
233 #. x in self
234
235 #. for x in self
236
237 """
238 if key in ('path', 'target',):
239 return self.target
240 elif key in ('op',):
241 return self.op
242 elif key in ('value','param',):
243 return self.value
244 elif key in ('from','src',):
245 return self.src
246
248 """Compares this pointer with x.
249
250 Args:
251 x: A valid Pointer.
252
253 Returns:
254 True or False
255
256 Raises:
257 JSONPointerException
258 """
259 return not self.__eq__(x)
260
262 """Prints the patch string in accordance to RFC6901.
263 """
264 ret = "{u'op': u'"+unicode(op2str[self.op])+"', u'path': u'"+unicode(self.target)+"'"
265 if self.op in (RFC6902_ADD,RFC6902_REPLACE,RFC6902_TEST):
266 if type(self.value) in (int,float):
267 ret += ", u'value': "+unicode(self.value)
268 elif type(self.value) in (dict,list):
269 ret += ", u'value': "+repr(self.value)
270 else:
271 ret += ", u'value': u'"+unicode(self.value)+"'"
272
273 elif self.op is RFC6902_REMOVE:
274 pass
275
276 elif self.op in (RFC6902_COPY,RFC6902_MOVE):
277 ret += ", u'from': u'"+unicode(self.src)+"'"
278 ret += "}"
279 return ret
280
282 """Prints the patch string in accordance to RFC6901.
283 """
284 ret = '{"op": "'+op2str[self.op]+'", "target": "'+str(self.target)
285 if self.op in (RFC6902_ADD,RFC6902_REPLACE,RFC6902_TEST):
286 if type(self.value) in (int,float):
287 ret += '", "value": '+str(self.value)+' }'
288 else:
289 ret += '", "value": "'+str(self.value)+'" }'
290
291 elif self.op is RFC6902_REMOVE:
292 ret += '" }'
293
294 elif self.op in (RFC6902_COPY,RFC6902_MOVE):
295 ret += '", "src": "'+str(self.src)+'" }'
296 return ret
297
298 - def apply(self,jsondata):
299 """Applies the present patch list on the provided JSON document.
300
301 Args:
302 jsondata: Document to be patched.
303 Returns:
304 When successful returns 'True', else raises an exception.
305 Or returns a tuple:
306 (n,lerr): n: number of present active entries
307 lerr: list of failed entries
308 Raises:
309 JSONPatchException:
310 """
311
312 if self.op is RFC6902_ADD:
313
314
315 nbranch = jsondata.branch_add(
316 self.target,
317 None,
318 self.value)
319 return True
320
321 if isinstance(jsondata,JSONDataSerializer):
322 jsondata = jsondata.data
323
324 if self.op is RFC6902_REPLACE:
325 n,b = self.target.get_node_and_child(jsondata)
326 n[unicode(b)] = unicode(self.value)
327
328 elif self.op is RFC6902_TEST:
329 n,b = JSONPointer(self.target,False).get_node_and_child(jsondata)
330 if type(self.value) is str:
331 self.value = unicode(self.value)
332 if type(n) is list:
333 return n[b] == self.value
334 return n[unicode(b)] == self.value
335 elif self.op is RFC6902_COPY:
336 val = JSONPointer(self.src).get_node_or_value(jsondata)
337 tn,tc = self.target.get_node_and_child(jsondata)
338 tn[tc] = val
339
340 elif self.op is RFC6902_MOVE:
341 val = JSONPointer(self.src).get_node_or_value(jsondata)
342 sn,sc = JSONPointer(self.src).get_node_and_child(jsondata)
343 sn.pop(sc)
344 tn,tc = self.target.get_node_and_child(jsondata)
345 if type(tn) is list:
346 if len(tn)<=tc:
347 tn.append(val)
348 else:
349 tn[tc] = val
350 else:
351 tn[tc] = val
352
353 elif self.op is RFC6902_REMOVE:
354 n,b = self.target.get_node_and_child(jsondata)
355 n.pop(b)
356
357 return True
358
360 """Prints the patch string for export in accordance to RFC6901.
361 """
362 ret = '{"op": "'+str(op2str[self.op])+'", "path": "'+str(self.target)+'"'
363 if self.op in (RFC6902_ADD,RFC6902_REPLACE,RFC6902_TEST):
364 if type(self.value) in (int,float):
365 ret += ', "value": '+str(self.value)
366 elif type(self.value) in (dict,list):
367 ret += ', "value": '+str(self.value)
368 else:
369 ret += ', "value": "'+str(self.value)+'"'
370
371 elif self.op is RFC6902_REMOVE:
372 pass
373
374 elif self.op in (RFC6902_COPY,RFC6902_MOVE):
375 ret += ', "from": "'+str(self.src)+'"'
376 ret += '}'
377 return ret
378
380 """Adds native patch strings or an unsorted dict for RFC6902.
381 """
383 """Parse a raw patch string in accordance to RFC6902.
384 """
385 if type(patchstring) in (str,unicode,):
386 ps = myjson.loads(patchstring)
387 sx = myjson.dumps(ps)
388
389
390
391
392 if len(sx.replace(" ","")) != len(patchstring.replace(" ","")):
393 raise JSONPatchItemException("Repetition is not compliant to RFC6902:"+str(patchstring))
394 elif type(patchstring) is dict:
395 ps = patchstring
396 else:
397 raise JSONPatchItemException("Type not supported:"+str(patchstring))
398
399 try:
400 target = ps['path']
401 op = getOp(ps['op'])
402
403 if op in (RFC6902_ADD,RFC6902_REPLACE,RFC6902_TEST):
404 param = ps['value']
405
406 elif op is RFC6902_REMOVE:
407 param = None
408
409 elif op in (RFC6902_COPY,RFC6902_MOVE):
410 param = ps['from']
411 except Exception as e:
412 raise JSONPatchItemException(e)
413
414 super(JSONPatchItemRaw,self).__init__(op,target,param)
415
417 """Filtering capabilities on the entries of patch lists.
418 """
420 """
421 Args:
422 **kargs: Filter parameters:
423 Common:
424
425 contain=(True|False): Contain, else equal.
426
427 type=<node-type>: Node is of type.
428
429 Paths:
430
431 branch=<branch>:
432
433 deep=(): Determines the depth of comparison.
434
435 prefix=<prefix>: Any node of prefix. If prefix is
436 absolute: the only and one, else None.
437 relative: any node prefixed by the path fragment.
438
439 Values:
440 val=<node-value>: Node ha the value.
441
442
443 Returns:
444 True or False
445
446 Raises:
447 JSONPointerException:
448 """
449 for k,v in kargs:
450 if k == 'prefix':
451 self.prefix = v
452 elif k == 'branch':
453 self.branch = v
454
455 pass
456
460
464
466 """ Representation of a JSONPatch task list for RFC6902.
467
468 Contains the defined methods from standards:
469
470 * add
471 * remove
472 * replace
473 * move
474 * copy
475 * test
476
477 Attributes:
478 patch: List of patch items.
479
480 """
482 self.patch = []
483 """List of patch tasks. """
484
485 self.deep = False
486 """Defines copy operations, True:=deep, False:=swallow"""
487
488
489
490
491
493 """Creates a copy of 'self' and adds a patch jobs to the task queue.
494 """
495 if not x:
496 raise JSONPatchException("Missing patch entry/patch")
497 if isinstance(x, JSONPatchItem):
498 return JSONPatch(self.patch).patch.append(x)
499 elif isinstance(x, JSONPatch):
500 return JSONPatch(self.patch).patch.extend(x.patch)
501 else:
502 raise JSONPatchException("Unknown input"+type(x))
503
505 """Evaluates the related task for the provided index.
506
507 Args:
508 x: Task index.
509
510 j: JSON data the task has to be
511 applied on.
512
513 Returns:
514 Returns a tuple of:
515 0: len of the job list
516 1: list of the execution status for
517 the tasks
518
519 Raises:
520 JSONPatchException:
521 """
522 if type(x) is NoneType:
523 return self.apply(j)
524 if self.patch[x](j):
525 return 1,[]
526 return 1,[0]
527
529 """Compares this pointer with x.
530
531 Args:
532 x: A valid Pointer.
533
534 Returns:
535 True or False
536
537 Raises:
538 JSONPointerException
539 """
540 match = len(self.patch)
541 if match != len(x):
542 return False
543
544 for p in sorted(self.patch):
545 for xi in sorted(x):
546 if p==xi:
547 match -= 1
548 continue
549 return match == 0
550
552 """Support of slices, for 'iterator' refer to self.__iter__.
553
554 #. self[key]
555
556 #. self[i:j:k]
557
558 #. x in self
559
560 #. for x in self
561
562 """
563 return self.patch[key]
564
566 """Adds patch jobs to the task queue in place.
567 """
568 if not x:
569 raise JSONPatchException("Missing patch entry/patch")
570 if isinstance(x, JSONPatchItem):
571 self.patch.append(x)
572 elif isinstance(x, JSONPatch):
573 self.patch.extend(x.patch)
574 else:
575 raise JSONPatchException("Unknown input"+type(x))
576 return self
577
579 """Removes the patch job from the task queue in place.
580
581 Removes one of the following type(x) variants:
582
583 int: The patch job with given index.
584
585 JSONPatchItem: The first matching entry from
586 the task queue.
587
588 Args:
589 x: Item to be removed.
590
591 Returns:
592 Returns resulting list without x.
593
594 Raises:
595 JSONPatchException:
596 """
597 if type(x) is int:
598 self.patch.pop(x)
599 else:
600 self.patch.remove(x)
601 return self
602
604 """Provides an iterator foreseen for large amounts of in-memory patches.
605 """
606 return iter(self.patch)
607
609 """The number of outstanding patches.
610 """
611 return len(self.patch)
612
614 """Compares this pointer with x.
615
616 Args:
617 x: A valid Pointer.
618
619 Returns:
620 True or False
621
622 Raises:
623 JSONPointerException
624 """
625 return not self.__eq__(x)
626
628 """Prints the representation format of a JSON patch list.
629 """
630 ret = "["
631 if self.patch:
632 if len(self.patch)>1:
633 for p in self.patch[:-1]:
634 ret += repr(p)+", "
635 ret += repr(self.patch[-1])
636 ret += "]"
637 return unicode(ret)
638
640 """Prints the display format.
641 """
642 ret = "[\n"
643 if self.patch:
644 if len(self.patch)>1:
645 for p in self.patch[:-1]:
646 ret += " "+repr(p)+",\n"
647 ret += " "+repr(self.patch[-1])+"\n"
648 ret += "]"
649 return str(ret)
650
652 """Removes the patch job from the task queue.
653
654 Removes one of the following type(x) variants:
655
656 int: The patch job with given index.
657
658 JSONPatchItem: The first matching entry from
659 the task queue.
660
661 Args:
662 x: Item to be removed.
663
664 Returns:
665 Returns resulting list without x.
666
667 Raises:
668 JSONPatchException:
669 """
670 ret = JSONPatch()
671 if self.deep:
672 ret.patch = self.patch[:]
673 else:
674 ret.patch = self.patch
675 if type(x) is int:
676 ret.patch.pop(x)
677 else:
678 ret.patch.remove(x)
679 return ret
680
681 - def apply(self,jsondata):
682 """Applies the JSONPatch task.
683
684 Args:
685 jsondata: JSON data the joblist has to be applied on.
686
687 Returns:
688 Returns a tuple of:
689 0: len of the job list
690 1: list of the execution status for the tasks
691
692 Raises:
693 JSONPatchException:
694 """
695 status = []
696 for p in self.patch:
697 if not p.apply(jsondata):
698 status.append(self.patch.index(p))
699 return len(self.patch),status
700
701 - def get(self,x=None):
702 """
703 """
704 ret = self.patch
705
706
707 return ret
708
710 """Exports the current task list.
711
712 Provided formats are:
713 RFC6902
714
715 Supports the formats:
716 RFC6902
717
718 Args:
719 patchfile:
720 JSON patch for export.
721 schema:
722 JSON-Schema for validation of the patch list.
723 **kargs:
724 validator: [default, draft3, off, ]
725 Sets schema validator for the data file.
726 The values are: default=validate, draft3=Draft3Validator,
727 off=None.
728 default:= validate
729
730 Returns:
731 When successful returns 'True', else raises an exception.
732
733 Raises:
734 JSONPatchException:
735
736 """
737 try:
738 with open(patchfile, 'w') as fp:
739 fp.writelines(self.repr_export())
740 except Exception as e:
741 raise JSONPatchException("open-"+str(e),"data.dump",str(patchfile))
742 return True
743
744 - def patch_import(self, patchfile, schemafile=None, **kargs):
745 """Imports a task list.
746
747 Supports the formats:
748 RFC6902
749
750 Args:
751 patchfile:
752 JSON patch filename containing the list of patch operations.
753 schemafile:
754 JSON-Schema filename for validation of the patch list.
755 **kargs:
756 validator: [default, draft3, off, ]
757 Sets schema validator for the data file.
758 The values are: default=validate, draft3=Draft3Validator,
759 off=None.
760 default:= validate
761
762 Returns:
763 When successful returns 'True', else raises an exception.
764
765 Raises:
766 JSONPatchException:
767
768 """
769 appname = _appname
770 kargs = {}
771 kargs['datafile'] = patchfile
772 kargs['schemafile'] = schemafile
773 kargs['validator'] = MODE_SCHEMA_OFF
774 for k,v in kargs.items():
775 if k == 'nodefaultpath':
776 kargs['nodefaultpath'] = True
777 elif k == 'pathlist':
778 kargs['pathlist'] = v
779 elif k == 'validator':
780 kargs['validator'] = v
781 elif k == 'appname':
782 appname = v
783 patchdata = JSONDataSerializer(appname,**kargs)
784
785 for pi in patchdata.data:
786 self += JSONPatchItemRaw(pi)
787 return True
788
790 """Prints the export representation format of a JSON patch list.
791 """
792 ret = "["
793 if self.patch:
794 if len(self.patch)>1:
795 for p in self.patch[:-1]:
796 ret += p.repr_export()+", "
797 ret += self.patch[-1].repr_export()
798 ret += "]"
799 return ret
800