dictature 0.9.2__tar.gz → 0.9.3__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.2/src/dictature.egg-info → dictature-0.9.3}/PKG-INFO +1 -1
- {dictature-0.9.2 → dictature-0.9.3}/pyproject.toml +1 -1
- {dictature-0.9.2 → dictature-0.9.3}/src/dictature/backend/directory.py +2 -1
- {dictature-0.9.2 → dictature-0.9.3}/src/dictature/dictature.py +9 -8
- {dictature-0.9.2 → dictature-0.9.3}/src/dictature/transformer/__init__.py +1 -0
- {dictature-0.9.2 → dictature-0.9.3}/src/dictature/transformer/aes.py +5 -9
- dictature-0.9.3/src/dictature/transformer/hmac.py +24 -0
- dictature-0.9.3/src/dictature/transformer/pipeline.py +22 -0
- {dictature-0.9.2 → dictature-0.9.3/src/dictature.egg-info}/PKG-INFO +1 -1
- {dictature-0.9.2 → dictature-0.9.3}/src/dictature.egg-info/SOURCES.txt +4 -1
- dictature-0.9.3/tests/test_operations.py +126 -0
- {dictature-0.9.2 → dictature-0.9.3}/LICENSE +0 -0
- {dictature-0.9.2 → dictature-0.9.3}/README.md +0 -0
- {dictature-0.9.2 → dictature-0.9.3}/setup.cfg +0 -0
- {dictature-0.9.2 → dictature-0.9.3}/src/dictature/__init__.py +0 -0
- {dictature-0.9.2 → dictature-0.9.3}/src/dictature/backend/__init__.py +0 -0
- {dictature-0.9.2 → dictature-0.9.3}/src/dictature/backend/mock.py +0 -0
- {dictature-0.9.2 → dictature-0.9.3}/src/dictature/backend/sqlite.py +0 -0
- {dictature-0.9.2 → dictature-0.9.3}/src/dictature/transformer/mock.py +0 -0
- {dictature-0.9.2 → dictature-0.9.3}/src/dictature/transformer/passthrough.py +0 -0
- {dictature-0.9.2 → dictature-0.9.3}/src/dictature.egg-info/dependency_links.txt +0 -0
- {dictature-0.9.2 → dictature-0.9.3}/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.3
|
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
|
@@ -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.3"
|
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" }
|
@@ -71,7 +71,8 @@ class DictatureTableDirectory(DictatureTableMock):
|
|
71
71
|
def _filename_encode(name: str, suffix: str = '.txt') -> str:
|
72
72
|
if name == sub(r'[^\w_. -]', '_', name):
|
73
73
|
return f"d_{name}{suffix}"
|
74
|
-
|
74
|
+
name = name.encode('utf-8').hex()
|
75
|
+
return f'e_{name}{suffix}'
|
75
76
|
|
76
77
|
@staticmethod
|
77
78
|
def _filename_decode(name: str, suffix: str = '.txt') -> str:
|
@@ -15,11 +15,13 @@ class Dictature:
|
|
15
15
|
backend: DictatureBackendMock,
|
16
16
|
name_transformer: MockTransformer = PassthroughTransformer(),
|
17
17
|
value_transformer: MockTransformer = PassthroughTransformer(),
|
18
|
+
table_name_transformer: Optional[MockTransformer] = None,
|
18
19
|
) -> None:
|
19
20
|
self.__backend = backend
|
20
21
|
self.__table_cache: Dict[str, "DictatureTable"] = {}
|
21
22
|
self.__name_transformer = name_transformer
|
22
23
|
self.__value_transformer = value_transformer
|
24
|
+
self.__table_name_transformer = table_name_transformer or name_transformer
|
23
25
|
|
24
26
|
def keys(self) -> Set[str]:
|
25
27
|
return set(map(self.__name_transformer.backward, self.__backend.keys()))
|
@@ -109,13 +111,12 @@ class DictatureTable:
|
|
109
111
|
saved_value = self.__table.get(self.__item_key(item))
|
110
112
|
mode = ValueMode(saved_value.mode)
|
111
113
|
value = self.__value_transformer.backward(saved_value.value)
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
return pickle.loads(decompress(b64decode(value.encode('ascii'))))
|
114
|
+
if mode == ValueMode.string:
|
115
|
+
return value
|
116
|
+
elif mode == ValueMode.json:
|
117
|
+
return json.loads(value)
|
118
|
+
elif mode == ValueMode.pickle:
|
119
|
+
return pickle.loads(decompress(b64decode(value.encode('ascii'))))
|
119
120
|
raise ValueError(f"Unknown mode '{mode}'")
|
120
121
|
|
121
122
|
def __setitem__(self, key: str, value: Any) -> None:
|
@@ -135,7 +136,7 @@ class DictatureTable:
|
|
135
136
|
self.__table.set(key, Value(value=value, mode=value_mode.value))
|
136
137
|
|
137
138
|
def __delitem__(self, key: str) -> None:
|
138
|
-
self.__table.delete(self.
|
139
|
+
self.__table.delete(self.__item_key(key))
|
139
140
|
|
140
141
|
def __contains__(self, item: str):
|
141
142
|
return item in self.keys()
|
@@ -15,7 +15,7 @@ class AESTransformer(MockTransformer):
|
|
15
15
|
self.__static = static_names_mode
|
16
16
|
|
17
17
|
def forward(self, text: str) -> str:
|
18
|
-
cipher = self.__cipher
|
18
|
+
cipher = self.__cipher()
|
19
19
|
if self.__mode == AES.MODE_GCM:
|
20
20
|
ciphertext, tag = cipher.encrypt_and_digest(pad(text.encode('utf8'), AES.block_size))
|
21
21
|
return (cipher.nonce + tag + ciphertext).hex()
|
@@ -24,19 +24,15 @@ class AESTransformer(MockTransformer):
|
|
24
24
|
|
25
25
|
def backward(self, text: str) -> str:
|
26
26
|
data = bytes.fromhex(text)
|
27
|
-
cipher = self.__cipher
|
28
27
|
if self.__mode == AES.MODE_GCM:
|
29
28
|
nonce, tag, ciphertext = data[:16], data[16:32], data[32:]
|
30
|
-
|
31
|
-
cipher = AES.new(self.__key, self.__mode, nonce=nonce)
|
32
|
-
return unpad(cipher.decrypt_and_verify(ciphertext, tag), AES.block_size).decode('utf8')
|
29
|
+
return unpad(self.__cipher(nonce=nonce).decrypt_and_verify(ciphertext, tag), AES.block_size).decode('utf8')
|
33
30
|
else:
|
34
|
-
return unpad(
|
31
|
+
return unpad(self.__cipher().decrypt(data), AES.block_size).decode('utf8')
|
35
32
|
|
36
|
-
|
37
|
-
def __cipher(self) -> AES:
|
33
|
+
def __cipher(self, **kwargs) -> AES:
|
38
34
|
# noinspection PyTypeChecker
|
39
|
-
return AES.new(self.__key, self.__mode)
|
35
|
+
return AES.new(self.__key, self.__mode, **kwargs)
|
40
36
|
|
41
37
|
@property
|
42
38
|
def static(self) -> bool:
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import hmac
|
2
|
+
from hashlib import sha256
|
3
|
+
from .mock import MockTransformer
|
4
|
+
|
5
|
+
|
6
|
+
class HmacTransformer(MockTransformer):
|
7
|
+
def __init__(self, secret: str = 'dictature') -> None:
|
8
|
+
self.__secret = secret
|
9
|
+
|
10
|
+
def forward(self, text: str) -> str:
|
11
|
+
return f"{self.__hmac(text)}-{text}"
|
12
|
+
|
13
|
+
def backward(self, text: str) -> str:
|
14
|
+
mac, text = text.split('-', 1)
|
15
|
+
if mac != self.__hmac(text):
|
16
|
+
raise ValueError('Invalid HMAC')
|
17
|
+
return text
|
18
|
+
|
19
|
+
def __hmac(self, text: str) -> str:
|
20
|
+
return hmac.new(self.__secret.encode('utf8'), text.encode('utf8'), sha256).hexdigest()
|
21
|
+
|
22
|
+
@property
|
23
|
+
def static(self) -> bool:
|
24
|
+
return True
|
@@ -0,0 +1,22 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
3
|
+
from .mock import MockTransformer
|
4
|
+
|
5
|
+
|
6
|
+
class PipelineTransformer(MockTransformer):
|
7
|
+
def __init__(self, transformers: List[MockTransformer]) -> None:
|
8
|
+
self.__transformers = transformers
|
9
|
+
|
10
|
+
def forward(self, text: str) -> str:
|
11
|
+
for transformer in self.__transformers:
|
12
|
+
text = transformer.forward(text)
|
13
|
+
return text
|
14
|
+
|
15
|
+
def backward(self, text: str) -> str:
|
16
|
+
for transformer in reversed(self.__transformers):
|
17
|
+
text = transformer.backward(text)
|
18
|
+
return text
|
19
|
+
|
20
|
+
@property
|
21
|
+
def static(self) -> bool:
|
22
|
+
return all(t.static for t in self.__transformers)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: dictature
|
3
|
-
Version: 0.9.
|
3
|
+
Version: 0.9.3
|
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
|
@@ -13,5 +13,8 @@ src/dictature/backend/mock.py
|
|
13
13
|
src/dictature/backend/sqlite.py
|
14
14
|
src/dictature/transformer/__init__.py
|
15
15
|
src/dictature/transformer/aes.py
|
16
|
+
src/dictature/transformer/hmac.py
|
16
17
|
src/dictature/transformer/mock.py
|
17
|
-
src/dictature/transformer/passthrough.py
|
18
|
+
src/dictature/transformer/passthrough.py
|
19
|
+
src/dictature/transformer/pipeline.py
|
20
|
+
tests/test_operations.py
|
@@ -0,0 +1,126 @@
|
|
1
|
+
import unittest
|
2
|
+
from itertools import product
|
3
|
+
from typing import NamedTuple, Optional
|
4
|
+
from tempfile import mkdtemp, mktemp
|
5
|
+
|
6
|
+
from parameterized import parameterized
|
7
|
+
|
8
|
+
from src.dictature import Dictature
|
9
|
+
from src.dictature.backend.mock import DictatureBackendMock
|
10
|
+
from src.dictature.backend.sqlite import DictatureBackendSQLite
|
11
|
+
from src.dictature.backend.directory import DictatureBackendDirectory
|
12
|
+
from src.dictature.transformer import PassthroughTransformer, PipelineTransformer
|
13
|
+
from src.dictature.transformer.mock import MockTransformer
|
14
|
+
from src.dictature.transformer.aes import AESTransformer
|
15
|
+
from src.dictature.transformer.hmac import HmacTransformer
|
16
|
+
|
17
|
+
|
18
|
+
BACKENDS = [
|
19
|
+
DictatureBackendDirectory(mkdtemp(prefix='dictature')),
|
20
|
+
DictatureBackendSQLite(mktemp(prefix='dictature', suffix='.sqlite3')),
|
21
|
+
]
|
22
|
+
|
23
|
+
TRANSFORMERS = [
|
24
|
+
PassthroughTransformer(),
|
25
|
+
AESTransformer('password', False),
|
26
|
+
AESTransformer('password', True),
|
27
|
+
HmacTransformer(),
|
28
|
+
HmacTransformer('password'),
|
29
|
+
PipelineTransformer([HmacTransformer(), AESTransformer('password', False)]),
|
30
|
+
]
|
31
|
+
|
32
|
+
|
33
|
+
class Settings(NamedTuple):
|
34
|
+
backend: DictatureBackendMock
|
35
|
+
name_transformer: MockTransformer
|
36
|
+
value_transformer: MockTransformer
|
37
|
+
table_name_transformer: Optional[MockTransformer]
|
38
|
+
|
39
|
+
|
40
|
+
SETTINGS = [
|
41
|
+
(Settings(backend, name_transformer, value_transformer, table_name_transformer),)
|
42
|
+
for backend, name_transformer, value_transformer, table_name_transformer in product(BACKENDS, TRANSFORMERS, TRANSFORMERS, [*TRANSFORMERS, None])
|
43
|
+
]
|
44
|
+
|
45
|
+
|
46
|
+
class TestOperations(unittest.TestCase):
|
47
|
+
def setUp(self):
|
48
|
+
self.backend = None
|
49
|
+
|
50
|
+
def tearDown(self):
|
51
|
+
if self.backend:
|
52
|
+
for table in self.backend.keys():
|
53
|
+
del self.backend[table]
|
54
|
+
|
55
|
+
@parameterized.expand(SETTINGS)
|
56
|
+
def test_basic_set_and_get(self, settings: Settings):
|
57
|
+
self.backend = Dictature(
|
58
|
+
backend=settings.backend,
|
59
|
+
name_transformer=settings.name_transformer,
|
60
|
+
value_transformer=settings.value_transformer,
|
61
|
+
table_name_transformer=settings.table_name_transformer
|
62
|
+
)
|
63
|
+
table = self.backend['table']
|
64
|
+
table['key'] = 'value'
|
65
|
+
table['key2'] = 'value2'
|
66
|
+
table['key'] = 'value3'
|
67
|
+
self.backend['table2']['key'] = 'value'
|
68
|
+
self.assertEqual(table['key'], 'value3')
|
69
|
+
self.assertEqual(table.keys(), {'key', 'key2'})
|
70
|
+
self.assertEqual(self.backend.keys(), {'table', 'table2'})
|
71
|
+
|
72
|
+
@parameterized.expand(SETTINGS)
|
73
|
+
def test_saving_json_value(self, settings: Settings):
|
74
|
+
self.backend = Dictature(
|
75
|
+
backend=settings.backend,
|
76
|
+
name_transformer=settings.name_transformer,
|
77
|
+
value_transformer=settings.value_transformer,
|
78
|
+
table_name_transformer=settings.table_name_transformer
|
79
|
+
)
|
80
|
+
value = {'key': 'value'}
|
81
|
+
self.backend['table']['key'] = value
|
82
|
+
self.assertDictEqual(self.backend['table']['key'], value)
|
83
|
+
self.backend['table']['key'] = 2
|
84
|
+
self.assertEqual(self.backend['table']['key'], 2)
|
85
|
+
|
86
|
+
@parameterized.expand(SETTINGS)
|
87
|
+
def test_saving_pickle_value(self, settings: Settings):
|
88
|
+
self.backend = Dictature(
|
89
|
+
backend=settings.backend,
|
90
|
+
name_transformer=settings.name_transformer,
|
91
|
+
value_transformer=settings.value_transformer,
|
92
|
+
table_name_transformer=settings.table_name_transformer
|
93
|
+
)
|
94
|
+
self.backend['table']['key'] = NamedTuple
|
95
|
+
self.assertEqual(self.backend['table']['key'], NamedTuple)
|
96
|
+
|
97
|
+
@parameterized.expand(SETTINGS)
|
98
|
+
def test_deletion_of_table_key(self, settings: Settings):
|
99
|
+
self.backend = Dictature(
|
100
|
+
backend=settings.backend,
|
101
|
+
name_transformer=settings.name_transformer,
|
102
|
+
value_transformer=settings.value_transformer,
|
103
|
+
table_name_transformer=settings.table_name_transformer
|
104
|
+
)
|
105
|
+
table = self.backend['table']
|
106
|
+
table['key'] = 'value'
|
107
|
+
table['key2'] = 'value2'
|
108
|
+
del table['key']
|
109
|
+
self.assertEqual({'key2'}, table.keys())
|
110
|
+
|
111
|
+
@parameterized.expand(SETTINGS)
|
112
|
+
def test_deletion_of_whole_table(self, settings: Settings):
|
113
|
+
self.backend = Dictature(
|
114
|
+
backend=settings.backend,
|
115
|
+
name_transformer=settings.name_transformer,
|
116
|
+
value_transformer=settings.value_transformer,
|
117
|
+
table_name_transformer=settings.table_name_transformer
|
118
|
+
)
|
119
|
+
self.backend['table2']['key'] = 'value'
|
120
|
+
self.backend['table']['key'] = 'value'
|
121
|
+
del self.backend['table']
|
122
|
+
self.assertEqual(self.backend.keys(), {'table2'})
|
123
|
+
|
124
|
+
|
125
|
+
if __name__ == '__main__':
|
126
|
+
unittest.main()
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|