Skip to content
Open
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
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
23 changes: 20 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 | None = None

@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 | None = None,
) -> "SimvueConfiguration":
"""Retrieve the Simvue configuration from this project

Expand All @@ -175,6 +180,8 @@ def fetch(
* online - send metrics and data to a server.
* offline - run in offline mode.
* disabled - run in disabled mode.
profile : str | None, optional
specify server profile to user for URL and token.

Return
------
Expand All @@ -184,16 +191,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 not profile:
_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 +251,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 | None = None,
) -> 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 | None, 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", (None, "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[None, "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