added experiment api
This commit is contained in:
569
dockerfile_build/src/api_endpoint/experiment_api.py
Normal file
569
dockerfile_build/src/api_endpoint/experiment_api.py
Normal 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,
|
||||
)
|
||||
Reference in New Issue
Block a user