GotthardG 5a0047b6d5 Refactor AareDB backend and update schemas and paths.
Revised backend schema definitions, removing unnecessary attributes and adding new configurations. Updated file path references to align with the aaredb structure. Cleaned up redundant notebook content and commented out unused database regeneration logic in the backend.

Added posting a result to the database
2025-03-17 11:51:07 +01:00

391 lines
12 KiB
Python

from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, Form
from sqlalchemy.orm import Session
from pathlib import Path
from typing import List
from datetime import datetime
import shutil
from app.schemas import (
Puck as PuckSchema,
Sample as SampleSchema,
SampleEventCreate,
Sample,
Image,
ImageCreate,
SampleResult,
ExperimentParametersCreate,
ExperimentParametersRead,
ImageInfo,
ResultResponse,
ResultCreate,
)
from app.models import (
Puck as PuckModel,
Sample as SampleModel,
SampleEvent as SampleEventModel,
Image as ImageModel,
Dewar as DewarModel,
ExperimentParameters as ExperimentParametersModel,
# ExperimentParameters,
Results as ResultsModel,
)
from app.dependencies import get_db
import logging
from sqlalchemy.orm import joinedload
router = APIRouter()
@router.get("/{puck_id}/samples", response_model=List[SampleSchema])
async def get_samples_with_events(puck_id: int, db: Session = Depends(get_db)):
puck = db.query(PuckModel).filter(PuckModel.id == puck_id).first()
if not puck:
raise HTTPException(status_code=404, detail="Puck not found")
samples = db.query(SampleModel).filter(SampleModel.puck_id == puck_id).all()
for sample in samples:
sample.events = (
db.query(SampleEventModel)
.filter(SampleEventModel.sample_id == sample.id)
.all()
)
return samples
@router.get("/pucks-samples", response_model=List[PuckSchema])
async def get_all_pucks_with_samples_and_events(
active_pgroup: str, db: Session = Depends(get_db)
):
logging.info(
"Fetching all pucks with " "samples and events for active_pgroup: %s",
active_pgroup,
)
pucks = (
db.query(PuckModel)
.join(PuckModel.samples) # Join samples related to the puck
.join(PuckModel.dewar) # Join the dewar from the puck
.join(SampleModel.events) # Join sample events
.filter(DewarModel.pgroups == active_pgroup) # Filter by the dewar's group
.options(
joinedload(PuckModel.samples).joinedload(SampleModel.events),
joinedload(PuckModel.dewar),
)
.distinct() # Avoid duplicate puck rows if there are multiple events/samples
.all()
)
if not pucks:
raise HTTPException(
status_code=404,
detail="No pucks found with" " sample events for the active pgroup",
)
# Extract samples from each puck if needed
filtered_samples = []
for puck in pucks:
if puck.dewar and getattr(puck.dewar, "pgroups", None) == active_pgroup:
for sample in puck.samples:
filtered_samples.append(sample)
# Depending on what your endpoint expects,
# you may choose to return pucks or samples.
# For now, we're returning the list of pucks.
return pucks
# Route to post a new sample event
@router.post("/samples/{sample_id}/events", response_model=Sample)
async def create_sample_event(
sample_id: int, event: SampleEventCreate, db: Session = Depends(get_db)
):
# Ensure the sample exists
sample = db.query(SampleModel).filter(SampleModel.id == sample_id).first()
if not sample:
raise HTTPException(status_code=404, detail="Sample not found")
# Create the event
sample_event = SampleEventModel(
sample_id=sample_id,
event_type=event.event_type,
timestamp=datetime.now(), # Use the current timestamp
)
db.add(sample_event)
db.commit()
db.refresh(sample_event)
# Load events for the sample to be serialized in the response
sample.events = (
db.query(SampleEventModel).filter(SampleEventModel.sample_id == sample_id).all()
)
return sample # Return the sample, now including `mount_count`
@router.post("/{sample_id}/upload-images", response_model=Image)
async def upload_sample_image(
sample_id: int,
uploaded_file: UploadFile = File(...),
comment: str = Form(None),
db: Session = Depends(get_db),
):
logging.info(f"Received file: {uploaded_file.filename}")
# Validate Sample
sample = db.query(SampleModel).filter(SampleModel.id == sample_id).first()
if not sample:
raise HTTPException(status_code=404, detail="Sample not found")
# Retrieve the most recent sample event for the sample
sample_event = (
db.query(SampleEventModel)
.filter(SampleEventModel.sample_id == sample_id)
.order_by(SampleEventModel.timestamp.desc()) # Sort by most recent event
.first()
)
if not sample_event:
logging.debug(f"No events found for sample with id: {sample_id}")
raise HTTPException(
status_code=404, detail="No events found for the specified sample"
)
# Log the found sample event for debugging
logging.debug(
f"Most recent event found for sample_id {sample_id}: "
f"event_id={sample_event.id}, "
f"type={sample_event.event_type}, "
f"timestamp={sample_event.timestamp}"
)
# Extract event type and timestamp for directory structure
event_type = sample_event.event_type
event_timestamp = sample_event.timestamp.strftime("%Y-%m-%d_%H-%M-%S")
# Define Directory Structure
pgroup = sample.puck.dewar.pgroups # adjust to sample or puck pgroups as needed
today = datetime.now().strftime("%Y-%m-%d")
dewar_name = (
sample.puck.dewar.dewar_name
if sample.puck and sample.puck.dewar
else "default_dewar"
)
puck_name = sample.puck.puck_name if sample.puck else "default_puck"
position = sample.position if sample.position else "default_position"
# Add 'run/event' specific details to the folder structure
base_dir = Path(
f"images/{pgroup}/{today}/{dewar_name}/{puck_name}/"
f"{position}/{event_type}_{event_timestamp}"
)
base_dir.mkdir(parents=True, exist_ok=True)
# Validate MIME type and Save the File
if not uploaded_file.content_type.startswith("image/"):
raise HTTPException(
status_code=400,
detail=f"Invalid file type: {uploaded_file.filename}."
f"Only images are accepted.",
)
file_path = base_dir / uploaded_file.filename
logging.debug(f"Saving file {uploaded_file.filename} to {file_path}")
try:
with file_path.open("wb") as buffer:
shutil.copyfileobj(uploaded_file.file, buffer)
logging.info(f"File saved: {file_path}")
except Exception as e:
logging.error(f"Error saving file {uploaded_file.filename}: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"Could not save file {uploaded_file.filename}. "
f"Ensure the server has correct permissions.",
)
# Create the payload from the Pydantic schema
image_payload = ImageCreate(
pgroup=pgroup,
comment=comment,
filepath=str(file_path),
status="active",
sample_id=sample_id,
sample_event_id=int(sample_event.id), # Link to the most recent sample event
).dict()
# Convert the payload to your mapped SQLAlchemy model instance
new_image = ImageModel(**image_payload)
db.add(new_image)
db.commit()
db.refresh(new_image)
logging.info(
f"Uploaded 1 file for sample {sample_id} and event {sample_event.id} and "
f"added record {new_image.id} to the database."
)
# Returning the mapped SQLAlchemy object, which will be converted to the
# Pydantic response model.
return new_image
@router.get("/results", response_model=List[SampleResult])
async def get_sample_results(active_pgroup: str, db: Session = Depends(get_db)):
# Query samples for the active pgroup using joins.
samples = (
db.query(SampleModel)
.join(SampleModel.puck)
.join(PuckModel.dewar)
.filter(DewarModel.pgroups == active_pgroup)
.all()
)
if not samples:
raise HTTPException(
status_code=404, detail="No samples found for the active pgroup"
)
results = []
for sample in samples:
# Query images associated with the sample, including the related event_type
images = (
db.query(ImageModel)
.options(joinedload(ImageModel.sample_event))
.filter(ImageModel.sample_id == sample.id)
.all()
)
# Query experiment parameters (which include beamline parameters) for the
# sample.
experiment_parameters = (
db.query(ExperimentParametersModel)
.filter(ExperimentParametersModel.sample_id == sample.id)
.all()
)
print("Experiment Parameters for sample", sample.id, experiment_parameters)
results.append(
SampleResult(
sample_id=sample.id,
sample_name=sample.sample_name,
puck_name=sample.puck.puck_name if sample.puck else None,
dewar_name=sample.puck.dewar.dewar_name
if (sample.puck and sample.puck.dewar)
else None,
images=[
ImageInfo(
id=img.id,
filepath=img.filepath,
event_type=img.sample_event.event_type
if img.sample_event
else "Unknown",
comment=img.comment,
)
for img in images
],
experiment_runs=[
ExperimentParametersRead(
id=ex.id,
run_number=ex.run_number,
beamline_parameters=ex.beamline_parameters,
sample_id=ex.sample_id,
)
for ex in experiment_parameters
],
)
)
return results
@router.post(
"/samples/{sample_id}/experiment_parameters",
response_model=ExperimentParametersRead,
)
def create_experiment_parameters_for_sample(
sample_id: int,
exp_params: ExperimentParametersCreate,
db: Session = Depends(get_db),
):
# Calculate the new run_number for the given sample.
# This assumes that the run_number is computed as one plus the maximum
# current value.
last_exp = (
db.query(ExperimentParametersModel)
.filter(ExperimentParametersModel.sample_id == sample_id)
.order_by(ExperimentParametersModel.run_number.desc())
.first()
)
new_run_number = last_exp.run_number + 1 if last_exp else 1
# Create a new ExperimentParameters record. The beamline_parameters are
# stored as JSON.
new_exp = ExperimentParametersModel(
run_number=new_run_number,
beamline_parameters=exp_params.beamline_parameters.dict()
if exp_params.beamline_parameters
else None,
sample_id=sample_id,
)
db.add(new_exp)
db.commit()
db.refresh(new_exp)
# Create a "Collecting" sample event associated with the new experiment parameters
new_event = SampleEventModel(
sample_id=sample_id,
event_type="Collecting", # The event type
timestamp=datetime.now(), # Use current timestamp
)
db.add(new_event)
db.commit()
return new_exp
@router.post("/processing-results", response_model=ResultResponse)
def create_result(payload: ResultCreate, db: Session = Depends(get_db)):
# Check experiment existence
experiment = (
db.query(ExperimentParametersModel)
.filter(ExperimentParametersModel.id == payload.run_id)
.first()
)
if not experiment:
raise HTTPException(
status_code=404, detail="Experiment parameters (run) not found"
)
result_entry = ResultsModel(
sample_id=payload.sample_id,
run_id=payload.run_id,
result=payload.result.model_dump(), # Serialize entire result to JSON
)
db.add(result_entry)
db.commit()
db.refresh(result_entry)
return ResultResponse(
id=result_entry.id,
sample_id=result_entry.sample_id,
run_id=result_entry.run_id,
result=payload.result, # return original payload directly
)
# @router.get("/results", response_model=list[ResultResponse])
# def get_results(sample_id: int, result_id: int, db: Session = Depends(get_db)):
# query = db.query(Results)
#
# if sample_id:
# query = query.filter(Results.sample_id == sample_id)
# if result_id:
# query = query.filter(Results.result_id == result_id)
#
# results = query.all()
# if not results:
# raise HTTPException(status_code=404, detail="No results found")
#
# return results