json-database 0.8.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- json_database/__init__.py +387 -0
- json_database/crypto.py +80 -0
- json_database/exceptions.py +22 -0
- json_database/search.py +169 -0
- json_database/utils.py +340 -0
- json_database/version.py +6 -0
- json_database/xdg_utils.py +174 -0
- json_database-0.8.0a1.dist-info/LICENSE +19 -0
- json_database-0.8.0a1.dist-info/METADATA +205 -0
- json_database-0.8.0a1.dist-info/RECORD +12 -0
- json_database-0.8.0a1.dist-info/WHEEL +5 -0
- json_database-0.8.0a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from os import makedirs, remove
|
|
5
|
+
from os.path import expanduser, isdir, dirname, exists, isfile, join
|
|
6
|
+
from pprint import pprint
|
|
7
|
+
from tempfile import gettempdir
|
|
8
|
+
|
|
9
|
+
from combo_lock import ComboLock
|
|
10
|
+
|
|
11
|
+
from json_database.crypto import decrypt_from_json, encrypt_as_json
|
|
12
|
+
from json_database.exceptions import InvalidItemID, DatabaseNotCommitted, \
|
|
13
|
+
SessionError, MatchError
|
|
14
|
+
from json_database.utils import DummyLock, load_commented_json, merge_dict, \
|
|
15
|
+
jsonify_recursively, get_key_recursively, get_key_recursively_fuzzy, \
|
|
16
|
+
get_value_recursively_fuzzy, get_value_recursively
|
|
17
|
+
from json_database.xdg_utils import xdg_cache_home, xdg_data_home, xdg_config_home
|
|
18
|
+
|
|
19
|
+
LOG = logging.getLogger("JsonDatabase")
|
|
20
|
+
LOG.setLevel("INFO")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class JsonStorage(dict):
|
|
24
|
+
"""
|
|
25
|
+
persistent python dict
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, path, disable_lock=False):
|
|
29
|
+
super().__init__()
|
|
30
|
+
lock_path = join(gettempdir(), path.split("/")[-1] + ".lock")
|
|
31
|
+
if disable_lock:
|
|
32
|
+
self.lock = DummyLock(lock_path)
|
|
33
|
+
else:
|
|
34
|
+
self.lock = ComboLock(lock_path)
|
|
35
|
+
self.path = path
|
|
36
|
+
if self.path:
|
|
37
|
+
self.load_local(self.path)
|
|
38
|
+
|
|
39
|
+
def load_local(self, path):
|
|
40
|
+
"""
|
|
41
|
+
Load local json file into self.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
path (str): file to load
|
|
45
|
+
"""
|
|
46
|
+
with self.lock:
|
|
47
|
+
path = expanduser(path)
|
|
48
|
+
if exists(path) and isfile(path):
|
|
49
|
+
self.clear()
|
|
50
|
+
try:
|
|
51
|
+
config = load_commented_json(path)
|
|
52
|
+
for key in config:
|
|
53
|
+
self[key] = config[key]
|
|
54
|
+
LOG.debug("Json {} loaded".format(path))
|
|
55
|
+
except Exception as e:
|
|
56
|
+
LOG.error("Error loading json '{}'".format(path))
|
|
57
|
+
LOG.error(repr(e))
|
|
58
|
+
else:
|
|
59
|
+
LOG.debug("Json '{}' not defined, skipping".format(path))
|
|
60
|
+
|
|
61
|
+
def clear(self):
|
|
62
|
+
for k in dict(self):
|
|
63
|
+
self.pop(k)
|
|
64
|
+
|
|
65
|
+
def reload(self):
|
|
66
|
+
if exists(self.path) and isfile(self.path):
|
|
67
|
+
self.load_local(self.path)
|
|
68
|
+
else:
|
|
69
|
+
raise DatabaseNotCommitted
|
|
70
|
+
|
|
71
|
+
def store(self, path=None):
|
|
72
|
+
"""
|
|
73
|
+
store the json db locally.
|
|
74
|
+
"""
|
|
75
|
+
with self.lock:
|
|
76
|
+
path = path or self.path
|
|
77
|
+
if not path:
|
|
78
|
+
LOG.warning("json db path not set")
|
|
79
|
+
return
|
|
80
|
+
path = expanduser(path)
|
|
81
|
+
if dirname(path) and not isdir(dirname(path)):
|
|
82
|
+
makedirs(dirname(path))
|
|
83
|
+
with open(path, 'w', encoding="utf-8") as f:
|
|
84
|
+
json.dump(self, f, indent=4, ensure_ascii=False)
|
|
85
|
+
|
|
86
|
+
def remove(self):
|
|
87
|
+
with self.lock:
|
|
88
|
+
if isfile(self.path):
|
|
89
|
+
remove(self.path)
|
|
90
|
+
|
|
91
|
+
def merge(self, conf, merge_lists=True, skip_empty=True, no_dupes=True,
|
|
92
|
+
new_only=False):
|
|
93
|
+
merge_dict(self, conf, merge_lists, skip_empty, no_dupes, new_only)
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def __enter__(self):
|
|
97
|
+
""" Context handler """
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
def __exit__(self, _type, value, traceback):
|
|
101
|
+
""" Commits changes and Closes the session """
|
|
102
|
+
try:
|
|
103
|
+
self.store()
|
|
104
|
+
except Exception as e:
|
|
105
|
+
LOG.error(e)
|
|
106
|
+
raise SessionError
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class EncryptedJsonStorage(JsonStorage):
|
|
110
|
+
"""persistent python dict, stored AES encrypted to file"""
|
|
111
|
+
|
|
112
|
+
def __init__(self, encrypt_key: str, path: str, disable_lock=False):
|
|
113
|
+
assert len(encrypt_key) == 16
|
|
114
|
+
self.encrypt_key = encrypt_key
|
|
115
|
+
super().__init__(path, disable_lock)
|
|
116
|
+
|
|
117
|
+
def load_local(self, path):
|
|
118
|
+
"""
|
|
119
|
+
Load local json file into self.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
path (str): file to load
|
|
123
|
+
"""
|
|
124
|
+
super().load_local(path)
|
|
125
|
+
# decrypt after load
|
|
126
|
+
if self:
|
|
127
|
+
decrypted = json.loads(decrypt_from_json(self.encrypt_key, dict(self)))
|
|
128
|
+
self.clear()
|
|
129
|
+
self.update(decrypted)
|
|
130
|
+
|
|
131
|
+
def store(self, path=None):
|
|
132
|
+
"""
|
|
133
|
+
store the json db locally.
|
|
134
|
+
"""
|
|
135
|
+
decrypted = dict(self)
|
|
136
|
+
encrypted = json.loads(encrypt_as_json(self.encrypt_key, decrypted))
|
|
137
|
+
self.clear()
|
|
138
|
+
self.merge(encrypted) # encrypt before storage
|
|
139
|
+
super().store()
|
|
140
|
+
self.clear()
|
|
141
|
+
self.update(decrypted) # keep it decrypted in memory
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class JsonDatabase(dict):
|
|
145
|
+
""" searchable persistent dict """
|
|
146
|
+
|
|
147
|
+
def __init__(self,
|
|
148
|
+
name,
|
|
149
|
+
path=None,
|
|
150
|
+
disable_lock=False,
|
|
151
|
+
extension="json"):
|
|
152
|
+
super().__init__()
|
|
153
|
+
self.name = name
|
|
154
|
+
self.path = path or f"{name}.{extension}"
|
|
155
|
+
self.db = JsonStorage(self.path, disable_lock=disable_lock)
|
|
156
|
+
self.db[name] = []
|
|
157
|
+
self.db.load_local(self.path)
|
|
158
|
+
|
|
159
|
+
# operator overloads
|
|
160
|
+
def __enter__(self):
|
|
161
|
+
""" Context handler """
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
def __exit__(self, _type, value, traceback):
|
|
165
|
+
""" Commits changes and Closes the session """
|
|
166
|
+
try:
|
|
167
|
+
self.commit()
|
|
168
|
+
except Exception as e:
|
|
169
|
+
LOG.error(e)
|
|
170
|
+
raise SessionError
|
|
171
|
+
|
|
172
|
+
def __repr__(self):
|
|
173
|
+
return str(jsonify_recursively(self))
|
|
174
|
+
|
|
175
|
+
def __len__(self):
|
|
176
|
+
return len(self.db.get(self.name, []))
|
|
177
|
+
|
|
178
|
+
def __getitem__(self, item):
|
|
179
|
+
if not isinstance(item, int):
|
|
180
|
+
try:
|
|
181
|
+
item_id = int(item)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
item_id = self.get_item_id(item)
|
|
184
|
+
if item_id < 0:
|
|
185
|
+
raise InvalidItemID
|
|
186
|
+
else:
|
|
187
|
+
item_id = item
|
|
188
|
+
if item_id >= len(self.db[self.name]):
|
|
189
|
+
raise InvalidItemID
|
|
190
|
+
return self.db[self.name][item_id]
|
|
191
|
+
|
|
192
|
+
def __setitem__(self, item_id, value):
|
|
193
|
+
if not isinstance(item_id, int) or item_id >= len(self) or item_id < 0:
|
|
194
|
+
raise InvalidItemID
|
|
195
|
+
else:
|
|
196
|
+
self.update_item(item_id, value)
|
|
197
|
+
|
|
198
|
+
def __iter__(self):
|
|
199
|
+
for item in self.db[self.name]:
|
|
200
|
+
yield item
|
|
201
|
+
|
|
202
|
+
def __contains__(self, item):
|
|
203
|
+
item = jsonify_recursively(item)
|
|
204
|
+
return item in self.db[self.name]
|
|
205
|
+
|
|
206
|
+
# database
|
|
207
|
+
def commit(self):
|
|
208
|
+
"""
|
|
209
|
+
store the json db locally.
|
|
210
|
+
"""
|
|
211
|
+
self.db.store(self.path)
|
|
212
|
+
|
|
213
|
+
def reset(self):
|
|
214
|
+
self.db.reload()
|
|
215
|
+
|
|
216
|
+
def print(self):
|
|
217
|
+
pprint(jsonify_recursively(self))
|
|
218
|
+
|
|
219
|
+
# item manipulations
|
|
220
|
+
def append(self, value):
|
|
221
|
+
value = jsonify_recursively(value)
|
|
222
|
+
self.db[self.name].append(value)
|
|
223
|
+
return len(self)
|
|
224
|
+
|
|
225
|
+
def add_item(self, value, allow_duplicates=False):
|
|
226
|
+
""" add an item to database
|
|
227
|
+
if allow_duplicates is True, item is added unconditionally,
|
|
228
|
+
else only if no exact match is present
|
|
229
|
+
"""
|
|
230
|
+
if allow_duplicates or value not in self:
|
|
231
|
+
self.append(value)
|
|
232
|
+
return len(self)
|
|
233
|
+
return self.get_item_id(value)
|
|
234
|
+
|
|
235
|
+
def match_item(self, value, match_strategy=None):
|
|
236
|
+
""" match value to some item in database
|
|
237
|
+
returns a list of matched items
|
|
238
|
+
"""
|
|
239
|
+
value = jsonify_recursively(value)
|
|
240
|
+
matches = []
|
|
241
|
+
for idx, item in enumerate(self):
|
|
242
|
+
|
|
243
|
+
# TODO match strategy
|
|
244
|
+
# - require exact match
|
|
245
|
+
# - require list of keys to match
|
|
246
|
+
# - require at least one of key list to match
|
|
247
|
+
# - require at exactly one of key list to match
|
|
248
|
+
|
|
249
|
+
# by default check for exact matches
|
|
250
|
+
if item == value:
|
|
251
|
+
matches.append((item, idx))
|
|
252
|
+
|
|
253
|
+
return matches
|
|
254
|
+
|
|
255
|
+
def merge_item(self, value, item_id=None, match_strategy=None,
|
|
256
|
+
merge_strategy=None):
|
|
257
|
+
""" search an item according to match criteria, merge fields"""
|
|
258
|
+
if item_id is None:
|
|
259
|
+
matches = self.match_item(value, match_strategy)
|
|
260
|
+
if not matches:
|
|
261
|
+
raise MatchError
|
|
262
|
+
match, item_id = matches[0][1]
|
|
263
|
+
else:
|
|
264
|
+
match = self[item_id]
|
|
265
|
+
# TODO merge strategy
|
|
266
|
+
# - only merge some keys
|
|
267
|
+
# - dont merge some keys
|
|
268
|
+
# - merge all keys
|
|
269
|
+
# - dont overwrite keys
|
|
270
|
+
value = jsonify_recursively(value)
|
|
271
|
+
self[item_id] = merge_dict(match, value)
|
|
272
|
+
|
|
273
|
+
def replace_item(self, value, item_id=None, match_strategy=None):
|
|
274
|
+
""" search an item according to match criteria, replace it"""
|
|
275
|
+
if item_id is None:
|
|
276
|
+
matches = self.match_item(value, match_strategy)
|
|
277
|
+
if not matches:
|
|
278
|
+
raise MatchError
|
|
279
|
+
match, item_id = matches[0][1]
|
|
280
|
+
value = jsonify_recursively(value)
|
|
281
|
+
self[item_id] = value
|
|
282
|
+
|
|
283
|
+
# item_id
|
|
284
|
+
def get_item_id(self, item):
|
|
285
|
+
"""
|
|
286
|
+
item_id is simply the index of the item in the database
|
|
287
|
+
WARNING: this is not immutable across sessions
|
|
288
|
+
"""
|
|
289
|
+
for match, idx in self.match_item(item):
|
|
290
|
+
return idx
|
|
291
|
+
return -1
|
|
292
|
+
|
|
293
|
+
def update_item(self, item_id, new_item):
|
|
294
|
+
"""
|
|
295
|
+
item_id is simply the index of the item in the database
|
|
296
|
+
WARNING: this is not immutable across sessions
|
|
297
|
+
"""
|
|
298
|
+
new_item = jsonify_recursively(new_item)
|
|
299
|
+
self.db[self.name][item_id] = new_item
|
|
300
|
+
|
|
301
|
+
def remove_item(self, item_id):
|
|
302
|
+
"""
|
|
303
|
+
item_id is simply the index of the item in the database
|
|
304
|
+
WARNING: this is not immutable across sessions
|
|
305
|
+
"""
|
|
306
|
+
return self.db[self.name].pop(item_id)
|
|
307
|
+
|
|
308
|
+
# search
|
|
309
|
+
def search_by_key(self, key, fuzzy=False, thresh=0.7, include_empty=False):
|
|
310
|
+
if fuzzy:
|
|
311
|
+
return get_key_recursively_fuzzy(self.db, key, thresh, not include_empty)
|
|
312
|
+
return get_key_recursively(self.db, key, not include_empty)
|
|
313
|
+
|
|
314
|
+
def search_by_value(self, key, value, fuzzy=False, thresh=0.7):
|
|
315
|
+
if fuzzy:
|
|
316
|
+
return get_value_recursively_fuzzy(self.db, key, value, thresh)
|
|
317
|
+
return get_value_recursively(self.db, key, value)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# XDG aware classes
|
|
321
|
+
|
|
322
|
+
class JsonStorageXDG(JsonStorage):
|
|
323
|
+
""" xdg respectful persistent dicts """
|
|
324
|
+
|
|
325
|
+
def __init__(self,
|
|
326
|
+
name,
|
|
327
|
+
xdg_folder=xdg_cache_home(),
|
|
328
|
+
disable_lock=False, subfolder="json_database",
|
|
329
|
+
extension="json"):
|
|
330
|
+
self.name = name
|
|
331
|
+
path = join(xdg_folder, subfolder, f"{name}.{extension}")
|
|
332
|
+
super().__init__(path, disable_lock=disable_lock)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class EncryptedJsonStorageXDG(EncryptedJsonStorage):
|
|
336
|
+
""" xdg respectful persistent dicts """
|
|
337
|
+
|
|
338
|
+
def __init__(self,
|
|
339
|
+
encrypt_key: str,
|
|
340
|
+
name: str,
|
|
341
|
+
xdg_folder=xdg_data_home(),
|
|
342
|
+
disable_lock=False,
|
|
343
|
+
subfolder="json_database",
|
|
344
|
+
extension="ejson"):
|
|
345
|
+
self.name = name
|
|
346
|
+
path = join(xdg_folder, subfolder, f"{name}.{extension}")
|
|
347
|
+
super().__init__(encrypt_key=encrypt_key, path=path,
|
|
348
|
+
disable_lock=disable_lock)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class JsonDatabaseXDG(JsonDatabase):
|
|
352
|
+
""" xdg respectful json database """
|
|
353
|
+
|
|
354
|
+
def __init__(self, name, xdg_folder=xdg_data_home(),
|
|
355
|
+
disable_lock=False, subfolder="json_database",
|
|
356
|
+
extension="jsondb"):
|
|
357
|
+
path = join(xdg_folder, subfolder, f"{name}.{extension}")
|
|
358
|
+
super().__init__(name, path, disable_lock=disable_lock, extension=extension)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class JsonConfigXDG(JsonStorageXDG):
|
|
362
|
+
""" xdg respectful config files, using json_storage.JsonStorageXDG """
|
|
363
|
+
|
|
364
|
+
def __init__(self, name, xdg_folder=xdg_config_home(),
|
|
365
|
+
disable_lock=False, subfolder="json_database",
|
|
366
|
+
extension="json"):
|
|
367
|
+
super().__init__(name, xdg_folder, disable_lock, subfolder, extension)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
if __name__ == "__main__":
|
|
371
|
+
# quick test
|
|
372
|
+
os.remove("/tmp/test.json")
|
|
373
|
+
db = EncryptedJsonStorage("S" * 16, "/tmp/test.json")
|
|
374
|
+
db["A"] = "42"
|
|
375
|
+
print(db) # {'A': '42'} - not encrypted in memory
|
|
376
|
+
db.store()
|
|
377
|
+
print(db) # {'A': '42'} - still decrypted
|
|
378
|
+
db.reload()
|
|
379
|
+
print(db) # {'A': '42'} - still decrypted
|
|
380
|
+
db = EncryptedJsonStorage("S" * 16, "/tmp/test.json")
|
|
381
|
+
print(db) # {'A': '42'} - still not encrypted
|
|
382
|
+
|
|
383
|
+
db = JsonStorage("/tmp/test.json")
|
|
384
|
+
print(db) # encrypted
|
|
385
|
+
# {'ciphertext': 'ad0da72dc412d6b1240e478560354893d62caf',
|
|
386
|
+
# 'tag': '3bc39dbfad7b0d7e50f3e652ee341819',
|
|
387
|
+
# 'nonce': '3020ddafc9853e7686ee0368f9be6e25'}
|
json_database/crypto.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import zlib
|
|
3
|
+
from binascii import hexlify
|
|
4
|
+
from binascii import unhexlify
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
# pycryptodomex
|
|
8
|
+
from Cryptodome.Cipher import AES
|
|
9
|
+
except ImportError:
|
|
10
|
+
# pycrypto + pycryptodome
|
|
11
|
+
try:
|
|
12
|
+
from Crypto.Cipher import AES
|
|
13
|
+
except:
|
|
14
|
+
AES = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def encrypt(key, text, nonce=None):
|
|
18
|
+
if AES is None:
|
|
19
|
+
raise ImportError("run pip install pycryptodomex")
|
|
20
|
+
if not isinstance(text, bytes):
|
|
21
|
+
text = bytes(text, encoding="utf-8")
|
|
22
|
+
if not isinstance(key, bytes):
|
|
23
|
+
key = bytes(key, encoding="utf-8")
|
|
24
|
+
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
|
|
25
|
+
text = compress_payload(text)
|
|
26
|
+
ciphertext, tag = cipher.encrypt_and_digest(text)
|
|
27
|
+
return ciphertext, tag, cipher.nonce
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def decrypt(key, ciphertext, tag, nonce) -> str:
|
|
31
|
+
if AES is None:
|
|
32
|
+
raise ImportError("run pip install pycryptodomex")
|
|
33
|
+
if not isinstance(key, bytes):
|
|
34
|
+
key = bytes(key, encoding="utf-8")
|
|
35
|
+
cipher = AES.new(key, AES.MODE_GCM, nonce)
|
|
36
|
+
data = cipher.decrypt_and_verify(ciphertext, tag)
|
|
37
|
+
text = decompress_payload(data).decode(encoding="utf-8")
|
|
38
|
+
return text
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def encrypt_as_json(key, data):
|
|
42
|
+
if isinstance(data, dict):
|
|
43
|
+
data = json.dumps(data)
|
|
44
|
+
if len(key) > 16:
|
|
45
|
+
key = key[0:16]
|
|
46
|
+
ciphertext, tag, nonce = encrypt(key, data)
|
|
47
|
+
return json.dumps({"ciphertext": hexlify(ciphertext).decode('utf-8'),
|
|
48
|
+
"tag": hexlify(tag).decode('utf-8'),
|
|
49
|
+
"nonce": hexlify(nonce).decode('utf-8')})
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def decrypt_from_json(key, data):
|
|
53
|
+
if isinstance(data, str):
|
|
54
|
+
data = json.loads(data)
|
|
55
|
+
if len(key) > 16:
|
|
56
|
+
key = key[0:16]
|
|
57
|
+
ciphertext = unhexlify(data["ciphertext"])
|
|
58
|
+
if data.get("tag") is None: # web crypto
|
|
59
|
+
ciphertext, tag = ciphertext[:-16], ciphertext[-16:]
|
|
60
|
+
else:
|
|
61
|
+
tag = unhexlify(data["tag"])
|
|
62
|
+
nonce = unhexlify(data["nonce"])
|
|
63
|
+
return decrypt(key, ciphertext, tag, nonce)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def compress_payload(text):
|
|
67
|
+
# Compressing text
|
|
68
|
+
if isinstance(text, str):
|
|
69
|
+
decompressed = text.encode("utf-8")
|
|
70
|
+
else:
|
|
71
|
+
decompressed = text
|
|
72
|
+
return zlib.compress(decompressed)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def decompress_payload(compressed):
|
|
76
|
+
# Decompressing text
|
|
77
|
+
if isinstance(compressed, str):
|
|
78
|
+
# assume hex
|
|
79
|
+
compressed = unhexlify(compressed)
|
|
80
|
+
return zlib.decompress(compressed)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class InvalidItemID(ValueError):
|
|
2
|
+
""" ItemID is invalid """
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DatabaseNotCommitted(FileNotFoundError):
|
|
6
|
+
""" Database has not been saved in yet """
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SessionError(RuntimeError):
|
|
10
|
+
""" Could not commit database"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MatchError(ValueError):
|
|
14
|
+
""" could not match an item in db """
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DecryptionKeyError(KeyError):
|
|
18
|
+
""" Could not decrypt payload """
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EncryptionKeyError(KeyError):
|
|
22
|
+
""" Could not encrypt payload """
|
json_database/search.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from json_database.utils import fuzzy_match, match_one
|
|
2
|
+
from json_database import JsonDatabase
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Query:
|
|
6
|
+
def __init__(self, db):
|
|
7
|
+
if isinstance(db, JsonDatabase):
|
|
8
|
+
self.result = list(db)
|
|
9
|
+
else:
|
|
10
|
+
self.result = [db]
|
|
11
|
+
|
|
12
|
+
def contains_key(self, key, fuzzy=False, thresh=0.7, ignore_case=False):
|
|
13
|
+
if fuzzy:
|
|
14
|
+
after = []
|
|
15
|
+
for e in self.result:
|
|
16
|
+
filter = True
|
|
17
|
+
for k in e:
|
|
18
|
+
if ignore_case:
|
|
19
|
+
score = fuzzy_match(k.lower(), key.lower())
|
|
20
|
+
else:
|
|
21
|
+
score = fuzzy_match(k, key)
|
|
22
|
+
if score < thresh:
|
|
23
|
+
continue
|
|
24
|
+
filter = False
|
|
25
|
+
if not filter:
|
|
26
|
+
after.append(e)
|
|
27
|
+
self.result = after
|
|
28
|
+
elif ignore_case:
|
|
29
|
+
self.result = [a for a in self.result
|
|
30
|
+
if a.get(key) or a.get(key.lower())]
|
|
31
|
+
else:
|
|
32
|
+
self.result = [a for a in self.result if a.get(key)]
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
def contains_value(self, key, value, fuzzy=False, thresh=0.75, ignore_case=False):
|
|
36
|
+
self.contains_key(key, ignore_case=ignore_case)
|
|
37
|
+
if fuzzy:
|
|
38
|
+
after = []
|
|
39
|
+
for e in self.result:
|
|
40
|
+
if isinstance(e[key], str):
|
|
41
|
+
if ignore_case:
|
|
42
|
+
score = fuzzy_match(value.lower(), e[key].lower())
|
|
43
|
+
else:
|
|
44
|
+
score = fuzzy_match(value, e[key])
|
|
45
|
+
if score > thresh:
|
|
46
|
+
after.append(e)
|
|
47
|
+
elif isinstance(e[key], list):
|
|
48
|
+
if ignore_case:
|
|
49
|
+
v, score = match_one(value.lower(),
|
|
50
|
+
[_.lower() for _ in e[key]])
|
|
51
|
+
else:
|
|
52
|
+
v, score = match_one(value, e[key])
|
|
53
|
+
if score < thresh:
|
|
54
|
+
continue
|
|
55
|
+
after.append(e)
|
|
56
|
+
elif isinstance(e[key], dict):
|
|
57
|
+
if ignore_case:
|
|
58
|
+
v, score = match_one(value.lower(),
|
|
59
|
+
[_.lower() for _ in e[key].keys()])
|
|
60
|
+
else:
|
|
61
|
+
v, score = match_one(value, e[key])
|
|
62
|
+
if score < thresh:
|
|
63
|
+
continue
|
|
64
|
+
after.append(e)
|
|
65
|
+
self.result = after
|
|
66
|
+
elif ignore_case and isinstance(value, str):
|
|
67
|
+
after = []
|
|
68
|
+
for a in self.result:
|
|
69
|
+
if isinstance(a[key], str) and value.lower() in a[key].lower():
|
|
70
|
+
after.append(a)
|
|
71
|
+
elif value.lower() in a[key] or value in a[key]:
|
|
72
|
+
after.append(a)
|
|
73
|
+
self.result = after
|
|
74
|
+
else:
|
|
75
|
+
self.result = [a for a in self.result if value in a[key]]
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def value_contains(self, key, value, ignore_case=False):
|
|
79
|
+
self.contains_key(key, ignore_case=ignore_case)
|
|
80
|
+
if ignore_case:
|
|
81
|
+
after = []
|
|
82
|
+
value = str(value).lower()
|
|
83
|
+
for e in self.result:
|
|
84
|
+
if isinstance(e[key], str) and ignore_case:
|
|
85
|
+
if value.lower() in e[key].lower():
|
|
86
|
+
after.append(e)
|
|
87
|
+
elif isinstance(e[key], list) and ignore_case:
|
|
88
|
+
if value.lower() in [str(_).lower() for _ in e[key]]:
|
|
89
|
+
after.append(e)
|
|
90
|
+
elif isinstance(e[key], dict) and ignore_case:
|
|
91
|
+
if value.lower() in [str(_).lower() for _ in e[key].keys()]:
|
|
92
|
+
after.append(e)
|
|
93
|
+
|
|
94
|
+
elif isinstance(e[key], str):
|
|
95
|
+
if value in e[key]:
|
|
96
|
+
after.append(e)
|
|
97
|
+
elif isinstance(e[key], list):
|
|
98
|
+
if value in [str(_) for _ in e[key]]:
|
|
99
|
+
after.append(e)
|
|
100
|
+
elif isinstance(e[key], dict):
|
|
101
|
+
if value in [str(_) for _ in e[key].keys()]:
|
|
102
|
+
after.append(e)
|
|
103
|
+
|
|
104
|
+
self.result = after
|
|
105
|
+
else:
|
|
106
|
+
self.result = [e for e in self.result if value in e[key]]
|
|
107
|
+
return self
|
|
108
|
+
|
|
109
|
+
def value_contains_token(self, key, value, fuzzy=False, thresh=0.75, ignore_case=False):
|
|
110
|
+
self.contains_key(key, ignore_case=ignore_case)
|
|
111
|
+
after = []
|
|
112
|
+
value = str(value)
|
|
113
|
+
for e in self.result:
|
|
114
|
+
if isinstance(e[key], str):
|
|
115
|
+
if fuzzy:
|
|
116
|
+
_, score = match_one(value.lower(),
|
|
117
|
+
e[key].lower().split(" "))
|
|
118
|
+
if score > thresh:
|
|
119
|
+
after.append(e)
|
|
120
|
+
elif ignore_case and value.lower() in e[key].lower().split(" "):
|
|
121
|
+
after.append(e)
|
|
122
|
+
elif value in e[key].split(" "):
|
|
123
|
+
after.append(e)
|
|
124
|
+
elif value in e[key]:
|
|
125
|
+
after.append(e)
|
|
126
|
+
self.result = after
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
def equal(self, key, value, ignore_case=False):
|
|
130
|
+
self.contains_key(key, ignore_case=ignore_case)
|
|
131
|
+
if ignore_case and isinstance(value, str):
|
|
132
|
+
self.result = [a for a in self.result
|
|
133
|
+
if a[key].lower() == value.lower()]
|
|
134
|
+
else:
|
|
135
|
+
self.result = [a for a in self.result if a[key] == value]
|
|
136
|
+
return self
|
|
137
|
+
|
|
138
|
+
def below(self, key, value, ignore_case=False):
|
|
139
|
+
self.contains_key(key, ignore_case=ignore_case)
|
|
140
|
+
self.result = [a for a in self.result if a[key] < value]
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def above(self, key, value, ignore_case=False):
|
|
144
|
+
self.contains_key(key, ignore_case=ignore_case)
|
|
145
|
+
self.result = [a for a in self.result if a[key] > value]
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
def below_or_equal(self, key, value, ignore_case=False):
|
|
149
|
+
self.contains_key(key, ignore_case=ignore_case)
|
|
150
|
+
self.result = [a for a in self.result if a[key] <= value]
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
def above_or_equal(self, key, value, ignore_case=False):
|
|
154
|
+
self.contains_key(key, ignore_case=ignore_case)
|
|
155
|
+
self.result = [a for a in self.result if a[key] >= value]
|
|
156
|
+
return self
|
|
157
|
+
|
|
158
|
+
def in_range(self, key, min_value, max_value, ignore_case=False):
|
|
159
|
+
self.contains_key(key, ignore_case=ignore_case)
|
|
160
|
+
self.result = [a for a in self.result if min_value < a[key] < max_value]
|
|
161
|
+
return self
|
|
162
|
+
|
|
163
|
+
def all(self):
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
def build(self):
|
|
167
|
+
return self.result
|
|
168
|
+
|
|
169
|
+
|