daplug-s3 1.0.0b1__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.
- daplug_s3/__init__.py +11 -0
- daplug_s3/adapter.py +233 -0
- daplug_s3/types/__init__.py +39 -0
- daplug_s3/types/s3.py +87 -0
- daplug_s3-1.0.0b1.dist-info/METADATA +407 -0
- daplug_s3-1.0.0b1.dist-info/RECORD +21 -0
- daplug_s3-1.0.0b1.dist-info/WHEEL +5 -0
- daplug_s3-1.0.0b1.dist-info/licenses/LICENSE +201 -0
- daplug_s3-1.0.0b1.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/integrations/__init__.py +1 -0
- tests/integrations/conftest.py +98 -0
- tests/integrations/mocks/__init__.py +1 -0
- tests/integrations/test_adapter_integration.py +66 -0
- tests/unit/__init__.py +1 -0
- tests/unit/conftest.py +52 -0
- tests/unit/mocks/__init__.py +1 -0
- tests/unit/mocks/base_adapter.py +20 -0
- tests/unit/test___init__.py +27 -0
- tests/unit/test_adapter.py +213 -0
- tests/unit/test_types.py +69 -0
daplug_s3/__init__.py
ADDED
daplug_s3/adapter.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
|
|
4
|
+
import boto3
|
|
5
|
+
import botocore
|
|
6
|
+
import jsonpickle
|
|
7
|
+
|
|
8
|
+
from botocore.config import Config
|
|
9
|
+
from botocore.exceptions import ClientError
|
|
10
|
+
|
|
11
|
+
from daplug_core.base_adapter import BaseAdapter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class S3Adapter(BaseAdapter):
|
|
15
|
+
|
|
16
|
+
def __init__(self, **kwargs):
|
|
17
|
+
super().__init__(**kwargs)
|
|
18
|
+
self.endpoint = kwargs.get('endpoint')
|
|
19
|
+
self.bucket = kwargs.get('bucket')
|
|
20
|
+
self.aws_access_key_id = kwargs.get('aws_access_key_id')
|
|
21
|
+
self.aws_secret_access_key = kwargs.get('aws_secret_access_key')
|
|
22
|
+
self.region = kwargs.get('region')
|
|
23
|
+
self.client = self.__make_client()
|
|
24
|
+
self.resource = self.__make_resource()
|
|
25
|
+
|
|
26
|
+
def __make_client(self, config=None):
|
|
27
|
+
return boto3.client(
|
|
28
|
+
's3',
|
|
29
|
+
endpoint_url=self.endpoint,
|
|
30
|
+
aws_access_key_id=self.aws_access_key_id,
|
|
31
|
+
aws_secret_access_key=self.aws_secret_access_key,
|
|
32
|
+
region_name=self.region,
|
|
33
|
+
config=config
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def __make_resource(self, config=None):
|
|
37
|
+
return boto3.resource(
|
|
38
|
+
's3',
|
|
39
|
+
endpoint_url=self.endpoint,
|
|
40
|
+
aws_access_key_id=self.aws_access_key_id,
|
|
41
|
+
aws_secret_access_key=self.aws_secret_access_key,
|
|
42
|
+
region_name=self.region,
|
|
43
|
+
config=config
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def create(self, **kwargs):
|
|
47
|
+
result = self.put(**kwargs)
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
def put(self, **kwargs):
|
|
51
|
+
acl = self.__set_acl(kwargs.get('public_read', False))
|
|
52
|
+
body = self.__set_body(**kwargs)
|
|
53
|
+
results = self.client.put_object(
|
|
54
|
+
ACL=acl,
|
|
55
|
+
Body=body,
|
|
56
|
+
Bucket=self.bucket,
|
|
57
|
+
Key=kwargs['s3_path']
|
|
58
|
+
)
|
|
59
|
+
super().publish(db_data=self.__generate_publish_data(**kwargs), **kwargs)
|
|
60
|
+
return results
|
|
61
|
+
|
|
62
|
+
def upload_stream(self, **kwargs):
|
|
63
|
+
conf = boto3.s3.transfer.TransferConfig(
|
|
64
|
+
multipart_threshold=kwargs.get('threshold', 10000),
|
|
65
|
+
max_concurrency=kwargs.get('concurrency', 4)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if kwargs.get('io'):
|
|
69
|
+
self.client.upload_fileobj(
|
|
70
|
+
kwargs['io'],
|
|
71
|
+
self.bucket,
|
|
72
|
+
kwargs['s3_path'],
|
|
73
|
+
ExtraArgs={
|
|
74
|
+
'ACL': self.__set_acl(kwargs.get('public_read'))
|
|
75
|
+
},
|
|
76
|
+
Config=conf
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
with BytesIO(bytes(kwargs['data'])) as data:
|
|
80
|
+
self.client.upload_fileobj(
|
|
81
|
+
data,
|
|
82
|
+
self.bucket,
|
|
83
|
+
kwargs['s3_path'],
|
|
84
|
+
ExtraArgs={
|
|
85
|
+
'ACL': self.__set_acl(kwargs.get('public_read'))
|
|
86
|
+
},
|
|
87
|
+
Config=conf
|
|
88
|
+
)
|
|
89
|
+
super().publish(db_data=self.__generate_publish_data(**kwargs), **kwargs)
|
|
90
|
+
|
|
91
|
+
def read(self, **kwargs):
|
|
92
|
+
return self.get(**kwargs)
|
|
93
|
+
|
|
94
|
+
def get(self, **kwargs):
|
|
95
|
+
results = self.client.get_object(
|
|
96
|
+
Bucket=self.bucket,
|
|
97
|
+
Key=kwargs['s3_path']
|
|
98
|
+
)
|
|
99
|
+
return self.__set_results(results, **kwargs)
|
|
100
|
+
|
|
101
|
+
def download(self, **kwargs):
|
|
102
|
+
self.__create_download_directory(kwargs['download_path'])
|
|
103
|
+
self.client.download_file(self.bucket, kwargs['s3_path'], kwargs['download_path'])
|
|
104
|
+
return kwargs['download_path']
|
|
105
|
+
|
|
106
|
+
def multipart_upload(self, **kwargs):
|
|
107
|
+
multipart = self.client.create_multipart_upload(Bucket=self.bucket, Key=kwargs['s3_path'])
|
|
108
|
+
parts = []
|
|
109
|
+
for part, chunk in enumerate(kwargs['chunks']):
|
|
110
|
+
part_number = part + 1 # @NOTE must be an integer between 1 and 10000, inclusive
|
|
111
|
+
part_response = self.__upload_part(
|
|
112
|
+
chunk=chunk, s3_path=kwargs['s3_path'], upload_id=multipart['UploadId'], part_number=part_number)
|
|
113
|
+
parts.append({'ETag': part_response['ETag'], 'PartNumber': part_number})
|
|
114
|
+
complete_response = self.__complete_multipart_upload(
|
|
115
|
+
s3_path=kwargs['s3_path'], parts=parts, upload_id=multipart['UploadId'])
|
|
116
|
+
super().publish(db_data=self.__generate_publish_data(**kwargs), **kwargs)
|
|
117
|
+
return complete_response
|
|
118
|
+
|
|
119
|
+
def create_public_url(self, **kwargs):
|
|
120
|
+
# we create a new client here to not modify other method calls
|
|
121
|
+
config = Config(signature_version=botocore.UNSIGNED)
|
|
122
|
+
client = self.__make_client(config=config)
|
|
123
|
+
return client.generate_presigned_url(
|
|
124
|
+
'get_object',
|
|
125
|
+
Params={
|
|
126
|
+
'Bucket': self.bucket,
|
|
127
|
+
'Key': kwargs['s3_path']
|
|
128
|
+
},
|
|
129
|
+
ExpiresIn=0,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def create_presigned_read_url(self, **kwargs):
|
|
133
|
+
results = self.client.generate_presigned_url(
|
|
134
|
+
'get_object',
|
|
135
|
+
Params={
|
|
136
|
+
'Bucket': self.bucket,
|
|
137
|
+
'Key': kwargs['s3_path']
|
|
138
|
+
},
|
|
139
|
+
ExpiresIn=kwargs.get('expiration', 3600)
|
|
140
|
+
)
|
|
141
|
+
return results
|
|
142
|
+
|
|
143
|
+
def create_presigned_post_url(self, **kwargs):
|
|
144
|
+
results = self.client.generate_presigned_post(
|
|
145
|
+
self.bucket,
|
|
146
|
+
kwargs['s3_path'],
|
|
147
|
+
Fields=kwargs.get('required_fields'),
|
|
148
|
+
Conditions=kwargs.get('required_conditions'),
|
|
149
|
+
ExpiresIn=kwargs.get('expiration', 3600)
|
|
150
|
+
)
|
|
151
|
+
return results
|
|
152
|
+
|
|
153
|
+
def object_exist(self, **kwargs):
|
|
154
|
+
try:
|
|
155
|
+
self.resource.Object(self.bucket, kwargs['s3_path']).load()
|
|
156
|
+
except ClientError as error:
|
|
157
|
+
if error.response['Error']['Code'] == '404':
|
|
158
|
+
return False
|
|
159
|
+
raise
|
|
160
|
+
|
|
161
|
+
return True
|
|
162
|
+
|
|
163
|
+
def list_dir_subfolders(self, **kwargs):
|
|
164
|
+
result = self.client.list_objects(Bucket=self.bucket, Prefix=kwargs.get('dir_name'), Delimiter='/')
|
|
165
|
+
return [obj['Prefix'] for obj in result['CommonPrefixes']]
|
|
166
|
+
|
|
167
|
+
def list_dir_files(self, **kwargs):
|
|
168
|
+
paginator = self.client.get_paginator('list_objects_v2')
|
|
169
|
+
pages = paginator.paginate(Bucket=self.bucket, Prefix=kwargs.get('dir_name'))
|
|
170
|
+
if kwargs.get('date'):
|
|
171
|
+
return [obj['Key'] for page in pages for obj in page['Contents'] if obj['LastModified'].replace(tzinfo=None) > kwargs.get('date')]
|
|
172
|
+
return [obj['Key'] for page in pages for obj in page['Contents']]
|
|
173
|
+
|
|
174
|
+
def rename_object(self, **kwargs):
|
|
175
|
+
old_file_key = kwargs.get('old_file_name')
|
|
176
|
+
self.resource.Object(self.bucket, kwargs.get('new_file_name')).copy_from(
|
|
177
|
+
CopySource={'Bucket': self.bucket, 'Key': old_file_key})
|
|
178
|
+
self.delete(s3_path=old_file_key)
|
|
179
|
+
|
|
180
|
+
def delete(self, **kwargs):
|
|
181
|
+
result = self.client.delete_object(
|
|
182
|
+
Bucket=self.bucket,
|
|
183
|
+
Key=kwargs['s3_path']
|
|
184
|
+
)
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
def __upload_part(self, **kwargs):
|
|
188
|
+
response = self.client.upload_part(
|
|
189
|
+
Body=kwargs['chunk'],
|
|
190
|
+
Bucket=self.bucket,
|
|
191
|
+
Key=kwargs['s3_path'],
|
|
192
|
+
UploadId=kwargs['upload_id'],
|
|
193
|
+
PartNumber=kwargs['part_number']
|
|
194
|
+
)
|
|
195
|
+
return response
|
|
196
|
+
|
|
197
|
+
def __complete_multipart_upload(self, **kwargs):
|
|
198
|
+
response = self.client.complete_multipart_upload(
|
|
199
|
+
Bucket=self.bucket,
|
|
200
|
+
Key=kwargs['s3_path'],
|
|
201
|
+
MultipartUpload={'Parts': kwargs['parts']},
|
|
202
|
+
UploadId=kwargs['upload_id']
|
|
203
|
+
)
|
|
204
|
+
return response
|
|
205
|
+
|
|
206
|
+
def __create_download_directory(self, download_path):
|
|
207
|
+
os.makedirs(os.path.dirname(download_path), exist_ok=True)
|
|
208
|
+
with open(download_path, 'wb') as path:
|
|
209
|
+
path.close()
|
|
210
|
+
|
|
211
|
+
def __set_results(self, results, **kwargs):
|
|
212
|
+
if kwargs.get('json'):
|
|
213
|
+
body = results['Body'].read().decode('utf-8')
|
|
214
|
+
return jsonpickle.decode(body)
|
|
215
|
+
if kwargs.get('decode', True):
|
|
216
|
+
return results['Body'].read().decode('utf-8')
|
|
217
|
+
return results
|
|
218
|
+
|
|
219
|
+
def __set_body(self, **kwargs):
|
|
220
|
+
data = kwargs['data']
|
|
221
|
+
if kwargs.get('json'):
|
|
222
|
+
data = jsonpickle.dumps(data, unpicklable=False, use_decimal=True)
|
|
223
|
+
if kwargs.get('encode', True):
|
|
224
|
+
data = bytes(data.encode('UTF-8'))
|
|
225
|
+
return data
|
|
226
|
+
|
|
227
|
+
def __set_acl(self, public_read):
|
|
228
|
+
if public_read:
|
|
229
|
+
return 'public-read'
|
|
230
|
+
return 'private'
|
|
231
|
+
|
|
232
|
+
def __generate_publish_data(self, **kwargs):
|
|
233
|
+
return {'presigned_url': self.create_presigned_read_url(**kwargs)}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Project-specific typing helpers."""
|
|
2
|
+
|
|
3
|
+
from .s3 import (
|
|
4
|
+
AdapterConfig,
|
|
5
|
+
DownloadKwargs,
|
|
6
|
+
GetKwargs,
|
|
7
|
+
JSONArray,
|
|
8
|
+
JSONObject,
|
|
9
|
+
JSONScalar,
|
|
10
|
+
JSONValue,
|
|
11
|
+
ListDirFilesKwargs,
|
|
12
|
+
ListDirSubfoldersKwargs,
|
|
13
|
+
MultipartUploadKwargs,
|
|
14
|
+
PresignedPostKwargs,
|
|
15
|
+
PresignedReadKwargs,
|
|
16
|
+
PublishMetadata,
|
|
17
|
+
PutKwargs,
|
|
18
|
+
RenameKwargs,
|
|
19
|
+
StreamUploadKwargs,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"AdapterConfig",
|
|
24
|
+
"DownloadKwargs",
|
|
25
|
+
"GetKwargs",
|
|
26
|
+
"JSONArray",
|
|
27
|
+
"JSONObject",
|
|
28
|
+
"JSONScalar",
|
|
29
|
+
"JSONValue",
|
|
30
|
+
"ListDirFilesKwargs",
|
|
31
|
+
"ListDirSubfoldersKwargs",
|
|
32
|
+
"MultipartUploadKwargs",
|
|
33
|
+
"PresignedPostKwargs",
|
|
34
|
+
"PresignedReadKwargs",
|
|
35
|
+
"PublishMetadata",
|
|
36
|
+
"PutKwargs",
|
|
37
|
+
"RenameKwargs",
|
|
38
|
+
"StreamUploadKwargs",
|
|
39
|
+
]
|
daplug_s3/types/s3.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import BinaryIO, Mapping, Sequence, TypedDict, Union
|
|
5
|
+
|
|
6
|
+
JSONScalar = Union[str, int, float, bool, None]
|
|
7
|
+
JSONArray = Sequence["JSONValue"]
|
|
8
|
+
JSONObject = Mapping[str, "JSONValue"]
|
|
9
|
+
JSONValue = Union[JSONScalar, JSONArray, JSONObject]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AdapterConfig(TypedDict, total=False):
|
|
13
|
+
endpoint: str | None
|
|
14
|
+
bucket: str
|
|
15
|
+
aws_access_key_id: str
|
|
16
|
+
aws_secret_access_key: str
|
|
17
|
+
region: str
|
|
18
|
+
sns_arn: str
|
|
19
|
+
sns_endpoint: str
|
|
20
|
+
sns_attributes: Mapping[str, JSONScalar]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PutKwargs(TypedDict, total=False):
|
|
24
|
+
s3_path: str
|
|
25
|
+
data: Union[JSONValue, bytes, str]
|
|
26
|
+
public_read: bool
|
|
27
|
+
json: bool
|
|
28
|
+
encode: bool
|
|
29
|
+
publish: bool
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class StreamUploadKwargs(TypedDict, total=False):
|
|
33
|
+
s3_path: str
|
|
34
|
+
io: BinaryIO
|
|
35
|
+
data: bytes
|
|
36
|
+
threshold: int
|
|
37
|
+
concurrency: int
|
|
38
|
+
public_read: bool
|
|
39
|
+
publish: bool
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class GetKwargs(TypedDict, total=False):
|
|
43
|
+
s3_path: str
|
|
44
|
+
json: bool
|
|
45
|
+
decode: bool
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DownloadKwargs(TypedDict, total=False):
|
|
49
|
+
s3_path: str
|
|
50
|
+
download_path: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MultipartUploadKwargs(TypedDict, total=False):
|
|
54
|
+
s3_path: str
|
|
55
|
+
chunks: Sequence[bytes]
|
|
56
|
+
publish: bool
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PresignedReadKwargs(TypedDict, total=False):
|
|
60
|
+
s3_path: str
|
|
61
|
+
expiration: int
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PresignedPostKwargs(TypedDict, total=False):
|
|
65
|
+
s3_path: str
|
|
66
|
+
required_fields: Mapping[str, str]
|
|
67
|
+
required_conditions: Sequence[Mapping[str, str]]
|
|
68
|
+
expiration: int
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ListDirSubfoldersKwargs(TypedDict, total=False):
|
|
72
|
+
dir_name: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ListDirFilesKwargs(TypedDict, total=False):
|
|
76
|
+
dir_name: str
|
|
77
|
+
date: datetime
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class RenameKwargs(TypedDict, total=False):
|
|
81
|
+
old_file_name: str
|
|
82
|
+
new_file_name: str
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class PublishMetadata(TypedDict):
|
|
86
|
+
action: str
|
|
87
|
+
presigned_url: str
|