Skip to content
Open
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
Empty file added src/common/__init__.py
Empty file.
64 changes: 64 additions & 0 deletions src/common/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Process-wide cached access to the server/client SCIoT configuration.

``sciot.config`` already owns YAML loading, env-var overrides, and schema
validation; every call site previously invoked it directly and re-parsed +
re-validated the YAML file on every call (see ``request_handler.py``'s
``_get_settings`` for the one exception, which had its own ad-hoc cache).
``Config`` centralizes that caching behind a single access point so callers
don't each need their own cache.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any, Mapping

from sciot.config import load_client_config, load_server_config

_SRC_DIR = Path(__file__).resolve().parent.parent
DEFAULT_SERVER_CONFIG_PATH = _SRC_DIR / "server" / "settings.yaml"
DEFAULT_CLIENT_CONFIG_PATH = _SRC_DIR / "client" / "python" / "http_config.yaml"


class Config:
"""Singleton-style cache around ``load_server_config``/``load_client_config``.

Configs are cached per resolved path so repeated calls with the default
(or same explicit) path reuse the already-loaded, already-validated dict
instead of re-reading and re-validating the YAML file.
"""

_server_cache: dict[Path, dict[str, Any]] = {}
_client_cache: dict[Path, dict[str, Any]] = {}

@classmethod
def get_server(
cls,
path: str | Path | None = None,
*,
environ: Mapping[str, str] | None = None,
force_reload: bool = False,
) -> dict[str, Any]:
resolved = Path(path or DEFAULT_SERVER_CONFIG_PATH).resolve()
if force_reload or resolved not in cls._server_cache:
cls._server_cache[resolved] = load_server_config(resolved, environ=environ)
return cls._server_cache[resolved]

@classmethod
def get_client(
cls,
path: str | Path | None = None,
*,
environ: Mapping[str, str] | None = None,
force_reload: bool = False,
) -> dict[str, Any]:
resolved = Path(path or DEFAULT_CLIENT_CONFIG_PATH).resolve()
if force_reload or resolved not in cls._client_cache:
cls._client_cache[resolved] = load_client_config(resolved, environ=environ)
return cls._client_cache[resolved]

@classmethod
def reset(cls) -> None:
"""Clear cached configs. Intended for tests."""
cls._server_cache.clear()
cls._client_cache.clear()
6 changes: 3 additions & 3 deletions src/server/edge/edge_initialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
from server.models.model_manager import ModelManager
from server.models.model_input_converter import ModelInputConverter
from server.core.variance_detector import VarianceDetector
from sciot.config import load_server_config
from common.config import Config
from pathlib import Path


def load_delay_config():
"""Load delay configuration from settings.yaml"""
settings_path = Path(__file__).parent.parent / "settings.yaml"
try:
settings = load_server_config(settings_path)
settings = Config.get_server(settings_path)
return settings.get("delay_simulation", {}).get("edge_computation")
except Exception as e:
print(f"Warning: Could not load delay config: {e}")
Expand Down Expand Up @@ -391,7 +391,7 @@ def initialization(
if __name__ == "__main__":
# Carichiamo il file settings.yaml
settings_path = Path(__file__).parent.parent / "settings.yaml"
config = load_server_config(settings_path)
config = Config.get_server(settings_path)

# Inizializza tutti i modelli disponibili
for model_name, model_config in config["model"].items():
Expand Down
4 changes: 2 additions & 2 deletions src/server/edge/run_edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from paho.mqtt import client as mqtt
from server.communication.request_handler import RequestHandler
from server.commons import ConfigurationFiles
from sciot.config import load_server_config
from common.config import Config


def initialize_models(config: dict) -> None:
Expand Down Expand Up @@ -97,7 +97,7 @@ def create_enabled_transports(
def main(config_path: str | None = None) -> int:
configure_logger_from_settings()
logger.info("Starting the [EDGE] communication transports")
config = load_server_config(
config = Config.get_server(
config_path or ConfigurationFiles.server_configuration_file_path
)

Expand Down
4 changes: 2 additions & 2 deletions src/server/logger/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import sys

from server.logger.log_config import LoggerConfig
from sciot.config import load_server_config
from common.config import Config

(CUSTOM_LOG_FOLDER, DEBUG_LOG_LEVEL, ERROR_LOG_LEVEL, LOG_HISTORY_DAYS_LIMIT) = (
LoggerConfig.LOG_FOLDER,
Expand Down Expand Up @@ -104,7 +104,7 @@ def configure_logger_from_settings():
"""
try:
settings_path = Path(__file__).parent.parent / "settings.yaml"
settings = load_server_config(settings_path)
settings = Config.get_server(settings_path)
verbose = settings.get("verbose", False)

if not verbose:
Expand Down
107 changes: 107 additions & 0 deletions tests/unit/test_common_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Tests for the common.config.Config singleton cache (SCIOT-008)."""

from pathlib import Path
from unittest.mock import patch

import pytest
import yaml

from common.config import Config

PROJECT_ROOT = Path(__file__).resolve().parents[2]
SERVER_SETTINGS = PROJECT_ROOT / "src/server/settings.yaml"
CLIENT_SETTINGS = PROJECT_ROOT / "src/client/python/http_config.yaml"


@pytest.fixture(autouse=True)
def reset_config_cache():
Config.reset()
yield
Config.reset()


def test_get_server_returns_valid_config():
config = Config.get_server(SERVER_SETTINGS)
assert "model" in config
assert "communication" in config


def test_get_client_returns_valid_config():
config = Config.get_client(CLIENT_SETTINGS)
assert "client" in config
assert "http" in config


def test_get_server_only_parses_file_once_per_path():
calls = []
real_load = __import__(
"sciot.config", fromlist=["load_server_config"]
).load_server_config

def counting_load(*args, **kwargs):
calls.append(1)
return real_load(*args, **kwargs)

with patch("common.config.load_server_config", side_effect=counting_load):
first = Config.get_server(SERVER_SETTINGS)
second = Config.get_server(SERVER_SETTINGS)

assert first is second
assert len(calls) == 1


def test_force_reload_reparses_file():
calls = []
real_load = __import__(
"sciot.config", fromlist=["load_server_config"]
).load_server_config

def counting_load(*args, **kwargs):
calls.append(1)
return real_load(*args, **kwargs)

with patch("common.config.load_server_config", side_effect=counting_load):
Config.get_server(SERVER_SETTINGS)
Config.get_server(SERVER_SETTINGS, force_reload=True)

assert len(calls) == 2


def test_different_paths_cached_independently(tmp_path):
alt_config = yaml.safe_load(SERVER_SETTINGS.read_text())
alt_path = tmp_path / "alt_settings.yaml"
alt_path.write_text(yaml.safe_dump(alt_config))

default_result = Config.get_server(SERVER_SETTINGS)
alt_result = Config.get_server(alt_path)

assert default_result is not alt_result
assert default_result == alt_result


def test_get_server_default_path_resolves_to_repo_settings():
config = Config.get_server()
assert config == Config.get_server(SERVER_SETTINGS)


def test_get_client_default_path_resolves_to_repo_settings():
config = Config.get_client()
assert config == Config.get_client(CLIENT_SETTINGS)


def test_reset_clears_cache():
calls = []
real_load = __import__(
"sciot.config", fromlist=["load_server_config"]
).load_server_config

def counting_load(*args, **kwargs):
calls.append(1)
return real_load(*args, **kwargs)

with patch("common.config.load_server_config", side_effect=counting_load):
Config.get_server(SERVER_SETTINGS)
Config.reset()
Config.get_server(SERVER_SETTINGS)

assert len(calls) == 2
Loading