diff --git a/simvue/config/parameters.py b/simvue/config/parameters.py index d32406c0..d9eb66f5 100644 --- a/simvue/config/parameters.py +++ b/simvue/config/parameters.py @@ -18,12 +18,25 @@ logger = logging.getLogger(__name__) +BASE_MODEL_CONFIG = pydantic.ConfigDict(extra="forbid") + + +class MetadataSpecifications(pydantic.BaseModel): + environment: bool = os.environ.get("SIMVUE_DISABLE_ENVIRONMENT_METRICS") is None + git: bool = os.environ.get("SIMVUE_DISABLE_GIT_METRICS") is None + system: bool = os.environ.get("SIMVUE_DISABLE_SYSTEM_METRICS") is None + record_shell_vars: list[str] | None = None + custom: dict[str, str | int | float | bool] | None = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = BASE_MODEL_CONFIG + class CertificateSpecifications(pydantic.BaseModel): storage_ca_cert: pydantic.FilePath | bool = True server_ca_cert: pydantic.FilePath | bool = True client_cert: pydantic.FilePath | None = None client_key: pydantic.SecretStr | None = None + model_config: typing.ClassVar[pydantic.ConfigDict] = BASE_MODEL_CONFIG @pydantic.model_validator(mode="before") @classmethod @@ -43,10 +56,9 @@ def check_for_cert_env( class ServerSpecifications(pydantic.BaseModel): - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="forbid", - strict=True, - ) + model_config: typing.ClassVar[pydantic.ConfigDict] = BASE_MODEL_CONFIG | { + "extra": "forbid" + } url: pydantic.AnyHttpUrl | None token: pydantic.SecretStr | None env: dict[str, str] | None = None @@ -78,6 +90,7 @@ def check_token(cls, v: pydantic.SecretStr | None) -> pydantic.SecretStr | None: class OfflineSpecifications(pydantic.BaseModel): cache: pathlib.Path | None = None + model_config: typing.ClassVar[pydantic.ConfigDict] = BASE_MODEL_CONFIG @pydantic.field_validator("cache") @classmethod @@ -94,6 +107,7 @@ def check_valid_cache_path(cls, cache: pathlib.Path) -> pathlib.Path: class MetricsSpecifications(pydantic.BaseModel): system_metrics_interval: pydantic.PositiveInt | None = -1 enable_emission_metrics: bool = False + model_config: typing.ClassVar[pydantic.ConfigDict] = BASE_MODEL_CONFIG class DefaultRunSpecifications(pydantic.BaseModel): @@ -101,10 +115,12 @@ class DefaultRunSpecifications(pydantic.BaseModel): description: str | None = None tags: list[str] | None = None folder: str = pydantic.Field(default="/", pattern=sv_models.FOLDER_REGEX) - metadata: dict[str, str | int | float | bool] | None = None + metadata: MetadataSpecifications = MetadataSpecifications() mode: typing.Literal["offline", "disabled", "online"] = "online" record_shell_vars: list[str] | None = None + model_config: typing.ClassVar[pydantic.ConfigDict] = BASE_MODEL_CONFIG class ClientGeneralOptions(pydantic.BaseModel): debug: bool = False + model_config: typing.ClassVar[pydantic.ConfigDict] = BASE_MODEL_CONFIG diff --git a/simvue/metadata.py b/simvue/metadata.py index 8c7e5714..e5815ec5 100644 --- a/simvue/metadata.py +++ b/simvue/metadata.py @@ -270,7 +270,7 @@ def _node_js_env(repository: pathlib.Path) -> dict[str, typing.Any]: return js_meta -def _environment_variables(glob_exprs: list[str]) -> dict[str, str]: +def _environment_variables(glob_exprs: set[str]) -> dict[str, str]: """Retrieve values for environment variables.""" _env_vars: list[str] = list(os.environ.keys()) _metadata: dict[str, str] = {} @@ -283,20 +283,23 @@ def _environment_variables(glob_exprs: list[str]) -> dict[str, str]: def environment( + *, repository: pathlib.Path | None = None, + record_repo_environment: bool = True, env_var_glob_exprs: set[str] | None = None, ) -> dict[str, typing.Any]: """Retrieve environment metadata.""" _environment_meta = {} - _repository: pathlib.Path = repository or pathlib.Path.cwd() - if _python_meta := _python_env(_repository): - _environment_meta["python"] = _python_meta - if _rust_meta := _rust_env(_repository): - _environment_meta["rust"] = _rust_meta - if _julia_meta := _julia_env(_repository): - _environment_meta["julia"] = _julia_meta - if _js_meta := _node_js_env(_repository): - _environment_meta["javascript"] = _js_meta + if record_repo_environment: + _repository: pathlib.Path = repository or pathlib.Path.cwd() + if _python_meta := _python_env(_repository): + _environment_meta["python"] = _python_meta + if _rust_meta := _rust_env(_repository): + _environment_meta["rust"] = _rust_meta + if _julia_meta := _julia_env(_repository): + _environment_meta["julia"] = _julia_meta + if _js_meta := _node_js_env(_repository): + _environment_meta["javascript"] = _js_meta if env_var_glob_exprs: _environment_meta["shell"] = _environment_variables(env_var_glob_exprs) return _environment_meta diff --git a/simvue/run.py b/simvue/run.py index 5c69a7a0..8378ad1b 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -741,7 +741,7 @@ def init( tags = (tags or []) + (self._user_config.run.tags or []) folder = folder or self._user_config.run.folder name = name or self._user_config.run.name - metadata = (metadata or {}) | (self._user_config.run.metadata or {}) + metadata = (metadata or {}) | (self._user_config.run.metadata.custom or {}) record_shell_vars = record_shell_vars or self._user_config.run.record_shell_vars self._term_color = not no_color @@ -809,17 +809,24 @@ def init( self._sv_obj.ttl = self._retention self._sv_obj.status = self._status self._sv_obj.tags = tags - self._sv_obj.metadata = ( - (metadata or {}) - | git_info(pathlib.Path.cwd()) - | environment(env_var_glob_exprs=record_shell_vars) + + _session_metadata = metadata or {} + + _session_metadata |= environment( + env_var_glob_exprs=record_shell_vars, + record_repo_environment=self._user_config.run.metadata.environment, ) + + if self._user_config.run.metadata.git: + _session_metadata |= git_info(pathlib.Path.cwd()) + self._sv_obj.heartbeat_timeout = timeout self._sv_obj.alerts = [] self._sv_obj.created = time.time() self._sv_obj.notifications = notification + self._sv_obj.metadata = _session_metadata - if self._status == "running": + if self._status == "running" and self._user_config.run.metadata.system: self._sv_obj.system = get_system() self._data = self._sv_obj.staging diff --git a/tests/conftest.py b/tests/conftest.py index 4ed8d58e..2c2418c6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -168,8 +168,11 @@ def create_test_run_offline(request, monkeypatch: pytest.MonkeyPatch, prevent_sc @pytest.fixture -def create_plain_run(request, prevent_script_exit, mocker) -> Generator[tuple[sv_run.Run, dict]]: +def create_plain_run(request, prevent_script_exit, mocker, monkeypatch) -> Generator[tuple[sv_run.Run, dict]]: _ = prevent_script_exit + monkeypatch.setenv("SIMVUE_DISABLE_ENVIRONMENT_METRICS", "True") + monkeypatch.setenv("SIMVUE_DISABLE_GIT_METRICS", "True") + monkeypatch.setenv("SIMVUE_DISABLE_SYSTEM_METRICS", "True") def testing_exit(status: int) -> None: raise SystemExit(status) with sv_run.Run() as run: diff --git a/tests/functional/test_run_class.py b/tests/functional/test_run_class.py index 4b4f84c8..b6ba71f3 100644 --- a/tests/functional/test_run_class.py +++ b/tests/functional/test_run_class.py @@ -1566,7 +1566,7 @@ def test_env_var_metadata() -> None: } os.environ.update(_recorded_env) with simvue.Run() as run: - run.init( + _ = run.init( name="test_reconnect", folder="/simvue_unit_testing", retention_period="2 minutes", @@ -1575,6 +1575,7 @@ def test_env_var_metadata() -> None: record_shell_vars={"SIMVUE_RUN_TEST_VAR_*"} ) _recorded_meta = RunObject(identifier=run.id).metadata + assert "shell" in _recorded_meta assert all(key in _recorded_meta.get("shell") for key in _recorded_env) @pytest.mark.run @@ -1630,7 +1631,7 @@ def test_run_environment_metadata(environment: str, mocker: pytest_mock.MockerFi running=False, visibility="tenant" if os.environ.get("CI") else None, ) - run.update_metadata(env_func(_target_dir)) + run.update_metadata(env_func(repository=_target_dir)) @pytest.mark.online diff --git a/tests/unit/test_metadata.py b/tests/unit/test_metadata.py index a8e75e1b..e8e948e8 100644 --- a/tests/unit/test_metadata.py +++ b/tests/unit/test_metadata.py @@ -52,7 +52,7 @@ def test_js_env() -> None: @pytest.mark.metadata @pytest.mark.local def test_environment() -> None: - metadata = sv_meta.environment(pathlib.Path(__file__).parents[1].joinpath("example_data")) + metadata = sv_meta.environment(repository=pathlib.Path(__file__).parents[1].joinpath("example_data")) assert metadata["python"]["project"]["name"] == "example-repo" assert metadata["rust"]["project"]["name"] == "example_project" assert metadata["julia"]["project"]["name"] == "Julia Demo Project" diff --git a/uv.lock b/uv.lock index 5d7e1232..2a18bdcf 100644 --- a/uv.lock +++ b/uv.lock @@ -2101,7 +2101,7 @@ requires-dist = [ { name = "numpy", specifier = ">=2.0.0,<3.0.0" }, { name = "pandas", specifier = ">=2.2.3,<3.0.6" }, { name = "pip", specifier = ">=26.1.2" }, - { name = "plotly", marker = "extra == 'plot'", specifier = ">=6.0.0,<7.0.0" }, + { name = "plotly", marker = "extra == 'plot'", specifier = ">=6.0.0,<8.0.0" }, { name = "psutil", specifier = ">=6.1.1,<8.0.0" }, { name = "pydantic", specifier = ">=2.11,<3.0.0" }, { name = "pydantic-extra-types", specifier = ">=2.10.5,<3.0.0" },