cvdlink 0.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.
- FeatureCloud/__init__.py +0 -0
- FeatureCloud/api/__init__.py +0 -0
- FeatureCloud/api/cli/__init__.py +0 -0
- FeatureCloud/api/cli/__main__.py +115 -0
- FeatureCloud/api/cli/app/__init__.py +0 -0
- FeatureCloud/api/cli/app/commands.py +182 -0
- FeatureCloud/api/cli/controller/__init__.py +0 -0
- FeatureCloud/api/cli/controller/commands.py +181 -0
- FeatureCloud/api/cli/test/__init__.py +0 -0
- FeatureCloud/api/cli/test/commands.py +251 -0
- FeatureCloud/api/cli/test/workflow/__init__.py +0 -0
- FeatureCloud/api/cli/test/workflow/commands.py +32 -0
- FeatureCloud/api/imp/__init__.py +0 -0
- FeatureCloud/api/imp/app/__init__.py +0 -0
- FeatureCloud/api/imp/app/commands.py +278 -0
- FeatureCloud/api/imp/controller/__init__.py +0 -0
- FeatureCloud/api/imp/controller/commands.py +246 -0
- FeatureCloud/api/imp/exceptions.py +29 -0
- FeatureCloud/api/imp/test/__init__.py +0 -0
- FeatureCloud/api/imp/test/api/__init__.py +0 -0
- FeatureCloud/api/imp/test/api/backend/__init__.py +0 -0
- FeatureCloud/api/imp/test/api/backend/auth.py +54 -0
- FeatureCloud/api/imp/test/api/backend/project.py +84 -0
- FeatureCloud/api/imp/test/api/controller.py +97 -0
- FeatureCloud/api/imp/test/commands.py +124 -0
- FeatureCloud/api/imp/test/helper.py +40 -0
- FeatureCloud/api/imp/util.py +45 -0
- FeatureCloud/app/__init__.py +0 -0
- FeatureCloud/app/api/__init__.py +0 -0
- FeatureCloud/app/api/http_ctrl.py +48 -0
- FeatureCloud/app/api/http_web.py +16 -0
- FeatureCloud/app/engine/__init__.py +0 -0
- FeatureCloud/app/engine/app.py +1214 -0
- FeatureCloud/app/engine/library.py +46 -0
- FeatureCloud/workflow/__init__.py +0 -0
- FeatureCloud/workflow/app.py +197 -0
- FeatureCloud/workflow/controller.py +17 -0
- FeatureCloud/workflow/example_wf.py +83 -0
- FeatureCloud/workflow/workflow.py +86 -0
- cvdlink-0.1.1.dist-info/METADATA +176 -0
- cvdlink-0.1.1.dist-info/RECORD +45 -0
- cvdlink-0.1.1.dist-info/WHEEL +5 -0
- cvdlink-0.1.1.dist-info/entry_points.txt +5 -0
- cvdlink-0.1.1.dist-info/licenses/LICENSE +201 -0
- cvdlink-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import tqdm
|
|
2
|
+
import time
|
|
3
|
+
import docker
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from FeatureCloud.api.imp.exceptions import FCException, ContainerNotFound
|
|
7
|
+
from FeatureCloud.api.imp.test.helper import http
|
|
8
|
+
|
|
9
|
+
from FeatureCloud.api.imp.util import getcwd_fslash, get_docker_client
|
|
10
|
+
|
|
11
|
+
DEFAULT_CONTROLLER_IMAGE = "featurecloud.ai/controller"
|
|
12
|
+
CONTROLLER_LABEL = "FCControllerLabel"
|
|
13
|
+
DEFAULT_PORT = 8000
|
|
14
|
+
DEFAULT_CONTROLLER_NAME = 'fc-controller'
|
|
15
|
+
DEFAULT_DATA_DIR = 'data'
|
|
16
|
+
LOG_FETCH_INTERVAL = 3 # seconds
|
|
17
|
+
LOG_LEVEL_CHOICES = ['debug', 'info', 'warn', 'error', 'fatal']
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def start(
|
|
21
|
+
name: str,
|
|
22
|
+
port: int,
|
|
23
|
+
data_dir: str,
|
|
24
|
+
controller_image: str,
|
|
25
|
+
with_gpu: bool,
|
|
26
|
+
mount: str,
|
|
27
|
+
blockchain_address: str,
|
|
28
|
+
profile: str = None,
|
|
29
|
+
global_endpoint: str = "",
|
|
30
|
+
registry: str = "",
|
|
31
|
+
relay_address: str = "",
|
|
32
|
+
poll_interval: int = 0,
|
|
33
|
+
query_interval: int = 0,
|
|
34
|
+
config_file: str = "",
|
|
35
|
+
):
|
|
36
|
+
client = get_docker_client()
|
|
37
|
+
|
|
38
|
+
data_dir = data_dir if data_dir else DEFAULT_DATA_DIR
|
|
39
|
+
if not controller_image:
|
|
40
|
+
if profile == "cvdlink":
|
|
41
|
+
controller_image = "fc.cvdlink-project.eu/controller:latest"
|
|
42
|
+
else:
|
|
43
|
+
controller_image = DEFAULT_CONTROLLER_IMAGE
|
|
44
|
+
# Create data dir if needed
|
|
45
|
+
try:
|
|
46
|
+
os.mkdir(data_dir)
|
|
47
|
+
except OSError as error:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
# cleanup unused controller containers and networks
|
|
51
|
+
prune_controllers()
|
|
52
|
+
|
|
53
|
+
# remove controller having the same name, if any
|
|
54
|
+
try:
|
|
55
|
+
client.api.remove_container(name, v=True, force=True)
|
|
56
|
+
except docker.errors.NotFound:
|
|
57
|
+
pass
|
|
58
|
+
except docker.errors.DockerException as e:
|
|
59
|
+
raise FCException(e)
|
|
60
|
+
|
|
61
|
+
# pull controller and display progress
|
|
62
|
+
try:
|
|
63
|
+
pull_proc = client.api.pull(repository=controller_image, stream=True)
|
|
64
|
+
for p in tqdm.tqdm(pull_proc, desc='Downloading...'):
|
|
65
|
+
pass
|
|
66
|
+
except docker.errors.DockerException as e:
|
|
67
|
+
raise FCException(e)
|
|
68
|
+
|
|
69
|
+
cont_name = name if name else DEFAULT_CONTROLLER_NAME
|
|
70
|
+
# forward slash works on all platforms
|
|
71
|
+
base_dir = getcwd_fslash()
|
|
72
|
+
|
|
73
|
+
# set the correct host folder to be mounted
|
|
74
|
+
if os.path.isabs(data_dir):
|
|
75
|
+
host_data_dir = data_dir
|
|
76
|
+
else:
|
|
77
|
+
host_data_dir = os.path.join(base_dir, data_dir)
|
|
78
|
+
|
|
79
|
+
volumes = [f'{host_data_dir}:/{data_dir}', '/var/run/docker.sock:/var/run/docker.sock']
|
|
80
|
+
if mount:
|
|
81
|
+
volumes.append(f'{mount}:/mnt')
|
|
82
|
+
|
|
83
|
+
# ---------- profile-based defaults ----------
|
|
84
|
+
# For featurecloud: use config.yml defaults unless user overrides via CLI.
|
|
85
|
+
# For cvdlink: provide sensible defaults if not explicitly set.
|
|
86
|
+
if profile == "cvdlink":
|
|
87
|
+
if not global_endpoint:
|
|
88
|
+
global_endpoint = "https://fc.cvdlink-project.eu/api/"
|
|
89
|
+
if not registry:
|
|
90
|
+
registry = "fc.cvdlink-project.eu"
|
|
91
|
+
if not relay_address:
|
|
92
|
+
relay_address = "relay.fc.cvdlink-project.eu"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------- build controller command ----------
|
|
96
|
+
# NB: we keep the original style with a single command string.
|
|
97
|
+
cmd_parts = [
|
|
98
|
+
f"--host-root='{host_data_dir}'",
|
|
99
|
+
f"--internal-root=/{data_dir}",
|
|
100
|
+
f"--controller-name={cont_name}",
|
|
101
|
+
]
|
|
102
|
+
device_requests = None
|
|
103
|
+
if with_gpu:
|
|
104
|
+
device_requests = [docker.types.DeviceRequest(count=-1, capabilities=[['gpu']])]
|
|
105
|
+
cmd_parts.append("--has-gpu")
|
|
106
|
+
|
|
107
|
+
if blockchain_address:
|
|
108
|
+
cmd_parts.append(f"--blockchain-address={blockchain_address}")
|
|
109
|
+
|
|
110
|
+
if config_file:
|
|
111
|
+
cmd_parts.append(f"--configfile={config_file}")
|
|
112
|
+
|
|
113
|
+
if global_endpoint:
|
|
114
|
+
cmd_parts.append(f"--endpoint='{global_endpoint}'")
|
|
115
|
+
|
|
116
|
+
if registry:
|
|
117
|
+
cmd_parts.append(f"--registry={registry}")
|
|
118
|
+
|
|
119
|
+
if relay_address:
|
|
120
|
+
cmd_parts.append(f"--address={relay_address}")
|
|
121
|
+
|
|
122
|
+
if poll_interval > 0:
|
|
123
|
+
cmd_parts.append(f"--poll-interval={poll_interval}")
|
|
124
|
+
|
|
125
|
+
if query_interval > 0:
|
|
126
|
+
cmd_parts.append(f"--query-interval={query_interval}")
|
|
127
|
+
|
|
128
|
+
command = " ".join(cmd_parts)
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
client.containers.run(
|
|
132
|
+
controller_image,
|
|
133
|
+
detach=True,
|
|
134
|
+
name=cont_name,
|
|
135
|
+
ports={8000: port if port else DEFAULT_PORT},
|
|
136
|
+
volumes=volumes,
|
|
137
|
+
labels=[CONTROLLER_LABEL],
|
|
138
|
+
device_requests=device_requests,
|
|
139
|
+
command=command,
|
|
140
|
+
)
|
|
141
|
+
except docker.errors.DockerException as e:
|
|
142
|
+
raise FCException(e)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
raise FCException(e)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def stop(name: str):
|
|
148
|
+
client = get_docker_client()
|
|
149
|
+
|
|
150
|
+
if not name:
|
|
151
|
+
name = DEFAULT_CONTROLLER_NAME
|
|
152
|
+
|
|
153
|
+
# Removing controllers filtered by name
|
|
154
|
+
removed = []
|
|
155
|
+
for container in client.containers.list(filters={"name": [name]}):
|
|
156
|
+
try:
|
|
157
|
+
client.api.remove_container(container.id, v=True, force=True)
|
|
158
|
+
removed.append(container.name)
|
|
159
|
+
except docker.errors.DockerException as e:
|
|
160
|
+
raise FCException(e)
|
|
161
|
+
|
|
162
|
+
return removed
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def prune_controllers():
|
|
166
|
+
client = get_docker_client()
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
client.containers.prune(filters={"label": [CONTROLLER_LABEL]})
|
|
170
|
+
except docker.errors.DockerException as e:
|
|
171
|
+
raise FCException(e)
|
|
172
|
+
|
|
173
|
+
# Remove the network
|
|
174
|
+
try:
|
|
175
|
+
# Attempt to remove the network
|
|
176
|
+
client.networks.get("fc-controller-internal").remove()
|
|
177
|
+
except docker.errors.NotFound:
|
|
178
|
+
pass
|
|
179
|
+
except docker.errors.APIError as e:
|
|
180
|
+
print(f"An API error occurred: {e}")
|
|
181
|
+
except Exception as e:
|
|
182
|
+
print(f"An unexpected error occurred: {e}")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def logs(name: str, tail: bool, log_level: str):
|
|
186
|
+
client = get_docker_client()
|
|
187
|
+
|
|
188
|
+
# get controller address
|
|
189
|
+
host_port = 0
|
|
190
|
+
try:
|
|
191
|
+
container = client.containers.get(name)
|
|
192
|
+
host_port = container.attrs['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort']
|
|
193
|
+
except docker.errors.NotFound:
|
|
194
|
+
raise ContainerNotFound(name)
|
|
195
|
+
except docker.errors.DockerException as e:
|
|
196
|
+
raise FCException(e)
|
|
197
|
+
|
|
198
|
+
# Get logs content from controller
|
|
199
|
+
lines_fetched = 0
|
|
200
|
+
while True:
|
|
201
|
+
response = http.get(url=f'http://localhost:{host_port}/logs/?from={lines_fetched}')
|
|
202
|
+
|
|
203
|
+
if response.status_code == 200:
|
|
204
|
+
logs_content = response.json()
|
|
205
|
+
lines_fetched += len(logs_content)
|
|
206
|
+
|
|
207
|
+
log_level = valid_log_level(log_level)
|
|
208
|
+
filtered = filter_logs(logs_content, log_level)
|
|
209
|
+
for line in filtered:
|
|
210
|
+
yield line
|
|
211
|
+
else:
|
|
212
|
+
raise FCException(response.json('detail'))
|
|
213
|
+
|
|
214
|
+
if not tail:
|
|
215
|
+
break
|
|
216
|
+
time.sleep(LOG_FETCH_INTERVAL)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def status(name: str):
|
|
220
|
+
client = get_docker_client()
|
|
221
|
+
try:
|
|
222
|
+
return client.containers.get(name)
|
|
223
|
+
except docker.errors.NotFound:
|
|
224
|
+
raise ContainerNotFound(name)
|
|
225
|
+
except docker.errors.DockerException as e:
|
|
226
|
+
raise FCException(e)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def ls():
|
|
230
|
+
client = get_docker_client()
|
|
231
|
+
try:
|
|
232
|
+
return client.containers.list(filters={'label': [CONTROLLER_LABEL]})
|
|
233
|
+
except docker.errors.DockerException as e:
|
|
234
|
+
raise FCException(e)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def filter_logs(logs_json, log_level):
|
|
238
|
+
log_level_index = LOG_LEVEL_CHOICES.index(log_level)
|
|
239
|
+
return [line for line in logs_json if LOG_LEVEL_CHOICES.index(line['level']) >= log_level_index]
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def valid_log_level(log_level):
|
|
243
|
+
if len(log_level) == 0 or log_level not in LOG_LEVEL_CHOICES:
|
|
244
|
+
log_level = LOG_LEVEL_CHOICES[0]
|
|
245
|
+
|
|
246
|
+
return log_level
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
class FCException(Exception):
|
|
3
|
+
"""
|
|
4
|
+
Base class for FeatureCloud exceptions.
|
|
5
|
+
"""
|
|
6
|
+
default_detail = 'an error occurred'
|
|
7
|
+
|
|
8
|
+
def __init__(self, detail=None):
|
|
9
|
+
if detail is None:
|
|
10
|
+
detail = self.default_detail
|
|
11
|
+
|
|
12
|
+
self.detail = detail
|
|
13
|
+
|
|
14
|
+
def __str__(self):
|
|
15
|
+
return str(self.detail)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DockerNotAvailable(FCException):
|
|
19
|
+
default_detail = 'Docker daemon is not available. Please make sure Docker is started.'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ControllerOffline(FCException):
|
|
23
|
+
def __init__(self, url=None):
|
|
24
|
+
super().__init__(f'Could not access controller on URL: {url}')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ContainerNotFound(FCException):
|
|
28
|
+
def __init__(self, name=None):
|
|
29
|
+
super().__init__(f'Container not found: {name}')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from FeatureCloud.api.utils.cli.helper import http
|
|
5
|
+
|
|
6
|
+
home = str(Path.home())
|
|
7
|
+
url = 'http://127.0.0.1:7000'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_user_logged_in():
|
|
11
|
+
headers = create_authorization_header()
|
|
12
|
+
r = http.get(url=f'{url}/user/info/', headers=headers)
|
|
13
|
+
if r.status_code == 200:
|
|
14
|
+
return True
|
|
15
|
+
else:
|
|
16
|
+
print("No user is logged in. Please login first.")
|
|
17
|
+
exit()
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def login(email: str, password: str):
|
|
22
|
+
r = http.post(url=f'{url}/auth/login/',
|
|
23
|
+
json={'username': email,
|
|
24
|
+
'password': password})
|
|
25
|
+
if r.status_code == 200:
|
|
26
|
+
refresh, access = r.json()['refresh'], r.json()['access']
|
|
27
|
+
login_details = {'email': email, 'password': password, 'refresh': refresh, 'access': access}
|
|
28
|
+
login_details = json.dumps(login_details)
|
|
29
|
+
file = open(f'{home}/.fc_cli', 'w')
|
|
30
|
+
file.write(login_details)
|
|
31
|
+
file.close()
|
|
32
|
+
return True
|
|
33
|
+
else:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def logout():
|
|
38
|
+
r = http.post(url=f'{url}/auth/logout/',
|
|
39
|
+
json={'refresh': None})
|
|
40
|
+
if r.status_code == 200:
|
|
41
|
+
with open(f'{home}/.fc_cli', 'w') as file:
|
|
42
|
+
file.write('')
|
|
43
|
+
file.close()
|
|
44
|
+
return True
|
|
45
|
+
else:
|
|
46
|
+
print(r.content)
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def create_authorization_header():
|
|
51
|
+
with open(f'{home}/.fc_cli') as jsonFile:
|
|
52
|
+
access_token = json.load(jsonFile)['access']
|
|
53
|
+
jsonFile.close()
|
|
54
|
+
return {"Authorization": f'Bearer {access_token}'}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from FeatureCloud.api.utils.cli.api.backend.auth import create_authorization_header
|
|
4
|
+
from FeatureCloud.api.utils.cli.helper import http
|
|
5
|
+
|
|
6
|
+
home = str(Path.home())
|
|
7
|
+
url = 'http://127.0.0.1:7000'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def info(project_id: str or int):
|
|
11
|
+
headers = create_authorization_header()
|
|
12
|
+
r = http.get(url=f'{url}/projects/{project_id}', headers=headers)
|
|
13
|
+
if r.status_code == 200:
|
|
14
|
+
return r.json()
|
|
15
|
+
else:
|
|
16
|
+
return False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create(name: str, description: str):
|
|
20
|
+
headers = create_authorization_header()
|
|
21
|
+
r = http.post(url=f'{url}/projects/',
|
|
22
|
+
json={'name': name,
|
|
23
|
+
'description': description},
|
|
24
|
+
headers=headers
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if r.status_code == 200:
|
|
28
|
+
return r.json()['id']
|
|
29
|
+
else:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def remove(proj_id: str or int):
|
|
34
|
+
headers = create_authorization_header()
|
|
35
|
+
r = http.delete(url=f'{url}/projects/{proj_id}/', headers=headers)
|
|
36
|
+
|
|
37
|
+
if r.status_code == 200:
|
|
38
|
+
return True
|
|
39
|
+
else:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def list_projects():
|
|
44
|
+
headers = create_authorization_header()
|
|
45
|
+
r = http.get(url=f'{url}/projects/', headers=headers)
|
|
46
|
+
|
|
47
|
+
if r.status_code == 200:
|
|
48
|
+
return r.json()
|
|
49
|
+
else:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def list_tokens(project_id):
|
|
54
|
+
headers = create_authorization_header()
|
|
55
|
+
r = http.get(url=f'{url}/project-tokens/{project_id}/', headers=headers)
|
|
56
|
+
if r.status_code == 200:
|
|
57
|
+
return r.json()
|
|
58
|
+
else:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def create_token(project_id: str or int):
|
|
63
|
+
headers = create_authorization_header()
|
|
64
|
+
r = http.post(url=f'{url}/project-tokens/{project_id}/',
|
|
65
|
+
json={'cmd': 'create'},
|
|
66
|
+
headers=headers
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if r.status_code == 200:
|
|
70
|
+
return r.json()
|
|
71
|
+
else:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def remove_token(token_id: str or int):
|
|
76
|
+
headers = create_authorization_header()
|
|
77
|
+
r = http.delete(url=f'{url}/project-tokens/token/{token_id}/', headers=headers)
|
|
78
|
+
|
|
79
|
+
if r.status_code == 200:
|
|
80
|
+
return r.json()
|
|
81
|
+
else:
|
|
82
|
+
print(r.status_code)
|
|
83
|
+
print(r.text)
|
|
84
|
+
return False
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from urllib.parse import urlencode
|
|
3
|
+
|
|
4
|
+
from FeatureCloud.api.imp.test.helper import http
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def is_online(url: str = 'http://localhost:8000/'):
|
|
8
|
+
try:
|
|
9
|
+
requests.get(url=url)
|
|
10
|
+
return True
|
|
11
|
+
except requests.exceptions.ConnectionError:
|
|
12
|
+
return False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def start_test(url: str, docker_image: str, directories: [], generic_directory: str, is_local_relay: bool,
|
|
16
|
+
query_interval: int, download_results: str):
|
|
17
|
+
"""
|
|
18
|
+
Start a new test
|
|
19
|
+
:param url:
|
|
20
|
+
:param docker_image:
|
|
21
|
+
:param directories:
|
|
22
|
+
:param generic_directory
|
|
23
|
+
:param is_local_relay:
|
|
24
|
+
:param query_interval:
|
|
25
|
+
:param download_results:
|
|
26
|
+
:return:
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
response = http.post(url=f'{url}/app/test/', json={'dockerImage': docker_image,
|
|
30
|
+
'directories': list(directories),
|
|
31
|
+
'genericDirectory': generic_directory,
|
|
32
|
+
'isLocalRelay': is_local_relay,
|
|
33
|
+
'queryInterval': query_interval,
|
|
34
|
+
'downloadResults': download_results})
|
|
35
|
+
if response.status_code == 200:
|
|
36
|
+
return True, response.json()
|
|
37
|
+
else:
|
|
38
|
+
return False, response.json()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def delete_test(url: str, test_id: str or int):
|
|
42
|
+
response = http.delete(url=f'{url}/app/test/{test_id}/?delete=true')
|
|
43
|
+
if response.status_code == 200:
|
|
44
|
+
return True, response
|
|
45
|
+
else:
|
|
46
|
+
return False, response.json()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def stop_test(url: str, test_id: str or int):
|
|
50
|
+
response = http.delete(url=f'{url}/app/test/{test_id}/?delete=false')
|
|
51
|
+
if response.status_code == 200:
|
|
52
|
+
return True, response.content
|
|
53
|
+
else:
|
|
54
|
+
return False, response.json()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_tests(url: str):
|
|
58
|
+
response = http.get(url=f'{url}/app/tests/')
|
|
59
|
+
|
|
60
|
+
if response.status_code == 200:
|
|
61
|
+
return True, response.json()
|
|
62
|
+
else:
|
|
63
|
+
return False, response.json()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def delete_tests(url: str):
|
|
67
|
+
response = http.delete(url=f'{url}/app/tests/')
|
|
68
|
+
if response.status_code == 200:
|
|
69
|
+
return True, response.content
|
|
70
|
+
else:
|
|
71
|
+
return False, response.json()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_test(url: str, test_id: str or int):
|
|
75
|
+
response = http.get(url=f'{url}/app/test/{test_id}/')
|
|
76
|
+
|
|
77
|
+
if response.status_code == 200:
|
|
78
|
+
return True, response.json()
|
|
79
|
+
else:
|
|
80
|
+
return False, response.json()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_traffic(url: str, test_id: str or int):
|
|
84
|
+
response = http.get(url=f'{url}/app/test/{test_id}/traffic/')
|
|
85
|
+
|
|
86
|
+
if response.status_code == 200:
|
|
87
|
+
return True, response.json()
|
|
88
|
+
else:
|
|
89
|
+
return False, response.json()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_logs(url: str, test_id: str or int, instance_id: str or int, from_param: str):
|
|
93
|
+
response = http.get(url=f'{url}/app/test/{test_id}/instance/{instance_id}/logs/?from={from_param}')
|
|
94
|
+
if response.status_code == 200:
|
|
95
|
+
return True, response.json()
|
|
96
|
+
else:
|
|
97
|
+
return False, response.json()
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from FeatureCloud.api.imp.test import helper
|
|
2
|
+
from FeatureCloud.api.imp.test.api import controller
|
|
3
|
+
from FeatureCloud.api.imp.exceptions import ControllerOffline, FCException
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def help():
|
|
7
|
+
return (None, """For registering and testing your apps or using other apps, please visit
|
|
8
|
+
our
|
|
9
|
+
website: \n https://featurecloud.ai.\n And for more information about
|
|
10
|
+
FeatureCloud architecture: \n
|
|
11
|
+
The FeatureCloud AI Store for Federated Learning in Biomedicine and
|
|
12
|
+
Beyond\n
|
|
13
|
+
https://arxiv.org/abs/2105.05734 """)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def start(controller_host: str, client_dirs: str, generic_dir: str, app_image: str, channel: str, query_interval: int,
|
|
17
|
+
download_results: str):
|
|
18
|
+
if not controller.is_online(controller_host):
|
|
19
|
+
raise ControllerOffline(controller_host)
|
|
20
|
+
|
|
21
|
+
success, result = controller.start_test(controller_host,
|
|
22
|
+
app_image,
|
|
23
|
+
filter(None, client_dirs.split(',')),
|
|
24
|
+
generic_dir,
|
|
25
|
+
channel == 'local',
|
|
26
|
+
query_interval,
|
|
27
|
+
download_results)
|
|
28
|
+
|
|
29
|
+
if success:
|
|
30
|
+
return result['id']
|
|
31
|
+
else:
|
|
32
|
+
raise FCException(result['detail'])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def stop(controller_host: str, test_id: str or int):
|
|
36
|
+
if not controller.is_online(controller_host):
|
|
37
|
+
raise ControllerOffline(controller_host)
|
|
38
|
+
|
|
39
|
+
success, result = controller.stop_test(controller_host, test_id)
|
|
40
|
+
|
|
41
|
+
if success:
|
|
42
|
+
return test_id
|
|
43
|
+
else:
|
|
44
|
+
raise FCException(result['detail'])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def delete(controller_host: str, test_id: str or int, del_all: str):
|
|
48
|
+
if not controller.is_online(controller_host):
|
|
49
|
+
raise ControllerOffline(controller_host)
|
|
50
|
+
|
|
51
|
+
if test_id is not None and del_all is None:
|
|
52
|
+
success, result = controller.delete_test(controller_host, test_id)
|
|
53
|
+
|
|
54
|
+
if success:
|
|
55
|
+
return test_id
|
|
56
|
+
else:
|
|
57
|
+
raise FCException(result['detail'])
|
|
58
|
+
|
|
59
|
+
elif test_id is None and len(del_all) > 0:
|
|
60
|
+
if del_all.lower() == 'all':
|
|
61
|
+
success, result = controller.delete_tests(controller_host)
|
|
62
|
+
|
|
63
|
+
if success:
|
|
64
|
+
return 'all'
|
|
65
|
+
else:
|
|
66
|
+
raise FCException(result['detail'])
|
|
67
|
+
else:
|
|
68
|
+
raise FCException(f'Unsupported argument {del_all}')
|
|
69
|
+
|
|
70
|
+
else:
|
|
71
|
+
raise FCException('Wrong combination of parameters. To delete a single test use option --test-id. To delete all tests use the "all" argument.')
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def list(controller_host: str, format: str = 'dataframe'):
|
|
75
|
+
if not controller.is_online(controller_host):
|
|
76
|
+
raise ControllerOffline(controller_host)
|
|
77
|
+
|
|
78
|
+
success, result = controller.get_tests(controller_host)
|
|
79
|
+
if success:
|
|
80
|
+
if format == 'json':
|
|
81
|
+
return result
|
|
82
|
+
else:
|
|
83
|
+
return helper.json_to_dataframe(result).set_index('id')
|
|
84
|
+
else:
|
|
85
|
+
raise FCException(result)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def info(controller_host: str, test_id: str or int, format: str = 'dataframe'):
|
|
89
|
+
if not controller.is_online(controller_host):
|
|
90
|
+
raise ControllerOffline(controller_host)
|
|
91
|
+
|
|
92
|
+
success, result = controller.get_test(controller_host, test_id)
|
|
93
|
+
if success:
|
|
94
|
+
if format == 'json':
|
|
95
|
+
return result
|
|
96
|
+
else:
|
|
97
|
+
return helper.json_to_dataframe(result, single_entry=True).set_index('id')
|
|
98
|
+
else:
|
|
99
|
+
raise FCException(result['detail'])
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def traffic(controller_host: str, test_id: str or int, format: str):
|
|
103
|
+
if not controller.is_online(controller_host):
|
|
104
|
+
raise ControllerOffline(controller_host)
|
|
105
|
+
|
|
106
|
+
success, result = controller.get_traffic(controller_host, test_id)
|
|
107
|
+
if success:
|
|
108
|
+
if format == 'json':
|
|
109
|
+
return result
|
|
110
|
+
else:
|
|
111
|
+
return helper.json_to_dataframe(result)
|
|
112
|
+
else:
|
|
113
|
+
raise FCException(result['detail'])
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def logs(controller_host: str, test_id: str or int, instance_id: str or int, from_param: str):
|
|
117
|
+
if not controller.is_online(controller_host):
|
|
118
|
+
raise ControllerOffline(controller_host)
|
|
119
|
+
|
|
120
|
+
success, result = controller.get_logs(controller_host, test_id, instance_id, from_param)
|
|
121
|
+
if success:
|
|
122
|
+
return result["logs"]
|
|
123
|
+
else:
|
|
124
|
+
raise FCException(result['detail'])
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import requests
|
|
3
|
+
from requests.adapters import HTTPAdapter
|
|
4
|
+
from urllib3 import Retry
|
|
5
|
+
|
|
6
|
+
retry_strategy = Retry(
|
|
7
|
+
total=3,
|
|
8
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
9
|
+
method_whitelist=["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"],
|
|
10
|
+
backoff_factor=1
|
|
11
|
+
)
|
|
12
|
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
13
|
+
http = requests.Session()
|
|
14
|
+
http.mount("https://", adapter)
|
|
15
|
+
http.mount("http://", adapter)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def json_to_dataframe(json, single_entry: bool = False, default_column: str = 'id'):
|
|
19
|
+
if len(json) == 0:
|
|
20
|
+
# return empty data frame
|
|
21
|
+
df = pd.DataFrame({default_column: []})
|
|
22
|
+
elif not single_entry:
|
|
23
|
+
df = pd.DataFrame.from_records(json)
|
|
24
|
+
else:
|
|
25
|
+
df = pd.DataFrame.from_dict(json, orient='index').T
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
df.createdAt = pd.to_datetime(df.createdAt).apply(lambda x: str(x).split(".")[0])
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
try:
|
|
32
|
+
df.finishedAt = pd.to_datetime(df.finishedAt).apply(lambda x: str(x).split(".")[0])
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
pd.set_option('display.max_rows', None)
|
|
36
|
+
pd.set_option('display.max_columns', None)
|
|
37
|
+
pd.set_option('display.width', None)
|
|
38
|
+
pd.set_option('display.max_colwidth', None)
|
|
39
|
+
|
|
40
|
+
return df
|