dictature 0.9.4__py3-none-any.whl → 0.9.6__py3-none-any.whl

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,14 +1,12 @@
1
- from re import sub
2
- from json import dumps, loads
3
1
  from pathlib import Path
4
2
  from typing import Iterable, Union
5
3
  from shutil import rmtree
6
4
 
7
- from .mock import DictatureTableMock, DictatureBackendMock, Value, ValueMode
5
+ from .mock import DictatureTableMock, DictatureBackendMock, Value, ValueMode, ValueSerializer, ValueSerializerMode
8
6
 
9
7
 
10
8
  class DictatureBackendDirectory(DictatureBackendMock):
11
- def __init__(self, directory: Union[Path, str], dir_prefix: str = 'db_') -> None:
9
+ def __init__(self, directory: Union[Path, str], dir_prefix: str = 'db_', item_prefix: str = 'item_') -> None:
12
10
  """
13
11
  Create a new directory backend
14
12
  :param directory: directory to store the data
@@ -18,6 +16,7 @@ class DictatureBackendDirectory(DictatureBackendMock):
18
16
  directory = Path(directory)
19
17
  self.__directory = directory
20
18
  self.__dir_prefix = dir_prefix
19
+ self.__item_prefix = item_prefix
21
20
 
22
21
  def keys(self) -> Iterable[str]:
23
22
  for child in self.__directory.iterdir():
@@ -26,13 +25,14 @@ class DictatureBackendDirectory(DictatureBackendMock):
26
25
  yield DictatureTableDirectory._filename_decode(child.name[len(self.__dir_prefix):], suffix='')
27
26
 
28
27
  def table(self, name: str) -> 'DictatureTableMock':
29
- return DictatureTableDirectory(self.__directory, name, self.__dir_prefix)
28
+ return DictatureTableDirectory(self.__directory, name, self.__dir_prefix, self.__item_prefix)
30
29
 
31
30
 
32
31
  class DictatureTableDirectory(DictatureTableMock):
33
- def __init__(self, path_root: Path, name: str, db_prefix: str, prefix: str = 'item_') -> None:
32
+ def __init__(self, path_root: Path, name: str, db_prefix: str, prefix: str) -> None:
34
33
  self.__path = path_root / (db_prefix + self._filename_encode(name, suffix=''))
35
34
  self.__prefix = prefix
35
+ self.__serializer = ValueSerializer(mode=ValueSerializerMode.filename_only)
36
36
 
37
37
  def keys(self) -> Iterable[str]:
38
38
  for child in self.__path.iterdir():
@@ -49,8 +49,7 @@ class DictatureTableDirectory(DictatureTableMock):
49
49
  file_target = self.__item_path(item)
50
50
  file_target_tmp = file_target.with_suffix('.tmp')
51
51
 
52
- save_as_json = value.mode != ValueMode.string.value or value.value.startswith('{')
53
- save_data = dumps({'value': value.value, 'mode': value.mode}, indent=1) if save_as_json else value.value
52
+ save_data = self.__serializer.serialize(value)
54
53
 
55
54
  file_target_tmp.write_text(save_data)
56
55
  file_target_tmp.rename(file_target)
@@ -58,10 +57,7 @@ class DictatureTableDirectory(DictatureTableMock):
58
57
  def get(self, item: str) -> Value:
59
58
  try:
60
59
  save_data = self.__item_path(item).read_text()
61
- if save_data.startswith('{'):
62
- data = loads(save_data)
63
- return Value(data['value'], data['mode'])
64
- return Value(save_data, ValueMode.string.value)
60
+ return self.__serializer.deserialize(save_data)
65
61
  except FileNotFoundError:
66
62
  raise KeyError(item)
67
63
 
@@ -74,14 +70,13 @@ class DictatureTableDirectory(DictatureTableMock):
74
70
 
75
71
  @staticmethod
76
72
  def _filename_encode(name: str, suffix: str = '.txt') -> str:
77
- if name == sub(r'[^\w_. -]', '_', name):
78
- return f"d_{name}{suffix}"
79
- name = name.encode('utf-8').hex()
80
- return f'e_{name}{suffix}'
73
+ return ValueSerializer(mode=ValueSerializerMode.filename_only).serialize(Value(
74
+ value=name,
75
+ mode=ValueMode.string.value
76
+ )) + suffix
81
77
 
82
78
  @staticmethod
83
79
  def _filename_decode(name: str, suffix: str = '.txt') -> str:
84
- encoded_name = name[2:-len(suffix) if suffix else len(name)]
85
- if name.startswith('d_'):
86
- return encoded_name
87
- return bytes.fromhex(encoded_name).decode('utf-8')
80
+ if suffix:
81
+ name = name[:-len(suffix)]
82
+ return ValueSerializer(mode=ValueSerializerMode.filename_only).deserialize(name).value
dictature/backend/misp.py CHANGED
@@ -1,8 +1,6 @@
1
- from json import dumps, loads
2
- from re import sub
3
1
  from typing import Iterable, Optional
4
2
 
5
- from .mock import DictatureTableMock, DictatureBackendMock, Value, ValueMode
3
+ from .mock import DictatureTableMock, DictatureBackendMock, Value, ValueMode, ValueSerializer, ValueSerializerMode
6
4
 
7
5
  try:
8
6
  from pymisp import PyMISP, MISPEvent, MISPAttribute
@@ -39,6 +37,7 @@ class DictatureTableMISP(DictatureTableMock):
39
37
  self.__event_description = event_description
40
38
  self.__tag = tag
41
39
  self.__event: Optional[MISPEvent] = None
40
+ self.__serializer = ValueSerializer(mode=ValueSerializerMode.ascii_only)
42
41
 
43
42
  def keys(self) -> Iterable[str]:
44
43
  for attribute in self.__event_attributes():
@@ -51,18 +50,18 @@ class DictatureTableMISP(DictatureTableMock):
51
50
  self.__get_event()
52
51
 
53
52
  def set(self, item: str, value: Value) -> None:
54
- save_as_json = value.mode != ValueMode.string.value or value.value.startswith('{')
55
- save_data = dumps({'value': value.value, 'mode': value.mode}, indent=1) if save_as_json else value.value
53
+ item_name = self.__serializer.serialize(Value(value=item, mode=ValueMode.string.value))
54
+ save_data = self.__serializer.serialize(value)
56
55
 
57
56
  for attribute in self.__event_attributes():
58
- if attribute.value == item:
59
- attribute.value = item
57
+ if attribute.value == item_name:
58
+ attribute.value = item_name
60
59
  attribute.comment = save_data
61
60
  self.__misp.update_attribute(attribute)
62
61
  break
63
62
  else:
64
63
  attribute = MISPAttribute()
65
- attribute.value = item
64
+ attribute.value = item_name
66
65
  attribute.comment = save_data
67
66
  attribute.type = 'comment'
68
67
  attribute.to_ids = False
@@ -71,12 +70,10 @@ class DictatureTableMISP(DictatureTableMock):
71
70
  self.__get_event().attributes.append(attribute)
72
71
 
73
72
  def get(self, item: str) -> Value:
73
+ item_name = self.__serializer.serialize(Value(value=item, mode=ValueMode.string.value))
74
74
  for attribute in self.__event_attributes():
75
- if attribute.value == item:
76
- if attribute.comment.startswith('{'):
77
- data = loads(attribute.comment)
78
- return Value(data['value'], data['mode'])
79
- return Value(attribute.comment, ValueMode.string.value)
75
+ if attribute.value == item_name:
76
+ return self.__serializer.deserialize(attribute.comment)
80
77
  raise KeyError(item)
81
78
 
82
79
  def delete(self, item: str) -> None:
@@ -110,17 +107,3 @@ class DictatureTableMISP(DictatureTableMock):
110
107
  if attribute.type != 'comment' or (hasattr(attribute, 'deleted') and attribute.deleted):
111
108
  continue
112
109
  yield attribute
113
-
114
- @staticmethod
115
- def _record_encode(name: str, suffix: str = '.txt') -> str:
116
- if name == sub(r'[^\w_. -]', '_', name):
117
- return f"d_{name}{suffix}"
118
- name = name.encode('utf-8').hex()
119
- return f'e_{name}{suffix}'
120
-
121
- @staticmethod
122
- def _record_decode(name: str, suffix: str = '.txt') -> str:
123
- encoded_name = name[2:-len(suffix) if suffix else len(name)]
124
- if name.startswith('d_'):
125
- return encoded_name
126
- return bytes.fromhex(encoded_name).decode('utf-8')
dictature/backend/mock.py CHANGED
@@ -1,3 +1,5 @@
1
+ from json import dumps, loads
2
+ from string import hexdigits, ascii_letters, digits, printable
1
3
  from typing import Iterable, NamedTuple
2
4
  from enum import Enum
3
5
 
@@ -13,6 +15,84 @@ class Value(NamedTuple):
13
15
  mode: int
14
16
 
15
17
 
18
+ class ValueSerializerMode(Enum):
19
+ any_string = 0
20
+ ascii_only = 1
21
+ filename_only = 2
22
+ hex_only = 3
23
+
24
+
25
+ class ValueSerializer:
26
+ prefix = 'a09e'
27
+
28
+ def __init__(self, mode: ValueSerializerMode = ValueSerializerMode.any_string):
29
+ self.__mode = mode
30
+
31
+ def deserialize(self, serialized: str) -> Value:
32
+ """
33
+ Deserialize a string into a Value object
34
+ :param serialized: serialized string
35
+ :return: Value object
36
+ """
37
+ # Check if the string starts with the prefix (hex-encoded)
38
+ if serialized.startswith(self.prefix):
39
+ # Decode the hex part
40
+ hex_data = serialized[len(self.prefix):]
41
+ decoded = bytes.fromhex(hex_data).decode('ascii')
42
+ # Recursively deserialize the decoded string
43
+ return ValueSerializer(mode=ValueSerializerMode.ascii_only).deserialize(decoded)
44
+
45
+ # Check if the string looks like JSON
46
+ if serialized.startswith('{'):
47
+ try:
48
+ data = loads(serialized)
49
+ return Value(value=data['value'], mode=data['mode'])
50
+ except (ValueError, KeyError):
51
+ pass # Not valid JSON or missing required keys
52
+
53
+ # Direct value (string mode)
54
+ return Value(value=serialized, mode=ValueMode.string.value)
55
+
56
+ def serialize(self, value: Value) -> str:
57
+ """
58
+ Serializes a `Value` object into a `str`, converting its data representation
59
+ based on the serialization mode set in `ValueSerializerMode`. Depending on
60
+ the mode provided, the object can be serialized directly if its string
61
+ representation conforms to certain criteria, or it may be converted into
62
+ a JSON format. Handles customization of allowed characters and uses prefix
63
+ to encode conditions if the direct representation is not permitted.
64
+
65
+ If the mode is incompatible or unsupported, raises a `NotImplementedError`.
66
+
67
+ :param value: Instance of `Value` to be serialized.
68
+
69
+ :return: Serialized representation of the `Value` object as a string.
70
+
71
+ :raises NotImplementedError: If the mode provided in `ValueSerializerMode`
72
+ is unsupported.
73
+ """
74
+ if self.__mode == ValueSerializerMode.hex_only:
75
+ allowed_chars = hexdigits
76
+ elif self.__mode == ValueSerializerMode.filename_only:
77
+ allowed_chars = ascii_letters + digits + '_.'
78
+ elif self.__mode in (ValueSerializerMode.any_string, ValueSerializerMode.ascii_only):
79
+ allowed_chars = None
80
+ else:
81
+ raise NotImplementedError(self.__mode)
82
+
83
+ if allowed_chars is not None:
84
+ # Only a subset of characters is allowed if all match and do not start with the reserved prefix, encode directly
85
+ if value.mode == ValueMode.string.value and all(map(lambda x: x in allowed_chars, value.value)) and not value.value.startswith(self.prefix):
86
+ return value.value
87
+ return self.prefix + ValueSerializer(mode=ValueSerializerMode.ascii_only).serialize(value).encode('ascii').hex()
88
+
89
+ # Save as JSON if not string or value is starting with { (indicating JSON)
90
+ save_as_json = value.mode != ValueMode.string.value or value.value.startswith('{') or value.value.startswith(self.prefix)
91
+ # Save as JSON if only ASCII strings are allowed
92
+ save_as_json = save_as_json or (self.__mode == ValueSerializerMode.ascii_only and any(filter(lambda x: x not in printable, value.value)))
93
+ return dumps({'value': value.value, 'mode': value.mode}, indent=1) if save_as_json else value.value
94
+
95
+
16
96
  class DictatureBackendMock:
17
97
  def keys(self) -> Iterable[str]:
18
98
  """
@@ -76,4 +156,4 @@ class DictatureTableMock:
76
156
  :param item: key to delete
77
157
  :return: None
78
158
  """
79
- raise NotImplementedError("This method should be implemented by the subclass")
159
+ raise NotImplementedError("This method should be implemented by the subclass")
dictature/dictature.py CHANGED
@@ -221,19 +221,19 @@ class DictatureTable:
221
221
  :return: None
222
222
  """
223
223
  self.__create_table()
224
- value_mode = ValueMode.string
224
+ value_mode: int = ValueMode.string.value
225
225
 
226
226
  if type(value) is not str:
227
227
  try:
228
228
  value = json.dumps(value)
229
- value_mode = ValueMode.json
229
+ value_mode = ValueMode.json.value
230
230
  except TypeError:
231
231
  value = b64encode(compress(pickle.dumps(value))).decode('ascii')
232
- value_mode = value_mode.pickle
232
+ value_mode = ValueMode.pickle.value
233
233
 
234
234
  key = self.__item_key(key)
235
235
  value = self.__value_transformer.forward(value)
236
- self.__table.set(key, Value(value=value, mode=value_mode.value))
236
+ self.__table.set(key, Value(value=value, mode=value_mode))
237
237
 
238
238
  def __delitem__(self, key: str) -> None:
239
239
  """
@@ -0,0 +1,53 @@
1
+ import gzip
2
+ import base64
3
+
4
+ from .mock import MockTransformer
5
+
6
+
7
+ class GzipTransformer(MockTransformer):
8
+ """
9
+ Compresses and decompresses text using Gzip.
10
+
11
+ The compressed data is Base64 encoded to ensure it can be represented as a string.
12
+ """
13
+ def __init__(self) -> None:
14
+ """
15
+ Initializes the GzipTransformer. No parameters needed for basic Gzip.
16
+ """
17
+ # No specific state needed for standard gzip compression/decompression
18
+ pass
19
+
20
+ def forward(self, text: str) -> str:
21
+ """
22
+ Compresses the input text using Gzip and returns a Base64 encoded string.
23
+ :param text: The original text string.
24
+ :return: A Base64 encoded string representing the Gzipped data.
25
+ """
26
+ byte_data = text.encode('utf-8')
27
+ compressed_bytes = gzip.compress(byte_data)
28
+ base64_bytes = base64.b64encode(compressed_bytes)
29
+ base64_string = base64_bytes.decode('ascii')
30
+ return base64_string
31
+
32
+ def backward(self, text: str) -> str:
33
+ """
34
+ Decompresses the Base64 encoded Gzip data back into the original text.
35
+ :param text: A Base64 encoded string representing Gzipped data.
36
+ :return: The original text string.
37
+ :raises ValueError: If the input string is not valid Base64 or not valid Gzip data.
38
+ """
39
+ try:
40
+ base64_bytes = text.encode('ascii')
41
+ compressed_bytes = base64.b64decode(base64_bytes)
42
+ original_bytes = gzip.decompress(compressed_bytes)
43
+ original_text = original_bytes.decode('utf-8')
44
+ return original_text
45
+ except (gzip.BadGzipFile, UnicodeDecodeError) as e:
46
+ # Catch errors related to Base64 decoding, Gzip decompression, or UTF-8 decoding
47
+ raise ValueError(f"Invalid input data for Gzip decompression: {e}") from e
48
+
49
+ @property
50
+ def static(self) -> bool:
51
+ return False
52
+
53
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dictature
3
- Version: 0.9.4
3
+ Version: 0.9.6
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
@@ -78,5 +78,6 @@ dictionary = Dictature(
78
78
  Currently, the following transformers are supported:
79
79
  - `AESTransformer`: encrypts/decrypts the data using AES
80
80
  - `HmacTransformer`: signs the data using HMAC or performs hash integrity checks
81
+ - `GzipTransformer`: compresses given data
81
82
  - `PassthroughTransformer`: does nothing
82
83
  - `PipelineTransformer`: chains multiple transformers
@@ -1,18 +1,19 @@
1
1
  dictature/__init__.py,sha256=UCPJKHeyirRZ0pCYoyeat-rwXa8pDezOJ3UWCipDdyc,33
2
- dictature/dictature.py,sha256=VM4RcXftQl2cFY8GxsRl7h3K8tsH7urNoT7GyGsxGuo,9314
2
+ dictature/dictature.py,sha256=SHwG_XvGwm6qpvMt5OjeS8BoU5wLgJi_4jIBKFmYFyI,9330
3
3
  dictature/backend/__init__.py,sha256=d5s6QCJOUzFglVNg8Cqqx_8b61S-AOTGjEUIF6FS69U,149
4
- dictature/backend/directory.py,sha256=Wf6dtllkqVA70DsnFoFzEHi_-RrTeOwqX5TFU-mYjrk,3451
5
- dictature/backend/misp.py,sha256=ZT392TkAkoP1fB5ZXjArD1Vsm00hvvq08msCv6QXbh8,4971
6
- dictature/backend/mock.py,sha256=Bllai6uZ1cRL8mKLbB3YQQo26mxTFepqzGu3FfOL1d4,2183
4
+ dictature/backend/directory.py,sha256=YuwZbWxVKOuL_Aup-EHyK96MXLaD91xzIaXj6nv5WpA,3280
5
+ dictature/backend/misp.py,sha256=iPjvgnJg6WveNP2wvgN7OK2vkX-SC9qYPrdoa9ahRT0,4411
6
+ dictature/backend/mock.py,sha256=BzfLstxkTIjk6mcMTdFKj8rSaFgIqn9-2Cyelslj8bY,5889
7
7
  dictature/backend/sqlite.py,sha256=zyphYEeLY4eGuBCor16i80_-brdipMpXZ3_kONwErsE,5237
8
8
  dictature/transformer/__init__.py,sha256=JIFJpXU6iB9hIUM8L7HL2o9Nqjm_YbMEuQBQC8ZJ6b4,124
9
9
  dictature/transformer/aes.py,sha256=ZhC1dT9QpnziErkDLriWLgXDEFNGQW0KG4aqSN2AZpA,1926
10
+ dictature/transformer/gzip.py,sha256=pngvJQeALa-lv98VBeJ1Pl6_gaAfGcPXD9UR7PexrYA,1921
10
11
  dictature/transformer/hmac.py,sha256=vURsB0HlzRPn_Vkl7lGmZV9OKempQuds8AanmadDxIo,834
11
12
  dictature/transformer/mock.py,sha256=7zu65ZqUV_AVRaPSzNd73cVMXixXt31SeuX9OKZxaJQ,948
12
13
  dictature/transformer/passthrough.py,sha256=Pt3N6G_Qh6HJ_q75ETL5nfAwYHLB-SjkVwUwbbbMik8,344
13
14
  dictature/transformer/pipeline.py,sha256=OaQaJeJ5NpICetJe08r8ontqstsXGuW8jDbKw1zxYs4,842
14
- dictature-0.9.4.dist-info/LICENSE,sha256=n1U9DKr8sM5EY2QHcvxSGiKTDWUT8MyXsOC79w94MT0,1072
15
- dictature-0.9.4.dist-info/METADATA,sha256=QklI5x9PnsuJZ-6jIcFskCDxSVveI3le5IJSIh_8E4Q,2826
16
- dictature-0.9.4.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
17
- dictature-0.9.4.dist-info/top_level.txt,sha256=-RO39WWCF44lqiXhSUcACVqbk6SkgReZTz7ZmHKH3-U,10
18
- dictature-0.9.4.dist-info/RECORD,,
15
+ dictature-0.9.6.dist-info/LICENSE,sha256=n1U9DKr8sM5EY2QHcvxSGiKTDWUT8MyXsOC79w94MT0,1072
16
+ dictature-0.9.6.dist-info/METADATA,sha256=46tXjvOFmBe0AxHyBr-DYMteFN2TVWcXFHa-YMdfJlw,2869
17
+ dictature-0.9.6.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
18
+ dictature-0.9.6.dist-info/top_level.txt,sha256=-RO39WWCF44lqiXhSUcACVqbk6SkgReZTz7ZmHKH3-U,10
19
+ dictature-0.9.6.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.3.0)
2
+ Generator: setuptools (75.3.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5