boto3-refresh-session 5.0.1__py3-none-any.whl → 5.1.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.
- boto3_refresh_session/__init__.py +1 -1
- boto3_refresh_session/methods/iot/x509.py +234 -30
- boto3_refresh_session/utils/typing.py +4 -0
- {boto3_refresh_session-5.0.1.dist-info → boto3_refresh_session-5.1.1.dist-info}/METADATA +10 -2
- {boto3_refresh_session-5.0.1.dist-info → boto3_refresh_session-5.1.1.dist-info}/RECORD +8 -8
- {boto3_refresh_session-5.0.1.dist-info → boto3_refresh_session-5.1.1.dist-info}/LICENSE +0 -0
- {boto3_refresh_session-5.0.1.dist-info → boto3_refresh_session-5.1.1.dist-info}/NOTICE +0 -0
- {boto3_refresh_session-5.0.1.dist-info → boto3_refresh_session-5.1.1.dist-info}/WHEEL +0 -0
@@ -2,10 +2,13 @@ __all__ = ["IOTX509RefreshableSession"]
|
|
2
2
|
|
3
3
|
import json
|
4
4
|
import re
|
5
|
+
from atexit import register
|
5
6
|
from pathlib import Path
|
6
|
-
from
|
7
|
+
from tempfile import NamedTemporaryFile
|
8
|
+
from typing import cast, get_args
|
7
9
|
from urllib.parse import ParseResult, urlparse
|
8
10
|
|
11
|
+
from awscrt import auth, io
|
9
12
|
from awscrt.exceptions import AwsCrtError
|
10
13
|
from awscrt.http import HttpClientConnection, HttpRequest
|
11
14
|
from awscrt.io import (
|
@@ -13,10 +16,14 @@ from awscrt.io import (
|
|
13
16
|
ClientTlsContext,
|
14
17
|
DefaultHostResolver,
|
15
18
|
EventLoopGroup,
|
19
|
+
LogLevel,
|
16
20
|
Pkcs11Lib,
|
17
21
|
TlsConnectionOptions,
|
18
22
|
TlsContextOptions,
|
23
|
+
init_logging,
|
19
24
|
)
|
25
|
+
from awscrt.mqtt import Connection
|
26
|
+
from awsiot import mqtt_connection_builder
|
20
27
|
|
21
28
|
from ...exceptions import BRSError, BRSWarning
|
22
29
|
from ...utils import (
|
@@ -24,10 +31,13 @@ from ...utils import (
|
|
24
31
|
AWSCRTResponse,
|
25
32
|
Identity,
|
26
33
|
TemporaryCredentials,
|
34
|
+
Transport,
|
27
35
|
refreshable_session,
|
28
36
|
)
|
29
37
|
from .core import BaseIoTRefreshableSession
|
30
38
|
|
39
|
+
_TEMP_PATHS: list[str] = []
|
40
|
+
|
31
41
|
|
32
42
|
@refreshable_session
|
33
43
|
class IOTX509RefreshableSession(
|
@@ -71,6 +81,9 @@ class IOTX509RefreshableSession(
|
|
71
81
|
The duration for which the temporary credentials are valid, in
|
72
82
|
seconds. Cannot exceed the value declared in the IAM policy.
|
73
83
|
Default is None.
|
84
|
+
awscrt_log_level : awscrt.LogLevel | None, optional
|
85
|
+
The logging level for the AWS CRT library, e.g.
|
86
|
+
``awscrt.LogLevel.INFO``. Default is None.
|
74
87
|
|
75
88
|
Notes
|
76
89
|
-----
|
@@ -90,34 +103,34 @@ class IOTX509RefreshableSession(
|
|
90
103
|
verify_peer: bool = True,
|
91
104
|
timeout: float | int | None = None,
|
92
105
|
duration_seconds: int | None = None,
|
106
|
+
awscrt_log_level: LogLevel | None = None,
|
93
107
|
**kwargs,
|
94
108
|
):
|
95
109
|
# initializing BRSSession
|
96
110
|
super().__init__(refresh_method="iot-x509", **kwargs)
|
97
111
|
|
112
|
+
# logging
|
113
|
+
if awscrt_log_level:
|
114
|
+
init_logging(log_level=awscrt_log_level, file_name="stdout")
|
115
|
+
|
98
116
|
# initializing public attributes
|
99
117
|
self.endpoint = self._normalize_iot_credential_endpoint(
|
100
118
|
endpoint=endpoint
|
101
119
|
)
|
102
120
|
self.role_alias = role_alias
|
103
|
-
self.certificate =
|
121
|
+
self.certificate = self._read_maybe_path_to_bytes(
|
122
|
+
certificate, fallback=None, name="certificate"
|
123
|
+
)
|
104
124
|
self.thing_name = thing_name
|
105
|
-
self.private_key =
|
106
|
-
|
107
|
-
|
125
|
+
self.private_key = self._read_maybe_path_to_bytes(
|
126
|
+
private_key, fallback=None, name="private_key"
|
127
|
+
)
|
128
|
+
self.pkcs11 = self._validate_pkcs11(pkcs11) if pkcs11 else None
|
129
|
+
self.ca = self._read_maybe_path_to_bytes(ca, fallback=None, name="ca")
|
108
130
|
self.verify_peer = verify_peer
|
109
131
|
self.timeout = 10.0 if timeout is None else timeout
|
110
132
|
self.duration_seconds = duration_seconds
|
111
133
|
|
112
|
-
# loading X.509 certificate if presented as a string, which
|
113
|
-
# is presumed to be the file path.
|
114
|
-
# if presented as bytes then self.certificate is presumed to be
|
115
|
-
# the actual certificate itself
|
116
|
-
if self.certificate and isinstance(self.certificate, str):
|
117
|
-
self.certificate = (
|
118
|
-
Path(self.certificate).expanduser().resolve().read_bytes()
|
119
|
-
)
|
120
|
-
|
121
134
|
# either private_key or pkcs11 must be provided
|
122
135
|
if self.private_key is None and self.pkcs11 is None:
|
123
136
|
raise BRSError(
|
@@ -130,22 +143,6 @@ class IOTX509RefreshableSession(
|
|
130
143
|
"Only one of 'private_key' or 'pkcs11' can be provided."
|
131
144
|
)
|
132
145
|
|
133
|
-
# if the provided private_key is bytes then it's presumed to be
|
134
|
-
# the actual private key. but if it's string then it's presumed
|
135
|
-
# to be the file path
|
136
|
-
if self.private_key and isinstance(self.private_key, str):
|
137
|
-
self.private_key = (
|
138
|
-
Path(self.private_key).expanduser().resolve().read_bytes()
|
139
|
-
)
|
140
|
-
|
141
|
-
# verifying PKCS#11 dict
|
142
|
-
if self.pkcs11:
|
143
|
-
self.pkcs11 = self._validate_pkcs11(pkcs11=self.pkcs11)
|
144
|
-
|
145
|
-
# ca is like many other attributes in that str implies file location
|
146
|
-
if self.ca and isinstance(self.ca, str):
|
147
|
-
self.ca = Path(self.ca).expanduser().resolve().read_bytes()
|
148
|
-
|
149
146
|
def _get_credentials(self) -> TemporaryCredentials:
|
150
147
|
url = urlparse(
|
151
148
|
f"https://{self.endpoint}/role-aliases/{self.role_alias}"
|
@@ -334,3 +331,210 @@ class IOTX509RefreshableSession(
|
|
334
331
|
pkcs11.setdefault("token_label", None)
|
335
332
|
pkcs11.setdefault("private_key_label", None)
|
336
333
|
return pkcs11
|
334
|
+
|
335
|
+
@staticmethod
|
336
|
+
def _read_maybe_path_to_bytes(
|
337
|
+
v: str | bytes | None, fallback: bytes | None, name: str
|
338
|
+
) -> bytes | None:
|
339
|
+
match v:
|
340
|
+
case None:
|
341
|
+
return fallback
|
342
|
+
case bytes():
|
343
|
+
return v
|
344
|
+
case str() as p if Path(p).expanduser().resolve().is_file():
|
345
|
+
return Path(p).expanduser().resolve().read_bytes()
|
346
|
+
case _:
|
347
|
+
raise BRSError(f"Invalid {name} provided.")
|
348
|
+
|
349
|
+
@staticmethod
|
350
|
+
def _bytes_to_tempfile(b: bytes, suffix: str = ".pem") -> str:
|
351
|
+
f = NamedTemporaryFile("wb", suffix=suffix, delete=False)
|
352
|
+
f.write(b)
|
353
|
+
f.flush()
|
354
|
+
f.close()
|
355
|
+
_TEMP_PATHS.append(f.name)
|
356
|
+
return f.name
|
357
|
+
|
358
|
+
@staticmethod
|
359
|
+
@register
|
360
|
+
def _cleanup_tempfiles():
|
361
|
+
for p in _TEMP_PATHS:
|
362
|
+
try:
|
363
|
+
Path(p).unlink(missing_ok=True)
|
364
|
+
except Exception:
|
365
|
+
...
|
366
|
+
|
367
|
+
def mqtt(
|
368
|
+
self,
|
369
|
+
*,
|
370
|
+
endpoint: str,
|
371
|
+
client_id: str,
|
372
|
+
transport: Transport = "x509",
|
373
|
+
certificate: str | bytes | None = None,
|
374
|
+
private_key: str | bytes | None = None,
|
375
|
+
ca: str | bytes | None = None,
|
376
|
+
pkcs11: PKCS11 | None = None,
|
377
|
+
region: str | None = None,
|
378
|
+
keep_alive_secs: int = 60,
|
379
|
+
clean_start: bool = True,
|
380
|
+
port: int | None = None,
|
381
|
+
use_alpn: bool = False,
|
382
|
+
) -> Connection:
|
383
|
+
"""Establishes an MQTT connection using the specified parameters.
|
384
|
+
|
385
|
+
.. versionadded:: 5.1.0
|
386
|
+
|
387
|
+
Parameters
|
388
|
+
----------
|
389
|
+
endpoint: str
|
390
|
+
The MQTT endpoint to connect to.
|
391
|
+
client_id: str
|
392
|
+
The client ID to use for the MQTT connection.
|
393
|
+
transport: Transport
|
394
|
+
The transport protocol to use (e.g., "x509" or "ws").
|
395
|
+
certificate: str | bytes | None, optional
|
396
|
+
The client certificate to use for the connection. Defaults to the
|
397
|
+
session certificate.
|
398
|
+
private_key: str | bytes | None, optional
|
399
|
+
The private key to use for the connection. Defaults to the
|
400
|
+
session private key.
|
401
|
+
ca: str | bytes | None, optional
|
402
|
+
The CA certificate to use for the connection. Defaults to the
|
403
|
+
session CA certificate.
|
404
|
+
pkcs11: PKCS11 | None, optional
|
405
|
+
PKCS#11 configuration for hardware-backed keys. Defaults to the
|
406
|
+
session PKCS#11 configuration.
|
407
|
+
region: str | None, optional
|
408
|
+
The AWS region to use for the connection. Defaults to the
|
409
|
+
session region.
|
410
|
+
keep_alive_secs: int, optional
|
411
|
+
The keep-alive interval for the MQTT connection. Default is 60
|
412
|
+
seconds.
|
413
|
+
clean_start: bool, optional
|
414
|
+
Whether to start a clean session. Default is True.
|
415
|
+
port: int | None, optional
|
416
|
+
The port to use for the MQTT connection. Default is 8883 if not
|
417
|
+
using ALPN, otherwise 443.
|
418
|
+
use_alpn: bool, optional
|
419
|
+
Whether to use ALPN for the connection. Default is False.
|
420
|
+
|
421
|
+
Returns
|
422
|
+
-------
|
423
|
+
awscrt.mqtt.Connection
|
424
|
+
The established MQTT connection.
|
425
|
+
"""
|
426
|
+
|
427
|
+
# Validate transport
|
428
|
+
if transport not in list(get_args(Transport)):
|
429
|
+
raise BRSError("Transport must be 'x509' or 'ws'")
|
430
|
+
|
431
|
+
# Region default (WS only)
|
432
|
+
if region is None:
|
433
|
+
region = self.region_name
|
434
|
+
|
435
|
+
# Normalize inputs to bytes using session defaults
|
436
|
+
cert_bytes = self._read_maybe_path_to_bytes(
|
437
|
+
certificate, getattr(self, "certificate", None), "certificate"
|
438
|
+
)
|
439
|
+
key_bytes = self._read_maybe_path_to_bytes(
|
440
|
+
private_key, getattr(self, "private_key", None), "private_key"
|
441
|
+
)
|
442
|
+
ca_bytes = self._read_maybe_path_to_bytes(
|
443
|
+
ca, getattr(self, "ca", None), "ca"
|
444
|
+
)
|
445
|
+
|
446
|
+
# Validate PKCS#11
|
447
|
+
match pkcs11:
|
448
|
+
case None:
|
449
|
+
pkcs11 = getattr(self, "pkcs11", None)
|
450
|
+
case dict():
|
451
|
+
pkcs11 = self._validate_pkcs11(pkcs11)
|
452
|
+
case _:
|
453
|
+
raise BRSError("Invalid PKCS#11 configuration provided.")
|
454
|
+
|
455
|
+
# X.509 invariants
|
456
|
+
if transport == "x509":
|
457
|
+
has_key = key_bytes is not None
|
458
|
+
has_hsm = pkcs11 is not None
|
459
|
+
if not has_key and not has_hsm:
|
460
|
+
raise BRSError(
|
461
|
+
"For transport='x509', provide either 'private_key' "
|
462
|
+
"(bytes/path) or 'pkcs11'."
|
463
|
+
)
|
464
|
+
if has_key and has_hsm:
|
465
|
+
raise BRSError(
|
466
|
+
"Provide only one of 'private_key' or 'pkcs11' for "
|
467
|
+
"transport='x509'."
|
468
|
+
)
|
469
|
+
if cert_bytes is None:
|
470
|
+
raise BRSError("Certificate is required for transport='x509'")
|
471
|
+
|
472
|
+
# CRT bootstrap
|
473
|
+
event_loop = io.EventLoopGroup(1)
|
474
|
+
host_resolver = io.DefaultHostResolver(event_loop)
|
475
|
+
bootstrap = io.ClientBootstrap(event_loop, host_resolver)
|
476
|
+
|
477
|
+
# Build connection
|
478
|
+
if transport == "x509":
|
479
|
+
if pkcs11 is not None:
|
480
|
+
# Cert must be a filepath for PKCS#11 builder → write temp
|
481
|
+
cert_path = self._bytes_to_tempfile(
|
482
|
+
cast(bytes, cert_bytes), ".crt"
|
483
|
+
)
|
484
|
+
ca_path = (
|
485
|
+
self._bytes_to_tempfile(ca_bytes, ".pem")
|
486
|
+
if ca_bytes
|
487
|
+
else None
|
488
|
+
)
|
489
|
+
|
490
|
+
return mqtt_connection_builder.mtls_with_pkcs11(
|
491
|
+
endpoint=endpoint,
|
492
|
+
client_bootstrap=bootstrap,
|
493
|
+
pkcs11_lib=Pkcs11Lib(file=pkcs11["pkcs11_lib"]),
|
494
|
+
user_pin=pkcs11.get("user_pin"),
|
495
|
+
slot_id=pkcs11.get("slot_id"),
|
496
|
+
token_label=pkcs11.get("token_label"),
|
497
|
+
private_key_object=pkcs11.get("private_key_label"),
|
498
|
+
cert_filepath=cert_path,
|
499
|
+
ca_filepath=ca_path,
|
500
|
+
client_id=client_id,
|
501
|
+
clean_session=clean_start,
|
502
|
+
keep_alive_secs=keep_alive_secs,
|
503
|
+
port=port or (443 if use_alpn else 8883),
|
504
|
+
alpn_list=["x-amzn-mqtt-ca"] if use_alpn else None,
|
505
|
+
)
|
506
|
+
else:
|
507
|
+
# pure mTLS with in-memory cert/key/CA
|
508
|
+
return mqtt_connection_builder.mtls_from_bytes(
|
509
|
+
endpoint=endpoint,
|
510
|
+
cert_bytes=cert_bytes,
|
511
|
+
pri_key_bytes=key_bytes,
|
512
|
+
ca_bytes=ca_bytes,
|
513
|
+
client_bootstrap=bootstrap,
|
514
|
+
client_id=client_id,
|
515
|
+
clean_session=clean_start,
|
516
|
+
keep_alive_secs=keep_alive_secs,
|
517
|
+
port=port or (443 if use_alpn else 8883),
|
518
|
+
alpn_list=["x-amzn-mqtt-ca"] if use_alpn else None,
|
519
|
+
)
|
520
|
+
|
521
|
+
else: # transport == "ws"
|
522
|
+
# WebSockets + SigV4
|
523
|
+
creds_provider = auth.AwsCredentialsProvider.new_delegate(
|
524
|
+
self._credentials
|
525
|
+
)
|
526
|
+
ca_path = (
|
527
|
+
self._bytes_to_tempfile(ca_bytes, ".pem") if ca_bytes else None
|
528
|
+
)
|
529
|
+
|
530
|
+
return mqtt_connection_builder.websockets_with_default_aws_signing(
|
531
|
+
endpoint=endpoint,
|
532
|
+
client_bootstrap=bootstrap,
|
533
|
+
region=region,
|
534
|
+
credentials_provider=creds_provider,
|
535
|
+
client_id=client_id,
|
536
|
+
clean_session=clean_start,
|
537
|
+
keep_alive_secs=keep_alive_secs,
|
538
|
+
ca_filepath=ca_path,
|
539
|
+
port=port or 443,
|
540
|
+
)
|
@@ -13,6 +13,7 @@ __all__ = [
|
|
13
13
|
"STSClientParams",
|
14
14
|
"TemporaryCredentials",
|
15
15
|
"RefreshableTemporaryCredentials",
|
16
|
+
"Transport",
|
16
17
|
]
|
17
18
|
|
18
19
|
from datetime import datetime
|
@@ -57,6 +58,9 @@ RegistryKey = TypeVar("RegistryKey", bound=str)
|
|
57
58
|
#: Type alias for values returned by get_identity
|
58
59
|
Identity: TypeAlias = dict[str, Any]
|
59
60
|
|
61
|
+
#: Type alias for acceptable transports
|
62
|
+
Transport: TypeAlias = Literal["x509", "ws"]
|
63
|
+
|
60
64
|
|
61
65
|
class TemporaryCredentials(TypedDict):
|
62
66
|
"""Temporary IAM credentials."""
|
@@ -1,9 +1,9 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: boto3-refresh-session
|
3
|
-
Version: 5.
|
3
|
+
Version: 5.1.1
|
4
4
|
Summary: A simple Python package for refreshing the temporary security credentials in a boto3.session.Session object automatically.
|
5
5
|
License: MIT
|
6
|
-
Keywords: boto3,botocore,aws,sts,credentials,token,refresh,iot,x509
|
6
|
+
Keywords: boto3,botocore,aws,sts,credentials,token,refresh,iot,x509,mqtt
|
7
7
|
Author: Mike Letts
|
8
8
|
Author-email: lettsmt@gmail.com
|
9
9
|
Maintainer: Michael Letts
|
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
17
17
|
Classifier: Programming Language :: Python :: 3.13
|
18
18
|
Requires-Dist: awscrt
|
19
|
+
Requires-Dist: awsiotsdk
|
19
20
|
Requires-Dist: boto3
|
20
21
|
Requires-Dist: botocore
|
21
22
|
Requires-Dist: requests
|
@@ -125,6 +126,7 @@ Description-Content-Type: text/markdown
|
|
125
126
|
- **STS**
|
126
127
|
- **IoT Core**
|
127
128
|
- X.509 certificates w/ role aliases over mTLS (PEM files and PKCS#11)
|
129
|
+
- MQTT actions are available!
|
128
130
|
- Custom authentication methods
|
129
131
|
- Natively supports all parameters supported by `boto3.session.Session`
|
130
132
|
- [Tested](https://github.com/michaelthomasletts/boto3-refresh-session/tree/main/tests), [documented](https://michaelthomasletts.github.io/boto3-refresh-session/index.html), and [published to PyPI](https://pypi.org/project/boto3-refresh-session/)
|
@@ -343,6 +345,8 @@ pip install boto3-refresh-session
|
|
343
345
|
|
344
346
|
## ⚠️ Changes
|
345
347
|
|
348
|
+
Browse through the various changes to `boto3-refresh-session` over time.
|
349
|
+
|
346
350
|
#### 😥 v3.0.0
|
347
351
|
|
348
352
|
**The changes introduced by v3.0.0 will not impact ~99% of users** who generally interact with `boto3-refresh-session` by only `RefreshableSession`, *which is the intended usage for this package after all.*
|
@@ -358,3 +362,7 @@ The `ecs` module has been dropped. For additional details and rationale, please
|
|
358
362
|
#### 😛 v5.0.0
|
359
363
|
|
360
364
|
Support for IoT Core via X.509 certificate-based authentication (over HTTPS) is now available!
|
365
|
+
|
366
|
+
#### ➕ v5.1.0
|
367
|
+
|
368
|
+
MQTT support added for IoT Core via X.509 certificate-based authentication.
|
@@ -1,17 +1,17 @@
|
|
1
|
-
boto3_refresh_session/__init__.py,sha256=
|
1
|
+
boto3_refresh_session/__init__.py,sha256=lLtO2SPPs53Xz2AGB-vW9jyUrjKgFQ_kD7vFJJ-1Il0,415
|
2
2
|
boto3_refresh_session/exceptions.py,sha256=QS5_xy3hNrfkdT_wKPZWH8WqSbFYCKPcK8DomGYIvcU,1218
|
3
3
|
boto3_refresh_session/methods/__init__.py,sha256=FpwWixSVpy_6pUe1u4fXmjO-_fDH--qTk_xrMnBCHxU,193
|
4
4
|
boto3_refresh_session/methods/custom.py,sha256=MLdUMU9s6NQoJWBKQ5Fsxeyxb_Xrm9V59pVX22M8fyI,4178
|
5
5
|
boto3_refresh_session/methods/iot/__init__.py,sha256=wIYp7HFZ_Q8XEHwWmpKjDNXxBm29C0RisP_9GSVwzZI,147
|
6
6
|
boto3_refresh_session/methods/iot/core.py,sha256=xtvbC23h6fw06lRZWN4r7TlnUEf3t9T7-zSPGCSlSLI,1151
|
7
|
-
boto3_refresh_session/methods/iot/x509.py,sha256=
|
7
|
+
boto3_refresh_session/methods/iot/x509.py,sha256=3j6gbZQAZnznZuNEipMyvOqux2RRDgQ65rxlF6LKVZE,19912
|
8
8
|
boto3_refresh_session/methods/sts.py,sha256=NGqJFJNLjG9Mve7o19tb_i6lvgQW1HoALIqF6lJNV9A,3336
|
9
9
|
boto3_refresh_session/session.py,sha256=UM_dWHSo0Wn8gLN99zg36SRVb-Yy_to1wk8UgZEuQZA,2086
|
10
10
|
boto3_refresh_session/utils/__init__.py,sha256=6F2ErbgBT2ZmZwFF3OzvQEd1Vh4XM3kaL6YGMTrcrkQ,156
|
11
11
|
boto3_refresh_session/utils/internal.py,sha256=HbuIzT0pC8QS4pgNj3M7POGaW-OEz2l3ESfYI1Qouuo,7072
|
12
|
-
boto3_refresh_session/utils/typing.py,sha256=
|
13
|
-
boto3_refresh_session-5.
|
14
|
-
boto3_refresh_session-5.
|
15
|
-
boto3_refresh_session-5.
|
16
|
-
boto3_refresh_session-5.
|
17
|
-
boto3_refresh_session-5.
|
12
|
+
boto3_refresh_session/utils/typing.py,sha256=YbnVYPe-ZEr79THp78u74PzF6LOIxpf91yGsvBmhEBM,3532
|
13
|
+
boto3_refresh_session-5.1.1.dist-info/LICENSE,sha256=I3ZYTXAjbIly6bm6J-TvFTuuHwTKws4h89QaY5c5HiY,1067
|
14
|
+
boto3_refresh_session-5.1.1.dist-info/METADATA,sha256=aZFKFsZXiNCmZqjRAHzaPVQBkHWPLsIJ4PxGpOPWE0E,14188
|
15
|
+
boto3_refresh_session-5.1.1.dist-info/NOTICE,sha256=1s8r33qbl1z0YvPB942iWgvbkP94P_e8AnROr1qXXuw,939
|
16
|
+
boto3_refresh_session-5.1.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
17
|
+
boto3_refresh_session-5.1.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|