dictature 0.11.0__py3-none-any.whl → 0.12.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/directory.py +7 -1
- dictature/backend/s3.py +176 -0
- {dictature-0.11.0.dist-info → dictature-0.12.1.dist-info}/METADATA +8 -2
- {dictature-0.11.0.dist-info → dictature-0.12.1.dist-info}/RECORD +7 -6
- {dictature-0.11.0.dist-info → dictature-0.12.1.dist-info}/LICENSE +0 -0
- {dictature-0.11.0.dist-info → dictature-0.12.1.dist-info}/WHEEL +0 -0
- {dictature-0.11.0.dist-info → dictature-0.12.1.dist-info}/top_level.txt +0 -0
dictature/backend/directory.py
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
import tempfile
|
2
|
+
import os
|
1
3
|
from pathlib import Path
|
2
4
|
from typing import Iterable, Union
|
3
5
|
from shutil import rmtree
|
@@ -48,7 +50,11 @@ class DictatureTableDirectory(DictatureTableMock):
|
|
48
50
|
|
49
51
|
def set(self, item: str, value: Value) -> None:
|
50
52
|
file_target = self.__item_path(item)
|
51
|
-
|
53
|
+
|
54
|
+
# Create temporary file for atomic write
|
55
|
+
handle, file_target_tmp_path = tempfile.mkstemp(prefix=file_target.name, suffix='.tmp', dir=file_target.parent)
|
56
|
+
os.close(handle)
|
57
|
+
file_target_tmp = Path(file_target_tmp_path)
|
52
58
|
|
53
59
|
save_data = self.__value_serializer.serialize(value)
|
54
60
|
|
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
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: dictature
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.12.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
|
@@ -13,7 +13,12 @@ License-File: LICENSE
|
|
13
13
|
|
14
14
|
# Dictature
|
15
15
|
|
16
|
-
A wrapper for Python's dictionary with multiple backends.
|
16
|
+
Make a Python dictionary out of anything. A wrapper for Python's dictionary with multiple backends.
|
17
|
+
Thread-safe, supports multiple backends, and allows for transformers to change how the data is stored.
|
18
|
+
|
19
|
+
SQLite, directory, webdav, and more backends are supported, and you can easily create your own backend.
|
20
|
+
|
21
|
+
[](https://badge.fury.io/py/dictature)
|
17
22
|
|
18
23
|
## Installation
|
19
24
|
|
@@ -55,6 +60,7 @@ Currently, the following backends are supported:
|
|
55
60
|
- `DictatureBackendSQLite`: stores the data in a SQLite database
|
56
61
|
- `DictatureBackendMISP`: stores the data in a MISP instance
|
57
62
|
- `DictatureBackendWebdav`: stores data in a WebDav share as files
|
63
|
+
- `DictatureBackendS3`: stores data in an S3 bucket
|
58
64
|
|
59
65
|
### Transformers
|
60
66
|
|
@@ -1,9 +1,10 @@
|
|
1
1
|
dictature/__init__.py,sha256=UCPJKHeyirRZ0pCYoyeat-rwXa8pDezOJ3UWCipDdyc,33
|
2
2
|
dictature/dictature.py,sha256=q4QFQz91r2boViuhsf6y-C9uRntaMD-VagKFUGZjnpQ,10015
|
3
3
|
dictature/backend/__init__.py,sha256=d5s6QCJOUzFglVNg8Cqqx_8b61S-AOTGjEUIF6FS69U,149
|
4
|
-
dictature/backend/directory.py,sha256=
|
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/s3.py,sha256=9Vcf4-RagyjpV9F4m9ZQ1vjb4lbZfEcOZQBuD_SPwZg,6580
|
7
8
|
dictature/backend/sqlite.py,sha256=zyphYEeLY4eGuBCor16i80_-brdipMpXZ3_kONwErsE,5237
|
8
9
|
dictature/backend/webdav.py,sha256=Y-3_WTcMyKVUnsVjiUZAxuy10FK0Yr-7Mgn13clg3po,5039
|
9
10
|
dictature/transformer/__init__.py,sha256=JIFJpXU6iB9hIUM8L7HL2o9Nqjm_YbMEuQBQC8ZJ6b4,124
|
@@ -13,8 +14,8 @@ dictature/transformer/hmac.py,sha256=vURsB0HlzRPn_Vkl7lGmZV9OKempQuds8AanmadDxIo
|
|
13
14
|
dictature/transformer/mock.py,sha256=7zu65ZqUV_AVRaPSzNd73cVMXixXt31SeuX9OKZxaJQ,948
|
14
15
|
dictature/transformer/passthrough.py,sha256=Pt3N6G_Qh6HJ_q75ETL5nfAwYHLB-SjkVwUwbbbMik8,344
|
15
16
|
dictature/transformer/pipeline.py,sha256=OaQaJeJ5NpICetJe08r8ontqstsXGuW8jDbKw1zxYs4,842
|
16
|
-
dictature-0.
|
17
|
-
dictature-0.
|
18
|
-
dictature-0.
|
19
|
-
dictature-0.
|
20
|
-
dictature-0.
|
17
|
+
dictature-0.12.1.dist-info/LICENSE,sha256=n1U9DKr8sM5EY2QHcvxSGiKTDWUT8MyXsOC79w94MT0,1072
|
18
|
+
dictature-0.12.1.dist-info/METADATA,sha256=FDWISHrqfqU3SooFoZ8xRyAQ2IF8ryNGbKZYed3cJyE,3335
|
19
|
+
dictature-0.12.1.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
20
|
+
dictature-0.12.1.dist-info/top_level.txt,sha256=-RO39WWCF44lqiXhSUcACVqbk6SkgReZTz7ZmHKH3-U,10
|
21
|
+
dictature-0.12.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|