awsimple 3.11.0__py3-none-any.whl → 4.1.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.

Potentially problematic release.


This version of awsimple might be problematic. Click here for more details.

awsimple/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from .__version__ import __application_name__, __version__, __author__, __title__
2
2
  from .mock import use_moto_mock_env_var, is_mock, use_localstack_env_var, is_using_localstack
3
- from .exceptions import DynamoDBItemAlreadyExists
3
+ from .exceptions import DynamoDBItemAlreadyExists, S3BucketAlreadyExistsNotOwnedByYou
4
4
  from .aws import AWSAccess, AWSimpleException, boto_error_to_string
5
5
  from .cache import get_disk_free, get_directory_size, lru_cache_write, CacheAccess, CACHE_DIR_ENV_VAR
6
6
  from .dynamodb import DynamoDBAccess, dict_to_dynamodb, DBItemNotFound, DynamoDBTableNotFound, dynamodb_to_json, dynamodb_to_dict, QuerySelection, DictKey, convert_serializable_special_cases
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.11.0"
4
+ __version__ = "4.1.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/dynamodb.py CHANGED
@@ -9,7 +9,7 @@ from collections import OrderedDict, defaultdict, namedtuple
9
9
  import datetime
10
10
  from pathlib import Path
11
11
  from os.path import getsize, getmtime
12
- from typing import List, Union, Any, Type, Dict, Callable, Literal
12
+ from typing import List, Union, Any, Type, Dict, Callable
13
13
  from pprint import pformat
14
14
  from itertools import islice
15
15
  import json
@@ -21,12 +21,10 @@ from collections.abc import Iterable
21
21
  from logging import getLogger
22
22
 
23
23
 
24
- import boto3
25
24
  from botocore.exceptions import EndpointConnectionError, ClientError
26
25
  from boto3.dynamodb.conditions import Key
27
26
  from typeguard import typechecked
28
27
  from dictim import dictim # type: ignore
29
- from yasf import sf
30
28
 
31
29
  from . import CacheAccess, __application_name__, AWSimpleException
32
30
  from .exceptions import DynamoDBItemAlreadyExists
@@ -48,6 +46,9 @@ handle_inexact_error = True
48
46
  # for scan to dict
49
47
  DictKey = namedtuple("DictKey", ["partition", "sort"]) # only for Primary Key with both partition and sort keys
50
48
 
49
+ metadata_table_name = f"__{__application_name__}_metadata__"
50
+ dynamodb_name = "dynamodb"
51
+
51
52
  log = getLogger(__application_name__)
52
53
 
53
54
 
@@ -237,8 +238,12 @@ class DynamoDBAccess(CacheAccess):
237
238
  self.secondary_index_postfix = "-index"
238
239
 
239
240
  self.table_name = table_name # can be None (the default) if we're only doing things that don't require a table name such as get_table_names()
240
- if self.table_name is not None:
241
- self.metadata_table = _DynamoDBMetadataTable(self.table_name)
241
+
242
+ # avoid recursion
243
+ if self.table_name is not None and self.table_name != metadata_table_name:
244
+ self.metadata_table = _DynamoDBMetadataTable(dynamodb_name, self.table_name, **kwargs)
245
+ else:
246
+ self.metadata_table = None
242
247
 
243
248
  super().__init__(resource_name="dynamodb", **kwargs)
244
249
 
@@ -513,7 +518,8 @@ class DynamoDBAccess(CacheAccess):
513
518
  created = True
514
519
  except ClientError as e:
515
520
  log.warning(e)
516
- self.metadata_table.update_table_mtime()
521
+ if self.metadata_table is not None:
522
+ self.metadata_table.update_table_mtime()
517
523
 
518
524
  return created
519
525
 
@@ -734,7 +740,8 @@ class DynamoDBAccess(CacheAccess):
734
740
  try:
735
741
  table = self.resource.Table(self.table_name)
736
742
  table.put_item(Item=item)
737
- self.metadata_table.update_table_mtime()
743
+ if self.metadata_table is not None:
744
+ self.metadata_table.update_table_mtime()
738
745
  except self.client.exceptions.ResourceNotFoundException:
739
746
  raise DynamoDBTableNotFound(str(self.table_name))
740
747
 
@@ -757,7 +764,8 @@ class DynamoDBAccess(CacheAccess):
757
764
  ConditionExpression="attribute_not_exists(#pk) AND attribute_not_exists(#sk)",
758
765
  ExpressionAttributeNames={"#pk": primary_partition_key, "#sk": primary_sort_key},
759
766
  )
760
- self.metadata_table.update_table_mtime()
767
+ if self.metadata_table is not None:
768
+ self.metadata_table.update_table_mtime()
761
769
  already_exists = False
762
770
  except self.client.exceptions.ResourceNotFoundException:
763
771
  raise DynamoDBTableNotFound(str(self.table_name))
@@ -935,37 +943,21 @@ class DynamoDBAccess(CacheAccess):
935
943
  log.info(f"wrote {len(filtered_rows)} rows to {file_path}")
936
944
 
937
945
 
938
- metadata_table_name = f"__{__application_name__}_metadata__"
939
-
940
-
941
- class _DynamoDBMetadataTable:
946
+ class _DynamoDBMetadataTable(DynamoDBAccess):
942
947
  """
943
948
  Access metadata (e.g. most recent modification time) for DynamoDB tables.
944
949
  """
945
950
 
946
951
  @typechecked()
947
- def __init__(self, table_name: str):
948
- self.table_name = table_name # table we're storing metadata *for* (not the name of this metadata table)
949
- self.primary_partition_key = "service"
952
+ def __init__(self, service: str, service_name: str, **kwargs):
953
+ self.service = service # DynamoDB, SNS, SQS, etc.
954
+ self.service_name = service_name # table name, queue name, topic name, etc.
955
+ self.primary_partition_key = "service" # e.g. "dynamodb", "sqs", "sns", etc.
950
956
  self.primary_sort_key = "name" # e.g. table name
951
957
  self.mtime_f_string = "mtime_f"
952
958
  self.mtime_human_string = "mtime_human"
953
- self.service = "dynamodb" # type: Literal["dynamodb"]
954
- self.client = boto3.client(self.service)
955
- self.resource = boto3.resource(self.service)
956
- self.table = self.resource.Table(metadata_table_name)
957
-
958
- def create_table(self):
959
- key_schema = [{"AttributeName": self.primary_partition_key, "KeyType": "HASH"}, {"AttributeName": self.primary_sort_key, "KeyType": "RANGE"}] # Partition key # Sort key
960
-
961
- attribute_definitions = [{"AttributeName": self.primary_partition_key, "AttributeType": "S"}, {"AttributeName": self.primary_sort_key, "AttributeType": "S"}]
962
-
963
- self.table = self.resource.create_table(
964
- TableName=metadata_table_name, KeySchema=key_schema, AttributeDefinitions=attribute_definitions, BillingMode="PAY_PER_REQUEST" # on-demand capacity mode
965
- )
966
-
967
- # Wait until the table exists
968
- self.table.meta.client.get_waiter("table_exists").wait(TableName=metadata_table_name)
959
+ super().__init__(metadata_table_name, **kwargs)
960
+ self.create_table(partition_key=self.primary_partition_key, sort_key=self.primary_sort_key, partition_key_type=str, sort_key_type=str)
969
961
 
970
962
  def update_table_mtime(self):
971
963
  """
@@ -975,27 +967,12 @@ class _DynamoDBMetadataTable:
975
967
  item = dict_to_dynamodb(
976
968
  {
977
969
  self.primary_partition_key: self.service,
978
- self.primary_sort_key: self.table_name,
970
+ self.primary_sort_key: self.service_name,
979
971
  self.mtime_f_string: m_time,
980
972
  self.mtime_human_string: datetime.datetime.fromtimestamp(m_time).astimezone().isoformat(),
981
973
  }
982
974
  )
983
- log.debug(sf(item=item))
984
- retry_put = False
985
- try:
986
- self.table.put_item(Item=item)
987
- except ClientError as e:
988
- error_code = e.response["Error"]["Code"]
989
- if error_code == "ValidationException":
990
- self.client.delete_table(TableName=metadata_table_name)
991
- self.table.meta.client.get_waiter("table_not_exists").wait(TableName=metadata_table_name)
992
- if error_code == "ValidationException" or error_code == "ResourceNotFoundException":
993
- self.create_table() # table doesn't exist, so create it
994
- retry_put = True # and retry the put
995
- else:
996
- raise # some other exception, so re-raise it
997
- if retry_put:
998
- self.table.put_item(Item=item)
975
+ self.put_item(item=item)
999
976
 
1000
977
  @typechecked()
1001
978
  def get_table_mtime_f(self) -> Union[float | None]:
@@ -1003,13 +980,10 @@ class _DynamoDBMetadataTable:
1003
980
  Get a table's mtime from the metadata table.
1004
981
  :return: table's mtime as a float or None if table hasn't been written to
1005
982
  """
1006
- mtime_f = None
1007
- key = {self.primary_partition_key: self.service, self.primary_sort_key: self.table_name}
1008
- try:
1009
- response = self.table.get_item(Key=key)
1010
- if (mtime_item := response.get("Item")) is not None and (mtime_f_as_decimal := mtime_item.get(self.mtime_f_string)) is not None:
1011
- assert isinstance(mtime_f_as_decimal, Decimal) # mainly for mypy
1012
- mtime_f = float(mtime_f_as_decimal) # boto3 uses Decimal for numbers, but we want a float
1013
- except DBItemNotFound:
1014
- pass
983
+ item = self.get_item(self.primary_partition_key, self.service, self.primary_sort_key, self.service_name)
984
+ if (mtime := item.get(self.mtime_f_string)) is None:
985
+ mtime_f = None
986
+ else:
987
+ mtime_f = float(mtime)
988
+
1015
989
  return mtime_f
awsimple/exceptions.py CHANGED
@@ -14,3 +14,11 @@ class DynamoDBItemAlreadyExists(AWSimpleExceptionBase):
14
14
  sort_message = f" and {primary_sort_value=}"
15
15
  message = f"Item with {primary_partition_value=}{sort_message} already exists in table {table_name}"
16
16
  super().__init__(message)
17
+
18
+
19
+ class S3BucketAlreadyExistsNotOwnedByYou(AWSimpleExceptionBase):
20
+ """Raised when an S3 bucket already exists and is not owned by you."""
21
+
22
+ def __init__(self, bucket_name: str):
23
+ message = f'S3 Bucket "{bucket_name}" already exists and is not owned by you'
24
+ super().__init__(message)
awsimple/pubsub.py CHANGED
@@ -3,7 +3,7 @@ pub/sub abstraction on top of AWS SNS and SQS using boto3.
3
3
  """
4
4
 
5
5
  import time
6
- from typing import Any, Dict, List, Callable
6
+ from typing import Any, Dict, List, Callable, Union
7
7
  from datetime import timedelta
8
8
  from multiprocessing import Process, Queue, Event
9
9
  from threading import Thread
@@ -24,9 +24,13 @@ log = Logger(__application_name__)
24
24
 
25
25
  queue_timeout = timedelta(days=30).total_seconds()
26
26
 
27
+ sqs_name = "sqs"
28
+
27
29
 
28
30
  @typechecked()
29
- def remove_old_queues(channel: str) -> list[str]:
31
+ def remove_old_queues(
32
+ channel: str, profile_name: Union[str, None] = None, aws_access_key_id: Union[str, None] = None, aws_secret_access_key: Union[str, None] = None, region_name: Union[str, None] = None
33
+ ) -> list[str]:
30
34
  """
31
35
  Remove old SQS queues that have not been used recently.
32
36
  """
@@ -35,10 +39,12 @@ def remove_old_queues(channel: str) -> list[str]:
35
39
  log.warning(f"blank channel ({channel=}) - not deleting any queues")
36
40
  return removed
37
41
  for sqs_queue_name in get_all_sqs_queues(channel):
38
- sqs_metadata = _DynamoDBMetadataTable(sqs_queue_name)
42
+ sqs_metadata = _DynamoDBMetadataTable(
43
+ sqs_name, sqs_queue_name, profile_name=profile_name, aws_access_key_id=aws_access_key_id, aws_secret_access_key=aws_secret_access_key, region_name=region_name
44
+ )
39
45
  mtime = sqs_metadata.get_table_mtime_f()
40
46
  if mtime is not None and time.time() - mtime > queue_timeout:
41
- sqs = SQSPollAccess(sqs_queue_name)
47
+ sqs = SQSPollAccess(sqs_queue_name, profile_name=profile_name, aws_access_key_id=aws_access_key_id, aws_secret_access_key=aws_secret_access_key, region_name=region_name)
42
48
  try:
43
49
  sqs.delete_queue()
44
50
  log.info(f'deleted "{sqs_queue_name}",{mtime=}')
@@ -49,20 +55,18 @@ def remove_old_queues(channel: str) -> list[str]:
49
55
 
50
56
 
51
57
  @typechecked()
52
- def _connect_sns_to_sqs(channel_name: str, sqs_queue_name: str, sqs: SQSPollAccess) -> None:
58
+ def _connect_sns_to_sqs(sqs: SQSPollAccess, sns: SNSAccess) -> None:
53
59
  """
54
60
  Connect an SQS queue to an SNS topic.
55
61
 
56
- :param channel_name: SNS topic name.
57
- :param sqs_queue_name: SQS queue name.
58
- :param sqs: SQSPollAccess instance for the SQS queue.
62
+ :param sqs: SQS access object
63
+ :param sns: SNS access object
59
64
  :return: None
60
65
  """
61
66
 
62
67
  sqs_arn = sqs.get_arn()
63
68
 
64
69
  # Find the topic by name
65
- sns = SNSAccess(channel_name)
66
70
  sns.create_topic()
67
71
  topic_arn = sns.get_arn()
68
72
  assert sns.resource is not None
@@ -71,7 +75,7 @@ def _connect_sns_to_sqs(channel_name: str, sqs_queue_name: str, sqs: SQSPollAcce
71
75
  # Subscribe queue to topic
72
76
  queue_arn = sqs.get_arn()
73
77
  subscription = topic.subscribe(Protocol="sqs", Endpoint=queue_arn)
74
- log.info(f"Subscribed {sqs_queue_name} to topic {topic_arn}. Subscription ARN: {subscription.arn}")
78
+ log.info(f"Subscribed {sqs.queue_name} to topic {topic_arn}. Subscription ARN: {subscription.arn}")
75
79
 
76
80
  # Update queue policy to allow SNS -> SQS
77
81
  policy = {
@@ -90,7 +94,7 @@ def _connect_sns_to_sqs(channel_name: str, sqs_queue_name: str, sqs: SQSPollAcce
90
94
  }
91
95
  assert sqs.queue is not None
92
96
  sqs.queue.set_attributes(Attributes={"Policy": json.dumps(policy)})
93
- log.debug(f"Queue {sqs_queue_name} policy updated to allow topic {topic_arn}.")
97
+ log.debug(f"Queue {sqs.queue_name} policy updated to allow topic {topic_arn}.")
94
98
 
95
99
 
96
100
  class _SubscriptionThread(Thread):
@@ -118,7 +122,16 @@ class _SubscriptionThread(Thread):
118
122
  class PubSub(Process):
119
123
 
120
124
  @typechecked()
121
- def __init__(self, channel: str, node_name: str = get_node_name(), sub_callback: Callable | None = None) -> None:
125
+ def __init__(
126
+ self,
127
+ channel: str,
128
+ node_name: str = get_node_name(),
129
+ sub_callback: Callable | None = None,
130
+ profile_name: Union[str, None] = None,
131
+ aws_access_key_id: Union[str, None] = None,
132
+ aws_secret_access_key: Union[str, None] = None,
133
+ region_name: Union[str, None] = None,
134
+ ) -> None:
122
135
  """
123
136
  Pub and Sub.
124
137
  Create in a separate process to offload from main thread. Also facilitates use of moto mock in tests.
@@ -127,10 +140,15 @@ class PubSub(Process):
127
140
  :param node_name: Node name (SQS queue name suffix). Defaults to a combination of computer name and username, but can be passed in for customization and/or testing.
128
141
  :param sub_callback: Optional thread and process safe callback function to be called when a new message is received. The function should accept a single argument, which will be the message as a dictionary.
129
142
  """
130
- self.channel = channel
131
- self.node_name = node_name # e.g., computer name
143
+ self.channel = channel.lower() # when subscribing SQS queues to SNS topics, the names must all be lowercase (bizarre AWS "gotcha")
144
+ self.node_name = node_name.lower() # e.g., computer name or user and computer name
132
145
  self.sub_callback = sub_callback
133
146
 
147
+ self.profile_name = profile_name
148
+ self.aws_access_key_id = aws_access_key_id
149
+ self.aws_secret_access_key = aws_secret_access_key
150
+ self.region_name = region_name
151
+
134
152
  self._pub_queue = Queue() # type: Queue[Dict[str, Any]]
135
153
  self._sub_queue = Queue() # type: Queue[str]
136
154
 
@@ -143,13 +161,31 @@ class PubSub(Process):
143
161
  sqs_prefix = f"{self.channel}-"
144
162
  sqs_queue_name = f"{sqs_prefix}{self.node_name}"
145
163
 
146
- sns = SNSAccess(self.channel, auto_create=True)
147
- sqs_metadata = _DynamoDBMetadataTable(sqs_queue_name)
148
-
149
- sqs = SQSPollAccess(sqs_queue_name)
164
+ sns = SNSAccess(
165
+ self.channel,
166
+ auto_create=True,
167
+ profile_name=self.profile_name,
168
+ aws_access_key_id=self.aws_access_key_id,
169
+ aws_secret_access_key=self.aws_secret_access_key,
170
+ region_name=self.region_name,
171
+ )
172
+ sqs_metadata = _DynamoDBMetadataTable(
173
+ sqs_name, sqs_queue_name, profile_name=self.profile_name, aws_access_key_id=self.aws_access_key_id, aws_secret_access_key=self.aws_secret_access_key, region_name=self.region_name
174
+ )
175
+
176
+ sqs = SQSPollAccess(
177
+ sqs_queue_name, profile_name=self.profile_name, aws_access_key_id=self.aws_access_key_id, aws_secret_access_key=self.aws_secret_access_key, region_name=self.region_name
178
+ )
150
179
  if not sqs.exists():
151
180
  sqs.create_queue()
152
- _connect_sns_to_sqs(self.channel, sqs_queue_name, sqs)
181
+ sns = SNSAccess(
182
+ topic_name=self.channel,
183
+ profile_name=self.profile_name,
184
+ aws_access_key_id=self.aws_access_key_id,
185
+ aws_secret_access_key=self.aws_secret_access_key,
186
+ region_name=self.region_name,
187
+ )
188
+ _connect_sns_to_sqs(sqs, sns)
153
189
 
154
190
  sqs_metadata.update_table_mtime() # update SQS use time (the existing infrastructure calls it a "table", but we're using it for the SQS queue)
155
191
  remove_old_queues(sqs_prefix) # clean up old queues
awsimple/s3.py CHANGED
@@ -23,7 +23,7 @@ from typeguard import typechecked
23
23
  from hashy import get_string_sha512, get_file_sha512, get_bytes_sha512, get_dls_sha512
24
24
  from yasf import sf
25
25
 
26
- from awsimple import CacheAccess, __application_name__, lru_cache_write, AWSimpleException, convert_serializable_special_cases
26
+ from awsimple import CacheAccess, __application_name__, lru_cache_write, AWSimpleException, convert_serializable_special_cases, S3BucketAlreadyExistsNotOwnedByYou
27
27
 
28
28
  # Use this project's name as a prefix to avoid string collisions. Use dashes instead of underscore since that's AWS's convention.
29
29
  sha512_string = f"{__application_name__}-sha512"
@@ -518,17 +518,18 @@ class S3Access(CacheAccess):
518
518
  else:
519
519
  location = {"LocationConstraint": region}
520
520
 
521
- created = False
522
- if not self.bucket_exists():
523
- try:
524
- if self.public_readable:
525
- self.client.create_bucket(Bucket=self.bucket_name, CreateBucketConfiguration=location, ACL="public-read")
526
- else:
527
- self.client.create_bucket(Bucket=self.bucket_name, CreateBucketConfiguration=location)
528
- self.client.get_waiter("bucket_exists").wait(Bucket=self.bucket_name)
529
- created = True
530
- except ClientError as e:
531
- log.warning(f"{self.bucket_name=} {e=}")
521
+ try:
522
+ if self.public_readable:
523
+ self.client.create_bucket(Bucket=self.bucket_name, CreateBucketConfiguration=location, ACL="public-read")
524
+ else:
525
+ self.client.create_bucket(Bucket=self.bucket_name, CreateBucketConfiguration=location)
526
+ self.client.get_waiter("bucket_exists").wait(Bucket=self.bucket_name)
527
+ created = True
528
+ except self.client.exceptions.BucketAlreadyOwnedByYou:
529
+ created = False # already exists and owned by you
530
+ except self.client.exceptions.BucketAlreadyExists as e:
531
+ # bucket already exists and is owned by someone else
532
+ raise S3BucketAlreadyExistsNotOwnedByYou(str(self.bucket_name)) from e
532
533
  return created
533
534
 
534
535
  @typechecked()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: awsimple
3
- Version: 3.11.0
3
+ Version: 4.1.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,7 +14,7 @@ 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<3
17
+ Requires-Dist: typeguard
18
18
  Requires-Dist: hashy>=0.1.1
19
19
  Requires-Dist: dictim
20
20
  Requires-Dist: appdirs
@@ -0,0 +1,21 @@
1
+ awsimple/__init__.py,sha256=RT9hATlcKnnvJYnSh7tCWEeeT2LyleL_vT1Zdz0nSkM,1051
2
+ awsimple/__version__.py,sha256=_8-4yMvCFU2QJpF5V0ofTEvVAuRaDuwWi01mBIjSNkU,323
3
+ awsimple/aws.py,sha256=NbRu1v_J5j2-pcefNZ5Xggii3mM9nHpeHMz9L9K9r-U,7653
4
+ awsimple/cache.py,sha256=_LvPx76215t8KhAJOin6Pe2b4lWpB6kbKpdzgR4FeA4,7206
5
+ awsimple/dynamodb.py,sha256=7MNxAutOCMTS4JSX-DLOwzaImJ2TzIc7kfQzQPAy5y8,41193
6
+ awsimple/dynamodb_miv.py,sha256=4xPxQDYkIM-BVDGyAre6uqwJHsxguEbHbof8ztt-V7g,4645
7
+ awsimple/exceptions.py,sha256=Ew-S8YkHVWrZFI_Yik5n0cJ7Ss4Kig5JsEPQ-9z18SU,922
8
+ awsimple/logs.py,sha256=s9FhdDFWjfxGCVDx-FNTPgJ-YN1AOAgz4HNxTVfRARE,4108
9
+ awsimple/mock.py,sha256=eScbnxFF9xAosOAsL-NZgp_P-fezB6StQMkb85Y3TNo,574
10
+ awsimple/platform.py,sha256=TObvLIVgRGh-Mh4ZCxFfxAmrn8KNMHktr6XkDZX8JYE,376
11
+ awsimple/pubsub.py,sha256=exWZWWI9NrE36fESZprhxU5Qj9BSgK5diYKQZHBOqBs,9546
12
+ awsimple/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ awsimple/s3.py,sha256=OhWF1uv4oLmBF5jAhKIi918iUQxx4CX8bTEvQ5wLYr8,24050
14
+ awsimple/sns.py,sha256=T_FyN8eSmBPo213HOfB3UBlMrvtBK768IaRo_ks-7do,3526
15
+ awsimple/sqs.py,sha256=9ZY7161CpmYpcxlCFIfW8bvMn9SGl4cgGR79I4MFLDk,17281
16
+ awsimple-4.1.0.dist-info/licenses/LICENSE,sha256=d956YAgtDaxgxQmccyUk__EfhORZyBjvmJ8pq6zYTxk,1093
17
+ awsimple-4.1.0.dist-info/licenses/LICENSE.txt,sha256=d956YAgtDaxgxQmccyUk__EfhORZyBjvmJ8pq6zYTxk,1093
18
+ awsimple-4.1.0.dist-info/METADATA,sha256=AYF-IUhIYqU2zGs9K_L6J3XCq4RVzOaf2RP5lu1YRek,6638
19
+ awsimple-4.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ awsimple-4.1.0.dist-info/top_level.txt,sha256=mwqCoH_8vAaK6iYioiRbasXmVCQM7AK3grNWOKp2VHE,9
21
+ awsimple-4.1.0.dist-info/RECORD,,
@@ -1,21 +0,0 @@
1
- awsimple/__init__.py,sha256=X7MV9cg5V1dYf86tf3TonaU-LMQWenPIW2PnjxoXYB4,1015
2
- awsimple/__version__.py,sha256=ey8QJTcIT2kFHhegrTZ0d01xX-GQD2kvye8fTm8hdp4,324
3
- awsimple/aws.py,sha256=NbRu1v_J5j2-pcefNZ5Xggii3mM9nHpeHMz9L9K9r-U,7653
4
- awsimple/cache.py,sha256=_LvPx76215t8KhAJOin6Pe2b4lWpB6kbKpdzgR4FeA4,7206
5
- awsimple/dynamodb.py,sha256=FZTk_BWOZYr3wYW7jfIgQOYaKQ_SMRMfozAT1UbMXiM,42662
6
- awsimple/dynamodb_miv.py,sha256=4xPxQDYkIM-BVDGyAre6uqwJHsxguEbHbof8ztt-V7g,4645
7
- awsimple/exceptions.py,sha256=dZQn8BKmZVlIl2-MIT5SAHRcwYK_bjzzWeG1AFbDUEg,609
8
- awsimple/logs.py,sha256=s9FhdDFWjfxGCVDx-FNTPgJ-YN1AOAgz4HNxTVfRARE,4108
9
- awsimple/mock.py,sha256=eScbnxFF9xAosOAsL-NZgp_P-fezB6StQMkb85Y3TNo,574
10
- awsimple/platform.py,sha256=TObvLIVgRGh-Mh4ZCxFfxAmrn8KNMHktr6XkDZX8JYE,376
11
- awsimple/pubsub.py,sha256=HzwkjIwFicoV--30ctov8oVlKfXWe7eaLOF1dieV7qk,7692
12
- awsimple/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- awsimple/s3.py,sha256=ydYLDj51b38lYI6LsJlc4i0PDj2vnWBuSkzdRKcWAUg,23858
14
- awsimple/sns.py,sha256=T_FyN8eSmBPo213HOfB3UBlMrvtBK768IaRo_ks-7do,3526
15
- awsimple/sqs.py,sha256=9ZY7161CpmYpcxlCFIfW8bvMn9SGl4cgGR79I4MFLDk,17281
16
- awsimple-3.11.0.dist-info/licenses/LICENSE,sha256=d956YAgtDaxgxQmccyUk__EfhORZyBjvmJ8pq6zYTxk,1093
17
- awsimple-3.11.0.dist-info/licenses/LICENSE.txt,sha256=d956YAgtDaxgxQmccyUk__EfhORZyBjvmJ8pq6zYTxk,1093
18
- awsimple-3.11.0.dist-info/METADATA,sha256=eIOptt5qOoQFZMW6qipEzGLbIuBhA46K3JZNqSabPq0,6641
19
- awsimple-3.11.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- awsimple-3.11.0.dist-info/top_level.txt,sha256=mwqCoH_8vAaK6iYioiRbasXmVCQM7AK3grNWOKp2VHE,9
21
- awsimple-3.11.0.dist-info/RECORD,,