Skip to content
Closed
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
85 changes: 65 additions & 20 deletions backend/app/features/simulation/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
SimulationOut,
SimulationSummaryCapabilitiesOut,
SimulationSummaryOut,
SimulationUpdate,
)
from app.features.user.manager import current_active_user
from app.features.user.models import User
Expand All @@ -24,6 +25,15 @@
case_router = APIRouter(prefix="/cases", tags=["Cases"])


def _simulation_detail_query(db: Session):
return db.query(Simulation).options(
joinedload(Simulation.case),
joinedload(Simulation.machine),
selectinload(Simulation.artifacts),
selectinload(Simulation.links),
)


@case_router.get(
"",
response_model=list[CaseOut],
Expand Down Expand Up @@ -256,15 +266,7 @@ def create_simulation(

# Re-query with relationships loaded
sim_loaded = (
db.query(Simulation)
.options(
joinedload(Simulation.case),
joinedload(Simulation.machine),
selectinload(Simulation.artifacts),
selectinload(Simulation.links),
)
.filter(Simulation.id == sim.id)
.one_or_none()
_simulation_detail_query(db).filter(Simulation.id == sim.id).one_or_none()
)

if sim_loaded is None:
Expand Down Expand Up @@ -336,6 +338,59 @@ def list_simulations(
return [_simulation_to_out(s) for s in sims]


@simulation_router.patch(
"/{sim_id}",
response_model=SimulationOut,
responses={
200: {"description": "Simulation updated successfully."},
401: {"description": "Unauthorized."},
404: {"description": "Simulation not found."},
422: {"description": "Validation error."},
500: {"description": "Internal server error."},
},
)
def update_simulation(
sim_id: UUID,
payload: SimulationUpdate,
db: Session = Depends(get_database_session),
user: User = Depends(current_active_user),
) -> SimulationOut:
"""Partially update allowed simulation metadata fields."""
sim = db.query(Simulation).filter(Simulation.id == sim_id).one_or_none()

if sim is None:
raise HTTPException(status_code=404, detail="Simulation not found")

now = datetime.now(timezone.utc)
updates = payload.model_dump(by_alias=False, exclude_unset=True)

if "git_repository_url" in updates and updates["git_repository_url"] is not None:
updates["git_repository_url"] = str(updates["git_repository_url"])

for field, value in updates.items():
setattr(sim, field, value)

sim.last_updated_by = user.id
sim.updated_at = now

with transaction(db):
db.add(sim)
db.flush()

db.expire_all()
sim_loaded = (
_simulation_detail_query(db).filter(Simulation.id == sim_id).one_or_none()
)

if sim_loaded is None:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to load updated simulation.",
)

return _simulation_to_out(sim_loaded)


@simulation_router.get(
"/{sim_id}",
response_model=SimulationOut,
Expand Down Expand Up @@ -367,17 +422,7 @@ def get_simulation(sim_id: UUID, db: Session = Depends(get_database_session)):
HTTPException
If the simulation with the given ID is not found, raises a 404 HTTP exception.
"""
sim = (
db.query(Simulation)
.options(
joinedload(Simulation.case),
joinedload(Simulation.machine),
selectinload(Simulation.artifacts),
selectinload(Simulation.links),
)
.filter(Simulation.id == sim_id)
.one_or_none()
)
sim = _simulation_detail_query(db).filter(Simulation.id == sim_id).one_or_none()

if not sim:
raise HTTPException(status_code=404, detail="Simulation not found")
Expand Down
67 changes: 66 additions & 1 deletion backend/app/features/simulation/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Annotated, Any
from uuid import UUID

from pydantic import Field, HttpUrl, computed_field
from pydantic import ConfigDict, Field, HttpUrl, computed_field

from app.common.schemas.base import CamelInBaseModel, CamelOutBaseModel
from app.features.machine.schemas import MachineOut
Expand Down Expand Up @@ -274,6 +274,71 @@ class SimulationCreate(CamelInBaseModel):
]


class SimulationUpdate(CamelInBaseModel):
"""Schema for narrow v1 simulation metadata updates."""

model_config = ConfigDict(extra="forbid")

description: Annotated[
str | None, Field(None, description="Optional description of the simulation")
]
campaign: Annotated[
str | None,
Field(
None, description="Campaign or run grouping (e.g. historical, amip, tuning)"
),
]
experiment_type: Annotated[
ExperimentType | str | None,
Field(
None,
description=(
"High-level experiment category (e.g. historical, amip, piControl). "
"Often aligned with CMIP experiment identifiers."
),
),
]
compiler: Annotated[
str | None, Field(None, description="Optional compiler used for the simulation")
]
hpc_username: Annotated[
str | None,
Field(
None,
description="HPC username for provenance (trusted, informational only)",
),
]
key_features: Annotated[
str | None, Field(None, description="Optional key features of the simulation")
]
known_issues: Annotated[
str | None, Field(None, description="Optional known issues with the simulation")
]
notes_markdown: Annotated[
str | None,
Field(None, description="Optional additional notes in markdown format"),
]
git_repository_url: Annotated[
HttpUrl | None, Field(None, description="Optional Git repository URL")
]
git_branch: Annotated[
str | None,
Field(
None, description="Optional Git branch name associated with the simulation"
),
]
git_tag: Annotated[
str | None, Field(None, description="Optional Git tag for the simulation")
]
git_commit_hash: Annotated[
str | None,
Field(
None,
description="Optional Git commit hash associated with the simulation",
),
]


class SimulationSummaryOut(CamelOutBaseModel):
"""Lightweight schema for simulation summaries nested inside CaseOut.

Expand Down
Loading
Loading