Skip to content

Commit bd90993

Browse files
authored
Merge pull request #714 from simvue-io/wk9874/2.0.0_a2
Bug Fixes for v2.0.0a2
2 parents ef9464a + c3cf76a commit bd90993

File tree

6 files changed

+94
-60
lines changed

6 files changed

+94
-60
lines changed

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/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/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/sender.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
import logging
1111
from concurrent.futures import ThreadPoolExecutor
1212
import threading
13+
import requests
1314
from simvue.config.user import SimvueConfiguration
1415

1516
import simvue.api.objects
17+
from simvue.version import __version__
1618

1719
UPLOAD_ORDER: list[str] = [
1820
"tenants",
@@ -134,7 +136,8 @@ def sender(
134136
objects_to_upload : list[str]
135137
Types of objects to upload, by default uploads all types of objects present in cache
136138
"""
137-
cache_dir = cache_dir or SimvueConfiguration.fetch().offline.cache
139+
_user_config = SimvueConfiguration.fetch()
140+
cache_dir = cache_dir or _user_config.offline.cache
138141
cache_dir.joinpath("server_ids").mkdir(parents=True, exist_ok=True)
139142
_id_mapping: dict[str, str] = {
140143
file_path.name.split(".")[0]: file_path.read_text()
@@ -160,4 +163,29 @@ def sender(
160163
),
161164
_offline_files,
162165
)
166+
167+
# Send heartbeats
168+
_headers: dict[str, str] = {
169+
"Authorization": f"Bearer {_user_config.server.token.get_secret_value()}",
170+
"User-Agent": f"Simvue Python client {__version__}",
171+
}
172+
173+
for _heartbeat_file in cache_dir.glob("runs/*.heartbeat"):
174+
_offline_id = _heartbeat_file.name.split(".")[0]
175+
_online_id = _id_mapping.get(_offline_id)
176+
if not _online_id:
177+
# Run has been closed - can just remove heartbeat and continue
178+
_heartbeat_file.unlink()
179+
continue
180+
_logger.info(f"Sending heartbeat to run {_online_id}")
181+
_response = requests.put(
182+
f"{_user_config.server.url}/runs/{_online_id}/heartbeat",
183+
headers=_headers,
184+
)
185+
if _response.status_code == 200:
186+
_heartbeat_file.unlink()
187+
else:
188+
_logger.warning(
189+
f"Attempting to send heartbeat to run {_online_id} returned status code {_response.status_code}."
190+
)
163191
return _id_mapping

tests/unit/test_metadata.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
@pytest.mark.local
99
def test_cargo_env() -> None:
1010
metadata = sv_meta._rust_env(pathlib.Path(__file__).parents[1].joinpath("example_data"))
11-
assert metadata["rust.environment.serde"] == "1.0.123"
12-
assert metadata["rust.project.name"] == "example_project"
11+
assert metadata["environment"]["serde"] == "1.0.123"
12+
assert metadata["project"]["name"] == "example_project"
1313

1414
@pytest.mark.metadata
1515
@pytest.mark.local
@@ -19,29 +19,36 @@ def test_cargo_env() -> None:
1919
def test_python_env(backend: str | None) -> None:
2020
if backend == "poetry":
2121
metadata = sv_meta._python_env(pathlib.Path(__file__).parents[1].joinpath("example_data", "python_poetry"))
22-
assert metadata["python.project.name"] == "example-repo"
22+
assert metadata["project"]["name"] == "example-repo"
2323
elif backend == "uv":
2424
metadata = sv_meta._python_env(pathlib.Path(__file__).parents[1].joinpath("example_data", "python_uv"))
25-
assert metadata["python.project.name"] == "example-repo"
25+
assert metadata["project"]["name"] == "example-repo"
2626
else:
2727
metadata = sv_meta._python_env(pathlib.Path(__file__).parents[1].joinpath("example_data"))
2828

29-
assert re.findall(r"\d+\.\d+\.\d+", metadata["python.environment.numpy"])
29+
assert re.findall(r"\d+\.\d+\.\d+", metadata["environment"]["numpy"])
3030

3131

3232
@pytest.mark.metadata
3333
@pytest.mark.local
3434
def test_julia_env() -> None:
3535
metadata = sv_meta._julia_env(pathlib.Path(__file__).parents[1].joinpath("example_data"))
36-
assert metadata["julia.project.name"] == "Julia Demo Project"
37-
assert re.findall(r"\d+\.\d+\.\d+", metadata["julia.environment.AbstractDifferentiation"])
36+
assert metadata["project"]["name"] == "Julia Demo Project"
37+
assert re.findall(r"\d+\.\d+\.\d+", metadata["environment"]["AbstractDifferentiation"])
3838

3939

4040
@pytest.mark.metadata
4141
@pytest.mark.local
4242
def test_js_env() -> None:
4343
metadata = sv_meta._node_js_env(pathlib.Path(__file__).parents[1].joinpath("example_data"))
44-
assert metadata["javascript.project.name"] == "my-awesome-project"
45-
assert re.findall(r"\d+\.\d+\.\d+", metadata["javascript.environment.node_modules/dotenv"])
46-
44+
assert metadata["project"]["name"] == "my-awesome-project"
45+
assert re.findall(r"\d+\.\d+\.\d+", metadata["environment"]["node_modules/dotenv"])
4746

47+
@pytest.mark.metadata
48+
@pytest.mark.local
49+
def test_environment() -> None:
50+
metadata = sv_meta.environment(pathlib.Path(__file__).parents[1].joinpath("example_data"))
51+
assert metadata["python"]["project"]["name"] == "example-repo"
52+
assert metadata["rust"]["project"]["name"] == "example_project"
53+
assert metadata["julia"]["project"]["name"] == "Julia Demo Project"
54+
assert metadata["javascript"]["project"]["name"] == "my-awesome-project"

0 commit comments

Comments
 (0)