Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions simvue/config/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import contextlib
import functools
import http
import json
import logging
import os
import pathlib
Expand Down Expand Up @@ -125,18 +126,40 @@ def _load_pyproject_configs(cls) -> dict | None:

return _simvue_setup

@classmethod
@staticmethod
@functools.lru_cache
def _check_server(
cls,
*,
token: str,
offline_cache: pathlib.Path | None,
url: str,
mode: typing.Literal["offline", "online", "disabled"],
verify: str | bool,
) -> tuple[semver.Version | None, semver.Version | None]:
if mode in {"offline", "disabled"}:
return None, None
_offline_version_data: tuple[semver.Version | None, semver.Version | None] = (
None,
None,
)

if (
offline_cache
and (_version_file := offline_cache.joinpath("version.json")).exists()
):
with _version_file.open() as in_f:
_local_version_data = json.load(in_f)
try:
_offline_version_data = (
semver.Version.parse(_local_version_data.get("server")),
semver.Version.parse(_local_version_data.get("nosim")),
)
except ValueError as e:
raise AssertionError(
"Failed to parse local server version information, "
f"is '{_version_file}' a valid JSON file?"
) from e

if mode in {"offline", "disabled"} or os.environ.get("SIMVUE_NO_SERVER_CHECK"):
return _offline_version_data

headers: dict[str, str] = {
"Authorization": f"Bearer {token}",
Expand Down Expand Up @@ -204,9 +227,6 @@ def write(self, out_directory: pydantic.DirectoryPath) -> None:

@pydantic.model_validator(mode="after")
def check_valid_server(self) -> Self:
if os.environ.get("SIMVUE_NO_SERVER_CHECK"):
return self

if not self.server.token:
raise ValueError("No token provided.")

Expand All @@ -215,6 +235,7 @@ def check_valid_server(self) -> Self:
url=self.server.url,
verify=self.server_verify,
mode=self.run.mode,
offline_cache=self.offline.cache,
)

return self
Expand Down
79 changes: 79 additions & 0 deletions simvue/sender/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,84 @@ def upload(
cls.logger.exception("CO2 Monitor initialisation failed")


class ServerVersionAction(UploadAction):
object_type: str = "version"

@override
@classmethod
def initialise_object(cls, online_id: ObjectID | None, **data) -> None:
"""No initialiser for this action."""
_ = online_id
_ = data

@override
@classmethod
def pre_tasks(
cls,
offline_id: str,
data: dict[str, typing.Any],
cache_directory: pathlib.Path,
) -> None:
"""No pre-tasks for this action."""
_ = offline_id
_ = data
_ = cache_directory

@override
@classmethod
def post_tasks(
cls,
offline_id: str,
online_id: ObjectID | None,
data: dict[str, typing.Any],
cache_directory: pathlib.Path,
) -> None:
"""No post-tasks for this action."""
_ = offline_id
_ = data
_ = cache_directory

@override
@classmethod
def upload(
cls,
*,
cache_directory: pathlib.Path,
throw_exceptions: bool = False,
retry_failed: bool = False,
**_: object,
) -> None:
"""Download latest version."""
_config: SimvueConfiguration = SimvueConfiguration.fetch(mode="online")

with cls.json_file(cache_directory).open("w") as out_f:
json.dump(
{
"server": f"{_config.server_version}",
"nosim": f"{_config.nosim_version}",
},
out_f,
indent=2,
)

@classmethod
def json_file(cls, cache_directory: pathlib.Path, *_: object) -> pathlib.Path:
"""Returns the local cache JSON file for an upload.

Parameters
----------
cache_directory : pathlib.Path
the cache directory to search

Returns
-------
pathlib.Path
path of local JSON file

"""
return cache_directory.joinpath(f"{cls.object_type}.json")


# Define the upload action ordering
UPLOAD_ACTION_ORDER: tuple[type[UploadAction], ...] = (
TenantUploadAction,
Expand All @@ -1157,4 +1235,5 @@ def upload(
EventsUploadAction,
HeartbeatUploadAction,
CO2IntensityUploadAction,
ServerVersionAction,
)
1 change: 1 addition & 0 deletions simvue/sender/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"events",
"heartbeat",
"co2_intensity",
"version",
]

UPLOAD_ORDER: list[str] = [action.object_type for action in UPLOAD_ACTION_ORDER]
Expand Down
44 changes: 35 additions & 9 deletions tests/functional/test_config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import json

import pytest
import typing
import semver
import uuid
import pathlib
import pytest_mock
Expand All @@ -24,13 +27,18 @@
"profile", (None, "other"),
ids=("default_profile", "alt_profile")
)
@pytest.mark.parametrize(
"mode", ("offline", "online"),
)
def test_config_setup(
use_env: bool,
use_file: typing.Literal["basic", "extended", "pyproject.toml"] | None,
use_args: bool,
profile: typing.Literal[None, "other"],
mode: typing.Literal["offline", "online"],
monkeypatch: pytest.MonkeyPatch,
mocker: pytest_mock.MockerFixture
mocker: pytest_mock.MockerFixture,
offline_cache_setup: tempfile.TemporaryDirectory
) -> None:
_token: str = f"{uuid.uuid4()}".replace('-', '')
_other_token: str = f"{uuid.uuid4()}".replace('-', '')
Expand All @@ -56,15 +64,29 @@ def test_config_setup(
_tags: list[str] = ["tag-test", "other-tag"]
_tags_ppt: list[str] = ["tag-test-ppt", "other-tag-ppt"]

_offline_sv_version = semver.Version.parse("1.4.5")
_offline_ns_version = semver.Version.parse("1.0.2")


if mode == "offline":
with pathlib.Path(offline_cache_setup.name).joinpath("version.json").open("w") as out_f:
json.dump({
"server": f"{_offline_sv_version}",
"nosim": f"{_offline_ns_version}"
}, out_f, indent=2)


# Deactivate the server checks for this test
monkeypatch.setenv("SIMVUE_NO_SERVER_CHECK", "True")
monkeypatch.delenv("SIMVUE_TOKEN", False)
monkeypatch.delenv("SIMVUE_URL", False)

if use_env:
monkeypatch.setenv("SIMVUE_TOKEN", _other_token)
monkeypatch.setenv("SIMVUE_URL", _other_url)

if use_args or use_env or use_file:
monkeypatch.setenv("SIMVUE_NO_SERVER_CHECK", "true")

with tempfile.TemporaryDirectory() as temp_d:
_config_file = None
_ppt_file = None
Expand All @@ -84,7 +106,7 @@ def test_config_setup(
with open((_ppt_file := pathlib.Path(temp_d).joinpath("pyproject.toml")), "w") as out_f:
out_f.write(_lines_ppt)
with open(_config_file := pathlib.Path(temp_d).joinpath("simvue.toml"), "w") as out_f:
_windows_safe = temp_d.replace("\\", "\\\\")
_windows_safe = offline_cache_setup.name.replace("\\", "\\\\")
_lines: str = f"""
[server]
url = "{_url}"
Expand Down Expand Up @@ -132,18 +154,18 @@ def _mocked_find(file_names: list[str], *_, ppt_file=_ppt_file, conf_file=_confi
_config: SimvueConfiguration = simvue.config.user.SimvueConfiguration.fetch(
server_url=_arg_url,
server_token=_arg_token,
mode="online"
mode=mode
)
elif profile == "other":
if not use_file:
with pytest.raises(RuntimeError):
_ = simvue.config.user.SimvueConfiguration.fetch(mode="online", profile="other")
_ = simvue.config.user.SimvueConfiguration.fetch(profile="other", mode=mode)
return
else:
_config = simvue.config.user.SimvueConfiguration.fetch(mode="online", profile="other")
_config = simvue.config.user.SimvueConfiguration.fetch(mode=mode, profile="other")

else:
_config = simvue.config.user.SimvueConfiguration.fetch(mode="online")
_config = simvue.config.user.SimvueConfiguration.fetch(mode=mode)

if use_file and use_file != "pyproject.toml":
assert _config.config_file() == _config_file
Expand All @@ -160,12 +182,12 @@ def _mocked_find(file_names: list[str], *_, ppt_file=_ppt_file, conf_file=_confi
assert _config.server.url == f"{_alt_url}api"
assert _config.server.token
assert _config.server.token.get_secret_value() == _alt_token
assert f"{_config.offline.cache}" == temp_d
assert f"{_config.offline.cache}" == offline_cache_setup.name
elif use_file and use_file != "pyproject.toml":
assert _config.server.url == f"{_url}api"
assert _config.server.token
assert _config.server.token.get_secret_value() == _token
assert f"{_config.offline.cache}" == temp_d
assert f"{_config.offline.cache}" == offline_cache_setup.name

if use_file == "extended":
assert _config.run.description == _description
Expand All @@ -185,5 +207,9 @@ def _mocked_find(file_names: list[str], *_, ppt_file=_ppt_file, conf_file=_confi
for key, value in _custom_env.items():
assert _config.server.env.get(key) == f"{value}"

if mode == "offline":
assert _config.server_version == _offline_sv_version
assert _config.nosim_version == _offline_ns_version

simvue.config.user.SimvueConfiguration.config_file.cache_clear()

17 changes: 17 additions & 0 deletions tests/unit/test_sender.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import json
import tempfile
import pytest
import time
import datetime
import uuid

import semver
from simvue.sender import Sender
from simvue.api.objects import Run, Metrics, Folder
from simvue.models import DATETIME_FORMAT
Expand Down Expand Up @@ -156,6 +159,20 @@ def test_sender_server_ids(offline_cache_setup, caplog, parallel):
assert len(list(pathlib.Path(offline_cache_setup.name).joinpath("runs").iterdir())) == 0
assert len(list(pathlib.Path(offline_cache_setup.name).joinpath("metrics").iterdir())) == 0


@pytest.mark.offline
def test_sender_version_get(offline_cache_setup: tempfile.TemporaryDirectory) -> None:
_sender = Sender()
_sender.upload()
assert (_version_file := pathlib.Path(offline_cache_setup.name).joinpath("version.json")).exists()
with _version_file.open() as in_f:
_data = json.load(in_f)
assert "server" in _data
assert "nosim" in _data
_ = semver.Version.parse(_data["server"])
_ = semver.Version.parse(_data["nosim"])


@pytest.mark.parametrize("parallel", (True, False))
def test_send_heartbeat(offline_cache_setup, parallel, mocker):
# Create an offline run
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading