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.
@@ -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()
@@ -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
@@ -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 'tb_%'")
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 = "`tb_%s`" % name.replace('`', '``')
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.11.1
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/sqlite.py,sha256=zyphYEeLY4eGuBCor16i80_-brdipMpXZ3_kONwErsE,5237
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.11.1.dist-info/LICENSE,sha256=n1U9DKr8sM5EY2QHcvxSGiKTDWUT8MyXsOC79w94MT0,1072
17
- dictature-0.11.1.dist-info/METADATA,sha256=b-VVWD1Tgz0GwX0OvoWHPCtx-g117pYEbyEWyNeBKbE,3283
18
- dictature-0.11.1.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
19
- dictature-0.11.1.dist-info/top_level.txt,sha256=-RO39WWCF44lqiXhSUcACVqbk6SkgReZTz7ZmHKH3-U,10
20
- dictature-0.11.1.dist-info/RECORD,,
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,,