dictature 0.9.4__tar.gz → 0.9.5__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 (24) hide show
  1. {dictature-0.9.4/src/dictature.egg-info → dictature-0.9.5}/PKG-INFO +1 -1
  2. {dictature-0.9.4 → dictature-0.9.5}/pyproject.toml +1 -1
  3. {dictature-0.9.4 → dictature-0.9.5}/src/dictature/backend/directory.py +11 -17
  4. {dictature-0.9.4 → dictature-0.9.5}/src/dictature/backend/misp.py +10 -27
  5. dictature-0.9.5/src/dictature/backend/mock.py +159 -0
  6. {dictature-0.9.4 → dictature-0.9.5}/src/dictature/dictature.py +4 -4
  7. {dictature-0.9.4 → dictature-0.9.5/src/dictature.egg-info}/PKG-INFO +1 -1
  8. dictature-0.9.4/src/dictature/backend/mock.py +0 -79
  9. {dictature-0.9.4 → dictature-0.9.5}/LICENSE +0 -0
  10. {dictature-0.9.4 → dictature-0.9.5}/README.md +0 -0
  11. {dictature-0.9.4 → dictature-0.9.5}/setup.cfg +0 -0
  12. {dictature-0.9.4 → dictature-0.9.5}/src/dictature/__init__.py +0 -0
  13. {dictature-0.9.4 → dictature-0.9.5}/src/dictature/backend/__init__.py +0 -0
  14. {dictature-0.9.4 → dictature-0.9.5}/src/dictature/backend/sqlite.py +0 -0
  15. {dictature-0.9.4 → dictature-0.9.5}/src/dictature/transformer/__init__.py +0 -0
  16. {dictature-0.9.4 → dictature-0.9.5}/src/dictature/transformer/aes.py +0 -0
  17. {dictature-0.9.4 → dictature-0.9.5}/src/dictature/transformer/hmac.py +0 -0
  18. {dictature-0.9.4 → dictature-0.9.5}/src/dictature/transformer/mock.py +0 -0
  19. {dictature-0.9.4 → dictature-0.9.5}/src/dictature/transformer/passthrough.py +0 -0
  20. {dictature-0.9.4 → dictature-0.9.5}/src/dictature/transformer/pipeline.py +0 -0
  21. {dictature-0.9.4 → dictature-0.9.5}/src/dictature.egg-info/SOURCES.txt +0 -0
  22. {dictature-0.9.4 → dictature-0.9.5}/src/dictature.egg-info/dependency_links.txt +0 -0
  23. {dictature-0.9.4 → dictature-0.9.5}/src/dictature.egg-info/top_level.txt +0 -0
  24. {dictature-0.9.4 → dictature-0.9.5}/tests/test_operations.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dictature
3
- Version: 0.9.4
3
+ Version: 0.9.5
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.4"
7
+ version = "0.9.5"
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" }
@@ -1,10 +1,8 @@
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):
@@ -33,6 +31,7 @@ class DictatureTableDirectory(DictatureTableMock):
33
31
  def __init__(self, path_root: Path, name: str, db_prefix: str, prefix: str = 'item_') -> None:
34
32
  self.__path = path_root / (db_prefix + self._filename_encode(name, suffix=''))
35
33
  self.__prefix = prefix
34
+ self.__serializer = ValueSerializer(mode=ValueSerializerMode.filename_only)
36
35
 
37
36
  def keys(self) -> Iterable[str]:
38
37
  for child in self.__path.iterdir():
@@ -49,8 +48,7 @@ class DictatureTableDirectory(DictatureTableMock):
49
48
  file_target = self.__item_path(item)
50
49
  file_target_tmp = file_target.with_suffix('.tmp')
51
50
 
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
51
+ save_data = self.__serializer.serialize(value)
54
52
 
55
53
  file_target_tmp.write_text(save_data)
56
54
  file_target_tmp.rename(file_target)
@@ -58,10 +56,7 @@ class DictatureTableDirectory(DictatureTableMock):
58
56
  def get(self, item: str) -> Value:
59
57
  try:
60
58
  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)
59
+ return self.__serializer.deserialize(save_data)
65
60
  except FileNotFoundError:
66
61
  raise KeyError(item)
67
62
 
@@ -74,14 +69,13 @@ class DictatureTableDirectory(DictatureTableMock):
74
69
 
75
70
  @staticmethod
76
71
  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}'
72
+ return ValueSerializer(mode=ValueSerializerMode.filename_only).serialize(Value(
73
+ value=name,
74
+ mode=ValueMode.string.value
75
+ )) + suffix
81
76
 
82
77
  @staticmethod
83
78
  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')
79
+ if suffix:
80
+ name = name[:-len(suffix)]
81
+ return ValueSerializer(mode=ValueSerializerMode.filename_only).deserialize(name).value
@@ -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')
@@ -0,0 +1,159 @@
1
+ from json import dumps, loads
2
+ from string import hexdigits, ascii_letters, digits, printable
3
+ from typing import Iterable, NamedTuple
4
+ from enum import Enum
5
+
6
+
7
+ class ValueMode(Enum):
8
+ string = 0
9
+ json = 1
10
+ pickle = 2
11
+
12
+
13
+ class Value(NamedTuple):
14
+ value: str
15
+ mode: int
16
+
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
+
96
+ class DictatureBackendMock:
97
+ def keys(self) -> Iterable[str]:
98
+ """
99
+ Return all table names
100
+ :return: all table names
101
+ """
102
+ raise NotImplementedError("This method should be implemented by the subclass")
103
+
104
+ def table(self, name: str) -> 'DictatureTableMock':
105
+ """
106
+ Create a table object based on the name
107
+ :param name: name of the table
108
+ :return: table object
109
+ """
110
+ raise NotImplementedError("This method should be implemented by the subclass")
111
+
112
+
113
+ class DictatureTableMock:
114
+ def keys(self) -> Iterable[str]:
115
+ """
116
+ Return all keys in the table
117
+ :return: all keys in the table
118
+ """
119
+ raise NotImplementedError("This method should be implemented by the subclass")
120
+
121
+ def drop(self) -> None:
122
+ """
123
+ Delete the table
124
+ :return: None
125
+ """
126
+ raise NotImplementedError("This method should be implemented by the subclass")
127
+
128
+ def create(self) -> None:
129
+ """
130
+ Create the table in the backend
131
+ :return: None
132
+ """
133
+ raise NotImplementedError("This method should be implemented by the subclass")
134
+
135
+ def set(self, item: str, value: Value) -> None:
136
+ """
137
+ Set a value in the table
138
+ :param item: key to set
139
+ :param value: value to set
140
+ :return: None
141
+ """
142
+ raise NotImplementedError("This method should be implemented by the subclass")
143
+
144
+ def get(self, item: str) -> Value:
145
+ """
146
+ Get a value from the table
147
+ :param item: key to get
148
+ :return: value
149
+ :raises KeyError: if the key does not exist
150
+ """
151
+ raise NotImplementedError("This method should be implemented by the subclass")
152
+
153
+ def delete(self, item: str) -> None:
154
+ """
155
+ Delete a value from the table
156
+ :param item: key to delete
157
+ :return: None
158
+ """
159
+ raise NotImplementedError("This method should be implemented by the subclass")
@@ -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
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dictature
3
- Version: 0.9.4
3
+ Version: 0.9.5
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
@@ -1,79 +0,0 @@
1
- from typing import Iterable, NamedTuple
2
- from enum import Enum
3
-
4
-
5
- class ValueMode(Enum):
6
- string = 0
7
- json = 1
8
- pickle = 2
9
-
10
-
11
- class Value(NamedTuple):
12
- value: str
13
- mode: int
14
-
15
-
16
- class DictatureBackendMock:
17
- def keys(self) -> Iterable[str]:
18
- """
19
- Return all table names
20
- :return: all table names
21
- """
22
- raise NotImplementedError("This method should be implemented by the subclass")
23
-
24
- def table(self, name: str) -> 'DictatureTableMock':
25
- """
26
- Create a table object based on the name
27
- :param name: name of the table
28
- :return: table object
29
- """
30
- raise NotImplementedError("This method should be implemented by the subclass")
31
-
32
-
33
- class DictatureTableMock:
34
- def keys(self) -> Iterable[str]:
35
- """
36
- Return all keys in the table
37
- :return: all keys in the table
38
- """
39
- raise NotImplementedError("This method should be implemented by the subclass")
40
-
41
- def drop(self) -> None:
42
- """
43
- Delete the table
44
- :return: None
45
- """
46
- raise NotImplementedError("This method should be implemented by the subclass")
47
-
48
- def create(self) -> None:
49
- """
50
- Create the table in the backend
51
- :return: None
52
- """
53
- raise NotImplementedError("This method should be implemented by the subclass")
54
-
55
- def set(self, item: str, value: Value) -> None:
56
- """
57
- Set a value in the table
58
- :param item: key to set
59
- :param value: value to set
60
- :return: None
61
- """
62
- raise NotImplementedError("This method should be implemented by the subclass")
63
-
64
- def get(self, item: str) -> Value:
65
- """
66
- Get a value from the table
67
- :param item: key to get
68
- :return: value
69
- :raises KeyError: if the key does not exist
70
- """
71
- raise NotImplementedError("This method should be implemented by the subclass")
72
-
73
- def delete(self, item: str) -> None:
74
- """
75
- Delete a value from the table
76
- :param item: key to delete
77
- :return: None
78
- """
79
- raise NotImplementedError("This method should be implemented by the subclass")
File without changes
File without changes
File without changes