NTV.json_ntv.ntv_patch

Created on Sept 10 2023

@author: Philippe@loco-labs.io

The ntv_patch module is part of the NTV.json_ntv package (specification document).

It contains the classes NtvOp, NtvPatch.

1 - NTV Patch

NTV Patch is a transposition of JSON Patch defined in RFC6902.

NTV Patch is a format for expressing a sequence of operations to be applied to a target NTV entity.

This format is also potentially useful in cases where it is necessary to make partial updates on an NTV entity.

The representation of an NTV Patch is a JSON-Array that can be added to an NTV entity (e.g. comments and change management).

2 - Example

    [
     {'op': 'add',    'path': '/0/liste/0', 'entity': {'new value': 51}}
     {'op': 'test',   'path': '/0/1/-',     'entity': {'new value': 51}}
     {'op': 'remove', 'path': '/0/1/-'}
     ]
  1# -*- coding: utf-8 -*-
  2"""
  3Created on Sept 10 2023
  4
  5@author: Philippe@loco-labs.io
  6
  7The `ntv_patch` module is part of the `NTV.json_ntv` package ([specification document](
  8https://loco-philippe.github.io/ES/JSON%20semantic%20format%20(JSON-NTV).htm)).
  9
 10It contains the classes `NtvOp`, `NtvPatch`.
 11
 12# 1 - NTV Patch
 13
 14NTV Patch is a transposition of JSON Patch defined in RFC6902.
 15
 16NTV Patch is a format for expressing a sequence of operations to be applied to a 
 17target NTV entity.
 18
 19This format is also potentially useful in cases where it is necessary to 
 20make partial updates on an NTV entity.
 21
 22The representation of an NTV Patch is a JSON-Array that can be added to an NTV 
 23entity (e.g. comments and change management).
 24
 25# 2 - Example
 26
 27```
 28    [
 29     {'op': 'add',    'path': '/0/liste/0', 'entity': {'new value': 51}}
 30     {'op': 'test',   'path': '/0/1/-',     'entity': {'new value': 51}}
 31     {'op': 'remove', 'path': '/0/1/-'}
 32     ]
 33```
 34"""
 35import json
 36from copy import copy
 37
 38OPERATIONS = ['add', 'test', 'move', 'remove', 'copy', 'replace']
 39
 40class NtvOp:
 41    ''' The NtvOp class defines operations to apply to an NTV entity'''
 42    
 43    def __init__(self, op, path=None, entity=None, comment=None, from_path=None):
 44        op = op.json if isinstance(op, NtvOp) else op
 45        dic = isinstance(op, dict)
 46        self.op        = op.get('op')         if dic else op
 47        self.entity    = op.get('entity')     if dic else entity
 48        self.comment   = op.get('comment')    if dic else comment
 49        self.from_path = NtvPointer(op.get('from')) if dic else NtvPointer(from_path)
 50        self.path      = NtvPointer(op.get('path')) if dic else NtvPointer(path)
 51        if not self.path or not self.op in OPERATIONS:
 52            raise NtvOpError('path or op is not correct')
 53        
 54    def __repr__(self):
 55        '''return the op and the path'''
 56        return 'op : ' + (self.op + ',').ljust(8, ' ') + ' path : ' + str(self.path)
 57
 58    def __str__(self):
 59        '''return json format'''
 60        return json.dumps(self.json)
 61    
 62    def __eq__(self, other):
 63        ''' equal if op, path, entity, comment and from_path are equal'''
 64        return self.__class__.__name__ == other.__class__.__name__ and\
 65            self.op == other.op and self.path == other.path and\
 66            self.entity == other.entity and self.comment == other.comment and\
 67            self.from_path == other.from_path
 68
 69    @property
 70    def json(self):
 71        '''return the json-value representation (dict)'''
 72        dic = {'op': self.op, 'path': str(self.path), 'entity': self.entity, 
 73               'comment':self.comment, 'from': str(self.from_path)}
 74        return {key: val for key, val in dic.items() if val}
 75
 76    def exe(self, ntv):
 77        '''execute the operation with ntv entity and return the resulting entity'''
 78        from json_ntv.ntv import Ntv
 79        ntv_res = copy(ntv)
 80        idx = self.path[-1]
 81        p_path = str(NtvPointer(self.path[:-1]))
 82        path = str(self.path)
 83        if self.op in ['move', 'copy', 'add']:
 84            if self.op == 'add' and self.entity:
 85                ntv = Ntv.obj(self.entity)
 86            elif self.op == 'copy' and self.from_path:
 87                ntv = copy(ntv_res[str(self.from_path)])                
 88            elif self.op == 'move' and self.from_path:
 89                ntv = ntv_res[str(self.from_path)]
 90                del ntv_res[str(NtvPointer(self.from_path[:-1]))][self.from_path[-1]]
 91                ntv.parent = None
 92            else:
 93                raise NtvOpError('op is not correct')
 94            if idx == '-':
 95                ntv_res[p_path].append(ntv)
 96            else:
 97                ntv_res[p_path].insert(idx, ntv)                            
 98        elif self.op == 'test' and self.entity:
 99            ntv = Ntv.obj(self.entity)
100            if not (idx == '-' and ntv in ntv_res[p_path]) and not (
101                    isinstance(idx, int) and ntv == ntv_res[path]):
102                raise NtvOpError('test is not correct')                
103        elif self.op == 'remove':
104            idx = self.path[-1]
105            idx = len(ntv[p_path]) - 1 if idx == '-' else idx
106            ntv_res[p_path+'/'+str(idx)].remove(index=idx)       
107        elif self.op == 'replace' and self.entity:
108            ntv_res[path].replace(Ntv.obj(self.entity))
109        else:
110            raise NtvOpError('op add no result')
111        return ntv_res
112
113class NtvPatch:
114    ''' The NtvPatch class defines a sequence of operations to apply to an 
115    NTV entity'''
116
117    def __init__(self, list_op=None):
118        list_op = [] if not list_op else list_op 
119        self.list_op = [NtvOp(ope) for ope in list_op]
120        
121    def __eq__(self, other):
122        ''' equal if list_op are equal'''
123        return self.__class__.__name__ == other.__class__.__name__ and\
124            self.list_op == other.list_op
125
126    def __copy__(self):
127        ''' Copy all the data '''
128        cop = self.__class__(self)
129        return cop
130
131    def __setitem__(self, ind, ope):
132        ''' replace op item at the `ind` row with `op`'''
133        if ind < 0 or ind >= len(self):
134            raise NtvOpError("out of bounds")
135        self.list_op[ind] = ope
136
137    def __delitem__(self, ind):
138        '''remove ntv_value item at the `ind` row'''
139        if isinstance(ind, int):
140            self.list_op.pop(ind)
141        else:            
142            self.list_op.pop(self.list_op.index(self[ind]))
143
144    def __len__(self):
145        ''' len of list_op'''
146        return len(self.list_op)
147
148    def __str__(self):
149        '''return list of op json format'''
150        return json.dumps([ope.json for ope in self.list_op])
151
152    def __repr__(self):
153        '''return classname and code'''
154        rep = 'NtvPatch :\n'
155        for ind, op in enumerate(self):
156            rep += '    op' + str(ind).ljust(3, ' ') + ' : ' + repr(op)[5:] + '\n'
157        return rep
158
159    def __contains__(self, item):
160        ''' item of NtvPatch'''
161        return item in self.list_op
162
163    def __iter__(self):
164        ''' iterator for op'''
165        return iter(self.list_op)
166
167    def __getitem__(self, selec):
168        ''' return ntv_value item '''
169        if selec is None or selec == [] or selec == () or selec == '':
170            return self
171        if isinstance(selec, (list, tuple)) and len(selec) == 1:
172            selec = selec[0]
173        if isinstance(selec, (list, tuple)):
174            return [self[i] for i in selec]
175        return self.list_op[selec]            
176
177    def append(self, ope):
178        '''append ope in the NtvPatch'''
179        self.list_op.append(ope)
180
181    def exe(self, ntv):
182        '''execute the included operations with ntv entity and return 
183        the resulting entity'''
184        ntv_res = ntv
185        for ope in self:
186            ntv_res = ope.exe(ntv_res)
187        return ntv_res
188
189class NtvPointer(list):
190    
191    def __init__(self, pointer):
192        if isinstance(pointer, (list, NtvPointer)):
193            super().__init__(pointer)
194        elif isinstance(pointer, (int, str)):
195            super().__init__(NtvPointer.pointer_list(pointer))
196
197    def __str__(self):
198        return self.json()
199
200    def json(self, default=''):
201        '''convert a pointer into a json_pointer 
202        
203        *Parameters*
204
205        - **default**: Str (default '') - default value if pointer is empty
206        ''' 
207        return NtvPointer.pointer_json(self)
208
209    def append(self, child):
210        '''append a child pointer into a pointer '''
211        self += NtvPointer(child)
212        
213    @staticmethod 
214    def split(path):
215        '''return the last pointer of the path and the path without the last pointer'''
216        pointer = NtvPointer(path)
217        if pointer == []:
218            return (None, None)
219        return (NtvPointer(pointer[-1]), NtvPointer(pointer[:-1]))
220
221    @staticmethod 
222    def pointer_json(list_pointer, default=''):
223        '''convert a list of pointer string into a json_pointer 
224        
225        *Parameters*
226
227        - **default**: Str (default '') - default value if pointer is empty
228        ''' 
229        json_p = ''
230        if list_pointer == []:
231            return default
232        for name in list_pointer:
233            json_p += '/' + str(name).replace('~', '~0').replace('/', '~1')
234        return json_p
235
236    @staticmethod 
237    def pointer_list(json_pointer):
238        '''convert a json_pointer string into a pointer list''' 
239        json_pointer = str(json_pointer)
240        split_pointer = json_pointer.split('/')
241        if len(split_pointer) == 0:
242            return []
243        if split_pointer[0] != '' and len(split_pointer) > 1:
244            raise NtvOpError("json_pointer is not correct")
245        if split_pointer[0] != '':
246            split_pointer.insert(0, '')
247        return [int(nam) if nam.isdigit() else nam.replace('~1', '/').replace('~0', '/') 
248                for nam in split_pointer[1:] ]       
249                     
250class NtvOpError(Exception):
251    ''' NtvOp Exception'''
252    # pass
class NtvOp:
 41class NtvOp:
 42    ''' The NtvOp class defines operations to apply to an NTV entity'''
 43    
 44    def __init__(self, op, path=None, entity=None, comment=None, from_path=None):
 45        op = op.json if isinstance(op, NtvOp) else op
 46        dic = isinstance(op, dict)
 47        self.op        = op.get('op')         if dic else op
 48        self.entity    = op.get('entity')     if dic else entity
 49        self.comment   = op.get('comment')    if dic else comment
 50        self.from_path = NtvPointer(op.get('from')) if dic else NtvPointer(from_path)
 51        self.path      = NtvPointer(op.get('path')) if dic else NtvPointer(path)
 52        if not self.path or not self.op in OPERATIONS:
 53            raise NtvOpError('path or op is not correct')
 54        
 55    def __repr__(self):
 56        '''return the op and the path'''
 57        return 'op : ' + (self.op + ',').ljust(8, ' ') + ' path : ' + str(self.path)
 58
 59    def __str__(self):
 60        '''return json format'''
 61        return json.dumps(self.json)
 62    
 63    def __eq__(self, other):
 64        ''' equal if op, path, entity, comment and from_path are equal'''
 65        return self.__class__.__name__ == other.__class__.__name__ and\
 66            self.op == other.op and self.path == other.path and\
 67            self.entity == other.entity and self.comment == other.comment and\
 68            self.from_path == other.from_path
 69
 70    @property
 71    def json(self):
 72        '''return the json-value representation (dict)'''
 73        dic = {'op': self.op, 'path': str(self.path), 'entity': self.entity, 
 74               'comment':self.comment, 'from': str(self.from_path)}
 75        return {key: val for key, val in dic.items() if val}
 76
 77    def exe(self, ntv):
 78        '''execute the operation with ntv entity and return the resulting entity'''
 79        from json_ntv.ntv import Ntv
 80        ntv_res = copy(ntv)
 81        idx = self.path[-1]
 82        p_path = str(NtvPointer(self.path[:-1]))
 83        path = str(self.path)
 84        if self.op in ['move', 'copy', 'add']:
 85            if self.op == 'add' and self.entity:
 86                ntv = Ntv.obj(self.entity)
 87            elif self.op == 'copy' and self.from_path:
 88                ntv = copy(ntv_res[str(self.from_path)])                
 89            elif self.op == 'move' and self.from_path:
 90                ntv = ntv_res[str(self.from_path)]
 91                del ntv_res[str(NtvPointer(self.from_path[:-1]))][self.from_path[-1]]
 92                ntv.parent = None
 93            else:
 94                raise NtvOpError('op is not correct')
 95            if idx == '-':
 96                ntv_res[p_path].append(ntv)
 97            else:
 98                ntv_res[p_path].insert(idx, ntv)                            
 99        elif self.op == 'test' and self.entity:
100            ntv = Ntv.obj(self.entity)
101            if not (idx == '-' and ntv in ntv_res[p_path]) and not (
102                    isinstance(idx, int) and ntv == ntv_res[path]):
103                raise NtvOpError('test is not correct')                
104        elif self.op == 'remove':
105            idx = self.path[-1]
106            idx = len(ntv[p_path]) - 1 if idx == '-' else idx
107            ntv_res[p_path+'/'+str(idx)].remove(index=idx)       
108        elif self.op == 'replace' and self.entity:
109            ntv_res[path].replace(Ntv.obj(self.entity))
110        else:
111            raise NtvOpError('op add no result')
112        return ntv_res

The NtvOp class defines operations to apply to an NTV entity

NtvOp(op, path=None, entity=None, comment=None, from_path=None)
44    def __init__(self, op, path=None, entity=None, comment=None, from_path=None):
45        op = op.json if isinstance(op, NtvOp) else op
46        dic = isinstance(op, dict)
47        self.op        = op.get('op')         if dic else op
48        self.entity    = op.get('entity')     if dic else entity
49        self.comment   = op.get('comment')    if dic else comment
50        self.from_path = NtvPointer(op.get('from')) if dic else NtvPointer(from_path)
51        self.path      = NtvPointer(op.get('path')) if dic else NtvPointer(path)
52        if not self.path or not self.op in OPERATIONS:
53            raise NtvOpError('path or op is not correct')
json

return the json-value representation (dict)

def exe(self, ntv):
 77    def exe(self, ntv):
 78        '''execute the operation with ntv entity and return the resulting entity'''
 79        from json_ntv.ntv import Ntv
 80        ntv_res = copy(ntv)
 81        idx = self.path[-1]
 82        p_path = str(NtvPointer(self.path[:-1]))
 83        path = str(self.path)
 84        if self.op in ['move', 'copy', 'add']:
 85            if self.op == 'add' and self.entity:
 86                ntv = Ntv.obj(self.entity)
 87            elif self.op == 'copy' and self.from_path:
 88                ntv = copy(ntv_res[str(self.from_path)])                
 89            elif self.op == 'move' and self.from_path:
 90                ntv = ntv_res[str(self.from_path)]
 91                del ntv_res[str(NtvPointer(self.from_path[:-1]))][self.from_path[-1]]
 92                ntv.parent = None
 93            else:
 94                raise NtvOpError('op is not correct')
 95            if idx == '-':
 96                ntv_res[p_path].append(ntv)
 97            else:
 98                ntv_res[p_path].insert(idx, ntv)                            
 99        elif self.op == 'test' and self.entity:
100            ntv = Ntv.obj(self.entity)
101            if not (idx == '-' and ntv in ntv_res[p_path]) and not (
102                    isinstance(idx, int) and ntv == ntv_res[path]):
103                raise NtvOpError('test is not correct')                
104        elif self.op == 'remove':
105            idx = self.path[-1]
106            idx = len(ntv[p_path]) - 1 if idx == '-' else idx
107            ntv_res[p_path+'/'+str(idx)].remove(index=idx)       
108        elif self.op == 'replace' and self.entity:
109            ntv_res[path].replace(Ntv.obj(self.entity))
110        else:
111            raise NtvOpError('op add no result')
112        return ntv_res

execute the operation with ntv entity and return the resulting entity

class NtvPatch:
114class NtvPatch:
115    ''' The NtvPatch class defines a sequence of operations to apply to an 
116    NTV entity'''
117
118    def __init__(self, list_op=None):
119        list_op = [] if not list_op else list_op 
120        self.list_op = [NtvOp(ope) for ope in list_op]
121        
122    def __eq__(self, other):
123        ''' equal if list_op are equal'''
124        return self.__class__.__name__ == other.__class__.__name__ and\
125            self.list_op == other.list_op
126
127    def __copy__(self):
128        ''' Copy all the data '''
129        cop = self.__class__(self)
130        return cop
131
132    def __setitem__(self, ind, ope):
133        ''' replace op item at the `ind` row with `op`'''
134        if ind < 0 or ind >= len(self):
135            raise NtvOpError("out of bounds")
136        self.list_op[ind] = ope
137
138    def __delitem__(self, ind):
139        '''remove ntv_value item at the `ind` row'''
140        if isinstance(ind, int):
141            self.list_op.pop(ind)
142        else:            
143            self.list_op.pop(self.list_op.index(self[ind]))
144
145    def __len__(self):
146        ''' len of list_op'''
147        return len(self.list_op)
148
149    def __str__(self):
150        '''return list of op json format'''
151        return json.dumps([ope.json for ope in self.list_op])
152
153    def __repr__(self):
154        '''return classname and code'''
155        rep = 'NtvPatch :\n'
156        for ind, op in enumerate(self):
157            rep += '    op' + str(ind).ljust(3, ' ') + ' : ' + repr(op)[5:] + '\n'
158        return rep
159
160    def __contains__(self, item):
161        ''' item of NtvPatch'''
162        return item in self.list_op
163
164    def __iter__(self):
165        ''' iterator for op'''
166        return iter(self.list_op)
167
168    def __getitem__(self, selec):
169        ''' return ntv_value item '''
170        if selec is None or selec == [] or selec == () or selec == '':
171            return self
172        if isinstance(selec, (list, tuple)) and len(selec) == 1:
173            selec = selec[0]
174        if isinstance(selec, (list, tuple)):
175            return [self[i] for i in selec]
176        return self.list_op[selec]            
177
178    def append(self, ope):
179        '''append ope in the NtvPatch'''
180        self.list_op.append(ope)
181
182    def exe(self, ntv):
183        '''execute the included operations with ntv entity and return 
184        the resulting entity'''
185        ntv_res = ntv
186        for ope in self:
187            ntv_res = ope.exe(ntv_res)
188        return ntv_res

The NtvPatch class defines a sequence of operations to apply to an NTV entity

NtvPatch(list_op=None)
118    def __init__(self, list_op=None):
119        list_op = [] if not list_op else list_op 
120        self.list_op = [NtvOp(ope) for ope in list_op]
def append(self, ope):
178    def append(self, ope):
179        '''append ope in the NtvPatch'''
180        self.list_op.append(ope)

append ope in the NtvPatch

def exe(self, ntv):
182    def exe(self, ntv):
183        '''execute the included operations with ntv entity and return 
184        the resulting entity'''
185        ntv_res = ntv
186        for ope in self:
187            ntv_res = ope.exe(ntv_res)
188        return ntv_res

execute the included operations with ntv entity and return the resulting entity

class NtvPointer(builtins.list):
190class NtvPointer(list):
191    
192    def __init__(self, pointer):
193        if isinstance(pointer, (list, NtvPointer)):
194            super().__init__(pointer)
195        elif isinstance(pointer, (int, str)):
196            super().__init__(NtvPointer.pointer_list(pointer))
197
198    def __str__(self):
199        return self.json()
200
201    def json(self, default=''):
202        '''convert a pointer into a json_pointer 
203        
204        *Parameters*
205
206        - **default**: Str (default '') - default value if pointer is empty
207        ''' 
208        return NtvPointer.pointer_json(self)
209
210    def append(self, child):
211        '''append a child pointer into a pointer '''
212        self += NtvPointer(child)
213        
214    @staticmethod 
215    def split(path):
216        '''return the last pointer of the path and the path without the last pointer'''
217        pointer = NtvPointer(path)
218        if pointer == []:
219            return (None, None)
220        return (NtvPointer(pointer[-1]), NtvPointer(pointer[:-1]))
221
222    @staticmethod 
223    def pointer_json(list_pointer, default=''):
224        '''convert a list of pointer string into a json_pointer 
225        
226        *Parameters*
227
228        - **default**: Str (default '') - default value if pointer is empty
229        ''' 
230        json_p = ''
231        if list_pointer == []:
232            return default
233        for name in list_pointer:
234            json_p += '/' + str(name).replace('~', '~0').replace('/', '~1')
235        return json_p
236
237    @staticmethod 
238    def pointer_list(json_pointer):
239        '''convert a json_pointer string into a pointer list''' 
240        json_pointer = str(json_pointer)
241        split_pointer = json_pointer.split('/')
242        if len(split_pointer) == 0:
243            return []
244        if split_pointer[0] != '' and len(split_pointer) > 1:
245            raise NtvOpError("json_pointer is not correct")
246        if split_pointer[0] != '':
247            split_pointer.insert(0, '')
248        return [int(nam) if nam.isdigit() else nam.replace('~1', '/').replace('~0', '/') 
249                for nam in split_pointer[1:] ]       

Built-in mutable sequence.

If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.

NtvPointer(pointer)
192    def __init__(self, pointer):
193        if isinstance(pointer, (list, NtvPointer)):
194            super().__init__(pointer)
195        elif isinstance(pointer, (int, str)):
196            super().__init__(NtvPointer.pointer_list(pointer))
def json(self, default=''):
201    def json(self, default=''):
202        '''convert a pointer into a json_pointer 
203        
204        *Parameters*
205
206        - **default**: Str (default '') - default value if pointer is empty
207        ''' 
208        return NtvPointer.pointer_json(self)

convert a pointer into a json_pointer

Parameters

  • default: Str (default '') - default value if pointer is empty
def append(self, child):
210    def append(self, child):
211        '''append a child pointer into a pointer '''
212        self += NtvPointer(child)

append a child pointer into a pointer

@staticmethod
def split(path):
214    @staticmethod 
215    def split(path):
216        '''return the last pointer of the path and the path without the last pointer'''
217        pointer = NtvPointer(path)
218        if pointer == []:
219            return (None, None)
220        return (NtvPointer(pointer[-1]), NtvPointer(pointer[:-1]))

return the last pointer of the path and the path without the last pointer

@staticmethod
def pointer_json(list_pointer, default=''):
222    @staticmethod 
223    def pointer_json(list_pointer, default=''):
224        '''convert a list of pointer string into a json_pointer 
225        
226        *Parameters*
227
228        - **default**: Str (default '') - default value if pointer is empty
229        ''' 
230        json_p = ''
231        if list_pointer == []:
232            return default
233        for name in list_pointer:
234            json_p += '/' + str(name).replace('~', '~0').replace('/', '~1')
235        return json_p

convert a list of pointer string into a json_pointer

Parameters

  • default: Str (default '') - default value if pointer is empty
@staticmethod
def pointer_list(json_pointer):
237    @staticmethod 
238    def pointer_list(json_pointer):
239        '''convert a json_pointer string into a pointer list''' 
240        json_pointer = str(json_pointer)
241        split_pointer = json_pointer.split('/')
242        if len(split_pointer) == 0:
243            return []
244        if split_pointer[0] != '' and len(split_pointer) > 1:
245            raise NtvOpError("json_pointer is not correct")
246        if split_pointer[0] != '':
247            split_pointer.insert(0, '')
248        return [int(nam) if nam.isdigit() else nam.replace('~1', '/').replace('~0', '/') 
249                for nam in split_pointer[1:] ]       

convert a json_pointer string into a pointer list

Inherited Members
builtins.list
clear
copy
insert
extend
pop
remove
index
count
reverse
sort
class NtvOpError(builtins.Exception):
251class NtvOpError(Exception):
252    ''' NtvOp Exception'''
253    # pass

NtvOp Exception

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback