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.

Files changed (55) hide show
  1. tests_cli/test_client_script.py +23 -0
  2. tests_cli/test_server_cli.py +7 -6
  3. tests_cli/test_wizard.py +7 -7
  4. vantage6/cli/__build__ +1 -1
  5. vantage6/cli/algorithm/generate_algorithm_json.py +531 -0
  6. vantage6/cli/algostore/list.py +2 -1
  7. vantage6/cli/algostore/start.py +6 -6
  8. vantage6/cli/algostore/stop.py +3 -2
  9. vantage6/cli/cli.py +25 -0
  10. vantage6/cli/common/decorator.py +3 -1
  11. vantage6/cli/common/start.py +221 -12
  12. vantage6/cli/common/stop.py +90 -0
  13. vantage6/cli/common/utils.py +15 -20
  14. vantage6/cli/config.py +260 -0
  15. vantage6/cli/configuration_manager.py +8 -14
  16. vantage6/cli/configuration_wizard.py +66 -111
  17. vantage6/cli/context/__init__.py +2 -1
  18. vantage6/cli/context/algorithm_store.py +10 -7
  19. vantage6/cli/context/node.py +38 -54
  20. vantage6/cli/context/server.py +36 -5
  21. vantage6/cli/dev/create.py +88 -29
  22. vantage6/cli/dev/data/km_dataset.csv +2401 -0
  23. vantage6/cli/dev/remove.py +99 -98
  24. vantage6/cli/globals.py +24 -4
  25. vantage6/cli/node/common/__init__.py +6 -5
  26. vantage6/cli/node/new.py +4 -3
  27. vantage6/cli/node/remove.py +4 -2
  28. vantage6/cli/node/start.py +33 -42
  29. vantage6/cli/prometheus/monitoring_manager.py +146 -0
  30. vantage6/cli/prometheus/prometheus.yml +5 -0
  31. vantage6/cli/server/files.py +4 -2
  32. vantage6/cli/server/import_.py +7 -7
  33. vantage6/cli/server/list.py +2 -1
  34. vantage6/cli/server/new.py +25 -6
  35. vantage6/cli/server/shell.py +5 -4
  36. vantage6/cli/server/start.py +44 -213
  37. vantage6/cli/server/stop.py +36 -105
  38. vantage6/cli/server/version.py +5 -4
  39. vantage6/cli/template/algo_store_config.j2 +0 -1
  40. vantage6/cli/template/node_config.j2 +3 -1
  41. vantage6/cli/template/server_import_config.j2 +0 -2
  42. vantage6/cli/test/algo_test_scripts/algo_test_arguments.py +29 -0
  43. vantage6/cli/test/algo_test_scripts/algo_test_script.py +92 -0
  44. vantage6/cli/test/client_script.py +151 -0
  45. vantage6/cli/test/common/diagnostic_runner.py +2 -2
  46. vantage6/cli/test/feature_tester.py +5 -2
  47. vantage6/cli/use/context.py +46 -0
  48. vantage6/cli/use/namespace.py +55 -0
  49. vantage6/cli/utils.py +70 -4
  50. {vantage6-5.0.0a22.dist-info → vantage6-5.0.0a29.dist-info}/METADATA +15 -11
  51. vantage6-5.0.0a29.dist-info/RECORD +84 -0
  52. {vantage6-5.0.0a22.dist-info → vantage6-5.0.0a29.dist-info}/WHEEL +1 -1
  53. vantage6-5.0.0a22.dist-info/RECORD +0 -72
  54. {vantage6-5.0.0a22.dist-info → vantage6-5.0.0a29.dist-info}/entry_points.txt +0 -0
  55. {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
+ # )
@@ -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.start import cli_server_start
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.value}"
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 patch, MagicMock
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
- 22
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
@@ -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.value)
16
+ get_server_configuration_list(InstanceType.ALGORITHM_STORE)
@@ -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 " "for debugging",
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.value)
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.value)
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,