awsimple 4.2.0__py3-none-any.whl → 6.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__ = "4.2.0"
4
+ __version__ = "6.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/pubsub.py CHANGED
@@ -6,14 +6,15 @@ 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
14
14
 
15
15
  from typeguard import typechecked
16
16
  from botocore.exceptions import ClientError
17
+ import strif
17
18
 
18
19
  from .sns import SNSAccess
19
20
  from .sqs import SQSPollAccess, get_all_sqs_queues
@@ -25,7 +26,9 @@ log = Logger(__application_name__)
25
26
 
26
27
  queue_timeout = timedelta(days=30).total_seconds()
27
28
 
28
- sqs_name = "sqs"
29
+ SQS_NAME = "sqs"
30
+
31
+ AWS_RESOURCE_PREFIX = "ps" # for pubsub
29
32
 
30
33
 
31
34
  @typechecked()
@@ -41,7 +44,7 @@ def remove_old_queues(
41
44
  return removed
42
45
  for sqs_queue_name in get_all_sqs_queues(channel):
43
46
  sqs_metadata = _DynamoDBMetadataTable(
44
- 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
45
48
  )
46
49
  mtime = sqs_metadata.get_table_mtime_f()
47
50
  if mtime is not None and time.time() - mtime > queue_timeout:
@@ -100,52 +103,56 @@ def _connect_sns_to_sqs(sqs: SQSPollAccess, sns: SNSAccess) -> None:
100
103
 
101
104
  class _SubscriptionThread(Thread):
102
105
  """
103
- 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.
104
107
  """
105
108
 
106
109
  @typechecked()
107
110
  def __init__(self, sqs: SQSPollAccess, new_event) -> None:
108
111
  super().__init__()
109
112
  self._sqs = sqs
110
- self.new_event = new_event
111
113
  self.sub_queue = Queue() # type: Queue[str]
114
+ self._exit_event = Event()
115
+ self._new_event = new_event
112
116
 
113
117
  def run(self):
114
- # exit by terminating the parent process
115
- while True:
118
+ while not self._exit_event.is_set():
116
119
  messages = self._sqs.receive_messages() # long poll
117
120
  for message in messages:
118
121
  message = json.loads(message.message)
119
122
  self.sub_queue.put(message["Message"])
120
- 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()
121
127
 
122
128
 
123
129
  @lru_cache
124
- def make_name_aws_safe(name: str) -> str:
130
+ def make_name_aws_safe(*args: str) -> str:
125
131
  """
126
- 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.
127
133
 
128
- :param name: input name
134
+ :params: input name(s)
129
135
  :return: AWS safe name
130
136
  """
131
- safe_name = "".join([c for c in name.strip().lower() if c.isalnum()]) # only allow alphanumeric characters
132
- if len(safe_name) < 1:
133
- raise ValueError(f'"{name}" is not valid after making AWS safe - result must contain at least one alphanumeric character.')
134
- return safe_name
135
137
 
138
+ base36 = strif.hash_string("".join(args)).base36
139
+ assert len(base36) == 31
140
+ return base36
136
141
 
137
- class PubSub(Process):
142
+
143
+ class _PubSub(Thread):
138
144
 
139
145
  @typechecked()
140
146
  def __init__(
141
147
  self,
142
148
  channel: str,
143
- node_name: str = get_node_name(),
144
- sub_callback: Callable | None = None,
145
- profile_name: Union[str, None] = None,
146
- aws_access_key_id: Union[str, None] = None,
147
- aws_secret_access_key: Union[str, None] = None,
148
- 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],
149
156
  ) -> None:
150
157
  """
151
158
  Pub and Sub.
@@ -154,11 +161,13 @@ class PubSub(Process):
154
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").
155
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.
156
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.
157
165
  """
158
-
159
- self.channel = "ps" + make_name_aws_safe(channel) # prefix with ps (pubsub) to avoid collisions with other uses of SNS topics and SQS queues
160
- self.node_name = make_name_aws_safe(node_name)
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
167
+ self.node_name = node_name
168
+ self.sqs_queue_name = AWS_RESOURCE_PREFIX + make_name_aws_safe(self.channel, self.node_name)
161
169
  self.sub_callback = sub_callback
170
+ self.use_sub_queue = use_sub_queue
162
171
 
163
172
  self.profile_name = profile_name
164
173
  self.aws_access_key_id = aws_access_key_id
@@ -168,14 +177,14 @@ class PubSub(Process):
168
177
  self._pub_queue = Queue() # type: Queue[Dict[str, Any]]
169
178
  self._sub_queue = Queue() # type: Queue[str]
170
179
 
171
- 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
172
183
 
173
- super().__init__()
184
+ super().__init__(daemon=True) # make daemon so an instance of this thread exits when the main program exits
174
185
 
175
186
  def run(self):
176
187
 
177
- sqs_queue_name = f"{self.channel}{self.node_name}"
178
-
179
188
  sns = SNSAccess(
180
189
  self.channel,
181
190
  auto_create=True,
@@ -184,12 +193,19 @@ 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, 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
199
+ SQS_NAME,
200
+ self.sqs_queue_name,
201
+ profile_name=self.profile_name,
202
+ aws_access_key_id=self.aws_access_key_id,
203
+ aws_secret_access_key=self.aws_secret_access_key,
204
+ region_name=self.region_name,
189
205
  )
190
206
 
191
207
  sqs = SQSPollAccess(
192
- 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
208
+ self.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
193
209
  )
194
210
  if not sqs.exists():
195
211
  sqs.create_queue()
@@ -205,10 +221,14 @@ class PubSub(Process):
205
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)
206
222
  remove_old_queues(self.channel) # clean up old queues
207
223
 
208
- sqs_thread = _SubscriptionThread(sqs, self._new_event)
209
- 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()
210
230
 
211
- while True:
231
+ while not self._exit_event.is_set():
212
232
 
213
233
  # pub
214
234
  try:
@@ -219,21 +239,26 @@ class PubSub(Process):
219
239
  pass
220
240
 
221
241
  # sub
222
- try:
223
- message_string = sqs_thread.sub_queue.get(False)
224
- # if a callback is provided, call it, otherwise put the message in the sub queue for later retrieval
225
- if self.sub_callback is None:
226
- self._sub_queue.put(message_string)
227
- else:
228
- message = json.loads(message_string)
229
- self.sub_callback(message)
230
- sqs_metadata.update_table_mtime()
231
- except Empty:
232
- pass # no message
233
-
234
- # this helps responsiveness
235
- self._new_event.clear()
236
- 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")
237
262
 
238
263
  @typechecked()
239
264
  def publish(self, message: dict) -> None:
@@ -248,10 +273,12 @@ class PubSub(Process):
248
273
  @typechecked()
249
274
  def get_messages(self) -> List[Dict[str, Any]]:
250
275
  """
251
- Get all available messages.
276
+ Get all available messages. Muse set sub_poll=True when creating the PubSub object to use this function.
252
277
 
253
278
  :return: list of messages as dictionaries
254
279
  """
280
+ if not self.use_sub_queue:
281
+ raise RuntimeError("use_sub_queue must be True to use get_messages()")
255
282
  messages = []
256
283
  while True:
257
284
  try:
@@ -262,3 +289,74 @@ class PubSub(Process):
262
289
  except Empty:
263
290
  break
264
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: 4.2.0
3
+ Version: 6.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
@@ -22,6 +22,7 @@ Requires-Dist: tobool
22
22
  Requires-Dist: urllib3
23
23
  Requires-Dist: python-dateutil
24
24
  Requires-Dist: yasf
25
+ Requires-Dist: strif
25
26
  Dynamic: author
26
27
  Dynamic: author-email
27
28
  Dynamic: description
@@ -1,5 +1,5 @@
1
- awsimple/__init__.py,sha256=RT9hATlcKnnvJYnSh7tCWEeeT2LyleL_vT1Zdz0nSkM,1051
2
- awsimple/__version__.py,sha256=fij2OF6RPCHAUREavC9bv6YK4lD3nxXPz58k91OHyT4,323
1
+ awsimple/__init__.py,sha256=1uECRpVL5WHv1HPyn9sKQpEMJHwlbb7vhkEv7cuS5C4,1053
2
+ awsimple/__version__.py,sha256=fkIKzxizC4RTJMw-JGHdn_etsHyWAnkoUeWNC-Ty-Sg,323
3
3
  awsimple/aws.py,sha256=NbRu1v_J5j2-pcefNZ5Xggii3mM9nHpeHMz9L9K9r-U,7653
4
4
  awsimple/cache.py,sha256=_LvPx76215t8KhAJOin6Pe2b4lWpB6kbKpdzgR4FeA4,7206
5
5
  awsimple/dynamodb.py,sha256=7MNxAutOCMTS4JSX-DLOwzaImJ2TzIc7kfQzQPAy5y8,41193
@@ -8,14 +8,14 @@ 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=Pw5foOHtpd-tAWMRlnSYuRpa4c-fi1tMZfesizn9UDc,10143
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-4.2.0.dist-info/licenses/LICENSE,sha256=d956YAgtDaxgxQmccyUk__EfhORZyBjvmJ8pq6zYTxk,1093
17
- awsimple-4.2.0.dist-info/licenses/LICENSE.txt,sha256=d956YAgtDaxgxQmccyUk__EfhORZyBjvmJ8pq6zYTxk,1093
18
- awsimple-4.2.0.dist-info/METADATA,sha256=TRNowXIo5ZYVRxy37xcGZ4o4UyIikkS1_JcjrqHGnBg,6638
19
- awsimple-4.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- awsimple-4.2.0.dist-info/top_level.txt,sha256=mwqCoH_8vAaK6iYioiRbasXmVCQM7AK3grNWOKp2VHE,9
21
- awsimple-4.2.0.dist-info/RECORD,,
15
+ awsimple/sqs.py,sha256=nS_rD38gJObYeoXkDSg8Dv5jZTlxc79VRHFbVfmseis,17376
16
+ awsimple-6.0.0.dist-info/licenses/LICENSE,sha256=d956YAgtDaxgxQmccyUk__EfhORZyBjvmJ8pq6zYTxk,1093
17
+ awsimple-6.0.0.dist-info/licenses/LICENSE.txt,sha256=d956YAgtDaxgxQmccyUk__EfhORZyBjvmJ8pq6zYTxk,1093
18
+ awsimple-6.0.0.dist-info/METADATA,sha256=jaIG-lKNlcttMgVSC0ZDnGAqe-CSxRbhWcmCuLtsZAU,6660
19
+ awsimple-6.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ awsimple-6.0.0.dist-info/top_level.txt,sha256=mwqCoH_8vAaK6iYioiRbasXmVCQM7AK3grNWOKp2VHE,9
21
+ awsimple-6.0.0.dist-info/RECORD,,