appmesh 1.3.3__py3-none-any.whl → 1.3.5__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.
- appmesh/appmesh_client.py +102 -85
- {appmesh-1.3.3.dist-info → appmesh-1.3.5.dist-info}/METADATA +1 -1
- appmesh-1.3.5.dist-info/RECORD +6 -0
- {appmesh-1.3.3.dist-info → appmesh-1.3.5.dist-info}/WHEEL +1 -1
- appmesh-1.3.3.dist-info/RECORD +0 -6
- {appmesh-1.3.3.dist-info → appmesh-1.3.5.dist-info}/top_level.txt +0 -0
appmesh/appmesh_client.py
CHANGED
@@ -25,13 +25,16 @@ DEFAULT_TOKEN_EXPIRE_SECONDS = "P1W" # default 7 day(s)
|
|
25
25
|
DEFAULT_RUN_APP_TIMEOUT_SECONDS = "P2D" # 2 days
|
26
26
|
DEFAULT_RUN_APP_LIFECYCLE_SECONDS = "P2DT12H" # 2.5 days
|
27
27
|
|
28
|
+
DEFAULT_SSL_CA_PEM_FILE = "/opt/appmesh/ssl/ca.pem"
|
29
|
+
DEFAULT_SSL_CLIENT_PEM_FILE = "/opt/appmesh/ssl/client.pem"
|
30
|
+
DEFAULT_SSL_CLIENT_PEM_KEY_FILE = "/opt/appmesh/ssl/client-key.pem"
|
31
|
+
|
32
|
+
# TLS-optimized chunk size (slightly less than maximum TLS record size)
|
33
|
+
# leaves some room for TLS overhead (like headers) within the 16 KB limit.
|
34
|
+
TCP_CHUNK_BLOCK_SIZE = 16 * 1024 - 256 # target to 16KB
|
35
|
+
TCP_MESSAGE_HEADER_LENGTH = 4
|
28
36
|
REST_TEXT_MESSAGE_JSON_KEY = "message"
|
29
37
|
MESSAGE_ENCODING_UTF8 = "utf-8"
|
30
|
-
TCP_MESSAGE_HEADER_LENGTH = 4
|
31
|
-
|
32
|
-
_SSL_CA_PEM_FILE = "/opt/appmesh/ssl/ca.pem"
|
33
|
-
_SSL_CLIENT_PEM_FILE = "/opt/appmesh/ssl/client.pem"
|
34
|
-
_SSL_CLIENT_PEM_KEY_FILE = "/opt/appmesh/ssl/client-key.pem"
|
35
38
|
|
36
39
|
HTTP_USER_AGENT = "appmesh/python"
|
37
40
|
HTTP_USER_AGENT_TCP = "appmesh/python/tcp"
|
@@ -437,8 +440,8 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
437
440
|
def __init__(
|
438
441
|
self,
|
439
442
|
rest_url: str = "https://127.0.0.1:6060",
|
440
|
-
rest_ssl_verify=
|
441
|
-
rest_ssl_client_cert=(
|
443
|
+
rest_ssl_verify=DEFAULT_SSL_CA_PEM_FILE if os.path.exists(DEFAULT_SSL_CA_PEM_FILE) else False,
|
444
|
+
rest_ssl_client_cert=(DEFAULT_SSL_CLIENT_PEM_FILE, DEFAULT_SSL_CLIENT_PEM_KEY_FILE) if os.path.exists(DEFAULT_SSL_CLIENT_PEM_FILE) else None,
|
442
445
|
rest_timeout=(60, 300),
|
443
446
|
jwt_token=None,
|
444
447
|
):
|
@@ -450,7 +453,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
450
453
|
the server's TLS certificate, or a string, in which case it must be a path to a CA bundle to use. Defaults to ``True``.
|
451
454
|
rest_ssl_client_cert (tuple, optional): SSL client certificate and key pair. If String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
|
452
455
|
rest_timeout (tuple, optional): HTTP timeout, Defaults to 60 seconds for connect timeout and 300 seconds for read timeout
|
453
|
-
jwt_token (str, optional): JWT token, provide correct token is same with login() &
|
456
|
+
jwt_token (str, optional): JWT token, provide correct token is same with login() & authentication().
|
454
457
|
"""
|
455
458
|
|
456
459
|
self.server_url = rest_url
|
@@ -1221,24 +1224,20 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1221
1224
|
########################################
|
1222
1225
|
# File management
|
1223
1226
|
########################################
|
1224
|
-
def file_download(self,
|
1227
|
+
def file_download(self, remote_file: str, local_file: str, apply_file_attributes: bool = True) -> None:
|
1225
1228
|
"""Copy a remote file to local. Optionally, the local file will have the same permission as the remote file.
|
1226
1229
|
|
1227
1230
|
Args:
|
1228
|
-
|
1231
|
+
remote_file (str): the remote file path.
|
1229
1232
|
local_file (str): the local file path to be downloaded.
|
1230
1233
|
apply_file_attributes (bool): whether to apply file attributes (permissions, owner, group) to the local file.
|
1231
|
-
|
1232
|
-
Returns:
|
1233
|
-
bool: success or failure.
|
1234
1234
|
"""
|
1235
|
-
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header={"File-Path":
|
1236
|
-
|
1237
|
-
raise Exception(resp.text)
|
1235
|
+
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header={"File-Path": remote_file})
|
1236
|
+
resp.raise_for_status()
|
1238
1237
|
|
1239
1238
|
# Write the file content locally
|
1240
1239
|
with open(local_file, "wb") as fp:
|
1241
|
-
for chunk in resp.iter_content(chunk_size=
|
1240
|
+
for chunk in resp.iter_content(chunk_size=8 * 1024): # 8 KB
|
1242
1241
|
if chunk:
|
1243
1242
|
fp.write(chunk)
|
1244
1243
|
|
@@ -1251,11 +1250,10 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1251
1250
|
file_gid = int(resp.headers["File-Group"])
|
1252
1251
|
try:
|
1253
1252
|
os.chown(path=local_file, uid=file_uid, gid=file_gid)
|
1254
|
-
except
|
1255
|
-
print(
|
1256
|
-
return resp.status_code == HTTPStatus.OK
|
1253
|
+
except PermissionError:
|
1254
|
+
print(f"Warning: Unable to change owner/group of {local_file}. Operation requires elevated privileges.")
|
1257
1255
|
|
1258
|
-
def file_upload(self, local_file: str,
|
1256
|
+
def file_upload(self, local_file: str, remote_file: str, apply_file_attributes: bool = True) -> None:
|
1259
1257
|
"""Upload a local file to the remote server. Optionally, the remote file will have the same permission as the local file.
|
1260
1258
|
|
1261
1259
|
Dependency:
|
@@ -1264,22 +1262,22 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1264
1262
|
|
1265
1263
|
Args:
|
1266
1264
|
local_file (str): the local file path.
|
1267
|
-
|
1265
|
+
remote_file (str): the target remote file to be uploaded.
|
1268
1266
|
apply_file_attributes (bool): whether to upload file attributes (permissions, owner, group) along with the file.
|
1269
|
-
|
1270
|
-
Returns:
|
1271
|
-
bool: success or failure.
|
1272
1267
|
"""
|
1268
|
+
if not os.path.exists(local_file):
|
1269
|
+
raise FileNotFoundError(f"Local file not found: {local_file}")
|
1270
|
+
|
1273
1271
|
from requests_toolbelt import MultipartEncoder
|
1274
1272
|
|
1275
1273
|
with open(file=local_file, mode="rb") as fp:
|
1276
|
-
encoder = MultipartEncoder(fields={"filename": os.path.basename(
|
1277
|
-
header = {"File-Path":
|
1274
|
+
encoder = MultipartEncoder(fields={"filename": os.path.basename(remote_file), "file": ("filename", fp, "application/octet-stream")})
|
1275
|
+
header = {"File-Path": remote_file, "Content-Type": encoder.content_type}
|
1278
1276
|
|
1279
1277
|
# Include file attributes (permissions, owner, group) if requested
|
1280
1278
|
if apply_file_attributes:
|
1281
1279
|
file_stat = os.stat(local_file)
|
1282
|
-
header["File-Mode"] = str(file_stat.st_mode)
|
1280
|
+
header["File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
|
1283
1281
|
header["File-User"] = str(file_stat.st_uid)
|
1284
1282
|
header["File-Group"] = str(file_stat.st_gid)
|
1285
1283
|
|
@@ -1291,9 +1289,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1291
1289
|
header=header,
|
1292
1290
|
body=encoder,
|
1293
1291
|
)
|
1294
|
-
|
1295
|
-
raise Exception(resp.text)
|
1296
|
-
return True
|
1292
|
+
resp.raise_for_status()
|
1297
1293
|
|
1298
1294
|
########################################
|
1299
1295
|
# Application run
|
@@ -1450,11 +1446,11 @@ class AppMeshClientTCP(AppMeshClient):
|
|
1450
1446
|
"""
|
1451
1447
|
Client SDK for interacting with the App Mesh service over TCP, with enhanced support for large file transfers.
|
1452
1448
|
|
1453
|
-
The `AppMeshClientTCP` class extends the functionality of `AppMeshClient` by offering a TCP-based communication layer
|
1454
|
-
for the App Mesh REST API. It overrides the file download and upload methods to support large file transfers with
|
1449
|
+
The `AppMeshClientTCP` class extends the functionality of `AppMeshClient` by offering a TCP-based communication layer
|
1450
|
+
for the App Mesh REST API. It overrides the file download and upload methods to support large file transfers with
|
1455
1451
|
improved performance, leveraging TCP for lower latency and higher throughput compared to HTTP.
|
1456
1452
|
|
1457
|
-
This client is suitable for applications requiring efficient data transfers and high-throughput operations within the
|
1453
|
+
This client is suitable for applications requiring efficient data transfers and high-throughput operations within the
|
1458
1454
|
App Mesh ecosystem, while maintaining compatibility with all other attributes and methods from `AppMeshClient`.
|
1459
1455
|
|
1460
1456
|
Dependency:
|
@@ -1482,7 +1478,7 @@ class AppMeshClientTCP(AppMeshClient):
|
|
1482
1478
|
|
1483
1479
|
def __init__(
|
1484
1480
|
self,
|
1485
|
-
rest_ssl_verify=
|
1481
|
+
rest_ssl_verify=DEFAULT_SSL_CA_PEM_FILE if os.path.exists(DEFAULT_SSL_CA_PEM_FILE) else False,
|
1486
1482
|
rest_ssl_client_cert=None,
|
1487
1483
|
jwt_token=None,
|
1488
1484
|
tcp_address=("localhost", 6059),
|
@@ -1493,13 +1489,13 @@ class AppMeshClientTCP(AppMeshClient):
|
|
1493
1489
|
rest_ssl_verify (str, optional): (optional) SSL CA certification. Either a boolean, in which case it controls whether we verify
|
1494
1490
|
the server's TLS certificate, or a string, in which case it must be a path to a CA bundle to use. Defaults to ``True``.
|
1495
1491
|
rest_ssl_client_cert (tuple, optional): SSL client certificate and key pair . If String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
|
1496
|
-
jwt_token (str, optional): JWT token, provide correct token is same with login() &
|
1492
|
+
jwt_token (str, optional): JWT token, provide correct token is same with login() & authentication().
|
1497
1493
|
|
1498
1494
|
tcp_address (tuple, optional): TCP connect address.
|
1499
1495
|
"""
|
1500
|
-
super().__init__(rest_ssl_verify=rest_ssl_verify, rest_ssl_client_cert=rest_ssl_client_cert, jwt_token=jwt_token)
|
1501
1496
|
self.tcp_address = tcp_address
|
1502
1497
|
self.__socket_client = None
|
1498
|
+
super().__init__(rest_ssl_verify=rest_ssl_verify, rest_ssl_client_cert=rest_ssl_client_cert, jwt_token=jwt_token)
|
1503
1499
|
|
1504
1500
|
def __del__(self) -> None:
|
1505
1501
|
"""De-construction"""
|
@@ -1508,15 +1504,31 @@ class AppMeshClientTCP(AppMeshClient):
|
|
1508
1504
|
def __connect_socket(self) -> None:
|
1509
1505
|
"""Establish tcp connection"""
|
1510
1506
|
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
1507
|
+
# Set minimum TLS version
|
1511
1508
|
if hasattr(context, "minimum_version"):
|
1512
1509
|
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
1513
1510
|
else:
|
1514
1511
|
context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
|
1515
|
-
|
1516
|
-
|
1517
|
-
|
1518
|
-
|
1519
|
-
context.
|
1512
|
+
# Configure SSL verification
|
1513
|
+
if not self.ssl_verify:
|
1514
|
+
context.verify_mode = ssl.CERT_NONE
|
1515
|
+
else:
|
1516
|
+
context.verify_mode = ssl.CERT_REQUIRED # Require certificate verification
|
1517
|
+
context.load_default_certs() # Load system's default CA certificates
|
1518
|
+
if isinstance(self.ssl_verify, str):
|
1519
|
+
if os.path.isfile(self.ssl_verify):
|
1520
|
+
# Add custom CA certificate file
|
1521
|
+
try:
|
1522
|
+
context.load_verify_locations(cafile=self.ssl_verify)
|
1523
|
+
except ssl.SSLError:
|
1524
|
+
# If loading fails, try using just the default CAs
|
1525
|
+
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
1526
|
+
elif os.path.isdir(self.ssl_verify):
|
1527
|
+
# Load CA certificates from directory
|
1528
|
+
context.load_verify_locations(capath=self.ssl_verify)
|
1529
|
+
else:
|
1530
|
+
raise ValueError(f"ssl_verify path '{self.ssl_verify}' is neither a file nor a directory")
|
1531
|
+
|
1520
1532
|
if self.ssl_client_cert is not None:
|
1521
1533
|
# Load client-side certificate and private key
|
1522
1534
|
context.load_cert_chain(certfile=self.ssl_client_cert[0], keyfile=self.ssl_client_cert[1])
|
@@ -1664,31 +1676,34 @@ class AppMeshClientTCP(AppMeshClient):
|
|
1664
1676
|
########################################
|
1665
1677
|
# File management
|
1666
1678
|
########################################
|
1667
|
-
def file_download(self,
|
1679
|
+
def file_download(self, remote_file: str, local_file: str, apply_file_attributes: bool = True) -> None:
|
1668
1680
|
"""Copy a remote file to local, the local file will have the same permission as the remote file
|
1669
1681
|
|
1670
1682
|
Args:
|
1671
|
-
|
1683
|
+
remote_file (str): the remote file path.
|
1672
1684
|
local_file (str): the local file path to be downloaded.
|
1673
|
-
|
1674
|
-
Returns:
|
1675
|
-
bool: success or failure.
|
1685
|
+
apply_file_attributes (bool): whether to apply file attributes (permissions, owner, group) to the local file.
|
1676
1686
|
"""
|
1677
|
-
header = {}
|
1678
|
-
header["File-Path"] = file_path
|
1687
|
+
header = {"File-Path": remote_file}
|
1679
1688
|
header[HTTP_HEADER_KEY_X_RECV_FILE_SOCKET] = "true"
|
1680
1689
|
resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header=header)
|
1681
|
-
|
1682
|
-
|
1683
|
-
|
1690
|
+
|
1691
|
+
resp.raise_for_status()
|
1692
|
+
if HTTP_HEADER_KEY_X_RECV_FILE_SOCKET not in resp.headers:
|
1693
|
+
raise ValueError(f"Server did not respond with socket transfer option: {HTTP_HEADER_KEY_X_RECV_FILE_SOCKET}")
|
1694
|
+
|
1695
|
+
with open(local_file, "wb") as fp:
|
1696
|
+
chunk_data = bytes()
|
1697
|
+
chunk_size = int.from_bytes(self.__recvall(TCP_MESSAGE_HEADER_LENGTH), byteorder="big", signed=False)
|
1698
|
+
while chunk_size > 0:
|
1699
|
+
chunk_data = self.__recvall(chunk_size)
|
1700
|
+
if chunk_data is None or len(chunk_data) == 0:
|
1701
|
+
self.__close_socket()
|
1702
|
+
raise Exception("socket connection broken")
|
1703
|
+
fp.write(chunk_data)
|
1684
1704
|
chunk_size = int.from_bytes(self.__recvall(TCP_MESSAGE_HEADER_LENGTH), byteorder="big", signed=False)
|
1685
|
-
|
1686
|
-
|
1687
|
-
if chunk_data is None or len(chunk_data) == 0:
|
1688
|
-
self.__close_socket()
|
1689
|
-
raise Exception("socket connection broken")
|
1690
|
-
fp.write(chunk_data)
|
1691
|
-
chunk_size = int.from_bytes(self.__recvall(TCP_MESSAGE_HEADER_LENGTH), byteorder="big", signed=False)
|
1705
|
+
|
1706
|
+
if apply_file_attributes:
|
1692
1707
|
if "File-Mode" in resp.headers:
|
1693
1708
|
os.chmod(path=local_file, mode=int(resp.headers["File-Mode"]))
|
1694
1709
|
if "File-User" in resp.headers and "File-Group" in resp.headers:
|
@@ -1696,12 +1711,10 @@ class AppMeshClientTCP(AppMeshClient):
|
|
1696
1711
|
file_gid = int(resp.headers["File-Group"])
|
1697
1712
|
try:
|
1698
1713
|
os.chown(path=local_file, uid=file_uid, gid=file_gid)
|
1699
|
-
except
|
1700
|
-
print(
|
1701
|
-
return True
|
1702
|
-
return False
|
1714
|
+
except PermissionError:
|
1715
|
+
print(f"Warning: Unable to change owner/group of {local_file}. Operation requires elevated privileges.")
|
1703
1716
|
|
1704
|
-
def file_upload(self, local_file: str,
|
1717
|
+
def file_upload(self, local_file: str, remote_file: str, apply_file_attributes: bool = True) -> None:
|
1705
1718
|
"""Upload a local file to the remote server, the remote file will have the same permission as the local file
|
1706
1719
|
|
1707
1720
|
Dependency:
|
@@ -1710,30 +1723,34 @@ class AppMeshClientTCP(AppMeshClient):
|
|
1710
1723
|
|
1711
1724
|
Args:
|
1712
1725
|
local_file (str): the local file path.
|
1713
|
-
|
1714
|
-
|
1715
|
-
Returns:
|
1716
|
-
bool: success or failure.
|
1717
|
-
str: text message.
|
1726
|
+
remote_file (str): the target remote file to be uploaded.
|
1727
|
+
apply_file_attributes (bool): whether to upload file attributes (permissions, owner, group) along with the file.
|
1718
1728
|
"""
|
1729
|
+
if not os.path.exists(local_file):
|
1730
|
+
raise FileNotFoundError(f"Local file not found: {local_file}")
|
1731
|
+
|
1719
1732
|
with open(file=local_file, mode="rb") as fp:
|
1720
|
-
|
1721
|
-
header = {}
|
1722
|
-
header["File-Path"] = file_path
|
1723
|
-
header["File-Mode"] = str(file_stat.st_mode)
|
1724
|
-
header["File-User"] = str(file_stat.st_uid)
|
1725
|
-
header["File-Group"] = str(file_stat.st_gid)
|
1726
|
-
header["Content-Type"] = "text/plain"
|
1733
|
+
header = {"File-Path": remote_file, "Content-Type": "text/plain"}
|
1727
1734
|
header[HTTP_HEADER_KEY_X_SEND_FILE_SOCKET] = "true"
|
1735
|
+
|
1736
|
+
if apply_file_attributes:
|
1737
|
+
file_stat = os.stat(local_file)
|
1738
|
+
header["File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
|
1739
|
+
header["File-User"] = str(file_stat.st_uid)
|
1740
|
+
header["File-Group"] = str(file_stat.st_gid)
|
1741
|
+
|
1728
1742
|
# https://stackoverflow.com/questions/22567306/python-requests-file-upload
|
1729
1743
|
resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/file/upload", header=header)
|
1730
|
-
|
1731
|
-
|
1744
|
+
|
1745
|
+
resp.raise_for_status()
|
1746
|
+
if HTTP_HEADER_KEY_X_SEND_FILE_SOCKET not in resp.headers:
|
1747
|
+
raise ValueError(f"Server did not respond with socket transfer option: {HTTP_HEADER_KEY_X_SEND_FILE_SOCKET}")
|
1748
|
+
|
1749
|
+
chunk_size = TCP_CHUNK_BLOCK_SIZE
|
1750
|
+
while True:
|
1732
1751
|
chunk_data = fp.read(chunk_size)
|
1733
|
-
|
1734
|
-
self.__socket_client.sendall(
|
1735
|
-
|
1736
|
-
|
1737
|
-
self.__socket_client.sendall(
|
1738
|
-
return True, ""
|
1739
|
-
return False, resp.json()[REST_TEXT_MESSAGE_JSON_KEY]
|
1752
|
+
if not chunk_data:
|
1753
|
+
self.__socket_client.sendall((0).to_bytes(TCP_MESSAGE_HEADER_LENGTH, byteorder="big", signed=False))
|
1754
|
+
break
|
1755
|
+
self.__socket_client.sendall(len(chunk_data).to_bytes(TCP_MESSAGE_HEADER_LENGTH, byteorder="big", signed=False))
|
1756
|
+
self.__socket_client.sendall(chunk_data)
|
@@ -0,0 +1,6 @@
|
|
1
|
+
appmesh/__init__.py,sha256=xRdXeFHEieRauuJZElbEBASgXG0ZzU1a5_0isAhM7Gw,11
|
2
|
+
appmesh/appmesh_client.py,sha256=bduPEDpI36XQyK_U9y3EvpZvO9CbpDeAB5402yf9qhU,69329
|
3
|
+
appmesh-1.3.5.dist-info/METADATA,sha256=VSkQis_Azjvw2FwjrK8qjOvYKw2WRNB37J2tYm_KEbw,11191
|
4
|
+
appmesh-1.3.5.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
5
|
+
appmesh-1.3.5.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
|
6
|
+
appmesh-1.3.5.dist-info/RECORD,,
|
appmesh-1.3.3.dist-info/RECORD
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
appmesh/__init__.py,sha256=xRdXeFHEieRauuJZElbEBASgXG0ZzU1a5_0isAhM7Gw,11
|
2
|
-
appmesh/appmesh_client.py,sha256=Hsp4at6YCLcAoz8eRx928amyCTqN4ZAIFnVHxEbzDrI,67720
|
3
|
-
appmesh-1.3.3.dist-info/METADATA,sha256=73_wYNYMLoHLoew35YGTd2bGKWhfScEObKhr07yyHx0,11191
|
4
|
-
appmesh-1.3.3.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
|
5
|
-
appmesh-1.3.3.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
|
6
|
-
appmesh-1.3.3.dist-info/RECORD,,
|
File without changes
|