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 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 PubSub
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__ = "5.0.0"
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 multiprocessing import Process, Queue, Event
10
- from threading import Thread
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
- sqs_name = "sqs"
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
- 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
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 process to read.
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
- # exit by terminating the parent process
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.new_event.set() # notify parent process that a new message is available
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. AWS has a bunch of undocumented restrictions on names, so we just hash the name to a base36 string.
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 PubSub(Process):
143
+ class _PubSub(Thread):
139
144
 
140
145
  @typechecked()
141
146
  def __init__(
142
147
  self,
143
148
  channel: str,
144
- node_name: str = get_node_name(),
145
- sub_callback: Callable | None = None,
146
- profile_name: Union[str, None] = None,
147
- aws_access_key_id: Union[str, None] = None,
148
- aws_secret_access_key: Union[str, None] = None,
149
- region_name: Union[str, None] = None,
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.aws_resource_prefix = "ps" # for pubsub
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 = self.aws_resource_prefix + make_name_aws_safe(self.channel, self.node_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._new_event = Event() # pub or sub sets this when a new message is available or has been sent
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
- sqs_name,
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
- sqs_thread = _SubscriptionThread(sqs, self._new_event)
214
- sqs_thread.start()
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 True:
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
- try:
228
- message_string = sqs_thread.sub_queue.get(False)
229
- # if a callback is provided, call it, otherwise put the message in the sub queue for later retrieval
230
- if self.sub_callback is None:
231
- self._sub_queue.put(message_string)
232
- else:
233
- message = json.loads(message_string)
234
- self.sub_callback(message)
235
- sqs_metadata.update_table_mtime()
236
- except Empty:
237
- pass # no message
238
-
239
- # this helps responsiveness
240
- self._new_event.clear()
241
- self._new_event.wait(3) # race conditions can occur, so don't make this wait timeout too long
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
- aws_messages = queue.receive_messages(
234
- MaxNumberOfMessages=min(max_number_of_messages, aws_sqs_max_messages), VisibilityTimeout=self.calculate_visibility_timeout(), WaitTimeSeconds=call_wait_time
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: awsimple
3
- Version: 5.0.0
3
+ Version: 7.0.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
@@ -1,21 +1,21 @@
1
- awsimple/__init__.py,sha256=RT9hATlcKnnvJYnSh7tCWEeeT2LyleL_vT1Zdz0nSkM,1051
2
- awsimple/__version__.py,sha256=xCHPAAnvsCqjRKpsIAQOEFC8V53loQAyz0sN0R7SAo0,323
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=7MNxAutOCMTS4JSX-DLOwzaImJ2TzIc7kfQzQPAy5y8,41193
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=Fxo3i4zo3GfBVhmGMGI4CQsRQ18x4Pp_bjpxSL-9xJY,10240
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=9ZY7161CpmYpcxlCFIfW8bvMn9SGl4cgGR79I4MFLDk,17281
16
- awsimple-5.0.0.dist-info/licenses/LICENSE,sha256=d956YAgtDaxgxQmccyUk__EfhORZyBjvmJ8pq6zYTxk,1093
17
- awsimple-5.0.0.dist-info/licenses/LICENSE.txt,sha256=d956YAgtDaxgxQmccyUk__EfhORZyBjvmJ8pq6zYTxk,1093
18
- awsimple-5.0.0.dist-info/METADATA,sha256=98gj_2M_A4xwO4-y5c8LLsRML1pfnoKeqE2-LCyEvMU,6660
19
- awsimple-5.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- awsimple-5.0.0.dist-info/top_level.txt,sha256=mwqCoH_8vAaK6iYioiRbasXmVCQM7AK3grNWOKp2VHE,9
21
- awsimple-5.0.0.dist-info/RECORD,,
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,,