From d4e211cd7ca090144325d4485f7ecd2dbe3e8412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Wed, 28 Jan 2026 16:32:38 +0000 Subject: [PATCH 1/3] Added option for alternative server config server-profiles --- simvue/config/user.py | 19 ++++++++++++++++--- tests/functional/test_config.py | 29 ++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/simvue/config/user.py b/simvue/config/user.py index 11ea57a6..c8264f12 100644 --- a/simvue/config/user.py +++ b/simvue/config/user.py @@ -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: @@ -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", ) -> "SimvueConfiguration": """Retrieve the Simvue configuration from this project @@ -190,10 +195,18 @@ def fetch( 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", {}) @@ -234,7 +247,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 diff --git a/tests/functional/test_config.py b/tests/functional/test_config.py index dcbb013d..43550121 100644 --- a/tests/functional/test_config.py +++ b/tests/functional/test_config.py @@ -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" @@ -75,6 +82,10 @@ def test_config_setup( url = "{_url}" token = "{_token}" + [profiles.other] + url = "{_alt_url}" + token = "{_alt_token}" + [offline] cache = "{_windows_safe}" """ @@ -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 @@ -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 From 886f6c0880fbe624d704f2103beee223d5d76ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Wed, 28 Jan 2026 21:57:14 +0000 Subject: [PATCH 2/3] Added ability to specify profile for server in Run class Also added specification of profile using SIMVUE_SERVER_PROFILE --- CHANGELOG.md | 5 +++++ simvue/config/user.py | 2 ++ simvue/run.py | 11 ++++++++++- tests/conftest.py | 4 ++-- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b05308ab..cbee6a26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/simvue/config/user.py b/simvue/config/user.py index c8264f12..acb7ad90 100644 --- a/simvue/config/user.py +++ b/simvue/config/user.py @@ -189,6 +189,8 @@ 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()) diff --git a/simvue/run.py b/simvue/run.py index 7e3e96fb..41ea5c2a 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -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 @@ -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 -------- @@ -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( diff --git a/tests/conftest.py b/tests/conftest.py index 5c94e50f..aee96c6f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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): @@ -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() From a64d40efdc51c28c612c4266d87133d9dc7e3423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Thu, 29 Jan 2026 15:49:53 +0000 Subject: [PATCH 3/3] Use None instead of default for default server profile --- simvue/config/user.py | 8 +++++--- simvue/run.py | 4 ++-- tests/functional/test_config.py | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/simvue/config/user.py b/simvue/config/user.py index acb7ad90..4733777c 100644 --- a/simvue/config/user.py +++ b/simvue/config/user.py @@ -62,7 +62,7 @@ class SimvueConfiguration(pydantic.BaseModel): offline: OfflineSpecifications = OfflineSpecifications() metrics: MetricsSpecifications = MetricsSpecifications() eco: EcoConfig = EcoConfig() - current_profile: str = "default" + current_profile: str | None = None @classmethod def _load_pyproject_configs(cls) -> dict | None: @@ -162,7 +162,7 @@ def fetch( mode: typing.Literal["offline", "online", "disabled"], server_url: str | None = None, server_token: str | None = None, - profile: str = "default", + profile: str | None = None, ) -> "SimvueConfiguration": """Retrieve the Simvue configuration from this project @@ -180,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 ------ @@ -200,7 +202,7 @@ def fetch( _config_dict |= {"server": {}} logger.debug("No config file found, checking environment variables") - if profile == "default": + if not profile: _config_dict["server"] = _config_dict.get("server", {}) elif not _config_dict.get("profiles", {}).get(profile): raise RuntimeError( diff --git a/simvue/run.py b/simvue/run.py index 41ea5c2a..b37c93e8 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -124,7 +124,7 @@ def __init__( server_token: pydantic.SecretStr | None = None, server_url: str | None = None, debug: bool = False, - server_profile: str = "default", + server_profile: str | None = None, ) -> None: """Initialise a new Simvue run @@ -145,7 +145,7 @@ def __init__( overwrite value for server URL, default is None debug : bool, optional run in debug mode, default is False - server_profile : str, optional + 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. diff --git a/tests/functional/test_config.py b/tests/functional/test_config.py index 43550121..25e2fc70 100644 --- a/tests/functional/test_config.py +++ b/tests/functional/test_config.py @@ -22,14 +22,14 @@ ids=("args", "no_args") ) @pytest.mark.parametrize( - "profile", ("default", "other"), + "profile", (None, "other"), ids=("default_profile", "alt_profile") ) def test_config_setup( use_env: bool, use_file: typing.Literal["basic", "extended", "pyproject.toml"] | None, use_args: bool, - profile: typing.Literal["default", "other"], + profile: typing.Literal[None, "other"], monkeypatch: pytest.MonkeyPatch, mocker: pytest_mock.MockerFixture ) -> None: