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  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',): # pragma: no cover 
 47      raise Exception("Requires Python-2.6.* or higher") 
 48  # if version < '2.7': # pragma: no cover 
 49  #     raise Exception("Requires Python-2.7.* or higher") 
 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  # for now the only one supported 
 59  from types import NoneType 
 60  from jsondata.JSONPointer import JSONPointer 
 61  from jsondata.JSONDataSerializer import JSONDataSerializer,MODE_SCHEMA_OFF 
 62   
 63  # default 
 64  _appname = "jsonpatch" 
 65  # Sets display for inetractive JSON/JSONschema design. 
 66  _interactive = False 
 67   
 68  # 
 69  # Operations in accordance to RFC6902  
 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  # Mapping for reverse transformation 
 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  # Mapping for reverse transformation 
 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  } 
98 -def getOp(x):
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
108 -class JSONPatchException(Exception):
109 pass
110
111 -class JSONPatchItemException(JSONPatchException):
112 pass
113
114 -class JSONPatchItem(object):
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
169 - def __call__(self, j):
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
187 - def __eq__(self,x):
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
226 - def __getitem__(self,key):
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
247 - def __ne__(self, x):
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
261 - def __repr__(self):
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
281 - def __str__(self):
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 #n,b = self.target.get_node_and_child(jsondata) 314 315 nbranch = jsondata.branch_add( 316 self.target, # target pointer 317 None, 318 self.value) # 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
359 - def repr_export(self):
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
379 -class JSONPatchItemRaw(JSONPatchItem):
380 """Adds native patch strings or an unsorted dict for RFC6902. 381 """
382 - def __init__(self,patchstring):
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 #print "<"+str(sx)+">" 389 #print "<"+str(patchstring)+">" 390 #l0 = len(sx.replace(" ","")) 391 #l1 = len(patchstring.replace(" ","")) 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
416 -class JSONPatchFilter(object):
417 """Filtering capabilities on the entries of patch lists. 418 """
419 - def __init__(self,**kargs):
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
457 - def __eq__(self,x):
458 459 pass
460
461 - def __ne__(self,x):
462 463 pass
464
465 -class JSONPatch(object):
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 """
481 - def __init__(self):
482 self.patch = [] 483 """List of patch tasks. """ 484 485 self.deep = False 486 """Defines copy operations, True:=deep, False:=swallow"""
487 488 # 489 #--- RFC6902 JSON patch files 490 # 491
492 - def __add__(self,x=None):
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
504 - def __call__(self, j, x=None):
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
528 - def __eq__(self,x):
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
551 - def __getitem__(self,key):
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
565 - def __iadd__(self,x=None):
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
578 - def __isub__(self,x):
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
603 - def __iter__(self):
604 """Provides an iterator foreseen for large amounts of in-memory patches. 605 """ 606 return iter(self.patch)
607
608 - def __len__(self):
609 """The number of outstanding patches. 610 """ 611 return len(self.patch)
612
613 - def __ne__(self, x):
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
627 - def __repr__(self):
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
639 - def __str__(self):
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
651 - def __sub__(self,x):
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)) # should not be called frequently 699 return len(self.patch),status
700
701 - def get(self,x=None):
702 """ 703 """ 704 ret = self.patch 705 706 #FIXME: 707 return ret
708
709 - def patch_export(self, patchfile, schema=None, **kargs):
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
789 - def repr_export(self):
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