added experiment api

This commit is contained in:
2026-05-06 20:05:41 +03:00
parent fad9fcefac
commit ea4d91f3bd
6 changed files with 1198 additions and 3 deletions

View File

@@ -0,0 +1,569 @@
from datetime import datetime
from typing import List
from connections.db import get_db
from connections.keycloak import (
get_current_token_payload,
)
from crud.experiment_crud import (
create_experiment,
create_experiment_type,
create_instance,
delete_experiment,
delete_instance,
get_all_experiment_types,
get_experiment_instances,
get_single_experiment,
get_single_instance,
get_user_experiments,
set_simulation_result,
update_experiment,
update_instance,
)
from crud.team_crud import check_team_permission
from fastapi import Depends, Query
from fastapi.routing import APIRouter
from rest_models.experiment_models import (
CreateExperimentRequest,
CreateExperimentResponse,
CreateExperimentTypeRequest,
CreateExperimentTypeResponse,
CreateInstanceRequest,
CreateInstanceResponse,
ExperimentData,
ExperimentListResponse,
ExperimentTypeData,
InstanceData,
InstanceListResponse,
StartExperimentRequest,
StartExperimentResponse,
UpdateExperimentRequest,
UpdateExperimentResponse,
UpdateInstanceRequest,
UpdateInstanceResponse,
)
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.exceptions import HTTPException
router = APIRouter(tags=["Experiments"])
# ============= EXPERIMENT TYPE ENDPOINTS =============
@router.get("/types", response_model=List[ExperimentTypeData])
async def get_experiment_types_request(
db: AsyncSession = Depends(get_db),
payload: dict = Depends(get_current_token_payload),
) -> List[ExperimentTypeData]:
"""Get all existing experiment types"""
keycloak_id = payload.get("sub")
if not keycloak_id:
raise HTTPException(403, "Permission denied")
experiment_types = await get_all_experiment_types(db)
return [
ExperimentTypeData(
id=exp_type.id,
name=exp_type.name,
description=exp_type.description,
file_frontend=exp_type.file_frontend,
server_path=exp_type.server_path,
file_comp_system=exp_type.file_comp_system,
)
for exp_type in experiment_types
]
@router.post("/types", response_model=CreateExperimentTypeResponse)
async def create_experiment_type_request(
create_data: CreateExperimentTypeRequest,
db: AsyncSession = Depends(get_db),
payload: dict = Depends(get_current_token_payload),
) -> CreateExperimentTypeResponse:
"""Create a new experiment type (admin only)"""
keycloak_id = payload.get("sub")
if not keycloak_id:
raise HTTPException(403, "Permission denied")
# TODO: Add admin permission check here if needed
experiment_type = await create_experiment_type(
db=db,
name=create_data.name,
file_frontend=create_data.file_frontend,
server_path=create_data.server_path,
file_comp_system=create_data.file_comp_system,
description=create_data.description,
)
return CreateExperimentTypeResponse(
id=experiment_type.id,
name=experiment_type.name,
description=experiment_type.description,
file_frontend=experiment_type.file_frontend,
server_path=experiment_type.server_path,
file_comp_system=experiment_type.file_comp_system,
)
# ============= EXPERIMENT ENDPOINTS =============
@router.post("", response_model=CreateExperimentResponse)
async def create_experiment_request(
create_data: CreateExperimentRequest,
db: AsyncSession = Depends(get_db),
payload: dict = Depends(get_current_token_payload),
) -> CreateExperimentResponse:
"""Create a new experiment"""
keycloak_id = payload.get("sub")
if not keycloak_id:
raise HTTPException(403, "Permission denied")
# Check if user has permission to create experiments in this team
await check_team_permission(
["manage_experiments"], create_data.team_id, db, keycloak_id
)
experiment = await create_experiment(
db=db,
team_id=create_data.team_id,
experiment_type_id=create_data.experiment_type_id,
name=create_data.name,
description=create_data.description,
)
return CreateExperimentResponse(
id=experiment.id,
team_id=experiment.team_id,
experiment_type_id=experiment.experiment_type_id,
name=experiment.name,
description=experiment.description,
created_at=experiment.created_at,
)
@router.get("", response_model=ExperimentData)
async def get_experiment_request(
experiment_id: int = Query(),
db: AsyncSession = Depends(get_db),
payload: dict = Depends(get_current_token_payload),
) -> ExperimentData:
"""Get a single experiment's data (without instances)"""
keycloak_id = payload.get("sub")
if not keycloak_id:
raise HTTPException(403, "Permission denied")
experiment = await get_single_experiment(db, experiment_id)
if not experiment:
raise HTTPException(404, "Experiment not found")
# Check if user has access to this experiment's team
await check_team_permission([], experiment.team_id, db, keycloak_id)
return ExperimentData(
id=experiment.id,
team_id=experiment.team_id,
team_name=experiment.team.name if experiment.team else None,
experiment_type_id=experiment.experiment_type_id,
experiment_type_name=experiment.experiment_type.name
if experiment.experiment_type
else None,
name=experiment.name,
description=experiment.description,
created_at=experiment.created_at,
instances_count=len(experiment.instances) if experiment.instances else 0,
)
@router.put("", response_model=UpdateExperimentResponse)
async def update_experiment_request(
update_data: UpdateExperimentRequest,
db: AsyncSession = Depends(get_db),
payload: dict = Depends(get_current_token_payload),
) -> UpdateExperimentResponse:
"""Update experiment data (cannot update type)"""
keycloak_id = payload.get("sub")
if not keycloak_id:
raise HTTPException(403, "Permission denied")
experiment = await get_single_experiment(db, update_data.experiment_id)
if not experiment:
raise HTTPException(404, "Experiment not found")
# Check if user has permission to update experiments in this team
await check_team_permission(
["manage_experiments"], experiment.team_id, db, keycloak_id
)
updated_experiment = await update_experiment(
db=db,
experiment_id=update_data.experiment_id,
name=update_data.name,
description=update_data.description,
)
return UpdateExperimentResponse(
id=updated_experiment.id,
team_id=updated_experiment.team_id,
experiment_type_id=updated_experiment.experiment_type_id,
name=updated_experiment.name,
description=updated_experiment.description,
created_at=updated_experiment.created_at,
)
@router.get("/user", response_model=ExperimentListResponse)
async def get_user_experiments_request(
page_num: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
db: AsyncSession = Depends(get_db),
payload: dict = Depends(get_current_token_payload),
) -> ExperimentListResponse:
"""Get all experiments accessible to the current user (via teams)"""
keycloak_id = payload.get("sub")
if not keycloak_id:
raise HTTPException(403, "Permission denied")
offset = (page_num - 1) * page_size
experiments, total_count = await get_user_experiments(
db=db, user_id=keycloak_id, offset=offset, limit=page_size
)
result = []
for experiment in experiments:
result.append(
ExperimentData(
id=experiment.id,
team_id=experiment.team_id,
team_name=experiment.team.name if experiment.team else None,
experiment_type_id=experiment.experiment_type_id,
experiment_type_name=experiment.experiment_type.name
if experiment.experiment_type
else None,
name=experiment.name,
description=experiment.description,
created_at=experiment.created_at,
instances_count=len(experiment.instances)
if experiment.instances
else 0,
)
)
return ExperimentListResponse(
experiments=result,
cur_page=page_num,
total_experiments=total_count,
page_size=page_size,
)
@router.delete("")
async def delete_experiment_request(
experiment_id: int = Query(...),
db: AsyncSession = Depends(get_db),
payload: dict = Depends(get_current_token_payload),
):
"""Delete an experiment (cascade will delete instances)"""
keycloak_id = payload.get("sub")
if not keycloak_id:
raise HTTPException(403, "Permission denied")
experiment = await get_single_experiment(db, experiment_id)
if not experiment:
raise HTTPException(404, "Experiment not found")
# Check if user has permission to delete experiments in this team
await check_team_permission(
["manage_experiments"], experiment.team_id, db, keycloak_id
)
deleted = await delete_experiment(db, experiment_id)
if not deleted:
raise HTTPException(404, "Experiment not found")
return {"message": "Experiment deleted successfully"}
# ============= INSTANCE ENDPOINTS =============
@router.post("/instance", response_model=CreateInstanceResponse)
async def create_instance_request(
create_data: CreateInstanceRequest,
db: AsyncSession = Depends(get_db),
payload: dict = Depends(get_current_token_payload),
) -> CreateInstanceResponse:
"""Add an instance to an experiment"""
keycloak_id = payload.get("sub")
if not keycloak_id:
raise HTTPException(403, "Permission denied")
experiment = await get_single_experiment(db, create_data.experiment_id)
if not experiment:
raise HTTPException(404, "Experiment not found")
# Check if user has permission to manage experiments in this team
await check_team_permission(
["manage_experiments"], experiment.team_id, db, keycloak_id
)
instance = await create_instance(
db=db,
experiment_id=create_data.experiment_id,
instance_data_id=create_data.instance_data_id,
name=create_data.name,
description=create_data.description,
)
return CreateInstanceResponse(
id=instance.id,
experiment_id=instance.experiment_id,
instance_data_id=instance.instance_data_id,
name=instance.name,
description=instance.description,
simulation_result_id=instance.simulation_result_id,
)
@router.put("/instance", response_model=UpdateInstanceResponse)
async def update_instance_request(
update_data: UpdateInstanceRequest,
db: AsyncSession = Depends(get_db),
payload: dict = Depends(get_current_token_payload),
) -> UpdateInstanceResponse:
"""Update instance data (cannot update instance_data_id)"""
keycloak_id = payload.get("sub")
if not keycloak_id:
raise HTTPException(403, "Permission denied")
# Get instance to find its experiment
instance = await get_single_instance(db, update_data.instance_id)
if not instance:
raise HTTPException(404, "Instance not found")
experiment = await get_single_experiment(db, instance.experiment_id)
if not experiment:
raise HTTPException(404, "Experiment not found")
# Check if user has permission to manage experiments in this team
await check_team_permission(
["manage_experiments"], experiment.team_id, db, keycloak_id
)
updated_instance = await update_instance(
db=db,
instance_id=update_data.instance_id,
name=update_data.name,
description=update_data.description,
)
return UpdateInstanceResponse(
id=updated_instance.id,
experiment_id=updated_instance.experiment_id,
instance_data_id=updated_instance.instance_data_id,
name=updated_instance.name,
description=updated_instance.description,
simulation_result_id=updated_instance.simulation_result_id,
)
@router.get("/instance", response_model=InstanceListResponse)
async def get_experiment_instances_request(
experiment_id: int = Query(),
page_num: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
db: AsyncSession = Depends(get_db),
payload: dict = Depends(get_current_token_payload),
) -> InstanceListResponse:
"""Get all instances of an experiment"""
keycloak_id = payload.get("sub")
if not keycloak_id:
raise HTTPException(403, "Permission denied")
experiment = await get_single_experiment(db, experiment_id)
if not experiment:
raise HTTPException(404, "Experiment not found")
# Check if user has access to this experiment's team
await check_team_permission([], experiment.team_id, db, keycloak_id)
offset = (page_num - 1) * page_size
instances, total_count = await get_experiment_instances(
db=db, experiment_id=experiment_id, offset=offset, limit=page_size
)
result = []
for instance in instances:
result.append(
InstanceData(
id=instance.id,
experiment_id=instance.experiment_id,
instance_data_id=instance.instance_data_id,
name=instance.name,
description=instance.description,
simulation_result_id=instance.simulation_result_id,
simulation_status=instance.simulation_result.status.name
if instance.simulation_result and instance.simulation_result.status
else None,
simulation_started_at=instance.simulation_result.started_at
if instance.simulation_result
else None,
simulation_ended_at=instance.simulation_result.ended_at
if instance.simulation_result
else None,
)
)
return InstanceListResponse(
instances=result,
cur_page=page_num,
total_instances=total_count,
page_size=page_size,
)
@router.delete("/instance")
async def delete_instance_request(
instance_id: int = Query(...),
db: AsyncSession = Depends(get_db),
payload: dict = Depends(get_current_token_payload),
):
"""Remove an instance from an experiment"""
keycloak_id = payload.get("sub")
if not keycloak_id:
raise HTTPException(403, "Permission denied")
# Get instance to find its experiment
instance = await get_single_instance(db, instance_id)
if not instance:
raise HTTPException(404, "Instance not found")
experiment = await get_single_experiment(db, instance.experiment_id)
if not experiment:
raise HTTPException(404, "Experiment not found")
# Check if user has permission to manage experiments in this team
await check_team_permission(
["manage_experiments"], experiment.team_id, db, keycloak_id
)
deleted = await delete_instance(db, instance_id)
if not deleted:
raise HTTPException(404, "Instance not found")
return {"message": "Instance deleted successfully"}
# ============= SIMULATION ENDPOINTS =============
@router.post("/start", response_model=StartExperimentResponse)
async def start_experiment_request(
start_data: StartExperimentRequest,
db: AsyncSession = Depends(get_db),
payload: dict = Depends(get_current_token_payload),
) -> StartExperimentResponse:
"""Start an experiment (create simulation result for an instance)"""
keycloak_id = payload.get("sub")
if not keycloak_id:
raise HTTPException(403, "Permission denied")
# Get instance to find its experiment
instance = await get_single_instance(db, start_data.instance_id)
if not instance:
raise HTTPException(404, "Instance not found")
experiment = await get_single_experiment(db, instance.experiment_id)
if not experiment:
raise HTTPException(404, "Experiment not found")
# Check if user has permission to run experiments in this team
await check_team_permission(
["run_experiments"], experiment.team_id, db, keycloak_id
)
# Create or update simulation result
simulation_result = await set_simulation_result(
db=db,
comp_system_id=start_data.comp_system_id,
simulation_result_id=start_data.simulation_result_id,
status_name="PENDING", # Start with PENDING status
started_at=datetime.now(),
)
# Link simulation result to instance if not already linked
if not instance.simulation_result_id:
await update_instance(
db=db,
instance_id=instance.id,
simulation_result_id=simulation_result.id,
)
return StartExperimentResponse(
instance_id=instance.id,
simulation_result_id=simulation_result.id,
status="PENDING",
started_at=simulation_result.started_at,
)
@router.get("/instance", response_model=InstanceData)
async def get_instance_request(
instance_id: int = Query(),
db: AsyncSession = Depends(get_db),
payload: dict = Depends(get_current_token_payload),
) -> InstanceData:
"""Get a single instance's data by ID"""
keycloak_id = payload.get("sub")
if not keycloak_id:
raise HTTPException(403, "Permission denied")
# Get instance with all relationships
instance = await get_single_instance(db, instance_id)
if not instance:
raise HTTPException(404, "Instance not found")
experiment = await get_single_experiment(db, instance.experiment_id)
if not experiment:
raise HTTPException(404, "Experiment not found")
# Check if user has access to this experiment's team
await check_team_permission([], experiment.team_id, db, keycloak_id)
return InstanceData(
id=instance.id,
experiment_id=instance.experiment_id,
instance_data_id=instance.instance_data_id,
name=instance.name,
description=instance.description,
simulation_result_id=instance.simulation_result_id,
simulation_status=instance.simulation_result.status.name
if instance.simulation_result and instance.simulation_result.status
else None,
simulation_started_at=instance.simulation_result.started_at
if instance.simulation_result
else None,
simulation_ended_at=instance.simulation_result.ended_at
if instance.simulation_result
else None,
)

View File

@@ -232,6 +232,9 @@ async def get_team_systems_request(
created_at = ts.created_at
break
if created_at == 0:
raise HTTPException(404, "Team data not found")
result.append(
SystemWithTeamResponse(
system=ComputationalSystemData(

View File

@@ -1,6 +1,7 @@
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from api_endpoint.experiment_api import router as experiment_router
from api_endpoint.health_api import router as health_router
from api_endpoint.machine_api import router as machine_router
from api_endpoint.teams_api import router as team_router
@@ -52,3 +53,4 @@ app.include_router(health_router, prefix="", tags=["Health"])
app.include_router(user_router, prefix="/user", tags=["User"])
app.include_router(team_router, prefix="/team", tags=["Team"])
app.include_router(machine_router, prefix="/machine", tags=["Machine"])
app.include_router(experiment_router, prefix="/experiment", tags=["Experiment"])

View File

@@ -0,0 +1,453 @@
from datetime import datetime
from typing import List, Optional, Tuple
from crud.team_crud import get_user_teams
from sql_models.models import (
Experiment,
ExperimentType,
Instance,
SimulationResult,
SimulationStatus,
Team,
)
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from sqlalchemy.sql.expression import func
from starlette.exceptions import HTTPException
# ============= HELPER FUNCTIONS =============
async def get_simulation_status_by_name(
db: AsyncSession, status_name: str
) -> SimulationStatus:
"""Get simulation status by name, raise exception if not found"""
result = await db.execute(
select(SimulationStatus).where(SimulationStatus.name == status_name)
)
status = result.scalar_one_or_none()
if not status:
raise HTTPException(
status_code=400,
detail=f"Invalid simulation status name: {status_name}. Valid statuses: PENDING, RUNNING, COMPLETED, FAILED",
)
return status
# ============= EXPERIMENT TYPE CRUD =============
async def get_all_experiment_types(
db: AsyncSession,
) -> List[ExperimentType]:
"""Get all existing experiment types"""
result = await db.execute(select(ExperimentType).order_by(ExperimentType.name))
return list(result.scalars().all())
async def create_experiment_type(
db: AsyncSession,
name: str,
file_frontend: str,
server_path: str,
file_comp_system: str,
description: Optional[str] = None,
) -> ExperimentType:
"""Create a new experiment type"""
# Check if experiment type with this name already exists
existing_result = await db.execute(
select(ExperimentType).where(ExperimentType.name == name)
)
existing = existing_result.scalar_one_or_none()
if existing:
raise HTTPException(
status_code=409, detail=f"Experiment type with name '{name}' already exists"
)
# Create new experiment type
experiment_type = ExperimentType(
name=name,
description=description,
file_frontend=file_frontend,
server_path=server_path,
file_comp_system=file_comp_system,
)
db.add(experiment_type)
await db.commit()
await db.refresh(experiment_type)
return experiment_type
# ============= EXPERIMENT CRUD =============
async def create_experiment(
db: AsyncSession,
team_id: int,
experiment_type_id: int,
name: str,
description: Optional[str] = None,
) -> Experiment:
"""Create an experiment of a specific type"""
team_result = await db.execute(select(Team).where(Team.id == team_id))
team = team_result.scalar_one_or_none()
if not team:
raise HTTPException(status_code=404, detail="Team not found")
# Verify experiment type exists
exp_type_result = await db.execute(
select(ExperimentType).where(ExperimentType.id == experiment_type_id)
)
experiment_type = exp_type_result.scalar_one_or_none()
if not experiment_type:
raise HTTPException(status_code=404, detail="Experiment type not found")
# Create experiment
experiment = Experiment(
team_id=team_id,
experiment_type_id=experiment_type_id,
name=name,
description=description,
created_at=datetime.now(),
)
db.add(experiment)
await db.commit()
await db.refresh(experiment, attribute_names=["team", "experiment_type"])
return experiment
async def update_experiment(
db: AsyncSession,
experiment_id: int,
name: Optional[str] = None,
description: Optional[str] = None,
) -> Experiment:
"""Update experiment data (cannot update type)"""
result = await db.execute(
select(Experiment)
.where(Experiment.id == experiment_id)
.options(
selectinload(Experiment.team), selectinload(Experiment.experiment_type)
)
)
experiment = result.scalar_one_or_none()
if not experiment:
raise HTTPException(status_code=404, detail="Experiment not found")
if name is not None:
experiment.name = name
if description is not None:
experiment.description = description
await db.commit()
await db.refresh(experiment, attribute_names=["team", "experiment_type"])
return experiment
async def get_user_experiments(
db: AsyncSession,
user_id: str,
offset: int = 0,
limit: int = 256,
) -> Tuple[List[Experiment], int]:
"""Get paginated experiments of user (via user's teams)"""
# Get user's team IDs first
user_teams_result, _ = await get_user_teams(db, user_id, 0, 255)
team_ids = [row.id for row in user_teams_result]
if not team_ids:
return [], 0
# Query experiments for those teams
query = select(Experiment).where(Experiment.team_id.in_(team_ids))
# Get paginated experiments
result = await db.execute(
query.options(
selectinload(Experiment.team),
selectinload(Experiment.experiment_type),
selectinload(Experiment.instances),
)
.order_by(Experiment.created_at.desc())
.offset(offset)
.limit(limit)
)
experiments = list(result.scalars().all())
# Get total count
count_query = (
select(func.count())
.select_from(Experiment)
.where(Experiment.team_id.in_(team_ids))
)
count_result = await db.execute(count_query)
total_count = count_result.scalar()
return experiments, total_count if total_count else 0
# Helper function to get single experiment (add to experiment_crud.py)
async def get_single_experiment(
db: AsyncSession,
experiment_id: int,
) -> Optional[Experiment]:
"""Get a single experiment with all relationships loaded"""
result = await db.execute(
select(Experiment)
.where(Experiment.id == experiment_id)
.options(
selectinload(Experiment.team),
selectinload(Experiment.experiment_type),
)
)
return result.scalar_one_or_none()
async def delete_experiment(
db: AsyncSession,
experiment_id: int,
) -> bool:
"""Delete an experiment (cascade will delete instances)"""
result = await db.execute(select(Experiment).where(Experiment.id == experiment_id))
experiment = result.scalar_one_or_none()
if not experiment:
return False
await db.delete(experiment)
await db.commit()
return True
async def create_instance(
db: AsyncSession,
experiment_id: int,
instance_data_id: int,
name: str,
description: Optional[str] = None,
) -> Instance:
"""Create an instance for an experiment"""
# Verify experiment exists
experiment_result = await db.execute(
select(Experiment).where(Experiment.id == experiment_id)
)
experiment = experiment_result.scalar_one_or_none()
if not experiment:
raise HTTPException(status_code=404, detail="Experiment not found")
# Create instance
instance = Instance(
experiment_id=experiment_id,
instance_data_id=instance_data_id,
name=name,
description=description,
)
db.add(instance)
await db.commit()
await db.refresh(instance, attribute_names=["experiment", "simulation_result"])
return instance
async def update_instance(
db: AsyncSession,
instance_id: int,
name: Optional[str] = None,
description: Optional[str] = None,
simulation_result_id: Optional[int] = None,
) -> Instance:
"""Update instance data (cannot update instance_data_id)"""
result = await db.execute(
select(Instance)
.where(Instance.id == instance_id)
.options(
selectinload(Instance.experiment), selectinload(Instance.simulation_result)
)
)
instance = result.scalar_one_or_none()
if not instance:
raise HTTPException(status_code=404, detail="Instance not found")
if name is not None:
instance.name = name
if description is not None:
instance.description = description
if simulation_result_id is not None:
# Verify simulation result exists
sim_result_result = await db.execute(
select(SimulationResult).where(SimulationResult.id == simulation_result_id)
)
if not sim_result_result.scalar_one_or_none():
raise HTTPException(status_code=404, detail="Simulation result not found")
instance.simulation_result_id = simulation_result_id
await db.commit()
await db.refresh(instance, attribute_names=["experiment", "simulation_result"])
return instance
async def get_experiment_instances(
db: AsyncSession,
experiment_id: int,
offset: int = 0,
limit: int = 256,
) -> Tuple[List[Instance], int]:
"""Get paginated instances of an experiment"""
# Verify experiment exists
experiment_result = await db.execute(
select(Experiment).where(Experiment.id == experiment_id)
)
if not experiment_result.scalar_one_or_none():
raise HTTPException(status_code=404, detail="Experiment not found")
# Query instances
query = select(Instance).where(Instance.experiment_id == experiment_id)
result = await db.execute(
query.options(
selectinload(Instance.experiment),
selectinload(Instance.simulation_result).selectinload(
SimulationResult.status
),
)
.order_by(Instance.id.desc())
.offset(offset)
.limit(limit)
)
instances = list(result.scalars().all())
# Get total count
count_query = (
select(func.count())
.select_from(Instance)
.where(Instance.experiment_id == experiment_id)
)
count_result = await db.execute(count_query)
total_count = count_result.scalar()
return instances, total_count if total_count else 0
async def get_single_instance(
db: AsyncSession,
instance_id: int,
) -> Optional[Instance]:
"""Get a single instance's data with all relationships loaded"""
result = await db.execute(
select(Instance)
.where(Instance.id == instance_id)
.options(
selectinload(Instance.experiment).selectinload(Experiment.experiment_type),
selectinload(Instance.simulation_result).selectinload(
SimulationResult.status
),
selectinload(Instance.simulation_result).selectinload(
SimulationResult.computational_system
),
)
)
return result.scalar_one_or_none()
async def delete_instance(
db: AsyncSession,
instance_id: int,
) -> bool:
"""Delete an instance from an experiment"""
result = await db.execute(select(Instance).where(Instance.id == instance_id))
instance = result.scalar_one_or_none()
if not instance:
return False
await db.delete(instance)
await db.commit()
return True
# ============= SIMULATION RESULT CRUD =============
async def set_simulation_result(
db: AsyncSession,
comp_system_id: int,
simulation_result_id: int,
status_name: str,
started_at: Optional[datetime] = None,
ended_at: Optional[datetime] = None,
) -> SimulationResult:
"""Create or update simulation result"""
# Verify computational system exists
from sql_models.models import ComputationalSystem
system_result = await db.execute(
select(ComputationalSystem).where(ComputationalSystem.id == comp_system_id)
)
if not system_result.scalar_one_or_none():
raise HTTPException(status_code=404, detail="Computational system not found")
# Get status
status = await get_simulation_status_by_name(db, status_name)
# Check if simulation result already exists
existing_result = await db.execute(
select(SimulationResult).where(
SimulationResult.simulation_result_id == simulation_result_id
)
)
simulation_result = existing_result.scalar_one_or_none()
if simulation_result:
# Update existing
simulation_result.comp_system_id = comp_system_id
simulation_result.status_id = status.id
simulation_result.started_at = started_at or simulation_result.started_at
simulation_result.ended_at = ended_at
else:
# Create new
simulation_result = SimulationResult(
comp_system_id=comp_system_id,
simulation_result_id=simulation_result_id,
status_id=status.id,
started_at=started_at or datetime.now(),
ended_at=ended_at,
)
db.add(simulation_result)
await db.commit()
await db.refresh(
simulation_result, attribute_names=["computational_system", "status"]
)
return simulation_result
async def get_simulation_result_by_id(
db: AsyncSession,
simulation_result_id: int,
) -> Optional[SimulationResult]:
"""Get a simulation result by its external ID"""
result = await db.execute(
select(SimulationResult)
.where(SimulationResult.simulation_result_id == simulation_result_id)
.options(
selectinload(SimulationResult.computational_system),
selectinload(SimulationResult.status),
)
)
return result.scalar_one_or_none()

View File

@@ -0,0 +1,171 @@
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel
# ============= EXPERIMENT TYPE MODELS =============
class ExperimentTypeData(BaseModel):
id: int
name: str
description: Optional[str] = None
file_frontend: str
server_path: str
file_comp_system: str
class CreateExperimentTypeRequest(BaseModel):
name: str
file_frontend: str
server_path: str
file_comp_system: str
description: Optional[str] = None
class CreateExperimentTypeResponse(BaseModel):
id: int
name: str
description: Optional[str] = None
file_frontend: str
server_path: str
file_comp_system: str
# ============= EXPERIMENT MODELS =============
class CreateExperimentRequest(BaseModel):
team_id: int
experiment_type_id: int
name: str
description: Optional[str] = None
class CreateExperimentResponse(BaseModel):
id: int
team_id: int
experiment_type_id: int
name: str
description: Optional[str] = None
created_at: datetime
class UpdateExperimentRequest(BaseModel):
experiment_id: int
name: Optional[str] = None
description: Optional[str] = None
class UpdateExperimentResponse(BaseModel):
id: int
team_id: int
experiment_type_id: int
name: str
description: Optional[str] = None
created_at: datetime
class ExperimentData(BaseModel):
id: int
team_id: int
team_name: Optional[str] = None
experiment_type_id: int
experiment_type_name: Optional[str] = None
name: str
description: Optional[str] = None
created_at: datetime
instances_count: int = 0
class ExperimentListResponse(BaseModel):
experiments: List[ExperimentData]
cur_page: int
total_experiments: int
page_size: int
# ============= INSTANCE MODELS =============
class CreateInstanceRequest(BaseModel):
experiment_id: int
instance_data_id: int
name: str
description: Optional[str] = None
class CreateInstanceResponse(BaseModel):
id: int
experiment_id: int
instance_data_id: int
name: str
description: Optional[str] = None
simulation_result_id: Optional[int] = None
class UpdateInstanceRequest(BaseModel):
instance_id: int
name: Optional[str] = None
description: Optional[str] = None
class UpdateInstanceResponse(BaseModel):
id: int
experiment_id: int
instance_data_id: int
name: str
description: Optional[str] = None
simulation_result_id: Optional[int] = None
class InstanceData(BaseModel):
id: int
experiment_id: int
instance_data_id: int
name: str
description: Optional[str] = None
simulation_result_id: Optional[int] = None
simulation_status: Optional[str] = None
simulation_started_at: Optional[datetime] = None
simulation_ended_at: Optional[datetime] = None
class InstanceListResponse(BaseModel):
instances: List[InstanceData]
cur_page: int
total_instances: int
page_size: int
# ============= SIMULATION MODELS =============
class StartExperimentRequest(BaseModel):
instance_id: int
comp_system_id: int
simulation_result_id: int
class StartExperimentResponse(BaseModel):
instance_id: int
simulation_result_id: int
status: str
started_at: datetime
class SetSimulationResultRequest(BaseModel):
comp_system_id: int
simulation_result_id: int
status_name: str
started_at: Optional[datetime] = None
ended_at: Optional[datetime] = None
class SimulationResultData(BaseModel):
id: int
simulation_result_id: int
comp_system_id: int
status: str
started_at: datetime
ended_at: Optional[datetime] = None

View File

@@ -202,9 +202,6 @@ class User(Base):
)
# Add these new classes before your existing User class
class ExperimentType(Base):
__tablename__ = "experiment_types"