Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Change Log

## Unreleased

- Added ability to specify above one server in the `simvue.toml` file using `profiles`.
- Enforced keyword arguments for readability and certainty in intent within initialiser for `simvue.Run`.

## [v2.3.0](https://github.com/simvue-io/client/releases/tag/v2.3.0) - 2025-12-11

- Refactored sender functionality introducing new `Sender` class.
Expand Down
21 changes: 18 additions & 3 deletions simvue/config/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,14 @@ class SimvueConfiguration(pydantic.BaseModel):
server: ServerSpecifications = pydantic.Field(
..., description="Specifications for Simvue server"
)
profiles: dict[str, ServerSpecifications] = pydantic.Field(
default_factory=dict[str, ServerSpecifications]
)
run: DefaultRunSpecifications = DefaultRunSpecifications()
offline: OfflineSpecifications = OfflineSpecifications()
metrics: MetricsSpecifications = MetricsSpecifications()
eco: EcoConfig = EcoConfig()
current_profile: str = "default"

@classmethod
def _load_pyproject_configs(cls) -> dict | None:
Expand Down Expand Up @@ -158,6 +162,7 @@ def fetch(
mode: typing.Literal["offline", "online", "disabled"],
server_url: str | None = None,
server_token: str | None = None,
profile: str = "default",
Comment thread
kzscisoft marked this conversation as resolved.
Outdated
) -> "SimvueConfiguration":
"""Retrieve the Simvue configuration from this project

Expand All @@ -184,16 +189,26 @@ def fetch(
"""
_config_dict: dict[str, dict[str, str]] = cls._load_pyproject_configs() or {}

profile = os.environ.get("SIMVUE_SERVER_PROFILE", profile)

try:
# NOTE: Legacy INI support has been removed
_config_dict |= toml.load(cls.config_file())

except FileNotFoundError:
if not server_token or not server_url:
_config_dict = {"server": {}}
_config_dict |= {"server": {}}
logger.debug("No config file found, checking environment variables")

_config_dict["server"] = _config_dict.get("server", {})
if profile == "default":
_config_dict["server"] = _config_dict.get("server", {})
elif not _config_dict.get("profiles", {}).get(profile):
raise RuntimeError(
f"Cannot load server configuration for '{profile}', "
"profile not found in configurations."
)
else:
_config_dict["server"] = _config_dict["profiles"][profile]

_config_dict["offline"] = _config_dict.get("offline", {})

Expand Down Expand Up @@ -234,7 +249,7 @@ def fetch(
_config_dict["server"]["url"] = _server_url
_config_dict["run"]["mode"] = _run_mode

return SimvueConfiguration(**_config_dict)
return SimvueConfiguration(profile=profile, **_config_dict)

@classmethod
@functools.lru_cache
Expand Down
11 changes: 10 additions & 1 deletion simvue/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,13 @@ class Run:
@pydantic.validate_call
def __init__(
self,
*,
mode: typing.Literal["online", "offline", "disabled"] = "online",
abort_callback: typing.Callable[[Self], None] | None = None,
server_token: pydantic.SecretStr | None = None,
server_url: str | None = None,
debug: bool = False,
server_profile: str = "default",
) -> None:
"""Initialise a new Simvue run

Expand All @@ -143,6 +145,10 @@ def __init__(
overwrite value for server URL, default is None
debug : bool, optional
run in debug mode, default is False
server_profile : str, optional
specify alternative profile to use for server, this assumes
additional profiles have been specified in the configuration.
Default is to use the main server.

Examples
--------
Expand Down Expand Up @@ -185,7 +191,10 @@ def __init__(
self._step: int = 0
self._active: bool = False
self._user_config: SimvueConfiguration = SimvueConfiguration.fetch(
server_url=server_url, server_token=server_token, mode=mode
server_url=server_url,
server_token=server_token,
mode=mode,
profile=server_profile,
)

logging.getLogger(self.__class__.__module__).setLevel(
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def create_test_run_offline(request, monkeypatch: pytest.MonkeyPatch, prevent_sc
_ = prevent_script_exit
with tempfile.TemporaryDirectory() as temp_d:
monkeypatch.setenv("SIMVUE_OFFLINE_DIRECTORY", temp_d)
with sv_run.Run("offline") as run:
with sv_run.Run(mode="offline") as run:
_test_run_data = setup_test_run(run, temp_dir=pathlib.Path(temp_d), create_objects=True, request=request)
yield run, _test_run_data
with contextlib.suppress(ObjectNotFoundError):
Expand Down Expand Up @@ -195,7 +195,7 @@ def create_plain_run_offline(request,prevent_script_exit,monkeypatch) -> Generat
_ = prevent_script_exit
with tempfile.TemporaryDirectory() as temp_d:
monkeypatch.setenv("SIMVUE_OFFLINE_DIRECTORY", temp_d)
with sv_run.Run("offline") as run:
with sv_run.Run(mode="offline") as run:
_temporary_directory = pathlib.Path(temp_d)
yield run, setup_test_run(run, temp_dir=_temporary_directory, create_objects=False, request=request)
clear_out_files()
Expand Down
29 changes: 26 additions & 3 deletions tests/functional/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,25 @@
"use_args", (True, False),
ids=("args", "no_args")
)
@pytest.mark.parametrize(
"profile", ("default", "other"),
ids=("default_profile", "alt_profile")
)
def test_config_setup(
use_env: bool,
use_file: str | None,
use_file: typing.Literal["basic", "extended", "pyproject.toml"] | None,
use_args: bool,
profile: typing.Literal["default", "other"],
monkeypatch: pytest.MonkeyPatch,
mocker: pytest_mock.MockerFixture
) -> None:
_token: str = f"{uuid.uuid4()}".replace('-', '')
_other_token: str = f"{uuid.uuid4()}".replace('-', '')
_arg_token: str = f"{uuid.uuid4()}".replace('-', '')
_alt_token: str = f"{uuid.uuid4()}".replace("-", "")
_url: str = "https://simvue.example.com/"
_other_url: str = "http://simvue.example.com/"
_alt_url: str = "https://simvue-dev.example.com/"
_arg_url: str = "http://simvue.example.io/"
_description: str = "test case for runs"
_description_ppt: str = "test case for runs using pyproject.toml"
Expand Down Expand Up @@ -75,6 +82,10 @@ def test_config_setup(
url = "{_url}"
token = "{_token}"

[profiles.other]
url = "{_alt_url}"
token = "{_alt_token}"

[offline]
cache = "{_windows_safe}"
"""
Expand Down Expand Up @@ -104,14 +115,22 @@ def _mocked_find(file_names: list[str], *_, ppt_file=_ppt_file, conf_file=_confi
simvue.config.user.SimvueConfiguration.fetch(mode="online")
return
elif use_args:
_config = simvue.config.user.SimvueConfiguration.fetch(
_config: SimvueConfiguration = simvue.config.user.SimvueConfiguration.fetch(
server_url=_arg_url,
server_token=_arg_token,
mode="online"
)
elif profile == "other":
if not use_file:
with pytest.raises(RuntimeError):
_ = simvue.config.user.SimvueConfiguration.fetch(mode="online", profile="other")
return
else:
_config = simvue.config.user.SimvueConfiguration.fetch(mode="online", profile="other")

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

if use_file and use_file != "pyproject.toml":
assert _config.config_file() == _config_file

Expand All @@ -121,6 +140,10 @@ def _mocked_find(file_names: list[str], *_, ppt_file=_ppt_file, conf_file=_confi
elif use_args:
assert _config.server.url == f"{_arg_url}api"
assert _config.server.token.get_secret_value() == _arg_token
elif use_file and profile == "other":
assert _config.server.url == f"{_alt_url}api"
assert _config.server.token.get_secret_value() == _alt_token
assert f"{_config.offline.cache}" == temp_d
elif use_file and use_file != "pyproject.toml":
assert _config.server.url == f"{_url}api"
assert _config.server.token.get_secret_value() == _token
Expand Down
Loading