awsimple 3.2.0__py3-none-any.whl → 3.4.0__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.
- awsimple/__version__.py +1 -1
- awsimple/aws.py +10 -26
- awsimple/s3.py +23 -12
- {awsimple-3.2.0.dist-info → awsimple-3.4.0.dist-info}/METADATA +10 -3
- {awsimple-3.2.0.dist-info → awsimple-3.4.0.dist-info}/RECORD +9 -9
- {awsimple-3.2.0.dist-info → awsimple-3.4.0.dist-info}/WHEEL +1 -1
- {awsimple-3.2.0.dist-info → awsimple-3.4.0.dist-info}/LICENSE +0 -0
- {awsimple-3.2.0.dist-info → awsimple-3.4.0.dist-info}/LICENSE.txt +0 -0
- {awsimple-3.2.0.dist-info → awsimple-3.4.0.dist-info}/top_level.txt +0 -0
awsimple/__version__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
__application_name__ = "awsimple"
|
|
2
2
|
__title__ = __application_name__
|
|
3
3
|
__author__ = "abel"
|
|
4
|
-
__version__ = "3.
|
|
4
|
+
__version__ = "3.4.0"
|
|
5
5
|
__author_email__ = "j@abel.co"
|
|
6
6
|
__url__ = "https://github.com/jamesabel/awsimple"
|
|
7
7
|
__download_url__ = "https://github.com/jamesabel/awsimple"
|
awsimple/aws.py
CHANGED
|
@@ -30,12 +30,12 @@ def boto_error_to_string(boto_error) -> Union[str, None]:
|
|
|
30
30
|
class AWSAccess:
|
|
31
31
|
@typechecked()
|
|
32
32
|
def __init__(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
self,
|
|
34
|
+
resource_name: Union[str, None] = None,
|
|
35
|
+
profile_name: Union[str, None] = None,
|
|
36
|
+
aws_access_key_id: Union[str, None] = None,
|
|
37
|
+
aws_secret_access_key: Union[str, None] = None,
|
|
38
|
+
region_name: Union[str, None] = None,
|
|
39
39
|
):
|
|
40
40
|
"""
|
|
41
41
|
AWSAccess - takes care of basic AWS access (e.g. session, client, resource), getting some basic AWS information, and mock support for testing.
|
|
@@ -79,20 +79,9 @@ class AWSAccess:
|
|
|
79
79
|
self._aws_keys_save[aws_key] = os.environ.get(aws_key) # will be None if not set
|
|
80
80
|
os.environ[aws_key] = "testing"
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
from moto import mock_s3 as moto_mock
|
|
84
|
-
elif self.resource_name == "sns":
|
|
85
|
-
from moto import mock_sns as moto_mock
|
|
86
|
-
elif self.resource_name == "sqs":
|
|
87
|
-
from moto import mock_sqs as moto_mock
|
|
88
|
-
elif self.resource_name == "dynamodb":
|
|
89
|
-
from moto import mock_dynamodb as moto_mock
|
|
90
|
-
elif self.resource_name == "logs":
|
|
91
|
-
from moto import mock_logs as moto_mock
|
|
92
|
-
else:
|
|
93
|
-
from moto import mock_iam as moto_mock
|
|
82
|
+
from moto import mock_aws
|
|
94
83
|
|
|
95
|
-
self._moto_mock =
|
|
84
|
+
self._moto_mock = mock_aws()
|
|
96
85
|
self._moto_mock.start()
|
|
97
86
|
region = "us-east-1"
|
|
98
87
|
if self.resource_name == "logs" or self.resource_name is None:
|
|
@@ -164,16 +153,11 @@ class AWSAccess:
|
|
|
164
153
|
|
|
165
154
|
def get_account_id(self):
|
|
166
155
|
"""
|
|
167
|
-
Get AWS account ID
|
|
156
|
+
Get AWS account ID *** HAS BEEN REMOVED ***
|
|
168
157
|
|
|
169
158
|
:return: account ID
|
|
170
159
|
"""
|
|
171
|
-
|
|
172
|
-
arn = self.session.resource("iam", endpoint_url=self._get_localstack_endpoint_url()).CurrentUser().arn
|
|
173
|
-
else:
|
|
174
|
-
arn = self.session.resource("iam").CurrentUser().arn
|
|
175
|
-
log.info("current user {arn=}")
|
|
176
|
-
return arn.split(":")[4]
|
|
160
|
+
raise NotImplementedError(".get_account_id() has been removed")
|
|
177
161
|
|
|
178
162
|
def test(self) -> bool:
|
|
179
163
|
"""
|
awsimple/s3.py
CHANGED
|
@@ -13,10 +13,11 @@ from typing import Dict, List, Union
|
|
|
13
13
|
import json
|
|
14
14
|
from logging import getLogger
|
|
15
15
|
|
|
16
|
+
import boto3
|
|
17
|
+
from botocore.client import Config
|
|
16
18
|
from botocore.exceptions import ClientError, EndpointConnectionError, ConnectionClosedError, SSLError
|
|
17
19
|
from boto3.s3.transfer import TransferConfig
|
|
18
20
|
from s3transfer import S3UploadFailedError
|
|
19
|
-
import urllib3
|
|
20
21
|
import urllib3.exceptions
|
|
21
22
|
from typeguard import typechecked
|
|
22
23
|
from hashy import get_string_sha512, get_file_sha512, get_bytes_sha512, get_dls_sha512 # type: ignore
|
|
@@ -31,6 +32,8 @@ json_extension = ".json"
|
|
|
31
32
|
|
|
32
33
|
log = getLogger(__application_name__)
|
|
33
34
|
|
|
35
|
+
connection_errors = (S3UploadFailedError, ClientError, EndpointConnectionError, SSLError, urllib3.exceptions.ProtocolError, ConnectionClosedError)
|
|
36
|
+
|
|
34
37
|
|
|
35
38
|
class BucketNotFound(AWSimpleException):
|
|
36
39
|
def __init__(self, bucket_name):
|
|
@@ -230,7 +233,7 @@ class S3Access(CacheAccess):
|
|
|
230
233
|
try:
|
|
231
234
|
self.client.upload_file(str(file_path), self.bucket_name, s3_key, ExtraArgs=extra_args, Config=self.get_s3_transfer_config())
|
|
232
235
|
uploaded_flag = True
|
|
233
|
-
except
|
|
236
|
+
except connection_errors as e:
|
|
234
237
|
log.warning(f"{file_path} to {self.bucket_name}:{s3_key} : {transfer_retry_count=} : {e}")
|
|
235
238
|
time.sleep(self.retry_sleep_time)
|
|
236
239
|
except RuntimeError as e:
|
|
@@ -282,7 +285,7 @@ class S3Access(CacheAccess):
|
|
|
282
285
|
else:
|
|
283
286
|
s3_object.put(Body=json_as_bytes, Metadata=meta_data)
|
|
284
287
|
uploaded_flag = True
|
|
285
|
-
except
|
|
288
|
+
except connection_errors as e:
|
|
286
289
|
log.warning(f"{self.bucket_name}:{s3_key} : {transfer_retry_count=} : {e}")
|
|
287
290
|
transfer_retry_count += 1
|
|
288
291
|
time.sleep(self.retry_sleep_time)
|
|
@@ -298,17 +301,18 @@ class S3Access(CacheAccess):
|
|
|
298
301
|
Download an S3 object
|
|
299
302
|
|
|
300
303
|
:param s3_key: S3 key
|
|
301
|
-
:param dest_path: destination file path
|
|
304
|
+
:param dest_path: destination file or directory path. If the path is a directory, the file will be downloaded to that directory with the same name as the S3 key.
|
|
302
305
|
:return: True if downloaded successfully
|
|
303
306
|
"""
|
|
304
307
|
|
|
305
308
|
if isinstance(dest_path, str):
|
|
306
309
|
log.info(f"{dest_path} is not Path object. Non-Path objects will be deprecated in the future")
|
|
307
310
|
|
|
308
|
-
|
|
309
|
-
|
|
311
|
+
assert isinstance(dest_path, Path)
|
|
312
|
+
if dest_path.is_dir():
|
|
313
|
+
dest_path = Path(dest_path, s3_key)
|
|
310
314
|
|
|
311
|
-
log.info(f'S3 download : {self.bucket_name}
|
|
315
|
+
log.info(f'S3 download : {self.bucket_name}:{s3_key} to "{dest_path}" ("{Path(dest_path).absolute()}")')
|
|
312
316
|
|
|
313
317
|
Path(dest_path).parent.mkdir(parents=True, exist_ok=True)
|
|
314
318
|
|
|
@@ -324,7 +328,7 @@ class S3Access(CacheAccess):
|
|
|
324
328
|
mtime_ts = s3_object_metadata.mtime.timestamp()
|
|
325
329
|
os.utime(dest_path, (mtime_ts, mtime_ts)) # set the file mtime to the mtime in S3
|
|
326
330
|
success = True
|
|
327
|
-
except
|
|
331
|
+
except connection_errors as e:
|
|
328
332
|
# ProtocolError can happen for a broken connection
|
|
329
333
|
log.warning(f"{self.bucket_name}/{s3_key} to {dest_path} ({Path(dest_path).absolute()}) : {transfer_retry_count=} : {e}")
|
|
330
334
|
time.sleep(self.retry_sleep_time)
|
|
@@ -337,11 +341,15 @@ class S3Access(CacheAccess):
|
|
|
337
341
|
"""
|
|
338
342
|
download from AWS S3 with caching
|
|
339
343
|
|
|
340
|
-
:param dest_path: destination full path
|
|
344
|
+
:param dest_path: destination full path or directory. If the path is a directory, the file will be downloaded to that directory with the same name as the S3 key.
|
|
341
345
|
:param s3_key: S3 key of source
|
|
342
346
|
:return: S3DownloadStatus instance
|
|
343
347
|
"""
|
|
344
348
|
|
|
349
|
+
if dest_path.is_dir():
|
|
350
|
+
dest_path = Path(dest_path, s3_key)
|
|
351
|
+
log.info(f'S3 download_cached : {self.bucket_name}:{s3_key} to "{dest_path}" ("{dest_path.absolute()}")')
|
|
352
|
+
|
|
345
353
|
self.download_status = S3DownloadStatus() # init
|
|
346
354
|
|
|
347
355
|
s3_object_metadata = self.get_s3_object_metadata(s3_key)
|
|
@@ -382,7 +390,6 @@ class S3Access(CacheAccess):
|
|
|
382
390
|
"""
|
|
383
391
|
download object from AWS S3 with caching
|
|
384
392
|
|
|
385
|
-
:param dest_path: destination full path
|
|
386
393
|
:param s3_key: S3 key of source
|
|
387
394
|
:return: S3DownloadStatus instance
|
|
388
395
|
"""
|
|
@@ -467,7 +474,6 @@ class S3Access(CacheAccess):
|
|
|
467
474
|
"""
|
|
468
475
|
determine if an s3 object exists
|
|
469
476
|
|
|
470
|
-
:param s3_bucket: the S3 bucket
|
|
471
477
|
:param s3_key: the S3 object key
|
|
472
478
|
:return: True if object exists
|
|
473
479
|
"""
|
|
@@ -485,8 +491,13 @@ class S3Access(CacheAccess):
|
|
|
485
491
|
|
|
486
492
|
:return: True if bucket exists
|
|
487
493
|
"""
|
|
494
|
+
|
|
495
|
+
# use a "custom" config so that .head_bucket() doesn't take a really long time if the bucket does not exist
|
|
496
|
+
config = Config(connect_timeout=5, retries={"max_attempts": 3, "mode": "standard"})
|
|
497
|
+
s3 = boto3.client("s3", config=config)
|
|
498
|
+
assert self.bucket_name is not None
|
|
488
499
|
try:
|
|
489
|
-
|
|
500
|
+
s3.head_bucket(Bucket=self.bucket_name)
|
|
490
501
|
exists = True
|
|
491
502
|
except ClientError as e:
|
|
492
503
|
log.info(f"{self.bucket_name=}{e=}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: awsimple
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.0
|
|
4
4
|
Summary: Simple AWS API for S3, DynamoDB, SNS, and SQS
|
|
5
5
|
Home-page: https://github.com/jamesabel/awsimple
|
|
6
6
|
Download-URL: https://github.com/jamesabel/awsimple
|
|
@@ -14,8 +14,8 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
License-File: LICENSE.txt
|
|
16
16
|
Requires-Dist: boto3
|
|
17
|
-
Requires-Dist: typeguard
|
|
18
|
-
Requires-Dist: hashy
|
|
17
|
+
Requires-Dist: typeguard<3
|
|
18
|
+
Requires-Dist: hashy>=0.1.1
|
|
19
19
|
Requires-Dist: dictim
|
|
20
20
|
Requires-Dist: appdirs
|
|
21
21
|
Requires-Dist: tobool
|
|
@@ -135,6 +135,8 @@ to test for file equivalency.
|
|
|
135
135
|
S3 objects and DynamoDB tables can be cached locally to reduce network traffic, minimize AWS costs,
|
|
136
136
|
and potentially offer a speedup.
|
|
137
137
|
|
|
138
|
+
DynamoDB cached table scans are particularly useful for tables that are infrequently updated.
|
|
139
|
+
|
|
138
140
|
## What`awsimple` Is Not
|
|
139
141
|
|
|
140
142
|
- `awsimple` is not necessarily the most memory and CPU efficient
|
|
@@ -143,6 +145,11 @@ and potentially offer a speedup.
|
|
|
143
145
|
|
|
144
146
|
- `awsimple` does not provide all the options and features that the regular AWS API (e.g. boto3) does
|
|
145
147
|
|
|
148
|
+
## Updates/Releases
|
|
149
|
+
|
|
150
|
+
3.x.x - Cache life for cached DynamoDB scans is now based on most recent table modification time (kept in a separate
|
|
151
|
+
table). Explict cache life is no longer required (parameter has been removed).
|
|
152
|
+
|
|
146
153
|
## Testing using moto mock and localstack
|
|
147
154
|
|
|
148
155
|
moto mock-ing can improve performance and reduce AWS costs. `awsimple` supports both moto mock and localstack.
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
awsimple/__init__.py,sha256=8aFfqWFAvRPweoZkKncvHAW2ytTW_5-AJ0nnmYqgUBw,916
|
|
2
|
-
awsimple/__version__.py,sha256=
|
|
3
|
-
awsimple/aws.py,sha256=
|
|
2
|
+
awsimple/__version__.py,sha256=QeAaw-gHVlOTWxX7VxuppELAYFtQHcjChg8cH2N11_Q,323
|
|
3
|
+
awsimple/aws.py,sha256=n5Mte2l0uUyLtxHx-Cv2RdVF2H2pvNiQPlrwrwddKcc,7636
|
|
4
4
|
awsimple/cache.py,sha256=tdLeMw2IYW9Y4lGT2SAGUI7u_aTX_TFQs2udXcqW6fI,7163
|
|
5
5
|
awsimple/dynamodb.py,sha256=xVPnRdedm19ORpmC1G0fMaQMnf9D72Ebq2lXKSLgtmc,39076
|
|
6
6
|
awsimple/dynamodb_miv.py,sha256=4xPxQDYkIM-BVDGyAre6uqwJHsxguEbHbof8ztt-V7g,4645
|
|
7
7
|
awsimple/logs.py,sha256=A2RmTT90pfFTthfENd7GSsEHSIBJXO8ICHPdA7sEsHY,4278
|
|
8
8
|
awsimple/mock.py,sha256=eScbnxFF9xAosOAsL-NZgp_P-fezB6StQMkb85Y3TNo,574
|
|
9
9
|
awsimple/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
awsimple/s3.py,sha256=
|
|
10
|
+
awsimple/s3.py,sha256=lwMS8Xr06TB2LkQ7z8yaK6pnH9oOFU3-DEI3Ba6dEwo,23874
|
|
11
11
|
awsimple/sns.py,sha256=dOx3VUS04xxeG1krGudN4A5fqoIpXeHqXNkBvfbr_6Q,3292
|
|
12
12
|
awsimple/sqs.py,sha256=ejV9twP15X8-mZ9IHGEUlYWqufEcasYuPf1xlGQt2a8,15506
|
|
13
|
-
awsimple-3.
|
|
14
|
-
awsimple-3.
|
|
15
|
-
awsimple-3.
|
|
16
|
-
awsimple-3.
|
|
17
|
-
awsimple-3.
|
|
18
|
-
awsimple-3.
|
|
13
|
+
awsimple-3.4.0.dist-info/LICENSE,sha256=d956YAgtDaxgxQmccyUk__EfhORZyBjvmJ8pq6zYTxk,1093
|
|
14
|
+
awsimple-3.4.0.dist-info/LICENSE.txt,sha256=d956YAgtDaxgxQmccyUk__EfhORZyBjvmJ8pq6zYTxk,1093
|
|
15
|
+
awsimple-3.4.0.dist-info/METADATA,sha256=nNYscDeRiwPL6X9UU36J7foOlyC9oiKUawl8VLdimGw,6087
|
|
16
|
+
awsimple-3.4.0.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
|
|
17
|
+
awsimple-3.4.0.dist-info/top_level.txt,sha256=mwqCoH_8vAaK6iYioiRbasXmVCQM7AK3grNWOKp2VHE,9
|
|
18
|
+
awsimple-3.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|