vantage6 4.10.2__py3-none-any.whl → 4.11.0__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.

vantage6/cli/globals.py CHANGED
@@ -42,6 +42,18 @@ DIAGNOSTICS_IMAGE = "harbor2.vantage6.ai/algorithms/diagnostic"
42
42
  # Address of community algorithm store
43
43
  COMMUNITY_STORE = "https://store.cotopaxi.vantage6.ai/api"
44
44
 
45
+ DEFAULT_PROMETHEUS_IMAGE = "prom/prometheus"
46
+ PROMETHEUS_CONFIG = "prometheus.yml"
47
+ PROMETHEUS_DIR = "prometheus"
48
+
49
+
50
+ # datasets included in the nodes of the dev network
51
+ class DefaultDatasets(str, Enum):
52
+ """Enum containing default datasets"""
53
+
54
+ OLYMPIC_ATHLETES = "olympic_athletes_2016.csv"
55
+ KAPLAN_MEIER_TEST = "km_dataset.csv"
56
+
45
57
 
46
58
  class ServerType(str, Enum):
47
59
  """Enum containing server types"""
@@ -0,0 +1,146 @@
1
+ import yaml
2
+ import docker
3
+ from pathlib import Path
4
+
5
+ from vantage6.common import info, error
6
+ from vantage6.common.docker.network_manager import NetworkManager
7
+ from vantage6.common.globals import DEFAULT_PROMETHEUS_EXPORTER_PORT
8
+ from vantage6.cli.context.server import ServerContext
9
+ from vantage6.cli.globals import (
10
+ DEFAULT_PROMETHEUS_IMAGE,
11
+ PROMETHEUS_CONFIG,
12
+ )
13
+
14
+
15
+ class PrometheusServer:
16
+ """
17
+ Manages the Prometheus Docker container
18
+ """
19
+
20
+ def __init__(
21
+ self, ctx: ServerContext, network_mgr: NetworkManager, image: str = None
22
+ ):
23
+ """
24
+ Initialize the PrometheusServer instance.
25
+
26
+ Parameters
27
+ ----------
28
+ ctx : ServerContext
29
+ The server context containing configuration and paths.
30
+ network_mgr : NetworkManager
31
+ The network manager responsible for managing Docker networks.
32
+ image : str, optional
33
+ The Docker image to use for the Prometheus container. If not provided,
34
+ the default Prometheus image will be used.
35
+ """
36
+ self.ctx = ctx
37
+ self.network_mgr = network_mgr
38
+ self.docker = docker.from_env()
39
+ self.image = image if image else DEFAULT_PROMETHEUS_IMAGE
40
+ self.config_file = Path(__file__).parent / PROMETHEUS_CONFIG
41
+ self.data_dir = self.ctx.prometheus_dir
42
+
43
+ def start(self):
44
+ """
45
+ Start a Docker container running Prometheus
46
+ """
47
+ self._prepare_config()
48
+
49
+ volumes = {
50
+ str(self.config_file): {
51
+ "bind": "/etc/prometheus/prometheus.yml",
52
+ "mode": "ro",
53
+ },
54
+ str(self.data_dir): {"bind": "/prometheus", "mode": "rw"},
55
+ }
56
+ ports = {"9090/tcp": 9090}
57
+
58
+ if self._is_container_running():
59
+ info("Prometheus is already running!")
60
+ return
61
+
62
+ self.docker.containers.run(
63
+ name=self.ctx.prometheus_container_name,
64
+ image=self.image,
65
+ volumes=volumes,
66
+ ports=ports,
67
+ detach=True,
68
+ restart_policy={"Name": "unless-stopped"},
69
+ network=self.network_mgr.network_name,
70
+ )
71
+ info("Prometheus container started successfully!")
72
+
73
+ def _prepare_config(self):
74
+ """
75
+ Prepare the Prometheus configuration and data directories
76
+ """
77
+ if not self.config_file.exists():
78
+ error(f"Prometheus configuration file {self.config_file} not found!")
79
+ raise FileNotFoundError(f"{self.config_file} not found!")
80
+
81
+ if not self.data_dir.exists():
82
+ self.data_dir.mkdir(parents=True, exist_ok=True)
83
+
84
+ self._update_prometheus_config()
85
+
86
+ def _update_prometheus_config(self):
87
+ """
88
+ Update the Prometheus configuration file with the server address.
89
+ """
90
+
91
+ try:
92
+ prometheus_exporter_port = self.ctx.config.get("prometheus", {}).get(
93
+ "exporter_port", DEFAULT_PROMETHEUS_EXPORTER_PORT
94
+ )
95
+ server_hostname = self.ctx.prometheus_container_name
96
+ server_address = f"{server_hostname}:{prometheus_exporter_port}"
97
+
98
+ info(
99
+ f"Using Docker container hostname '{server_hostname}' for Prometheus "
100
+ "target. Ensure Prometheus is in the same Docker network to resolve "
101
+ "this address."
102
+ )
103
+
104
+ with open(self.config_file, "r") as f:
105
+ config = yaml.safe_load(f)
106
+
107
+ job_name = "vantage6_server_metrics"
108
+ job_exists = any(
109
+ job.get("job_name") == job_name
110
+ for job in config.get("scrape_configs", [])
111
+ )
112
+
113
+ if not job_exists:
114
+ new_job = {
115
+ "job_name": job_name,
116
+ "static_configs": [{"targets": [server_address]}],
117
+ }
118
+ config.setdefault("scrape_configs", []).append(new_job)
119
+ else:
120
+ for job in config["scrape_configs"]:
121
+ if job.get("job_name") == job_name:
122
+ job["static_configs"] = [{"targets": [server_address]}]
123
+
124
+ with open(self.config_file, "w") as f:
125
+ yaml.dump(config, f)
126
+
127
+ info(f"Prometheus configuration updated with target: {server_address}")
128
+
129
+ except Exception as e:
130
+ error(f"Failed to update Prometheus configuration: {e}")
131
+ raise
132
+
133
+ def _is_container_running(self) -> bool:
134
+ """
135
+ Check if a Prometheus container is already running.
136
+
137
+ Returns
138
+ -------
139
+ bool
140
+ True if the Prometheus container is running, False otherwise.
141
+ """
142
+ try:
143
+ container = self.docker.containers.get(self.ctx.prometheus_container_name)
144
+ return container.status == "running"
145
+ except docker.errors.NotFound:
146
+ return False
@@ -0,0 +1,5 @@
1
+ global:
2
+ # How frequently to scrape targets by default.
3
+ scrape_interval: 1m
4
+
5
+ scrape_configs: []
@@ -11,11 +11,12 @@ from vantage6.common.globals import (
11
11
  InstanceType,
12
12
  )
13
13
 
14
- from vantage6.common.globals import Ports
14
+ from vantage6.common.globals import Ports, DEFAULT_PROMETHEUS_EXPORTER_PORT
15
15
  from vantage6.cli.context.server import ServerContext
16
16
  from vantage6.cli.rabbitmq.queue_manager import RabbitMQManager
17
17
  from vantage6.cli.server.common import stop_ui
18
18
  from vantage6.cli.common.decorator import click_insert_context
19
+ from vantage6.cli.prometheus.monitoring_manager import PrometheusServer
19
20
  from vantage6.cli.common.start import (
20
21
  attach_logs,
21
22
  check_for_start,
@@ -49,6 +50,14 @@ from vantage6.cli.common.start import (
49
50
  "container - use in development only",
50
51
  )
51
52
  @click.option("--rabbitmq-image", default=None, help="RabbitMQ docker image to use")
53
+ @click.option(
54
+ "--with-prometheus",
55
+ "start_prometheus",
56
+ flag_value=True,
57
+ default=False,
58
+ help="Start Prometheus monitoring as a local container",
59
+ )
60
+ @click.option("--prometheus-image", default=None, help="Prometheus docker image to use")
52
61
  @click.option(
53
62
  "--keep/--auto-remove",
54
63
  default=False,
@@ -75,6 +84,8 @@ def cli_server_start(
75
84
  ui_port: int,
76
85
  start_rabbitmq: bool,
77
86
  rabbitmq_image: str,
87
+ start_prometheus: bool,
88
+ prometheus_image: str,
78
89
  keep: bool,
79
90
  mount_src: str,
80
91
  attach: bool,
@@ -135,6 +146,24 @@ def cli_server_start(
135
146
  "cannot be scaled horizontally!"
136
147
  )
137
148
 
149
+ if (
150
+ start_prometheus
151
+ or ctx.config.get("prometheus")
152
+ and ctx.config["prometheus"].get("start_with_server", False)
153
+ ):
154
+ info("Starting Prometheus container")
155
+ _start_prometheus(ctx, prometheus_image, server_network_mgr)
156
+ elif ctx.config.get("prometheus"):
157
+ info(
158
+ "Prometheus is provided in the config file as external service. "
159
+ "Assuming this service is up and running."
160
+ )
161
+ else:
162
+ warning(
163
+ "Monitoring is not set up! This means that the vantage6 server "
164
+ "cannot be monitored with Prometheus!"
165
+ )
166
+
138
167
  # start the UI if requested
139
168
  if start_ui or ctx.config.get("ui") and ctx.config["ui"].get("enabled"):
140
169
  _start_ui(docker_client, ctx, ui_port)
@@ -152,6 +181,9 @@ def cli_server_start(
152
181
 
153
182
  info("Run Docker container")
154
183
  port_ = str(port or ctx.config["port"] or Ports.DEV_SERVER.value)
184
+ prometheus_exporter_port = ctx.config.get("prometheus", {}).get(
185
+ "exporter_port", DEFAULT_PROMETHEUS_EXPORTER_PORT
186
+ )
155
187
  container = docker_client.containers.run(
156
188
  image,
157
189
  command=cmd,
@@ -162,7 +194,10 @@ def cli_server_start(
162
194
  "name": ctx.config_file_name,
163
195
  },
164
196
  environment=environment_vars,
165
- ports={f"{internal_port}/tcp": (ip, port_)},
197
+ ports={
198
+ f"{internal_port}/tcp": (ip, port_), # API port
199
+ f"{prometheus_exporter_port}/tcp": prometheus_exporter_port,
200
+ },
166
201
  name=ctx.docker_container_name,
167
202
  auto_remove=not keep,
168
203
  tty=True,
@@ -202,6 +237,27 @@ def _start_rabbitmq(
202
237
  rabbit_mgr.start()
203
238
 
204
239
 
240
+ def _start_prometheus(
241
+ ctx: ServerContext, prometheus_image: str, network_mgr: NetworkManager
242
+ ) -> None:
243
+ """
244
+ Start the Prometheus container if it is not already running.
245
+
246
+ Parameters
247
+ ----------
248
+ ctx : ServerContext
249
+ Server context object
250
+ prometheus_image : str
251
+ Prometheus image to use
252
+ network_mgr : NetworkManager
253
+ Network manager object
254
+ """
255
+ prometheus_server = PrometheusServer(
256
+ ctx=ctx, network_mgr=network_mgr, image=prometheus_image
257
+ )
258
+ prometheus_server.start()
259
+
260
+
205
261
  def _start_ui(client: DockerClient, ctx: ServerContext, ui_port: int) -> None:
206
262
  """
207
263
  Start the UI container.
@@ -117,3 +117,10 @@ def _stop_server_containers(
117
117
  f"Stopped the {Fore.GREEN}{rabbit_container_name}"
118
118
  f"{Style.RESET_ALL} container."
119
119
  )
120
+
121
+ if ctx.config.get("prometheus", {}).get("start_with_server"):
122
+ remove_container_if_exists(client, name=ctx.prometheus_container_name)
123
+ info(
124
+ f"Stopped the {Fore.GREEN}{ctx.prometheus_container_name}"
125
+ f"{Style.RESET_ALL} container."
126
+ )
@@ -1,11 +1,13 @@
1
1
  api_key: {{ api_key }}
2
2
  api_path: /api
3
3
  databases:
4
- {% for label, path in databases.items() %}
4
+ {% for db in databases %}
5
+ {% for label, path in db.items() %}
5
6
  - label: {{ label }}
6
7
  uri: {{ path }}
7
8
  type: csv
8
9
  {% endfor %}
10
+ {% endfor %}
9
11
  encryption:
10
12
  enabled: false
11
13
  private_key: null
@@ -31,4 +33,4 @@ logging:
31
33
  port: {{ port }}
32
34
  server_url: {{ server_url }}
33
35
  task_dir: {{ task_dir}}
34
- {{ user_provided_config }}
36
+ {{- user_provided_config -}}
@@ -0,0 +1,33 @@
1
+ average = {
2
+ "collaboration": 1,
3
+ "organizations": [1],
4
+ "name": "test_average_task",
5
+ "image": "harbor2.vantage6.ai/demo/average",
6
+ "description": "",
7
+ "input_": {
8
+ "method": "central_average",
9
+ "args": [],
10
+ "kwargs": {"column_name": "Age"},
11
+ },
12
+ "databases": [{"label": "olympic_athletes"}],
13
+ }
14
+
15
+ kaplan_meier = {
16
+ "collaboration": 1,
17
+ "organizations": [1],
18
+ "name": "test_average_task",
19
+ "image": "harbor2.vantage6.ai/algorithms/kaplan-meier",
20
+ "description": "",
21
+ "input_": {
22
+ "method": "kaplan_meier_central",
23
+ "args": [],
24
+ "kwargs": {
25
+ "time_column_name": "days",
26
+ "censor_column_name": "censor",
27
+ "organizations_to_include": [1, 2, 3],
28
+ },
29
+ },
30
+ "databases": [{"label": "kaplan_meier_test"}],
31
+ }
32
+
33
+ args = {"average": average, "kaplan_meier": kaplan_meier}
@@ -0,0 +1,88 @@
1
+ import logging
2
+ import algo_test_arguments as arguments
3
+ import json
4
+ import sys
5
+
6
+ import vantage6.common.task_status as task_status
7
+
8
+ from vantage6.client import Client
9
+ from vantage6.common import error
10
+ from vantage6.common.globals import Ports
11
+
12
+
13
+ def create_and_run_task(client: Client, task_args: dict, algo_name: str = "algorithm"):
14
+ """
15
+ Create and run a task using the provided client and task arguments.
16
+
17
+ Parameters
18
+ ----------
19
+ client: Client
20
+ The client instance to use for creating and running the task.
21
+ task_args: dict
22
+ The arguments to pass to the task creation method.
23
+ algo_name: str, optional
24
+ The name of the algorithm for logging purposes. Default is "algorithm".
25
+
26
+ Raises
27
+ ------
28
+ AssertionError: If the task fails.
29
+ """
30
+ task = client.task.create(**task_args)
31
+ task_id = task["id"]
32
+ client.wait_for_results(task_id)
33
+
34
+ try:
35
+ # check if the task has failed
36
+ assert not task_status.has_task_failed(client.task.get(task_id)["status"])
37
+
38
+ logging.info(f"Task for {algo_name} completed successfully.")
39
+
40
+ except AssertionError:
41
+ error(
42
+ f"Task for {algo_name} failed. Check the log file of the task "
43
+ f"{task_id} for more information."
44
+ )
45
+ exit(1)
46
+
47
+
48
+ def run_test(custom_args: dict | None = None):
49
+ """
50
+ Run a test by creating and running tasks using the provided arguments.
51
+
52
+ Parameters
53
+ ----------
54
+ custom_args: dict, optional
55
+ The arguments to pass to the task creation method. If not provided,
56
+ the arguments from the `arguments` module will be used.
57
+ """
58
+ # Create a client and authenticate
59
+ client = Client("http://localhost", Ports.DEV_SERVER.value, "/api")
60
+ try:
61
+ client.authenticate("dev_admin", "password")
62
+ except ConnectionError:
63
+ error(
64
+ "Could not connect to the server. Please check if a dev network is running."
65
+ )
66
+ exit(1)
67
+
68
+ # if custom arguments are provided, use them for running the task
69
+ if custom_args:
70
+ create_and_run_task(client, custom_args)
71
+
72
+ else:
73
+ # Run the task for each algorithm in the arguments file
74
+ for algo, task_args in arguments.args.items():
75
+ logging.info("Running task for %s", algo)
76
+ logging.info("Task arguments: %s", task_args)
77
+ create_and_run_task(client, task_args, algo)
78
+
79
+
80
+ if __name__ == "__main__":
81
+ # check if arguments are provided
82
+ if len(sys.argv) > 1:
83
+ input_string = sys.argv[1].replace("'", '"')
84
+ json_input = json.loads(input_string)
85
+ else:
86
+ json_input = None
87
+
88
+ run_test(json_input)
@@ -0,0 +1,150 @@
1
+ import click
2
+ import json
3
+ import subprocess
4
+ import sys
5
+
6
+ from pathlib import Path
7
+
8
+ from rich.console import Console
9
+
10
+ from vantage6.cli.dev.create import create_demo_network
11
+ from vantage6.cli.dev.remove import remove_demo_network
12
+ from vantage6.cli.dev.start import start_demo_network
13
+ from vantage6.cli.dev.stop import stop_demo_network
14
+ from vantage6.cli.utils import prompt_config_name
15
+ from vantage6.common.globals import Ports
16
+
17
+ TEST_FILE_PATH = Path(__file__).parent / "algo_test_scripts" / "algo_test_script.py"
18
+
19
+
20
+ @click.command()
21
+ @click.option(
22
+ "--script",
23
+ type=click.Path(),
24
+ default=TEST_FILE_PATH,
25
+ help="Path of the script to test the algorithm. If a script is not provided, the default script is used.",
26
+ )
27
+ @click.option(
28
+ "--task-arguments",
29
+ type=str,
30
+ default=None,
31
+ help="Arguments to be provided to Task.create function. If --script is provided, this should not be set.",
32
+ )
33
+ @click.option(
34
+ "--create-dev-network",
35
+ is_flag=True,
36
+ help="Create a new dev network to run the test",
37
+ )
38
+ @click.option(
39
+ "--start-dev-network",
40
+ is_flag=True,
41
+ help="Start a dev network to run the test",
42
+ )
43
+ @click.option(
44
+ "-n", "--name", default=None, type=str, help="Name for your development setup"
45
+ )
46
+ @click.option(
47
+ "--server-url",
48
+ type=str,
49
+ default="http://host.docker.internal",
50
+ help="Server URL to point to. If you are using Docker Desktop, "
51
+ "the default http://host.docker.internal should not be changed.",
52
+ )
53
+ @click.option(
54
+ "-i", "--image", type=str, default=None, help="Server Docker image to use"
55
+ )
56
+ @click.option(
57
+ "--keep",
58
+ type=bool,
59
+ default=False,
60
+ help="Keep the dev network after finishing the test",
61
+ )
62
+ @click.option(
63
+ "--add-dataset",
64
+ type=(str, click.Path()),
65
+ default=[],
66
+ multiple=True,
67
+ help="Add a dataset to the nodes. The first argument is the label of the database, "
68
+ "the second is the path to the dataset file.",
69
+ )
70
+ @click.pass_context
71
+ def cli_test_client_script(
72
+ click_ctx: click.Context,
73
+ script: Path | None,
74
+ task_arguments: str | None,
75
+ name: str,
76
+ server_url: str,
77
+ create_dev_network: bool,
78
+ start_dev_network: bool,
79
+ image: str,
80
+ keep: bool,
81
+ add_dataset: list[tuple[str, Path]] = (),
82
+ ) -> int:
83
+ """
84
+ Run a script for testing an algorithm on a dev network.
85
+ The path to the script must be provided as an argument.
86
+ """
87
+ if not (script or task_arguments):
88
+ raise click.UsageError("--script or --task-arguments must be set.")
89
+ elif script != TEST_FILE_PATH and task_arguments:
90
+ raise click.UsageError("--script and --task-arguments cannot be set together.")
91
+
92
+ # Check if the task_arguments is a valid JSON string
93
+ if task_arguments:
94
+ try:
95
+ json.loads(task_arguments.replace("'", '"'))
96
+ except json.JSONDecodeError:
97
+ raise click.UsageError("task-arguments must be a valid JSON string.")
98
+
99
+ name = prompt_config_name(name)
100
+
101
+ # create the network
102
+ if create_dev_network:
103
+ click_ctx.invoke(
104
+ create_demo_network,
105
+ name=name,
106
+ num_nodes=3,
107
+ server_url=server_url,
108
+ server_port=Ports.DEV_SERVER.value,
109
+ image=image,
110
+ extra_server_config=None,
111
+ extra_node_config=None,
112
+ add_dataset=add_dataset,
113
+ )
114
+
115
+ # start the server and nodes
116
+ if create_dev_network or start_dev_network:
117
+ click_ctx.invoke(
118
+ start_demo_network,
119
+ name=name,
120
+ server_image=image,
121
+ node_image=image,
122
+ )
123
+
124
+ # run the test script and get the result
125
+ if not task_arguments:
126
+ subprocess_args = ["python", script]
127
+ else:
128
+ subprocess_args = ["python", script, task_arguments]
129
+
130
+ result = subprocess.run(subprocess_args, stdout=sys.stdout, stderr=sys.stderr)
131
+
132
+ # check the exit code. If the test passed, it should be 0
133
+ if result.returncode == 0:
134
+ msg = ":heavy_check_mark: [green]Test passed[/green]"
135
+ else:
136
+ msg = ":x: [red]Test failed[/red]"
137
+
138
+ console = Console()
139
+ console.print(msg)
140
+
141
+ # clean up the test resources. Keep the network if --keep is set, or if the network
142
+ # was created for this test. If the network was started for this test, stop it but
143
+ # do not remove it.
144
+ if not keep:
145
+ if create_dev_network or start_dev_network:
146
+ click_ctx.invoke(stop_demo_network, name=name)
147
+ if create_dev_network:
148
+ click_ctx.invoke(remove_demo_network, name=name)
149
+
150
+ return result.returncode
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vantage6
3
- Version: 4.10.2
3
+ Version: 4.11.0
4
4
  Summary: vantage6 command line interface
5
5
  Home-page: https://github.com/vantage6/vantage6
6
6
  Requires-Python: >=3.10
@@ -16,8 +16,8 @@ Requires-Dist: questionary==1.10.0
16
16
  Requires-Dist: rich==13.5.2
17
17
  Requires-Dist: schema==0.7.5
18
18
  Requires-Dist: SQLAlchemy==1.4.46
19
- Requires-Dist: vantage6-common==4.10.2
20
- Requires-Dist: vantage6-client==4.10.2
19
+ Requires-Dist: vantage6-common==4.11.0
20
+ Requires-Dist: vantage6-client==4.11.0
21
21
  Provides-Extra: dev
22
22
  Requires-Dist: coverage==6.4.4; extra == "dev"
23
23
  Requires-Dist: black; extra == "dev"