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,
)