From d75222f632043cbdfbf670836a9e74f03f606fe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Wed, 6 Mar 2024 15:30:44 +0000 Subject: [PATCH 01/17] Switch to TOML configuration file with further options --- .gitignore | 1 + README.md | 10 ++-- poetry.lock | 21 ++++++- pyproject.toml | 1 + pytest.ini | 3 + simvue/config/__init__.py | 1 + simvue/config/parameters.py | 34 +++++++++++ simvue/config/user.py | 67 +++++++++++++++++++++ simvue/factory/__init__.py | 6 ++ simvue/factory/remote.py | 37 ++++++------ simvue/run.py | 34 ++++++++--- simvue/utilities.py | 109 ++++++++++++++++++++++++++-------- tests/refactor/test_config.py | 98 ++++++++++++++++++++++++++++++ 13 files changed, 364 insertions(+), 58 deletions(-) create mode 100644 pytest.ini create mode 100644 simvue/config/__init__.py create mode 100644 simvue/config/parameters.py create mode 100644 simvue/config/user.py create mode 100644 tests/refactor/test_config.py diff --git a/.gitignore b/.gitignore index e664f5f6..d5e715f2 100644 --- a/.gitignore +++ b/.gitignore @@ -134,6 +134,7 @@ dmypy.json # Simvue files simvue.ini +simvue.toml # Pyenv .python-version diff --git a/README.md b/README.md index ab52bd05..23a5f1d4 100644 --- a/README.md +++ b/README.md @@ -25,20 +25,20 @@ Collect metadata, metrics and artifacts from simulations, processing and AI/ML t ## Configuration The service URL and token can be defined as environment variables: -``` +```sh export SIMVUE_URL=... export SIMVUE_TOKEN=... ``` or a file `simvue.ini` can be created containing: -``` +```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. ## Usage example -``` +```python from simvue import Run ... diff --git a/poetry.lock b/poetry.lock index ae714864..8f943061 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -1318,6 +1318,23 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-mock" +version = "3.12.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, + {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + [[package]] name = "python-dateutil" version = "2.8.2" @@ -1568,4 +1585,4 @@ torch = ["torch"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "5b3598d88f5aeea8176fe1d0ad5f4933ff8643df0c25653f1f7912b8fb4559c3" +content-hash = "509a248be328cbb62de5b403e34acd2643e77af31c5933c3acdf2721711580aa" diff --git a/pyproject.toml b/pyproject.toml index dc4a1368..e8872651 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ simvue_sender = "simvue.bin.sender:run" [tool.poetry.group.dev.dependencies] pytest = "^8.0.0" +pytest-mock = "^3.12.0" [build-system] requires = ["poetry-core"] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..78498523 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +markers = + config: tests of simvue configuration \ No newline at end of file diff --git a/simvue/config/__init__.py b/simvue/config/__init__.py new file mode 100644 index 00000000..8da076b2 --- /dev/null +++ b/simvue/config/__init__.py @@ -0,0 +1 @@ +from .user import SimvueConfiguration \ No newline at end of file diff --git a/simvue/config/parameters.py b/simvue/config/parameters.py new file mode 100644 index 00000000..3d0a0429 --- /dev/null +++ b/simvue/config/parameters.py @@ -0,0 +1,34 @@ +import logging +import pydantic +import typing + +import simvue.models as sv_models + +CONFIG_FILE_NAMES: list[str] = [ + "simvue.toml", + ".simvue.toml" +] + +logger = logging.getLogger(__file__) + +class ServerSpecifications(pydantic.BaseModel): + url: pydantic.AnyHttpUrl + token: str + + @pydantic.field_validator("url") + @classmethod + def url_to_str(cls, v: typing.Any) -> str: + return f"{v}" + + +class DefaultRunSpecifications(pydantic.BaseModel): + description: typing.Optional[str]=None + tags: list[str] | None=None + folder: str = pydantic.Field( + "/", + pattern=sv_models.FOLDER_REGEX + ) + + +class ClientGeneralOptions(pydantic.BaseModel): + debug: bool = False \ No newline at end of file diff --git a/simvue/config/user.py b/simvue/config/user.py new file mode 100644 index 00000000..7395efbc --- /dev/null +++ b/simvue/config/user.py @@ -0,0 +1,67 @@ +import pydantic +import logging +import toml +import typing +import os + +import simvue.utilities as sv_util + +from simvue.config.parameters import ( + ClientGeneralOptions, + ServerSpecifications, + DefaultRunSpecifications, + CONFIG_FILE_NAMES +) + +logger = logging.getLogger(__file__) + +class SimvueConfiguration(pydantic.BaseModel): + client: ClientGeneralOptions=ClientGeneralOptions() + server: ServerSpecifications=pydantic.Field( + ..., + description="Specifications for Simvue server" + ) + run: DefaultRunSpecifications=DefaultRunSpecifications() + + @classmethod + def fetch( + cls, + server_url: typing.Optional[str]=None, + server_token: typing.Optional[str]=None + ) -> "SimvueConfiguration": + _config_dict: dict[str, dict[str, str]] = {} + + try: + logger.info(f"Using config file '{cls.config_file()}'") + _config_dict = toml.load(cls.config_file()) + except FileNotFoundError: + if not server_token or not server_url: + _config_dict = {"server": {}} + logger.warning("No config file found, checking environment variables") + + _config_dict["server"] = _config_dict.get("server", {}) + + if server_url: + _config_dict["server"]["url"] = server_url + if server_token: + _config_dict["server"]["token"] = server_token + + _config_dict["server"]["url"] = os.environ.get( + "SIMVUE_URL", + _config_dict["server"].get("url") + ) + _config_dict["server"]["token"] = os.environ.get( + "SIMVUE_TOKEN", + _config_dict["server"].get("token") + ) + + return SimvueConfiguration(**_config_dict) + + @classmethod + def config_file(cls) -> str: + _config_file: typing.Optional[str] = sv_util.find_first_instance_of_file(CONFIG_FILE_NAMES, check_user_space=True) + if not _config_file: + raise FileNotFoundError( + "Failed to find Simvue configuration file" + ) + return _config_file \ No newline at end of file diff --git a/simvue/factory/__init__.py b/simvue/factory/__init__.py index 5cb001ef..1c3a6059 100644 --- a/simvue/factory/__init__.py +++ b/simvue/factory/__init__.py @@ -1,12 +1,18 @@ +import typing + from .remote import Remote from .offline import Offline from .base import SimvueBaseClass +if typing.TYPE_CHECKING: + from simvue.config import SimvueConfiguration + def Simvue( name: str, uniq_id: str, mode: str, + config: SimvueConfiguration, suppress_errors: bool = True ) -> SimvueBaseClass: if mode == "offline": diff --git a/simvue/factory/remote.py b/simvue/factory/remote.py index 7f6470c7..5b761e9c 100644 --- a/simvue/factory/remote.py +++ b/simvue/factory/remote.py @@ -2,6 +2,9 @@ import time import typing +if typing.TYPE_CHECKING: + from simvue.config import SimvueConfiguration + from simvue.api import post, put, get from simvue.utilities import ( get_auth, @@ -24,10 +27,10 @@ class Remote(SimvueBaseClass): Class which interacts with Simvue REST API """ - def __init__(self, name: str, uniq_id: str, suppress_errors: bool = True) -> None: - self._url, self._token = get_auth() + def __init__(self, name: str, uniq_id: str, config: SimvueConfiguration, suppress_errors: bool = True) -> None: + self._config = config self._headers: dict[str, str] = { - "Authorization": f"Bearer {self._token}", + "Authorization": f"Bearer {self._config.server.token}", "User-Agent": f"Simvue Python client {__version__}", } self._headers_mp: dict[str, str] = self._headers | { @@ -43,7 +46,7 @@ def create_run(self, data) -> tuple[typing.Optional[str], typing.Optional[int]]: logger.debug('Creating run with data: "%s"', data) try: - response = post(f"{self._url}/api/runs", self._headers, data) + response = post(f"{self._config.server.url}/api/runs", self._headers, data) except Exception as err: self._error(f"Exception creating run: {str(err)}") return (None, None) @@ -84,7 +87,7 @@ def update( logger.debug('Updating run with data: "%s"', data) try: - response = put(f"{self._url}/api/runs", self._headers, data) + response = put(f"{self._config.server.url}/api/runs", self._headers, data) except Exception as err: self._error(f"Exception updating run: {err}") return None @@ -106,11 +109,11 @@ def set_folder_details(self, data, run=None) -> typing.Optional[dict[str, typing """ Set folder details """ - if run is not None and not self._version: + if run is not None and not __version__: data["name"] = run try: - response = post(f"{self._url}/api/folders", self._headers, data) + response = post(f"{self._config.server.url}/api/folders", self._headers, data) except Exception as err: self._error(f"Exception creatig folder: {err}") return None @@ -127,7 +130,7 @@ def set_folder_details(self, data, run=None) -> typing.Optional[dict[str, typing logger.debug('Setting folder details with data: "%s"', data) try: - response = put(f"{self._url}/api/folders", self._headers, data) + response = put(f"{self._config.server.url}/api/folders", self._headers, data) except Exception as err: self._error(f"Exception setting folder details: {err}") return None @@ -156,7 +159,7 @@ def save_file(self, data: dict[str, typing.Any]) -> typing.Optional[dict[str, ty # Get presigned URL try: response = post( - f"{self._url}/api/artifacts", self._headers, prepare_for_api(data) + f"{self._config.server.url}/api/artifacts", self._headers, prepare_for_api(data) ) except Exception as err: self._error( @@ -238,7 +241,7 @@ def save_file(self, data: dict[str, typing.Any]) -> typing.Optional[dict[str, ty return None if storage_id: - path = f"{self._url}/api/runs/{self._id}/artifacts" + path = f"{self._config.server.url}/api/runs/{self._id}/artifacts" data["storage"] = storage_id try: @@ -268,7 +271,7 @@ def add_alert(self, data, run=None): logger.debug('Adding alert with data: "%s"', data) try: - response = post(f"{self._url}/api/alerts", self._headers, data) + response = post(f"{self._config.server.url}/api/alerts", self._headers, data) except Exception as err: self._error(f"Got exception when creating an alert: {str(err)}") return False @@ -292,7 +295,7 @@ def set_alert_state(self, alert_id, status) -> typing.Optional[dict[str, typing. """ data = {"run": self._id, "alert": alert_id, "status": status} try: - response = put(f"{self._url}/api/alerts/status", self._headers, data) + response = put(f"{self._config.server.url}/api/alerts/status", self._headers, data) except Exception as err: self._error(f"Got exception when setting alert state: {err}") return {} @@ -308,7 +311,7 @@ def list_alerts(self) -> list[dict[str, typing.Any]]: List alerts """ try: - response = get(f"{self._url}/api/alerts", self._headers) + response = get(f"{self._config.server.url}/api/alerts", self._headers) except Exception as err: self._error(f"Got exception when listing alerts: {str(err)}") return [] @@ -328,7 +331,7 @@ def send_metrics(self, data: dict[str, typing.Any]) -> typing.Optional[dict[str, try: response = post( - f"{self._url}/api/metrics", self._headers_mp, data, is_json=False + f"{self._config.server.url}/api/metrics", self._headers_mp, data, is_json=False ) except Exception as err: self._error(f"Exception sending metrics: {str(err)}") @@ -351,7 +354,7 @@ def send_event(self, data: dict[str, typing.Any]) -> typing.Optional[dict[str, t try: response = post( - f"{self._url}/api/events", self._headers_mp, data, is_json=False + f"{self._config.server.url}/api/events", self._headers_mp, data, is_json=False ) except Exception as err: self._error(f"Exception sending event: {str(err)}") @@ -374,7 +377,7 @@ def send_heartbeat(self) -> typing.Optional[dict[str, typing.Any]]: try: response = put( - f"{self._url}/api/runs/heartbeat", self._headers, {"id": self._id} + f"{self._config.server.url}/api/runs/heartbeat", self._headers, {"id": self._id} ) except Exception as err: self._error(f"Exception creating run: {str(err)}") @@ -393,7 +396,7 @@ def check_token(self) -> bool: """ Check token """ - if time.time() - get_expiry(self._token) > 0: + if time.time() - get_expiry(self._config.server.token) > 0: self._error("Token has expired") return False return True diff --git a/simvue/run.py b/simvue/run.py index c4d4baae..50589706 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -17,6 +17,7 @@ from .factory import Simvue from .serialization import Serializer from .models import RunInput +from .config import SimvueConfiguration from .utilities import get_auth, get_expiry, print_nice from .executor import Executor from pydantic import ValidationError @@ -165,11 +166,11 @@ def validate_timestamp(timestamp): return True -class Run(object): +class Run: """ Track simulation details based on token and URL """ - def __init__(self, mode='online'): + def __init__(self, mode='online', server_token=None, server_url=None, debug=False): self._uuid = str(uuid.uuid4()) self._mode = mode self._name = None @@ -187,8 +188,19 @@ def __init__(self, mode='online'): self._metrics_queue = None self._events_queue = None self._active = False - self._url, self._token = get_auth() - self._headers = {"Authorization": f"Bearer {self._token}"} + self._config = SimvueConfiguration.fetch( + server_token=server_token, + server_url=server_url + ) + + logging.getLogger(self.__class__.__module__).setLevel( + logging.DEBUG + if (debug is not None and debug) + or (debug is None and self._config.client.debug) + else logging.INFO + ) + + self._headers = {"Authorization": f"Bearer {self._config.server.token}"} self._simvue = None self._pid = 0 self._resources_metrics_interval = 30 @@ -226,7 +238,7 @@ def _check_token(self): """ Check if token is valid """ - if self._mode == 'online' and tm.time() - get_expiry(self._token) > 0: + if self._mode == 'online' and tm.time() - get_expiry(self._config.server.token) > 0: self._error('token has expired or is invalid') def _start(self, reconnect=False): @@ -266,7 +278,7 @@ def _start(self, reconnect=False): self._uuid, self._name, self._id, - self._url, + self._config.server.url, self._headers, self._mode, self._pid, @@ -290,13 +302,17 @@ def init(self, name=None, metadata={}, tags=[], description=None, folder='/', ru """ Initialise a run """ + description = description or self._config.run.description + tags = tags or self._config.run.tags + folder = folder or self._config.run.folder + if self._mode not in ('online', 'offline', 'disabled'): self._error('invalid mode specified, must be online, offline or disabled') if self._mode == 'disabled': return True - if not self._token or not self._url: + if not self._config.server.token or not self._config.run.description: self._error('Unable to get URL and token from environment variables or config file') if name: @@ -339,7 +355,7 @@ def init(self, name=None, metadata={}, tags=[], description=None, folder='/', ru except ValidationError as err: self._error(err) - self._simvue = Simvue(self._name, self._uuid, self._mode, self._suppress_errors) + self._simvue = Simvue(self._name, self._uuid, self._mode, self._config, self._suppress_errors) name, self._id = self._simvue.create_run(data) if not name: @@ -511,7 +527,7 @@ def reconnect(self, run_id, uid=None): self._uuid = uid self._id = run_id - self._simvue = Simvue(self._name, self._uuid, self._id, self._mode, self._suppress_errors) + self._simvue = Simvue(self._name, self._uuid, self._id, self._mode, self._config, self._suppress_errors) self._start(reconnect=True) def set_pid(self, pid): diff --git a/simvue/utilities.py b/simvue/utilities.py index 54fdc00b..9a30b730 100644 --- a/simvue/utilities.py +++ b/simvue/utilities.py @@ -3,11 +3,48 @@ import jwt import logging import os -import requests +import pathlib import typing logger = logging.getLogger(__name__) + +def find_first_instance_of_file( + file_names: typing.Union[list[str], str], check_user_space: bool = True +) -> typing.Optional[str]: + """Traverses a file hierarchy from bottom upwards to find file + + Returns the first instance of 'file_names' found when moving + upward from the current directory. + + Parameters + ---------- + file_name: list[str] | str + candidate names of file to locate + check_user_space: bool, optional + check the users home area if current working directory is not + within it. Default is True. + """ + if isinstance(file_names, str): + file_names = [file_names] + + for root, _, files in os.walk(os.getcwd(), topdown=False): + for file_name in file_names: + if file_name in files: + return os.path.join(root, file_name) + + # If the user is running on different mounted volume or outside + # of their user space then the above will not return the file + if check_user_space: + for file_name in file_names: + if os.path.exists( + _user_file := os.path.join(pathlib.Path.home(), file_name) + ): + return _user_file + + return None + + def check_extra(extra_name: str) -> typing.Callable: def decorator(class_func: typing.Callable) -> typing.Callable: def wrapper(self, *args, **kwargs) -> typing.Any: @@ -16,29 +53,37 @@ def wrapper(self, *args, **kwargs) -> typing.Any: import matplotlib import plotly except ImportError: - raise RuntimeError(f"Plotting features require the '{extra_name}' extension to Simvue") + raise RuntimeError( + f"Plotting features require the '{extra_name}' extension to Simvue" + ) elif extra_name == "torch": try: import torch except ImportError: - raise RuntimeError(f"PyTorch features require the '{extra_name}' extension to Simvue") + raise RuntimeError( + f"PyTorch features require the '{extra_name}' extension to Simvue" + ) elif extra_name == "dataset": try: import pandas import numpy except ImportError: - raise RuntimeError(f"Dataset features require the '{extra_name}' extension to Simvue") + raise RuntimeError( + f"Dataset features require the '{extra_name}' extension to Simvue" + ) else: raise RuntimeError(f"Unrecognised extra '{extra_name}'") return class_func(self, *args, **kwargs) + return wrapper + return decorator def skip_if_failed( failure_attr: str, ignore_exc_attr: str, - on_failure_return: typing.Optional[typing.Any] = None + on_failure_return: typing.Optional[typing.Any] = None, ) -> typing.Callable: """Decorator for ensuring if Simvue throws an exception any other code continues. @@ -62,13 +107,15 @@ def skip_if_failed( typing.Callable wrapped class method """ + def decorator(class_func: typing.Callable) -> typing.Callable: def wrapper(self, *args, **kwargs) -> typing.Any: - if ( - getattr(self, failure_attr, None) and - getattr(self, ignore_exc_attr, None) + if getattr(self, failure_attr, None) and getattr( + self, ignore_exc_attr, None ): - logger.debug(f"Skipping call to '{class_func.__name__}', client in fail state (see logs).") + logger.debug( + f"Skipping call to '{class_func.__name__}', client in fail state (see logs)." + ) return on_failure_return return class_func(self, *args, **kwargs) @@ -85,32 +132,39 @@ def get_auth(): token = None # Try reading from config file - for filename in (os.path.join(os.path.expanduser("~"), '.simvue.ini'), 'simvue.ini'): + for filename in ( + os.path.join(os.path.expanduser("~"), ".simvue.ini"), + "simvue.ini", + ): try: config = configparser.ConfigParser() config.read(filename) - token = config.get('server', 'token') - url = config.get('server', 'url') + token = config.get("server", "token") + url = config.get("server", "url") except: pass # Try environment variables - token = os.getenv('SIMVUE_TOKEN', token) - url = os.getenv('SIMVUE_URL', url) + token = os.getenv("SIMVUE_TOKEN", token) + url = os.getenv("SIMVUE_URL", url) return url, token + def get_offline_directory(): """ Get directory for offline cache """ directory = None - for filename in (os.path.join(os.path.expanduser("~"), '.simvue.ini'), 'simvue.ini'): + for filename in ( + os.path.join(os.path.expanduser("~"), ".simvue.ini"), + "simvue.ini", + ): try: config = configparser.ConfigParser() config.read(filename) - directory = config.get('offline', 'cache') + directory = config.get("offline", "cache") except: pass @@ -119,15 +173,17 @@ def get_offline_directory(): return directory + def create_file(filename): """ Create an empty file """ try: - with open(filename, 'w') as fh: - fh.write('') + with open(filename, "w") as fh: + fh.write("") except Exception as err: - logger.error('Unable to write file %s due to: %s', filename, str(err)) + logger.error("Unable to write file %s due to: %s", filename, str(err)) + def remove_file(filename): """ @@ -137,7 +193,8 @@ def remove_file(filename): try: os.remove(filename) except Exception as err: - logger.error('Unable to remove file %s due to: %s', filename, str(err)) + logger.error("Unable to remove file %s due to: %s", filename, str(err)) + def get_expiry(token): """ @@ -145,22 +202,24 @@ def get_expiry(token): """ expiry = 0 try: - expiry = jwt.decode(token, options={"verify_signature": False})['exp'] + expiry = jwt.decode(token, options={"verify_signature": False})["exp"] except: pass return expiry + def prepare_for_api(data_in, all=True): """ Remove references to pickling """ data = data_in.copy() - if 'pickled' in data: - del data['pickled'] - if 'pickledFile' in data and all: - del data['pickledFile'] + if "pickled" in data: + del data["pickled"] + if "pickledFile" in data and all: + del data["pickledFile"] return data + def print_nice(message): """ Log message in a way which hopefully can be distiguished from the user's application diff --git a/tests/refactor/test_config.py b/tests/refactor/test_config.py new file mode 100644 index 00000000..42f59b18 --- /dev/null +++ b/tests/refactor/test_config.py @@ -0,0 +1,98 @@ +import pytest +import uuid +import os.path +import pydantic +import pytest_mock +import tempfile +import simvue.config as sv_config + + +@pytest.mark.config +@pytest.mark.parametrize( + "use_env", (True, False), + ids=("use_env", "no_env") +) +@pytest.mark.parametrize( + "use_file", (None, "basic", "extended"), + ids=("no_file", "basic_file", "extended_file") +) +@pytest.mark.parametrize( + "use_args", (True, False), + ids=("args", "no_args") +) +def test_config_setup( + use_env: bool, + use_file: str | None, + use_args: bool, + 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('-', '') + _url: str = "https://simvue.example.com/" + _other_url: str = "http://simvue.example.com/" + _arg_url: str = "http://simvue.example.io/" + _description: str = "test case for runs" + _folder: str = "/test-case" + _tags: list[str] = ["tag-test", "other-tag"] + + if use_env: + monkeypatch.setenv("SIMVUE_TOKEN", _other_token) + monkeypatch.setenv("SIMVUE_URL", _other_url) + else: + monkeypatch.delenv("SIMVUE_TOKEN", False) + monkeypatch.delenv("SIMVUE_URL", False) + + with tempfile.TemporaryDirectory() as temp_d: + mocker.patch("pathlib.Path.home", lambda: temp_d) + if use_file: + with open(_config_file := os.path.join(temp_d, "simvue.toml"), "w") as out_f: + _lines: str = f""" +[server] +url = "{_url}" +token = "{_token}" +""" + if use_file == "extended": + _lines += f""" +[run] +description = "{_description}" +folder = "{_folder}" +tags = {_tags} +""" + out_f.write(_lines) + os.chdir(temp_d) + + if not use_file and not use_env and not use_args: + with pytest.raises(pydantic.ValidationError): + sv_config.SimvueConfiguration.fetch() + return + elif use_args: + _config = sv_config.SimvueConfiguration.fetch( + server_url=_arg_url, + server_token=_arg_token + ) + else: + _config = sv_config.SimvueConfiguration.fetch() + + if use_file: + assert _config.config_file() == _config_file + + if use_env: + assert _config.server.url == _other_url + assert _config.server.token == _other_token + elif use_args: + assert _config.server.url == _arg_url + assert _config.server.token == _arg_token + elif use_file: + assert _config.server.url == _url + assert _config.server.token == _token + + if use_file == "extended": + assert _config.run.description == _description + assert _config.run.folder == _folder + assert _config.run.tags == _tags + elif use_file: + assert _config.run.folder == "/" + assert not _config.run.description + assert not _config.run.tags \ No newline at end of file From f1978c6f0e92bbb59139a769fe063e9e37408127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Mon, 5 Aug 2024 13:35:14 +0100 Subject: [PATCH 02/17] Add legacy config support --- poetry.lock | 245 ++++++++++++++++------------- simvue/config/parameters.py | 33 ++-- simvue/config/user.py | 74 +++++++-- simvue/factory/proxy/__init__.py | 13 +- simvue/factory/proxy/offline.py | 3 + simvue/factory/proxy/remote.py | 17 +- simvue/run.py | 8 +- simvue/utilities.py | 13 +- tests/refactor/test_config.py | 39 +++-- tests/refactor/test_executor.py | 1 - tests/unit/test_suppress_errors.py | 3 +- 11 files changed, 272 insertions(+), 177 deletions(-) diff --git a/poetry.lock b/poetry.lock index 87dbb35d..bf2c229a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -211,63 +211,83 @@ test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] [[package]] name = "coverage" -version = "7.6.0" +version = "7.6.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd"}, - {file = "coverage-7.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c"}, - {file = "coverage-7.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463"}, - {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791"}, - {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c"}, - {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783"}, - {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6"}, - {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb"}, - {file = "coverage-7.6.0-cp310-cp310-win32.whl", hash = "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c"}, - {file = "coverage-7.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169"}, - {file = "coverage-7.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933"}, - {file = "coverage-7.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d"}, - {file = "coverage-7.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94"}, - {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1"}, - {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac"}, - {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57"}, - {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d"}, - {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63"}, - {file = "coverage-7.6.0-cp311-cp311-win32.whl", hash = "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713"}, - {file = "coverage-7.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1"}, - {file = "coverage-7.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b"}, - {file = "coverage-7.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8"}, - {file = "coverage-7.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5"}, - {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807"}, - {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382"}, - {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b"}, - {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee"}, - {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605"}, - {file = "coverage-7.6.0-cp312-cp312-win32.whl", hash = "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da"}, - {file = "coverage-7.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67"}, - {file = "coverage-7.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b"}, - {file = "coverage-7.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d"}, - {file = "coverage-7.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca"}, - {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b"}, - {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44"}, - {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03"}, - {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6"}, - {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b"}, - {file = "coverage-7.6.0-cp38-cp38-win32.whl", hash = "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428"}, - {file = "coverage-7.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8"}, - {file = "coverage-7.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c"}, - {file = "coverage-7.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2"}, - {file = "coverage-7.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390"}, - {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b"}, - {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450"}, - {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6"}, - {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166"}, - {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd"}, - {file = "coverage-7.6.0-cp39-cp39-win32.whl", hash = "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2"}, - {file = "coverage-7.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca"}, - {file = "coverage-7.6.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6"}, - {file = "coverage-7.6.0.tar.gz", hash = "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, + {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, + {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, + {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, + {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, ] [package.dependencies] @@ -700,40 +720,40 @@ files = [ [[package]] name = "matplotlib" -version = "3.9.1" +version = "3.9.0" description = "Python plotting package" optional = true python-versions = ">=3.9" files = [ - {file = "matplotlib-3.9.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7ccd6270066feb9a9d8e0705aa027f1ff39f354c72a87efe8fa07632f30fc6bb"}, - {file = "matplotlib-3.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:591d3a88903a30a6d23b040c1e44d1afdd0d778758d07110eb7596f811f31842"}, - {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2a59ff4b83d33bca3b5ec58203cc65985367812cb8c257f3e101632be86d92"}, - {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc001516ffcf1a221beb51198b194d9230199d6842c540108e4ce109ac05cc0"}, - {file = "matplotlib-3.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:83c6a792f1465d174c86d06f3ae85a8fe36e6f5964633ae8106312ec0921fdf5"}, - {file = "matplotlib-3.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:421851f4f57350bcf0811edd754a708d2275533e84f52f6760b740766c6747a7"}, - {file = "matplotlib-3.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b3fce58971b465e01b5c538f9d44915640c20ec5ff31346e963c9e1cd66fa812"}, - {file = "matplotlib-3.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a973c53ad0668c53e0ed76b27d2eeeae8799836fd0d0caaa4ecc66bf4e6676c0"}, - {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd5acf8f3ef43f7532c2f230249720f5dc5dd40ecafaf1c60ac8200d46d7eb"}, - {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab38a4f3772523179b2f772103d8030215b318fef6360cb40558f585bf3d017f"}, - {file = "matplotlib-3.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2315837485ca6188a4b632c5199900e28d33b481eb083663f6a44cfc8987ded3"}, - {file = "matplotlib-3.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0c977c5c382f6696caf0bd277ef4f936da7e2aa202ff66cad5f0ac1428ee15b"}, - {file = "matplotlib-3.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:565d572efea2b94f264dd86ef27919515aa6d629252a169b42ce5f570db7f37b"}, - {file = "matplotlib-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d397fd8ccc64af2ec0af1f0efc3bacd745ebfb9d507f3f552e8adb689ed730a"}, - {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26040c8f5121cd1ad712abffcd4b5222a8aec3a0fe40bc8542c94331deb8780d"}, - {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12cb1837cffaac087ad6b44399d5e22b78c729de3cdae4629e252067b705e2b"}, - {file = "matplotlib-3.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0e835c6988edc3d2d08794f73c323cc62483e13df0194719ecb0723b564e0b5c"}, - {file = "matplotlib-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:44a21d922f78ce40435cb35b43dd7d573cf2a30138d5c4b709d19f00e3907fd7"}, - {file = "matplotlib-3.9.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0c584210c755ae921283d21d01f03a49ef46d1afa184134dd0f95b0202ee6f03"}, - {file = "matplotlib-3.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11fed08f34fa682c2b792942f8902e7aefeed400da71f9e5816bea40a7ce28fe"}, - {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0000354e32efcfd86bda75729716b92f5c2edd5b947200be9881f0a671565c33"}, - {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db17fea0ae3aceb8e9ac69c7e3051bae0b3d083bfec932240f9bf5d0197a049"}, - {file = "matplotlib-3.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:208cbce658b72bf6a8e675058fbbf59f67814057ae78165d8a2f87c45b48d0ff"}, - {file = "matplotlib-3.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:dc23f48ab630474264276be156d0d7710ac6c5a09648ccdf49fef9200d8cbe80"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3fda72d4d472e2ccd1be0e9ccb6bf0d2eaf635e7f8f51d737ed7e465ac020cb3"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:84b3ba8429935a444f1fdc80ed930babbe06725bcf09fbeb5c8757a2cd74af04"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b918770bf3e07845408716e5bbda17eadfc3fcbd9307dc67f37d6cf834bb3d98"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f1f2e5d29e9435c97ad4c36fb6668e89aee13d48c75893e25cef064675038ac9"}, - {file = "matplotlib-3.9.1.tar.gz", hash = "sha256:de06b19b8db95dd33d0dc17c926c7c9ebed9f572074b6fac4f65068a6814d010"}, + {file = "matplotlib-3.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2bcee1dffaf60fe7656183ac2190bd630842ff87b3153afb3e384d966b57fe56"}, + {file = "matplotlib-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f988bafb0fa39d1074ddd5bacd958c853e11def40800c5824556eb630f94d3b"}, + {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe428e191ea016bb278758c8ee82a8129c51d81d8c4bc0846c09e7e8e9057241"}, + {file = "matplotlib-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaf3978060a106fab40c328778b148f590e27f6fa3cd15a19d6892575bce387d"}, + {file = "matplotlib-3.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2e7f03e5cbbfacdd48c8ea394d365d91ee8f3cae7e6ec611409927b5ed997ee4"}, + {file = "matplotlib-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:13beb4840317d45ffd4183a778685e215939be7b08616f431c7795276e067463"}, + {file = "matplotlib-3.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:063af8587fceeac13b0936c42a2b6c732c2ab1c98d38abc3337e430e1ff75e38"}, + {file = "matplotlib-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a2fa6d899e17ddca6d6526cf6e7ba677738bf2a6a9590d702c277204a7c6152"}, + {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550cdda3adbd596078cca7d13ed50b77879104e2e46392dcd7c75259d8f00e85"}, + {file = "matplotlib-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cce0f31b351e3551d1f3779420cf8f6ec0d4a8cf9c0237a3b549fd28eb4abb"}, + {file = "matplotlib-3.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c53aeb514ccbbcbab55a27f912d79ea30ab21ee0531ee2c09f13800efb272674"}, + {file = "matplotlib-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5be985db2596d761cdf0c2eaf52396f26e6a64ab46bd8cd810c48972349d1be"}, + {file = "matplotlib-3.9.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c79f3a585f1368da6049318bdf1f85568d8d04b2e89fc24b7e02cc9b62017382"}, + {file = "matplotlib-3.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bdd1ecbe268eb3e7653e04f451635f0fb0f77f07fd070242b44c076c9106da84"}, + {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e85a1a6d732f645f1403ce5e6727fd9418cd4574521d5803d3d94911038e5"}, + {file = "matplotlib-3.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a490715b3b9984fa609116481b22178348c1a220a4499cda79132000a79b4db"}, + {file = "matplotlib-3.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8146ce83cbc5dc71c223a74a1996d446cd35cfb6a04b683e1446b7e6c73603b7"}, + {file = "matplotlib-3.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:d91a4ffc587bacf5c4ce4ecfe4bcd23a4b675e76315f2866e588686cc97fccdf"}, + {file = "matplotlib-3.9.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:616fabf4981a3b3c5a15cd95eba359c8489c4e20e03717aea42866d8d0465956"}, + {file = "matplotlib-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd53c79fd02f1c1808d2cfc87dd3cf4dbc63c5244a58ee7944497107469c8d8a"}, + {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06a478f0d67636554fa78558cfbcd7b9dba85b51f5c3b5a0c9be49010cf5f321"}, + {file = "matplotlib-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c40af649d19c85f8073e25e5806926986806fa6d54be506fbf02aef47d5a89"}, + {file = "matplotlib-3.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52146fc3bd7813cc784562cb93a15788be0b2875c4655e2cc6ea646bfa30344b"}, + {file = "matplotlib-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:0fc51eaa5262553868461c083d9adadb11a6017315f3a757fc45ec6ec5f02888"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bd4f2831168afac55b881db82a7730992aa41c4f007f1913465fb182d6fb20c0"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:290d304e59be2b33ef5c2d768d0237f5bd132986bdcc66f80bc9bcc300066a03"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff2e239c26be4f24bfa45860c20ffccd118d270c5b5d081fa4ea409b5469fcd"}, + {file = "matplotlib-3.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:af4001b7cae70f7eaacfb063db605280058246de590fa7874f00f62259f2df7e"}, + {file = "matplotlib-3.9.0.tar.gz", hash = "sha256:e6d29ea6c19e34b30fb7d88b7081f869a03014f66fe06d62cc77d5a6ea88ed7a"}, ] [package.dependencies] @@ -1234,19 +1254,19 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pyjwt" -version = "2.8.0" +version = "2.9.0" description = "JSON Web Token implementation in Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, - {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, + {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, + {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, ] [package.extras] crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] @@ -1445,45 +1465,46 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.5.5" +version = "0.5.6" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.5.5-py3-none-linux_armv6l.whl", hash = "sha256:605d589ec35d1da9213a9d4d7e7a9c761d90bba78fc8790d1c5e65026c1b9eaf"}, - {file = "ruff-0.5.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00817603822a3e42b80f7c3298c8269e09f889ee94640cd1fc7f9329788d7bf8"}, - {file = "ruff-0.5.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:187a60f555e9f865a2ff2c6984b9afeffa7158ba6e1eab56cb830404c942b0f3"}, - {file = "ruff-0.5.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe26fc46fa8c6e0ae3f47ddccfbb136253c831c3289bba044befe68f467bfb16"}, - {file = "ruff-0.5.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad25dd9c5faac95c8e9efb13e15803cd8bbf7f4600645a60ffe17c73f60779b"}, - {file = "ruff-0.5.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f70737c157d7edf749bcb952d13854e8f745cec695a01bdc6e29c29c288fc36e"}, - {file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:cfd7de17cef6ab559e9f5ab859f0d3296393bc78f69030967ca4d87a541b97a0"}, - {file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a09b43e02f76ac0145f86a08e045e2ea452066f7ba064fd6b0cdccb486f7c3e7"}, - {file = "ruff-0.5.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0b856cb19c60cd40198be5d8d4b556228e3dcd545b4f423d1ad812bfdca5884"}, - {file = "ruff-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3687d002f911e8a5faf977e619a034d159a8373514a587249cc00f211c67a091"}, - {file = "ruff-0.5.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ac9dc814e510436e30d0ba535f435a7f3dc97f895f844f5b3f347ec8c228a523"}, - {file = "ruff-0.5.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:af9bdf6c389b5add40d89b201425b531e0a5cceb3cfdcc69f04d3d531c6be74f"}, - {file = "ruff-0.5.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d40a8533ed545390ef8315b8e25c4bb85739b90bd0f3fe1280a29ae364cc55d8"}, - {file = "ruff-0.5.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cab904683bf9e2ecbbe9ff235bfe056f0eba754d0168ad5407832928d579e7ab"}, - {file = "ruff-0.5.5-py3-none-win32.whl", hash = "sha256:696f18463b47a94575db635ebb4c178188645636f05e934fdf361b74edf1bb2d"}, - {file = "ruff-0.5.5-py3-none-win_amd64.whl", hash = "sha256:50f36d77f52d4c9c2f1361ccbfbd09099a1b2ea5d2b2222c586ab08885cf3445"}, - {file = "ruff-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3191317d967af701f1b73a31ed5788795936e423b7acce82a2b63e26eb3e89d6"}, - {file = "ruff-0.5.5.tar.gz", hash = "sha256:cc5516bdb4858d972fbc31d246bdb390eab8df1a26e2353be2dbc0c2d7f5421a"}, + {file = "ruff-0.5.6-py3-none-linux_armv6l.whl", hash = "sha256:a0ef5930799a05522985b9cec8290b185952f3fcd86c1772c3bdbd732667fdcd"}, + {file = "ruff-0.5.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b652dc14f6ef5d1552821e006f747802cc32d98d5509349e168f6bf0ee9f8f42"}, + {file = "ruff-0.5.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:80521b88d26a45e871f31e4b88938fd87db7011bb961d8afd2664982dfc3641a"}, + {file = "ruff-0.5.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9bc8f328a9f1309ae80e4d392836e7dbc77303b38ed4a7112699e63d3b066ab"}, + {file = "ruff-0.5.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d394940f61f7720ad371ddedf14722ee1d6250fd8d020f5ea5a86e7be217daf"}, + {file = "ruff-0.5.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111a99cdb02f69ddb2571e2756e017a1496c2c3a2aeefe7b988ddab38b416d36"}, + {file = "ruff-0.5.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e395daba77a79f6dc0d07311f94cc0560375ca20c06f354c7c99af3bf4560c5d"}, + {file = "ruff-0.5.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c476acb43c3c51e3c614a2e878ee1589655fa02dab19fe2db0423a06d6a5b1b6"}, + {file = "ruff-0.5.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2ff8003f5252fd68425fd53d27c1f08b201d7ed714bb31a55c9ac1d4c13e2eb"}, + {file = "ruff-0.5.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c94e084ba3eaa80c2172918c2ca2eb2230c3f15925f4ed8b6297260c6ef179ad"}, + {file = "ruff-0.5.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f77c1c3aa0669fb230b06fb24ffa3e879391a3ba3f15e3d633a752da5a3e670"}, + {file = "ruff-0.5.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f908148c93c02873210a52cad75a6eda856b2cbb72250370ce3afef6fb99b1ed"}, + {file = "ruff-0.5.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:563a7ae61ad284187d3071d9041c08019975693ff655438d8d4be26e492760bd"}, + {file = "ruff-0.5.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:94fe60869bfbf0521e04fd62b74cbca21cbc5beb67cbb75ab33fe8c174f54414"}, + {file = "ruff-0.5.6-py3-none-win32.whl", hash = "sha256:e6a584c1de6f8591c2570e171cc7ce482bb983d49c70ddf014393cd39e9dfaed"}, + {file = "ruff-0.5.6-py3-none-win_amd64.whl", hash = "sha256:d7fe7dccb1a89dc66785d7aa0ac283b2269712d8ed19c63af908fdccca5ccc1a"}, + {file = "ruff-0.5.6-py3-none-win_arm64.whl", hash = "sha256:57c6c0dd997b31b536bff49b9eee5ed3194d60605a4427f735eeb1f9c1b8d264"}, + {file = "ruff-0.5.6.tar.gz", hash = "sha256:07c9e3c2a8e1fe377dd460371c3462671a728c981c3205a5217291422209f642"}, ] [[package]] name = "setuptools" -version = "70.3.0" +version = "72.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"}, - {file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"}, + {file = "setuptools-72.1.0-py3-none-any.whl", hash = "sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1"}, + {file = "setuptools-72.1.0.tar.gz", hash = "sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec"}, ] [package.extras] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" diff --git a/simvue/config/parameters.py b/simvue/config/parameters.py index 3d0a0429..ba05e92e 100644 --- a/simvue/config/parameters.py +++ b/simvue/config/parameters.py @@ -1,34 +1,45 @@ import logging +import time import pydantic import typing +import pathlib import simvue.models as sv_models +from simvue.utilities import get_expiry -CONFIG_FILE_NAMES: list[str] = [ - "simvue.toml", - ".simvue.toml" +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: str + 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) -> bool: + if not (expiry := get_expiry(v)): + raise AssertionError("Failed to parse Simvue token - invalid token form") + if time.time() - expiry > 0: + raise AssertionError("Simvue token has expired") + return v + class DefaultRunSpecifications(pydantic.BaseModel): - description: typing.Optional[str]=None - tags: list[str] | None=None - folder: str = pydantic.Field( - "/", - pattern=sv_models.FOLDER_REGEX - ) + description: typing.Optional[str] = None + tags: list[str] | None = None + folder: str = pydantic.Field("/", pattern=sv_models.FOLDER_REGEX) class ClientGeneralOptions(pydantic.BaseModel): - debug: bool = False \ No newline at end of file + debug: bool = False diff --git a/simvue/config/user.py b/simvue/config/user.py index 49026b01..fdc2eecf 100644 --- a/simvue/config/user.py +++ b/simvue/config/user.py @@ -2,6 +2,10 @@ import logging import os import typing +import pathlib +import configparser +import contextlib +import warnings import pydantic import toml @@ -9,6 +13,7 @@ import simvue.utilities as sv_util from simvue.config.parameters import ( CONFIG_FILE_NAMES, + CONFIG_INI_FILE_NAMES, ClientGeneralOptions, DefaultRunSpecifications, ServerSpecifications, @@ -18,12 +23,37 @@ class SimvueConfiguration(pydantic.BaseModel): + # Hide values as they contain token and URL + model_config = pydantic.ConfigDict(hide_input_in_errors=True) client: ClientGeneralOptions = ClientGeneralOptions() server: ServerSpecifications = pydantic.Field( ..., description="Specifications for Simvue server" ) run: DefaultRunSpecifications = DefaultRunSpecifications() + @classmethod + def _parse_ini_config(cls, ini_file: pathlib.Path) -> dict[str, dict[str, str]]: + """Parse a legacy INI config file if found.""" + # NOTE: Legacy INI support, this will be removed + warnings.warn( + "Support for legacy INI based configuration files will be dropped in simvue>=1.2, " + "please switch to TOML based configuration.", + DeprecationWarning, + stacklevel=2, + ) + + config_dict: dict[str, dict[str, str]] = {"server": {}} + + with contextlib.suppress(Exception): + parser = configparser.ConfigParser() + parser.read(f"{ini_file}") + if token := parser.get("server", "token"): + config_dict["server"]["token"] = token + if url := parser.get("server", "url"): + config_dict["server"]["url"] = url + + return config_dict + @classmethod def fetch( cls, @@ -34,7 +64,13 @@ def fetch( try: logger.info(f"Using config file '{cls.config_file()}'") - _config_dict = toml.load(cls.config_file()) + + # NOTE: Legacy INI support, this will be removed + if cls.config_file().suffix == ".toml": + _config_dict = toml.load(cls.config_file()) + else: + _config_dict = cls._parse_ini_config(cls.config_file()) + except FileNotFoundError: if not server_token or not server_url: _config_dict = {"server": {}} @@ -42,32 +78,46 @@ def fetch( _config_dict["server"] = _config_dict.get("server", {}) - _server_url = os.environ.get("SIMVUE_URL", _config_dict["server"].get("url")) - - if not _server_url: - raise RuntimeError("No server URL was specified") + # Ranking of configurations for token and URl is: + # Envionment Variables > Run Definition > Configuration File - _config_dict["server"]["url"] = _server_url + _server_url = os.environ.get( + "SIMVUE_URL", server_url or _config_dict["server"].get("url") + ) _server_token = os.environ.get( - "SIMVUE_TOKEN", _config_dict["server"].get("token") + "SIMVUE_TOKEN", server_token or _config_dict["server"].get("token") ) + if not _server_url: + raise RuntimeError("No server URL was specified") + if not _server_token: raise RuntimeError("No server token was specified") _config_dict["server"]["token"] = _server_token + _config_dict["server"]["url"] = _server_url return SimvueConfiguration(**_config_dict) @classmethod @functools.lru_cache - def config_file(cls) -> str: - _config_file: typing.Optional[str] = sv_util.find_first_instance_of_file( - CONFIG_FILE_NAMES, check_user_space=True + def config_file(cls) -> pathlib.Path: + _config_file: typing.Optional[pathlib.Path] = ( + sv_util.find_first_instance_of_file( + CONFIG_FILE_NAMES, check_user_space=True + ) ) + + # NOTE: Legacy INI support, this will be removed + if not _config_file: + _config_file: typing.Optional[pathlib.Path] = ( + sv_util.find_first_instance_of_file( + CONFIG_INI_FILE_NAMES, check_user_space=True + ) + ) + if not _config_file: raise FileNotFoundError("Failed to find Simvue configuration file") - else: - logger.debug(f"Using config file '{_config_file}'") + return _config_file diff --git a/simvue/factory/proxy/__init__.py b/simvue/factory/proxy/__init__.py index 0f3267d1..91065558 100644 --- a/simvue/factory/proxy/__init__.py +++ b/simvue/factory/proxy/__init__.py @@ -9,15 +9,22 @@ if typing.TYPE_CHECKING: from .base import SimvueBaseClass + from simvue.config import SimvueConfiguration from .offline import Offline from .remote import Remote def Simvue( - name: typing.Optional[str], uniq_id: str, mode: str, suppress_errors: bool = True + name: typing.Optional[str], + uniq_id: str, + mode: str, + config: "SimvueConfiguration", + suppress_errors: bool = True, ) -> "SimvueBaseClass": if mode == "offline": - return Offline(name, uniq_id, suppress_errors) + return Offline(name=name, uniq_id=uniq_id, suppress_errors=suppress_errors) else: - return Remote(name, uniq_id, suppress_errors) + return Remote( + name=name, uniq_id=uniq_id, config=config, suppress_errors=suppress_errors + ) diff --git a/simvue/factory/proxy/offline.py b/simvue/factory/proxy/offline.py index 31f20e3f..4d4034e5 100644 --- a/simvue/factory/proxy/offline.py +++ b/simvue/factory/proxy/offline.py @@ -16,6 +16,9 @@ skip_if_failed, ) +if typing.TYPE_CHECKING: + pass + logger = logging.getLogger(__name__) diff --git a/simvue/factory/proxy/remote.py b/simvue/factory/proxy/remote.py index 3656c8e4..523c17da 100644 --- a/simvue/factory/proxy/remote.py +++ b/simvue/factory/proxy/remote.py @@ -1,5 +1,4 @@ import logging -import time import typing if typing.TYPE_CHECKING: @@ -7,7 +6,7 @@ from simvue.api import get, post, put from simvue.factory.proxy.base import SimvueBaseClass -from simvue.utilities import get_expiry, prepare_for_api, skip_if_failed +from simvue.utilities import prepare_for_api, skip_if_failed from simvue.version import __version__ logger = logging.getLogger(__name__) @@ -478,20 +477,6 @@ def send_heartbeat(self) -> typing.Optional[dict[str, typing.Any]]: self._error(f"Got status code {response.status_code} when sending heartbeat") return None - @skip_if_failed("_aborted", "_suppress_errors", False) - def check_token(self) -> bool: - """ - Check token - """ - if not (expiry := get_expiry(self._config.server.token)): - self._error("Failed to parse user token") - return False - - if time.time() - expiry > 0: - self._error("Token has expired") - return False - return True - @skip_if_failed("_aborted", "_suppress_errors", False) def get_abort_status(self) -> bool: logger.debug("Retrieving alert status") diff --git a/simvue/run.py b/simvue/run.py index 973dbe35..82a343bd 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -90,9 +90,9 @@ class Run: @pydantic.validate_call def __init__( self, + mode: typing.Literal["online", "offline", "disabled"] = "online", server_token: typing.Optional[str] = None, server_url: typing.Optional[str] = None, - mode: typing.Literal["online", "offline", "disabled"] = "online", debug: bool = False, ) -> None: """Initialise a new Simvue run @@ -644,7 +644,11 @@ def init( return False self._simvue = Simvue( - self._name, self._uuid, self._mode, self._config, self._suppress_errors + name=self._name, + uniq_id=self._uuid, + mode=self._mode, + config=self._config, + suppress_errors=self._suppress_errors, ) name, self._id = self._simvue.create_run(data) diff --git a/simvue/utilities.py b/simvue/utilities.py index 05d4c0af..de6c4d2b 100644 --- a/simvue/utilities.py +++ b/simvue/utilities.py @@ -25,7 +25,7 @@ def find_first_instance_of_file( file_names: typing.Union[list[str], str], check_user_space: bool = True -) -> typing.Optional[str]: +) -> typing.Optional[pathlib.Path]: """Traverses a file hierarchy from bottom upwards to find file Returns the first instance of 'file_names' found when moving @@ -38,6 +38,11 @@ def find_first_instance_of_file( check_user_space: bool, optional check the users home area if current working directory is not within it. Default is True. + + Returns + ------- + pathlib.Path | None + first matching file if found """ if isinstance(file_names, str): file_names = [file_names] @@ -45,15 +50,13 @@ def find_first_instance_of_file( for root, _, files in os.walk(os.getcwd(), topdown=False): for file_name in file_names: if file_name in files: - return os.path.join(root, file_name) + return pathlib.Path(root).joinpath(file_name) # If the user is running on different mounted volume or outside # of their user space then the above will not return the file if check_user_space: for file_name in file_names: - if os.path.exists( - _user_file := os.path.join(pathlib.Path.home(), file_name) - ): + if os.path.exists(_user_file := pathlib.Path.home().joinpath(file_name)): return _user_file return None diff --git a/tests/refactor/test_config.py b/tests/refactor/test_config.py index 42f59b18..0a99eb26 100644 --- a/tests/refactor/test_config.py +++ b/tests/refactor/test_config.py @@ -1,10 +1,10 @@ import pytest import uuid -import os.path +import pathlib import pydantic import pytest_mock import tempfile -import simvue.config as sv_config +import simvue.config @pytest.mark.config @@ -13,8 +13,8 @@ ids=("use_env", "no_env") ) @pytest.mark.parametrize( - "use_file", (None, "basic", "extended"), - ids=("no_file", "basic_file", "extended_file") + "use_file", (None, "basic", "extended", "ini"), + ids=("no_file", "basic_file", "extended_file", "legacy_file") ) @pytest.mark.parametrize( "use_args", (True, False), @@ -45,14 +45,22 @@ def test_config_setup( monkeypatch.delenv("SIMVUE_URL", False) with tempfile.TemporaryDirectory() as temp_d: - mocker.patch("pathlib.Path.home", lambda: temp_d) + _config_file = None if use_file: - with open(_config_file := os.path.join(temp_d, "simvue.toml"), "w") as out_f: - _lines: str = f""" + with open(_config_file := pathlib.Path(temp_d).joinpath(f"simvue.{'toml' if use_file != 'ini' else 'ini'}"), "w") as out_f: + if use_file != "ini": + _lines: str = f""" [server] url = "{_url}" token = "{_token}" """ + else: + _lines = f""" +[server] +url = {_url} +token = {_token} +""" + if use_file == "extended": _lines += f""" [run] @@ -61,19 +69,21 @@ def test_config_setup( tags = {_tags} """ out_f.write(_lines) - os.chdir(temp_d) + simvue.config.SimvueConfiguration.config_file.cache_clear() + + mocker.patch("simvue.config.user.sv_util.find_first_instance_of_file", lambda *_, **__: _config_file) if not use_file and not use_env and not use_args: - with pytest.raises(pydantic.ValidationError): - sv_config.SimvueConfiguration.fetch() + with pytest.raises(RuntimeError): + simvue.config.SimvueConfiguration.fetch() return elif use_args: - _config = sv_config.SimvueConfiguration.fetch( + _config = simvue.config.SimvueConfiguration.fetch( server_url=_arg_url, server_token=_arg_token ) else: - _config = sv_config.SimvueConfiguration.fetch() + _config = simvue.config.SimvueConfiguration.fetch() if use_file: assert _config.config_file() == _config_file @@ -95,4 +105,7 @@ def test_config_setup( elif use_file: assert _config.run.folder == "/" assert not _config.run.description - assert not _config.run.tags \ No newline at end of file + assert not _config.run.tags + + simvue.config.SimvueConfiguration.config_file.cache_clear() + diff --git a/tests/refactor/test_executor.py b/tests/refactor/test_executor.py index d5a16fae..05f5ce26 100644 --- a/tests/refactor/test_executor.py +++ b/tests/refactor/test_executor.py @@ -131,7 +131,6 @@ def test_add_process_command_assembly(request: pytest.FixtureRequest) -> None: def test_completion_callbacks_var_change(request: pytest.FixtureRequest) -> None: success: dict[str, bool] = {"complete": False} def completion_callback(*_, success: dict[str, bool]=success, **__): - print("YH BOI") success["complete"] = True with simvue.Run() as run: diff --git a/tests/unit/test_suppress_errors.py b/tests/unit/test_suppress_errors.py index 650ba16c..8a1b9f29 100644 --- a/tests/unit/test_suppress_errors.py +++ b/tests/unit/test_suppress_errors.py @@ -13,7 +13,6 @@ def test_suppress_errors_false(): suppress_errors=False, disable_resources_metrics=123, ) - print(e.value) assert "Input should be a valid boolean, unable to interpret input" in f"{e.value}" def test_suppress_errors_true(caplog): @@ -44,4 +43,4 @@ def test_suppress_errors_default(caplog): caplog.set_level(logging.ERROR) - assert "Input should be a valid boolean, unable to interpret input" in caplog.text \ No newline at end of file + assert "Input should be a valid boolean, unable to interpret input" in caplog.text From 0c3e61793f1fa740894d656d6f98cfe4ad61c3a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Mon, 5 Aug 2024 15:36:42 +0100 Subject: [PATCH 03/17] Fix tests for config updates --- simvue/config/parameters.py | 6 +++--- simvue/factory/proxy/base.py | 4 ---- simvue/factory/proxy/offline.py | 4 ---- simvue/factory/proxy/remote.py | 5 +++-- simvue/run.py | 3 --- simvue/sender.py | 17 +++++++++-------- tests/refactor/conftest.py | 1 + tests/refactor/test_config.py | 9 ++++++--- 8 files changed, 22 insertions(+), 27 deletions(-) diff --git a/simvue/config/parameters.py b/simvue/config/parameters.py index ba05e92e..97a90ca5 100644 --- a/simvue/config/parameters.py +++ b/simvue/config/parameters.py @@ -27,12 +27,12 @@ def url_to_str(cls, v: typing.Any) -> str: return f"{v}" @pydantic.field_validator("token") - def check_token(cls, v: typing.Any) -> bool: - if not (expiry := get_expiry(v)): + def check_token(cls, v: pydantic.SecretStr) -> str: + if not (expiry := get_expiry(v.get_secret_value())): raise AssertionError("Failed to parse Simvue token - invalid token form") if time.time() - expiry > 0: raise AssertionError("Simvue token has expired") - return v + return v.get_secret_value() class DefaultRunSpecifications(pydantic.BaseModel): diff --git a/simvue/factory/proxy/base.py b/simvue/factory/proxy/base.py index 1ca9060c..2dc3c13d 100644 --- a/simvue/factory/proxy/base.py +++ b/simvue/factory/proxy/base.py @@ -86,10 +86,6 @@ def send_event( def send_heartbeat(self) -> typing.Optional[dict[str, typing.Any]]: pass - @abc.abstractmethod - def check_token(self) -> bool: - pass - @abc.abstractmethod def get_abort_status(self) -> bool: pass diff --git a/simvue/factory/proxy/offline.py b/simvue/factory/proxy/offline.py index 4d4034e5..5ec1ccc6 100644 --- a/simvue/factory/proxy/offline.py +++ b/simvue/factory/proxy/offline.py @@ -219,7 +219,3 @@ def send_heartbeat(self) -> typing.Optional[dict[str, typing.Any]]: ) pathlib.Path(os.path.join(self._directory, "heartbeat")).touch() return {"success": True} - - @skip_if_failed("_aborted", "_suppress_errors", False) - def check_token(self) -> bool: - return True diff --git a/simvue/factory/proxy/remote.py b/simvue/factory/proxy/remote.py index 523c17da..f52900f0 100644 --- a/simvue/factory/proxy/remote.py +++ b/simvue/factory/proxy/remote.py @@ -37,7 +37,6 @@ def __init__( "Content-Type": "application/msgpack" } super().__init__(name, uniq_id, suppress_errors) - self.check_token() self._id = uniq_id @@ -45,7 +44,9 @@ def __init__( def list_tags(self) -> list[str]: logger.debug("Retrieving existing tags") try: - response = get(f"{self._url}/api/runs/{self._id}", self._headers) + response = get( + f"{self._config.server.url}/api/runs/{self._id}", self._headers + ) except Exception as err: self._error(f"Exception retrieving tags: {str(err)}") return [] diff --git a/simvue/run.py b/simvue/run.py index 0b654879..f159dea2 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -434,9 +434,6 @@ def _start(self, reconnect: bool = False) -> bool: logger.debug("Starting run") - if self._simvue and not self._simvue.check_token(): - return False - data: dict[str, typing.Any] = {"status": self._status} if reconnect: diff --git a/simvue/sender.py b/simvue/sender.py index 8bee03d7..7a95b610 100644 --- a/simvue/sender.py +++ b/simvue/sender.py @@ -8,6 +8,8 @@ import msgpack +from simvue.config.user import SimvueConfiguration + from .factory.proxy.remote import Remote from .utilities import create_file, get_offline_directory, remove_file @@ -170,11 +172,11 @@ def process(run): # Create run if it hasn't previously been created created_file = f"{current}/init" name = None + config = SimvueConfiguration() if not os.path.isfile(created_file): - remote = Remote(run_init["name"], id, suppress_errors=False) - - # Check token - remote.check_token() + remote = Remote( + name=run_init["name"], uniq_id=id, config=config, suppress_errors=False + ) name, run_id = remote.create_run(run_init) if name: @@ -187,10 +189,9 @@ def process(run): else: name, run_id = get_details(created_file) run_init["name"] = name - remote = Remote(run_init["name"], run_id, suppress_errors=False) - - # Check token - remote.check_token() + remote = Remote( + name=run_init["name"], uniq_id=run_id, config=config, suppress_errors=False + ) if status == "running": # Check for recent heartbeat diff --git a/tests/refactor/conftest.py b/tests/refactor/conftest.py index 09928da0..778d29cc 100644 --- a/tests/refactor/conftest.py +++ b/tests/refactor/conftest.py @@ -138,3 +138,4 @@ def setup_test_run(run: sv_run.Run, create_objects: bool, request: pytest.Fixtur time.sleep(1.) return TEST_DATA + diff --git a/tests/refactor/test_config.py b/tests/refactor/test_config.py index 0a99eb26..30e1d412 100644 --- a/tests/refactor/test_config.py +++ b/tests/refactor/test_config.py @@ -1,10 +1,10 @@ import pytest +import typing import uuid import pathlib -import pydantic import pytest_mock import tempfile -import simvue.config +from simvue.config import SimvueConfiguration @pytest.mark.config @@ -69,10 +69,13 @@ def test_config_setup( tags = {_tags} """ out_f.write(_lines) - simvue.config.SimvueConfiguration.config_file.cache_clear() + SimvueConfiguration.config_file.cache_clear() + mocker.patch("simvue.config.parameters.get_expiry", lambda *_, **__: 1e10) mocker.patch("simvue.config.user.sv_util.find_first_instance_of_file", lambda *_, **__: _config_file) + import simvue.config + if not use_file and not use_env and not use_args: with pytest.raises(RuntimeError): simvue.config.SimvueConfiguration.fetch() From 4fbd2381460ecf82481179ac6eea25085fc2680d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Mon, 5 Aug 2024 15:56:07 +0100 Subject: [PATCH 04/17] Fixed all references to self._url in Run --- simvue/factory/proxy/remote.py | 4 +++- tests/refactor/test_run_class.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/simvue/factory/proxy/remote.py b/simvue/factory/proxy/remote.py index f52900f0..75a297ed 100644 --- a/simvue/factory/proxy/remote.py +++ b/simvue/factory/proxy/remote.py @@ -483,7 +483,9 @@ def get_abort_status(self) -> bool: logger.debug("Retrieving alert status") try: - response = get(f"{self._url}/api/runs/{self._id}/abort", self._headers_mp) + response = get( + f"{self._config.server.url}/api/runs/{self._id}/abort", self._headers_mp + ) except Exception as err: self._error(f"Exception retrieving abort status: {str(err)}") return False diff --git a/tests/refactor/test_run_class.py b/tests/refactor/test_run_class.py index 106f9983..83a074ad 100644 --- a/tests/refactor/test_run_class.py +++ b/tests/refactor/test_run_class.py @@ -481,6 +481,7 @@ def testing_exit(status: int) -> None: client = sv_cl.Client() client.abort_run(run._id, reason="testing abort") time.sleep(4) + assert run._resources_metrics_interval == 1 for child in child_processes: assert not child.is_running() if not run._status == "terminated": From 11723df57aabb3a4e949cfdf1cdc8802ec7c0e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Wed, 11 Sep 2024 13:11:18 +0100 Subject: [PATCH 05/17] Fix linting issues --- simvue/config/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simvue/config/__init__.py b/simvue/config/__init__.py index 8da076b2..c3a62b8a 100644 --- a/simvue/config/__init__.py +++ b/simvue/config/__init__.py @@ -1 +1 @@ -from .user import SimvueConfiguration \ No newline at end of file +from .user import SimvueConfiguration as SimvueConfiguration From b01005f927993693485fb831fbc3273f6439512e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Wed, 25 Sep 2024 15:10:00 +0100 Subject: [PATCH 06/17] Move token server check to validation --- simvue/config/parameters.py | 31 ++++++++++++++++++++++++++++--- simvue/factory/proxy/remote.py | 33 +-------------------------------- 2 files changed, 29 insertions(+), 35 deletions(-) diff --git a/simvue/config/parameters.py b/simvue/config/parameters.py index 97a90ca5..4ce414a4 100644 --- a/simvue/config/parameters.py +++ b/simvue/config/parameters.py @@ -3,9 +3,12 @@ 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"] @@ -27,12 +30,34 @@ def url_to_str(cls, v: typing.Any) -> str: return f"{v}" @pydantic.field_validator("token") - def check_token(cls, v: pydantic.SecretStr) -> str: - if not (expiry := get_expiry(v.get_secret_value())): + def check_token(cls, v: typing.Any) -> str: + if not (expiry := get_expiry(v)): raise AssertionError("Failed to parse Simvue token - invalid token form") if time.time() - expiry > 0: raise AssertionError("Simvue token has expired") - return v.get_secret_value() + return v + + @pydantic.model_validator(mode="after") + def check_valid_server(cls, values: dict) -> bool: + url = values["url"] + token = values["token"] + headers: dict[str, str] = { + "Authorization": f"Bearer {token}", + "User-Agent": f"Simvue Python client {__version__}", + } + try: + response = get(f"{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)}") class DefaultRunSpecifications(pydantic.BaseModel): diff --git a/simvue/factory/proxy/remote.py b/simvue/factory/proxy/remote.py index 69812ea7..f61ed0d3 100644 --- a/simvue/factory/proxy/remote.py +++ b/simvue/factory/proxy/remote.py @@ -1,14 +1,13 @@ import logging import typing import http -import time if typing.TYPE_CHECKING: from simvue.config import SimvueConfiguration from simvue.api import get, post, put from simvue.factory.proxy.base import SimvueBaseClass -from simvue.utilities import prepare_for_api, skip_if_failed, get_expiry +from simvue.utilities import prepare_for_api, skip_if_failed from simvue.version import __version__ logger = logging.getLogger(__name__) @@ -483,36 +482,6 @@ def send_heartbeat(self) -> typing.Optional[dict[str, typing.Any]]: self._error(f"Got status code {response.status_code} when sending heartbeat") return None - @skip_if_failed("_aborted", "_suppress_errors", False) - def check_token(self) -> bool: - """ - Check token - """ - if not (expiry := get_expiry(self._token)): - self._error("Failed to parse user token") - return False - - if time.time() - expiry > 0: - self._error("Token has expired") - return False - - try: - response = get(f"{self._url}/api/version", self._headers) - - if response.status_code != http.HTTPStatus.OK or not response.json().get( - "version" - ): - raise AssertionError - - if response.status_code == http.HTTPStatus.UNAUTHORIZED: - self._error("Unauthorised token") - return False - - except Exception as err: - self._error(f"Exception retrieving server version: {str(err)}") - return False - return True - @skip_if_failed("_aborted", "_suppress_errors", False) def get_abort_status(self) -> bool: logger.debug("Retrieving alert status") From bbafb5f048e905c8c606b804ea8bbd5136a889fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Thu, 26 Sep 2024 10:54:20 +0100 Subject: [PATCH 07/17] Fix token processing in validation --- simvue/config/parameters.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/simvue/config/parameters.py b/simvue/config/parameters.py index 4ce414a4..772859f8 100644 --- a/simvue/config/parameters.py +++ b/simvue/config/parameters.py @@ -31,22 +31,22 @@ def url_to_str(cls, v: typing.Any) -> str: @pydantic.field_validator("token") def check_token(cls, v: typing.Any) -> str: - if not (expiry := get_expiry(v)): + 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 v + return value @pydantic.model_validator(mode="after") - def check_valid_server(cls, values: dict) -> bool: - url = values["url"] - token = values["token"] + @classmethod + def check_valid_server(cls, values: "ServerSpecifications") -> bool: headers: dict[str, str] = { - "Authorization": f"Bearer {token}", + "Authorization": f"Bearer {values.token}", "User-Agent": f"Simvue Python client {__version__}", } try: - response = get(f"{url}/api/version", headers) + response = get(f"{values.url}/api/version", headers) if response.status_code != http.HTTPStatus.OK or not response.json().get( "version" @@ -59,6 +59,8 @@ def check_valid_server(cls, values: dict) -> bool: except Exception as err: raise AssertionError(f"Exception retrieving server version: {str(err)}") + return values + class DefaultRunSpecifications(pydantic.BaseModel): description: typing.Optional[str] = None From b4438faa78ea118e6f63df5cdc1184d5ca112b1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Fri, 27 Sep 2024 08:21:34 +0100 Subject: [PATCH 08/17] Added URL check deactivation environment variable --- simvue/config/parameters.py | 4 ++++ tests/refactor/test_config.py | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/simvue/config/parameters.py b/simvue/config/parameters.py index 772859f8..9f241c59 100644 --- a/simvue/config/parameters.py +++ b/simvue/config/parameters.py @@ -1,4 +1,5 @@ import logging +import os import time import pydantic import typing @@ -41,6 +42,9 @@ def check_token(cls, v: typing.Any) -> str: @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__}", diff --git a/tests/refactor/test_config.py b/tests/refactor/test_config.py index 30e1d412..f7236a1d 100644 --- a/tests/refactor/test_config.py +++ b/tests/refactor/test_config.py @@ -1,5 +1,6 @@ import pytest import typing +import os import uuid import pathlib import pytest_mock @@ -37,6 +38,9 @@ def test_config_setup( _folder: str = "/test-case" _tags: list[str] = ["tag-test", "other-tag"] + # Deactivate the server checks for this test + monkeypatch.setenv("SIMVUE_NO_SERVER_CHECK", True) + if use_env: monkeypatch.setenv("SIMVUE_TOKEN", _other_token) monkeypatch.setenv("SIMVUE_URL", _other_url) @@ -70,7 +74,7 @@ def test_config_setup( """ out_f.write(_lines) SimvueConfiguration.config_file.cache_clear() - + mocker.patch("simvue.config.parameters.get_expiry", lambda *_, **__: 1e10) mocker.patch("simvue.config.user.sv_util.find_first_instance_of_file", lambda *_, **__: _config_file) From d91487fe07133a33caef71a4ef127848ac167471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Fri, 27 Sep 2024 08:47:16 +0100 Subject: [PATCH 09/17] Remove outdated test --- tests/refactor/test_proxies.py | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 tests/refactor/test_proxies.py diff --git a/tests/refactor/test_proxies.py b/tests/refactor/test_proxies.py deleted file mode 100644 index 00a09c3b..00000000 --- a/tests/refactor/test_proxies.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest - -from simvue.factory.proxy import Simvue - -@pytest.mark.proxy -def test_simvue_url_check() -> None: - """Checks the Token/URL checker""" - remote = Simvue( - name="", - uniq_id="", - mode="online", - suppress_errors=False - ) - assert remote.check_token() - From ed7dfe76aaf181d595974c5b55022aa849c6e434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Wed, 9 Oct 2024 10:28:56 +0100 Subject: [PATCH 10/17] Remove remnants of legacy token retrieval --- simvue/client.py | 64 ++++++++++++++++++++++---------------- simvue/run.py | 10 +++--- simvue/utilities.py | 39 ----------------------- tests/refactor/conftest.py | 2 +- 4 files changed, 43 insertions(+), 72 deletions(-) diff --git a/simvue/client.py b/simvue/client.py index 713f457a..38331c28 100644 --- a/simvue/client.py +++ b/simvue/client.py @@ -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 @@ -90,18 +91,29 @@ 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, @@ -165,7 +177,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( @@ -215,7 +227,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( @@ -331,7 +343,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() @@ -380,7 +392,7 @@ 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( @@ -415,7 +427,7 @@ 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 ( @@ -458,7 +470,7 @@ 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: @@ -522,7 +534,7 @@ 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( @@ -551,7 +563,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: @@ -586,7 +598,7 @@ 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( @@ -610,7 +622,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, ) @@ -649,7 +661,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, ) @@ -843,7 +855,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, ) @@ -935,7 +947,7 @@ 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( @@ -980,7 +992,7 @@ 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( @@ -1014,7 +1026,7 @@ 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( @@ -1271,7 +1283,7 @@ 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( @@ -1317,7 +1329,7 @@ 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], @@ -1326,7 +1338,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( diff --git a/simvue/run.py b/simvue/run.py index 2440536c..6c3658b9 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -47,7 +47,6 @@ calculate_sha256, compare_alerts, skip_if_failed, - get_auth, get_offline_directory, validate_timestamp, simvue_timestamp, @@ -163,9 +162,8 @@ def __init__( ) self._aborted: bool = False - self._url, self._token = get_auth() self._resources_metrics_interval: typing.Optional[int] = HEARTBEAT_INTERVAL - self._headers: dict[str, str] = {"Authorization": f"Bearer {self._token}"} + self._headers: dict[str, str] = {"Authorization": f"Bearer {self._config.server.token}"} self._simvue: typing.Optional[SimvueBaseClass] = None self._pid: typing.Optional[int] = 0 self._shutdown_event: typing.Optional[threading.Event] = None @@ -298,7 +296,7 @@ def _create_heartbeat_callback( self, ) -> typing.Callable[[threading.Event], None]: if ( - self._mode == "online" and (not self._url or not self._id) + self._mode == "online" and (not self._config.server.url or not self._id) ) or not self._heartbeat_termination_trigger: raise RuntimeError("Could not commence heartbeat, run not initialised") @@ -380,7 +378,7 @@ def _create_dispatch_callback( if self._mode == "online" and not self._id: raise RuntimeError("Expected identifier for run") - if not self._url: + if not self._config.server.url: raise RuntimeError("Cannot commence dispatch, run not initialised") def _offline_dispatch_callback( @@ -415,7 +413,7 @@ def _offline_dispatch_callback( def _online_dispatch_callback( buffer: list[typing.Any], category: str, - url: str = self._url, + url: str = self._config.server.url, run_id: typing.Optional[str] = self._id, headers: dict[str, str] = self._headers, ) -> None: diff --git a/simvue/utilities.py b/simvue/utilities.py index b711ed93..f7bc0724 100644 --- a/simvue/utilities.py +++ b/simvue/utilities.py @@ -249,45 +249,6 @@ def wrapper(self, *args, **kwargs) -> typing.Any: return wrapper -def get_auth() -> tuple[str, str]: - """ - Get the URL and access token - """ - url: typing.Optional[str] = None - token: typing.Optional[str] = None - token_source: str = "" - url_source: str = "" - - # Try reading from config file - for filename in ( - os.path.join(os.path.expanduser("~"), ".simvue.ini"), - "simvue.ini", - ): - with contextlib.suppress(Exception): - config = configparser.ConfigParser() - config.read(filename) - token = config.get("server", "token") - token_source = filename - url = config.get("server", "url") - url_source = filename - - # Try environment variables - if not token and (token := os.getenv("SIMVUE_TOKEN")): - token_source = "env:SIMVUE_TOKEN" - if not url and (url := os.getenv("SIMVUE_URL")): - url_source = "env:SIMVUE_URL" - - if not token: - raise ValueError("No Simvue server token was specified") - if not url: - raise ValueError("No Simvue server URL was specified") - - logger.info(f"Using '{token_source}' as source for Simvue token") - logger.info(f"Using '{url_source}' as source for Simvue URL") - - return url, token - - def get_offline_directory() -> str: """ Get directory for offline cache diff --git a/tests/refactor/conftest.py b/tests/refactor/conftest.py index b56f81cf..61214b10 100644 --- a/tests/refactor/conftest.py +++ b/tests/refactor/conftest.py @@ -113,7 +113,7 @@ def setup_test_run(run: sv_run.Run, create_objects: bool, request: pytest.Fixtur TEST_DATA["metrics"] = ("metric_counter", "metric_val") TEST_DATA["run_id"] = run._id TEST_DATA["run_name"] = run._name - TEST_DATA["url"] = run._url + TEST_DATA["url"] = run._config.server.url TEST_DATA["headers"] = run._headers TEST_DATA["pid"] = run._pid TEST_DATA["resources_metrics_interval"] = run._resources_metrics_interval From 922ce0a86b4c989e71530d41f2375f03844d90d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Wed, 9 Oct 2024 10:50:35 +0100 Subject: [PATCH 11/17] Handle folder defaults correctly --- simvue/run.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/simvue/run.py b/simvue/run.py index 6c3658b9..91f6afc9 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -163,7 +163,9 @@ def __init__( self._aborted: bool = False self._resources_metrics_interval: typing.Optional[int] = HEARTBEAT_INTERVAL - self._headers: dict[str, str] = {"Authorization": f"Bearer {self._config.server.token}"} + self._headers: dict[str, str] = { + "Authorization": f"Bearer {self._config.server.token}" + } self._simvue: typing.Optional[SimvueBaseClass] = None self._pid: typing.Optional[int] = 0 self._shutdown_event: typing.Optional[threading.Event] = None @@ -561,7 +563,9 @@ def init( metadata: typing.Optional[dict[str, typing.Any]] = None, tags: typing.Optional[list[str]] = None, description: typing.Optional[str] = None, - folder: typing.Annotated[str, pydantic.Field(pattern=FOLDER_REGEX)] = "/", + folder: typing.Annotated[ + str, pydantic.Field(None, pattern=FOLDER_REGEX) + ] = None, running: bool = True, retention_period: typing.Optional[str] = None, timeout: typing.Optional[int] = 180, @@ -614,7 +618,7 @@ def init( description = description or self._config.run.description tags = tags or self._config.run.tags - folder = folder or self._config.run.folder + folder = folder or self._config.run.folder or "/" self._term_color = not no_color From beb624cf8078960c722ebae91d98444c4b57f86d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Wed, 9 Oct 2024 11:00:44 +0100 Subject: [PATCH 12/17] Add 'name' to config file --- simvue/config/parameters.py | 1 + simvue/run.py | 1 + 2 files changed, 2 insertions(+) diff --git a/simvue/config/parameters.py b/simvue/config/parameters.py index 9f241c59..ee468553 100644 --- a/simvue/config/parameters.py +++ b/simvue/config/parameters.py @@ -67,6 +67,7 @@ def check_valid_server(cls, values: "ServerSpecifications") -> bool: class DefaultRunSpecifications(pydantic.BaseModel): + name: typing.Optional[str] = None description: typing.Optional[str] = None tags: list[str] | None = None folder: str = pydantic.Field("/", pattern=sv_models.FOLDER_REGEX) diff --git a/simvue/run.py b/simvue/run.py index 91f6afc9..30841bf0 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -619,6 +619,7 @@ def init( description = description or self._config.run.description tags = tags or self._config.run.tags folder = folder or self._config.run.folder or "/" + name = name or self._config.run.name self._term_color = not no_color From 34d25fc08aa5864516318a777a991bf67c7f5025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Wed, 9 Oct 2024 11:01:36 +0100 Subject: [PATCH 13/17] Reformat with Ruff --- simvue/client.py | 51 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/simvue/client.py b/simvue/client.py index 38331c28..28f448df 100644 --- a/simvue/client.py +++ b/simvue/client.py @@ -93,8 +93,8 @@ class Client: def __init__( self, - server_token: typing.Optional[str]=None, - server_url: typing.Optional[str]=None + server_token: typing.Optional[str] = None, + server_url: typing.Optional[str] = None, ) -> None: """Initialise an instance of the Simvue client @@ -109,11 +109,15 @@ def __init__( server_token=server_token, server_url=server_url ) - for label, value in zip(("URL", "API token"), (self._config.server.url, self._config.server.url)): + 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._config.server.token}"} + self._headers: dict[str, str] = { + "Authorization": f"Bearer {self._config.server.token}" + } def _get_json_from_response( self, @@ -392,7 +396,8 @@ def delete_run(self, run_identifier: str) -> typing.Optional[dict]: """ response = requests.delete( - f"{self._config.server.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( @@ -427,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._config.server.url}/api/folders", headers=self._headers, params=params + f"{self._config.server.url}/api/folders", + headers=self._headers, + params=params, ) if ( @@ -470,7 +477,9 @@ def delete_runs( params: dict[str, bool] = {"runs_only": True, "runs": True} response = requests.delete( - f"{self._config.server.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: @@ -534,7 +543,9 @@ def delete_folder( params |= {"recursive": recursive} response = requests.delete( - f"{self._config.server.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( @@ -598,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._config.server.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( @@ -947,7 +960,9 @@ def get_folders( } response: requests.Response = requests.get( - f"{self._config.server.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( @@ -992,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._config.server.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( @@ -1026,7 +1043,9 @@ def _get_run_metrics_from_server( } metrics_response: requests.Response = requests.get( - f"{self._config.server.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( @@ -1283,7 +1302,9 @@ def get_events( } response = requests.get( - f"{self._config.server.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( @@ -1329,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._config.server.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], From f844a3c57521812697a15088956dc499f76667ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Wed, 9 Oct 2024 11:12:44 +0100 Subject: [PATCH 14/17] Add metadata to config, combine config tags with session tags --- simvue/config/parameters.py | 3 ++- simvue/config/user.py | 1 + simvue/run.py | 5 +++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/simvue/config/parameters.py b/simvue/config/parameters.py index ee468553..7b3bd04a 100644 --- a/simvue/config/parameters.py +++ b/simvue/config/parameters.py @@ -69,8 +69,9 @@ def check_valid_server(cls, values: "ServerSpecifications") -> bool: class DefaultRunSpecifications(pydantic.BaseModel): name: typing.Optional[str] = None description: typing.Optional[str] = None - tags: list[str] | None = None + tags: typing.Optional[list[str]] = None folder: str = pydantic.Field("/", pattern=sv_models.FOLDER_REGEX) + metadata: typing.Optional[dict[str, str]] = None class ClientGeneralOptions(pydantic.BaseModel): diff --git a/simvue/config/user.py b/simvue/config/user.py index fdc2eecf..44f15957 100644 --- a/simvue/config/user.py +++ b/simvue/config/user.py @@ -55,6 +55,7 @@ def _parse_ini_config(cls, ini_file: pathlib.Path) -> dict[str, dict[str, str]]: return config_dict @classmethod + @sv_util.prettify_pydantic def fetch( cls, server_url: typing.Optional[str] = None, diff --git a/simvue/run.py b/simvue/run.py index 30841bf0..c67661ac 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -617,9 +617,10 @@ def init( return True description = description or self._config.run.description - tags = tags or self._config.run.tags - folder = folder or self._config.run.folder or "/" + tags = (tags or []) + (self._config.run.tags or []) + folder = folder or self._config.run.folder name = name or self._config.run.name + metadata = (metadata or {}) | (self._config.run.metadata or {}) self._term_color = not no_color From c3485bf9cd3e131776a440b4d5dffb3046b68556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Wed, 9 Oct 2024 11:18:02 +0100 Subject: [PATCH 15/17] Add headers to files --- simvue/config/__init__.py | 8 ++++++++ simvue/config/parameters.py | 8 ++++++++ simvue/config/user.py | 8 ++++++++ 3 files changed, 24 insertions(+) diff --git a/simvue/config/__init__.py b/simvue/config/__init__.py index c3a62b8a..db386d99 100644 --- a/simvue/config/__init__.py +++ b/simvue/config/__init__.py @@ -1 +1,9 @@ +""" +Simvue Configuration +==================== + +This module contains definitions for the Simvue configuration options + +""" + from .user import SimvueConfiguration as SimvueConfiguration diff --git a/simvue/config/parameters.py b/simvue/config/parameters.py index 7b3bd04a..4d7f4193 100644 --- a/simvue/config/parameters.py +++ b/simvue/config/parameters.py @@ -1,3 +1,11 @@ +""" +Simvue Configuration File Models +================================ + +Pydantic models for elements of the Simvue configuration file + +""" + import logging import os import time diff --git a/simvue/config/user.py b/simvue/config/user.py index 44f15957..b0ab6d8e 100644 --- a/simvue/config/user.py +++ b/simvue/config/user.py @@ -1,3 +1,11 @@ +""" +Simvue Configuration File Model +=============================== + +Pydantic model for the Simvue TOML configuration file + +""" + import functools import logging import os From f3e368fea7f47ee918cc8904d9447ec9442807a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Wed, 9 Oct 2024 11:27:15 +0100 Subject: [PATCH 16/17] Allow str, int, float, bool for metadata --- simvue/config/parameters.py | 2 +- simvue/run.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/simvue/config/parameters.py b/simvue/config/parameters.py index 4d7f4193..7b378d72 100644 --- a/simvue/config/parameters.py +++ b/simvue/config/parameters.py @@ -79,7 +79,7 @@ class DefaultRunSpecifications(pydantic.BaseModel): 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, str]] = None + metadata: typing.Optional[dict[str, typing.Union[str, int, float, bool]]] = None class ClientGeneralOptions(pydantic.BaseModel): diff --git a/simvue/run.py b/simvue/run.py index c67661ac..16fd4e47 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -560,7 +560,9 @@ def init( typing.Annotated[str, pydantic.Field(pattern=NAME_REGEX)] ] = None, *, - metadata: typing.Optional[dict[str, typing.Any]] = None, + metadata: typing.Optional[ + dict[str, typing.Union[str, int, float, bool]] + ] = None, tags: typing.Optional[list[str]] = None, description: typing.Optional[str] = None, folder: typing.Annotated[ From aa205aea2f1040690216f4f526f486686f793365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Wed, 9 Oct 2024 14:34:06 +0100 Subject: [PATCH 17/17] Try lengthening wait for test --- tests/refactor/test_run_class.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/refactor/test_run_class.py b/tests/refactor/test_run_class.py index 3b5fed56..8ff44a89 100644 --- a/tests/refactor/test_run_class.py +++ b/tests/refactor/test_run_class.py @@ -529,10 +529,10 @@ def testing_exit(status: int) -> None: if i == 4: client.abort_run(run._id, reason="testing abort") i += 1 - if abort_set.is_set() or i > 9: + if abort_set.is_set() or i > 11: break - assert i < 7 + assert i < 10 assert run._status == "terminated"