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 +1 -1
- awsimple/__version__.py +1 -1
- awsimple/pubsub.py +149 -51
- awsimple/sqs.py +3 -3
- {awsimple-4.2.0.dist-info → awsimple-6.0.0.dist-info}/METADATA +2 -1
- {awsimple-4.2.0.dist-info → awsimple-6.0.0.dist-info}/RECORD +10 -10
- {awsimple-4.2.0.dist-info → awsimple-6.0.0.dist-info}/WHEEL +0 -0
- {awsimple-4.2.0.dist-info → awsimple-6.0.0.dist-info}/licenses/LICENSE +0 -0
- {awsimple-4.2.0.dist-info → awsimple-6.0.0.dist-info}/licenses/LICENSE.txt +0 -0
- {awsimple-4.2.0.dist-info → awsimple-6.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__ = "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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
|
144
|
-
sub_callback: Callable | None
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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.
|
|
160
|
-
self.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
#
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: awsimple
|
|
3
|
-
Version:
|
|
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=
|
|
2
|
-
awsimple/__version__.py,sha256=
|
|
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=
|
|
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-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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|