json_db.py (6390B)
1 #!/usr/bin/env python 2 # 3 # Electrum - lightweight Bitcoin client 4 # Copyright (C) 2019 The Electrum Developers 5 # 6 # Permission is hereby granted, free of charge, to any person 7 # obtaining a copy of this software and associated documentation files 8 # (the "Software"), to deal in the Software without restriction, 9 # including without limitation the rights to use, copy, modify, merge, 10 # publish, distribute, sublicense, and/or sell copies of the Software, 11 # and to permit persons to whom the Software is furnished to do so, 12 # subject to the following conditions: 13 # 14 # The above copyright notice and this permission notice shall be 15 # included in all copies or substantial portions of the Software. 16 # 17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 # SOFTWARE. 25 import threading 26 import copy 27 import json 28 29 from . import util 30 from .logging import Logger 31 32 JsonDBJsonEncoder = util.MyEncoder 33 34 def modifier(func): 35 def wrapper(self, *args, **kwargs): 36 with self.lock: 37 self._modified = True 38 return func(self, *args, **kwargs) 39 return wrapper 40 41 def locked(func): 42 def wrapper(self, *args, **kwargs): 43 with self.lock: 44 return func(self, *args, **kwargs) 45 return wrapper 46 47 48 class StoredObject: 49 50 db = None 51 52 def __setattr__(self, key, value): 53 if self.db: 54 self.db.set_modified(True) 55 object.__setattr__(self, key, value) 56 57 def set_db(self, db): 58 self.db = db 59 60 def to_json(self): 61 d = dict(vars(self)) 62 d.pop('db', None) 63 # don't expose/store private stuff 64 d = {k: v for k, v in d.items() 65 if not k.startswith('_')} 66 return d 67 68 69 _RaiseKeyError = object() # singleton for no-default behavior 70 71 class StoredDict(dict): 72 73 def __init__(self, data, db, path): 74 self.db = db 75 self.lock = self.db.lock if self.db else threading.RLock() 76 self.path = path 77 # recursively convert dicts to StoredDict 78 for k, v in list(data.items()): 79 self.__setitem__(k, v) 80 81 def convert_key(self, key): 82 """Convert int keys to str keys, as only those are allowed in json.""" 83 # NOTE: this is evil. really hard to keep in mind and reason about. :( 84 # e.g.: imagine setting int keys everywhere, and then iterating over the dict: 85 # suddenly the keys are str... 86 return str(int(key)) if isinstance(key, int) else key 87 88 @locked 89 def __setitem__(self, key, v): 90 key = self.convert_key(key) 91 is_new = key not in self 92 # early return to prevent unnecessary disk writes 93 if not is_new and self[key] == v: 94 return 95 # recursively set db and path 96 if isinstance(v, StoredDict): 97 v.db = self.db 98 v.path = self.path + [key] 99 for k, vv in v.items(): 100 v[k] = vv 101 # recursively convert dict to StoredDict. 102 # _convert_dict is called breadth-first 103 elif isinstance(v, dict): 104 if self.db: 105 v = self.db._convert_dict(self.path, key, v) 106 if not self.db or self.db._should_convert_to_stored_dict(key): 107 v = StoredDict(v, self.db, self.path + [key]) 108 # convert_value is called depth-first 109 if isinstance(v, dict) or isinstance(v, str): 110 if self.db: 111 v = self.db._convert_value(self.path, key, v) 112 # set parent of StoredObject 113 if isinstance(v, StoredObject): 114 v.set_db(self.db) 115 # set item 116 dict.__setitem__(self, key, v) 117 if self.db: 118 self.db.set_modified(True) 119 120 @locked 121 def __delitem__(self, key): 122 key = self.convert_key(key) 123 dict.__delitem__(self, key) 124 if self.db: 125 self.db.set_modified(True) 126 127 @locked 128 def __getitem__(self, key): 129 key = self.convert_key(key) 130 return dict.__getitem__(self, key) 131 132 @locked 133 def __contains__(self, key): 134 key = self.convert_key(key) 135 return dict.__contains__(self, key) 136 137 @locked 138 def pop(self, key, v=_RaiseKeyError): 139 key = self.convert_key(key) 140 if v is _RaiseKeyError: 141 r = dict.pop(self, key) 142 else: 143 r = dict.pop(self, key, v) 144 if self.db: 145 self.db.set_modified(True) 146 return r 147 148 @locked 149 def get(self, key, default=None): 150 key = self.convert_key(key) 151 return dict.get(self, key, default) 152 153 154 155 156 class JsonDB(Logger): 157 158 def __init__(self, data): 159 Logger.__init__(self) 160 self.lock = threading.RLock() 161 self.data = data 162 self._modified = False 163 164 def set_modified(self, b): 165 with self.lock: 166 self._modified = b 167 168 def modified(self): 169 return self._modified 170 171 @locked 172 def get(self, key, default=None): 173 v = self.data.get(key) 174 if v is None: 175 v = default 176 return v 177 178 @modifier 179 def put(self, key, value): 180 try: 181 json.dumps(key, cls=JsonDBJsonEncoder) 182 json.dumps(value, cls=JsonDBJsonEncoder) 183 except: 184 self.logger.info(f"json error: cannot save {repr(key)} ({repr(value)})") 185 return False 186 if value is not None: 187 if self.data.get(key) != value: 188 self.data[key] = copy.deepcopy(value) 189 return True 190 elif key in self.data: 191 self.data.pop(key) 192 return True 193 return False 194 195 @locked 196 def dump(self, *, human_readable: bool = True) -> str: 197 """Serializes the DB as a string. 198 'human_readable': makes the json indented and sorted, but this is ~2x slower 199 """ 200 return json.dumps( 201 self.data, 202 indent=4 if human_readable else None, 203 sort_keys=bool(human_readable), 204 cls=JsonDBJsonEncoder, 205 ) 206 207 def _should_convert_to_stored_dict(self, key) -> bool: 208 return True