Skip to content

Commit 286ec1e

Browse files
authored
Merge pull request #913 from simvue-io/feature/server-profiles
Allow user to specify alternative server from config and enforce `simvue.Run` keyword arguments
2 parents f408749 + 181f346 commit 286ec1e

5 files changed

Lines changed: 63 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Change Log
22

3+
## Unreleased
4+
5+
- Added ability to specify above one server in the `simvue.toml` file using `profiles`.
6+
- Enforced keyword arguments for readability and certainty in intent within initialiser for `simvue.Run`.
7+
38
## [v2.3.5](https://github.com/simvue-io/python-api/releases/tag/v2.3.5) - 2026-02-12
49

510
- Ensure runs do not enter lost state during metric upload.

simvue/config/user.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,14 @@ class SimvueConfiguration(pydantic.BaseModel):
5858
server: ServerSpecifications = pydantic.Field(
5959
..., description="Specifications for Simvue server"
6060
)
61+
profiles: dict[str, ServerSpecifications] = pydantic.Field(
62+
default_factory=dict[str, ServerSpecifications]
63+
)
6164
run: DefaultRunSpecifications = DefaultRunSpecifications()
6265
offline: OfflineSpecifications = OfflineSpecifications()
6366
metrics: MetricsSpecifications = MetricsSpecifications()
6467
eco: EcoConfig = EcoConfig()
68+
current_profile: str | None = None
6569

6670
@classmethod
6771
def _load_pyproject_configs(cls) -> dict | None:
@@ -161,6 +165,7 @@ def fetch(
161165
mode: typing.Literal["offline", "online", "disabled"],
162166
server_url: str | None = None,
163167
server_token: str | None = None,
168+
profile: str | None = None,
164169
) -> "SimvueConfiguration":
165170
"""Retrieve the Simvue configuration from this project
166171
@@ -178,6 +183,8 @@ def fetch(
178183
* online - send metrics and data to a server.
179184
* offline - run in offline mode.
180185
* disabled - run in disabled mode.
186+
profile : str | None, optional
187+
specify server profile to user for URL and token.
181188
182189
Return
183190
------
@@ -187,16 +194,26 @@ def fetch(
187194
"""
188195
_config_dict: dict[str, dict[str, str]] = cls._load_pyproject_configs() or {}
189196

197+
profile = os.environ.get("SIMVUE_SERVER_PROFILE", profile)
198+
190199
try:
191200
# NOTE: Legacy INI support has been removed
192201
_config_dict |= toml.load(cls.config_file())
193202

194203
except FileNotFoundError:
195204
if not server_token or not server_url:
196-
_config_dict = {"server": {}}
205+
_config_dict |= {"server": {}}
197206
logger.debug("No config file found, checking environment variables")
198207

199-
_config_dict["server"] = _config_dict.get("server", {})
208+
if not profile:
209+
_config_dict["server"] = _config_dict.get("server", {})
210+
elif not _config_dict.get("profiles", {}).get(profile):
211+
raise RuntimeError(
212+
f"Cannot load server configuration for '{profile}', "
213+
"profile not found in configurations."
214+
)
215+
else:
216+
_config_dict["server"] = _config_dict["profiles"][profile]
200217

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

@@ -237,7 +254,7 @@ def fetch(
237254
_config_dict["server"]["url"] = _server_url
238255
_config_dict["run"]["mode"] = _run_mode
239256

240-
return SimvueConfiguration(**_config_dict)
257+
return SimvueConfiguration(current_profile=profile, **_config_dict)
241258

242259
@classmethod
243260
@functools.lru_cache

simvue/run.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,13 @@ class Run:
120120
@pydantic.validate_call
121121
def __init__(
122122
self,
123+
*,
123124
mode: typing.Literal["online", "offline", "disabled"] = "online",
124125
abort_callback: typing.Callable[[Self], None] | None = None,
125126
server_token: pydantic.SecretStr | None = None,
126127
server_url: str | None = None,
127128
debug: bool = False,
129+
server_profile: str | None = None,
128130
) -> None:
129131
"""Initialise a new Simvue run
130132
@@ -145,6 +147,10 @@ def __init__(
145147
overwrite value for server URL, default is None
146148
debug : bool, optional
147149
run in debug mode, default is False
150+
server_profile : str | None, optional
151+
specify alternative profile to use for server, this assumes
152+
additional profiles have been specified in the configuration.
153+
Default is to use the main server.
148154
149155
Examples
150156
--------
@@ -188,7 +194,10 @@ def __init__(
188194
self._step: int = 0
189195
self._active: bool = False
190196
self._user_config: SimvueConfiguration = SimvueConfiguration.fetch(
191-
server_url=server_url, server_token=server_token, mode=mode
197+
server_url=server_url,
198+
server_token=server_token,
199+
mode=mode,
200+
profile=server_profile,
192201
)
193202

194203
logging.getLogger(self.__class__.__module__).setLevel(

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def create_test_run_offline(request, monkeypatch: pytest.MonkeyPatch, prevent_sc
158158
_ = prevent_script_exit
159159
with tempfile.TemporaryDirectory() as temp_d:
160160
monkeypatch.setenv("SIMVUE_OFFLINE_DIRECTORY", temp_d)
161-
with sv_run.Run("offline") as run:
161+
with sv_run.Run(mode="offline") as run:
162162
_test_run_data = setup_test_run(run, temp_dir=pathlib.Path(temp_d), create_objects=True, request=request)
163163
yield run, _test_run_data
164164
with contextlib.suppress(ObjectNotFoundError):
@@ -195,7 +195,7 @@ def create_plain_run_offline(request,prevent_script_exit,monkeypatch) -> Generat
195195
_ = prevent_script_exit
196196
with tempfile.TemporaryDirectory() as temp_d:
197197
monkeypatch.setenv("SIMVUE_OFFLINE_DIRECTORY", temp_d)
198-
with sv_run.Run("offline") as run:
198+
with sv_run.Run(mode="offline") as run:
199199
_temporary_directory = pathlib.Path(temp_d)
200200
yield run, setup_test_run(run, temp_dir=_temporary_directory, create_objects=False, request=request)
201201
clear_out_files()

tests/functional/test_config.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,25 @@
2121
"use_args", (True, False),
2222
ids=("args", "no_args")
2323
)
24+
@pytest.mark.parametrize(
25+
"profile", (None, "other"),
26+
ids=("default_profile", "alt_profile")
27+
)
2428
def test_config_setup(
2529
use_env: bool,
26-
use_file: str | None,
30+
use_file: typing.Literal["basic", "extended", "pyproject.toml"] | None,
2731
use_args: bool,
32+
profile: typing.Literal[None, "other"],
2833
monkeypatch: pytest.MonkeyPatch,
2934
mocker: pytest_mock.MockerFixture
3035
) -> None:
3136
_token: str = f"{uuid.uuid4()}".replace('-', '')
3237
_other_token: str = f"{uuid.uuid4()}".replace('-', '')
3338
_arg_token: str = f"{uuid.uuid4()}".replace('-', '')
39+
_alt_token: str = f"{uuid.uuid4()}".replace("-", "")
3440
_url: str = "https://simvue.example.com/"
3541
_other_url: str = "http://simvue.example.com/"
42+
_alt_url: str = "https://simvue-dev.example.com/"
3643
_arg_url: str = "http://simvue.example.io/"
3744
_description: str = "test case for runs"
3845
_description_ppt: str = "test case for runs using pyproject.toml"
@@ -75,6 +82,10 @@ def test_config_setup(
7582
url = "{_url}"
7683
token = "{_token}"
7784
85+
[profiles.other]
86+
url = "{_alt_url}"
87+
token = "{_alt_token}"
88+
7889
[offline]
7990
cache = "{_windows_safe}"
8091
"""
@@ -104,14 +115,22 @@ def _mocked_find(file_names: list[str], *_, ppt_file=_ppt_file, conf_file=_confi
104115
simvue.config.user.SimvueConfiguration.fetch(mode="online")
105116
return
106117
elif use_args:
107-
_config = simvue.config.user.SimvueConfiguration.fetch(
118+
_config: SimvueConfiguration = simvue.config.user.SimvueConfiguration.fetch(
108119
server_url=_arg_url,
109120
server_token=_arg_token,
110121
mode="online"
111122
)
123+
elif profile == "other":
124+
if not use_file:
125+
with pytest.raises(RuntimeError):
126+
_ = simvue.config.user.SimvueConfiguration.fetch(mode="online", profile="other")
127+
return
128+
else:
129+
_config = simvue.config.user.SimvueConfiguration.fetch(mode="online", profile="other")
130+
112131
else:
113132
_config = simvue.config.user.SimvueConfiguration.fetch(mode="online")
114-
133+
115134
if use_file and use_file != "pyproject.toml":
116135
assert _config.config_file() == _config_file
117136

@@ -121,6 +140,10 @@ def _mocked_find(file_names: list[str], *_, ppt_file=_ppt_file, conf_file=_confi
121140
elif use_args:
122141
assert _config.server.url == f"{_arg_url}api"
123142
assert _config.server.token.get_secret_value() == _arg_token
143+
elif use_file and profile == "other":
144+
assert _config.server.url == f"{_alt_url}api"
145+
assert _config.server.token.get_secret_value() == _alt_token
146+
assert f"{_config.offline.cache}" == temp_d
124147
elif use_file and use_file != "pyproject.toml":
125148
assert _config.server.url == f"{_url}api"
126149
assert _config.server.token.get_secret_value() == _token

0 commit comments

Comments
 (0)