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.
Files changed (22) hide show
  1. {dictature-0.9.2/src/dictature.egg-info → dictature-0.9.3}/PKG-INFO +1 -1
  2. {dictature-0.9.2 → dictature-0.9.3}/pyproject.toml +1 -1
  3. {dictature-0.9.2 → dictature-0.9.3}/src/dictature/backend/directory.py +2 -1
  4. {dictature-0.9.2 → dictature-0.9.3}/src/dictature/dictature.py +9 -8
  5. {dictature-0.9.2 → dictature-0.9.3}/src/dictature/transformer/__init__.py +1 -0
  6. {dictature-0.9.2 → dictature-0.9.3}/src/dictature/transformer/aes.py +5 -9
  7. dictature-0.9.3/src/dictature/transformer/hmac.py +24 -0
  8. dictature-0.9.3/src/dictature/transformer/pipeline.py +22 -0
  9. {dictature-0.9.2 → dictature-0.9.3/src/dictature.egg-info}/PKG-INFO +1 -1
  10. {dictature-0.9.2 → dictature-0.9.3}/src/dictature.egg-info/SOURCES.txt +4 -1
  11. dictature-0.9.3/tests/test_operations.py +126 -0
  12. {dictature-0.9.2 → dictature-0.9.3}/LICENSE +0 -0
  13. {dictature-0.9.2 → dictature-0.9.3}/README.md +0 -0
  14. {dictature-0.9.2 → dictature-0.9.3}/setup.cfg +0 -0
  15. {dictature-0.9.2 → dictature-0.9.3}/src/dictature/__init__.py +0 -0
  16. {dictature-0.9.2 → dictature-0.9.3}/src/dictature/backend/__init__.py +0 -0
  17. {dictature-0.9.2 → dictature-0.9.3}/src/dictature/backend/mock.py +0 -0
  18. {dictature-0.9.2 → dictature-0.9.3}/src/dictature/backend/sqlite.py +0 -0
  19. {dictature-0.9.2 → dictature-0.9.3}/src/dictature/transformer/mock.py +0 -0
  20. {dictature-0.9.2 → dictature-0.9.3}/src/dictature/transformer/passthrough.py +0 -0
  21. {dictature-0.9.2 → dictature-0.9.3}/src/dictature.egg-info/dependency_links.txt +0 -0
  22. {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.2
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.2"
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
- return f'e_{name.encode('utf-8').hex()}{suffix}'
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
- match mode:
113
- case ValueMode.string:
114
- return value
115
- case ValueMode.json:
116
- return json.loads(value)
117
- case ValueMode.pickle:
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.__name_transformer.forward(key))
139
+ self.__table.delete(self.__item_key(key))
139
140
 
140
141
  def __contains__(self, item: str):
141
142
  return item in self.keys()
@@ -1,2 +1,3 @@
1
1
  from .mock import MockTransformer
2
2
  from .passthrough import PassthroughTransformer
3
+ from .pipeline import PipelineTransformer
@@ -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
- # 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')
29
+ return unpad(self.__cipher(nonce=nonce).decrypt_and_verify(ciphertext, tag), AES.block_size).decode('utf8')
33
30
  else:
34
- return unpad(cipher.decrypt(data), AES.block_size).decode('utf8')
31
+ return unpad(self.__cipher().decrypt(data), AES.block_size).decode('utf8')
35
32
 
36
- @property
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.2
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