From 487ef903d7687ba93ca4e841745e05aadaef2d55 Mon Sep 17 00:00:00 2001 From: Henry Bae <69275685+BaeHenryS@users.noreply.github.com> Date: Thu, 21 Aug 2025 19:16:28 -0700 Subject: [PATCH] Load metadata for run --- fluidize/adapters/local/runs.py | 8 ++-- fluidize/core/modules/graph/processor.py | 1 + fluidize/core/utils/exceptions/__init__.py | 26 +++++++++++++ fluidize/managers/registry.py | 14 +++++++ fluidize/managers/runs.py | 10 ++--- tests/unit/managers/test_projects.py | 45 +++++++++++++++++++++- 6 files changed, 92 insertions(+), 12 deletions(-) create mode 100644 fluidize/core/utils/exceptions/__init__.py diff --git a/fluidize/adapters/local/runs.py b/fluidize/adapters/local/runs.py index 2f37883..5790e58 100644 --- a/fluidize/adapters/local/runs.py +++ b/fluidize/adapters/local/runs.py @@ -12,7 +12,7 @@ from fluidize.core.modules.graph.process import ProcessGraph from fluidize.core.modules.run.project.project_runner import ProjectRunner from fluidize.core.types.project import ProjectSummary -from fluidize.core.types.runs import RunFlowPayload +from fluidize.core.types.runs import RunFlowPayload, projectRunMetadata from fluidize.core.utils.dataloader.data_loader import DataLoader from fluidize.core.utils.pathfinder.path_finder import PathFinder @@ -110,7 +110,7 @@ def list_runs(self, project: ProjectSummary) -> list[str]: """ return DataLoader.list_runs(project) - def get_run_status(self, project: ProjectSummary, run_number: int) -> dict[str, Any]: + def get_run_metadata(self, project: ProjectSummary, run_number: int) -> projectRunMetadata: """ Get the status of a specific run. @@ -121,9 +121,7 @@ def get_run_status(self, project: ProjectSummary, run_number: int) -> dict[str, Returns: Dictionary with run status information """ - # This would load run metadata and return status - # Implementation depends on how run status is stored - return {"run_number": run_number, "status": "unknown"} + return projectRunMetadata.from_file(directory=PathFinder.get_run_path(project, run_number)) def list_node_outputs(self, project: ProjectSummary, run_number: int, node_id: str) -> list[str]: """ diff --git a/fluidize/core/modules/graph/processor.py b/fluidize/core/modules/graph/processor.py index e4a18bb..708b8fa 100644 --- a/fluidize/core/modules/graph/processor.py +++ b/fluidize/core/modules/graph/processor.py @@ -60,6 +60,7 @@ def get_graph(self) -> GraphData: print(f"Error loading graph for project {self.project.id}: {e!s}") return GraphData(nodes=[], edges=[]) + # TODO : FIX THIS GRAPH NODE ADDITION HERE IN THE API! (THE TRAILING SLASHES GIVE PROBLEMS WHEN COPYING NODE DIRECTORY) def insert_node(self, node: GraphNode, sim_global: bool = True) -> GraphNode: """ Inserts a node from the list of simulations or creates a new one. diff --git a/fluidize/core/utils/exceptions/__init__.py b/fluidize/core/utils/exceptions/__init__.py new file mode 100644 index 0000000..9a31693 --- /dev/null +++ b/fluidize/core/utils/exceptions/__init__.py @@ -0,0 +1,26 @@ +""" +Custom exceptions for the Fluidize project. + +This module provides custom exception classes for better error handling +and debugging throughout the Fluidize application. +""" + + +class FluidizeError(Exception): + """Base exception class for all Fluidize-related errors.""" + + pass + + +class ProjectAlreadyExistsError(FluidizeError): + """Raised when attempting to create a project that already exists.""" + + def __init__(self, project_id: str) -> None: + """ + Initialize the exception. + + Args: + project_id: The ID of the project that already exists + """ + super().__init__(f"Project '{project_id}' already exists. Use update to modify existing projects.") + self.project_id = project_id diff --git a/fluidize/managers/registry.py b/fluidize/managers/registry.py index c59e321..7f3fe02 100644 --- a/fluidize/managers/registry.py +++ b/fluidize/managers/registry.py @@ -1,5 +1,7 @@ from typing import Any, Optional +from fluidize.core.utils.exceptions import ProjectAlreadyExistsError + from .project import ProjectManager @@ -38,7 +40,19 @@ def create( Returns: Created project wrapped in Project class + + Raises: + ProjectAlreadyExistsError: If a project with the same ID already exists """ + # Check if project already exists + try: + self.get(project_id) + # If we get here, project exists - raise error + raise ProjectAlreadyExistsError(project_id) + except FileNotFoundError: + # Project doesn't exist, proceed with creation + pass + project_summary = self.adapter.projects.upsert( id=project_id, label=label, diff --git a/fluidize/managers/runs.py b/fluidize/managers/runs.py index 2719022..be1bcde 100644 --- a/fluidize/managers/runs.py +++ b/fluidize/managers/runs.py @@ -7,7 +7,7 @@ from upath import UPath from fluidize.core.types.project import ProjectSummary -from fluidize.core.types.runs import RunFlowPayload +from fluidize.core.types.runs import RunFlowPayload, projectRunMetadata class RunsManager: @@ -48,17 +48,17 @@ def list_runs(self) -> list[str]: """ return self.adapter.runs.list_runs(self.project) # type: ignore[no-any-return] - def get_status(self, run_number: int) -> dict[str, Any]: + def get_metadata(self, run_number: int) -> projectRunMetadata: """ - Get the status of a specific run for this project. + Get the metadata of a specific run for this project. Args: run_number: The run number to check Returns: - Dictionary with run status information + Dictionary with run metadata information """ - return self.adapter.runs.get_run_status(self.project, run_number) # type: ignore[no-any-return] + return self.adapter.runs.get_run_metadata(self.project, run_number) # type: ignore[no-any-return] def list_node_outputs(self, run_number: int, node_id: str) -> list[str]: """ diff --git a/tests/unit/managers/test_projects.py b/tests/unit/managers/test_projects.py index a5d3d41..d5ec53b 100644 --- a/tests/unit/managers/test_projects.py +++ b/tests/unit/managers/test_projects.py @@ -4,6 +4,7 @@ import pytest +from fluidize.core.utils.exceptions import ProjectAlreadyExistsError from fluidize.managers.registry import RegistryManager from tests.fixtures.sample_projects import SampleProjects @@ -35,6 +36,8 @@ def test_create_project_with_all_fields(self, projects_manager, mock_adapter): sample_project = SampleProjects.standard_project() mock_adapter.projects.upsert.return_value = sample_project + # Mock retrieve to raise FileNotFoundError (project doesn't exist yet) + mock_adapter.projects.retrieve.side_effect = FileNotFoundError("Project not found") result = projects_manager.create( project_id=sample_project.id, @@ -65,6 +68,8 @@ def test_create_project_minimal(self, projects_manager, mock_adapter): project_id = "minimal-create" minimal_project = SampleProjects.minimal_project() mock_adapter.projects.upsert.return_value = minimal_project + # Mock retrieve to raise FileNotFoundError (project doesn't exist yet) + mock_adapter.projects.retrieve.side_effect = FileNotFoundError("Project not found") result = projects_manager.create(project_id) @@ -81,6 +86,8 @@ def test_create_project_partial_fields(self, projects_manager, mock_adapter): sample_project = SampleProjects.standard_project() mock_adapter.projects.upsert.return_value = sample_project + # Mock retrieve to raise FileNotFoundError (project doesn't exist yet) + mock_adapter.projects.retrieve.side_effect = FileNotFoundError("Project not found") result = projects_manager.create( project_id="partial-create", label="Partial Project", description="Only some fields provided" @@ -120,6 +127,25 @@ def test_get_project_not_found(self, projects_manager, mock_adapter): mock_adapter.projects.retrieve.assert_called_once_with(project_id) + def test_create_project_already_exists(self, projects_manager, mock_adapter): + """Test create method raises error when project already exists.""" + sample_project = SampleProjects.standard_project() + project_id = sample_project.id + + # Mock get to return existing project (no FileNotFoundError) + mock_adapter.projects.retrieve.return_value = sample_project + + with pytest.raises(ProjectAlreadyExistsError) as exc_info: + projects_manager.create(project_id, label="New Label") + + # Verify error message contains project ID + assert project_id in str(exc_info.value) + assert exc_info.value.project_id == project_id + + # Verify retrieve was called but upsert was not + mock_adapter.projects.retrieve.assert_called_once_with(project_id) + mock_adapter.projects.upsert.assert_not_called() + def test_list_projects_empty(self, projects_manager, mock_adapter): """Test list method when no projects exist.""" mock_adapter.projects.list.return_value = [] @@ -258,7 +284,8 @@ def test_update_filters_none_values(self, projects_manager, mock_adapter): def test_adapter_error_propagation(self, projects_manager, mock_adapter): """Test that adapter errors are properly propagated through manager methods.""" - # Test create error + # Test create error - first mock retrieve to return FileNotFoundError (project doesn't exist) + mock_adapter.projects.retrieve.side_effect = FileNotFoundError("Project not found") mock_adapter.projects.upsert.side_effect = ValueError("Invalid project data") with pytest.raises(ValueError, match="Invalid project data"): @@ -290,6 +317,12 @@ def test_manager_adapter_delegation(self, mock_adapter): mock_adapter.projects.list.return_value = [test_project] # Call all manager methods + # Mock retrieve to raise FileNotFoundError (project doesn't exist yet) + mock_adapter.projects.retrieve.side_effect = [ + FileNotFoundError("Project not found"), + test_project, + test_project, + ] manager.create("test-create") manager.get("test-get") manager.list() @@ -297,7 +330,7 @@ def test_manager_adapter_delegation(self, mock_adapter): # Verify adapter was called assert mock_adapter.projects.upsert.call_count == 2 # create + update - mock_adapter.projects.retrieve.assert_called_once() + assert mock_adapter.projects.retrieve.call_count == 2 # create (check if exists) + get mock_adapter.projects.list.assert_called_once() def test_manager_interface_compatibility(self, mock_adapter): @@ -327,6 +360,12 @@ def test_project_wrapper_return_types(self, mock_adapter): mock_adapter.projects.list.return_value = [sample_project] # Test create returns Project wrapper + # Mock retrieve to raise FileNotFoundError for create (project doesn't exist yet) + mock_adapter.projects.retrieve.side_effect = [ + FileNotFoundError("Project not found"), + sample_project, + sample_project, + ] created_project = manager.create("test-create") assert isinstance(created_project, ProjectManager) assert created_project.id == sample_project.id @@ -356,6 +395,8 @@ def test_project_wrapper_graph_property_access(self, mock_adapter): mock_adapter.projects.upsert.return_value = sample_project mock_adapter.graph = Mock() # Mock graph handler + # Mock retrieve to raise FileNotFoundError (project doesn't exist yet) + mock_adapter.projects.retrieve.side_effect = FileNotFoundError("Project not found") project = manager.create("test-graph-access") # Verify project has graph property