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 ADDED
@@ -0,0 +1,11 @@
1
+ from typing import Any
2
+
3
+ from .adapter import S3Adapter
4
+
5
+
6
+ def adapter(**kwargs: Any) -> S3Adapter:
7
+ """Factory helper for creating a S3 adapter."""
8
+ return S3Adapter(**kwargs)
9
+
10
+
11
+ __all__ = ["adapter", "S3Adapter"]
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