addftool 0.1.3__py3-none-any.whl → 0.1.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.
- addftool/deploy/__init__.py +35 -0
- addftool/deploy/azure.py +39 -0
- addftool/deploy/ssh_server.py +137 -0
- addftool/process/__init__.py +7 -6
- addftool/process/utils.py +6 -5
- addftool/ssh/__init__.py +0 -0
- addftool/sync/__init__.py +42 -0
- {addftool-0.1.3.dist-info → addftool-0.1.5.dist-info}/METADATA +1 -1
- addftool-0.1.5.dist-info/RECORD +17 -0
- addftool-0.1.3.dist-info/RECORD +0 -12
- {addftool-0.1.3.dist-info → addftool-0.1.5.dist-info}/WHEEL +0 -0
- {addftool-0.1.3.dist-info → addftool-0.1.5.dist-info}/entry_points.txt +0 -0
- {addftool-0.1.3.dist-info → addftool-0.1.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from .ssh_server import add_deploy_ssh_server_args, deploy_ssh_server_main
|
|
4
|
+
from .azure import add_deploy_azure_args, deploy_azure_main
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def add_deploy_args(subparsers: argparse._SubParsersAction):
|
|
8
|
+
"""
|
|
9
|
+
Add deploy arguments to the parser.
|
|
10
|
+
Args:
|
|
11
|
+
subparsers (argparse._SubParsersAction): The subparsers object to add the arguments to.
|
|
12
|
+
"""
|
|
13
|
+
deploy_parsers = subparsers.add_parser('deploy', help='deploy/install software, script, tools, etc.')
|
|
14
|
+
deploy_subparsers = deploy_parsers.add_subparsers(dest='deploy_type', help='Deploy options')
|
|
15
|
+
|
|
16
|
+
ssh_server_parser = deploy_subparsers.add_parser('ssh-server', help='SSH options')
|
|
17
|
+
add_deploy_ssh_server_args(ssh_server_parser)
|
|
18
|
+
|
|
19
|
+
azure_parser = deploy_subparsers.add_parser('azure', help='Azure options')
|
|
20
|
+
add_deploy_azure_args(azure_parser)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def deploy_main(args):
|
|
24
|
+
"""
|
|
25
|
+
Main function for deploy.
|
|
26
|
+
Args:
|
|
27
|
+
args (argparse.Namespace): The parsed arguments.
|
|
28
|
+
"""
|
|
29
|
+
if args.deploy_type == 'ssh-server':
|
|
30
|
+
deploy_ssh_server_main(args)
|
|
31
|
+
elif args.deploy_type == 'azure':
|
|
32
|
+
deploy_azure_main(args)
|
|
33
|
+
else:
|
|
34
|
+
print("Unknown deploy type")
|
|
35
|
+
print("Deploy type: ", args.deploy_type)
|
addftool/deploy/azure.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from addftool.util import need_sudo, execute_command, get_ubuntu_version, install_packages
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def deploy_azure(packages):
|
|
5
|
+
ubuntu_version = get_ubuntu_version()
|
|
6
|
+
print("Get ubuntu version: ", ubuntu_version)
|
|
7
|
+
|
|
8
|
+
command = f"wget https://packages.microsoft.com/config/ubuntu/{ubuntu_version}/packages-microsoft-prod.deb -O /tmp/packages-microsoft-prod.deb"
|
|
9
|
+
print("Install packages-microsoft-prod.deb")
|
|
10
|
+
execute_command(command)
|
|
11
|
+
command_prefix = "sudo " if need_sudo() else ""
|
|
12
|
+
command = "dpkg -i /tmp/packages-microsoft-prod.deb"
|
|
13
|
+
execute_command(command_prefix + command)
|
|
14
|
+
|
|
15
|
+
install_packages(packages)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def add_deploy_azure_args(parser):
|
|
19
|
+
"""
|
|
20
|
+
Add Azure deployment arguments to the parser.
|
|
21
|
+
Args:
|
|
22
|
+
parser (argparse.ArgumentParser): The argument parser to add the arguments to.
|
|
23
|
+
"""
|
|
24
|
+
parser.add_argument("packages", help="packages", default=['fuse3', 'blobfuse2', 'azcopy'], nargs="*")
|
|
25
|
+
|
|
26
|
+
def deploy_azure_main(args):
|
|
27
|
+
# print(args.packages)
|
|
28
|
+
deploy_azure(args.packages)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
import argparse
|
|
33
|
+
|
|
34
|
+
parser = argparse.ArgumentParser(description="Azure deployment arguments")
|
|
35
|
+
add_deploy_azure_args(parser)
|
|
36
|
+
|
|
37
|
+
args = parser.parse_args()
|
|
38
|
+
|
|
39
|
+
deploy_azure_main(args)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
from addftool.util import need_sudo, execute_command, is_running_in_docker
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def convert_ssh2_to_openssh(ssh2_public_key_content):
|
|
8
|
+
lines = ssh2_public_key_content.strip().split('\n')
|
|
9
|
+
encoded_key = ""
|
|
10
|
+
start_found = False
|
|
11
|
+
for line in lines:
|
|
12
|
+
line = line.strip()
|
|
13
|
+
if line == "---- BEGIN SSH2 PUBLIC KEY ----":
|
|
14
|
+
start_found = True
|
|
15
|
+
continue
|
|
16
|
+
elif line == "---- END SSH2 PUBLIC KEY ----":
|
|
17
|
+
break
|
|
18
|
+
elif start_found and line.startswith("Comment:"):
|
|
19
|
+
comment = line.split(":", 1)[1].strip().strip('"')
|
|
20
|
+
elif start_found:
|
|
21
|
+
encoded_key += line
|
|
22
|
+
|
|
23
|
+
if encoded_key:
|
|
24
|
+
# Assuming it's an RSA key based on the comment
|
|
25
|
+
openssh_key = f"ssh-rsa {encoded_key} \"{comment}\""
|
|
26
|
+
return openssh_key
|
|
27
|
+
else:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_key_from_file(file_path):
|
|
32
|
+
"""
|
|
33
|
+
解析 SSH 公钥文件,支持 SSH2 和 OpenSSH 格式
|
|
34
|
+
:param file_path: 公钥文件路径
|
|
35
|
+
:return: 公钥内容
|
|
36
|
+
"""
|
|
37
|
+
with open(file_path, "r") as f:
|
|
38
|
+
content = f.read()
|
|
39
|
+
if "---- BEGIN SSH2 PUBLIC KEY ----" in content:
|
|
40
|
+
return convert_ssh2_to_openssh(content)
|
|
41
|
+
else:
|
|
42
|
+
return content.strip()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def configure_ssh_on_ubuntu(port, username, ssh_public_key="", password=""):
|
|
46
|
+
if not is_running_in_docker():
|
|
47
|
+
print("This script is not running in a Docker container. Exiting...")
|
|
48
|
+
return
|
|
49
|
+
print(f"deploy ssh server on port: {port}, username: {username}, ssh_public_key: {ssh_public_key}, password: {password is not None}")
|
|
50
|
+
if not(len(ssh_public_key) > 0 or len(password) > 0):
|
|
51
|
+
print("ssh_public_key or password must be provided.")
|
|
52
|
+
return
|
|
53
|
+
command_prefix = "sudo " if need_sudo() else ""
|
|
54
|
+
try:
|
|
55
|
+
need_install_ssh = True
|
|
56
|
+
try:
|
|
57
|
+
# execute_command(["which", "sshd"])
|
|
58
|
+
response = execute_command("which sshd", only_stdout=False)
|
|
59
|
+
if response["returncode"] == 0:
|
|
60
|
+
print("SSH server is already installed.")
|
|
61
|
+
need_install_ssh = False
|
|
62
|
+
except subprocess.CalledProcessError:
|
|
63
|
+
pass
|
|
64
|
+
if need_install_ssh:
|
|
65
|
+
print("SSH server is not installed. Installing...")
|
|
66
|
+
execute_command(command_prefix + "apt-get update")
|
|
67
|
+
execute_command(command_prefix + "apt-get install -y openssh-server")
|
|
68
|
+
|
|
69
|
+
print("Modifying SSH configuration...")
|
|
70
|
+
execute_command(command_prefix + f'sed -i "s/#Port 22/Port {port}/" /etc/ssh/sshd_config')
|
|
71
|
+
execute_command(command_prefix + 'sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/" /etc/ssh/sshd_config')
|
|
72
|
+
|
|
73
|
+
if not os.path.exists("/etc/ssh/ssh_host_rsa_key"):
|
|
74
|
+
print("SSH host keys not found. Generating new keys...")
|
|
75
|
+
execute_command(command_prefix + 'ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N ""')
|
|
76
|
+
|
|
77
|
+
print(f'Creating user {username} and configuring SSH key...')
|
|
78
|
+
try:
|
|
79
|
+
# run_command(["id", "-u", username])
|
|
80
|
+
execute_command(command_prefix + f"id -u {username}")
|
|
81
|
+
except subprocess.CalledProcessError:
|
|
82
|
+
print(f"User {username} does not exist. Creating user...")
|
|
83
|
+
execute_command(command_prefix + f"useradd -m -s /bin/bash {username}")
|
|
84
|
+
|
|
85
|
+
ssh_dir = f"/home/{username}/.ssh" if username != "root" else "/root/.ssh"
|
|
86
|
+
authorized_keys_file = f"{ssh_dir}/authorized_keys"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
execute_command(command_prefix + f"mkdir -p {ssh_dir}")
|
|
90
|
+
execute_command(command_prefix + f"chmod 700 {ssh_dir}")
|
|
91
|
+
|
|
92
|
+
if ssh_public_key:
|
|
93
|
+
key_content = parse_key_from_file(ssh_public_key)
|
|
94
|
+
execute_command(command_prefix + f"bash -c 'echo \"{key_content}\" >> {authorized_keys_file}'", hide=True)
|
|
95
|
+
execute_command(command_prefix + f"chmod 600 {authorized_keys_file}")
|
|
96
|
+
execute_command(command_prefix + f"chown -R {username}:{username} {ssh_dir}")
|
|
97
|
+
|
|
98
|
+
if password:
|
|
99
|
+
execute_command(command_prefix + f"echo '{username}:{password}' | chpasswd", hide=True)
|
|
100
|
+
|
|
101
|
+
print("Restarting SSH service...")
|
|
102
|
+
# run_command(["sudo", "systemctl", "restart", "ssh"])
|
|
103
|
+
execute_command("/etc/init.d/ssh restart")
|
|
104
|
+
|
|
105
|
+
print(f"ssh is configured! You can connect using the following command: ssh -p {port} {username}@<host_ip>")
|
|
106
|
+
|
|
107
|
+
except subprocess.CalledProcessError as e:
|
|
108
|
+
print(f"Command called process error: {e}")
|
|
109
|
+
except FileNotFoundError as e:
|
|
110
|
+
print(f"File not found error: {e}")
|
|
111
|
+
except Exception as e:
|
|
112
|
+
print(f"An error occurred: {e}")
|
|
113
|
+
|
|
114
|
+
def add_deploy_ssh_server_args(parser):
|
|
115
|
+
"""添加 SSH 配置参数"""
|
|
116
|
+
parser.add_argument("--port", type=int, required=True, help="SSH server port")
|
|
117
|
+
parser.add_argument("--username", required=True, help="SSH username")
|
|
118
|
+
parser.add_argument("--password", help="SSH password", type=str, default="")
|
|
119
|
+
parser.add_argument("--ssh-public-key", help="SSH public key", type=str, default="")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def deploy_ssh_server_main(args):
|
|
124
|
+
"""
|
|
125
|
+
部署 SSH 服务器
|
|
126
|
+
:param args: 命令行参数
|
|
127
|
+
"""
|
|
128
|
+
configure_ssh_on_ubuntu(args.port, args.username, args.ssh_public_key, args.password)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
parser = argparse.ArgumentParser(description="在 Ubuntu 系统中配置 SSH")
|
|
133
|
+
add_deploy_ssh_server_args(parser)
|
|
134
|
+
|
|
135
|
+
args = parser.parse_args()
|
|
136
|
+
|
|
137
|
+
deploy_ssh_server_main(args)
|
addftool/process/__init__.py
CHANGED
|
@@ -7,8 +7,7 @@ from .utils import get_processes, get_process_using_rocm, get_process_using_cuda
|
|
|
7
7
|
def add_killer_args(subparsers):
|
|
8
8
|
process_killer_parser = subparsers.add_parser('kill', help='process kill')
|
|
9
9
|
|
|
10
|
-
process_killer_parser.add_argument("--
|
|
11
|
-
process_killer_parser.add_argument("--contains", help="contains of command", default=None)
|
|
10
|
+
process_killer_parser.add_argument("-c", "--contains", help="contains of command", action='store_true', default=False)
|
|
12
11
|
process_killer_parser.add_argument("--timeout", help="timeout of command", default=5, type=int)
|
|
13
12
|
process_killer_parser.add_argument("--try_count", help="try count of command", default=3, type=int)
|
|
14
13
|
|
|
@@ -17,6 +16,8 @@ def add_killer_args(subparsers):
|
|
|
17
16
|
|
|
18
17
|
process_killer_parser.add_argument("-v", "--view", help="view process", action='store_true', default=False)
|
|
19
18
|
|
|
19
|
+
process_killer_parser.add_argument("name", nargs='?', help="name of process", type=str, default="")
|
|
20
|
+
|
|
20
21
|
|
|
21
22
|
def kill_process(processes, timeout=5, try_count=3):
|
|
22
23
|
for process in processes:
|
|
@@ -57,8 +58,8 @@ def kill_process(processes, timeout=5, try_count=3):
|
|
|
57
58
|
exit(1)
|
|
58
59
|
|
|
59
60
|
|
|
60
|
-
def find_and_kill_process(
|
|
61
|
-
do_not_do_anything =
|
|
61
|
+
def find_and_kill_process(command="", contains=False, use_rocm=False, use_cuda=False, timeout=5, try_count=3, only_view=False):
|
|
62
|
+
do_not_do_anything = command is None or len(command) == 0
|
|
62
63
|
if use_rocm:
|
|
63
64
|
processes = get_process_using_rocm()
|
|
64
65
|
elif use_cuda:
|
|
@@ -67,7 +68,7 @@ def find_and_kill_process(prefix=None, contains=None, use_rocm=False, use_cuda=F
|
|
|
67
68
|
print("Use top or htop to find the process you want to kill")
|
|
68
69
|
return
|
|
69
70
|
else:
|
|
70
|
-
processes = get_processes(
|
|
71
|
+
processes = get_processes(command=command, contains=contains)
|
|
71
72
|
|
|
72
73
|
if only_view:
|
|
73
74
|
print(f"Found {len(processes)} processes")
|
|
@@ -82,6 +83,6 @@ def find_and_kill_process(prefix=None, contains=None, use_rocm=False, use_cuda=F
|
|
|
82
83
|
|
|
83
84
|
def killer_main(args):
|
|
84
85
|
find_and_kill_process(
|
|
85
|
-
args.
|
|
86
|
+
args.name, args.contains, use_rocm=args.rocm, use_cuda=args.cuda,
|
|
86
87
|
timeout=args.timeout, try_count=args.try_count, only_view=args.view,
|
|
87
88
|
)
|
addftool/process/utils.py
CHANGED
|
@@ -2,7 +2,7 @@ import psutil
|
|
|
2
2
|
import subprocess
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
def get_processes(
|
|
5
|
+
def get_processes(command="", contains=False, pids=None):
|
|
6
6
|
"""获取进程的PID和命令"""
|
|
7
7
|
|
|
8
8
|
processes = []
|
|
@@ -14,10 +14,11 @@ def get_processes(command_prefix=None, prefix_contains=None, pids=None):
|
|
|
14
14
|
continue
|
|
15
15
|
if pids is not None and pid not in pids:
|
|
16
16
|
continue
|
|
17
|
-
if
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
if len(command) > 0:
|
|
18
|
+
if contains and command not in cmdline[0]:
|
|
19
|
+
continue
|
|
20
|
+
if not contains and command != cmdline[0]:
|
|
21
|
+
continue
|
|
21
22
|
if len(cmdline) > 1 and cmdline[1].endswith('addf'):
|
|
22
23
|
continue
|
|
23
24
|
processes.append({'pid': pid, 'command': cmdline})
|
addftool/ssh/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def add_sync_args(subparsers):
|
|
5
|
+
process_killer_parser = subparsers.add_parser('sync', help='download and sync folder from master node to other nodes')
|
|
6
|
+
|
|
7
|
+
process_killer_parser.add_argument("--from_blob_url", help="download from blob url to master node before sync", type=str, default="")
|
|
8
|
+
process_killer_parser.add_argument("--sas_token", help="sas token for blob url", type=str, default="")
|
|
9
|
+
process_killer_parser.add_argument("--tool", help="tool name", type=str, default="torch_nccl", choices=["torch_nccl", "rsync"])
|
|
10
|
+
process_killer_parser.add_argument("--hostfile", help="host file, sync file from node-0 to others", type=str, default="")
|
|
11
|
+
|
|
12
|
+
# distributed downloader from blob
|
|
13
|
+
process_killer_parser.add_argument("--donwload_nodes", help="download nodes, default is node-0", type=int, default=1)
|
|
14
|
+
|
|
15
|
+
process_killer_parser.add_argument("folder", nargs='?', help="the folder need to sync", type=str, default="")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def sync_main(args):
|
|
19
|
+
print(args)
|
|
20
|
+
exit(0)
|
|
21
|
+
if args.source == "" or args.target == "":
|
|
22
|
+
print("Please provide source and target folder")
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
# check if source is a folder
|
|
26
|
+
if not os.path.isdir(args.source):
|
|
27
|
+
print(f"Source {args.source} is not a folder")
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
# check if target is a folder
|
|
31
|
+
if not os.path.isdir(args.target):
|
|
32
|
+
print(f"Target {args.target} is not a folder")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
# check if source and target are the same
|
|
36
|
+
if os.path.abspath(args.source) == os.path.abspath(args.target):
|
|
37
|
+
print(f"Source and target are the same")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
# sync source to target
|
|
41
|
+
command = f"rsync -avz --delete {args.source} {args.target}"
|
|
42
|
+
os.system(command)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
addftool/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
addftool/addf_portal.py,sha256=U52TdNcwWOEvv_C5r-guWYxn3ntzwYI2eBzJIE7IdcY,493
|
|
3
|
+
addftool/blob.py,sha256=NZOItDyFUIdV1tfhJZJJBEzGy296CE5NCictTzP4OPc,8282
|
|
4
|
+
addftool/tool.py,sha256=EuKQ2t2InN7yB-_oYLcdsA7vRqzRGTunwIxplUSqEG0,2054
|
|
5
|
+
addftool/util.py,sha256=Q3A68vJDxgfeNiEFmk54HuMuworVndocXpSbVpvGMfc,362
|
|
6
|
+
addftool/deploy/__init__.py,sha256=tpyoTh3SqAQojPizsJDvQohu1Pcb3-w-DP5sO4-5lBM,1220
|
|
7
|
+
addftool/deploy/azure.py,sha256=UQR1hOEYUtsm2fbWBczsnEB_mh7yUuN2NDv3sgMMsac,1246
|
|
8
|
+
addftool/deploy/ssh_server.py,sha256=f2T8fgwACVljPfdcimMywUjsFnLCWRde7iWPAILpRz8,5463
|
|
9
|
+
addftool/process/__init__.py,sha256=OB-cZXP1jK7l8uN8nKfhg_bCX6Slz6DeeBpEodt-IK4,3515
|
|
10
|
+
addftool/process/utils.py,sha256=me4HqMz5OgRcQMUJmVhKdTJh4SW5BB-pd_lq7g8-UwE,2252
|
|
11
|
+
addftool/ssh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
addftool/sync/__init__.py,sha256=wOqFCOA51rFUttBjOO44W3Fc66mhX5ir2R89lsO6gR0,1702
|
|
13
|
+
addftool-0.1.5.dist-info/METADATA,sha256=8fCVZ1r4rS685bprhRuQE5Le7MV3gu8dbNRkvFfo05w,148
|
|
14
|
+
addftool-0.1.5.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
15
|
+
addftool-0.1.5.dist-info/entry_points.txt,sha256=9lkmuWMInwUAtev8w8poNkNd7iML9Bjd5CBCFVxg2b8,111
|
|
16
|
+
addftool-0.1.5.dist-info/top_level.txt,sha256=jqj56-plrBbyzY0tIxB6wPzjAA8kte4hUlajyyQygN4,9
|
|
17
|
+
addftool-0.1.5.dist-info/RECORD,,
|
addftool-0.1.3.dist-info/RECORD
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
addftool/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
addftool/addf_portal.py,sha256=U52TdNcwWOEvv_C5r-guWYxn3ntzwYI2eBzJIE7IdcY,493
|
|
3
|
-
addftool/blob.py,sha256=NZOItDyFUIdV1tfhJZJJBEzGy296CE5NCictTzP4OPc,8282
|
|
4
|
-
addftool/tool.py,sha256=EuKQ2t2InN7yB-_oYLcdsA7vRqzRGTunwIxplUSqEG0,2054
|
|
5
|
-
addftool/util.py,sha256=Q3A68vJDxgfeNiEFmk54HuMuworVndocXpSbVpvGMfc,362
|
|
6
|
-
addftool/process/__init__.py,sha256=EFJ90RcF04M4imGm5NLgb9rmpwxBslZUL_4LKA52uv8,3487
|
|
7
|
-
addftool/process/utils.py,sha256=kS1bYYiWymB1_SObqQLfGiVbZdCJqd57SO6DrwN5H1s,2246
|
|
8
|
-
addftool-0.1.3.dist-info/METADATA,sha256=YwSmBYP8v9T_PnVr-3WM-V2KkCcPg17IwMKllfp-pc0,148
|
|
9
|
-
addftool-0.1.3.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
10
|
-
addftool-0.1.3.dist-info/entry_points.txt,sha256=9lkmuWMInwUAtev8w8poNkNd7iML9Bjd5CBCFVxg2b8,111
|
|
11
|
-
addftool-0.1.3.dist-info/top_level.txt,sha256=jqj56-plrBbyzY0tIxB6wPzjAA8kte4hUlajyyQygN4,9
|
|
12
|
-
addftool-0.1.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|