awsimple 5.0.0__py3-none-any.whl → 7.0.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 +1 -1
- awsimple/__version__.py +1 -1
- awsimple/dynamodb.py +3 -0
- awsimple/pubsub.py +135 -42
- awsimple/sqs.py +3 -3
- {awsimple-5.0.0.dist-info → awsimple-7.0.0.dist-info}/METADATA +1 -1
- {awsimple-5.0.0.dist-info → awsimple-7.0.0.dist-info}/RECORD +11 -11
- {awsimple-5.0.0.dist-info → awsimple-7.0.0.dist-info}/WHEEL +0 -0
- {awsimple-5.0.0.dist-info → awsimple-7.0.0.dist-info}/licenses/LICENSE +0 -0
- {awsimple-5.0.0.dist-info → awsimple-7.0.0.dist-info}/licenses/LICENSE.txt +0 -0
- {awsimple-5.0.0.dist-info → awsimple-7.0.0.dist-info}/top_level.txt +0 -0
awsimple/__init__.py
CHANGED
|
@@ -9,5 +9,5 @@ from .dynamodb_miv import DynamoDBMIVUI, miv_string, get_time_us, miv_us_to_time
|
|
|
9
9
|
from .s3 import S3Access, S3DownloadStatus, S3ObjectMetadata, BucketNotFound
|
|
10
10
|
from .sqs import SQSAccess, SQSPollAccess, aws_sqs_long_poll_max_wait_time, aws_sqs_max_messages, get_all_sqs_queues
|
|
11
11
|
from .sns import SNSAccess
|
|
12
|
-
from .pubsub import
|
|
12
|
+
from .pubsub import Pub, Sub
|
|
13
13
|
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__ = "
|
|
4
|
+
__version__ = "7.0.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
|
@@ -519,6 +519,7 @@ class DynamoDBAccess(CacheAccess):
|
|
|
519
519
|
except ClientError as e:
|
|
520
520
|
log.warning(e)
|
|
521
521
|
if self.metadata_table is not None:
|
|
522
|
+
self.metadata_table.create_metadata_table()
|
|
522
523
|
self.metadata_table.update_table_mtime()
|
|
523
524
|
|
|
524
525
|
return created
|
|
@@ -957,6 +958,8 @@ class _DynamoDBMetadataTable(DynamoDBAccess):
|
|
|
957
958
|
self.mtime_f_string = "mtime_f"
|
|
958
959
|
self.mtime_human_string = "mtime_human"
|
|
959
960
|
super().__init__(metadata_table_name, **kwargs)
|
|
961
|
+
|
|
962
|
+
def create_metadata_table(self):
|
|
960
963
|
self.create_table(partition_key=self.primary_partition_key, sort_key=self.primary_sort_key, partition_key_type=str, sort_key_type=str)
|
|
961
964
|
|
|
962
965
|
def update_table_mtime(self):
|
awsimple/pubsub.py
CHANGED
|
@@ -6,8 +6,8 @@ import time
|
|
|
6
6
|
from functools import lru_cache
|
|
7
7
|
from typing import Any, Dict, List, Callable, Union
|
|
8
8
|
from datetime import timedelta
|
|
9
|
-
from
|
|
10
|
-
from
|
|
9
|
+
from threading import Thread, Event
|
|
10
|
+
from queue import Queue
|
|
11
11
|
from queue import Empty
|
|
12
12
|
from logging import Logger
|
|
13
13
|
import json
|
|
@@ -26,7 +26,9 @@ log = Logger(__application_name__)
|
|
|
26
26
|
|
|
27
27
|
queue_timeout = timedelta(days=30).total_seconds()
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
SQS_NAME = "sqs"
|
|
30
|
+
|
|
31
|
+
AWS_RESOURCE_PREFIX = "ps" # for pubsub
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
@typechecked()
|
|
@@ -42,7 +44,7 @@ def remove_old_queues(
|
|
|
42
44
|
return removed
|
|
43
45
|
for sqs_queue_name in get_all_sqs_queues(channel):
|
|
44
46
|
sqs_metadata = _DynamoDBMetadataTable(
|
|
45
|
-
|
|
47
|
+
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
|
|
46
48
|
)
|
|
47
49
|
mtime = sqs_metadata.get_table_mtime_f()
|
|
48
50
|
if mtime is not None and time.time() - mtime > queue_timeout:
|
|
@@ -101,30 +103,33 @@ def _connect_sns_to_sqs(sqs: SQSPollAccess, sns: SNSAccess) -> None:
|
|
|
101
103
|
|
|
102
104
|
class _SubscriptionThread(Thread):
|
|
103
105
|
"""
|
|
104
|
-
Thread to poll SQS for new messages and put them in a queue for the parent
|
|
106
|
+
Thread to poll SQS for new messages and put them in a queue for the parent thread to read.
|
|
105
107
|
"""
|
|
106
108
|
|
|
107
109
|
@typechecked()
|
|
108
110
|
def __init__(self, sqs: SQSPollAccess, new_event) -> None:
|
|
109
111
|
super().__init__()
|
|
110
112
|
self._sqs = sqs
|
|
111
|
-
self.new_event = new_event
|
|
112
113
|
self.sub_queue = Queue() # type: Queue[str]
|
|
114
|
+
self._exit_event = Event()
|
|
115
|
+
self._new_event = new_event
|
|
113
116
|
|
|
114
117
|
def run(self):
|
|
115
|
-
|
|
116
|
-
while True:
|
|
118
|
+
while not self._exit_event.is_set():
|
|
117
119
|
messages = self._sqs.receive_messages() # long poll
|
|
118
120
|
for message in messages:
|
|
119
121
|
message = json.loads(message.message)
|
|
120
122
|
self.sub_queue.put(message["Message"])
|
|
121
|
-
self.
|
|
123
|
+
self._new_event.set()
|
|
124
|
+
|
|
125
|
+
def request_exit(self):
|
|
126
|
+
self._exit_event.set()
|
|
122
127
|
|
|
123
128
|
|
|
124
129
|
@lru_cache
|
|
125
130
|
def make_name_aws_safe(*args: str) -> str:
|
|
126
131
|
"""
|
|
127
|
-
Make a name safe for an SQS queue to subscribe to an SNS topic.
|
|
132
|
+
Make a name safe for an SQS queue to subscribe to an SNS topic. This ensures we adhere to name restrictions such as acceptable characters and length.
|
|
128
133
|
|
|
129
134
|
:params: input name(s)
|
|
130
135
|
:return: AWS safe name
|
|
@@ -135,18 +140,19 @@ def make_name_aws_safe(*args: str) -> str:
|
|
|
135
140
|
return base36
|
|
136
141
|
|
|
137
142
|
|
|
138
|
-
class
|
|
143
|
+
class _PubSub(Thread):
|
|
139
144
|
|
|
140
145
|
@typechecked()
|
|
141
146
|
def __init__(
|
|
142
147
|
self,
|
|
143
148
|
channel: str,
|
|
144
|
-
node_name: str
|
|
145
|
-
sub_callback: Callable | None
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
node_name: str,
|
|
150
|
+
sub_callback: Callable | None,
|
|
151
|
+
use_sub_queue: bool,
|
|
152
|
+
profile_name: Union[str, None],
|
|
153
|
+
aws_access_key_id: Union[str, None],
|
|
154
|
+
aws_secret_access_key: Union[str, None],
|
|
155
|
+
region_name: Union[str, None],
|
|
150
156
|
) -> None:
|
|
151
157
|
"""
|
|
152
158
|
Pub and Sub.
|
|
@@ -155,12 +161,13 @@ class PubSub(Process):
|
|
|
155
161
|
:param channel: Channel name (used for SNS topic name). This must not be a prefix of other channel names to avoid collisions (don't name one channel "a" and another "ab").
|
|
156
162
|
: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.
|
|
157
163
|
: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.
|
|
164
|
+
:param use_sub_queue: If True, use an internal queue to store received messages. If False, messages must be handled by the callback function. Default is False.
|
|
158
165
|
"""
|
|
159
|
-
self.
|
|
160
|
-
self.channel = self.aws_resource_prefix + make_name_aws_safe(channel) # prefix with ps (pubsub) to avoid collisions with other uses of SNS topics and SQS queues
|
|
166
|
+
self.channel = AWS_RESOURCE_PREFIX + make_name_aws_safe(channel) # prefix with ps (pubsub) to avoid collisions with other uses of SNS topics and SQS queues
|
|
161
167
|
self.node_name = node_name
|
|
162
|
-
self.sqs_queue_name =
|
|
168
|
+
self.sqs_queue_name = AWS_RESOURCE_PREFIX + make_name_aws_safe(self.channel, self.node_name)
|
|
163
169
|
self.sub_callback = sub_callback
|
|
170
|
+
self.use_sub_queue = use_sub_queue
|
|
164
171
|
|
|
165
172
|
self.profile_name = profile_name
|
|
166
173
|
self.aws_access_key_id = aws_access_key_id
|
|
@@ -170,9 +177,11 @@ class PubSub(Process):
|
|
|
170
177
|
self._pub_queue = Queue() # type: Queue[Dict[str, Any]]
|
|
171
178
|
self._sub_queue = Queue() # type: Queue[str]
|
|
172
179
|
|
|
173
|
-
self.
|
|
180
|
+
self._exit_event = Event() # set this to request exit
|
|
181
|
+
self._new_event = Event()
|
|
182
|
+
self._new_event_wait_time = 10 # seconds
|
|
174
183
|
|
|
175
|
-
super().__init__()
|
|
184
|
+
super().__init__(daemon=True) # make daemon so an instance of this thread exits when the main program exits
|
|
176
185
|
|
|
177
186
|
def run(self):
|
|
178
187
|
|
|
@@ -184,8 +193,10 @@ class PubSub(Process):
|
|
|
184
193
|
aws_secret_access_key=self.aws_secret_access_key,
|
|
185
194
|
region_name=self.region_name,
|
|
186
195
|
)
|
|
196
|
+
sns.create_topic()
|
|
197
|
+
|
|
187
198
|
sqs_metadata = _DynamoDBMetadataTable(
|
|
188
|
-
|
|
199
|
+
SQS_NAME,
|
|
189
200
|
self.sqs_queue_name,
|
|
190
201
|
profile_name=self.profile_name,
|
|
191
202
|
aws_access_key_id=self.aws_access_key_id,
|
|
@@ -210,10 +221,14 @@ class PubSub(Process):
|
|
|
210
221
|
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)
|
|
211
222
|
remove_old_queues(self.channel) # clean up old queues
|
|
212
223
|
|
|
213
|
-
|
|
214
|
-
|
|
224
|
+
if self.sub_callback is None and not self.use_sub_queue:
|
|
225
|
+
# not being used as a subscriber
|
|
226
|
+
sqs_thread = None
|
|
227
|
+
else:
|
|
228
|
+
sqs_thread = _SubscriptionThread(sqs, self._new_event)
|
|
229
|
+
sqs_thread.start()
|
|
215
230
|
|
|
216
|
-
while
|
|
231
|
+
while not self._exit_event.is_set():
|
|
217
232
|
|
|
218
233
|
# pub
|
|
219
234
|
try:
|
|
@@ -224,21 +239,26 @@ class PubSub(Process):
|
|
|
224
239
|
pass
|
|
225
240
|
|
|
226
241
|
# sub
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
#
|
|
240
|
-
|
|
241
|
-
|
|
242
|
+
if sqs_thread is not None:
|
|
243
|
+
try:
|
|
244
|
+
message_string = sqs_thread.sub_queue.get(False)
|
|
245
|
+
if self.use_sub_queue:
|
|
246
|
+
self._sub_queue.put(message_string)
|
|
247
|
+
if self.sub_callback is not None:
|
|
248
|
+
message = json.loads(message_string)
|
|
249
|
+
self.sub_callback(message)
|
|
250
|
+
sqs_metadata.update_table_mtime()
|
|
251
|
+
except Empty:
|
|
252
|
+
pass # no message
|
|
253
|
+
|
|
254
|
+
if self._new_event.wait(self._new_event_wait_time): # timeout in case the new event technique fails
|
|
255
|
+
self._new_event.clear()
|
|
256
|
+
|
|
257
|
+
if sqs_thread is not None:
|
|
258
|
+
sqs_thread.request_exit()
|
|
259
|
+
sqs_thread.join(30)
|
|
260
|
+
if sqs_thread.is_alive():
|
|
261
|
+
log.error("sqs_thread did not exit cleanly")
|
|
242
262
|
|
|
243
263
|
@typechecked()
|
|
244
264
|
def publish(self, message: dict) -> None:
|
|
@@ -253,10 +273,12 @@ class PubSub(Process):
|
|
|
253
273
|
@typechecked()
|
|
254
274
|
def get_messages(self) -> List[Dict[str, Any]]:
|
|
255
275
|
"""
|
|
256
|
-
Get all available messages.
|
|
276
|
+
Get all available messages. Muse set sub_poll=True when creating the PubSub object to use this function.
|
|
257
277
|
|
|
258
278
|
:return: list of messages as dictionaries
|
|
259
279
|
"""
|
|
280
|
+
if not self.use_sub_queue:
|
|
281
|
+
raise RuntimeError("use_sub_queue must be True to use get_messages()")
|
|
260
282
|
messages = []
|
|
261
283
|
while True:
|
|
262
284
|
try:
|
|
@@ -267,3 +289,74 @@ class PubSub(Process):
|
|
|
267
289
|
except Empty:
|
|
268
290
|
break
|
|
269
291
|
return messages
|
|
292
|
+
|
|
293
|
+
def request_exit(self) -> None:
|
|
294
|
+
"""
|
|
295
|
+
Request the process to exit.
|
|
296
|
+
"""
|
|
297
|
+
self._exit_event.set()
|
|
298
|
+
self._new_event.set()
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class Pub(_PubSub):
|
|
302
|
+
|
|
303
|
+
@typechecked()
|
|
304
|
+
def __init__(
|
|
305
|
+
self,
|
|
306
|
+
channel: str,
|
|
307
|
+
node_name: str = get_node_name(),
|
|
308
|
+
profile_name: Union[str, None] = None,
|
|
309
|
+
aws_access_key_id: Union[str, None] = None,
|
|
310
|
+
aws_secret_access_key: Union[str, None] = None,
|
|
311
|
+
region_name: Union[str, None] = None,
|
|
312
|
+
) -> None:
|
|
313
|
+
"""
|
|
314
|
+
Pub only.
|
|
315
|
+
Create in a separate process to offload from main thread. Also facilitates use of moto mock in tests.
|
|
316
|
+
|
|
317
|
+
:param channel: Channel name (used for SNS topic name). This must not be a prefix of other channel names to avoid collisions (don't name one channel "a" and another "ab").
|
|
318
|
+
"""
|
|
319
|
+
super().__init__(
|
|
320
|
+
channel=channel,
|
|
321
|
+
node_name=node_name,
|
|
322
|
+
sub_callback=None, # pub only
|
|
323
|
+
use_sub_queue=False, # pub only
|
|
324
|
+
profile_name=profile_name,
|
|
325
|
+
aws_access_key_id=aws_access_key_id,
|
|
326
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
327
|
+
region_name=region_name,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class Sub(_PubSub):
|
|
332
|
+
|
|
333
|
+
@typechecked()
|
|
334
|
+
def __init__(
|
|
335
|
+
self,
|
|
336
|
+
channel: str,
|
|
337
|
+
node_name: str = get_node_name(),
|
|
338
|
+
sub_callback: Callable | None = None,
|
|
339
|
+
profile_name: Union[str, None] = None,
|
|
340
|
+
aws_access_key_id: Union[str, None] = None,
|
|
341
|
+
aws_secret_access_key: Union[str, None] = None,
|
|
342
|
+
region_name: Union[str, None] = None,
|
|
343
|
+
) -> None:
|
|
344
|
+
"""
|
|
345
|
+
Sub only.
|
|
346
|
+
Create in a separate process to offload from main thread. Also facilitates use of moto mock in tests.
|
|
347
|
+
|
|
348
|
+
:param channel: Channel name (used for SNS topic name). This must not be a prefix of other channel names to avoid collisions (don't name one channel "a" and another "ab").
|
|
349
|
+
: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.
|
|
350
|
+
:param sub_callback: Optional 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.
|
|
351
|
+
If this is not used, then get_messages() should be used to retrieve messages.
|
|
352
|
+
"""
|
|
353
|
+
super().__init__(
|
|
354
|
+
channel=channel,
|
|
355
|
+
node_name=node_name,
|
|
356
|
+
sub_callback=sub_callback,
|
|
357
|
+
use_sub_queue=sub_callback is None, # if no callback, use internal queue
|
|
358
|
+
profile_name=profile_name,
|
|
359
|
+
aws_access_key_id=aws_access_key_id,
|
|
360
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
361
|
+
region_name=region_name,
|
|
362
|
+
)
|
awsimple/sqs.py
CHANGED
|
@@ -230,9 +230,9 @@ class SQSAccess(AWSAccess):
|
|
|
230
230
|
if (queue := self._get_queue()) is None:
|
|
231
231
|
log.warning(f"could not get queue {self.queue_name}")
|
|
232
232
|
else:
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
)
|
|
233
|
+
visibility_timeout = self.calculate_visibility_timeout()
|
|
234
|
+
max_number_of_messages = min(max_number_of_messages, aws_sqs_max_messages) # AWS limit
|
|
235
|
+
aws_messages = queue.receive_messages(MaxNumberOfMessages=max_number_of_messages, VisibilityTimeout=visibility_timeout, WaitTimeSeconds=call_wait_time)
|
|
236
236
|
|
|
237
237
|
for m in aws_messages:
|
|
238
238
|
if self.immediate_delete:
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
awsimple/__init__.py,sha256=
|
|
2
|
-
awsimple/__version__.py,sha256=
|
|
1
|
+
awsimple/__init__.py,sha256=1uECRpVL5WHv1HPyn9sKQpEMJHwlbb7vhkEv7cuS5C4,1053
|
|
2
|
+
awsimple/__version__.py,sha256=IwHeKF_Imptby8oSPG0-2P79ZV5YxNB8SS8Sg5-vFgY,323
|
|
3
3
|
awsimple/aws.py,sha256=NbRu1v_J5j2-pcefNZ5Xggii3mM9nHpeHMz9L9K9r-U,7653
|
|
4
4
|
awsimple/cache.py,sha256=_LvPx76215t8KhAJOin6Pe2b4lWpB6kbKpdzgR4FeA4,7206
|
|
5
|
-
awsimple/dynamodb.py,sha256=
|
|
5
|
+
awsimple/dynamodb.py,sha256=3xqqs31RvW_UAVOk6aY2QhlXcnlz42POFmOjrkokTK4,41290
|
|
6
6
|
awsimple/dynamodb_miv.py,sha256=4xPxQDYkIM-BVDGyAre6uqwJHsxguEbHbof8ztt-V7g,4645
|
|
7
7
|
awsimple/exceptions.py,sha256=Ew-S8YkHVWrZFI_Yik5n0cJ7Ss4Kig5JsEPQ-9z18SU,922
|
|
8
8
|
awsimple/logs.py,sha256=s9FhdDFWjfxGCVDx-FNTPgJ-YN1AOAgz4HNxTVfRARE,4108
|
|
9
9
|
awsimple/mock.py,sha256=eScbnxFF9xAosOAsL-NZgp_P-fezB6StQMkb85Y3TNo,574
|
|
10
10
|
awsimple/platform.py,sha256=TObvLIVgRGh-Mh4ZCxFfxAmrn8KNMHktr6XkDZX8JYE,376
|
|
11
|
-
awsimple/pubsub.py,sha256=
|
|
11
|
+
awsimple/pubsub.py,sha256=lsGN9HUQDh4pSVjVB0Y4T5QoiI69kIy8-m0G27PUsWU,14000
|
|
12
12
|
awsimple/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
awsimple/s3.py,sha256=OhWF1uv4oLmBF5jAhKIi918iUQxx4CX8bTEvQ5wLYr8,24050
|
|
14
14
|
awsimple/sns.py,sha256=T_FyN8eSmBPo213HOfB3UBlMrvtBK768IaRo_ks-7do,3526
|
|
15
|
-
awsimple/sqs.py,sha256=
|
|
16
|
-
awsimple-
|
|
17
|
-
awsimple-
|
|
18
|
-
awsimple-
|
|
19
|
-
awsimple-
|
|
20
|
-
awsimple-
|
|
21
|
-
awsimple-
|
|
15
|
+
awsimple/sqs.py,sha256=nS_rD38gJObYeoXkDSg8Dv5jZTlxc79VRHFbVfmseis,17376
|
|
16
|
+
awsimple-7.0.0.dist-info/licenses/LICENSE,sha256=d956YAgtDaxgxQmccyUk__EfhORZyBjvmJ8pq6zYTxk,1093
|
|
17
|
+
awsimple-7.0.0.dist-info/licenses/LICENSE.txt,sha256=d956YAgtDaxgxQmccyUk__EfhORZyBjvmJ8pq6zYTxk,1093
|
|
18
|
+
awsimple-7.0.0.dist-info/METADATA,sha256=QdkdmQ4c0u0zQtsprenJgqyUcGcdfFEQ6suMSKSw1KI,6660
|
|
19
|
+
awsimple-7.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
20
|
+
awsimple-7.0.0.dist-info/top_level.txt,sha256=mwqCoH_8vAaK6iYioiRbasXmVCQM7AK3grNWOKp2VHE,9
|
|
21
|
+
awsimple-7.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|