URI: 
       tjson_db.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       tjson_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