dictature 0.11.1__py3-none-any.whl → 0.13.1__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.
- dictature/backend/mysql.py +119 -0
- dictature/backend/s3.py +176 -0
- dictature/backend/sqlite.py +8 -6
- {dictature-0.11.1.dist-info → dictature-0.13.1.dist-info}/METADATA +3 -1
- {dictature-0.11.1.dist-info → dictature-0.13.1.dist-info}/RECORD +8 -6
- {dictature-0.11.1.dist-info → dictature-0.13.1.dist-info}/LICENSE +0 -0
- {dictature-0.11.1.dist-info → dictature-0.13.1.dist-info}/WHEEL +0 -0
- {dictature-0.11.1.dist-info → dictature-0.13.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,119 @@
|
|
1
|
+
try:
|
2
|
+
import mysql.connector
|
3
|
+
except ImportError:
|
4
|
+
raise ImportError('Requires: pip install mysql-connector-python') from None
|
5
|
+
from typing import Iterable
|
6
|
+
|
7
|
+
from .mock import DictatureBackendMock, DictatureTableMock, Value
|
8
|
+
|
9
|
+
|
10
|
+
class DictatureBackendMySQL(DictatureBackendMock):
|
11
|
+
def __init__(self, host: str, port: int = 3306, user: str = None, password: str = None,
|
12
|
+
database: str = None, prefix: str = 'tb_', **kwargs) -> None:
|
13
|
+
"""
|
14
|
+
Create a new MySQL backend
|
15
|
+
:param host: MySQL server host
|
16
|
+
:param port: MySQL server port (default: 3306)
|
17
|
+
:param user: MySQL username
|
18
|
+
:param password: MySQL password
|
19
|
+
:param database: MySQL database name
|
20
|
+
:param kwargs: Additional connection parameters
|
21
|
+
"""
|
22
|
+
self.__connection_params = {
|
23
|
+
'host': host,
|
24
|
+
'port': port,
|
25
|
+
'user': user,
|
26
|
+
'password': password,
|
27
|
+
'database': database,
|
28
|
+
**kwargs
|
29
|
+
}
|
30
|
+
|
31
|
+
# Remove None values from connection params
|
32
|
+
self.__connection_params = {k: v for k, v in self.__connection_params.items() if v is not None}
|
33
|
+
|
34
|
+
self.__connection = mysql.connector.connect(**self.__connection_params)
|
35
|
+
self.__cursor = self.__connection.cursor()
|
36
|
+
self.__prefix = prefix.replace('`', '').replace("'", '')
|
37
|
+
|
38
|
+
def keys(self) -> Iterable[str]:
|
39
|
+
# noinspection SqlResolve
|
40
|
+
tables = self._execute(f"SELECT table_name FROM information_schema.tables WHERE table_schema = %s AND table_name LIKE '{self.__prefix}%'", (self.__connection_params['database'],))
|
41
|
+
return {table[0][3:] for table in tables}
|
42
|
+
|
43
|
+
def table(self, name: str) -> 'DictatureTableMock':
|
44
|
+
return DictatureTableMySQL(self, name, self.__prefix)
|
45
|
+
|
46
|
+
def _execute(self, query: str, data: tuple = ()) -> list:
|
47
|
+
self.__cursor.execute(query, data)
|
48
|
+
return self.__cursor.fetchall()
|
49
|
+
|
50
|
+
def _commit(self) -> None:
|
51
|
+
self.__connection.commit()
|
52
|
+
|
53
|
+
def __del__(self):
|
54
|
+
if hasattr(self, '_DictatureBackendMySQL__connection'):
|
55
|
+
self.__connection.close()
|
56
|
+
|
57
|
+
|
58
|
+
class DictatureTableMySQL(DictatureTableMock):
|
59
|
+
def __init__(self, parent: "DictatureBackendMySQL", name: str, prefix: str) -> None:
|
60
|
+
self.__parent = parent
|
61
|
+
# MySQL table names don't need backticks for simple names, but we'll use them for consistency
|
62
|
+
self.__table = f"`{prefix}{name.replace('`', '')}`"
|
63
|
+
|
64
|
+
def keys(self) -> Iterable[str]:
|
65
|
+
# noinspection PyProtectedMember
|
66
|
+
result = self.__parent._execute(f"SELECT `key` FROM {self.__table}")
|
67
|
+
return set(map(lambda x: x[0], result))
|
68
|
+
|
69
|
+
def drop(self) -> None:
|
70
|
+
# noinspection PyProtectedMember
|
71
|
+
self.__parent._execute(f"DROP TABLE {self.__table}")
|
72
|
+
|
73
|
+
def key_exists(self, item: str) -> bool:
|
74
|
+
# noinspection PyProtectedMember
|
75
|
+
result = self.__parent._execute(f"SELECT `value` FROM {self.__table} WHERE `key`=%s", (item,))
|
76
|
+
return len(result) > 0
|
77
|
+
|
78
|
+
def create(self) -> None:
|
79
|
+
# noinspection PyProtectedMember
|
80
|
+
self.__parent._execute(f"""
|
81
|
+
CREATE TABLE IF NOT EXISTS {self.__table} (
|
82
|
+
`key` VARCHAR(700) NOT NULL UNIQUE,
|
83
|
+
`value` TEXT,
|
84
|
+
`type` INT NOT NULL DEFAULT 0,
|
85
|
+
PRIMARY KEY (`key`)
|
86
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
87
|
+
""")
|
88
|
+
|
89
|
+
def set(self, item: str, value: Value) -> None:
|
90
|
+
if self.key_exists(item):
|
91
|
+
# noinspection PyProtectedMember
|
92
|
+
self.__parent._execute(f"""
|
93
|
+
UPDATE {self.__table} SET `value`=%s, `type`=%s WHERE `key`=%s
|
94
|
+
""", (value.value, value.mode, item))
|
95
|
+
else:
|
96
|
+
# noinspection PyProtectedMember
|
97
|
+
self.__parent._execute(
|
98
|
+
f"INSERT INTO {self.__table} (`key`, `value`, `type`) VALUES (%s, %s, %s)",
|
99
|
+
(item, value.value, value.mode)
|
100
|
+
)
|
101
|
+
# noinspection PyProtectedMember
|
102
|
+
self.__parent._commit()
|
103
|
+
|
104
|
+
def get(self, item: str) -> Value:
|
105
|
+
# noinspection PyProtectedMember
|
106
|
+
r = self.__parent._execute(f"SELECT `value`, `type` FROM {self.__table} WHERE `key`=%s", (item,))
|
107
|
+
if r:
|
108
|
+
value: str = r[0][0]
|
109
|
+
type_value: int = r[0][1]
|
110
|
+
return Value(value=value, mode=type_value)
|
111
|
+
raise KeyError(item)
|
112
|
+
|
113
|
+
def delete(self, item: str) -> None:
|
114
|
+
# noinspection PyProtectedMember
|
115
|
+
self.__parent._execute(
|
116
|
+
f"DELETE FROM {self.__table} WHERE `key`=%s", (item,)
|
117
|
+
)
|
118
|
+
# noinspection PyProtectedMember
|
119
|
+
self.__parent._commit()
|
dictature/backend/s3.py
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
import io
|
2
|
+
import posixpath
|
3
|
+
from typing import Iterable
|
4
|
+
|
5
|
+
try:
|
6
|
+
import boto3
|
7
|
+
from botocore.exceptions import ClientError
|
8
|
+
except ImportError:
|
9
|
+
raise ImportError('Requires: pip install boto3')
|
10
|
+
|
11
|
+
from .mock import DictatureTableMock, DictatureBackendMock, Value, ValueMode, ValueSerializer, ValueSerializerMode
|
12
|
+
|
13
|
+
|
14
|
+
class DictatureBackendS3(DictatureBackendMock):
|
15
|
+
def __init__(
|
16
|
+
self,
|
17
|
+
bucket_name: str,
|
18
|
+
aws_access_key_id: str = None,
|
19
|
+
aws_secret_access_key: str = None,
|
20
|
+
region_name: str = None,
|
21
|
+
endpoint_url: str = None,
|
22
|
+
dir_prefix: str = 'db_',
|
23
|
+
item_prefix: str = 'item_'
|
24
|
+
) -> None:
|
25
|
+
"""
|
26
|
+
Initialize S3 backend
|
27
|
+
|
28
|
+
:param bucket_name: S3 bucket name
|
29
|
+
:param aws_access_key_id: AWS access key ID
|
30
|
+
:param aws_secret_access_key: AWS secret access key
|
31
|
+
:param region_name: AWS region name
|
32
|
+
:param endpoint_url: Custom endpoint URL for alternative S3-compatible storage (e.g., MinIO)
|
33
|
+
:param dir_prefix: Prefix for table directories
|
34
|
+
:param item_prefix: Prefix for item files
|
35
|
+
"""
|
36
|
+
self.__s3 = boto3.resource(
|
37
|
+
's3',
|
38
|
+
aws_access_key_id=aws_access_key_id,
|
39
|
+
aws_secret_access_key=aws_secret_access_key,
|
40
|
+
region_name=region_name,
|
41
|
+
endpoint_url=endpoint_url
|
42
|
+
)
|
43
|
+
self.__bucket = self.__s3.Bucket(bucket_name)
|
44
|
+
self.__bucket_name = bucket_name
|
45
|
+
self.__dir_prefix = dir_prefix
|
46
|
+
self.__item_prefix = item_prefix
|
47
|
+
|
48
|
+
def keys(self) -> Iterable[str]:
|
49
|
+
"""
|
50
|
+
Get all table names in the S3 bucket
|
51
|
+
"""
|
52
|
+
# Group objects by their "directory" prefix
|
53
|
+
prefixes = set()
|
54
|
+
for obj in self.__bucket.objects.filter(Prefix=self.__dir_prefix):
|
55
|
+
key = obj.key
|
56
|
+
parts = key.split('/')
|
57
|
+
if len(parts) > 1 and parts[0].startswith(self.__dir_prefix):
|
58
|
+
prefixes.add(parts[0])
|
59
|
+
|
60
|
+
for prefix in prefixes:
|
61
|
+
table_name_encoded = prefix[len(self.__dir_prefix):]
|
62
|
+
# noinspection PyProtectedMember
|
63
|
+
yield DictatureTableS3._filename_decode(table_name_encoded, suffix='')
|
64
|
+
|
65
|
+
def table(self, name: str) -> 'DictatureTableMock':
|
66
|
+
return DictatureTableS3(
|
67
|
+
self.__s3,
|
68
|
+
self.__bucket_name,
|
69
|
+
name,
|
70
|
+
self.__dir_prefix,
|
71
|
+
self.__item_prefix
|
72
|
+
)
|
73
|
+
|
74
|
+
|
75
|
+
class DictatureTableS3(DictatureTableMock):
|
76
|
+
def __init__(
|
77
|
+
self,
|
78
|
+
s3_resource,
|
79
|
+
bucket_name: str,
|
80
|
+
name: str,
|
81
|
+
db_prefix: str,
|
82
|
+
prefix: str
|
83
|
+
) -> None:
|
84
|
+
self.__s3 = s3_resource
|
85
|
+
self.__bucket = self.__s3.Bucket(bucket_name)
|
86
|
+
self.__bucket_name = bucket_name
|
87
|
+
self.__encoded_name = self._filename_encode(name, suffix='')
|
88
|
+
self.__path = f"{db_prefix}{self.__encoded_name}/"
|
89
|
+
self.__prefix = prefix
|
90
|
+
self.__name_serializer = ValueSerializer(mode=ValueSerializerMode.filename_only)
|
91
|
+
self.__value_serializer = ValueSerializer(mode=ValueSerializerMode.any_string)
|
92
|
+
|
93
|
+
def keys(self) -> Iterable[str]:
|
94
|
+
try:
|
95
|
+
for obj in self.__bucket.objects.filter(Prefix=self.__path):
|
96
|
+
key = obj.key
|
97
|
+
filename = posixpath.basename(key)
|
98
|
+
if filename.startswith(self.__prefix):
|
99
|
+
item_name_encoded = filename[len(self.__prefix):]
|
100
|
+
yield self._filename_decode(item_name_encoded, suffix='.txt')
|
101
|
+
except ClientError:
|
102
|
+
pass # Return empty generator if bucket doesn't exist or other error
|
103
|
+
|
104
|
+
def drop(self) -> None:
|
105
|
+
"""
|
106
|
+
Delete all objects in this table's directory
|
107
|
+
"""
|
108
|
+
try:
|
109
|
+
self.__bucket.objects.filter(Prefix=self.__path).delete()
|
110
|
+
except ClientError:
|
111
|
+
pass # Ignore if objects don't exist or other error
|
112
|
+
|
113
|
+
def create(self) -> None:
|
114
|
+
"""
|
115
|
+
S3 doesn't need explicit directory creation, but we can create a marker file
|
116
|
+
to indicate the table's existence if it doesn't already have items
|
117
|
+
"""
|
118
|
+
try:
|
119
|
+
# Check if the "directory" exists by looking for any objects with this prefix
|
120
|
+
objects = list(self.__bucket.objects.filter(Prefix=self.__path).limit(1))
|
121
|
+
if not objects:
|
122
|
+
# Create an empty marker object to represent the directory
|
123
|
+
self.__bucket.put_object(Key=f"{self.__path}.keep")
|
124
|
+
except ClientError:
|
125
|
+
# If bucket doesn't exist, this will fail, but that's expected
|
126
|
+
pass
|
127
|
+
|
128
|
+
def set(self, item: str, value: Value) -> None:
|
129
|
+
item_path = self.__item_path(item)
|
130
|
+
save_data: str = self.__value_serializer.serialize(value)
|
131
|
+
data_bytes = save_data.encode('utf-8')
|
132
|
+
|
133
|
+
with io.BytesIO(data_bytes) as buffer:
|
134
|
+
self.__bucket.upload_fileobj(buffer, item_path)
|
135
|
+
|
136
|
+
def get(self, item: str) -> Value:
|
137
|
+
item_path = self.__item_path(item)
|
138
|
+
buffer = io.BytesIO()
|
139
|
+
try:
|
140
|
+
self.__bucket.download_fileobj(item_path, buffer)
|
141
|
+
buffer.seek(0)
|
142
|
+
save_data = buffer.read().decode('utf-8')
|
143
|
+
except ClientError as e:
|
144
|
+
if e.response['Error']['Code'] == 'NoSuchKey':
|
145
|
+
raise KeyError(item)
|
146
|
+
raise # Re-raise for other errors
|
147
|
+
return self.__value_serializer.deserialize(save_data)
|
148
|
+
|
149
|
+
def delete(self, item: str) -> None:
|
150
|
+
"""
|
151
|
+
Delete the item from S3
|
152
|
+
"""
|
153
|
+
item_path = self.__item_path(item)
|
154
|
+
try:
|
155
|
+
self.__s3.Object(self.__bucket_name, item_path).delete()
|
156
|
+
except ClientError as e:
|
157
|
+
if e.response['Error']['Code'] != 'NoSuchKey':
|
158
|
+
raise # Only ignore "not found" errors
|
159
|
+
|
160
|
+
def __item_path(self, item: str) -> str:
|
161
|
+
encoded_item_name = self.__prefix + self._filename_encode(item, suffix='.txt')
|
162
|
+
full_path = f"{self.__path}{encoded_item_name}"
|
163
|
+
return full_path
|
164
|
+
|
165
|
+
@staticmethod
|
166
|
+
def _filename_encode(name: str, suffix: str = '.txt') -> str:
|
167
|
+
return ValueSerializer(mode=ValueSerializerMode.filename_only).serialize(Value(
|
168
|
+
value=name,
|
169
|
+
mode=ValueMode.string.value
|
170
|
+
)) + suffix
|
171
|
+
|
172
|
+
@staticmethod
|
173
|
+
def _filename_decode(name: str, suffix: str = '.txt') -> str:
|
174
|
+
if suffix and name.endswith(suffix):
|
175
|
+
name = name[:-len(suffix)]
|
176
|
+
return ValueSerializer(mode=ValueSerializerMode.filename_only).deserialize(name).value
|
dictature/backend/sqlite.py
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
import sqlite3
|
2
|
-
from typing import Iterable, Union, Optional
|
3
2
|
from pathlib import Path
|
3
|
+
from typing import Iterable, Union, Optional
|
4
4
|
|
5
5
|
from .mock import DictatureBackendMock, DictatureTableMock, Value, ValueMode
|
6
6
|
|
7
7
|
|
8
8
|
class DictatureBackendSQLite(DictatureBackendMock):
|
9
|
-
def __init__(self, file: Union[str, Path]) -> None:
|
9
|
+
def __init__(self, file: Union[str, Path], prefix: str = 'tb_') -> None:
|
10
10
|
"""
|
11
11
|
Create a new SQLite backend
|
12
12
|
:param file: file to store the database
|
13
|
+
:param prefix: prefix for the tables (default: 'tb_')
|
13
14
|
"""
|
14
15
|
if isinstance(file, str):
|
15
16
|
file = Path(file)
|
@@ -19,13 +20,14 @@ class DictatureBackendSQLite(DictatureBackendMock):
|
|
19
20
|
check_same_thread=False if sqlite3.threadsafety >= 3 else True
|
20
21
|
)
|
21
22
|
self.__cursor = self.__connection.cursor()
|
23
|
+
self.__prefix = prefix.replace("'", "").replace('`', '')
|
22
24
|
|
23
25
|
def keys(self) -> Iterable[str]:
|
24
|
-
tables = self._execute("SELECT tbl_name FROM sqlite_master WHERE type='table' AND tbl_name LIKE '
|
26
|
+
tables = self._execute(f"SELECT tbl_name FROM sqlite_master WHERE type='table' AND tbl_name LIKE '{self.__prefix}%'")
|
25
27
|
return {table[0][3:] for table in tables}
|
26
28
|
|
27
29
|
def table(self, name: str) -> 'DictatureTableMock':
|
28
|
-
return DictatureTableSQLite(self, name)
|
30
|
+
return DictatureTableSQLite(self, name, self.__prefix)
|
29
31
|
|
30
32
|
def _execute(self, query: str, data: tuple = ()) -> list:
|
31
33
|
return list(self.__cursor.execute(query, data))
|
@@ -38,10 +40,10 @@ class DictatureBackendSQLite(DictatureBackendMock):
|
|
38
40
|
|
39
41
|
|
40
42
|
class DictatureTableSQLite(DictatureTableMock):
|
41
|
-
def __init__(self, parent: "DictatureBackendSQLite", name: str) -> None:
|
43
|
+
def __init__(self, parent: "DictatureBackendSQLite", name: str, prefix: str) -> None:
|
42
44
|
self.__parent = parent
|
43
45
|
self.__supports_jsonization: Optional[bool] = None
|
44
|
-
self.__table = "
|
46
|
+
self.__table = "`%s`" % (prefix + name).replace('`', '``')
|
45
47
|
|
46
48
|
def keys(self) -> Iterable[str]:
|
47
49
|
# noinspection PyProtectedMember
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: dictature
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.13.1
|
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
|
@@ -60,6 +60,8 @@ Currently, the following backends are supported:
|
|
60
60
|
- `DictatureBackendSQLite`: stores the data in a SQLite database
|
61
61
|
- `DictatureBackendMISP`: stores the data in a MISP instance
|
62
62
|
- `DictatureBackendWebdav`: stores data in a WebDav share as files
|
63
|
+
- `DictatureBackendS3`: stores data in an S3 bucket
|
64
|
+
- `DictatureBackendMySQL`: stores data in a MySQL database
|
63
65
|
|
64
66
|
### Transformers
|
65
67
|
|
@@ -4,7 +4,9 @@ dictature/backend/__init__.py,sha256=d5s6QCJOUzFglVNg8Cqqx_8b61S-AOTGjEUIF6FS69U
|
|
4
4
|
dictature/backend/directory.py,sha256=_-8Fx7Ef4tFbOQWHcARfKjm0kbZUpmN1f1ysyWINOr4,3600
|
5
5
|
dictature/backend/misp.py,sha256=R9osuEJa79SoXErwByIwhViJ26cEfw2nKhWu8XxkO7Y,4860
|
6
6
|
dictature/backend/mock.py,sha256=o4YBl6Wk-q6IHFdykGeryTKcQFsJHvymlONPbbwVkak,5891
|
7
|
-
dictature/backend/
|
7
|
+
dictature/backend/mysql.py,sha256=LdbcytXCnu2pqx3LfOH__-dFy85IwB-HGMFOxDIGBvA,4624
|
8
|
+
dictature/backend/s3.py,sha256=9Vcf4-RagyjpV9F4m9ZQ1vjb4lbZfEcOZQBuD_SPwZg,6580
|
9
|
+
dictature/backend/sqlite.py,sha256=mHEfRufqRcDfl0mIPl9eK0ODyUmgYD9SWjbllI87Hp0,5434
|
8
10
|
dictature/backend/webdav.py,sha256=Y-3_WTcMyKVUnsVjiUZAxuy10FK0Yr-7Mgn13clg3po,5039
|
9
11
|
dictature/transformer/__init__.py,sha256=JIFJpXU6iB9hIUM8L7HL2o9Nqjm_YbMEuQBQC8ZJ6b4,124
|
10
12
|
dictature/transformer/aes.py,sha256=ZhC1dT9QpnziErkDLriWLgXDEFNGQW0KG4aqSN2AZpA,1926
|
@@ -13,8 +15,8 @@ dictature/transformer/hmac.py,sha256=vURsB0HlzRPn_Vkl7lGmZV9OKempQuds8AanmadDxIo
|
|
13
15
|
dictature/transformer/mock.py,sha256=7zu65ZqUV_AVRaPSzNd73cVMXixXt31SeuX9OKZxaJQ,948
|
14
16
|
dictature/transformer/passthrough.py,sha256=Pt3N6G_Qh6HJ_q75ETL5nfAwYHLB-SjkVwUwbbbMik8,344
|
15
17
|
dictature/transformer/pipeline.py,sha256=OaQaJeJ5NpICetJe08r8ontqstsXGuW8jDbKw1zxYs4,842
|
16
|
-
dictature-0.
|
17
|
-
dictature-0.
|
18
|
-
dictature-0.
|
19
|
-
dictature-0.
|
20
|
-
dictature-0.
|
18
|
+
dictature-0.13.1.dist-info/LICENSE,sha256=n1U9DKr8sM5EY2QHcvxSGiKTDWUT8MyXsOC79w94MT0,1072
|
19
|
+
dictature-0.13.1.dist-info/METADATA,sha256=hJZ0-CPWRgnNPvMY2l3uhPiEO9-xuNDgSwUmVM0adfs,3394
|
20
|
+
dictature-0.13.1.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
21
|
+
dictature-0.13.1.dist-info/top_level.txt,sha256=-RO39WWCF44lqiXhSUcACVqbk6SkgReZTz7ZmHKH3-U,10
|
22
|
+
dictature-0.13.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|