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.
- {dictature-0.9.4/src/dictature.egg-info → dictature-0.9.5}/PKG-INFO +1 -1
- {dictature-0.9.4 → dictature-0.9.5}/pyproject.toml +1 -1
- {dictature-0.9.4 → dictature-0.9.5}/src/dictature/backend/directory.py +11 -17
- {dictature-0.9.4 → dictature-0.9.5}/src/dictature/backend/misp.py +10 -27
- dictature-0.9.5/src/dictature/backend/mock.py +159 -0
- {dictature-0.9.4 → dictature-0.9.5}/src/dictature/dictature.py +4 -4
- {dictature-0.9.4 → dictature-0.9.5/src/dictature.egg-info}/PKG-INFO +1 -1
- dictature-0.9.4/src/dictature/backend/mock.py +0 -79
- {dictature-0.9.4 → dictature-0.9.5}/LICENSE +0 -0
- {dictature-0.9.4 → dictature-0.9.5}/README.md +0 -0
- {dictature-0.9.4 → dictature-0.9.5}/setup.cfg +0 -0
- {dictature-0.9.4 → dictature-0.9.5}/src/dictature/__init__.py +0 -0
- {dictature-0.9.4 → dictature-0.9.5}/src/dictature/backend/__init__.py +0 -0
- {dictature-0.9.4 → dictature-0.9.5}/src/dictature/backend/sqlite.py +0 -0
- {dictature-0.9.4 → dictature-0.9.5}/src/dictature/transformer/__init__.py +0 -0
- {dictature-0.9.4 → dictature-0.9.5}/src/dictature/transformer/aes.py +0 -0
- {dictature-0.9.4 → dictature-0.9.5}/src/dictature/transformer/hmac.py +0 -0
- {dictature-0.9.4 → dictature-0.9.5}/src/dictature/transformer/mock.py +0 -0
- {dictature-0.9.4 → dictature-0.9.5}/src/dictature/transformer/passthrough.py +0 -0
- {dictature-0.9.4 → dictature-0.9.5}/src/dictature/transformer/pipeline.py +0 -0
- {dictature-0.9.4 → dictature-0.9.5}/src/dictature.egg-info/SOURCES.txt +0 -0
- {dictature-0.9.4 → dictature-0.9.5}/src/dictature.egg-info/dependency_links.txt +0 -0
- {dictature-0.9.4 → dictature-0.9.5}/src/dictature.egg-info/top_level.txt +0 -0
- {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.
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
55
|
-
save_data =
|
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 ==
|
59
|
-
attribute.value =
|
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 =
|
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 ==
|
76
|
-
|
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 =
|
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
|
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.
|
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
|
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
|
File without changes
|
File without changes
|