All checks were successful
Build and Deploy Docker Image / build-and-push (push) Successful in 5m5s
working basic setup - added profile picture upload - fied a lot of errors with rabbitmq - updated the "module" tables - added experimental runner
969 lines
31 KiB
Python
969 lines
31 KiB
Python
import json
|
|
from datetime import datetime
|
|
from typing import List, Optional
|
|
|
|
from connections.db import get_db
|
|
from connections.keycloak import (
|
|
get_current_token_payload,
|
|
)
|
|
from connections.minio import (
|
|
ensure_bucket_exists,
|
|
get_file_from_minio,
|
|
upload_file_to_minio,
|
|
)
|
|
from connections.rabbitmq import (
|
|
bind_queues_to_team_exchange,
|
|
publish_task_to_team,
|
|
)
|
|
from crud.experiment_crud import (
|
|
create_experiment,
|
|
create_experiment_type,
|
|
create_instance,
|
|
delete_experiment,
|
|
delete_instance,
|
|
get_all_experiment_types,
|
|
get_experiment_instances,
|
|
get_experiment_type_by_id,
|
|
get_single_experiment,
|
|
get_single_instance,
|
|
get_user_experiments,
|
|
set_simulation_result,
|
|
update_experiment,
|
|
update_experiment_type,
|
|
update_instance,
|
|
)
|
|
|
|
# Get team's systems to find max qubits available
|
|
from crud.team_crud import (
|
|
check_team_permission,
|
|
get_team, # You'll need to import this
|
|
)
|
|
from fastapi import Depends, File, Query, UploadFile
|
|
from fastapi.param_functions import Form
|
|
from fastapi.routing import APIRouter
|
|
from rest_models.experiment_models import (
|
|
CreateExperimentRequest,
|
|
CreateExperimentTypeResponse,
|
|
CreateInstanceRequest,
|
|
ExperimentData,
|
|
ExperimentListResponse,
|
|
ExperimentTypeList,
|
|
InstanceData,
|
|
InstanceListResponse,
|
|
SimpleInstanceData,
|
|
SimulationResultData,
|
|
StartExperimentRequest,
|
|
UpdateExperimentRequest,
|
|
UpdateInstanceRequest,
|
|
)
|
|
from rest_models.machine_models import (
|
|
ComputationalSystemShortData,
|
|
)
|
|
from rest_models.team_models import (
|
|
TeamsShortListResponse,
|
|
)
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from starlette.exceptions import HTTPException
|
|
from starlette.responses import StreamingResponse
|
|
from starlette.status import HTTP_200_OK
|
|
|
|
router = APIRouter(tags=["Experiments"])
|
|
|
|
|
|
# ============= HELPER FUNC ===========================
|
|
#
|
|
async def validate_instance_qubits(
|
|
db: AsyncSession, team_id: int, qubits_needed: int
|
|
) -> None:
|
|
"""
|
|
Validate that qubits_needed is within allowed range for the team.
|
|
|
|
Args:
|
|
db: Database session
|
|
team_id: Team ID to check systems for
|
|
qubits_needed: Number of qubits needed for the instance
|
|
|
|
Raises:
|
|
HTTPException: If validation fails
|
|
"""
|
|
# Check qubits is positive
|
|
if qubits_needed <= 0:
|
|
raise HTTPException(
|
|
400,
|
|
f"Invalid qubits_needed: {qubits_needed}. Qubits must be greater than 0.",
|
|
)
|
|
|
|
team_systems = await get_team(db, team_id)
|
|
|
|
if not team_systems:
|
|
raise HTTPException(
|
|
400,
|
|
"Team has no computational systems configured. "
|
|
"Cannot validate qubits requirement.",
|
|
)
|
|
|
|
max_qubits = max(system.qubits_given for system in team_systems.team_systems)
|
|
|
|
if qubits_needed > max_qubits:
|
|
raise HTTPException(
|
|
400,
|
|
f"Instance requires {qubits_needed} qubits, but the maximum available "
|
|
f"in team's systems is {max_qubits} qubits.",
|
|
)
|
|
|
|
|
|
def get_experiment_collective_status(simulation_statuses: list[str]) -> str:
|
|
"""
|
|
Get the collective status of an experiment based on its simulation statuses.
|
|
|
|
Args:
|
|
simulation_statuses: List of simulation status strings (e.g., ["DRAFT", "COMPLETE", ...])
|
|
|
|
Returns:
|
|
Collective experiment status: "DRAFT", "PROCESSING", "IN QUEUE", or "COMPLETE"
|
|
"""
|
|
if not simulation_statuses:
|
|
return "DRAFT"
|
|
|
|
# Check if all are DRAFT (or not set/empty)
|
|
if all(status == "DRAFT" for status in simulation_statuses):
|
|
return "DRAFT"
|
|
|
|
# Check if any is PROCESSING
|
|
if any(
|
|
status == "PROCESSING" or status == "IN SYSTEM"
|
|
for status in simulation_statuses
|
|
):
|
|
return "RUNNING"
|
|
|
|
# Check if any is IN QUEUE (and none are PROCESSING)
|
|
if any(status == "IN QUEUE" for status in simulation_statuses):
|
|
return "IN QUEUE"
|
|
|
|
# If all are COMPLETE or ERROR
|
|
if all(status in ["COMPLETE", "ERROR"] for status in simulation_statuses):
|
|
if any(status == "ERROR" for status in simulation_statuses):
|
|
return "COMPLETE WITH ERROR"
|
|
return "COMPLETE"
|
|
|
|
# Default fallback (should not happen with valid statuses)
|
|
return "DRAFT"
|
|
|
|
|
|
# ============= EXPERIMENT TYPE ENDPOINTS =============
|
|
|
|
|
|
@router.get("/types", response_model=List[ExperimentTypeList])
|
|
async def get_experiment_types_request(
|
|
db: AsyncSession = Depends(get_db),
|
|
payload: dict = Depends(get_current_token_payload),
|
|
) -> List[ExperimentTypeList]:
|
|
"""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 [
|
|
ExperimentTypeList(
|
|
id=exp_type.id,
|
|
name=exp_type.name,
|
|
description=exp_type.description,
|
|
)
|
|
for exp_type in experiment_types
|
|
]
|
|
|
|
|
|
@router.post("/types", response_model=CreateExperimentTypeResponse)
|
|
async def create_experiment_type_request(
|
|
name: str = Form(),
|
|
file_frontend: UploadFile = File(...),
|
|
file_comp_system: UploadFile = File(...),
|
|
description: Optional[str] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
payload: dict = Depends(get_current_token_payload),
|
|
) -> CreateExperimentTypeResponse:
|
|
"""Create a new experiment type with file uploads to MinIO (admin only)"""
|
|
|
|
# Check authentication
|
|
keycloak_id = payload.get("sub")
|
|
if not keycloak_id:
|
|
raise HTTPException(status_code=403, detail="Permission denied")
|
|
|
|
# TODO: Add admin permission check here if needed
|
|
|
|
# Ensure MinIO bucket exists
|
|
await ensure_bucket_exists()
|
|
|
|
# First create the experiment type in DB to get ID
|
|
experiment_type = await create_experiment_type(
|
|
db=db,
|
|
name=name,
|
|
description=description,
|
|
file_frontend="", # Temporary, will update
|
|
file_comp_system="", # Temporary, will update
|
|
)
|
|
|
|
try:
|
|
# Upload files using the new ID
|
|
frontend_path = await upload_file_to_minio(
|
|
file=file_frontend, experiment_type_id=experiment_type.id
|
|
)
|
|
|
|
comp_system_path = await upload_file_to_minio(
|
|
file=file_comp_system, experiment_type_id=experiment_type.id
|
|
)
|
|
|
|
# Update experiment type with file paths
|
|
experiment_type.file_frontend = frontend_path
|
|
experiment_type.file_comp_system = comp_system_path
|
|
await db.commit()
|
|
await db.refresh(experiment_type)
|
|
|
|
return CreateExperimentTypeResponse(
|
|
id=experiment_type.id,
|
|
name=experiment_type.name,
|
|
description=experiment_type.description,
|
|
)
|
|
|
|
except Exception as e:
|
|
# Rollback if upload fails
|
|
await db.rollback()
|
|
raise HTTPException(
|
|
500,
|
|
detail=f"Failed to create experiment type: {str(e)}",
|
|
)
|
|
|
|
|
|
@router.put("/types", response_model=CreateExperimentTypeResponse)
|
|
async def update_type_request(
|
|
type_id: int = Form(),
|
|
name: Optional[str] = Form(None),
|
|
file_frontend: Optional[UploadFile] | None = File(None),
|
|
file_comp_system: Optional[UploadFile] | None = File(None),
|
|
description: Optional[str] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
payload: dict = Depends(get_current_token_payload),
|
|
) -> CreateExperimentTypeResponse:
|
|
"""Create a new experiment type with file uploads to MinIO (admin only)"""
|
|
|
|
# Check authentication
|
|
keycloak_id = payload.get("sub")
|
|
if not keycloak_id:
|
|
raise HTTPException(status_code=403, detail="Permission denied")
|
|
|
|
# Ensure MinIO bucket exists
|
|
await ensure_bucket_exists()
|
|
|
|
frontend_path = None
|
|
comp_path = None
|
|
|
|
if file_frontend:
|
|
frontend_path = await upload_file_to_minio(
|
|
file=file_frontend, experiment_type_id=type_id
|
|
)
|
|
|
|
if file_comp_system:
|
|
comp_path = await upload_file_to_minio(
|
|
file=file_comp_system, experiment_type_id=type_id
|
|
)
|
|
|
|
# First create the experiment type in DB to get ID
|
|
experiment_type = await update_experiment_type(
|
|
db=db,
|
|
id=type_id,
|
|
name=name,
|
|
description=description,
|
|
file_frontend=frontend_path, # Temporary, will update
|
|
file_comp_system=comp_path, # Temporary, will update
|
|
)
|
|
|
|
return CreateExperimentTypeResponse(
|
|
id=experiment_type.id,
|
|
name=experiment_type.name,
|
|
description=experiment_type.description,
|
|
)
|
|
|
|
|
|
@router.get("/types/frontend")
|
|
async def get_frontend_file(
|
|
experiment_type_id: int = Query(),
|
|
db: AsyncSession = Depends(get_db),
|
|
payload: dict = Depends(get_current_token_payload),
|
|
):
|
|
"""Get the frontend HTML file"""
|
|
|
|
# Get experiment type from database
|
|
experiment_type = await get_experiment_type_by_id(db, experiment_type_id)
|
|
|
|
if not experiment_type:
|
|
raise HTTPException(status_code=404, detail="Experiment type not found")
|
|
|
|
if not experiment_type.file_frontend:
|
|
raise HTTPException(status_code=404, detail="Frontend file not found")
|
|
|
|
# Get file from MinIO
|
|
file_response = await get_file_from_minio(experiment_type.file_frontend)
|
|
|
|
# Return file as streaming response with correct content type
|
|
return StreamingResponse(
|
|
file_response,
|
|
media_type="text/html",
|
|
headers={
|
|
"Content-Disposition": f"inline; filename=frontend_{experiment_type_id}.html"
|
|
},
|
|
)
|
|
|
|
|
|
@router.get("/types/comp-system")
|
|
async def get_comp_system_file(
|
|
experiment_type_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
payload: dict = Depends(get_current_token_payload),
|
|
):
|
|
"""Get the comp system Python file"""
|
|
|
|
# Get experiment type from database
|
|
experiment_type = await get_experiment_type_by_id(db, experiment_type_id)
|
|
|
|
if not experiment_type:
|
|
raise HTTPException(status_code=404, detail="Experiment type not found")
|
|
|
|
if not experiment_type.file_comp_system:
|
|
raise HTTPException(status_code=404, detail="Comp system file not found")
|
|
|
|
# Get file from MinIO
|
|
file_response = await get_file_from_minio(experiment_type.file_comp_system)
|
|
|
|
# Return Python file for download (since it will be imported dynamically)
|
|
return StreamingResponse(
|
|
file_response,
|
|
media_type="text/x-python",
|
|
headers={
|
|
"Content-Disposition": f"attachment; filename=comp_system_{experiment_type_id}.py"
|
|
},
|
|
)
|
|
|
|
|
|
# ============= EXPERIMENT ENDPOINTS =============
|
|
|
|
|
|
@router.post("", response_model=ExperimentData)
|
|
async def create_experiment_request(
|
|
create_data: CreateExperimentRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
payload: dict = Depends(get_current_token_payload),
|
|
) -> ExperimentData:
|
|
"""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 ExperimentData(
|
|
id=experiment.id,
|
|
team=TeamsShortListResponse(
|
|
team_id=experiment.team.id, team_name=experiment.team.name
|
|
),
|
|
name=experiment.name,
|
|
description=experiment.description,
|
|
created_at=experiment.created_at,
|
|
experiment_type=ExperimentTypeList(
|
|
id=experiment.experiment_type.id,
|
|
name=experiment.experiment_type.name,
|
|
description=experiment.experiment_type.description,
|
|
),
|
|
instances_count=0,
|
|
status=get_experiment_collective_status(
|
|
list(
|
|
map(
|
|
lambda x: (
|
|
x.simulation_result.status.name
|
|
if x.simulation_result
|
|
else "DRAFT"
|
|
),
|
|
experiment.instances,
|
|
)
|
|
)
|
|
),
|
|
instance_preview=list(
|
|
map(
|
|
lambda inst: SimpleInstanceData(
|
|
id=inst.id,
|
|
instance_data={},
|
|
name=inst.name,
|
|
qubits_needed=inst.qubits_needed,
|
|
description=inst.description,
|
|
),
|
|
experiment.instances[:6],
|
|
)
|
|
),
|
|
)
|
|
|
|
|
|
@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=TeamsShortListResponse(
|
|
team_id=experiment.team.id, team_name=experiment.team.name
|
|
),
|
|
name=experiment.name,
|
|
description=experiment.description,
|
|
created_at=experiment.created_at,
|
|
experiment_type=ExperimentTypeList(
|
|
id=experiment.experiment_type.id,
|
|
name=experiment.experiment_type.name,
|
|
description=experiment.experiment_type.description,
|
|
),
|
|
instances_count=len(experiment.instances),
|
|
status=get_experiment_collective_status(
|
|
list(
|
|
map(
|
|
lambda x: (
|
|
x.simulation_result.status.name
|
|
if x.simulation_result
|
|
else "DRAFT"
|
|
),
|
|
experiment.instances,
|
|
)
|
|
)
|
|
),
|
|
instance_preview=list(
|
|
map(
|
|
lambda inst: SimpleInstanceData(
|
|
id=inst.id,
|
|
instance_data="{}",
|
|
name=inst.name,
|
|
qubits_needed=inst.qubits_needed,
|
|
description=inst.description,
|
|
),
|
|
experiment.instances[:6],
|
|
)
|
|
),
|
|
)
|
|
|
|
|
|
@router.put("", response_model=ExperimentData)
|
|
async def update_experiment_request(
|
|
update_data: UpdateExperimentRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
payload: dict = Depends(get_current_token_payload),
|
|
) -> ExperimentData:
|
|
"""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 ExperimentData(
|
|
id=updated_experiment.id,
|
|
team=TeamsShortListResponse(
|
|
team_id=updated_experiment.team.id, team_name=updated_experiment.team.name
|
|
),
|
|
name=updated_experiment.name,
|
|
description=updated_experiment.description,
|
|
created_at=updated_experiment.created_at,
|
|
experiment_type=ExperimentTypeList(
|
|
id=updated_experiment.experiment_type.id,
|
|
name=updated_experiment.experiment_type.name,
|
|
description=updated_experiment.experiment_type.description,
|
|
),
|
|
instances_count=len(experiment.instances),
|
|
status=get_experiment_collective_status(
|
|
list(
|
|
map(
|
|
lambda x: (
|
|
x.simulation_result.status.name
|
|
if x.simulation_result
|
|
else "DRAFT"
|
|
),
|
|
experiment.instances,
|
|
)
|
|
)
|
|
),
|
|
instance_preview=list(
|
|
map(
|
|
lambda inst: SimpleInstanceData(
|
|
id=inst.id,
|
|
instance_data=json.dumps(inst.instance_data),
|
|
name=inst.name,
|
|
qubits_needed=inst.qubits_needed,
|
|
description=inst.description,
|
|
),
|
|
updated_experiment.instances[:6],
|
|
)
|
|
),
|
|
)
|
|
|
|
|
|
@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=TeamsShortListResponse(
|
|
team_id=experiment.team.id, team_name=experiment.team.name
|
|
),
|
|
name=experiment.name,
|
|
description=experiment.description,
|
|
created_at=experiment.created_at,
|
|
experiment_type=ExperimentTypeList(
|
|
id=experiment.experiment_type.id,
|
|
name=experiment.experiment_type.name,
|
|
description=experiment.experiment_type.description,
|
|
),
|
|
instances_count=len(experiment.instances),
|
|
status=get_experiment_collective_status(
|
|
list(
|
|
map(
|
|
lambda x: (
|
|
x.simulation_result.status.name
|
|
if x.simulation_result
|
|
else "DRAFT"
|
|
),
|
|
experiment.instances,
|
|
)
|
|
)
|
|
),
|
|
instance_preview=list(
|
|
map(
|
|
lambda inst: SimpleInstanceData(
|
|
id=inst.id,
|
|
instance_data="{}",
|
|
name=inst.name,
|
|
qubits_needed=inst.qubits_needed,
|
|
description=inst.description,
|
|
),
|
|
experiment.instances[:6],
|
|
)
|
|
),
|
|
)
|
|
)
|
|
|
|
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=SimpleInstanceData)
|
|
async def create_instance_request(
|
|
create_data: CreateInstanceRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
payload: dict = Depends(get_current_token_payload),
|
|
) -> SimpleInstanceData:
|
|
"""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=create_data.instance_data,
|
|
name=create_data.name,
|
|
description=create_data.description,
|
|
)
|
|
|
|
return SimpleInstanceData(
|
|
id=instance.id,
|
|
instance_data=json.dumps(create_data.instance_data),
|
|
name=instance.name,
|
|
qubits_needed=instance.qubits_needed,
|
|
description=instance.description,
|
|
)
|
|
|
|
|
|
@router.put("/instance", response_model=SimpleInstanceData)
|
|
async def update_instance_request(
|
|
update_data: UpdateInstanceRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
payload: dict = Depends(get_current_token_payload),
|
|
) -> SimpleInstanceData:
|
|
"""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
|
|
)
|
|
|
|
if update_data.qubits_needed is not None:
|
|
await validate_instance_qubits(
|
|
db, experiment.team_id, update_data.qubits_needed
|
|
)
|
|
|
|
updated_instance = await update_instance(
|
|
db=db,
|
|
instance_id=update_data.instance_id,
|
|
name=update_data.name,
|
|
description=update_data.description,
|
|
instance_data=update_data.instance_data,
|
|
qubits_needed=update_data.qubits_needed,
|
|
)
|
|
|
|
return SimpleInstanceData(
|
|
id=updated_instance.id,
|
|
instance_data=json.dumps(updated_instance.instance_data),
|
|
name=updated_instance.name,
|
|
qubits_needed=updated_instance.qubits_needed,
|
|
description=updated_instance.description,
|
|
)
|
|
|
|
|
|
@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(
|
|
instance_id=instance.id,
|
|
instance_data=json.dumps(instance.instance_data),
|
|
name=instance.name,
|
|
qubits_needed=instance.qubits_needed,
|
|
description=instance.description,
|
|
simulation_result=SimulationResultData(
|
|
id=instance.simulation_result.id,
|
|
comp_system=ComputationalSystemShortData(
|
|
system_id=instance.simulation_result.computational_system.id,
|
|
system_name=instance.simulation_result.computational_system.system_name,
|
|
)
|
|
if instance.simulation_result.computational_system
|
|
else None,
|
|
simulation_result=json.dumps(
|
|
instance.simulation_result.simulation_result
|
|
),
|
|
status=instance.simulation_result.status.name,
|
|
started_at=instance.simulation_result.started_at,
|
|
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"}
|
|
|
|
|
|
@router.get("/instance/id", 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(
|
|
instance_id=instance.id,
|
|
instance_data=json.dumps(instance.instance_data),
|
|
name=instance.name,
|
|
description=instance.description,
|
|
qubits_needed=instance.qubits_needed,
|
|
simulation_result=SimulationResultData(
|
|
id=instance.simulation_result.id,
|
|
comp_system=ComputationalSystemShortData(
|
|
system_id=instance.simulation_result.computational_system.id,
|
|
system_name=instance.simulation_result.computational_system.system_name,
|
|
)
|
|
if instance.simulation_result.computational_system
|
|
else None,
|
|
simulation_result=json.dumps(instance.simulation_result.simulation_result),
|
|
status=instance.simulation_result.status.name,
|
|
started_at=instance.simulation_result.started_at,
|
|
ended_at=instance.simulation_result.ended_at,
|
|
)
|
|
if instance.simulation_result
|
|
else None,
|
|
)
|
|
|
|
|
|
# ============= SIMULATION ENDPOINTS =============
|
|
|
|
|
|
@router.post("/start")
|
|
async def start_experiment_request(
|
|
start_data: StartExperimentRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
payload: dict = Depends(get_current_token_payload),
|
|
):
|
|
"""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
|
|
experiment = await get_single_experiment(db, start_data.experiment_id)
|
|
if not experiment:
|
|
raise HTTPException(404, "Experiment not found")
|
|
|
|
if not experiment.instances:
|
|
raise HTTPException(400, "No instances")
|
|
|
|
for task in experiment.instances:
|
|
if task.simulation_result:
|
|
# If simulation result exists, check its status
|
|
current_status = task.simulation_result.status.name
|
|
if current_status != "DRAFT":
|
|
raise HTTPException(
|
|
400,
|
|
f"Cannot start experiment. Task {task.id} has status '{current_status}'. "
|
|
f"Only tasks with 'draft' status can be started.",
|
|
)
|
|
|
|
# Check if user has permission to run experiments in this team
|
|
await check_team_permission(
|
|
["run_experiments"], experiment.team_id, db, keycloak_id
|
|
)
|
|
|
|
# TODO: add the starting rabbitMQ logic
|
|
max_qubits = max(i.qubits_given for i in experiment.team.team_systems)
|
|
|
|
for task in experiment.instances:
|
|
if task.qubits_needed > max_qubits or task.qubits_needed <= 0:
|
|
raise HTTPException(
|
|
400,
|
|
f"Cannot start experiment. Instance {task.id} requires {task.qubits_needed} qubits, "
|
|
f"but the maximum available in team's systems is {max_qubits} qubits.",
|
|
)
|
|
|
|
await bind_queues_to_team_exchange(experiment.team_id, max_qubits)
|
|
|
|
for task in sorted(
|
|
experiment.instances, key=lambda x: x.qubits_needed, reverse=True
|
|
):
|
|
try:
|
|
# Publish task to queue
|
|
await publish_task_to_team(task)
|
|
|
|
# Create simulation result with IN_QUEUE status
|
|
# Pass None for comp_system_id since it's not assigned yet
|
|
simulation_result = await set_simulation_result(
|
|
db=db,
|
|
comp_system_id=None, # No system assigned yet
|
|
instance_id=task.id,
|
|
status_name="IN QUEUE",
|
|
started_at=datetime.now(),
|
|
ended_at=None,
|
|
simulation_result_data={},
|
|
)
|
|
|
|
except Exception as e:
|
|
# Create failed simulation result
|
|
await set_simulation_result(
|
|
db=db,
|
|
comp_system_id=None,
|
|
instance_id=task.id,
|
|
status_name="ERROR",
|
|
started_at=datetime.now(),
|
|
ended_at=datetime.now(),
|
|
simulation_result_data={"error": str(e)},
|
|
)
|
|
raise HTTPException(500, f"Failed to publish task {task.id}: {str(e)}")
|
|
|
|
return HTTP_200_OK
|