Skip to content

Use TOML as config definition #335

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d75222f
Switch to TOML configuration file with further options
kzscisoft Mar 6, 2024
0b7f1ad
Merge branch 'dev' into feature/toml-config
kzscisoft Apr 2, 2024
3b3ada2
Merge branch 'dev' into feature/toml-config
kzscisoft Apr 19, 2024
3797d09
Merge branch 'dev' into feature/toml-config
kzscisoft Apr 30, 2024
01bcc94
Merge branch 'dev' into feature/toml-config
kzscisoft May 20, 2024
dde2ce2
Merge branch 'dev' into feature/toml-config
kzscisoft Jun 10, 2024
f568604
Merge branch 'dev' into feature/toml-config
kzscisoft Aug 2, 2024
f1978c6
Add legacy config support
kzscisoft Aug 5, 2024
3d1ac23
Merge branch 'dev' into feature/toml-config
kzscisoft Aug 5, 2024
0c3e617
Fix tests for config updates
kzscisoft Aug 5, 2024
4fbd238
Fixed all references to self._url in Run
kzscisoft Aug 5, 2024
22f4bfc
Merge branch 'dev' into feature/toml-config
kzscisoft Sep 11, 2024
11723df
Fix linting issues
kzscisoft Sep 11, 2024
5a11f73
Merge branch 'dev' into feature/toml-config
kzscisoft Sep 12, 2024
a8c9cf6
Merge branch 'dev' into feature/toml-config
kzscisoft Sep 25, 2024
b01005f
Move token server check to validation
kzscisoft Sep 25, 2024
bbafb5f
Fix token processing in validation
kzscisoft Sep 26, 2024
b4438fa
Added URL check deactivation environment variable
kzscisoft Sep 27, 2024
d91487f
Remove outdated test
kzscisoft Sep 27, 2024
c0def37
Merge branch 'dev' into feature/toml-config
kzscisoft Oct 4, 2024
21a7de9
Merge branch 'dev' into feature/toml-config
kzscisoft Oct 4, 2024
ed7dfe7
Remove remnants of legacy token retrieval
kzscisoft Oct 9, 2024
922ce0a
Handle folder defaults correctly
kzscisoft Oct 9, 2024
beb624c
Add 'name' to config file
kzscisoft Oct 9, 2024
34d25fc
Reformat with Ruff
kzscisoft Oct 9, 2024
f844a3c
Add metadata to config, combine config tags with session tags
kzscisoft Oct 9, 2024
c3485bf
Add headers to files
kzscisoft Oct 9, 2024
f3e368f
Allow str, int, float, bool for metadata
kzscisoft Oct 9, 2024
4956768
Merge branch 'dev' into feature/toml-config
kzscisoft Oct 9, 2024
aa205ae
Try lengthening wait for test
kzscisoft Oct 9, 2024
a2e4c5c
Merge branch 'dev' into feature/toml-config
kzscisoft Oct 9, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ dmypy.json

# Simvue files
simvue.ini
simvue.toml
offline/

# Pyenv
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ export SIMVUE_URL=...
export SIMVUE_TOKEN=...
```
or a file `simvue.ini` can be created containing:
```ini
```toml
[server]
url = ...
token = ...
url = "..."
token = "..."
```
The exact contents of both of the above options can be obtained directly by clicking the **Create new run** button on the web UI. Note that the environment variables have preference over the config file.

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ markers = [
"utilities: test simvue utilities module",
"scenario: test scenarios",
"executor: tests of executors",
"config: tests of simvue configuration",
"api: tests of RestAPI functionality",
"unix: tests for UNIX systems only",
"metadata: tests of metadata gathering functions",
Expand Down
87 changes: 61 additions & 26 deletions simvue/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@
)
from .serialization import deserialize_data
from .simvue_types import DeserializedContent
from .utilities import check_extra, get_auth, prettify_pydantic
from .utilities import check_extra, prettify_pydantic
from .models import FOLDER_REGEX, NAME_REGEX
from .config import SimvueConfiguration

if typing.TYPE_CHECKING:
pass
Expand Down Expand Up @@ -90,18 +91,33 @@ class Client:
Class for querying Simvue
"""

def __init__(self) -> None:
"""Initialise an instance of the Simvue client"""
self._url: typing.Optional[str]
self._token: typing.Optional[str]
def __init__(
self,
server_token: typing.Optional[str] = None,
server_url: typing.Optional[str] = None,
) -> None:
"""Initialise an instance of the Simvue client

self._url, self._token = get_auth()
Parameters
----------
server_token : str, optional
specify token, if unset this is read from the config file
server_url : str, optional
specify URL, if unset this is read from the config file
"""
self._config = SimvueConfiguration.fetch(
server_token=server_token, server_url=server_url
)

for label, value in zip(("URL", "API token"), (self._url, self._token)):
for label, value in zip(
("URL", "API token"), (self._config.server.url, self._config.server.url)
):
if not value:
logger.warning(f"No {label} specified")

self._headers: dict[str, str] = {"Authorization": f"Bearer {self._token}"}
self._headers: dict[str, str] = {
"Authorization": f"Bearer {self._config.server.token}"
}

def _get_json_from_response(
self,
Expand Down Expand Up @@ -165,7 +181,7 @@ def get_run_id_from_name(
params: dict[str, str] = {"filters": json.dumps([f"name == {name}"])}

response: requests.Response = requests.get(
f"{self._url}/api/runs", headers=self._headers, params=params
f"{self._config.server.url}/api/runs", headers=self._headers, params=params
)

json_response = self._get_json_from_response(
Expand Down Expand Up @@ -215,7 +231,7 @@ def get_run(self, run_id: str) -> typing.Optional[dict[str, typing.Any]]:
"""

response: requests.Response = requests.get(
f"{self._url}/api/runs/{run_id}", headers=self._headers
f"{self._config.server.url}/api/runs/{run_id}", headers=self._headers
)

json_response = self._get_json_from_response(
Expand Down Expand Up @@ -331,7 +347,7 @@ def get_runs(
}

response = requests.get(
f"{self._url}/api/runs", headers=self._headers, params=params
f"{self._config.server.url}/api/runs", headers=self._headers, params=params
)

response.raise_for_status()
Expand Down Expand Up @@ -380,7 +396,8 @@ def delete_run(self, run_identifier: str) -> typing.Optional[dict]:
"""

response = requests.delete(
f"{self._url}/api/runs/{run_identifier}", headers=self._headers
f"{self._config.server.url}/api/runs/{run_identifier}",
headers=self._headers,
)

json_response = self._get_json_from_response(
Expand Down Expand Up @@ -415,7 +432,9 @@ def _get_folder_id_from_path(self, path: str) -> typing.Optional[str]:
params: dict[str, str] = {"filters": json.dumps([f"path == {path}"])}

response: requests.Response = requests.get(
f"{self._url}/api/folders", headers=self._headers, params=params
f"{self._config.server.url}/api/folders",
headers=self._headers,
params=params,
)

if (
Expand Down Expand Up @@ -458,7 +477,9 @@ def delete_runs(
params: dict[str, bool] = {"runs_only": True, "runs": True}

response = requests.delete(
f"{self._url}/api/folders/{folder_id}", headers=self._headers, params=params
f"{self._config.server.url}/api/folders/{folder_id}",
headers=self._headers,
params=params,
)

if response.status_code == http.HTTPStatus.OK:
Expand Down Expand Up @@ -522,7 +543,9 @@ def delete_folder(
params |= {"recursive": recursive}

response = requests.delete(
f"{self._url}/api/folders/{folder_id}", headers=self._headers, params=params
f"{self._config.server.url}/api/folders/{folder_id}",
headers=self._headers,
params=params,
)

json_response = self._get_json_from_response(
Expand Down Expand Up @@ -551,7 +574,7 @@ def delete_alert(self, alert_id: str) -> None:
the unique identifier for the alert
"""
response = requests.delete(
f"{self._url}/api/alerts/{alert_id}", headers=self._headers
f"{self._config.server.url}/api/alerts/{alert_id}", headers=self._headers
)

if response.status_code == http.HTTPStatus.OK:
Expand Down Expand Up @@ -586,7 +609,9 @@ def list_artifacts(self, run_id: str) -> list[dict[str, typing.Any]]:
params: dict[str, str] = {"runs": json.dumps([run_id])}

response: requests.Response = requests.get(
f"{self._url}/api/artifacts", headers=self._headers, params=params
f"{self._config.server.url}/api/artifacts",
headers=self._headers,
params=params,
)

json_response = self._get_json_from_response(
Expand All @@ -610,7 +635,7 @@ def _retrieve_artifact_from_server(
params: dict[str, str | None] = {"name": name}

response = requests.get(
f"{self._url}/api/runs/{run_id}/artifacts",
f"{self._config.server.url}/api/runs/{run_id}/artifacts",
headers=self._headers,
params=params,
)
Expand Down Expand Up @@ -649,7 +674,7 @@ def abort_run(self, run_id: str, reason: str) -> typing.Union[dict, list]:
body: dict[str, str | None] = {"id": run_id, "reason": reason}

response = requests.put(
f"{self._url}/api/runs/abort",
f"{self._config.server.url}/api/runs/abort",
headers=self._headers,
json=body,
)
Expand Down Expand Up @@ -843,7 +868,7 @@ def get_artifacts_as_files(
params: dict[str, typing.Optional[str]] = {"category": category}

response: requests.Response = requests.get(
f"{self._url}/api/runs/{run_id}/artifacts",
f"{self._config.server.url}/api/runs/{run_id}/artifacts",
headers=self._headers,
params=params,
)
Expand Down Expand Up @@ -935,7 +960,9 @@ def get_folders(
}

response: requests.Response = requests.get(
f"{self._url}/api/folders", headers=self._headers, params=params
f"{self._config.server.url}/api/folders",
headers=self._headers,
params=params,
)

json_response = self._get_json_from_response(
Expand Down Expand Up @@ -980,7 +1007,9 @@ def get_metrics_names(self, run_id: str) -> list[str]:
params = {"runs": json.dumps([run_id])}

response: requests.Response = requests.get(
f"{self._url}/api/metrics/names", headers=self._headers, params=params
f"{self._config.server.url}/api/metrics/names",
headers=self._headers,
params=params,
)

json_response = self._get_json_from_response(
Expand Down Expand Up @@ -1014,7 +1043,9 @@ def _get_run_metrics_from_server(
}

metrics_response: requests.Response = requests.get(
f"{self._url}/api/metrics", headers=self._headers, params=params
f"{self._config.server.url}/api/metrics",
headers=self._headers,
params=params,
)

json_response = self._get_json_from_response(
Expand Down Expand Up @@ -1271,7 +1302,9 @@ def get_events(
}

response = requests.get(
f"{self._url}/api/events", headers=self._headers, params=params
f"{self._config.server.url}/api/events",
headers=self._headers,
params=params,
)

json_response = self._get_json_from_response(
Expand Down Expand Up @@ -1317,7 +1350,9 @@ def get_alerts(
if there was a failure retrieving data from the server
"""
if not run_id:
response = requests.get(f"{self._url}/api/alerts/", headers=self._headers)
response = requests.get(
f"{self._config.server.url}/api/alerts/", headers=self._headers
)

json_response = self._get_json_from_response(
expected_status=[http.HTTPStatus.OK],
Expand All @@ -1326,7 +1361,7 @@ def get_alerts(
)
else:
response = requests.get(
f"{self._url}/api/runs/{run_id}", headers=self._headers
f"{self._config.server.url}/api/runs/{run_id}", headers=self._headers
)

json_response = self._get_json_from_response(
Expand Down
9 changes: 9 additions & 0 deletions simvue/config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
Simvue Configuration
====================

This module contains definitions for the Simvue configuration options

"""

from .user import SimvueConfiguration as SimvueConfiguration
86 changes: 86 additions & 0 deletions simvue/config/parameters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""
Simvue Configuration File Models
================================

Pydantic models for elements of the Simvue configuration file

"""

import logging
import os
import time
import pydantic
import typing
import pathlib
import http

import simvue.models as sv_models
from simvue.utilities import get_expiry
from simvue.version import __version__
from simvue.api import get

CONFIG_FILE_NAMES: list[str] = ["simvue.toml", ".simvue.toml"]

CONFIG_INI_FILE_NAMES: list[str] = [
f'{pathlib.Path.cwd().joinpath("simvue.ini")}',
f'{pathlib.Path.home().joinpath(".simvue.ini")}',
]

logger = logging.getLogger(__file__)


class ServerSpecifications(pydantic.BaseModel):
url: pydantic.AnyHttpUrl
token: pydantic.SecretStr

@pydantic.field_validator("url")
@classmethod
def url_to_str(cls, v: typing.Any) -> str:
return f"{v}"

@pydantic.field_validator("token")
def check_token(cls, v: typing.Any) -> str:
value = v.get_secret_value()
if not (expiry := get_expiry(value)):
raise AssertionError("Failed to parse Simvue token - invalid token form")
if time.time() - expiry > 0:
raise AssertionError("Simvue token has expired")
return value

@pydantic.model_validator(mode="after")
@classmethod
def check_valid_server(cls, values: "ServerSpecifications") -> bool:
if os.environ.get("SIMVUE_NO_SERVER_CHECK"):
return values

headers: dict[str, str] = {
"Authorization": f"Bearer {values.token}",
"User-Agent": f"Simvue Python client {__version__}",
}
try:
response = get(f"{values.url}/api/version", headers)

if response.status_code != http.HTTPStatus.OK or not response.json().get(
"version"
):
raise AssertionError

if response.status_code == http.HTTPStatus.UNAUTHORIZED:
raise AssertionError("Unauthorised token")

except Exception as err:
raise AssertionError(f"Exception retrieving server version: {str(err)}")

return values


class DefaultRunSpecifications(pydantic.BaseModel):
name: typing.Optional[str] = None
description: typing.Optional[str] = None
tags: typing.Optional[list[str]] = None
folder: str = pydantic.Field("/", pattern=sv_models.FOLDER_REGEX)
metadata: typing.Optional[dict[str, typing.Union[str, int, float, bool]]] = None


class ClientGeneralOptions(pydantic.BaseModel):
debug: bool = False
Loading
Loading