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