electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

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