Skip to content

Commit a7dbfc3

Browse files
committed
Fixed adding metrics to existing runs
Completed tests
1 parent ff064c2 commit a7dbfc3

File tree

5 files changed

+147
-30
lines changed

5 files changed

+147
-30
lines changed

simvue.ini

Whitespace-only changes.

simvue_cli/cli/__init__.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,17 @@ def simvue(ctx, plain: bool) -> None:
5757

5858

5959
@simvue.command("ping")
60-
def ping_server() -> None:
60+
@click.option("-t", "--timeout", help="Timeout the command after n seconds", default=None, type=int)
61+
def ping_server(timeout: int | None) -> None:
6162
"""Ping the Simvue server"""
6263
successful_pings: int = 0
6364
with contextlib.suppress(KeyboardInterrupt):
64-
url = simvue_client.Client()._url
65+
url = simvue_client.Client()._config.server.url
6566
ip_address = simvue_cli.server.get_ip_of_url(url)
67+
counter: int = 0
6668
while True:
69+
if timeout and counter > timeout:
70+
return
6771
start_time = time.time()
6872
try:
6973
server_version: int | str = simvue_cli.run.get_server_version()
@@ -77,6 +81,7 @@ def ping_server() -> None:
7781
click.secho(f"Reply from {url} ({ip_address}): status_code={status_code}, error")
7882

7983
time.sleep(1)
84+
counter += 1
8085

8186

8287
@simvue.command("whoami")
@@ -103,28 +108,24 @@ def whoami(user: bool, tenant: bool) -> None:
103108
@click.pass_context
104109
def about_simvue(ctx) -> None:
105110
"""Display full information on Simvue instance"""
111+
width = shutil.get_terminal_size().columns
106112
click.echo(
107-
SIMVUE_LOGO
113+
"\n".join(f"{'\t' * int(0.015 * width)}{r}" for r in SIMVUE_LOGO.split("\n"))
108114
)
109-
width = shutil.get_terminal_size().columns
110115
click.echo(f"\n{width * '='}\n")
111116
click.echo(f"\n{'\t' * int(0.04 * width)} Provided under the Apache-2.0 License")
112117
click.echo(f"{'\t' * int(0.04 * width)}© Copyright {datetime.datetime.today().strftime('%Y')} Simvue Development Team\n")
118+
out_table: list[list[str]] = []
113119
with contextlib.suppress(importlib.metadata.PackageNotFoundError):
114-
click.echo(
115-
f"{'\t' * int(0.04 * width)}CLI Version:\t{importlib.metadata.version(simvue_cli.__name__)}"
116-
)
120+
out_table.append(["CLI Version: ", importlib.metadata.version(simvue_cli.__name__)])
117121
with contextlib.suppress(importlib.metadata.PackageNotFoundError):
118-
click.echo(
119-
f"{'\t' * int(0.04 * width)}Python API Version:\t{importlib.metadata.version(simvue_client.__name__)}"
120-
)
122+
out_table.append(["Python API Version: ", importlib.metadata.version(simvue_client.__name__)])
121123
with contextlib.suppress(Exception):
122124
server_version: int | str = simvue_cli.run.get_server_version()
123125
if isinstance(server_version, int):
124126
raise RuntimeError
125-
click.echo(
126-
f"{'\t' * int(0.04 * width)}Server Version:\t{server_version}"
127-
)
127+
out_table.append(["Server Version: ", server_version])
128+
click.echo("\n".join(f"{'\t' * int(0.045 * width)}{r}" for r in tabulate.tabulate(out_table, tablefmt="plain").__str__().split("\n")))
128129
click.echo(f"\n{width * '='}\n")
129130

130131

@@ -445,10 +446,9 @@ def simvue_alert(ctx) -> None:
445446
@click.option("--email", is_flag=True, help="Notify by email if triggered", show_default=True)
446447
def create_alert(ctx, name: str, abort: bool=False, email: bool=False) -> None:
447448
"""Create a User alert"""
448-
simvue_cli.run.create_user_alert(name, abort, email)
449-
click.echo(
450-
f"Created alert '{name}'"
451-
)
449+
result = simvue_cli.run.create_user_alert(name, abort, email)
450+
alert_id = result["id"]
451+
click.echo(click.style(alert_id) if not ctx.obj["plain"] else alert_id)
452452

453453

454454
@simvue.command("monitor")

simvue_cli/run.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ def _check_run_exists(run_id: str) -> pathlib.Path:
4444
if run_shelf_file.exists():
4545
run_shelf_file.unlink()
4646
raise ValueError(f"Run '{run_id}' status is '{status}'.")
47+
48+
# If the run was created by other means, need to make a local cache file
49+
# retrieve last time step, and the start time of the run
50+
if not run_shelf_file.exists():
51+
metrics = run["metrics"]
52+
out_data = {"step": 0, "start_time": time.time()}
53+
if metrics and (step := max(metric.get("step") for metric in metrics)):
54+
out_data["step"] = step
55+
if metrics and (time_now := min(metric.get("time") for metric in metrics)):
56+
out_data["start_time"] = time_now
57+
with run_shelf_file.open("w") as out_f:
58+
json.dump(out_data, out_f)
59+
4760
return run_shelf_file
4861

4962

@@ -228,7 +241,7 @@ def get_server_version() -> typing.Union[str, int]:
228241
"""
229242
simvue_instance = Simvue(name=None, uniq_id="", mode="online", config=SimvueConfiguration.fetch())
230243
response = sv_api.get(
231-
f"{simvue_instance._url}/api/version", headers=simvue_instance._headers
244+
f"{simvue_instance._config.server.url}/api/version", headers=simvue_instance._headers
232245
)
233246
if response.status_code != 200:
234247
return response.status_code
@@ -246,7 +259,7 @@ def user_info() -> dict:
246259
"""
247260
simvue_instance = Simvue(name=None, uniq_id="", mode="online", config=SimvueConfiguration.fetch())
248261
response = sv_api.get(
249-
f"{simvue_instance._url}/api/whoami", headers=simvue_instance._headers
262+
f"{simvue_instance._config.server.url}/api/whoami", headers=simvue_instance._headers
250263
)
251264
if response.status_code != 200:
252265
return response.status_code
@@ -287,7 +300,7 @@ def get_alerts(**kwargs) -> None:
287300
client.get_alerts()
288301

289302

290-
def create_user_alert(name: str, trigger_abort: bool, email_notify: bool) -> None:
303+
def create_user_alert(name: str, trigger_abort: bool, email_notify: bool) -> dict | None:
291304
"""Create a User alert
292305
293306
Parameters
@@ -298,12 +311,17 @@ def create_user_alert(name: str, trigger_abort: bool, email_notify: bool) -> Non
298311
whether triggering of this alert will terminate the relevant simulation
299312
email_notify : bool
300313
whether trigger of this alert will send an email to the creator
314+
315+
Returns
316+
-------
317+
dict | None
318+
server response on alert creation
301319
"""
302320
alert_data = {
303321
"name": name,
304322
"source": "user",
305323
"abort": trigger_abort,
306324
"notification": "email" if email_notify else "none",
307325
}
308-
Simvue(name=None, uniq_id="undefined", mode="online", config=SimvueConfiguration.fetch()).add_alert(alert_data)
326+
return Simvue(name=None, uniq_id="undefined", mode="online", config=SimvueConfiguration.fetch()).add_alert(alert_data)
309327

simvue_cli/validation.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ def __init__(self) -> None:
3535
def convert(self, value: str, param: Parameter | None, ctx: Context | None) -> dict:
3636
try:
3737
return json.loads(value)
38-
except json.JSONDecodeError:
39-
self.fail(f"Failed to load '{value}', invalid JSON string")
38+
except json.JSONDecodeError as e:
39+
self.fail(f"Failed to load '{value}', invalid JSON string: {e}")
40+
except Exception as e:
41+
self.fail(f"{e}")
4042

4143

4244
SimvueFolder = PatternMatch(FOLDER_REGEX)

tests/test_command_line_interface.py

Lines changed: 104 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def test_config_update(component: str, tmpdir: LEGACY_PATH) -> None:
3333
TEST_SERVER if component == "url" else TEST_TOKEN
3434
]
3535
)
36-
assert result.exit_code == 0
36+
assert result.exit_code == 0, result.output
3737

3838
config = toml.load(SIMVUE_CONFIG_FILENAME)
3939

@@ -56,7 +56,7 @@ def test_runs_list(create_plain_run: tuple[simvue.Run, dict], tab_format: str) -
5656
f"--format={tab_format}"
5757
]
5858
)
59-
assert result.exit_code == 0
59+
assert result.exit_code == 0, result.output
6060
assert run.id and run.id in result.output
6161

6262

@@ -72,7 +72,7 @@ def test_runs_json(create_test_run: tuple[simvue.Run, dict]) -> None:
7272
run.id
7373
]
7474
)
75-
assert result.exit_code == 0
75+
assert result.exit_code == 0, result.output
7676
json_data = json.loads(result.output)
7777
assert json_data.get("tags") == run_data["tags"]
7878

@@ -97,7 +97,7 @@ def test_run_creation(state: str) -> None:
9797
sv_cli.simvue,
9898
cmd
9999
)
100-
assert result.exit_code == 0
100+
assert result.exit_code == 0, result.output
101101
if state == "create":
102102
return
103103
time.sleep(1.0)
@@ -111,12 +111,106 @@ def test_run_creation(state: str) -> None:
111111
run_id
112112
]
113113
)
114-
assert result.exit_code == 0
114+
assert result.exit_code == 0, result.output
115115
time.sleep(1)
116116
run_data = client.get_run(run_id)
117117
assert run_data["status"] == "terminated" if state == "abort" else "completed"
118118

119119

120+
@pytest.mark.parametrize(
121+
"existing_run", (False, True), ids=("new", "existing")
122+
)
123+
def test_log_metrics(create_plain_run: tuple[simvue.Run, dict], existing_run: bool) -> None:
124+
run, run_data = create_plain_run
125+
assert run.id
126+
if existing_run:
127+
run.log_metrics({"x": -1, "y": (-1)*(-1) + 2*(-1) - 3})
128+
runner = click.testing.CliRunner()
129+
130+
for i in range(10):
131+
time.sleep(0.5)
132+
x = i - 5
133+
metrics = json.dumps({"x": x, "y": x*x + 2*x - 3})
134+
result = runner.invoke(
135+
sv_cli.simvue,
136+
[
137+
"run",
138+
"log.metrics",
139+
run.id,
140+
f"{metrics}"
141+
]
142+
)
143+
assert result.exit_code == 0, result.output
144+
time.sleep(2.0)
145+
client = simvue.Client()
146+
assert (results := client.get_metric_values(run_ids=[run.id], xaxis="step", metric_names=["x", "y"]))
147+
assert len(results["x"]) == 10
148+
assert len(results["y"]) == 10
149+
150+
151+
def test_log_events(create_plain_run: tuple[simvue.Run, dict]) -> None:
152+
run, run_data = create_plain_run
153+
assert run.id
154+
runner = click.testing.CliRunner()
155+
156+
for i in range(5):
157+
time.sleep(0.5)
158+
x = i - 5
159+
y = x*x + 2*x - 3
160+
result = runner.invoke(
161+
sv_cli.simvue,
162+
[
163+
"run",
164+
"log.event",
165+
run.id,
166+
f"Event: x={x}, y={y}"
167+
]
168+
)
169+
assert result.exit_code == 0, result.output
170+
time.sleep(2.0)
171+
client = simvue.Client()
172+
assert (results := client.get_events(run.id))
173+
assert len(results) == 5
174+
175+
176+
def test_user_alert() -> None:
177+
runner = click.testing.CliRunner()
178+
result = runner.invoke(
179+
sv_cli.simvue,
180+
[
181+
"alert",
182+
"create",
183+
"test/test_user_alert"
184+
]
185+
)
186+
assert result.exit_code == 0, result.output
187+
time.sleep(1.0)
188+
client = simvue.Client()
189+
client.delete_alert(result.output.strip())
190+
191+
192+
def test_server_ping() -> None:
193+
runner = click.testing.CliRunner()
194+
result = runner.invoke(
195+
sv_cli.simvue,
196+
[
197+
"ping",
198+
"--timeout=5"
199+
]
200+
)
201+
assert result.exit_code == 0, result.output
202+
203+
204+
def test_about() -> None:
205+
runner = click.testing.CliRunner()
206+
result = runner.invoke(
207+
sv_cli.simvue,
208+
[
209+
"about"
210+
]
211+
)
212+
assert result.exit_code == 0, result.output
213+
120214
@pytest.mark.parametrize(
121215
"tab_format", tabulate._table_formats.keys()
122216
)
@@ -137,7 +231,7 @@ def test_folder_list(create_plain_run: tuple[simvue.Run, dict], tab_format: str)
137231
f"--format={tab_format}"
138232
]
139233
)
140-
assert result.exit_code == 0
234+
assert result.exit_code == 0, result.output
141235
assert run_data["folder"] in result.output
142236

143237

@@ -180,9 +274,12 @@ def test_simvue_monitor() -> None:
180274

181275
command_1.stdout.close()
182276
command_2.communicate()
183-
assert command_2.returncode == 0
277+
assert command_2.returncode == 0, result.output
184278
time.sleep(1.0)
185279
client = simvue.Client()
186280
run_data = client.get_runs(filters=["has tag.test_simvue_monitor"])
187281
assert run_data
188282
assert client.get_metric_values(run_ids=[run_data[0]["id"]], metric_names=["x", "y"], xaxis="step")
283+
284+
285+

0 commit comments

Comments
 (0)