json-database 0.6.0__tar.gz → 0.8.0a1__tar.gz

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.
Files changed (23) hide show
  1. json_database-0.8.0a1/PKG-INFO +202 -0
  2. {json_database-0.6.0 → json_database-0.8.0a1}/json_database/__init__.py +92 -15
  3. json_database-0.8.0a1/json_database/crypto.py +80 -0
  4. {json_database-0.6.0 → json_database-0.8.0a1}/json_database/exceptions.py +8 -0
  5. {json_database-0.6.0 → json_database-0.8.0a1}/json_database/search.py +1 -1
  6. json_database-0.6.0/json_database/utils/__init__.py → json_database-0.8.0a1/json_database/utils.py +33 -0
  7. json_database-0.8.0a1/json_database/version.py +6 -0
  8. json_database-0.8.0a1/json_database/xdg_utils.py +174 -0
  9. json_database-0.8.0a1/json_database.egg-info/PKG-INFO +202 -0
  10. {json_database-0.6.0 → json_database-0.8.0a1}/json_database.egg-info/SOURCES.txt +5 -2
  11. json_database-0.8.0a1/json_database.egg-info/requires.txt +1 -0
  12. json_database-0.8.0a1/setup.py +73 -0
  13. json_database-0.8.0a1/test/test_crypto.py +53 -0
  14. json_database-0.6.0/PKG-INFO +0 -13
  15. json_database-0.6.0/json_database/utils/combo_lock.py +0 -108
  16. json_database-0.6.0/json_database.egg-info/PKG-INFO +0 -13
  17. json_database-0.6.0/json_database.egg-info/requires.txt +0 -2
  18. json_database-0.6.0/setup.py +0 -13
  19. {json_database-0.6.0 → json_database-0.8.0a1}/LICENSE +0 -0
  20. {json_database-0.6.0 → json_database-0.8.0a1}/README.md +0 -0
  21. {json_database-0.6.0 → json_database-0.8.0a1}/json_database.egg-info/dependency_links.txt +0 -0
  22. {json_database-0.6.0 → json_database-0.8.0a1}/json_database.egg-info/top_level.txt +0 -0
  23. {json_database-0.6.0 → json_database-0.8.0a1}/setup.cfg +0 -0
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.1
2
+ Name: json_database
3
+ Version: 0.8.0a1
4
+ Summary: searchable json database with persistence
5
+ Home-page: https://github.com/TigreGotico/json_database
6
+ Author: jarbasAI
7
+ Author-email: jarbasai@mailfence.com
8
+ License: MIT
9
+ Description: # Json Database
10
+
11
+ Python dict based database with persistence and search capabilities
12
+
13
+ For those times when you need something simple and sql is overkill
14
+
15
+
16
+ ## Features
17
+
18
+ - pure python
19
+ - save and load from file
20
+ - search recursively by key and key/value pairs
21
+ - fuzzy search
22
+ - supports arbitrary objects
23
+ - supports comments in saved files
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install json_database
29
+ ```
30
+
31
+ ## Usage
32
+
33
+
34
+ ### JsonStorage
35
+
36
+ Sometimes you need persistent dicts that you can save and load from file
37
+
38
+ ```python
39
+ from json_database import JsonStorage
40
+ from os.path import exists
41
+
42
+ save_path = "my_dict.conf"
43
+
44
+ my_config = JsonStorage(save_path)
45
+
46
+ my_config["lang"] = "pt"
47
+ my_config["secondary_lang"] = "en"
48
+ my_config["email"] = "jarbasai@mailfence.com"
49
+
50
+ # my_config is a python dict
51
+ assert isinstance(my_config, dict)
52
+
53
+ # save to file
54
+ my_config.store()
55
+
56
+ my_config["lang"] = "pt-pt"
57
+
58
+ # revert to previous saved file
59
+ my_config.reload()
60
+ assert my_config["lang"] == "pt"
61
+
62
+ # clear all fields
63
+ my_config.clear()
64
+ assert my_config == {}
65
+
66
+ # load from a specific path
67
+ my_config.load_local(save_path)
68
+ assert my_config == JsonStorage(save_path)
69
+
70
+ # delete stored file
71
+ my_config.remove()
72
+ assert not exists(save_path)
73
+
74
+ # keep working with dict in memory
75
+ print(my_config)
76
+ ```
77
+
78
+ ### JsonDatabase
79
+
80
+ Ever wanted to search a dict?
81
+
82
+ Let's create a dummy database with users
83
+
84
+ ```python
85
+ from json_database import JsonDatabase
86
+
87
+ db_path = "users.db"
88
+
89
+ with JsonDatabase("users", db_path) as db:
90
+ # add some users to the database
91
+
92
+ for user in [
93
+ {"name": "bob", "age": 12},
94
+ {"name": "bobby"},
95
+ {"name": ["joe", "jony"]},
96
+ {"name": "john"},
97
+ {"name": "jones", "age": 35},
98
+ {"name": "joey", "birthday": "may 12"}]:
99
+ db.add_item(user)
100
+
101
+ # pretty print database contents
102
+ db.print()
103
+
104
+
105
+ # auto saved when used with context manager
106
+ # db.commit()
107
+
108
+
109
+ ```
110
+
111
+ search entries by key
112
+
113
+ ```python
114
+ from json_database import JsonDatabase
115
+
116
+ db_path = "users.db"
117
+
118
+ db = JsonDatabase("users", db_path) # load db created in previous example
119
+
120
+ # search by exact key match
121
+ users_with_defined_age = db.search_by_key("age")
122
+
123
+ for user in users_with_defined_age:
124
+ print(user["name"], user["age"])
125
+
126
+ # fuzzy search
127
+ users = db.search_by_key("birth", fuzzy=True)
128
+ for user, conf in users:
129
+ print("matched with confidence", conf)
130
+ print(user["name"], user["birthday"])
131
+ ```
132
+
133
+ search by key value pair
134
+
135
+ ```python
136
+ # search by key/value pair
137
+ users_12years_old = db.search_by_value("age", 12)
138
+
139
+ for user in users_12years_old:
140
+ assert user["age"] == 12
141
+
142
+ # fuzzy search
143
+ jon_users = db.search_by_value("name", "jon", fuzzy=True)
144
+ for user, conf in jon_users:
145
+ print(user["name"])
146
+ print("matched with confidence", conf)
147
+ # NOTE that one of the users has a list instead of a string in the name, it also matches
148
+ ```
149
+
150
+ updating an existing entry
151
+
152
+ ```python
153
+ # get database item
154
+ item = {"name": "bobby"}
155
+
156
+ item_id = db.get_item_id(item)
157
+
158
+ if item_id >= 0:
159
+ new_item = {"name": "don't call me bobby"}
160
+ db.update_item(item_id, new_item)
161
+ else:
162
+ print("item not found in database")
163
+
164
+ # clear changes since last commit
165
+ db.reset()
166
+ ```
167
+
168
+ You can save arbitrary objects to the database
169
+
170
+ ```python
171
+ from json_database import JsonDatabase
172
+
173
+ db = JsonDatabase("users", "~/databases/users.json")
174
+
175
+
176
+ class User:
177
+ def __init__(self, email, key=None, data=None):
178
+ self.email = email
179
+ self.secret_key = key
180
+ self.data = data
181
+
182
+ user1 = User("first@mail.net", data={"name": "jonas", "birthday": "12 May"})
183
+ user2 = User("second@mail.net", "secret", data={"name": ["joe", "jony"], "age": 12})
184
+
185
+ # objects will be "jsonified" here, they will no longer be User objects
186
+ # if you need them to be a specific class use some ORM lib instead (SQLAlchemy is great)
187
+ db.add_item(user1)
188
+ db.add_item(user2)
189
+
190
+ # search entries with non empty key
191
+ print(db.search_by_key("secret_key"))
192
+
193
+ # search in user provided data
194
+ print(db.search_by_key("birth", fuzzy=True))
195
+
196
+ # search entries with a certain value
197
+ print(db.search_by_value("age", 12))
198
+ print(db.search_by_value("name", "jon", fuzzy=True))
199
+
200
+ ```
201
+ Platform: UNKNOWN
202
+ Description-Content-Type: text/markdown
@@ -1,16 +1,21 @@
1
- from json_database.utils import *
2
- from json_database.exceptions import InvalidItemID, DatabaseNotCommitted, \
3
- SessionError, MatchError
4
- from os.path import expanduser, isdir, dirname, exists, isfile, join
5
- from os import makedirs, remove
6
1
  import json
7
2
  import logging
3
+ import os
4
+ from os import makedirs, remove
5
+ from os.path import expanduser, isdir, dirname, exists, isfile, join
8
6
  from pprint import pprint
9
- from xdg import BaseDirectory
10
- from json_database.utils.combo_lock import ComboLock, DummyLock
11
-
12
7
  from tempfile import gettempdir
13
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
+
14
19
  LOG = logging.getLogger("JsonDatabase")
15
20
  LOG.setLevel("INFO")
16
21
 
@@ -101,13 +106,49 @@ class JsonStorage(dict):
101
106
  raise SessionError
102
107
 
103
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
+
104
144
  class JsonDatabase(dict):
105
145
  """ searchable persistent dict """
146
+
106
147
  def __init__(self,
107
- name,
108
- path=None,
109
- disable_lock=False,
110
- extension="json"):
148
+ name,
149
+ path=None,
150
+ disable_lock=False,
151
+ extension="json"):
111
152
  super().__init__()
112
153
  self.name = name
113
154
  self.path = path or f"{name}.{extension}"
@@ -283,7 +324,7 @@ class JsonStorageXDG(JsonStorage):
283
324
 
284
325
  def __init__(self,
285
326
  name,
286
- xdg_folder=BaseDirectory.xdg_cache_home,
327
+ xdg_folder=xdg_cache_home(),
287
328
  disable_lock=False, subfolder="json_database",
288
329
  extension="json"):
289
330
  self.name = name
@@ -291,10 +332,26 @@ class JsonStorageXDG(JsonStorage):
291
332
  super().__init__(path, disable_lock=disable_lock)
292
333
 
293
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
+
294
351
  class JsonDatabaseXDG(JsonDatabase):
295
352
  """ xdg respectful json database """
296
353
 
297
- def __init__(self, name, xdg_folder=BaseDirectory.xdg_data_home,
354
+ def __init__(self, name, xdg_folder=xdg_data_home(),
298
355
  disable_lock=False, subfolder="json_database",
299
356
  extension="jsondb"):
300
357
  path = join(xdg_folder, subfolder, f"{name}.{extension}")
@@ -304,7 +361,27 @@ class JsonDatabaseXDG(JsonDatabase):
304
361
  class JsonConfigXDG(JsonStorageXDG):
305
362
  """ xdg respectful config files, using json_storage.JsonStorageXDG """
306
363
 
307
- def __init__(self, name, xdg_folder=BaseDirectory.xdg_config_home,
364
+ def __init__(self, name, xdg_folder=xdg_config_home(),
308
365
  disable_lock=False, subfolder="json_database",
309
366
  extension="json"):
310
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'}
@@ -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)
@@ -12,3 +12,11 @@ class SessionError(RuntimeError):
12
12
 
13
13
  class MatchError(ValueError):
14
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 """
@@ -1,5 +1,5 @@
1
1
  from json_database.utils import fuzzy_match, match_one
2
- from json_database import JsonDatabase, JsonStorageXDG
2
+ from json_database import JsonDatabase
3
3
 
4
4
 
5
5
  class Query:
@@ -2,6 +2,39 @@ import json
2
2
  from difflib import SequenceMatcher
3
3
 
4
4
 
5
+ class DummyLock:
6
+ """ A fake lock.
7
+
8
+ Arguments:
9
+ path (str): path to the lockfile for the lock
10
+ """
11
+ def __init__(self, path=""):
12
+ self.path = path
13
+
14
+ def acquire(self, blocking=True):
15
+ """ Acquire lock, locks thread and process lock.
16
+
17
+ Arguments:
18
+ blocking(bool): Set's blocking mode of acquire operation.
19
+ Default True.
20
+
21
+ Returns: True if lock succeeded otherwise False
22
+ """
23
+ return True
24
+
25
+ def release(self):
26
+ """ Release acquired lock. """
27
+ pass
28
+
29
+ def __enter__(self):
30
+ """ Context handler, acquires lock in blocking mode. """
31
+ return self
32
+
33
+ def __exit__(self, _type, value, traceback):
34
+ """ Releases the lock. """
35
+ pass
36
+
37
+
5
38
  def fuzzy_match(x, against):
6
39
  """Perform a 'fuzzy' comparison between two strings.
7
40
  Returns:
@@ -0,0 +1,6 @@
1
+ # START_VERSION_BLOCK
2
+ VERSION_MAJOR = 0
3
+ VERSION_MINOR = 8
4
+ VERSION_BUILD = 0
5
+ VERSION_ALPHA = 1
6
+ # END_VERSION_BLOCK
@@ -0,0 +1,174 @@
1
+ # Copyright © 2016-2021 Scott Stevenson <scott@stevenson.io>
2
+ #
3
+ # Permission to use, copy, modify, and/or distribute this software for
4
+ # any purpose with or without fee is hereby granted, provided that the
5
+ # above copyright notice and this permission notice appear in all
6
+ # copies.
7
+ #
8
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
9
+ # WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
10
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
11
+ # AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
12
+ # DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
13
+ # PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
14
+ # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ # PERFORMANCE OF THIS SOFTWARE.
16
+
17
+ """XDG Base Directory Specification variables.
18
+
19
+ xdg_cache_home(), xdg_config_home(), xdg_data_home(), and xdg_state_home()
20
+ return pathlib.Path objects containing the value of the environment variable
21
+ named XDG_CACHE_HOME, XDG_CONFIG_HOME, XDG_DATA_HOME, and XDG_STATE_HOME
22
+ respectively, or the default defined in the specification if the environment
23
+ variable is unset, empty, or contains a relative path rather than absolute
24
+ path.
25
+
26
+ xdg_config_dirs() and xdg_data_dirs() return a list of pathlib.Path
27
+ objects containing the value, split on colons, of the environment
28
+ variable named XDG_CONFIG_DIRS and XDG_DATA_DIRS respectively, or the
29
+ default defined in the specification if the environment variable is
30
+ unset or empty. Relative paths are ignored, as per the specification.
31
+
32
+ xdg_runtime_dir() returns a pathlib.Path object containing the value of
33
+ the XDG_RUNTIME_DIR environment variable, or None if the environment
34
+ variable is not set, or contains a relative path rather than absolute path.
35
+
36
+ """
37
+
38
+ # pylint: disable=fixme
39
+
40
+ import os
41
+ from pathlib import Path
42
+ from typing import List, Optional
43
+
44
+ __all__ = [
45
+ "xdg_cache_home",
46
+ "xdg_config_dirs",
47
+ "xdg_config_home",
48
+ "xdg_data_dirs",
49
+ "xdg_data_home",
50
+ "xdg_runtime_dir",
51
+ "xdg_state_home",
52
+ "XDG_CACHE_HOME",
53
+ "XDG_CONFIG_DIRS",
54
+ "XDG_CONFIG_HOME",
55
+ "XDG_DATA_DIRS",
56
+ "XDG_DATA_HOME",
57
+ "XDG_RUNTIME_DIR",
58
+ ]
59
+
60
+
61
+ def _path_from_env(variable: str, default: Path) -> Path:
62
+ """Read an environment variable as a path.
63
+
64
+ The environment variable with the specified name is read, and its
65
+ value returned as a path. If the environment variable is not set, is
66
+ set to the empty string, or is set to a relative rather than
67
+ absolute path, the default value is returned.
68
+
69
+ Parameters
70
+ ----------
71
+ variable : str
72
+ Name of the environment variable.
73
+ default : Path
74
+ Default value.
75
+
76
+ Returns
77
+ -------
78
+ Path
79
+ Value from environment or default.
80
+
81
+ """
82
+ # TODO(srstevenson): Use assignment expression in Python 3.8.
83
+ value = os.environ.get(variable)
84
+ if value and os.path.isabs(value):
85
+ return Path(value)
86
+ return default
87
+
88
+
89
+ def _paths_from_env(variable: str, default: List[Path]) -> List[Path]:
90
+ """Read an environment variable as a list of paths.
91
+
92
+ The environment variable with the specified name is read, and its
93
+ value split on colons and returned as a list of paths. If the
94
+ environment variable is not set, or set to the empty string, the
95
+ default value is returned. Relative paths are ignored, as per the
96
+ specification.
97
+
98
+ Parameters
99
+ ----------
100
+ variable : str
101
+ Name of the environment variable.
102
+ default : List[Path]
103
+ Default value.
104
+
105
+ Returns
106
+ -------
107
+ List[Path]
108
+ Value from environment or default.
109
+
110
+ """
111
+ # TODO(srstevenson): Use assignment expression in Python 3.8.
112
+ value = os.environ.get(variable)
113
+ if value:
114
+ paths = [
115
+ Path(path) for path in value.split(":") if os.path.isabs(path)
116
+ ]
117
+ if paths:
118
+ return paths
119
+ return default
120
+
121
+
122
+ def xdg_cache_home() -> Path:
123
+ """Return a Path corresponding to XDG_CACHE_HOME."""
124
+ return _path_from_env("XDG_CACHE_HOME", Path.home() / ".cache")
125
+
126
+
127
+ def xdg_config_dirs() -> List[Path]:
128
+ """Return a list of Paths corresponding to XDG_CONFIG_DIRS."""
129
+ return _paths_from_env("XDG_CONFIG_DIRS", [Path("/etc/xdg")])
130
+
131
+
132
+ def xdg_config_home() -> Path:
133
+ """Return a Path corresponding to XDG_CONFIG_HOME."""
134
+ return _path_from_env("XDG_CONFIG_HOME", Path.home() / ".config")
135
+
136
+
137
+ def xdg_data_dirs() -> List[Path]:
138
+ """Return a list of Paths corresponding to XDG_DATA_DIRS."""
139
+ return _paths_from_env(
140
+ "XDG_DATA_DIRS",
141
+ [Path(path) for path in "/usr/local/share/:/usr/share/".split(":")],
142
+ )
143
+
144
+
145
+ def xdg_data_home() -> Path:
146
+ """Return a Path corresponding to XDG_DATA_HOME."""
147
+ return _path_from_env("XDG_DATA_HOME", Path.home() / ".local" / "share")
148
+
149
+
150
+ def xdg_runtime_dir() -> Optional[Path]:
151
+ """Return a Path corresponding to XDG_RUNTIME_DIR.
152
+
153
+ If the XDG_RUNTIME_DIR environment variable is not set, None will be
154
+ returned as per the specification.
155
+
156
+ """
157
+ value = os.getenv("XDG_RUNTIME_DIR")
158
+ if value and os.path.isabs(value):
159
+ return Path(value)
160
+ return None
161
+
162
+
163
+ def xdg_state_home() -> Path:
164
+ """Return a Path corresponding to XDG_STATE_HOME."""
165
+ return _path_from_env("XDG_STATE_HOME", Path.home() / ".local" / "state")
166
+
167
+
168
+ # The following variables are deprecated, but remain for backward compatibility.
169
+ XDG_CACHE_HOME = xdg_cache_home()
170
+ XDG_CONFIG_DIRS = xdg_config_dirs()
171
+ XDG_CONFIG_HOME = xdg_config_home()
172
+ XDG_DATA_DIRS = xdg_data_dirs()
173
+ XDG_DATA_HOME = xdg_data_home()
174
+ XDG_RUNTIME_DIR = xdg_runtime_dir()
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.1
2
+ Name: json-database
3
+ Version: 0.8.0a1
4
+ Summary: searchable json database with persistence
5
+ Home-page: https://github.com/TigreGotico/json_database
6
+ Author: jarbasAI
7
+ Author-email: jarbasai@mailfence.com
8
+ License: MIT
9
+ Description: # Json Database
10
+
11
+ Python dict based database with persistence and search capabilities
12
+
13
+ For those times when you need something simple and sql is overkill
14
+
15
+
16
+ ## Features
17
+
18
+ - pure python
19
+ - save and load from file
20
+ - search recursively by key and key/value pairs
21
+ - fuzzy search
22
+ - supports arbitrary objects
23
+ - supports comments in saved files
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install json_database
29
+ ```
30
+
31
+ ## Usage
32
+
33
+
34
+ ### JsonStorage
35
+
36
+ Sometimes you need persistent dicts that you can save and load from file
37
+
38
+ ```python
39
+ from json_database import JsonStorage
40
+ from os.path import exists
41
+
42
+ save_path = "my_dict.conf"
43
+
44
+ my_config = JsonStorage(save_path)
45
+
46
+ my_config["lang"] = "pt"
47
+ my_config["secondary_lang"] = "en"
48
+ my_config["email"] = "jarbasai@mailfence.com"
49
+
50
+ # my_config is a python dict
51
+ assert isinstance(my_config, dict)
52
+
53
+ # save to file
54
+ my_config.store()
55
+
56
+ my_config["lang"] = "pt-pt"
57
+
58
+ # revert to previous saved file
59
+ my_config.reload()
60
+ assert my_config["lang"] == "pt"
61
+
62
+ # clear all fields
63
+ my_config.clear()
64
+ assert my_config == {}
65
+
66
+ # load from a specific path
67
+ my_config.load_local(save_path)
68
+ assert my_config == JsonStorage(save_path)
69
+
70
+ # delete stored file
71
+ my_config.remove()
72
+ assert not exists(save_path)
73
+
74
+ # keep working with dict in memory
75
+ print(my_config)
76
+ ```
77
+
78
+ ### JsonDatabase
79
+
80
+ Ever wanted to search a dict?
81
+
82
+ Let's create a dummy database with users
83
+
84
+ ```python
85
+ from json_database import JsonDatabase
86
+
87
+ db_path = "users.db"
88
+
89
+ with JsonDatabase("users", db_path) as db:
90
+ # add some users to the database
91
+
92
+ for user in [
93
+ {"name": "bob", "age": 12},
94
+ {"name": "bobby"},
95
+ {"name": ["joe", "jony"]},
96
+ {"name": "john"},
97
+ {"name": "jones", "age": 35},
98
+ {"name": "joey", "birthday": "may 12"}]:
99
+ db.add_item(user)
100
+
101
+ # pretty print database contents
102
+ db.print()
103
+
104
+
105
+ # auto saved when used with context manager
106
+ # db.commit()
107
+
108
+
109
+ ```
110
+
111
+ search entries by key
112
+
113
+ ```python
114
+ from json_database import JsonDatabase
115
+
116
+ db_path = "users.db"
117
+
118
+ db = JsonDatabase("users", db_path) # load db created in previous example
119
+
120
+ # search by exact key match
121
+ users_with_defined_age = db.search_by_key("age")
122
+
123
+ for user in users_with_defined_age:
124
+ print(user["name"], user["age"])
125
+
126
+ # fuzzy search
127
+ users = db.search_by_key("birth", fuzzy=True)
128
+ for user, conf in users:
129
+ print("matched with confidence", conf)
130
+ print(user["name"], user["birthday"])
131
+ ```
132
+
133
+ search by key value pair
134
+
135
+ ```python
136
+ # search by key/value pair
137
+ users_12years_old = db.search_by_value("age", 12)
138
+
139
+ for user in users_12years_old:
140
+ assert user["age"] == 12
141
+
142
+ # fuzzy search
143
+ jon_users = db.search_by_value("name", "jon", fuzzy=True)
144
+ for user, conf in jon_users:
145
+ print(user["name"])
146
+ print("matched with confidence", conf)
147
+ # NOTE that one of the users has a list instead of a string in the name, it also matches
148
+ ```
149
+
150
+ updating an existing entry
151
+
152
+ ```python
153
+ # get database item
154
+ item = {"name": "bobby"}
155
+
156
+ item_id = db.get_item_id(item)
157
+
158
+ if item_id >= 0:
159
+ new_item = {"name": "don't call me bobby"}
160
+ db.update_item(item_id, new_item)
161
+ else:
162
+ print("item not found in database")
163
+
164
+ # clear changes since last commit
165
+ db.reset()
166
+ ```
167
+
168
+ You can save arbitrary objects to the database
169
+
170
+ ```python
171
+ from json_database import JsonDatabase
172
+
173
+ db = JsonDatabase("users", "~/databases/users.json")
174
+
175
+
176
+ class User:
177
+ def __init__(self, email, key=None, data=None):
178
+ self.email = email
179
+ self.secret_key = key
180
+ self.data = data
181
+
182
+ user1 = User("first@mail.net", data={"name": "jonas", "birthday": "12 May"})
183
+ user2 = User("second@mail.net", "secret", data={"name": ["joe", "jony"], "age": 12})
184
+
185
+ # objects will be "jsonified" here, they will no longer be User objects
186
+ # if you need them to be a specific class use some ORM lib instead (SQLAlchemy is great)
187
+ db.add_item(user1)
188
+ db.add_item(user2)
189
+
190
+ # search entries with non empty key
191
+ print(db.search_by_key("secret_key"))
192
+
193
+ # search in user provided data
194
+ print(db.search_by_key("birth", fuzzy=True))
195
+
196
+ # search entries with a certain value
197
+ print(db.search_by_value("age", 12))
198
+ print(db.search_by_value("name", "jon", fuzzy=True))
199
+
200
+ ```
201
+ Platform: UNKNOWN
202
+ Description-Content-Type: text/markdown
@@ -2,12 +2,15 @@ LICENSE
2
2
  README.md
3
3
  setup.py
4
4
  json_database/__init__.py
5
+ json_database/crypto.py
5
6
  json_database/exceptions.py
6
7
  json_database/search.py
8
+ json_database/utils.py
9
+ json_database/version.py
10
+ json_database/xdg_utils.py
7
11
  json_database.egg-info/PKG-INFO
8
12
  json_database.egg-info/SOURCES.txt
9
13
  json_database.egg-info/dependency_links.txt
10
14
  json_database.egg-info/requires.txt
11
15
  json_database.egg-info/top_level.txt
12
- json_database/utils/__init__.py
13
- json_database/utils/combo_lock.py
16
+ test/test_crypto.py
@@ -0,0 +1 @@
1
+ combo_lock<1.0.0,>=0.2.1
@@ -0,0 +1,73 @@
1
+ import os
2
+ import os.path
3
+
4
+ from setuptools import setup
5
+
6
+ BASEDIR = os.path.abspath(os.path.dirname(__file__))
7
+
8
+
9
+ def package_files(directory):
10
+ paths = []
11
+ for (path, _, filenames) in os.walk(directory):
12
+ for filename in filenames:
13
+ paths.append(os.path.join('..', path, filename))
14
+ return paths
15
+
16
+
17
+ def required(requirements_file):
18
+ """ Read requirements file and remove comments and empty lines. """
19
+ with open(os.path.join(BASEDIR, requirements_file), 'r') as f:
20
+ requirements = f.read().splitlines()
21
+ if 'MYCROFT_LOOSE_REQUIREMENTS' in os.environ:
22
+ print('USING LOOSE REQUIREMENTS!')
23
+ requirements = [r.replace('==', '>=').replace('~=', '>=') for r in requirements]
24
+ return [pkg for pkg in requirements
25
+ if pkg.strip() and not pkg.startswith("#")]
26
+
27
+
28
+ def get_version():
29
+ """ Find the version of ovos-core"""
30
+ version = None
31
+ version_file = os.path.join(BASEDIR, 'json_database', 'version.py')
32
+ major, minor, build, alpha = (None, None, None, None)
33
+ with open(version_file) as f:
34
+ for line in f:
35
+ if 'VERSION_MAJOR' in line:
36
+ major = line.split('=')[1].strip()
37
+ elif 'VERSION_MINOR' in line:
38
+ minor = line.split('=')[1].strip()
39
+ elif 'VERSION_BUILD' in line:
40
+ build = line.split('=')[1].strip()
41
+ elif 'VERSION_ALPHA' in line:
42
+ alpha = line.split('=')[1].strip()
43
+
44
+ if ((major and minor and build and alpha) or
45
+ '# END_VERSION_BLOCK' in line):
46
+ break
47
+ version = f"{major}.{minor}.{build}"
48
+ if int(alpha):
49
+ version += f"a{alpha}"
50
+ return version
51
+
52
+
53
+ def get_description():
54
+ with open(os.path.join(BASEDIR, "README.md"), "r") as f:
55
+ long_description = f.read()
56
+ return long_description
57
+
58
+
59
+ setup(
60
+ name='json_database',
61
+ version=get_version(),
62
+ packages=['json_database'],
63
+ package_data={'': package_files('json_database')},
64
+ include_package_data=True,
65
+ url='https://github.com/TigreGotico/json_database',
66
+ license='MIT',
67
+ author='jarbasAI',
68
+ author_email='jarbasai@mailfence.com',
69
+ install_requires=required('requirements.txt'),
70
+ description='searchable json database with persistence',
71
+ long_description=get_description(),
72
+ long_description_content_type="text/markdown",
73
+ )
@@ -0,0 +1,53 @@
1
+ import os
2
+ import unittest
3
+
4
+ from json_database import EncryptedJsonStorage, JsonStorage # Replace with actual import
5
+
6
+
7
+ class TestEncryptedJsonStorage(unittest.TestCase):
8
+ def setUp(self):
9
+ self.key = "S" * 16 # Replace with actual key generation if needed
10
+ self.file_path = "/tmp/test.json"
11
+ # Ensure the test file doesn't exist at the start of each test
12
+ if os.path.exists(self.file_path):
13
+ os.remove(self.file_path)
14
+
15
+ def tearDown(self):
16
+ # Clean up the test file after each test
17
+ if os.path.exists(self.file_path):
18
+ os.remove(self.file_path)
19
+
20
+ def test_add_and_store_data(self):
21
+ db = EncryptedJsonStorage(self.key, self.file_path)
22
+ db["A"] = "42"
23
+ self.assertEqual(db["A"], "42") # Check in-memory data
24
+ db.store()
25
+ self.assertTrue(os.path.exists(self.file_path)) # File should be created
26
+
27
+ def test_encryption_in_file(self):
28
+ db = EncryptedJsonStorage(self.key, self.file_path)
29
+ db["A"] = "42"
30
+ db.store()
31
+ with open(self.file_path, "r") as file:
32
+ file_data = file.read()
33
+ self.assertNotIn("42", file_data) # Data should be encrypted
34
+
35
+ def test_decryption_after_reload(self):
36
+ db = EncryptedJsonStorage(self.key, self.file_path)
37
+ db["A"] = "42"
38
+ db.store()
39
+ db.reload()
40
+ self.assertEqual(db["A"], "42") # Data should be decrypted correctly
41
+
42
+ def test_jsonstorage_read_encrypted_data(self):
43
+ encrypted_db = EncryptedJsonStorage(self.key, self.file_path)
44
+ encrypted_db["A"] = "42"
45
+ encrypted_db.store()
46
+
47
+ db = JsonStorage(self.file_path)
48
+ self.assertIn("ciphertext", db) # Check that it's encrypted
49
+ self.assertNotIn("A", db)
50
+
51
+
52
+ if __name__ == "__main__":
53
+ unittest.main()
@@ -1,13 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: json_database
3
- Version: 0.6.0
4
- Summary: searchable json database with persistence
5
- Home-page: https://github.com/HelloChatterbox/json_database
6
- Author: jarbasAI
7
- Author-email: jarbasai@mailfence.com
8
- License: MIT
9
- Platform: UNKNOWN
10
- License-File: LICENSE
11
-
12
- UNKNOWN
13
-
@@ -1,108 +0,0 @@
1
- # Copyright 2018 Mycroft AI Inc.
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License");
4
- # you may not use this file except in compliance with the License.
5
- # You may obtain a copy of the License at
6
- #
7
- # http://www.apache.org/licenses/LICENSE-2.0
8
- #
9
- # Unless required by applicable law or agreed to in writing, software
10
- # distributed under the License is distributed on an "AS IS" BASIS,
11
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- # See the License for the specific language governing permissions and
13
- # limitations under the License.
14
- #
15
- from threading import Lock
16
- from fasteners.process_lock import InterProcessLock
17
- from os.path import exists
18
- from os import chmod
19
-
20
-
21
- class ComboLock:
22
- """ A combined process and thread lock.
23
-
24
- Arguments:
25
- path (str): path to the lockfile for the lock
26
- """
27
- def __init__(self, path):
28
- self.path = path
29
- self.plock = InterProcessLock(self.path)
30
- self.tlock = Lock()
31
-
32
- def acquire(self, blocking=True):
33
- """ Acquire lock, locks thread and process lock.
34
-
35
- Arguments:
36
- blocking(bool): Set's blocking mode of acquire operation.
37
- Default True.
38
-
39
- Returns: True if lock succeeded otherwise False
40
- """
41
- # Create lock file if it doesn't exist and set permissions for
42
- # all users to lock/unlock
43
- if not exists(self.path):
44
- f = open(self.path, 'w+')
45
- f.close()
46
- chmod(self.path, 0o777)
47
- if not blocking:
48
- # Lock thread
49
- tlocked = self.tlock.acquire(blocking=False)
50
- if not tlocked:
51
- return False
52
- # Lock process
53
- plocked = self.plock.acquire(blocking=False)
54
- if not plocked:
55
- # Release thread lock if process couldn't be locked
56
- self.tlock.release()
57
- return False
58
- else: # blocking, just wait and acquire ALL THE LOCKS!!!
59
- self.tlock.acquire()
60
- self.plock.acquire()
61
- return True
62
-
63
- def release(self):
64
- """ Release acquired lock. """
65
- self.plock.release()
66
- self.tlock.release()
67
-
68
- def __enter__(self):
69
- """ Context handler, acquires lock in blocking mode. """
70
- self.acquire()
71
- return self
72
-
73
- def __exit__(self, _type, value, traceback):
74
- """ Releases the lock. """
75
- self.release()
76
-
77
-
78
- class DummyLock:
79
- """ A combined process and thread lock.
80
-
81
- Arguments:
82
- path (str): path to the lockfile for the lock
83
- """
84
- def __init__(self, path):
85
- self.path = path
86
-
87
- def acquire(self, blocking=True):
88
- """ Acquire lock, locks thread and process lock.
89
-
90
- Arguments:
91
- blocking(bool): Set's blocking mode of acquire operation.
92
- Default True.
93
-
94
- Returns: True if lock succeeded otherwise False
95
- """
96
- return True
97
-
98
- def release(self):
99
- """ Release acquired lock. """
100
- pass
101
-
102
- def __enter__(self):
103
- """ Context handler, acquires lock in blocking mode. """
104
- return self
105
-
106
- def __exit__(self, _type, value, traceback):
107
- """ Releases the lock. """
108
- pass
@@ -1,13 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: json-database
3
- Version: 0.6.0
4
- Summary: searchable json database with persistence
5
- Home-page: https://github.com/HelloChatterbox/json_database
6
- Author: jarbasAI
7
- Author-email: jarbasai@mailfence.com
8
- License: MIT
9
- Platform: UNKNOWN
10
- License-File: LICENSE
11
-
12
- UNKNOWN
13
-
@@ -1,2 +0,0 @@
1
- pyxdg
2
- fasteners
@@ -1,13 +0,0 @@
1
- from setuptools import setup
2
-
3
- setup(
4
- name='json_database',
5
- version='0.6.0',
6
- packages=['json_database', 'json_database.utils'],
7
- url='https://github.com/HelloChatterbox/json_database',
8
- license='MIT',
9
- author='jarbasAI',
10
- author_email='jarbasai@mailfence.com',
11
- install_requires=["pyxdg", "fasteners"],
12
- description='searchable json database with persistence'
13
- )
File without changes
File without changes
File without changes