Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion simvue/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from .simvue_types import DeserializedContent
from .utilities import check_extra, prettify_pydantic
from .models import FOLDER_REGEX, NAME_REGEX
from .config import SimvueConfiguration
from .config.user import SimvueConfiguration

if typing.TYPE_CHECKING:
pass
Expand Down
2 changes: 0 additions & 2 deletions simvue/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,3 @@
This module contains definitions for the Simvue configuration options

"""

from .user import SimvueConfiguration as SimvueConfiguration
18 changes: 18 additions & 0 deletions simvue/config/files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""
Simvue Config File Lists
========================

Contains lists of valid Simvue configuration file names.

"""

import pathlib

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")}',
]

DEFAULT_OFFLINE_DIRECTORY: str = f"{pathlib.Path.home().joinpath('.simvue')}"
31 changes: 18 additions & 13 deletions simvue/config/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,13 @@
import typing
import pathlib
import http
import functools

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

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

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

logger = logging.getLogger(__file__)

Expand All @@ -47,18 +42,15 @@ def check_token(cls, v: typing.Any) -> str:
raise AssertionError("Simvue token has expired")
return value

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

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

if response.status_code != http.HTTPStatus.OK or not response.json().get(
"version"
Expand All @@ -71,12 +63,25 @@ def check_valid_server(cls, values: "ServerSpecifications") -> bool:
except Exception as err:
raise AssertionError(f"Exception retrieving server version: {str(err)}")

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

cls._check_server(values.token, values.url)

return values


class OfflineSpecifications(pydantic.BaseModel):
cache: typing.Optional[pathlib.Path] = None

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


class DefaultRunSpecifications(pydantic.BaseModel):
name: typing.Optional[str] = None
Expand Down
41 changes: 38 additions & 3 deletions simvue/config/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,18 @@

import simvue.utilities as sv_util
from simvue.config.parameters import (
CONFIG_FILE_NAMES,
CONFIG_INI_FILE_NAMES,
ClientGeneralOptions,
DefaultRunSpecifications,
ServerSpecifications,
OfflineSpecifications,
)

from simvue.config.files import (
CONFIG_FILE_NAMES,
CONFIG_INI_FILE_NAMES,
DEFAULT_OFFLINE_DIRECTORY,
)

logger = logging.getLogger(__name__)


Expand All @@ -52,7 +56,7 @@ def _parse_ini_config(cls, ini_file: pathlib.Path) -> dict[str, dict[str, str]]:
stacklevel=2,
)

config_dict: dict[str, dict[str, str]] = {"server": {}}
config_dict: dict[str, dict[str, str]] = {"server": {}, "offline": {}}

with contextlib.suppress(Exception):
parser = configparser.ConfigParser()
Expand All @@ -61,6 +65,8 @@ def _parse_ini_config(cls, ini_file: pathlib.Path) -> dict[str, dict[str, str]]:
config_dict["server"]["token"] = token
if url := parser.get("server", "url"):
config_dict["server"]["url"] = url
if offline_dir := parser.get("offline", "cache"):
config_dict["offline"]["cache"] = offline_dir

return config_dict

Expand All @@ -71,6 +77,24 @@ def fetch(
server_url: typing.Optional[str] = None,
server_token: typing.Optional[str] = None,
) -> "SimvueConfiguration":
"""Retrieve the Simvue configuration from this project

Will retrieve the configuration options set for this project either using
local or global configurations.

Parameters
----------
server_url : str, optional
override the URL used for this session
server_token : str, optional
override the token used for this session

Return
------
SimvueConfiguration
object containing configurations

"""
_config_dict: dict[str, dict[str, str]] = {}

try:
Expand All @@ -89,6 +113,16 @@ def fetch(

_config_dict["server"] = _config_dict.get("server", {})

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

# Allow override of specification of offline directory via environment variable
if not (_default_dir := os.environ.get("SIMVUE_OFFLINE_DIRECTORY")):
_default_dir = _config_dict["offline"].get(
"cache", DEFAULT_OFFLINE_DIRECTORY
)

_config_dict["offline"]["cache"] = _default_dir

# Ranking of configurations for token and URl is:
# Envionment Variables > Run Definition > Configuration File

Expand All @@ -114,6 +148,7 @@ def fetch(
@classmethod
@functools.lru_cache
def config_file(cls) -> pathlib.Path:
"""Returns the path of top level configuration file used for the session"""
_config_file: typing.Optional[pathlib.Path] = (
sv_util.find_first_instance_of_file(
CONFIG_FILE_NAMES, check_user_space=True
Expand Down
5 changes: 3 additions & 2 deletions simvue/factory/proxy/offline.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
import randomname

from simvue.factory.proxy.base import SimvueBaseClass
from simvue.config.user import SimvueConfiguration
from simvue.utilities import (
create_file,
get_offline_directory,
prepare_for_api,
skip_if_failed,
)
Expand All @@ -32,7 +32,8 @@ def __init__(
) -> None:
super().__init__(name, uniq_id, suppress_errors)

self._directory: str = os.path.join(get_offline_directory(), self._uuid)
_offline_dir = SimvueConfiguration.fetch().offline.cache
self._directory: str = os.path.join(_offline_dir, self._uuid)

os.makedirs(self._directory, exist_ok=True)

Expand Down
2 changes: 1 addition & 1 deletion simvue/factory/proxy/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import http

if typing.TYPE_CHECKING:
from simvue.config import SimvueConfiguration
from simvue.config.user import SimvueConfiguration

from simvue.api import get, post, put
from simvue.factory.proxy.base import SimvueBaseClass
Expand Down
6 changes: 3 additions & 3 deletions simvue/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import psutil
from pydantic import ValidationError

from .config import SimvueConfiguration
from .config.user import SimvueConfiguration
import simvue.api as sv_api

from .factory.dispatch import Dispatcher
Expand All @@ -48,7 +48,6 @@
calculate_sha256,
compare_alerts,
skip_if_failed,
get_offline_directory,
validate_timestamp,
simvue_timestamp,
)
Expand Down Expand Up @@ -405,7 +404,8 @@ def _offline_dispatch_callback(
run_id: typing.Optional[str] = self._id,
uuid: str = self._uuid,
) -> None:
if not os.path.exists((_offline_directory := get_offline_directory())):
_offline_directory = self.config.offline.cache
if not os.path.exists(_offline_directory):
logger.error(
f"Cannot write to offline directory '{_offline_directory}', directory not found."
)
Expand Down
4 changes: 2 additions & 2 deletions simvue/sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from simvue.config.user import SimvueConfiguration

from .factory.proxy.remote import Remote
from .utilities import create_file, get_offline_directory, remove_file
from .utilities import create_file, remove_file

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -88,7 +88,7 @@ def sender() -> str:
"""
Asynchronous upload of runs to Simvue server
"""
directory = get_offline_directory()
directory = SimvueConfiguration.fetch().offline.cache

# Clean up old runs after waiting 5 mins
runs = glob.glob(f"{directory}/*/sent")
Expand Down
26 changes: 1 addition & 25 deletions simvue/utilities.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import configparser
import datetime
import hashlib
import logging
Expand All @@ -16,6 +15,7 @@

from datetime import timezone


CHECKSUM_BLOCK_SIZE = 4096
EXTRAS: tuple[str, ...] = ("plot", "torch")

Expand Down Expand Up @@ -256,30 +256,6 @@ def wrapper(self, *args, **kwargs) -> typing.Any:
return wrapper


def get_offline_directory() -> str:
"""
Get directory for offline cache
"""
directory = None

for filename in (
os.path.join(os.path.expanduser("~"), ".simvue.ini"),
"simvue.ini",
):
if not filename or not os.path.exists(filename):
continue

with contextlib.suppress(Exception):
config = configparser.ConfigParser()
config.read(filename)
directory = config.get("offline", "cache")

if not (directory := os.environ.get("SIMVUE_OFFLINE_DIRECTORY", directory)):
directory = os.path.join(os.path.expanduser("~"), ".simvue")

return directory


def create_file(filename: str) -> None:
"""
Create an empty file
Expand Down
12 changes: 6 additions & 6 deletions tests/refactor/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ def __init__(self, level=logging.DEBUG):
super().__init__(level)
self.counts = []
self.captures = []

def emit(self, record):
if len(self.captures) != len(self.counts):
self.counts = [0] * len(self.captures)

for i, capture in enumerate(self.captures):
if capture in record.msg:
self.counts[i] += 1
Expand All @@ -48,9 +48,9 @@ def create_test_run(request) -> typing.Generator[typing.Tuple[sv_run.Run, dict],


@pytest.fixture
def create_test_run_offline(mocker: pytest_mock.MockerFixture, request) -> typing.Generator[typing.Tuple[sv_run.Run, dict], None, None]:
def create_test_run_offline(mocker: pytest_mock.MockerFixture, request, monkeypatch: pytest.MonkeyPatch) -> typing.Generator[typing.Tuple[sv_run.Run, dict], None, None]:
with tempfile.TemporaryDirectory() as temp_d:
mocker.patch.object(simvue.utilities, "get_offline_directory", lambda *_: temp_d)
monkeypatch.setenv("SIMVUE_OFFLINE_DIRECTORY", temp_d)
with sv_run.Run("offline") as run:
yield run, setup_test_run(run, True, request)

Expand All @@ -62,9 +62,9 @@ def create_plain_run(request) -> typing.Generator[typing.Tuple[sv_run.Run, dict]


@pytest.fixture
def create_plain_run_offline(mocker: pytest_mock.MockerFixture, request) -> typing.Generator[typing.Tuple[sv_run.Run, dict], None, None]:
def create_plain_run_offline(mocker: pytest_mock.MockerFixture, request, monkeypatch: pytest.MonkeyPatch) -> typing.Generator[typing.Tuple[sv_run.Run, dict], None, None]:
with tempfile.TemporaryDirectory() as temp_d:
mocker.patch.object(simvue.utilities, "get_offline_directory", lambda *_: temp_d)
monkeypatch.setenv("SIMVUE_OFFLINE_DIRECTORY", temp_d)
with sv_run.Run("offline") as run:

yield run, setup_test_run(run, False, request)
Expand Down
Loading