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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dictature
3
- Version: 0.9.0
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
- # SQLiDictature
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
- # SQLiDictature
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.0"
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
- yield child.name[len(self.__dir_prefix):]
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.__filename_encode(name, suffix=''))
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.__filename_decode(child.name[len(self.__prefix):])
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.__filename_encode(item))
68
+ return self.__path / (self.__prefix + self._filename_encode(item))
68
69
 
69
70
  @staticmethod
70
- def __filename_encode(name: str, suffix: str = '.txt') -> str:
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 __filename_decode(name: str) -> str:
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 name[2:-5]
79
- return bytes.fromhex(name[2:-5]).decode('utf-8')
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 'db_%'")
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__(self, backend: DictatureBackendMock) -> None:
13
- self.__db = backend
14
- self.__db_cache: Dict[str, "DictatureTable"] = {}
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.__db.keys())
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.__db_cache) > 128:
34
- del self.__db_cache[choice(list(self.__db_cache.keys()))]
35
- if item not in self.__db_cache:
36
- self.__db_cache[item] = DictatureTable(self.__db, item)
37
- return self.__db_cache[item]
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__(self, db: DictatureBackendMock, table_name: str):
51
- self.__db = db
52
- self.__table = self.__db.table(table_name)
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
- value = self.__table.get(item)
89
- mode = ValueMode(value.mode)
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.value
114
+ return value
93
115
  case ValueMode.json:
94
- return json.loads(value.value)
116
+ return json.loads(value)
95
117
  case ValueMode.pickle:
96
- return pickle.loads(decompress(b64decode(value.value.encode('ascii'))))
97
- raise ValueError(f"Unknown mode '{value.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,2 @@
1
+ from .mock import MockTransformer
2
+ from .passthrough import PassthroughTransformer
@@ -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")
@@ -0,0 +1,10 @@
1
+ from .mock import MockTransformer
2
+
3
+ class PassthroughTransformer(MockTransformer):
4
+ def forward(self, text: str) -> str:
5
+ return text
6
+ def backward(self, text: str) -> str:
7
+ return text
8
+ @property
9
+ def static(self) -> bool:
10
+ return True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dictature
3
- Version: 0.9.0
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
- # SQLiDictature
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