
Standardized event types with stricter validation using `Literal`. Adjusted related data and logic to align with new types, including changes to PGROUP assignments, event timeline increments, and schema updates. Cleaned up unused code and clarified database initialization behavior.
361 lines
12 KiB
Python
361 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,
|
|
# 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,
|
|
)
|
|
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.
|
|
images = db.query(ImageModel).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(
|
|
{
|
|
"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": [
|
|
{"id": img.id, "filepath": img.filepath, "comment": img.comment}
|
|
for img in images
|
|
],
|
|
"experiment_runs": [
|
|
{
|
|
"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)
|
|
|
|
return new_exp
|
|
|
|
|
|
# @router.post("/results", response_model=ResultResponse)
|
|
# def create_result(result: ResultCreate, db: Session = Depends(get_db)):
|
|
# # Validate sample_id and result_id (optional but recommended)
|
|
# sample = db.query(SampleModel).filter_by(id=result.sample_id).first()
|
|
# if not sample:
|
|
# raise HTTPException(status_code=404, detail="Sample not found")
|
|
#
|
|
# experiment = db.query(ExperimentParameters).filter_by(id=result.result_id).first()
|
|
# if not experiment:
|
|
# raise HTTPException(status_code=404, detail="Experiment parameters not found")
|
|
#
|
|
# # Create a new Results entry
|
|
# result_obj = Results(
|
|
# sample_id=result.sample_id,
|
|
# result_id=result.result_id,
|
|
# result=result.result
|
|
# )
|
|
# db.add(result_obj)
|
|
# db.commit()
|
|
# db.refresh(result_obj)
|
|
#
|
|
# return result_obj
|
|
#
|
|
# @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
|