vantage6 5.0.0a22__py3-none-any.whl → 5.0.0a29__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 vantage6 might be problematic. Click here for more details.
- tests_cli/test_client_script.py +23 -0
- tests_cli/test_server_cli.py +7 -6
- tests_cli/test_wizard.py +7 -7
- vantage6/cli/__build__ +1 -1
- vantage6/cli/algorithm/generate_algorithm_json.py +531 -0
- vantage6/cli/algostore/list.py +2 -1
- vantage6/cli/algostore/start.py +6 -6
- vantage6/cli/algostore/stop.py +3 -2
- vantage6/cli/cli.py +25 -0
- vantage6/cli/common/decorator.py +3 -1
- vantage6/cli/common/start.py +221 -12
- vantage6/cli/common/stop.py +90 -0
- vantage6/cli/common/utils.py +15 -20
- vantage6/cli/config.py +260 -0
- vantage6/cli/configuration_manager.py +8 -14
- vantage6/cli/configuration_wizard.py +66 -111
- vantage6/cli/context/__init__.py +2 -1
- vantage6/cli/context/algorithm_store.py +10 -7
- vantage6/cli/context/node.py +38 -54
- vantage6/cli/context/server.py +36 -5
- vantage6/cli/dev/create.py +88 -29
- vantage6/cli/dev/data/km_dataset.csv +2401 -0
- vantage6/cli/dev/remove.py +99 -98
- vantage6/cli/globals.py +24 -4
- vantage6/cli/node/common/__init__.py +6 -5
- vantage6/cli/node/new.py +4 -3
- vantage6/cli/node/remove.py +4 -2
- vantage6/cli/node/start.py +33 -42
- vantage6/cli/prometheus/monitoring_manager.py +146 -0
- vantage6/cli/prometheus/prometheus.yml +5 -0
- vantage6/cli/server/files.py +4 -2
- vantage6/cli/server/import_.py +7 -7
- vantage6/cli/server/list.py +2 -1
- vantage6/cli/server/new.py +25 -6
- vantage6/cli/server/shell.py +5 -4
- vantage6/cli/server/start.py +44 -213
- vantage6/cli/server/stop.py +36 -105
- vantage6/cli/server/version.py +5 -4
- vantage6/cli/template/algo_store_config.j2 +0 -1
- vantage6/cli/template/node_config.j2 +3 -1
- vantage6/cli/template/server_import_config.j2 +0 -2
- vantage6/cli/test/algo_test_scripts/algo_test_arguments.py +29 -0
- vantage6/cli/test/algo_test_scripts/algo_test_script.py +92 -0
- vantage6/cli/test/client_script.py +151 -0
- vantage6/cli/test/common/diagnostic_runner.py +2 -2
- vantage6/cli/test/feature_tester.py +5 -2
- vantage6/cli/use/context.py +46 -0
- vantage6/cli/use/namespace.py +55 -0
- vantage6/cli/utils.py +70 -4
- {vantage6-5.0.0a22.dist-info → vantage6-5.0.0a29.dist-info}/METADATA +15 -11
- vantage6-5.0.0a29.dist-info/RECORD +84 -0
- {vantage6-5.0.0a22.dist-info → vantage6-5.0.0a29.dist-info}/WHEEL +1 -1
- vantage6-5.0.0a22.dist-info/RECORD +0 -72
- {vantage6-5.0.0a22.dist-info → vantage6-5.0.0a29.dist-info}/entry_points.txt +0 -0
- {vantage6-5.0.0a22.dist-info → vantage6-5.0.0a29.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# from click import UsageError
|
|
2
|
+
# from vantage6.cli.test.client_script import cli_test_client_script
|
|
3
|
+
|
|
4
|
+
# import click
|
|
5
|
+
# import unittest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# class TestScriptTest(unittest.TestCase):
|
|
9
|
+
# def test_script_incorrect_usage(self):
|
|
10
|
+
# ctx = click.Context(cli_test_client_script)
|
|
11
|
+
|
|
12
|
+
# with self.assertRaises(UsageError):
|
|
13
|
+
# ctx.invoke(
|
|
14
|
+
# cli_test_client_script,
|
|
15
|
+
# script="path/to/script.py",
|
|
16
|
+
# task_arguments="{'my_arg': 1}",
|
|
17
|
+
# )
|
|
18
|
+
|
|
19
|
+
# with self.assertRaises(UsageError):
|
|
20
|
+
# ctx.invoke(
|
|
21
|
+
# cli_test_client_script,
|
|
22
|
+
# task_arguments="not_a_json",
|
|
23
|
+
# )
|
tests_cli/test_server_cli.py
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import unittest
|
|
2
|
-
|
|
3
|
-
from unittest.mock import MagicMock, patch
|
|
4
2
|
from pathlib import Path
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
5
|
from click.testing import CliRunner
|
|
6
6
|
|
|
7
7
|
from vantage6.common.globals import APPNAME, InstanceType
|
|
8
|
+
|
|
8
9
|
from vantage6.cli.common.utils import attach_logs
|
|
9
|
-
from vantage6.cli.server.
|
|
10
|
-
from vantage6.cli.server.list import cli_server_configuration_list
|
|
10
|
+
from vantage6.cli.server.attach import cli_server_attach
|
|
11
11
|
from vantage6.cli.server.files import cli_server_files
|
|
12
12
|
from vantage6.cli.server.import_ import cli_server_import
|
|
13
|
+
from vantage6.cli.server.list import cli_server_configuration_list
|
|
13
14
|
from vantage6.cli.server.new import cli_server_new
|
|
15
|
+
from vantage6.cli.server.start import cli_server_start
|
|
14
16
|
from vantage6.cli.server.stop import cli_server_stop
|
|
15
|
-
from vantage6.cli.server.attach import cli_server_attach
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class ServerCLITest(unittest.TestCase):
|
|
@@ -133,7 +134,7 @@ class ServerCLITest(unittest.TestCase):
|
|
|
133
134
|
"""Stop server without errors."""
|
|
134
135
|
|
|
135
136
|
container1 = MagicMock()
|
|
136
|
-
container1.name = f"{APPNAME}-iknl-system-{InstanceType.SERVER
|
|
137
|
+
container1.name = f"{APPNAME}-iknl-system-{InstanceType.SERVER}"
|
|
137
138
|
containers.containers.list.return_value = [container1]
|
|
138
139
|
|
|
139
140
|
runner = CliRunner()
|
tests_cli/test_wizard.py
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import unittest
|
|
2
|
-
|
|
3
2
|
from pathlib import Path
|
|
4
|
-
from unittest.mock import
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
from vantage6.common.globals import InstanceType, NodePolicy
|
|
5
6
|
|
|
6
7
|
from vantage6.cli.configuration_wizard import (
|
|
7
|
-
node_configuration_questionaire,
|
|
8
|
-
server_configuration_questionaire,
|
|
9
8
|
configuration_wizard,
|
|
9
|
+
node_configuration_questionaire,
|
|
10
10
|
select_configuration_questionaire,
|
|
11
|
+
server_configuration_questionaire,
|
|
11
12
|
)
|
|
12
|
-
from vantage6.common.globals import InstanceType, NodePolicy
|
|
13
13
|
|
|
14
14
|
module_path = "vantage6.cli.configuration_wizard"
|
|
15
15
|
|
|
@@ -70,8 +70,8 @@ class WizardTest(unittest.TestCase):
|
|
|
70
70
|
for key in keys:
|
|
71
71
|
self.assertIn(key, config)
|
|
72
72
|
nested_keys = [
|
|
73
|
-
["policies", NodePolicy.ALLOWED_ALGORITHMS],
|
|
74
|
-
["policies", NodePolicy.ALLOWED_ALGORITHM_STORES],
|
|
73
|
+
["policies", NodePolicy.ALLOWED_ALGORITHMS.value],
|
|
74
|
+
["policies", NodePolicy.ALLOWED_ALGORITHM_STORES.value],
|
|
75
75
|
]
|
|
76
76
|
for nesting in nested_keys:
|
|
77
77
|
current_config = config
|
vantage6/cli/__build__
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
29
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import inspect
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from inspect import getmembers, isfunction, ismodule, signature
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from types import ModuleType, UnionType
|
|
9
|
+
from typing import Any, OrderedDict
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
import pandas as pd
|
|
13
|
+
import questionary as q
|
|
14
|
+
|
|
15
|
+
from vantage6.common import error, info, warning
|
|
16
|
+
from vantage6.common.algorithm_function import (
|
|
17
|
+
get_vantage6_decorator_type,
|
|
18
|
+
is_vantage6_algorithm_func,
|
|
19
|
+
)
|
|
20
|
+
from vantage6.common.enum import AlgorithmArgumentType, AlgorithmStepType, StrEnumBase
|
|
21
|
+
|
|
22
|
+
from vantage6.algorithm.tools import DecoratorStepType
|
|
23
|
+
|
|
24
|
+
from vantage6.algorithm.client import AlgorithmClient
|
|
25
|
+
from vantage6.algorithm.preprocessing.algorithm_json_data import (
|
|
26
|
+
PREPROCESSING_FUNCTIONS_JSON_DATA,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MergePreference:
|
|
31
|
+
"""Singleton class to manage global merge preference state"""
|
|
32
|
+
|
|
33
|
+
_instance = None
|
|
34
|
+
_prefer_existing = None
|
|
35
|
+
|
|
36
|
+
def __new__(cls):
|
|
37
|
+
if cls._instance is None:
|
|
38
|
+
cls._instance = super(MergePreference, cls).__new__(cls)
|
|
39
|
+
return cls._instance
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def get_preference(cls) -> bool | None:
|
|
43
|
+
"""Get the current merge preference"""
|
|
44
|
+
return cls._prefer_existing
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def set_preference(cls, prefer_existing: bool) -> None:
|
|
48
|
+
"""Set the merge preference globally"""
|
|
49
|
+
cls._prefer_existing = prefer_existing
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def reset(cls) -> None:
|
|
53
|
+
"""Reset the preference to None"""
|
|
54
|
+
cls._prefer_existing = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class FunctionArgumentType(StrEnumBase):
|
|
58
|
+
"""Type of the function argument"""
|
|
59
|
+
|
|
60
|
+
PARAMETER = "parameter"
|
|
61
|
+
DATAFRAME = "dataframe"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Function:
|
|
65
|
+
"""Class to handle a function and its JSON representation"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, func: callable):
|
|
68
|
+
self.func = func
|
|
69
|
+
self.name = func.__name__
|
|
70
|
+
self.signature = signature(func)
|
|
71
|
+
self.docstring = func.__doc__
|
|
72
|
+
self.json = None
|
|
73
|
+
self.step_type = None
|
|
74
|
+
|
|
75
|
+
def prepare_json(self) -> None:
|
|
76
|
+
"""Convert the function to a JSON format"""
|
|
77
|
+
self.step_type = self._get_step_type()
|
|
78
|
+
function_json = {
|
|
79
|
+
"name": self.name,
|
|
80
|
+
"display_name": self._pretty_print_name(self.name),
|
|
81
|
+
"standalone": True,
|
|
82
|
+
"description": self._extract_headline_of_docstring(),
|
|
83
|
+
"step_type": self.step_type.value if self.step_type else None,
|
|
84
|
+
"ui_visualizations": [],
|
|
85
|
+
"arguments": [],
|
|
86
|
+
"databases": [],
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
parameters = OrderedDict(self.signature.parameters)
|
|
90
|
+
|
|
91
|
+
# if the function is a data extraction function, the first argument is a dict
|
|
92
|
+
# with database connection details. This argument should not be added to the
|
|
93
|
+
# function json. Instead, a database should be added to the function json.
|
|
94
|
+
if self.step_type == AlgorithmStepType.DATA_EXTRACTION:
|
|
95
|
+
function_json["databases"].append(
|
|
96
|
+
{
|
|
97
|
+
"name": "Database",
|
|
98
|
+
"description": "Database to extract data from",
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
# remove database connection details from the signature
|
|
102
|
+
parameters.popitem(last=False)
|
|
103
|
+
|
|
104
|
+
# add the arguments to the function json
|
|
105
|
+
for name, param in parameters.items():
|
|
106
|
+
arg_json, arg_type = self._get_argument_json(name, param)
|
|
107
|
+
if arg_json is None:
|
|
108
|
+
continue
|
|
109
|
+
elif arg_type == FunctionArgumentType.DATAFRAME:
|
|
110
|
+
function_json["databases"].append(arg_json)
|
|
111
|
+
else:
|
|
112
|
+
function_json["arguments"].append(arg_json)
|
|
113
|
+
self.json = function_json
|
|
114
|
+
|
|
115
|
+
def merge_with_template_json_data(self) -> None:
|
|
116
|
+
"""
|
|
117
|
+
Merge the function jsons with the json data from the algorithm_json_data module
|
|
118
|
+
"""
|
|
119
|
+
# Only merge the function jsons with template json data if it is an
|
|
120
|
+
# infrastructure-defined function
|
|
121
|
+
if (
|
|
122
|
+
not self.func.__module__.startswith("vantage6.algorithm.")
|
|
123
|
+
or not self.json["name"] in PREPROCESSING_FUNCTIONS_JSON_DATA
|
|
124
|
+
):
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
# get the template json data for the function
|
|
128
|
+
template_json = PREPROCESSING_FUNCTIONS_JSON_DATA[self.json["name"]]
|
|
129
|
+
# merge the dicts, with the template dict taking precedence
|
|
130
|
+
for argument in self.json["arguments"]:
|
|
131
|
+
if argument["name"] in template_json["arguments"]:
|
|
132
|
+
argument.update(template_json["arguments"][argument["name"]])
|
|
133
|
+
# Add any frontend arguments specified in the template json
|
|
134
|
+
if "frontend_arguments" in template_json:
|
|
135
|
+
for frontend_argument in template_json["frontend_arguments"]:
|
|
136
|
+
self._add_frontend_argument(template_json, frontend_argument)
|
|
137
|
+
|
|
138
|
+
def merge_with_existing_json(self, existing_json: dict) -> None:
|
|
139
|
+
"""Merge the function json with the existing json data"""
|
|
140
|
+
self._merge_dicts(self.json, existing_json)
|
|
141
|
+
|
|
142
|
+
def _merge_dicts(self, target: dict, source: dict) -> None:
|
|
143
|
+
"""
|
|
144
|
+
Recursively merge source dict into target dict, with source taking precedence
|
|
145
|
+
"""
|
|
146
|
+
for key, value in source.items():
|
|
147
|
+
if key in target:
|
|
148
|
+
if isinstance(value, dict) and isinstance(target[key], dict):
|
|
149
|
+
# Recursively merge nested dictionaries
|
|
150
|
+
self._merge_dicts(target[key], value)
|
|
151
|
+
else:
|
|
152
|
+
self._replace_target_with_source(target, key, value)
|
|
153
|
+
else:
|
|
154
|
+
# Add new key-value pair from source to target
|
|
155
|
+
self._replace_target_with_source(target, key, value)
|
|
156
|
+
|
|
157
|
+
def _replace_target_with_source(self, target: dict, key: str, value: Any) -> None:
|
|
158
|
+
"""Replace the value in target with the one from source"""
|
|
159
|
+
if target[key] == value:
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
prefer_existing = MergePreference.get_preference()
|
|
163
|
+
if prefer_existing:
|
|
164
|
+
target[key] = value
|
|
165
|
+
elif prefer_existing is None:
|
|
166
|
+
info(
|
|
167
|
+
f"Different values for the same key '{key}' in function '{self.name}' "
|
|
168
|
+
"were found."
|
|
169
|
+
)
|
|
170
|
+
info(f"Value from function itself: {target[key]}")
|
|
171
|
+
info(f"Value from algorithm.json: {value}")
|
|
172
|
+
result = q.select(
|
|
173
|
+
"Please select the value to keep:",
|
|
174
|
+
choices=[
|
|
175
|
+
"function itself",
|
|
176
|
+
"algorithm.json",
|
|
177
|
+
"function itself (also for all other conflicts)",
|
|
178
|
+
"algorithm.json (also for all other conflicts)",
|
|
179
|
+
],
|
|
180
|
+
).unsafe_ask()
|
|
181
|
+
if result == "algorithm.json":
|
|
182
|
+
target[key] = value
|
|
183
|
+
elif result == "function itself":
|
|
184
|
+
pass # do nothing
|
|
185
|
+
elif result == "function itself (also for all other conflicts)":
|
|
186
|
+
MergePreference.set_preference(False)
|
|
187
|
+
elif result == "algorithm.json (also for all other conflicts)":
|
|
188
|
+
MergePreference.set_preference(True)
|
|
189
|
+
target[key] = value
|
|
190
|
+
|
|
191
|
+
def _get_argument_json(
|
|
192
|
+
self, name: str, param: inspect.Parameter
|
|
193
|
+
) -> tuple[dict | None, FunctionArgumentType | None]:
|
|
194
|
+
"""Get the argument JSON"""
|
|
195
|
+
|
|
196
|
+
if param.annotation is None:
|
|
197
|
+
error(f"Function {self.name} has no annotation for argument {name}")
|
|
198
|
+
info(f"Please add a type annotation to the argument {name}")
|
|
199
|
+
info(f"For example, for string arguments: 'def {self.name}({name}: str)'")
|
|
200
|
+
exit(1)
|
|
201
|
+
|
|
202
|
+
if param.annotation is AlgorithmClient:
|
|
203
|
+
# Algorithm client arguments do not have to be provided by the user
|
|
204
|
+
return None, None
|
|
205
|
+
elif param.annotation is pd.DataFrame:
|
|
206
|
+
# this is an argument that requires the user to supply a dataframe. That
|
|
207
|
+
# only requires a name and description.
|
|
208
|
+
return {
|
|
209
|
+
"name": name if name != "df" else "Data to use",
|
|
210
|
+
"description": self._extract_parameter_description(name),
|
|
211
|
+
}, FunctionArgumentType.DATAFRAME
|
|
212
|
+
else:
|
|
213
|
+
# This is a regular function parameter
|
|
214
|
+
type_ = self._get_argument_type(param, name)
|
|
215
|
+
arg_json = {
|
|
216
|
+
"name": name,
|
|
217
|
+
"display_name": self._pretty_print_name(name),
|
|
218
|
+
"description": self._extract_parameter_description(name),
|
|
219
|
+
"type": type_.value if type_ else None,
|
|
220
|
+
"required": param.default == inspect.Parameter.empty,
|
|
221
|
+
"has_default_value": param.default != inspect.Parameter.empty,
|
|
222
|
+
"is_frontend_only": False,
|
|
223
|
+
}
|
|
224
|
+
if param.default != inspect.Parameter.empty:
|
|
225
|
+
arg_json["default"] = param.default
|
|
226
|
+
return arg_json, FunctionArgumentType.PARAMETER
|
|
227
|
+
|
|
228
|
+
def _add_frontend_argument(
|
|
229
|
+
self, template_json: dict, frontend_argument: str
|
|
230
|
+
) -> None:
|
|
231
|
+
"""Add a frontend argument to the function json"""
|
|
232
|
+
frontend_argument_json: dict = template_json["frontend_arguments"][
|
|
233
|
+
frontend_argument
|
|
234
|
+
]
|
|
235
|
+
before_arg_name = frontend_argument_json.pop("before_argument")
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
before_arg_idx = next(
|
|
239
|
+
idx
|
|
240
|
+
for idx, arg in enumerate(self.json["arguments"])
|
|
241
|
+
if arg["name"] == before_arg_name
|
|
242
|
+
)
|
|
243
|
+
self.json["arguments"].insert(before_arg_idx, frontend_argument_json)
|
|
244
|
+
except StopIteration:
|
|
245
|
+
warning(
|
|
246
|
+
f"Could not find argument {before_arg_name} in function "
|
|
247
|
+
f"{self.json['name']}. Frontend argument {frontend_argument} "
|
|
248
|
+
"will not be added."
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def _get_argument_type(
|
|
252
|
+
self, param: inspect.Parameter, name: str
|
|
253
|
+
) -> AlgorithmArgumentType | None:
|
|
254
|
+
"""Get the type of the argument"""
|
|
255
|
+
if isinstance(param.annotation, UnionType):
|
|
256
|
+
# Arguments with default values may have type 'str | None'. If that is the
|
|
257
|
+
# case, we want to use the type of the first element in the union.
|
|
258
|
+
if len(param.annotation.__args__) > 2:
|
|
259
|
+
# if there are more than 2 elements in the union, don't handle
|
|
260
|
+
warning(
|
|
261
|
+
f"Unsupported argument type: {param.annotation} for argument {name}"
|
|
262
|
+
f" in function {self.name}"
|
|
263
|
+
)
|
|
264
|
+
return None
|
|
265
|
+
elif len(param.annotation.__args__) == 2:
|
|
266
|
+
# if there are two, we want to use the first one if the second is None
|
|
267
|
+
if param.annotation.__args__[1] is type(None):
|
|
268
|
+
type_ = param.annotation.__args__[0]
|
|
269
|
+
else:
|
|
270
|
+
warning(
|
|
271
|
+
f"Unsupported argument type: {param.annotation} for argument "
|
|
272
|
+
f"{name} in function {self.name}"
|
|
273
|
+
)
|
|
274
|
+
return None
|
|
275
|
+
else:
|
|
276
|
+
# normally, unions have 2+ elements. If there is only one, use that
|
|
277
|
+
type_ = param.annotation.__args__[0]
|
|
278
|
+
else:
|
|
279
|
+
type_ = param.annotation
|
|
280
|
+
|
|
281
|
+
if type_ == str:
|
|
282
|
+
return AlgorithmArgumentType.STRING
|
|
283
|
+
elif type_ == dict:
|
|
284
|
+
return AlgorithmArgumentType.JSON
|
|
285
|
+
elif type_ == int:
|
|
286
|
+
return AlgorithmArgumentType.INTEGER
|
|
287
|
+
elif type_ == float:
|
|
288
|
+
return AlgorithmArgumentType.FLOAT
|
|
289
|
+
elif type_ == bool:
|
|
290
|
+
return AlgorithmArgumentType.BOOLEAN
|
|
291
|
+
elif type_ == list:
|
|
292
|
+
return AlgorithmArgumentType.STRINGS
|
|
293
|
+
elif type_ == list[str]:
|
|
294
|
+
return AlgorithmArgumentType.STRINGS
|
|
295
|
+
elif type_ == list[int]:
|
|
296
|
+
return AlgorithmArgumentType.INTEGERS
|
|
297
|
+
elif type_ == list[float]:
|
|
298
|
+
return AlgorithmArgumentType.FLOATS
|
|
299
|
+
else:
|
|
300
|
+
warning(
|
|
301
|
+
f"Unsupported argument type: {param.annotation} for argument {name} "
|
|
302
|
+
f"in function {self.name}"
|
|
303
|
+
)
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
def _pretty_print_name(self, name: str) -> str:
|
|
307
|
+
"""Pretty print the name of the function"""
|
|
308
|
+
pretty = name.replace("_", " ")
|
|
309
|
+
if len(pretty):
|
|
310
|
+
pretty = pretty[0].upper() + pretty[1:]
|
|
311
|
+
return pretty
|
|
312
|
+
|
|
313
|
+
def _extract_headline_of_docstring(self) -> str:
|
|
314
|
+
"""Extract the headline of the docstring"""
|
|
315
|
+
if not self.docstring:
|
|
316
|
+
return ""
|
|
317
|
+
|
|
318
|
+
# Split by double newlines to get the first paragraph
|
|
319
|
+
paragraphs = self.docstring.split("\n\n")
|
|
320
|
+
first_paragraph = paragraphs[0]
|
|
321
|
+
|
|
322
|
+
# Split by single newlines and join the lines with spaces
|
|
323
|
+
lines = first_paragraph.split("\n")
|
|
324
|
+
header = " ".join(line.strip() for line in lines if line.strip() != "")
|
|
325
|
+
return header
|
|
326
|
+
|
|
327
|
+
def _get_step_type(self) -> AlgorithmStepType | None:
|
|
328
|
+
"""Get the step type of the function"""
|
|
329
|
+
decorator_type = get_vantage6_decorator_type(self.func)
|
|
330
|
+
if decorator_type == DecoratorStepType.FEDERATED:
|
|
331
|
+
return AlgorithmStepType.FEDERATED_COMPUTE
|
|
332
|
+
elif decorator_type == DecoratorStepType.CENTRAL:
|
|
333
|
+
return AlgorithmStepType.CENTRAL_COMPUTE
|
|
334
|
+
elif decorator_type == DecoratorStepType.PREPROCESSING:
|
|
335
|
+
return AlgorithmStepType.PREPROCESSING
|
|
336
|
+
elif decorator_type == DecoratorStepType.DATA_EXTRACTION:
|
|
337
|
+
return AlgorithmStepType.DATA_EXTRACTION
|
|
338
|
+
else:
|
|
339
|
+
warning(
|
|
340
|
+
f"Unsupported decorator type: {decorator_type} for function {self.name}"
|
|
341
|
+
)
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
def _extract_parameter_description(self, name: str) -> str:
|
|
345
|
+
"""Extract the description of the parameter"""
|
|
346
|
+
if not self.docstring:
|
|
347
|
+
return ""
|
|
348
|
+
|
|
349
|
+
# Try both patterns: "{name}:" and "{name} :"
|
|
350
|
+
patterns = [f"{name}:", f"{name} :"]
|
|
351
|
+
|
|
352
|
+
for pattern in patterns:
|
|
353
|
+
if pattern in self.docstring:
|
|
354
|
+
return self.docstring.split(pattern)[1].split("\n")[1].strip()
|
|
355
|
+
|
|
356
|
+
return ""
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@click.command()
|
|
360
|
+
@click.option(
|
|
361
|
+
"--algo-function-file",
|
|
362
|
+
default=None,
|
|
363
|
+
type=str,
|
|
364
|
+
help="Path to the file containing or importing the algorithm functions",
|
|
365
|
+
)
|
|
366
|
+
@click.option(
|
|
367
|
+
"--current-json",
|
|
368
|
+
default=None,
|
|
369
|
+
type=str,
|
|
370
|
+
help="Path to the current algorithm.json file",
|
|
371
|
+
)
|
|
372
|
+
@click.option(
|
|
373
|
+
"--output-file",
|
|
374
|
+
default="new-algorithm.json",
|
|
375
|
+
type=str,
|
|
376
|
+
help="Path to the output file",
|
|
377
|
+
)
|
|
378
|
+
def cli_algorithm_generate_algorithm_json(
|
|
379
|
+
algo_function_file: str, current_json: str, output_file: str
|
|
380
|
+
) -> dict:
|
|
381
|
+
"""
|
|
382
|
+
Generate an updated algorithm.json file to submit to the algorithm store.
|
|
383
|
+
|
|
384
|
+
You should provide the path to the file where the algorithm functions are
|
|
385
|
+
defined.
|
|
386
|
+
|
|
387
|
+
Note that if you do asterisk ('from x import *') imports, all functions from the
|
|
388
|
+
imported module will be added to the algorithm.json file.
|
|
389
|
+
"""
|
|
390
|
+
algo_function_file = _get_algo_function_file_location(algo_function_file)
|
|
391
|
+
|
|
392
|
+
current_json = _get_current_json_location(current_json)
|
|
393
|
+
|
|
394
|
+
# read the current algorithm.json file
|
|
395
|
+
with open(current_json, "r", encoding="utf-8") as f:
|
|
396
|
+
current_json_data = json.load(f)
|
|
397
|
+
|
|
398
|
+
# get the functions from the file
|
|
399
|
+
info(f"Importing functions from {algo_function_file}...")
|
|
400
|
+
functions = _get_functions_from_file(algo_function_file)
|
|
401
|
+
function_objs = [Function(f) for f in functions]
|
|
402
|
+
|
|
403
|
+
info("Converting functions to JSON...")
|
|
404
|
+
for function in function_objs:
|
|
405
|
+
function.prepare_json()
|
|
406
|
+
function.merge_with_template_json_data()
|
|
407
|
+
|
|
408
|
+
# merge the function jsons with the existing json data
|
|
409
|
+
current_json_func = [
|
|
410
|
+
f for f in current_json_data["functions"] if f["name"] == function.name
|
|
411
|
+
]
|
|
412
|
+
if current_json_func:
|
|
413
|
+
function.merge_with_existing_json(current_json_func[0])
|
|
414
|
+
|
|
415
|
+
# write the new algorithm.json file
|
|
416
|
+
info(f"Writing new algorithm.json file to {output_file}...")
|
|
417
|
+
current_json_data["functions"] = [f.json for f in function_objs]
|
|
418
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
419
|
+
json.dump(current_json_data, f, indent=2)
|
|
420
|
+
|
|
421
|
+
info(f"New algorithm.json file written to {output_file}")
|
|
422
|
+
|
|
423
|
+
warning("-" * 80)
|
|
424
|
+
warning("Always check the generated algorithm.json file before submitting it to ")
|
|
425
|
+
warning("the algorithm store!")
|
|
426
|
+
warning("-" * 80)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _get_functions_from_file(file_path: str) -> None:
|
|
430
|
+
"""Get the functions from the file
|
|
431
|
+
|
|
432
|
+
Parameters
|
|
433
|
+
----------
|
|
434
|
+
file_path : str
|
|
435
|
+
Path to the file containing or importing the algorithm functions
|
|
436
|
+
"""
|
|
437
|
+
# Convert path to absolute path
|
|
438
|
+
file_path = str(Path(file_path).resolve())
|
|
439
|
+
|
|
440
|
+
# Get the package root directory (two levels up from the file)
|
|
441
|
+
package_root = str(Path(file_path).parent.parent)
|
|
442
|
+
if package_root not in sys.path:
|
|
443
|
+
sys.path.insert(0, package_root)
|
|
444
|
+
|
|
445
|
+
# Get the module name from the file path, including the package name
|
|
446
|
+
package_name = Path(file_path).parent.name
|
|
447
|
+
module_name = f"{package_name}.{Path(file_path).stem}"
|
|
448
|
+
|
|
449
|
+
# Import the module
|
|
450
|
+
try:
|
|
451
|
+
module = importlib.import_module(module_name)
|
|
452
|
+
except ImportError as e:
|
|
453
|
+
raise ImportError(f"Could not import module {module_name}: {str(e)}") from e
|
|
454
|
+
|
|
455
|
+
def get_members_from_module(module: ModuleType) -> list:
|
|
456
|
+
"""Get the functions from the module"""
|
|
457
|
+
return [
|
|
458
|
+
member for name, member in getmembers(module) if not name.startswith("_")
|
|
459
|
+
]
|
|
460
|
+
|
|
461
|
+
# get the functions from the algorithm module
|
|
462
|
+
import_members = get_members_from_module(module)
|
|
463
|
+
import_functions = [
|
|
464
|
+
m for m in import_members if isfunction(m) and is_vantage6_algorithm_func(m)
|
|
465
|
+
]
|
|
466
|
+
import_modules = [m for m in import_members if ismodule(m)]
|
|
467
|
+
|
|
468
|
+
# add the functions from the imported modules (only 1 level deep). This is so that
|
|
469
|
+
# if you do e.g. 'from vantage6.algorithm.preprocessing import *', all functions
|
|
470
|
+
# from within those modules are also imported.
|
|
471
|
+
for import_module in import_modules:
|
|
472
|
+
second_level_import_members = get_members_from_module(import_module)
|
|
473
|
+
import_functions.extend(
|
|
474
|
+
[
|
|
475
|
+
m
|
|
476
|
+
for m in second_level_import_members
|
|
477
|
+
if isfunction(m) and is_vantage6_algorithm_func(m)
|
|
478
|
+
]
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
return import_functions
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _get_algo_function_file_location(algo_function_file: str | None) -> None:
|
|
485
|
+
"""Get user input for the algorithm creation
|
|
486
|
+
|
|
487
|
+
Parameters
|
|
488
|
+
----------
|
|
489
|
+
algo_function_file : str
|
|
490
|
+
Path to the file containing or importingthe algorithm functions
|
|
491
|
+
"""
|
|
492
|
+
if not algo_function_file:
|
|
493
|
+
default_dir = str(Path(os.getcwd()) / "__init__.py")
|
|
494
|
+
algo_function_file = q.text(
|
|
495
|
+
"Path to the file containing or importing the algorithm functions:",
|
|
496
|
+
default=default_dir,
|
|
497
|
+
).unsafe_ask()
|
|
498
|
+
|
|
499
|
+
# Convert to absolute path using pathlib
|
|
500
|
+
algo_function_file = str(Path(algo_function_file).resolve())
|
|
501
|
+
|
|
502
|
+
# check if the file exists
|
|
503
|
+
if not Path(algo_function_file).exists():
|
|
504
|
+
raise FileNotFoundError(f"File {algo_function_file} does not exist")
|
|
505
|
+
|
|
506
|
+
return algo_function_file
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _get_current_json_location(current_json: str) -> None:
|
|
510
|
+
"""Get user input for the current algorithm.json file
|
|
511
|
+
|
|
512
|
+
Parameters
|
|
513
|
+
----------
|
|
514
|
+
current_json : str
|
|
515
|
+
Path to the current algorithm.json file
|
|
516
|
+
"""
|
|
517
|
+
if not current_json:
|
|
518
|
+
default_dir = str(Path(os.getcwd()) / "algorithm_store.json")
|
|
519
|
+
current_json = q.text(
|
|
520
|
+
"Path to the current algorithm.json file:",
|
|
521
|
+
default=default_dir,
|
|
522
|
+
).unsafe_ask()
|
|
523
|
+
|
|
524
|
+
# Convert to absolute path using pathlib
|
|
525
|
+
current_json = str(Path(current_json).resolve())
|
|
526
|
+
|
|
527
|
+
# check if the file exists
|
|
528
|
+
if not Path(current_json).exists():
|
|
529
|
+
raise FileNotFoundError(f"File {current_json} does not exist")
|
|
530
|
+
|
|
531
|
+
return current_json
|
vantage6/cli/algostore/list.py
CHANGED
|
@@ -2,6 +2,7 @@ import click
|
|
|
2
2
|
|
|
3
3
|
from vantage6.common.docker.addons import check_docker_running
|
|
4
4
|
from vantage6.common.globals import InstanceType
|
|
5
|
+
|
|
5
6
|
from vantage6.cli.common.utils import get_server_configuration_list
|
|
6
7
|
|
|
7
8
|
|
|
@@ -12,4 +13,4 @@ def cli_algo_store_configuration_list() -> None:
|
|
|
12
13
|
"""
|
|
13
14
|
check_docker_running()
|
|
14
15
|
|
|
15
|
-
get_server_configuration_list(InstanceType.ALGORITHM_STORE
|
|
16
|
+
get_server_configuration_list(InstanceType.ALGORITHM_STORE)
|
vantage6/cli/algostore/start.py
CHANGED
|
@@ -7,6 +7,8 @@ from vantage6.common.globals import (
|
|
|
7
7
|
InstanceType,
|
|
8
8
|
Ports,
|
|
9
9
|
)
|
|
10
|
+
|
|
11
|
+
from vantage6.cli.common.decorator import click_insert_context
|
|
10
12
|
from vantage6.cli.common.start import (
|
|
11
13
|
attach_logs,
|
|
12
14
|
check_for_start,
|
|
@@ -17,7 +19,6 @@ from vantage6.cli.common.start import (
|
|
|
17
19
|
pull_infra_image,
|
|
18
20
|
)
|
|
19
21
|
from vantage6.cli.context.algorithm_store import AlgorithmStoreContext
|
|
20
|
-
from vantage6.cli.common.decorator import click_insert_context
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
@click.command()
|
|
@@ -27,13 +28,12 @@ from vantage6.cli.common.decorator import click_insert_context
|
|
|
27
28
|
@click.option(
|
|
28
29
|
"--keep/--auto-remove",
|
|
29
30
|
default=False,
|
|
30
|
-
help="Keep image after algorithm store has been stopped. Useful
|
|
31
|
+
help="Keep image after algorithm store has been stopped. Useful for debugging",
|
|
31
32
|
)
|
|
32
33
|
@click.option(
|
|
33
34
|
"--mount-src",
|
|
34
35
|
default="",
|
|
35
|
-
help="Override vantage6 source code in container with the source"
|
|
36
|
-
" code in this path",
|
|
36
|
+
help="Override vantage6 source code in container with the source code in this path",
|
|
37
37
|
)
|
|
38
38
|
@click.option(
|
|
39
39
|
"--attach/--detach",
|
|
@@ -54,7 +54,7 @@ def cli_algo_store_start(
|
|
|
54
54
|
Start the algorithm store server.
|
|
55
55
|
"""
|
|
56
56
|
info("Starting algorithm store...")
|
|
57
|
-
docker_client = check_for_start(ctx, InstanceType.ALGORITHM_STORE
|
|
57
|
+
docker_client = check_for_start(ctx, InstanceType.ALGORITHM_STORE)
|
|
58
58
|
|
|
59
59
|
image = get_image(image, ctx, "algorithm-store", DEFAULT_ALGO_STORE_IMAGE)
|
|
60
60
|
|
|
@@ -84,7 +84,7 @@ def cli_algo_store_start(
|
|
|
84
84
|
info(cmd)
|
|
85
85
|
|
|
86
86
|
info("Run Docker container")
|
|
87
|
-
port_ = str(port or ctx.config["port"] or Ports.DEV_ALGO_STORE
|
|
87
|
+
port_ = str(port or ctx.config["port"] or Ports.DEV_ALGO_STORE)
|
|
88
88
|
container = docker_client.containers.run(
|
|
89
89
|
image,
|
|
90
90
|
command=cmd,
|