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
8 changes: 3 additions & 5 deletions fluidize/adapters/local/runs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.

Expand All @@ -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]:
"""
Expand Down
1 change: 1 addition & 0 deletions fluidize/core/modules/graph/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
26 changes: 26 additions & 0 deletions fluidize/core/utils/exceptions/__init__.py
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions fluidize/managers/registry.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from typing import Any, Optional

from fluidize.core.utils.exceptions import ProjectAlreadyExistsError

from .project import ProjectManager


Expand Down Expand Up @@ -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,
Expand Down
10 changes: 5 additions & 5 deletions fluidize/managers/runs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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]:
"""
Expand Down
45 changes: 43 additions & 2 deletions tests/unit/managers/test_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand All @@ -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"
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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"):
Expand Down Expand Up @@ -290,14 +317,20 @@ 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()
manager.update("test-update", label="Updated")

# 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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down