Skip to content

Commit 4ef75c0

Browse files
authored
Merge pull request #734 from simvue-io/dev
v2.0.0 Alpha 2 Release
2 parents 5fb84ac + 3297e1f commit 4ef75c0

File tree

17 files changed

+397
-142
lines changed

17 files changed

+397
-142
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ repos:
2323
args: [--branch, main, --branch, dev]
2424
- id: check-added-large-files
2525
- repo: https://github.com/astral-sh/ruff-pre-commit
26-
rev: v0.9.6
26+
rev: v0.9.7
2727
hooks:
2828
- id: ruff
2929
args: [ --fix, --exit-non-zero-on-fix, "--ignore=C901" ]

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Change log
22

3+
## [v2.0.0-alpha2](https://github.com/simvue-io/client/releases/tag/v2.0.0a2) - 2025-02-27
4+
* Removed 'no config file' and 'unstaged changes' warnings from Offline mode as they do not apply
5+
* Made `staging_check` not apply in Offline mode
6+
* Added heartbeat functionality to Offline mode
7+
* Moved away from `FlatDict` module for metadata collection, fixes Simvue in Jupyter notebooks
8+
* Fixed `reconnect()` by setting `read_only` to False and added tests
9+
* Fixed resource metrics collection to return measurement on startup and use short interval for more accurate measurements
10+
* Fixed `set_pid` so that resource metrics are also collected for child processes of it
11+
* Improved sender by having all cached files read at start and lock file so only one sender runs at once
12+
* Added `name` option in `log_alert` and added tests
13+
* Fixed client `get_alerts` and improved tests
14+
* Removed all server config checks in Offline mode
15+
316
## [v2.0.0-alpha1](https://github.com/simvue-io/client/releases/tag/v2.0.0a1) - 2025-02-19
417
* Fixed `add_alerts` so that it now works with both IDs and names
518
* Improved alert and folder deduplication methods to rely on 409 responses from server upon creation

CITATION.cff

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ keywords:
4242
- alerting
4343
- simulation
4444
license: Apache-2.0
45-
commit: 124b2993d91dbff2e475aa972916b11e7bd02fa4
46-
version: 2.0.0a1
47-
date-released: '2025-02-19'
45+
commit: 83b9144abd2092d4be304bf742d72a249ad1d8ff
46+
version: 2.0.0a2
47+
date-released: '2025-02-27'
4848
references:
4949
- title: mlco2/codecarbon
5050
version: v2.8.2

poetry.lock

Lines changed: 25 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "simvue"
3-
version = "2.0.0a1"
3+
version = "2.0.0a2"
44
description = "Simulation tracking and monitoring"
55
authors = [
66
{name = "Simvue Development Team", email = "[email protected]"}

simvue/api/objects/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ def _wrapper(self) -> typing.Any:
5050
raise RuntimeError(
5151
f"Cannot use 'staging_check' decorator on type '{type(self).__name__}'"
5252
)
53+
if _sv_obj._offline:
54+
return member_func(self)
5355
if not _sv_obj._read_only and member_func.__name__ in _sv_obj._staging:
5456
_sv_obj._logger.warning(
5557
f"Uncommitted change found for attribute '{member_func.__name__}'"

simvue/api/objects/run.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,14 @@ def events(
315315

316316
@write_only
317317
def send_heartbeat(self) -> dict[str, typing.Any] | None:
318-
if self._offline or not self._identifier:
318+
if not self._identifier:
319+
return None
320+
321+
if self._offline:
322+
if not (_dir := self._local_staging_file.parent).exists():
323+
_dir.mkdir(parents=True)
324+
_heartbeat_file = self._local_staging_file.with_suffix(".heartbeat")
325+
_heartbeat_file.touch()
319326
return None
320327

321328
_url = self._base_url

simvue/client.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -990,17 +990,23 @@ def get_alerts(
990990
RuntimeError
991991
if there was a failure retrieving data from the server
992992
"""
993-
994993
if not run_id:
994+
if critical_only:
995+
raise RuntimeError(
996+
"critical_only is ambiguous when returning alerts with no run ID specified."
997+
)
995998
return [alert.name if names_only else alert for _, alert in Alert.get()] # type: ignore
996999

997-
return [
998-
alert.get("name")
999-
if names_only
1000-
else Alert(identifier=alert.get("id"), **alert)
1000+
_alerts = [
1001+
Alert(identifier=alert.get("id"), **alert)
10011002
for alert in Run(identifier=run_id).get_alert_details()
1002-
if not critical_only or alert["status"].get("current") == "critical"
1003-
] # type: ignore
1003+
]
1004+
1005+
return [
1006+
alert.name if names_only else alert
1007+
for alert in _alerts
1008+
if not critical_only or alert.get_status(run_id) == "critical"
1009+
]
10041010

10051011
@prettify_pydantic
10061012
@pydantic.validate_call

simvue/config/user.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def fetch(
180180
except FileNotFoundError:
181181
if not server_token or not server_url:
182182
_config_dict = {"server": {}}
183-
logger.warning("No config file found, checking environment variables")
183+
logger.debug("No config file found, checking environment variables")
184184

185185
_config_dict["server"] = _config_dict.get("server", {})
186186

simvue/executor.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,9 +348,11 @@ def _update_alerts(self) -> None:
348348
if self._runner._dispatcher:
349349
self._runner._dispatcher.purge()
350350

351-
self._runner.log_alert(self._alert_ids[proc_id], "critical")
351+
self._runner.log_alert(
352+
identifier=self._alert_ids[proc_id], state="critical"
353+
)
352354
else:
353-
self._runner.log_alert(self._alert_ids[proc_id], "ok")
355+
self._runner.log_alert(identifier=self._alert_ids[proc_id], state="ok")
354356

355357
_current_time: float = 0
356358
while (

simvue/metadata.py

Lines changed: 37 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import toml
1313
import logging
1414
import pathlib
15-
import flatdict
1615

1716
from simvue.utilities import simvue_timestamp
1817

@@ -64,7 +63,7 @@ def git_info(repository: str) -> dict[str, typing.Any]:
6463
)
6564
return {
6665
"git": {
67-
"authors": json.dumps(list(author_list)),
66+
"authors": list(author_list),
6867
"ref": ref,
6968
"msg": current_commit.message.strip(),
7069
"time_stamp": simvue_timestamp(current_commit.committed_datetime),
@@ -84,34 +83,32 @@ def _python_env(repository: pathlib.Path) -> dict[str, typing.Any]:
8483
if (pyproject_file := pathlib.Path(repository).joinpath("pyproject.toml")).exists():
8584
content = toml.load(pyproject_file)
8685
if (poetry_content := content.get("tool", {}).get("poetry", {})).get("name"):
87-
python_meta |= {
88-
"python.project.name": poetry_content["name"],
89-
"python.project.version": poetry_content["version"],
86+
python_meta["project"] = {
87+
"name": poetry_content["name"],
88+
"version": poetry_content["version"],
9089
}
9190
elif other_content := content.get("project"):
92-
python_meta |= {
93-
"python.project.name": other_content["name"],
94-
"python.project.version": other_content["version"],
91+
python_meta["project"] = {
92+
"name": other_content["name"],
93+
"version": other_content["version"],
9594
}
9695

9796
if (poetry_lock_file := pathlib.Path(repository).joinpath("poetry.lock")).exists():
9897
content = toml.load(poetry_lock_file).get("package", {})
99-
python_meta |= {
100-
f"python.environment.{package['name']}": package["version"]
101-
for package in content
98+
python_meta["environment"] = {
99+
package["name"]: package["version"] for package in content
102100
}
103101
elif (uv_lock_file := pathlib.Path(repository).joinpath("uv.lock")).exists():
104102
content = toml.load(uv_lock_file).get("package", {})
105-
python_meta |= {
106-
f"python.environment.{package['name']}": package["version"]
107-
for package in content
103+
python_meta["environment"] = {
104+
package["name"]: package["version"] for package in content
108105
}
109106
else:
110107
with contextlib.suppress((KeyError, ImportError)):
111108
from pip._internal.operations.freeze import freeze
112109

113-
python_meta |= {
114-
f"python.environment.{entry[0]}": entry[-1]
110+
python_meta["environment"] = {
111+
entry[0]: entry[-1]
115112
for line in freeze(local_only=True)
116113
if (entry := line.split("=="))
117114
}
@@ -126,35 +123,33 @@ def _rust_env(repository: pathlib.Path) -> dict[str, typing.Any]:
126123
if (cargo_file := pathlib.Path(repository).joinpath("Cargo.toml")).exists():
127124
content = toml.load(cargo_file).get("package", {})
128125
if version := content.get("version"):
129-
rust_meta |= {"rust.project.version": version}
126+
rust_meta.setdefault("project", {})["version"] = version
130127

131128
if name := content.get("name"):
132-
rust_meta |= {"rust.project.name": name}
129+
rust_meta.setdefault("project", {})["name"] = name
133130

134131
if not (cargo_lock := pathlib.Path(repository).joinpath("Cargo.lock")).exists():
135-
return {}
132+
return rust_meta
136133

137134
cargo_dat = toml.load(cargo_lock)
138-
139-
return rust_meta | {
140-
f"rust.environment.{dependency['name']}": dependency["version"]
135+
rust_meta["environment"] = {
136+
dependency["name"]: dependency["version"]
141137
for dependency in cargo_dat.get("package")
142138
}
143139

140+
return rust_meta
141+
144142

145143
def _julia_env(repository: pathlib.Path) -> dict[str, typing.Any]:
146144
"""Retrieve a dictionary of Julia dependencies if a project file is available"""
147145
julia_meta: dict[str, str] = {}
148146
if (project_file := pathlib.Path(repository).joinpath("Project.toml")).exists():
149147
content = toml.load(project_file)
150-
julia_meta |= {
151-
f"julia.project.{key}": value
152-
for key, value in content.items()
153-
if not isinstance(value, dict)
148+
julia_meta["project"] = {
149+
key: value for key, value in content.items() if not isinstance(value, dict)
154150
}
155-
julia_meta |= {
156-
f"julia.environment.{key}": value
157-
for key, value in content.get("compat", {}).items()
151+
julia_meta["environment"] = {
152+
key: value for key, value in content.get("compat", {}).items()
158153
}
159154
return julia_meta
160155

@@ -171,13 +166,11 @@ def _node_js_env(repository: pathlib.Path) -> dict[str, typing.Any]:
171166
)
172167
return {}
173168

174-
js_meta |= {
175-
f"javascript.project.{key}": value
176-
for key, value in content.items()
177-
if key in ("name", "version")
169+
js_meta["project"] = {
170+
key: value for key, value in content.items() if key in ("name", "version")
178171
}
179-
js_meta |= {
180-
f"javascript.environment.{key.replace('@', '')}": value["version"]
172+
js_meta["environment"] = {
173+
key.replace("@", ""): value["version"]
181174
for key, value in content.get(
182175
"packages" if lfv in (2, 3) else "dependencies", {}
183176
).items()
@@ -188,16 +181,13 @@ def _node_js_env(repository: pathlib.Path) -> dict[str, typing.Any]:
188181

189182
def environment(repository: pathlib.Path = pathlib.Path.cwd()) -> dict[str, typing.Any]:
190183
"""Retrieve environment metadata"""
191-
_environment_meta = flatdict.FlatDict(
192-
_python_env(repository), delimiter="."
193-
).as_dict()
194-
_environment_meta |= flatdict.FlatDict(
195-
_rust_env(repository), delimiter="."
196-
).as_dict()
197-
_environment_meta |= flatdict.FlatDict(
198-
_julia_env(repository), delimiter="."
199-
).as_dict()
200-
_environment_meta |= flatdict.FlatDict(
201-
_node_js_env(repository), delimiter="."
202-
).as_dict()
184+
_environment_meta = {}
185+
if _python_meta := _python_env(repository):
186+
_environment_meta["python"] = _python_meta
187+
if _rust_meta := _rust_env(repository):
188+
_environment_meta["rust"] = _rust_meta
189+
if _julia_meta := _julia_env(repository):
190+
_environment_meta["julia"] = _julia_meta
191+
if _js_meta := _node_js_env(repository):
192+
_environment_meta["javascript"] = _js_meta
203193
return _environment_meta

simvue/metrics.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,18 @@ def get_process_memory(processes: list[psutil.Process]) -> int:
3636
return rss
3737

3838

39-
def get_process_cpu(processes: list[psutil.Process]) -> int:
39+
def get_process_cpu(
40+
processes: list[psutil.Process], interval: float | None = None
41+
) -> int:
4042
"""
4143
Get the CPU usage
44+
45+
If first time being called, use a small interval to collect initial CPU metrics.
4246
"""
4347
cpu_percent: int = 0
4448
for process in processes:
4549
with contextlib.suppress(Exception):
46-
cpu_percent += process.cpu_percent()
50+
cpu_percent += process.cpu_percent(interval=interval)
4751

4852
return cpu_percent
4953

0 commit comments

Comments
 (0)