From 13752312ff6769f2b29dafe0ca6b072709a7880d Mon Sep 17 00:00:00 2001 From: Anton Fedotov Date: Tue, 12 May 2026 18:02:15 +0400 Subject: [PATCH 1/3] QUAL-1854 load full config for projects --- qualibrate_config/core/project/model.py | 1 + qualibrate_config/core/project/p_list.py | 40 +++++++++--------------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/qualibrate_config/core/project/model.py b/qualibrate_config/core/project/model.py index 6863d76..73e5e02 100644 --- a/qualibrate_config/core/project/model.py +++ b/qualibrate_config/core/project/model.py @@ -13,6 +13,7 @@ class Project(BaseModel): created_at: AwareDatetime last_modified_at: AwareDatetime updates: Annotated[Mapping[str, Any], Field(default_factory=dict)] + config: Annotated[Mapping[str, Any] | None, Field(default=None)] @field_serializer("created_at", "last_modified_at") def dt_serializer(self, dt: AwareDatetime) -> str: diff --git a/qualibrate_config/core/project/p_list.py b/qualibrate_config/core/project/p_list.py index f316ff9..5086827 100644 --- a/qualibrate_config/core/project/p_list.py +++ b/qualibrate_config/core/project/p_list.py @@ -43,9 +43,7 @@ def _created_at_from_storage( ) if first_create_p is None: return _dt_from_ts(_stat_ctime(storage_stat)) - return _dt_from_ts( - min(_stat_ctime(storage_stat), _file_stat_ctime(first_create_p)) - ) + return _dt_from_ts(min(_stat_ctime(storage_stat), _file_stat_ctime(first_create_p))) def _last_modified_at_from_storage( @@ -58,9 +56,7 @@ def _last_modified_at_from_storage( ) if first_create_p is None: return _dt_from_ts(storage_stat.st_mtime) - return _dt_from_ts( - max(storage_stat.st_mtime, _file_stat_mtime(first_create_p)) - ) + return _dt_from_ts(max(storage_stat.st_mtime, _file_stat_mtime(first_create_p))) def _storage_stat( @@ -72,9 +68,7 @@ def _storage_stat( ) storage_stat = storage_path.stat() created_at = _created_at_from_storage(storage_path, storage_stat) - last_modified_at = _last_modified_at_from_storage( - storage_path, storage_stat - ) + last_modified_at = _last_modified_at_from_storage(storage_path, storage_stat) return nodes_number, created_at, last_modified_at @@ -86,46 +80,40 @@ def _project_stat_dir( def project_stat( - qualibrate_path: Path, project: str, config_path: Path + qualibrate_path: Path, + project: str, + config_path: Path, + with_config: bool = False, ) -> Project: project_path = get_project_path(qualibrate_path, project) config_dict = read_config_file(config_path, override_project=project) project_config = read_project_config_file(config_path, project) storage_location = ( - config_dict.get(QUALIBRATE_CONFIG_KEY, {}) - .get("storage", {}) - .get("location") + config_dict.get(QUALIBRATE_CONFIG_KEY, {}).get("storage", {}).get("location") ) if storage_location: storage_path = Path(storage_location) if storage_path.is_dir(): - nodes_number, created_at, last_modified_at = _storage_stat( - storage_path - ) + nodes_number, created_at, last_modified_at = _storage_stat(storage_path) else: - nodes_number, created_at, last_modified_at = _project_stat_dir( - project_path - ) + nodes_number, created_at, last_modified_at = _project_stat_dir(project_path) else: - nodes_number, created_at, last_modified_at = _project_stat_dir( - project_path - ) + nodes_number, created_at, last_modified_at = _project_stat_dir(project_path) return Project( name=project, nodes_number=nodes_number, created_at=created_at.astimezone(), last_modified_at=last_modified_at.astimezone(), updates=project_config, + config=config_dict if with_config else None, ) def list_projects(qualibrate_path: Path) -> list[str]: projects_path = get_projects_path(qualibrate_path) if not projects_path.is_dir(): - raise NotADirectoryError( - f"Projects path '{projects_path}' is not a directory" - ) + raise NotADirectoryError(f"Projects path '{projects_path}' is not a directory") return list( map( lambda p: p.name, @@ -139,7 +127,7 @@ def verbose_list_projects( ) -> dict[str, Project]: qualibrate_path = config_path.parent return { - p_name: project_stat(qualibrate_path, p_name, config_path) + p_name: project_stat(qualibrate_path, p_name, config_path, with_config=True) for p_name in list_projects(qualibrate_path) } From a6bff7437059465aca684ff15193c8ad23a5f04b Mon Sep 17 00:00:00 2001 From: Anton Fedotov Date: Wed, 13 May 2026 11:47:24 +0400 Subject: [PATCH 2/3] QUAL-1854 fix lint --- qualibrate_config/core/project/p_list.py | 36 ++++++++++++++++++------ 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/qualibrate_config/core/project/p_list.py b/qualibrate_config/core/project/p_list.py index 5086827..2fe5a92 100644 --- a/qualibrate_config/core/project/p_list.py +++ b/qualibrate_config/core/project/p_list.py @@ -43,7 +43,9 @@ def _created_at_from_storage( ) if first_create_p is None: return _dt_from_ts(_stat_ctime(storage_stat)) - return _dt_from_ts(min(_stat_ctime(storage_stat), _file_stat_ctime(first_create_p))) + return _dt_from_ts( + min(_stat_ctime(storage_stat), _file_stat_ctime(first_create_p)) + ) def _last_modified_at_from_storage( @@ -56,7 +58,9 @@ def _last_modified_at_from_storage( ) if first_create_p is None: return _dt_from_ts(storage_stat.st_mtime) - return _dt_from_ts(max(storage_stat.st_mtime, _file_stat_mtime(first_create_p))) + return _dt_from_ts( + max(storage_stat.st_mtime, _file_stat_mtime(first_create_p)) + ) def _storage_stat( @@ -68,7 +72,9 @@ def _storage_stat( ) storage_stat = storage_path.stat() created_at = _created_at_from_storage(storage_path, storage_stat) - last_modified_at = _last_modified_at_from_storage(storage_path, storage_stat) + last_modified_at = _last_modified_at_from_storage( + storage_path, storage_stat + ) return nodes_number, created_at, last_modified_at @@ -90,16 +96,24 @@ def project_stat( config_dict = read_config_file(config_path, override_project=project) project_config = read_project_config_file(config_path, project) storage_location = ( - config_dict.get(QUALIBRATE_CONFIG_KEY, {}).get("storage", {}).get("location") + config_dict.get(QUALIBRATE_CONFIG_KEY, {}) + .get("storage", {}) + .get("location") ) if storage_location: storage_path = Path(storage_location) if storage_path.is_dir(): - nodes_number, created_at, last_modified_at = _storage_stat(storage_path) + nodes_number, created_at, last_modified_at = _storage_stat( + storage_path + ) else: - nodes_number, created_at, last_modified_at = _project_stat_dir(project_path) + nodes_number, created_at, last_modified_at = _project_stat_dir( + project_path + ) else: - nodes_number, created_at, last_modified_at = _project_stat_dir(project_path) + nodes_number, created_at, last_modified_at = _project_stat_dir( + project_path + ) return Project( name=project, nodes_number=nodes_number, @@ -113,7 +127,9 @@ def project_stat( def list_projects(qualibrate_path: Path) -> list[str]: projects_path = get_projects_path(qualibrate_path) if not projects_path.is_dir(): - raise NotADirectoryError(f"Projects path '{projects_path}' is not a directory") + raise NotADirectoryError( + f"Projects path '{projects_path}' is not a directory" + ) return list( map( lambda p: p.name, @@ -127,7 +143,9 @@ def verbose_list_projects( ) -> dict[str, Project]: qualibrate_path = config_path.parent return { - p_name: project_stat(qualibrate_path, p_name, config_path, with_config=True) + p_name: project_stat( + qualibrate_path, p_name, config_path, with_config=True + ) for p_name in list_projects(qualibrate_path) } From f3bcf06059503e14be044873dfcb589e3e60168a Mon Sep 17 00:00:00 2001 From: Anton Fedotov Date: Wed, 13 May 2026 13:22:03 +0400 Subject: [PATCH 3/3] QUAL-1854 add tests --- .../test_core/test_project/test_p_list.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/unit/test_core/test_project/test_p_list.py b/tests/unit/test_core/test_project/test_p_list.py index da9b98b..7523e99 100644 --- a/tests/unit/test_core/test_project/test_p_list.py +++ b/tests/unit/test_core/test_project/test_p_list.py @@ -219,6 +219,65 @@ def test_project_stat_with_no_storage_defined(mocker, tmp_path): assert isinstance(project_obj.last_modified_at, datetime) +def test_project_stat_with_config_includes_full_config(mocker, tmp_path): + project = "proj" + project_path = tmp_path / project + project_path.mkdir() + config_path = tmp_path / "config.toml" + config_dict = {"qualibrate": {"project": project, "storage": {}}} + project_config = {"qualibrate": {"project": project}} + now = datetime.now().astimezone() + + mocker.patch( + "qualibrate_config.core.project.p_list.get_project_path", + return_value=project_path, + ) + mocker.patch( + "qualibrate_config.core.project.p_list.read_config_file", + return_value=config_dict, + ) + mocker.patch( + "qualibrate_config.core.project.p_list.read_project_config_file", + return_value=project_config, + ) + mocker.patch( + "qualibrate_config.core.project.p_list._project_stat_dir", + return_value=(0, now, now), + ) + + project_obj = p_list.project_stat( + tmp_path, project, config_path, with_config=True + ) + + assert project_obj.config == config_dict + assert project_obj.updates == project_config + + +def test_verbose_list_projects_requests_config(mocker, tmp_path): + config_path = tmp_path / "config.toml" + now = datetime.now().astimezone() + + mocker.patch( + "qualibrate_config.core.project.p_list.list_projects", + return_value=["p1"], + ) + project_stat = mocker.patch( + "qualibrate_config.core.project.p_list.project_stat", + return_value=p_list.Project( + name="p1", + nodes_number=1, + created_at=now, + last_modified_at=now, + ), + ) + + p_list.verbose_list_projects(config_path) + + project_stat.assert_called_once_with( + config_path.parent, "p1", config_path, with_config=True + ) + + def test_list_projects_not_exists(mocker, tmp_path): projects = tmp_path / "p1" mock_path = mocker.patch(