awsimple 3.5.0__py3-none-any.whl → 3.9.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.

Potentially problematic release.


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

awsimple/__init__.py CHANGED
@@ -6,6 +6,7 @@ from .dynamodb import DynamoDBAccess, dict_to_dynamodb, DBItemNotFound, DynamoDB
6
6
  from .dynamodb import KeyType, aws_name_to_key_type
7
7
  from .dynamodb_miv import DynamoDBMIVUI, miv_string, get_time_us, miv_us_to_timestamp
8
8
  from .s3 import S3Access, S3DownloadStatus, S3ObjectMetadata, BucketNotFound
9
- from .sqs import SQSAccess, SQSPollAccess, aws_sqs_long_poll_max_wait_time, aws_sqs_max_messages
9
+ from .sqs import SQSAccess, SQSPollAccess, aws_sqs_long_poll_max_wait_time, aws_sqs_max_messages, get_all_sqs_queues
10
10
  from .sns import SNSAccess
11
+ from .pubsub import PubSub
11
12
  from .logs import LogsAccess
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.5.0"
4
+ __version__ = "3.9.1"
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
- 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,
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.
@@ -113,8 +113,8 @@ class AWSAccess:
113
113
  self.resource = None
114
114
  else:
115
115
  self.client = self.session.client(self.resource_name, config=self._get_config()) # type: ignore
116
- if self.resource_name == "logs":
117
- # logs don't have resource
116
+ if self.resource_name == "logs" or self.resource_name == "rds":
117
+ # logs and rds don't have a resource
118
118
  self.resource = None
119
119
  else:
120
120
  self.resource = self.session.resource(self.resource_name, config=self._get_config()) # type: ignore
awsimple/cache.py CHANGED
@@ -82,7 +82,7 @@ def lru_cache_write(new_data: Union[Path, bytes], cache_dir: Path, cache_file_na
82
82
  least_recently_used_size = None
83
83
  for file_path in cache_dir.rglob("*"):
84
84
  access_time = os.path.getatime(file_path)
85
- if least_recently_used_path is None or access_time < least_recently_used_access_time:
85
+ if least_recently_used_path is None or least_recently_used_access_time is None or access_time < least_recently_used_access_time:
86
86
  least_recently_used_path = file_path
87
87
  least_recently_used_access_time = access_time
88
88
  least_recently_used_size = os.path.getsize(file_path)
awsimple/logs.py CHANGED
@@ -1,22 +1,10 @@
1
1
  import time
2
- import getpass
3
- import platform
4
- from functools import lru_cache
5
2
  from typing import Union
6
3
  from pathlib import Path
7
4
  from datetime import datetime
8
5
 
9
- from awsimple import AWSAccess
10
-
11
-
12
- @lru_cache()
13
- def get_user_name() -> str:
14
- return getpass.getuser()
15
-
16
-
17
- @lru_cache()
18
- def get_computer_name() -> str:
19
- return platform.node()
6
+ from .aws import AWSAccess
7
+ from .platform import get_user_name, get_computer_name
20
8
 
21
9
 
22
10
  class LogsAccess(AWSAccess):
awsimple/platform.py ADDED
@@ -0,0 +1,20 @@
1
+ from functools import cache
2
+
3
+ import getpass
4
+ import platform
5
+
6
+
7
+ @cache
8
+ def get_user_name() -> str:
9
+ return getpass.getuser()
10
+
11
+
12
+ @cache
13
+ def get_computer_name() -> str:
14
+ return platform.node()
15
+
16
+
17
+ @cache
18
+ def get_node_name() -> str:
19
+ node_name = f"{get_computer_name()}-{get_user_name()}" # AWS SNS and SQS names can include hyphens
20
+ return node_name
awsimple/pubsub.py ADDED
@@ -0,0 +1,213 @@
1
+ """
2
+ pub/sub abstraction on top of AWS SNS and SQS using boto3.
3
+ """
4
+
5
+ import time
6
+ from typing import Any, Dict, List, Callable
7
+ from datetime import timedelta
8
+ from multiprocessing import Process, Queue, Event
9
+ from threading import Thread
10
+ from queue import Empty
11
+ from logging import Logger
12
+ import json
13
+
14
+ from typeguard import typechecked
15
+ from botocore.exceptions import ClientError
16
+
17
+ from .sns import SNSAccess
18
+ from .sqs import SQSPollAccess, get_all_sqs_queues
19
+ from .dynamodb import _DynamoDBMetadataTable
20
+ from .platform import get_node_name
21
+ from .__version__ import __application_name__
22
+
23
+ log = Logger(__application_name__)
24
+
25
+ queue_timeout = timedelta(days=30).total_seconds()
26
+
27
+
28
+ @typechecked()
29
+ def remove_old_queues(channel: str) -> list[str]:
30
+ """
31
+ Remove old SQS queues that have not been used recently.
32
+ """
33
+ removed = [] # type: list[str]
34
+ if len(channel) < 2: # avoid deleting all queues
35
+ log.warning(f"blank channel ({channel=}) - not deleting any queues")
36
+ return removed
37
+ for sqs_queue_name in get_all_sqs_queues(channel):
38
+ sqs_metadata = _DynamoDBMetadataTable(sqs_queue_name)
39
+ mtime = sqs_metadata.get_table_mtime_f()
40
+ if mtime is not None and time.time() - mtime > queue_timeout:
41
+ sqs = SQSPollAccess(sqs_queue_name)
42
+ try:
43
+ sqs.delete_queue()
44
+ log.info(f'deleted "{sqs_queue_name}",{mtime=}')
45
+ except ClientError:
46
+ log.info(f'"{sqs_queue_name}" already does not exist,{mtime=}') # already doesn't exist - this is benign
47
+ removed.append(sqs_queue_name)
48
+ return removed
49
+
50
+
51
+ @typechecked()
52
+ def _connect_sns_to_sqs(channel_name: str, sqs_queue_name: str, sqs: SQSPollAccess) -> None:
53
+ """
54
+ Connect an SQS queue to an SNS topic.
55
+
56
+ :param channel_name: SNS topic name.
57
+ :param sqs_queue_name: SQS queue name.
58
+ :param sqs: SQSPollAccess instance for the SQS queue.
59
+ :return: None
60
+ """
61
+
62
+ sqs_arn = sqs.get_arn()
63
+
64
+ # Find the topic by name
65
+ sns = SNSAccess(channel_name)
66
+ sns.create_topic()
67
+ topic_arn = sns.get_arn()
68
+ assert sns.resource is not None
69
+ topic = sns.resource.Topic(topic_arn)
70
+
71
+ # Subscribe queue to topic
72
+ queue_arn = sqs.get_arn()
73
+ subscription = topic.subscribe(Protocol="sqs", Endpoint=queue_arn)
74
+ log.info(f"Subscribed {sqs_queue_name} to topic {topic_arn}. Subscription ARN: {subscription.arn}")
75
+
76
+ # Update queue policy to allow SNS -> SQS
77
+ policy = {
78
+ "Version": "2012-10-17",
79
+ "Id": "sns-sqs-subscription-policy",
80
+ "Statement": [
81
+ {
82
+ "Sid": "Allow-SNS-SendMessage",
83
+ "Effect": "Allow",
84
+ "Principal": {"Service": "sns.amazonaws.com"},
85
+ "Action": "sqs:SendMessage",
86
+ "Resource": sqs_arn,
87
+ "Condition": {"ArnEquals": {"aws:SourceArn": topic_arn}},
88
+ }
89
+ ],
90
+ }
91
+ assert sqs.queue is not None
92
+ sqs.queue.set_attributes(Attributes={"Policy": json.dumps(policy)})
93
+ log.debug(f"Queue {sqs_queue_name} policy updated to allow topic {topic_arn}.")
94
+
95
+
96
+ class _SubscriptionThread(Thread):
97
+ """
98
+ Thread to poll SQS for new messages and put them in a queue for the parent process to read.
99
+ """
100
+
101
+ @typechecked()
102
+ def __init__(self, sqs: SQSPollAccess, new_event) -> None:
103
+ super().__init__()
104
+ self._sqs = sqs
105
+ self.new_event = new_event
106
+ self.sub_queue = Queue() # type: Queue[str]
107
+
108
+ def run(self):
109
+ # exit by terminating the parent process
110
+ while True:
111
+ messages = self._sqs.receive_messages() # long poll
112
+ for message in messages:
113
+ message = json.loads(message.message)
114
+ self.sub_queue.put(message["Message"])
115
+ self.new_event.set() # notify parent process that a new message is available
116
+
117
+
118
+ class PubSub(Process):
119
+
120
+ @typechecked()
121
+ def __init__(self, channel: str, node_name: str = get_node_name(), sub_callback: Callable | None = None) -> None:
122
+ """
123
+ Pub and Sub.
124
+ Create in a separate process to offload from main thread. Also facilitates use of moto mock in tests.
125
+
126
+ :param channel: Channel name (SNS topic name).
127
+ :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
+ :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
+ """
130
+ self.channel = channel
131
+ self.node_name = node_name # e.g., computer name
132
+ self.sub_callback = sub_callback
133
+
134
+ self._pub_queue = Queue() # type: Queue[Dict[str, Any]]
135
+ self._sub_queue = Queue() # type: Queue[str]
136
+
137
+ self._new_event = Event() # pub or sub sets this when a new message is available or has been sent
138
+
139
+ super().__init__()
140
+
141
+ def run(self):
142
+
143
+ sqs_prefix = f"{self.channel}-"
144
+ sqs_queue_name = f"{sqs_prefix}{self.node_name}"
145
+
146
+ sns = SNSAccess(self.channel, auto_create=True)
147
+ sqs_metadata = _DynamoDBMetadataTable(sqs_queue_name)
148
+
149
+ sqs = SQSPollAccess(sqs_queue_name)
150
+ if not sqs.exists():
151
+ sqs.create_queue()
152
+ _connect_sns_to_sqs(self.channel, sqs_queue_name, sqs)
153
+
154
+ 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
+ remove_old_queues(sqs_prefix) # clean up old queues
156
+
157
+ sqs_thread = _SubscriptionThread(sqs, self._new_event)
158
+ sqs_thread.start()
159
+
160
+ while True:
161
+
162
+ # pub
163
+ try:
164
+ message = self._pub_queue.get(False)
165
+ message_string = json.dumps(message)
166
+ sns.publish(message_string)
167
+ except Empty:
168
+ pass
169
+
170
+ # sub
171
+ try:
172
+ message_string = sqs_thread.sub_queue.get(False)
173
+ # if a callback is provided, call it, otherwise put the message in the sub queue for later retrieval
174
+ if self.sub_callback is None:
175
+ self._sub_queue.put(message_string)
176
+ else:
177
+ message = json.loads(message_string)
178
+ self.sub_callback(message)
179
+ sqs_metadata.update_table_mtime()
180
+ except Empty:
181
+ pass # no message
182
+
183
+ # this helps responsiveness
184
+ self._new_event.clear()
185
+ self._new_event.wait(3) # race conditions can occur, so don't make this wait timeout too long
186
+
187
+ @typechecked()
188
+ def publish(self, message: dict) -> None:
189
+ """
190
+ Publish a message.
191
+
192
+ :param message: message as a dictionary
193
+ """
194
+ self._pub_queue.put(message)
195
+ self._new_event.set()
196
+
197
+ @typechecked()
198
+ def get_messages(self) -> List[Dict[str, Any]]:
199
+ """
200
+ Get all available messages.
201
+
202
+ :return: list of messages as dictionaries
203
+ """
204
+ messages = []
205
+ while True:
206
+ try:
207
+ message_string = self._sub_queue.get(block=False)
208
+ message = json.loads(message_string)
209
+ log.debug(f"{message=}")
210
+ messages.append(message)
211
+ except Empty:
212
+ break
213
+ return messages
awsimple/s3.py CHANGED
@@ -20,7 +20,7 @@ from boto3.s3.transfer import TransferConfig
20
20
  from s3transfer import S3UploadFailedError
21
21
  import urllib3.exceptions
22
22
  from typeguard import typechecked
23
- from hashy import get_string_sha512, get_file_sha512, get_bytes_sha512, get_dls_sha512 # type: ignore
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
26
  from awsimple import CacheAccess, __application_name__, lru_cache_write, AWSimpleException, convert_serializable_special_cases
awsimple/sns.py CHANGED
@@ -2,7 +2,8 @@
2
2
  SNS Access
3
3
  """
4
4
 
5
- from typing import Union, Dict
5
+ from typing import Union, Dict, Any
6
+ from functools import cache
6
7
 
7
8
  from typeguard import typechecked
8
9
 
@@ -11,7 +12,7 @@ from awsimple import AWSAccess, SQSAccess
11
12
 
12
13
  class SNSAccess(AWSAccess):
13
14
  @typechecked()
14
- def __init__(self, topic_name: str, **kwargs):
15
+ def __init__(self, topic_name: str, auto_create: bool = False, **kwargs):
15
16
  """
16
17
  SNS Access
17
18
 
@@ -20,26 +21,31 @@ class SNSAccess(AWSAccess):
20
21
  """
21
22
  super().__init__(resource_name="sns", **kwargs)
22
23
  self.topic_name = topic_name
24
+ self.auto_create = auto_create
23
25
 
24
- def get_topic(self):
26
+ @cache
27
+ def get_topic(self) -> Any:
25
28
  """
26
29
  gets the associated SNS Topic instance
27
30
 
28
- :param topic_name: topic name
29
31
  :return: sns.Topic instance
30
32
  """
31
33
  topic = None
34
+ assert self.resource is not None
32
35
  for t in self.resource.topics.all():
33
36
  if t.arn.split(":")[-1] == self.topic_name:
34
37
  topic = t
38
+ if self.auto_create and topic is None:
39
+ self.create_topic()
40
+ self.auto_create = False # only do this once
41
+ topic = self.get_topic()
35
42
  return topic
36
43
 
37
- @typechecked()
44
+ @cache
38
45
  def get_arn(self) -> str:
39
46
  """
40
47
  get topic ARN from topic name
41
48
 
42
- :param topic_name: topic name string
43
49
  :return: topic ARN
44
50
  """
45
51
  return self.get_topic().arn
@@ -68,7 +74,7 @@ class SNSAccess(AWSAccess):
68
74
  """
69
75
  Subscribe to an SNS topic
70
76
 
71
- :param subscriber: email or SQS queue
77
+ :param subscriber: email
72
78
  :return: subscription ARN
73
79
  """
74
80
  if isinstance(subscriber, str) and "@" in subscriber:
awsimple/sqs.py CHANGED
@@ -49,7 +49,7 @@ aws_sqs_max_messages = 10
49
49
 
50
50
  class SQSAccess(AWSAccess):
51
51
  @typechecked()
52
- def __init__(self, queue_name: str, immediate_delete: bool = True, visibility_timeout: Union[int, None] = None, minimum_visibility_timeout: int = 0, **kwargs):
52
+ def __init__(self, queue_name: str, immediate_delete: bool = True, visibility_timeout: Union[int, None] = None, minimum_visibility_timeout: int = 0, auto_create: bool = False, **kwargs):
53
53
  """
54
54
  SQS access
55
55
 
@@ -66,6 +66,7 @@ class SQSAccess(AWSAccess):
66
66
  self.immediate_delete = immediate_delete # True to immediately delete messages
67
67
  self.user_provided_timeout = visibility_timeout # the queue will re-try a message (make it re-visible) if not deleted within this time
68
68
  self.user_provided_minimum_timeout = minimum_visibility_timeout # the timeout will be at least this long
69
+ self.auto_create = auto_create # automatically create the queue if it doesn't exist
69
70
  self.auto_timeout_multiplier = 10.0 # for automatic timeout calculations, multiply this times the median run time to get the timeout
70
71
 
71
72
  self.sqs_call_wait_time = 0 # short (0) or long poll (> 0, usually 20)
@@ -80,21 +81,38 @@ class SQSAccess(AWSAccess):
80
81
  # We write the history out as a file so don't make this too big. We take the median (for the nominal run time) so make this big enough to tolerate a fair number of outliers.
81
82
  self.max_history = 20
82
83
 
83
- def _get_queue(self):
84
+ self.was_created = False
85
+
86
+ def _get_queue(self) -> Any:
87
+ """
88
+ Get the SQS queue instance. If the queue doesn't exist and auto_create is True, it will be created.
89
+
90
+ :return: SQS queue instance
91
+ """
84
92
  if self.queue is None:
85
- try:
86
- queue = self.resource.get_queue_by_name(QueueName=self.queue_name)
87
- except self.client.exceptions.QueueDoesNotExist as e:
88
- log.debug(f"{self.queue_name},{e=}")
89
- queue = None
90
- except self.client.exceptions.ClientError as e:
91
- error_code = e.response["Error"].get("Code")
92
- if "NonExistentQueue" in error_code:
93
- log.debug(f"{self.queue_name},{e=},{error_code=}")
93
+ queue = None
94
+ try_count = 0
95
+ assert self.resource is not None
96
+ while self.queue is None and try_count < 3:
97
+ create_queue = False
98
+ try:
99
+ queue = self.resource.get_queue_by_name(QueueName=self.queue_name)
100
+ except self.client.exceptions.QueueDoesNotExist as e:
101
+ if self.auto_create:
102
+ create_queue = True
103
+ log.debug(f"{self.queue_name},{e=}")
94
104
  queue = None
95
- else:
96
- # other errors (e.g. connection errors, etc.)
97
- raise
105
+ except self.client.exceptions.ClientError as e:
106
+ error_code = e.response["Error"].get("Code")
107
+ if "NonExistentQueue" in error_code:
108
+ log.debug(f"{self.queue_name},{e=},{error_code=}")
109
+ queue = None
110
+ else:
111
+ # other errors (e.g. connection errors, etc.)
112
+ raise
113
+ if create_queue and self.create_queue() is None:
114
+ time.sleep(60) # if we just deleted the queue, we may have to wait 60 seconds before we can re-create it
115
+ try_count += 1
98
116
 
99
117
  if queue is not None:
100
118
  # kludge so when moto mocking we return None if it can't get the queue
@@ -119,14 +137,19 @@ class SQSAccess(AWSAccess):
119
137
  return p
120
138
 
121
139
  @typechecked()
122
- def create_queue(self) -> str:
140
+ def create_queue(self) -> str | None:
123
141
  """
124
142
  create SQS queue
125
143
 
126
- :return: queue URL
144
+ :return: queue URL or None if not successful
127
145
  """
128
- response = self.client.create_queue(QueueName=self.queue_name)
129
- url = response.get("QueueUrl", "")
146
+ try:
147
+ response = self.client.create_queue(QueueName=self.queue_name)
148
+ url = response.get("QueueUrl")
149
+ self.was_created = True
150
+ except self.client.exceptions.QueueDeletedRecently as e:
151
+ log.warning(f"{self.queue_name},{e}") # can happen if a queue was recently deleted, and we try to re-create it too quickly
152
+ url = None
130
153
  return url
131
154
 
132
155
  def delete_queue(self):
@@ -367,6 +390,30 @@ class SQSAccess(AWSAccess):
367
390
 
368
391
 
369
392
  class SQSPollAccess(SQSAccess):
393
+ """
394
+ SQS Access with long polling.
395
+ """
396
+
370
397
  def __init__(self, queue_name: str, **kwargs):
371
- super().__init__(queue_name, **kwargs)
398
+ super().__init__(queue_name=queue_name, **kwargs)
372
399
  self.sqs_call_wait_time = aws_sqs_long_poll_max_wait_time
400
+
401
+
402
+ @typechecked()
403
+ def get_all_sqs_queues(prefix: str = "") -> List[str]:
404
+ """
405
+ get all SQS queues
406
+
407
+
408
+ :param prefix: prefix to filter queue names (empty string for all queues)
409
+ :return: list of queue names
410
+ """
411
+ queue_names = []
412
+
413
+ sqs = AWSAccess("sqs")
414
+ for queue in list(sqs.resource.queues.all()):
415
+ queue_name = queue.url.split("/")[-1]
416
+ if queue_name.startswith(prefix):
417
+ queue_names.append(queue_name)
418
+
419
+ return queue_names
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: awsimple
3
- Version: 3.5.0
3
+ Version: 3.9.1
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
@@ -22,6 +22,19 @@ Requires-Dist: tobool
22
22
  Requires-Dist: urllib3
23
23
  Requires-Dist: python-dateutil
24
24
  Requires-Dist: yasf
25
+ Dynamic: author
26
+ Dynamic: author-email
27
+ Dynamic: description
28
+ Dynamic: description-content-type
29
+ Dynamic: download-url
30
+ Dynamic: home-page
31
+ Dynamic: keywords
32
+ Dynamic: license
33
+ Dynamic: license-file
34
+ Dynamic: project-url
35
+ Dynamic: requires-dist
36
+ Dynamic: requires-python
37
+ Dynamic: summary
25
38
 
26
39
 
27
40
  <p align="center">
@@ -62,26 +75,28 @@ Full documentation available on [Read the Docs](https://awsimple.readthedocs.io/
62
75
 
63
76
  ### Features:
64
77
 
65
- - Simple Object Oriented API on top of boto3
78
+ - Simple Object-Oriented API on top of boto3.
79
+ - Eliminates the need to worry about `clients`, `resources`, `sessions`, and pagination.
66
80
 
67
- - One-line S3 file write, read, and delete
81
+ - Locally cached S3 accesses. Reduces network traffic, AWS costs, and can speed up access.
68
82
 
69
- - Automatic S3 retries
83
+ - `pubsub` functionality (via SNS topics and SQS queues).
70
84
 
71
- - Locally cached S3 accesses
85
+ - DynamoDB full table scans (with local cache option that only rescans if the table has changed).
72
86
 
73
- - True file hashing (SHA512) for S3 files (S3's etag is not a true file hash)
87
+ - True file hashing (SHA512) for S3 files (S3's etag is not a true file hash).
74
88
 
75
- - DynamoDB full table scans (with local cache option)
89
+ - Supports moto mock and localstack. Handy for testing and CI.
76
90
 
77
- - DynamoDB secondary indexes
91
+ - Automatic S3 retries.
78
92
 
79
- - Built-in pagination (e.g. for DynamoDB table scans and queries). Always get everything you asked for.
93
+ - One-line S3 file write, read, and delete.
80
94
 
81
- - Can automatically set SQS timeouts based on runtime data (can also be user-specified)
95
+ - DynamoDB secondary indexes.
82
96
 
83
- - Supports moto mock and localstack. Handy for testing and CI.
97
+ - Built-in pagination (e.g. for DynamoDB table scans and queries). Always get everything you asked for.
84
98
 
99
+ - Can automatically set SQS timeouts based on runtime data (can also be user-specified).
85
100
 
86
101
  ## Usage
87
102
 
@@ -0,0 +1,20 @@
1
+ awsimple/__init__.py,sha256=yo9COyuJZonzuW_5h2yvsQOtTqnwh8e_iMsMBqQxir0,964
2
+ awsimple/__version__.py,sha256=j9qPCE9XWQi2f7AWe1m9-8MYilnfMgESTbkSf6OE2LE,323
3
+ awsimple/aws.py,sha256=NbRu1v_J5j2-pcefNZ5Xggii3mM9nHpeHMz9L9K9r-U,7653
4
+ awsimple/cache.py,sha256=_LvPx76215t8KhAJOin6Pe2b4lWpB6kbKpdzgR4FeA4,7206
5
+ awsimple/dynamodb.py,sha256=8OlGbMg7uU1rpWbkjK9HdHMSfopRBrLb3AFALGgfLHg,39156
6
+ awsimple/dynamodb_miv.py,sha256=4xPxQDYkIM-BVDGyAre6uqwJHsxguEbHbof8ztt-V7g,4645
7
+ awsimple/logs.py,sha256=s9FhdDFWjfxGCVDx-FNTPgJ-YN1AOAgz4HNxTVfRARE,4108
8
+ awsimple/mock.py,sha256=eScbnxFF9xAosOAsL-NZgp_P-fezB6StQMkb85Y3TNo,574
9
+ awsimple/platform.py,sha256=TObvLIVgRGh-Mh4ZCxFfxAmrn8KNMHktr6XkDZX8JYE,376
10
+ awsimple/pubsub.py,sha256=HzwkjIwFicoV--30ctov8oVlKfXWe7eaLOF1dieV7qk,7692
11
+ awsimple/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ awsimple/s3.py,sha256=ydYLDj51b38lYI6LsJlc4i0PDj2vnWBuSkzdRKcWAUg,23858
13
+ awsimple/sns.py,sha256=T_FyN8eSmBPo213HOfB3UBlMrvtBK768IaRo_ks-7do,3526
14
+ awsimple/sqs.py,sha256=9ZY7161CpmYpcxlCFIfW8bvMn9SGl4cgGR79I4MFLDk,17281
15
+ awsimple-3.9.1.dist-info/licenses/LICENSE,sha256=d956YAgtDaxgxQmccyUk__EfhORZyBjvmJ8pq6zYTxk,1093
16
+ awsimple-3.9.1.dist-info/licenses/LICENSE.txt,sha256=d956YAgtDaxgxQmccyUk__EfhORZyBjvmJ8pq6zYTxk,1093
17
+ awsimple-3.9.1.dist-info/METADATA,sha256=KL-CjrA-YV-N3eY0YypcWlsbEQX3-kH5I6zMbL8swmY,6640
18
+ awsimple-3.9.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ awsimple-3.9.1.dist-info/top_level.txt,sha256=mwqCoH_8vAaK6iYioiRbasXmVCQM7AK3grNWOKp2VHE,9
20
+ awsimple-3.9.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (74.1.2)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,18 +0,0 @@
1
- awsimple/__init__.py,sha256=8aFfqWFAvRPweoZkKncvHAW2ytTW_5-AJ0nnmYqgUBw,916
2
- awsimple/__version__.py,sha256=lvkr0XziFtstOGw6YpJrnL1ET2diD3UgBeG9D0jTGzQ,323
3
- awsimple/aws.py,sha256=n5Mte2l0uUyLtxHx-Cv2RdVF2H2pvNiQPlrwrwddKcc,7636
4
- awsimple/cache.py,sha256=tdLeMw2IYW9Y4lGT2SAGUI7u_aTX_TFQs2udXcqW6fI,7163
5
- awsimple/dynamodb.py,sha256=8OlGbMg7uU1rpWbkjK9HdHMSfopRBrLb3AFALGgfLHg,39156
6
- awsimple/dynamodb_miv.py,sha256=4xPxQDYkIM-BVDGyAre6uqwJHsxguEbHbof8ztt-V7g,4645
7
- awsimple/logs.py,sha256=A2RmTT90pfFTthfENd7GSsEHSIBJXO8ICHPdA7sEsHY,4278
8
- awsimple/mock.py,sha256=eScbnxFF9xAosOAsL-NZgp_P-fezB6StQMkb85Y3TNo,574
9
- awsimple/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- awsimple/s3.py,sha256=lwMS8Xr06TB2LkQ7z8yaK6pnH9oOFU3-DEI3Ba6dEwo,23874
11
- awsimple/sns.py,sha256=dOx3VUS04xxeG1krGudN4A5fqoIpXeHqXNkBvfbr_6Q,3292
12
- awsimple/sqs.py,sha256=ejV9twP15X8-mZ9IHGEUlYWqufEcasYuPf1xlGQt2a8,15506
13
- awsimple-3.5.0.dist-info/LICENSE,sha256=d956YAgtDaxgxQmccyUk__EfhORZyBjvmJ8pq6zYTxk,1093
14
- awsimple-3.5.0.dist-info/LICENSE.txt,sha256=d956YAgtDaxgxQmccyUk__EfhORZyBjvmJ8pq6zYTxk,1093
15
- awsimple-3.5.0.dist-info/METADATA,sha256=LyAC6YLIzErBg7QqjFG7VgAkXQ6An3JjVWdD7P5DSas,6087
16
- awsimple-3.5.0.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
17
- awsimple-3.5.0.dist-info/top_level.txt,sha256=mwqCoH_8vAaK6iYioiRbasXmVCQM7AK3grNWOKp2VHE,9
18
- awsimple-3.5.0.dist-info/RECORD,,