Package jsondata :: Module jsonpatch
[hide private]
[frames] | no frames]

Source Code for Module jsondata.jsonpatch

   1  # -*- coding:utf-8   -*- 
   2  """The JSONPatch module provides for the alteration of JSON data compliant to RFC6902. 
   3  """ 
   4  from __future__ import absolute_import 
   5  from __future__ import print_function 
   6  from __future__ import division 
   7   
   8  import sys 
   9  import copy 
  10   
  11  # pylint: disable-msg=F0401 
  12  if sys.modules.get('json'): 
  13      import json as myjson  # @UnusedImport 
  14  elif sys.modules.get('ujson'): 
  15      import ujson as myjson  # @UnusedImport @Reimport @UnresolvedImport 
  16  else: 
  17      import json as myjson  # @Reimport 
  18  # pylint: enable-msg=F0401 
  19   
  20  # for now the only one supported 
  21  from jsondata.jsonpointer import JSONPointer 
  22  from jsondata.jsondataserializer import JSONDataSerializer, MS_OFF 
  23  from jsondata.jsondata import JSONData 
  24  from jsondata import V3K, JSONDataPatchError, JSONDataPatchItemError, \ 
  25      C_SHALLOW, C_DEEP, C_REF, \ 
  26      SD_INPUT, SD_OUTPUT 
  27   
  28   
  29  __author__ = 'Arno-Can Uestuensoez' 
  30  __maintainer__ = 'Arno-Can Uestuensoez' 
  31  __license__ = "Artistic-License-2.0 + Forced-Fairplay-Constraints" 
  32  __copyright__ = "Copyright (C) 2015-2016 Arno-Can Uestuensoez" \ 
  33                  " @Ingenieurbuero Arno-Can Uestuensoez" 
  34  __version__ = '0.2.21' 
  35  __uuid__ = '63b597d6-4ada-4880-9f99-f5e0961351fb' 
  36   
  37  if V3K: 
  38      unicode = str 
  39   
  40  # Sets display for inetractive JSON/JSONschema design. 
  41  _interactive = False 
  42   
  43  # 
  44  # Operations in accordance to RFC6902 
  45  RFC6902_ADD = 1 
  46  RFC6902_COPY = 2 
  47  RFC6902_MOVE = 3 
  48  RFC6902_REMOVE = 4 
  49  RFC6902_REPLACE = 5 
  50  RFC6902_TEST = 6 
  51   
  52  # 
  53  # Mapping for reverse transformation 
  54  op2str = { 
  55      RFC6902_ADD: "add", 
  56      RFC6902_COPY: "copy", 
  57      RFC6902_MOVE: "move", 
  58      RFC6902_REMOVE: "remove", 
  59      RFC6902_REPLACE: "replace", 
  60      RFC6902_TEST: "test" 
  61  } 
  62   
  63  # 
  64  # Mapping for reverse transformation 
  65  str2op = { 
  66      "add": RFC6902_ADD, 
  67      "copy": RFC6902_COPY, 
  68      "move": RFC6902_MOVE, 
  69      "remove": RFC6902_REMOVE, 
  70      "replace": RFC6902_REPLACE, 
  71      "test": RFC6902_TEST 
  72  } 
  73   
  74   
75 -def getOp(x):
76 """Converts input into corresponding enumeration. 77 """ 78 if type(x) in ( 79 int, 80 float, ): 81 return int(x) 82 elif type(x) is ( 83 str, 84 unicode, ) and x.isdigit(): 85 return int(x) 86 return str2op.get(x, None)
87 88
89 -class JSONPatchItem(object):
90 """Record entry for list of patch tasks. 91 92 Attributes: 93 **op**: 94 operations: :: 95 96 add, copy, move, remove, replace, test 97 98 **target**: 99 JSONPointer for the modification target, see RFC6902. 100 101 **value**: 102 Value, either a branch, or a leaf of the JSON data structure. 103 104 **src**: 105 JSONPointer for the modification source, see RFC6902. 106 107 """ 108
109 - def __init__(self, op, target, param=None, **kargs):
110 """Create an entry for the patch list. 111 112 Args: 113 **op**: 114 Operation: :: 115 116 add, copy, move, remove, replace, test 117 118 **target**: 119 Target node. :: 120 121 target := ( 122 <rfc6901-string> 123 | JSONPointer 124 | <path-items-list> 125 ) 126 127 **param**: 128 Specific parameter for the operation. 129 130 +-------+--------------------+ 131 | type | operation | 132 +=======+====================+ 133 | value | add, replace, test | 134 +-------+--------------------+ 135 | src | copy, move | 136 +-------+--------------------+ 137 | param | None for 'remove' | 138 +-------+--------------------+ 139 140 kargs: 141 **replace**: 142 Replace masked characters in *target* specification. :: 143 144 replace := ( 145 True # replaces rfc6901 escape sequences: ~0 and ~1 146 | False # omit unescaping 147 ) 148 149 .. note:: 150 151 Match operations are proceeded literally, thus the 152 escaped characters should be consistent, 153 see rfc6901, Section 3. 154 155 default := False 156 157 Returns: 158 When successful returns 'True', else returns either 'False', or 159 raises an exception. 160 Success is the complete addition only, thus one failure returns 161 False. 162 163 Raises: 164 JSONDataPatchItemError 165 166 """ 167 self.replace = kargs.get('replace', False) 168 169 self.value = None 170 self.src = None 171 172 self.op = getOp(op) 173 self.target = JSONPointer(target, replace=self.replace) 174 175 if self.op in (RFC6902_ADD, RFC6902_REPLACE, RFC6902_TEST): 176 self.value = param 177 178 elif self.op is RFC6902_REMOVE: 179 pass 180 181 elif self.op in (RFC6902_COPY, RFC6902_MOVE): 182 self.src = param 183 184 else: 185 raise JSONDataPatchItemError("Unknown operation.")
186
187 - def __add__(self, x=None):
188 if x == None: 189 raise JSONDataPatchError("Missing patch entry/patch") 190 if isinstance(x, JSONPatchItem): 191 ret = JSONPatch() 192 ret.patch.append(self) 193 ret.patch.append(x) 194 return ret 195 elif isinstance(x, JSONPatch): 196 ret = JSONPatch(x.patch) 197 ret.patch.insert(0, self) 198 return ret 199 else: 200 raise JSONDataPatchError("Unknown input" + type(x))
201
202 - def __call__(self, jdata):
203 """Evaluates the related task for the provided data. 204 205 Args: 206 **jdata**: 207 JSON data the task has to be 208 applied on. 209 210 Returns: 211 Returns a tuple of: :: 212 213 (n,lerr): 214 215 n: number of present active entries 216 lerr: list of failed entries 217 218 Raises: 219 JSONDataPatchError: 220 """ 221 return self.apply(jdata)
222
223 - def __eq__(self, x=None):
224 """Compares this pointer with x. 225 226 Args: 227 **x**: 228 A valid Pointer. 229 230 Returns: 231 *True* or *False*. 232 233 Raises: 234 JSONPointerError 235 """ 236 if x == None: 237 return False 238 239 ret = True 240 241 if type(x) == dict: 242 ret &= self.target == x['path'] 243 else: 244 ret &= self.target == x['target'] 245 246 if self.op == RFC6902_ADD: 247 ret &= x['op'] in ('add', RFC6902_ADD) 248 ret &= self.value == x['value'] 249 elif self.op == RFC6902_REMOVE: 250 ret &= x['op'] in ('remove', RFC6902_REMOVE) 251 elif self.op == RFC6902_REPLACE: 252 ret &= x['op'] in ('replace', RFC6902_REPLACE) 253 ret &= self.value == x['value'] 254 elif self.op == RFC6902_MOVE: 255 ret &= x['op'] in ('move', RFC6902_MOVE) 256 ret &= self.src == x['from'] 257 elif self.op == RFC6902_COPY: 258 ret &= x['op'] in ('copy', RFC6902_COPY) 259 ret &= self.src == x['from'] 260 elif self.op == RFC6902_TEST: 261 ret &= x['op'] in ('test', RFC6902_TEST) 262 ret &= self.value == x['value'] 263 264 return ret
265
266 - def __getitem__(self, key):
267 """Support of various mappings. 268 269 #. self[key] 270 271 #. self[i:j:k] 272 273 #. x in self 274 275 #. for x in self 276 277 """ 278 if key in ( 279 'path', 280 'target', ): 281 return self.target 282 elif key in ('op', ): 283 return self.op 284 elif key in ( 285 'value', 286 'param', ): 287 return self.value 288 elif key in ( 289 'from', 290 'src', ): 291 return self.src
292
293 - def __ne__(self, x):
294 """Compares this pointer with x. 295 296 Args: 297 **x**: A valid Pointer. 298 299 Returns: 300 *True* or *False*. 301 302 Raises: 303 JSONPointerError 304 """ 305 return not self.__eq__(x)
306
307 - def __radd__(self, x=None):
308 if x == None: 309 raise JSONDataPatchError("Missing patch entry/patch") 310 if isinstance(x, JSONPatchItem): 311 ret = JSONPatch() 312 ret.patch.append(x) 313 ret.patch.append(self) 314 return ret 315 elif isinstance(x, JSONPatch): 316 ret = JSONPatch() 317 ret.patch.extend(x.patch) 318 ret.patch.append(self) 319 return ret 320 else: 321 raise JSONDataPatchError("Unknown input" + type(x))
322
323 - def __repr__(self):
324 """Prints the patch string in accordance to RFC6901. 325 """ 326 ret = '{"op": "' + unicode(op2str[self.op]) + \ 327 '", "path": "' + unicode(self.target) + '"' 328 if self.op in (RFC6902_ADD, RFC6902_REPLACE, RFC6902_TEST): 329 if type(self.value) in (int, float): 330 ret += ', "value": ' + unicode(self.value) 331 elif type(self.value) in (dict, list): 332 ret += ', "value": ' + repr(self.value) 333 else: 334 ret += ', "value": "' + unicode(self.value) + '"' 335 336 elif self.op is RFC6902_REMOVE: 337 pass 338 339 elif self.op in (RFC6902_COPY, RFC6902_MOVE): 340 ret += ', "from": "' + unicode(self.src) + '"' 341 ret += "}" 342 return ret
343
344 - def __str__(self):
345 """Prints the patch string in accordance to RFC6901. 346 """ 347 ret = '{"op": "' + op2str[self.op] + \ 348 '", "target": "' + str(self.target) 349 if self.op in (RFC6902_ADD, RFC6902_REPLACE, RFC6902_TEST): 350 if type(self.value) in (int, float): 351 ret += '", "value": ' + str(self.value) + ' }' 352 else: 353 ret += '", "value": "' + str(self.value) + '" }' 354 355 elif self.op is RFC6902_REMOVE: 356 ret += '" }' 357 358 elif self.op in (RFC6902_COPY, RFC6902_MOVE): 359 ret += '", "src": "' + str(self.src) + '" }' 360 return ret
361
362 - def apply(self, jsondata, **kargs):
363 """Applies the present patch list on the provided JSON document. 364 365 Args: 366 **jsondata**: 367 Document to be patched. 368 369 kargs: 370 **replace**: 371 Replace masked characters in *target* specification. :: 372 373 replace := ( 374 True # replaces rfc6901 escape sequences: ~0 and ~1 375 | False # omit unescaping 376 ) 377 378 .. note:: 379 380 Match operations are proceeded literally, thus the 381 escaped characters should be consistent, 382 see rfc6901, Section 3. 383 384 If already decoded e.g. by the constructor, than should be 385 *FALSE*, is not idempotent. 386 387 default := False 388 389 Returns: 390 When successful returns 'True', else raises an exception. 391 Or returns a tuple: :: 392 393 (n,lerr): 394 395 n: number of present active entries 396 lerr: list of failed entries 397 398 Raises: 399 JSONDataPatchError: 400 """ 401 replace = kargs.get('replace', self.replace) 402 403 if self.op is RFC6902_ADD: 404 return jsondata.branch_add( 405 self.value, self.target) 406 407 if isinstance(jsondata, JSONDataSerializer): 408 jsondata = jsondata.data 409 410 if self.op is RFC6902_REPLACE: 411 n, b = self.target.get_node_and_key(jsondata) 412 n[b] = self.value 413 414 elif self.op is RFC6902_TEST: 415 n, b = JSONPointer(self.target, replace=replace).get_node_and_key(jsondata) 416 # if type(self.value) is str: 417 # self.value = unicode(self.value) 418 # if type(n) is list: 419 # return n[b] == self.value 420 # return n[unicode(b)] == self.value 421 if isinstance(n, list): 422 return n[int(b)] == self.value 423 elif isinstance(n, dict): 424 return n[unicode(b)] == self.value 425 426 elif self.op is RFC6902_COPY: 427 val = JSONPointer(self.src, replace=replace).get_node_value(jsondata) 428 tn, tc = self.target.get_node_and_key(jsondata) 429 tn[tc] = val 430 431 elif self.op is RFC6902_MOVE: 432 val = JSONPointer(self.src, replace=replace).get_node_value(jsondata) 433 sn, sc = JSONPointer(self.src, replace=replace).get_node_and_key(jsondata) 434 sn.pop(sc) 435 tn, tc = self.target.get_node_and_key(jsondata) 436 if type(tn) is list: 437 if len(tn) <= tc: 438 tn.append(val) 439 else: 440 tn[tc] = val 441 else: 442 tn[tc] = val 443 444 elif self.op is RFC6902_REMOVE: 445 n, b = self.target.get_node_and_key(jsondata) 446 n.pop(b) 447 448 return True
449
450 - def repr_export(self):
451 """Prints the patch string for export in accordance to RFC6901. 452 """ 453 ret = '{"op": "' + str(op2str[self.op]) + \ 454 '", "path": "' + str(self.target) + '"' 455 if self.op in (RFC6902_ADD, RFC6902_REPLACE, RFC6902_TEST): 456 if type(self.value) in (int, float): 457 ret += ', "value": ' + str(self.value) 458 elif type(self.value) in (dict, list): 459 ret += ', "value": ' + str(self.value) 460 elif type(self.value) is None: 461 ret += ', "value": null' 462 elif type(self.value) is False: 463 ret += ', "value": false' 464 elif type(self.value) is True: 465 ret += ', "value": true' 466 else: 467 ret += ', "value": "' + str(self.value) + '"' 468 469 elif self.op is RFC6902_REMOVE: 470 pass 471 472 elif self.op in (RFC6902_COPY, RFC6902_MOVE): 473 ret += ', "from": "' + str(self.src) + '"' 474 ret += '}' 475 return ret
476
477 - def str_export(self):
478 """Pretty prints the patch string for export in accordance to RFC6901. 479 """ 480 ret = '{"op": "' + str(op2str[self.op]) + \ 481 '", "path": "' + str(self.target) + '"' 482 if self.op in (RFC6902_ADD, RFC6902_REPLACE, RFC6902_TEST): 483 if type(self.value) in (int, float): 484 ret += ', "value": ' + str(self.value) 485 elif type(self.value) in (dict, list): 486 ret += ', "value": ' + str(self.value) 487 elif type(self.value) is None: 488 ret += ', "value": null' 489 elif type(self.value) is False: 490 ret += ', "value": false' 491 elif type(self.value) is True: 492 ret += ', "value": true' 493 else: 494 ret += ', "value": "' + str(self.value) + '"' 495 496 elif self.op is RFC6902_REMOVE: 497 pass 498 499 elif self.op in (RFC6902_COPY, RFC6902_MOVE): 500 ret += ', "from": "' + str(self.src) + '"' 501 ret += '}' 502 return ret
503
504 -class JSONPatchItemRaw(JSONPatchItem):
505 """Adds native patch strings or an unsorted dict for RFC6902. 506 507 Calls parent *JSONPatchItem*. 508 509 """ 510
511 - def __init__(self, patchstring, **kargs):
512 """Parse a raw patch string in accordance to RFC6902. 513 """ 514 self.replace = kargs.get('replace', False) 515 516 if type(patchstring) in ( 517 str, 518 unicode, ): 519 ps = myjson.loads(patchstring) 520 sx = myjson.dumps(ps) 521 if len(sx.replace(" ", "")) != len(patchstring.replace(" ", "")): 522 raise JSONDataPatchItemError( 523 "Repetition is not compliant to RFC6902:" + 524 str(patchstring)) 525 elif type(patchstring) is dict: 526 ps = patchstring 527 else: 528 raise JSONDataPatchItemError("Type not supported:" + 529 str(patchstring)) 530 531 try: 532 target = ps['path'] 533 op = getOp(ps['op']) 534 535 if op in (RFC6902_ADD, RFC6902_REPLACE, RFC6902_TEST): 536 param = ps['value'] 537 538 elif op is RFC6902_REMOVE: 539 param = None 540 541 elif op in (RFC6902_COPY, RFC6902_MOVE): 542 param = ps['from'] 543 except Exception as e: 544 raise JSONDataPatchItemError(e) 545 546 super(JSONPatchItemRaw, self).__init__(op, target, param, **kargs)
547 548
549 -class JSONPatchFilter(object):
550 """Filtering capabilities on the entries of patch lists. 551 552 .. warning:: 553 554 Not yet implemented. 555 556 """ 557
558 - def __init__(self, **kargs):
559 """ 560 Args: 561 kargs: 562 Filter parameters: 563 564 **common**: 565 566 contain=(True|False): Contain, else equal. 567 568 type=<node-type>: Node is of type. 569 570 **paths**: 571 572 branch=<branch>: 573 574 deep=(): Determines the depth of comparison. 575 576 prefix=<prefix>: Any node of prefix. If prefix is 577 absolute: the only and one, else None. 578 relative: any node prefixed by the path fragment. 579 580 **values**: 581 val=<node-value>: Node ha the value. 582 583 584 Returns: 585 True or False 586 587 Raises: 588 JSONPointerError: 589 """ 590 for k, v in kargs: 591 if k == 'prefix': 592 self.prefix = v 593 elif k == 'branch': 594 self.branch = v
595
596 - def __eq__(self, x):
597 598 pass
599
600 - def __ne__(self, x):
601 602 pass
603 604
605 -class JSONPatch(object):
606 """ Representation of a JSONPatch task list for RFC6902. 607 608 Contains the defined methods from standards: 609 610 * add 611 * remove 612 * replace 613 * move 614 * copy 615 * test 616 617 Attributes: 618 **patch**: 619 List of patch items. 620 621 """ 622
623 - def __init__(self, p=None, **kargs):
624 """List of patch tasks. 625 626 Args: 627 **p**: 628 Patch list. :: 629 630 p := ( 631 JSONPatch 632 | <list-of-patch-items> 633 ) 634 635 kargs: 636 **replace**: 637 Replace masked characters in *target* specification. :: 638 639 replace := ( 640 True # replaces rfc6901 escape sequences: ~0 and ~1 641 | False # omit unescaping 642 ) 643 644 .. note:: 645 646 Match operations are proceeded literally, thus the 647 escaped characters should be consistent, 648 see rfc6901, Section 3. 649 650 default := False 651 652 Returns: 653 self. 654 655 Raises: 656 pass-through 657 """ 658 assert isinstance(p, list) or p == None 659 660 self.replace = kargs.get('replace', False) 661 662 if p: 663 self.patch = p 664 else: 665 self.patch = [] 666 667 self.deep = False 668 """Defines copy operations, True:=deep, False:=swallow"""
669
670 - def __add__(self, x=None):
671 """Creates and adds patch job to the task queue. 672 673 Args: 674 **x**: 675 Extension of pathc job, eithe a *JSONPatch*, 676 or a *JSONPatchItem*. 677 678 Returns: 679 Returns a patch job, or raises Exception. 680 681 Raises: 682 JSONDataPatchError 683 684 """ 685 if x == None: 686 raise JSONDataPatchError("Missing patch entry/patch") 687 if isinstance(x, JSONPatchItem): 688 ret = JSONPatch(self.patch) 689 ret.patch.append(x) 690 return ret 691 elif isinstance(x, JSONPatch): 692 ret = JSONPatch(self.patch) 693 ret.patch.extend(x.patch) 694 return ret 695 else: 696 raise JSONDataPatchError("Unknown input" + type(x))
697
698 - def __call__(self, jdata, x=None):
699 """Evaluates the related task for the provided index. 700 701 Args: 702 **x**: 703 Task index. 704 705 **jdata**: 706 JSON data the task has to be 707 applied on. 708 709 Returns: 710 Returns a tuple of: :: 711 712 (n, lerr): 713 714 n: number of present active entries 715 lerr: list of failed entries 716 717 Raises: 718 JSONDataPatchError: 719 """ 720 if x is None: 721 return self.apply(jdata) 722 if self.patch[x](jdata): 723 return 1, [] 724 return 1, [0]
725
726 - def __eq__(self, x):
727 """Compares this pointer with x. 728 729 Args: 730 **x**: 731 A valid Pointer. 732 733 Returns: 734 *True* or *False* 735 736 Raises: 737 JSONPointerError 738 """ 739 if x == None: 740 return False 741 742 match = len(self.patch) 743 if match != len(x): 744 return False 745 746 if isinstance(x, JSONPatch): 747 return self.patch == x.patch 748 elif isinstance(x, list): 749 ret = True 750 for i in range(match): 751 for k in x[i].keys(): 752 if k == 'op': 753 ret &= str(x[i][k]) == op2str[self.patch[i][k]] 754 else: 755 ret &= str(x[i][k]) == str(self.patch[i][k]) 756 return ret 757 else: 758 raise JSONDataPatchError("Type requires JSONPatch or list, got" + str(type(x)))
759
760 - def __getitem__(self, key):
761 """Support of slices, for 'iterator' refer to self.__iter__. 762 763 #. self[key] 764 765 #. self[i:j:k] 766 767 #. x in self 768 769 #. for x in self 770 771 """ 772 return self.patch[key]
773
774 - def __iadd__(self, x=None):
775 """Adds patch jobs to the task queue in place. 776 """ 777 if x == None: 778 raise JSONDataPatchError("Missing patch entry/patch") 779 if isinstance(x, JSONPatchItem): 780 self.patch.append(x) 781 elif isinstance(x, JSONPatch): 782 self.patch.extend(x.patch) 783 else: 784 raise JSONDataPatchError("Unknown input" + type(x)) 785 return self
786
787 - def __isub__(self, x):
788 """Removes the patch job from the task queue in place. 789 790 Removes one of the following type(x) variants: 791 792 *int*: 793 The patch job with given index. 794 795 *JSONPatchItem*: 796 The first matching entry from 797 the task queue. 798 799 Args: 800 **x**: 801 Item to be removed. :: 802 803 x := ( 804 int 805 | JSONPatchItem 806 ) 807 808 Returns: 809 Returns resulting list without x. 810 811 Raises: 812 JSONDataPatchError: 813 """ 814 if type(x) is int: 815 self.patch.pop(x) 816 else: 817 self.patch.remove(x) 818 return self
819
820 - def __iter__(self):
821 """Provides an iterator foreseen for large amounts of in-memory patches. 822 """ 823 return iter(self.patch)
824
825 - def __len__(self):
826 """The number of outstanding patches. 827 """ 828 return len(self.patch)
829
830 - def __ne__(self, x):
831 """Compares this pointer with x. 832 833 Args: 834 **x**: 835 A valid Pointer. 836 837 Returns: 838 *True* or *False* 839 840 Raises: 841 JSONPointerError 842 """ 843 return not self.__eq__(x)
844
845 - def __radd__(self, x=None):
846 """Adds a copy of xto the task queue. 847 """ 848 if x == None: 849 raise JSONDataPatchError("Missing patch entry/patch") 850 if isinstance(x, JSONPatchItem): 851 ret = JSONPatch(x) 852 ret.patch.append(self.patch) 853 return ret 854 elif isinstance(x, JSONPatch): 855 ret = JSONPatch(x) 856 ret.patch.extend(self.patch) 857 return ret 858 else: 859 raise JSONDataPatchError("Unknown input" + type(x))
860
861 - def __repr__(self):
862 """Prints the representation format of a JSON patch list. 863 """ 864 ret = "[" 865 if self.patch: 866 if len(self.patch) > 1: 867 for p in self.patch[:-1]: 868 ret += repr(p) + ", " 869 ret += repr(self.patch[-1]) 870 ret += "]" 871 return unicode(ret)
872
873 - def __str__(self):
874 """Prints the display format. 875 """ 876 ret = "[\n" 877 if self.patch: 878 if len(self.patch) > 1: 879 for p in self.patch[:-1]: 880 ret += " " + repr(p) + ",\n" 881 ret += " " + repr(self.patch[-1]) + "\n" 882 ret += "]" 883 return str(ret)
884
885 - def __sub__(self, x):
886 """Removes the patch job from the task queue. 887 888 Removes one of the following type(x) variants: 889 890 *int*: 891 The patch job with given index. 892 893 *JSONPatchItem*: 894 The first matching entry from 895 the task queue. 896 897 Args: 898 **x**: 899 Item to be removed. :: 900 901 x := ( 902 int 903 | JSONPatchItem 904 ) 905 906 Returns: 907 Returns resulting list without x. 908 909 Raises: 910 JSONDataPatchError: 911 """ 912 ret = JSONPatch() 913 if self.deep: 914 ret.patch = self.patch[:] 915 else: 916 ret.patch = self.patch 917 918 if type(x) is int: 919 ret.patch.pop(x) 920 else: 921 ret.patch.remove(x) 922 return ret
923
924 - def apply(self, jsondata, **kargs):
925 """Applies the JSONPatch task. 926 927 Args: 928 **jsondata**: 929 JSON data the joblist has to be applied on. 930 931 Returns: 932 Returns a tuple of: :: 933 934 (n, lerr): 935 936 n: number of present active entries 937 lerr: list of failed entries 938 939 Raises: 940 JSONDataPatchError: 941 """ 942 status = [] 943 for p in self.patch: 944 if not p.apply(jsondata, **kargs): 945 # should not be called frequently 946 status.append(self.patch.index(p)) 947 return len(self.patch), status
948
949 - def getpatchitem(self, x=None):
950 """Gets the reference to a single patch item. 951 952 Args: 953 **x**: 954 Requested item. :: 955 956 x := ( 957 int 958 | JSONPatchItem 959 ) 960 961 int: index of patch item 962 JSONPatchItem: the reference to the patch item 963 964 Returns: 965 The selected patch item. 966 967 Raises: 968 None 969 """ 970 if x == None: 971 return 972 if type(x) is int: 973 return self.patch[x] 974 return self.patch[self.patch.index(x)]
975
976 - def getpatchitems(self, *args, **kargs):
977 """Gets a list of references of patch items. 978 979 Args: 980 args: 981 Requested items. :: 982 983 *args := ( 984 <item> 985 | <item-list> 986 | None 987 ) 988 item-list := <item>[, <item-list>] 989 item := ( 990 int 991 | JSONPatchItem 992 ) 993 None := "all items of the current patch list" 994 995 int: index of patch item 996 JSONPatchItem: the reference to the patch item 997 998 kargs: 999 **idxlist**: 1000 Print with index: :: 1001 1002 idxlist := ( 1003 True # format: [{<index>: <JSONPatchItem>}] 1004 | False # format: [<JSONPatchItem>] 1005 ) 1006 1007 **copydata**: 1008 Creates a copy of each resulting item. :: 1009 1010 copydata := (C_DEEP | C_SHALLOW | C_REF) 1011 1012 default := C_REF # no copy 1013 1014 Returns: 1015 A list of the selected patch items. 1016 1017 Raises: 1018 None 1019 """ 1020 ret = [] 1021 _copy = kargs.get('copydata', C_REF) 1022 1023 if args and args[0] == None: 1024 args = self.patch 1025 1026 for x in args: 1027 if type(x) is int: 1028 _c = self.patch[x] 1029 else: 1030 x = self.patch.index(x) 1031 _c = self.patch[x] 1032 1033 if _copy == C_SHALLOW: 1034 _c = copy.copy(_c) 1035 elif _copy == C_DEEP: 1036 _c = copy.deepcopy(_c) 1037 1038 if kargs.get('idxlist'): 1039 ret.append({x: _c}) 1040 else: 1041 ret.append(_c) 1042 1043 return ret
1044
1045 - def gettree(self, *args, **kargs):
1046 """Gets the resulting logical JSON data structure 1047 constructed from the patch items of the current set. 1048 1049 Args: 1050 args: 1051 Requested items. :: 1052 1053 *args := ( 1054 <item> 1055 | <item-list> 1056 | None 1057 ) 1058 item-list := <item>[, <item-list>] 1059 item := ( 1060 int 1061 | JSONPatchItem 1062 ) 1063 None := "all items of the current patch list" 1064 1065 int: index of patch item 1066 JSONPatchItem: the reference to the patch item 1067 1068 kargs: 1069 **data**: 1070 An optional JSON data structure, when provided 1071 the actual data as selected by the patch list 1072 is returned. Else the paths only. 1073 1074 default := None 1075 1076 **scope**: 1077 Defines the source scope of the data structure. :: 1078 1079 scope := ( 1080 "in" # input data, e.g. source for "copy" 1081 | "out" # output data, e.g. target for "copy 1082 ) 1083 1084 default := "out" 1085 1086 Returns: 1087 The combined list of the selected patch items contained 1088 in an object JSONData. 1089 1090 Raises: 1091 None 1092 """ 1093 _data = kargs.get('data', None) 1094 _scope = kargs.get('scope', 'out') 1095 if _scope == 'in': 1096 _scope = True 1097 else: 1098 _scope = False 1099 1100 _pl = self.getpatchitems(*args) 1101 1102 ret = JSONData('') 1103 1104 for x in _pl: 1105 if _scope == SD_INPUT: 1106 if x['op'] in (RFC6902_ADD,): 1107 ret.branch_add('add', x['path']) 1108 elif x['op'] in (RFC6902_COPY,): 1109 ret.branch_add('copy', x['from']) 1110 elif x['op'] in (RFC6902_MOVE,): 1111 ret.branch_add('move', x['from']) 1112 elif x['op'] in (RFC6902_REMOVE,): 1113 ret.branch_add('remove', x['path']) 1114 elif x['op'] in (RFC6902_REPLACE,): 1115 ret.branch_add('replace', x['path']) 1116 elif x['op'] in (RFC6902_TEST,): 1117 ret.branch_add('test', x['path']) 1118 1119 elif _scope == SD_OUTPUT: 1120 if x['op'] in (RFC6902_ADD,): 1121 ret.branch_add('add', x['path']) 1122 elif x['op'] in (RFC6902_COPY,): 1123 ret.branch_add('copy', x['path']) 1124 elif x['op'] in (RFC6902_MOVE,): 1125 ret.branch_add('move', x['path']) 1126 elif x['op'] in (RFC6902_REMOVE,): 1127 ret.branch_add('remove', x['path']) 1128 elif x['op'] in (RFC6902_REPLACE,): 1129 ret.branch_add('replace', x['path']) 1130 elif x['op'] in (RFC6902_TEST,): 1131 ret.branch_add('test', x['path']) 1132 1133 else: # both 1134 if x['op'] in (RFC6902_ADD,): 1135 ret.branch_add('add', x['path']) 1136 elif x['op'] in (RFC6902_COPY,): 1137 ret.branch_add('copy', x['from']) 1138 ret.branch_add('copy', x['path']) 1139 elif x['op'] in (RFC6902_MOVE,): 1140 ret.branch_add('move', x['from']) 1141 ret.branch_add('move', x['path']) 1142 elif x['op'] in (RFC6902_REMOVE,): 1143 ret.branch_add('remove', x['path']) 1144 elif x['op'] in (RFC6902_REPLACE,): 1145 ret.branch_add('replace', x['path']) 1146 elif x['op'] in (RFC6902_TEST,): 1147 ret.branch_add('test', x['path']) 1148 1149 return ret
1150
1151 - def patch_export(self, patchfile, schema=None, **kargs):
1152 """Exports the current task list. 1153 1154 Args: 1155 **patchfile**: 1156 JSON patch for export. 1157 1158 **schema**: 1159 JSON-Schema for validation of the patch list. 1160 1161 kargs: 1162 **validator**: [default, draft3, off, ] 1163 Sets schema validator for the data file. 1164 The values are: :: 1165 1166 default = validate 1167 draft3 = Draft3Validator 1168 off = None 1169 1170 default:= validate 1171 1172 **pretty**: 1173 If True exports as tree format, 1174 else all as one line. 1175 1176 Returns: 1177 When successful returns 'True', else raises an exception. 1178 1179 Raises: 1180 JSONDataPatchError: 1181 1182 """ 1183 pretty = kargs.get('pretty', False) 1184 1185 if not pretty: 1186 try: 1187 with open(patchfile, 'w') as fp: 1188 fp.writelines(self.repr_export()) 1189 except Exception as e: 1190 raise JSONDataPatchError("open-" + str(e), "data.dump", 1191 str(patchfile)) 1192 1193 else: 1194 try: 1195 with open(patchfile, 'w') as fp: 1196 fp.writelines(str(self.str_export())) 1197 except Exception as e: 1198 raise JSONDataPatchError("open-" + str(e), "data.dump", 1199 str(patchfile)) 1200 1201 return True
1202
1203 - def patch_import(self, patchfile, schemafile=None, **kargs):
1204 """Imports a task list. 1205 1206 Args: 1207 **patchfile**: 1208 JSON patch filename containing the list of patch operations. 1209 1210 **schemafile**: 1211 JSON-Schema filename for validation of the patch list. 1212 1213 kargs: 1214 **replace**: 1215 Replace masked characters in *target* specification. :: 1216 1217 replace := ( 1218 True # replaces rfc6901 escape sequences: ~0 and ~1 1219 | False # omit unescaping 1220 ) 1221 1222 .. note:: 1223 1224 Match operations are proceeded literally, thus the 1225 escaped characters should be consistent, 1226 see rfc6901, Section 3. 1227 1228 default := False 1229 1230 **validator**: 1231 Sets schema validator for the data file. 1232 Curren release relies on *jsonschema*, which 1233 supports at the time of writing draft-03 and 1234 draft-04. 1235 1236 The values are: :: 1237 1238 validator := ( 1239 MS_DRAFT3 | 'draft3' 1240 | MS_DRAFT4 | 'draft4' 1241 | MS_ON | 'on' 1242 | MS_OFF | 'off' 1243 | MODE_SCHEMA_DEFAULT | 'default' 1244 ) 1245 1246 default:= MS_OFF 1247 1248 Returns: 1249 When successful returns 'True', else raises an exception. 1250 1251 Raises: 1252 JSONDataPatchError: 1253 1254 """ 1255 replace = kargs.get('replace', self.replace) 1256 validator = kargs.get('validator', MS_OFF) 1257 1258 patchdata = JSONDataSerializer( 1259 [], 1260 datafile=patchfile, 1261 schemafile=schemafile, 1262 validator=MS_OFF, 1263 ) 1264 1265 for pi in patchdata.data: 1266 self += JSONPatchItemRaw(pi, replace=replace) 1267 return True
1268
1269 - def repr_export(self):
1270 """Prints the export representation format of a JSON patch list. 1271 """ 1272 ret = "[" 1273 if self.patch: 1274 if len(self.patch) > 1: 1275 for p in self.patch[:-1]: 1276 ret += p.repr_export() + ", " 1277 ret += self.patch[-1].repr_export() 1278 ret += "]" 1279 return ret
1280
1281 - def str_export(self):
1282 """Pretty prints the export representation format of a JSON patch list. 1283 """ 1284 ret = "[\n" 1285 if self.patch: 1286 if len(self.patch) > 1: 1287 for p in self.patch[:-1]: 1288 ret += " " + p.str_export() + ",\n" 1289 ret += " " + self.patch[-1].str_export() + "\n" 1290 ret += "]" 1291 return str(ret)
1292