v1.0
All checks were successful
Build and Deploy Docker Image / build-and-push (push) Successful in 5m5s
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
This commit is contained in:
@@ -1,10 +1,20 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
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,
|
||||
@@ -13,49 +23,141 @@ from crud.experiment_crud import (
|
||||
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,
|
||||
)
|
||||
from crud.team_crud import check_team_permission
|
||||
from fastapi import Depends, Query
|
||||
|
||||
# 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,
|
||||
CreateExperimentResponse,
|
||||
CreateExperimentTypeRequest,
|
||||
CreateExperimentTypeResponse,
|
||||
CreateInstanceRequest,
|
||||
CreateInstanceResponse,
|
||||
ExperimentData,
|
||||
ExperimentListResponse,
|
||||
ExperimentTypeData,
|
||||
ExperimentTypeList,
|
||||
InstanceData,
|
||||
InstanceListResponse,
|
||||
SimpleInstanceData,
|
||||
SimulationResultData,
|
||||
StartExperimentRequest,
|
||||
StartExperimentResponse,
|
||||
UpdateExperimentRequest,
|
||||
UpdateExperimentResponse,
|
||||
UpdateInstanceRequest,
|
||||
UpdateInstanceResponse,
|
||||
)
|
||||
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[ExperimentTypeData])
|
||||
@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[ExperimentTypeData]:
|
||||
) -> List[ExperimentTypeList]:
|
||||
"""Get all existing experiment types"""
|
||||
keycloak_id = payload.get("sub")
|
||||
|
||||
@@ -65,13 +167,10 @@ async def get_experiment_types_request(
|
||||
experiment_types = await get_all_experiment_types(db)
|
||||
|
||||
return [
|
||||
ExperimentTypeData(
|
||||
ExperimentTypeList(
|
||||
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
|
||||
]
|
||||
@@ -79,46 +178,184 @@ async def get_experiment_types_request(
|
||||
|
||||
@router.post("/types", response_model=CreateExperimentTypeResponse)
|
||||
async def create_experiment_type_request(
|
||||
create_data: CreateExperimentTypeRequest,
|
||||
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 (admin only)"""
|
||||
keycloak_id = payload.get("sub")
|
||||
"""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(403, "Permission denied")
|
||||
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=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,
|
||||
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,
|
||||
file_frontend=experiment_type.file_frontend,
|
||||
server_path=experiment_type.server_path,
|
||||
file_comp_system=experiment_type.file_comp_system,
|
||||
)
|
||||
|
||||
|
||||
@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=CreateExperimentResponse)
|
||||
@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),
|
||||
) -> CreateExperimentResponse:
|
||||
) -> ExperimentData:
|
||||
"""Create a new experiment"""
|
||||
keycloak_id = payload.get("sub")
|
||||
|
||||
@@ -138,13 +375,44 @@ async def create_experiment_request(
|
||||
description=create_data.description,
|
||||
)
|
||||
|
||||
return CreateExperimentResponse(
|
||||
return ExperimentData(
|
||||
id=experiment.id,
|
||||
team_id=experiment.team_id,
|
||||
experiment_type_id=experiment.experiment_type_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],
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -169,25 +437,51 @@ async def get_experiment_request(
|
||||
|
||||
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,
|
||||
team=TeamsShortListResponse(
|
||||
team_id=experiment.team.id, team_name=experiment.team.name
|
||||
),
|
||||
name=experiment.name,
|
||||
description=experiment.description,
|
||||
created_at=experiment.created_at,
|
||||
instances_count=len(experiment.instances) if experiment.instances else 0,
|
||||
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=UpdateExperimentResponse)
|
||||
@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),
|
||||
) -> UpdateExperimentResponse:
|
||||
) -> ExperimentData:
|
||||
"""Update experiment data (cannot update type)"""
|
||||
keycloak_id = payload.get("sub")
|
||||
|
||||
@@ -210,13 +504,44 @@ async def update_experiment_request(
|
||||
description=update_data.description,
|
||||
)
|
||||
|
||||
return UpdateExperimentResponse(
|
||||
return ExperimentData(
|
||||
id=updated_experiment.id,
|
||||
team_id=updated_experiment.team_id,
|
||||
experiment_type_id=updated_experiment.experiment_type_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],
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -244,18 +569,42 @@ async def get_user_experiments_request(
|
||||
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,
|
||||
team=TeamsShortListResponse(
|
||||
team_id=experiment.team.id, team_name=experiment.team.name
|
||||
),
|
||||
name=experiment.name,
|
||||
description=experiment.description,
|
||||
created_at=experiment.created_at,
|
||||
instances_count=len(experiment.instances)
|
||||
if experiment.instances
|
||||
else 0,
|
||||
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],
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -299,12 +648,12 @@ async def delete_experiment_request(
|
||||
# ============= INSTANCE ENDPOINTS =============
|
||||
|
||||
|
||||
@router.post("/instance", response_model=CreateInstanceResponse)
|
||||
@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),
|
||||
) -> CreateInstanceResponse:
|
||||
) -> SimpleInstanceData:
|
||||
"""Add an instance to an experiment"""
|
||||
keycloak_id = payload.get("sub")
|
||||
|
||||
@@ -323,27 +672,26 @@ async def create_instance_request(
|
||||
instance = await create_instance(
|
||||
db=db,
|
||||
experiment_id=create_data.experiment_id,
|
||||
instance_data_id=create_data.instance_data_id,
|
||||
instance_data=create_data.instance_data,
|
||||
name=create_data.name,
|
||||
description=create_data.description,
|
||||
)
|
||||
|
||||
return CreateInstanceResponse(
|
||||
return SimpleInstanceData(
|
||||
id=instance.id,
|
||||
experiment_id=instance.experiment_id,
|
||||
instance_data_id=instance.instance_data_id,
|
||||
instance_data=json.dumps(create_data.instance_data),
|
||||
name=instance.name,
|
||||
qubits_needed=instance.qubits_needed,
|
||||
description=instance.description,
|
||||
simulation_result_id=instance.simulation_result_id,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/instance", response_model=UpdateInstanceResponse)
|
||||
@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),
|
||||
) -> UpdateInstanceResponse:
|
||||
) -> SimpleInstanceData:
|
||||
"""Update instance data (cannot update instance_data_id)"""
|
||||
keycloak_id = payload.get("sub")
|
||||
|
||||
@@ -364,20 +712,26 @@ async def update_instance_request(
|
||||
["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 UpdateInstanceResponse(
|
||||
return SimpleInstanceData(
|
||||
id=updated_instance.id,
|
||||
experiment_id=updated_instance.experiment_id,
|
||||
instance_data_id=updated_instance.instance_data_id,
|
||||
instance_data=json.dumps(updated_instance.instance_data),
|
||||
name=updated_instance.name,
|
||||
qubits_needed=updated_instance.qubits_needed,
|
||||
description=updated_instance.description,
|
||||
simulation_result_id=updated_instance.simulation_result_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -412,19 +766,26 @@ async def get_experiment_instances_request(
|
||||
for instance in instances:
|
||||
result.append(
|
||||
InstanceData(
|
||||
id=instance.id,
|
||||
experiment_id=instance.experiment_id,
|
||||
instance_data_id=instance.instance_data_id,
|
||||
instance_id=instance.id,
|
||||
instance_data=json.dumps(instance.instance_data),
|
||||
name=instance.name,
|
||||
qubits_needed=instance.qubits_needed,
|
||||
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
|
||||
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,
|
||||
)
|
||||
@@ -472,61 +833,7 @@ async def delete_instance_request(
|
||||
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)
|
||||
@router.get("/instance/id", response_model=InstanceData)
|
||||
async def get_instance_request(
|
||||
instance_id: int = Query(),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
@@ -551,19 +858,111 @@ async def get_instance_request(
|
||||
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,
|
||||
instance_id=instance.id,
|
||||
instance_data=json.dumps(instance.instance_data),
|
||||
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
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user