vantage6 5.0.0a22__py3-none-any.whl → 5.0.0a26__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 (39) hide show
  1. tests_cli/test_client_script.py +23 -0
  2. vantage6/cli/__build__ +1 -1
  3. vantage6/cli/algorithm/generate_algorithm_json.py +529 -0
  4. vantage6/cli/cli.py +25 -0
  5. vantage6/cli/common/start.py +220 -9
  6. vantage6/cli/common/stop.py +90 -0
  7. vantage6/cli/common/utils.py +8 -7
  8. vantage6/cli/config.py +260 -0
  9. vantage6/cli/configuration_manager.py +3 -11
  10. vantage6/cli/configuration_wizard.py +60 -101
  11. vantage6/cli/context/node.py +34 -45
  12. vantage6/cli/context/server.py +26 -0
  13. vantage6/cli/dev/create.py +78 -17
  14. vantage6/cli/dev/data/km_dataset.csv +2401 -0
  15. vantage6/cli/dev/remove.py +99 -98
  16. vantage6/cli/globals.py +20 -0
  17. vantage6/cli/node/new.py +4 -3
  18. vantage6/cli/node/remove.py +4 -2
  19. vantage6/cli/node/start.py +17 -20
  20. vantage6/cli/prometheus/monitoring_manager.py +146 -0
  21. vantage6/cli/prometheus/prometheus.yml +5 -0
  22. vantage6/cli/server/new.py +25 -6
  23. vantage6/cli/server/start.py +42 -212
  24. vantage6/cli/server/stop.py +35 -105
  25. vantage6/cli/template/algo_store_config.j2 +0 -1
  26. vantage6/cli/template/node_config.j2 +1 -1
  27. vantage6/cli/template/server_import_config.j2 +0 -2
  28. vantage6/cli/test/algo_test_scripts/algo_test_arguments.py +29 -0
  29. vantage6/cli/test/algo_test_scripts/algo_test_script.py +91 -0
  30. vantage6/cli/test/client_script.py +151 -0
  31. vantage6/cli/test/common/diagnostic_runner.py +2 -2
  32. vantage6/cli/use/context.py +46 -0
  33. vantage6/cli/use/namespace.py +55 -0
  34. vantage6/cli/utils.py +70 -4
  35. {vantage6-5.0.0a22.dist-info → vantage6-5.0.0a26.dist-info}/METADATA +5 -8
  36. {vantage6-5.0.0a22.dist-info → vantage6-5.0.0a26.dist-info}/RECORD +39 -27
  37. {vantage6-5.0.0a22.dist-info → vantage6-5.0.0a26.dist-info}/WHEEL +0 -0
  38. {vantage6-5.0.0a22.dist-info → vantage6-5.0.0a26.dist-info}/entry_points.txt +0 -0
  39. {vantage6-5.0.0a22.dist-info → vantage6-5.0.0a26.dist-info}/top_level.txt +0 -0
@@ -1,25 +1,27 @@
1
1
  import os
2
2
  from pathlib import Path
3
+ from typing import Any
3
4
 
4
5
  import questionary as q
5
6
 
6
- from vantage6.common import generate_apikey
7
+ from vantage6.common import error, info, warning
8
+ from vantage6.common.client.node_client import NodeClient
9
+ from vantage6.common.context import AppContext
7
10
  from vantage6.common.globals import (
8
11
  DATABASE_TYPES,
12
+ DEFAULT_API_PATH,
9
13
  InstanceType,
10
14
  NodePolicy,
11
15
  Ports,
12
- DEFAULT_API_PATH,
13
16
  RequiredNodeEnvVars,
14
17
  )
15
- from vantage6.common.client.node_client import NodeClient
16
- from vantage6.common.context import AppContext
17
- from vantage6.common import error, warning, info
18
- from vantage6.cli.context import select_context_class
18
+
19
+ from vantage6.cli.config import CliConfig
19
20
  from vantage6.cli.configuration_manager import (
20
21
  NodeConfigurationManager,
21
22
  ServerConfigurationManager,
22
23
  )
24
+ from vantage6.cli.context import select_context_class
23
25
 
24
26
 
25
27
  def node_configuration_questionaire(dirs: dict, instance_name: str) -> dict:
@@ -368,115 +370,69 @@ def _get_common_server_config(instance_type: InstanceType, instance_name: str) -
368
370
  return config
369
371
 
370
372
 
371
- def server_configuration_questionaire(instance_name: str) -> dict:
373
+ def server_configuration_questionaire(
374
+ instance_name: str,
375
+ ) -> dict[str, Any]:
372
376
  """
373
- Questionary to generate a config file for the server instance.
377
+ Kubernetes-specific questionnaire to generate Helm values for server.
374
378
 
375
379
  Parameters
376
380
  ----------
377
381
  instance_name : str
378
- Name of the server instance.
382
+ Name of the server instance
379
383
 
380
384
  Returns
381
385
  -------
382
- dict
383
- Dictionary with the new server configuration
386
+ dict[str, Any]
387
+ dictionary with Helm values for the server configuration
384
388
  """
389
+ # Get active kube namespace
390
+ cli_config = CliConfig()
391
+ kube_namespace = cli_config.get_last_namespace()
385
392
 
386
- config = _get_common_server_config(InstanceType.SERVER, instance_name)
387
-
388
- constant_jwt_secret = q.confirm("Do you want a constant JWT secret?").unsafe_ask()
389
- if constant_jwt_secret:
390
- config["jwt_secret_key"] = generate_apikey()
393
+ # Initialize config with basic structure
394
+ config = {"server": {}, "database": {}, "ui": {}}
391
395
 
392
- is_mfa = q.confirm("Do you want to enforce two-factor authentication?").unsafe_ask()
393
- if is_mfa:
394
- config["two_factor_auth"] = is_mfa
396
+ # === Server settings ===
397
+ config["server"]["description"] = q.text(
398
+ "Enter a human-readable description:",
399
+ default=f"Vantage6 server {instance_name}",
400
+ ).unsafe_ask()
395
401
 
396
- current_server_url = f"http://localhost:{config['port']}{config['api_path']}"
397
- config["server_url"] = q.text(
398
- "What is the server url exposed to the users? If you are running a"
399
- " development server running locally, this is the same as the "
400
- "server url. If you are running a production server, this is the "
401
- "url that users will connect to.",
402
- default=current_server_url,
402
+ config["server"]["image"] = q.text(
403
+ "Server Docker image:",
404
+ default="harbor2.vantage6.ai/infrastructure/server:latest",
403
405
  ).unsafe_ask()
404
406
 
405
- is_add_vpn = q.confirm(
406
- "Do you want to add a VPN server?", default=False
407
+ # === UI settings ===
408
+ config["ui"]["image"] = q.text(
409
+ "UI Docker image:",
410
+ default="harbor2.vantage6.ai/infrastructure/ui:latest",
407
411
  ).unsafe_ask()
408
- if is_add_vpn:
409
- vpn_config = q.unsafe_prompt(
410
- [
411
- {
412
- "type": "text",
413
- "name": "url",
414
- "message": "VPN server URL:",
415
- },
416
- {
417
- "type": "text",
418
- "name": "portal_username",
419
- "message": "VPN portal username:",
420
- },
421
- {
422
- "type": "password",
423
- "name": "portal_userpass",
424
- "message": "VPN portal password:",
425
- },
426
- {
427
- "type": "text",
428
- "name": "client_id",
429
- "message": "VPN client username:",
430
- },
431
- {
432
- "type": "password",
433
- "name": "client_secret",
434
- "message": "VPN client password:",
435
- },
436
- {
437
- "type": "text",
438
- "name": "redirect_url",
439
- "message": "Redirect url (should be local address of server)",
440
- "default": "http://localhost",
441
- },
442
- ]
443
- )
444
- config["vpn_server"] = vpn_config
445
412
 
446
- is_add_rabbitmq = q.confirm(
447
- "Do you want to add a RabbitMQ message queue?"
413
+ # === Database settings ===
414
+ config["database"]["volumePath"] = q.text(
415
+ "Where is your server database located on the host machine?",
416
+ default=f"{Path.cwd()}/dev/.db/db_pv_server",
448
417
  ).unsafe_ask()
449
- if is_add_rabbitmq:
450
- rabbit_uri = q.text(message="Enter the URI for your RabbitMQ:").unsafe_ask()
451
- run_rabbit_locally = q.confirm(
452
- "Do you want to run RabbitMQ locally? (Use only for testing)"
453
- ).unsafe_ask()
454
- config["rabbitmq"] = {
455
- "uri": rabbit_uri,
456
- "start_with_server": run_rabbit_locally,
457
- }
458
-
459
- # add algorithm stores to this server
460
- is_add_community_store = q.confirm(
461
- "Do you want to make the algorithms from the community algorithm store "
462
- "available to your users?"
418
+
419
+ config["database"]["k8sNodeName"] = q.text(
420
+ "What is the name of the k8s node where the databases are running?",
421
+ default="docker-desktop",
463
422
  ).unsafe_ask()
464
- algorithm_stores = []
465
- if is_add_community_store:
466
- algorithm_stores.append(
467
- {"name": "Community store", "url": "https://store.cotopaxi.vantage6.ai"}
468
- )
469
- add_more_stores = q.confirm(
470
- "Do you want to add more algorithm stores?", default=False
423
+
424
+ # === Keycloak settings ===
425
+ keycloak_url = f"http://vantage6-auth-keycloak.{kube_namespace}.svc.cluster.local"
426
+ config["server"]["keycloakUrl"] = keycloak_url
427
+
428
+ # === Other settings ===
429
+ log_level = q.select(
430
+ "Which level of logging would you like?",
431
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
432
+ default="INFO",
471
433
  ).unsafe_ask()
472
- while add_more_stores:
473
- store_name = q.text(message="Enter the name of the store:").unsafe_ask()
474
- store_url = q.text(message="Enter the URL of the store:").unsafe_ask()
475
- algorithm_stores.append({"name": store_name, "url": store_url})
476
- add_more_stores = q.confirm(
477
- "Do you want to add more algorithm stores?", default=False
478
- ).unsafe_ask()
479
- config["algorithm_stores"] = algorithm_stores
434
+
435
+ config["server"]["logging"] = {"level": log_level}
480
436
 
481
437
  return config
482
438
 
@@ -520,7 +476,6 @@ def algo_store_configuration_questionaire(instance_name: str) -> dict:
520
476
  }
521
477
 
522
478
  # ask about openness of the algorithm store
523
- config["policies"] = {"allow_localhost": False}
524
479
  is_open = q.confirm(
525
480
  "Do you want to open the algorithm store to the public? This will allow anyone "
526
481
  "to view the algorithms, but they cannot modify them.",
@@ -530,19 +485,21 @@ def algo_store_configuration_questionaire(instance_name: str) -> dict:
530
485
  open_algos_policy = "public"
531
486
  else:
532
487
  is_open_to_whitelist = q.confirm(
533
- "Do you want to allow all users of whitelisted vantage6 servers to access "
488
+ "Do you want to allow all authenticated users to access "
534
489
  "the algorithms in the store? If not allowing this, you will have to assign"
535
490
  " the appropriate permissions to each user individually.",
536
491
  default=True,
537
492
  ).unsafe_ask()
538
- open_algos_policy = "whitelisted" if is_open_to_whitelist else "private"
493
+ open_algos_policy = "authenticated" if is_open_to_whitelist else "private"
539
494
  config["policies"]["algorithm_view"] = open_algos_policy
540
495
 
541
496
  return config
542
497
 
543
498
 
544
499
  def configuration_wizard(
545
- type_: InstanceType, instance_name: str, system_folders: bool
500
+ type_: InstanceType,
501
+ instance_name: str,
502
+ system_folders: bool,
546
503
  ) -> Path:
547
504
  """
548
505
  Create a configuration file for a node or server instance.
@@ -570,7 +527,9 @@ def configuration_wizard(
570
527
  config = node_configuration_questionaire(dirs, instance_name)
571
528
  elif type_ == InstanceType.SERVER:
572
529
  conf_manager = ServerConfigurationManager
573
- config = server_configuration_questionaire(instance_name)
530
+ config = server_configuration_questionaire(
531
+ instance_name=instance_name,
532
+ )
574
533
  else:
575
534
  conf_manager = ServerConfigurationManager
576
535
  config = algo_store_configuration_questionaire(instance_name)
@@ -16,9 +16,6 @@ class NodeContext(AppContext):
16
16
  """
17
17
  Node context object for the host system.
18
18
 
19
- See DockerNodeContext for the node instance mounts when running as a
20
- dockerized service.
21
-
22
19
  Parameters
23
20
  ----------
24
21
  instance_name : str
@@ -188,48 +185,6 @@ class NodeContext(AppContext):
188
185
  """
189
186
  return os.environ.get("DATA_VOLUME_NAME", f"{self.docker_container_name}-vol")
190
187
 
191
- @property
192
- def docker_vpn_volume_name(self) -> str:
193
- """
194
- Docker volume in which the VPN configuration is stored.
195
-
196
- Returns
197
- -------
198
- str
199
- Docker volume name
200
- """
201
- return os.environ.get(
202
- "VPN_VOLUME_NAME", f"{self.docker_container_name}-vpn-vol"
203
- )
204
-
205
- @property
206
- def docker_ssh_volume_name(self) -> str:
207
- """
208
- Docker volume in which the SSH configuration is stored.
209
-
210
- Returns
211
- -------
212
- str
213
- Docker volume name
214
- """
215
- return os.environ.get(
216
- "SSH_TUNNEL_VOLUME_NAME", f"{self.docker_container_name}-ssh-vol"
217
- )
218
-
219
- @property
220
- def docker_squid_volume_name(self) -> str:
221
- """
222
- Docker volume in which the SSH configuration is stored.
223
-
224
- Returns
225
- -------
226
- str
227
- Docker volume name
228
- """
229
- return os.environ.get(
230
- "SSH_SQUID_VOLUME_NAME", f"{self.docker_container_name}-squid-vol"
231
- )
232
-
233
188
  @property
234
189
  def proxy_log_file(self):
235
190
  return self.log_file_name(type_="proxy_server")
@@ -250,6 +205,40 @@ class NodeContext(AppContext):
250
205
  """
251
206
  return self.config["databases"][label]
252
207
 
208
+ def set_folders(
209
+ self, instance_type: InstanceType, instance_name: str, system_folders: bool
210
+ ) -> None:
211
+ """
212
+ Set the folders for the node.
213
+
214
+ Parameters
215
+ ----------
216
+ instance_type : InstanceType
217
+ Instance type
218
+ instance_name : str
219
+ Instance name
220
+ system_folders : bool
221
+ Whether to use system folders
222
+ """
223
+ dirs = self.instance_folders(instance_type, instance_name, system_folders)
224
+
225
+ self.log_dir = dirs.get("log")
226
+ self.data_dir = dirs.get("data")
227
+ self.config_dir = dirs.get("config")
228
+
229
+ @staticmethod
230
+ def instance_folders(*_args, **_kwargs) -> dict:
231
+ """Log, data and config folders are always mounted. The node manager
232
+ should take care of this.
233
+ """
234
+ mnt = Path("/mnt")
235
+
236
+ return {
237
+ "log": mnt / "log",
238
+ "data": mnt / "data",
239
+ "config": mnt / "config",
240
+ }
241
+
253
242
  def __create_node_identifier(self) -> str:
254
243
  """
255
244
  Create a unique identifier for the node.
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
+ from pathlib import Path
2
3
 
3
4
  from vantage6.common.globals import APPNAME, InstanceType
4
5
  from vantage6.cli.configuration_manager import ServerConfigurationManager
5
6
  from vantage6.cli.globals import (
6
7
  DEFAULT_SERVER_SYSTEM_FOLDERS as S_FOL,
8
+ PROMETHEUS_DIR,
7
9
  ServerType,
8
10
  ServerGlobals,
9
11
  )
@@ -57,6 +59,30 @@ class ServerContext(BaseServerContext):
57
59
  """
58
60
  return f"{APPNAME}-{self.name}-{self.scope}-{ServerType.V6SERVER}"
59
61
 
62
+ @property
63
+ def prometheus_container_name(self) -> str:
64
+ """
65
+ Get the name of the Prometheus Docker container for this server.
66
+
67
+ Returns
68
+ -------
69
+ str
70
+ Prometheus container name, unique to this server instance.
71
+ """
72
+ return f"{APPNAME}-{self.name}-{self.scope}-prometheus"
73
+
74
+ @property
75
+ def prometheus_dir(self) -> Path:
76
+ """
77
+ Get the Prometheus directory path.
78
+
79
+ Returns
80
+ -------
81
+ Path
82
+ Path to the Prometheus directory
83
+ """
84
+ return self.data_dir / PROMETHEUS_DIR
85
+
60
86
  @classmethod
61
87
  def from_external_config_file(
62
88
  cls, path: str, system_folders: bool = S_FOL
@@ -12,7 +12,7 @@ from vantage6.common import ensure_config_dir_writable, info, error, generate_ap
12
12
 
13
13
  import vantage6.cli.dev.data as data_dir
14
14
  from vantage6.cli.context.algorithm_store import AlgorithmStoreContext
15
- from vantage6.cli.globals import PACKAGE_FOLDER
15
+ from vantage6.cli.globals import PACKAGE_FOLDER, DefaultDatasets
16
16
  from vantage6.cli.context.server import ServerContext
17
17
  from vantage6.cli.context.node import NodeContext
18
18
  from vantage6.cli.server.common import get_server_context
@@ -20,7 +20,9 @@ from vantage6.cli.server.import_ import cli_server_import
20
20
  from vantage6.cli.utils import prompt_config_name
21
21
 
22
22
 
23
- def create_node_data_files(num_nodes: int, server_name: str) -> list[Path]:
23
+ def create_node_data_files(
24
+ num_nodes: int, server_name: str, dataset: tuple[str, Path]
25
+ ) -> list[tuple[str, Path]]:
24
26
  """Create data files for nodes.
25
27
 
26
28
  Parameters
@@ -29,15 +31,16 @@ def create_node_data_files(num_nodes: int, server_name: str) -> list[Path]:
29
31
  Number of nodes to create data files for.
30
32
  server_name : str
31
33
  Name of the server.
32
-
34
+ dataset : tuple[str, Path]
35
+ Tuple containing the name and the path to the dataset.
33
36
  Returns
34
37
  -------
35
- list[Path]
36
- List of paths to the created data files.
38
+ list[tuple[str, Path]]
39
+ List of the label and paths to the created data files.
37
40
  """
38
41
  info(f"Creating data files for {num_nodes} nodes.")
39
42
  data_files = []
40
- full_df = pd.read_csv(impresources.files(data_dir) / "olympic_athletes_2016.csv")
43
+ full_df = pd.read_csv(dataset[1])
41
44
  length_df = len(full_df)
42
45
  for i in range(num_nodes):
43
46
  node_name = f"{server_name}_node_{i + 1}"
@@ -49,16 +52,20 @@ def create_node_data_files(num_nodes: int, server_name: str) -> list[Path]:
49
52
  start = i * length_df // num_nodes
50
53
  end = (i + 1) * length_df // num_nodes
51
54
  data = full_df[start:end]
52
- data_file = data_folder / f"df_{node_name}.csv"
55
+ data_file = data_folder / f"df_{dataset[0]}_{node_name}.csv"
53
56
 
54
57
  # write data to file
55
58
  data.to_csv(data_file, index=False)
56
- data_files.append(data_file)
59
+ data_files.append((dataset[0], data_file))
57
60
  return data_files
58
61
 
59
62
 
60
63
  def create_node_config_file(
61
- server_url: str, port: int, config: dict, server_name: str, datafile: Path
64
+ server_url: str,
65
+ port: int,
66
+ config: dict,
67
+ server_name: str,
68
+ datasets: list[tuple[str, Path]] = (),
62
69
  ) -> None:
63
70
  """Create a node configuration file (YAML).
64
71
 
@@ -77,8 +84,8 @@ def create_node_config_file(
77
84
  additional user_defined_config.
78
85
  server_name : str
79
86
  Configuration name of the dummy server.
80
- datafile : Path
81
- Path to the data file for the node to use.
87
+ datasets : list[tuple[str, Path]]
88
+ List of tuples containing the labels and the paths to the datasets
82
89
  """
83
90
  environment = Environment(
84
91
  loader=FileSystemLoader(PACKAGE_FOLDER / APPNAME / "cli" / "template"),
@@ -102,10 +109,12 @@ def create_node_config_file(
102
109
  error(f"Node configuration file already exists: {full_path}")
103
110
  exit(1)
104
111
 
112
+ databases = [{dataset[0]: dataset[1]} for dataset in datasets]
113
+
105
114
  node_config = template.render(
106
115
  {
107
116
  "api_key": config["api_key"],
108
- "databases": {"default": datafile},
117
+ "databases": databases,
109
118
  "logging": {"file": f"{node_name}.log"},
110
119
  "port": port,
111
120
  "server_url": server_url,
@@ -124,8 +133,7 @@ def create_node_config_file(
124
133
  f.write(node_config)
125
134
 
126
135
  info(
127
- f"Spawned node for organization {Fore.GREEN}{config['org_id']}"
128
- f"{Style.RESET_ALL}"
136
+ f"Spawned node for organization {Fore.GREEN}{config['org_id']}{Style.RESET_ALL}"
129
137
  )
130
138
 
131
139
 
@@ -156,6 +164,7 @@ def generate_node_configs(
156
164
  port: int,
157
165
  server_name: str,
158
166
  extra_node_config: Path | None,
167
+ extra_datasets: list[tuple[str, Path]],
159
168
  ) -> list[dict]:
160
169
  """Generates ``num_nodes`` node configuration files.
161
170
 
@@ -171,6 +180,8 @@ def generate_node_configs(
171
180
  Configuration name of the dummy server.
172
181
  extra_node_config : Path | None
173
182
  Path to file with additional node configuration.
183
+ extra_datasets : list[tuple[str, Path]]
184
+ List of tuples containing the labels and the paths to extra datasets
174
185
 
175
186
  Returns
176
187
  -------
@@ -178,8 +189,36 @@ def generate_node_configs(
178
189
  List of dictionaries containing node configurations.
179
190
  """
180
191
  configs = []
192
+ node_data_files = []
181
193
  extra_config = _read_extra_config_file(extra_node_config)
182
- node_data_files = create_node_data_files(num_nodes, server_name)
194
+
195
+ data_directory = impresources.files(data_dir)
196
+
197
+ # Add default datasets to the list of dataset provided
198
+ for default_dataset in DefaultDatasets:
199
+ extra_datasets.append(
200
+ (default_dataset.name.lower(), data_directory / default_dataset.value)
201
+ )
202
+
203
+ # Check for duplicate dataset labels
204
+ seen_labels = set()
205
+ duplicates = [
206
+ label
207
+ for label in [dataset[0] for dataset in extra_datasets]
208
+ if (label in seen_labels or seen_labels.add(label))
209
+ ]
210
+
211
+ if len(duplicates) > 0:
212
+ error(
213
+ f"Duplicate dataset labels found: {duplicates}. "
214
+ f"Please make sure all dataset labels are unique."
215
+ )
216
+ exit(1)
217
+
218
+ # create the data files for the nodes and get the path and label for each dataset
219
+ for dataset in extra_datasets:
220
+ node_data_files.append(create_node_data_files(num_nodes, server_name, dataset))
221
+
183
222
  for i in range(num_nodes):
184
223
  config = {
185
224
  "org_id": i + 1,
@@ -188,7 +227,11 @@ def generate_node_configs(
188
227
  "user_defined_config": extra_config,
189
228
  }
190
229
  create_node_config_file(
191
- server_url, port, config, server_name, node_data_files[i]
230
+ server_url,
231
+ port,
232
+ config,
233
+ server_name,
234
+ [files[i] for files in node_data_files],
192
235
  )
193
236
  configs.append(config)
194
237
 
@@ -421,6 +464,7 @@ def demo_network(
421
464
  ui_image: str,
422
465
  ui_port: int,
423
466
  algorithm_store_port: int,
467
+ extra_datasets: list[tuple[str, Path]],
424
468
  ) -> tuple[list[dict], Path, Path]:
425
469
  """Generates the demo network.
426
470
 
@@ -447,6 +491,8 @@ def demo_network(
447
491
  Port to run the UI on.
448
492
  algorithm_store_port : int
449
493
  Port to run the algorithm store on.
494
+ extra_datasets : list[tuple[str, Path]]
495
+ List of tuples containing the labels and the paths to extra datasets
450
496
 
451
497
  Returns
452
498
  -------
@@ -454,7 +500,12 @@ def demo_network(
454
500
  Tuple containing node, server import and server configurations.
455
501
  """
456
502
  node_configs = generate_node_configs(
457
- num_nodes, server_url, server_port, server_name, extra_node_config
503
+ num_nodes,
504
+ server_url,
505
+ server_port,
506
+ server_name,
507
+ extra_node_config,
508
+ extra_datasets,
458
509
  )
459
510
  server_import_config = create_vserver_import_config(node_configs, server_name)
460
511
  server_config = create_vserver_config(
@@ -548,6 +599,14 @@ def demo_network(
548
599
  help="YAML File with additional algorithm store configuration. This will be"
549
600
  " appended to the algorithm store configuration file",
550
601
  )
602
+ @click.option(
603
+ "--add-dataset",
604
+ type=(str, click.Path()),
605
+ default=(),
606
+ multiple=True,
607
+ help="Add a dataset to the nodes. The first argument is the label of the database, "
608
+ "the second is the path to the dataset file.",
609
+ )
551
610
  @click.pass_context
552
611
  def create_demo_network(
553
612
  click_ctx: click.Context,
@@ -562,6 +621,7 @@ def create_demo_network(
562
621
  extra_server_config: Path = None,
563
622
  extra_node_config: Path = None,
564
623
  extra_store_config: Path = None,
624
+ add_dataset: list[tuple[str, Path]] = (),
565
625
  ) -> dict:
566
626
  """Creates a demo network.
567
627
 
@@ -583,6 +643,7 @@ def create_demo_network(
583
643
  ui_image,
584
644
  ui_port,
585
645
  algorithm_store_port,
646
+ list(add_dataset),
586
647
  )
587
648
  info(
588
649
  f"Created {Fore.GREEN}{len(demo[0])}{Style.RESET_ALL} node "