dictature 0.9.0__tar.gz → 0.9.2__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.
- {dictature-0.9.0/src/dictature.egg-info → dictature-0.9.2}/PKG-INFO +27 -3
- {dictature-0.9.0 → dictature-0.9.2}/README.md +26 -2
- {dictature-0.9.0 → dictature-0.9.2}/pyproject.toml +1 -1
- {dictature-0.9.0 → dictature-0.9.2}/src/dictature/backend/directory.py +10 -8
- {dictature-0.9.0 → dictature-0.9.2}/src/dictature/backend/sqlite.py +1 -1
- {dictature-0.9.0 → dictature-0.9.2}/src/dictature/dictature.py +58 -20
- dictature-0.9.2/src/dictature/transformer/__init__.py +2 -0
- dictature-0.9.2/src/dictature/transformer/aes.py +43 -0
- dictature-0.9.2/src/dictature/transformer/mock.py +9 -0
- dictature-0.9.2/src/dictature/transformer/passthrough.py +10 -0
- {dictature-0.9.0 → dictature-0.9.2/src/dictature.egg-info}/PKG-INFO +27 -3
- {dictature-0.9.0 → dictature-0.9.2}/src/dictature.egg-info/SOURCES.txt +5 -1
- {dictature-0.9.0 → dictature-0.9.2}/LICENSE +0 -0
- {dictature-0.9.0 → dictature-0.9.2}/setup.cfg +0 -0
- {dictature-0.9.0 → dictature-0.9.2}/src/dictature/__init__.py +0 -0
- {dictature-0.9.0 → dictature-0.9.2}/src/dictature/backend/__init__.py +0 -0
- {dictature-0.9.0 → dictature-0.9.2}/src/dictature/backend/mock.py +0 -0
- {dictature-0.9.0 → dictature-0.9.2}/src/dictature.egg-info/dependency_links.txt +0 -0
- {dictature-0.9.0 → dictature-0.9.2}/src/dictature.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: dictature
|
3
|
-
Version: 0.9.
|
3
|
+
Version: 0.9.2
|
4
4
|
Summary: dictature -- A generic wrapper around dict-like interface with mulitple backends
|
5
5
|
Author-email: Adam Hlavacek <git@adamhlavacek.com>
|
6
6
|
Project-URL: Homepage, https://github.com/esoadamo/dictature
|
@@ -11,7 +11,7 @@ Classifier: Operating System :: OS Independent
|
|
11
11
|
Description-Content-Type: text/markdown
|
12
12
|
License-File: LICENSE
|
13
13
|
|
14
|
-
#
|
14
|
+
# Dictature
|
15
15
|
|
16
16
|
A wrapper for Python's dictionary with multiple backends.
|
17
17
|
|
@@ -22,7 +22,7 @@ pip install dictature
|
|
22
22
|
```
|
23
23
|
|
24
24
|
## Dictature usage
|
25
|
-
This package also includes a class that allows you to use your SQLite db as a Python dictionary:
|
25
|
+
This package also includes a class that allows you to use your SQLite db or any backend as a Python dictionary:
|
26
26
|
|
27
27
|
```python
|
28
28
|
from dictature import Dictature
|
@@ -49,3 +49,27 @@ print(dictionary['test']['thread']) # prints <class 'threading.Thread'>
|
|
49
49
|
del dictionary['test']['list'] # deletes the record
|
50
50
|
del dictionary['test'] # drops whole table
|
51
51
|
```
|
52
|
+
|
53
|
+
Currently, the following backends are supported:
|
54
|
+
- `DictatureBackendDirectory`: stores the data in a directory as json files
|
55
|
+
- `DictatureBackendSQLite`: stores the data in a SQLite database
|
56
|
+
|
57
|
+
### Transformers
|
58
|
+
|
59
|
+
You can also use transformers to change how the values are stored. E.g. to encrypt data, you can use the
|
60
|
+
`AESTransformer` (which requires the `pycryptodome` package):
|
61
|
+
|
62
|
+
```python
|
63
|
+
from dictature import Dictature
|
64
|
+
from dictature.backend import DictatureBackendDirectory
|
65
|
+
from dictature.transformer.aes import AESTransformer
|
66
|
+
|
67
|
+
name_transformer = AESTransformer('password1', True)
|
68
|
+
value_transformer = AESTransformer('password2', False)
|
69
|
+
|
70
|
+
dictionary = Dictature(
|
71
|
+
DictatureBackendSQLite('test_data.sqlite3'),
|
72
|
+
name_transformer=name_transformer,
|
73
|
+
value_transformer=value_transformer
|
74
|
+
)
|
75
|
+
```
|
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# Dictature
|
2
2
|
|
3
3
|
A wrapper for Python's dictionary with multiple backends.
|
4
4
|
|
@@ -9,7 +9,7 @@ pip install dictature
|
|
9
9
|
```
|
10
10
|
|
11
11
|
## Dictature usage
|
12
|
-
This package also includes a class that allows you to use your SQLite db as a Python dictionary:
|
12
|
+
This package also includes a class that allows you to use your SQLite db or any backend as a Python dictionary:
|
13
13
|
|
14
14
|
```python
|
15
15
|
from dictature import Dictature
|
@@ -36,3 +36,27 @@ print(dictionary['test']['thread']) # prints <class 'threading.Thread'>
|
|
36
36
|
del dictionary['test']['list'] # deletes the record
|
37
37
|
del dictionary['test'] # drops whole table
|
38
38
|
```
|
39
|
+
|
40
|
+
Currently, the following backends are supported:
|
41
|
+
- `DictatureBackendDirectory`: stores the data in a directory as json files
|
42
|
+
- `DictatureBackendSQLite`: stores the data in a SQLite database
|
43
|
+
|
44
|
+
### Transformers
|
45
|
+
|
46
|
+
You can also use transformers to change how the values are stored. E.g. to encrypt data, you can use the
|
47
|
+
`AESTransformer` (which requires the `pycryptodome` package):
|
48
|
+
|
49
|
+
```python
|
50
|
+
from dictature import Dictature
|
51
|
+
from dictature.backend import DictatureBackendDirectory
|
52
|
+
from dictature.transformer.aes import AESTransformer
|
53
|
+
|
54
|
+
name_transformer = AESTransformer('password1', True)
|
55
|
+
value_transformer = AESTransformer('password2', False)
|
56
|
+
|
57
|
+
dictionary = Dictature(
|
58
|
+
DictatureBackendSQLite('test_data.sqlite3'),
|
59
|
+
name_transformer=name_transformer,
|
60
|
+
value_transformer=value_transformer
|
61
|
+
)
|
62
|
+
```
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "dictature"
|
7
|
-
version = "0.9.
|
7
|
+
version = "0.9.2"
|
8
8
|
description = "dictature -- A generic wrapper around dict-like interface with mulitple backends"
|
9
9
|
authors = [
|
10
10
|
{ name = "Adam Hlavacek", email = "git@adamhlavacek.com" }
|
@@ -17,7 +17,8 @@ class DictatureBackendDirectory(DictatureBackendMock):
|
|
17
17
|
def keys(self) -> Iterable[str]:
|
18
18
|
for child in self.__directory.iterdir():
|
19
19
|
if child.is_dir() and child.name.startswith(self.__dir_prefix):
|
20
|
-
|
20
|
+
# noinspection PyProtectedMember
|
21
|
+
yield DictatureTableDirectory._filename_decode(child.name[len(self.__dir_prefix):], suffix='')
|
21
22
|
|
22
23
|
def table(self, name: str) -> 'DictatureTableMock':
|
23
24
|
return DictatureTableDirectory(self.__directory, name, self.__dir_prefix)
|
@@ -25,13 +26,13 @@ class DictatureBackendDirectory(DictatureBackendMock):
|
|
25
26
|
|
26
27
|
class DictatureTableDirectory(DictatureTableMock):
|
27
28
|
def __init__(self, path_root: Path, name: str, db_prefix: str, prefix: str = 'item_') -> None:
|
28
|
-
self.__path = path_root / (db_prefix + self.
|
29
|
+
self.__path = path_root / (db_prefix + self._filename_encode(name, suffix=''))
|
29
30
|
self.__prefix = prefix
|
30
31
|
|
31
32
|
def keys(self) -> Iterable[str]:
|
32
33
|
for child in self.__path.iterdir():
|
33
34
|
if child.is_file() and child.name.startswith(self.__prefix) and not child.name.endswith('.tmp'):
|
34
|
-
yield self.
|
35
|
+
yield self._filename_decode(child.name[len(self.__prefix):])
|
35
36
|
|
36
37
|
def drop(self) -> None:
|
37
38
|
rmtree(self.__path)
|
@@ -64,16 +65,17 @@ class DictatureTableDirectory(DictatureTableMock):
|
|
64
65
|
self.__item_path(item).unlink()
|
65
66
|
|
66
67
|
def __item_path(self, item: str) -> Path:
|
67
|
-
return self.__path / (self.__prefix + self.
|
68
|
+
return self.__path / (self.__prefix + self._filename_encode(item))
|
68
69
|
|
69
70
|
@staticmethod
|
70
|
-
def
|
71
|
+
def _filename_encode(name: str, suffix: str = '.txt') -> str:
|
71
72
|
if name == sub(r'[^\w_. -]', '_', name):
|
72
73
|
return f"d_{name}{suffix}"
|
73
74
|
return f'e_{name.encode('utf-8').hex()}{suffix}'
|
74
75
|
|
75
76
|
@staticmethod
|
76
|
-
def
|
77
|
+
def _filename_decode(name: str, suffix: str = '.txt') -> str:
|
78
|
+
encoded_name = name[2:-len(suffix) if suffix else len(name)]
|
77
79
|
if name.startswith('d_'):
|
78
|
-
return
|
79
|
-
return bytes.fromhex(
|
80
|
+
return encoded_name
|
81
|
+
return bytes.fromhex(encoded_name).decode('utf-8')
|
@@ -17,7 +17,7 @@ class DictatureBackendSQLite(DictatureBackendMock):
|
|
17
17
|
self.__cursor = self.__connection.cursor()
|
18
18
|
|
19
19
|
def keys(self) -> Iterable[str]:
|
20
|
-
tables = self._execute("SELECT tbl_name FROM sqlite_master WHERE type='table' AND tbl_name LIKE '
|
20
|
+
tables = self._execute("SELECT tbl_name FROM sqlite_master WHERE type='table' AND tbl_name LIKE 'tb_%'")
|
21
21
|
return {table[0][3:] for table in tables}
|
22
22
|
|
23
23
|
def table(self, name: str) -> 'DictatureTableMock':
|
@@ -6,15 +6,23 @@ from random import choice
|
|
6
6
|
from typing import Optional, Dict, Any, Set, Iterator, Tuple
|
7
7
|
|
8
8
|
from .backend import DictatureBackendMock, ValueMode, Value
|
9
|
+
from .transformer import MockTransformer, PassthroughTransformer
|
9
10
|
|
10
11
|
|
11
12
|
class Dictature:
|
12
|
-
def __init__(
|
13
|
-
|
14
|
-
|
13
|
+
def __init__(
|
14
|
+
self,
|
15
|
+
backend: DictatureBackendMock,
|
16
|
+
name_transformer: MockTransformer = PassthroughTransformer(),
|
17
|
+
value_transformer: MockTransformer = PassthroughTransformer(),
|
18
|
+
) -> None:
|
19
|
+
self.__backend = backend
|
20
|
+
self.__table_cache: Dict[str, "DictatureTable"] = {}
|
21
|
+
self.__name_transformer = name_transformer
|
22
|
+
self.__value_transformer = value_transformer
|
15
23
|
|
16
24
|
def keys(self) -> Set[str]:
|
17
|
-
return set(self.
|
25
|
+
return set(map(self.__name_transformer.backward, self.__backend.keys()))
|
18
26
|
|
19
27
|
def values(self) -> Iterator["DictatureTable"]:
|
20
28
|
return map(lambda x: x[1], self.items())
|
@@ -30,11 +38,16 @@ class Dictature:
|
|
30
38
|
return str(self.to_dict())
|
31
39
|
|
32
40
|
def __getitem__(self, item: str) -> "DictatureTable":
|
33
|
-
if len(self.
|
34
|
-
del self.
|
35
|
-
if item not in self.
|
36
|
-
self.
|
37
|
-
|
41
|
+
if len(self.__table_cache) > 128:
|
42
|
+
del self.__table_cache[choice(list(self.__table_cache.keys()))]
|
43
|
+
if item not in self.__table_cache:
|
44
|
+
self.__table_cache[item] = DictatureTable(
|
45
|
+
self.__backend,
|
46
|
+
item,
|
47
|
+
name_transformer=self.__name_transformer,
|
48
|
+
value_transformer=self.__value_transformer
|
49
|
+
)
|
50
|
+
return self.__table_cache[item]
|
38
51
|
|
39
52
|
def __delitem__(self, key: str) -> None:
|
40
53
|
self[key].drop()
|
@@ -47,9 +60,17 @@ class Dictature:
|
|
47
60
|
|
48
61
|
|
49
62
|
class DictatureTable:
|
50
|
-
def __init__(
|
51
|
-
|
52
|
-
|
63
|
+
def __init__(
|
64
|
+
self,
|
65
|
+
backend: DictatureBackendMock,
|
66
|
+
table_name: str,
|
67
|
+
name_transformer: MockTransformer = PassthroughTransformer(),
|
68
|
+
value_transformer: MockTransformer = PassthroughTransformer()
|
69
|
+
):
|
70
|
+
self.__backend = backend
|
71
|
+
self.__name_transformer = name_transformer
|
72
|
+
self.__value_transformer = value_transformer
|
73
|
+
self.__table = self.__backend.table(self.__table_key(table_name))
|
53
74
|
self.__table_created = False
|
54
75
|
|
55
76
|
def get(self, item: str, default: Optional[Any] = None) -> Any:
|
@@ -64,7 +85,7 @@ class DictatureTable:
|
|
64
85
|
|
65
86
|
def keys(self) -> Set[str]:
|
66
87
|
self.__create_table()
|
67
|
-
return set(self.__table.keys())
|
88
|
+
return set(map(self.__name_transformer.backward, self.__table.keys()))
|
68
89
|
|
69
90
|
def values(self) -> Iterator[Any]:
|
70
91
|
return map(lambda x: x[1], self.items())
|
@@ -85,16 +106,17 @@ class DictatureTable:
|
|
85
106
|
|
86
107
|
def __getitem__(self, item: str) -> Any:
|
87
108
|
self.__create_table()
|
88
|
-
|
89
|
-
mode = ValueMode(
|
109
|
+
saved_value = self.__table.get(self.__item_key(item))
|
110
|
+
mode = ValueMode(saved_value.mode)
|
111
|
+
value = self.__value_transformer.backward(saved_value.value)
|
90
112
|
match mode:
|
91
113
|
case ValueMode.string:
|
92
|
-
return value
|
114
|
+
return value
|
93
115
|
case ValueMode.json:
|
94
|
-
return json.loads(value
|
116
|
+
return json.loads(value)
|
95
117
|
case ValueMode.pickle:
|
96
|
-
return pickle.loads(decompress(b64decode(value.
|
97
|
-
raise ValueError(f"Unknown mode '{
|
118
|
+
return pickle.loads(decompress(b64decode(value.encode('ascii'))))
|
119
|
+
raise ValueError(f"Unknown mode '{mode}'")
|
98
120
|
|
99
121
|
def __setitem__(self, key: str, value: Any) -> None:
|
100
122
|
self.__create_table()
|
@@ -108,10 +130,12 @@ class DictatureTable:
|
|
108
130
|
value = b64encode(compress(pickle.dumps(value))).decode('ascii')
|
109
131
|
value_mode = value_mode.pickle
|
110
132
|
|
133
|
+
key = self.__item_key(key)
|
134
|
+
value = self.__value_transformer.forward(value)
|
111
135
|
self.__table.set(key, Value(value=value, mode=value_mode.value))
|
112
136
|
|
113
137
|
def __delitem__(self, key: str) -> None:
|
114
|
-
self.__table.delete(key)
|
138
|
+
self.__table.delete(self.__name_transformer.forward(key))
|
115
139
|
|
116
140
|
def __contains__(self, item: str):
|
117
141
|
return item in self.keys()
|
@@ -124,3 +148,17 @@ class DictatureTable:
|
|
124
148
|
return
|
125
149
|
self.__table.create()
|
126
150
|
self.__table_created = True
|
151
|
+
|
152
|
+
def __item_key(self, item: str) -> str:
|
153
|
+
if not self.__name_transformer.static:
|
154
|
+
for key in self.__table.keys():
|
155
|
+
if self.__name_transformer.backward(key) == item:
|
156
|
+
return key
|
157
|
+
return self.__name_transformer.forward(item)
|
158
|
+
|
159
|
+
def __table_key(self, table_name: str) -> str:
|
160
|
+
if not self.__name_transformer.static:
|
161
|
+
for key in self.__backend.keys():
|
162
|
+
if self.__name_transformer.backward(key) == table_name:
|
163
|
+
return key
|
164
|
+
return self.__name_transformer.forward(table_name)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
try:
|
2
|
+
from Crypto.Cipher import AES
|
3
|
+
from Crypto.Util.Padding import pad, unpad
|
4
|
+
from Crypto.Protocol.KDF import scrypt
|
5
|
+
except ImportError:
|
6
|
+
raise ImportError("PyCryptodome is required to use this module -- pip install pycryptodome")
|
7
|
+
|
8
|
+
from .mock import MockTransformer
|
9
|
+
|
10
|
+
|
11
|
+
class AESTransformer(MockTransformer):
|
12
|
+
def __init__(self, passphrase: str, static_names_mode: bool, salt: str = 'dictature') -> None:
|
13
|
+
self.__key = scrypt(passphrase, salt, 16, N=2 ** 14, r=8, p=1)
|
14
|
+
self.__mode = AES.MODE_GCM if not static_names_mode else AES.MODE_ECB
|
15
|
+
self.__static = static_names_mode
|
16
|
+
|
17
|
+
def forward(self, text: str) -> str:
|
18
|
+
cipher = self.__cipher
|
19
|
+
if self.__mode == AES.MODE_GCM:
|
20
|
+
ciphertext, tag = cipher.encrypt_and_digest(pad(text.encode('utf8'), AES.block_size))
|
21
|
+
return (cipher.nonce + tag + ciphertext).hex()
|
22
|
+
else:
|
23
|
+
return cipher.encrypt(pad(text.encode('utf8'), AES.block_size)).hex()
|
24
|
+
|
25
|
+
def backward(self, text: str) -> str:
|
26
|
+
data = bytes.fromhex(text)
|
27
|
+
cipher = self.__cipher
|
28
|
+
if self.__mode == AES.MODE_GCM:
|
29
|
+
nonce, tag, ciphertext = data[:16], data[16:32], data[32:]
|
30
|
+
# noinspection PyTypeChecker
|
31
|
+
cipher = AES.new(self.__key, self.__mode, nonce=nonce)
|
32
|
+
return unpad(cipher.decrypt_and_verify(ciphertext, tag), AES.block_size).decode('utf8')
|
33
|
+
else:
|
34
|
+
return unpad(cipher.decrypt(data), AES.block_size).decode('utf8')
|
35
|
+
|
36
|
+
@property
|
37
|
+
def __cipher(self) -> AES:
|
38
|
+
# noinspection PyTypeChecker
|
39
|
+
return AES.new(self.__key, self.__mode)
|
40
|
+
|
41
|
+
@property
|
42
|
+
def static(self) -> bool:
|
43
|
+
return self.__static
|
@@ -0,0 +1,9 @@
|
|
1
|
+
|
2
|
+
class MockTransformer:
|
3
|
+
def forward(self, text: str) -> str:
|
4
|
+
raise NotImplementedError("This method should be implemented by the child class")
|
5
|
+
def backward(self, text: str) -> str:
|
6
|
+
raise NotImplementedError("This method should be implemented by the child class")
|
7
|
+
@property
|
8
|
+
def static(self) -> bool:
|
9
|
+
raise NotImplementedError("This method should be implemented by the child class")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: dictature
|
3
|
-
Version: 0.9.
|
3
|
+
Version: 0.9.2
|
4
4
|
Summary: dictature -- A generic wrapper around dict-like interface with mulitple backends
|
5
5
|
Author-email: Adam Hlavacek <git@adamhlavacek.com>
|
6
6
|
Project-URL: Homepage, https://github.com/esoadamo/dictature
|
@@ -11,7 +11,7 @@ Classifier: Operating System :: OS Independent
|
|
11
11
|
Description-Content-Type: text/markdown
|
12
12
|
License-File: LICENSE
|
13
13
|
|
14
|
-
#
|
14
|
+
# Dictature
|
15
15
|
|
16
16
|
A wrapper for Python's dictionary with multiple backends.
|
17
17
|
|
@@ -22,7 +22,7 @@ pip install dictature
|
|
22
22
|
```
|
23
23
|
|
24
24
|
## Dictature usage
|
25
|
-
This package also includes a class that allows you to use your SQLite db as a Python dictionary:
|
25
|
+
This package also includes a class that allows you to use your SQLite db or any backend as a Python dictionary:
|
26
26
|
|
27
27
|
```python
|
28
28
|
from dictature import Dictature
|
@@ -49,3 +49,27 @@ print(dictionary['test']['thread']) # prints <class 'threading.Thread'>
|
|
49
49
|
del dictionary['test']['list'] # deletes the record
|
50
50
|
del dictionary['test'] # drops whole table
|
51
51
|
```
|
52
|
+
|
53
|
+
Currently, the following backends are supported:
|
54
|
+
- `DictatureBackendDirectory`: stores the data in a directory as json files
|
55
|
+
- `DictatureBackendSQLite`: stores the data in a SQLite database
|
56
|
+
|
57
|
+
### Transformers
|
58
|
+
|
59
|
+
You can also use transformers to change how the values are stored. E.g. to encrypt data, you can use the
|
60
|
+
`AESTransformer` (which requires the `pycryptodome` package):
|
61
|
+
|
62
|
+
```python
|
63
|
+
from dictature import Dictature
|
64
|
+
from dictature.backend import DictatureBackendDirectory
|
65
|
+
from dictature.transformer.aes import AESTransformer
|
66
|
+
|
67
|
+
name_transformer = AESTransformer('password1', True)
|
68
|
+
value_transformer = AESTransformer('password2', False)
|
69
|
+
|
70
|
+
dictionary = Dictature(
|
71
|
+
DictatureBackendSQLite('test_data.sqlite3'),
|
72
|
+
name_transformer=name_transformer,
|
73
|
+
value_transformer=value_transformer
|
74
|
+
)
|
75
|
+
```
|
@@ -10,4 +10,8 @@ src/dictature.egg-info/top_level.txt
|
|
10
10
|
src/dictature/backend/__init__.py
|
11
11
|
src/dictature/backend/directory.py
|
12
12
|
src/dictature/backend/mock.py
|
13
|
-
src/dictature/backend/sqlite.py
|
13
|
+
src/dictature/backend/sqlite.py
|
14
|
+
src/dictature/transformer/__init__.py
|
15
|
+
src/dictature/transformer/aes.py
|
16
|
+
src/dictature/transformer/mock.py
|
17
|
+
src/dictature/transformer/passthrough.py
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|