dictature 0.11.1__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.
@@ -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.11.1
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
@@ -60,6 +60,7 @@ 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
63
64
 
64
65
  ### Transformers
65
66
 
@@ -4,6 +4,7 @@ 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/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.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,,
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,,